diff --git a/4.1.md b/4.1.md index ebabce88..956de5ef 100644 --- a/4.1.md +++ b/4.1.md @@ -15,9 +15,9 @@ -上面递交表单到/login里面,当用户输入信息之后点击登陆之后,跳转到我们的login里面,我们首先要判断这个是什么方式过来,post还是get呢? +上面递交表单到服务器的`/login`,当用户输入信息点击登陆之后,会跳转到服务器的路由`login`里面,我们首先要判断这个是什么方式传递过来,POST还是GET呢? -http包里面有一个很简单的方式就可以获取,我们在前面web的例子基础之上来看看怎么处理login,怎么读取form数据 +http包里面有一个很简单的方式就可以获取,我们在前面web的例子的基础上来看看怎么处理login页面的form数据 package main @@ -31,7 +31,8 @@ http包里面有一个很简单的方式就可以获取,我们在前面web的 ) func sayhelloName(w http.ResponseWriter, r *http.Request) { - r.ParseForm() //解析参数,默认是不会解析的 + r.ParseForm() //解析url传递的参数,对于POST则解析响应包的主体(request body) + //注意:如果没有调用parseForm方法,下面无法获取表单的数据 fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息 fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) @@ -69,19 +70,19 @@ http包里面有一个很简单的方式就可以获取,我们在前面web的 login函数中我们根据`r.Method`来判断是显示登录界面还是处理登录逻辑。当GET方式请求时显示登录界面,其他方式请求时则处理登录逻辑,如查询数据库、验证登录信息等。 -当我们在浏览器里面输入`http://127.0.0.1:9090/login`的时候,出现如下界面 +当我们在浏览器里面打开`http://127.0.0.1:9090/login`的时候,出现如下界面 ![](images/4.1.login.png?raw=true) -我们输入用户名和密码之后发现在服务器端是不会打印出来任何东西的,为什么呢?默认情况下,Handler里面是不会自动解析form的,必须显式的调用`r.ParseForm()`后,你才能对这个参数进行操作。我们修改一下代码,在`fmt.Println("username:", r.Form["username"])`之前加一行`r.ParseForm()`,重新编译,再次测试输入递交,现在是不是在服务器端有输出你的输入的用户名和密码了。 +我们输入用户名和密码之后发现在服务器端是不会打印出来任何输出的,为什么呢?默认情况下,Handler里面是不会自动解析form的,必须显式的调用`r.ParseForm()`后,你才能对这个表单数据进行操作。我们修改一下代码,在`fmt.Println("username:", r.Form["username"])`之前加一行`r.ParseForm()`,重新编译,再次测试输入递交,现在是不是在服务器端有输出你的输入的用户名和密码了。 -`r.Form`里面包含了所有请求的URL中query-string、POST的数据、PUT的数据,所有当你在URL的query-string字段和POST冲突时,会保存成一个slice,里面存储了多个值,Go官方文档中说在接下来的版本里面将会把POST、GET这些数据分离开来。 +`r.Form`里面包含了所有请求的参数,比如URL中query-string、POST的数据、PUT的数据,所有当你在URL的query-string字段和POST冲突时,会保存成一个slice,里面存储了多个值,Go官方文档中说在接下来的版本里面将会把POST、GET这些数据分离开来。 现在我们修改一下login.gtpl里面form的action值`http://127.0.0.1:9090/login`修改为`http://127.0.0.1:9090/login?username=astaxie`,再次测试,服务器的输出username是不是一个slice。服务器端的输出如下: ![](images/4.1.slice.png?raw=true) -`r.Form`是一个url.Values类型,里面存储的是对应的key=value信息,下面展示了可以对form进行的一些操作: +`request.Form`是一个url.Values类型,里面存储的是对应的类似`key=value`的信息,下面展示了可以对form数据进行的一些操作: v := url.Values{} v.Set("name", "Ava") @@ -93,7 +94,8 @@ login函数中我们根据`r.Method`来判断是显示登录界面还是处理 fmt.Println(v.Get("friend")) fmt.Println(v["friend"]) -Tips: Request请求也提供了FormValue()函数来获取用户提交的参数。如r.Form["username"]也可写成r.FormValue("username")。调用r.FormValue时会自动调用r.ParseForm,所以不必提前调用。r.FormValue只会返回同名参数中的第一个,若参数不存在则返回空字符串。 +>**Tips**: +Request本身也提供了FormValue()函数来获取用户提交的参数。如r.Form["username"]也可写成r.FormValue("username")。调用r.FormValue时会自动调用r.ParseForm,所以不必提前调用。r.FormValue只会返回同名参数中的第一个,若参数不存在则返回空字符串。 ## links * [目录]() diff --git a/4.2.md b/4.2.md index 47e5583c..6a63af4c 100644 --- a/4.2.md +++ b/4.2.md @@ -1,11 +1,11 @@ # 4.2验证表单的输入 -我们开发Web的一个原则就是,不能信任用户输入的任何信息,所以验证和过滤用户的输入信息就变得相当重要,我们经常会在微博、新闻中听到某某网站被入侵了,存在什么漏洞,这些大多是是因为对于用户输入的信息没有做严格的验证引起的,所以为了编写出安全可靠的Web程序,验证表单输入变得相当的重要。 +我们开发Web的一个原则就是,不能信任用户输入的任何信息,所以验证和过滤用户的输入信息就变得非常重要,我们经常会在微博、新闻中听到某某网站被入侵了,存在什么漏洞,这些大多是是因为网站对于用户输入的信息没有做严格的验证引起的,所以为了编写出安全可靠的Web程序,验证表单输入的意义重大。 -我们平常编写Web应用主要有两方面的数据验证,一个是在页面端的js验证(目前很多这方面的验证库),一个是在服务器端的验证,我们这小节讲解的是如何在服务器端验证。 +我们平常编写Web应用主要有两方面的数据验证,一个是在页面端的js验证(目前在这方面有很多的插件库,比如ValidationJS插件),一个是在服务器端的验证,我们这小节讲解的是如何在服务器端验证。 ## 必填字段 -你想要确保从一个表单元素中得到一个值,例如前面小节里面的用户名,我们如何处理呢?Go有一个内置函数`len`可以获取字符串的长度,这样我们就可以通过len来测试获取数据的长度,例如: +你想要确保从一个表单元素中得到一个值,例如前面小节里面的用户名,我们如何处理呢?Go有一个内置函数`len`可以获取字符串的长度,这样我们就可以通过len来获取数据的长度,例如: if len(r.Form["username"][0])==0{ //为空的处理 @@ -133,14 +133,14 @@ ## 日期和时间 你想确定用户填写的日期或时间是否有效。例如 -,你想确保用户在日程表中安排8月份的第45天开会,或者提供还没到的时间作为生日。 +,用户在日程表中安排8月份的第45天开会,或者提供未来的某个时间作为生日。 Go里面提供了一个time的处理包,我们可以把用户的输入年月日转化成相应的时间,然后进行逻辑判断 t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) fmt.Printf("Go launched at %s\n", t.Local()) -获取时间之后我们就可以进行很多时间函数的操作。具体的判断就根据自己的需求调整。 +获取time之后我们就可以进行很多时间函数的操作。具体的判断就根据自己的需求调整。 ## 身份证号码 如果我们想验证表单输入的是否是身份证,通过正则也可以方便的验证,但是身份证有15位和18位,我们两个都需要验证 diff --git a/4.3.md b/4.3.md index c6680497..919f1b39 100644 --- a/4.3.md +++ b/4.3.md @@ -2,9 +2,9 @@ 现在的网站包含大量的动态内容以提高用户体验,比过去要复杂得多。所谓动态内容,就是根据用户环境和需要,Web应用程序能够输出相应的内容。动态站点会受到一种名为“跨站脚本攻击”(Cross Site Scripting, 安全专家们通常将其缩写成 XSS)的威胁,而静态站点则完全不受其影响。 -攻击者通常会在有漏洞的程序中插入JavaScript、VBScript、 ActiveX或Flash以欺骗用户。一旦得手,他们可以盗取用户帐户,修改用户设置,盗取/污染cookie,做虚假广告等。 +攻击者通常会在有漏洞的程序中插入JavaScript、VBScript、 ActiveX或Flash以欺骗用户。一旦得手,他们可以盗取用户帐户信息,修改用户设置,盗取/污染cookie和植入恶意广告等。 -对XSS最佳的防护应该结合以下两种方法:验证所有输入数据,有效检测攻击(这个我们前面小节已经有过介绍);对所有输出数据进行适当的编码,以防止任何已成功注入的脚本在浏览器端运行。 +对XSS最佳的防护应该结合以下两种方法:一是验证所有输入数据,有效检测攻击(这个我们前面小节已经有过介绍);另一个是对所有输出数据进行适当的处理,以防止任何已成功注入的脚本在浏览器端运行。 那么Go里面是怎么做这个有效防护的呢?Go的html/template里面带有下面几个函数可以帮你转义 @@ -23,7 +23,7 @@ ![](images/4.3.escape.png?raw=true) -那么我们在输出我们的模板的时候怎么处理的呢?Go的html/template包默认帮你过滤了html元素,但是有时候你又想输出这样的信息,请使用text/template。请看下面的例子所示 +Go的html/template包默认帮你过滤了html标签,但是有时候你只想要输出这个``看起来正常的信息,该怎么处理?请使用text/template。请看下面的例子: import "text/template" ... @@ -45,7 +45,7 @@ Hello, ! -转换成template.HTML后,变量的内容也不会被转义 +转换成`template.HTML`后,变量的内容也不会被转义 转义的例子: diff --git a/4.5.md b/4.5.md index c5427940..e74c173e 100644 --- a/4.5.md +++ b/4.5.md @@ -1,13 +1,13 @@ # 4.5处理文件上传 -你想处理一个由用户上传的文件。比如你正在建设一个类似Instagram的网站,你需要存储用户拍摄的照片。这种需求该如何实现呢? +你想处理一个由用户上传的文件,比如你正在建设一个类似Instagram的网站,你需要存储用户拍摄的照片。这种需求该如何实现呢? -文件要能够上传,首先第一步就是要修改form的`enctype`属性,`enctype`属性有如下三种情况: +要使表单能够上传文件,首先第一步就是要添加form的`enctype`属性,`enctype`属性有如下三种情况: application/x-www-form-urlencoded 表示在发送前编码所有字符(默认) multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。 text/plain 空格转换为 "+" 加号,但不对特殊字符编码。 -如果要支持文件上传,我们的html应该类似于: +所以,表单的html代码应该类似于: @@ -15,8 +15,8 @@
- - + +
@@ -26,7 +26,7 @@ http.HandleFunc("/upload", upload) - // upload + // 处理/upload 逻辑 func upload(w http.ResponseWriter, r *http.Request) { fmt.Println("method:", r.Method) //获取请求的方法 if r.Method == "GET" { @@ -39,7 +39,7 @@ t.Execute(w, token) } else { r.ParseMultipartForm(32 << 20) - file, handler, err := r.FormFile("file") + file, handler, err := r.FormFile("uploadfile") if err != nil { fmt.Println(err) return @@ -62,9 +62,9 @@ 通过上面的实例我们可以看到我们上传文件主要三步处理: -- 1、表单中增加enctype="multipart/form-data" -- 2、服务端调用`r.ParseMultipartForm`,把上传的文件存储在内存和临时文件中 -- 3、使用`r.FormFile`获取文件句柄,然后对文件进行存储等处理。 +1. 表单中增加enctype="multipart/form-data" +2. 服务端调用`r.ParseMultipartForm`,把上传的文件存储在内存和临时文件中 +3. 使用`r.FormFile`获取文件句柄,然后对文件进行存储等处理。 文件handler是multipart.FileHeader,里面存储了如下结构信息 @@ -100,7 +100,7 @@ bodyWriter := multipart.NewWriter(bodyBuf) //关键的一步操作 - fileWriter, err := bodyWriter.CreateFormFile("file", filename) + fileWriter, err := bodyWriter.CreateFormFile("uploadfile", filename) if err != nil { fmt.Println("error writing to buffer") return err @@ -144,7 +144,7 @@ } -上面的例子详细展示了如何上传一个文件,客户端上传文件通过multipart的Write把文件信息写入缓存,然后调用http的post方法上传文件。 +上面的例子详细展示了客户端如何向服务器上传一个文件的例子,客户端通过multipart.Write把文件的文本流写入一个缓存中,然后调用http的Post方法把缓存传到服务器。 >如果你还有其他普通字段例如username之类的需要同时写入,那么可以调用multipart的WriteField方法写很多其他类似的字段。 diff --git a/4.6.md b/4.6.md index 408b26ab..bf9d5363 100644 --- a/4.6.md +++ b/4.6.md @@ -1,7 +1,7 @@ # 4.6 小结 -这一章里面我们学习了Go里面如何处理表单信息,我们通过一个登陆、一个上传例子展示了Go处理form表单信息,处理文件上传的能力。但是在处理表单过程中我们需要验证用户输入的信息,考虑到网站安全、数据过滤就变得相当重要了,因此专门一个小节讲解了各方面的数据过滤,顺带讲了一下Go里面对正则的处理。 +这一章里面我们学习了Go如何处理表单信息,我们通过用户登陆、上传文件的例子展示了Go处理form表单信息及上传文件的手段。但是在处理表单过程中我们需要验证用户输入的信息,考虑到网站安全的重要性,数据过滤就显得相当重要了,因此后面的章节中专门写了一个小节来讲解了不同方面的数据过滤,顺带讲一下Go对字符串的正则处理。 -通过这一章能够让你了解客户端和服务器端如何进行数据的交互,让客户端的数据进入我们的系服务器统,让我们系统处理之后的数据展现给客户端。 +通过这一章能够让你了解客户端和服务器端是如何进行数据上的交互,客户端将数据传递给服务器系统,服务器接受数据又把处理结果反馈给客户端。 ## links * [目录]() diff --git a/5.1.md b/5.1.md index e5b11b03..43cceb53 100644 --- a/5.1.md +++ b/5.1.md @@ -1,5 +1,5 @@ # 5.1 database/sql接口 -Go和PHP不同的地方是,他没有官方提供数据库驱动,而是为开发数据库驱动定义了一些标准接口,第三方用户可以根据定义的接口来开发相应的数据库驱动,这样做有一个好处,我们按照标准接口开发的代码, 在需要迁移数据库时,不需要任何修改。那么Go都定义了那些标准接口呢?让我们来详细的分析一下 +Go与PHP不同的地方是Go没有官方提供数据库驱动,而是为开发者开发数据库驱动定义了一些标准接口,开发者可以根据定义的接口来开发相应的数据库驱动,这样做有一个好处,只要按照标准接口开发的代码, 以后需要迁移数据库时,不需要任何修改。那么Go都定义了哪些标准接口呢?让我们来详细的分析一下 ## sql.Register 这个存在于database/sql的函数是用来注册数据库驱动的,当第三方开发者开发数据库驱动时,都会实现init函数,在init里面会调用这个`Register(name string, driver driver.Driver)`完成本驱动的注册。 @@ -19,7 +19,7 @@ Go和PHP不同的地方是,他没有官方提供数据库驱动,而是为开 sql.Register("mymysql", &d) } -我们看到第三方驱动都是通过调用这个函数来注册自己的驱动名称以及相应的driver。在database/sql内部通过一个map来存储相应的驱动。 +我们看到第三方数据库驱动都是通过调用这个函数来注册自己的数据库驱动名称以及相应的driver实现。在database/sql内部通过一个map来存储用户定义的相应驱动。 var drivers = make(map[string]driver.Driver) @@ -27,12 +27,16 @@ Go和PHP不同的地方是,他没有官方提供数据库驱动,而是为开 因此通过database/sql的注册函数可以同时注册多个数据库驱动,只要不重复。 ->在我们使用database/sql接口和第三方库的时候经常看到如下的import +>在我们使用database/sql接口和第三方库的时候经常看到如下: - "database/sql" - _ "github.com/mattn/go-sqlite3" +> import ( +> "database/sql" +> _ "github.com/mattn/go-sqlite3" +> ) ->新手都会被这个`_`所迷惑,其实这个就是Go设计的巧妙之处,我们在变量赋值的时候经常看到这个,它是用来忽略变量的占位符,那么这个包引入也是,这儿的意思是引入此包而不直接使用这个包中定义的函数,变量等资源,我们在2.3流程和函数里面介绍过init函数的初始化过程,包在引入的时候会去调用包的init函数以完成对包的初始化,因此我们引入上面的数据库驱动包之后会去调用init函数,然后在init函数里面注册了这个数据库驱动,这样我们就可以在接下来的代码中直接使用这个数据库驱动了。 +>新手都会被这个`_`所迷惑,其实这个就是Go设计的巧妙之处,我们在变量赋值的时候经常看到这个符号,它是用来忽略变量赋值的占位符,那么包引入用到这个符号也是相似的作用,这儿使用`_`的意思是引入后面的包名而不直接使用这个包中定义的函数,变量等资源。 + +>我们在2.3节流程和函数的一节中介绍过init函数的初始化过程,包在引入的时候会自动调用包的init函数以完成对包的初始化。因此,我们引入上面的数据库驱动包之后要手动去调用init函数,然后在init函数里面注册这个数据库驱动,这样我们就可以在接下来的代码中直接使用这个数据库驱动了。 ## driver.Driver Driver是一个数据库驱动的接口,他定义了一个method: Open(name string),这个方法返回一个数据库的Conn接口。 @@ -48,7 +52,7 @@ Driver是一个数据库驱动的接口,他定义了一个method: Open(name go goroutineB (Conn) //执行插入操作 ... -上面这样的代码可能会导致不知道某个操作究竟是由哪个goroutine发起的,从而数据混乱,比如可能会把goroutine A里面执行的查询操作的结果返回给goroutine B从而使B错误的把此结果当成自己执行的插入结果。 +上面这样的代码可能会使Go不知道某个操作究竟是由哪个goroutine发起的,从而导致数据混乱,比如可能会把goroutineA里面执行的查询操作的结果返回给goroutineB从而使B错误地把此结果当成自己执行的插入数据。 第三方驱动都会定义这个函数,它会解析name参数来获取相关数据库的连接信息,解析完成后,它将使用此信息来初始化一个Conn并返回它。 @@ -61,14 +65,14 @@ Conn是一个数据库连接的接口定义,他定义了一系列方法,这 Begin() (Tx, error) } -Prepare函数返回与当前连接相关的准备好Sql语句的状态,可以进行查询、删除等操作。 +Prepare函数返回与当前连接相关的执行Sql语句的准备状态,可以进行查询、删除等操作。 -Close函数关闭当前的连接,以及执行释放连接拥有的资源等清理工作。因为database/sql里面实现了建议的conn pool,所以你不要再自己去实现缓存conn之类的,这样容易引起问题。 +Close函数关闭当前的连接,执行释放连接拥有的资源等清理工作。因为驱动实现了database/sql里面建议的conn pool,所以你不用再去实现缓存conn之类的,这样会容易引起问题。 Begin函数返回一个代表事务处理的Tx,通过它你可以进行查询,更新等操作,或者对事务进行回滚、递交。 ## driver.Stmt -Stmt是一种准备好的状态,和Conn相关联,而且是只能应用于一个goroutine中,不能应用在多个goroutine中。 +Stmt是一种准备好的状态,和Conn相关联,而且只能应用于一个goroutine中,不能应用于多个goroutine。 type Stmt interface { Close() error @@ -113,7 +117,7 @@ Query函数执行Prepare准备好的sql,传入需要的参数执行select操 RowsAffected() (int64, error) } -LastInsertId函数返回由数据库执行插入操作得到的自动增长ID号。 +LastInsertId函数返回由数据库执行插入操作得到的自增ID号。 RowsAffected函数返回query操作影响的数据条目数。 @@ -126,7 +130,7 @@ Rows是执行查询返回的结果集接口定义 Next(dest []Value) error } -Columns函数返回查询数据库表的字段信息,这个返回的slice和你sql查询的字段一一对应,而不是返回整个表的所有字段。 +Columns函数返回查询数据库表的字段信息,这个返回的slice和sql查询的字段一一对应,而不是返回整个表的所有字段。 Close函数用来关闭Rows迭代器。 @@ -147,7 +151,7 @@ Value其实就是一个空接口,他可以容纳任何的数据 type Value interface{} -Value的值必须所有的驱动里面控制的,Value要么是nil,要么是下面的任意一种 +drive的Value是驱动必须能够操作的Value,Value要么是nil,要么是下面的任意一种 int64 float64 @@ -175,22 +179,23 @@ Valuer接口定义了返回一个driver.Value的方式 type Valuer interface { Value() (Value, error) } + 很多类型都实现了这个Value方法,用来自身与driver.Value的转化。 通过上面的讲解,你应该对于驱动的开发有了一个基本的了解,一个驱动只要实现了这些接口就能完成增删查改等基本操作了,剩下的就是与相应的数据库进行数据交互等细节问题了,在此不再赘述。 ## database/sql -database/sql在database/sql/driver提供的接口基础上定义了一些更高阶的方法,用以简化数据库操作,同时内部还实现了一个建议的conn pool。 +database/sql在database/sql/driver提供的接口基础上定义了一些更高阶的方法,用以简化数据库操作,同时内部还建议性地实现一个conn pool。 type DB struct { - driver driver.Driver - dsn string + driver driver.Driver + dsn string mu sync.Mutex // protects freeConn and closed freeConn []driver.Conn closed bool } -我们可以看到Open函数返回的是DB对象,里面有一个freeConn,,它就是那个简易的连接池。它的实现相当简单或者说简陋,就是当执行Db.prepare的时候会defer:db.putConn(ci, err),也就是放入连接池,每次调用conn的时候会先判断freeConn的长度是否大于0,大于0说明有可以复用的conn,直接拿出来用就是了,如果不大于0,则创建一个conn,然后再返回之。 +我们可以看到Open函数返回的是DB对象,里面有一个freeConn,它就是那个简易的连接池。它的实现相当简单或者说简陋,就是当执行Db.prepare的时候会`defer db.putConn(ci, err)`,也就是把这个连接放入连接池,每次调用conn的时候会先判断freeConn的长度是否大于0,大于0说明有可以复用的conn,直接拿出来用就是了,如果不大于0,则创建一个conn,然后再返回之。 ## links