Merge branch 'master' of https://github.com/jameswpm/build-web-application-with-golang
This commit is contained in:
@@ -1,19 +1,19 @@
|
||||
# 2 Go语言基础
|
||||
|
||||
Go是一门类似C的编译型语言,但是它的编译速度非常快。这门语言的关键字总共也就二十五个,比英文字母还少一个,这对于我们的学习来说就简单了很多。先让我们看一眼这些关键字都长什么样:
|
||||
|
||||
break default func interface select
|
||||
case defer go map struct
|
||||
chan else goto package switch
|
||||
const fallthrough if range type
|
||||
continue for import return var
|
||||
|
||||
在接下来的这一章中,我将带领你去学习这门语言的基础。通过每一小节的介绍,你将发现,Go的世界是那么地简洁,设计是如此地美妙,编写Go将会是一件愉快的事情。等回过头来,你就会发现这二十五个关键字是多么地亲切。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第一章总结](<01.5.md>)
|
||||
* 下一节: [你好,Go](<02.1.md>)
|
||||
# 2 Go语言基础
|
||||
|
||||
Go是一门类似C的编译型语言,但是它的编译速度非常快。这门语言的关键字总共也就二十五个,比英文字母还少一个,这对于我们的学习来说就简单了很多。先让我们看一眼这些关键字都长什么样:
|
||||
|
||||
break default func interface select
|
||||
case defer go map struct
|
||||
chan else goto package switch
|
||||
const fallthrough if range type
|
||||
continue for import return var
|
||||
|
||||
在接下来的这一章中,我将带领你去学习这门语言的基础。通过每一小节的介绍,你将发现,Go的世界是那么地简洁,设计是如此地美妙,编写Go将会是一件愉快的事情。等回过头来,你就会发现这二十五个关键字是多么地亲切。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第一章总结](<01.5.md>)
|
||||
* 下一节: [你好,Go](<02.1.md>)
|
||||
19
zh/02.0.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
19
zh/02.0.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,19 @@
|
||||
# 2 Go语言基础
|
||||
|
||||
Go是一门类似C的编译型语言,但是它的编译速度非常快。这门语言的关键字总共也就二十五个,比英文字母还少一个,这对于我们的学习来说就简单了很多。先让我们看一眼这些关键字都长什么样:
|
||||
|
||||
break default func interface select
|
||||
case defer go map struct
|
||||
chan else goto package switch
|
||||
const fallthrough if range type
|
||||
continue for import return var
|
||||
|
||||
在接下来的这一章中,我将带领你去学习这门语言的基础。通过每一小节的介绍,你将发现,Go的世界是那么地简洁,设计是如此地美妙,编写Go将会是一件愉快的事情。等回过头来,你就会发现这二十五个关键字是多么地亲切。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第一章总结](<01.5.md>)
|
||||
* 下一节: [你好,Go](<02.1.md>)
|
||||
@@ -1,52 +1,52 @@
|
||||
# 2.1 你好,Go
|
||||
|
||||
在开始编写应用之前,我们先从最基本的程序开始。就像你造房子之前不知道什么是地基一样,编写程序也不知道如何开始。因此,在本节中,我们要学习用最基本的语法让Go程序运行起来。
|
||||
|
||||
## 程序
|
||||
|
||||
这就像一个传统,在学习大部分语言之前,你先学会如何编写一个可以输出`hello world`的程序。
|
||||
|
||||
准备好了吗?Let's Go!
|
||||
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Printf("Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちはせかい\n")
|
||||
}
|
||||
|
||||
输出如下:
|
||||
|
||||
Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちはせかい
|
||||
|
||||
## 详解
|
||||
首先我们要了解一个概念,Go程序是通过`package`来组织的
|
||||
|
||||
`package <pkgName>`(在我们的例子中是`package main`)这一行告诉我们当前文件属于哪个包,而包名`main`则告诉我们它是一个可独立运行的包,它在编译后会产生可执行文件。除了`main`包之外,其它的包最后都会生成`*.a`文件(也就是包文件)并放置在`$GOPATH/pkg/$GOOS_$GOARCH`中(以Mac为例就是`$GOPATH/pkg/darwin_amd64`)。
|
||||
|
||||
>每一个可独立运行的Go程序,必定包含一个`package main`,在这个`main`包中必定包含一个入口函数`main`,而这个函数既没有参数,也没有返回值。
|
||||
|
||||
为了打印`Hello, world...`,我们调用了一个函数`Printf`,这个函数来自于`fmt`包,所以我们在第三行中导入了系统级别的`fmt`包:`import "fmt"`。
|
||||
|
||||
包的概念和Python中的package类似,它们都有一些特别的好处:模块化(能够把你的程序分成多个模块)和可重用性(每个模块都能被其它应用程序反复使用)。我们在这里只是先了解一下包的概念,后面我们将会编写自己的包。
|
||||
|
||||
在第五行中,我们通过关键字`func`定义了一个`main`函数,函数体被放在`{}`(大括号)中,就像我们平时写C、C++或Java时一样。
|
||||
|
||||
大家可以看到`main`函数是没有任何的参数的,我们接下来就学习如何编写带参数的、返回0个或多个值的函数。
|
||||
|
||||
第六行,我们调用了`fmt`包里面定义的函数`Printf`。大家可以看到,这个函数是通过`<pkgName>.<funcName>`的方式调用的,这一点和Python十分相似。
|
||||
|
||||
>前面提到过,包名和包所在的文件夹名可以是不同的,此处的`<pkgName>`即为通过`package <pkgName>`声明的包名,而非文件夹名。
|
||||
|
||||
最后大家可以看到我们输出的内容里面包含了很多非ASCII码字符。实际上,Go是天生支持UTF-8的,任何字符都可以直接输出,你甚至可以用UTF-8中的任何字符作为标识符。
|
||||
|
||||
|
||||
## 结论
|
||||
|
||||
Go使用`package`(和Python的模块类似)来组织代码。`main.main()`函数(这个函数位于主包)是每一个独立的可运行程序的入口点。Go使用UTF-8字符串和标识符(因为UTF-8的发明者也就是Go的发明者之一),所以它天生支持多语言。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [Go语言基础](<02.0.md>)
|
||||
* 下一节: [Go基础](<02.2.md>)
|
||||
# 2.1 你好,Go
|
||||
|
||||
在开始编写应用之前,我们先从最基本的程序开始。就像你造房子之前不知道什么是地基一样,编写程序也不知道如何开始。因此,在本节中,我们要学习用最基本的语法让Go程序运行起来。
|
||||
|
||||
## 程序
|
||||
|
||||
这就像一个传统,在学习大部分语言之前,你先学会如何编写一个可以输出`hello world`的程序。
|
||||
|
||||
准备好了吗?Let's Go!
|
||||
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Printf("Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちはせかい\n")
|
||||
}
|
||||
|
||||
输出如下:
|
||||
|
||||
Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちはせかい
|
||||
|
||||
## 详解
|
||||
首先我们要了解一个概念,Go程序是通过`package`来组织的
|
||||
|
||||
`package <pkgName>`(在我们的例子中是`package main`)这一行告诉我们当前文件属于哪个包,而包名`main`则告诉我们它是一个可独立运行的包,它在编译后会产生可执行文件。除了`main`包之外,其它的包最后都会生成`*.a`文件(也就是包文件)并放置在`$GOPATH/pkg/$GOOS_$GOARCH`中(以Mac为例就是`$GOPATH/pkg/darwin_amd64`)。
|
||||
|
||||
>每一个可独立运行的Go程序,必定包含一个`package main`,在这个`main`包中必定包含一个入口函数`main`,而这个函数既没有参数,也没有返回值。
|
||||
|
||||
为了打印`Hello, world...`,我们调用了一个函数`Printf`,这个函数来自于`fmt`包,所以我们在第三行中导入了系统级别的`fmt`包:`import "fmt"`。
|
||||
|
||||
包的概念和Python中的package类似,它们都有一些特别的好处:模块化(能够把你的程序分成多个模块)和可重用性(每个模块都能被其它应用程序反复使用)。我们在这里只是先了解一下包的概念,后面我们将会编写自己的包。
|
||||
|
||||
在第五行中,我们通过关键字`func`定义了一个`main`函数,函数体被放在`{}`(大括号)中,就像我们平时写C、C++或Java时一样。
|
||||
|
||||
大家可以看到`main`函数是没有任何的参数的,我们接下来就学习如何编写带参数的、返回0个或多个值的函数。
|
||||
|
||||
第六行,我们调用了`fmt`包里面定义的函数`Printf`。大家可以看到,这个函数是通过`<pkgName>.<funcName>`的方式调用的,这一点和Python十分相似。
|
||||
|
||||
>前面提到过,包名和包所在的文件夹名可以是不同的,此处的`<pkgName>`即为通过`package <pkgName>`声明的包名,而非文件夹名。
|
||||
|
||||
最后大家可以看到我们输出的内容里面包含了很多非ASCII码字符。实际上,Go是天生支持UTF-8的,任何字符都可以直接输出,你甚至可以用UTF-8中的任何字符作为标识符。
|
||||
|
||||
|
||||
## 结论
|
||||
|
||||
Go使用`package`(和Python的模块类似)来组织代码。`main.main()`函数(这个函数位于主包)是每一个独立的可运行程序的入口点。Go使用UTF-8字符串和标识符(因为UTF-8的发明者也就是Go的发明者之一),所以它天生支持多语言。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [Go语言基础](<02.0.md>)
|
||||
* 下一节: [Go基础](<02.2.md>)
|
||||
52
zh/02.1.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
52
zh/02.1.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,52 @@
|
||||
# 2.1 你好,Go
|
||||
|
||||
在开始编写应用之前,我们先从最基本的程序开始。就像你造房子之前不知道什么是地基一样,编写程序也不知道如何开始。因此,在本节中,我们要学习用最基本的语法让Go程序运行起来。
|
||||
|
||||
## 程序
|
||||
|
||||
这就像一个传统,在学习大部分语言之前,你先学会如何编写一个可以输出`hello world`的程序。
|
||||
|
||||
准备好了吗?Let's Go!
|
||||
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Printf("Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちはせかい\n")
|
||||
}
|
||||
|
||||
输出如下:
|
||||
|
||||
Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちはせかい
|
||||
|
||||
## 详解
|
||||
首先我们要了解一个概念,Go程序是通过`package`来组织的
|
||||
|
||||
`package <pkgName>`(在我们的例子中是`package main`)这一行告诉我们当前文件属于哪个包,而包名`main`则告诉我们它是一个可独立运行的包,它在编译后会产生可执行文件。除了`main`包之外,其它的包最后都会生成`*.a`文件(也就是包文件)并放置在`$GOPATH/pkg/$GOOS_$GOARCH`中(以Mac为例就是`$GOPATH/pkg/darwin_amd64`)。
|
||||
|
||||
>每一个可独立运行的Go程序,必定包含一个`package main`,在这个`main`包中必定包含一个入口函数`main`,而这个函数既没有参数,也没有返回值。
|
||||
|
||||
为了打印`Hello, world...`,我们调用了一个函数`Printf`,这个函数来自于`fmt`包,所以我们在第三行中导入了系统级别的`fmt`包:`import "fmt"`。
|
||||
|
||||
包的概念和Python中的package类似,它们都有一些特别的好处:模块化(能够把你的程序分成多个模块)和可重用性(每个模块都能被其它应用程序反复使用)。我们在这里只是先了解一下包的概念,后面我们将会编写自己的包。
|
||||
|
||||
在第五行中,我们通过关键字`func`定义了一个`main`函数,函数体被放在`{}`(大括号)中,就像我们平时写C、C++或Java时一样。
|
||||
|
||||
大家可以看到`main`函数是没有任何的参数的,我们接下来就学习如何编写带参数的、返回0个或多个值的函数。
|
||||
|
||||
第六行,我们调用了`fmt`包里面定义的函数`Printf`。大家可以看到,这个函数是通过`<pkgName>.<funcName>`的方式调用的,这一点和Python十分相似。
|
||||
|
||||
>前面提到过,包名和包所在的文件夹名可以是不同的,此处的`<pkgName>`即为通过`package <pkgName>`声明的包名,而非文件夹名。
|
||||
|
||||
最后大家可以看到我们输出的内容里面包含了很多非ASCII码字符。实际上,Go是天生支持UTF-8的,任何字符都可以直接输出,你甚至可以用UTF-8中的任何字符作为标识符。
|
||||
|
||||
|
||||
## 结论
|
||||
|
||||
Go使用`package`(和Python的模块类似)来组织代码。`main.main()`函数(这个函数位于主包)是每一个独立的可运行程序的入口点。Go使用UTF-8字符串和标识符(因为UTF-8的发明者也就是Go的发明者之一),所以它天生支持多语言。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [Go语言基础](<02.0.md>)
|
||||
* 下一节: [Go基础](<02.2.md>)
|
||||
@@ -404,7 +404,11 @@ slice有一些简便的操作
|
||||
|
||||
`map`也就是Python中字典的概念,它的格式为`map[keyType]valueType`
|
||||
|
||||
<<<<<<< HEAD:zh/02.2.md
|
||||
我们看下面的代码,`map`的读取和设置也类似`slice`一样,通过`key`来操作,只是`slice`的`index`只能是`int`类型,而`map`多了很多类型,可以是`int`,可以是`string`及所有完全定义了`==`与`!=`操作的类型。
|
||||
=======
|
||||
我们看下面的代码,`map`的读取和设置也类似`slice`一样,通过`key`来操作,只是`slice`的`index`只能是`int`类型,而`map`多了很多类型,可以是`int`,可以是`string`及所有完全定义了`==`与`!=`操作的类型。
|
||||
>>>>>>> eead24cf064976b648de5826eab51880c803b0fa:zh/02.2.md
|
||||
|
||||
// 声明一个key是字符串,值为int的字典,这种方式的声明需要在使用之前使用make初始化
|
||||
var numbers map[string]int
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
520
zh/02.3.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
520
zh/02.3.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,520 @@
|
||||
# 2.3 流程和函数
|
||||
这小节我们要介绍Go里面的流程控制以及函数操作。
|
||||
## 流程控制
|
||||
流程控制在编程语言中是最伟大的发明了,因为有了它,你可以通过很简单的流程描述来表达很复杂的逻辑。Go中流程控制分三大类:条件判断,循环控制和无条件跳转。
|
||||
### if
|
||||
`if`也许是各种编程语言中最常见的了,它的语法概括起来就是:如果满足条件就做某事,否则做另一件事。
|
||||
|
||||
Go里面`if`条件判断语句中不需要括号,如下代码所示
|
||||
|
||||
if x > 10 {
|
||||
fmt.Println("x is greater than 10")
|
||||
} else {
|
||||
fmt.Println("x is less than 10")
|
||||
}
|
||||
|
||||
Go的`if`还有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了,如下所示
|
||||
|
||||
// 计算获取值x,然后根据x返回的大小,判断是否大于10。
|
||||
if x := computedValue(); x > 10 {
|
||||
fmt.Println("x is greater than 10")
|
||||
} else {
|
||||
fmt.Println("x is less than 10")
|
||||
}
|
||||
|
||||
//这个地方如果这样调用就编译出错了,因为x是条件里面的变量
|
||||
fmt.Println(x)
|
||||
|
||||
多个条件的时候如下所示:
|
||||
|
||||
if integer == 3 {
|
||||
fmt.Println("The integer is equal to 3")
|
||||
} else if integer < 3 {
|
||||
fmt.Println("The integer is less than 3")
|
||||
} else {
|
||||
fmt.Println("The integer is greater than 3")
|
||||
}
|
||||
|
||||
### goto
|
||||
|
||||
Go有`goto`语句——请明智地使用它。用`goto`跳转到必须在当前函数内定义的标签。例如假设这样一个循环:
|
||||
|
||||
func myFunc() {
|
||||
i := 0
|
||||
Here: //这行的第一个词,以冒号结束作为标签
|
||||
println(i)
|
||||
i++
|
||||
goto Here //跳转到Here去
|
||||
}
|
||||
|
||||
>标签名是大小写敏感的。
|
||||
|
||||
### for
|
||||
Go里面最强大的一个控制逻辑就是`for`,它即可以用来循环读取数据,又可以当作`while`来控制逻辑,还能迭代操作。它的语法如下:
|
||||
|
||||
for expression1; expression2; expression3 {
|
||||
//...
|
||||
}
|
||||
|
||||
`expression1`、`expression2`和`expression3`都是表达式,其中`expression1`和`expression3`是变量声明或者函数调用返回值之类的,`expression2`是用来条件判断,`expression1`在循环开始之前调用,`expression3`在每轮循环结束之时调用。
|
||||
|
||||
一个例子比上面讲那么多更有用,那么我们看看下面的例子吧:
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
func main(){
|
||||
sum := 0;
|
||||
for index:=0; index < 10 ; index++ {
|
||||
sum += index
|
||||
}
|
||||
fmt.Println("sum is equal to ", sum)
|
||||
}
|
||||
// 输出:sum is equal to 45
|
||||
|
||||
有些时候需要进行多个赋值操作,由于Go里面没有`,`操作符,那么可以使用平行赋值`i, j = i+1, j-1`
|
||||
|
||||
|
||||
有些时候如果我们忽略`expression1`和`expression3`:
|
||||
|
||||
sum := 1
|
||||
for ; sum < 1000; {
|
||||
sum += sum
|
||||
}
|
||||
|
||||
其中`;`也可以省略,那么就变成如下的代码了,是不是似曾相识?对,这就是`while`的功能。
|
||||
|
||||
sum := 1
|
||||
for sum < 1000 {
|
||||
sum += sum
|
||||
}
|
||||
|
||||
在循环里面有两个关键操作`break`和`continue` ,`break`操作是跳出当前循环,`continue`是跳过本次循环。当嵌套过深的时候,`break`可以配合标签使用,即跳转至标签所指定的位置,详细参考如下例子:
|
||||
|
||||
for index := 10; index>0; index-- {
|
||||
if index == 5{
|
||||
break // 或者continue
|
||||
}
|
||||
fmt.Println(index)
|
||||
}
|
||||
// break打印出来10、9、8、7、6
|
||||
// continue打印出来10、9、8、7、6、4、3、2、1
|
||||
|
||||
`break`和`continue`还可以跟着标号,用来跳到多重循环中的外层循环
|
||||
|
||||
`for`配合`range`可以用于读取`slice`和`map`的数据:
|
||||
|
||||
for k,v:=range map {
|
||||
fmt.Println("map's key:",k)
|
||||
fmt.Println("map's val:",v)
|
||||
}
|
||||
|
||||
由于 Go 支持 “多值返回”, 而对于“声明而未被调用”的变量, 编译器会报错, 在这种情况下, 可以使用`_`来丢弃不需要的返回值
|
||||
例如
|
||||
|
||||
for _, v := range map{
|
||||
fmt.Println("map's val:", v)
|
||||
}
|
||||
|
||||
|
||||
### switch
|
||||
有些时候你需要写很多的`if-else`来实现一些逻辑处理,这个时候代码看上去就很丑很冗长,而且也不易于以后的维护,这个时候`switch`就能很好的解决这个问题。它的语法如下
|
||||
|
||||
switch sExpr {
|
||||
case expr1:
|
||||
some instructions
|
||||
case expr2:
|
||||
some other instructions
|
||||
case expr3:
|
||||
some other instructions
|
||||
default:
|
||||
other code
|
||||
}
|
||||
|
||||
`sExpr`和`expr1`、`expr2`、`expr3`的类型必须一致。Go的`switch`非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;而如果`switch`没有表达式,它会匹配`true`。
|
||||
|
||||
i := 10
|
||||
switch i {
|
||||
case 1:
|
||||
fmt.Println("i is equal to 1")
|
||||
case 2, 3, 4:
|
||||
fmt.Println("i is equal to 2, 3 or 4")
|
||||
case 10:
|
||||
fmt.Println("i is equal to 10")
|
||||
default:
|
||||
fmt.Println("All I know is that i is an integer")
|
||||
}
|
||||
|
||||
在第5行中,我们把很多值聚合在了一个`case`里面,同时,Go里面`switch`默认相当于每个`case`最后带有`break`,匹配成功后不会自动向下执行其他case,而是跳出整个`switch`, 但是可以使用`fallthrough`强制执行后面的case代码。
|
||||
|
||||
integer := 6
|
||||
switch integer {
|
||||
case 4:
|
||||
fmt.Println("The integer was <= 4")
|
||||
fallthrough
|
||||
case 5:
|
||||
fmt.Println("The integer was <= 5")
|
||||
fallthrough
|
||||
case 6:
|
||||
fmt.Println("The integer was <= 6")
|
||||
fallthrough
|
||||
case 7:
|
||||
fmt.Println("The integer was <= 7")
|
||||
fallthrough
|
||||
case 8:
|
||||
fmt.Println("The integer was <= 8")
|
||||
fallthrough
|
||||
default:
|
||||
fmt.Println("default case")
|
||||
}
|
||||
|
||||
上面的程序将输出
|
||||
|
||||
The integer was <= 6
|
||||
The integer was <= 7
|
||||
The integer was <= 8
|
||||
default case
|
||||
|
||||
|
||||
## 函数
|
||||
函数是Go里面的核心设计,它通过关键字`func`来声明,它的格式如下:
|
||||
|
||||
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
|
||||
//这里是处理逻辑代码
|
||||
//返回多个值
|
||||
return value1, value2
|
||||
}
|
||||
|
||||
上面的代码我们看出
|
||||
|
||||
- 关键字`func`用来声明一个函数`funcName`
|
||||
- 函数可以有一个或者多个参数,每个参数后面带有类型,通过`,`分隔
|
||||
- 函数可以返回多个值
|
||||
- 上面返回值声明了两个变量`output1`和`output2`,如果你不想声明也可以,直接就两个类型
|
||||
- 如果只有一个返回值且不声明返回值变量,那么你可以省略 包括返回值 的括号
|
||||
- 如果没有返回值,那么就直接省略最后的返回信息
|
||||
- 如果有返回值, 那么必须在函数的外层添加return语句
|
||||
|
||||
下面我们来看一个实际应用函数的例子(用来计算Max值)
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
// 返回a、b中最大值.
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func main() {
|
||||
x := 3
|
||||
y := 4
|
||||
z := 5
|
||||
|
||||
max_xy := max(x, y) //调用函数max(x, y)
|
||||
max_xz := max(x, z) //调用函数max(x, z)
|
||||
|
||||
fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy)
|
||||
fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz)
|
||||
fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 也可在这直接调用它
|
||||
}
|
||||
|
||||
上面这个里面我们可以看到`max`函数有两个参数,它们的类型都是`int`,那么第一个变量的类型可以省略(即 a,b int,而非 a int, b int),默认为离它最近的类型,同理多于2个同类型的变量或者返回值。同时我们注意到它的返回值就是一个类型,这个就是省略写法。
|
||||
|
||||
### 多个返回值
|
||||
Go语言比C更先进的特性,其中一点就是函数能够返回多个值。
|
||||
|
||||
我们直接上代码看例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
//返回 A+B 和 A*B
|
||||
func SumAndProduct(A, B int) (int, int) {
|
||||
return A+B, A*B
|
||||
}
|
||||
|
||||
func main() {
|
||||
x := 3
|
||||
y := 4
|
||||
|
||||
xPLUSy, xTIMESy := SumAndProduct(x, y)
|
||||
|
||||
fmt.Printf("%d + %d = %d\n", x, y, xPLUSy)
|
||||
fmt.Printf("%d * %d = %d\n", x, y, xTIMESy)
|
||||
}
|
||||
|
||||
上面的例子我们可以看到直接返回了两个参数,当然我们也可以命名返回参数的变量,这个例子里面只是用了两个类型,我们也可以改成如下这样的定义,然后返回的时候不用带上变量名,因为直接在函数里面初始化了。但如果你的函数是导出的(首字母大写),官方建议:最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差。
|
||||
|
||||
func SumAndProduct(A, B int) (add int, Multiplied int) {
|
||||
add = A+B
|
||||
Multiplied = A*B
|
||||
return
|
||||
}
|
||||
|
||||
### 变参
|
||||
Go函数支持变参。接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参:
|
||||
|
||||
func myfunc(arg ...int) {}
|
||||
`arg ...int`告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是`int`。在函数体中,变量`arg`是一个`int`的`slice`:
|
||||
|
||||
for _, n := range arg {
|
||||
fmt.Printf("And the number is: %d\n", n)
|
||||
}
|
||||
|
||||
### 传值与传指针
|
||||
当我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。
|
||||
|
||||
为了验证我们上面的说法,我们来看一个例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
//简单的一个函数,实现了参数+1的操作
|
||||
func add1(a int) int {
|
||||
a = a+1 // 我们改变了a的值
|
||||
return a //返回一个新值
|
||||
}
|
||||
|
||||
func main() {
|
||||
x := 3
|
||||
|
||||
fmt.Println("x = ", x) // 应该输出 "x = 3"
|
||||
|
||||
x1 := add1(x) //调用add1(x)
|
||||
|
||||
fmt.Println("x+1 = ", x1) // 应该输出"x+1 = 4"
|
||||
fmt.Println("x = ", x) // 应该输出"x = 3"
|
||||
}
|
||||
|
||||
看到了吗?虽然我们调用了`add1`函数,并且在`add1`中执行`a = a+1`操作,但是上面例子中`x`变量的值没有发生变化
|
||||
|
||||
理由很简单:因为当我们调用`add1`的时候,`add1`接收的参数其实是`x`的copy,而不是`x`本身。
|
||||
|
||||
那你也许会问了,如果真的需要传这个`x`本身,该怎么办呢?
|
||||
|
||||
这就牵扯到了所谓的指针。我们知道,变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内存。只有`add1`函数知道`x`变量所在的地址,才能修改`x`变量的值。所以我们需要将`x`所在地址`&x`传入函数,并将函数的参数的类型由`int`改为`*int`,即改为指针类型,才能在函数中修改`x`变量的值。此时参数仍然是按copy传递的,只是copy的是一个指针。请看下面的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
//简单的一个函数,实现了参数+1的操作
|
||||
func add1(a *int) int { // 请注意,
|
||||
*a = *a+1 // 修改了a的值
|
||||
return *a // 返回新值
|
||||
}
|
||||
|
||||
func main() {
|
||||
x := 3
|
||||
|
||||
fmt.Println("x = ", x) // 应该输出 "x = 3"
|
||||
|
||||
x1 := add1(&x) // 调用 add1(&x) 传x的地址
|
||||
|
||||
fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"
|
||||
fmt.Println("x = ", x) // 应该输出 "x = 4"
|
||||
}
|
||||
|
||||
这样,我们就达到了修改`x`的目的。那么到底传指针有什么好处呢?
|
||||
|
||||
- 传指针使得多个函数能操作同一个对象。
|
||||
- 传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。
|
||||
- Go语言中`channel`,`slice`,`map`这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变`slice`的长度,则仍需要取地址传递指针)
|
||||
|
||||
### defer
|
||||
Go语言中有种不错的设计,即延迟(defer)语句,你可以在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。如下代码所示,我们一般写打开一个资源是这样操作的:
|
||||
|
||||
func ReadWrite() bool {
|
||||
file.Open("file")
|
||||
// 做一些工作
|
||||
if failureX {
|
||||
file.Close()
|
||||
return false
|
||||
}
|
||||
|
||||
if failureY {
|
||||
file.Close()
|
||||
return false
|
||||
}
|
||||
|
||||
file.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
我们看到上面有很多重复的代码,Go的`defer`有效解决了这个问题。使用它后,不但代码量减少了很多,而且程序变得更优雅。在`defer`后指定的函数会在函数退出前调用。
|
||||
|
||||
func ReadWrite() bool {
|
||||
file.Open("file")
|
||||
defer file.Close()
|
||||
if failureX {
|
||||
return false
|
||||
}
|
||||
if failureY {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
如果有很多调用`defer`,那么`defer`是采用后进先出模式,所以如下代码会输出`4 3 2 1 0`
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
defer fmt.Printf("%d ", i)
|
||||
}
|
||||
|
||||
### 函数作为值、类型
|
||||
|
||||
在Go中函数也是一种变量,我们可以通过`type`来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型
|
||||
|
||||
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])
|
||||
|
||||
函数作为类型到底有什么好处呢?那就是可以把这个类型的函数当做值来传递,请看下面的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type testInt func(int) bool // 声明了一个函数类型
|
||||
|
||||
func isOdd(integer int) bool {
|
||||
if integer%2 == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isEven(integer int) bool {
|
||||
if integer%2 == 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 声明的函数类型在这个地方当做了一个参数
|
||||
|
||||
func filter(slice []int, f testInt) []int {
|
||||
var result []int
|
||||
for _, value := range slice {
|
||||
if f(value) {
|
||||
result = append(result, value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func main(){
|
||||
slice := []int {1, 2, 3, 4, 5, 7}
|
||||
fmt.Println("slice = ", slice)
|
||||
odd := filter(slice, isOdd) // 函数当做值来传递了
|
||||
fmt.Println("Odd elements of slice are: ", odd)
|
||||
even := filter(slice, isEven) // 函数当做值来传递了
|
||||
fmt.Println("Even elements of slice are: ", even)
|
||||
}
|
||||
|
||||
函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到`testInt`这个类型是一个函数类型,然后两个`filter`函数的参数和返回值与`testInt`类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。
|
||||
|
||||
### Panic和Recover
|
||||
|
||||
Go没有像Java那样的异常机制,它不能抛出异常,而是使用了`panic`和`recover`机制。一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有`panic`的东西。这是个强大的工具,请明智地使用它。那么,我们应该如何使用它呢?
|
||||
|
||||
Panic
|
||||
>是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函数`F`调用`panic`,函数F的执行被中断,但是`F`中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,`F`的行为就像调用了`panic`。这一过程继续向上,直到发生`panic`的`goroutine`中所有调用的函数返回,此时程序退出。恐慌可以直接调用`panic`产生。也可以由运行时错误产生,例如访问越界的数组。
|
||||
|
||||
Recover
|
||||
>是一个内建的函数,可以让进入令人恐慌的流程中的`goroutine`恢复过来。`recover`仅在延迟函数中有效。在正常的执行过程中,调用`recover`会返回`nil`,并且没有其它任何效果。如果当前的`goroutine`陷入恐慌,调用`recover`可以捕获到`panic`的输入值,并且恢复正常的执行。
|
||||
|
||||
下面这个函数演示了如何在过程中使用`panic`
|
||||
|
||||
var user = os.Getenv("USER")
|
||||
|
||||
func init() {
|
||||
if user == "" {
|
||||
panic("no value for $USER")
|
||||
}
|
||||
}
|
||||
|
||||
下面这个函数检查作为其参数的函数在执行时是否会产生`panic`:
|
||||
|
||||
func throwsPanic(f func()) (b bool) {
|
||||
defer func() {
|
||||
if x := recover(); x != nil {
|
||||
b = true
|
||||
}
|
||||
}()
|
||||
f() //执行函数f,如果f中出现了panic,那么就可以恢复回来
|
||||
return
|
||||
}
|
||||
|
||||
### `main`函数和`init`函数
|
||||
|
||||
Go里面有两个保留的函数:`init`函数(能够应用于所有的`package`)和`main`函数(只能应用于`package main`)。这两个函数在定义时不能有任何的参数和返回值。虽然一个`package`里面可以写任意多个`init`函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个`package`中每个文件只写一个`init`函数。
|
||||
|
||||
Go程序会自动调用`init()`和`main()`,所以你不需要在任何地方调用这两个函数。每个`package`中的`init`函数都是可选的,但`package main`就必须包含一个`main`函数。
|
||||
|
||||
程序的初始化和执行都起始于`main`包。如果`main`包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到`fmt`包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行`init`函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对`main`包中的包级常量和变量进行初始化,然后执行`main`包中的`init`函数(如果存在的话),最后执行`main`函数。下图详细地解释了整个执行过程:
|
||||
|
||||

|
||||
|
||||
图2.6 main函数引入包初始化流程图
|
||||
|
||||
### import
|
||||
我们在写Go代码的时候经常用到import这个命令用来导入包文件,而我们经常看到的方式参考如下:
|
||||
|
||||
import(
|
||||
"fmt"
|
||||
)
|
||||
|
||||
然后我们代码里面可以通过如下的方式调用
|
||||
|
||||
fmt.Println("hello world")
|
||||
|
||||
上面这个fmt是Go语言的标准库,其实是去`GOROOT`环境变量指定目录下去加载该模块,当然Go的import还支持如下两种方式来加载自己写的模块:
|
||||
|
||||
1. 相对路径
|
||||
|
||||
import “./model” //当前文件同一目录的model目录,但是不建议这种方式来import
|
||||
|
||||
2. 绝对路径
|
||||
|
||||
import “shorturl/model” //加载gopath/src/shorturl/model模块
|
||||
|
||||
|
||||
上面展示了一些import常用的几种方式,但是还有一些特殊的import,让很多新手很费解,下面我们来一一讲解一下到底是怎么一回事
|
||||
|
||||
|
||||
1. 点操作
|
||||
|
||||
我们有时候会看到如下的方式导入包
|
||||
|
||||
import(
|
||||
. "fmt"
|
||||
)
|
||||
|
||||
这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println("hello world")可以省略的写成Println("hello world")
|
||||
|
||||
2. 别名操作
|
||||
|
||||
别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字
|
||||
|
||||
import(
|
||||
f "fmt"
|
||||
)
|
||||
|
||||
别名操作的话调用包函数时前缀变成了我们的前缀,即f.Println("hello world")
|
||||
|
||||
3. _操作
|
||||
|
||||
这个操作经常是让很多人费解的一个操作符,请看下面这个import
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
_ "github.com/ziutek/mymysql/godrv"
|
||||
)
|
||||
|
||||
_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [Go基础](<02.2.md>)
|
||||
* 下一节: [struct类型](<02.4.md>)
|
||||
@@ -1,213 +1,213 @@
|
||||
# 2.4 struct类型
|
||||
## struct
|
||||
Go语言中,也和C或者其他语言一样,我们可以声明新的类型,作为其它类型的属性或字段的容器。例如,我们可以创建一个自定义类型`person`代表一个人的实体。这个实体拥有属性:姓名和年龄。这样的类型我们称之`struct`。如下代码所示:
|
||||
|
||||
type person struct {
|
||||
name string
|
||||
age int
|
||||
}
|
||||
看到了吗?声明一个struct如此简单,上面的类型包含有两个字段
|
||||
- 一个string类型的字段name,用来保存用户名称这个属性
|
||||
- 一个int类型的字段age,用来保存用户年龄这个属性
|
||||
|
||||
如何使用struct呢?请看下面的代码
|
||||
|
||||
type person struct {
|
||||
name string
|
||||
age int
|
||||
}
|
||||
|
||||
var P person // P现在就是person类型的变量了
|
||||
|
||||
P.name = "Astaxie" // 赋值"Astaxie"给P的name属性.
|
||||
P.age = 25 // 赋值"25"给变量P的age属性
|
||||
fmt.Printf("The person's name is %s", P.name) // 访问P的name属性.
|
||||
除了上面这种P的声明使用之外,还有另外几种声明使用方式:
|
||||
|
||||
- 1.按照顺序提供初始化值
|
||||
|
||||
P := person{"Tom", 25}
|
||||
|
||||
- 2.通过`field:value`的方式初始化,这样可以任意顺序
|
||||
|
||||
P := person{age:24, name:"Tom"}
|
||||
|
||||
- 3.当然也可以通过`new`函数分配一个指针,此处P的类型为*person
|
||||
|
||||
P := new(person)
|
||||
|
||||
下面我们看一个完整的使用struct的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
// 声明一个新的类型
|
||||
type person struct {
|
||||
name string
|
||||
age int
|
||||
}
|
||||
|
||||
// 比较两个人的年龄,返回年龄大的那个人,并且返回年龄差
|
||||
// struct也是传值的
|
||||
func Older(p1, p2 person) (person, int) {
|
||||
if p1.age>p2.age { // 比较p1和p2这两个人的年龄
|
||||
return p1, p1.age-p2.age
|
||||
}
|
||||
return p2, p2.age-p1.age
|
||||
}
|
||||
|
||||
func main() {
|
||||
var tom person
|
||||
|
||||
// 赋值初始化
|
||||
tom.name, tom.age = "Tom", 18
|
||||
|
||||
// 两个字段都写清楚的初始化
|
||||
bob := person{age:25, name:"Bob"}
|
||||
|
||||
// 按照struct定义顺序初始化值
|
||||
paul := person{"Paul", 43}
|
||||
|
||||
tb_Older, tb_diff := Older(tom, bob)
|
||||
tp_Older, tp_diff := Older(tom, paul)
|
||||
bp_Older, bp_diff := Older(bob, paul)
|
||||
|
||||
fmt.Printf("Of %s and %s, %s is older by %d years\n",
|
||||
tom.name, bob.name, tb_Older.name, tb_diff)
|
||||
|
||||
fmt.Printf("Of %s and %s, %s is older by %d years\n",
|
||||
tom.name, paul.name, tp_Older.name, tp_diff)
|
||||
|
||||
fmt.Printf("Of %s and %s, %s is older by %d years\n",
|
||||
bob.name, paul.name, bp_Older.name, bp_diff)
|
||||
}
|
||||
|
||||
### struct的匿名字段
|
||||
我们上面介绍了如何定义一个struct,定义的时候是字段名与其类型一一对应,实际上Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。
|
||||
|
||||
当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。
|
||||
|
||||
让我们来看一个例子,让上面说的这些更具体化
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
weight int
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
Human // 匿名字段,那么默认Student就包含了Human的所有字段
|
||||
speciality string
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 我们初始化一个学生
|
||||
mark := Student{Human{"Mark", 25, 120}, "Computer Science"}
|
||||
|
||||
// 我们访问相应的字段
|
||||
fmt.Println("His name is ", mark.name)
|
||||
fmt.Println("His age is ", mark.age)
|
||||
fmt.Println("His weight is ", mark.weight)
|
||||
fmt.Println("His speciality is ", mark.speciality)
|
||||
// 修改对应的备注信息
|
||||
mark.speciality = "AI"
|
||||
fmt.Println("Mark changed his speciality")
|
||||
fmt.Println("His speciality is ", mark.speciality)
|
||||
// 修改他的年龄信息
|
||||
fmt.Println("Mark become old")
|
||||
mark.age = 46
|
||||
fmt.Println("His age is", mark.age)
|
||||
// 修改他的体重信息
|
||||
fmt.Println("Mark is not an athlet anymore")
|
||||
mark.weight += 60
|
||||
fmt.Println("His weight is", mark.weight)
|
||||
}
|
||||
|
||||
图例如下:
|
||||
|
||||

|
||||
|
||||
图2.7 struct组合,Student组合了Human struct和string基本类型
|
||||
|
||||
我们看到Student访问属性age和name的时候,就像访问自己所有用的字段一样,对,匿名字段就是这样,能够实现字段的继承。是不是很酷啊?还有比这个更酷的呢,那就是student还能访问Human这个字段作为字段名。请看下面的代码,是不是更酷了。
|
||||
|
||||
mark.Human = Human{"Marcus", 55, 220}
|
||||
mark.Human.age -= 1
|
||||
|
||||
通过匿名访问和修改字段相当的有用,但是不仅仅是struct字段哦,所有的内置类型和自定义类型都是可以作为匿名字段的。请看下面的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Skills []string
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
weight int
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
Human // 匿名字段,struct
|
||||
Skills // 匿名字段,自定义的类型string slice
|
||||
int // 内置类型作为匿名字段
|
||||
speciality string
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 初始化学生Jane
|
||||
jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"}
|
||||
// 现在我们来访问相应的字段
|
||||
fmt.Println("Her name is ", jane.name)
|
||||
fmt.Println("Her age is ", jane.age)
|
||||
fmt.Println("Her weight is ", jane.weight)
|
||||
fmt.Println("Her speciality is ", jane.speciality)
|
||||
// 我们来修改他的skill技能字段
|
||||
jane.Skills = []string{"anatomy"}
|
||||
fmt.Println("Her skills are ", jane.Skills)
|
||||
fmt.Println("She acquired two new ones ")
|
||||
jane.Skills = append(jane.Skills, "physics", "golang")
|
||||
fmt.Println("Her skills now are ", jane.Skills)
|
||||
// 修改匿名内置类型字段
|
||||
jane.int = 3
|
||||
fmt.Println("Her preferred number is", jane.int)
|
||||
}
|
||||
|
||||
从上面例子我们看出来struct不仅仅能够将struct作为匿名字段、自定义类型、内置类型都可以作为匿名字段,而且可以在相应的字段上面进行函数操作(如例子中的append)。
|
||||
|
||||
这里有一个问题:如果human里面有一个字段叫做phone,而student也有一个字段叫做phone,那么该怎么办呢?
|
||||
|
||||
Go里面很简单的解决了这个问题,最外层的优先访问,也就是当你通过`student.phone`访问的时候,是访问student里面的字段,而不是human里面的字段。
|
||||
|
||||
这样就允许我们去重载通过匿名字段继承的一些字段,当然如果我们想访问重载后对应匿名类型里面的字段,可以通过匿名字段名来访问。请看下面的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
phone string // Human类型拥有的字段
|
||||
}
|
||||
|
||||
type Employee struct {
|
||||
Human // 匿名字段Human
|
||||
speciality string
|
||||
phone string // 雇员的phone字段
|
||||
}
|
||||
|
||||
func main() {
|
||||
Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"}
|
||||
fmt.Println("Bob's work phone is:", Bob.phone)
|
||||
// 如果我们要访问Human的phone字段
|
||||
fmt.Println("Bob's personal phone is:", Bob.Human.phone)
|
||||
}
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [流程和函数](<02.3.md>)
|
||||
* 下一节: [面向对象](<02.5.md>)
|
||||
# 2.4 struct类型
|
||||
## struct
|
||||
Go语言中,也和C或者其他语言一样,我们可以声明新的类型,作为其它类型的属性或字段的容器。例如,我们可以创建一个自定义类型`person`代表一个人的实体。这个实体拥有属性:姓名和年龄。这样的类型我们称之`struct`。如下代码所示:
|
||||
|
||||
type person struct {
|
||||
name string
|
||||
age int
|
||||
}
|
||||
看到了吗?声明一个struct如此简单,上面的类型包含有两个字段
|
||||
- 一个string类型的字段name,用来保存用户名称这个属性
|
||||
- 一个int类型的字段age,用来保存用户年龄这个属性
|
||||
|
||||
如何使用struct呢?请看下面的代码
|
||||
|
||||
type person struct {
|
||||
name string
|
||||
age int
|
||||
}
|
||||
|
||||
var P person // P现在就是person类型的变量了
|
||||
|
||||
P.name = "Astaxie" // 赋值"Astaxie"给P的name属性.
|
||||
P.age = 25 // 赋值"25"给变量P的age属性
|
||||
fmt.Printf("The person's name is %s", P.name) // 访问P的name属性.
|
||||
除了上面这种P的声明使用之外,还有另外几种声明使用方式:
|
||||
|
||||
- 1.按照顺序提供初始化值
|
||||
|
||||
P := person{"Tom", 25}
|
||||
|
||||
- 2.通过`field:value`的方式初始化,这样可以任意顺序
|
||||
|
||||
P := person{age:24, name:"Tom"}
|
||||
|
||||
- 3.当然也可以通过`new`函数分配一个指针,此处P的类型为*person
|
||||
|
||||
P := new(person)
|
||||
|
||||
下面我们看一个完整的使用struct的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
// 声明一个新的类型
|
||||
type person struct {
|
||||
name string
|
||||
age int
|
||||
}
|
||||
|
||||
// 比较两个人的年龄,返回年龄大的那个人,并且返回年龄差
|
||||
// struct也是传值的
|
||||
func Older(p1, p2 person) (person, int) {
|
||||
if p1.age>p2.age { // 比较p1和p2这两个人的年龄
|
||||
return p1, p1.age-p2.age
|
||||
}
|
||||
return p2, p2.age-p1.age
|
||||
}
|
||||
|
||||
func main() {
|
||||
var tom person
|
||||
|
||||
// 赋值初始化
|
||||
tom.name, tom.age = "Tom", 18
|
||||
|
||||
// 两个字段都写清楚的初始化
|
||||
bob := person{age:25, name:"Bob"}
|
||||
|
||||
// 按照struct定义顺序初始化值
|
||||
paul := person{"Paul", 43}
|
||||
|
||||
tb_Older, tb_diff := Older(tom, bob)
|
||||
tp_Older, tp_diff := Older(tom, paul)
|
||||
bp_Older, bp_diff := Older(bob, paul)
|
||||
|
||||
fmt.Printf("Of %s and %s, %s is older by %d years\n",
|
||||
tom.name, bob.name, tb_Older.name, tb_diff)
|
||||
|
||||
fmt.Printf("Of %s and %s, %s is older by %d years\n",
|
||||
tom.name, paul.name, tp_Older.name, tp_diff)
|
||||
|
||||
fmt.Printf("Of %s and %s, %s is older by %d years\n",
|
||||
bob.name, paul.name, bp_Older.name, bp_diff)
|
||||
}
|
||||
|
||||
### struct的匿名字段
|
||||
我们上面介绍了如何定义一个struct,定义的时候是字段名与其类型一一对应,实际上Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。
|
||||
|
||||
当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。
|
||||
|
||||
让我们来看一个例子,让上面说的这些更具体化
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
weight int
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
Human // 匿名字段,那么默认Student就包含了Human的所有字段
|
||||
speciality string
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 我们初始化一个学生
|
||||
mark := Student{Human{"Mark", 25, 120}, "Computer Science"}
|
||||
|
||||
// 我们访问相应的字段
|
||||
fmt.Println("His name is ", mark.name)
|
||||
fmt.Println("His age is ", mark.age)
|
||||
fmt.Println("His weight is ", mark.weight)
|
||||
fmt.Println("His speciality is ", mark.speciality)
|
||||
// 修改对应的备注信息
|
||||
mark.speciality = "AI"
|
||||
fmt.Println("Mark changed his speciality")
|
||||
fmt.Println("His speciality is ", mark.speciality)
|
||||
// 修改他的年龄信息
|
||||
fmt.Println("Mark become old")
|
||||
mark.age = 46
|
||||
fmt.Println("His age is", mark.age)
|
||||
// 修改他的体重信息
|
||||
fmt.Println("Mark is not an athlet anymore")
|
||||
mark.weight += 60
|
||||
fmt.Println("His weight is", mark.weight)
|
||||
}
|
||||
|
||||
图例如下:
|
||||
|
||||

|
||||
|
||||
图2.7 struct组合,Student组合了Human struct和string基本类型
|
||||
|
||||
我们看到Student访问属性age和name的时候,就像访问自己所有用的字段一样,对,匿名字段就是这样,能够实现字段的继承。是不是很酷啊?还有比这个更酷的呢,那就是student还能访问Human这个字段作为字段名。请看下面的代码,是不是更酷了。
|
||||
|
||||
mark.Human = Human{"Marcus", 55, 220}
|
||||
mark.Human.age -= 1
|
||||
|
||||
通过匿名访问和修改字段相当的有用,但是不仅仅是struct字段哦,所有的内置类型和自定义类型都是可以作为匿名字段的。请看下面的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Skills []string
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
weight int
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
Human // 匿名字段,struct
|
||||
Skills // 匿名字段,自定义的类型string slice
|
||||
int // 内置类型作为匿名字段
|
||||
speciality string
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 初始化学生Jane
|
||||
jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"}
|
||||
// 现在我们来访问相应的字段
|
||||
fmt.Println("Her name is ", jane.name)
|
||||
fmt.Println("Her age is ", jane.age)
|
||||
fmt.Println("Her weight is ", jane.weight)
|
||||
fmt.Println("Her speciality is ", jane.speciality)
|
||||
// 我们来修改他的skill技能字段
|
||||
jane.Skills = []string{"anatomy"}
|
||||
fmt.Println("Her skills are ", jane.Skills)
|
||||
fmt.Println("She acquired two new ones ")
|
||||
jane.Skills = append(jane.Skills, "physics", "golang")
|
||||
fmt.Println("Her skills now are ", jane.Skills)
|
||||
// 修改匿名内置类型字段
|
||||
jane.int = 3
|
||||
fmt.Println("Her preferred number is", jane.int)
|
||||
}
|
||||
|
||||
从上面例子我们看出来struct不仅仅能够将struct作为匿名字段、自定义类型、内置类型都可以作为匿名字段,而且可以在相应的字段上面进行函数操作(如例子中的append)。
|
||||
|
||||
这里有一个问题:如果human里面有一个字段叫做phone,而student也有一个字段叫做phone,那么该怎么办呢?
|
||||
|
||||
Go里面很简单的解决了这个问题,最外层的优先访问,也就是当你通过`student.phone`访问的时候,是访问student里面的字段,而不是human里面的字段。
|
||||
|
||||
这样就允许我们去重载通过匿名字段继承的一些字段,当然如果我们想访问重载后对应匿名类型里面的字段,可以通过匿名字段名来访问。请看下面的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
phone string // Human类型拥有的字段
|
||||
}
|
||||
|
||||
type Employee struct {
|
||||
Human // 匿名字段Human
|
||||
speciality string
|
||||
phone string // 雇员的phone字段
|
||||
}
|
||||
|
||||
func main() {
|
||||
Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"}
|
||||
fmt.Println("Bob's work phone is:", Bob.phone)
|
||||
// 如果我们要访问Human的phone字段
|
||||
fmt.Println("Bob's personal phone is:", Bob.Human.phone)
|
||||
}
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [流程和函数](<02.3.md>)
|
||||
* 下一节: [面向对象](<02.5.md>)
|
||||
213
zh/02.4.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
213
zh/02.4.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,213 @@
|
||||
# 2.4 struct类型
|
||||
## struct
|
||||
Go语言中,也和C或者其他语言一样,我们可以声明新的类型,作为其它类型的属性或字段的容器。例如,我们可以创建一个自定义类型`person`代表一个人的实体。这个实体拥有属性:姓名和年龄。这样的类型我们称之`struct`。如下代码所示:
|
||||
|
||||
type person struct {
|
||||
name string
|
||||
age int
|
||||
}
|
||||
看到了吗?声明一个struct如此简单,上面的类型包含有两个字段
|
||||
- 一个string类型的字段name,用来保存用户名称这个属性
|
||||
- 一个int类型的字段age,用来保存用户年龄这个属性
|
||||
|
||||
如何使用struct呢?请看下面的代码
|
||||
|
||||
type person struct {
|
||||
name string
|
||||
age int
|
||||
}
|
||||
|
||||
var P person // P现在就是person类型的变量了
|
||||
|
||||
P.name = "Astaxie" // 赋值"Astaxie"给P的name属性.
|
||||
P.age = 25 // 赋值"25"给变量P的age属性
|
||||
fmt.Printf("The person's name is %s", P.name) // 访问P的name属性.
|
||||
除了上面这种P的声明使用之外,还有另外几种声明使用方式:
|
||||
|
||||
- 1.按照顺序提供初始化值
|
||||
|
||||
P := person{"Tom", 25}
|
||||
|
||||
- 2.通过`field:value`的方式初始化,这样可以任意顺序
|
||||
|
||||
P := person{age:24, name:"Tom"}
|
||||
|
||||
- 3.当然也可以通过`new`函数分配一个指针,此处P的类型为*person
|
||||
|
||||
P := new(person)
|
||||
|
||||
下面我们看一个完整的使用struct的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
// 声明一个新的类型
|
||||
type person struct {
|
||||
name string
|
||||
age int
|
||||
}
|
||||
|
||||
// 比较两个人的年龄,返回年龄大的那个人,并且返回年龄差
|
||||
// struct也是传值的
|
||||
func Older(p1, p2 person) (person, int) {
|
||||
if p1.age>p2.age { // 比较p1和p2这两个人的年龄
|
||||
return p1, p1.age-p2.age
|
||||
}
|
||||
return p2, p2.age-p1.age
|
||||
}
|
||||
|
||||
func main() {
|
||||
var tom person
|
||||
|
||||
// 赋值初始化
|
||||
tom.name, tom.age = "Tom", 18
|
||||
|
||||
// 两个字段都写清楚的初始化
|
||||
bob := person{age:25, name:"Bob"}
|
||||
|
||||
// 按照struct定义顺序初始化值
|
||||
paul := person{"Paul", 43}
|
||||
|
||||
tb_Older, tb_diff := Older(tom, bob)
|
||||
tp_Older, tp_diff := Older(tom, paul)
|
||||
bp_Older, bp_diff := Older(bob, paul)
|
||||
|
||||
fmt.Printf("Of %s and %s, %s is older by %d years\n",
|
||||
tom.name, bob.name, tb_Older.name, tb_diff)
|
||||
|
||||
fmt.Printf("Of %s and %s, %s is older by %d years\n",
|
||||
tom.name, paul.name, tp_Older.name, tp_diff)
|
||||
|
||||
fmt.Printf("Of %s and %s, %s is older by %d years\n",
|
||||
bob.name, paul.name, bp_Older.name, bp_diff)
|
||||
}
|
||||
|
||||
### struct的匿名字段
|
||||
我们上面介绍了如何定义一个struct,定义的时候是字段名与其类型一一对应,实际上Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。
|
||||
|
||||
当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。
|
||||
|
||||
让我们来看一个例子,让上面说的这些更具体化
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
weight int
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
Human // 匿名字段,那么默认Student就包含了Human的所有字段
|
||||
speciality string
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 我们初始化一个学生
|
||||
mark := Student{Human{"Mark", 25, 120}, "Computer Science"}
|
||||
|
||||
// 我们访问相应的字段
|
||||
fmt.Println("His name is ", mark.name)
|
||||
fmt.Println("His age is ", mark.age)
|
||||
fmt.Println("His weight is ", mark.weight)
|
||||
fmt.Println("His speciality is ", mark.speciality)
|
||||
// 修改对应的备注信息
|
||||
mark.speciality = "AI"
|
||||
fmt.Println("Mark changed his speciality")
|
||||
fmt.Println("His speciality is ", mark.speciality)
|
||||
// 修改他的年龄信息
|
||||
fmt.Println("Mark become old")
|
||||
mark.age = 46
|
||||
fmt.Println("His age is", mark.age)
|
||||
// 修改他的体重信息
|
||||
fmt.Println("Mark is not an athlet anymore")
|
||||
mark.weight += 60
|
||||
fmt.Println("His weight is", mark.weight)
|
||||
}
|
||||
|
||||
图例如下:
|
||||
|
||||

|
||||
|
||||
图2.7 struct组合,Student组合了Human struct和string基本类型
|
||||
|
||||
我们看到Student访问属性age和name的时候,就像访问自己所有用的字段一样,对,匿名字段就是这样,能够实现字段的继承。是不是很酷啊?还有比这个更酷的呢,那就是student还能访问Human这个字段作为字段名。请看下面的代码,是不是更酷了。
|
||||
|
||||
mark.Human = Human{"Marcus", 55, 220}
|
||||
mark.Human.age -= 1
|
||||
|
||||
通过匿名访问和修改字段相当的有用,但是不仅仅是struct字段哦,所有的内置类型和自定义类型都是可以作为匿名字段的。请看下面的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Skills []string
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
weight int
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
Human // 匿名字段,struct
|
||||
Skills // 匿名字段,自定义的类型string slice
|
||||
int // 内置类型作为匿名字段
|
||||
speciality string
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 初始化学生Jane
|
||||
jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"}
|
||||
// 现在我们来访问相应的字段
|
||||
fmt.Println("Her name is ", jane.name)
|
||||
fmt.Println("Her age is ", jane.age)
|
||||
fmt.Println("Her weight is ", jane.weight)
|
||||
fmt.Println("Her speciality is ", jane.speciality)
|
||||
// 我们来修改他的skill技能字段
|
||||
jane.Skills = []string{"anatomy"}
|
||||
fmt.Println("Her skills are ", jane.Skills)
|
||||
fmt.Println("She acquired two new ones ")
|
||||
jane.Skills = append(jane.Skills, "physics", "golang")
|
||||
fmt.Println("Her skills now are ", jane.Skills)
|
||||
// 修改匿名内置类型字段
|
||||
jane.int = 3
|
||||
fmt.Println("Her preferred number is", jane.int)
|
||||
}
|
||||
|
||||
从上面例子我们看出来struct不仅仅能够将struct作为匿名字段、自定义类型、内置类型都可以作为匿名字段,而且可以在相应的字段上面进行函数操作(如例子中的append)。
|
||||
|
||||
这里有一个问题:如果human里面有一个字段叫做phone,而student也有一个字段叫做phone,那么该怎么办呢?
|
||||
|
||||
Go里面很简单的解决了这个问题,最外层的优先访问,也就是当你通过`student.phone`访问的时候,是访问student里面的字段,而不是human里面的字段。
|
||||
|
||||
这样就允许我们去重载通过匿名字段继承的一些字段,当然如果我们想访问重载后对应匿名类型里面的字段,可以通过匿名字段名来访问。请看下面的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
phone string // Human类型拥有的字段
|
||||
}
|
||||
|
||||
type Employee struct {
|
||||
Human // 匿名字段Human
|
||||
speciality string
|
||||
phone string // 雇员的phone字段
|
||||
}
|
||||
|
||||
func main() {
|
||||
Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"}
|
||||
fmt.Println("Bob's work phone is:", Bob.phone)
|
||||
// 如果我们要访问Human的phone字段
|
||||
fmt.Println("Bob's personal phone is:", Bob.Human.phone)
|
||||
}
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [流程和函数](<02.3.md>)
|
||||
* 下一节: [面向对象](<02.5.md>)
|
||||
@@ -1,325 +1,325 @@
|
||||
# 2.5 面向对象
|
||||
前面两章我们介绍了函数和struct,那你是否想过函数当作struct的字段一样来处理呢?今天我们就讲解一下函数的另一种形态,带有接收者的函数,我们称为`method`
|
||||
|
||||
## method
|
||||
现在假设有这么一个场景,你定义了一个struct叫做长方形,你现在想要计算他的面积,那么按照我们一般的思路应该会用下面的方式来实现
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Rectangle struct {
|
||||
width, height float64
|
||||
}
|
||||
|
||||
func area(r Rectangle) float64 {
|
||||
return r.width*r.height
|
||||
}
|
||||
|
||||
func main() {
|
||||
r1 := Rectangle{12, 2}
|
||||
r2 := Rectangle{9, 4}
|
||||
fmt.Println("Area of r1 is: ", area(r1))
|
||||
fmt.Println("Area of r2 is: ", area(r2))
|
||||
}
|
||||
|
||||
这段代码可以计算出来长方形的面积,但是area()不是作为Rectangle的方法实现的(类似面向对象里面的方法),而是将Rectangle的对象(如r1,r2)作为参数传入函数计算面积的。
|
||||
|
||||
这样实现当然没有问题咯,但是当需要增加圆形、正方形、五边形甚至其它多边形的时候,你想计算他们的面积的时候怎么办啊?那就只能增加新的函数咯,但是函数名你就必须要跟着换了,变成`area_rectangle, area_circle, area_triangle...`
|
||||
|
||||
像下图所表示的那样, 椭圆代表函数, 而这些函数并不从属于struct(或者以面向对象的术语来说,并不属于class),他们是单独存在于struct外围,而非在概念上属于某个struct的。
|
||||
|
||||

|
||||
|
||||
图2.8 方法和struct的关系图
|
||||
|
||||
很显然,这样的实现并不优雅,并且从概念上来说"面积"是"形状"的一个属性,它是属于这个特定的形状的,就像长方形的长和宽一样。
|
||||
|
||||
基于上面的原因所以就有了`method`的概念,`method`是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在`func`后面增加了一个receiver(也就是method所依从的主体)。
|
||||
|
||||
用上面提到的形状的例子来说,method `area()` 是依赖于某个形状(比如说Rectangle)来发生作用的。Rectangle.area()的发出者是Rectangle, area()是属于Rectangle的方法,而非一个外围函数。
|
||||
|
||||
更具体地说,Rectangle存在字段length 和 width, 同时存在方法area(), 这些字段和方法都属于Rectangle。
|
||||
|
||||
用Rob Pike的话来说就是:
|
||||
|
||||
>"A method is a function with an implicit first argument, called a receiver."
|
||||
|
||||
method的语法如下:
|
||||
|
||||
func (r ReceiverType) funcName(parameters) (results)
|
||||
|
||||
下面我们用最开始的例子用method来实现:
|
||||
|
||||
package main
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
type Rectangle struct {
|
||||
width, height float64
|
||||
}
|
||||
|
||||
type Circle struct {
|
||||
radius float64
|
||||
}
|
||||
|
||||
func (r Rectangle) area() float64 {
|
||||
return r.width*r.height
|
||||
}
|
||||
|
||||
func (c Circle) area() float64 {
|
||||
return c.radius * c.radius * math.Pi
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
r1 := Rectangle{12, 2}
|
||||
r2 := Rectangle{9, 4}
|
||||
c1 := Circle{10}
|
||||
c2 := Circle{25}
|
||||
|
||||
fmt.Println("Area of r1 is: ", r1.area())
|
||||
fmt.Println("Area of r2 is: ", r2.area())
|
||||
fmt.Println("Area of c1 is: ", c1.area())
|
||||
fmt.Println("Area of c2 is: ", c2.area())
|
||||
}
|
||||
|
||||
|
||||
|
||||
在使用method的时候重要注意几点
|
||||
|
||||
- 虽然method的名字一模一样,但是如果接收者不一样,那么method就不一样
|
||||
- method里面可以访问接收者的字段
|
||||
- 调用method通过`.`访问,就像struct里面访问字段一样
|
||||
|
||||
图示如下:
|
||||
|
||||

|
||||
|
||||
图2.9 不同struct的method不同
|
||||
|
||||
在上例,method area() 分别属于Rectangle和Circle, 于是他们的 Receiver 就变成了Rectangle 和 Circle, 或者说,这个area()方法 是由 Rectangle/Circle 发出的。
|
||||
|
||||
>值得说明的一点是,图示中method用虚线标出,意思是此处方法的Receiver是以值传递,而非引用传递,是的,Receiver还可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。后文对此会有详细论述。
|
||||
|
||||
那是不是method只能作用在struct上面呢?当然不是咯,他可以定义在任何你自定义的类型、内置类型、struct等各种类型上面。这里你是不是有点迷糊了,什么叫自定义类型,自定义类型不就是struct嘛,不是这样的哦,struct只是自定义类型里面一种比较特殊的类型而已,还有其他自定义类型申明,可以通过如下这样的申明来实现。
|
||||
|
||||
type typeName typeLiteral
|
||||
|
||||
请看下面这个申明自定义类型的代码
|
||||
|
||||
type ages int
|
||||
|
||||
type money float32
|
||||
|
||||
type months map[string]int
|
||||
|
||||
m := months {
|
||||
"January":31,
|
||||
"February":28,
|
||||
...
|
||||
"December":31,
|
||||
}
|
||||
|
||||
看到了吗?简单的很吧,这样你就可以在自己的代码里面定义有意义的类型了,实际上只是一个定义了一个别名,有点类似于c中的typedef,例如上面ages替代了int
|
||||
|
||||
好了,让我们回到`method`
|
||||
|
||||
你可以在任何的自定义类型中定义任意多的`method`,接下来让我们看一个复杂一点的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
const(
|
||||
WHITE = iota
|
||||
BLACK
|
||||
BLUE
|
||||
RED
|
||||
YELLOW
|
||||
)
|
||||
|
||||
type Color byte
|
||||
|
||||
type Box struct {
|
||||
width, height, depth float64
|
||||
color Color
|
||||
}
|
||||
|
||||
type BoxList []Box //a slice of boxes
|
||||
|
||||
func (b Box) Volume() float64 {
|
||||
return b.width * b.height * b.depth
|
||||
}
|
||||
|
||||
func (b *Box) SetColor(c Color) {
|
||||
b.color = c
|
||||
}
|
||||
|
||||
func (bl BoxList) BiggestColor() Color {
|
||||
v := 0.00
|
||||
k := Color(WHITE)
|
||||
for _, b := range bl {
|
||||
if bv := b.Volume(); bv > v {
|
||||
v = bv
|
||||
k = b.color
|
||||
}
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
func (bl BoxList) PaintItBlack() {
|
||||
for i, _ := range bl {
|
||||
bl[i].SetColor(BLACK)
|
||||
}
|
||||
}
|
||||
|
||||
func (c Color) String() string {
|
||||
strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
|
||||
return strings[c]
|
||||
}
|
||||
|
||||
func main() {
|
||||
boxes := BoxList {
|
||||
Box{4, 4, 4, RED},
|
||||
Box{10, 10, 1, YELLOW},
|
||||
Box{1, 1, 20, BLACK},
|
||||
Box{10, 10, 1, BLUE},
|
||||
Box{10, 30, 1, WHITE},
|
||||
Box{20, 20, 20, YELLOW},
|
||||
}
|
||||
|
||||
fmt.Printf("We have %d boxes in our set\n", len(boxes))
|
||||
fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
|
||||
fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String())
|
||||
fmt.Println("The biggest one is", boxes.BiggestColor().String())
|
||||
|
||||
fmt.Println("Let's paint them all black")
|
||||
boxes.PaintItBlack()
|
||||
fmt.Println("The color of the second one is", boxes[1].color.String())
|
||||
|
||||
fmt.Println("Obviously, now, the biggest one is", boxes.BiggestColor().String())
|
||||
}
|
||||
|
||||
上面的代码通过const定义了一些常量,然后定义了一些自定义类型
|
||||
|
||||
- Color作为byte的别名
|
||||
- 定义了一个struct:Box,含有三个长宽高字段和一个颜色属性
|
||||
- 定义了一个slice:BoxList,含有Box
|
||||
|
||||
然后以上面的自定义类型为接收者定义了一些method
|
||||
|
||||
- Volume()定义了接收者为Box,返回Box的容量
|
||||
- SetColor(c Color),把Box的颜色改为c
|
||||
- BiggestColor()定在在BoxList上面,返回list里面容量最大的颜色
|
||||
- PaintItBlack()把BoxList里面所有Box的颜色全部变成黑色
|
||||
- String()定义在Color上面,返回Color的具体颜色(字符串格式)
|
||||
|
||||
上面的代码通过文字描述出来之后是不是很简单?我们一般解决问题都是通过问题的描述,去写相应的代码实现。
|
||||
|
||||
### 指针作为receiver
|
||||
现在让我们回过头来看看SetColor这个method,它的receiver是一个指向Box的指针,是的,你可以使用*Box。想想为啥要使用指针而不是Box本身呢?
|
||||
|
||||
我们定义SetColor的真正目的是想改变这个Box的颜色,如果不传Box的指针,那么SetColor接受的其实是Box的一个copy,也就是说method内对于颜色值的修改,其实只作用于Box的copy,而不是真正的Box。所以我们需要传入指针。
|
||||
|
||||
这里可以把receiver当作method的第一个参数来看,然后结合前面函数讲解的传值和传引用就不难理解
|
||||
|
||||
这里你也许会问了那SetColor函数里面应该这样定义`*b.Color=c`,而不是`b.Color=c`,因为我们需要读取到指针相应的值。
|
||||
|
||||
你是对的,其实Go里面这两种方式都是正确的,当你用指针去访问相应的字段时(虽然指针没有任何的字段),Go知道你要通过指针去获取这个值,看到了吧,Go的设计是不是越来越吸引你了。
|
||||
|
||||
也许细心的读者会问这样的问题,PaintItBlack里面调用SetColor的时候是不是应该写成`(&bl[i]).SetColor(BLACK)`,因为SetColor的receiver是*Box,而不是Box。
|
||||
|
||||
你又说对的,这两种方式都可以,因为Go知道receiver是指针,他自动帮你转了。
|
||||
|
||||
也就是说:
|
||||
>如果一个method的receiver是*T,你可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method
|
||||
|
||||
类似的
|
||||
>如果一个method的receiver是T,你可以在一个*T类型的变量P上面调用这个method,而不需要 *P去调用这个method
|
||||
|
||||
所以,你不用担心你是调用的指针的method还是不是指针的method,Go知道你要做的一切,这对于有多年C/C++编程经验的同学来说,真是解决了一个很大的痛苦。
|
||||
|
||||
### method继承
|
||||
前面一章我们学习了字段的继承,那么你也会发现Go的一个神奇之处,method也是可以继承的。如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。让我们来看下面这个例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
phone string
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
Human //匿名字段
|
||||
school string
|
||||
}
|
||||
|
||||
type Employee struct {
|
||||
Human //匿名字段
|
||||
company string
|
||||
}
|
||||
|
||||
//在human上面定义了一个method
|
||||
func (h *Human) SayHi() {
|
||||
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
|
||||
}
|
||||
|
||||
func main() {
|
||||
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
|
||||
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
|
||||
|
||||
mark.SayHi()
|
||||
sam.SayHi()
|
||||
}
|
||||
|
||||
### method重写
|
||||
上面的例子中,如果Employee想要实现自己的SayHi,怎么办?简单,和匿名字段冲突一样的道理,我们可以在Employee上面定义一个method,重写了匿名字段的方法。请看下面的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
phone string
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
Human //匿名字段
|
||||
school string
|
||||
}
|
||||
|
||||
type Employee struct {
|
||||
Human //匿名字段
|
||||
company string
|
||||
}
|
||||
|
||||
//Human定义method
|
||||
func (h *Human) SayHi() {
|
||||
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
|
||||
}
|
||||
|
||||
//Employee的method重写Human的method
|
||||
func (e *Employee) SayHi() {
|
||||
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
|
||||
e.company, e.phone) //Yes you can split into 2 lines here.
|
||||
}
|
||||
|
||||
func main() {
|
||||
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
|
||||
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
|
||||
|
||||
mark.SayHi()
|
||||
sam.SayHi()
|
||||
}
|
||||
|
||||
上面的代码设计的是如此的美妙,让人不自觉的为Go的设计惊叹!
|
||||
|
||||
通过这些内容,我们可以设计出基本的面向对象的程序了,但是Go里面的面向对象是如此的简单,没有任何的私有、公有关键字,通过大小写来实现(大写开头的为公有,小写开头的为私有),方法也同样适用这个原则。
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [struct类型](<02.4.md>)
|
||||
* 下一节: [interface](<02.6.md>)
|
||||
# 2.5 面向对象
|
||||
前面两章我们介绍了函数和struct,那你是否想过函数当作struct的字段一样来处理呢?今天我们就讲解一下函数的另一种形态,带有接收者的函数,我们称为`method`
|
||||
|
||||
## method
|
||||
现在假设有这么一个场景,你定义了一个struct叫做长方形,你现在想要计算他的面积,那么按照我们一般的思路应该会用下面的方式来实现
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Rectangle struct {
|
||||
width, height float64
|
||||
}
|
||||
|
||||
func area(r Rectangle) float64 {
|
||||
return r.width*r.height
|
||||
}
|
||||
|
||||
func main() {
|
||||
r1 := Rectangle{12, 2}
|
||||
r2 := Rectangle{9, 4}
|
||||
fmt.Println("Area of r1 is: ", area(r1))
|
||||
fmt.Println("Area of r2 is: ", area(r2))
|
||||
}
|
||||
|
||||
这段代码可以计算出来长方形的面积,但是area()不是作为Rectangle的方法实现的(类似面向对象里面的方法),而是将Rectangle的对象(如r1,r2)作为参数传入函数计算面积的。
|
||||
|
||||
这样实现当然没有问题咯,但是当需要增加圆形、正方形、五边形甚至其它多边形的时候,你想计算他们的面积的时候怎么办啊?那就只能增加新的函数咯,但是函数名你就必须要跟着换了,变成`area_rectangle, area_circle, area_triangle...`
|
||||
|
||||
像下图所表示的那样, 椭圆代表函数, 而这些函数并不从属于struct(或者以面向对象的术语来说,并不属于class),他们是单独存在于struct外围,而非在概念上属于某个struct的。
|
||||
|
||||

|
||||
|
||||
图2.8 方法和struct的关系图
|
||||
|
||||
很显然,这样的实现并不优雅,并且从概念上来说"面积"是"形状"的一个属性,它是属于这个特定的形状的,就像长方形的长和宽一样。
|
||||
|
||||
基于上面的原因所以就有了`method`的概念,`method`是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在`func`后面增加了一个receiver(也就是method所依从的主体)。
|
||||
|
||||
用上面提到的形状的例子来说,method `area()` 是依赖于某个形状(比如说Rectangle)来发生作用的。Rectangle.area()的发出者是Rectangle, area()是属于Rectangle的方法,而非一个外围函数。
|
||||
|
||||
更具体地说,Rectangle存在字段length 和 width, 同时存在方法area(), 这些字段和方法都属于Rectangle。
|
||||
|
||||
用Rob Pike的话来说就是:
|
||||
|
||||
>"A method is a function with an implicit first argument, called a receiver."
|
||||
|
||||
method的语法如下:
|
||||
|
||||
func (r ReceiverType) funcName(parameters) (results)
|
||||
|
||||
下面我们用最开始的例子用method来实现:
|
||||
|
||||
package main
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
type Rectangle struct {
|
||||
width, height float64
|
||||
}
|
||||
|
||||
type Circle struct {
|
||||
radius float64
|
||||
}
|
||||
|
||||
func (r Rectangle) area() float64 {
|
||||
return r.width*r.height
|
||||
}
|
||||
|
||||
func (c Circle) area() float64 {
|
||||
return c.radius * c.radius * math.Pi
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
r1 := Rectangle{12, 2}
|
||||
r2 := Rectangle{9, 4}
|
||||
c1 := Circle{10}
|
||||
c2 := Circle{25}
|
||||
|
||||
fmt.Println("Area of r1 is: ", r1.area())
|
||||
fmt.Println("Area of r2 is: ", r2.area())
|
||||
fmt.Println("Area of c1 is: ", c1.area())
|
||||
fmt.Println("Area of c2 is: ", c2.area())
|
||||
}
|
||||
|
||||
|
||||
|
||||
在使用method的时候重要注意几点
|
||||
|
||||
- 虽然method的名字一模一样,但是如果接收者不一样,那么method就不一样
|
||||
- method里面可以访问接收者的字段
|
||||
- 调用method通过`.`访问,就像struct里面访问字段一样
|
||||
|
||||
图示如下:
|
||||
|
||||

|
||||
|
||||
图2.9 不同struct的method不同
|
||||
|
||||
在上例,method area() 分别属于Rectangle和Circle, 于是他们的 Receiver 就变成了Rectangle 和 Circle, 或者说,这个area()方法 是由 Rectangle/Circle 发出的。
|
||||
|
||||
>值得说明的一点是,图示中method用虚线标出,意思是此处方法的Receiver是以值传递,而非引用传递,是的,Receiver还可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。后文对此会有详细论述。
|
||||
|
||||
那是不是method只能作用在struct上面呢?当然不是咯,他可以定义在任何你自定义的类型、内置类型、struct等各种类型上面。这里你是不是有点迷糊了,什么叫自定义类型,自定义类型不就是struct嘛,不是这样的哦,struct只是自定义类型里面一种比较特殊的类型而已,还有其他自定义类型申明,可以通过如下这样的申明来实现。
|
||||
|
||||
type typeName typeLiteral
|
||||
|
||||
请看下面这个申明自定义类型的代码
|
||||
|
||||
type ages int
|
||||
|
||||
type money float32
|
||||
|
||||
type months map[string]int
|
||||
|
||||
m := months {
|
||||
"January":31,
|
||||
"February":28,
|
||||
...
|
||||
"December":31,
|
||||
}
|
||||
|
||||
看到了吗?简单的很吧,这样你就可以在自己的代码里面定义有意义的类型了,实际上只是一个定义了一个别名,有点类似于c中的typedef,例如上面ages替代了int
|
||||
|
||||
好了,让我们回到`method`
|
||||
|
||||
你可以在任何的自定义类型中定义任意多的`method`,接下来让我们看一个复杂一点的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
const(
|
||||
WHITE = iota
|
||||
BLACK
|
||||
BLUE
|
||||
RED
|
||||
YELLOW
|
||||
)
|
||||
|
||||
type Color byte
|
||||
|
||||
type Box struct {
|
||||
width, height, depth float64
|
||||
color Color
|
||||
}
|
||||
|
||||
type BoxList []Box //a slice of boxes
|
||||
|
||||
func (b Box) Volume() float64 {
|
||||
return b.width * b.height * b.depth
|
||||
}
|
||||
|
||||
func (b *Box) SetColor(c Color) {
|
||||
b.color = c
|
||||
}
|
||||
|
||||
func (bl BoxList) BiggestColor() Color {
|
||||
v := 0.00
|
||||
k := Color(WHITE)
|
||||
for _, b := range bl {
|
||||
if bv := b.Volume(); bv > v {
|
||||
v = bv
|
||||
k = b.color
|
||||
}
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
func (bl BoxList) PaintItBlack() {
|
||||
for i, _ := range bl {
|
||||
bl[i].SetColor(BLACK)
|
||||
}
|
||||
}
|
||||
|
||||
func (c Color) String() string {
|
||||
strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
|
||||
return strings[c]
|
||||
}
|
||||
|
||||
func main() {
|
||||
boxes := BoxList {
|
||||
Box{4, 4, 4, RED},
|
||||
Box{10, 10, 1, YELLOW},
|
||||
Box{1, 1, 20, BLACK},
|
||||
Box{10, 10, 1, BLUE},
|
||||
Box{10, 30, 1, WHITE},
|
||||
Box{20, 20, 20, YELLOW},
|
||||
}
|
||||
|
||||
fmt.Printf("We have %d boxes in our set\n", len(boxes))
|
||||
fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
|
||||
fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String())
|
||||
fmt.Println("The biggest one is", boxes.BiggestColor().String())
|
||||
|
||||
fmt.Println("Let's paint them all black")
|
||||
boxes.PaintItBlack()
|
||||
fmt.Println("The color of the second one is", boxes[1].color.String())
|
||||
|
||||
fmt.Println("Obviously, now, the biggest one is", boxes.BiggestColor().String())
|
||||
}
|
||||
|
||||
上面的代码通过const定义了一些常量,然后定义了一些自定义类型
|
||||
|
||||
- Color作为byte的别名
|
||||
- 定义了一个struct:Box,含有三个长宽高字段和一个颜色属性
|
||||
- 定义了一个slice:BoxList,含有Box
|
||||
|
||||
然后以上面的自定义类型为接收者定义了一些method
|
||||
|
||||
- Volume()定义了接收者为Box,返回Box的容量
|
||||
- SetColor(c Color),把Box的颜色改为c
|
||||
- BiggestColor()定在在BoxList上面,返回list里面容量最大的颜色
|
||||
- PaintItBlack()把BoxList里面所有Box的颜色全部变成黑色
|
||||
- String()定义在Color上面,返回Color的具体颜色(字符串格式)
|
||||
|
||||
上面的代码通过文字描述出来之后是不是很简单?我们一般解决问题都是通过问题的描述,去写相应的代码实现。
|
||||
|
||||
### 指针作为receiver
|
||||
现在让我们回过头来看看SetColor这个method,它的receiver是一个指向Box的指针,是的,你可以使用*Box。想想为啥要使用指针而不是Box本身呢?
|
||||
|
||||
我们定义SetColor的真正目的是想改变这个Box的颜色,如果不传Box的指针,那么SetColor接受的其实是Box的一个copy,也就是说method内对于颜色值的修改,其实只作用于Box的copy,而不是真正的Box。所以我们需要传入指针。
|
||||
|
||||
这里可以把receiver当作method的第一个参数来看,然后结合前面函数讲解的传值和传引用就不难理解
|
||||
|
||||
这里你也许会问了那SetColor函数里面应该这样定义`*b.Color=c`,而不是`b.Color=c`,因为我们需要读取到指针相应的值。
|
||||
|
||||
你是对的,其实Go里面这两种方式都是正确的,当你用指针去访问相应的字段时(虽然指针没有任何的字段),Go知道你要通过指针去获取这个值,看到了吧,Go的设计是不是越来越吸引你了。
|
||||
|
||||
也许细心的读者会问这样的问题,PaintItBlack里面调用SetColor的时候是不是应该写成`(&bl[i]).SetColor(BLACK)`,因为SetColor的receiver是*Box,而不是Box。
|
||||
|
||||
你又说对的,这两种方式都可以,因为Go知道receiver是指针,他自动帮你转了。
|
||||
|
||||
也就是说:
|
||||
>如果一个method的receiver是*T,你可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method
|
||||
|
||||
类似的
|
||||
>如果一个method的receiver是T,你可以在一个*T类型的变量P上面调用这个method,而不需要 *P去调用这个method
|
||||
|
||||
所以,你不用担心你是调用的指针的method还是不是指针的method,Go知道你要做的一切,这对于有多年C/C++编程经验的同学来说,真是解决了一个很大的痛苦。
|
||||
|
||||
### method继承
|
||||
前面一章我们学习了字段的继承,那么你也会发现Go的一个神奇之处,method也是可以继承的。如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。让我们来看下面这个例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
phone string
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
Human //匿名字段
|
||||
school string
|
||||
}
|
||||
|
||||
type Employee struct {
|
||||
Human //匿名字段
|
||||
company string
|
||||
}
|
||||
|
||||
//在human上面定义了一个method
|
||||
func (h *Human) SayHi() {
|
||||
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
|
||||
}
|
||||
|
||||
func main() {
|
||||
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
|
||||
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
|
||||
|
||||
mark.SayHi()
|
||||
sam.SayHi()
|
||||
}
|
||||
|
||||
### method重写
|
||||
上面的例子中,如果Employee想要实现自己的SayHi,怎么办?简单,和匿名字段冲突一样的道理,我们可以在Employee上面定义一个method,重写了匿名字段的方法。请看下面的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
phone string
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
Human //匿名字段
|
||||
school string
|
||||
}
|
||||
|
||||
type Employee struct {
|
||||
Human //匿名字段
|
||||
company string
|
||||
}
|
||||
|
||||
//Human定义method
|
||||
func (h *Human) SayHi() {
|
||||
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
|
||||
}
|
||||
|
||||
//Employee的method重写Human的method
|
||||
func (e *Employee) SayHi() {
|
||||
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
|
||||
e.company, e.phone) //Yes you can split into 2 lines here.
|
||||
}
|
||||
|
||||
func main() {
|
||||
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
|
||||
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
|
||||
|
||||
mark.SayHi()
|
||||
sam.SayHi()
|
||||
}
|
||||
|
||||
上面的代码设计的是如此的美妙,让人不自觉的为Go的设计惊叹!
|
||||
|
||||
通过这些内容,我们可以设计出基本的面向对象的程序了,但是Go里面的面向对象是如此的简单,没有任何的私有、公有关键字,通过大小写来实现(大写开头的为公有,小写开头的为私有),方法也同样适用这个原则。
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [struct类型](<02.4.md>)
|
||||
* 下一节: [interface](<02.6.md>)
|
||||
325
zh/02.5.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
325
zh/02.5.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,325 @@
|
||||
# 2.5 面向对象
|
||||
前面两章我们介绍了函数和struct,那你是否想过函数当作struct的字段一样来处理呢?今天我们就讲解一下函数的另一种形态,带有接收者的函数,我们称为`method`
|
||||
|
||||
## method
|
||||
现在假设有这么一个场景,你定义了一个struct叫做长方形,你现在想要计算他的面积,那么按照我们一般的思路应该会用下面的方式来实现
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Rectangle struct {
|
||||
width, height float64
|
||||
}
|
||||
|
||||
func area(r Rectangle) float64 {
|
||||
return r.width*r.height
|
||||
}
|
||||
|
||||
func main() {
|
||||
r1 := Rectangle{12, 2}
|
||||
r2 := Rectangle{9, 4}
|
||||
fmt.Println("Area of r1 is: ", area(r1))
|
||||
fmt.Println("Area of r2 is: ", area(r2))
|
||||
}
|
||||
|
||||
这段代码可以计算出来长方形的面积,但是area()不是作为Rectangle的方法实现的(类似面向对象里面的方法),而是将Rectangle的对象(如r1,r2)作为参数传入函数计算面积的。
|
||||
|
||||
这样实现当然没有问题咯,但是当需要增加圆形、正方形、五边形甚至其它多边形的时候,你想计算他们的面积的时候怎么办啊?那就只能增加新的函数咯,但是函数名你就必须要跟着换了,变成`area_rectangle, area_circle, area_triangle...`
|
||||
|
||||
像下图所表示的那样, 椭圆代表函数, 而这些函数并不从属于struct(或者以面向对象的术语来说,并不属于class),他们是单独存在于struct外围,而非在概念上属于某个struct的。
|
||||
|
||||

|
||||
|
||||
图2.8 方法和struct的关系图
|
||||
|
||||
很显然,这样的实现并不优雅,并且从概念上来说"面积"是"形状"的一个属性,它是属于这个特定的形状的,就像长方形的长和宽一样。
|
||||
|
||||
基于上面的原因所以就有了`method`的概念,`method`是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在`func`后面增加了一个receiver(也就是method所依从的主体)。
|
||||
|
||||
用上面提到的形状的例子来说,method `area()` 是依赖于某个形状(比如说Rectangle)来发生作用的。Rectangle.area()的发出者是Rectangle, area()是属于Rectangle的方法,而非一个外围函数。
|
||||
|
||||
更具体地说,Rectangle存在字段length 和 width, 同时存在方法area(), 这些字段和方法都属于Rectangle。
|
||||
|
||||
用Rob Pike的话来说就是:
|
||||
|
||||
>"A method is a function with an implicit first argument, called a receiver."
|
||||
|
||||
method的语法如下:
|
||||
|
||||
func (r ReceiverType) funcName(parameters) (results)
|
||||
|
||||
下面我们用最开始的例子用method来实现:
|
||||
|
||||
package main
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
type Rectangle struct {
|
||||
width, height float64
|
||||
}
|
||||
|
||||
type Circle struct {
|
||||
radius float64
|
||||
}
|
||||
|
||||
func (r Rectangle) area() float64 {
|
||||
return r.width*r.height
|
||||
}
|
||||
|
||||
func (c Circle) area() float64 {
|
||||
return c.radius * c.radius * math.Pi
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
r1 := Rectangle{12, 2}
|
||||
r2 := Rectangle{9, 4}
|
||||
c1 := Circle{10}
|
||||
c2 := Circle{25}
|
||||
|
||||
fmt.Println("Area of r1 is: ", r1.area())
|
||||
fmt.Println("Area of r2 is: ", r2.area())
|
||||
fmt.Println("Area of c1 is: ", c1.area())
|
||||
fmt.Println("Area of c2 is: ", c2.area())
|
||||
}
|
||||
|
||||
|
||||
|
||||
在使用method的时候重要注意几点
|
||||
|
||||
- 虽然method的名字一模一样,但是如果接收者不一样,那么method就不一样
|
||||
- method里面可以访问接收者的字段
|
||||
- 调用method通过`.`访问,就像struct里面访问字段一样
|
||||
|
||||
图示如下:
|
||||
|
||||

|
||||
|
||||
图2.9 不同struct的method不同
|
||||
|
||||
在上例,method area() 分别属于Rectangle和Circle, 于是他们的 Receiver 就变成了Rectangle 和 Circle, 或者说,这个area()方法 是由 Rectangle/Circle 发出的。
|
||||
|
||||
>值得说明的一点是,图示中method用虚线标出,意思是此处方法的Receiver是以值传递,而非引用传递,是的,Receiver还可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。后文对此会有详细论述。
|
||||
|
||||
那是不是method只能作用在struct上面呢?当然不是咯,他可以定义在任何你自定义的类型、内置类型、struct等各种类型上面。这里你是不是有点迷糊了,什么叫自定义类型,自定义类型不就是struct嘛,不是这样的哦,struct只是自定义类型里面一种比较特殊的类型而已,还有其他自定义类型申明,可以通过如下这样的申明来实现。
|
||||
|
||||
type typeName typeLiteral
|
||||
|
||||
请看下面这个申明自定义类型的代码
|
||||
|
||||
type ages int
|
||||
|
||||
type money float32
|
||||
|
||||
type months map[string]int
|
||||
|
||||
m := months {
|
||||
"January":31,
|
||||
"February":28,
|
||||
...
|
||||
"December":31,
|
||||
}
|
||||
|
||||
看到了吗?简单的很吧,这样你就可以在自己的代码里面定义有意义的类型了,实际上只是一个定义了一个别名,有点类似于c中的typedef,例如上面ages替代了int
|
||||
|
||||
好了,让我们回到`method`
|
||||
|
||||
你可以在任何的自定义类型中定义任意多的`method`,接下来让我们看一个复杂一点的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
const(
|
||||
WHITE = iota
|
||||
BLACK
|
||||
BLUE
|
||||
RED
|
||||
YELLOW
|
||||
)
|
||||
|
||||
type Color byte
|
||||
|
||||
type Box struct {
|
||||
width, height, depth float64
|
||||
color Color
|
||||
}
|
||||
|
||||
type BoxList []Box //a slice of boxes
|
||||
|
||||
func (b Box) Volume() float64 {
|
||||
return b.width * b.height * b.depth
|
||||
}
|
||||
|
||||
func (b *Box) SetColor(c Color) {
|
||||
b.color = c
|
||||
}
|
||||
|
||||
func (bl BoxList) BiggestColor() Color {
|
||||
v := 0.00
|
||||
k := Color(WHITE)
|
||||
for _, b := range bl {
|
||||
if bv := b.Volume(); bv > v {
|
||||
v = bv
|
||||
k = b.color
|
||||
}
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
func (bl BoxList) PaintItBlack() {
|
||||
for i, _ := range bl {
|
||||
bl[i].SetColor(BLACK)
|
||||
}
|
||||
}
|
||||
|
||||
func (c Color) String() string {
|
||||
strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
|
||||
return strings[c]
|
||||
}
|
||||
|
||||
func main() {
|
||||
boxes := BoxList {
|
||||
Box{4, 4, 4, RED},
|
||||
Box{10, 10, 1, YELLOW},
|
||||
Box{1, 1, 20, BLACK},
|
||||
Box{10, 10, 1, BLUE},
|
||||
Box{10, 30, 1, WHITE},
|
||||
Box{20, 20, 20, YELLOW},
|
||||
}
|
||||
|
||||
fmt.Printf("We have %d boxes in our set\n", len(boxes))
|
||||
fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
|
||||
fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String())
|
||||
fmt.Println("The biggest one is", boxes.BiggestColor().String())
|
||||
|
||||
fmt.Println("Let's paint them all black")
|
||||
boxes.PaintItBlack()
|
||||
fmt.Println("The color of the second one is", boxes[1].color.String())
|
||||
|
||||
fmt.Println("Obviously, now, the biggest one is", boxes.BiggestColor().String())
|
||||
}
|
||||
|
||||
上面的代码通过const定义了一些常量,然后定义了一些自定义类型
|
||||
|
||||
- Color作为byte的别名
|
||||
- 定义了一个struct:Box,含有三个长宽高字段和一个颜色属性
|
||||
- 定义了一个slice:BoxList,含有Box
|
||||
|
||||
然后以上面的自定义类型为接收者定义了一些method
|
||||
|
||||
- Volume()定义了接收者为Box,返回Box的容量
|
||||
- SetColor(c Color),把Box的颜色改为c
|
||||
- BiggestColor()定在在BoxList上面,返回list里面容量最大的颜色
|
||||
- PaintItBlack()把BoxList里面所有Box的颜色全部变成黑色
|
||||
- String()定义在Color上面,返回Color的具体颜色(字符串格式)
|
||||
|
||||
上面的代码通过文字描述出来之后是不是很简单?我们一般解决问题都是通过问题的描述,去写相应的代码实现。
|
||||
|
||||
### 指针作为receiver
|
||||
现在让我们回过头来看看SetColor这个method,它的receiver是一个指向Box的指针,是的,你可以使用*Box。想想为啥要使用指针而不是Box本身呢?
|
||||
|
||||
我们定义SetColor的真正目的是想改变这个Box的颜色,如果不传Box的指针,那么SetColor接受的其实是Box的一个copy,也就是说method内对于颜色值的修改,其实只作用于Box的copy,而不是真正的Box。所以我们需要传入指针。
|
||||
|
||||
这里可以把receiver当作method的第一个参数来看,然后结合前面函数讲解的传值和传引用就不难理解
|
||||
|
||||
这里你也许会问了那SetColor函数里面应该这样定义`*b.Color=c`,而不是`b.Color=c`,因为我们需要读取到指针相应的值。
|
||||
|
||||
你是对的,其实Go里面这两种方式都是正确的,当你用指针去访问相应的字段时(虽然指针没有任何的字段),Go知道你要通过指针去获取这个值,看到了吧,Go的设计是不是越来越吸引你了。
|
||||
|
||||
也许细心的读者会问这样的问题,PaintItBlack里面调用SetColor的时候是不是应该写成`(&bl[i]).SetColor(BLACK)`,因为SetColor的receiver是*Box,而不是Box。
|
||||
|
||||
你又说对的,这两种方式都可以,因为Go知道receiver是指针,他自动帮你转了。
|
||||
|
||||
也就是说:
|
||||
>如果一个method的receiver是*T,你可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method
|
||||
|
||||
类似的
|
||||
>如果一个method的receiver是T,你可以在一个*T类型的变量P上面调用这个method,而不需要 *P去调用这个method
|
||||
|
||||
所以,你不用担心你是调用的指针的method还是不是指针的method,Go知道你要做的一切,这对于有多年C/C++编程经验的同学来说,真是解决了一个很大的痛苦。
|
||||
|
||||
### method继承
|
||||
前面一章我们学习了字段的继承,那么你也会发现Go的一个神奇之处,method也是可以继承的。如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。让我们来看下面这个例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
phone string
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
Human //匿名字段
|
||||
school string
|
||||
}
|
||||
|
||||
type Employee struct {
|
||||
Human //匿名字段
|
||||
company string
|
||||
}
|
||||
|
||||
//在human上面定义了一个method
|
||||
func (h *Human) SayHi() {
|
||||
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
|
||||
}
|
||||
|
||||
func main() {
|
||||
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
|
||||
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
|
||||
|
||||
mark.SayHi()
|
||||
sam.SayHi()
|
||||
}
|
||||
|
||||
### method重写
|
||||
上面的例子中,如果Employee想要实现自己的SayHi,怎么办?简单,和匿名字段冲突一样的道理,我们可以在Employee上面定义一个method,重写了匿名字段的方法。请看下面的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
phone string
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
Human //匿名字段
|
||||
school string
|
||||
}
|
||||
|
||||
type Employee struct {
|
||||
Human //匿名字段
|
||||
company string
|
||||
}
|
||||
|
||||
//Human定义method
|
||||
func (h *Human) SayHi() {
|
||||
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
|
||||
}
|
||||
|
||||
//Employee的method重写Human的method
|
||||
func (e *Employee) SayHi() {
|
||||
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
|
||||
e.company, e.phone) //Yes you can split into 2 lines here.
|
||||
}
|
||||
|
||||
func main() {
|
||||
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
|
||||
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
|
||||
|
||||
mark.SayHi()
|
||||
sam.SayHi()
|
||||
}
|
||||
|
||||
上面的代码设计的是如此的美妙,让人不自觉的为Go的设计惊叹!
|
||||
|
||||
通过这些内容,我们可以设计出基本的面向对象的程序了,但是Go里面的面向对象是如此的简单,没有任何的私有、公有关键字,通过大小写来实现(大写开头的为公有,小写开头的为私有),方法也同样适用这个原则。
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [struct类型](<02.4.md>)
|
||||
* 下一节: [interface](<02.6.md>)
|
||||
@@ -1,31 +1,31 @@
|
||||
# 2.8 总结
|
||||
|
||||
这一章我们主要介绍了Go语言的一些语法,通过语法我们可以发现Go是多么的简单,只有二十五个关键字。让我们再来回顾一下这些关键字都是用来干什么的。
|
||||
|
||||
break default func interface select
|
||||
case defer go map struct
|
||||
chan else goto package switch
|
||||
const fallthrough if range type
|
||||
continue for import return var
|
||||
|
||||
- var和const参考2.2Go语言基础里面的变量和常量申明
|
||||
- package和import已经有过短暂的接触
|
||||
- func 用于定义函数和方法
|
||||
- return 用于从函数返回
|
||||
- defer 用于类似析构函数
|
||||
- go 用于并发
|
||||
- select 用于选择不同类型的通讯
|
||||
- interface 用于定义接口,参考2.6小节
|
||||
- struct 用于定义抽象数据类型,参考2.5小节
|
||||
- break、case、continue、for、fallthrough、else、if、switch、goto、default这些参考2.3流程介绍里面
|
||||
- chan用于channel通讯
|
||||
- type用于声明自定义类型
|
||||
- map用于声明map类型数据
|
||||
- range用于读取slice、map、channel数据
|
||||
|
||||
上面这二十五个关键字记住了,那么Go你也已经差不多学会了。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [并发](<02.7.md>)
|
||||
* 下一章: [Web基础](<03.0.md>)
|
||||
# 2.8 总结
|
||||
|
||||
这一章我们主要介绍了Go语言的一些语法,通过语法我们可以发现Go是多么的简单,只有二十五个关键字。让我们再来回顾一下这些关键字都是用来干什么的。
|
||||
|
||||
break default func interface select
|
||||
case defer go map struct
|
||||
chan else goto package switch
|
||||
const fallthrough if range type
|
||||
continue for import return var
|
||||
|
||||
- var和const参考2.2Go语言基础里面的变量和常量申明
|
||||
- package和import已经有过短暂的接触
|
||||
- func 用于定义函数和方法
|
||||
- return 用于从函数返回
|
||||
- defer 用于类似析构函数
|
||||
- go 用于并发
|
||||
- select 用于选择不同类型的通讯
|
||||
- interface 用于定义接口,参考2.6小节
|
||||
- struct 用于定义抽象数据类型,参考2.5小节
|
||||
- break、case、continue、for、fallthrough、else、if、switch、goto、default这些参考2.3流程介绍里面
|
||||
- chan用于channel通讯
|
||||
- type用于声明自定义类型
|
||||
- map用于声明map类型数据
|
||||
- range用于读取slice、map、channel数据
|
||||
|
||||
上面这二十五个关键字记住了,那么Go你也已经差不多学会了。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [并发](<02.7.md>)
|
||||
* 下一章: [Web基础](<03.0.md>)
|
||||
31
zh/02.8.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
31
zh/02.8.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,31 @@
|
||||
# 2.8 总结
|
||||
|
||||
这一章我们主要介绍了Go语言的一些语法,通过语法我们可以发现Go是多么的简单,只有二十五个关键字。让我们再来回顾一下这些关键字都是用来干什么的。
|
||||
|
||||
break default func interface select
|
||||
case defer go map struct
|
||||
chan else goto package switch
|
||||
const fallthrough if range type
|
||||
continue for import return var
|
||||
|
||||
- var和const参考2.2Go语言基础里面的变量和常量申明
|
||||
- package和import已经有过短暂的接触
|
||||
- func 用于定义函数和方法
|
||||
- return 用于从函数返回
|
||||
- defer 用于类似析构函数
|
||||
- go 用于并发
|
||||
- select 用于选择不同类型的通讯
|
||||
- interface 用于定义接口,参考2.6小节
|
||||
- struct 用于定义抽象数据类型,参考2.5小节
|
||||
- break、case、continue、for、fallthrough、else、if、switch、goto、default这些参考2.3流程介绍里面
|
||||
- chan用于channel通讯
|
||||
- type用于声明自定义类型
|
||||
- map用于声明map类型数据
|
||||
- range用于读取slice、map、channel数据
|
||||
|
||||
上面这二十五个关键字记住了,那么Go你也已经差不多学会了。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [并发](<02.7.md>)
|
||||
* 下一章: [Web基础](<03.0.md>)
|
||||
@@ -1,11 +1,11 @@
|
||||
# 3 Web基础
|
||||
|
||||
学习基于Web的编程可能正是你读本书的原因。事实上,如何通过Go来编写Web应用也是我编写这本书的初衷。前面已经介绍过,Go目前已经拥有了成熟的HTTP处理包,这使得编写能做任何事情的动态Web程序易如反掌。在接下来的各章中将要介绍的内容,都是属于Web编程的范畴。本章则集中讨论一些与Web相关的概念和Go如何运行Web程序的话题。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第二章总结](<02.8.md>)
|
||||
* 下一节: [Web工作方式](<03.1.md>)
|
||||
# 3 Web基础
|
||||
|
||||
学习基于Web的编程可能正是你读本书的原因。事实上,如何通过Go来编写Web应用也是我编写这本书的初衷。前面已经介绍过,Go目前已经拥有了成熟的HTTP处理包,这使得编写能做任何事情的动态Web程序易如反掌。在接下来的各章中将要介绍的内容,都是属于Web编程的范畴。本章则集中讨论一些与Web相关的概念和Go如何运行Web程序的话题。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第二章总结](<02.8.md>)
|
||||
* 下一节: [Web工作方式](<03.1.md>)
|
||||
11
zh/03.0.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
11
zh/03.0.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,11 @@
|
||||
# 3 Web基础
|
||||
|
||||
学习基于Web的编程可能正是你读本书的原因。事实上,如何通过Go来编写Web应用也是我编写这本书的初衷。前面已经介绍过,Go目前已经拥有了成熟的HTTP处理包,这使得编写能做任何事情的动态Web程序易如反掌。在接下来的各章中将要介绍的内容,都是属于Web编程的范畴。本章则集中讨论一些与Web相关的概念和Go如何运行Web程序的话题。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第二章总结](<02.8.md>)
|
||||
* 下一节: [Web工作方式](<03.1.md>)
|
||||
@@ -1,66 +1,66 @@
|
||||
# 3.2 Go搭建一个Web服务器
|
||||
|
||||
前面小节已经介绍了Web是基于http协议的一个服务,Go语言里面提供了一个完善的net/http包,通过http包可以很方便的就搭建起来一个可以运行的Web服务。同时使用这个包能很简单地对Web的路由,静态文件,模版,cookie等数据进行设置和操作。
|
||||
|
||||
## http包建立Web服务器
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"log"
|
||||
)
|
||||
|
||||
func sayhelloName(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm() //解析参数,默认是不会解析的
|
||||
fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息
|
||||
fmt.Println("path", r.URL.Path)
|
||||
fmt.Println("scheme", r.URL.Scheme)
|
||||
fmt.Println(r.Form["url_long"])
|
||||
for k, v := range r.Form {
|
||||
fmt.Println("key:", k)
|
||||
fmt.Println("val:", strings.Join(v, ""))
|
||||
}
|
||||
fmt.Fprintf(w, "Hello astaxie!") //这个写入到w的是输出到客户端的
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", sayhelloName) //设置访问的路由
|
||||
err := http.ListenAndServe(":9090", nil) //设置监听的端口
|
||||
if err != nil {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
上面这个代码,我们build之后,然后执行web.exe,这个时候其实已经在9090端口监听http链接请求了。
|
||||
|
||||
在浏览器输入`http://localhost:9090`
|
||||
|
||||
可以看到浏览器页面输出了`Hello astaxie!`
|
||||
|
||||
可以换一个地址试试:`http://localhost:9090/?url_long=111&url_long=222`
|
||||
|
||||
看看浏览器输出的是什么,服务器输出的是什么?
|
||||
|
||||
在服务器端输出的信息如下:
|
||||
|
||||

|
||||
|
||||
图3.8 用户访问Web之后服务器端打印的信息
|
||||
|
||||
我们看到上面的代码,要编写一个Web服务器很简单,只要调用http包的两个函数就可以了。
|
||||
|
||||
>如果你以前是PHP程序员,那你也许就会问,我们的nginx、apache服务器不需要吗?Go就是不需要这些,因为他直接就监听tcp端口了,做了nginx做的事情,然后sayhelloName这个其实就是我们写的逻辑函数了,跟php里面的控制层(controller)函数类似。
|
||||
|
||||
>如果你以前是Python程序员,那么你一定听说过tornado,这个代码和他是不是很像,对,没错,Go就是拥有类似Python这样动态语言的特性,写Web应用很方便。
|
||||
|
||||
>如果你以前是Ruby程序员,会发现和ROR的/script/server启动有点类似。
|
||||
|
||||
我们看到Go通过简单的几行代码就已经运行起来一个Web服务了,而且这个Web服务内部有支持高并发的特性,我将会在接下来的两个小节里面详细的讲解一下Go是如何实现Web高并发的。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [Web工作方式](<03.1.md>)
|
||||
* 下一节: [Go如何使得web工作](<03.3.md>)
|
||||
# 3.2 Go搭建一个Web服务器
|
||||
|
||||
前面小节已经介绍了Web是基于http协议的一个服务,Go语言里面提供了一个完善的net/http包,通过http包可以很方便的就搭建起来一个可以运行的Web服务。同时使用这个包能很简单地对Web的路由,静态文件,模版,cookie等数据进行设置和操作。
|
||||
|
||||
## http包建立Web服务器
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"log"
|
||||
)
|
||||
|
||||
func sayhelloName(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm() //解析参数,默认是不会解析的
|
||||
fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息
|
||||
fmt.Println("path", r.URL.Path)
|
||||
fmt.Println("scheme", r.URL.Scheme)
|
||||
fmt.Println(r.Form["url_long"])
|
||||
for k, v := range r.Form {
|
||||
fmt.Println("key:", k)
|
||||
fmt.Println("val:", strings.Join(v, ""))
|
||||
}
|
||||
fmt.Fprintf(w, "Hello astaxie!") //这个写入到w的是输出到客户端的
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", sayhelloName) //设置访问的路由
|
||||
err := http.ListenAndServe(":9090", nil) //设置监听的端口
|
||||
if err != nil {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
上面这个代码,我们build之后,然后执行web.exe,这个时候其实已经在9090端口监听http链接请求了。
|
||||
|
||||
在浏览器输入`http://localhost:9090`
|
||||
|
||||
可以看到浏览器页面输出了`Hello astaxie!`
|
||||
|
||||
可以换一个地址试试:`http://localhost:9090/?url_long=111&url_long=222`
|
||||
|
||||
看看浏览器输出的是什么,服务器输出的是什么?
|
||||
|
||||
在服务器端输出的信息如下:
|
||||
|
||||

|
||||
|
||||
图3.8 用户访问Web之后服务器端打印的信息
|
||||
|
||||
我们看到上面的代码,要编写一个Web服务器很简单,只要调用http包的两个函数就可以了。
|
||||
|
||||
>如果你以前是PHP程序员,那你也许就会问,我们的nginx、apache服务器不需要吗?Go就是不需要这些,因为他直接就监听tcp端口了,做了nginx做的事情,然后sayhelloName这个其实就是我们写的逻辑函数了,跟php里面的控制层(controller)函数类似。
|
||||
|
||||
>如果你以前是Python程序员,那么你一定听说过tornado,这个代码和他是不是很像,对,没错,Go就是拥有类似Python这样动态语言的特性,写Web应用很方便。
|
||||
|
||||
>如果你以前是Ruby程序员,会发现和ROR的/script/server启动有点类似。
|
||||
|
||||
我们看到Go通过简单的几行代码就已经运行起来一个Web服务了,而且这个Web服务内部有支持高并发的特性,我将会在接下来的两个小节里面详细的讲解一下Go是如何实现Web高并发的。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [Web工作方式](<03.1.md>)
|
||||
* 下一节: [Go如何使得web工作](<03.3.md>)
|
||||
66
zh/03.2.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
66
zh/03.2.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,66 @@
|
||||
# 3.2 Go搭建一个Web服务器
|
||||
|
||||
前面小节已经介绍了Web是基于http协议的一个服务,Go语言里面提供了一个完善的net/http包,通过http包可以很方便的就搭建起来一个可以运行的Web服务。同时使用这个包能很简单地对Web的路由,静态文件,模版,cookie等数据进行设置和操作。
|
||||
|
||||
## http包建立Web服务器
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"log"
|
||||
)
|
||||
|
||||
func sayhelloName(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm() //解析参数,默认是不会解析的
|
||||
fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息
|
||||
fmt.Println("path", r.URL.Path)
|
||||
fmt.Println("scheme", r.URL.Scheme)
|
||||
fmt.Println(r.Form["url_long"])
|
||||
for k, v := range r.Form {
|
||||
fmt.Println("key:", k)
|
||||
fmt.Println("val:", strings.Join(v, ""))
|
||||
}
|
||||
fmt.Fprintf(w, "Hello astaxie!") //这个写入到w的是输出到客户端的
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", sayhelloName) //设置访问的路由
|
||||
err := http.ListenAndServe(":9090", nil) //设置监听的端口
|
||||
if err != nil {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
上面这个代码,我们build之后,然后执行web.exe,这个时候其实已经在9090端口监听http链接请求了。
|
||||
|
||||
在浏览器输入`http://localhost:9090`
|
||||
|
||||
可以看到浏览器页面输出了`Hello astaxie!`
|
||||
|
||||
可以换一个地址试试:`http://localhost:9090/?url_long=111&url_long=222`
|
||||
|
||||
看看浏览器输出的是什么,服务器输出的是什么?
|
||||
|
||||
在服务器端输出的信息如下:
|
||||
|
||||

|
||||
|
||||
图3.8 用户访问Web之后服务器端打印的信息
|
||||
|
||||
我们看到上面的代码,要编写一个Web服务器很简单,只要调用http包的两个函数就可以了。
|
||||
|
||||
>如果你以前是PHP程序员,那你也许就会问,我们的nginx、apache服务器不需要吗?Go就是不需要这些,因为他直接就监听tcp端口了,做了nginx做的事情,然后sayhelloName这个其实就是我们写的逻辑函数了,跟php里面的控制层(controller)函数类似。
|
||||
|
||||
>如果你以前是Python程序员,那么你一定听说过tornado,这个代码和他是不是很像,对,没错,Go就是拥有类似Python这样动态语言的特性,写Web应用很方便。
|
||||
|
||||
>如果你以前是Ruby程序员,会发现和ROR的/script/server启动有点类似。
|
||||
|
||||
我们看到Go通过简单的几行代码就已经运行起来一个Web服务了,而且这个Web服务内部有支持高并发的特性,我将会在接下来的两个小节里面详细的讲解一下Go是如何实现Web高并发的。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [Web工作方式](<03.1.md>)
|
||||
* 下一节: [Go如何使得web工作](<03.3.md>)
|
||||
@@ -1,25 +1,25 @@
|
||||
# 4 表单
|
||||
|
||||
表单是我们平常编写Web应用常用的工具,通过表单我们可以方便的让客户端和服务器进行数据的交互。对于以前开发过Web的用户来说表单都非常熟悉,但是对于C/C++程序员来说,这可能是一个有些陌生的东西,那么什么是表单呢?
|
||||
|
||||
表单是一个包含表单元素的区域。表单元素是允许用户在表单中(比如:文本域、下拉列表、单选框、复选框等等)输入信息的元素。表单使用表单标签(\<form\>)定义。
|
||||
|
||||
<form>
|
||||
...
|
||||
input 元素
|
||||
...
|
||||
</form>
|
||||
|
||||
Go里面对于form处理已经有很方便的方法了,在Request里面的有专门的form处理,可以很方便的整合到Web开发里面来,4.1小节里面将讲解Go如何处理表单的输入。由于不能信任任何用户的输入,所以我们需要对这些输入进行有效性验证,4.2小节将就如何进行一些普通的验证进行详细的演示。
|
||||
|
||||
HTTP协议是一种无状态的协议,那么如何才能辨别是否是同一个用户呢?同时又如何保证一个表单不出现多次递交的情况呢?4.3和4.4小节里面将对cookie(cookie是存储在客户端的信息,能够每次通过header和服务器进行交互的数据)等进行详细讲解。
|
||||
|
||||
表单还有一个很大的功能就是能够上传文件,那么Go是如何处理文件上传的呢?针对大文件上传我们如何有效的处理呢?4.5小节我们将一起学习Go处理文件上传的知识。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第三章总结](<03.5.md>)
|
||||
* 下一节: [处理表单的输入](<04.1.md>)
|
||||
# 4 表单
|
||||
|
||||
表单是我们平常编写Web应用常用的工具,通过表单我们可以方便的让客户端和服务器进行数据的交互。对于以前开发过Web的用户来说表单都非常熟悉,但是对于C/C++程序员来说,这可能是一个有些陌生的东西,那么什么是表单呢?
|
||||
|
||||
表单是一个包含表单元素的区域。表单元素是允许用户在表单中(比如:文本域、下拉列表、单选框、复选框等等)输入信息的元素。表单使用表单标签(\<form\>)定义。
|
||||
|
||||
<form>
|
||||
...
|
||||
input 元素
|
||||
...
|
||||
</form>
|
||||
|
||||
Go里面对于form处理已经有很方便的方法了,在Request里面的有专门的form处理,可以很方便的整合到Web开发里面来,4.1小节里面将讲解Go如何处理表单的输入。由于不能信任任何用户的输入,所以我们需要对这些输入进行有效性验证,4.2小节将就如何进行一些普通的验证进行详细的演示。
|
||||
|
||||
HTTP协议是一种无状态的协议,那么如何才能辨别是否是同一个用户呢?同时又如何保证一个表单不出现多次递交的情况呢?4.3和4.4小节里面将对cookie(cookie是存储在客户端的信息,能够每次通过header和服务器进行交互的数据)等进行详细讲解。
|
||||
|
||||
表单还有一个很大的功能就是能够上传文件,那么Go是如何处理文件上传的呢?针对大文件上传我们如何有效的处理呢?4.5小节我们将一起学习Go处理文件上传的知识。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第三章总结](<03.5.md>)
|
||||
* 下一节: [处理表单的输入](<04.1.md>)
|
||||
25
zh/04.0.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
25
zh/04.0.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,25 @@
|
||||
# 4 表单
|
||||
|
||||
表单是我们平常编写Web应用常用的工具,通过表单我们可以方便的让客户端和服务器进行数据的交互。对于以前开发过Web的用户来说表单都非常熟悉,但是对于C/C++程序员来说,这可能是一个有些陌生的东西,那么什么是表单呢?
|
||||
|
||||
表单是一个包含表单元素的区域。表单元素是允许用户在表单中(比如:文本域、下拉列表、单选框、复选框等等)输入信息的元素。表单使用表单标签(\<form\>)定义。
|
||||
|
||||
<form>
|
||||
...
|
||||
input 元素
|
||||
...
|
||||
</form>
|
||||
|
||||
Go里面对于form处理已经有很方便的方法了,在Request里面的有专门的form处理,可以很方便的整合到Web开发里面来,4.1小节里面将讲解Go如何处理表单的输入。由于不能信任任何用户的输入,所以我们需要对这些输入进行有效性验证,4.2小节将就如何进行一些普通的验证进行详细的演示。
|
||||
|
||||
HTTP协议是一种无状态的协议,那么如何才能辨别是否是同一个用户呢?同时又如何保证一个表单不出现多次递交的情况呢?4.3和4.4小节里面将对cookie(cookie是存储在客户端的信息,能够每次通过header和服务器进行交互的数据)等进行详细讲解。
|
||||
|
||||
表单还有一个很大的功能就是能够上传文件,那么Go是如何处理文件上传的呢?针对大文件上传我们如何有效的处理呢?4.5小节我们将一起学习Go处理文件上传的知识。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第三章总结](<03.5.md>)
|
||||
* 下一节: [处理表单的输入](<04.1.md>)
|
||||
@@ -1,109 +1,109 @@
|
||||
# 4.1 处理表单的输入
|
||||
|
||||
先来看一个表单递交的例子,我们有如下的表单内容,命名成文件login.gtpl(放入当前新建项目的目录里面)
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<form action="/login" method="post">
|
||||
用户名:<input type="text" name="username">
|
||||
密码:<input type="password" name="password">
|
||||
<input type="submit" value="登陆">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
上面递交表单到服务器的`/login`,当用户输入信息点击登陆之后,会跳转到服务器的路由`login`里面,我们首先要判断这个是什么方式传递过来,POST还是GET呢?
|
||||
|
||||
http包里面有一个很简单的方式就可以获取,我们在前面web的例子的基础上来看看怎么处理login页面的form数据
|
||||
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func sayhelloName(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm() //解析url传递的参数,对于POST则解析响应包的主体(request body)
|
||||
//注意:如果没有调用ParseForm方法,下面无法获取表单的数据
|
||||
fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息
|
||||
fmt.Println("path", r.URL.Path)
|
||||
fmt.Println("scheme", r.URL.Scheme)
|
||||
fmt.Println(r.Form["url_long"])
|
||||
for k, v := range r.Form {
|
||||
fmt.Println("key:", k)
|
||||
fmt.Println("val:", strings.Join(v, ""))
|
||||
}
|
||||
fmt.Fprintf(w, "Hello astaxie!") //这个写入到w的是输出到客户端的
|
||||
}
|
||||
|
||||
func login(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("method:", r.Method) //获取请求的方法
|
||||
if r.Method == "GET" {
|
||||
t, _ := template.ParseFiles("login.gtpl")
|
||||
log.Println(t.Execute(w, nil))
|
||||
} else {
|
||||
//请求的是登陆数据,那么执行登陆的逻辑判断
|
||||
fmt.Println("username:", r.Form["username"])
|
||||
fmt.Println("password:", r.Form["password"])
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", sayhelloName) //设置访问的路由
|
||||
http.HandleFunc("/login", login) //设置访问的路由
|
||||
err := http.ListenAndServe(":9090", nil) //设置监听的端口
|
||||
if err != nil {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
通过上面的代码我们可以看出获取请求方法是通过`r.Method`来完成的,这是个字符串类型的变量,返回GET, POST, PUT等method信息。
|
||||
|
||||
login函数中我们根据`r.Method`来判断是显示登录界面还是处理登录逻辑。当GET方式请求时显示登录界面,其他方式请求时则处理登录逻辑,如查询数据库、验证登录信息等。
|
||||
|
||||
当我们在浏览器里面打开`http://127.0.0.1:9090/login`的时候,出现如下界面
|
||||
|
||||

|
||||
|
||||
如果你看到一个空页面,可能是你写的 login.gtpl 文件中有错误,请根据控制台中的日志进行修复。
|
||||
|
||||
图4.1 用户登录界面
|
||||
|
||||
我们输入用户名和密码之后发现在服务器端是不会打印出来任何输出的,为什么呢?默认情况下,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这些数据分离开来。
|
||||
|
||||
现在我们修改一下login.gtpl里面form的action值`http://127.0.0.1:9090/login`修改为`http://127.0.0.1:9090/login?username=astaxie`,再次测试,服务器的输出username是不是一个slice。服务器端的输出如下:
|
||||
|
||||

|
||||
|
||||
图4.2 服务器端打印接受到的信息
|
||||
|
||||
`request.Form`是一个url.Values类型,里面存储的是对应的类似`key=value`的信息,下面展示了可以对form数据进行的一些操作:
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("name", "Ava")
|
||||
v.Add("friend", "Jess")
|
||||
v.Add("friend", "Sarah")
|
||||
v.Add("friend", "Zoe")
|
||||
// v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe"
|
||||
fmt.Println(v.Get("name"))
|
||||
fmt.Println(v.Get("friend"))
|
||||
fmt.Println(v["friend"])
|
||||
|
||||
>**Tips**:
|
||||
Request本身也提供了FormValue()函数来获取用户提交的参数。如r.Form["username"]也可写成r.FormValue("username")。调用r.FormValue时会自动调用r.ParseForm,所以不必提前调用。r.FormValue只会返回同名参数中的第一个,若参数不存在则返回空字符串。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [表单](<04.0.md>)
|
||||
* 下一节: [验证表单的输入](<04.2.md>)
|
||||
# 4.1 处理表单的输入
|
||||
|
||||
先来看一个表单递交的例子,我们有如下的表单内容,命名成文件login.gtpl(放入当前新建项目的目录里面)
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<form action="/login" method="post">
|
||||
用户名:<input type="text" name="username">
|
||||
密码:<input type="password" name="password">
|
||||
<input type="submit" value="登陆">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
上面递交表单到服务器的`/login`,当用户输入信息点击登陆之后,会跳转到服务器的路由`login`里面,我们首先要判断这个是什么方式传递过来,POST还是GET呢?
|
||||
|
||||
http包里面有一个很简单的方式就可以获取,我们在前面web的例子的基础上来看看怎么处理login页面的form数据
|
||||
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func sayhelloName(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm() //解析url传递的参数,对于POST则解析响应包的主体(request body)
|
||||
//注意:如果没有调用ParseForm方法,下面无法获取表单的数据
|
||||
fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息
|
||||
fmt.Println("path", r.URL.Path)
|
||||
fmt.Println("scheme", r.URL.Scheme)
|
||||
fmt.Println(r.Form["url_long"])
|
||||
for k, v := range r.Form {
|
||||
fmt.Println("key:", k)
|
||||
fmt.Println("val:", strings.Join(v, ""))
|
||||
}
|
||||
fmt.Fprintf(w, "Hello astaxie!") //这个写入到w的是输出到客户端的
|
||||
}
|
||||
|
||||
func login(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("method:", r.Method) //获取请求的方法
|
||||
if r.Method == "GET" {
|
||||
t, _ := template.ParseFiles("login.gtpl")
|
||||
log.Println(t.Execute(w, nil))
|
||||
} else {
|
||||
//请求的是登陆数据,那么执行登陆的逻辑判断
|
||||
fmt.Println("username:", r.Form["username"])
|
||||
fmt.Println("password:", r.Form["password"])
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", sayhelloName) //设置访问的路由
|
||||
http.HandleFunc("/login", login) //设置访问的路由
|
||||
err := http.ListenAndServe(":9090", nil) //设置监听的端口
|
||||
if err != nil {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
通过上面的代码我们可以看出获取请求方法是通过`r.Method`来完成的,这是个字符串类型的变量,返回GET, POST, PUT等method信息。
|
||||
|
||||
login函数中我们根据`r.Method`来判断是显示登录界面还是处理登录逻辑。当GET方式请求时显示登录界面,其他方式请求时则处理登录逻辑,如查询数据库、验证登录信息等。
|
||||
|
||||
当我们在浏览器里面打开`http://127.0.0.1:9090/login`的时候,出现如下界面
|
||||
|
||||

|
||||
|
||||
如果你看到一个空页面,可能是你写的 login.gtpl 文件中有错误,请根据控制台中的日志进行修复。
|
||||
|
||||
图4.1 用户登录界面
|
||||
|
||||
我们输入用户名和密码之后发现在服务器端是不会打印出来任何输出的,为什么呢?默认情况下,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这些数据分离开来。
|
||||
|
||||
现在我们修改一下login.gtpl里面form的action值`http://127.0.0.1:9090/login`修改为`http://127.0.0.1:9090/login?username=astaxie`,再次测试,服务器的输出username是不是一个slice。服务器端的输出如下:
|
||||
|
||||

|
||||
|
||||
图4.2 服务器端打印接受到的信息
|
||||
|
||||
`request.Form`是一个url.Values类型,里面存储的是对应的类似`key=value`的信息,下面展示了可以对form数据进行的一些操作:
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("name", "Ava")
|
||||
v.Add("friend", "Jess")
|
||||
v.Add("friend", "Sarah")
|
||||
v.Add("friend", "Zoe")
|
||||
// v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe"
|
||||
fmt.Println(v.Get("name"))
|
||||
fmt.Println(v.Get("friend"))
|
||||
fmt.Println(v["friend"])
|
||||
|
||||
>**Tips**:
|
||||
Request本身也提供了FormValue()函数来获取用户提交的参数。如r.Form["username"]也可写成r.FormValue("username")。调用r.FormValue时会自动调用r.ParseForm,所以不必提前调用。r.FormValue只会返回同名参数中的第一个,若参数不存在则返回空字符串。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [表单](<04.0.md>)
|
||||
* 下一节: [验证表单的输入](<04.2.md>)
|
||||
109
zh/04.1.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
109
zh/04.1.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,109 @@
|
||||
# 4.1 处理表单的输入
|
||||
|
||||
先来看一个表单递交的例子,我们有如下的表单内容,命名成文件login.gtpl(放入当前新建项目的目录里面)
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<form action="/login" method="post">
|
||||
用户名:<input type="text" name="username">
|
||||
密码:<input type="password" name="password">
|
||||
<input type="submit" value="登陆">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
上面递交表单到服务器的`/login`,当用户输入信息点击登陆之后,会跳转到服务器的路由`login`里面,我们首先要判断这个是什么方式传递过来,POST还是GET呢?
|
||||
|
||||
http包里面有一个很简单的方式就可以获取,我们在前面web的例子的基础上来看看怎么处理login页面的form数据
|
||||
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func sayhelloName(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm() //解析url传递的参数,对于POST则解析响应包的主体(request body)
|
||||
//注意:如果没有调用ParseForm方法,下面无法获取表单的数据
|
||||
fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息
|
||||
fmt.Println("path", r.URL.Path)
|
||||
fmt.Println("scheme", r.URL.Scheme)
|
||||
fmt.Println(r.Form["url_long"])
|
||||
for k, v := range r.Form {
|
||||
fmt.Println("key:", k)
|
||||
fmt.Println("val:", strings.Join(v, ""))
|
||||
}
|
||||
fmt.Fprintf(w, "Hello astaxie!") //这个写入到w的是输出到客户端的
|
||||
}
|
||||
|
||||
func login(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("method:", r.Method) //获取请求的方法
|
||||
if r.Method == "GET" {
|
||||
t, _ := template.ParseFiles("login.gtpl")
|
||||
log.Println(t.Execute(w, nil))
|
||||
} else {
|
||||
//请求的是登陆数据,那么执行登陆的逻辑判断
|
||||
fmt.Println("username:", r.Form["username"])
|
||||
fmt.Println("password:", r.Form["password"])
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", sayhelloName) //设置访问的路由
|
||||
http.HandleFunc("/login", login) //设置访问的路由
|
||||
err := http.ListenAndServe(":9090", nil) //设置监听的端口
|
||||
if err != nil {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
通过上面的代码我们可以看出获取请求方法是通过`r.Method`来完成的,这是个字符串类型的变量,返回GET, POST, PUT等method信息。
|
||||
|
||||
login函数中我们根据`r.Method`来判断是显示登录界面还是处理登录逻辑。当GET方式请求时显示登录界面,其他方式请求时则处理登录逻辑,如查询数据库、验证登录信息等。
|
||||
|
||||
当我们在浏览器里面打开`http://127.0.0.1:9090/login`的时候,出现如下界面
|
||||
|
||||

|
||||
|
||||
如果你看到一个空页面,可能是你写的 login.gtpl 文件中有错误,请根据控制台中的日志进行修复。
|
||||
|
||||
图4.1 用户登录界面
|
||||
|
||||
我们输入用户名和密码之后发现在服务器端是不会打印出来任何输出的,为什么呢?默认情况下,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这些数据分离开来。
|
||||
|
||||
现在我们修改一下login.gtpl里面form的action值`http://127.0.0.1:9090/login`修改为`http://127.0.0.1:9090/login?username=astaxie`,再次测试,服务器的输出username是不是一个slice。服务器端的输出如下:
|
||||
|
||||

|
||||
|
||||
图4.2 服务器端打印接受到的信息
|
||||
|
||||
`request.Form`是一个url.Values类型,里面存储的是对应的类似`key=value`的信息,下面展示了可以对form数据进行的一些操作:
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("name", "Ava")
|
||||
v.Add("friend", "Jess")
|
||||
v.Add("friend", "Sarah")
|
||||
v.Add("friend", "Zoe")
|
||||
// v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe"
|
||||
fmt.Println(v.Get("name"))
|
||||
fmt.Println(v.Get("friend"))
|
||||
fmt.Println(v["friend"])
|
||||
|
||||
>**Tips**:
|
||||
Request本身也提供了FormValue()函数来获取用户提交的参数。如r.Form["username"]也可写成r.FormValue("username")。调用r.FormValue时会自动调用r.ParseForm,所以不必提前调用。r.FormValue只会返回同名参数中的第一个,若参数不存在则返回空字符串。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [表单](<04.0.md>)
|
||||
* 下一节: [验证表单的输入](<04.2.md>)
|
||||
@@ -1,162 +1,162 @@
|
||||
# 4.2 验证表单的输入
|
||||
|
||||
开发Web的一个原则就是,不能信任用户输入的任何信息,所以验证和过滤用户的输入信息就变得非常重要,我们经常会在微博、新闻中听到某某网站被入侵了,存在什么漏洞,这些大多是因为网站对于用户输入的信息没有做严格的验证引起的,所以为了编写出安全可靠的Web程序,验证表单输入的意义重大。
|
||||
|
||||
我们平常编写Web应用主要有两方面的数据验证,一个是在页面端的js验证(目前在这方面有很多的插件库,比如ValidationJS插件),一个是在服务器端的验证,我们这小节讲解的是如何在服务器端验证。
|
||||
|
||||
## 必填字段
|
||||
你想要确保从一个表单元素中得到一个值,例如前面小节里面的用户名,我们如何处理呢?Go有一个内置函数`len`可以获取字符串的长度,这样我们就可以通过len来获取数据的长度,例如:
|
||||
|
||||
if len(r.Form["username"][0])==0{
|
||||
//为空的处理
|
||||
}
|
||||
|
||||
`r.Form`对不同类型的表单元素的留空有不同的处理, 对于空文本框、空文本区域以及文件上传,元素的值为空值,而如果是未选中的复选框和单选按钮,则根本不会在r.Form中产生相应条目,如果我们用上面例子中的方式去获取数据时程序就会报错。所以我们需要通过`r.Form.Get()`来获取值,因为如果字段不存在,通过该方式获取的是空值。但是通过`r.Form.Get()`只能获取单个的值,如果是map的值,必须通过上面的方式来获取。
|
||||
|
||||
## 数字
|
||||
你想要确保一个表单输入框中获取的只能是数字,例如,你想通过表单获取某个人的具体年龄是50岁还是10岁,而不是像“一把年纪了”或“年轻着呢”这种描述
|
||||
|
||||
如果我们是判断正整数,那么我们先转化成int类型,然后进行处理
|
||||
|
||||
getint,err:=strconv.Atoi(r.Form.Get("age"))
|
||||
if err!=nil{
|
||||
//数字转化出错了,那么可能就不是数字
|
||||
}
|
||||
|
||||
//接下来就可以判断这个数字的大小范围了
|
||||
if getint >100 {
|
||||
//太大了
|
||||
}
|
||||
|
||||
还有一种方式就是正则匹配的方式
|
||||
|
||||
if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
对于性能要求很高的用户来说,这是一个老生常谈的问题了,他们认为应该尽量避免使用正则表达式,因为使用正则表达式的速度会比较慢。但是在目前机器性能那么强劲的情况下,对于这种简单的正则表达式效率和类型转换函数是没有什么差别的。如果你对正则表达式很熟悉,而且你在其它语言中也在使用它,那么在Go里面使用正则表达式将是一个便利的方式。
|
||||
|
||||
>Go实现的正则是[RE2](http://code.google.com/p/re2/wiki/Syntax),所有的字符都是UTF-8编码的。
|
||||
|
||||
## 中文
|
||||
有时候我们想通过表单元素获取一个用户的中文名字,但是又为了保证获取的是正确的中文,我们需要进行验证,而不是用户随便的一些输入。对于中文我们目前有两种方式来验证,可以使用 `unicode` 包提供的 `func Is(rangeTab *RangeTable, r rune) bool` 来验证,也可以使用正则方式来验证,这里使用最简单的正则方式,如下代码所示
|
||||
|
||||
if m, _ := regexp.MatchString("^\\p{Han}+$", r.Form.Get("realname")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
## 英文
|
||||
我们期望通过表单元素获取一个英文值,例如我们想知道一个用户的英文名,应该是astaxie,而不是asta谢。
|
||||
|
||||
我们可以很简单的通过正则验证数据:
|
||||
|
||||
if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
## 电子邮件地址
|
||||
你想知道用户输入的一个Email地址是否正确,通过如下这个方式可以验证:
|
||||
|
||||
if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m {
|
||||
fmt.Println("no")
|
||||
}else{
|
||||
fmt.Println("yes")
|
||||
}
|
||||
|
||||
|
||||
## 手机号码
|
||||
你想要判断用户输入的手机号码是否正确,通过正则也可以验证:
|
||||
|
||||
if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
## 下拉菜单
|
||||
如果我们想要判断表单里面`<select>`元素生成的下拉菜单中是否有被选中的项目。有些时候黑客可能会伪造这个下拉菜单不存在的值发送给你,那么如何判断这个值是否是我们预设的值呢?
|
||||
|
||||
我们的select可能是这样的一些元素
|
||||
|
||||
<select name="fruit">
|
||||
<option value="apple">apple</option>
|
||||
<option value="pear">pear</option>
|
||||
<option value="banane">banane</option>
|
||||
</select>
|
||||
|
||||
那么我们可以这样来验证
|
||||
|
||||
slice:=[]string{"apple","pear","banane"}
|
||||
|
||||
for _, v := range slice {
|
||||
if v == r.Form.Get("fruit") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
## 单选按钮
|
||||
如果我们想要判断radio按钮是否有一个被选中了,我们页面的输出可能就是一个男、女性别的选择,但是也可能一个15岁大的无聊小孩,一手拿着http协议的书,另一只手通过telnet客户端向你的程序在发送请求呢,你设定的性别男值是1,女是2,他给你发送一个3,你的程序会出现异常吗?因此我们也需要像下拉菜单的判断方式类似,判断我们获取的值是我们预设的值,而不是额外的值。
|
||||
|
||||
<input type="radio" name="gender" value="1">男
|
||||
<input type="radio" name="gender" value="2">女
|
||||
|
||||
那我们也可以类似下拉菜单的做法一样
|
||||
|
||||
slice:=[]int{1,2}
|
||||
|
||||
for _, v := range slice {
|
||||
if v == r.Form.Get("gender") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
## 复选框
|
||||
有一项选择兴趣的复选框,你想确定用户选中的和你提供给用户选择的是同一个类型的数据。
|
||||
|
||||
<input type="checkbox" name="interest" value="football">足球
|
||||
<input type="checkbox" name="interest" value="basketball">篮球
|
||||
<input type="checkbox" name="interest" value="tennis">网球
|
||||
|
||||
对于复选框我们的验证和单选有点不一样,因为接收到的数据是一个slice
|
||||
|
||||
slice:=[]string{"football","basketball","tennis"}
|
||||
a:=Slice_diff(r.Form["interest"],slice)
|
||||
if a == nil{
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
上面这个函数`Slice_diff`包含在我开源的一个库里面(操作slice和map的库),[https://github.com/astaxie/beeku](https://github.com/astaxie/beeku)
|
||||
|
||||
## 日期和时间
|
||||
你想确定用户填写的日期或时间是否有效。例如
|
||||
,用户在日程表中安排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位,我们两个都需要验证
|
||||
|
||||
//验证15位身份证,15位的是全部数字
|
||||
if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
//验证18位身份证,18位前17位为数字,最后一位是校验位,可能为数字或字符X。
|
||||
if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
上面列出了我们一些常用的服务器端的表单元素验证,希望通过这个引导入门,能够让你对Go的数据验证有所了解,特别是Go里面的正则处理。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [处理表单的输入](<04.1.md>)
|
||||
* 下一节: [预防跨站脚本](<04.3.md>)
|
||||
# 4.2 验证表单的输入
|
||||
|
||||
开发Web的一个原则就是,不能信任用户输入的任何信息,所以验证和过滤用户的输入信息就变得非常重要,我们经常会在微博、新闻中听到某某网站被入侵了,存在什么漏洞,这些大多是因为网站对于用户输入的信息没有做严格的验证引起的,所以为了编写出安全可靠的Web程序,验证表单输入的意义重大。
|
||||
|
||||
我们平常编写Web应用主要有两方面的数据验证,一个是在页面端的js验证(目前在这方面有很多的插件库,比如ValidationJS插件),一个是在服务器端的验证,我们这小节讲解的是如何在服务器端验证。
|
||||
|
||||
## 必填字段
|
||||
你想要确保从一个表单元素中得到一个值,例如前面小节里面的用户名,我们如何处理呢?Go有一个内置函数`len`可以获取字符串的长度,这样我们就可以通过len来获取数据的长度,例如:
|
||||
|
||||
if len(r.Form["username"][0])==0{
|
||||
//为空的处理
|
||||
}
|
||||
|
||||
`r.Form`对不同类型的表单元素的留空有不同的处理, 对于空文本框、空文本区域以及文件上传,元素的值为空值,而如果是未选中的复选框和单选按钮,则根本不会在r.Form中产生相应条目,如果我们用上面例子中的方式去获取数据时程序就会报错。所以我们需要通过`r.Form.Get()`来获取值,因为如果字段不存在,通过该方式获取的是空值。但是通过`r.Form.Get()`只能获取单个的值,如果是map的值,必须通过上面的方式来获取。
|
||||
|
||||
## 数字
|
||||
你想要确保一个表单输入框中获取的只能是数字,例如,你想通过表单获取某个人的具体年龄是50岁还是10岁,而不是像“一把年纪了”或“年轻着呢”这种描述
|
||||
|
||||
如果我们是判断正整数,那么我们先转化成int类型,然后进行处理
|
||||
|
||||
getint,err:=strconv.Atoi(r.Form.Get("age"))
|
||||
if err!=nil{
|
||||
//数字转化出错了,那么可能就不是数字
|
||||
}
|
||||
|
||||
//接下来就可以判断这个数字的大小范围了
|
||||
if getint >100 {
|
||||
//太大了
|
||||
}
|
||||
|
||||
还有一种方式就是正则匹配的方式
|
||||
|
||||
if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
对于性能要求很高的用户来说,这是一个老生常谈的问题了,他们认为应该尽量避免使用正则表达式,因为使用正则表达式的速度会比较慢。但是在目前机器性能那么强劲的情况下,对于这种简单的正则表达式效率和类型转换函数是没有什么差别的。如果你对正则表达式很熟悉,而且你在其它语言中也在使用它,那么在Go里面使用正则表达式将是一个便利的方式。
|
||||
|
||||
>Go实现的正则是[RE2](http://code.google.com/p/re2/wiki/Syntax),所有的字符都是UTF-8编码的。
|
||||
|
||||
## 中文
|
||||
有时候我们想通过表单元素获取一个用户的中文名字,但是又为了保证获取的是正确的中文,我们需要进行验证,而不是用户随便的一些输入。对于中文我们目前有两种方式来验证,可以使用 `unicode` 包提供的 `func Is(rangeTab *RangeTable, r rune) bool` 来验证,也可以使用正则方式来验证,这里使用最简单的正则方式,如下代码所示
|
||||
|
||||
if m, _ := regexp.MatchString("^\\p{Han}+$", r.Form.Get("realname")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
## 英文
|
||||
我们期望通过表单元素获取一个英文值,例如我们想知道一个用户的英文名,应该是astaxie,而不是asta谢。
|
||||
|
||||
我们可以很简单的通过正则验证数据:
|
||||
|
||||
if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
## 电子邮件地址
|
||||
你想知道用户输入的一个Email地址是否正确,通过如下这个方式可以验证:
|
||||
|
||||
if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m {
|
||||
fmt.Println("no")
|
||||
}else{
|
||||
fmt.Println("yes")
|
||||
}
|
||||
|
||||
|
||||
## 手机号码
|
||||
你想要判断用户输入的手机号码是否正确,通过正则也可以验证:
|
||||
|
||||
if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
## 下拉菜单
|
||||
如果我们想要判断表单里面`<select>`元素生成的下拉菜单中是否有被选中的项目。有些时候黑客可能会伪造这个下拉菜单不存在的值发送给你,那么如何判断这个值是否是我们预设的值呢?
|
||||
|
||||
我们的select可能是这样的一些元素
|
||||
|
||||
<select name="fruit">
|
||||
<option value="apple">apple</option>
|
||||
<option value="pear">pear</option>
|
||||
<option value="banane">banane</option>
|
||||
</select>
|
||||
|
||||
那么我们可以这样来验证
|
||||
|
||||
slice:=[]string{"apple","pear","banane"}
|
||||
|
||||
for _, v := range slice {
|
||||
if v == r.Form.Get("fruit") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
## 单选按钮
|
||||
如果我们想要判断radio按钮是否有一个被选中了,我们页面的输出可能就是一个男、女性别的选择,但是也可能一个15岁大的无聊小孩,一手拿着http协议的书,另一只手通过telnet客户端向你的程序在发送请求呢,你设定的性别男值是1,女是2,他给你发送一个3,你的程序会出现异常吗?因此我们也需要像下拉菜单的判断方式类似,判断我们获取的值是我们预设的值,而不是额外的值。
|
||||
|
||||
<input type="radio" name="gender" value="1">男
|
||||
<input type="radio" name="gender" value="2">女
|
||||
|
||||
那我们也可以类似下拉菜单的做法一样
|
||||
|
||||
slice:=[]int{1,2}
|
||||
|
||||
for _, v := range slice {
|
||||
if v == r.Form.Get("gender") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
## 复选框
|
||||
有一项选择兴趣的复选框,你想确定用户选中的和你提供给用户选择的是同一个类型的数据。
|
||||
|
||||
<input type="checkbox" name="interest" value="football">足球
|
||||
<input type="checkbox" name="interest" value="basketball">篮球
|
||||
<input type="checkbox" name="interest" value="tennis">网球
|
||||
|
||||
对于复选框我们的验证和单选有点不一样,因为接收到的数据是一个slice
|
||||
|
||||
slice:=[]string{"football","basketball","tennis"}
|
||||
a:=Slice_diff(r.Form["interest"],slice)
|
||||
if a == nil{
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
上面这个函数`Slice_diff`包含在我开源的一个库里面(操作slice和map的库),[https://github.com/astaxie/beeku](https://github.com/astaxie/beeku)
|
||||
|
||||
## 日期和时间
|
||||
你想确定用户填写的日期或时间是否有效。例如
|
||||
,用户在日程表中安排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位,我们两个都需要验证
|
||||
|
||||
//验证15位身份证,15位的是全部数字
|
||||
if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
//验证18位身份证,18位前17位为数字,最后一位是校验位,可能为数字或字符X。
|
||||
if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
上面列出了我们一些常用的服务器端的表单元素验证,希望通过这个引导入门,能够让你对Go的数据验证有所了解,特别是Go里面的正则处理。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [处理表单的输入](<04.1.md>)
|
||||
* 下一节: [预防跨站脚本](<04.3.md>)
|
||||
162
zh/04.2.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
162
zh/04.2.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,162 @@
|
||||
# 4.2 验证表单的输入
|
||||
|
||||
开发Web的一个原则就是,不能信任用户输入的任何信息,所以验证和过滤用户的输入信息就变得非常重要,我们经常会在微博、新闻中听到某某网站被入侵了,存在什么漏洞,这些大多是因为网站对于用户输入的信息没有做严格的验证引起的,所以为了编写出安全可靠的Web程序,验证表单输入的意义重大。
|
||||
|
||||
我们平常编写Web应用主要有两方面的数据验证,一个是在页面端的js验证(目前在这方面有很多的插件库,比如ValidationJS插件),一个是在服务器端的验证,我们这小节讲解的是如何在服务器端验证。
|
||||
|
||||
## 必填字段
|
||||
你想要确保从一个表单元素中得到一个值,例如前面小节里面的用户名,我们如何处理呢?Go有一个内置函数`len`可以获取字符串的长度,这样我们就可以通过len来获取数据的长度,例如:
|
||||
|
||||
if len(r.Form["username"][0])==0{
|
||||
//为空的处理
|
||||
}
|
||||
|
||||
`r.Form`对不同类型的表单元素的留空有不同的处理, 对于空文本框、空文本区域以及文件上传,元素的值为空值,而如果是未选中的复选框和单选按钮,则根本不会在r.Form中产生相应条目,如果我们用上面例子中的方式去获取数据时程序就会报错。所以我们需要通过`r.Form.Get()`来获取值,因为如果字段不存在,通过该方式获取的是空值。但是通过`r.Form.Get()`只能获取单个的值,如果是map的值,必须通过上面的方式来获取。
|
||||
|
||||
## 数字
|
||||
你想要确保一个表单输入框中获取的只能是数字,例如,你想通过表单获取某个人的具体年龄是50岁还是10岁,而不是像“一把年纪了”或“年轻着呢”这种描述
|
||||
|
||||
如果我们是判断正整数,那么我们先转化成int类型,然后进行处理
|
||||
|
||||
getint,err:=strconv.Atoi(r.Form.Get("age"))
|
||||
if err!=nil{
|
||||
//数字转化出错了,那么可能就不是数字
|
||||
}
|
||||
|
||||
//接下来就可以判断这个数字的大小范围了
|
||||
if getint >100 {
|
||||
//太大了
|
||||
}
|
||||
|
||||
还有一种方式就是正则匹配的方式
|
||||
|
||||
if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
对于性能要求很高的用户来说,这是一个老生常谈的问题了,他们认为应该尽量避免使用正则表达式,因为使用正则表达式的速度会比较慢。但是在目前机器性能那么强劲的情况下,对于这种简单的正则表达式效率和类型转换函数是没有什么差别的。如果你对正则表达式很熟悉,而且你在其它语言中也在使用它,那么在Go里面使用正则表达式将是一个便利的方式。
|
||||
|
||||
>Go实现的正则是[RE2](http://code.google.com/p/re2/wiki/Syntax),所有的字符都是UTF-8编码的。
|
||||
|
||||
## 中文
|
||||
有时候我们想通过表单元素获取一个用户的中文名字,但是又为了保证获取的是正确的中文,我们需要进行验证,而不是用户随便的一些输入。对于中文我们目前有两种方式来验证,可以使用 `unicode` 包提供的 `func Is(rangeTab *RangeTable, r rune) bool` 来验证,也可以使用正则方式来验证,这里使用最简单的正则方式,如下代码所示
|
||||
|
||||
if m, _ := regexp.MatchString("^\\p{Han}+$", r.Form.Get("realname")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
## 英文
|
||||
我们期望通过表单元素获取一个英文值,例如我们想知道一个用户的英文名,应该是astaxie,而不是asta谢。
|
||||
|
||||
我们可以很简单的通过正则验证数据:
|
||||
|
||||
if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
## 电子邮件地址
|
||||
你想知道用户输入的一个Email地址是否正确,通过如下这个方式可以验证:
|
||||
|
||||
if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m {
|
||||
fmt.Println("no")
|
||||
}else{
|
||||
fmt.Println("yes")
|
||||
}
|
||||
|
||||
|
||||
## 手机号码
|
||||
你想要判断用户输入的手机号码是否正确,通过正则也可以验证:
|
||||
|
||||
if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
## 下拉菜单
|
||||
如果我们想要判断表单里面`<select>`元素生成的下拉菜单中是否有被选中的项目。有些时候黑客可能会伪造这个下拉菜单不存在的值发送给你,那么如何判断这个值是否是我们预设的值呢?
|
||||
|
||||
我们的select可能是这样的一些元素
|
||||
|
||||
<select name="fruit">
|
||||
<option value="apple">apple</option>
|
||||
<option value="pear">pear</option>
|
||||
<option value="banane">banane</option>
|
||||
</select>
|
||||
|
||||
那么我们可以这样来验证
|
||||
|
||||
slice:=[]string{"apple","pear","banane"}
|
||||
|
||||
for _, v := range slice {
|
||||
if v == r.Form.Get("fruit") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
## 单选按钮
|
||||
如果我们想要判断radio按钮是否有一个被选中了,我们页面的输出可能就是一个男、女性别的选择,但是也可能一个15岁大的无聊小孩,一手拿着http协议的书,另一只手通过telnet客户端向你的程序在发送请求呢,你设定的性别男值是1,女是2,他给你发送一个3,你的程序会出现异常吗?因此我们也需要像下拉菜单的判断方式类似,判断我们获取的值是我们预设的值,而不是额外的值。
|
||||
|
||||
<input type="radio" name="gender" value="1">男
|
||||
<input type="radio" name="gender" value="2">女
|
||||
|
||||
那我们也可以类似下拉菜单的做法一样
|
||||
|
||||
slice:=[]int{1,2}
|
||||
|
||||
for _, v := range slice {
|
||||
if v == r.Form.Get("gender") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
## 复选框
|
||||
有一项选择兴趣的复选框,你想确定用户选中的和你提供给用户选择的是同一个类型的数据。
|
||||
|
||||
<input type="checkbox" name="interest" value="football">足球
|
||||
<input type="checkbox" name="interest" value="basketball">篮球
|
||||
<input type="checkbox" name="interest" value="tennis">网球
|
||||
|
||||
对于复选框我们的验证和单选有点不一样,因为接收到的数据是一个slice
|
||||
|
||||
slice:=[]string{"football","basketball","tennis"}
|
||||
a:=Slice_diff(r.Form["interest"],slice)
|
||||
if a == nil{
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
上面这个函数`Slice_diff`包含在我开源的一个库里面(操作slice和map的库),[https://github.com/astaxie/beeku](https://github.com/astaxie/beeku)
|
||||
|
||||
## 日期和时间
|
||||
你想确定用户填写的日期或时间是否有效。例如
|
||||
,用户在日程表中安排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位,我们两个都需要验证
|
||||
|
||||
//验证15位身份证,15位的是全部数字
|
||||
if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
//验证18位身份证,18位前17位为数字,最后一位是校验位,可能为数字或字符X。
|
||||
if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
上面列出了我们一些常用的服务器端的表单元素验证,希望通过这个引导入门,能够让你对Go的数据验证有所了解,特别是Go里面的正则处理。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [处理表单的输入](<04.1.md>)
|
||||
* 下一节: [预防跨站脚本](<04.3.md>)
|
||||
@@ -1,68 +1,68 @@
|
||||
# 4.3 预防跨站脚本
|
||||
|
||||
现在的网站包含大量的动态内容以提高用户体验,比过去要复杂得多。所谓动态内容,就是根据用户环境和需要,Web应用程序能够输出相应的内容。动态站点会受到一种名为“跨站脚本攻击”(Cross Site Scripting, 安全专家们通常将其缩写成 XSS)的威胁,而静态站点则完全不受其影响。
|
||||
|
||||
攻击者通常会在有漏洞的程序中插入JavaScript、VBScript、 ActiveX或Flash以欺骗用户。一旦得手,他们可以盗取用户帐户信息,修改用户设置,盗取/污染cookie和植入恶意广告等。
|
||||
|
||||
对XSS最佳的防护应该结合以下两种方法:一是验证所有输入数据,有效检测攻击(这个我们前面小节已经有过介绍);另一个是对所有输出数据进行适当的处理,以防止任何已成功注入的脚本在浏览器端运行。
|
||||
|
||||
那么Go里面是怎么做这个有效防护的呢?Go的html/template里面带有下面几个函数可以帮你转义
|
||||
|
||||
- func HTMLEscape(w io.Writer, b []byte) //把b进行转义之后写到w
|
||||
- func HTMLEscapeString(s string) string //转义s之后返回结果字符串
|
||||
- func HTMLEscaper(args ...interface{}) string //支持多个参数一起转义,返回结果字符串
|
||||
|
||||
|
||||
我们看4.1小节的例子
|
||||
|
||||
fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //输出到服务器端
|
||||
fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password")))
|
||||
template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端
|
||||
|
||||
如果我们输入的username是`<script>alert()</script>`,那么我们可以在浏览器上面看到输出如下所示:
|
||||
|
||||

|
||||
|
||||
图4.3 Javascript过滤之后的输出
|
||||
|
||||
Go的html/template包默认帮你过滤了html标签,但是有时候你只想要输出这个`<script>alert()</script>`看起来正常的信息,该怎么处理?请使用text/template。请看下面的例子:
|
||||
|
||||
import "text/template"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
|
||||
|
||||
输出
|
||||
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
或者使用template.HTML类型
|
||||
|
||||
import "html/template"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", template.HTML("<script>alert('you have been pwned')</script>"))
|
||||
|
||||
输出
|
||||
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
转换成`template.HTML`后,变量的内容也不会被转义
|
||||
|
||||
转义的例子:
|
||||
|
||||
import "html/template"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
|
||||
|
||||
转义之后的输出:
|
||||
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [验证的输入](<04.2.md>)
|
||||
* 下一节: [防止多次递交表单](<04.4.md>)
|
||||
# 4.3 预防跨站脚本
|
||||
|
||||
现在的网站包含大量的动态内容以提高用户体验,比过去要复杂得多。所谓动态内容,就是根据用户环境和需要,Web应用程序能够输出相应的内容。动态站点会受到一种名为“跨站脚本攻击”(Cross Site Scripting, 安全专家们通常将其缩写成 XSS)的威胁,而静态站点则完全不受其影响。
|
||||
|
||||
攻击者通常会在有漏洞的程序中插入JavaScript、VBScript、 ActiveX或Flash以欺骗用户。一旦得手,他们可以盗取用户帐户信息,修改用户设置,盗取/污染cookie和植入恶意广告等。
|
||||
|
||||
对XSS最佳的防护应该结合以下两种方法:一是验证所有输入数据,有效检测攻击(这个我们前面小节已经有过介绍);另一个是对所有输出数据进行适当的处理,以防止任何已成功注入的脚本在浏览器端运行。
|
||||
|
||||
那么Go里面是怎么做这个有效防护的呢?Go的html/template里面带有下面几个函数可以帮你转义
|
||||
|
||||
- func HTMLEscape(w io.Writer, b []byte) //把b进行转义之后写到w
|
||||
- func HTMLEscapeString(s string) string //转义s之后返回结果字符串
|
||||
- func HTMLEscaper(args ...interface{}) string //支持多个参数一起转义,返回结果字符串
|
||||
|
||||
|
||||
我们看4.1小节的例子
|
||||
|
||||
fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //输出到服务器端
|
||||
fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password")))
|
||||
template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端
|
||||
|
||||
如果我们输入的username是`<script>alert()</script>`,那么我们可以在浏览器上面看到输出如下所示:
|
||||
|
||||

|
||||
|
||||
图4.3 Javascript过滤之后的输出
|
||||
|
||||
Go的html/template包默认帮你过滤了html标签,但是有时候你只想要输出这个`<script>alert()</script>`看起来正常的信息,该怎么处理?请使用text/template。请看下面的例子:
|
||||
|
||||
import "text/template"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
|
||||
|
||||
输出
|
||||
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
或者使用template.HTML类型
|
||||
|
||||
import "html/template"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", template.HTML("<script>alert('you have been pwned')</script>"))
|
||||
|
||||
输出
|
||||
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
转换成`template.HTML`后,变量的内容也不会被转义
|
||||
|
||||
转义的例子:
|
||||
|
||||
import "html/template"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
|
||||
|
||||
转义之后的输出:
|
||||
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [验证的输入](<04.2.md>)
|
||||
* 下一节: [防止多次递交表单](<04.4.md>)
|
||||
68
zh/04.3.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
68
zh/04.3.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,68 @@
|
||||
# 4.3 预防跨站脚本
|
||||
|
||||
现在的网站包含大量的动态内容以提高用户体验,比过去要复杂得多。所谓动态内容,就是根据用户环境和需要,Web应用程序能够输出相应的内容。动态站点会受到一种名为“跨站脚本攻击”(Cross Site Scripting, 安全专家们通常将其缩写成 XSS)的威胁,而静态站点则完全不受其影响。
|
||||
|
||||
攻击者通常会在有漏洞的程序中插入JavaScript、VBScript、 ActiveX或Flash以欺骗用户。一旦得手,他们可以盗取用户帐户信息,修改用户设置,盗取/污染cookie和植入恶意广告等。
|
||||
|
||||
对XSS最佳的防护应该结合以下两种方法:一是验证所有输入数据,有效检测攻击(这个我们前面小节已经有过介绍);另一个是对所有输出数据进行适当的处理,以防止任何已成功注入的脚本在浏览器端运行。
|
||||
|
||||
那么Go里面是怎么做这个有效防护的呢?Go的html/template里面带有下面几个函数可以帮你转义
|
||||
|
||||
- func HTMLEscape(w io.Writer, b []byte) //把b进行转义之后写到w
|
||||
- func HTMLEscapeString(s string) string //转义s之后返回结果字符串
|
||||
- func HTMLEscaper(args ...interface{}) string //支持多个参数一起转义,返回结果字符串
|
||||
|
||||
|
||||
我们看4.1小节的例子
|
||||
|
||||
fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //输出到服务器端
|
||||
fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password")))
|
||||
template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端
|
||||
|
||||
如果我们输入的username是`<script>alert()</script>`,那么我们可以在浏览器上面看到输出如下所示:
|
||||
|
||||

|
||||
|
||||
图4.3 Javascript过滤之后的输出
|
||||
|
||||
Go的html/template包默认帮你过滤了html标签,但是有时候你只想要输出这个`<script>alert()</script>`看起来正常的信息,该怎么处理?请使用text/template。请看下面的例子:
|
||||
|
||||
import "text/template"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
|
||||
|
||||
输出
|
||||
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
或者使用template.HTML类型
|
||||
|
||||
import "html/template"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", template.HTML("<script>alert('you have been pwned')</script>"))
|
||||
|
||||
输出
|
||||
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
转换成`template.HTML`后,变量的内容也不会被转义
|
||||
|
||||
转义的例子:
|
||||
|
||||
import "html/template"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
|
||||
|
||||
转义之后的输出:
|
||||
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [验证的输入](<04.2.md>)
|
||||
* 下一节: [防止多次递交表单](<04.4.md>)
|
||||
@@ -1,58 +1,58 @@
|
||||
# 4.4 防止多次递交表单
|
||||
|
||||
不知道你是否曾经看到过一个论坛或者博客,在一个帖子或者文章后面出现多条重复的记录,这些大多数是因为用户重复递交了留言的表单引起的。由于种种原因,用户经常会重复递交表单。通常这只是鼠标的误操作,如双击了递交按钮,也可能是为了编辑或者再次核对填写过的信息,点击了浏览器的后退按钮,然后又再次点击了递交按钮而不是浏览器的前进按钮。当然,也可能是故意的——比如,在某项在线调查或者博彩活动中重复投票。那我们如何有效的防止用户多次递交相同的表单呢?
|
||||
|
||||
解决方案是在表单中添加一个带有唯一值的隐藏字段。在验证表单时,先检查带有该惟一值的表单是否已经递交过了。如果是,拒绝再次递交;如果不是,则处理表单进行逻辑处理。另外,如果是采用了Ajax模式递交表单的话,当表单递交后,通过javascript来禁用表单的递交按钮。
|
||||
|
||||
我继续拿4.2小节的例子优化:
|
||||
|
||||
<input type="checkbox" name="interest" value="football">足球
|
||||
<input type="checkbox" name="interest" value="basketball">篮球
|
||||
<input type="checkbox" name="interest" value="tennis">网球
|
||||
用户名:<input type="text" name="username">
|
||||
密码:<input type="password" name="password">
|
||||
<input type="hidden" name="token" value="{{.}}">
|
||||
<input type="submit" value="登陆">
|
||||
|
||||
我们在模版里面增加了一个隐藏字段`token`,这个值我们通过MD5(时间戳)来获取惟一值,然后我们把这个值存储到服务器端(session来控制,我们将在第六章讲解如何保存),以方便表单提交时比对判定。
|
||||
|
||||
func login(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("method:", r.Method) //获取请求的方法
|
||||
if r.Method == "GET" {
|
||||
crutime := time.Now().Unix()
|
||||
h := md5.New()
|
||||
io.WriteString(h, strconv.FormatInt(crutime, 10))
|
||||
token := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
t, _ := template.ParseFiles("login.gtpl")
|
||||
t.Execute(w, token)
|
||||
} else {
|
||||
//请求的是登陆数据,那么执行登陆的逻辑判断
|
||||
r.ParseForm()
|
||||
token := r.Form.Get("token")
|
||||
if token != "" {
|
||||
//验证token的合法性
|
||||
} else {
|
||||
//不存在token报错
|
||||
}
|
||||
fmt.Println("username length:", len(r.Form["username"][0]))
|
||||
fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //输出到服务器端
|
||||
fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password")))
|
||||
template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端
|
||||
}
|
||||
}
|
||||
|
||||
上面的代码输出到页面的源码如下:
|
||||
|
||||

|
||||
|
||||
图4.4 增加token之后在客户端输出的源码信息
|
||||
|
||||
我们看到token已经有输出值,你可以不断的刷新,可以看到这个值在不断的变化。这样就保证了每次显示form表单的时候都是唯一的,用户递交的表单保持了唯一性。
|
||||
|
||||
我们的解决方案可以防止非恶意的攻击,并能使恶意用户暂时不知所措,然后,它却不能排除所有的欺骗性的动机,对此类情况还需要更复杂的工作。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [预防跨站脚本](<04.3.md>)
|
||||
* 下一节: [处理文件上传](<04.5.md>)
|
||||
# 4.4 防止多次递交表单
|
||||
|
||||
不知道你是否曾经看到过一个论坛或者博客,在一个帖子或者文章后面出现多条重复的记录,这些大多数是因为用户重复递交了留言的表单引起的。由于种种原因,用户经常会重复递交表单。通常这只是鼠标的误操作,如双击了递交按钮,也可能是为了编辑或者再次核对填写过的信息,点击了浏览器的后退按钮,然后又再次点击了递交按钮而不是浏览器的前进按钮。当然,也可能是故意的——比如,在某项在线调查或者博彩活动中重复投票。那我们如何有效的防止用户多次递交相同的表单呢?
|
||||
|
||||
解决方案是在表单中添加一个带有唯一值的隐藏字段。在验证表单时,先检查带有该惟一值的表单是否已经递交过了。如果是,拒绝再次递交;如果不是,则处理表单进行逻辑处理。另外,如果是采用了Ajax模式递交表单的话,当表单递交后,通过javascript来禁用表单的递交按钮。
|
||||
|
||||
我继续拿4.2小节的例子优化:
|
||||
|
||||
<input type="checkbox" name="interest" value="football">足球
|
||||
<input type="checkbox" name="interest" value="basketball">篮球
|
||||
<input type="checkbox" name="interest" value="tennis">网球
|
||||
用户名:<input type="text" name="username">
|
||||
密码:<input type="password" name="password">
|
||||
<input type="hidden" name="token" value="{{.}}">
|
||||
<input type="submit" value="登陆">
|
||||
|
||||
我们在模版里面增加了一个隐藏字段`token`,这个值我们通过MD5(时间戳)来获取惟一值,然后我们把这个值存储到服务器端(session来控制,我们将在第六章讲解如何保存),以方便表单提交时比对判定。
|
||||
|
||||
func login(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("method:", r.Method) //获取请求的方法
|
||||
if r.Method == "GET" {
|
||||
crutime := time.Now().Unix()
|
||||
h := md5.New()
|
||||
io.WriteString(h, strconv.FormatInt(crutime, 10))
|
||||
token := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
t, _ := template.ParseFiles("login.gtpl")
|
||||
t.Execute(w, token)
|
||||
} else {
|
||||
//请求的是登陆数据,那么执行登陆的逻辑判断
|
||||
r.ParseForm()
|
||||
token := r.Form.Get("token")
|
||||
if token != "" {
|
||||
//验证token的合法性
|
||||
} else {
|
||||
//不存在token报错
|
||||
}
|
||||
fmt.Println("username length:", len(r.Form["username"][0]))
|
||||
fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //输出到服务器端
|
||||
fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password")))
|
||||
template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端
|
||||
}
|
||||
}
|
||||
|
||||
上面的代码输出到页面的源码如下:
|
||||
|
||||

|
||||
|
||||
图4.4 增加token之后在客户端输出的源码信息
|
||||
|
||||
我们看到token已经有输出值,你可以不断的刷新,可以看到这个值在不断的变化。这样就保证了每次显示form表单的时候都是唯一的,用户递交的表单保持了唯一性。
|
||||
|
||||
我们的解决方案可以防止非恶意的攻击,并能使恶意用户暂时不知所措,然后,它却不能排除所有的欺骗性的动机,对此类情况还需要更复杂的工作。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [预防跨站脚本](<04.3.md>)
|
||||
* 下一节: [处理文件上传](<04.5.md>)
|
||||
58
zh/04.4.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
58
zh/04.4.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,58 @@
|
||||
# 4.4 防止多次递交表单
|
||||
|
||||
不知道你是否曾经看到过一个论坛或者博客,在一个帖子或者文章后面出现多条重复的记录,这些大多数是因为用户重复递交了留言的表单引起的。由于种种原因,用户经常会重复递交表单。通常这只是鼠标的误操作,如双击了递交按钮,也可能是为了编辑或者再次核对填写过的信息,点击了浏览器的后退按钮,然后又再次点击了递交按钮而不是浏览器的前进按钮。当然,也可能是故意的——比如,在某项在线调查或者博彩活动中重复投票。那我们如何有效的防止用户多次递交相同的表单呢?
|
||||
|
||||
解决方案是在表单中添加一个带有唯一值的隐藏字段。在验证表单时,先检查带有该惟一值的表单是否已经递交过了。如果是,拒绝再次递交;如果不是,则处理表单进行逻辑处理。另外,如果是采用了Ajax模式递交表单的话,当表单递交后,通过javascript来禁用表单的递交按钮。
|
||||
|
||||
我继续拿4.2小节的例子优化:
|
||||
|
||||
<input type="checkbox" name="interest" value="football">足球
|
||||
<input type="checkbox" name="interest" value="basketball">篮球
|
||||
<input type="checkbox" name="interest" value="tennis">网球
|
||||
用户名:<input type="text" name="username">
|
||||
密码:<input type="password" name="password">
|
||||
<input type="hidden" name="token" value="{{.}}">
|
||||
<input type="submit" value="登陆">
|
||||
|
||||
我们在模版里面增加了一个隐藏字段`token`,这个值我们通过MD5(时间戳)来获取惟一值,然后我们把这个值存储到服务器端(session来控制,我们将在第六章讲解如何保存),以方便表单提交时比对判定。
|
||||
|
||||
func login(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("method:", r.Method) //获取请求的方法
|
||||
if r.Method == "GET" {
|
||||
crutime := time.Now().Unix()
|
||||
h := md5.New()
|
||||
io.WriteString(h, strconv.FormatInt(crutime, 10))
|
||||
token := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
t, _ := template.ParseFiles("login.gtpl")
|
||||
t.Execute(w, token)
|
||||
} else {
|
||||
//请求的是登陆数据,那么执行登陆的逻辑判断
|
||||
r.ParseForm()
|
||||
token := r.Form.Get("token")
|
||||
if token != "" {
|
||||
//验证token的合法性
|
||||
} else {
|
||||
//不存在token报错
|
||||
}
|
||||
fmt.Println("username length:", len(r.Form["username"][0]))
|
||||
fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //输出到服务器端
|
||||
fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password")))
|
||||
template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端
|
||||
}
|
||||
}
|
||||
|
||||
上面的代码输出到页面的源码如下:
|
||||
|
||||

|
||||
|
||||
图4.4 增加token之后在客户端输出的源码信息
|
||||
|
||||
我们看到token已经有输出值,你可以不断的刷新,可以看到这个值在不断的变化。这样就保证了每次显示form表单的时候都是唯一的,用户递交的表单保持了唯一性。
|
||||
|
||||
我们的解决方案可以防止非恶意的攻击,并能使恶意用户暂时不知所措,然后,它却不能排除所有的欺骗性的动机,对此类情况还需要更复杂的工作。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [预防跨站脚本](<04.3.md>)
|
||||
* 下一节: [处理文件上传](<04.5.md>)
|
||||
@@ -1,156 +1,156 @@
|
||||
# 4.5 处理文件上传
|
||||
你想处理一个由用户上传的文件,比如你正在建设一个类似Instagram的网站,你需要存储用户拍摄的照片。这种需求该如何实现呢?
|
||||
|
||||
要使表单能够上传文件,首先第一步就是要添加form的`enctype`属性,`enctype`属性有如下三种情况:
|
||||
|
||||
application/x-www-form-urlencoded 表示在发送前编码所有字符(默认)
|
||||
multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。
|
||||
text/plain 空格转换为 "+" 加号,但不对特殊字符编码。
|
||||
|
||||
所以,创建新的表单html文件, 命名为upload.gtpl, html代码应该类似于:
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>上传文件</title>
|
||||
</head>
|
||||
<body>
|
||||
<form enctype="multipart/form-data" action="/upload" method="post">
|
||||
<input type="file" name="uploadfile" />
|
||||
<input type="hidden" name="token" value="{{.}}"/>
|
||||
<input type="submit" value="upload" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
在服务器端,我们增加一个handlerFunc:
|
||||
|
||||
http.HandleFunc("/upload", upload)
|
||||
|
||||
// 处理/upload 逻辑
|
||||
func upload(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("method:", r.Method) //获取请求的方法
|
||||
if r.Method == "GET" {
|
||||
crutime := time.Now().Unix()
|
||||
h := md5.New()
|
||||
io.WriteString(h, strconv.FormatInt(crutime, 10))
|
||||
token := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
t, _ := template.ParseFiles("upload.gtpl")
|
||||
t.Execute(w, token)
|
||||
} else {
|
||||
r.ParseMultipartForm(32 << 20)
|
||||
file, handler, err := r.FormFile("uploadfile")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
fmt.Fprintf(w, "%v", handler.Header)
|
||||
f, err := os.OpenFile("./test/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666) // 此处假设当前目录下已存在test目录
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
io.Copy(f, file)
|
||||
}
|
||||
}
|
||||
|
||||
通过上面的代码可以看到,处理文件上传我们需要调用`r.ParseMultipartForm`,里面的参数表示`maxMemory`,调用`ParseMultipartForm`之后,上传的文件存储在`maxMemory`大小的内存里面,如果文件大小超过了`maxMemory`,那么剩下的部分将存储在系统的临时文件中。我们可以通过`r.FormFile`获取上面的文件句柄,然后实例中使用了`io.Copy`来存储文件。
|
||||
|
||||
>获取其他非文件字段信息的时候就不需要调用`r.ParseForm`,因为在需要的时候Go自动会去调用。而且`ParseMultipartForm`调用一次之后,后面再次调用不会再有效果。
|
||||
|
||||
通过上面的实例我们可以看到我们上传文件主要三步处理:
|
||||
|
||||
1. 表单中增加enctype="multipart/form-data"
|
||||
2. 服务端调用`r.ParseMultipartForm`,把上传的文件存储在内存和临时文件中
|
||||
3. 使用`r.FormFile`获取文件句柄,然后对文件进行存储等处理。
|
||||
|
||||
文件handler是multipart.FileHeader,里面存储了如下结构信息
|
||||
|
||||
type FileHeader struct {
|
||||
Filename string
|
||||
Header textproto.MIMEHeader
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
|
||||
我们通过上面的实例代码打印出来上传文件的信息如下
|
||||
|
||||

|
||||
|
||||
图4.5 打印文件上传后服务器端接受的信息
|
||||
|
||||
## 客户端上传文件
|
||||
|
||||
我们上面的例子演示了如何通过表单上传文件,然后在服务器端处理文件,其实Go支持模拟客户端表单功能支持文件上传,详细用法请看如下示例:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
func postFile(filename string, targetUrl string) error {
|
||||
bodyBuf := &bytes.Buffer{}
|
||||
bodyWriter := multipart.NewWriter(bodyBuf)
|
||||
|
||||
//关键的一步操作
|
||||
fileWriter, err := bodyWriter.CreateFormFile("uploadfile", filename)
|
||||
if err != nil {
|
||||
fmt.Println("error writing to buffer")
|
||||
return err
|
||||
}
|
||||
|
||||
//打开文件句柄操作
|
||||
fh, err := os.Open(filename)
|
||||
if err != nil {
|
||||
fmt.Println("error opening file")
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
//iocopy
|
||||
_, err = io.Copy(fileWriter, fh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contentType := bodyWriter.FormDataContentType()
|
||||
bodyWriter.Close()
|
||||
|
||||
resp, err := http.Post(targetUrl, contentType, bodyBuf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
resp_body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(resp.Status)
|
||||
fmt.Println(string(resp_body))
|
||||
return nil
|
||||
}
|
||||
|
||||
// sample usage
|
||||
func main() {
|
||||
target_url := "http://localhost:9090/upload"
|
||||
filename := "./astaxie.pdf"
|
||||
postFile(filename, target_url)
|
||||
}
|
||||
|
||||
|
||||
上面的例子详细展示了客户端如何向服务器上传一个文件的例子,客户端通过multipart.Write把文件的文本流写入一个缓存中,然后调用http的Post方法把缓存传到服务器。
|
||||
|
||||
>如果你还有其他普通字段例如username之类的需要同时写入,那么可以调用multipart的WriteField方法写很多其他类似的字段。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [防止多次递交表单](<04.4.md>)
|
||||
* 下一节: [小结](<04.6.md>)
|
||||
# 4.5 处理文件上传
|
||||
你想处理一个由用户上传的文件,比如你正在建设一个类似Instagram的网站,你需要存储用户拍摄的照片。这种需求该如何实现呢?
|
||||
|
||||
要使表单能够上传文件,首先第一步就是要添加form的`enctype`属性,`enctype`属性有如下三种情况:
|
||||
|
||||
application/x-www-form-urlencoded 表示在发送前编码所有字符(默认)
|
||||
multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。
|
||||
text/plain 空格转换为 "+" 加号,但不对特殊字符编码。
|
||||
|
||||
所以,创建新的表单html文件, 命名为upload.gtpl, html代码应该类似于:
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>上传文件</title>
|
||||
</head>
|
||||
<body>
|
||||
<form enctype="multipart/form-data" action="/upload" method="post">
|
||||
<input type="file" name="uploadfile" />
|
||||
<input type="hidden" name="token" value="{{.}}"/>
|
||||
<input type="submit" value="upload" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
在服务器端,我们增加一个handlerFunc:
|
||||
|
||||
http.HandleFunc("/upload", upload)
|
||||
|
||||
// 处理/upload 逻辑
|
||||
func upload(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("method:", r.Method) //获取请求的方法
|
||||
if r.Method == "GET" {
|
||||
crutime := time.Now().Unix()
|
||||
h := md5.New()
|
||||
io.WriteString(h, strconv.FormatInt(crutime, 10))
|
||||
token := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
t, _ := template.ParseFiles("upload.gtpl")
|
||||
t.Execute(w, token)
|
||||
} else {
|
||||
r.ParseMultipartForm(32 << 20)
|
||||
file, handler, err := r.FormFile("uploadfile")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
fmt.Fprintf(w, "%v", handler.Header)
|
||||
f, err := os.OpenFile("./test/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666) // 此处假设当前目录下已存在test目录
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
io.Copy(f, file)
|
||||
}
|
||||
}
|
||||
|
||||
通过上面的代码可以看到,处理文件上传我们需要调用`r.ParseMultipartForm`,里面的参数表示`maxMemory`,调用`ParseMultipartForm`之后,上传的文件存储在`maxMemory`大小的内存里面,如果文件大小超过了`maxMemory`,那么剩下的部分将存储在系统的临时文件中。我们可以通过`r.FormFile`获取上面的文件句柄,然后实例中使用了`io.Copy`来存储文件。
|
||||
|
||||
>获取其他非文件字段信息的时候就不需要调用`r.ParseForm`,因为在需要的时候Go自动会去调用。而且`ParseMultipartForm`调用一次之后,后面再次调用不会再有效果。
|
||||
|
||||
通过上面的实例我们可以看到我们上传文件主要三步处理:
|
||||
|
||||
1. 表单中增加enctype="multipart/form-data"
|
||||
2. 服务端调用`r.ParseMultipartForm`,把上传的文件存储在内存和临时文件中
|
||||
3. 使用`r.FormFile`获取文件句柄,然后对文件进行存储等处理。
|
||||
|
||||
文件handler是multipart.FileHeader,里面存储了如下结构信息
|
||||
|
||||
type FileHeader struct {
|
||||
Filename string
|
||||
Header textproto.MIMEHeader
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
|
||||
我们通过上面的实例代码打印出来上传文件的信息如下
|
||||
|
||||

|
||||
|
||||
图4.5 打印文件上传后服务器端接受的信息
|
||||
|
||||
## 客户端上传文件
|
||||
|
||||
我们上面的例子演示了如何通过表单上传文件,然后在服务器端处理文件,其实Go支持模拟客户端表单功能支持文件上传,详细用法请看如下示例:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
func postFile(filename string, targetUrl string) error {
|
||||
bodyBuf := &bytes.Buffer{}
|
||||
bodyWriter := multipart.NewWriter(bodyBuf)
|
||||
|
||||
//关键的一步操作
|
||||
fileWriter, err := bodyWriter.CreateFormFile("uploadfile", filename)
|
||||
if err != nil {
|
||||
fmt.Println("error writing to buffer")
|
||||
return err
|
||||
}
|
||||
|
||||
//打开文件句柄操作
|
||||
fh, err := os.Open(filename)
|
||||
if err != nil {
|
||||
fmt.Println("error opening file")
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
//iocopy
|
||||
_, err = io.Copy(fileWriter, fh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contentType := bodyWriter.FormDataContentType()
|
||||
bodyWriter.Close()
|
||||
|
||||
resp, err := http.Post(targetUrl, contentType, bodyBuf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
resp_body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(resp.Status)
|
||||
fmt.Println(string(resp_body))
|
||||
return nil
|
||||
}
|
||||
|
||||
// sample usage
|
||||
func main() {
|
||||
target_url := "http://localhost:9090/upload"
|
||||
filename := "./astaxie.pdf"
|
||||
postFile(filename, target_url)
|
||||
}
|
||||
|
||||
|
||||
上面的例子详细展示了客户端如何向服务器上传一个文件的例子,客户端通过multipart.Write把文件的文本流写入一个缓存中,然后调用http的Post方法把缓存传到服务器。
|
||||
|
||||
>如果你还有其他普通字段例如username之类的需要同时写入,那么可以调用multipart的WriteField方法写很多其他类似的字段。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [防止多次递交表单](<04.4.md>)
|
||||
* 下一节: [小结](<04.6.md>)
|
||||
156
zh/04.5.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
156
zh/04.5.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,156 @@
|
||||
# 4.5 处理文件上传
|
||||
你想处理一个由用户上传的文件,比如你正在建设一个类似Instagram的网站,你需要存储用户拍摄的照片。这种需求该如何实现呢?
|
||||
|
||||
要使表单能够上传文件,首先第一步就是要添加form的`enctype`属性,`enctype`属性有如下三种情况:
|
||||
|
||||
application/x-www-form-urlencoded 表示在发送前编码所有字符(默认)
|
||||
multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。
|
||||
text/plain 空格转换为 "+" 加号,但不对特殊字符编码。
|
||||
|
||||
所以,创建新的表单html文件, 命名为upload.gtpl, html代码应该类似于:
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>上传文件</title>
|
||||
</head>
|
||||
<body>
|
||||
<form enctype="multipart/form-data" action="/upload" method="post">
|
||||
<input type="file" name="uploadfile" />
|
||||
<input type="hidden" name="token" value="{{.}}"/>
|
||||
<input type="submit" value="upload" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
在服务器端,我们增加一个handlerFunc:
|
||||
|
||||
http.HandleFunc("/upload", upload)
|
||||
|
||||
// 处理/upload 逻辑
|
||||
func upload(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("method:", r.Method) //获取请求的方法
|
||||
if r.Method == "GET" {
|
||||
crutime := time.Now().Unix()
|
||||
h := md5.New()
|
||||
io.WriteString(h, strconv.FormatInt(crutime, 10))
|
||||
token := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
t, _ := template.ParseFiles("upload.gtpl")
|
||||
t.Execute(w, token)
|
||||
} else {
|
||||
r.ParseMultipartForm(32 << 20)
|
||||
file, handler, err := r.FormFile("uploadfile")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
fmt.Fprintf(w, "%v", handler.Header)
|
||||
f, err := os.OpenFile("./test/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666) // 此处假设当前目录下已存在test目录
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
io.Copy(f, file)
|
||||
}
|
||||
}
|
||||
|
||||
通过上面的代码可以看到,处理文件上传我们需要调用`r.ParseMultipartForm`,里面的参数表示`maxMemory`,调用`ParseMultipartForm`之后,上传的文件存储在`maxMemory`大小的内存里面,如果文件大小超过了`maxMemory`,那么剩下的部分将存储在系统的临时文件中。我们可以通过`r.FormFile`获取上面的文件句柄,然后实例中使用了`io.Copy`来存储文件。
|
||||
|
||||
>获取其他非文件字段信息的时候就不需要调用`r.ParseForm`,因为在需要的时候Go自动会去调用。而且`ParseMultipartForm`调用一次之后,后面再次调用不会再有效果。
|
||||
|
||||
通过上面的实例我们可以看到我们上传文件主要三步处理:
|
||||
|
||||
1. 表单中增加enctype="multipart/form-data"
|
||||
2. 服务端调用`r.ParseMultipartForm`,把上传的文件存储在内存和临时文件中
|
||||
3. 使用`r.FormFile`获取文件句柄,然后对文件进行存储等处理。
|
||||
|
||||
文件handler是multipart.FileHeader,里面存储了如下结构信息
|
||||
|
||||
type FileHeader struct {
|
||||
Filename string
|
||||
Header textproto.MIMEHeader
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
|
||||
我们通过上面的实例代码打印出来上传文件的信息如下
|
||||
|
||||

|
||||
|
||||
图4.5 打印文件上传后服务器端接受的信息
|
||||
|
||||
## 客户端上传文件
|
||||
|
||||
我们上面的例子演示了如何通过表单上传文件,然后在服务器端处理文件,其实Go支持模拟客户端表单功能支持文件上传,详细用法请看如下示例:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
func postFile(filename string, targetUrl string) error {
|
||||
bodyBuf := &bytes.Buffer{}
|
||||
bodyWriter := multipart.NewWriter(bodyBuf)
|
||||
|
||||
//关键的一步操作
|
||||
fileWriter, err := bodyWriter.CreateFormFile("uploadfile", filename)
|
||||
if err != nil {
|
||||
fmt.Println("error writing to buffer")
|
||||
return err
|
||||
}
|
||||
|
||||
//打开文件句柄操作
|
||||
fh, err := os.Open(filename)
|
||||
if err != nil {
|
||||
fmt.Println("error opening file")
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
//iocopy
|
||||
_, err = io.Copy(fileWriter, fh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contentType := bodyWriter.FormDataContentType()
|
||||
bodyWriter.Close()
|
||||
|
||||
resp, err := http.Post(targetUrl, contentType, bodyBuf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
resp_body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(resp.Status)
|
||||
fmt.Println(string(resp_body))
|
||||
return nil
|
||||
}
|
||||
|
||||
// sample usage
|
||||
func main() {
|
||||
target_url := "http://localhost:9090/upload"
|
||||
filename := "./astaxie.pdf"
|
||||
postFile(filename, target_url)
|
||||
}
|
||||
|
||||
|
||||
上面的例子详细展示了客户端如何向服务器上传一个文件的例子,客户端通过multipart.Write把文件的文本流写入一个缓存中,然后调用http的Post方法把缓存传到服务器。
|
||||
|
||||
>如果你还有其他普通字段例如username之类的需要同时写入,那么可以调用multipart的WriteField方法写很多其他类似的字段。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [防止多次递交表单](<04.4.md>)
|
||||
* 下一节: [小结](<04.6.md>)
|
||||
@@ -1,16 +1,16 @@
|
||||
# 5 访问数据库
|
||||
对许多Web应用程序而言,数据库都是其核心所在。数据库几乎可以用来存储你想查询和修改的任何信息,比如用户信息、产品目录或者新闻列表等。
|
||||
|
||||
Go没有内置的驱动支持任何的数据库,但是Go定义了database/sql接口,用户可以基于驱动接口开发相应数据库的驱动,5.1小节里面介绍Go设计的一些驱动,介绍Go是如何设计数据库驱动接口的。5.2至5.4小节介绍目前使用的比较多的一些关系型数据驱动以及如何使用,5.5小节介绍我自己开发一个ORM库,基于database/sql标准接口开发的,可以兼容几乎所有支持database/sql的数据库驱动,可以方便的使用Go style来进行数据库操作。
|
||||
|
||||
目前NOSQL已经成为Web开发的一个潮流,很多应用采用了NOSQL作为数据库,而不是以前的缓存,5.6小节将介绍MongoDB和Redis两种NOSQL数据库。
|
||||
|
||||
>[Go database/sql tutorial](http://go-database-sql.org/) 里提供了惯用的范例及详细的说明。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第四章总结](<04.6.md>)
|
||||
* 下一节: [database/sql接口](<05.1.md>)
|
||||
# 5 访问数据库
|
||||
对许多Web应用程序而言,数据库都是其核心所在。数据库几乎可以用来存储你想查询和修改的任何信息,比如用户信息、产品目录或者新闻列表等。
|
||||
|
||||
Go没有内置的驱动支持任何的数据库,但是Go定义了database/sql接口,用户可以基于驱动接口开发相应数据库的驱动,5.1小节里面介绍Go设计的一些驱动,介绍Go是如何设计数据库驱动接口的。5.2至5.4小节介绍目前使用的比较多的一些关系型数据驱动以及如何使用,5.5小节介绍我自己开发一个ORM库,基于database/sql标准接口开发的,可以兼容几乎所有支持database/sql的数据库驱动,可以方便的使用Go style来进行数据库操作。
|
||||
|
||||
目前NOSQL已经成为Web开发的一个潮流,很多应用采用了NOSQL作为数据库,而不是以前的缓存,5.6小节将介绍MongoDB和Redis两种NOSQL数据库。
|
||||
|
||||
>[Go database/sql tutorial](http://go-database-sql.org/) 里提供了惯用的范例及详细的说明。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第四章总结](<04.6.md>)
|
||||
* 下一节: [database/sql接口](<05.1.md>)
|
||||
16
zh/05.0.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
16
zh/05.0.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,16 @@
|
||||
# 5 访问数据库
|
||||
对许多Web应用程序而言,数据库都是其核心所在。数据库几乎可以用来存储你想查询和修改的任何信息,比如用户信息、产品目录或者新闻列表等。
|
||||
|
||||
Go没有内置的驱动支持任何的数据库,但是Go定义了database/sql接口,用户可以基于驱动接口开发相应数据库的驱动,5.1小节里面介绍Go设计的一些驱动,介绍Go是如何设计数据库驱动接口的。5.2至5.4小节介绍目前使用的比较多的一些关系型数据驱动以及如何使用,5.5小节介绍我自己开发一个ORM库,基于database/sql标准接口开发的,可以兼容几乎所有支持database/sql的数据库驱动,可以方便的使用Go style来进行数据库操作。
|
||||
|
||||
目前NOSQL已经成为Web开发的一个潮流,很多应用采用了NOSQL作为数据库,而不是以前的缓存,5.6小节将介绍MongoDB和Redis两种NOSQL数据库。
|
||||
|
||||
>[Go database/sql tutorial](http://go-database-sql.org/) 里提供了惯用的范例及详细的说明。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第四章总结](<04.6.md>)
|
||||
* 下一节: [database/sql接口](<05.1.md>)
|
||||
@@ -1,204 +1,204 @@
|
||||
# 5.1 database/sql接口
|
||||
Go与PHP不同的地方是Go官方没有提供数据库驱动,而是为开发数据库驱动定义了一些标准接口,开发者可以根据定义的接口来开发相应的数据库驱动,这样做有一个好处,只要是按照标准接口开发的代码, 以后需要迁移数据库时,不需要任何修改。那么Go都定义了哪些标准接口呢?让我们来详细的分析一下
|
||||
|
||||
## sql.Register
|
||||
这个存在于database/sql的函数是用来注册数据库驱动的,当第三方开发者开发数据库驱动时,都会实现init函数,在init里面会调用这个`Register(name string, driver driver.Driver)`完成本驱动的注册。
|
||||
|
||||
我们来看一下mymysql、sqlite3的驱动里面都是怎么调用的:
|
||||
|
||||
//https://github.com/mattn/go-sqlite3驱动
|
||||
func init() {
|
||||
sql.Register("sqlite3", &SQLiteDriver{})
|
||||
}
|
||||
|
||||
//https://github.com/mikespook/mymysql驱动
|
||||
// Driver automatically registered in database/sql
|
||||
var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"}
|
||||
func init() {
|
||||
Register("SET NAMES utf8")
|
||||
sql.Register("mymysql", &d)
|
||||
}
|
||||
|
||||
我们看到第三方数据库驱动都是通过调用这个函数来注册自己的数据库驱动名称以及相应的driver实现。在database/sql内部通过一个map来存储用户定义的相应驱动。
|
||||
|
||||
var drivers = make(map[string]driver.Driver)
|
||||
|
||||
drivers[name] = driver
|
||||
|
||||
因此通过database/sql的注册函数可以同时注册多个数据库驱动,只要不重复。
|
||||
|
||||
>在我们使用database/sql接口和第三方库的时候经常看到如下:
|
||||
|
||||
> import (
|
||||
> "database/sql"
|
||||
> _ "github.com/mattn/go-sqlite3"
|
||||
> )
|
||||
|
||||
>新手都会被这个`_`所迷惑,其实这个就是Go设计的巧妙之处,我们在变量赋值的时候经常看到这个符号,它是用来忽略变量赋值的占位符,那么包引入用到这个符号也是相似的作用,这儿使用`_`的意思是引入后面的包名而不直接使用这个包中定义的函数,变量等资源。
|
||||
|
||||
>我们在2.3节流程和函数一节中介绍过init函数的初始化过程,包在引入的时候会自动调用包的init函数以完成对包的初始化。因此,我们引入上面的数据库驱动包之后会自动去调用init函数,然后在init函数里面注册这个数据库驱动,这样我们就可以在接下来的代码中直接使用这个数据库驱动了。
|
||||
|
||||
## driver.Driver
|
||||
Driver是一个数据库驱动的接口,他定义了一个method: Open(name string),这个方法返回一个数据库的Conn接口。
|
||||
|
||||
type Driver interface {
|
||||
Open(name string) (Conn, error)
|
||||
}
|
||||
|
||||
返回的Conn只能用来进行一次goroutine的操作,也就是说不能把这个Conn应用于Go的多个goroutine里面。如下代码会出现错误
|
||||
|
||||
...
|
||||
go goroutineA (Conn) //执行查询操作
|
||||
go goroutineB (Conn) //执行插入操作
|
||||
...
|
||||
|
||||
上面这样的代码可能会使Go不知道某个操作究竟是由哪个goroutine发起的,从而导致数据混乱,比如可能会把goroutineA里面执行的查询操作的结果返回给goroutineB从而使B错误地把此结果当成自己执行的插入数据。
|
||||
|
||||
第三方驱动都会定义这个函数,它会解析name参数来获取相关数据库的连接信息,解析完成后,它将使用此信息来初始化一个Conn并返回它。
|
||||
|
||||
## driver.Conn
|
||||
Conn是一个数据库连接的接口定义,他定义了一系列方法,这个Conn只能应用在一个goroutine里面,不能使用在多个goroutine里面,详情请参考上面的说明。
|
||||
|
||||
type Conn interface {
|
||||
Prepare(query string) (Stmt, error)
|
||||
Close() error
|
||||
Begin() (Tx, error)
|
||||
}
|
||||
|
||||
Prepare函数返回与当前连接相关的执行Sql语句的准备状态,可以进行查询、删除等操作。
|
||||
|
||||
Close函数关闭当前的连接,执行释放连接拥有的资源等清理工作。因为驱动实现了database/sql里面建议的conn pool,所以你不用再去实现缓存conn之类的,这样会容易引起问题。
|
||||
|
||||
Begin函数返回一个代表事务处理的Tx,通过它你可以进行查询,更新等操作,或者对事务进行回滚、递交。
|
||||
|
||||
## driver.Stmt
|
||||
Stmt是一种准备好的状态,和Conn相关联,而且只能应用于一个goroutine中,不能应用于多个goroutine。
|
||||
|
||||
type Stmt interface {
|
||||
Close() error
|
||||
NumInput() int
|
||||
Exec(args []Value) (Result, error)
|
||||
Query(args []Value) (Rows, error)
|
||||
}
|
||||
|
||||
Close函数关闭当前的链接状态,但是如果当前正在执行query,query还是有效返回rows数据。
|
||||
|
||||
NumInput函数返回当前预留参数的个数,当返回>=0时数据库驱动就会智能检查调用者的参数。当数据库驱动包不知道预留参数的时候,返回-1。
|
||||
|
||||
Exec函数执行Prepare准备好的sql,传入参数执行update/insert等操作,返回Result数据
|
||||
|
||||
Query函数执行Prepare准备好的sql,传入需要的参数执行select操作,返回Rows结果集
|
||||
|
||||
|
||||
## driver.Tx
|
||||
事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以
|
||||
|
||||
type Tx interface {
|
||||
Commit() error
|
||||
Rollback() error
|
||||
}
|
||||
|
||||
这两个函数一个用来递交一个事务,一个用来回滚事务。
|
||||
|
||||
## driver.Execer
|
||||
这是一个Conn可选择实现的接口
|
||||
|
||||
type Execer interface {
|
||||
Exec(query string, args []Value) (Result, error)
|
||||
}
|
||||
|
||||
如果这个接口没有定义,那么在调用DB.Exec,就会首先调用Prepare返回Stmt,然后执行Stmt的Exec,然后关闭Stmt。
|
||||
|
||||
## driver.Result
|
||||
这个是执行Update/Insert等操作返回的结果接口定义
|
||||
|
||||
type Result interface {
|
||||
LastInsertId() (int64, error)
|
||||
RowsAffected() (int64, error)
|
||||
}
|
||||
|
||||
LastInsertId函数返回由数据库执行插入操作得到的自增ID号。
|
||||
|
||||
RowsAffected函数返回query操作影响的数据条目数。
|
||||
|
||||
## driver.Rows
|
||||
Rows是执行查询返回的结果集接口定义
|
||||
|
||||
type Rows interface {
|
||||
Columns() []string
|
||||
Close() error
|
||||
Next(dest []Value) error
|
||||
}
|
||||
|
||||
Columns函数返回查询数据库表的字段信息,这个返回的slice和sql查询的字段一一对应,而不是返回整个表的所有字段。
|
||||
|
||||
Close函数用来关闭Rows迭代器。
|
||||
|
||||
Next函数用来返回下一条数据,把数据赋值给dest。dest里面的元素必须是driver.Value的值除了string,返回的数据里面所有的string都必须要转换成[]byte。如果最后没数据了,Next函数最后返回io.EOF。
|
||||
|
||||
|
||||
## driver.RowsAffected
|
||||
RowsAffected其实就是一个int64的别名,但是他实现了Result接口,用来底层实现Result的表示方式
|
||||
|
||||
type RowsAffected int64
|
||||
|
||||
func (RowsAffected) LastInsertId() (int64, error)
|
||||
|
||||
func (v RowsAffected) RowsAffected() (int64, error)
|
||||
|
||||
## driver.Value
|
||||
Value其实就是一个空接口,他可以容纳任何的数据
|
||||
|
||||
type Value interface{}
|
||||
|
||||
drive的Value是驱动必须能够操作的Value,Value要么是nil,要么是下面的任意一种
|
||||
|
||||
int64
|
||||
float64
|
||||
bool
|
||||
[]byte
|
||||
string [*]除了Rows.Next返回的不能是string.
|
||||
time.Time
|
||||
|
||||
## driver.ValueConverter
|
||||
ValueConverter接口定义了如何把一个普通的值转化成driver.Value的接口
|
||||
|
||||
type ValueConverter interface {
|
||||
ConvertValue(v interface{}) (Value, error)
|
||||
}
|
||||
|
||||
在开发的数据库驱动包里面实现这个接口的函数在很多地方会使用到,这个ValueConverter有很多好处:
|
||||
|
||||
- 转化driver.value到数据库表相应的字段,例如int64的数据如何转化成数据库表uint16字段
|
||||
- 把数据库查询结果转化成driver.Value值
|
||||
- 在scan函数里面如何把driver.Value值转化成用户定义的值
|
||||
|
||||
## driver.Valuer
|
||||
Valuer接口定义了返回一个driver.Value的方式
|
||||
|
||||
type Valuer interface {
|
||||
Value() (Value, error)
|
||||
}
|
||||
|
||||
很多类型都实现了这个Value方法,用来自身与driver.Value的转化。
|
||||
|
||||
通过上面的讲解,你应该对于驱动的开发有了一个基本的了解,一个驱动只要实现了这些接口就能完成增删查改等基本操作了,剩下的就是与相应的数据库进行数据交互等细节问题了,在此不再赘述。
|
||||
|
||||
## database/sql
|
||||
database/sql在database/sql/driver提供的接口基础上定义了一些更高阶的方法,用以简化数据库操作,同时内部还建议性地实现一个conn pool。
|
||||
|
||||
type DB struct {
|
||||
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,然后再返回之。
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [访问数据库](<05.0.md>)
|
||||
* 下一节: [使用MySQL数据库](<05.2.md>)
|
||||
# 5.1 database/sql接口
|
||||
Go与PHP不同的地方是Go官方没有提供数据库驱动,而是为开发数据库驱动定义了一些标准接口,开发者可以根据定义的接口来开发相应的数据库驱动,这样做有一个好处,只要是按照标准接口开发的代码, 以后需要迁移数据库时,不需要任何修改。那么Go都定义了哪些标准接口呢?让我们来详细的分析一下
|
||||
|
||||
## sql.Register
|
||||
这个存在于database/sql的函数是用来注册数据库驱动的,当第三方开发者开发数据库驱动时,都会实现init函数,在init里面会调用这个`Register(name string, driver driver.Driver)`完成本驱动的注册。
|
||||
|
||||
我们来看一下mymysql、sqlite3的驱动里面都是怎么调用的:
|
||||
|
||||
//https://github.com/mattn/go-sqlite3驱动
|
||||
func init() {
|
||||
sql.Register("sqlite3", &SQLiteDriver{})
|
||||
}
|
||||
|
||||
//https://github.com/mikespook/mymysql驱动
|
||||
// Driver automatically registered in database/sql
|
||||
var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"}
|
||||
func init() {
|
||||
Register("SET NAMES utf8")
|
||||
sql.Register("mymysql", &d)
|
||||
}
|
||||
|
||||
我们看到第三方数据库驱动都是通过调用这个函数来注册自己的数据库驱动名称以及相应的driver实现。在database/sql内部通过一个map来存储用户定义的相应驱动。
|
||||
|
||||
var drivers = make(map[string]driver.Driver)
|
||||
|
||||
drivers[name] = driver
|
||||
|
||||
因此通过database/sql的注册函数可以同时注册多个数据库驱动,只要不重复。
|
||||
|
||||
>在我们使用database/sql接口和第三方库的时候经常看到如下:
|
||||
|
||||
> import (
|
||||
> "database/sql"
|
||||
> _ "github.com/mattn/go-sqlite3"
|
||||
> )
|
||||
|
||||
>新手都会被这个`_`所迷惑,其实这个就是Go设计的巧妙之处,我们在变量赋值的时候经常看到这个符号,它是用来忽略变量赋值的占位符,那么包引入用到这个符号也是相似的作用,这儿使用`_`的意思是引入后面的包名而不直接使用这个包中定义的函数,变量等资源。
|
||||
|
||||
>我们在2.3节流程和函数一节中介绍过init函数的初始化过程,包在引入的时候会自动调用包的init函数以完成对包的初始化。因此,我们引入上面的数据库驱动包之后会自动去调用init函数,然后在init函数里面注册这个数据库驱动,这样我们就可以在接下来的代码中直接使用这个数据库驱动了。
|
||||
|
||||
## driver.Driver
|
||||
Driver是一个数据库驱动的接口,他定义了一个method: Open(name string),这个方法返回一个数据库的Conn接口。
|
||||
|
||||
type Driver interface {
|
||||
Open(name string) (Conn, error)
|
||||
}
|
||||
|
||||
返回的Conn只能用来进行一次goroutine的操作,也就是说不能把这个Conn应用于Go的多个goroutine里面。如下代码会出现错误
|
||||
|
||||
...
|
||||
go goroutineA (Conn) //执行查询操作
|
||||
go goroutineB (Conn) //执行插入操作
|
||||
...
|
||||
|
||||
上面这样的代码可能会使Go不知道某个操作究竟是由哪个goroutine发起的,从而导致数据混乱,比如可能会把goroutineA里面执行的查询操作的结果返回给goroutineB从而使B错误地把此结果当成自己执行的插入数据。
|
||||
|
||||
第三方驱动都会定义这个函数,它会解析name参数来获取相关数据库的连接信息,解析完成后,它将使用此信息来初始化一个Conn并返回它。
|
||||
|
||||
## driver.Conn
|
||||
Conn是一个数据库连接的接口定义,他定义了一系列方法,这个Conn只能应用在一个goroutine里面,不能使用在多个goroutine里面,详情请参考上面的说明。
|
||||
|
||||
type Conn interface {
|
||||
Prepare(query string) (Stmt, error)
|
||||
Close() error
|
||||
Begin() (Tx, error)
|
||||
}
|
||||
|
||||
Prepare函数返回与当前连接相关的执行Sql语句的准备状态,可以进行查询、删除等操作。
|
||||
|
||||
Close函数关闭当前的连接,执行释放连接拥有的资源等清理工作。因为驱动实现了database/sql里面建议的conn pool,所以你不用再去实现缓存conn之类的,这样会容易引起问题。
|
||||
|
||||
Begin函数返回一个代表事务处理的Tx,通过它你可以进行查询,更新等操作,或者对事务进行回滚、递交。
|
||||
|
||||
## driver.Stmt
|
||||
Stmt是一种准备好的状态,和Conn相关联,而且只能应用于一个goroutine中,不能应用于多个goroutine。
|
||||
|
||||
type Stmt interface {
|
||||
Close() error
|
||||
NumInput() int
|
||||
Exec(args []Value) (Result, error)
|
||||
Query(args []Value) (Rows, error)
|
||||
}
|
||||
|
||||
Close函数关闭当前的链接状态,但是如果当前正在执行query,query还是有效返回rows数据。
|
||||
|
||||
NumInput函数返回当前预留参数的个数,当返回>=0时数据库驱动就会智能检查调用者的参数。当数据库驱动包不知道预留参数的时候,返回-1。
|
||||
|
||||
Exec函数执行Prepare准备好的sql,传入参数执行update/insert等操作,返回Result数据
|
||||
|
||||
Query函数执行Prepare准备好的sql,传入需要的参数执行select操作,返回Rows结果集
|
||||
|
||||
|
||||
## driver.Tx
|
||||
事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以
|
||||
|
||||
type Tx interface {
|
||||
Commit() error
|
||||
Rollback() error
|
||||
}
|
||||
|
||||
这两个函数一个用来递交一个事务,一个用来回滚事务。
|
||||
|
||||
## driver.Execer
|
||||
这是一个Conn可选择实现的接口
|
||||
|
||||
type Execer interface {
|
||||
Exec(query string, args []Value) (Result, error)
|
||||
}
|
||||
|
||||
如果这个接口没有定义,那么在调用DB.Exec,就会首先调用Prepare返回Stmt,然后执行Stmt的Exec,然后关闭Stmt。
|
||||
|
||||
## driver.Result
|
||||
这个是执行Update/Insert等操作返回的结果接口定义
|
||||
|
||||
type Result interface {
|
||||
LastInsertId() (int64, error)
|
||||
RowsAffected() (int64, error)
|
||||
}
|
||||
|
||||
LastInsertId函数返回由数据库执行插入操作得到的自增ID号。
|
||||
|
||||
RowsAffected函数返回query操作影响的数据条目数。
|
||||
|
||||
## driver.Rows
|
||||
Rows是执行查询返回的结果集接口定义
|
||||
|
||||
type Rows interface {
|
||||
Columns() []string
|
||||
Close() error
|
||||
Next(dest []Value) error
|
||||
}
|
||||
|
||||
Columns函数返回查询数据库表的字段信息,这个返回的slice和sql查询的字段一一对应,而不是返回整个表的所有字段。
|
||||
|
||||
Close函数用来关闭Rows迭代器。
|
||||
|
||||
Next函数用来返回下一条数据,把数据赋值给dest。dest里面的元素必须是driver.Value的值除了string,返回的数据里面所有的string都必须要转换成[]byte。如果最后没数据了,Next函数最后返回io.EOF。
|
||||
|
||||
|
||||
## driver.RowsAffected
|
||||
RowsAffected其实就是一个int64的别名,但是他实现了Result接口,用来底层实现Result的表示方式
|
||||
|
||||
type RowsAffected int64
|
||||
|
||||
func (RowsAffected) LastInsertId() (int64, error)
|
||||
|
||||
func (v RowsAffected) RowsAffected() (int64, error)
|
||||
|
||||
## driver.Value
|
||||
Value其实就是一个空接口,他可以容纳任何的数据
|
||||
|
||||
type Value interface{}
|
||||
|
||||
drive的Value是驱动必须能够操作的Value,Value要么是nil,要么是下面的任意一种
|
||||
|
||||
int64
|
||||
float64
|
||||
bool
|
||||
[]byte
|
||||
string [*]除了Rows.Next返回的不能是string.
|
||||
time.Time
|
||||
|
||||
## driver.ValueConverter
|
||||
ValueConverter接口定义了如何把一个普通的值转化成driver.Value的接口
|
||||
|
||||
type ValueConverter interface {
|
||||
ConvertValue(v interface{}) (Value, error)
|
||||
}
|
||||
|
||||
在开发的数据库驱动包里面实现这个接口的函数在很多地方会使用到,这个ValueConverter有很多好处:
|
||||
|
||||
- 转化driver.value到数据库表相应的字段,例如int64的数据如何转化成数据库表uint16字段
|
||||
- 把数据库查询结果转化成driver.Value值
|
||||
- 在scan函数里面如何把driver.Value值转化成用户定义的值
|
||||
|
||||
## driver.Valuer
|
||||
Valuer接口定义了返回一个driver.Value的方式
|
||||
|
||||
type Valuer interface {
|
||||
Value() (Value, error)
|
||||
}
|
||||
|
||||
很多类型都实现了这个Value方法,用来自身与driver.Value的转化。
|
||||
|
||||
通过上面的讲解,你应该对于驱动的开发有了一个基本的了解,一个驱动只要实现了这些接口就能完成增删查改等基本操作了,剩下的就是与相应的数据库进行数据交互等细节问题了,在此不再赘述。
|
||||
|
||||
## database/sql
|
||||
database/sql在database/sql/driver提供的接口基础上定义了一些更高阶的方法,用以简化数据库操作,同时内部还建议性地实现一个conn pool。
|
||||
|
||||
type DB struct {
|
||||
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,然后再返回之。
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [访问数据库](<05.0.md>)
|
||||
* 下一节: [使用MySQL数据库](<05.2.md>)
|
||||
204
zh/05.1.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
204
zh/05.1.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,204 @@
|
||||
# 5.1 database/sql接口
|
||||
Go与PHP不同的地方是Go官方没有提供数据库驱动,而是为开发数据库驱动定义了一些标准接口,开发者可以根据定义的接口来开发相应的数据库驱动,这样做有一个好处,只要是按照标准接口开发的代码, 以后需要迁移数据库时,不需要任何修改。那么Go都定义了哪些标准接口呢?让我们来详细的分析一下
|
||||
|
||||
## sql.Register
|
||||
这个存在于database/sql的函数是用来注册数据库驱动的,当第三方开发者开发数据库驱动时,都会实现init函数,在init里面会调用这个`Register(name string, driver driver.Driver)`完成本驱动的注册。
|
||||
|
||||
我们来看一下mymysql、sqlite3的驱动里面都是怎么调用的:
|
||||
|
||||
//https://github.com/mattn/go-sqlite3驱动
|
||||
func init() {
|
||||
sql.Register("sqlite3", &SQLiteDriver{})
|
||||
}
|
||||
|
||||
//https://github.com/mikespook/mymysql驱动
|
||||
// Driver automatically registered in database/sql
|
||||
var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"}
|
||||
func init() {
|
||||
Register("SET NAMES utf8")
|
||||
sql.Register("mymysql", &d)
|
||||
}
|
||||
|
||||
我们看到第三方数据库驱动都是通过调用这个函数来注册自己的数据库驱动名称以及相应的driver实现。在database/sql内部通过一个map来存储用户定义的相应驱动。
|
||||
|
||||
var drivers = make(map[string]driver.Driver)
|
||||
|
||||
drivers[name] = driver
|
||||
|
||||
因此通过database/sql的注册函数可以同时注册多个数据库驱动,只要不重复。
|
||||
|
||||
>在我们使用database/sql接口和第三方库的时候经常看到如下:
|
||||
|
||||
> import (
|
||||
> "database/sql"
|
||||
> _ "github.com/mattn/go-sqlite3"
|
||||
> )
|
||||
|
||||
>新手都会被这个`_`所迷惑,其实这个就是Go设计的巧妙之处,我们在变量赋值的时候经常看到这个符号,它是用来忽略变量赋值的占位符,那么包引入用到这个符号也是相似的作用,这儿使用`_`的意思是引入后面的包名而不直接使用这个包中定义的函数,变量等资源。
|
||||
|
||||
>我们在2.3节流程和函数一节中介绍过init函数的初始化过程,包在引入的时候会自动调用包的init函数以完成对包的初始化。因此,我们引入上面的数据库驱动包之后会自动去调用init函数,然后在init函数里面注册这个数据库驱动,这样我们就可以在接下来的代码中直接使用这个数据库驱动了。
|
||||
|
||||
## driver.Driver
|
||||
Driver是一个数据库驱动的接口,他定义了一个method: Open(name string),这个方法返回一个数据库的Conn接口。
|
||||
|
||||
type Driver interface {
|
||||
Open(name string) (Conn, error)
|
||||
}
|
||||
|
||||
返回的Conn只能用来进行一次goroutine的操作,也就是说不能把这个Conn应用于Go的多个goroutine里面。如下代码会出现错误
|
||||
|
||||
...
|
||||
go goroutineA (Conn) //执行查询操作
|
||||
go goroutineB (Conn) //执行插入操作
|
||||
...
|
||||
|
||||
上面这样的代码可能会使Go不知道某个操作究竟是由哪个goroutine发起的,从而导致数据混乱,比如可能会把goroutineA里面执行的查询操作的结果返回给goroutineB从而使B错误地把此结果当成自己执行的插入数据。
|
||||
|
||||
第三方驱动都会定义这个函数,它会解析name参数来获取相关数据库的连接信息,解析完成后,它将使用此信息来初始化一个Conn并返回它。
|
||||
|
||||
## driver.Conn
|
||||
Conn是一个数据库连接的接口定义,他定义了一系列方法,这个Conn只能应用在一个goroutine里面,不能使用在多个goroutine里面,详情请参考上面的说明。
|
||||
|
||||
type Conn interface {
|
||||
Prepare(query string) (Stmt, error)
|
||||
Close() error
|
||||
Begin() (Tx, error)
|
||||
}
|
||||
|
||||
Prepare函数返回与当前连接相关的执行Sql语句的准备状态,可以进行查询、删除等操作。
|
||||
|
||||
Close函数关闭当前的连接,执行释放连接拥有的资源等清理工作。因为驱动实现了database/sql里面建议的conn pool,所以你不用再去实现缓存conn之类的,这样会容易引起问题。
|
||||
|
||||
Begin函数返回一个代表事务处理的Tx,通过它你可以进行查询,更新等操作,或者对事务进行回滚、递交。
|
||||
|
||||
## driver.Stmt
|
||||
Stmt是一种准备好的状态,和Conn相关联,而且只能应用于一个goroutine中,不能应用于多个goroutine。
|
||||
|
||||
type Stmt interface {
|
||||
Close() error
|
||||
NumInput() int
|
||||
Exec(args []Value) (Result, error)
|
||||
Query(args []Value) (Rows, error)
|
||||
}
|
||||
|
||||
Close函数关闭当前的链接状态,但是如果当前正在执行query,query还是有效返回rows数据。
|
||||
|
||||
NumInput函数返回当前预留参数的个数,当返回>=0时数据库驱动就会智能检查调用者的参数。当数据库驱动包不知道预留参数的时候,返回-1。
|
||||
|
||||
Exec函数执行Prepare准备好的sql,传入参数执行update/insert等操作,返回Result数据
|
||||
|
||||
Query函数执行Prepare准备好的sql,传入需要的参数执行select操作,返回Rows结果集
|
||||
|
||||
|
||||
## driver.Tx
|
||||
事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以
|
||||
|
||||
type Tx interface {
|
||||
Commit() error
|
||||
Rollback() error
|
||||
}
|
||||
|
||||
这两个函数一个用来递交一个事务,一个用来回滚事务。
|
||||
|
||||
## driver.Execer
|
||||
这是一个Conn可选择实现的接口
|
||||
|
||||
type Execer interface {
|
||||
Exec(query string, args []Value) (Result, error)
|
||||
}
|
||||
|
||||
如果这个接口没有定义,那么在调用DB.Exec,就会首先调用Prepare返回Stmt,然后执行Stmt的Exec,然后关闭Stmt。
|
||||
|
||||
## driver.Result
|
||||
这个是执行Update/Insert等操作返回的结果接口定义
|
||||
|
||||
type Result interface {
|
||||
LastInsertId() (int64, error)
|
||||
RowsAffected() (int64, error)
|
||||
}
|
||||
|
||||
LastInsertId函数返回由数据库执行插入操作得到的自增ID号。
|
||||
|
||||
RowsAffected函数返回query操作影响的数据条目数。
|
||||
|
||||
## driver.Rows
|
||||
Rows是执行查询返回的结果集接口定义
|
||||
|
||||
type Rows interface {
|
||||
Columns() []string
|
||||
Close() error
|
||||
Next(dest []Value) error
|
||||
}
|
||||
|
||||
Columns函数返回查询数据库表的字段信息,这个返回的slice和sql查询的字段一一对应,而不是返回整个表的所有字段。
|
||||
|
||||
Close函数用来关闭Rows迭代器。
|
||||
|
||||
Next函数用来返回下一条数据,把数据赋值给dest。dest里面的元素必须是driver.Value的值除了string,返回的数据里面所有的string都必须要转换成[]byte。如果最后没数据了,Next函数最后返回io.EOF。
|
||||
|
||||
|
||||
## driver.RowsAffected
|
||||
RowsAffected其实就是一个int64的别名,但是他实现了Result接口,用来底层实现Result的表示方式
|
||||
|
||||
type RowsAffected int64
|
||||
|
||||
func (RowsAffected) LastInsertId() (int64, error)
|
||||
|
||||
func (v RowsAffected) RowsAffected() (int64, error)
|
||||
|
||||
## driver.Value
|
||||
Value其实就是一个空接口,他可以容纳任何的数据
|
||||
|
||||
type Value interface{}
|
||||
|
||||
drive的Value是驱动必须能够操作的Value,Value要么是nil,要么是下面的任意一种
|
||||
|
||||
int64
|
||||
float64
|
||||
bool
|
||||
[]byte
|
||||
string [*]除了Rows.Next返回的不能是string.
|
||||
time.Time
|
||||
|
||||
## driver.ValueConverter
|
||||
ValueConverter接口定义了如何把一个普通的值转化成driver.Value的接口
|
||||
|
||||
type ValueConverter interface {
|
||||
ConvertValue(v interface{}) (Value, error)
|
||||
}
|
||||
|
||||
在开发的数据库驱动包里面实现这个接口的函数在很多地方会使用到,这个ValueConverter有很多好处:
|
||||
|
||||
- 转化driver.value到数据库表相应的字段,例如int64的数据如何转化成数据库表uint16字段
|
||||
- 把数据库查询结果转化成driver.Value值
|
||||
- 在scan函数里面如何把driver.Value值转化成用户定义的值
|
||||
|
||||
## driver.Valuer
|
||||
Valuer接口定义了返回一个driver.Value的方式
|
||||
|
||||
type Valuer interface {
|
||||
Value() (Value, error)
|
||||
}
|
||||
|
||||
很多类型都实现了这个Value方法,用来自身与driver.Value的转化。
|
||||
|
||||
通过上面的讲解,你应该对于驱动的开发有了一个基本的了解,一个驱动只要实现了这些接口就能完成增删查改等基本操作了,剩下的就是与相应的数据库进行数据交互等细节问题了,在此不再赘述。
|
||||
|
||||
## database/sql
|
||||
database/sql在database/sql/driver提供的接口基础上定义了一些更高阶的方法,用以简化数据库操作,同时内部还建议性地实现一个conn pool。
|
||||
|
||||
type DB struct {
|
||||
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,然后再返回之。
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [访问数据库](<05.0.md>)
|
||||
* 下一节: [使用MySQL数据库](<05.2.md>)
|
||||
@@ -1,137 +1,137 @@
|
||||
# 5.2 使用MySQL数据库
|
||||
目前Internet上流行的网站构架方式是LAMP,其中的M即MySQL, 作为数据库,MySQL以免费、开源、使用方便为优势成为了很多Web开发的后端数据库存储引擎。
|
||||
|
||||
## MySQL驱动
|
||||
Go中支持MySQL的驱动目前比较多,有如下几种,有些是支持database/sql标准,而有些是采用了自己的实现接口,常用的有如下几种:
|
||||
|
||||
- https://github.com/go-sql-driver/mysql 支持database/sql,全部采用go写。
|
||||
- https://github.com/ziutek/mymysql 支持database/sql,也支持自定义的接口,全部采用go写。
|
||||
- https://github.com/Philio/GoMySQL 不支持database/sql,自定义接口,全部采用go写。
|
||||
|
||||
接下来的例子我主要以第一个驱动为例(我目前项目中也是采用它来驱动),也推荐大家采用它,主要理由:
|
||||
|
||||
- 这个驱动比较新,维护的比较好
|
||||
- 完全支持database/sql接口
|
||||
- 支持keepalive,保持长连接,虽然[星星](http://www.mikespook.com)fork的mymysql也支持keepalive,但不是线程安全的,这个从底层就支持了keepalive。
|
||||
|
||||
## 示例代码
|
||||
接下来的几个小节里面我们都将采用同一个数据库表结构:数据库test,用户表userinfo,关联用户信息表userdetail。
|
||||
|
||||
CREATE TABLE `userinfo` (
|
||||
`uid` INT(10) NOT NULL AUTO_INCREMENT,
|
||||
`username` VARCHAR(64) NULL DEFAULT NULL,
|
||||
`departname` VARCHAR(64) NULL DEFAULT NULL,
|
||||
`created` DATE NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`uid`)
|
||||
)
|
||||
|
||||
CREATE TABLE `userdetail` (
|
||||
`uid` INT(10) NOT NULL DEFAULT '0',
|
||||
`intro` TEXT NULL,
|
||||
`profile` TEXT NULL,
|
||||
PRIMARY KEY (`uid`)
|
||||
)
|
||||
|
||||
如下示例将示范如何使用database/sql接口对数据库表进行增删改查操作
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
//"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8")
|
||||
checkErr(err)
|
||||
|
||||
//插入数据
|
||||
stmt, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?")
|
||||
checkErr(err)
|
||||
|
||||
res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09")
|
||||
checkErr(err)
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(id)
|
||||
//更新数据
|
||||
stmt, err = db.Prepare("update userinfo set username=? where uid=?")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec("astaxieupdate", id)
|
||||
checkErr(err)
|
||||
|
||||
affect, err := res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
//查询数据
|
||||
rows, err := db.Query("SELECT * FROM userinfo")
|
||||
checkErr(err)
|
||||
|
||||
for rows.Next() {
|
||||
var uid int
|
||||
var username string
|
||||
var department string
|
||||
var created string
|
||||
err = rows.Scan(&uid, &username, &department, &created)
|
||||
checkErr(err)
|
||||
fmt.Println(uid)
|
||||
fmt.Println(username)
|
||||
fmt.Println(department)
|
||||
fmt.Println(created)
|
||||
}
|
||||
|
||||
//删除数据
|
||||
stmt, err = db.Prepare("delete from userinfo where uid=?")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec(id)
|
||||
checkErr(err)
|
||||
|
||||
affect, err = res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
db.Close()
|
||||
|
||||
}
|
||||
|
||||
func checkErr(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
通过上面的代码我们可以看出,Go操作Mysql数据库是很方便的。
|
||||
|
||||
关键的几个函数我解释一下:
|
||||
|
||||
sql.Open()函数用来打开一个注册过的数据库驱动,go-sql-driver中注册了mysql这个数据库驱动,第二个参数是DSN(Data Source Name),它是go-sql-driver定义的一些数据库链接和配置信息。它支持如下格式:
|
||||
|
||||
user@unix(/path/to/socket)/dbname?charset=utf8
|
||||
user:password@tcp(localhost:5555)/dbname?charset=utf8
|
||||
user:password@/dbname
|
||||
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname
|
||||
|
||||
db.Prepare()函数用来返回准备要执行的sql操作,然后返回准备完毕的执行状态。
|
||||
|
||||
db.Query()函数用来直接执行Sql返回Rows结果。
|
||||
|
||||
stmt.Exec()函数用来执行stmt准备好的SQL语句
|
||||
|
||||
我们可以看到我们传入的参数都是=?对应的数据,这样做的方式可以一定程度上防止SQL注入。
|
||||
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [database/sql接口](<05.1.md>)
|
||||
* 下一节: [使用SQLite数据库](<05.3.md>)
|
||||
# 5.2 使用MySQL数据库
|
||||
目前Internet上流行的网站构架方式是LAMP,其中的M即MySQL, 作为数据库,MySQL以免费、开源、使用方便为优势成为了很多Web开发的后端数据库存储引擎。
|
||||
|
||||
## MySQL驱动
|
||||
Go中支持MySQL的驱动目前比较多,有如下几种,有些是支持database/sql标准,而有些是采用了自己的实现接口,常用的有如下几种:
|
||||
|
||||
- https://github.com/go-sql-driver/mysql 支持database/sql,全部采用go写。
|
||||
- https://github.com/ziutek/mymysql 支持database/sql,也支持自定义的接口,全部采用go写。
|
||||
- https://github.com/Philio/GoMySQL 不支持database/sql,自定义接口,全部采用go写。
|
||||
|
||||
接下来的例子我主要以第一个驱动为例(我目前项目中也是采用它来驱动),也推荐大家采用它,主要理由:
|
||||
|
||||
- 这个驱动比较新,维护的比较好
|
||||
- 完全支持database/sql接口
|
||||
- 支持keepalive,保持长连接,虽然[星星](http://www.mikespook.com)fork的mymysql也支持keepalive,但不是线程安全的,这个从底层就支持了keepalive。
|
||||
|
||||
## 示例代码
|
||||
接下来的几个小节里面我们都将采用同一个数据库表结构:数据库test,用户表userinfo,关联用户信息表userdetail。
|
||||
|
||||
CREATE TABLE `userinfo` (
|
||||
`uid` INT(10) NOT NULL AUTO_INCREMENT,
|
||||
`username` VARCHAR(64) NULL DEFAULT NULL,
|
||||
`departname` VARCHAR(64) NULL DEFAULT NULL,
|
||||
`created` DATE NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`uid`)
|
||||
)
|
||||
|
||||
CREATE TABLE `userdetail` (
|
||||
`uid` INT(10) NOT NULL DEFAULT '0',
|
||||
`intro` TEXT NULL,
|
||||
`profile` TEXT NULL,
|
||||
PRIMARY KEY (`uid`)
|
||||
)
|
||||
|
||||
如下示例将示范如何使用database/sql接口对数据库表进行增删改查操作
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
//"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8")
|
||||
checkErr(err)
|
||||
|
||||
//插入数据
|
||||
stmt, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?")
|
||||
checkErr(err)
|
||||
|
||||
res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09")
|
||||
checkErr(err)
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(id)
|
||||
//更新数据
|
||||
stmt, err = db.Prepare("update userinfo set username=? where uid=?")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec("astaxieupdate", id)
|
||||
checkErr(err)
|
||||
|
||||
affect, err := res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
//查询数据
|
||||
rows, err := db.Query("SELECT * FROM userinfo")
|
||||
checkErr(err)
|
||||
|
||||
for rows.Next() {
|
||||
var uid int
|
||||
var username string
|
||||
var department string
|
||||
var created string
|
||||
err = rows.Scan(&uid, &username, &department, &created)
|
||||
checkErr(err)
|
||||
fmt.Println(uid)
|
||||
fmt.Println(username)
|
||||
fmt.Println(department)
|
||||
fmt.Println(created)
|
||||
}
|
||||
|
||||
//删除数据
|
||||
stmt, err = db.Prepare("delete from userinfo where uid=?")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec(id)
|
||||
checkErr(err)
|
||||
|
||||
affect, err = res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
db.Close()
|
||||
|
||||
}
|
||||
|
||||
func checkErr(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
通过上面的代码我们可以看出,Go操作Mysql数据库是很方便的。
|
||||
|
||||
关键的几个函数我解释一下:
|
||||
|
||||
sql.Open()函数用来打开一个注册过的数据库驱动,go-sql-driver中注册了mysql这个数据库驱动,第二个参数是DSN(Data Source Name),它是go-sql-driver定义的一些数据库链接和配置信息。它支持如下格式:
|
||||
|
||||
user@unix(/path/to/socket)/dbname?charset=utf8
|
||||
user:password@tcp(localhost:5555)/dbname?charset=utf8
|
||||
user:password@/dbname
|
||||
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname
|
||||
|
||||
db.Prepare()函数用来返回准备要执行的sql操作,然后返回准备完毕的执行状态。
|
||||
|
||||
db.Query()函数用来直接执行Sql返回Rows结果。
|
||||
|
||||
stmt.Exec()函数用来执行stmt准备好的SQL语句
|
||||
|
||||
我们可以看到我们传入的参数都是=?对应的数据,这样做的方式可以一定程度上防止SQL注入。
|
||||
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [database/sql接口](<05.1.md>)
|
||||
* 下一节: [使用SQLite数据库](<05.3.md>)
|
||||
137
zh/05.2.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
137
zh/05.2.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,137 @@
|
||||
# 5.2 使用MySQL数据库
|
||||
目前Internet上流行的网站构架方式是LAMP,其中的M即MySQL, 作为数据库,MySQL以免费、开源、使用方便为优势成为了很多Web开发的后端数据库存储引擎。
|
||||
|
||||
## MySQL驱动
|
||||
Go中支持MySQL的驱动目前比较多,有如下几种,有些是支持database/sql标准,而有些是采用了自己的实现接口,常用的有如下几种:
|
||||
|
||||
- https://github.com/go-sql-driver/mysql 支持database/sql,全部采用go写。
|
||||
- https://github.com/ziutek/mymysql 支持database/sql,也支持自定义的接口,全部采用go写。
|
||||
- https://github.com/Philio/GoMySQL 不支持database/sql,自定义接口,全部采用go写。
|
||||
|
||||
接下来的例子我主要以第一个驱动为例(我目前项目中也是采用它来驱动),也推荐大家采用它,主要理由:
|
||||
|
||||
- 这个驱动比较新,维护的比较好
|
||||
- 完全支持database/sql接口
|
||||
- 支持keepalive,保持长连接,虽然[星星](http://www.mikespook.com)fork的mymysql也支持keepalive,但不是线程安全的,这个从底层就支持了keepalive。
|
||||
|
||||
## 示例代码
|
||||
接下来的几个小节里面我们都将采用同一个数据库表结构:数据库test,用户表userinfo,关联用户信息表userdetail。
|
||||
|
||||
CREATE TABLE `userinfo` (
|
||||
`uid` INT(10) NOT NULL AUTO_INCREMENT,
|
||||
`username` VARCHAR(64) NULL DEFAULT NULL,
|
||||
`departname` VARCHAR(64) NULL DEFAULT NULL,
|
||||
`created` DATE NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`uid`)
|
||||
)
|
||||
|
||||
CREATE TABLE `userdetail` (
|
||||
`uid` INT(10) NOT NULL DEFAULT '0',
|
||||
`intro` TEXT NULL,
|
||||
`profile` TEXT NULL,
|
||||
PRIMARY KEY (`uid`)
|
||||
)
|
||||
|
||||
如下示例将示范如何使用database/sql接口对数据库表进行增删改查操作
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
//"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8")
|
||||
checkErr(err)
|
||||
|
||||
//插入数据
|
||||
stmt, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?")
|
||||
checkErr(err)
|
||||
|
||||
res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09")
|
||||
checkErr(err)
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(id)
|
||||
//更新数据
|
||||
stmt, err = db.Prepare("update userinfo set username=? where uid=?")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec("astaxieupdate", id)
|
||||
checkErr(err)
|
||||
|
||||
affect, err := res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
//查询数据
|
||||
rows, err := db.Query("SELECT * FROM userinfo")
|
||||
checkErr(err)
|
||||
|
||||
for rows.Next() {
|
||||
var uid int
|
||||
var username string
|
||||
var department string
|
||||
var created string
|
||||
err = rows.Scan(&uid, &username, &department, &created)
|
||||
checkErr(err)
|
||||
fmt.Println(uid)
|
||||
fmt.Println(username)
|
||||
fmt.Println(department)
|
||||
fmt.Println(created)
|
||||
}
|
||||
|
||||
//删除数据
|
||||
stmt, err = db.Prepare("delete from userinfo where uid=?")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec(id)
|
||||
checkErr(err)
|
||||
|
||||
affect, err = res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
db.Close()
|
||||
|
||||
}
|
||||
|
||||
func checkErr(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
通过上面的代码我们可以看出,Go操作Mysql数据库是很方便的。
|
||||
|
||||
关键的几个函数我解释一下:
|
||||
|
||||
sql.Open()函数用来打开一个注册过的数据库驱动,go-sql-driver中注册了mysql这个数据库驱动,第二个参数是DSN(Data Source Name),它是go-sql-driver定义的一些数据库链接和配置信息。它支持如下格式:
|
||||
|
||||
user@unix(/path/to/socket)/dbname?charset=utf8
|
||||
user:password@tcp(localhost:5555)/dbname?charset=utf8
|
||||
user:password@/dbname
|
||||
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname
|
||||
|
||||
db.Prepare()函数用来返回准备要执行的sql操作,然后返回准备完毕的执行状态。
|
||||
|
||||
db.Query()函数用来直接执行Sql返回Rows结果。
|
||||
|
||||
stmt.Exec()函数用来执行stmt准备好的SQL语句
|
||||
|
||||
我们可以看到我们传入的参数都是=?对应的数据,这样做的方式可以一定程度上防止SQL注入。
|
||||
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [database/sql接口](<05.1.md>)
|
||||
* 下一节: [使用SQLite数据库](<05.3.md>)
|
||||
@@ -1,119 +1,119 @@
|
||||
# 5.3 使用SQLite数据库
|
||||
|
||||
SQLite 是一个开源的嵌入式关系数据库,实现自包容、零配置、支持事务的SQL数据库引擎。其特点是高度便携、使用方便、结构紧凑、高效、可靠。 与其他数据库管理系统不同,SQLite 的安装和运行非常简单,在大多数情况下,只要确保SQLite的二进制文件存在即可开始创建、连接和使用数据库。如果您正在寻找一个嵌入式数据库项目或解决方案,SQLite是绝对值得考虑。SQLite可以是说开源的Access。
|
||||
|
||||
## 驱动
|
||||
Go支持sqlite的驱动也比较多,但是好多都是不支持database/sql接口的
|
||||
|
||||
- https://github.com/mattn/go-sqlite3 支持database/sql接口,基于cgo(关于cgo的知识请参看官方文档或者本书后面的章节)写的
|
||||
- https://github.com/feyeleanor/gosqlite3 不支持database/sql接口,基于cgo写的
|
||||
- https://github.com/phf/go-sqlite3 不支持database/sql接口,基于cgo写的
|
||||
|
||||
目前支持database/sql的SQLite数据库驱动只有第一个,我目前也是采用它来开发项目的。采用标准接口有利于以后出现更好的驱动的时候做迁移。
|
||||
|
||||
## 实例代码
|
||||
示例的数据库表结构如下所示,相应的建表SQL:
|
||||
|
||||
CREATE TABLE `userinfo` (
|
||||
`uid` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
`username` VARCHAR(64) NULL,
|
||||
`departname` VARCHAR(64) NULL,
|
||||
`created` DATE NULL
|
||||
);
|
||||
|
||||
CREATE TABLE `userdeatail` (
|
||||
`uid` INT(10) NULL,
|
||||
`intro` TEXT NULL,
|
||||
`profile` TEXT NULL,
|
||||
PRIMARY KEY (`uid`)
|
||||
);
|
||||
|
||||
看下面Go程序是如何操作数据库表数据:增删改查
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("sqlite3", "./foo.db")
|
||||
checkErr(err)
|
||||
|
||||
//插入数据
|
||||
stmt, err := db.Prepare("INSERT INTO userinfo(username, departname, created) values(?,?,?)")
|
||||
checkErr(err)
|
||||
|
||||
res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09")
|
||||
checkErr(err)
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(id)
|
||||
//更新数据
|
||||
stmt, err = db.Prepare("update userinfo set username=? where uid=?")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec("astaxieupdate", id)
|
||||
checkErr(err)
|
||||
|
||||
affect, err := res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
//查询数据
|
||||
rows, err := db.Query("SELECT * FROM userinfo")
|
||||
checkErr(err)
|
||||
|
||||
for rows.Next() {
|
||||
var uid int
|
||||
var username string
|
||||
var department string
|
||||
var created time.Time
|
||||
err = rows.Scan(&uid, &username, &department, &created)
|
||||
checkErr(err)
|
||||
fmt.Println(uid)
|
||||
fmt.Println(username)
|
||||
fmt.Println(department)
|
||||
fmt.Println(created)
|
||||
}
|
||||
|
||||
//删除数据
|
||||
stmt, err = db.Prepare("delete from userinfo where uid=?")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec(id)
|
||||
checkErr(err)
|
||||
|
||||
affect, err = res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
db.Close()
|
||||
|
||||
}
|
||||
|
||||
func checkErr(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
我们可以看到上面的代码和MySQL例子里面的代码几乎是一模一样的,唯一改变的就是导入的驱动改变了,然后调用`sql.Open`是采用了SQLite的方式打开。
|
||||
|
||||
|
||||
>sqlite管理工具:http://sqliteadmin.orbmu2k.de/
|
||||
|
||||
>可以方便的新建数据库管理。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [使用MySQL数据库](<05.2.md>)
|
||||
* 下一节: [使用PostgreSQL数据库](<05.4.md>)
|
||||
# 5.3 使用SQLite数据库
|
||||
|
||||
SQLite 是一个开源的嵌入式关系数据库,实现自包容、零配置、支持事务的SQL数据库引擎。其特点是高度便携、使用方便、结构紧凑、高效、可靠。 与其他数据库管理系统不同,SQLite 的安装和运行非常简单,在大多数情况下,只要确保SQLite的二进制文件存在即可开始创建、连接和使用数据库。如果您正在寻找一个嵌入式数据库项目或解决方案,SQLite是绝对值得考虑。SQLite可以是说开源的Access。
|
||||
|
||||
## 驱动
|
||||
Go支持sqlite的驱动也比较多,但是好多都是不支持database/sql接口的
|
||||
|
||||
- https://github.com/mattn/go-sqlite3 支持database/sql接口,基于cgo(关于cgo的知识请参看官方文档或者本书后面的章节)写的
|
||||
- https://github.com/feyeleanor/gosqlite3 不支持database/sql接口,基于cgo写的
|
||||
- https://github.com/phf/go-sqlite3 不支持database/sql接口,基于cgo写的
|
||||
|
||||
目前支持database/sql的SQLite数据库驱动只有第一个,我目前也是采用它来开发项目的。采用标准接口有利于以后出现更好的驱动的时候做迁移。
|
||||
|
||||
## 实例代码
|
||||
示例的数据库表结构如下所示,相应的建表SQL:
|
||||
|
||||
CREATE TABLE `userinfo` (
|
||||
`uid` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
`username` VARCHAR(64) NULL,
|
||||
`departname` VARCHAR(64) NULL,
|
||||
`created` DATE NULL
|
||||
);
|
||||
|
||||
CREATE TABLE `userdeatail` (
|
||||
`uid` INT(10) NULL,
|
||||
`intro` TEXT NULL,
|
||||
`profile` TEXT NULL,
|
||||
PRIMARY KEY (`uid`)
|
||||
);
|
||||
|
||||
看下面Go程序是如何操作数据库表数据:增删改查
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("sqlite3", "./foo.db")
|
||||
checkErr(err)
|
||||
|
||||
//插入数据
|
||||
stmt, err := db.Prepare("INSERT INTO userinfo(username, departname, created) values(?,?,?)")
|
||||
checkErr(err)
|
||||
|
||||
res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09")
|
||||
checkErr(err)
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(id)
|
||||
//更新数据
|
||||
stmt, err = db.Prepare("update userinfo set username=? where uid=?")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec("astaxieupdate", id)
|
||||
checkErr(err)
|
||||
|
||||
affect, err := res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
//查询数据
|
||||
rows, err := db.Query("SELECT * FROM userinfo")
|
||||
checkErr(err)
|
||||
|
||||
for rows.Next() {
|
||||
var uid int
|
||||
var username string
|
||||
var department string
|
||||
var created time.Time
|
||||
err = rows.Scan(&uid, &username, &department, &created)
|
||||
checkErr(err)
|
||||
fmt.Println(uid)
|
||||
fmt.Println(username)
|
||||
fmt.Println(department)
|
||||
fmt.Println(created)
|
||||
}
|
||||
|
||||
//删除数据
|
||||
stmt, err = db.Prepare("delete from userinfo where uid=?")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec(id)
|
||||
checkErr(err)
|
||||
|
||||
affect, err = res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
db.Close()
|
||||
|
||||
}
|
||||
|
||||
func checkErr(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
我们可以看到上面的代码和MySQL例子里面的代码几乎是一模一样的,唯一改变的就是导入的驱动改变了,然后调用`sql.Open`是采用了SQLite的方式打开。
|
||||
|
||||
|
||||
>sqlite管理工具:http://sqliteadmin.orbmu2k.de/
|
||||
|
||||
>可以方便的新建数据库管理。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [使用MySQL数据库](<05.2.md>)
|
||||
* 下一节: [使用PostgreSQL数据库](<05.4.md>)
|
||||
119
zh/05.3.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
119
zh/05.3.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,119 @@
|
||||
# 5.3 使用SQLite数据库
|
||||
|
||||
SQLite 是一个开源的嵌入式关系数据库,实现自包容、零配置、支持事务的SQL数据库引擎。其特点是高度便携、使用方便、结构紧凑、高效、可靠。 与其他数据库管理系统不同,SQLite 的安装和运行非常简单,在大多数情况下,只要确保SQLite的二进制文件存在即可开始创建、连接和使用数据库。如果您正在寻找一个嵌入式数据库项目或解决方案,SQLite是绝对值得考虑。SQLite可以是说开源的Access。
|
||||
|
||||
## 驱动
|
||||
Go支持sqlite的驱动也比较多,但是好多都是不支持database/sql接口的
|
||||
|
||||
- https://github.com/mattn/go-sqlite3 支持database/sql接口,基于cgo(关于cgo的知识请参看官方文档或者本书后面的章节)写的
|
||||
- https://github.com/feyeleanor/gosqlite3 不支持database/sql接口,基于cgo写的
|
||||
- https://github.com/phf/go-sqlite3 不支持database/sql接口,基于cgo写的
|
||||
|
||||
目前支持database/sql的SQLite数据库驱动只有第一个,我目前也是采用它来开发项目的。采用标准接口有利于以后出现更好的驱动的时候做迁移。
|
||||
|
||||
## 实例代码
|
||||
示例的数据库表结构如下所示,相应的建表SQL:
|
||||
|
||||
CREATE TABLE `userinfo` (
|
||||
`uid` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
`username` VARCHAR(64) NULL,
|
||||
`departname` VARCHAR(64) NULL,
|
||||
`created` DATE NULL
|
||||
);
|
||||
|
||||
CREATE TABLE `userdeatail` (
|
||||
`uid` INT(10) NULL,
|
||||
`intro` TEXT NULL,
|
||||
`profile` TEXT NULL,
|
||||
PRIMARY KEY (`uid`)
|
||||
);
|
||||
|
||||
看下面Go程序是如何操作数据库表数据:增删改查
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("sqlite3", "./foo.db")
|
||||
checkErr(err)
|
||||
|
||||
//插入数据
|
||||
stmt, err := db.Prepare("INSERT INTO userinfo(username, departname, created) values(?,?,?)")
|
||||
checkErr(err)
|
||||
|
||||
res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09")
|
||||
checkErr(err)
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(id)
|
||||
//更新数据
|
||||
stmt, err = db.Prepare("update userinfo set username=? where uid=?")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec("astaxieupdate", id)
|
||||
checkErr(err)
|
||||
|
||||
affect, err := res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
//查询数据
|
||||
rows, err := db.Query("SELECT * FROM userinfo")
|
||||
checkErr(err)
|
||||
|
||||
for rows.Next() {
|
||||
var uid int
|
||||
var username string
|
||||
var department string
|
||||
var created time.Time
|
||||
err = rows.Scan(&uid, &username, &department, &created)
|
||||
checkErr(err)
|
||||
fmt.Println(uid)
|
||||
fmt.Println(username)
|
||||
fmt.Println(department)
|
||||
fmt.Println(created)
|
||||
}
|
||||
|
||||
//删除数据
|
||||
stmt, err = db.Prepare("delete from userinfo where uid=?")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec(id)
|
||||
checkErr(err)
|
||||
|
||||
affect, err = res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
db.Close()
|
||||
|
||||
}
|
||||
|
||||
func checkErr(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
我们可以看到上面的代码和MySQL例子里面的代码几乎是一模一样的,唯一改变的就是导入的驱动改变了,然后调用`sql.Open`是采用了SQLite的方式打开。
|
||||
|
||||
|
||||
>sqlite管理工具:http://sqliteadmin.orbmu2k.de/
|
||||
|
||||
>可以方便的新建数据库管理。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [使用MySQL数据库](<05.2.md>)
|
||||
* 下一节: [使用PostgreSQL数据库](<05.4.md>)
|
||||
@@ -1,124 +1,124 @@
|
||||
# 5.4 使用PostgreSQL数据库
|
||||
|
||||
PostgreSQL 是一个自由的对象-关系数据库服务器(数据库管理系统),它在灵活的 BSD-风格许可证下发行。它提供了相对其他开放源代码数据库系统(比如 MySQL 和 Firebird),和对专有系统比如 Oracle、Sybase、IBM 的 DB2 和 Microsoft SQL Server的一种选择。
|
||||
|
||||
PostgreSQL和MySQL比较,它更加庞大一点,因为它是用来替代Oracle而设计的。所以在企业应用中采用PostgreSQL是一个明智的选择。
|
||||
|
||||
MySQL被Oracle收购之后正在逐步的封闭(自MySQL 5.5.31以后的所有版本将不再遵循GPL协议),鉴于此,将来我们也许会选择PostgreSQL而不是MySQL作为项目的后端数据库。
|
||||
|
||||
## 驱动
|
||||
Go实现的支持PostgreSQL的驱动也很多,因为国外很多人在开发中使用了这个数据库。
|
||||
|
||||
- https://github.com/lib/pq 支持database/sql驱动,纯Go写的
|
||||
- https://github.com/jbarham/gopgsqldriver 支持database/sql驱动,纯Go写的
|
||||
- https://github.com/lxn/go-pgsql 支持database/sql驱动,纯Go写的
|
||||
|
||||
在下面的示例中我采用了第一个驱动,因为它目前使用的人最多,在github上也比较活跃。
|
||||
|
||||
## 实例代码
|
||||
数据库建表语句:
|
||||
|
||||
CREATE TABLE userinfo
|
||||
(
|
||||
uid serial NOT NULL,
|
||||
username character varying(100) NOT NULL,
|
||||
departname character varying(500) NOT NULL,
|
||||
Created date,
|
||||
CONSTRAINT userinfo_pkey PRIMARY KEY (uid)
|
||||
)
|
||||
WITH (OIDS=FALSE);
|
||||
|
||||
CREATE TABLE userdeatail
|
||||
(
|
||||
uid integer,
|
||||
intro character varying(100),
|
||||
profile character varying(100)
|
||||
)
|
||||
WITH(OIDS=FALSE);
|
||||
|
||||
看下面这个Go如何操作数据库表数据:增删改查
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
_ "https://github.com/lib/pq"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("postgres", "user=astaxie password=astaxie dbname=test sslmode=disable")
|
||||
checkErr(err)
|
||||
|
||||
//插入数据
|
||||
stmt, err := db.Prepare("INSERT INTO userinfo(username,departname,created) VALUES($1,$2,$3) RETURNING uid")
|
||||
checkErr(err)
|
||||
|
||||
res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09")
|
||||
checkErr(err)
|
||||
|
||||
//pg不支持这个函数,因为他没有类似MySQL的自增ID
|
||||
id, err := res.LastInsertId()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(id)
|
||||
|
||||
//更新数据
|
||||
stmt, err = db.Prepare("update userinfo set username=$1 where uid=$2")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec("astaxieupdate", 1)
|
||||
checkErr(err)
|
||||
|
||||
affect, err := res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
//查询数据
|
||||
rows, err := db.Query("SELECT * FROM userinfo")
|
||||
checkErr(err)
|
||||
|
||||
for rows.Next() {
|
||||
var uid int
|
||||
var username string
|
||||
var department string
|
||||
var created string
|
||||
err = rows.Scan(&uid, &username, &department, &created)
|
||||
checkErr(err)
|
||||
fmt.Println(uid)
|
||||
fmt.Println(username)
|
||||
fmt.Println(department)
|
||||
fmt.Println(created)
|
||||
}
|
||||
|
||||
//删除数据
|
||||
stmt, err = db.Prepare("delete from userinfo where uid=$1")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec(1)
|
||||
checkErr(err)
|
||||
|
||||
affect, err = res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
db.Close()
|
||||
|
||||
}
|
||||
|
||||
func checkErr(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
从上面的代码我们可以看到,PostgreSQL是通过`$1`,`$2`这种方式来指定要传递的参数,而不是MySQL中的`?`,另外在sql.Open中的dsn信息的格式也与MySQL的驱动中的dsn格式不一样,所以在使用时请注意它们的差异。
|
||||
|
||||
还有pg不支持LastInsertId函数,因为PostgreSQL内部没有实现类似MySQL的自增ID返回,其他的代码几乎是一模一样。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [使用SQLite数据库](<05.3.md>)
|
||||
* 下一节: [使用beedb库进行ORM开发](<05.5.md>)
|
||||
# 5.4 使用PostgreSQL数据库
|
||||
|
||||
PostgreSQL 是一个自由的对象-关系数据库服务器(数据库管理系统),它在灵活的 BSD-风格许可证下发行。它提供了相对其他开放源代码数据库系统(比如 MySQL 和 Firebird),和对专有系统比如 Oracle、Sybase、IBM 的 DB2 和 Microsoft SQL Server的一种选择。
|
||||
|
||||
PostgreSQL和MySQL比较,它更加庞大一点,因为它是用来替代Oracle而设计的。所以在企业应用中采用PostgreSQL是一个明智的选择。
|
||||
|
||||
MySQL被Oracle收购之后正在逐步的封闭(自MySQL 5.5.31以后的所有版本将不再遵循GPL协议),鉴于此,将来我们也许会选择PostgreSQL而不是MySQL作为项目的后端数据库。
|
||||
|
||||
## 驱动
|
||||
Go实现的支持PostgreSQL的驱动也很多,因为国外很多人在开发中使用了这个数据库。
|
||||
|
||||
- https://github.com/lib/pq 支持database/sql驱动,纯Go写的
|
||||
- https://github.com/jbarham/gopgsqldriver 支持database/sql驱动,纯Go写的
|
||||
- https://github.com/lxn/go-pgsql 支持database/sql驱动,纯Go写的
|
||||
|
||||
在下面的示例中我采用了第一个驱动,因为它目前使用的人最多,在github上也比较活跃。
|
||||
|
||||
## 实例代码
|
||||
数据库建表语句:
|
||||
|
||||
CREATE TABLE userinfo
|
||||
(
|
||||
uid serial NOT NULL,
|
||||
username character varying(100) NOT NULL,
|
||||
departname character varying(500) NOT NULL,
|
||||
Created date,
|
||||
CONSTRAINT userinfo_pkey PRIMARY KEY (uid)
|
||||
)
|
||||
WITH (OIDS=FALSE);
|
||||
|
||||
CREATE TABLE userdeatail
|
||||
(
|
||||
uid integer,
|
||||
intro character varying(100),
|
||||
profile character varying(100)
|
||||
)
|
||||
WITH(OIDS=FALSE);
|
||||
|
||||
看下面这个Go如何操作数据库表数据:增删改查
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
_ "https://github.com/lib/pq"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("postgres", "user=astaxie password=astaxie dbname=test sslmode=disable")
|
||||
checkErr(err)
|
||||
|
||||
//插入数据
|
||||
stmt, err := db.Prepare("INSERT INTO userinfo(username,departname,created) VALUES($1,$2,$3) RETURNING uid")
|
||||
checkErr(err)
|
||||
|
||||
res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09")
|
||||
checkErr(err)
|
||||
|
||||
//pg不支持这个函数,因为他没有类似MySQL的自增ID
|
||||
id, err := res.LastInsertId()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(id)
|
||||
|
||||
//更新数据
|
||||
stmt, err = db.Prepare("update userinfo set username=$1 where uid=$2")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec("astaxieupdate", 1)
|
||||
checkErr(err)
|
||||
|
||||
affect, err := res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
//查询数据
|
||||
rows, err := db.Query("SELECT * FROM userinfo")
|
||||
checkErr(err)
|
||||
|
||||
for rows.Next() {
|
||||
var uid int
|
||||
var username string
|
||||
var department string
|
||||
var created string
|
||||
err = rows.Scan(&uid, &username, &department, &created)
|
||||
checkErr(err)
|
||||
fmt.Println(uid)
|
||||
fmt.Println(username)
|
||||
fmt.Println(department)
|
||||
fmt.Println(created)
|
||||
}
|
||||
|
||||
//删除数据
|
||||
stmt, err = db.Prepare("delete from userinfo where uid=$1")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec(1)
|
||||
checkErr(err)
|
||||
|
||||
affect, err = res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
db.Close()
|
||||
|
||||
}
|
||||
|
||||
func checkErr(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
从上面的代码我们可以看到,PostgreSQL是通过`$1`,`$2`这种方式来指定要传递的参数,而不是MySQL中的`?`,另外在sql.Open中的dsn信息的格式也与MySQL的驱动中的dsn格式不一样,所以在使用时请注意它们的差异。
|
||||
|
||||
还有pg不支持LastInsertId函数,因为PostgreSQL内部没有实现类似MySQL的自增ID返回,其他的代码几乎是一模一样。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [使用SQLite数据库](<05.3.md>)
|
||||
* 下一节: [使用beedb库进行ORM开发](<05.5.md>)
|
||||
124
zh/05.4.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
124
zh/05.4.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,124 @@
|
||||
# 5.4 使用PostgreSQL数据库
|
||||
|
||||
PostgreSQL 是一个自由的对象-关系数据库服务器(数据库管理系统),它在灵活的 BSD-风格许可证下发行。它提供了相对其他开放源代码数据库系统(比如 MySQL 和 Firebird),和对专有系统比如 Oracle、Sybase、IBM 的 DB2 和 Microsoft SQL Server的一种选择。
|
||||
|
||||
PostgreSQL和MySQL比较,它更加庞大一点,因为它是用来替代Oracle而设计的。所以在企业应用中采用PostgreSQL是一个明智的选择。
|
||||
|
||||
MySQL被Oracle收购之后正在逐步的封闭(自MySQL 5.5.31以后的所有版本将不再遵循GPL协议),鉴于此,将来我们也许会选择PostgreSQL而不是MySQL作为项目的后端数据库。
|
||||
|
||||
## 驱动
|
||||
Go实现的支持PostgreSQL的驱动也很多,因为国外很多人在开发中使用了这个数据库。
|
||||
|
||||
- https://github.com/lib/pq 支持database/sql驱动,纯Go写的
|
||||
- https://github.com/jbarham/gopgsqldriver 支持database/sql驱动,纯Go写的
|
||||
- https://github.com/lxn/go-pgsql 支持database/sql驱动,纯Go写的
|
||||
|
||||
在下面的示例中我采用了第一个驱动,因为它目前使用的人最多,在github上也比较活跃。
|
||||
|
||||
## 实例代码
|
||||
数据库建表语句:
|
||||
|
||||
CREATE TABLE userinfo
|
||||
(
|
||||
uid serial NOT NULL,
|
||||
username character varying(100) NOT NULL,
|
||||
departname character varying(500) NOT NULL,
|
||||
Created date,
|
||||
CONSTRAINT userinfo_pkey PRIMARY KEY (uid)
|
||||
)
|
||||
WITH (OIDS=FALSE);
|
||||
|
||||
CREATE TABLE userdeatail
|
||||
(
|
||||
uid integer,
|
||||
intro character varying(100),
|
||||
profile character varying(100)
|
||||
)
|
||||
WITH(OIDS=FALSE);
|
||||
|
||||
看下面这个Go如何操作数据库表数据:增删改查
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
_ "https://github.com/lib/pq"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("postgres", "user=astaxie password=astaxie dbname=test sslmode=disable")
|
||||
checkErr(err)
|
||||
|
||||
//插入数据
|
||||
stmt, err := db.Prepare("INSERT INTO userinfo(username,departname,created) VALUES($1,$2,$3) RETURNING uid")
|
||||
checkErr(err)
|
||||
|
||||
res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09")
|
||||
checkErr(err)
|
||||
|
||||
//pg不支持这个函数,因为他没有类似MySQL的自增ID
|
||||
id, err := res.LastInsertId()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(id)
|
||||
|
||||
//更新数据
|
||||
stmt, err = db.Prepare("update userinfo set username=$1 where uid=$2")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec("astaxieupdate", 1)
|
||||
checkErr(err)
|
||||
|
||||
affect, err := res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
//查询数据
|
||||
rows, err := db.Query("SELECT * FROM userinfo")
|
||||
checkErr(err)
|
||||
|
||||
for rows.Next() {
|
||||
var uid int
|
||||
var username string
|
||||
var department string
|
||||
var created string
|
||||
err = rows.Scan(&uid, &username, &department, &created)
|
||||
checkErr(err)
|
||||
fmt.Println(uid)
|
||||
fmt.Println(username)
|
||||
fmt.Println(department)
|
||||
fmt.Println(created)
|
||||
}
|
||||
|
||||
//删除数据
|
||||
stmt, err = db.Prepare("delete from userinfo where uid=$1")
|
||||
checkErr(err)
|
||||
|
||||
res, err = stmt.Exec(1)
|
||||
checkErr(err)
|
||||
|
||||
affect, err = res.RowsAffected()
|
||||
checkErr(err)
|
||||
|
||||
fmt.Println(affect)
|
||||
|
||||
db.Close()
|
||||
|
||||
}
|
||||
|
||||
func checkErr(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
从上面的代码我们可以看到,PostgreSQL是通过`$1`,`$2`这种方式来指定要传递的参数,而不是MySQL中的`?`,另外在sql.Open中的dsn信息的格式也与MySQL的驱动中的dsn格式不一样,所以在使用时请注意它们的差异。
|
||||
|
||||
还有pg不支持LastInsertId函数,因为PostgreSQL内部没有实现类似MySQL的自增ID返回,其他的代码几乎是一模一样。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [使用SQLite数据库](<05.3.md>)
|
||||
* 下一节: [使用beedb库进行ORM开发](<05.5.md>)
|
||||
@@ -1,249 +1,249 @@
|
||||
# 5.5 使用beedb库进行ORM开发
|
||||
beedb是我开发的一个Go进行ORM操作的库,它采用了Go style方式对数据库进行操作,实现了struct到数据表记录的映射。beedb是一个十分轻量级的Go ORM框架,开发这个库的本意降低复杂的ORM学习曲线,尽可能在ORM的运行效率和功能之间寻求一个平衡,beedb是目前开源的Go ORM框架中实现比较完整的一个库,而且运行效率相当不错,功能也基本能满足需求。但是目前还不支持关系关联,这个是接下来版本升级的重点。
|
||||
|
||||
beedb是支持database/sql标准接口的ORM库,所以理论上来说,只要数据库驱动支持database/sql接口就可以无缝的接入beedb。目前我测试过的驱动包括下面几个:
|
||||
|
||||
Mysql:github.com/ziutek/mymysql/godrv[*]
|
||||
|
||||
Mysql:code.google.com/p/go-mysql-driver[*]
|
||||
|
||||
PostgreSQL:github.com/bmizerany/pq[*]
|
||||
|
||||
SQLite:github.com/mattn/go-sqlite3[*]
|
||||
|
||||
MS ADODB: github.com/mattn/go-adodb[*]
|
||||
|
||||
ODBC: bitbucket.org/miquella/mgodbc[*]
|
||||
|
||||
## 安装
|
||||
|
||||
beedb支持go get方式安装,是完全按照Go Style的方式来实现的。
|
||||
|
||||
go get github.com/astaxie/beedb
|
||||
|
||||
## 如何初始化
|
||||
首先你需要import相应的数据库驱动包、database/sql标准接口包以及beedb包,如下所示:
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/astaxie/beedb"
|
||||
_ "github.com/ziutek/mymysql/godrv"
|
||||
)
|
||||
|
||||
导入必须的package之后,我们需要打开到数据库的链接,然后创建一个beedb对象(以MySQL为例),如下所示
|
||||
|
||||
db, err := sql.Open("mymysql", "test/xiemengjun/123456")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
orm := beedb.New(db)
|
||||
|
||||
beedb的New函数实际上应该有两个参数,第一个参数标准接口的db,第二个参数是使用的数据库引擎,如果你使用的数据库引擎是MySQL/Sqlite,那么第二个参数都可以省略。
|
||||
|
||||
如果你使用的数据库是SQLServer,那么初始化需要:
|
||||
|
||||
orm = beedb.New(db, "mssql")
|
||||
|
||||
如果你使用了PostgreSQL,那么初始化需要:
|
||||
|
||||
orm = beedb.New(db, "pg")
|
||||
|
||||
目前beedb支持打印调试,你可以通过如下的代码实现调试
|
||||
|
||||
beedb.OnDebug=true
|
||||
|
||||
接下来我们的例子采用前面的数据库表Userinfo,现在我们建立相应的struct
|
||||
|
||||
type Userinfo struct {
|
||||
Uid int `PK` //如果表的主键不是id,那么需要加上pk注释,显式的说这个字段是主键
|
||||
Username string
|
||||
Departname string
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
>注意一点,beedb针对驼峰命名会自动帮你转化成下划线字段,例如你定义了Struct名字为`UserInfo`,那么转化成底层实现的时候是`user_info`,字段命名也遵循该规则。
|
||||
|
||||
## 插入数据
|
||||
下面的代码演示了如何插入一条记录,可以看到我们操作的是struct对象,而不是原生的sql语句,最后通过调用Save接口将数据保存到数据库。
|
||||
|
||||
var saveone Userinfo
|
||||
saveone.Username = "Test Add User"
|
||||
saveone.Departname = "Test Add Departname"
|
||||
saveone.Created = time.Now()
|
||||
orm.Save(&saveone)
|
||||
|
||||
我们看到插入之后`saveone.Uid`就是插入成功之后的自增ID。Save接口会自动帮你存进去。
|
||||
|
||||
beedb接口提供了另外一种插入的方式,map数据插入。
|
||||
|
||||
add := make(map[string]interface{})
|
||||
add["username"] = "astaxie"
|
||||
add["departname"] = "cloud develop"
|
||||
add["created"] = "2012-12-02"
|
||||
orm.SetTable("userinfo").Insert(add)
|
||||
|
||||
插入多条数据
|
||||
|
||||
addslice := make([]map[string]interface{}, 0)
|
||||
add:=make(map[string]interface{})
|
||||
add2:=make(map[string]interface{})
|
||||
add["username"] = "astaxie"
|
||||
add["departname"] = "cloud develop"
|
||||
add["created"] = "2012-12-02"
|
||||
add2["username"] = "astaxie2"
|
||||
add2["departname"] = "cloud develop2"
|
||||
add2["created"] = "2012-12-02"
|
||||
addslice =append(addslice, add, add2)
|
||||
orm.SetTable("userinfo").InsertBatch(addslice)
|
||||
|
||||
上面的操作方式有点类似链式查询,熟悉jquery的同学应该会觉得很亲切,每次调用的method都会返回原orm对象,以便可以继续调用该对象上的其他method。
|
||||
|
||||
上面我们调用的SetTable函数是显式的告诉ORM,我要执行的这个map对应的数据库表是`userinfo`。
|
||||
|
||||
## 更新数据
|
||||
继续上面的例子来演示更新操作,现在saveone的主键已经有值了,此时调用save接口,beedb内部会自动调用update以进行数据的更新而非插入操作。
|
||||
|
||||
saveone.Username = "Update Username"
|
||||
saveone.Departname = "Update Departname"
|
||||
saveone.Created = time.Now()
|
||||
orm.Save(&saveone) //现在saveone有了主键值,就执行更新操作
|
||||
|
||||
更新数据也支持直接使用map操作
|
||||
|
||||
t := make(map[string]interface{})
|
||||
t["username"] = "astaxie"
|
||||
orm.SetTable("userinfo").SetPK("uid").Where(2).Update(t)
|
||||
|
||||
这里我们调用了几个beedb的函数
|
||||
|
||||
SetPK:显式的告诉ORM,数据库表`userinfo`的主键是`uid`。
|
||||
|
||||
Where:用来设置条件,支持多个参数,第一个参数如果为整数,相当于调用了Where("主键=?",值)。
|
||||
Updata函数接收map类型的数据,执行更新数据。
|
||||
|
||||
## 查询数据
|
||||
beedb的查询接口比较灵活,具体使用请看下面的例子
|
||||
|
||||
例子1,根据主键获取数据:
|
||||
|
||||
var user Userinfo
|
||||
//Where接受两个参数,支持整形参数
|
||||
orm.Where("uid=?", 27).Find(&user)
|
||||
|
||||
|
||||
例子2:
|
||||
|
||||
var user2 Userinfo
|
||||
orm.Where(3).Find(&user2) // 这是上面版本的缩写版,可以省略主键
|
||||
|
||||
例子3,不是主键类型的的条件:
|
||||
|
||||
var user3 Userinfo
|
||||
//Where接受两个参数,支持字符型的参数
|
||||
orm.Where("name = ?", "john").Find(&user3)
|
||||
例子4,更加复杂的条件:
|
||||
|
||||
var user4 Userinfo
|
||||
//Where支持三个参数
|
||||
orm.Where("name = ? and age < ?", "john", 88).Find(&user4)
|
||||
|
||||
|
||||
可以通过如下接口获取多条数据,请看示例
|
||||
|
||||
例子1,根据条件id>3,获取20位置开始的10条数据的数据
|
||||
|
||||
var allusers []Userinfo
|
||||
err := orm.Where("id > ?", "3").Limit(10,20).FindAll(&allusers)
|
||||
|
||||
例子2,省略limit第二个参数,默认从0开始,获取10条数据
|
||||
|
||||
var tenusers []Userinfo
|
||||
err := orm.Where("id > ?", "3").Limit(10).FindAll(&tenusers)
|
||||
|
||||
例子3,获取全部数据
|
||||
|
||||
var everyone []Userinfo
|
||||
err := orm.OrderBy("uid desc,username asc").FindAll(&everyone)
|
||||
|
||||
上面这些里面里面我们看到一个函数Limit,他是用来控制查询结构条数的。
|
||||
|
||||
Limit:支持两个参数,第一个参数表示查询的条数,第二个参数表示读取数据的起始位置,默认为0。
|
||||
|
||||
OrderBy:这个函数用来进行查询排序,参数是需要排序的条件。
|
||||
|
||||
上面这些例子都是将获取的的数据直接映射成struct对象,如果我们只是想获取一些数据到map,以下方式可以实现:
|
||||
|
||||
a, _ := orm.SetTable("userinfo").SetPK("uid").Where(2).Select("uid,username").FindMap()
|
||||
|
||||
上面和这个例子里面又出现了一个新的接口函数Select,这个函数用来指定需要查询多少个字段。默认为全部字段`*`。
|
||||
|
||||
FindMap()函数返回的是`[]map[string][]byte`类型,所以你需要自己作类型转换。
|
||||
|
||||
## 删除数据
|
||||
beedb提供了丰富的删除数据接口,请看下面的例子
|
||||
|
||||
例子1,删除单条数据
|
||||
|
||||
//saveone就是上面示例中的那个saveone
|
||||
orm.Delete(&saveone)
|
||||
|
||||
例子2,删除多条数据
|
||||
|
||||
//alluser就是上面定义的获取多条数据的slice
|
||||
orm.DeleteAll(&alluser)
|
||||
|
||||
例子3,根据sql删除数据
|
||||
|
||||
orm.SetTable("userinfo").Where("uid>?", 3).DeleteRow()
|
||||
|
||||
|
||||
## 关联查询
|
||||
目前beedb还不支持struct的关联关系,但是有些应用却需要用到连接查询,所以现在beedb提供了一个简陋的实现方案:
|
||||
|
||||
a, _ := orm.SetTable("userinfo").Join("LEFT", "userdeatail", "userinfo.uid=userdeatail.uid").Where("userinfo.uid=?", 1).Select("userinfo.uid,userinfo.username,userdeatail.profile").FindMap()
|
||||
|
||||
上面代码中我们看到了一个新的接口Join函数,这个函数带有三个参数
|
||||
|
||||
- 第一个参数可以是:INNER, LEFT, OUTER, CROSS等
|
||||
- 第二个参数表示连接的表
|
||||
- 第三个参数表示连接的条件
|
||||
|
||||
|
||||
## Group By和Having
|
||||
针对有些应用需要用到group by和having的功能,beedb也提供了一个简陋的实现
|
||||
|
||||
a, _ := orm.SetTable("userinfo").GroupBy("username").Having("username='astaxie'").FindMap()
|
||||
|
||||
上面的代码中出现了两个新接口函数
|
||||
|
||||
GroupBy:用来指定进行groupby的字段
|
||||
|
||||
Having:用来指定having执行的时候的条件
|
||||
|
||||
## 进一步的发展
|
||||
目前beedb已经获得了很多来自国内外用户的反馈,我目前也正在考虑重构,接下来会在几个方面进行改进
|
||||
|
||||
- 实现interface设计,类似databse/sql/driver的设计,设计beedb的接口,然后去实现相应数据库的CRUD操作
|
||||
- 实现关联数据库设计,支持一对一,一对多,多对多的实现,示例代码如下:
|
||||
|
||||
|
||||
type Profile struct{
|
||||
Nickname string
|
||||
Mobile string
|
||||
}
|
||||
|
||||
type Userinfo struct {
|
||||
Uid int `PK`
|
||||
Username string
|
||||
Departname string
|
||||
Created time.Time
|
||||
Profile `HasOne`
|
||||
}
|
||||
|
||||
- 自动建库建表建索引
|
||||
- 实现连接池的实现,采用goroutine
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [使用PostgreSQL数据库](<05.4.md>)
|
||||
* 下一节: [NOSQL数据库操作](<05.6.md>)
|
||||
# 5.5 使用beedb库进行ORM开发
|
||||
beedb是我开发的一个Go进行ORM操作的库,它采用了Go style方式对数据库进行操作,实现了struct到数据表记录的映射。beedb是一个十分轻量级的Go ORM框架,开发这个库的本意降低复杂的ORM学习曲线,尽可能在ORM的运行效率和功能之间寻求一个平衡,beedb是目前开源的Go ORM框架中实现比较完整的一个库,而且运行效率相当不错,功能也基本能满足需求。但是目前还不支持关系关联,这个是接下来版本升级的重点。
|
||||
|
||||
beedb是支持database/sql标准接口的ORM库,所以理论上来说,只要数据库驱动支持database/sql接口就可以无缝的接入beedb。目前我测试过的驱动包括下面几个:
|
||||
|
||||
Mysql:github.com/ziutek/mymysql/godrv[*]
|
||||
|
||||
Mysql:code.google.com/p/go-mysql-driver[*]
|
||||
|
||||
PostgreSQL:github.com/bmizerany/pq[*]
|
||||
|
||||
SQLite:github.com/mattn/go-sqlite3[*]
|
||||
|
||||
MS ADODB: github.com/mattn/go-adodb[*]
|
||||
|
||||
ODBC: bitbucket.org/miquella/mgodbc[*]
|
||||
|
||||
## 安装
|
||||
|
||||
beedb支持go get方式安装,是完全按照Go Style的方式来实现的。
|
||||
|
||||
go get github.com/astaxie/beedb
|
||||
|
||||
## 如何初始化
|
||||
首先你需要import相应的数据库驱动包、database/sql标准接口包以及beedb包,如下所示:
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/astaxie/beedb"
|
||||
_ "github.com/ziutek/mymysql/godrv"
|
||||
)
|
||||
|
||||
导入必须的package之后,我们需要打开到数据库的链接,然后创建一个beedb对象(以MySQL为例),如下所示
|
||||
|
||||
db, err := sql.Open("mymysql", "test/xiemengjun/123456")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
orm := beedb.New(db)
|
||||
|
||||
beedb的New函数实际上应该有两个参数,第一个参数标准接口的db,第二个参数是使用的数据库引擎,如果你使用的数据库引擎是MySQL/Sqlite,那么第二个参数都可以省略。
|
||||
|
||||
如果你使用的数据库是SQLServer,那么初始化需要:
|
||||
|
||||
orm = beedb.New(db, "mssql")
|
||||
|
||||
如果你使用了PostgreSQL,那么初始化需要:
|
||||
|
||||
orm = beedb.New(db, "pg")
|
||||
|
||||
目前beedb支持打印调试,你可以通过如下的代码实现调试
|
||||
|
||||
beedb.OnDebug=true
|
||||
|
||||
接下来我们的例子采用前面的数据库表Userinfo,现在我们建立相应的struct
|
||||
|
||||
type Userinfo struct {
|
||||
Uid int `PK` //如果表的主键不是id,那么需要加上pk注释,显式的说这个字段是主键
|
||||
Username string
|
||||
Departname string
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
>注意一点,beedb针对驼峰命名会自动帮你转化成下划线字段,例如你定义了Struct名字为`UserInfo`,那么转化成底层实现的时候是`user_info`,字段命名也遵循该规则。
|
||||
|
||||
## 插入数据
|
||||
下面的代码演示了如何插入一条记录,可以看到我们操作的是struct对象,而不是原生的sql语句,最后通过调用Save接口将数据保存到数据库。
|
||||
|
||||
var saveone Userinfo
|
||||
saveone.Username = "Test Add User"
|
||||
saveone.Departname = "Test Add Departname"
|
||||
saveone.Created = time.Now()
|
||||
orm.Save(&saveone)
|
||||
|
||||
我们看到插入之后`saveone.Uid`就是插入成功之后的自增ID。Save接口会自动帮你存进去。
|
||||
|
||||
beedb接口提供了另外一种插入的方式,map数据插入。
|
||||
|
||||
add := make(map[string]interface{})
|
||||
add["username"] = "astaxie"
|
||||
add["departname"] = "cloud develop"
|
||||
add["created"] = "2012-12-02"
|
||||
orm.SetTable("userinfo").Insert(add)
|
||||
|
||||
插入多条数据
|
||||
|
||||
addslice := make([]map[string]interface{}, 0)
|
||||
add:=make(map[string]interface{})
|
||||
add2:=make(map[string]interface{})
|
||||
add["username"] = "astaxie"
|
||||
add["departname"] = "cloud develop"
|
||||
add["created"] = "2012-12-02"
|
||||
add2["username"] = "astaxie2"
|
||||
add2["departname"] = "cloud develop2"
|
||||
add2["created"] = "2012-12-02"
|
||||
addslice =append(addslice, add, add2)
|
||||
orm.SetTable("userinfo").InsertBatch(addslice)
|
||||
|
||||
上面的操作方式有点类似链式查询,熟悉jquery的同学应该会觉得很亲切,每次调用的method都会返回原orm对象,以便可以继续调用该对象上的其他method。
|
||||
|
||||
上面我们调用的SetTable函数是显式的告诉ORM,我要执行的这个map对应的数据库表是`userinfo`。
|
||||
|
||||
## 更新数据
|
||||
继续上面的例子来演示更新操作,现在saveone的主键已经有值了,此时调用save接口,beedb内部会自动调用update以进行数据的更新而非插入操作。
|
||||
|
||||
saveone.Username = "Update Username"
|
||||
saveone.Departname = "Update Departname"
|
||||
saveone.Created = time.Now()
|
||||
orm.Save(&saveone) //现在saveone有了主键值,就执行更新操作
|
||||
|
||||
更新数据也支持直接使用map操作
|
||||
|
||||
t := make(map[string]interface{})
|
||||
t["username"] = "astaxie"
|
||||
orm.SetTable("userinfo").SetPK("uid").Where(2).Update(t)
|
||||
|
||||
这里我们调用了几个beedb的函数
|
||||
|
||||
SetPK:显式的告诉ORM,数据库表`userinfo`的主键是`uid`。
|
||||
|
||||
Where:用来设置条件,支持多个参数,第一个参数如果为整数,相当于调用了Where("主键=?",值)。
|
||||
Updata函数接收map类型的数据,执行更新数据。
|
||||
|
||||
## 查询数据
|
||||
beedb的查询接口比较灵活,具体使用请看下面的例子
|
||||
|
||||
例子1,根据主键获取数据:
|
||||
|
||||
var user Userinfo
|
||||
//Where接受两个参数,支持整形参数
|
||||
orm.Where("uid=?", 27).Find(&user)
|
||||
|
||||
|
||||
例子2:
|
||||
|
||||
var user2 Userinfo
|
||||
orm.Where(3).Find(&user2) // 这是上面版本的缩写版,可以省略主键
|
||||
|
||||
例子3,不是主键类型的的条件:
|
||||
|
||||
var user3 Userinfo
|
||||
//Where接受两个参数,支持字符型的参数
|
||||
orm.Where("name = ?", "john").Find(&user3)
|
||||
例子4,更加复杂的条件:
|
||||
|
||||
var user4 Userinfo
|
||||
//Where支持三个参数
|
||||
orm.Where("name = ? and age < ?", "john", 88).Find(&user4)
|
||||
|
||||
|
||||
可以通过如下接口获取多条数据,请看示例
|
||||
|
||||
例子1,根据条件id>3,获取20位置开始的10条数据的数据
|
||||
|
||||
var allusers []Userinfo
|
||||
err := orm.Where("id > ?", "3").Limit(10,20).FindAll(&allusers)
|
||||
|
||||
例子2,省略limit第二个参数,默认从0开始,获取10条数据
|
||||
|
||||
var tenusers []Userinfo
|
||||
err := orm.Where("id > ?", "3").Limit(10).FindAll(&tenusers)
|
||||
|
||||
例子3,获取全部数据
|
||||
|
||||
var everyone []Userinfo
|
||||
err := orm.OrderBy("uid desc,username asc").FindAll(&everyone)
|
||||
|
||||
上面这些里面里面我们看到一个函数Limit,他是用来控制查询结构条数的。
|
||||
|
||||
Limit:支持两个参数,第一个参数表示查询的条数,第二个参数表示读取数据的起始位置,默认为0。
|
||||
|
||||
OrderBy:这个函数用来进行查询排序,参数是需要排序的条件。
|
||||
|
||||
上面这些例子都是将获取的的数据直接映射成struct对象,如果我们只是想获取一些数据到map,以下方式可以实现:
|
||||
|
||||
a, _ := orm.SetTable("userinfo").SetPK("uid").Where(2).Select("uid,username").FindMap()
|
||||
|
||||
上面和这个例子里面又出现了一个新的接口函数Select,这个函数用来指定需要查询多少个字段。默认为全部字段`*`。
|
||||
|
||||
FindMap()函数返回的是`[]map[string][]byte`类型,所以你需要自己作类型转换。
|
||||
|
||||
## 删除数据
|
||||
beedb提供了丰富的删除数据接口,请看下面的例子
|
||||
|
||||
例子1,删除单条数据
|
||||
|
||||
//saveone就是上面示例中的那个saveone
|
||||
orm.Delete(&saveone)
|
||||
|
||||
例子2,删除多条数据
|
||||
|
||||
//alluser就是上面定义的获取多条数据的slice
|
||||
orm.DeleteAll(&alluser)
|
||||
|
||||
例子3,根据sql删除数据
|
||||
|
||||
orm.SetTable("userinfo").Where("uid>?", 3).DeleteRow()
|
||||
|
||||
|
||||
## 关联查询
|
||||
目前beedb还不支持struct的关联关系,但是有些应用却需要用到连接查询,所以现在beedb提供了一个简陋的实现方案:
|
||||
|
||||
a, _ := orm.SetTable("userinfo").Join("LEFT", "userdeatail", "userinfo.uid=userdeatail.uid").Where("userinfo.uid=?", 1).Select("userinfo.uid,userinfo.username,userdeatail.profile").FindMap()
|
||||
|
||||
上面代码中我们看到了一个新的接口Join函数,这个函数带有三个参数
|
||||
|
||||
- 第一个参数可以是:INNER, LEFT, OUTER, CROSS等
|
||||
- 第二个参数表示连接的表
|
||||
- 第三个参数表示连接的条件
|
||||
|
||||
|
||||
## Group By和Having
|
||||
针对有些应用需要用到group by和having的功能,beedb也提供了一个简陋的实现
|
||||
|
||||
a, _ := orm.SetTable("userinfo").GroupBy("username").Having("username='astaxie'").FindMap()
|
||||
|
||||
上面的代码中出现了两个新接口函数
|
||||
|
||||
GroupBy:用来指定进行groupby的字段
|
||||
|
||||
Having:用来指定having执行的时候的条件
|
||||
|
||||
## 进一步的发展
|
||||
目前beedb已经获得了很多来自国内外用户的反馈,我目前也正在考虑重构,接下来会在几个方面进行改进
|
||||
|
||||
- 实现interface设计,类似databse/sql/driver的设计,设计beedb的接口,然后去实现相应数据库的CRUD操作
|
||||
- 实现关联数据库设计,支持一对一,一对多,多对多的实现,示例代码如下:
|
||||
|
||||
|
||||
type Profile struct{
|
||||
Nickname string
|
||||
Mobile string
|
||||
}
|
||||
|
||||
type Userinfo struct {
|
||||
Uid int `PK`
|
||||
Username string
|
||||
Departname string
|
||||
Created time.Time
|
||||
Profile `HasOne`
|
||||
}
|
||||
|
||||
- 自动建库建表建索引
|
||||
- 实现连接池的实现,采用goroutine
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [使用PostgreSQL数据库](<05.4.md>)
|
||||
* 下一节: [NOSQL数据库操作](<05.6.md>)
|
||||
249
zh/05.5.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
249
zh/05.5.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,249 @@
|
||||
# 5.5 使用beedb库进行ORM开发
|
||||
beedb是我开发的一个Go进行ORM操作的库,它采用了Go style方式对数据库进行操作,实现了struct到数据表记录的映射。beedb是一个十分轻量级的Go ORM框架,开发这个库的本意降低复杂的ORM学习曲线,尽可能在ORM的运行效率和功能之间寻求一个平衡,beedb是目前开源的Go ORM框架中实现比较完整的一个库,而且运行效率相当不错,功能也基本能满足需求。但是目前还不支持关系关联,这个是接下来版本升级的重点。
|
||||
|
||||
beedb是支持database/sql标准接口的ORM库,所以理论上来说,只要数据库驱动支持database/sql接口就可以无缝的接入beedb。目前我测试过的驱动包括下面几个:
|
||||
|
||||
Mysql:github.com/ziutek/mymysql/godrv[*]
|
||||
|
||||
Mysql:code.google.com/p/go-mysql-driver[*]
|
||||
|
||||
PostgreSQL:github.com/bmizerany/pq[*]
|
||||
|
||||
SQLite:github.com/mattn/go-sqlite3[*]
|
||||
|
||||
MS ADODB: github.com/mattn/go-adodb[*]
|
||||
|
||||
ODBC: bitbucket.org/miquella/mgodbc[*]
|
||||
|
||||
## 安装
|
||||
|
||||
beedb支持go get方式安装,是完全按照Go Style的方式来实现的。
|
||||
|
||||
go get github.com/astaxie/beedb
|
||||
|
||||
## 如何初始化
|
||||
首先你需要import相应的数据库驱动包、database/sql标准接口包以及beedb包,如下所示:
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/astaxie/beedb"
|
||||
_ "github.com/ziutek/mymysql/godrv"
|
||||
)
|
||||
|
||||
导入必须的package之后,我们需要打开到数据库的链接,然后创建一个beedb对象(以MySQL为例),如下所示
|
||||
|
||||
db, err := sql.Open("mymysql", "test/xiemengjun/123456")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
orm := beedb.New(db)
|
||||
|
||||
beedb的New函数实际上应该有两个参数,第一个参数标准接口的db,第二个参数是使用的数据库引擎,如果你使用的数据库引擎是MySQL/Sqlite,那么第二个参数都可以省略。
|
||||
|
||||
如果你使用的数据库是SQLServer,那么初始化需要:
|
||||
|
||||
orm = beedb.New(db, "mssql")
|
||||
|
||||
如果你使用了PostgreSQL,那么初始化需要:
|
||||
|
||||
orm = beedb.New(db, "pg")
|
||||
|
||||
目前beedb支持打印调试,你可以通过如下的代码实现调试
|
||||
|
||||
beedb.OnDebug=true
|
||||
|
||||
接下来我们的例子采用前面的数据库表Userinfo,现在我们建立相应的struct
|
||||
|
||||
type Userinfo struct {
|
||||
Uid int `PK` //如果表的主键不是id,那么需要加上pk注释,显式的说这个字段是主键
|
||||
Username string
|
||||
Departname string
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
>注意一点,beedb针对驼峰命名会自动帮你转化成下划线字段,例如你定义了Struct名字为`UserInfo`,那么转化成底层实现的时候是`user_info`,字段命名也遵循该规则。
|
||||
|
||||
## 插入数据
|
||||
下面的代码演示了如何插入一条记录,可以看到我们操作的是struct对象,而不是原生的sql语句,最后通过调用Save接口将数据保存到数据库。
|
||||
|
||||
var saveone Userinfo
|
||||
saveone.Username = "Test Add User"
|
||||
saveone.Departname = "Test Add Departname"
|
||||
saveone.Created = time.Now()
|
||||
orm.Save(&saveone)
|
||||
|
||||
我们看到插入之后`saveone.Uid`就是插入成功之后的自增ID。Save接口会自动帮你存进去。
|
||||
|
||||
beedb接口提供了另外一种插入的方式,map数据插入。
|
||||
|
||||
add := make(map[string]interface{})
|
||||
add["username"] = "astaxie"
|
||||
add["departname"] = "cloud develop"
|
||||
add["created"] = "2012-12-02"
|
||||
orm.SetTable("userinfo").Insert(add)
|
||||
|
||||
插入多条数据
|
||||
|
||||
addslice := make([]map[string]interface{}, 0)
|
||||
add:=make(map[string]interface{})
|
||||
add2:=make(map[string]interface{})
|
||||
add["username"] = "astaxie"
|
||||
add["departname"] = "cloud develop"
|
||||
add["created"] = "2012-12-02"
|
||||
add2["username"] = "astaxie2"
|
||||
add2["departname"] = "cloud develop2"
|
||||
add2["created"] = "2012-12-02"
|
||||
addslice =append(addslice, add, add2)
|
||||
orm.SetTable("userinfo").InsertBatch(addslice)
|
||||
|
||||
上面的操作方式有点类似链式查询,熟悉jquery的同学应该会觉得很亲切,每次调用的method都会返回原orm对象,以便可以继续调用该对象上的其他method。
|
||||
|
||||
上面我们调用的SetTable函数是显式的告诉ORM,我要执行的这个map对应的数据库表是`userinfo`。
|
||||
|
||||
## 更新数据
|
||||
继续上面的例子来演示更新操作,现在saveone的主键已经有值了,此时调用save接口,beedb内部会自动调用update以进行数据的更新而非插入操作。
|
||||
|
||||
saveone.Username = "Update Username"
|
||||
saveone.Departname = "Update Departname"
|
||||
saveone.Created = time.Now()
|
||||
orm.Save(&saveone) //现在saveone有了主键值,就执行更新操作
|
||||
|
||||
更新数据也支持直接使用map操作
|
||||
|
||||
t := make(map[string]interface{})
|
||||
t["username"] = "astaxie"
|
||||
orm.SetTable("userinfo").SetPK("uid").Where(2).Update(t)
|
||||
|
||||
这里我们调用了几个beedb的函数
|
||||
|
||||
SetPK:显式的告诉ORM,数据库表`userinfo`的主键是`uid`。
|
||||
|
||||
Where:用来设置条件,支持多个参数,第一个参数如果为整数,相当于调用了Where("主键=?",值)。
|
||||
Updata函数接收map类型的数据,执行更新数据。
|
||||
|
||||
## 查询数据
|
||||
beedb的查询接口比较灵活,具体使用请看下面的例子
|
||||
|
||||
例子1,根据主键获取数据:
|
||||
|
||||
var user Userinfo
|
||||
//Where接受两个参数,支持整形参数
|
||||
orm.Where("uid=?", 27).Find(&user)
|
||||
|
||||
|
||||
例子2:
|
||||
|
||||
var user2 Userinfo
|
||||
orm.Where(3).Find(&user2) // 这是上面版本的缩写版,可以省略主键
|
||||
|
||||
例子3,不是主键类型的的条件:
|
||||
|
||||
var user3 Userinfo
|
||||
//Where接受两个参数,支持字符型的参数
|
||||
orm.Where("name = ?", "john").Find(&user3)
|
||||
例子4,更加复杂的条件:
|
||||
|
||||
var user4 Userinfo
|
||||
//Where支持三个参数
|
||||
orm.Where("name = ? and age < ?", "john", 88).Find(&user4)
|
||||
|
||||
|
||||
可以通过如下接口获取多条数据,请看示例
|
||||
|
||||
例子1,根据条件id>3,获取20位置开始的10条数据的数据
|
||||
|
||||
var allusers []Userinfo
|
||||
err := orm.Where("id > ?", "3").Limit(10,20).FindAll(&allusers)
|
||||
|
||||
例子2,省略limit第二个参数,默认从0开始,获取10条数据
|
||||
|
||||
var tenusers []Userinfo
|
||||
err := orm.Where("id > ?", "3").Limit(10).FindAll(&tenusers)
|
||||
|
||||
例子3,获取全部数据
|
||||
|
||||
var everyone []Userinfo
|
||||
err := orm.OrderBy("uid desc,username asc").FindAll(&everyone)
|
||||
|
||||
上面这些里面里面我们看到一个函数Limit,他是用来控制查询结构条数的。
|
||||
|
||||
Limit:支持两个参数,第一个参数表示查询的条数,第二个参数表示读取数据的起始位置,默认为0。
|
||||
|
||||
OrderBy:这个函数用来进行查询排序,参数是需要排序的条件。
|
||||
|
||||
上面这些例子都是将获取的的数据直接映射成struct对象,如果我们只是想获取一些数据到map,以下方式可以实现:
|
||||
|
||||
a, _ := orm.SetTable("userinfo").SetPK("uid").Where(2).Select("uid,username").FindMap()
|
||||
|
||||
上面和这个例子里面又出现了一个新的接口函数Select,这个函数用来指定需要查询多少个字段。默认为全部字段`*`。
|
||||
|
||||
FindMap()函数返回的是`[]map[string][]byte`类型,所以你需要自己作类型转换。
|
||||
|
||||
## 删除数据
|
||||
beedb提供了丰富的删除数据接口,请看下面的例子
|
||||
|
||||
例子1,删除单条数据
|
||||
|
||||
//saveone就是上面示例中的那个saveone
|
||||
orm.Delete(&saveone)
|
||||
|
||||
例子2,删除多条数据
|
||||
|
||||
//alluser就是上面定义的获取多条数据的slice
|
||||
orm.DeleteAll(&alluser)
|
||||
|
||||
例子3,根据sql删除数据
|
||||
|
||||
orm.SetTable("userinfo").Where("uid>?", 3).DeleteRow()
|
||||
|
||||
|
||||
## 关联查询
|
||||
目前beedb还不支持struct的关联关系,但是有些应用却需要用到连接查询,所以现在beedb提供了一个简陋的实现方案:
|
||||
|
||||
a, _ := orm.SetTable("userinfo").Join("LEFT", "userdeatail", "userinfo.uid=userdeatail.uid").Where("userinfo.uid=?", 1).Select("userinfo.uid,userinfo.username,userdeatail.profile").FindMap()
|
||||
|
||||
上面代码中我们看到了一个新的接口Join函数,这个函数带有三个参数
|
||||
|
||||
- 第一个参数可以是:INNER, LEFT, OUTER, CROSS等
|
||||
- 第二个参数表示连接的表
|
||||
- 第三个参数表示连接的条件
|
||||
|
||||
|
||||
## Group By和Having
|
||||
针对有些应用需要用到group by和having的功能,beedb也提供了一个简陋的实现
|
||||
|
||||
a, _ := orm.SetTable("userinfo").GroupBy("username").Having("username='astaxie'").FindMap()
|
||||
|
||||
上面的代码中出现了两个新接口函数
|
||||
|
||||
GroupBy:用来指定进行groupby的字段
|
||||
|
||||
Having:用来指定having执行的时候的条件
|
||||
|
||||
## 进一步的发展
|
||||
目前beedb已经获得了很多来自国内外用户的反馈,我目前也正在考虑重构,接下来会在几个方面进行改进
|
||||
|
||||
- 实现interface设计,类似databse/sql/driver的设计,设计beedb的接口,然后去实现相应数据库的CRUD操作
|
||||
- 实现关联数据库设计,支持一对一,一对多,多对多的实现,示例代码如下:
|
||||
|
||||
|
||||
type Profile struct{
|
||||
Nickname string
|
||||
Mobile string
|
||||
}
|
||||
|
||||
type Userinfo struct {
|
||||
Uid int `PK`
|
||||
Username string
|
||||
Departname string
|
||||
Created time.Time
|
||||
Profile `HasOne`
|
||||
}
|
||||
|
||||
- 自动建库建表建索引
|
||||
- 实现连接池的实现,采用goroutine
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [使用PostgreSQL数据库](<05.4.md>)
|
||||
* 下一节: [NOSQL数据库操作](<05.6.md>)
|
||||
@@ -1,114 +1,114 @@
|
||||
# 5.6 NOSQL数据库操作
|
||||
NoSQL(Not Only SQL),指的是非关系型的数据库。随着Web2.0的兴起,传统的关系数据库在应付Web2.0网站,特别是超大规模和高并发的SNS类型的Web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。
|
||||
|
||||
而Go语言作为21世纪的C语言,对NOSQL的支持也是很好,目前流行的NOSQL主要有redis、mongoDB、Cassandra和Membase等。这些数据库都有高性能、高并发读写等特点,目前已经广泛应用于各种应用中。我接下来主要讲解一下redis和mongoDB的操作。
|
||||
|
||||
## redis
|
||||
redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)和zset(有序集合)。
|
||||
|
||||
目前应用redis最广泛的应该是新浪微博平台,其次还有Facebook收购的图片社交网站instagram。以及其他一些有名的[互联网企业](http://redis.io/topics/whos-using-redis)
|
||||
|
||||
Go目前支持redis的驱动有如下
|
||||
- https://github.com/alphazero/Go-Redis
|
||||
- http://code.google.com/p/tideland-rdc/
|
||||
- https://github.com/simonz05/godis
|
||||
- https://github.com/hoisie/redis.go
|
||||
|
||||
目前我fork了最后一个驱动,更新了一些bug,目前应用在我自己的短域名服务项目中(每天200W左右的PV值)
|
||||
|
||||
https://github.com/astaxie/goredis
|
||||
|
||||
接下来的以我自己fork的这个redis驱动为例来演示如何进行数据的操作
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/astaxie/goredis"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var client goredis.Client
|
||||
// 设置端口为redis默认端口
|
||||
client.Addr = "127.0.0.1:6379"
|
||||
|
||||
//字符串操作
|
||||
client.Set("a", []byte("hello"))
|
||||
val, _ := client.Get("a")
|
||||
fmt.Println(string(val))
|
||||
client.Del("a")
|
||||
|
||||
//list操作
|
||||
vals := []string{"a", "b", "c", "d", "e"}
|
||||
for _, v := range vals {
|
||||
client.Rpush("l", []byte(v))
|
||||
}
|
||||
dbvals,_ := client.Lrange("l", 0, 4)
|
||||
for i, v := range dbvals {
|
||||
println(i,":",string(v))
|
||||
}
|
||||
client.Del("l")
|
||||
}
|
||||
|
||||
我们可以看到操作redis非常的方便,而且我实际项目中应用下来性能也很高。client的命令和redis的命令基本保持一致。所以和原生态操作redis非常类似。
|
||||
|
||||
## mongoDB
|
||||
|
||||
MongoDB是一个高性能,开源,无模式的文档型数据库,是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。他支持的数据结构非常松散,采用的是类似json的bjson格式来存储数据,因此可以存储比较复杂的数据类型。Mongo最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。
|
||||
|
||||
下图展示了mysql和mongoDB之间的对应关系,我们可以看出来非常的方便,但是mongoDB的性能非常好。
|
||||
|
||||

|
||||
|
||||
图5.1 MongoDB和Mysql的操作对比图
|
||||
|
||||
目前Go支持mongoDB最好的驱动就是[mgo](http://labix.org/mgo),这个驱动目前最有可能成为官方的pkg。
|
||||
|
||||
下面我将演示如何通过Go来操作mongoDB:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"labix.org/v2/mgo"
|
||||
"labix.org/v2/mgo/bson"
|
||||
)
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Phone string
|
||||
}
|
||||
|
||||
func main() {
|
||||
session, err := mgo.Dial("server1.example.com,server2.example.com")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
session.SetMode(mgo.Monotonic, true)
|
||||
|
||||
c := session.DB("test").C("people")
|
||||
err = c.Insert(&Person{"Ale", "+55 53 8116 9639"},
|
||||
&Person{"Cla", "+55 53 8402 8510"})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
result := Person{}
|
||||
err = c.Find(bson.M{"name": "Ale"}).One(&result)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println("Phone:", result.Phone)
|
||||
}
|
||||
|
||||
我们可以看出来mgo的操作方式和beedb的操作方式几乎类似,都是基于struct的操作方式,这个就是Go Style。
|
||||
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [使用beedb库进行ORM开发](<05.5.md>)
|
||||
* 下一节: [小结](<05.7.md>)
|
||||
# 5.6 NOSQL数据库操作
|
||||
NoSQL(Not Only SQL),指的是非关系型的数据库。随着Web2.0的兴起,传统的关系数据库在应付Web2.0网站,特别是超大规模和高并发的SNS类型的Web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。
|
||||
|
||||
而Go语言作为21世纪的C语言,对NOSQL的支持也是很好,目前流行的NOSQL主要有redis、mongoDB、Cassandra和Membase等。这些数据库都有高性能、高并发读写等特点,目前已经广泛应用于各种应用中。我接下来主要讲解一下redis和mongoDB的操作。
|
||||
|
||||
## redis
|
||||
redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)和zset(有序集合)。
|
||||
|
||||
目前应用redis最广泛的应该是新浪微博平台,其次还有Facebook收购的图片社交网站instagram。以及其他一些有名的[互联网企业](http://redis.io/topics/whos-using-redis)
|
||||
|
||||
Go目前支持redis的驱动有如下
|
||||
- https://github.com/alphazero/Go-Redis
|
||||
- http://code.google.com/p/tideland-rdc/
|
||||
- https://github.com/simonz05/godis
|
||||
- https://github.com/hoisie/redis.go
|
||||
|
||||
目前我fork了最后一个驱动,更新了一些bug,目前应用在我自己的短域名服务项目中(每天200W左右的PV值)
|
||||
|
||||
https://github.com/astaxie/goredis
|
||||
|
||||
接下来的以我自己fork的这个redis驱动为例来演示如何进行数据的操作
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/astaxie/goredis"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var client goredis.Client
|
||||
// 设置端口为redis默认端口
|
||||
client.Addr = "127.0.0.1:6379"
|
||||
|
||||
//字符串操作
|
||||
client.Set("a", []byte("hello"))
|
||||
val, _ := client.Get("a")
|
||||
fmt.Println(string(val))
|
||||
client.Del("a")
|
||||
|
||||
//list操作
|
||||
vals := []string{"a", "b", "c", "d", "e"}
|
||||
for _, v := range vals {
|
||||
client.Rpush("l", []byte(v))
|
||||
}
|
||||
dbvals,_ := client.Lrange("l", 0, 4)
|
||||
for i, v := range dbvals {
|
||||
println(i,":",string(v))
|
||||
}
|
||||
client.Del("l")
|
||||
}
|
||||
|
||||
我们可以看到操作redis非常的方便,而且我实际项目中应用下来性能也很高。client的命令和redis的命令基本保持一致。所以和原生态操作redis非常类似。
|
||||
|
||||
## mongoDB
|
||||
|
||||
MongoDB是一个高性能,开源,无模式的文档型数据库,是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。他支持的数据结构非常松散,采用的是类似json的bjson格式来存储数据,因此可以存储比较复杂的数据类型。Mongo最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。
|
||||
|
||||
下图展示了mysql和mongoDB之间的对应关系,我们可以看出来非常的方便,但是mongoDB的性能非常好。
|
||||
|
||||

|
||||
|
||||
图5.1 MongoDB和Mysql的操作对比图
|
||||
|
||||
目前Go支持mongoDB最好的驱动就是[mgo](http://labix.org/mgo),这个驱动目前最有可能成为官方的pkg。
|
||||
|
||||
下面我将演示如何通过Go来操作mongoDB:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"labix.org/v2/mgo"
|
||||
"labix.org/v2/mgo/bson"
|
||||
)
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Phone string
|
||||
}
|
||||
|
||||
func main() {
|
||||
session, err := mgo.Dial("server1.example.com,server2.example.com")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
session.SetMode(mgo.Monotonic, true)
|
||||
|
||||
c := session.DB("test").C("people")
|
||||
err = c.Insert(&Person{"Ale", "+55 53 8116 9639"},
|
||||
&Person{"Cla", "+55 53 8402 8510"})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
result := Person{}
|
||||
err = c.Find(bson.M{"name": "Ale"}).One(&result)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println("Phone:", result.Phone)
|
||||
}
|
||||
|
||||
我们可以看出来mgo的操作方式和beedb的操作方式几乎类似,都是基于struct的操作方式,这个就是Go Style。
|
||||
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [使用beedb库进行ORM开发](<05.5.md>)
|
||||
* 下一节: [小结](<05.7.md>)
|
||||
114
zh/05.6.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
114
zh/05.6.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,114 @@
|
||||
# 5.6 NOSQL数据库操作
|
||||
NoSQL(Not Only SQL),指的是非关系型的数据库。随着Web2.0的兴起,传统的关系数据库在应付Web2.0网站,特别是超大规模和高并发的SNS类型的Web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。
|
||||
|
||||
而Go语言作为21世纪的C语言,对NOSQL的支持也是很好,目前流行的NOSQL主要有redis、mongoDB、Cassandra和Membase等。这些数据库都有高性能、高并发读写等特点,目前已经广泛应用于各种应用中。我接下来主要讲解一下redis和mongoDB的操作。
|
||||
|
||||
## redis
|
||||
redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)和zset(有序集合)。
|
||||
|
||||
目前应用redis最广泛的应该是新浪微博平台,其次还有Facebook收购的图片社交网站instagram。以及其他一些有名的[互联网企业](http://redis.io/topics/whos-using-redis)
|
||||
|
||||
Go目前支持redis的驱动有如下
|
||||
- https://github.com/alphazero/Go-Redis
|
||||
- http://code.google.com/p/tideland-rdc/
|
||||
- https://github.com/simonz05/godis
|
||||
- https://github.com/hoisie/redis.go
|
||||
|
||||
目前我fork了最后一个驱动,更新了一些bug,目前应用在我自己的短域名服务项目中(每天200W左右的PV值)
|
||||
|
||||
https://github.com/astaxie/goredis
|
||||
|
||||
接下来的以我自己fork的这个redis驱动为例来演示如何进行数据的操作
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/astaxie/goredis"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var client goredis.Client
|
||||
// 设置端口为redis默认端口
|
||||
client.Addr = "127.0.0.1:6379"
|
||||
|
||||
//字符串操作
|
||||
client.Set("a", []byte("hello"))
|
||||
val, _ := client.Get("a")
|
||||
fmt.Println(string(val))
|
||||
client.Del("a")
|
||||
|
||||
//list操作
|
||||
vals := []string{"a", "b", "c", "d", "e"}
|
||||
for _, v := range vals {
|
||||
client.Rpush("l", []byte(v))
|
||||
}
|
||||
dbvals,_ := client.Lrange("l", 0, 4)
|
||||
for i, v := range dbvals {
|
||||
println(i,":",string(v))
|
||||
}
|
||||
client.Del("l")
|
||||
}
|
||||
|
||||
我们可以看到操作redis非常的方便,而且我实际项目中应用下来性能也很高。client的命令和redis的命令基本保持一致。所以和原生态操作redis非常类似。
|
||||
|
||||
## mongoDB
|
||||
|
||||
MongoDB是一个高性能,开源,无模式的文档型数据库,是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。他支持的数据结构非常松散,采用的是类似json的bjson格式来存储数据,因此可以存储比较复杂的数据类型。Mongo最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。
|
||||
|
||||
下图展示了mysql和mongoDB之间的对应关系,我们可以看出来非常的方便,但是mongoDB的性能非常好。
|
||||
|
||||

|
||||
|
||||
图5.1 MongoDB和Mysql的操作对比图
|
||||
|
||||
目前Go支持mongoDB最好的驱动就是[mgo](http://labix.org/mgo),这个驱动目前最有可能成为官方的pkg。
|
||||
|
||||
下面我将演示如何通过Go来操作mongoDB:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"labix.org/v2/mgo"
|
||||
"labix.org/v2/mgo/bson"
|
||||
)
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Phone string
|
||||
}
|
||||
|
||||
func main() {
|
||||
session, err := mgo.Dial("server1.example.com,server2.example.com")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
session.SetMode(mgo.Monotonic, true)
|
||||
|
||||
c := session.DB("test").C("people")
|
||||
err = c.Insert(&Person{"Ale", "+55 53 8116 9639"},
|
||||
&Person{"Cla", "+55 53 8402 8510"})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
result := Person{}
|
||||
err = c.Find(bson.M{"name": "Ale"}).One(&result)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println("Phone:", result.Phone)
|
||||
}
|
||||
|
||||
我们可以看出来mgo的操作方式和beedb的操作方式几乎类似,都是基于struct的操作方式,这个就是Go Style。
|
||||
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [使用beedb库进行ORM开发](<05.5.md>)
|
||||
* 下一节: [小结](<05.7.md>)
|
||||
@@ -1,11 +1,11 @@
|
||||
# 5.7 小结
|
||||
这一章我们讲解了Go如何设计database/sql接口,然后介绍了各种第三方关系型数据库驱动的使用。接着介绍了beedb,一种基于关系型数据库的ORM库,如何对数据库进行简单的操作。最后介绍了NOSQL的一些知识,目前Go对于NOSQL支持还是不错,因为Go作为21世纪的C语言,那么对于21世纪的数据库也是支持的相当好。
|
||||
|
||||
通过这一章的学习,我们学会了如何操作各种数据库,那么就解决了我们数据存储的问题,这是Web里面最重要的一部分,所以希望大家能够深入的去了解database/sql的设计思想。
|
||||
|
||||
>[Go database/sql tutorial](http://go-database-sql.org/) 里提供了惯用的范例及详细的说明。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [NOSQL数据库操作](<05.6.md>)
|
||||
* 下一章: [session和数据存储](<06.0.md>)
|
||||
# 5.7 小结
|
||||
这一章我们讲解了Go如何设计database/sql接口,然后介绍了各种第三方关系型数据库驱动的使用。接着介绍了beedb,一种基于关系型数据库的ORM库,如何对数据库进行简单的操作。最后介绍了NOSQL的一些知识,目前Go对于NOSQL支持还是不错,因为Go作为21世纪的C语言,那么对于21世纪的数据库也是支持的相当好。
|
||||
|
||||
通过这一章的学习,我们学会了如何操作各种数据库,那么就解决了我们数据存储的问题,这是Web里面最重要的一部分,所以希望大家能够深入的去了解database/sql的设计思想。
|
||||
|
||||
>[Go database/sql tutorial](http://go-database-sql.org/) 里提供了惯用的范例及详细的说明。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [NOSQL数据库操作](<05.6.md>)
|
||||
* 下一章: [session和数据存储](<06.0.md>)
|
||||
11
zh/05.7.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
11
zh/05.7.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,11 @@
|
||||
# 5.7 小结
|
||||
这一章我们讲解了Go如何设计database/sql接口,然后介绍了各种第三方关系型数据库驱动的使用。接着介绍了beedb,一种基于关系型数据库的ORM库,如何对数据库进行简单的操作。最后介绍了NOSQL的一些知识,目前Go对于NOSQL支持还是不错,因为Go作为21世纪的C语言,那么对于21世纪的数据库也是支持的相当好。
|
||||
|
||||
通过这一章的学习,我们学会了如何操作各种数据库,那么就解决了我们数据存储的问题,这是Web里面最重要的一部分,所以希望大家能够深入的去了解database/sql的设计思想。
|
||||
|
||||
>[Go database/sql tutorial](http://go-database-sql.org/) 里提供了惯用的范例及详细的说明。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [NOSQL数据库操作](<05.6.md>)
|
||||
* 下一章: [session和数据存储](<06.0.md>)
|
||||
@@ -1,12 +1,12 @@
|
||||
# 6 session和数据存储
|
||||
Web开发中一个很重要的议题就是如何做好用户的整个浏览过程的控制,因为HTTP协议是无状态的,所以用户的每一次请求都是无状态的,我们不知道在整个Web操作过程中哪些连接与该用户有关,我们应该如何来解决这个问题呢?Web里面经典的解决方案是cookie和session,cookie机制是一种客户端机制,把用户数据保存在客户端,而session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构来保存信息,每一个网站访客都会被分配给一个唯一的标志符,即sessionID,它的存放形式无非两种:要么经过url传递,要么保存在客户端的cookies里.当然,你也可以将Session保存到数据库里,这样会更安全,但效率方面会有所下降。
|
||||
|
||||
6.1小节里面讲介绍session机制和cookie机制的关系和区别,6.2讲解Go语言如何来实现session,里面讲实现一个简易的session管理器,6.3小节讲解如何防止session被劫持的情况,如何有效的保护session。我们知道session其实可以存储在任何地方,6.3小节里面实现的session是存储在内存中的,但是如果我们的应用进一步扩展了,要实现应用的session共享,那么我们可以把session存储在数据库中(memcache或者redis),6.4小节将详细的讲解如何实现这些功能。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第五章总结](<05.7.md>)
|
||||
* 下一节: [session和cookie](<06.1.md>)
|
||||
# 6 session和数据存储
|
||||
Web开发中一个很重要的议题就是如何做好用户的整个浏览过程的控制,因为HTTP协议是无状态的,所以用户的每一次请求都是无状态的,我们不知道在整个Web操作过程中哪些连接与该用户有关,我们应该如何来解决这个问题呢?Web里面经典的解决方案是cookie和session,cookie机制是一种客户端机制,把用户数据保存在客户端,而session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构来保存信息,每一个网站访客都会被分配给一个唯一的标志符,即sessionID,它的存放形式无非两种:要么经过url传递,要么保存在客户端的cookies里.当然,你也可以将Session保存到数据库里,这样会更安全,但效率方面会有所下降。
|
||||
|
||||
6.1小节里面讲介绍session机制和cookie机制的关系和区别,6.2讲解Go语言如何来实现session,里面讲实现一个简易的session管理器,6.3小节讲解如何防止session被劫持的情况,如何有效的保护session。我们知道session其实可以存储在任何地方,6.3小节里面实现的session是存储在内存中的,但是如果我们的应用进一步扩展了,要实现应用的session共享,那么我们可以把session存储在数据库中(memcache或者redis),6.4小节将详细的讲解如何实现这些功能。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第五章总结](<05.7.md>)
|
||||
* 下一节: [session和cookie](<06.1.md>)
|
||||
12
zh/06.0.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
12
zh/06.0.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,12 @@
|
||||
# 6 session和数据存储
|
||||
Web开发中一个很重要的议题就是如何做好用户的整个浏览过程的控制,因为HTTP协议是无状态的,所以用户的每一次请求都是无状态的,我们不知道在整个Web操作过程中哪些连接与该用户有关,我们应该如何来解决这个问题呢?Web里面经典的解决方案是cookie和session,cookie机制是一种客户端机制,把用户数据保存在客户端,而session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构来保存信息,每一个网站访客都会被分配给一个唯一的标志符,即sessionID,它的存放形式无非两种:要么经过url传递,要么保存在客户端的cookies里.当然,你也可以将Session保存到数据库里,这样会更安全,但效率方面会有所下降。
|
||||
|
||||
6.1小节里面讲介绍session机制和cookie机制的关系和区别,6.2讲解Go语言如何来实现session,里面讲实现一个简易的session管理器,6.3小节讲解如何防止session被劫持的情况,如何有效的保护session。我们知道session其实可以存储在任何地方,6.3小节里面实现的session是存储在内存中的,但是如果我们的应用进一步扩展了,要实现应用的session共享,那么我们可以把session存储在数据库中(memcache或者redis),6.4小节将详细的讲解如何实现这些功能。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第五章总结](<05.7.md>)
|
||||
* 下一节: [session和cookie](<06.1.md>)
|
||||
@@ -1,105 +1,105 @@
|
||||
# 6.1 session和cookie
|
||||
session和cookie是网站浏览中较为常见的两个概念,也是比较难以辨析的两个概念,但它们在浏览需要认证的服务页面以及页面统计中却相当关键。我们先来了解一下session和cookie怎么来的?考虑这样一个问题:
|
||||
|
||||
如何抓取一个访问受限的网页?如新浪微博好友的主页,个人微博页面等。
|
||||
|
||||
显然,通过浏览器,我们可以手动输入用户名和密码来访问页面,而所谓的“抓取”,其实就是使用程序来模拟完成同样的工作,因此我们需要了解“登陆”过程中到底发生了什么。
|
||||
|
||||
当用户来到微博登陆页面,输入用户名和密码之后点击“登录”后浏览器将认证信息POST给远端的服务器,服务器执行验证逻辑,如果验证通过,则浏览器会跳转到登录用户的微博首页,在登录成功后,服务器如何验证我们对其他受限制页面的访问呢?因为HTTP协议是无状态的,所以很显然服务器不可能知道我们已经在上一次的HTTP请求中通过了验证。当然,最简单的解决方案就是所有的请求里面都带上用户名和密码,这样虽然可行,但大大加重了服务器的负担(对于每个request都需要到数据库验证),也大大降低了用户体验(每个页面都需要重新输入用户名密码,每个页面都带有登录表单)。既然直接在请求中带上用户名与密码不可行,那么就只有在服务器或客户端保存一些类似的可以代表身份的信息了,所以就有了cookie与session。
|
||||
|
||||
cookie,简而言之就是在本地计算机保存一些用户操作的历史信息(当然包括登录信息),并在用户再次访问该站点时浏览器通过HTTP协议将本地cookie内容发送给服务器,从而完成验证,或继续上一步操作。
|
||||
|
||||

|
||||
|
||||
图6.1 cookie的原理图
|
||||
|
||||
session,简而言之就是在服务器上保存用户操作的历史信息。服务器使用session id来标识session,session id由服务器负责产生,保证随机性与唯一性,相当于一个随机密钥,避免在握手或传输中暴露用户真实密码。但该方式下,仍然需要将发送请求的客户端与session进行对应,所以可以借助cookie机制来获取客户端的标识(即session id),也可以通过GET方式将id提交给服务器。
|
||||
|
||||

|
||||
|
||||
图6.2 session的原理图
|
||||
|
||||
## cookie
|
||||
Cookie是由浏览器维持的,存储在客户端的一小段文本信息,伴随着用户请求和页面在Web服务器和浏览器之间传递。用户每次访问站点时,Web应用程序都可以读取cookie包含的信息。浏览器设置里面有cookie隐私数据选项,打开它,可以看到很多已访问网站的cookies,如下图所示:
|
||||
|
||||

|
||||
|
||||
图6.3 浏览器端保存的cookie信息
|
||||
|
||||
cookie是有时间限制的,根据生命期不同分成两种:会话cookie和持久cookie;
|
||||
|
||||
如果不设置过期时间,则表示这个cookie生命周期为从创建到浏览器关闭止,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览会话期的cookie被称为会话cookie。会话cookie一般不保存在硬盘上而是保存在内存里。
|
||||
|
||||
如果设置了过期时间(setMaxAge(60*60*24)),浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie依然有效直到超过设定的过期时间。存储在硬盘上的cookie可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存的cookie,不同的浏览器有不同的处理方式。
|
||||
|
||||
|
||||
### Go设置cookie
|
||||
Go语言中通过net/http包中的SetCookie来设置:
|
||||
|
||||
http.SetCookie(w ResponseWriter, cookie *Cookie)
|
||||
|
||||
w表示需要写入的response,cookie是一个struct,让我们来看一下cookie对象是怎么样的
|
||||
|
||||
type Cookie struct {
|
||||
Name string
|
||||
Value string
|
||||
Path string
|
||||
Domain string
|
||||
Expires time.Time
|
||||
RawExpires string
|
||||
|
||||
// MaxAge=0 means no 'Max-Age' attribute specified.
|
||||
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
|
||||
// MaxAge>0 means Max-Age attribute present and given in seconds
|
||||
MaxAge int
|
||||
Secure bool
|
||||
HttpOnly bool
|
||||
Raw string
|
||||
Unparsed []string // Raw text of unparsed attribute-value pairs
|
||||
}
|
||||
|
||||
我们来看一个例子,如何设置cookie
|
||||
|
||||
expiration := time.Now()
|
||||
expiration = expiration.AddDate(1, 0, 0)
|
||||
cookie := http.Cookie{Name: "username", Value: "astaxie", Expires: expiration}
|
||||
http.SetCookie(w, &cookie)
|
||||
|
||||
|
||||
### Go读取cookie
|
||||
上面的例子演示了如何设置cookie数据,我们这里来演示一下如何读取cookie
|
||||
|
||||
cookie, _ := r.Cookie("username")
|
||||
fmt.Fprint(w, cookie)
|
||||
|
||||
还有另外一种读取方式
|
||||
|
||||
for _, cookie := range r.Cookies() {
|
||||
fmt.Fprint(w, cookie.Name)
|
||||
}
|
||||
|
||||
可以看到通过request获取cookie非常方便。
|
||||
|
||||
## session
|
||||
|
||||
session,中文经常翻译为会话,其本来的含义是指有始有终的一系列动作/消息,比如打电话是从拿起电话拨号到挂断电话这中间的一系列过程可以称之为一个session。然而当session一词与网络协议相关联时,它又往往隐含了“面向连接”和/或“保持状态”这样两个含义。
|
||||
|
||||
session在Web开发环境下的语义又有了新的扩展,它的含义是指一类用来在客户端与服务器端之间保持状态的解决方案。有时候Session也用来指这种解决方案的存储结构。
|
||||
|
||||
session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。
|
||||
|
||||
但程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否包含了一个session标识-称为session id,如果已经包含一个session id则说明以前已经为此客户创建过session,服务器就按照session id把这个session检索出来使用(如果检索不到,可能会新建一个,这种情况可能出现在服务端已经删除了该用户对应的session对象,但用户人为地在请求的URL后面附加上一个JSESSION的参数)。如果客户请求不包含session id,则为此客户创建一个session并且同时生成一个与此session相关联的session id,这个session id将在本次响应中返回给客户端保存。
|
||||
|
||||
session机制本身并不复杂,然而其实现和配置上的灵活性却使得具体情况复杂多变。这也要求我们不能把仅仅某一次的经验或者某一个浏览器,服务器的经验当作普遍适用的。
|
||||
|
||||
## 小结
|
||||
|
||||
如上文所述,session和cookie的目的相同,都是为了克服http协议无状态的缺陷,但完成的方法不同。session通过cookie,在客户端保存session id,而将用户的其他会话消息保存在服务端的session对象中,与此相对的,cookie需要将所有信息都保存在客户端。因此cookie存在着一定的安全隐患,例如本地cookie中保存的用户名密码被破译,或cookie被其他网站收集(例如:1. appA主动设置域B cookie,让域B cookie获取;2. XSS,在appA上通过javascript获取document.cookie,并传递给自己的appB)。
|
||||
|
||||
|
||||
通过上面的一些简单介绍我们了解了cookie和session的一些基础知识,知道他们之间的联系和区别,做web开发之前,有必要将一些必要知识了解清楚,才不会在用到时捉襟见肘,或是在调bug时候如无头苍蝇乱转。接下来的几小节我们将详细介绍session相关的知识。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [session和数据存储](<06.0.md>)
|
||||
* 下一节: [Go如何使用session](<06.2.md>)
|
||||
# 6.1 session和cookie
|
||||
session和cookie是网站浏览中较为常见的两个概念,也是比较难以辨析的两个概念,但它们在浏览需要认证的服务页面以及页面统计中却相当关键。我们先来了解一下session和cookie怎么来的?考虑这样一个问题:
|
||||
|
||||
如何抓取一个访问受限的网页?如新浪微博好友的主页,个人微博页面等。
|
||||
|
||||
显然,通过浏览器,我们可以手动输入用户名和密码来访问页面,而所谓的“抓取”,其实就是使用程序来模拟完成同样的工作,因此我们需要了解“登陆”过程中到底发生了什么。
|
||||
|
||||
当用户来到微博登陆页面,输入用户名和密码之后点击“登录”后浏览器将认证信息POST给远端的服务器,服务器执行验证逻辑,如果验证通过,则浏览器会跳转到登录用户的微博首页,在登录成功后,服务器如何验证我们对其他受限制页面的访问呢?因为HTTP协议是无状态的,所以很显然服务器不可能知道我们已经在上一次的HTTP请求中通过了验证。当然,最简单的解决方案就是所有的请求里面都带上用户名和密码,这样虽然可行,但大大加重了服务器的负担(对于每个request都需要到数据库验证),也大大降低了用户体验(每个页面都需要重新输入用户名密码,每个页面都带有登录表单)。既然直接在请求中带上用户名与密码不可行,那么就只有在服务器或客户端保存一些类似的可以代表身份的信息了,所以就有了cookie与session。
|
||||
|
||||
cookie,简而言之就是在本地计算机保存一些用户操作的历史信息(当然包括登录信息),并在用户再次访问该站点时浏览器通过HTTP协议将本地cookie内容发送给服务器,从而完成验证,或继续上一步操作。
|
||||
|
||||

|
||||
|
||||
图6.1 cookie的原理图
|
||||
|
||||
session,简而言之就是在服务器上保存用户操作的历史信息。服务器使用session id来标识session,session id由服务器负责产生,保证随机性与唯一性,相当于一个随机密钥,避免在握手或传输中暴露用户真实密码。但该方式下,仍然需要将发送请求的客户端与session进行对应,所以可以借助cookie机制来获取客户端的标识(即session id),也可以通过GET方式将id提交给服务器。
|
||||
|
||||

|
||||
|
||||
图6.2 session的原理图
|
||||
|
||||
## cookie
|
||||
Cookie是由浏览器维持的,存储在客户端的一小段文本信息,伴随着用户请求和页面在Web服务器和浏览器之间传递。用户每次访问站点时,Web应用程序都可以读取cookie包含的信息。浏览器设置里面有cookie隐私数据选项,打开它,可以看到很多已访问网站的cookies,如下图所示:
|
||||
|
||||

|
||||
|
||||
图6.3 浏览器端保存的cookie信息
|
||||
|
||||
cookie是有时间限制的,根据生命期不同分成两种:会话cookie和持久cookie;
|
||||
|
||||
如果不设置过期时间,则表示这个cookie生命周期为从创建到浏览器关闭止,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览会话期的cookie被称为会话cookie。会话cookie一般不保存在硬盘上而是保存在内存里。
|
||||
|
||||
如果设置了过期时间(setMaxAge(60*60*24)),浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie依然有效直到超过设定的过期时间。存储在硬盘上的cookie可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存的cookie,不同的浏览器有不同的处理方式。
|
||||
|
||||
|
||||
### Go设置cookie
|
||||
Go语言中通过net/http包中的SetCookie来设置:
|
||||
|
||||
http.SetCookie(w ResponseWriter, cookie *Cookie)
|
||||
|
||||
w表示需要写入的response,cookie是一个struct,让我们来看一下cookie对象是怎么样的
|
||||
|
||||
type Cookie struct {
|
||||
Name string
|
||||
Value string
|
||||
Path string
|
||||
Domain string
|
||||
Expires time.Time
|
||||
RawExpires string
|
||||
|
||||
// MaxAge=0 means no 'Max-Age' attribute specified.
|
||||
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
|
||||
// MaxAge>0 means Max-Age attribute present and given in seconds
|
||||
MaxAge int
|
||||
Secure bool
|
||||
HttpOnly bool
|
||||
Raw string
|
||||
Unparsed []string // Raw text of unparsed attribute-value pairs
|
||||
}
|
||||
|
||||
我们来看一个例子,如何设置cookie
|
||||
|
||||
expiration := time.Now()
|
||||
expiration = expiration.AddDate(1, 0, 0)
|
||||
cookie := http.Cookie{Name: "username", Value: "astaxie", Expires: expiration}
|
||||
http.SetCookie(w, &cookie)
|
||||
|
||||
|
||||
### Go读取cookie
|
||||
上面的例子演示了如何设置cookie数据,我们这里来演示一下如何读取cookie
|
||||
|
||||
cookie, _ := r.Cookie("username")
|
||||
fmt.Fprint(w, cookie)
|
||||
|
||||
还有另外一种读取方式
|
||||
|
||||
for _, cookie := range r.Cookies() {
|
||||
fmt.Fprint(w, cookie.Name)
|
||||
}
|
||||
|
||||
可以看到通过request获取cookie非常方便。
|
||||
|
||||
## session
|
||||
|
||||
session,中文经常翻译为会话,其本来的含义是指有始有终的一系列动作/消息,比如打电话是从拿起电话拨号到挂断电话这中间的一系列过程可以称之为一个session。然而当session一词与网络协议相关联时,它又往往隐含了“面向连接”和/或“保持状态”这样两个含义。
|
||||
|
||||
session在Web开发环境下的语义又有了新的扩展,它的含义是指一类用来在客户端与服务器端之间保持状态的解决方案。有时候Session也用来指这种解决方案的存储结构。
|
||||
|
||||
session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。
|
||||
|
||||
但程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否包含了一个session标识-称为session id,如果已经包含一个session id则说明以前已经为此客户创建过session,服务器就按照session id把这个session检索出来使用(如果检索不到,可能会新建一个,这种情况可能出现在服务端已经删除了该用户对应的session对象,但用户人为地在请求的URL后面附加上一个JSESSION的参数)。如果客户请求不包含session id,则为此客户创建一个session并且同时生成一个与此session相关联的session id,这个session id将在本次响应中返回给客户端保存。
|
||||
|
||||
session机制本身并不复杂,然而其实现和配置上的灵活性却使得具体情况复杂多变。这也要求我们不能把仅仅某一次的经验或者某一个浏览器,服务器的经验当作普遍适用的。
|
||||
|
||||
## 小结
|
||||
|
||||
如上文所述,session和cookie的目的相同,都是为了克服http协议无状态的缺陷,但完成的方法不同。session通过cookie,在客户端保存session id,而将用户的其他会话消息保存在服务端的session对象中,与此相对的,cookie需要将所有信息都保存在客户端。因此cookie存在着一定的安全隐患,例如本地cookie中保存的用户名密码被破译,或cookie被其他网站收集(例如:1. appA主动设置域B cookie,让域B cookie获取;2. XSS,在appA上通过javascript获取document.cookie,并传递给自己的appB)。
|
||||
|
||||
|
||||
通过上面的一些简单介绍我们了解了cookie和session的一些基础知识,知道他们之间的联系和区别,做web开发之前,有必要将一些必要知识了解清楚,才不会在用到时捉襟见肘,或是在调bug时候如无头苍蝇乱转。接下来的几小节我们将详细介绍session相关的知识。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [session和数据存储](<06.0.md>)
|
||||
* 下一节: [Go如何使用session](<06.2.md>)
|
||||
105
zh/06.1.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
105
zh/06.1.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,105 @@
|
||||
# 6.1 session和cookie
|
||||
session和cookie是网站浏览中较为常见的两个概念,也是比较难以辨析的两个概念,但它们在浏览需要认证的服务页面以及页面统计中却相当关键。我们先来了解一下session和cookie怎么来的?考虑这样一个问题:
|
||||
|
||||
如何抓取一个访问受限的网页?如新浪微博好友的主页,个人微博页面等。
|
||||
|
||||
显然,通过浏览器,我们可以手动输入用户名和密码来访问页面,而所谓的“抓取”,其实就是使用程序来模拟完成同样的工作,因此我们需要了解“登陆”过程中到底发生了什么。
|
||||
|
||||
当用户来到微博登陆页面,输入用户名和密码之后点击“登录”后浏览器将认证信息POST给远端的服务器,服务器执行验证逻辑,如果验证通过,则浏览器会跳转到登录用户的微博首页,在登录成功后,服务器如何验证我们对其他受限制页面的访问呢?因为HTTP协议是无状态的,所以很显然服务器不可能知道我们已经在上一次的HTTP请求中通过了验证。当然,最简单的解决方案就是所有的请求里面都带上用户名和密码,这样虽然可行,但大大加重了服务器的负担(对于每个request都需要到数据库验证),也大大降低了用户体验(每个页面都需要重新输入用户名密码,每个页面都带有登录表单)。既然直接在请求中带上用户名与密码不可行,那么就只有在服务器或客户端保存一些类似的可以代表身份的信息了,所以就有了cookie与session。
|
||||
|
||||
cookie,简而言之就是在本地计算机保存一些用户操作的历史信息(当然包括登录信息),并在用户再次访问该站点时浏览器通过HTTP协议将本地cookie内容发送给服务器,从而完成验证,或继续上一步操作。
|
||||
|
||||

|
||||
|
||||
图6.1 cookie的原理图
|
||||
|
||||
session,简而言之就是在服务器上保存用户操作的历史信息。服务器使用session id来标识session,session id由服务器负责产生,保证随机性与唯一性,相当于一个随机密钥,避免在握手或传输中暴露用户真实密码。但该方式下,仍然需要将发送请求的客户端与session进行对应,所以可以借助cookie机制来获取客户端的标识(即session id),也可以通过GET方式将id提交给服务器。
|
||||
|
||||

|
||||
|
||||
图6.2 session的原理图
|
||||
|
||||
## cookie
|
||||
Cookie是由浏览器维持的,存储在客户端的一小段文本信息,伴随着用户请求和页面在Web服务器和浏览器之间传递。用户每次访问站点时,Web应用程序都可以读取cookie包含的信息。浏览器设置里面有cookie隐私数据选项,打开它,可以看到很多已访问网站的cookies,如下图所示:
|
||||
|
||||

|
||||
|
||||
图6.3 浏览器端保存的cookie信息
|
||||
|
||||
cookie是有时间限制的,根据生命期不同分成两种:会话cookie和持久cookie;
|
||||
|
||||
如果不设置过期时间,则表示这个cookie生命周期为从创建到浏览器关闭止,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览会话期的cookie被称为会话cookie。会话cookie一般不保存在硬盘上而是保存在内存里。
|
||||
|
||||
如果设置了过期时间(setMaxAge(60*60*24)),浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie依然有效直到超过设定的过期时间。存储在硬盘上的cookie可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存的cookie,不同的浏览器有不同的处理方式。
|
||||
|
||||
|
||||
### Go设置cookie
|
||||
Go语言中通过net/http包中的SetCookie来设置:
|
||||
|
||||
http.SetCookie(w ResponseWriter, cookie *Cookie)
|
||||
|
||||
w表示需要写入的response,cookie是一个struct,让我们来看一下cookie对象是怎么样的
|
||||
|
||||
type Cookie struct {
|
||||
Name string
|
||||
Value string
|
||||
Path string
|
||||
Domain string
|
||||
Expires time.Time
|
||||
RawExpires string
|
||||
|
||||
// MaxAge=0 means no 'Max-Age' attribute specified.
|
||||
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
|
||||
// MaxAge>0 means Max-Age attribute present and given in seconds
|
||||
MaxAge int
|
||||
Secure bool
|
||||
HttpOnly bool
|
||||
Raw string
|
||||
Unparsed []string // Raw text of unparsed attribute-value pairs
|
||||
}
|
||||
|
||||
我们来看一个例子,如何设置cookie
|
||||
|
||||
expiration := time.Now()
|
||||
expiration = expiration.AddDate(1, 0, 0)
|
||||
cookie := http.Cookie{Name: "username", Value: "astaxie", Expires: expiration}
|
||||
http.SetCookie(w, &cookie)
|
||||
|
||||
|
||||
### Go读取cookie
|
||||
上面的例子演示了如何设置cookie数据,我们这里来演示一下如何读取cookie
|
||||
|
||||
cookie, _ := r.Cookie("username")
|
||||
fmt.Fprint(w, cookie)
|
||||
|
||||
还有另外一种读取方式
|
||||
|
||||
for _, cookie := range r.Cookies() {
|
||||
fmt.Fprint(w, cookie.Name)
|
||||
}
|
||||
|
||||
可以看到通过request获取cookie非常方便。
|
||||
|
||||
## session
|
||||
|
||||
session,中文经常翻译为会话,其本来的含义是指有始有终的一系列动作/消息,比如打电话是从拿起电话拨号到挂断电话这中间的一系列过程可以称之为一个session。然而当session一词与网络协议相关联时,它又往往隐含了“面向连接”和/或“保持状态”这样两个含义。
|
||||
|
||||
session在Web开发环境下的语义又有了新的扩展,它的含义是指一类用来在客户端与服务器端之间保持状态的解决方案。有时候Session也用来指这种解决方案的存储结构。
|
||||
|
||||
session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。
|
||||
|
||||
但程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否包含了一个session标识-称为session id,如果已经包含一个session id则说明以前已经为此客户创建过session,服务器就按照session id把这个session检索出来使用(如果检索不到,可能会新建一个,这种情况可能出现在服务端已经删除了该用户对应的session对象,但用户人为地在请求的URL后面附加上一个JSESSION的参数)。如果客户请求不包含session id,则为此客户创建一个session并且同时生成一个与此session相关联的session id,这个session id将在本次响应中返回给客户端保存。
|
||||
|
||||
session机制本身并不复杂,然而其实现和配置上的灵活性却使得具体情况复杂多变。这也要求我们不能把仅仅某一次的经验或者某一个浏览器,服务器的经验当作普遍适用的。
|
||||
|
||||
## 小结
|
||||
|
||||
如上文所述,session和cookie的目的相同,都是为了克服http协议无状态的缺陷,但完成的方法不同。session通过cookie,在客户端保存session id,而将用户的其他会话消息保存在服务端的session对象中,与此相对的,cookie需要将所有信息都保存在客户端。因此cookie存在着一定的安全隐患,例如本地cookie中保存的用户名密码被破译,或cookie被其他网站收集(例如:1. appA主动设置域B cookie,让域B cookie获取;2. XSS,在appA上通过javascript获取document.cookie,并传递给自己的appB)。
|
||||
|
||||
|
||||
通过上面的一些简单介绍我们了解了cookie和session的一些基础知识,知道他们之间的联系和区别,做web开发之前,有必要将一些必要知识了解清楚,才不会在用到时捉襟见肘,或是在调bug时候如无头苍蝇乱转。接下来的几小节我们将详细介绍session相关的知识。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [session和数据存储](<06.0.md>)
|
||||
* 下一节: [Go如何使用session](<06.2.md>)
|
||||
@@ -1,215 +1,215 @@
|
||||
# 6.2 Go如何使用session
|
||||
通过上一小节的介绍,我们知道session是在服务器端实现的一种用户和服务器之间认证的解决方案,目前Go标准包没有为session提供任何支持,这小节我们将会自己动手来实现go版本的session管理和创建。
|
||||
|
||||
## session创建过程
|
||||
session的基本原理是由服务器为每个会话维护一份信息数据,客户端和服务端依靠一个全局唯一的标识来访问这份数据,以达到交互的目的。当用户访问Web应用时,服务端程序会随需要创建session,这个过程可以概括为三个步骤:
|
||||
|
||||
- 生成全局唯一标识符(sessionid);
|
||||
- 开辟数据存储空间。一般会在内存中创建相应的数据结构,但这种情况下,系统一旦掉电,所有的会话数据就会丢失,如果是电子商务类网站,这将造成严重的后果。所以为了解决这类问题,你可以将会话数据写到文件里或存储在数据库中,当然这样会增加I/O开销,但是它可以实现某种程度的session持久化,也更有利于session的共享;
|
||||
- 将session的全局唯一标示符发送给客户端。
|
||||
|
||||
以上三个步骤中,最关键的是如何发送这个session的唯一标识这一步上。考虑到HTTP协议的定义,数据无非可以放到请求行、头域或Body里,所以一般来说会有两种常用的方式:cookie和URL重写。
|
||||
|
||||
1. Cookie
|
||||
服务端通过设置Set-cookie头就可以将session的标识符传送到客户端,而客户端此后的每一次请求都会带上这个标识符,另外一般包含session信息的cookie会将失效时间设置为0(会话cookie),即浏览器进程有效时间。至于浏览器怎么处理这个0,每个浏览器都有自己的方案,但差别都不会太大(一般体现在新建浏览器窗口的时候);
|
||||
2. URL重写
|
||||
所谓URL重写,就是在返回给用户的页面里的所有的URL后面追加session标识符,这样用户在收到响应之后,无论点击响应页面里的哪个链接或提交表单,都会自动带上session标识符,从而就实现了会话的保持。虽然这种做法比较麻烦,但是,如果客户端禁用了cookie的话,此种方案将会是首选。
|
||||
|
||||
## Go实现session管理
|
||||
通过上面session创建过程的讲解,读者应该对session有了一个大体的认识,但是具体到动态页面技术里面,又是怎么实现session的呢?下面我们将结合session的生命周期(lifecycle),来实现go语言版本的session管理。
|
||||
|
||||
### session管理设计
|
||||
我们知道session管理涉及到如下几个因素
|
||||
|
||||
- 全局session管理器
|
||||
- 保证sessionid 的全局唯一性
|
||||
- 为每个客户关联一个session
|
||||
- session 的存储(可以存储到内存、文件、数据库等)
|
||||
- session 过期处理
|
||||
|
||||
接下来我将讲解一下我关于session管理的整个设计思路以及相应的go代码示例:
|
||||
|
||||
### Session管理器
|
||||
|
||||
定义一个全局的session管理器
|
||||
|
||||
type Manager struct {
|
||||
cookieName string //private cookiename
|
||||
lock sync.Mutex // protects session
|
||||
provider Provider
|
||||
maxlifetime int64
|
||||
}
|
||||
|
||||
func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) {
|
||||
provider, ok := provides[provideName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName)
|
||||
}
|
||||
return &Manager{provider: provider, cookieName: cookieName, maxlifetime: maxlifetime}, nil
|
||||
}
|
||||
|
||||
Go实现整个的流程应该也是这样的,在main包中创建一个全局的session管理器
|
||||
|
||||
var globalSessions *session.Manager
|
||||
//然后在init函数中初始化
|
||||
func init() {
|
||||
globalSessions, _ = NewManager("memory","gosessionid",3600)
|
||||
}
|
||||
|
||||
我们知道session是保存在服务器端的数据,它可以以任何的方式存储,比如存储在内存、数据库或者文件中。因此我们抽象出一个Provider接口,用以表征session管理器底层存储结构。
|
||||
|
||||
type Provider interface {
|
||||
SessionInit(sid string) (Session, error)
|
||||
SessionRead(sid string) (Session, error)
|
||||
SessionDestroy(sid string) error
|
||||
SessionGC(maxLifeTime int64)
|
||||
}
|
||||
|
||||
- SessionInit函数实现Session的初始化,操作成功则返回此新的Session变量
|
||||
- SessionRead函数返回sid所代表的Session变量,如果不存在,那么将以sid为参数调用SessionInit函数创建并返回一个新的Session变量
|
||||
- SessionDestroy函数用来销毁sid对应的Session变量
|
||||
- SessionGC根据maxLifeTime来删除过期的数据
|
||||
|
||||
那么Session接口需要实现什么样的功能呢?有过Web开发经验的读者知道,对Session的处理基本就 设置值、读取值、删除值以及获取当前sessionID这四个操作,所以我们的Session接口也就实现这四个操作。
|
||||
|
||||
type Session interface {
|
||||
Set(key, value interface{}) error //set session value
|
||||
Get(key interface{}) interface{} //get session value
|
||||
Delete(key interface{}) error //delete session value
|
||||
SessionID() string //back current sessionID
|
||||
}
|
||||
|
||||
>以上设计思路来源于database/sql/driver,先定义好接口,然后具体的存储session的结构实现相应的接口并注册后,相应功能这样就可以使用了,以下是用来随需注册存储session的结构的Register函数的实现。
|
||||
|
||||
var provides = make(map[string]Provider)
|
||||
|
||||
// Register makes a session provide available by the provided name.
|
||||
// If Register is called twice with the same name or if driver is nil,
|
||||
// it panics.
|
||||
func Register(name string, provider Provider) {
|
||||
if provider == nil {
|
||||
panic("session: Register provide is nil")
|
||||
}
|
||||
if _, dup := provides[name]; dup {
|
||||
panic("session: Register called twice for provide " + name)
|
||||
}
|
||||
provides[name] = provider
|
||||
}
|
||||
|
||||
### 全局唯一的Session ID
|
||||
|
||||
Session ID是用来识别访问Web应用的每一个用户,因此必须保证它是全局唯一的(GUID),下面代码展示了如何满足这一需求:
|
||||
|
||||
func (manager *Manager) sessionId() string {
|
||||
b := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||
return ""
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
### session创建
|
||||
我们需要为每个来访用户分配或获取与他相关连的Session,以便后面根据Session信息来验证操作。SessionStart这个函数就是用来检测是否已经有某个Session与当前来访用户发生了关联,如果没有则创建之。
|
||||
|
||||
func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
|
||||
manager.lock.Lock()
|
||||
defer manager.lock.Unlock()
|
||||
cookie, err := r.Cookie(manager.cookieName)
|
||||
if err != nil || cookie.Value == "" {
|
||||
sid := manager.sessionId()
|
||||
session, _ = manager.provider.SessionInit(sid)
|
||||
cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)}
|
||||
http.SetCookie(w, &cookie)
|
||||
} else {
|
||||
sid, _ := url.QueryUnescape(cookie.Value)
|
||||
session, _ = manager.provider.SessionRead(sid)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
我们用前面login操作来演示session的运用:
|
||||
|
||||
func login(w http.ResponseWriter, r *http.Request) {
|
||||
sess := globalSessions.SessionStart(w, r)
|
||||
r.ParseForm()
|
||||
if r.Method == "GET" {
|
||||
t, _ := template.ParseFiles("login.gtpl")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
t.Execute(w, sess.Get("username"))
|
||||
} else {
|
||||
sess.Set("username", r.Form["username"])
|
||||
http.Redirect(w, r, "/", 302)
|
||||
}
|
||||
}
|
||||
|
||||
### 操作值:设置、读取和删除
|
||||
SessionStart函数返回的是一个满足Session接口的变量,那么我们该如何用他来对session数据进行操作呢?
|
||||
|
||||
上面的例子中的代码`session.Get("uid")`已经展示了基本的读取数据的操作,现在我们再来看一下详细的操作:
|
||||
|
||||
func count(w http.ResponseWriter, r *http.Request) {
|
||||
sess := globalSessions.SessionStart(w, r)
|
||||
createtime := sess.Get("createtime")
|
||||
if createtime == nil {
|
||||
sess.Set("createtime", time.Now().Unix())
|
||||
} else if (createtime.(int64) + 360) < (time.Now().Unix()) {
|
||||
globalSessions.SessionDestroy(w, r)
|
||||
sess = globalSessions.SessionStart(w, r)
|
||||
}
|
||||
ct := sess.Get("countnum")
|
||||
if ct == nil {
|
||||
sess.Set("countnum", 1)
|
||||
} else {
|
||||
sess.Set("countnum", (ct.(int) + 1))
|
||||
}
|
||||
t, _ := template.ParseFiles("count.gtpl")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
t.Execute(w, sess.Get("countnum"))
|
||||
}
|
||||
|
||||
通过上面的例子可以看到,Session的操作和操作key/value数据库类似:Set、Get、Delete等操作
|
||||
|
||||
因为Session有过期的概念,所以我们定义了GC操作,当访问过期时间满足GC的触发条件后将会引起GC,但是当我们进行了任意一个session操作,都会对Session实体进行更新,都会触发对最后访问时间的修改,这样当GC的时候就不会误删除还在使用的Session实体。
|
||||
|
||||
### session重置
|
||||
我们知道,Web应用中有用户退出这个操作,那么当用户退出应用的时候,我们需要对该用户的session数据进行销毁操作,上面的代码已经演示了如何使用session重置操作,下面这个函数就是实现了这个功能:
|
||||
|
||||
//Destroy sessionid
|
||||
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){
|
||||
cookie, err := r.Cookie(manager.cookieName)
|
||||
if err != nil || cookie.Value == "" {
|
||||
return
|
||||
} else {
|
||||
manager.lock.Lock()
|
||||
defer manager.lock.Unlock()
|
||||
manager.provider.SessionDestroy(cookie.Value)
|
||||
expiration := time.Now()
|
||||
cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1}
|
||||
http.SetCookie(w, &cookie)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
### session销毁
|
||||
我们来看一下Session管理器如何来管理销毁,只要我们在Main启动的时候启动:
|
||||
|
||||
func init() {
|
||||
go globalSessions.GC()
|
||||
}
|
||||
|
||||
func (manager *Manager) GC() {
|
||||
manager.lock.Lock()
|
||||
defer manager.lock.Unlock()
|
||||
manager.provider.SessionGC(manager.maxlifetime)
|
||||
time.AfterFunc(time.Duration(manager.maxlifetime), func() { manager.GC() })
|
||||
}
|
||||
|
||||
我们可以看到GC充分利用了time包中的定时器功能,当超时`maxLifeTime`之后调用GC函数,这样就可以保证`maxLifeTime`时间内的session都是可用的,类似的方案也可以用于统计在线用户数之类的。
|
||||
|
||||
## 总结
|
||||
至此 我们实现了一个用来在Web应用中全局管理Session的SessionManager,定义了用来提供Session存储实现Provider的接口,下一小节,我们将会通过接口定义来实现一些Provider,供大家参考学习。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [session和cookie](<06.1.md>)
|
||||
* 下一节: [session存储](<06.3.md>)
|
||||
# 6.2 Go如何使用session
|
||||
通过上一小节的介绍,我们知道session是在服务器端实现的一种用户和服务器之间认证的解决方案,目前Go标准包没有为session提供任何支持,这小节我们将会自己动手来实现go版本的session管理和创建。
|
||||
|
||||
## session创建过程
|
||||
session的基本原理是由服务器为每个会话维护一份信息数据,客户端和服务端依靠一个全局唯一的标识来访问这份数据,以达到交互的目的。当用户访问Web应用时,服务端程序会随需要创建session,这个过程可以概括为三个步骤:
|
||||
|
||||
- 生成全局唯一标识符(sessionid);
|
||||
- 开辟数据存储空间。一般会在内存中创建相应的数据结构,但这种情况下,系统一旦掉电,所有的会话数据就会丢失,如果是电子商务类网站,这将造成严重的后果。所以为了解决这类问题,你可以将会话数据写到文件里或存储在数据库中,当然这样会增加I/O开销,但是它可以实现某种程度的session持久化,也更有利于session的共享;
|
||||
- 将session的全局唯一标示符发送给客户端。
|
||||
|
||||
以上三个步骤中,最关键的是如何发送这个session的唯一标识这一步上。考虑到HTTP协议的定义,数据无非可以放到请求行、头域或Body里,所以一般来说会有两种常用的方式:cookie和URL重写。
|
||||
|
||||
1. Cookie
|
||||
服务端通过设置Set-cookie头就可以将session的标识符传送到客户端,而客户端此后的每一次请求都会带上这个标识符,另外一般包含session信息的cookie会将失效时间设置为0(会话cookie),即浏览器进程有效时间。至于浏览器怎么处理这个0,每个浏览器都有自己的方案,但差别都不会太大(一般体现在新建浏览器窗口的时候);
|
||||
2. URL重写
|
||||
所谓URL重写,就是在返回给用户的页面里的所有的URL后面追加session标识符,这样用户在收到响应之后,无论点击响应页面里的哪个链接或提交表单,都会自动带上session标识符,从而就实现了会话的保持。虽然这种做法比较麻烦,但是,如果客户端禁用了cookie的话,此种方案将会是首选。
|
||||
|
||||
## Go实现session管理
|
||||
通过上面session创建过程的讲解,读者应该对session有了一个大体的认识,但是具体到动态页面技术里面,又是怎么实现session的呢?下面我们将结合session的生命周期(lifecycle),来实现go语言版本的session管理。
|
||||
|
||||
### session管理设计
|
||||
我们知道session管理涉及到如下几个因素
|
||||
|
||||
- 全局session管理器
|
||||
- 保证sessionid 的全局唯一性
|
||||
- 为每个客户关联一个session
|
||||
- session 的存储(可以存储到内存、文件、数据库等)
|
||||
- session 过期处理
|
||||
|
||||
接下来我将讲解一下我关于session管理的整个设计思路以及相应的go代码示例:
|
||||
|
||||
### Session管理器
|
||||
|
||||
定义一个全局的session管理器
|
||||
|
||||
type Manager struct {
|
||||
cookieName string //private cookiename
|
||||
lock sync.Mutex // protects session
|
||||
provider Provider
|
||||
maxlifetime int64
|
||||
}
|
||||
|
||||
func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) {
|
||||
provider, ok := provides[provideName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName)
|
||||
}
|
||||
return &Manager{provider: provider, cookieName: cookieName, maxlifetime: maxlifetime}, nil
|
||||
}
|
||||
|
||||
Go实现整个的流程应该也是这样的,在main包中创建一个全局的session管理器
|
||||
|
||||
var globalSessions *session.Manager
|
||||
//然后在init函数中初始化
|
||||
func init() {
|
||||
globalSessions, _ = NewManager("memory","gosessionid",3600)
|
||||
}
|
||||
|
||||
我们知道session是保存在服务器端的数据,它可以以任何的方式存储,比如存储在内存、数据库或者文件中。因此我们抽象出一个Provider接口,用以表征session管理器底层存储结构。
|
||||
|
||||
type Provider interface {
|
||||
SessionInit(sid string) (Session, error)
|
||||
SessionRead(sid string) (Session, error)
|
||||
SessionDestroy(sid string) error
|
||||
SessionGC(maxLifeTime int64)
|
||||
}
|
||||
|
||||
- SessionInit函数实现Session的初始化,操作成功则返回此新的Session变量
|
||||
- SessionRead函数返回sid所代表的Session变量,如果不存在,那么将以sid为参数调用SessionInit函数创建并返回一个新的Session变量
|
||||
- SessionDestroy函数用来销毁sid对应的Session变量
|
||||
- SessionGC根据maxLifeTime来删除过期的数据
|
||||
|
||||
那么Session接口需要实现什么样的功能呢?有过Web开发经验的读者知道,对Session的处理基本就 设置值、读取值、删除值以及获取当前sessionID这四个操作,所以我们的Session接口也就实现这四个操作。
|
||||
|
||||
type Session interface {
|
||||
Set(key, value interface{}) error //set session value
|
||||
Get(key interface{}) interface{} //get session value
|
||||
Delete(key interface{}) error //delete session value
|
||||
SessionID() string //back current sessionID
|
||||
}
|
||||
|
||||
>以上设计思路来源于database/sql/driver,先定义好接口,然后具体的存储session的结构实现相应的接口并注册后,相应功能这样就可以使用了,以下是用来随需注册存储session的结构的Register函数的实现。
|
||||
|
||||
var provides = make(map[string]Provider)
|
||||
|
||||
// Register makes a session provide available by the provided name.
|
||||
// If Register is called twice with the same name or if driver is nil,
|
||||
// it panics.
|
||||
func Register(name string, provider Provider) {
|
||||
if provider == nil {
|
||||
panic("session: Register provide is nil")
|
||||
}
|
||||
if _, dup := provides[name]; dup {
|
||||
panic("session: Register called twice for provide " + name)
|
||||
}
|
||||
provides[name] = provider
|
||||
}
|
||||
|
||||
### 全局唯一的Session ID
|
||||
|
||||
Session ID是用来识别访问Web应用的每一个用户,因此必须保证它是全局唯一的(GUID),下面代码展示了如何满足这一需求:
|
||||
|
||||
func (manager *Manager) sessionId() string {
|
||||
b := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||
return ""
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
### session创建
|
||||
我们需要为每个来访用户分配或获取与他相关连的Session,以便后面根据Session信息来验证操作。SessionStart这个函数就是用来检测是否已经有某个Session与当前来访用户发生了关联,如果没有则创建之。
|
||||
|
||||
func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
|
||||
manager.lock.Lock()
|
||||
defer manager.lock.Unlock()
|
||||
cookie, err := r.Cookie(manager.cookieName)
|
||||
if err != nil || cookie.Value == "" {
|
||||
sid := manager.sessionId()
|
||||
session, _ = manager.provider.SessionInit(sid)
|
||||
cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)}
|
||||
http.SetCookie(w, &cookie)
|
||||
} else {
|
||||
sid, _ := url.QueryUnescape(cookie.Value)
|
||||
session, _ = manager.provider.SessionRead(sid)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
我们用前面login操作来演示session的运用:
|
||||
|
||||
func login(w http.ResponseWriter, r *http.Request) {
|
||||
sess := globalSessions.SessionStart(w, r)
|
||||
r.ParseForm()
|
||||
if r.Method == "GET" {
|
||||
t, _ := template.ParseFiles("login.gtpl")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
t.Execute(w, sess.Get("username"))
|
||||
} else {
|
||||
sess.Set("username", r.Form["username"])
|
||||
http.Redirect(w, r, "/", 302)
|
||||
}
|
||||
}
|
||||
|
||||
### 操作值:设置、读取和删除
|
||||
SessionStart函数返回的是一个满足Session接口的变量,那么我们该如何用他来对session数据进行操作呢?
|
||||
|
||||
上面的例子中的代码`session.Get("uid")`已经展示了基本的读取数据的操作,现在我们再来看一下详细的操作:
|
||||
|
||||
func count(w http.ResponseWriter, r *http.Request) {
|
||||
sess := globalSessions.SessionStart(w, r)
|
||||
createtime := sess.Get("createtime")
|
||||
if createtime == nil {
|
||||
sess.Set("createtime", time.Now().Unix())
|
||||
} else if (createtime.(int64) + 360) < (time.Now().Unix()) {
|
||||
globalSessions.SessionDestroy(w, r)
|
||||
sess = globalSessions.SessionStart(w, r)
|
||||
}
|
||||
ct := sess.Get("countnum")
|
||||
if ct == nil {
|
||||
sess.Set("countnum", 1)
|
||||
} else {
|
||||
sess.Set("countnum", (ct.(int) + 1))
|
||||
}
|
||||
t, _ := template.ParseFiles("count.gtpl")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
t.Execute(w, sess.Get("countnum"))
|
||||
}
|
||||
|
||||
通过上面的例子可以看到,Session的操作和操作key/value数据库类似:Set、Get、Delete等操作
|
||||
|
||||
因为Session有过期的概念,所以我们定义了GC操作,当访问过期时间满足GC的触发条件后将会引起GC,但是当我们进行了任意一个session操作,都会对Session实体进行更新,都会触发对最后访问时间的修改,这样当GC的时候就不会误删除还在使用的Session实体。
|
||||
|
||||
### session重置
|
||||
我们知道,Web应用中有用户退出这个操作,那么当用户退出应用的时候,我们需要对该用户的session数据进行销毁操作,上面的代码已经演示了如何使用session重置操作,下面这个函数就是实现了这个功能:
|
||||
|
||||
//Destroy sessionid
|
||||
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){
|
||||
cookie, err := r.Cookie(manager.cookieName)
|
||||
if err != nil || cookie.Value == "" {
|
||||
return
|
||||
} else {
|
||||
manager.lock.Lock()
|
||||
defer manager.lock.Unlock()
|
||||
manager.provider.SessionDestroy(cookie.Value)
|
||||
expiration := time.Now()
|
||||
cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1}
|
||||
http.SetCookie(w, &cookie)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
### session销毁
|
||||
我们来看一下Session管理器如何来管理销毁,只要我们在Main启动的时候启动:
|
||||
|
||||
func init() {
|
||||
go globalSessions.GC()
|
||||
}
|
||||
|
||||
func (manager *Manager) GC() {
|
||||
manager.lock.Lock()
|
||||
defer manager.lock.Unlock()
|
||||
manager.provider.SessionGC(manager.maxlifetime)
|
||||
time.AfterFunc(time.Duration(manager.maxlifetime), func() { manager.GC() })
|
||||
}
|
||||
|
||||
我们可以看到GC充分利用了time包中的定时器功能,当超时`maxLifeTime`之后调用GC函数,这样就可以保证`maxLifeTime`时间内的session都是可用的,类似的方案也可以用于统计在线用户数之类的。
|
||||
|
||||
## 总结
|
||||
至此 我们实现了一个用来在Web应用中全局管理Session的SessionManager,定义了用来提供Session存储实现Provider的接口,下一小节,我们将会通过接口定义来实现一些Provider,供大家参考学习。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [session和cookie](<06.1.md>)
|
||||
* 下一节: [session存储](<06.3.md>)
|
||||
215
zh/06.2.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
215
zh/06.2.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,215 @@
|
||||
# 6.2 Go如何使用session
|
||||
通过上一小节的介绍,我们知道session是在服务器端实现的一种用户和服务器之间认证的解决方案,目前Go标准包没有为session提供任何支持,这小节我们将会自己动手来实现go版本的session管理和创建。
|
||||
|
||||
## session创建过程
|
||||
session的基本原理是由服务器为每个会话维护一份信息数据,客户端和服务端依靠一个全局唯一的标识来访问这份数据,以达到交互的目的。当用户访问Web应用时,服务端程序会随需要创建session,这个过程可以概括为三个步骤:
|
||||
|
||||
- 生成全局唯一标识符(sessionid);
|
||||
- 开辟数据存储空间。一般会在内存中创建相应的数据结构,但这种情况下,系统一旦掉电,所有的会话数据就会丢失,如果是电子商务类网站,这将造成严重的后果。所以为了解决这类问题,你可以将会话数据写到文件里或存储在数据库中,当然这样会增加I/O开销,但是它可以实现某种程度的session持久化,也更有利于session的共享;
|
||||
- 将session的全局唯一标示符发送给客户端。
|
||||
|
||||
以上三个步骤中,最关键的是如何发送这个session的唯一标识这一步上。考虑到HTTP协议的定义,数据无非可以放到请求行、头域或Body里,所以一般来说会有两种常用的方式:cookie和URL重写。
|
||||
|
||||
1. Cookie
|
||||
服务端通过设置Set-cookie头就可以将session的标识符传送到客户端,而客户端此后的每一次请求都会带上这个标识符,另外一般包含session信息的cookie会将失效时间设置为0(会话cookie),即浏览器进程有效时间。至于浏览器怎么处理这个0,每个浏览器都有自己的方案,但差别都不会太大(一般体现在新建浏览器窗口的时候);
|
||||
2. URL重写
|
||||
所谓URL重写,就是在返回给用户的页面里的所有的URL后面追加session标识符,这样用户在收到响应之后,无论点击响应页面里的哪个链接或提交表单,都会自动带上session标识符,从而就实现了会话的保持。虽然这种做法比较麻烦,但是,如果客户端禁用了cookie的话,此种方案将会是首选。
|
||||
|
||||
## Go实现session管理
|
||||
通过上面session创建过程的讲解,读者应该对session有了一个大体的认识,但是具体到动态页面技术里面,又是怎么实现session的呢?下面我们将结合session的生命周期(lifecycle),来实现go语言版本的session管理。
|
||||
|
||||
### session管理设计
|
||||
我们知道session管理涉及到如下几个因素
|
||||
|
||||
- 全局session管理器
|
||||
- 保证sessionid 的全局唯一性
|
||||
- 为每个客户关联一个session
|
||||
- session 的存储(可以存储到内存、文件、数据库等)
|
||||
- session 过期处理
|
||||
|
||||
接下来我将讲解一下我关于session管理的整个设计思路以及相应的go代码示例:
|
||||
|
||||
### Session管理器
|
||||
|
||||
定义一个全局的session管理器
|
||||
|
||||
type Manager struct {
|
||||
cookieName string //private cookiename
|
||||
lock sync.Mutex // protects session
|
||||
provider Provider
|
||||
maxlifetime int64
|
||||
}
|
||||
|
||||
func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) {
|
||||
provider, ok := provides[provideName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName)
|
||||
}
|
||||
return &Manager{provider: provider, cookieName: cookieName, maxlifetime: maxlifetime}, nil
|
||||
}
|
||||
|
||||
Go实现整个的流程应该也是这样的,在main包中创建一个全局的session管理器
|
||||
|
||||
var globalSessions *session.Manager
|
||||
//然后在init函数中初始化
|
||||
func init() {
|
||||
globalSessions, _ = NewManager("memory","gosessionid",3600)
|
||||
}
|
||||
|
||||
我们知道session是保存在服务器端的数据,它可以以任何的方式存储,比如存储在内存、数据库或者文件中。因此我们抽象出一个Provider接口,用以表征session管理器底层存储结构。
|
||||
|
||||
type Provider interface {
|
||||
SessionInit(sid string) (Session, error)
|
||||
SessionRead(sid string) (Session, error)
|
||||
SessionDestroy(sid string) error
|
||||
SessionGC(maxLifeTime int64)
|
||||
}
|
||||
|
||||
- SessionInit函数实现Session的初始化,操作成功则返回此新的Session变量
|
||||
- SessionRead函数返回sid所代表的Session变量,如果不存在,那么将以sid为参数调用SessionInit函数创建并返回一个新的Session变量
|
||||
- SessionDestroy函数用来销毁sid对应的Session变量
|
||||
- SessionGC根据maxLifeTime来删除过期的数据
|
||||
|
||||
那么Session接口需要实现什么样的功能呢?有过Web开发经验的读者知道,对Session的处理基本就 设置值、读取值、删除值以及获取当前sessionID这四个操作,所以我们的Session接口也就实现这四个操作。
|
||||
|
||||
type Session interface {
|
||||
Set(key, value interface{}) error //set session value
|
||||
Get(key interface{}) interface{} //get session value
|
||||
Delete(key interface{}) error //delete session value
|
||||
SessionID() string //back current sessionID
|
||||
}
|
||||
|
||||
>以上设计思路来源于database/sql/driver,先定义好接口,然后具体的存储session的结构实现相应的接口并注册后,相应功能这样就可以使用了,以下是用来随需注册存储session的结构的Register函数的实现。
|
||||
|
||||
var provides = make(map[string]Provider)
|
||||
|
||||
// Register makes a session provide available by the provided name.
|
||||
// If Register is called twice with the same name or if driver is nil,
|
||||
// it panics.
|
||||
func Register(name string, provider Provider) {
|
||||
if provider == nil {
|
||||
panic("session: Register provide is nil")
|
||||
}
|
||||
if _, dup := provides[name]; dup {
|
||||
panic("session: Register called twice for provide " + name)
|
||||
}
|
||||
provides[name] = provider
|
||||
}
|
||||
|
||||
### 全局唯一的Session ID
|
||||
|
||||
Session ID是用来识别访问Web应用的每一个用户,因此必须保证它是全局唯一的(GUID),下面代码展示了如何满足这一需求:
|
||||
|
||||
func (manager *Manager) sessionId() string {
|
||||
b := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||
return ""
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
### session创建
|
||||
我们需要为每个来访用户分配或获取与他相关连的Session,以便后面根据Session信息来验证操作。SessionStart这个函数就是用来检测是否已经有某个Session与当前来访用户发生了关联,如果没有则创建之。
|
||||
|
||||
func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
|
||||
manager.lock.Lock()
|
||||
defer manager.lock.Unlock()
|
||||
cookie, err := r.Cookie(manager.cookieName)
|
||||
if err != nil || cookie.Value == "" {
|
||||
sid := manager.sessionId()
|
||||
session, _ = manager.provider.SessionInit(sid)
|
||||
cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)}
|
||||
http.SetCookie(w, &cookie)
|
||||
} else {
|
||||
sid, _ := url.QueryUnescape(cookie.Value)
|
||||
session, _ = manager.provider.SessionRead(sid)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
我们用前面login操作来演示session的运用:
|
||||
|
||||
func login(w http.ResponseWriter, r *http.Request) {
|
||||
sess := globalSessions.SessionStart(w, r)
|
||||
r.ParseForm()
|
||||
if r.Method == "GET" {
|
||||
t, _ := template.ParseFiles("login.gtpl")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
t.Execute(w, sess.Get("username"))
|
||||
} else {
|
||||
sess.Set("username", r.Form["username"])
|
||||
http.Redirect(w, r, "/", 302)
|
||||
}
|
||||
}
|
||||
|
||||
### 操作值:设置、读取和删除
|
||||
SessionStart函数返回的是一个满足Session接口的变量,那么我们该如何用他来对session数据进行操作呢?
|
||||
|
||||
上面的例子中的代码`session.Get("uid")`已经展示了基本的读取数据的操作,现在我们再来看一下详细的操作:
|
||||
|
||||
func count(w http.ResponseWriter, r *http.Request) {
|
||||
sess := globalSessions.SessionStart(w, r)
|
||||
createtime := sess.Get("createtime")
|
||||
if createtime == nil {
|
||||
sess.Set("createtime", time.Now().Unix())
|
||||
} else if (createtime.(int64) + 360) < (time.Now().Unix()) {
|
||||
globalSessions.SessionDestroy(w, r)
|
||||
sess = globalSessions.SessionStart(w, r)
|
||||
}
|
||||
ct := sess.Get("countnum")
|
||||
if ct == nil {
|
||||
sess.Set("countnum", 1)
|
||||
} else {
|
||||
sess.Set("countnum", (ct.(int) + 1))
|
||||
}
|
||||
t, _ := template.ParseFiles("count.gtpl")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
t.Execute(w, sess.Get("countnum"))
|
||||
}
|
||||
|
||||
通过上面的例子可以看到,Session的操作和操作key/value数据库类似:Set、Get、Delete等操作
|
||||
|
||||
因为Session有过期的概念,所以我们定义了GC操作,当访问过期时间满足GC的触发条件后将会引起GC,但是当我们进行了任意一个session操作,都会对Session实体进行更新,都会触发对最后访问时间的修改,这样当GC的时候就不会误删除还在使用的Session实体。
|
||||
|
||||
### session重置
|
||||
我们知道,Web应用中有用户退出这个操作,那么当用户退出应用的时候,我们需要对该用户的session数据进行销毁操作,上面的代码已经演示了如何使用session重置操作,下面这个函数就是实现了这个功能:
|
||||
|
||||
//Destroy sessionid
|
||||
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){
|
||||
cookie, err := r.Cookie(manager.cookieName)
|
||||
if err != nil || cookie.Value == "" {
|
||||
return
|
||||
} else {
|
||||
manager.lock.Lock()
|
||||
defer manager.lock.Unlock()
|
||||
manager.provider.SessionDestroy(cookie.Value)
|
||||
expiration := time.Now()
|
||||
cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1}
|
||||
http.SetCookie(w, &cookie)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
### session销毁
|
||||
我们来看一下Session管理器如何来管理销毁,只要我们在Main启动的时候启动:
|
||||
|
||||
func init() {
|
||||
go globalSessions.GC()
|
||||
}
|
||||
|
||||
func (manager *Manager) GC() {
|
||||
manager.lock.Lock()
|
||||
defer manager.lock.Unlock()
|
||||
manager.provider.SessionGC(manager.maxlifetime)
|
||||
time.AfterFunc(time.Duration(manager.maxlifetime), func() { manager.GC() })
|
||||
}
|
||||
|
||||
我们可以看到GC充分利用了time包中的定时器功能,当超时`maxLifeTime`之后调用GC函数,这样就可以保证`maxLifeTime`时间内的session都是可用的,类似的方案也可以用于统计在线用户数之类的。
|
||||
|
||||
## 总结
|
||||
至此 我们实现了一个用来在Web应用中全局管理Session的SessionManager,定义了用来提供Session存储实现Provider的接口,下一小节,我们将会通过接口定义来实现一些Provider,供大家参考学习。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [session和cookie](<06.1.md>)
|
||||
* 下一节: [session存储](<06.3.md>)
|
||||
@@ -1,137 +1,137 @@
|
||||
# 6.3 session存储
|
||||
上一节我们介绍了Session管理器的实现原理,定义了存储session的接口,这小节我们将示例一个基于内存的session存储接口的实现,其他的存储方式,读者可以自行参考示例来实现,内存的实现请看下面的例子代码
|
||||
|
||||
package memory
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"github.com/astaxie/session"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var pder = &Provider{list: list.New()}
|
||||
|
||||
type SessionStore struct {
|
||||
sid string //session id唯一标示
|
||||
timeAccessed time.Time //最后访问时间
|
||||
value map[interface{}]interface{} //session里面存储的值
|
||||
}
|
||||
|
||||
func (st *SessionStore) Set(key, value interface{}) error {
|
||||
st.value[key] = value
|
||||
pder.SessionUpdate(st.sid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *SessionStore) Get(key interface{}) interface{} {
|
||||
pder.SessionUpdate(st.sid)
|
||||
if v, ok := st.value[key]; ok {
|
||||
return v
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *SessionStore) Delete(key interface{}) error {
|
||||
delete(st.value, key)
|
||||
pder.SessionUpdate(st.sid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *SessionStore) SessionID() string {
|
||||
return st.sid
|
||||
}
|
||||
|
||||
type Provider struct {
|
||||
lock sync.Mutex //用来锁
|
||||
sessions map[string]*list.Element //用来存储在内存
|
||||
list *list.List //用来做gc
|
||||
}
|
||||
|
||||
func (pder *Provider) SessionInit(sid string) (session.Session, error) {
|
||||
pder.lock.Lock()
|
||||
defer pder.lock.Unlock()
|
||||
v := make(map[interface{}]interface{}, 0)
|
||||
newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v}
|
||||
element := pder.list.PushBack(newsess)
|
||||
pder.sessions[sid] = element
|
||||
return newsess, nil
|
||||
}
|
||||
|
||||
func (pder *Provider) SessionRead(sid string) (session.Session, error) {
|
||||
if element, ok := pder.sessions[sid]; ok {
|
||||
return element.Value.(*SessionStore), nil
|
||||
} else {
|
||||
sess, err := pder.SessionInit(sid)
|
||||
return sess, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (pder *Provider) SessionDestroy(sid string) error {
|
||||
if element, ok := pder.sessions[sid]; ok {
|
||||
delete(pder.sessions, sid)
|
||||
pder.list.Remove(element)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pder *Provider) SessionGC(maxlifetime int64) {
|
||||
pder.lock.Lock()
|
||||
defer pder.lock.Unlock()
|
||||
|
||||
for {
|
||||
element := pder.list.Back()
|
||||
if element == nil {
|
||||
break
|
||||
}
|
||||
if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() {
|
||||
pder.list.Remove(element)
|
||||
delete(pder.sessions, element.Value.(*SessionStore).sid)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pder *Provider) SessionUpdate(sid string) error {
|
||||
pder.lock.Lock()
|
||||
defer pder.lock.Unlock()
|
||||
if element, ok := pder.sessions[sid]; ok {
|
||||
element.Value.(*SessionStore).timeAccessed = time.Now()
|
||||
pder.list.MoveToFront(element)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
pder.sessions = make(map[string]*list.Element, 0)
|
||||
session.Register("memory", pder)
|
||||
}
|
||||
|
||||
上面这个代码实现了一个内存存储的session机制。通过init函数注册到session管理器中。这样就可以方便的调用了。我们如何来调用该引擎呢?请看下面的代码
|
||||
|
||||
import (
|
||||
"github.com/astaxie/session"
|
||||
_ "github.com/astaxie/session/providers/memory"
|
||||
)
|
||||
|
||||
当import的时候已经执行了memory函数里面的init函数,这样就已经注册到session管理器中,我们就可以使用了,通过如下方式就可以初始化一个session管理器:
|
||||
|
||||
var globalSessions *session.Manager
|
||||
|
||||
//然后在init函数中初始化
|
||||
func init() {
|
||||
globalSessions, _ = session.NewManager("memory", "gosessionid", 3600)
|
||||
go globalSessions.GC()
|
||||
}
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [Go如何使用session](<06.2.md>)
|
||||
* 下一节: [预防session劫持](<06.4.md>)
|
||||
# 6.3 session存储
|
||||
上一节我们介绍了Session管理器的实现原理,定义了存储session的接口,这小节我们将示例一个基于内存的session存储接口的实现,其他的存储方式,读者可以自行参考示例来实现,内存的实现请看下面的例子代码
|
||||
|
||||
package memory
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"github.com/astaxie/session"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var pder = &Provider{list: list.New()}
|
||||
|
||||
type SessionStore struct {
|
||||
sid string //session id唯一标示
|
||||
timeAccessed time.Time //最后访问时间
|
||||
value map[interface{}]interface{} //session里面存储的值
|
||||
}
|
||||
|
||||
func (st *SessionStore) Set(key, value interface{}) error {
|
||||
st.value[key] = value
|
||||
pder.SessionUpdate(st.sid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *SessionStore) Get(key interface{}) interface{} {
|
||||
pder.SessionUpdate(st.sid)
|
||||
if v, ok := st.value[key]; ok {
|
||||
return v
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *SessionStore) Delete(key interface{}) error {
|
||||
delete(st.value, key)
|
||||
pder.SessionUpdate(st.sid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *SessionStore) SessionID() string {
|
||||
return st.sid
|
||||
}
|
||||
|
||||
type Provider struct {
|
||||
lock sync.Mutex //用来锁
|
||||
sessions map[string]*list.Element //用来存储在内存
|
||||
list *list.List //用来做gc
|
||||
}
|
||||
|
||||
func (pder *Provider) SessionInit(sid string) (session.Session, error) {
|
||||
pder.lock.Lock()
|
||||
defer pder.lock.Unlock()
|
||||
v := make(map[interface{}]interface{}, 0)
|
||||
newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v}
|
||||
element := pder.list.PushBack(newsess)
|
||||
pder.sessions[sid] = element
|
||||
return newsess, nil
|
||||
}
|
||||
|
||||
func (pder *Provider) SessionRead(sid string) (session.Session, error) {
|
||||
if element, ok := pder.sessions[sid]; ok {
|
||||
return element.Value.(*SessionStore), nil
|
||||
} else {
|
||||
sess, err := pder.SessionInit(sid)
|
||||
return sess, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (pder *Provider) SessionDestroy(sid string) error {
|
||||
if element, ok := pder.sessions[sid]; ok {
|
||||
delete(pder.sessions, sid)
|
||||
pder.list.Remove(element)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pder *Provider) SessionGC(maxlifetime int64) {
|
||||
pder.lock.Lock()
|
||||
defer pder.lock.Unlock()
|
||||
|
||||
for {
|
||||
element := pder.list.Back()
|
||||
if element == nil {
|
||||
break
|
||||
}
|
||||
if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() {
|
||||
pder.list.Remove(element)
|
||||
delete(pder.sessions, element.Value.(*SessionStore).sid)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pder *Provider) SessionUpdate(sid string) error {
|
||||
pder.lock.Lock()
|
||||
defer pder.lock.Unlock()
|
||||
if element, ok := pder.sessions[sid]; ok {
|
||||
element.Value.(*SessionStore).timeAccessed = time.Now()
|
||||
pder.list.MoveToFront(element)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
pder.sessions = make(map[string]*list.Element, 0)
|
||||
session.Register("memory", pder)
|
||||
}
|
||||
|
||||
上面这个代码实现了一个内存存储的session机制。通过init函数注册到session管理器中。这样就可以方便的调用了。我们如何来调用该引擎呢?请看下面的代码
|
||||
|
||||
import (
|
||||
"github.com/astaxie/session"
|
||||
_ "github.com/astaxie/session/providers/memory"
|
||||
)
|
||||
|
||||
当import的时候已经执行了memory函数里面的init函数,这样就已经注册到session管理器中,我们就可以使用了,通过如下方式就可以初始化一个session管理器:
|
||||
|
||||
var globalSessions *session.Manager
|
||||
|
||||
//然后在init函数中初始化
|
||||
func init() {
|
||||
globalSessions, _ = session.NewManager("memory", "gosessionid", 3600)
|
||||
go globalSessions.GC()
|
||||
}
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [Go如何使用session](<06.2.md>)
|
||||
* 下一节: [预防session劫持](<06.4.md>)
|
||||
137
zh/06.3.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
137
zh/06.3.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,137 @@
|
||||
# 6.3 session存储
|
||||
上一节我们介绍了Session管理器的实现原理,定义了存储session的接口,这小节我们将示例一个基于内存的session存储接口的实现,其他的存储方式,读者可以自行参考示例来实现,内存的实现请看下面的例子代码
|
||||
|
||||
package memory
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"github.com/astaxie/session"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var pder = &Provider{list: list.New()}
|
||||
|
||||
type SessionStore struct {
|
||||
sid string //session id唯一标示
|
||||
timeAccessed time.Time //最后访问时间
|
||||
value map[interface{}]interface{} //session里面存储的值
|
||||
}
|
||||
|
||||
func (st *SessionStore) Set(key, value interface{}) error {
|
||||
st.value[key] = value
|
||||
pder.SessionUpdate(st.sid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *SessionStore) Get(key interface{}) interface{} {
|
||||
pder.SessionUpdate(st.sid)
|
||||
if v, ok := st.value[key]; ok {
|
||||
return v
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *SessionStore) Delete(key interface{}) error {
|
||||
delete(st.value, key)
|
||||
pder.SessionUpdate(st.sid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *SessionStore) SessionID() string {
|
||||
return st.sid
|
||||
}
|
||||
|
||||
type Provider struct {
|
||||
lock sync.Mutex //用来锁
|
||||
sessions map[string]*list.Element //用来存储在内存
|
||||
list *list.List //用来做gc
|
||||
}
|
||||
|
||||
func (pder *Provider) SessionInit(sid string) (session.Session, error) {
|
||||
pder.lock.Lock()
|
||||
defer pder.lock.Unlock()
|
||||
v := make(map[interface{}]interface{}, 0)
|
||||
newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v}
|
||||
element := pder.list.PushBack(newsess)
|
||||
pder.sessions[sid] = element
|
||||
return newsess, nil
|
||||
}
|
||||
|
||||
func (pder *Provider) SessionRead(sid string) (session.Session, error) {
|
||||
if element, ok := pder.sessions[sid]; ok {
|
||||
return element.Value.(*SessionStore), nil
|
||||
} else {
|
||||
sess, err := pder.SessionInit(sid)
|
||||
return sess, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (pder *Provider) SessionDestroy(sid string) error {
|
||||
if element, ok := pder.sessions[sid]; ok {
|
||||
delete(pder.sessions, sid)
|
||||
pder.list.Remove(element)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pder *Provider) SessionGC(maxlifetime int64) {
|
||||
pder.lock.Lock()
|
||||
defer pder.lock.Unlock()
|
||||
|
||||
for {
|
||||
element := pder.list.Back()
|
||||
if element == nil {
|
||||
break
|
||||
}
|
||||
if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() {
|
||||
pder.list.Remove(element)
|
||||
delete(pder.sessions, element.Value.(*SessionStore).sid)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pder *Provider) SessionUpdate(sid string) error {
|
||||
pder.lock.Lock()
|
||||
defer pder.lock.Unlock()
|
||||
if element, ok := pder.sessions[sid]; ok {
|
||||
element.Value.(*SessionStore).timeAccessed = time.Now()
|
||||
pder.list.MoveToFront(element)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
pder.sessions = make(map[string]*list.Element, 0)
|
||||
session.Register("memory", pder)
|
||||
}
|
||||
|
||||
上面这个代码实现了一个内存存储的session机制。通过init函数注册到session管理器中。这样就可以方便的调用了。我们如何来调用该引擎呢?请看下面的代码
|
||||
|
||||
import (
|
||||
"github.com/astaxie/session"
|
||||
_ "github.com/astaxie/session/providers/memory"
|
||||
)
|
||||
|
||||
当import的时候已经执行了memory函数里面的init函数,这样就已经注册到session管理器中,我们就可以使用了,通过如下方式就可以初始化一个session管理器:
|
||||
|
||||
var globalSessions *session.Manager
|
||||
|
||||
//然后在init函数中初始化
|
||||
func init() {
|
||||
globalSessions, _ = session.NewManager("memory", "gosessionid", 3600)
|
||||
go globalSessions.GC()
|
||||
}
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [Go如何使用session](<06.2.md>)
|
||||
* 下一节: [预防session劫持](<06.4.md>)
|
||||
@@ -1,89 +1,89 @@
|
||||
# 6.4 预防session劫持
|
||||
session劫持是一种广泛存在的比较严重的安全威胁,在session技术中,客户端和服务端通过session的标识符来维护会话, 但这个标识符很容易就能被嗅探到,从而被其他人利用.它是中间人攻击的一种类型。
|
||||
|
||||
本节将通过一个实例来演示会话劫持,希望通过这个实例,能让读者更好地理解session的本质。
|
||||
## session劫持过程
|
||||
我们写了如下的代码来展示一个count计数器:
|
||||
|
||||
func count(w http.ResponseWriter, r *http.Request) {
|
||||
sess := globalSessions.SessionStart(w, r)
|
||||
ct := sess.Get("countnum")
|
||||
if ct == nil {
|
||||
sess.Set("countnum", 1)
|
||||
} else {
|
||||
sess.Set("countnum", (ct.(int) + 1))
|
||||
}
|
||||
t, _ := template.ParseFiles("count.gtpl")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
t.Execute(w, sess.Get("countnum"))
|
||||
}
|
||||
|
||||
|
||||
count.gtpl的代码如下所示:
|
||||
|
||||
Hi. Now count:{{.}}
|
||||
|
||||
然后我们在浏览器里面刷新可以看到如下内容:
|
||||
|
||||

|
||||
|
||||
图6.4 浏览器端显示count数
|
||||
|
||||
随着刷新,数字将不断增长,当数字显示为6的时候,打开浏览器(以chrome为例)的cookie管理器,可以看到类似如下的信息:
|
||||
|
||||
|
||||

|
||||
|
||||
图6.5 获取浏览器端保存的cookie
|
||||
|
||||
下面这个步骤最为关键: 打开另一个浏览器(这里我打开了firefox浏览器),复制chrome地址栏里的地址到新打开的浏览器的地址栏中。然后打开firefox的cookie模拟插件,新建一个cookie,把按上图中cookie内容原样在firefox中重建一份:
|
||||
|
||||

|
||||
|
||||
图6.6 模拟cookie
|
||||
|
||||
回车后,你将看到如下内容:
|
||||
|
||||

|
||||
|
||||
图6.7 劫持session成功
|
||||
|
||||
可以看到虽然换了浏览器,但是我们却获得了sessionID,然后模拟了cookie存储的过程。这个例子是在同一台计算机上做的,不过即使换用两台来做,其结果仍然一样。此时如果交替点击两个浏览器里的链接你会发现它们其实操纵的是同一个计数器。不必惊讶,此处firefox盗用了chrome和goserver之间的维持会话的钥匙,即gosessionid,这是一种类型的“会话劫持”。在goserver看来,它从http请求中得到了一个gosessionid,由于HTTP协议的无状态性,它无法得知这个gosessionid是从chrome那里“劫持”来的,它依然会去查找对应的session,并执行相关计算。与此同时 chrome也无法得知自己保持的会话已经被“劫持”。
|
||||
## session劫持防范
|
||||
### cookieonly和token
|
||||
通过上面session劫持的简单演示可以了解到session一旦被其他人劫持,就非常危险,劫持者可以假装成被劫持者进行很多非法操作。那么如何有效的防止session劫持呢?
|
||||
|
||||
其中一个解决方案就是sessionID的值只允许cookie设置,而不是通过URL重置方式设置,同时设置cookie的httponly为true,这个属性是设置是否可通过客户端脚本访问这个设置的cookie,第一这个可以防止这个cookie被XSS读取从而引起session劫持,第二cookie设置不会像URL重置方式那么容易获取sessionID。
|
||||
|
||||
第二步就是在每个请求里面加上token,实现类似前面章节里面讲的防止form重复递交类似的功能,我们在每个请求里面加上一个隐藏的token,然后每次验证这个token,从而保证用户的请求都是唯一性。
|
||||
|
||||
h := md5.New()
|
||||
salt:="astaxie%^7&8888"
|
||||
io.WriteString(h,salt+time.Now().String())
|
||||
token:=fmt.Sprintf("%x",h.Sum(nil))
|
||||
if r.Form["token"]!=token{
|
||||
//提示登录
|
||||
}
|
||||
sess.Set("token",token)
|
||||
|
||||
|
||||
### 间隔生成新的SID
|
||||
还有一个解决方案就是,我们给session额外设置一个创建时间的值,一旦过了一定的时间,我们销毁这个sessionID,重新生成新的session,这样可以一定程度上防止session劫持的问题。
|
||||
|
||||
createtime := sess.Get("createtime")
|
||||
if createtime == nil {
|
||||
sess.Set("createtime", time.Now().Unix())
|
||||
} else if (createtime.(int64) + 60) < (time.Now().Unix()) {
|
||||
globalSessions.SessionDestroy(w, r)
|
||||
sess = globalSessions.SessionStart(w, r)
|
||||
}
|
||||
|
||||
session启动后,我们设置了一个值,用于记录生成sessionID的时间。通过判断每次请求是否过期(这里设置了60秒)定期生成新的ID,这样使得攻击者获取有效sessionID的机会大大降低。
|
||||
|
||||
上面两个手段的组合可以在实践中消除session劫持的风险,一方面, 由于sessionID频繁改变,使攻击者难有机会获取有效的sessionID;另一方面,因为sessionID只能在cookie中传递,然后设置了httponly,所以基于URL攻击的可能性为零,同时被XSS获取sessionID也不可能。最后,由于我们还设置了MaxAge=0,这样就相当于session cookie不会留在浏览器的历史记录里面。
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [session存储](<06.3.md>)
|
||||
* 下一节: [小结](<06.5.md>)
|
||||
# 6.4 预防session劫持
|
||||
session劫持是一种广泛存在的比较严重的安全威胁,在session技术中,客户端和服务端通过session的标识符来维护会话, 但这个标识符很容易就能被嗅探到,从而被其他人利用.它是中间人攻击的一种类型。
|
||||
|
||||
本节将通过一个实例来演示会话劫持,希望通过这个实例,能让读者更好地理解session的本质。
|
||||
## session劫持过程
|
||||
我们写了如下的代码来展示一个count计数器:
|
||||
|
||||
func count(w http.ResponseWriter, r *http.Request) {
|
||||
sess := globalSessions.SessionStart(w, r)
|
||||
ct := sess.Get("countnum")
|
||||
if ct == nil {
|
||||
sess.Set("countnum", 1)
|
||||
} else {
|
||||
sess.Set("countnum", (ct.(int) + 1))
|
||||
}
|
||||
t, _ := template.ParseFiles("count.gtpl")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
t.Execute(w, sess.Get("countnum"))
|
||||
}
|
||||
|
||||
|
||||
count.gtpl的代码如下所示:
|
||||
|
||||
Hi. Now count:{{.}}
|
||||
|
||||
然后我们在浏览器里面刷新可以看到如下内容:
|
||||
|
||||

|
||||
|
||||
图6.4 浏览器端显示count数
|
||||
|
||||
随着刷新,数字将不断增长,当数字显示为6的时候,打开浏览器(以chrome为例)的cookie管理器,可以看到类似如下的信息:
|
||||
|
||||
|
||||

|
||||
|
||||
图6.5 获取浏览器端保存的cookie
|
||||
|
||||
下面这个步骤最为关键: 打开另一个浏览器(这里我打开了firefox浏览器),复制chrome地址栏里的地址到新打开的浏览器的地址栏中。然后打开firefox的cookie模拟插件,新建一个cookie,把按上图中cookie内容原样在firefox中重建一份:
|
||||
|
||||

|
||||
|
||||
图6.6 模拟cookie
|
||||
|
||||
回车后,你将看到如下内容:
|
||||
|
||||

|
||||
|
||||
图6.7 劫持session成功
|
||||
|
||||
可以看到虽然换了浏览器,但是我们却获得了sessionID,然后模拟了cookie存储的过程。这个例子是在同一台计算机上做的,不过即使换用两台来做,其结果仍然一样。此时如果交替点击两个浏览器里的链接你会发现它们其实操纵的是同一个计数器。不必惊讶,此处firefox盗用了chrome和goserver之间的维持会话的钥匙,即gosessionid,这是一种类型的“会话劫持”。在goserver看来,它从http请求中得到了一个gosessionid,由于HTTP协议的无状态性,它无法得知这个gosessionid是从chrome那里“劫持”来的,它依然会去查找对应的session,并执行相关计算。与此同时 chrome也无法得知自己保持的会话已经被“劫持”。
|
||||
## session劫持防范
|
||||
### cookieonly和token
|
||||
通过上面session劫持的简单演示可以了解到session一旦被其他人劫持,就非常危险,劫持者可以假装成被劫持者进行很多非法操作。那么如何有效的防止session劫持呢?
|
||||
|
||||
其中一个解决方案就是sessionID的值只允许cookie设置,而不是通过URL重置方式设置,同时设置cookie的httponly为true,这个属性是设置是否可通过客户端脚本访问这个设置的cookie,第一这个可以防止这个cookie被XSS读取从而引起session劫持,第二cookie设置不会像URL重置方式那么容易获取sessionID。
|
||||
|
||||
第二步就是在每个请求里面加上token,实现类似前面章节里面讲的防止form重复递交类似的功能,我们在每个请求里面加上一个隐藏的token,然后每次验证这个token,从而保证用户的请求都是唯一性。
|
||||
|
||||
h := md5.New()
|
||||
salt:="astaxie%^7&8888"
|
||||
io.WriteString(h,salt+time.Now().String())
|
||||
token:=fmt.Sprintf("%x",h.Sum(nil))
|
||||
if r.Form["token"]!=token{
|
||||
//提示登录
|
||||
}
|
||||
sess.Set("token",token)
|
||||
|
||||
|
||||
### 间隔生成新的SID
|
||||
还有一个解决方案就是,我们给session额外设置一个创建时间的值,一旦过了一定的时间,我们销毁这个sessionID,重新生成新的session,这样可以一定程度上防止session劫持的问题。
|
||||
|
||||
createtime := sess.Get("createtime")
|
||||
if createtime == nil {
|
||||
sess.Set("createtime", time.Now().Unix())
|
||||
} else if (createtime.(int64) + 60) < (time.Now().Unix()) {
|
||||
globalSessions.SessionDestroy(w, r)
|
||||
sess = globalSessions.SessionStart(w, r)
|
||||
}
|
||||
|
||||
session启动后,我们设置了一个值,用于记录生成sessionID的时间。通过判断每次请求是否过期(这里设置了60秒)定期生成新的ID,这样使得攻击者获取有效sessionID的机会大大降低。
|
||||
|
||||
上面两个手段的组合可以在实践中消除session劫持的风险,一方面, 由于sessionID频繁改变,使攻击者难有机会获取有效的sessionID;另一方面,因为sessionID只能在cookie中传递,然后设置了httponly,所以基于URL攻击的可能性为零,同时被XSS获取sessionID也不可能。最后,由于我们还设置了MaxAge=0,这样就相当于session cookie不会留在浏览器的历史记录里面。
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [session存储](<06.3.md>)
|
||||
* 下一节: [小结](<06.5.md>)
|
||||
89
zh/06.4.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
89
zh/06.4.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,89 @@
|
||||
# 6.4 预防session劫持
|
||||
session劫持是一种广泛存在的比较严重的安全威胁,在session技术中,客户端和服务端通过session的标识符来维护会话, 但这个标识符很容易就能被嗅探到,从而被其他人利用.它是中间人攻击的一种类型。
|
||||
|
||||
本节将通过一个实例来演示会话劫持,希望通过这个实例,能让读者更好地理解session的本质。
|
||||
## session劫持过程
|
||||
我们写了如下的代码来展示一个count计数器:
|
||||
|
||||
func count(w http.ResponseWriter, r *http.Request) {
|
||||
sess := globalSessions.SessionStart(w, r)
|
||||
ct := sess.Get("countnum")
|
||||
if ct == nil {
|
||||
sess.Set("countnum", 1)
|
||||
} else {
|
||||
sess.Set("countnum", (ct.(int) + 1))
|
||||
}
|
||||
t, _ := template.ParseFiles("count.gtpl")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
t.Execute(w, sess.Get("countnum"))
|
||||
}
|
||||
|
||||
|
||||
count.gtpl的代码如下所示:
|
||||
|
||||
Hi. Now count:{{.}}
|
||||
|
||||
然后我们在浏览器里面刷新可以看到如下内容:
|
||||
|
||||

|
||||
|
||||
图6.4 浏览器端显示count数
|
||||
|
||||
随着刷新,数字将不断增长,当数字显示为6的时候,打开浏览器(以chrome为例)的cookie管理器,可以看到类似如下的信息:
|
||||
|
||||
|
||||

|
||||
|
||||
图6.5 获取浏览器端保存的cookie
|
||||
|
||||
下面这个步骤最为关键: 打开另一个浏览器(这里我打开了firefox浏览器),复制chrome地址栏里的地址到新打开的浏览器的地址栏中。然后打开firefox的cookie模拟插件,新建一个cookie,把按上图中cookie内容原样在firefox中重建一份:
|
||||
|
||||

|
||||
|
||||
图6.6 模拟cookie
|
||||
|
||||
回车后,你将看到如下内容:
|
||||
|
||||

|
||||
|
||||
图6.7 劫持session成功
|
||||
|
||||
可以看到虽然换了浏览器,但是我们却获得了sessionID,然后模拟了cookie存储的过程。这个例子是在同一台计算机上做的,不过即使换用两台来做,其结果仍然一样。此时如果交替点击两个浏览器里的链接你会发现它们其实操纵的是同一个计数器。不必惊讶,此处firefox盗用了chrome和goserver之间的维持会话的钥匙,即gosessionid,这是一种类型的“会话劫持”。在goserver看来,它从http请求中得到了一个gosessionid,由于HTTP协议的无状态性,它无法得知这个gosessionid是从chrome那里“劫持”来的,它依然会去查找对应的session,并执行相关计算。与此同时 chrome也无法得知自己保持的会话已经被“劫持”。
|
||||
## session劫持防范
|
||||
### cookieonly和token
|
||||
通过上面session劫持的简单演示可以了解到session一旦被其他人劫持,就非常危险,劫持者可以假装成被劫持者进行很多非法操作。那么如何有效的防止session劫持呢?
|
||||
|
||||
其中一个解决方案就是sessionID的值只允许cookie设置,而不是通过URL重置方式设置,同时设置cookie的httponly为true,这个属性是设置是否可通过客户端脚本访问这个设置的cookie,第一这个可以防止这个cookie被XSS读取从而引起session劫持,第二cookie设置不会像URL重置方式那么容易获取sessionID。
|
||||
|
||||
第二步就是在每个请求里面加上token,实现类似前面章节里面讲的防止form重复递交类似的功能,我们在每个请求里面加上一个隐藏的token,然后每次验证这个token,从而保证用户的请求都是唯一性。
|
||||
|
||||
h := md5.New()
|
||||
salt:="astaxie%^7&8888"
|
||||
io.WriteString(h,salt+time.Now().String())
|
||||
token:=fmt.Sprintf("%x",h.Sum(nil))
|
||||
if r.Form["token"]!=token{
|
||||
//提示登录
|
||||
}
|
||||
sess.Set("token",token)
|
||||
|
||||
|
||||
### 间隔生成新的SID
|
||||
还有一个解决方案就是,我们给session额外设置一个创建时间的值,一旦过了一定的时间,我们销毁这个sessionID,重新生成新的session,这样可以一定程度上防止session劫持的问题。
|
||||
|
||||
createtime := sess.Get("createtime")
|
||||
if createtime == nil {
|
||||
sess.Set("createtime", time.Now().Unix())
|
||||
} else if (createtime.(int64) + 60) < (time.Now().Unix()) {
|
||||
globalSessions.SessionDestroy(w, r)
|
||||
sess = globalSessions.SessionStart(w, r)
|
||||
}
|
||||
|
||||
session启动后,我们设置了一个值,用于记录生成sessionID的时间。通过判断每次请求是否过期(这里设置了60秒)定期生成新的ID,这样使得攻击者获取有效sessionID的机会大大降低。
|
||||
|
||||
上面两个手段的组合可以在实践中消除session劫持的风险,一方面, 由于sessionID频繁改变,使攻击者难有机会获取有效的sessionID;另一方面,因为sessionID只能在cookie中传递,然后设置了httponly,所以基于URL攻击的可能性为零,同时被XSS获取sessionID也不可能。最后,由于我们还设置了MaxAge=0,这样就相当于session cookie不会留在浏览器的历史记录里面。
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [session存储](<06.3.md>)
|
||||
* 下一节: [小结](<06.5.md>)
|
||||
@@ -1,6 +1,6 @@
|
||||
# 6.5 小结
|
||||
这章我们学习了什么是session,什么是cookie,以及他们两者之间的关系。但是目前Go官方标准包里面不支持session,所以我们设计了一个session管理器,实现了session从创建到销毁的整个过程。然后定义了Provider的接口,使得可以支持各种后端的session存储,然后我们在第三小节里面介绍了如何使用内存存储来实现session的管理。第四小节我们讲解了session劫持的过程,以及我们如何有效的来防止session劫持。通过这一章的讲解,希望能够让读者了解整个sesison的执行原理以及如何实现,而且是如何更加安全的使用session。
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [session存储](<06.4.md>)
|
||||
* 下一章: [文本处理](<07.0.md>)
|
||||
# 6.5 小结
|
||||
这章我们学习了什么是session,什么是cookie,以及他们两者之间的关系。但是目前Go官方标准包里面不支持session,所以我们设计了一个session管理器,实现了session从创建到销毁的整个过程。然后定义了Provider的接口,使得可以支持各种后端的session存储,然后我们在第三小节里面介绍了如何使用内存存储来实现session的管理。第四小节我们讲解了session劫持的过程,以及我们如何有效的来防止session劫持。通过这一章的讲解,希望能够让读者了解整个sesison的执行原理以及如何实现,而且是如何更加安全的使用session。
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [session存储](<06.4.md>)
|
||||
* 下一章: [文本处理](<07.0.md>)
|
||||
6
zh/06.5.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
6
zh/06.5.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,6 @@
|
||||
# 6.5 小结
|
||||
这章我们学习了什么是session,什么是cookie,以及他们两者之间的关系。但是目前Go官方标准包里面不支持session,所以我们设计了一个session管理器,实现了session从创建到销毁的整个过程。然后定义了Provider的接口,使得可以支持各种后端的session存储,然后我们在第三小节里面介绍了如何使用内存存储来实现session的管理。第四小节我们讲解了session劫持的过程,以及我们如何有效的来防止session劫持。通过这一章的讲解,希望能够让读者了解整个sesison的执行原理以及如何实现,而且是如何更加安全的使用session。
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [session存储](<06.4.md>)
|
||||
* 下一章: [文本处理](<07.0.md>)
|
||||
@@ -1,12 +1,12 @@
|
||||
# 7 文本处理
|
||||
Web开发中对于文本处理是非常重要的一部分,我们往往需要对输出或者输入的内容进行处理,这里的文本包括字符串、数字、Json、XMl等等。Go语言作为一门高性能的语言,对这些文本的处理都有官方的标准库来支持。而且在你使用中你会发现Go标准库的一些设计相当的巧妙,而且对于使用者来说也很方便就能处理这些文本。本章我们将通过四个小节的介绍,让用户对Go语言处理文本有一个很好的认识。
|
||||
|
||||
XML是目前很多标准接口的交互语言,很多时候和一些Java编写的webserver进行交互都是基于XML标准进行交互,7.1小节将介绍如何处理XML文本,我们使用XML之后发现它太复杂了,现在很多互联网企业对外的API大多数采用了JSON格式,这种格式描述简单,但是又能很好的表达意思,7.2小节我们将讲述如何来处理这样的JSON格式数据。正则是一个让人又爱又恨的工具,它处理文本的能力非常强大,我们在前面表单验证里面已经有所领略它的强大,7.3小节将详细的更深入的讲解如何利用好Go的正则。Web开发中一个很重要的部分就是MVC分离,在Go语言的Web开发中V有一个专门的包来支持`template`,7.4小节将详细的讲解如何使用模版来进行输出内容。7.5小节将详细介绍如何进行文件和文件夹的操作。7.6小结介绍了字符串的相关操作。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第六章总结](<06.5.md>)
|
||||
* 下一节: [XML处理](<07.1.md>)
|
||||
# 7 文本处理
|
||||
Web开发中对于文本处理是非常重要的一部分,我们往往需要对输出或者输入的内容进行处理,这里的文本包括字符串、数字、Json、XMl等等。Go语言作为一门高性能的语言,对这些文本的处理都有官方的标准库来支持。而且在你使用中你会发现Go标准库的一些设计相当的巧妙,而且对于使用者来说也很方便就能处理这些文本。本章我们将通过四个小节的介绍,让用户对Go语言处理文本有一个很好的认识。
|
||||
|
||||
XML是目前很多标准接口的交互语言,很多时候和一些Java编写的webserver进行交互都是基于XML标准进行交互,7.1小节将介绍如何处理XML文本,我们使用XML之后发现它太复杂了,现在很多互联网企业对外的API大多数采用了JSON格式,这种格式描述简单,但是又能很好的表达意思,7.2小节我们将讲述如何来处理这样的JSON格式数据。正则是一个让人又爱又恨的工具,它处理文本的能力非常强大,我们在前面表单验证里面已经有所领略它的强大,7.3小节将详细的更深入的讲解如何利用好Go的正则。Web开发中一个很重要的部分就是MVC分离,在Go语言的Web开发中V有一个专门的包来支持`template`,7.4小节将详细的讲解如何使用模版来进行输出内容。7.5小节将详细介绍如何进行文件和文件夹的操作。7.6小结介绍了字符串的相关操作。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第六章总结](<06.5.md>)
|
||||
* 下一节: [XML处理](<07.1.md>)
|
||||
12
zh/07.0.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
12
zh/07.0.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,12 @@
|
||||
# 7 文本处理
|
||||
Web开发中对于文本处理是非常重要的一部分,我们往往需要对输出或者输入的内容进行处理,这里的文本包括字符串、数字、Json、XMl等等。Go语言作为一门高性能的语言,对这些文本的处理都有官方的标准库来支持。而且在你使用中你会发现Go标准库的一些设计相当的巧妙,而且对于使用者来说也很方便就能处理这些文本。本章我们将通过四个小节的介绍,让用户对Go语言处理文本有一个很好的认识。
|
||||
|
||||
XML是目前很多标准接口的交互语言,很多时候和一些Java编写的webserver进行交互都是基于XML标准进行交互,7.1小节将介绍如何处理XML文本,我们使用XML之后发现它太复杂了,现在很多互联网企业对外的API大多数采用了JSON格式,这种格式描述简单,但是又能很好的表达意思,7.2小节我们将讲述如何来处理这样的JSON格式数据。正则是一个让人又爱又恨的工具,它处理文本的能力非常强大,我们在前面表单验证里面已经有所领略它的强大,7.3小节将详细的更深入的讲解如何利用好Go的正则。Web开发中一个很重要的部分就是MVC分离,在Go语言的Web开发中V有一个专门的包来支持`template`,7.4小节将详细的讲解如何使用模版来进行输出内容。7.5小节将详细介绍如何进行文件和文件夹的操作。7.6小结介绍了字符串的相关操作。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第六章总结](<06.5.md>)
|
||||
* 下一节: [XML处理](<07.1.md>)
|
||||
@@ -1,221 +1,221 @@
|
||||
# 7.1 XML处理
|
||||
XML作为一种数据交换和信息传递的格式已经十分普及。而随着Web服务日益广泛的应用,现在XML在日常的开发工作中也扮演了愈发重要的角色。这一小节, 我们将就Go语言标准包中的XML相关处理的包进行介绍。
|
||||
|
||||
这个小节不会涉及XML规范相关的内容(如需了解相关知识请参考其他文献),而是介绍如何用Go语言来编解码XML文件相关的知识。
|
||||
|
||||
假如你是一名运维人员,你为你所管理的所有服务器生成了如下内容的xml的配置文件:
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<servers version="1">
|
||||
<server>
|
||||
<serverName>Shanghai_VPN</serverName>
|
||||
<serverIP>127.0.0.1</serverIP>
|
||||
</server>
|
||||
<server>
|
||||
<serverName>Beijing_VPN</serverName>
|
||||
<serverIP>127.0.0.2</serverIP>
|
||||
</server>
|
||||
</servers>
|
||||
|
||||
上面的XML文档描述了两个服务器的信息,包含了服务器名和服务器的IP信息,接下来的Go例子以此XML描述的信息进行操作。
|
||||
|
||||
## 解析XML
|
||||
如何解析如上这个XML文件呢? 我们可以通过xml包的`Unmarshal`函数来达到我们的目的
|
||||
|
||||
func Unmarshal(data []byte, v interface{}) error
|
||||
|
||||
data接收的是XML数据流,v是需要输出的结构,定义为interface,也就是可以把XML转换为任意的格式。我们这里主要介绍struct的转换,因为struct和XML都有类似树结构的特征。
|
||||
|
||||
示例代码如下:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Recurlyservers struct {
|
||||
XMLName xml.Name `xml:"servers"`
|
||||
Version string `xml:"version,attr"`
|
||||
Svs []server `xml:"server"`
|
||||
Description string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type server struct {
|
||||
XMLName xml.Name `xml:"server"`
|
||||
ServerName string `xml:"serverName"`
|
||||
ServerIP string `xml:"serverIP"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
file, err := os.Open("servers.xml") // For read access.
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v", err)
|
||||
return
|
||||
}
|
||||
v := Recurlyservers{}
|
||||
err = xml.Unmarshal(data, &v)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(v)
|
||||
}
|
||||
|
||||
|
||||
XML本质上是一种树形的数据格式,而我们可以定义与之匹配的go 语言的 struct类型,然后通过xml.Unmarshal来将xml中的数据解析成对应的struct对象。如上例子输出如下数据
|
||||
|
||||
{{ servers} 1 [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}]
|
||||
<server>
|
||||
<serverName>Shanghai_VPN</serverName>
|
||||
<serverIP>127.0.0.1</serverIP>
|
||||
</server>
|
||||
<server>
|
||||
<serverName>Beijing_VPN</serverName>
|
||||
<serverIP>127.0.0.2</serverIP>
|
||||
</server>
|
||||
}
|
||||
|
||||
|
||||
上面的例子中,将xml文件解析成对应的struct对象是通过`xml.Unmarshal`来完成的,这个过程是如何实现的?可以看到我们的struct定义后面多了一些类似于`xml:"serverName"`这样的内容,这个是struct的一个特性,它们被称为 struct tag,它们是用来辅助反射的。我们来看一下`Unmarshal`的定义:
|
||||
|
||||
func Unmarshal(data []byte, v interface{}) error
|
||||
|
||||
我们看到函数定义了两个参数,第一个是XML数据流,第二个是存储的对应类型,目前支持struct、slice和string,XML包内部采用了反射来进行数据的映射,所以v里面的字段必须是导出的。`Unmarshal`解析的时候XML元素和字段怎么对应起来的呢?这是有一个优先级读取流程的,首先会读取struct tag,如果没有,那么就会对应字段名。必须注意一点的是解析的时候tag、字段名、XML元素都是大小写敏感的,所以必须一一对应字段。
|
||||
|
||||
Go语言的反射机制,可以利用这些tag信息来将来自XML文件中的数据反射成对应的struct对象,关于反射如何利用struct tag的更多内容请参阅reflect中的相关内容。
|
||||
|
||||
解析XML到struct的时候遵循如下的规则:
|
||||
|
||||
- 如果struct的一个字段是string或者[]byte类型且它的tag含有`",innerxml"`,Unmarshal将会将此字段所对应的元素内所有内嵌的原始xml累加到此字段上,如上面例子Description定义。最后的输出是
|
||||
|
||||
<server>
|
||||
<serverName>Shanghai_VPN</serverName>
|
||||
<serverIP>127.0.0.1</serverIP>
|
||||
</server>
|
||||
<server>
|
||||
<serverName>Beijing_VPN</serverName>
|
||||
<serverIP>127.0.0.2</serverIP>
|
||||
</server>
|
||||
|
||||
- 如果struct中有一个叫做XMLName,且类型为xml.Name字段,那么在解析的时候就会保存这个element的名字到该字段,如上面例子中的servers。
|
||||
- 如果某个struct字段的tag定义中含有XML结构中element的名称,那么解析的时候就会把相应的element值赋值给该字段,如上servername和serverip定义。
|
||||
- 如果某个struct字段的tag定义了中含有`",attr"`,那么解析的时候就会将该结构所对应的element的与字段同名的属性的值赋值给该字段,如上version定义。
|
||||
- 如果某个struct字段的tag定义 型如`"a>b>c"`,则解析的时候,会将xml结构a下面的b下面的c元素的值赋值给该字段。
|
||||
- 如果某个struct字段的tag定义了`"-"`,那么不会为该字段解析匹配任何xml数据。
|
||||
- 如果struct字段后面的tag定义了`",any"`,如果他的子元素在不满足其他的规则的时候就会匹配到这个字段。
|
||||
- 如果某个XML元素包含一条或者多条注释,那么这些注释将被累加到第一个tag含有",comments"的字段上,这个字段的类型可能是[]byte或string,如果没有这样的字段存在,那么注释将会被抛弃。
|
||||
|
||||
上面详细讲述了如何定义struct的tag。 只要设置对了tag,那么XML解析就如上面示例般简单,tag和XML的element是一一对应的关系,如上所示,我们还可以通过slice来表示多个同级元素。
|
||||
|
||||
>注意: 为了正确解析,go语言的xml包要求struct定义中的所有字段必须是可导出的(即首字母大写)
|
||||
|
||||
## 输出XML
|
||||
假若我们不是要解析如上所示的XML文件,而是生成它,那么在go语言中又该如何实现呢? xml包中提供了`Marshal`和`MarshalIndent`两个函数,来满足我们的需求。这两个函数主要的区别是第二个函数会增加前缀和缩进,函数的定义如下所示:
|
||||
|
||||
func Marshal(v interface{}) ([]byte, error)
|
||||
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
|
||||
|
||||
两个函数第一个参数是用来生成XML的结构定义类型数据,都是返回生成的XML数据流。
|
||||
|
||||
下面我们来看一下如何输出如上的XML:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Servers struct {
|
||||
XMLName xml.Name `xml:"servers"`
|
||||
Version string `xml:"version,attr"`
|
||||
Svs []server `xml:"server"`
|
||||
}
|
||||
|
||||
type server struct {
|
||||
ServerName string `xml:"serverName"`
|
||||
ServerIP string `xml:"serverIP"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
v := &Servers{Version: "1"}
|
||||
v.Svs = append(v.Svs, server{"Shanghai_VPN", "127.0.0.1"})
|
||||
v.Svs = append(v.Svs, server{"Beijing_VPN", "127.0.0.2"})
|
||||
output, err := xml.MarshalIndent(v, " ", " ")
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
}
|
||||
os.Stdout.Write([]byte(xml.Header))
|
||||
|
||||
os.Stdout.Write(output)
|
||||
}
|
||||
上面的代码输出如下信息:
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<servers version="1">
|
||||
<server>
|
||||
<serverName>Shanghai_VPN</serverName>
|
||||
<serverIP>127.0.0.1</serverIP>
|
||||
</server>
|
||||
<server>
|
||||
<serverName>Beijing_VPN</serverName>
|
||||
<serverIP>127.0.0.2</serverIP>
|
||||
</server>
|
||||
</servers>
|
||||
|
||||
和我们之前定义的文件的格式一模一样,之所以会有`os.Stdout.Write([]byte(xml.Header))` 这句代码的出现,是因为`xml.MarshalIndent`或者`xml.Marshal`输出的信息都是不带XML头的,为了生成正确的xml文件,我们使用了xml包预定义的Header变量。
|
||||
|
||||
我们看到`Marshal`函数接收的参数v是interface{}类型的,即它可以接受任意类型的参数,那么xml包,根据什么规则来生成相应的XML文件呢?
|
||||
|
||||
- 如果v是 array或者slice,那么输出每一个元素,类似<type>value</type>
|
||||
- 如果v是指针,那么会Marshal指针指向的内容,如果指针为空,什么都不输出
|
||||
- 如果v是interface,那么就处理interface所包含的数据
|
||||
- 如果v是其他数据类型,就会输出这个数据类型所拥有的字段信息
|
||||
|
||||
生成的XML文件中的element的名字又是根据什么决定的呢?元素名按照如下优先级从struct中获取:
|
||||
|
||||
- 如果v是struct,XMLName的tag中定义的名称
|
||||
- 类型为xml.Name的名叫XMLName的字段的值
|
||||
- 通过struct中字段的tag来获取
|
||||
- 通过struct的字段名用来获取
|
||||
- marshall的类型名称
|
||||
|
||||
我们应如何设置struct 中字段的tag信息以控制最终xml文件的生成呢?
|
||||
|
||||
- XMLName不会被输出
|
||||
- tag中含有`"-"`的字段不会输出
|
||||
- tag中含有`"name,attr"`,会以name作为属性名,字段值作为值输出为这个XML元素的属性,如上version字段所描述
|
||||
- tag中含有`",attr"`,会以这个struct的字段名作为属性名输出为XML元素的属性,类似上一条,只是这个name默认是字段名了。
|
||||
- tag中含有`",chardata"`,输出为xml的 character data而非element。
|
||||
- tag中含有`",innerxml"`,将会被原样输出,而不会进行常规的编码过程
|
||||
- tag中含有`",comment"`,将被当作xml注释来输出,而不会进行常规的编码过程,字段值中不能含有"--"字符串
|
||||
- tag中含有`"omitempty"`,如果该字段的值为空值那么该字段就不会被输出到XML,空值包括:false、0、nil指针或nil接口,任何长度为0的array, slice, map或者string
|
||||
- tag中含有`"a>b>c"`,那么就会循环输出三个元素a包含b,b包含c,例如如下代码就会输出
|
||||
|
||||
FirstName string `xml:"name>first"`
|
||||
LastName string `xml:"name>last"`
|
||||
|
||||
<name>
|
||||
<first>Asta</first>
|
||||
<last>Xie</last>
|
||||
</name>
|
||||
|
||||
|
||||
上面我们介绍了如何使用Go语言的xml包来编/解码XML文件,重要的一点是对XML的所有操作都是通过struct tag来实现的,所以学会对struct tag的运用变得非常重要,在文章中我们简要的列举了如何定义tag。更多内容或tag定义请参看相应的官方资料。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [文本处理](<07.0.md>)
|
||||
* 下一节: [Json处理](<07.2.md>)
|
||||
# 7.1 XML处理
|
||||
XML作为一种数据交换和信息传递的格式已经十分普及。而随着Web服务日益广泛的应用,现在XML在日常的开发工作中也扮演了愈发重要的角色。这一小节, 我们将就Go语言标准包中的XML相关处理的包进行介绍。
|
||||
|
||||
这个小节不会涉及XML规范相关的内容(如需了解相关知识请参考其他文献),而是介绍如何用Go语言来编解码XML文件相关的知识。
|
||||
|
||||
假如你是一名运维人员,你为你所管理的所有服务器生成了如下内容的xml的配置文件:
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<servers version="1">
|
||||
<server>
|
||||
<serverName>Shanghai_VPN</serverName>
|
||||
<serverIP>127.0.0.1</serverIP>
|
||||
</server>
|
||||
<server>
|
||||
<serverName>Beijing_VPN</serverName>
|
||||
<serverIP>127.0.0.2</serverIP>
|
||||
</server>
|
||||
</servers>
|
||||
|
||||
上面的XML文档描述了两个服务器的信息,包含了服务器名和服务器的IP信息,接下来的Go例子以此XML描述的信息进行操作。
|
||||
|
||||
## 解析XML
|
||||
如何解析如上这个XML文件呢? 我们可以通过xml包的`Unmarshal`函数来达到我们的目的
|
||||
|
||||
func Unmarshal(data []byte, v interface{}) error
|
||||
|
||||
data接收的是XML数据流,v是需要输出的结构,定义为interface,也就是可以把XML转换为任意的格式。我们这里主要介绍struct的转换,因为struct和XML都有类似树结构的特征。
|
||||
|
||||
示例代码如下:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Recurlyservers struct {
|
||||
XMLName xml.Name `xml:"servers"`
|
||||
Version string `xml:"version,attr"`
|
||||
Svs []server `xml:"server"`
|
||||
Description string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type server struct {
|
||||
XMLName xml.Name `xml:"server"`
|
||||
ServerName string `xml:"serverName"`
|
||||
ServerIP string `xml:"serverIP"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
file, err := os.Open("servers.xml") // For read access.
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v", err)
|
||||
return
|
||||
}
|
||||
v := Recurlyservers{}
|
||||
err = xml.Unmarshal(data, &v)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(v)
|
||||
}
|
||||
|
||||
|
||||
XML本质上是一种树形的数据格式,而我们可以定义与之匹配的go 语言的 struct类型,然后通过xml.Unmarshal来将xml中的数据解析成对应的struct对象。如上例子输出如下数据
|
||||
|
||||
{{ servers} 1 [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}]
|
||||
<server>
|
||||
<serverName>Shanghai_VPN</serverName>
|
||||
<serverIP>127.0.0.1</serverIP>
|
||||
</server>
|
||||
<server>
|
||||
<serverName>Beijing_VPN</serverName>
|
||||
<serverIP>127.0.0.2</serverIP>
|
||||
</server>
|
||||
}
|
||||
|
||||
|
||||
上面的例子中,将xml文件解析成对应的struct对象是通过`xml.Unmarshal`来完成的,这个过程是如何实现的?可以看到我们的struct定义后面多了一些类似于`xml:"serverName"`这样的内容,这个是struct的一个特性,它们被称为 struct tag,它们是用来辅助反射的。我们来看一下`Unmarshal`的定义:
|
||||
|
||||
func Unmarshal(data []byte, v interface{}) error
|
||||
|
||||
我们看到函数定义了两个参数,第一个是XML数据流,第二个是存储的对应类型,目前支持struct、slice和string,XML包内部采用了反射来进行数据的映射,所以v里面的字段必须是导出的。`Unmarshal`解析的时候XML元素和字段怎么对应起来的呢?这是有一个优先级读取流程的,首先会读取struct tag,如果没有,那么就会对应字段名。必须注意一点的是解析的时候tag、字段名、XML元素都是大小写敏感的,所以必须一一对应字段。
|
||||
|
||||
Go语言的反射机制,可以利用这些tag信息来将来自XML文件中的数据反射成对应的struct对象,关于反射如何利用struct tag的更多内容请参阅reflect中的相关内容。
|
||||
|
||||
解析XML到struct的时候遵循如下的规则:
|
||||
|
||||
- 如果struct的一个字段是string或者[]byte类型且它的tag含有`",innerxml"`,Unmarshal将会将此字段所对应的元素内所有内嵌的原始xml累加到此字段上,如上面例子Description定义。最后的输出是
|
||||
|
||||
<server>
|
||||
<serverName>Shanghai_VPN</serverName>
|
||||
<serverIP>127.0.0.1</serverIP>
|
||||
</server>
|
||||
<server>
|
||||
<serverName>Beijing_VPN</serverName>
|
||||
<serverIP>127.0.0.2</serverIP>
|
||||
</server>
|
||||
|
||||
- 如果struct中有一个叫做XMLName,且类型为xml.Name字段,那么在解析的时候就会保存这个element的名字到该字段,如上面例子中的servers。
|
||||
- 如果某个struct字段的tag定义中含有XML结构中element的名称,那么解析的时候就会把相应的element值赋值给该字段,如上servername和serverip定义。
|
||||
- 如果某个struct字段的tag定义了中含有`",attr"`,那么解析的时候就会将该结构所对应的element的与字段同名的属性的值赋值给该字段,如上version定义。
|
||||
- 如果某个struct字段的tag定义 型如`"a>b>c"`,则解析的时候,会将xml结构a下面的b下面的c元素的值赋值给该字段。
|
||||
- 如果某个struct字段的tag定义了`"-"`,那么不会为该字段解析匹配任何xml数据。
|
||||
- 如果struct字段后面的tag定义了`",any"`,如果他的子元素在不满足其他的规则的时候就会匹配到这个字段。
|
||||
- 如果某个XML元素包含一条或者多条注释,那么这些注释将被累加到第一个tag含有",comments"的字段上,这个字段的类型可能是[]byte或string,如果没有这样的字段存在,那么注释将会被抛弃。
|
||||
|
||||
上面详细讲述了如何定义struct的tag。 只要设置对了tag,那么XML解析就如上面示例般简单,tag和XML的element是一一对应的关系,如上所示,我们还可以通过slice来表示多个同级元素。
|
||||
|
||||
>注意: 为了正确解析,go语言的xml包要求struct定义中的所有字段必须是可导出的(即首字母大写)
|
||||
|
||||
## 输出XML
|
||||
假若我们不是要解析如上所示的XML文件,而是生成它,那么在go语言中又该如何实现呢? xml包中提供了`Marshal`和`MarshalIndent`两个函数,来满足我们的需求。这两个函数主要的区别是第二个函数会增加前缀和缩进,函数的定义如下所示:
|
||||
|
||||
func Marshal(v interface{}) ([]byte, error)
|
||||
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
|
||||
|
||||
两个函数第一个参数是用来生成XML的结构定义类型数据,都是返回生成的XML数据流。
|
||||
|
||||
下面我们来看一下如何输出如上的XML:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Servers struct {
|
||||
XMLName xml.Name `xml:"servers"`
|
||||
Version string `xml:"version,attr"`
|
||||
Svs []server `xml:"server"`
|
||||
}
|
||||
|
||||
type server struct {
|
||||
ServerName string `xml:"serverName"`
|
||||
ServerIP string `xml:"serverIP"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
v := &Servers{Version: "1"}
|
||||
v.Svs = append(v.Svs, server{"Shanghai_VPN", "127.0.0.1"})
|
||||
v.Svs = append(v.Svs, server{"Beijing_VPN", "127.0.0.2"})
|
||||
output, err := xml.MarshalIndent(v, " ", " ")
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
}
|
||||
os.Stdout.Write([]byte(xml.Header))
|
||||
|
||||
os.Stdout.Write(output)
|
||||
}
|
||||
上面的代码输出如下信息:
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<servers version="1">
|
||||
<server>
|
||||
<serverName>Shanghai_VPN</serverName>
|
||||
<serverIP>127.0.0.1</serverIP>
|
||||
</server>
|
||||
<server>
|
||||
<serverName>Beijing_VPN</serverName>
|
||||
<serverIP>127.0.0.2</serverIP>
|
||||
</server>
|
||||
</servers>
|
||||
|
||||
和我们之前定义的文件的格式一模一样,之所以会有`os.Stdout.Write([]byte(xml.Header))` 这句代码的出现,是因为`xml.MarshalIndent`或者`xml.Marshal`输出的信息都是不带XML头的,为了生成正确的xml文件,我们使用了xml包预定义的Header变量。
|
||||
|
||||
我们看到`Marshal`函数接收的参数v是interface{}类型的,即它可以接受任意类型的参数,那么xml包,根据什么规则来生成相应的XML文件呢?
|
||||
|
||||
- 如果v是 array或者slice,那么输出每一个元素,类似<type>value</type>
|
||||
- 如果v是指针,那么会Marshal指针指向的内容,如果指针为空,什么都不输出
|
||||
- 如果v是interface,那么就处理interface所包含的数据
|
||||
- 如果v是其他数据类型,就会输出这个数据类型所拥有的字段信息
|
||||
|
||||
生成的XML文件中的element的名字又是根据什么决定的呢?元素名按照如下优先级从struct中获取:
|
||||
|
||||
- 如果v是struct,XMLName的tag中定义的名称
|
||||
- 类型为xml.Name的名叫XMLName的字段的值
|
||||
- 通过struct中字段的tag来获取
|
||||
- 通过struct的字段名用来获取
|
||||
- marshall的类型名称
|
||||
|
||||
我们应如何设置struct 中字段的tag信息以控制最终xml文件的生成呢?
|
||||
|
||||
- XMLName不会被输出
|
||||
- tag中含有`"-"`的字段不会输出
|
||||
- tag中含有`"name,attr"`,会以name作为属性名,字段值作为值输出为这个XML元素的属性,如上version字段所描述
|
||||
- tag中含有`",attr"`,会以这个struct的字段名作为属性名输出为XML元素的属性,类似上一条,只是这个name默认是字段名了。
|
||||
- tag中含有`",chardata"`,输出为xml的 character data而非element。
|
||||
- tag中含有`",innerxml"`,将会被原样输出,而不会进行常规的编码过程
|
||||
- tag中含有`",comment"`,将被当作xml注释来输出,而不会进行常规的编码过程,字段值中不能含有"--"字符串
|
||||
- tag中含有`"omitempty"`,如果该字段的值为空值那么该字段就不会被输出到XML,空值包括:false、0、nil指针或nil接口,任何长度为0的array, slice, map或者string
|
||||
- tag中含有`"a>b>c"`,那么就会循环输出三个元素a包含b,b包含c,例如如下代码就会输出
|
||||
|
||||
FirstName string `xml:"name>first"`
|
||||
LastName string `xml:"name>last"`
|
||||
|
||||
<name>
|
||||
<first>Asta</first>
|
||||
<last>Xie</last>
|
||||
</name>
|
||||
|
||||
|
||||
上面我们介绍了如何使用Go语言的xml包来编/解码XML文件,重要的一点是对XML的所有操作都是通过struct tag来实现的,所以学会对struct tag的运用变得非常重要,在文章中我们简要的列举了如何定义tag。更多内容或tag定义请参看相应的官方资料。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [文本处理](<07.0.md>)
|
||||
* 下一节: [Json处理](<07.2.md>)
|
||||
221
zh/07.1.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
221
zh/07.1.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,221 @@
|
||||
# 7.1 XML处理
|
||||
XML作为一种数据交换和信息传递的格式已经十分普及。而随着Web服务日益广泛的应用,现在XML在日常的开发工作中也扮演了愈发重要的角色。这一小节, 我们将就Go语言标准包中的XML相关处理的包进行介绍。
|
||||
|
||||
这个小节不会涉及XML规范相关的内容(如需了解相关知识请参考其他文献),而是介绍如何用Go语言来编解码XML文件相关的知识。
|
||||
|
||||
假如你是一名运维人员,你为你所管理的所有服务器生成了如下内容的xml的配置文件:
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<servers version="1">
|
||||
<server>
|
||||
<serverName>Shanghai_VPN</serverName>
|
||||
<serverIP>127.0.0.1</serverIP>
|
||||
</server>
|
||||
<server>
|
||||
<serverName>Beijing_VPN</serverName>
|
||||
<serverIP>127.0.0.2</serverIP>
|
||||
</server>
|
||||
</servers>
|
||||
|
||||
上面的XML文档描述了两个服务器的信息,包含了服务器名和服务器的IP信息,接下来的Go例子以此XML描述的信息进行操作。
|
||||
|
||||
## 解析XML
|
||||
如何解析如上这个XML文件呢? 我们可以通过xml包的`Unmarshal`函数来达到我们的目的
|
||||
|
||||
func Unmarshal(data []byte, v interface{}) error
|
||||
|
||||
data接收的是XML数据流,v是需要输出的结构,定义为interface,也就是可以把XML转换为任意的格式。我们这里主要介绍struct的转换,因为struct和XML都有类似树结构的特征。
|
||||
|
||||
示例代码如下:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Recurlyservers struct {
|
||||
XMLName xml.Name `xml:"servers"`
|
||||
Version string `xml:"version,attr"`
|
||||
Svs []server `xml:"server"`
|
||||
Description string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type server struct {
|
||||
XMLName xml.Name `xml:"server"`
|
||||
ServerName string `xml:"serverName"`
|
||||
ServerIP string `xml:"serverIP"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
file, err := os.Open("servers.xml") // For read access.
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v", err)
|
||||
return
|
||||
}
|
||||
v := Recurlyservers{}
|
||||
err = xml.Unmarshal(data, &v)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(v)
|
||||
}
|
||||
|
||||
|
||||
XML本质上是一种树形的数据格式,而我们可以定义与之匹配的go 语言的 struct类型,然后通过xml.Unmarshal来将xml中的数据解析成对应的struct对象。如上例子输出如下数据
|
||||
|
||||
{{ servers} 1 [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}]
|
||||
<server>
|
||||
<serverName>Shanghai_VPN</serverName>
|
||||
<serverIP>127.0.0.1</serverIP>
|
||||
</server>
|
||||
<server>
|
||||
<serverName>Beijing_VPN</serverName>
|
||||
<serverIP>127.0.0.2</serverIP>
|
||||
</server>
|
||||
}
|
||||
|
||||
|
||||
上面的例子中,将xml文件解析成对应的struct对象是通过`xml.Unmarshal`来完成的,这个过程是如何实现的?可以看到我们的struct定义后面多了一些类似于`xml:"serverName"`这样的内容,这个是struct的一个特性,它们被称为 struct tag,它们是用来辅助反射的。我们来看一下`Unmarshal`的定义:
|
||||
|
||||
func Unmarshal(data []byte, v interface{}) error
|
||||
|
||||
我们看到函数定义了两个参数,第一个是XML数据流,第二个是存储的对应类型,目前支持struct、slice和string,XML包内部采用了反射来进行数据的映射,所以v里面的字段必须是导出的。`Unmarshal`解析的时候XML元素和字段怎么对应起来的呢?这是有一个优先级读取流程的,首先会读取struct tag,如果没有,那么就会对应字段名。必须注意一点的是解析的时候tag、字段名、XML元素都是大小写敏感的,所以必须一一对应字段。
|
||||
|
||||
Go语言的反射机制,可以利用这些tag信息来将来自XML文件中的数据反射成对应的struct对象,关于反射如何利用struct tag的更多内容请参阅reflect中的相关内容。
|
||||
|
||||
解析XML到struct的时候遵循如下的规则:
|
||||
|
||||
- 如果struct的一个字段是string或者[]byte类型且它的tag含有`",innerxml"`,Unmarshal将会将此字段所对应的元素内所有内嵌的原始xml累加到此字段上,如上面例子Description定义。最后的输出是
|
||||
|
||||
<server>
|
||||
<serverName>Shanghai_VPN</serverName>
|
||||
<serverIP>127.0.0.1</serverIP>
|
||||
</server>
|
||||
<server>
|
||||
<serverName>Beijing_VPN</serverName>
|
||||
<serverIP>127.0.0.2</serverIP>
|
||||
</server>
|
||||
|
||||
- 如果struct中有一个叫做XMLName,且类型为xml.Name字段,那么在解析的时候就会保存这个element的名字到该字段,如上面例子中的servers。
|
||||
- 如果某个struct字段的tag定义中含有XML结构中element的名称,那么解析的时候就会把相应的element值赋值给该字段,如上servername和serverip定义。
|
||||
- 如果某个struct字段的tag定义了中含有`",attr"`,那么解析的时候就会将该结构所对应的element的与字段同名的属性的值赋值给该字段,如上version定义。
|
||||
- 如果某个struct字段的tag定义 型如`"a>b>c"`,则解析的时候,会将xml结构a下面的b下面的c元素的值赋值给该字段。
|
||||
- 如果某个struct字段的tag定义了`"-"`,那么不会为该字段解析匹配任何xml数据。
|
||||
- 如果struct字段后面的tag定义了`",any"`,如果他的子元素在不满足其他的规则的时候就会匹配到这个字段。
|
||||
- 如果某个XML元素包含一条或者多条注释,那么这些注释将被累加到第一个tag含有",comments"的字段上,这个字段的类型可能是[]byte或string,如果没有这样的字段存在,那么注释将会被抛弃。
|
||||
|
||||
上面详细讲述了如何定义struct的tag。 只要设置对了tag,那么XML解析就如上面示例般简单,tag和XML的element是一一对应的关系,如上所示,我们还可以通过slice来表示多个同级元素。
|
||||
|
||||
>注意: 为了正确解析,go语言的xml包要求struct定义中的所有字段必须是可导出的(即首字母大写)
|
||||
|
||||
## 输出XML
|
||||
假若我们不是要解析如上所示的XML文件,而是生成它,那么在go语言中又该如何实现呢? xml包中提供了`Marshal`和`MarshalIndent`两个函数,来满足我们的需求。这两个函数主要的区别是第二个函数会增加前缀和缩进,函数的定义如下所示:
|
||||
|
||||
func Marshal(v interface{}) ([]byte, error)
|
||||
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
|
||||
|
||||
两个函数第一个参数是用来生成XML的结构定义类型数据,都是返回生成的XML数据流。
|
||||
|
||||
下面我们来看一下如何输出如上的XML:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Servers struct {
|
||||
XMLName xml.Name `xml:"servers"`
|
||||
Version string `xml:"version,attr"`
|
||||
Svs []server `xml:"server"`
|
||||
}
|
||||
|
||||
type server struct {
|
||||
ServerName string `xml:"serverName"`
|
||||
ServerIP string `xml:"serverIP"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
v := &Servers{Version: "1"}
|
||||
v.Svs = append(v.Svs, server{"Shanghai_VPN", "127.0.0.1"})
|
||||
v.Svs = append(v.Svs, server{"Beijing_VPN", "127.0.0.2"})
|
||||
output, err := xml.MarshalIndent(v, " ", " ")
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
}
|
||||
os.Stdout.Write([]byte(xml.Header))
|
||||
|
||||
os.Stdout.Write(output)
|
||||
}
|
||||
上面的代码输出如下信息:
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<servers version="1">
|
||||
<server>
|
||||
<serverName>Shanghai_VPN</serverName>
|
||||
<serverIP>127.0.0.1</serverIP>
|
||||
</server>
|
||||
<server>
|
||||
<serverName>Beijing_VPN</serverName>
|
||||
<serverIP>127.0.0.2</serverIP>
|
||||
</server>
|
||||
</servers>
|
||||
|
||||
和我们之前定义的文件的格式一模一样,之所以会有`os.Stdout.Write([]byte(xml.Header))` 这句代码的出现,是因为`xml.MarshalIndent`或者`xml.Marshal`输出的信息都是不带XML头的,为了生成正确的xml文件,我们使用了xml包预定义的Header变量。
|
||||
|
||||
我们看到`Marshal`函数接收的参数v是interface{}类型的,即它可以接受任意类型的参数,那么xml包,根据什么规则来生成相应的XML文件呢?
|
||||
|
||||
- 如果v是 array或者slice,那么输出每一个元素,类似<type>value</type>
|
||||
- 如果v是指针,那么会Marshal指针指向的内容,如果指针为空,什么都不输出
|
||||
- 如果v是interface,那么就处理interface所包含的数据
|
||||
- 如果v是其他数据类型,就会输出这个数据类型所拥有的字段信息
|
||||
|
||||
生成的XML文件中的element的名字又是根据什么决定的呢?元素名按照如下优先级从struct中获取:
|
||||
|
||||
- 如果v是struct,XMLName的tag中定义的名称
|
||||
- 类型为xml.Name的名叫XMLName的字段的值
|
||||
- 通过struct中字段的tag来获取
|
||||
- 通过struct的字段名用来获取
|
||||
- marshall的类型名称
|
||||
|
||||
我们应如何设置struct 中字段的tag信息以控制最终xml文件的生成呢?
|
||||
|
||||
- XMLName不会被输出
|
||||
- tag中含有`"-"`的字段不会输出
|
||||
- tag中含有`"name,attr"`,会以name作为属性名,字段值作为值输出为这个XML元素的属性,如上version字段所描述
|
||||
- tag中含有`",attr"`,会以这个struct的字段名作为属性名输出为XML元素的属性,类似上一条,只是这个name默认是字段名了。
|
||||
- tag中含有`",chardata"`,输出为xml的 character data而非element。
|
||||
- tag中含有`",innerxml"`,将会被原样输出,而不会进行常规的编码过程
|
||||
- tag中含有`",comment"`,将被当作xml注释来输出,而不会进行常规的编码过程,字段值中不能含有"--"字符串
|
||||
- tag中含有`"omitempty"`,如果该字段的值为空值那么该字段就不会被输出到XML,空值包括:false、0、nil指针或nil接口,任何长度为0的array, slice, map或者string
|
||||
- tag中含有`"a>b>c"`,那么就会循环输出三个元素a包含b,b包含c,例如如下代码就会输出
|
||||
|
||||
FirstName string `xml:"name>first"`
|
||||
LastName string `xml:"name>last"`
|
||||
|
||||
<name>
|
||||
<first>Asta</first>
|
||||
<last>Xie</last>
|
||||
</name>
|
||||
|
||||
|
||||
上面我们介绍了如何使用Go语言的xml包来编/解码XML文件,重要的一点是对XML的所有操作都是通过struct tag来实现的,所以学会对struct tag的运用变得非常重要,在文章中我们简要的列举了如何定义tag。更多内容或tag定义请参看相应的官方资料。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [文本处理](<07.0.md>)
|
||||
* 下一节: [Json处理](<07.2.md>)
|
||||
@@ -1,224 +1,224 @@
|
||||
# 7.2 JSON处理
|
||||
JSON(Javascript Object Notation)是一种轻量级的数据交换语言,以文字为基础,具有自我描述性且易于让人阅读。尽管JSON是Javascript的一个子集,但JSON是独立于语言的文本格式,并且采用了类似于C语言家族的一些习惯。JSON与XML最大的不同在于XML是一个完整的标记语言,而JSON不是。JSON由于比XML更小、更快,更易解析,以及浏览器的内建快速解析支持,使得其更适用于网络数据传输领域。目前我们看到很多的开放平台,基本上都是采用了JSON作为他们的数据交互的接口。既然JSON在Web开发中如此重要,那么Go语言对JSON支持的怎么样呢?Go语言的标准库已经非常好的支持了JSON,可以很容易的对JSON数据进行编、解码的工作。
|
||||
|
||||
前一小节的运维的例子用json来表示,结果描述如下:
|
||||
|
||||
{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}
|
||||
|
||||
本小节余下的内容将以此JSON数据为基础,来介绍go语言的json包对JSON数据的编、解码。
|
||||
## 解析JSON
|
||||
|
||||
### 解析到结构体
|
||||
假如有了上面的JSON串,那么我们如何来解析这个JSON串呢?Go的JSON包中有如下函数
|
||||
|
||||
func Unmarshal(data []byte, v interface{}) error
|
||||
|
||||
通过这个函数我们就可以实现解析的目的,详细的解析例子请看如下代码:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
ServerName string
|
||||
ServerIP string
|
||||
}
|
||||
|
||||
type Serverslice struct {
|
||||
Servers []Server
|
||||
}
|
||||
|
||||
func main() {
|
||||
var s Serverslice
|
||||
str := `{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}`
|
||||
json.Unmarshal([]byte(str), &s)
|
||||
fmt.Println(s)
|
||||
}
|
||||
|
||||
在上面的示例代码中,我们首先定义了与json数据对应的结构体,数组对应slice,字段名对应JSON里面的KEY,在解析的时候,如何将json数据与struct字段相匹配呢?例如JSON的key是`Foo`,那么怎么找对应的字段呢?
|
||||
|
||||
- 首先查找tag含有`Foo`的可导出的struct字段(首字母大写)
|
||||
- 其次查找字段名是`Foo`的导出字段
|
||||
- 最后查找类似`FOO`或者`FoO`这样的除了首字母之外其他大小写不敏感的导出字段
|
||||
|
||||
聪明的你一定注意到了这一点:能够被赋值的字段必须是可导出字段(即首字母大写)。同时JSON解析的时候只会解析能找得到的字段,找不到的字段会被忽略,这样的一个好处是:当你接收到一个很大的JSON数据结构而你却只想获取其中的部分数据的时候,你只需将你想要的数据对应的字段名大写,即可轻松解决这个问题。
|
||||
|
||||
### 解析到interface
|
||||
上面那种解析方式是在我们知晓被解析的JSON数据的结构的前提下采取的方案,如果我们不知道被解析的数据的格式,又应该如何来解析呢?
|
||||
|
||||
我们知道interface{}可以用来存储任意数据类型的对象,这种数据结构正好用于存储解析的未知结构的json数据的结果。JSON包中采用map[string]interface{}和[]interface{}结构来存储任意的JSON对象和数组。Go类型和JSON类型的对应关系如下:
|
||||
|
||||
- bool 代表 JSON booleans,
|
||||
- float64 代表 JSON numbers,
|
||||
- string 代表 JSON strings,
|
||||
- nil 代表 JSON null.
|
||||
|
||||
现在我们假设有如下的JSON数据
|
||||
|
||||
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
|
||||
|
||||
如果在我们不知道他的结构的情况下,我们把他解析到interface{}里面
|
||||
|
||||
var f interface{}
|
||||
err := json.Unmarshal(b, &f)
|
||||
|
||||
这个时候f里面存储了一个map类型,他们的key是string,值存储在空的interface{}里
|
||||
|
||||
f = map[string]interface{}{
|
||||
"Name": "Wednesday",
|
||||
"Age": 6,
|
||||
"Parents": []interface{}{
|
||||
"Gomez",
|
||||
"Morticia",
|
||||
},
|
||||
}
|
||||
|
||||
那么如何来访问这些数据呢?通过断言的方式:
|
||||
|
||||
m := f.(map[string]interface{})
|
||||
|
||||
通过断言之后,你就可以通过如下方式来访问里面的数据了
|
||||
|
||||
for k, v := range m {
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
fmt.Println(k, "is string", vv)
|
||||
case int:
|
||||
fmt.Println(k, "is int", vv)
|
||||
case float64:
|
||||
fmt.Println(k,"is float64",vv)
|
||||
case []interface{}:
|
||||
fmt.Println(k, "is an array:")
|
||||
for i, u := range vv {
|
||||
fmt.Println(i, u)
|
||||
}
|
||||
default:
|
||||
fmt.Println(k, "is of a type I don't know how to handle")
|
||||
}
|
||||
}
|
||||
通过上面的示例可以看到,通过interface{}与type assert的配合,我们就可以解析未知结构的JSON数了。
|
||||
|
||||
上面这个是官方提供的解决方案,其实很多时候我们通过类型断言,操作起来不是很方便,目前bitly公司开源了一个叫做`simplejson`的包,在处理未知结构体的JSON时相当方便,详细例子如下所示:
|
||||
|
||||
js, err := NewJson([]byte(`{
|
||||
"test": {
|
||||
"array": [1, "2", 3],
|
||||
"int": 10,
|
||||
"float": 5.150,
|
||||
"bignum": 9223372036854775807,
|
||||
"string": "simplejson",
|
||||
"bool": true
|
||||
}
|
||||
}`))
|
||||
|
||||
arr, _ := js.Get("test").Get("array").Array()
|
||||
i, _ := js.Get("test").Get("int").Int()
|
||||
ms := js.Get("test").Get("string").MustString()
|
||||
|
||||
可以看到,使用这个库操作JSON比起官方包来说,简单的多,详细的请参考如下地址:https://github.com/bitly/go-simplejson
|
||||
|
||||
## 生成JSON
|
||||
我们开发很多应用的时候,最后都是要输出JSON数据串,那么如何来处理呢?JSON包里面通过`Marshal`函数来处理,函数定义如下:
|
||||
|
||||
func Marshal(v interface{}) ([]byte, error)
|
||||
|
||||
假设我们还是需要生成上面的服务器列表信息,那么如何来处理呢?请看下面的例子:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
ServerName string
|
||||
ServerIP string
|
||||
}
|
||||
|
||||
type Serverslice struct {
|
||||
Servers []Server
|
||||
}
|
||||
|
||||
func main() {
|
||||
var s Serverslice
|
||||
s.Servers = append(s.Servers, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1"})
|
||||
s.Servers = append(s.Servers, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2"})
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
fmt.Println("json err:", err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
}
|
||||
|
||||
输出如下内容:
|
||||
|
||||
{"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]}
|
||||
|
||||
我们看到上面的输出字段名的首字母都是大写的,如果你想用小写的首字母怎么办呢?把结构体的字段名改成首字母小写的?JSON输出的时候必须注意,只有导出的字段才会被输出,如果修改字段名,那么就会发现什么都不会输出,所以必须通过struct tag定义来实现:
|
||||
|
||||
type Server struct {
|
||||
ServerName string `json:"serverName"`
|
||||
ServerIP string `json:"serverIP"`
|
||||
}
|
||||
|
||||
type Serverslice struct {
|
||||
Servers []Server `json:"servers"`
|
||||
}
|
||||
|
||||
通过修改上面的结构体定义,输出的JSON串就和我们最开始定义的JSON串保持一致了。
|
||||
|
||||
针对JSON的输出,我们在定义struct tag的时候需要注意的几点是:
|
||||
|
||||
- 字段的tag是`"-"`,那么这个字段不会输出到JSON
|
||||
- tag中带有自定义名称,那么这个自定义名称会出现在JSON的字段名中,例如上面例子中serverName
|
||||
- tag中如果带有`"omitempty"`选项,那么如果该字段值为空,就不会输出到JSON串中
|
||||
- 如果字段类型是bool, string, int, int64等,而tag中带有`",string"`选项,那么这个字段在输出到JSON的时候会把该字段对应的值转换成JSON字符串
|
||||
|
||||
|
||||
举例来说:
|
||||
|
||||
type Server struct {
|
||||
// ID 不会导出到JSON中
|
||||
ID int `json:"-"`
|
||||
|
||||
// ServerName2 的值会进行二次JSON编码
|
||||
ServerName string `json:"serverName"`
|
||||
ServerName2 string `json:"serverName2,string"`
|
||||
|
||||
// 如果 ServerIP 为空,则不输出到JSON串中
|
||||
ServerIP string `json:"serverIP,omitempty"`
|
||||
}
|
||||
|
||||
s := Server {
|
||||
ID: 3,
|
||||
ServerName: `Go "1.0" `,
|
||||
ServerName2: `Go "1.0" `,
|
||||
ServerIP: ``,
|
||||
}
|
||||
b, _ := json.Marshal(s)
|
||||
os.Stdout.Write(b)
|
||||
|
||||
会输出以下内容:
|
||||
|
||||
{"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""}
|
||||
|
||||
|
||||
Marshal函数只有在转换成功的时候才会返回数据,在转换的过程中我们需要注意几点:
|
||||
|
||||
|
||||
- JSON对象只支持string作为key,所以要编码一个map,那么必须是map[string]T这种类型(T是Go语言中任意的类型)
|
||||
- Channel, complex和function是不能被编码成JSON的
|
||||
- 嵌套的数据是不能编码的,不然会让JSON编码进入死循环
|
||||
- 指针在编码的时候会输出指针指向的内容,而空指针会输出null
|
||||
|
||||
|
||||
本小节,我们介绍了如何使用Go语言的json标准包来编解码JSON数据,同时也简要介绍了如何使用第三方包`go-simplejson`来在一些情况下简化操作,学会并熟练运用它们将对我们接下来的Web开发相当重要。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [XML处理](<07.1.md>)
|
||||
* 下一节: [正则处理](<07.3.md>)
|
||||
# 7.2 JSON处理
|
||||
JSON(Javascript Object Notation)是一种轻量级的数据交换语言,以文字为基础,具有自我描述性且易于让人阅读。尽管JSON是Javascript的一个子集,但JSON是独立于语言的文本格式,并且采用了类似于C语言家族的一些习惯。JSON与XML最大的不同在于XML是一个完整的标记语言,而JSON不是。JSON由于比XML更小、更快,更易解析,以及浏览器的内建快速解析支持,使得其更适用于网络数据传输领域。目前我们看到很多的开放平台,基本上都是采用了JSON作为他们的数据交互的接口。既然JSON在Web开发中如此重要,那么Go语言对JSON支持的怎么样呢?Go语言的标准库已经非常好的支持了JSON,可以很容易的对JSON数据进行编、解码的工作。
|
||||
|
||||
前一小节的运维的例子用json来表示,结果描述如下:
|
||||
|
||||
{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}
|
||||
|
||||
本小节余下的内容将以此JSON数据为基础,来介绍go语言的json包对JSON数据的编、解码。
|
||||
## 解析JSON
|
||||
|
||||
### 解析到结构体
|
||||
假如有了上面的JSON串,那么我们如何来解析这个JSON串呢?Go的JSON包中有如下函数
|
||||
|
||||
func Unmarshal(data []byte, v interface{}) error
|
||||
|
||||
通过这个函数我们就可以实现解析的目的,详细的解析例子请看如下代码:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
ServerName string
|
||||
ServerIP string
|
||||
}
|
||||
|
||||
type Serverslice struct {
|
||||
Servers []Server
|
||||
}
|
||||
|
||||
func main() {
|
||||
var s Serverslice
|
||||
str := `{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}`
|
||||
json.Unmarshal([]byte(str), &s)
|
||||
fmt.Println(s)
|
||||
}
|
||||
|
||||
在上面的示例代码中,我们首先定义了与json数据对应的结构体,数组对应slice,字段名对应JSON里面的KEY,在解析的时候,如何将json数据与struct字段相匹配呢?例如JSON的key是`Foo`,那么怎么找对应的字段呢?
|
||||
|
||||
- 首先查找tag含有`Foo`的可导出的struct字段(首字母大写)
|
||||
- 其次查找字段名是`Foo`的导出字段
|
||||
- 最后查找类似`FOO`或者`FoO`这样的除了首字母之外其他大小写不敏感的导出字段
|
||||
|
||||
聪明的你一定注意到了这一点:能够被赋值的字段必须是可导出字段(即首字母大写)。同时JSON解析的时候只会解析能找得到的字段,找不到的字段会被忽略,这样的一个好处是:当你接收到一个很大的JSON数据结构而你却只想获取其中的部分数据的时候,你只需将你想要的数据对应的字段名大写,即可轻松解决这个问题。
|
||||
|
||||
### 解析到interface
|
||||
上面那种解析方式是在我们知晓被解析的JSON数据的结构的前提下采取的方案,如果我们不知道被解析的数据的格式,又应该如何来解析呢?
|
||||
|
||||
我们知道interface{}可以用来存储任意数据类型的对象,这种数据结构正好用于存储解析的未知结构的json数据的结果。JSON包中采用map[string]interface{}和[]interface{}结构来存储任意的JSON对象和数组。Go类型和JSON类型的对应关系如下:
|
||||
|
||||
- bool 代表 JSON booleans,
|
||||
- float64 代表 JSON numbers,
|
||||
- string 代表 JSON strings,
|
||||
- nil 代表 JSON null.
|
||||
|
||||
现在我们假设有如下的JSON数据
|
||||
|
||||
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
|
||||
|
||||
如果在我们不知道他的结构的情况下,我们把他解析到interface{}里面
|
||||
|
||||
var f interface{}
|
||||
err := json.Unmarshal(b, &f)
|
||||
|
||||
这个时候f里面存储了一个map类型,他们的key是string,值存储在空的interface{}里
|
||||
|
||||
f = map[string]interface{}{
|
||||
"Name": "Wednesday",
|
||||
"Age": 6,
|
||||
"Parents": []interface{}{
|
||||
"Gomez",
|
||||
"Morticia",
|
||||
},
|
||||
}
|
||||
|
||||
那么如何来访问这些数据呢?通过断言的方式:
|
||||
|
||||
m := f.(map[string]interface{})
|
||||
|
||||
通过断言之后,你就可以通过如下方式来访问里面的数据了
|
||||
|
||||
for k, v := range m {
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
fmt.Println(k, "is string", vv)
|
||||
case int:
|
||||
fmt.Println(k, "is int", vv)
|
||||
case float64:
|
||||
fmt.Println(k,"is float64",vv)
|
||||
case []interface{}:
|
||||
fmt.Println(k, "is an array:")
|
||||
for i, u := range vv {
|
||||
fmt.Println(i, u)
|
||||
}
|
||||
default:
|
||||
fmt.Println(k, "is of a type I don't know how to handle")
|
||||
}
|
||||
}
|
||||
通过上面的示例可以看到,通过interface{}与type assert的配合,我们就可以解析未知结构的JSON数了。
|
||||
|
||||
上面这个是官方提供的解决方案,其实很多时候我们通过类型断言,操作起来不是很方便,目前bitly公司开源了一个叫做`simplejson`的包,在处理未知结构体的JSON时相当方便,详细例子如下所示:
|
||||
|
||||
js, err := NewJson([]byte(`{
|
||||
"test": {
|
||||
"array": [1, "2", 3],
|
||||
"int": 10,
|
||||
"float": 5.150,
|
||||
"bignum": 9223372036854775807,
|
||||
"string": "simplejson",
|
||||
"bool": true
|
||||
}
|
||||
}`))
|
||||
|
||||
arr, _ := js.Get("test").Get("array").Array()
|
||||
i, _ := js.Get("test").Get("int").Int()
|
||||
ms := js.Get("test").Get("string").MustString()
|
||||
|
||||
可以看到,使用这个库操作JSON比起官方包来说,简单的多,详细的请参考如下地址:https://github.com/bitly/go-simplejson
|
||||
|
||||
## 生成JSON
|
||||
我们开发很多应用的时候,最后都是要输出JSON数据串,那么如何来处理呢?JSON包里面通过`Marshal`函数来处理,函数定义如下:
|
||||
|
||||
func Marshal(v interface{}) ([]byte, error)
|
||||
|
||||
假设我们还是需要生成上面的服务器列表信息,那么如何来处理呢?请看下面的例子:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
ServerName string
|
||||
ServerIP string
|
||||
}
|
||||
|
||||
type Serverslice struct {
|
||||
Servers []Server
|
||||
}
|
||||
|
||||
func main() {
|
||||
var s Serverslice
|
||||
s.Servers = append(s.Servers, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1"})
|
||||
s.Servers = append(s.Servers, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2"})
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
fmt.Println("json err:", err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
}
|
||||
|
||||
输出如下内容:
|
||||
|
||||
{"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]}
|
||||
|
||||
我们看到上面的输出字段名的首字母都是大写的,如果你想用小写的首字母怎么办呢?把结构体的字段名改成首字母小写的?JSON输出的时候必须注意,只有导出的字段才会被输出,如果修改字段名,那么就会发现什么都不会输出,所以必须通过struct tag定义来实现:
|
||||
|
||||
type Server struct {
|
||||
ServerName string `json:"serverName"`
|
||||
ServerIP string `json:"serverIP"`
|
||||
}
|
||||
|
||||
type Serverslice struct {
|
||||
Servers []Server `json:"servers"`
|
||||
}
|
||||
|
||||
通过修改上面的结构体定义,输出的JSON串就和我们最开始定义的JSON串保持一致了。
|
||||
|
||||
针对JSON的输出,我们在定义struct tag的时候需要注意的几点是:
|
||||
|
||||
- 字段的tag是`"-"`,那么这个字段不会输出到JSON
|
||||
- tag中带有自定义名称,那么这个自定义名称会出现在JSON的字段名中,例如上面例子中serverName
|
||||
- tag中如果带有`"omitempty"`选项,那么如果该字段值为空,就不会输出到JSON串中
|
||||
- 如果字段类型是bool, string, int, int64等,而tag中带有`",string"`选项,那么这个字段在输出到JSON的时候会把该字段对应的值转换成JSON字符串
|
||||
|
||||
|
||||
举例来说:
|
||||
|
||||
type Server struct {
|
||||
// ID 不会导出到JSON中
|
||||
ID int `json:"-"`
|
||||
|
||||
// ServerName2 的值会进行二次JSON编码
|
||||
ServerName string `json:"serverName"`
|
||||
ServerName2 string `json:"serverName2,string"`
|
||||
|
||||
// 如果 ServerIP 为空,则不输出到JSON串中
|
||||
ServerIP string `json:"serverIP,omitempty"`
|
||||
}
|
||||
|
||||
s := Server {
|
||||
ID: 3,
|
||||
ServerName: `Go "1.0" `,
|
||||
ServerName2: `Go "1.0" `,
|
||||
ServerIP: ``,
|
||||
}
|
||||
b, _ := json.Marshal(s)
|
||||
os.Stdout.Write(b)
|
||||
|
||||
会输出以下内容:
|
||||
|
||||
{"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""}
|
||||
|
||||
|
||||
Marshal函数只有在转换成功的时候才会返回数据,在转换的过程中我们需要注意几点:
|
||||
|
||||
|
||||
- JSON对象只支持string作为key,所以要编码一个map,那么必须是map[string]T这种类型(T是Go语言中任意的类型)
|
||||
- Channel, complex和function是不能被编码成JSON的
|
||||
- 嵌套的数据是不能编码的,不然会让JSON编码进入死循环
|
||||
- 指针在编码的时候会输出指针指向的内容,而空指针会输出null
|
||||
|
||||
|
||||
本小节,我们介绍了如何使用Go语言的json标准包来编解码JSON数据,同时也简要介绍了如何使用第三方包`go-simplejson`来在一些情况下简化操作,学会并熟练运用它们将对我们接下来的Web开发相当重要。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [XML处理](<07.1.md>)
|
||||
* 下一节: [正则处理](<07.3.md>)
|
||||
224
zh/07.2.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
224
zh/07.2.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,224 @@
|
||||
# 7.2 JSON处理
|
||||
JSON(Javascript Object Notation)是一种轻量级的数据交换语言,以文字为基础,具有自我描述性且易于让人阅读。尽管JSON是Javascript的一个子集,但JSON是独立于语言的文本格式,并且采用了类似于C语言家族的一些习惯。JSON与XML最大的不同在于XML是一个完整的标记语言,而JSON不是。JSON由于比XML更小、更快,更易解析,以及浏览器的内建快速解析支持,使得其更适用于网络数据传输领域。目前我们看到很多的开放平台,基本上都是采用了JSON作为他们的数据交互的接口。既然JSON在Web开发中如此重要,那么Go语言对JSON支持的怎么样呢?Go语言的标准库已经非常好的支持了JSON,可以很容易的对JSON数据进行编、解码的工作。
|
||||
|
||||
前一小节的运维的例子用json来表示,结果描述如下:
|
||||
|
||||
{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}
|
||||
|
||||
本小节余下的内容将以此JSON数据为基础,来介绍go语言的json包对JSON数据的编、解码。
|
||||
## 解析JSON
|
||||
|
||||
### 解析到结构体
|
||||
假如有了上面的JSON串,那么我们如何来解析这个JSON串呢?Go的JSON包中有如下函数
|
||||
|
||||
func Unmarshal(data []byte, v interface{}) error
|
||||
|
||||
通过这个函数我们就可以实现解析的目的,详细的解析例子请看如下代码:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
ServerName string
|
||||
ServerIP string
|
||||
}
|
||||
|
||||
type Serverslice struct {
|
||||
Servers []Server
|
||||
}
|
||||
|
||||
func main() {
|
||||
var s Serverslice
|
||||
str := `{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}`
|
||||
json.Unmarshal([]byte(str), &s)
|
||||
fmt.Println(s)
|
||||
}
|
||||
|
||||
在上面的示例代码中,我们首先定义了与json数据对应的结构体,数组对应slice,字段名对应JSON里面的KEY,在解析的时候,如何将json数据与struct字段相匹配呢?例如JSON的key是`Foo`,那么怎么找对应的字段呢?
|
||||
|
||||
- 首先查找tag含有`Foo`的可导出的struct字段(首字母大写)
|
||||
- 其次查找字段名是`Foo`的导出字段
|
||||
- 最后查找类似`FOO`或者`FoO`这样的除了首字母之外其他大小写不敏感的导出字段
|
||||
|
||||
聪明的你一定注意到了这一点:能够被赋值的字段必须是可导出字段(即首字母大写)。同时JSON解析的时候只会解析能找得到的字段,找不到的字段会被忽略,这样的一个好处是:当你接收到一个很大的JSON数据结构而你却只想获取其中的部分数据的时候,你只需将你想要的数据对应的字段名大写,即可轻松解决这个问题。
|
||||
|
||||
### 解析到interface
|
||||
上面那种解析方式是在我们知晓被解析的JSON数据的结构的前提下采取的方案,如果我们不知道被解析的数据的格式,又应该如何来解析呢?
|
||||
|
||||
我们知道interface{}可以用来存储任意数据类型的对象,这种数据结构正好用于存储解析的未知结构的json数据的结果。JSON包中采用map[string]interface{}和[]interface{}结构来存储任意的JSON对象和数组。Go类型和JSON类型的对应关系如下:
|
||||
|
||||
- bool 代表 JSON booleans,
|
||||
- float64 代表 JSON numbers,
|
||||
- string 代表 JSON strings,
|
||||
- nil 代表 JSON null.
|
||||
|
||||
现在我们假设有如下的JSON数据
|
||||
|
||||
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
|
||||
|
||||
如果在我们不知道他的结构的情况下,我们把他解析到interface{}里面
|
||||
|
||||
var f interface{}
|
||||
err := json.Unmarshal(b, &f)
|
||||
|
||||
这个时候f里面存储了一个map类型,他们的key是string,值存储在空的interface{}里
|
||||
|
||||
f = map[string]interface{}{
|
||||
"Name": "Wednesday",
|
||||
"Age": 6,
|
||||
"Parents": []interface{}{
|
||||
"Gomez",
|
||||
"Morticia",
|
||||
},
|
||||
}
|
||||
|
||||
那么如何来访问这些数据呢?通过断言的方式:
|
||||
|
||||
m := f.(map[string]interface{})
|
||||
|
||||
通过断言之后,你就可以通过如下方式来访问里面的数据了
|
||||
|
||||
for k, v := range m {
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
fmt.Println(k, "is string", vv)
|
||||
case int:
|
||||
fmt.Println(k, "is int", vv)
|
||||
case float64:
|
||||
fmt.Println(k,"is float64",vv)
|
||||
case []interface{}:
|
||||
fmt.Println(k, "is an array:")
|
||||
for i, u := range vv {
|
||||
fmt.Println(i, u)
|
||||
}
|
||||
default:
|
||||
fmt.Println(k, "is of a type I don't know how to handle")
|
||||
}
|
||||
}
|
||||
通过上面的示例可以看到,通过interface{}与type assert的配合,我们就可以解析未知结构的JSON数了。
|
||||
|
||||
上面这个是官方提供的解决方案,其实很多时候我们通过类型断言,操作起来不是很方便,目前bitly公司开源了一个叫做`simplejson`的包,在处理未知结构体的JSON时相当方便,详细例子如下所示:
|
||||
|
||||
js, err := NewJson([]byte(`{
|
||||
"test": {
|
||||
"array": [1, "2", 3],
|
||||
"int": 10,
|
||||
"float": 5.150,
|
||||
"bignum": 9223372036854775807,
|
||||
"string": "simplejson",
|
||||
"bool": true
|
||||
}
|
||||
}`))
|
||||
|
||||
arr, _ := js.Get("test").Get("array").Array()
|
||||
i, _ := js.Get("test").Get("int").Int()
|
||||
ms := js.Get("test").Get("string").MustString()
|
||||
|
||||
可以看到,使用这个库操作JSON比起官方包来说,简单的多,详细的请参考如下地址:https://github.com/bitly/go-simplejson
|
||||
|
||||
## 生成JSON
|
||||
我们开发很多应用的时候,最后都是要输出JSON数据串,那么如何来处理呢?JSON包里面通过`Marshal`函数来处理,函数定义如下:
|
||||
|
||||
func Marshal(v interface{}) ([]byte, error)
|
||||
|
||||
假设我们还是需要生成上面的服务器列表信息,那么如何来处理呢?请看下面的例子:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
ServerName string
|
||||
ServerIP string
|
||||
}
|
||||
|
||||
type Serverslice struct {
|
||||
Servers []Server
|
||||
}
|
||||
|
||||
func main() {
|
||||
var s Serverslice
|
||||
s.Servers = append(s.Servers, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1"})
|
||||
s.Servers = append(s.Servers, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2"})
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
fmt.Println("json err:", err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
}
|
||||
|
||||
输出如下内容:
|
||||
|
||||
{"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]}
|
||||
|
||||
我们看到上面的输出字段名的首字母都是大写的,如果你想用小写的首字母怎么办呢?把结构体的字段名改成首字母小写的?JSON输出的时候必须注意,只有导出的字段才会被输出,如果修改字段名,那么就会发现什么都不会输出,所以必须通过struct tag定义来实现:
|
||||
|
||||
type Server struct {
|
||||
ServerName string `json:"serverName"`
|
||||
ServerIP string `json:"serverIP"`
|
||||
}
|
||||
|
||||
type Serverslice struct {
|
||||
Servers []Server `json:"servers"`
|
||||
}
|
||||
|
||||
通过修改上面的结构体定义,输出的JSON串就和我们最开始定义的JSON串保持一致了。
|
||||
|
||||
针对JSON的输出,我们在定义struct tag的时候需要注意的几点是:
|
||||
|
||||
- 字段的tag是`"-"`,那么这个字段不会输出到JSON
|
||||
- tag中带有自定义名称,那么这个自定义名称会出现在JSON的字段名中,例如上面例子中serverName
|
||||
- tag中如果带有`"omitempty"`选项,那么如果该字段值为空,就不会输出到JSON串中
|
||||
- 如果字段类型是bool, string, int, int64等,而tag中带有`",string"`选项,那么这个字段在输出到JSON的时候会把该字段对应的值转换成JSON字符串
|
||||
|
||||
|
||||
举例来说:
|
||||
|
||||
type Server struct {
|
||||
// ID 不会导出到JSON中
|
||||
ID int `json:"-"`
|
||||
|
||||
// ServerName2 的值会进行二次JSON编码
|
||||
ServerName string `json:"serverName"`
|
||||
ServerName2 string `json:"serverName2,string"`
|
||||
|
||||
// 如果 ServerIP 为空,则不输出到JSON串中
|
||||
ServerIP string `json:"serverIP,omitempty"`
|
||||
}
|
||||
|
||||
s := Server {
|
||||
ID: 3,
|
||||
ServerName: `Go "1.0" `,
|
||||
ServerName2: `Go "1.0" `,
|
||||
ServerIP: ``,
|
||||
}
|
||||
b, _ := json.Marshal(s)
|
||||
os.Stdout.Write(b)
|
||||
|
||||
会输出以下内容:
|
||||
|
||||
{"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""}
|
||||
|
||||
|
||||
Marshal函数只有在转换成功的时候才会返回数据,在转换的过程中我们需要注意几点:
|
||||
|
||||
|
||||
- JSON对象只支持string作为key,所以要编码一个map,那么必须是map[string]T这种类型(T是Go语言中任意的类型)
|
||||
- Channel, complex和function是不能被编码成JSON的
|
||||
- 嵌套的数据是不能编码的,不然会让JSON编码进入死循环
|
||||
- 指针在编码的时候会输出指针指向的内容,而空指针会输出null
|
||||
|
||||
|
||||
本小节,我们介绍了如何使用Go语言的json标准包来编解码JSON数据,同时也简要介绍了如何使用第三方包`go-simplejson`来在一些情况下简化操作,学会并熟练运用它们将对我们接下来的Web开发相当重要。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [XML处理](<07.1.md>)
|
||||
* 下一节: [正则处理](<07.3.md>)
|
||||
@@ -1,237 +1,237 @@
|
||||
# 7.3 正则处理
|
||||
正则表达式是一种进行模式匹配和文本操纵的复杂而又强大的工具。虽然正则表达式比纯粹的文本匹配效率低,但是它却更灵活。按照它的语法规则,随需构造出的匹配模式就能够从原始文本中筛选出几乎任何你想要得到的字符组合。如果你在Web开发中需要从一些文本数据源中获取数据,那么你只需要按照它的语法规则,随需构造出正确的模式字符串就能够从原数据源提取出有意义的文本信息。
|
||||
|
||||
Go语言通过`regexp`标准包为正则表达式提供了官方支持,如果你已经使用过其他编程语言提供的正则相关功能,那么你应该对Go语言版本的不会太陌生,但是它们之间也有一些小的差异,因为Go实现的是RE2标准,除了\C,详细的语法描述参考:`http://code.google.com/p/re2/wiki/Syntax`
|
||||
|
||||
其实字符串处理我们可以使用`strings`包来进行搜索(Contains、Index)、替换(Replace)和解析(Split、Join)等操作,但是这些都是简单的字符串操作,他们的搜索都是大小写敏感,而且固定的字符串,如果我们需要匹配可变的那种就没办法实现了,当然如果`strings`包能解决你的问题,那么就尽量使用它来解决。因为他们足够简单、而且性能和可读性都会比正则好。
|
||||
|
||||
如果你还记得,在前面表单验证的小节里,我们已经接触过正则处理,在那里我们利用了它来验证输入的信息是否满足某些预设的条件。在使用中需要注意的一点就是:所有的字符都是UTF-8编码的。接下来让我们更加深入的来学习Go语言的`regexp`包相关知识吧。
|
||||
|
||||
## 通过正则判断是否匹配
|
||||
`regexp`包中含有三个函数用来判断是否匹配,如果匹配返回true,否则返回false
|
||||
|
||||
func Match(pattern string, b []byte) (matched bool, error error)
|
||||
func MatchReader(pattern string, r io.RuneReader) (matched bool, error error)
|
||||
func MatchString(pattern string, s string) (matched bool, error error)
|
||||
|
||||
上面的三个函数实现了同一个功能,就是判断`pattern`是否和输入源匹配,匹配的话就返回true,如果解析正则出错则返回error。三个函数的输入源分别是byte slice、RuneReader和string。
|
||||
|
||||
如果要验证一个输入是不是IP地址,那么如何来判断呢?请看如下实现
|
||||
|
||||
func IsIP(ip string) (b bool) {
|
||||
if m, _ := regexp.MatchString("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", ip); !m {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
可以看到,`regexp`的pattern和我们平常使用的正则一模一样。再来看一个例子:当用户输入一个字符串,我们想知道是不是一次合法的输入:
|
||||
|
||||
func main() {
|
||||
if len(os.Args) == 1 {
|
||||
fmt.Println("Usage: regexp [string]")
|
||||
os.Exit(1)
|
||||
} else if m, _ := regexp.MatchString("^[0-9]+$", os.Args[1]); m {
|
||||
fmt.Println("数字")
|
||||
} else {
|
||||
fmt.Println("不是数字")
|
||||
}
|
||||
}
|
||||
|
||||
在上面的两个小例子中,我们采用了Match(Reader|String)来判断一些字符串是否符合我们的描述需求,它们使用起来非常方便。
|
||||
|
||||
## 通过正则获取内容
|
||||
Match模式只能用来对字符串的判断,而无法截取字符串的一部分、过滤字符串、或者提取出符合条件的一批字符串。如果想要满足这些需求,那就需要使用正则表达式的复杂模式。
|
||||
|
||||
我们经常需要一些爬虫程序,下面就以爬虫为例来说明如何使用正则来过滤或截取抓取到的数据:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
resp, err := http.Get("http://www.baidu.com")
|
||||
if err != nil {
|
||||
fmt.Println("http get error.")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Println("http read error")
|
||||
return
|
||||
}
|
||||
|
||||
src := string(body)
|
||||
|
||||
//将HTML标签全转换成小写
|
||||
re, _ := regexp.Compile("\\<[\\S\\s]+?\\>")
|
||||
src = re.ReplaceAllStringFunc(src, strings.ToLower)
|
||||
|
||||
//去除STYLE
|
||||
re, _ = regexp.Compile("\\<style[\\S\\s]+?\\</style\\>")
|
||||
src = re.ReplaceAllString(src, "")
|
||||
|
||||
//去除SCRIPT
|
||||
re, _ = regexp.Compile("\\<script[\\S\\s]+?\\</script\\>")
|
||||
src = re.ReplaceAllString(src, "")
|
||||
|
||||
//去除所有尖括号内的HTML代码,并换成换行符
|
||||
re, _ = regexp.Compile("\\<[\\S\\s]+?\\>")
|
||||
src = re.ReplaceAllString(src, "\n")
|
||||
|
||||
//去除连续的换行符
|
||||
re, _ = regexp.Compile("\\s{2,}")
|
||||
src = re.ReplaceAllString(src, "\n")
|
||||
|
||||
fmt.Println(strings.TrimSpace(src))
|
||||
}
|
||||
|
||||
从这个示例可以看出,使用复杂的正则首先是Compile,它会解析正则表达式是否合法,如果正确,那么就会返回一个Regexp,然后就可以利用返回的Regexp在任意的字符串上面执行需要的操作。
|
||||
|
||||
解析正则表达式的有如下几个方法:
|
||||
|
||||
func Compile(expr string) (*Regexp, error)
|
||||
func CompilePOSIX(expr string) (*Regexp, error)
|
||||
func MustCompile(str string) *Regexp
|
||||
func MustCompilePOSIX(str string) *Regexp
|
||||
|
||||
CompilePOSIX和Compile的不同点在于POSIX必须使用POSIX语法,它使用最左最长方式搜索,而Compile是采用的则只采用最左方式搜索(例如[a-z]{2,4}这样一个正则表达式,应用于"aa09aaa88aaaa"这个文本串时,CompilePOSIX返回了aaaa,而Compile的返回的是aa)。前缀有Must的函数表示,在解析正则语法的时候,如果匹配模式串不满足正确的语法则直接panic,而不加Must的则只是返回错误。
|
||||
|
||||
在了解了如何新建一个Regexp之后,我们再来看一下这个struct提供了哪些方法来辅助我们操作字符串,首先我们来看下面这些用来搜索的函数:
|
||||
|
||||
func (re *Regexp) Find(b []byte) []byte
|
||||
func (re *Regexp) FindAll(b []byte, n int) [][]byte
|
||||
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int
|
||||
func (re *Regexp) FindAllString(s string, n int) []string
|
||||
func (re *Regexp) FindAllStringIndex(s string, n int) [][]int
|
||||
func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string
|
||||
func (re *Regexp) FindAllStringSubmatchIndex(s string, n int) [][]int
|
||||
func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte
|
||||
func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int
|
||||
func (re *Regexp) FindIndex(b []byte) (loc []int)
|
||||
func (re *Regexp) FindReaderIndex(r io.RuneReader) (loc []int)
|
||||
func (re *Regexp) FindReaderSubmatchIndex(r io.RuneReader) []int
|
||||
func (re *Regexp) FindString(s string) string
|
||||
func (re *Regexp) FindStringIndex(s string) (loc []int)
|
||||
func (re *Regexp) FindStringSubmatch(s string) []string
|
||||
func (re *Regexp) FindStringSubmatchIndex(s string) []int
|
||||
func (re *Regexp) FindSubmatch(b []byte) [][]byte
|
||||
func (re *Regexp) FindSubmatchIndex(b []byte) []int
|
||||
|
||||
上面这18个函数我们根据输入源(byte slice、string和io.RuneReader)不同还可以继续简化成如下几个,其他的只是输入源不一样,其他功能基本是一样的:
|
||||
|
||||
func (re *Regexp) Find(b []byte) []byte
|
||||
func (re *Regexp) FindAll(b []byte, n int) [][]byte
|
||||
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int
|
||||
func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte
|
||||
func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int
|
||||
func (re *Regexp) FindIndex(b []byte) (loc []int)
|
||||
func (re *Regexp) FindSubmatch(b []byte) [][]byte
|
||||
func (re *Regexp) FindSubmatchIndex(b []byte) []int
|
||||
|
||||
对于这些函数的使用我们来看下面这个例子
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
a := "I am learning Go language"
|
||||
|
||||
re, _ := regexp.Compile("[a-z]{2,4}")
|
||||
|
||||
//查找符合正则的第一个
|
||||
one := re.Find([]byte(a))
|
||||
fmt.Println("Find:", string(one))
|
||||
|
||||
//查找符合正则的所有slice,n小于0表示返回全部符合的字符串,不然就是返回指定的长度
|
||||
all := re.FindAll([]byte(a), -1)
|
||||
fmt.Println("FindAll", all)
|
||||
|
||||
//查找符合条件的index位置,开始位置和结束位置
|
||||
index := re.FindIndex([]byte(a))
|
||||
fmt.Println("FindIndex", index)
|
||||
|
||||
//查找符合条件的所有的index位置,n同上
|
||||
allindex := re.FindAllIndex([]byte(a), -1)
|
||||
fmt.Println("FindAllIndex", allindex)
|
||||
|
||||
re2, _ := regexp.Compile("am(.*)lang(.*)")
|
||||
|
||||
//查找Submatch,返回数组,第一个元素是匹配的全部元素,第二个元素是第一个()里面的,第三个是第二个()里面的
|
||||
//下面的输出第一个元素是"am learning Go language"
|
||||
//第二个元素是" learning Go ",注意包含空格的输出
|
||||
//第三个元素是"uage"
|
||||
submatch := re2.FindSubmatch([]byte(a))
|
||||
fmt.Println("FindSubmatch", submatch)
|
||||
for _, v := range submatch {
|
||||
fmt.Println(string(v))
|
||||
}
|
||||
|
||||
//定义和上面的FindIndex一样
|
||||
submatchindex := re2.FindSubmatchIndex([]byte(a))
|
||||
fmt.Println(submatchindex)
|
||||
|
||||
//FindAllSubmatch,查找所有符合条件的子匹配
|
||||
submatchall := re2.FindAllSubmatch([]byte(a), -1)
|
||||
fmt.Println(submatchall)
|
||||
|
||||
//FindAllSubmatchIndex,查找所有字匹配的index
|
||||
submatchallindex := re2.FindAllSubmatchIndex([]byte(a), -1)
|
||||
fmt.Println(submatchallindex)
|
||||
}
|
||||
|
||||
前面介绍过匹配函数,Regexp也定义了三个函数,它们和同名的外部函数功能一模一样,其实外部函数就是调用了这Regexp的三个函数来实现的:
|
||||
|
||||
func (re *Regexp) Match(b []byte) bool
|
||||
func (re *Regexp) MatchReader(r io.RuneReader) bool
|
||||
func (re *Regexp) MatchString(s string) bool
|
||||
|
||||
接下里让我们来了解替换函数是怎么操作的?
|
||||
|
||||
func (re *Regexp) ReplaceAll(src, repl []byte) []byte
|
||||
func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte
|
||||
func (re *Regexp) ReplaceAllLiteral(src, repl []byte) []byte
|
||||
func (re *Regexp) ReplaceAllLiteralString(src, repl string) string
|
||||
func (re *Regexp) ReplaceAllString(src, repl string) string
|
||||
func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string
|
||||
|
||||
这些替换函数我们在上面的抓网页的例子有详细应用示例,
|
||||
|
||||
接下来我们看一下Expand的解释:
|
||||
|
||||
func (re *Regexp) Expand(dst []byte, template []byte, src []byte, match []int) []byte
|
||||
func (re *Regexp) ExpandString(dst []byte, template string, src string, match []int) []byte
|
||||
|
||||
那么这个Expand到底用来干嘛的呢?请看下面的例子:
|
||||
|
||||
func main() {
|
||||
src := []byte(`
|
||||
call hello alice
|
||||
hello bob
|
||||
call hello eve
|
||||
`)
|
||||
pat := regexp.MustCompile(`(?m)(call)\s+(?P<cmd>\w+)\s+(?P<arg>.+)\s*$`)
|
||||
res := []byte{}
|
||||
for _, s := range pat.FindAllSubmatchIndex(src, -1) {
|
||||
res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s)
|
||||
}
|
||||
fmt.Println(string(res))
|
||||
}
|
||||
|
||||
至此我们已经全部介绍完Go语言的`regexp`包,通过对它的主要函数介绍及演示,相信大家应该能够通过Go语言的正则包进行一些基本的正则的操作了。
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [Json处理](<07.2.md>)
|
||||
* 下一节: [模板处理](<07.4.md>)
|
||||
# 7.3 正则处理
|
||||
正则表达式是一种进行模式匹配和文本操纵的复杂而又强大的工具。虽然正则表达式比纯粹的文本匹配效率低,但是它却更灵活。按照它的语法规则,随需构造出的匹配模式就能够从原始文本中筛选出几乎任何你想要得到的字符组合。如果你在Web开发中需要从一些文本数据源中获取数据,那么你只需要按照它的语法规则,随需构造出正确的模式字符串就能够从原数据源提取出有意义的文本信息。
|
||||
|
||||
Go语言通过`regexp`标准包为正则表达式提供了官方支持,如果你已经使用过其他编程语言提供的正则相关功能,那么你应该对Go语言版本的不会太陌生,但是它们之间也有一些小的差异,因为Go实现的是RE2标准,除了\C,详细的语法描述参考:`http://code.google.com/p/re2/wiki/Syntax`
|
||||
|
||||
其实字符串处理我们可以使用`strings`包来进行搜索(Contains、Index)、替换(Replace)和解析(Split、Join)等操作,但是这些都是简单的字符串操作,他们的搜索都是大小写敏感,而且固定的字符串,如果我们需要匹配可变的那种就没办法实现了,当然如果`strings`包能解决你的问题,那么就尽量使用它来解决。因为他们足够简单、而且性能和可读性都会比正则好。
|
||||
|
||||
如果你还记得,在前面表单验证的小节里,我们已经接触过正则处理,在那里我们利用了它来验证输入的信息是否满足某些预设的条件。在使用中需要注意的一点就是:所有的字符都是UTF-8编码的。接下来让我们更加深入的来学习Go语言的`regexp`包相关知识吧。
|
||||
|
||||
## 通过正则判断是否匹配
|
||||
`regexp`包中含有三个函数用来判断是否匹配,如果匹配返回true,否则返回false
|
||||
|
||||
func Match(pattern string, b []byte) (matched bool, error error)
|
||||
func MatchReader(pattern string, r io.RuneReader) (matched bool, error error)
|
||||
func MatchString(pattern string, s string) (matched bool, error error)
|
||||
|
||||
上面的三个函数实现了同一个功能,就是判断`pattern`是否和输入源匹配,匹配的话就返回true,如果解析正则出错则返回error。三个函数的输入源分别是byte slice、RuneReader和string。
|
||||
|
||||
如果要验证一个输入是不是IP地址,那么如何来判断呢?请看如下实现
|
||||
|
||||
func IsIP(ip string) (b bool) {
|
||||
if m, _ := regexp.MatchString("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", ip); !m {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
可以看到,`regexp`的pattern和我们平常使用的正则一模一样。再来看一个例子:当用户输入一个字符串,我们想知道是不是一次合法的输入:
|
||||
|
||||
func main() {
|
||||
if len(os.Args) == 1 {
|
||||
fmt.Println("Usage: regexp [string]")
|
||||
os.Exit(1)
|
||||
} else if m, _ := regexp.MatchString("^[0-9]+$", os.Args[1]); m {
|
||||
fmt.Println("数字")
|
||||
} else {
|
||||
fmt.Println("不是数字")
|
||||
}
|
||||
}
|
||||
|
||||
在上面的两个小例子中,我们采用了Match(Reader|String)来判断一些字符串是否符合我们的描述需求,它们使用起来非常方便。
|
||||
|
||||
## 通过正则获取内容
|
||||
Match模式只能用来对字符串的判断,而无法截取字符串的一部分、过滤字符串、或者提取出符合条件的一批字符串。如果想要满足这些需求,那就需要使用正则表达式的复杂模式。
|
||||
|
||||
我们经常需要一些爬虫程序,下面就以爬虫为例来说明如何使用正则来过滤或截取抓取到的数据:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
resp, err := http.Get("http://www.baidu.com")
|
||||
if err != nil {
|
||||
fmt.Println("http get error.")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Println("http read error")
|
||||
return
|
||||
}
|
||||
|
||||
src := string(body)
|
||||
|
||||
//将HTML标签全转换成小写
|
||||
re, _ := regexp.Compile("\\<[\\S\\s]+?\\>")
|
||||
src = re.ReplaceAllStringFunc(src, strings.ToLower)
|
||||
|
||||
//去除STYLE
|
||||
re, _ = regexp.Compile("\\<style[\\S\\s]+?\\</style\\>")
|
||||
src = re.ReplaceAllString(src, "")
|
||||
|
||||
//去除SCRIPT
|
||||
re, _ = regexp.Compile("\\<script[\\S\\s]+?\\</script\\>")
|
||||
src = re.ReplaceAllString(src, "")
|
||||
|
||||
//去除所有尖括号内的HTML代码,并换成换行符
|
||||
re, _ = regexp.Compile("\\<[\\S\\s]+?\\>")
|
||||
src = re.ReplaceAllString(src, "\n")
|
||||
|
||||
//去除连续的换行符
|
||||
re, _ = regexp.Compile("\\s{2,}")
|
||||
src = re.ReplaceAllString(src, "\n")
|
||||
|
||||
fmt.Println(strings.TrimSpace(src))
|
||||
}
|
||||
|
||||
从这个示例可以看出,使用复杂的正则首先是Compile,它会解析正则表达式是否合法,如果正确,那么就会返回一个Regexp,然后就可以利用返回的Regexp在任意的字符串上面执行需要的操作。
|
||||
|
||||
解析正则表达式的有如下几个方法:
|
||||
|
||||
func Compile(expr string) (*Regexp, error)
|
||||
func CompilePOSIX(expr string) (*Regexp, error)
|
||||
func MustCompile(str string) *Regexp
|
||||
func MustCompilePOSIX(str string) *Regexp
|
||||
|
||||
CompilePOSIX和Compile的不同点在于POSIX必须使用POSIX语法,它使用最左最长方式搜索,而Compile是采用的则只采用最左方式搜索(例如[a-z]{2,4}这样一个正则表达式,应用于"aa09aaa88aaaa"这个文本串时,CompilePOSIX返回了aaaa,而Compile的返回的是aa)。前缀有Must的函数表示,在解析正则语法的时候,如果匹配模式串不满足正确的语法则直接panic,而不加Must的则只是返回错误。
|
||||
|
||||
在了解了如何新建一个Regexp之后,我们再来看一下这个struct提供了哪些方法来辅助我们操作字符串,首先我们来看下面这些用来搜索的函数:
|
||||
|
||||
func (re *Regexp) Find(b []byte) []byte
|
||||
func (re *Regexp) FindAll(b []byte, n int) [][]byte
|
||||
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int
|
||||
func (re *Regexp) FindAllString(s string, n int) []string
|
||||
func (re *Regexp) FindAllStringIndex(s string, n int) [][]int
|
||||
func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string
|
||||
func (re *Regexp) FindAllStringSubmatchIndex(s string, n int) [][]int
|
||||
func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte
|
||||
func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int
|
||||
func (re *Regexp) FindIndex(b []byte) (loc []int)
|
||||
func (re *Regexp) FindReaderIndex(r io.RuneReader) (loc []int)
|
||||
func (re *Regexp) FindReaderSubmatchIndex(r io.RuneReader) []int
|
||||
func (re *Regexp) FindString(s string) string
|
||||
func (re *Regexp) FindStringIndex(s string) (loc []int)
|
||||
func (re *Regexp) FindStringSubmatch(s string) []string
|
||||
func (re *Regexp) FindStringSubmatchIndex(s string) []int
|
||||
func (re *Regexp) FindSubmatch(b []byte) [][]byte
|
||||
func (re *Regexp) FindSubmatchIndex(b []byte) []int
|
||||
|
||||
上面这18个函数我们根据输入源(byte slice、string和io.RuneReader)不同还可以继续简化成如下几个,其他的只是输入源不一样,其他功能基本是一样的:
|
||||
|
||||
func (re *Regexp) Find(b []byte) []byte
|
||||
func (re *Regexp) FindAll(b []byte, n int) [][]byte
|
||||
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int
|
||||
func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte
|
||||
func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int
|
||||
func (re *Regexp) FindIndex(b []byte) (loc []int)
|
||||
func (re *Regexp) FindSubmatch(b []byte) [][]byte
|
||||
func (re *Regexp) FindSubmatchIndex(b []byte) []int
|
||||
|
||||
对于这些函数的使用我们来看下面这个例子
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
a := "I am learning Go language"
|
||||
|
||||
re, _ := regexp.Compile("[a-z]{2,4}")
|
||||
|
||||
//查找符合正则的第一个
|
||||
one := re.Find([]byte(a))
|
||||
fmt.Println("Find:", string(one))
|
||||
|
||||
//查找符合正则的所有slice,n小于0表示返回全部符合的字符串,不然就是返回指定的长度
|
||||
all := re.FindAll([]byte(a), -1)
|
||||
fmt.Println("FindAll", all)
|
||||
|
||||
//查找符合条件的index位置,开始位置和结束位置
|
||||
index := re.FindIndex([]byte(a))
|
||||
fmt.Println("FindIndex", index)
|
||||
|
||||
//查找符合条件的所有的index位置,n同上
|
||||
allindex := re.FindAllIndex([]byte(a), -1)
|
||||
fmt.Println("FindAllIndex", allindex)
|
||||
|
||||
re2, _ := regexp.Compile("am(.*)lang(.*)")
|
||||
|
||||
//查找Submatch,返回数组,第一个元素是匹配的全部元素,第二个元素是第一个()里面的,第三个是第二个()里面的
|
||||
//下面的输出第一个元素是"am learning Go language"
|
||||
//第二个元素是" learning Go ",注意包含空格的输出
|
||||
//第三个元素是"uage"
|
||||
submatch := re2.FindSubmatch([]byte(a))
|
||||
fmt.Println("FindSubmatch", submatch)
|
||||
for _, v := range submatch {
|
||||
fmt.Println(string(v))
|
||||
}
|
||||
|
||||
//定义和上面的FindIndex一样
|
||||
submatchindex := re2.FindSubmatchIndex([]byte(a))
|
||||
fmt.Println(submatchindex)
|
||||
|
||||
//FindAllSubmatch,查找所有符合条件的子匹配
|
||||
submatchall := re2.FindAllSubmatch([]byte(a), -1)
|
||||
fmt.Println(submatchall)
|
||||
|
||||
//FindAllSubmatchIndex,查找所有字匹配的index
|
||||
submatchallindex := re2.FindAllSubmatchIndex([]byte(a), -1)
|
||||
fmt.Println(submatchallindex)
|
||||
}
|
||||
|
||||
前面介绍过匹配函数,Regexp也定义了三个函数,它们和同名的外部函数功能一模一样,其实外部函数就是调用了这Regexp的三个函数来实现的:
|
||||
|
||||
func (re *Regexp) Match(b []byte) bool
|
||||
func (re *Regexp) MatchReader(r io.RuneReader) bool
|
||||
func (re *Regexp) MatchString(s string) bool
|
||||
|
||||
接下里让我们来了解替换函数是怎么操作的?
|
||||
|
||||
func (re *Regexp) ReplaceAll(src, repl []byte) []byte
|
||||
func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte
|
||||
func (re *Regexp) ReplaceAllLiteral(src, repl []byte) []byte
|
||||
func (re *Regexp) ReplaceAllLiteralString(src, repl string) string
|
||||
func (re *Regexp) ReplaceAllString(src, repl string) string
|
||||
func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string
|
||||
|
||||
这些替换函数我们在上面的抓网页的例子有详细应用示例,
|
||||
|
||||
接下来我们看一下Expand的解释:
|
||||
|
||||
func (re *Regexp) Expand(dst []byte, template []byte, src []byte, match []int) []byte
|
||||
func (re *Regexp) ExpandString(dst []byte, template string, src string, match []int) []byte
|
||||
|
||||
那么这个Expand到底用来干嘛的呢?请看下面的例子:
|
||||
|
||||
func main() {
|
||||
src := []byte(`
|
||||
call hello alice
|
||||
hello bob
|
||||
call hello eve
|
||||
`)
|
||||
pat := regexp.MustCompile(`(?m)(call)\s+(?P<cmd>\w+)\s+(?P<arg>.+)\s*$`)
|
||||
res := []byte{}
|
||||
for _, s := range pat.FindAllSubmatchIndex(src, -1) {
|
||||
res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s)
|
||||
}
|
||||
fmt.Println(string(res))
|
||||
}
|
||||
|
||||
至此我们已经全部介绍完Go语言的`regexp`包,通过对它的主要函数介绍及演示,相信大家应该能够通过Go语言的正则包进行一些基本的正则的操作了。
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [Json处理](<07.2.md>)
|
||||
* 下一节: [模板处理](<07.4.md>)
|
||||
237
zh/07.3.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
237
zh/07.3.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,237 @@
|
||||
# 7.3 正则处理
|
||||
正则表达式是一种进行模式匹配和文本操纵的复杂而又强大的工具。虽然正则表达式比纯粹的文本匹配效率低,但是它却更灵活。按照它的语法规则,随需构造出的匹配模式就能够从原始文本中筛选出几乎任何你想要得到的字符组合。如果你在Web开发中需要从一些文本数据源中获取数据,那么你只需要按照它的语法规则,随需构造出正确的模式字符串就能够从原数据源提取出有意义的文本信息。
|
||||
|
||||
Go语言通过`regexp`标准包为正则表达式提供了官方支持,如果你已经使用过其他编程语言提供的正则相关功能,那么你应该对Go语言版本的不会太陌生,但是它们之间也有一些小的差异,因为Go实现的是RE2标准,除了\C,详细的语法描述参考:`http://code.google.com/p/re2/wiki/Syntax`
|
||||
|
||||
其实字符串处理我们可以使用`strings`包来进行搜索(Contains、Index)、替换(Replace)和解析(Split、Join)等操作,但是这些都是简单的字符串操作,他们的搜索都是大小写敏感,而且固定的字符串,如果我们需要匹配可变的那种就没办法实现了,当然如果`strings`包能解决你的问题,那么就尽量使用它来解决。因为他们足够简单、而且性能和可读性都会比正则好。
|
||||
|
||||
如果你还记得,在前面表单验证的小节里,我们已经接触过正则处理,在那里我们利用了它来验证输入的信息是否满足某些预设的条件。在使用中需要注意的一点就是:所有的字符都是UTF-8编码的。接下来让我们更加深入的来学习Go语言的`regexp`包相关知识吧。
|
||||
|
||||
## 通过正则判断是否匹配
|
||||
`regexp`包中含有三个函数用来判断是否匹配,如果匹配返回true,否则返回false
|
||||
|
||||
func Match(pattern string, b []byte) (matched bool, error error)
|
||||
func MatchReader(pattern string, r io.RuneReader) (matched bool, error error)
|
||||
func MatchString(pattern string, s string) (matched bool, error error)
|
||||
|
||||
上面的三个函数实现了同一个功能,就是判断`pattern`是否和输入源匹配,匹配的话就返回true,如果解析正则出错则返回error。三个函数的输入源分别是byte slice、RuneReader和string。
|
||||
|
||||
如果要验证一个输入是不是IP地址,那么如何来判断呢?请看如下实现
|
||||
|
||||
func IsIP(ip string) (b bool) {
|
||||
if m, _ := regexp.MatchString("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", ip); !m {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
可以看到,`regexp`的pattern和我们平常使用的正则一模一样。再来看一个例子:当用户输入一个字符串,我们想知道是不是一次合法的输入:
|
||||
|
||||
func main() {
|
||||
if len(os.Args) == 1 {
|
||||
fmt.Println("Usage: regexp [string]")
|
||||
os.Exit(1)
|
||||
} else if m, _ := regexp.MatchString("^[0-9]+$", os.Args[1]); m {
|
||||
fmt.Println("数字")
|
||||
} else {
|
||||
fmt.Println("不是数字")
|
||||
}
|
||||
}
|
||||
|
||||
在上面的两个小例子中,我们采用了Match(Reader|String)来判断一些字符串是否符合我们的描述需求,它们使用起来非常方便。
|
||||
|
||||
## 通过正则获取内容
|
||||
Match模式只能用来对字符串的判断,而无法截取字符串的一部分、过滤字符串、或者提取出符合条件的一批字符串。如果想要满足这些需求,那就需要使用正则表达式的复杂模式。
|
||||
|
||||
我们经常需要一些爬虫程序,下面就以爬虫为例来说明如何使用正则来过滤或截取抓取到的数据:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
resp, err := http.Get("http://www.baidu.com")
|
||||
if err != nil {
|
||||
fmt.Println("http get error.")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Println("http read error")
|
||||
return
|
||||
}
|
||||
|
||||
src := string(body)
|
||||
|
||||
//将HTML标签全转换成小写
|
||||
re, _ := regexp.Compile("\\<[\\S\\s]+?\\>")
|
||||
src = re.ReplaceAllStringFunc(src, strings.ToLower)
|
||||
|
||||
//去除STYLE
|
||||
re, _ = regexp.Compile("\\<style[\\S\\s]+?\\</style\\>")
|
||||
src = re.ReplaceAllString(src, "")
|
||||
|
||||
//去除SCRIPT
|
||||
re, _ = regexp.Compile("\\<script[\\S\\s]+?\\</script\\>")
|
||||
src = re.ReplaceAllString(src, "")
|
||||
|
||||
//去除所有尖括号内的HTML代码,并换成换行符
|
||||
re, _ = regexp.Compile("\\<[\\S\\s]+?\\>")
|
||||
src = re.ReplaceAllString(src, "\n")
|
||||
|
||||
//去除连续的换行符
|
||||
re, _ = regexp.Compile("\\s{2,}")
|
||||
src = re.ReplaceAllString(src, "\n")
|
||||
|
||||
fmt.Println(strings.TrimSpace(src))
|
||||
}
|
||||
|
||||
从这个示例可以看出,使用复杂的正则首先是Compile,它会解析正则表达式是否合法,如果正确,那么就会返回一个Regexp,然后就可以利用返回的Regexp在任意的字符串上面执行需要的操作。
|
||||
|
||||
解析正则表达式的有如下几个方法:
|
||||
|
||||
func Compile(expr string) (*Regexp, error)
|
||||
func CompilePOSIX(expr string) (*Regexp, error)
|
||||
func MustCompile(str string) *Regexp
|
||||
func MustCompilePOSIX(str string) *Regexp
|
||||
|
||||
CompilePOSIX和Compile的不同点在于POSIX必须使用POSIX语法,它使用最左最长方式搜索,而Compile是采用的则只采用最左方式搜索(例如[a-z]{2,4}这样一个正则表达式,应用于"aa09aaa88aaaa"这个文本串时,CompilePOSIX返回了aaaa,而Compile的返回的是aa)。前缀有Must的函数表示,在解析正则语法的时候,如果匹配模式串不满足正确的语法则直接panic,而不加Must的则只是返回错误。
|
||||
|
||||
在了解了如何新建一个Regexp之后,我们再来看一下这个struct提供了哪些方法来辅助我们操作字符串,首先我们来看下面这些用来搜索的函数:
|
||||
|
||||
func (re *Regexp) Find(b []byte) []byte
|
||||
func (re *Regexp) FindAll(b []byte, n int) [][]byte
|
||||
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int
|
||||
func (re *Regexp) FindAllString(s string, n int) []string
|
||||
func (re *Regexp) FindAllStringIndex(s string, n int) [][]int
|
||||
func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string
|
||||
func (re *Regexp) FindAllStringSubmatchIndex(s string, n int) [][]int
|
||||
func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte
|
||||
func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int
|
||||
func (re *Regexp) FindIndex(b []byte) (loc []int)
|
||||
func (re *Regexp) FindReaderIndex(r io.RuneReader) (loc []int)
|
||||
func (re *Regexp) FindReaderSubmatchIndex(r io.RuneReader) []int
|
||||
func (re *Regexp) FindString(s string) string
|
||||
func (re *Regexp) FindStringIndex(s string) (loc []int)
|
||||
func (re *Regexp) FindStringSubmatch(s string) []string
|
||||
func (re *Regexp) FindStringSubmatchIndex(s string) []int
|
||||
func (re *Regexp) FindSubmatch(b []byte) [][]byte
|
||||
func (re *Regexp) FindSubmatchIndex(b []byte) []int
|
||||
|
||||
上面这18个函数我们根据输入源(byte slice、string和io.RuneReader)不同还可以继续简化成如下几个,其他的只是输入源不一样,其他功能基本是一样的:
|
||||
|
||||
func (re *Regexp) Find(b []byte) []byte
|
||||
func (re *Regexp) FindAll(b []byte, n int) [][]byte
|
||||
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int
|
||||
func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte
|
||||
func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int
|
||||
func (re *Regexp) FindIndex(b []byte) (loc []int)
|
||||
func (re *Regexp) FindSubmatch(b []byte) [][]byte
|
||||
func (re *Regexp) FindSubmatchIndex(b []byte) []int
|
||||
|
||||
对于这些函数的使用我们来看下面这个例子
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
a := "I am learning Go language"
|
||||
|
||||
re, _ := regexp.Compile("[a-z]{2,4}")
|
||||
|
||||
//查找符合正则的第一个
|
||||
one := re.Find([]byte(a))
|
||||
fmt.Println("Find:", string(one))
|
||||
|
||||
//查找符合正则的所有slice,n小于0表示返回全部符合的字符串,不然就是返回指定的长度
|
||||
all := re.FindAll([]byte(a), -1)
|
||||
fmt.Println("FindAll", all)
|
||||
|
||||
//查找符合条件的index位置,开始位置和结束位置
|
||||
index := re.FindIndex([]byte(a))
|
||||
fmt.Println("FindIndex", index)
|
||||
|
||||
//查找符合条件的所有的index位置,n同上
|
||||
allindex := re.FindAllIndex([]byte(a), -1)
|
||||
fmt.Println("FindAllIndex", allindex)
|
||||
|
||||
re2, _ := regexp.Compile("am(.*)lang(.*)")
|
||||
|
||||
//查找Submatch,返回数组,第一个元素是匹配的全部元素,第二个元素是第一个()里面的,第三个是第二个()里面的
|
||||
//下面的输出第一个元素是"am learning Go language"
|
||||
//第二个元素是" learning Go ",注意包含空格的输出
|
||||
//第三个元素是"uage"
|
||||
submatch := re2.FindSubmatch([]byte(a))
|
||||
fmt.Println("FindSubmatch", submatch)
|
||||
for _, v := range submatch {
|
||||
fmt.Println(string(v))
|
||||
}
|
||||
|
||||
//定义和上面的FindIndex一样
|
||||
submatchindex := re2.FindSubmatchIndex([]byte(a))
|
||||
fmt.Println(submatchindex)
|
||||
|
||||
//FindAllSubmatch,查找所有符合条件的子匹配
|
||||
submatchall := re2.FindAllSubmatch([]byte(a), -1)
|
||||
fmt.Println(submatchall)
|
||||
|
||||
//FindAllSubmatchIndex,查找所有字匹配的index
|
||||
submatchallindex := re2.FindAllSubmatchIndex([]byte(a), -1)
|
||||
fmt.Println(submatchallindex)
|
||||
}
|
||||
|
||||
前面介绍过匹配函数,Regexp也定义了三个函数,它们和同名的外部函数功能一模一样,其实外部函数就是调用了这Regexp的三个函数来实现的:
|
||||
|
||||
func (re *Regexp) Match(b []byte) bool
|
||||
func (re *Regexp) MatchReader(r io.RuneReader) bool
|
||||
func (re *Regexp) MatchString(s string) bool
|
||||
|
||||
接下里让我们来了解替换函数是怎么操作的?
|
||||
|
||||
func (re *Regexp) ReplaceAll(src, repl []byte) []byte
|
||||
func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte
|
||||
func (re *Regexp) ReplaceAllLiteral(src, repl []byte) []byte
|
||||
func (re *Regexp) ReplaceAllLiteralString(src, repl string) string
|
||||
func (re *Regexp) ReplaceAllString(src, repl string) string
|
||||
func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string
|
||||
|
||||
这些替换函数我们在上面的抓网页的例子有详细应用示例,
|
||||
|
||||
接下来我们看一下Expand的解释:
|
||||
|
||||
func (re *Regexp) Expand(dst []byte, template []byte, src []byte, match []int) []byte
|
||||
func (re *Regexp) ExpandString(dst []byte, template string, src string, match []int) []byte
|
||||
|
||||
那么这个Expand到底用来干嘛的呢?请看下面的例子:
|
||||
|
||||
func main() {
|
||||
src := []byte(`
|
||||
call hello alice
|
||||
hello bob
|
||||
call hello eve
|
||||
`)
|
||||
pat := regexp.MustCompile(`(?m)(call)\s+(?P<cmd>\w+)\s+(?P<arg>.+)\s*$`)
|
||||
res := []byte{}
|
||||
for _, s := range pat.FindAllSubmatchIndex(src, -1) {
|
||||
res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s)
|
||||
}
|
||||
fmt.Println(string(res))
|
||||
}
|
||||
|
||||
至此我们已经全部介绍完Go语言的`regexp`包,通过对它的主要函数介绍及演示,相信大家应该能够通过Go语言的正则包进行一些基本的正则的操作了。
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [Json处理](<07.2.md>)
|
||||
* 下一节: [模板处理](<07.4.md>)
|
||||
@@ -1,350 +1,350 @@
|
||||
# 7.4 模板处理
|
||||
## 什么是模板
|
||||
你一定听说过一种叫做MVC的设计模式,Model处理数据,View展现结果,Controller控制用户的请求,至于View层的处理,在很多动态语言里面都是通过在静态HTML中插入动态语言生成的数据,例如JSP中通过插入`<%=....=%>`,PHP中通过插入`<?php.....?>`来实现的。
|
||||
|
||||
通过下面这个图可以说明模板的机制
|
||||
|
||||

|
||||
|
||||
图7.1 模板机制图
|
||||
|
||||
Web应用反馈给客户端的信息中的大部分内容是静态的,不变的,而另外少部分是根据用户的请求来动态生成的,例如要显示用户的访问记录列表。用户之间只有记录数据是不同的,而列表的样式则是固定的,此时采用模板可以复用很多静态代码。
|
||||
|
||||
## Go模板使用
|
||||
在Go语言中,我们使用`template`包来进行模板处理,使用类似`Parse`、`ParseFile`、`Execute`等方法从文件或者字符串加载模板,然后执行类似上面图片展示的模板的merge操作。请看下面的例子:
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
t := template.New("some template") //创建一个模板
|
||||
t, _ = t.ParseFiles("tmpl/welcome.html", nil) //解析模板文件
|
||||
user := GetUser() //获取当前用户信息
|
||||
t.Execute(w, user) //执行模板的merger操作
|
||||
}
|
||||
|
||||
通过上面的例子我们可以看到Go语言的模板操作非常的简单方便,和其他语言的模板处理类似,都是先获取数据,然后渲染数据。
|
||||
|
||||
为了演示和测试代码的方便,我们在接下来的例子中采用如下格式的代码
|
||||
|
||||
- 使用Parse代替ParseFiles,因为Parse可以直接测试一个字符串,而不需要额外的文件
|
||||
- 不使用handler来写演示代码,而是每个测试一个main,方便测试
|
||||
- 使用`os.Stdout`代替`http.ResponseWriter`,因为`os.Stdout`实现了`io.Writer`接口
|
||||
|
||||
## 模板中如何插入数据?
|
||||
上面我们演示了如何解析并渲染模板,接下来让我们来更加详细的了解如何把数据渲染出来。一个模板都是应用在一个Go的对象之上,Go对象的字段如何插入到模板中呢?
|
||||
|
||||
### 字段操作
|
||||
Go语言的模板通过`{{}}`来包含需要在渲染时被替换的字段,`{{.}}`表示当前的对象,这和Java或者C++中的this类似,如果要访问当前对象的字段通过`{{.FieldName}}`,但是需要注意一点:这个字段必须是导出的(字段首字母必须是大写的),否则在渲染的时候就会报错,请看下面的这个例子:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Person struct {
|
||||
UserName string
|
||||
}
|
||||
|
||||
func main() {
|
||||
t := template.New("fieldname example")
|
||||
t, _ = t.Parse("hello {{.UserName}}!")
|
||||
p := Person{UserName: "Astaxie"}
|
||||
t.Execute(os.Stdout, p)
|
||||
}
|
||||
|
||||
上面的代码我们可以正确的输出`hello Astaxie`,但是如果我们稍微修改一下代码,在模板中含有了未导出的字段,那么就会报错
|
||||
|
||||
type Person struct {
|
||||
UserName string
|
||||
email string //未导出的字段,首字母是小写的
|
||||
}
|
||||
|
||||
t, _ = t.Parse("hello {{.UserName}}! {{.email}}")
|
||||
|
||||
上面的代码就会报错,因为我们调用了一个未导出的字段,但是如果我们调用了一个不存在的字段是不会报错的,而是输出为空。
|
||||
|
||||
如果模板中输出`{{.}}`,这个一般应用于字符串对象,默认会调用fmt包输出字符串的内容。
|
||||
|
||||
### 输出嵌套字段内容
|
||||
上面我们例子展示了如何针对一个对象的字段输出,那么如果字段里面还有对象,如何来循环的输出这些内容呢?我们可以使用`{{with …}}…{{end}}`和`{{range …}}{{end}}`来进行数据的输出。
|
||||
|
||||
- {{range}} 这个和Go语法里面的range类似,循环操作数据
|
||||
- {{with}}操作是指当前对象的值,类似上下文的概念
|
||||
|
||||
详细的使用请看下面的例子:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Friend struct {
|
||||
Fname string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
UserName string
|
||||
Emails []string
|
||||
Friends []*Friend
|
||||
}
|
||||
|
||||
func main() {
|
||||
f1 := Friend{Fname: "minux.ma"}
|
||||
f2 := Friend{Fname: "xushiwei"}
|
||||
t := template.New("fieldname example")
|
||||
t, _ = t.Parse(`hello {{.UserName}}!
|
||||
{{range .Emails}}
|
||||
an email {{.}}
|
||||
{{end}}
|
||||
{{with .Friends}}
|
||||
{{range .}}
|
||||
my friend name is {{.Fname}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
`)
|
||||
p := Person{UserName: "Astaxie",
|
||||
Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"},
|
||||
Friends: []*Friend{&f1, &f2}}
|
||||
t.Execute(os.Stdout, p)
|
||||
}
|
||||
|
||||
### 条件处理
|
||||
在Go模板里面如果需要进行条件判断,那么我们可以使用和Go语言的`if-else`语法类似的方式来处理,如果pipeline为空,那么if就认为是false,下面的例子展示了如何使用`if-else`语法:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func main() {
|
||||
tEmpty := template.New("template test")
|
||||
tEmpty = template.Must(tEmpty.Parse("空 pipeline if demo: {{if ``}} 不会输出. {{end}}\n"))
|
||||
tEmpty.Execute(os.Stdout, nil)
|
||||
|
||||
tWithValue := template.New("template test")
|
||||
tWithValue = template.Must(tWithValue.Parse("不为空的 pipeline if demo: {{if `anything`}} 我有内容,我会输出. {{end}}\n"))
|
||||
tWithValue.Execute(os.Stdout, nil)
|
||||
|
||||
tIfElse := template.New("template test")
|
||||
tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if部分 {{else}} else部分.{{end}}\n"))
|
||||
tIfElse.Execute(os.Stdout, nil)
|
||||
}
|
||||
|
||||
通过上面的演示代码我们知道`if-else`语法相当的简单,在使用过程中很容易集成到我们的模板代码中。
|
||||
|
||||
> 注意:if里面无法使用条件判断,例如.Mail=="astaxie@gmail.com",这样的判断是不正确的,if里面只能是bool值
|
||||
|
||||
### pipelines
|
||||
Unix用户已经很熟悉什么是`pipe`了,`ls | grep "beego"`类似这样的语法你是不是经常使用,过滤当前目录下面的文件,显示含有"beego"的数据,表达的意思就是前面的输出可以当做后面的输入,最后显示我们想要的数据,而Go语言模板最强大的一点就是支持pipe数据,在Go语言里面任何`{{}}`里面的都是pipelines数据,例如我们上面输出的email里面如果还有一些可能引起XSS注入的,那么我们如何来进行转化呢?
|
||||
|
||||
{{. | html}}
|
||||
|
||||
在email输出的地方我们可以采用如上方式可以把输出全部转化html的实体,上面的这种方式和我们平常写Unix的方式是不是一模一样,操作起来相当的简便,调用其他的函数也是类似的方式。
|
||||
|
||||
### 模板变量
|
||||
有时候,我们在模板使用过程中需要定义一些局部变量,我们可以在一些操作中申明局部变量,例如`with``range``if`过程中申明局部变量,这个变量的作用域是`{{end}}`之前,Go语言通过申明的局部变量格式如下所示:
|
||||
|
||||
$variable := pipeline
|
||||
|
||||
详细的例子看下面的:
|
||||
|
||||
{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
|
||||
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
|
||||
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}
|
||||
### 模板函数
|
||||
模板在输出对象的字段值时,采用了`fmt`包把对象转化成了字符串。但是有时候我们的需求可能不是这样的,例如有时候我们为了防止垃圾邮件发送者通过采集网页的方式来发送给我们的邮箱信息,我们希望把`@`替换成`at`例如:`astaxie at beego.me`,如果要实现这样的功能,我们就需要自定义函数来做这个功能。
|
||||
|
||||
每一个模板函数都有一个唯一值的名字,然后与一个Go函数关联,通过如下的方式来关联
|
||||
|
||||
type FuncMap map[string]interface{}
|
||||
|
||||
例如,如果我们想要的email函数的模板函数名是`emailDeal`,它关联的Go函数名称是`EmailDealWith`,那么我们可以通过下面的方式来注册这个函数
|
||||
|
||||
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
|
||||
|
||||
`EmailDealWith`这个函数的参数和返回值定义如下:
|
||||
|
||||
func EmailDealWith(args …interface{}) string
|
||||
|
||||
我们来看下面的实现例子:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Friend struct {
|
||||
Fname string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
UserName string
|
||||
Emails []string
|
||||
Friends []*Friend
|
||||
}
|
||||
|
||||
func EmailDealWith(args ...interface{}) string {
|
||||
ok := false
|
||||
var s string
|
||||
if len(args) == 1 {
|
||||
s, ok = args[0].(string)
|
||||
}
|
||||
if !ok {
|
||||
s = fmt.Sprint(args...)
|
||||
}
|
||||
// find the @ symbol
|
||||
substrs := strings.Split(s, "@")
|
||||
if len(substrs) != 2 {
|
||||
return s
|
||||
}
|
||||
// replace the @ by " at "
|
||||
return (substrs[0] + " at " + substrs[1])
|
||||
}
|
||||
|
||||
func main() {
|
||||
f1 := Friend{Fname: "minux.ma"}
|
||||
f2 := Friend{Fname: "xushiwei"}
|
||||
t := template.New("fieldname example")
|
||||
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
|
||||
t, _ = t.Parse(`hello {{.UserName}}!
|
||||
{{range .Emails}}
|
||||
an emails {{.|emailDeal}}
|
||||
{{end}}
|
||||
{{with .Friends}}
|
||||
{{range .}}
|
||||
my friend name is {{.Fname}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
`)
|
||||
p := Person{UserName: "Astaxie",
|
||||
Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"},
|
||||
Friends: []*Friend{&f1, &f2}}
|
||||
t.Execute(os.Stdout, p)
|
||||
}
|
||||
|
||||
|
||||
上面演示了如何自定义函数,其实,在模板包内部已经有内置的实现函数,下面代码截取自模板包里面
|
||||
|
||||
var builtins = FuncMap{
|
||||
"and": and,
|
||||
"call": call,
|
||||
"html": HTMLEscaper,
|
||||
"index": index,
|
||||
"js": JSEscaper,
|
||||
"len": length,
|
||||
"not": not,
|
||||
"or": or,
|
||||
"print": fmt.Sprint,
|
||||
"printf": fmt.Sprintf,
|
||||
"println": fmt.Sprintln,
|
||||
"urlquery": URLQueryEscaper,
|
||||
}
|
||||
|
||||
|
||||
## Must操作
|
||||
模板包里面有一个函数`Must`,它的作用是检测模板是否正确,例如大括号是否匹配,注释是否正确的关闭,变量是否正确的书写。接下来我们演示一个例子,用Must来判断模板是否正确:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func main() {
|
||||
tOk := template.New("first")
|
||||
template.Must(tOk.Parse(" some static text /* and a comment */"))
|
||||
fmt.Println("The first one parsed OK.")
|
||||
|
||||
template.Must(template.New("second").Parse("some static text {{ .Name }}"))
|
||||
fmt.Println("The second one parsed OK.")
|
||||
|
||||
fmt.Println("The next one ought to fail.")
|
||||
tErr := template.New("check parse error with Must")
|
||||
template.Must(tErr.Parse(" some static text {{ .Name }"))
|
||||
}
|
||||
|
||||
将输出如下内容
|
||||
|
||||
The first one parsed OK.
|
||||
The second one parsed OK.
|
||||
The next one ought to fail.
|
||||
panic: template: check parse error with Must:1: unexpected "}" in command
|
||||
|
||||
## 嵌套模板
|
||||
我们平常开发Web应用的时候,经常会遇到一些模板有些部分是固定不变的,然后可以抽取出来作为一个独立的部分,例如一个博客的头部和尾部是不变的,而唯一改变的是中间的内容部分。所以我们可以定义成`header`、`content`、`footer`三个部分。Go语言中通过如下的语法来申明
|
||||
|
||||
{{define "子模板名称"}}内容{{end}}
|
||||
|
||||
通过如下方式来调用:
|
||||
|
||||
{{template "子模板名称"}}
|
||||
|
||||
接下来我们演示如何使用嵌套模板,我们定义三个文件,`header.tmpl`、`content.tmpl`、`footer.tmpl`文件,里面的内容如下
|
||||
|
||||
//header.tmpl
|
||||
{{define "header"}}
|
||||
<html>
|
||||
<head>
|
||||
<title>演示信息</title>
|
||||
</head>
|
||||
<body>
|
||||
{{end}}
|
||||
|
||||
//content.tmpl
|
||||
{{define "content"}}
|
||||
{{template "header"}}
|
||||
<h1>演示嵌套</h1>
|
||||
<ul>
|
||||
<li>嵌套使用define定义子模板</li>
|
||||
<li>调用使用template</li>
|
||||
</ul>
|
||||
{{template "footer"}}
|
||||
{{end}}
|
||||
|
||||
//footer.tmpl
|
||||
{{define "footer"}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
演示代码如下:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func main() {
|
||||
s1, _ := template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl")
|
||||
s1.ExecuteTemplate(os.Stdout, "header", nil)
|
||||
fmt.Println()
|
||||
s1.ExecuteTemplate(os.Stdout, "content", nil)
|
||||
fmt.Println()
|
||||
s1.ExecuteTemplate(os.Stdout, "footer", nil)
|
||||
fmt.Println()
|
||||
s1.Execute(os.Stdout, nil)
|
||||
}
|
||||
|
||||
通过上面的例子我们可以看到通过`template.ParseFiles`把所有的嵌套模板全部解析到模板里面,其实每一个定义的{{define}}都是一个独立的模板,他们相互独立,是并行存在的关系,内部其实存储的是类似map的一种关系(key是模板的名称,value是模板的内容),然后我们通过`ExecuteTemplate`来执行相应的子模板内容,我们可以看到header、footer都是相对独立的,都能输出内容,content 中因为嵌套了header和footer的内容,就会同时输出三个的内容。但是当我们执行`s1.Execute`,没有任何的输出,因为在默认的情况下没有默认的子模板,所以不会输出任何的东西。
|
||||
|
||||
>同一个集合类的模板是互相知晓的,如果同一模板被多个集合使用,则它需要在多个集合中分别解析
|
||||
|
||||
## 总结
|
||||
通过上面对模板的详细介绍,我们了解了如何把动态数据与模板融合:如何输出循环数据、如何自定义函数、如何嵌套模板等等。通过模板技术的应用,我们可以完成MVC模式中V的处理,接下来的章节我们将介绍如何来处理M和C。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [正则处理](<07.3.md>)
|
||||
* 下一节: [文件操作](<07.5.md>)
|
||||
# 7.4 模板处理
|
||||
## 什么是模板
|
||||
你一定听说过一种叫做MVC的设计模式,Model处理数据,View展现结果,Controller控制用户的请求,至于View层的处理,在很多动态语言里面都是通过在静态HTML中插入动态语言生成的数据,例如JSP中通过插入`<%=....=%>`,PHP中通过插入`<?php.....?>`来实现的。
|
||||
|
||||
通过下面这个图可以说明模板的机制
|
||||
|
||||

|
||||
|
||||
图7.1 模板机制图
|
||||
|
||||
Web应用反馈给客户端的信息中的大部分内容是静态的,不变的,而另外少部分是根据用户的请求来动态生成的,例如要显示用户的访问记录列表。用户之间只有记录数据是不同的,而列表的样式则是固定的,此时采用模板可以复用很多静态代码。
|
||||
|
||||
## Go模板使用
|
||||
在Go语言中,我们使用`template`包来进行模板处理,使用类似`Parse`、`ParseFile`、`Execute`等方法从文件或者字符串加载模板,然后执行类似上面图片展示的模板的merge操作。请看下面的例子:
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
t := template.New("some template") //创建一个模板
|
||||
t, _ = t.ParseFiles("tmpl/welcome.html", nil) //解析模板文件
|
||||
user := GetUser() //获取当前用户信息
|
||||
t.Execute(w, user) //执行模板的merger操作
|
||||
}
|
||||
|
||||
通过上面的例子我们可以看到Go语言的模板操作非常的简单方便,和其他语言的模板处理类似,都是先获取数据,然后渲染数据。
|
||||
|
||||
为了演示和测试代码的方便,我们在接下来的例子中采用如下格式的代码
|
||||
|
||||
- 使用Parse代替ParseFiles,因为Parse可以直接测试一个字符串,而不需要额外的文件
|
||||
- 不使用handler来写演示代码,而是每个测试一个main,方便测试
|
||||
- 使用`os.Stdout`代替`http.ResponseWriter`,因为`os.Stdout`实现了`io.Writer`接口
|
||||
|
||||
## 模板中如何插入数据?
|
||||
上面我们演示了如何解析并渲染模板,接下来让我们来更加详细的了解如何把数据渲染出来。一个模板都是应用在一个Go的对象之上,Go对象的字段如何插入到模板中呢?
|
||||
|
||||
### 字段操作
|
||||
Go语言的模板通过`{{}}`来包含需要在渲染时被替换的字段,`{{.}}`表示当前的对象,这和Java或者C++中的this类似,如果要访问当前对象的字段通过`{{.FieldName}}`,但是需要注意一点:这个字段必须是导出的(字段首字母必须是大写的),否则在渲染的时候就会报错,请看下面的这个例子:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Person struct {
|
||||
UserName string
|
||||
}
|
||||
|
||||
func main() {
|
||||
t := template.New("fieldname example")
|
||||
t, _ = t.Parse("hello {{.UserName}}!")
|
||||
p := Person{UserName: "Astaxie"}
|
||||
t.Execute(os.Stdout, p)
|
||||
}
|
||||
|
||||
上面的代码我们可以正确的输出`hello Astaxie`,但是如果我们稍微修改一下代码,在模板中含有了未导出的字段,那么就会报错
|
||||
|
||||
type Person struct {
|
||||
UserName string
|
||||
email string //未导出的字段,首字母是小写的
|
||||
}
|
||||
|
||||
t, _ = t.Parse("hello {{.UserName}}! {{.email}}")
|
||||
|
||||
上面的代码就会报错,因为我们调用了一个未导出的字段,但是如果我们调用了一个不存在的字段是不会报错的,而是输出为空。
|
||||
|
||||
如果模板中输出`{{.}}`,这个一般应用于字符串对象,默认会调用fmt包输出字符串的内容。
|
||||
|
||||
### 输出嵌套字段内容
|
||||
上面我们例子展示了如何针对一个对象的字段输出,那么如果字段里面还有对象,如何来循环的输出这些内容呢?我们可以使用`{{with …}}…{{end}}`和`{{range …}}{{end}}`来进行数据的输出。
|
||||
|
||||
- {{range}} 这个和Go语法里面的range类似,循环操作数据
|
||||
- {{with}}操作是指当前对象的值,类似上下文的概念
|
||||
|
||||
详细的使用请看下面的例子:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Friend struct {
|
||||
Fname string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
UserName string
|
||||
Emails []string
|
||||
Friends []*Friend
|
||||
}
|
||||
|
||||
func main() {
|
||||
f1 := Friend{Fname: "minux.ma"}
|
||||
f2 := Friend{Fname: "xushiwei"}
|
||||
t := template.New("fieldname example")
|
||||
t, _ = t.Parse(`hello {{.UserName}}!
|
||||
{{range .Emails}}
|
||||
an email {{.}}
|
||||
{{end}}
|
||||
{{with .Friends}}
|
||||
{{range .}}
|
||||
my friend name is {{.Fname}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
`)
|
||||
p := Person{UserName: "Astaxie",
|
||||
Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"},
|
||||
Friends: []*Friend{&f1, &f2}}
|
||||
t.Execute(os.Stdout, p)
|
||||
}
|
||||
|
||||
### 条件处理
|
||||
在Go模板里面如果需要进行条件判断,那么我们可以使用和Go语言的`if-else`语法类似的方式来处理,如果pipeline为空,那么if就认为是false,下面的例子展示了如何使用`if-else`语法:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func main() {
|
||||
tEmpty := template.New("template test")
|
||||
tEmpty = template.Must(tEmpty.Parse("空 pipeline if demo: {{if ``}} 不会输出. {{end}}\n"))
|
||||
tEmpty.Execute(os.Stdout, nil)
|
||||
|
||||
tWithValue := template.New("template test")
|
||||
tWithValue = template.Must(tWithValue.Parse("不为空的 pipeline if demo: {{if `anything`}} 我有内容,我会输出. {{end}}\n"))
|
||||
tWithValue.Execute(os.Stdout, nil)
|
||||
|
||||
tIfElse := template.New("template test")
|
||||
tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if部分 {{else}} else部分.{{end}}\n"))
|
||||
tIfElse.Execute(os.Stdout, nil)
|
||||
}
|
||||
|
||||
通过上面的演示代码我们知道`if-else`语法相当的简单,在使用过程中很容易集成到我们的模板代码中。
|
||||
|
||||
> 注意:if里面无法使用条件判断,例如.Mail=="astaxie@gmail.com",这样的判断是不正确的,if里面只能是bool值
|
||||
|
||||
### pipelines
|
||||
Unix用户已经很熟悉什么是`pipe`了,`ls | grep "beego"`类似这样的语法你是不是经常使用,过滤当前目录下面的文件,显示含有"beego"的数据,表达的意思就是前面的输出可以当做后面的输入,最后显示我们想要的数据,而Go语言模板最强大的一点就是支持pipe数据,在Go语言里面任何`{{}}`里面的都是pipelines数据,例如我们上面输出的email里面如果还有一些可能引起XSS注入的,那么我们如何来进行转化呢?
|
||||
|
||||
{{. | html}}
|
||||
|
||||
在email输出的地方我们可以采用如上方式可以把输出全部转化html的实体,上面的这种方式和我们平常写Unix的方式是不是一模一样,操作起来相当的简便,调用其他的函数也是类似的方式。
|
||||
|
||||
### 模板变量
|
||||
有时候,我们在模板使用过程中需要定义一些局部变量,我们可以在一些操作中申明局部变量,例如`with``range``if`过程中申明局部变量,这个变量的作用域是`{{end}}`之前,Go语言通过申明的局部变量格式如下所示:
|
||||
|
||||
$variable := pipeline
|
||||
|
||||
详细的例子看下面的:
|
||||
|
||||
{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
|
||||
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
|
||||
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}
|
||||
### 模板函数
|
||||
模板在输出对象的字段值时,采用了`fmt`包把对象转化成了字符串。但是有时候我们的需求可能不是这样的,例如有时候我们为了防止垃圾邮件发送者通过采集网页的方式来发送给我们的邮箱信息,我们希望把`@`替换成`at`例如:`astaxie at beego.me`,如果要实现这样的功能,我们就需要自定义函数来做这个功能。
|
||||
|
||||
每一个模板函数都有一个唯一值的名字,然后与一个Go函数关联,通过如下的方式来关联
|
||||
|
||||
type FuncMap map[string]interface{}
|
||||
|
||||
例如,如果我们想要的email函数的模板函数名是`emailDeal`,它关联的Go函数名称是`EmailDealWith`,那么我们可以通过下面的方式来注册这个函数
|
||||
|
||||
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
|
||||
|
||||
`EmailDealWith`这个函数的参数和返回值定义如下:
|
||||
|
||||
func EmailDealWith(args …interface{}) string
|
||||
|
||||
我们来看下面的实现例子:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Friend struct {
|
||||
Fname string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
UserName string
|
||||
Emails []string
|
||||
Friends []*Friend
|
||||
}
|
||||
|
||||
func EmailDealWith(args ...interface{}) string {
|
||||
ok := false
|
||||
var s string
|
||||
if len(args) == 1 {
|
||||
s, ok = args[0].(string)
|
||||
}
|
||||
if !ok {
|
||||
s = fmt.Sprint(args...)
|
||||
}
|
||||
// find the @ symbol
|
||||
substrs := strings.Split(s, "@")
|
||||
if len(substrs) != 2 {
|
||||
return s
|
||||
}
|
||||
// replace the @ by " at "
|
||||
return (substrs[0] + " at " + substrs[1])
|
||||
}
|
||||
|
||||
func main() {
|
||||
f1 := Friend{Fname: "minux.ma"}
|
||||
f2 := Friend{Fname: "xushiwei"}
|
||||
t := template.New("fieldname example")
|
||||
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
|
||||
t, _ = t.Parse(`hello {{.UserName}}!
|
||||
{{range .Emails}}
|
||||
an emails {{.|emailDeal}}
|
||||
{{end}}
|
||||
{{with .Friends}}
|
||||
{{range .}}
|
||||
my friend name is {{.Fname}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
`)
|
||||
p := Person{UserName: "Astaxie",
|
||||
Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"},
|
||||
Friends: []*Friend{&f1, &f2}}
|
||||
t.Execute(os.Stdout, p)
|
||||
}
|
||||
|
||||
|
||||
上面演示了如何自定义函数,其实,在模板包内部已经有内置的实现函数,下面代码截取自模板包里面
|
||||
|
||||
var builtins = FuncMap{
|
||||
"and": and,
|
||||
"call": call,
|
||||
"html": HTMLEscaper,
|
||||
"index": index,
|
||||
"js": JSEscaper,
|
||||
"len": length,
|
||||
"not": not,
|
||||
"or": or,
|
||||
"print": fmt.Sprint,
|
||||
"printf": fmt.Sprintf,
|
||||
"println": fmt.Sprintln,
|
||||
"urlquery": URLQueryEscaper,
|
||||
}
|
||||
|
||||
|
||||
## Must操作
|
||||
模板包里面有一个函数`Must`,它的作用是检测模板是否正确,例如大括号是否匹配,注释是否正确的关闭,变量是否正确的书写。接下来我们演示一个例子,用Must来判断模板是否正确:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func main() {
|
||||
tOk := template.New("first")
|
||||
template.Must(tOk.Parse(" some static text /* and a comment */"))
|
||||
fmt.Println("The first one parsed OK.")
|
||||
|
||||
template.Must(template.New("second").Parse("some static text {{ .Name }}"))
|
||||
fmt.Println("The second one parsed OK.")
|
||||
|
||||
fmt.Println("The next one ought to fail.")
|
||||
tErr := template.New("check parse error with Must")
|
||||
template.Must(tErr.Parse(" some static text {{ .Name }"))
|
||||
}
|
||||
|
||||
将输出如下内容
|
||||
|
||||
The first one parsed OK.
|
||||
The second one parsed OK.
|
||||
The next one ought to fail.
|
||||
panic: template: check parse error with Must:1: unexpected "}" in command
|
||||
|
||||
## 嵌套模板
|
||||
我们平常开发Web应用的时候,经常会遇到一些模板有些部分是固定不变的,然后可以抽取出来作为一个独立的部分,例如一个博客的头部和尾部是不变的,而唯一改变的是中间的内容部分。所以我们可以定义成`header`、`content`、`footer`三个部分。Go语言中通过如下的语法来申明
|
||||
|
||||
{{define "子模板名称"}}内容{{end}}
|
||||
|
||||
通过如下方式来调用:
|
||||
|
||||
{{template "子模板名称"}}
|
||||
|
||||
接下来我们演示如何使用嵌套模板,我们定义三个文件,`header.tmpl`、`content.tmpl`、`footer.tmpl`文件,里面的内容如下
|
||||
|
||||
//header.tmpl
|
||||
{{define "header"}}
|
||||
<html>
|
||||
<head>
|
||||
<title>演示信息</title>
|
||||
</head>
|
||||
<body>
|
||||
{{end}}
|
||||
|
||||
//content.tmpl
|
||||
{{define "content"}}
|
||||
{{template "header"}}
|
||||
<h1>演示嵌套</h1>
|
||||
<ul>
|
||||
<li>嵌套使用define定义子模板</li>
|
||||
<li>调用使用template</li>
|
||||
</ul>
|
||||
{{template "footer"}}
|
||||
{{end}}
|
||||
|
||||
//footer.tmpl
|
||||
{{define "footer"}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
演示代码如下:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func main() {
|
||||
s1, _ := template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl")
|
||||
s1.ExecuteTemplate(os.Stdout, "header", nil)
|
||||
fmt.Println()
|
||||
s1.ExecuteTemplate(os.Stdout, "content", nil)
|
||||
fmt.Println()
|
||||
s1.ExecuteTemplate(os.Stdout, "footer", nil)
|
||||
fmt.Println()
|
||||
s1.Execute(os.Stdout, nil)
|
||||
}
|
||||
|
||||
通过上面的例子我们可以看到通过`template.ParseFiles`把所有的嵌套模板全部解析到模板里面,其实每一个定义的{{define}}都是一个独立的模板,他们相互独立,是并行存在的关系,内部其实存储的是类似map的一种关系(key是模板的名称,value是模板的内容),然后我们通过`ExecuteTemplate`来执行相应的子模板内容,我们可以看到header、footer都是相对独立的,都能输出内容,content 中因为嵌套了header和footer的内容,就会同时输出三个的内容。但是当我们执行`s1.Execute`,没有任何的输出,因为在默认的情况下没有默认的子模板,所以不会输出任何的东西。
|
||||
|
||||
>同一个集合类的模板是互相知晓的,如果同一模板被多个集合使用,则它需要在多个集合中分别解析
|
||||
|
||||
## 总结
|
||||
通过上面对模板的详细介绍,我们了解了如何把动态数据与模板融合:如何输出循环数据、如何自定义函数、如何嵌套模板等等。通过模板技术的应用,我们可以完成MVC模式中V的处理,接下来的章节我们将介绍如何来处理M和C。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [正则处理](<07.3.md>)
|
||||
* 下一节: [文件操作](<07.5.md>)
|
||||
350
zh/07.4.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
350
zh/07.4.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,350 @@
|
||||
# 7.4 模板处理
|
||||
## 什么是模板
|
||||
你一定听说过一种叫做MVC的设计模式,Model处理数据,View展现结果,Controller控制用户的请求,至于View层的处理,在很多动态语言里面都是通过在静态HTML中插入动态语言生成的数据,例如JSP中通过插入`<%=....=%>`,PHP中通过插入`<?php.....?>`来实现的。
|
||||
|
||||
通过下面这个图可以说明模板的机制
|
||||
|
||||

|
||||
|
||||
图7.1 模板机制图
|
||||
|
||||
Web应用反馈给客户端的信息中的大部分内容是静态的,不变的,而另外少部分是根据用户的请求来动态生成的,例如要显示用户的访问记录列表。用户之间只有记录数据是不同的,而列表的样式则是固定的,此时采用模板可以复用很多静态代码。
|
||||
|
||||
## Go模板使用
|
||||
在Go语言中,我们使用`template`包来进行模板处理,使用类似`Parse`、`ParseFile`、`Execute`等方法从文件或者字符串加载模板,然后执行类似上面图片展示的模板的merge操作。请看下面的例子:
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
t := template.New("some template") //创建一个模板
|
||||
t, _ = t.ParseFiles("tmpl/welcome.html", nil) //解析模板文件
|
||||
user := GetUser() //获取当前用户信息
|
||||
t.Execute(w, user) //执行模板的merger操作
|
||||
}
|
||||
|
||||
通过上面的例子我们可以看到Go语言的模板操作非常的简单方便,和其他语言的模板处理类似,都是先获取数据,然后渲染数据。
|
||||
|
||||
为了演示和测试代码的方便,我们在接下来的例子中采用如下格式的代码
|
||||
|
||||
- 使用Parse代替ParseFiles,因为Parse可以直接测试一个字符串,而不需要额外的文件
|
||||
- 不使用handler来写演示代码,而是每个测试一个main,方便测试
|
||||
- 使用`os.Stdout`代替`http.ResponseWriter`,因为`os.Stdout`实现了`io.Writer`接口
|
||||
|
||||
## 模板中如何插入数据?
|
||||
上面我们演示了如何解析并渲染模板,接下来让我们来更加详细的了解如何把数据渲染出来。一个模板都是应用在一个Go的对象之上,Go对象的字段如何插入到模板中呢?
|
||||
|
||||
### 字段操作
|
||||
Go语言的模板通过`{{}}`来包含需要在渲染时被替换的字段,`{{.}}`表示当前的对象,这和Java或者C++中的this类似,如果要访问当前对象的字段通过`{{.FieldName}}`,但是需要注意一点:这个字段必须是导出的(字段首字母必须是大写的),否则在渲染的时候就会报错,请看下面的这个例子:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Person struct {
|
||||
UserName string
|
||||
}
|
||||
|
||||
func main() {
|
||||
t := template.New("fieldname example")
|
||||
t, _ = t.Parse("hello {{.UserName}}!")
|
||||
p := Person{UserName: "Astaxie"}
|
||||
t.Execute(os.Stdout, p)
|
||||
}
|
||||
|
||||
上面的代码我们可以正确的输出`hello Astaxie`,但是如果我们稍微修改一下代码,在模板中含有了未导出的字段,那么就会报错
|
||||
|
||||
type Person struct {
|
||||
UserName string
|
||||
email string //未导出的字段,首字母是小写的
|
||||
}
|
||||
|
||||
t, _ = t.Parse("hello {{.UserName}}! {{.email}}")
|
||||
|
||||
上面的代码就会报错,因为我们调用了一个未导出的字段,但是如果我们调用了一个不存在的字段是不会报错的,而是输出为空。
|
||||
|
||||
如果模板中输出`{{.}}`,这个一般应用于字符串对象,默认会调用fmt包输出字符串的内容。
|
||||
|
||||
### 输出嵌套字段内容
|
||||
上面我们例子展示了如何针对一个对象的字段输出,那么如果字段里面还有对象,如何来循环的输出这些内容呢?我们可以使用`{{with …}}…{{end}}`和`{{range …}}{{end}}`来进行数据的输出。
|
||||
|
||||
- {{range}} 这个和Go语法里面的range类似,循环操作数据
|
||||
- {{with}}操作是指当前对象的值,类似上下文的概念
|
||||
|
||||
详细的使用请看下面的例子:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Friend struct {
|
||||
Fname string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
UserName string
|
||||
Emails []string
|
||||
Friends []*Friend
|
||||
}
|
||||
|
||||
func main() {
|
||||
f1 := Friend{Fname: "minux.ma"}
|
||||
f2 := Friend{Fname: "xushiwei"}
|
||||
t := template.New("fieldname example")
|
||||
t, _ = t.Parse(`hello {{.UserName}}!
|
||||
{{range .Emails}}
|
||||
an email {{.}}
|
||||
{{end}}
|
||||
{{with .Friends}}
|
||||
{{range .}}
|
||||
my friend name is {{.Fname}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
`)
|
||||
p := Person{UserName: "Astaxie",
|
||||
Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"},
|
||||
Friends: []*Friend{&f1, &f2}}
|
||||
t.Execute(os.Stdout, p)
|
||||
}
|
||||
|
||||
### 条件处理
|
||||
在Go模板里面如果需要进行条件判断,那么我们可以使用和Go语言的`if-else`语法类似的方式来处理,如果pipeline为空,那么if就认为是false,下面的例子展示了如何使用`if-else`语法:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func main() {
|
||||
tEmpty := template.New("template test")
|
||||
tEmpty = template.Must(tEmpty.Parse("空 pipeline if demo: {{if ``}} 不会输出. {{end}}\n"))
|
||||
tEmpty.Execute(os.Stdout, nil)
|
||||
|
||||
tWithValue := template.New("template test")
|
||||
tWithValue = template.Must(tWithValue.Parse("不为空的 pipeline if demo: {{if `anything`}} 我有内容,我会输出. {{end}}\n"))
|
||||
tWithValue.Execute(os.Stdout, nil)
|
||||
|
||||
tIfElse := template.New("template test")
|
||||
tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if部分 {{else}} else部分.{{end}}\n"))
|
||||
tIfElse.Execute(os.Stdout, nil)
|
||||
}
|
||||
|
||||
通过上面的演示代码我们知道`if-else`语法相当的简单,在使用过程中很容易集成到我们的模板代码中。
|
||||
|
||||
> 注意:if里面无法使用条件判断,例如.Mail=="astaxie@gmail.com",这样的判断是不正确的,if里面只能是bool值
|
||||
|
||||
### pipelines
|
||||
Unix用户已经很熟悉什么是`pipe`了,`ls | grep "beego"`类似这样的语法你是不是经常使用,过滤当前目录下面的文件,显示含有"beego"的数据,表达的意思就是前面的输出可以当做后面的输入,最后显示我们想要的数据,而Go语言模板最强大的一点就是支持pipe数据,在Go语言里面任何`{{}}`里面的都是pipelines数据,例如我们上面输出的email里面如果还有一些可能引起XSS注入的,那么我们如何来进行转化呢?
|
||||
|
||||
{{. | html}}
|
||||
|
||||
在email输出的地方我们可以采用如上方式可以把输出全部转化html的实体,上面的这种方式和我们平常写Unix的方式是不是一模一样,操作起来相当的简便,调用其他的函数也是类似的方式。
|
||||
|
||||
### 模板变量
|
||||
有时候,我们在模板使用过程中需要定义一些局部变量,我们可以在一些操作中申明局部变量,例如`with``range``if`过程中申明局部变量,这个变量的作用域是`{{end}}`之前,Go语言通过申明的局部变量格式如下所示:
|
||||
|
||||
$variable := pipeline
|
||||
|
||||
详细的例子看下面的:
|
||||
|
||||
{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
|
||||
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
|
||||
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}
|
||||
### 模板函数
|
||||
模板在输出对象的字段值时,采用了`fmt`包把对象转化成了字符串。但是有时候我们的需求可能不是这样的,例如有时候我们为了防止垃圾邮件发送者通过采集网页的方式来发送给我们的邮箱信息,我们希望把`@`替换成`at`例如:`astaxie at beego.me`,如果要实现这样的功能,我们就需要自定义函数来做这个功能。
|
||||
|
||||
每一个模板函数都有一个唯一值的名字,然后与一个Go函数关联,通过如下的方式来关联
|
||||
|
||||
type FuncMap map[string]interface{}
|
||||
|
||||
例如,如果我们想要的email函数的模板函数名是`emailDeal`,它关联的Go函数名称是`EmailDealWith`,那么我们可以通过下面的方式来注册这个函数
|
||||
|
||||
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
|
||||
|
||||
`EmailDealWith`这个函数的参数和返回值定义如下:
|
||||
|
||||
func EmailDealWith(args …interface{}) string
|
||||
|
||||
我们来看下面的实现例子:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Friend struct {
|
||||
Fname string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
UserName string
|
||||
Emails []string
|
||||
Friends []*Friend
|
||||
}
|
||||
|
||||
func EmailDealWith(args ...interface{}) string {
|
||||
ok := false
|
||||
var s string
|
||||
if len(args) == 1 {
|
||||
s, ok = args[0].(string)
|
||||
}
|
||||
if !ok {
|
||||
s = fmt.Sprint(args...)
|
||||
}
|
||||
// find the @ symbol
|
||||
substrs := strings.Split(s, "@")
|
||||
if len(substrs) != 2 {
|
||||
return s
|
||||
}
|
||||
// replace the @ by " at "
|
||||
return (substrs[0] + " at " + substrs[1])
|
||||
}
|
||||
|
||||
func main() {
|
||||
f1 := Friend{Fname: "minux.ma"}
|
||||
f2 := Friend{Fname: "xushiwei"}
|
||||
t := template.New("fieldname example")
|
||||
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
|
||||
t, _ = t.Parse(`hello {{.UserName}}!
|
||||
{{range .Emails}}
|
||||
an emails {{.|emailDeal}}
|
||||
{{end}}
|
||||
{{with .Friends}}
|
||||
{{range .}}
|
||||
my friend name is {{.Fname}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
`)
|
||||
p := Person{UserName: "Astaxie",
|
||||
Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"},
|
||||
Friends: []*Friend{&f1, &f2}}
|
||||
t.Execute(os.Stdout, p)
|
||||
}
|
||||
|
||||
|
||||
上面演示了如何自定义函数,其实,在模板包内部已经有内置的实现函数,下面代码截取自模板包里面
|
||||
|
||||
var builtins = FuncMap{
|
||||
"and": and,
|
||||
"call": call,
|
||||
"html": HTMLEscaper,
|
||||
"index": index,
|
||||
"js": JSEscaper,
|
||||
"len": length,
|
||||
"not": not,
|
||||
"or": or,
|
||||
"print": fmt.Sprint,
|
||||
"printf": fmt.Sprintf,
|
||||
"println": fmt.Sprintln,
|
||||
"urlquery": URLQueryEscaper,
|
||||
}
|
||||
|
||||
|
||||
## Must操作
|
||||
模板包里面有一个函数`Must`,它的作用是检测模板是否正确,例如大括号是否匹配,注释是否正确的关闭,变量是否正确的书写。接下来我们演示一个例子,用Must来判断模板是否正确:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func main() {
|
||||
tOk := template.New("first")
|
||||
template.Must(tOk.Parse(" some static text /* and a comment */"))
|
||||
fmt.Println("The first one parsed OK.")
|
||||
|
||||
template.Must(template.New("second").Parse("some static text {{ .Name }}"))
|
||||
fmt.Println("The second one parsed OK.")
|
||||
|
||||
fmt.Println("The next one ought to fail.")
|
||||
tErr := template.New("check parse error with Must")
|
||||
template.Must(tErr.Parse(" some static text {{ .Name }"))
|
||||
}
|
||||
|
||||
将输出如下内容
|
||||
|
||||
The first one parsed OK.
|
||||
The second one parsed OK.
|
||||
The next one ought to fail.
|
||||
panic: template: check parse error with Must:1: unexpected "}" in command
|
||||
|
||||
## 嵌套模板
|
||||
我们平常开发Web应用的时候,经常会遇到一些模板有些部分是固定不变的,然后可以抽取出来作为一个独立的部分,例如一个博客的头部和尾部是不变的,而唯一改变的是中间的内容部分。所以我们可以定义成`header`、`content`、`footer`三个部分。Go语言中通过如下的语法来申明
|
||||
|
||||
{{define "子模板名称"}}内容{{end}}
|
||||
|
||||
通过如下方式来调用:
|
||||
|
||||
{{template "子模板名称"}}
|
||||
|
||||
接下来我们演示如何使用嵌套模板,我们定义三个文件,`header.tmpl`、`content.tmpl`、`footer.tmpl`文件,里面的内容如下
|
||||
|
||||
//header.tmpl
|
||||
{{define "header"}}
|
||||
<html>
|
||||
<head>
|
||||
<title>演示信息</title>
|
||||
</head>
|
||||
<body>
|
||||
{{end}}
|
||||
|
||||
//content.tmpl
|
||||
{{define "content"}}
|
||||
{{template "header"}}
|
||||
<h1>演示嵌套</h1>
|
||||
<ul>
|
||||
<li>嵌套使用define定义子模板</li>
|
||||
<li>调用使用template</li>
|
||||
</ul>
|
||||
{{template "footer"}}
|
||||
{{end}}
|
||||
|
||||
//footer.tmpl
|
||||
{{define "footer"}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
演示代码如下:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func main() {
|
||||
s1, _ := template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl")
|
||||
s1.ExecuteTemplate(os.Stdout, "header", nil)
|
||||
fmt.Println()
|
||||
s1.ExecuteTemplate(os.Stdout, "content", nil)
|
||||
fmt.Println()
|
||||
s1.ExecuteTemplate(os.Stdout, "footer", nil)
|
||||
fmt.Println()
|
||||
s1.Execute(os.Stdout, nil)
|
||||
}
|
||||
|
||||
通过上面的例子我们可以看到通过`template.ParseFiles`把所有的嵌套模板全部解析到模板里面,其实每一个定义的{{define}}都是一个独立的模板,他们相互独立,是并行存在的关系,内部其实存储的是类似map的一种关系(key是模板的名称,value是模板的内容),然后我们通过`ExecuteTemplate`来执行相应的子模板内容,我们可以看到header、footer都是相对独立的,都能输出内容,content 中因为嵌套了header和footer的内容,就会同时输出三个的内容。但是当我们执行`s1.Execute`,没有任何的输出,因为在默认的情况下没有默认的子模板,所以不会输出任何的东西。
|
||||
|
||||
>同一个集合类的模板是互相知晓的,如果同一模板被多个集合使用,则它需要在多个集合中分别解析
|
||||
|
||||
## 总结
|
||||
通过上面对模板的详细介绍,我们了解了如何把动态数据与模板融合:如何输出循环数据、如何自定义函数、如何嵌套模板等等。通过模板技术的应用,我们可以完成MVC模式中V的处理,接下来的章节我们将介绍如何来处理M和C。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [正则处理](<07.3.md>)
|
||||
* 下一节: [文件操作](<07.5.md>)
|
||||
@@ -1,7 +1,7 @@
|
||||
# 7.7 小结
|
||||
这一章给大家介绍了一些文本处理的工具,包括XML、JSON、正则和模板技术,XML和JSON是数据交互的工具,通过XML和JSON你可以表达各种含义,通过正则你可以处理文本(搜索、替换、截取),通过模板技术你可以展现这些数据给用户。这些都是你开发Web应用过程中需要用到的技术,通过这个小节的介绍你能够了解如何处理文本、展现文本。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [字符串处理](<07.6.md>)
|
||||
* 下一节: [Web服务](<08.0.md>)
|
||||
# 7.7 小结
|
||||
这一章给大家介绍了一些文本处理的工具,包括XML、JSON、正则和模板技术,XML和JSON是数据交互的工具,通过XML和JSON你可以表达各种含义,通过正则你可以处理文本(搜索、替换、截取),通过模板技术你可以展现这些数据给用户。这些都是你开发Web应用过程中需要用到的技术,通过这个小节的介绍你能够了解如何处理文本、展现文本。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [字符串处理](<07.6.md>)
|
||||
* 下一节: [Web服务](<08.0.md>)
|
||||
7
zh/07.7.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
7
zh/07.7.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,7 @@
|
||||
# 7.7 小结
|
||||
这一章给大家介绍了一些文本处理的工具,包括XML、JSON、正则和模板技术,XML和JSON是数据交互的工具,通过XML和JSON你可以表达各种含义,通过正则你可以处理文本(搜索、替换、截取),通过模板技术你可以展现这些数据给用户。这些都是你开发Web应用过程中需要用到的技术,通过这个小节的介绍你能够了解如何处理文本、展现文本。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [字符串处理](<07.6.md>)
|
||||
* 下一节: [Web服务](<08.0.md>)
|
||||
@@ -1,20 +1,20 @@
|
||||
# 8 Web服务
|
||||
Web服务可以让你在HTTP协议的基础上通过XML或者JSON来交换信息。如果你想知道上海的天气预报、中国石油的股价或者淘宝商家的一个商品信息,你可以编写一段简短的代码,通过抓取这些信息然后通过标准的接口开放出来,就如同你调用一个本地函数并返回一个值。
|
||||
|
||||
Web服务背后的关键在于平台的无关性,你可以运行你的服务在Linux系统,可以与其他Windows的asp.net程序交互,同样的,也可以通过同一个接口和运行在FreeBSD上面的JSP无障碍地通信。
|
||||
|
||||
目前主流的有如下几种Web服务:REST、SOAP。
|
||||
|
||||
REST请求是很直观的,因为REST是基于HTTP协议的一个补充,他的每一次请求都是一个HTTP请求,然后根据不同的method来处理不同的逻辑,很多Web开发者都熟悉HTTP协议,所以学习REST是一件比较容易的事情。所以我们在8.3小节讲详细的讲解如何在Go语言中来实现REST方式。
|
||||
|
||||
SOAP是W3C在跨网络信息传递和远程计算机函数调用方面的一个标准。但是SOAP非常复杂,其完整的规范篇幅很长,而且内容仍然在增加。Go语言是以简单著称,所以我们不会介绍SOAP这样复杂的东西。而Go语言提供了一种天生性能很不错,开发起来很方便的RPC机制,我们将会在8.4小节详细介绍如何使用Go语言来实现RPC。
|
||||
|
||||
Go语言是21世纪的C语言,我们追求的是性能、简单,所以我们在8.1小节里面介绍如何使用Socket编程,很多游戏服务都是采用Socket来编写服务端,因为HTTP协议相对而言比较耗费性能,让我们看看Go语言如何来Socket编程。目前随着HTML5的发展,webSockets也逐渐的成为很多页游公司接下来开发的一些手段,我们将在8.2小节里面讲解Go语言如何编写webSockets的代码。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第七章总结](<07.7.md>)
|
||||
* 下一节: [Socket编程](<08.1.md>)
|
||||
# 8 Web服务
|
||||
Web服务可以让你在HTTP协议的基础上通过XML或者JSON来交换信息。如果你想知道上海的天气预报、中国石油的股价或者淘宝商家的一个商品信息,你可以编写一段简短的代码,通过抓取这些信息然后通过标准的接口开放出来,就如同你调用一个本地函数并返回一个值。
|
||||
|
||||
Web服务背后的关键在于平台的无关性,你可以运行你的服务在Linux系统,可以与其他Windows的asp.net程序交互,同样的,也可以通过同一个接口和运行在FreeBSD上面的JSP无障碍地通信。
|
||||
|
||||
目前主流的有如下几种Web服务:REST、SOAP。
|
||||
|
||||
REST请求是很直观的,因为REST是基于HTTP协议的一个补充,他的每一次请求都是一个HTTP请求,然后根据不同的method来处理不同的逻辑,很多Web开发者都熟悉HTTP协议,所以学习REST是一件比较容易的事情。所以我们在8.3小节讲详细的讲解如何在Go语言中来实现REST方式。
|
||||
|
||||
SOAP是W3C在跨网络信息传递和远程计算机函数调用方面的一个标准。但是SOAP非常复杂,其完整的规范篇幅很长,而且内容仍然在增加。Go语言是以简单著称,所以我们不会介绍SOAP这样复杂的东西。而Go语言提供了一种天生性能很不错,开发起来很方便的RPC机制,我们将会在8.4小节详细介绍如何使用Go语言来实现RPC。
|
||||
|
||||
Go语言是21世纪的C语言,我们追求的是性能、简单,所以我们在8.1小节里面介绍如何使用Socket编程,很多游戏服务都是采用Socket来编写服务端,因为HTTP协议相对而言比较耗费性能,让我们看看Go语言如何来Socket编程。目前随着HTML5的发展,webSockets也逐渐的成为很多页游公司接下来开发的一些手段,我们将在8.2小节里面讲解Go语言如何编写webSockets的代码。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第七章总结](<07.7.md>)
|
||||
* 下一节: [Socket编程](<08.1.md>)
|
||||
20
zh/08.0.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
20
zh/08.0.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,20 @@
|
||||
# 8 Web服务
|
||||
Web服务可以让你在HTTP协议的基础上通过XML或者JSON来交换信息。如果你想知道上海的天气预报、中国石油的股价或者淘宝商家的一个商品信息,你可以编写一段简短的代码,通过抓取这些信息然后通过标准的接口开放出来,就如同你调用一个本地函数并返回一个值。
|
||||
|
||||
Web服务背后的关键在于平台的无关性,你可以运行你的服务在Linux系统,可以与其他Windows的asp.net程序交互,同样的,也可以通过同一个接口和运行在FreeBSD上面的JSP无障碍地通信。
|
||||
|
||||
目前主流的有如下几种Web服务:REST、SOAP。
|
||||
|
||||
REST请求是很直观的,因为REST是基于HTTP协议的一个补充,他的每一次请求都是一个HTTP请求,然后根据不同的method来处理不同的逻辑,很多Web开发者都熟悉HTTP协议,所以学习REST是一件比较容易的事情。所以我们在8.3小节讲详细的讲解如何在Go语言中来实现REST方式。
|
||||
|
||||
SOAP是W3C在跨网络信息传递和远程计算机函数调用方面的一个标准。但是SOAP非常复杂,其完整的规范篇幅很长,而且内容仍然在增加。Go语言是以简单著称,所以我们不会介绍SOAP这样复杂的东西。而Go语言提供了一种天生性能很不错,开发起来很方便的RPC机制,我们将会在8.4小节详细介绍如何使用Go语言来实现RPC。
|
||||
|
||||
Go语言是21世纪的C语言,我们追求的是性能、简单,所以我们在8.1小节里面介绍如何使用Socket编程,很多游戏服务都是采用Socket来编写服务端,因为HTTP协议相对而言比较耗费性能,让我们看看Go语言如何来Socket编程。目前随着HTML5的发展,webSockets也逐渐的成为很多页游公司接下来开发的一些手段,我们将在8.2小节里面讲解Go语言如何编写webSockets的代码。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第七章总结](<07.7.md>)
|
||||
* 下一节: [Socket编程](<08.1.md>)
|
||||
@@ -1,151 +1,151 @@
|
||||
# 8.2 WebSocket
|
||||
WebSocket是HTML5的重要特性,它实现了基于浏览器的远程socket,它使浏览器和服务器可以进行全双工通信,许多浏览器(Firefox、Google Chrome和Safari)都已对此做了支持。
|
||||
|
||||
在WebSocket出现之前,为了实现即时通信,采用的技术都是“轮询”,即在特定的时间间隔内,由浏览器对服务器发出HTTP Request,服务器在收到请求后,返回最新的数据给浏览器刷新,“轮询”使得浏览器需要对服务器不断发出请求,这样会占用大量带宽。
|
||||
|
||||
WebSocket采用了一些特殊的报头,使得浏览器和服务器只需要做一个握手的动作,就可以在浏览器和服务器之间建立一条连接通道。且此连接会保持在活动状态,你可以使用JavaScript来向连接写入或从中接收数据,就像在使用一个常规的TCP Socket一样。它解决了Web实时化的问题,相比传统HTTP有如下好处:
|
||||
|
||||
- 一个Web客户端只建立一个TCP连接
|
||||
- Websocket服务端可以推送(push)数据到web客户端.
|
||||
- 有更加轻量级的头,减少数据传送量
|
||||
|
||||
WebSocket URL的起始输入是ws://或是wss://(在SSL上)。下图展示了WebSocket的通信过程,一个带有特定报头的HTTP握手被发送到了服务器端,接着在服务器端或是客户端就可以通过JavaScript来使用某种套接口(socket),这一套接口可被用来通过事件句柄异步地接收数据。
|
||||
|
||||

|
||||
|
||||
图8.2 WebSocket原理图
|
||||
|
||||
## WebSocket原理
|
||||
WebSocket的协议颇为简单,在第一次handshake通过以后,连接便建立成功,其后的通讯数据都是以”\x00″开头,以”\xFF”结尾。在客户端,这个是透明的,WebSocket组件会自动将原始数据“掐头去尾”。
|
||||
|
||||
浏览器发出WebSocket连接请求,然后服务器发出回应,然后连接建立成功,这个过程通常称为“握手” (handshaking)。请看下面的请求和反馈信息:
|
||||
|
||||

|
||||
|
||||
图8.3 WebSocket的request和response信息
|
||||
|
||||
在请求中的"Sec-WebSocket-Key"是随机的,对于整天跟编码打交到的程序员,一眼就可以看出来:这个是一个经过base64编码后的数据。服务器端接收到这个请求之后需要把这个字符串连接上一个固定的字符串:
|
||||
|
||||
258EAFA5-E914-47DA-95CA-C5AB0DC85B11
|
||||
|
||||
即:`f7cb4ezEAl6C3wRaU6JORA==`连接上那一串固定字符串,生成一个这样的字符串:
|
||||
|
||||
f7cb4ezEAl6C3wRaU6JORA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
|
||||
|
||||
对该字符串先用 sha1安全散列算法计算出二进制的值,然后用base64对其进行编码,即可以得到握手后的字符串:
|
||||
|
||||
rE91AJhfC+6JdVcVXOGJEADEJdQ=
|
||||
|
||||
将之作为响应头`Sec-WebSocket-Accept`的值反馈给客户端。
|
||||
|
||||
## Go实现WebSocket
|
||||
Go语言标准包里面没有提供对WebSocket的支持,但是在由官方维护的go.net子包中有对这个的支持,你可以通过如下的命令获取该包:
|
||||
|
||||
go get code.google.com/p/go.net/websocket
|
||||
|
||||
WebSocket分为客户端和服务端,接下来我们将实现一个简单的例子:用户输入信息,客户端通过WebSocket将信息发送给服务器端,服务器端收到信息之后主动Push信息到客户端,然后客户端将输出其收到的信息,客户端的代码如下:
|
||||
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
var sock = null;
|
||||
var wsuri = "ws://127.0.0.1:1234";
|
||||
|
||||
window.onload = function() {
|
||||
|
||||
console.log("onload");
|
||||
|
||||
sock = new WebSocket(wsuri);
|
||||
|
||||
sock.onopen = function() {
|
||||
console.log("connected to " + wsuri);
|
||||
}
|
||||
|
||||
sock.onclose = function(e) {
|
||||
console.log("connection closed (" + e.code + ")");
|
||||
}
|
||||
|
||||
sock.onmessage = function(e) {
|
||||
console.log("message received: " + e.data);
|
||||
}
|
||||
};
|
||||
|
||||
function send() {
|
||||
var msg = document.getElementById('message').value;
|
||||
sock.send(msg);
|
||||
};
|
||||
</script>
|
||||
<h1>WebSocket Echo Test</h1>
|
||||
<form>
|
||||
<p>
|
||||
Message: <input id="message" type="text" value="Hello, world!">
|
||||
</p>
|
||||
</form>
|
||||
<button onclick="send();">Send Message</button>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
可以看到客户端JS,很容易的就通过WebSocket函数建立了一个与服务器的连接sock,当握手成功后,会触发WebScoket对象的onopen事件,告诉客户端连接已经成功建立。客户端一共绑定了四个事件。
|
||||
|
||||
- 1)onopen 建立连接后触发
|
||||
- 2)onmessage 收到消息后触发
|
||||
- 3)onerror 发生错误时触发
|
||||
- 4)onclose 关闭连接时触发
|
||||
|
||||
我们服务器端的实现如下:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/net/websocket"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Echo(ws *websocket.Conn) {
|
||||
var err error
|
||||
|
||||
for {
|
||||
var reply string
|
||||
|
||||
if err = websocket.Message.Receive(ws, &reply); err != nil {
|
||||
fmt.Println("Can't receive")
|
||||
break
|
||||
}
|
||||
|
||||
fmt.Println("Received back from client: " + reply)
|
||||
|
||||
msg := "Received: " + reply
|
||||
fmt.Println("Sending to client: " + msg)
|
||||
|
||||
if err = websocket.Message.Send(ws, msg); err != nil {
|
||||
fmt.Println("Can't send")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.Handle("/", websocket.Handler(Echo))
|
||||
|
||||
if err := http.ListenAndServe(":1234", nil); err != nil {
|
||||
log.Fatal("ListenAndServe:", err)
|
||||
}
|
||||
}
|
||||
|
||||
当客户端将用户输入的信息Send之后,服务器端通过Receive接收到了相应信息,然后通过Send发送了应答信息。
|
||||
|
||||

|
||||
|
||||
图8.4 WebSocket服务器端接收到的信息
|
||||
|
||||
通过上面的例子我们看到客户端和服务器端实现WebSocket非常的方便,Go的源码net分支中已经实现了这个的协议,我们可以直接拿来用,目前随着HTML5的发展,我想未来WebSocket会是Web开发的一个重点,我们需要储备这方面的知识。
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [Socket编程](<08.1.md>)
|
||||
* 下一节: [REST](<08.3.md>)
|
||||
# 8.2 WebSocket
|
||||
WebSocket是HTML5的重要特性,它实现了基于浏览器的远程socket,它使浏览器和服务器可以进行全双工通信,许多浏览器(Firefox、Google Chrome和Safari)都已对此做了支持。
|
||||
|
||||
在WebSocket出现之前,为了实现即时通信,采用的技术都是“轮询”,即在特定的时间间隔内,由浏览器对服务器发出HTTP Request,服务器在收到请求后,返回最新的数据给浏览器刷新,“轮询”使得浏览器需要对服务器不断发出请求,这样会占用大量带宽。
|
||||
|
||||
WebSocket采用了一些特殊的报头,使得浏览器和服务器只需要做一个握手的动作,就可以在浏览器和服务器之间建立一条连接通道。且此连接会保持在活动状态,你可以使用JavaScript来向连接写入或从中接收数据,就像在使用一个常规的TCP Socket一样。它解决了Web实时化的问题,相比传统HTTP有如下好处:
|
||||
|
||||
- 一个Web客户端只建立一个TCP连接
|
||||
- Websocket服务端可以推送(push)数据到web客户端.
|
||||
- 有更加轻量级的头,减少数据传送量
|
||||
|
||||
WebSocket URL的起始输入是ws://或是wss://(在SSL上)。下图展示了WebSocket的通信过程,一个带有特定报头的HTTP握手被发送到了服务器端,接着在服务器端或是客户端就可以通过JavaScript来使用某种套接口(socket),这一套接口可被用来通过事件句柄异步地接收数据。
|
||||
|
||||

|
||||
|
||||
图8.2 WebSocket原理图
|
||||
|
||||
## WebSocket原理
|
||||
WebSocket的协议颇为简单,在第一次handshake通过以后,连接便建立成功,其后的通讯数据都是以”\x00″开头,以”\xFF”结尾。在客户端,这个是透明的,WebSocket组件会自动将原始数据“掐头去尾”。
|
||||
|
||||
浏览器发出WebSocket连接请求,然后服务器发出回应,然后连接建立成功,这个过程通常称为“握手” (handshaking)。请看下面的请求和反馈信息:
|
||||
|
||||

|
||||
|
||||
图8.3 WebSocket的request和response信息
|
||||
|
||||
在请求中的"Sec-WebSocket-Key"是随机的,对于整天跟编码打交到的程序员,一眼就可以看出来:这个是一个经过base64编码后的数据。服务器端接收到这个请求之后需要把这个字符串连接上一个固定的字符串:
|
||||
|
||||
258EAFA5-E914-47DA-95CA-C5AB0DC85B11
|
||||
|
||||
即:`f7cb4ezEAl6C3wRaU6JORA==`连接上那一串固定字符串,生成一个这样的字符串:
|
||||
|
||||
f7cb4ezEAl6C3wRaU6JORA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
|
||||
|
||||
对该字符串先用 sha1安全散列算法计算出二进制的值,然后用base64对其进行编码,即可以得到握手后的字符串:
|
||||
|
||||
rE91AJhfC+6JdVcVXOGJEADEJdQ=
|
||||
|
||||
将之作为响应头`Sec-WebSocket-Accept`的值反馈给客户端。
|
||||
|
||||
## Go实现WebSocket
|
||||
Go语言标准包里面没有提供对WebSocket的支持,但是在由官方维护的go.net子包中有对这个的支持,你可以通过如下的命令获取该包:
|
||||
|
||||
go get code.google.com/p/go.net/websocket
|
||||
|
||||
WebSocket分为客户端和服务端,接下来我们将实现一个简单的例子:用户输入信息,客户端通过WebSocket将信息发送给服务器端,服务器端收到信息之后主动Push信息到客户端,然后客户端将输出其收到的信息,客户端的代码如下:
|
||||
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
var sock = null;
|
||||
var wsuri = "ws://127.0.0.1:1234";
|
||||
|
||||
window.onload = function() {
|
||||
|
||||
console.log("onload");
|
||||
|
||||
sock = new WebSocket(wsuri);
|
||||
|
||||
sock.onopen = function() {
|
||||
console.log("connected to " + wsuri);
|
||||
}
|
||||
|
||||
sock.onclose = function(e) {
|
||||
console.log("connection closed (" + e.code + ")");
|
||||
}
|
||||
|
||||
sock.onmessage = function(e) {
|
||||
console.log("message received: " + e.data);
|
||||
}
|
||||
};
|
||||
|
||||
function send() {
|
||||
var msg = document.getElementById('message').value;
|
||||
sock.send(msg);
|
||||
};
|
||||
</script>
|
||||
<h1>WebSocket Echo Test</h1>
|
||||
<form>
|
||||
<p>
|
||||
Message: <input id="message" type="text" value="Hello, world!">
|
||||
</p>
|
||||
</form>
|
||||
<button onclick="send();">Send Message</button>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
可以看到客户端JS,很容易的就通过WebSocket函数建立了一个与服务器的连接sock,当握手成功后,会触发WebScoket对象的onopen事件,告诉客户端连接已经成功建立。客户端一共绑定了四个事件。
|
||||
|
||||
- 1)onopen 建立连接后触发
|
||||
- 2)onmessage 收到消息后触发
|
||||
- 3)onerror 发生错误时触发
|
||||
- 4)onclose 关闭连接时触发
|
||||
|
||||
我们服务器端的实现如下:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/net/websocket"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Echo(ws *websocket.Conn) {
|
||||
var err error
|
||||
|
||||
for {
|
||||
var reply string
|
||||
|
||||
if err = websocket.Message.Receive(ws, &reply); err != nil {
|
||||
fmt.Println("Can't receive")
|
||||
break
|
||||
}
|
||||
|
||||
fmt.Println("Received back from client: " + reply)
|
||||
|
||||
msg := "Received: " + reply
|
||||
fmt.Println("Sending to client: " + msg)
|
||||
|
||||
if err = websocket.Message.Send(ws, msg); err != nil {
|
||||
fmt.Println("Can't send")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.Handle("/", websocket.Handler(Echo))
|
||||
|
||||
if err := http.ListenAndServe(":1234", nil); err != nil {
|
||||
log.Fatal("ListenAndServe:", err)
|
||||
}
|
||||
}
|
||||
|
||||
当客户端将用户输入的信息Send之后,服务器端通过Receive接收到了相应信息,然后通过Send发送了应答信息。
|
||||
|
||||

|
||||
|
||||
图8.4 WebSocket服务器端接收到的信息
|
||||
|
||||
通过上面的例子我们看到客户端和服务器端实现WebSocket非常的方便,Go的源码net分支中已经实现了这个的协议,我们可以直接拿来用,目前随着HTML5的发展,我想未来WebSocket会是Web开发的一个重点,我们需要储备这方面的知识。
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [Socket编程](<08.1.md>)
|
||||
* 下一节: [REST](<08.3.md>)
|
||||
151
zh/08.2.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
151
zh/08.2.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,151 @@
|
||||
# 8.2 WebSocket
|
||||
WebSocket是HTML5的重要特性,它实现了基于浏览器的远程socket,它使浏览器和服务器可以进行全双工通信,许多浏览器(Firefox、Google Chrome和Safari)都已对此做了支持。
|
||||
|
||||
在WebSocket出现之前,为了实现即时通信,采用的技术都是“轮询”,即在特定的时间间隔内,由浏览器对服务器发出HTTP Request,服务器在收到请求后,返回最新的数据给浏览器刷新,“轮询”使得浏览器需要对服务器不断发出请求,这样会占用大量带宽。
|
||||
|
||||
WebSocket采用了一些特殊的报头,使得浏览器和服务器只需要做一个握手的动作,就可以在浏览器和服务器之间建立一条连接通道。且此连接会保持在活动状态,你可以使用JavaScript来向连接写入或从中接收数据,就像在使用一个常规的TCP Socket一样。它解决了Web实时化的问题,相比传统HTTP有如下好处:
|
||||
|
||||
- 一个Web客户端只建立一个TCP连接
|
||||
- Websocket服务端可以推送(push)数据到web客户端.
|
||||
- 有更加轻量级的头,减少数据传送量
|
||||
|
||||
WebSocket URL的起始输入是ws://或是wss://(在SSL上)。下图展示了WebSocket的通信过程,一个带有特定报头的HTTP握手被发送到了服务器端,接着在服务器端或是客户端就可以通过JavaScript来使用某种套接口(socket),这一套接口可被用来通过事件句柄异步地接收数据。
|
||||
|
||||

|
||||
|
||||
图8.2 WebSocket原理图
|
||||
|
||||
## WebSocket原理
|
||||
WebSocket的协议颇为简单,在第一次handshake通过以后,连接便建立成功,其后的通讯数据都是以”\x00″开头,以”\xFF”结尾。在客户端,这个是透明的,WebSocket组件会自动将原始数据“掐头去尾”。
|
||||
|
||||
浏览器发出WebSocket连接请求,然后服务器发出回应,然后连接建立成功,这个过程通常称为“握手” (handshaking)。请看下面的请求和反馈信息:
|
||||
|
||||

|
||||
|
||||
图8.3 WebSocket的request和response信息
|
||||
|
||||
在请求中的"Sec-WebSocket-Key"是随机的,对于整天跟编码打交到的程序员,一眼就可以看出来:这个是一个经过base64编码后的数据。服务器端接收到这个请求之后需要把这个字符串连接上一个固定的字符串:
|
||||
|
||||
258EAFA5-E914-47DA-95CA-C5AB0DC85B11
|
||||
|
||||
即:`f7cb4ezEAl6C3wRaU6JORA==`连接上那一串固定字符串,生成一个这样的字符串:
|
||||
|
||||
f7cb4ezEAl6C3wRaU6JORA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
|
||||
|
||||
对该字符串先用 sha1安全散列算法计算出二进制的值,然后用base64对其进行编码,即可以得到握手后的字符串:
|
||||
|
||||
rE91AJhfC+6JdVcVXOGJEADEJdQ=
|
||||
|
||||
将之作为响应头`Sec-WebSocket-Accept`的值反馈给客户端。
|
||||
|
||||
## Go实现WebSocket
|
||||
Go语言标准包里面没有提供对WebSocket的支持,但是在由官方维护的go.net子包中有对这个的支持,你可以通过如下的命令获取该包:
|
||||
|
||||
go get code.google.com/p/go.net/websocket
|
||||
|
||||
WebSocket分为客户端和服务端,接下来我们将实现一个简单的例子:用户输入信息,客户端通过WebSocket将信息发送给服务器端,服务器端收到信息之后主动Push信息到客户端,然后客户端将输出其收到的信息,客户端的代码如下:
|
||||
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
var sock = null;
|
||||
var wsuri = "ws://127.0.0.1:1234";
|
||||
|
||||
window.onload = function() {
|
||||
|
||||
console.log("onload");
|
||||
|
||||
sock = new WebSocket(wsuri);
|
||||
|
||||
sock.onopen = function() {
|
||||
console.log("connected to " + wsuri);
|
||||
}
|
||||
|
||||
sock.onclose = function(e) {
|
||||
console.log("connection closed (" + e.code + ")");
|
||||
}
|
||||
|
||||
sock.onmessage = function(e) {
|
||||
console.log("message received: " + e.data);
|
||||
}
|
||||
};
|
||||
|
||||
function send() {
|
||||
var msg = document.getElementById('message').value;
|
||||
sock.send(msg);
|
||||
};
|
||||
</script>
|
||||
<h1>WebSocket Echo Test</h1>
|
||||
<form>
|
||||
<p>
|
||||
Message: <input id="message" type="text" value="Hello, world!">
|
||||
</p>
|
||||
</form>
|
||||
<button onclick="send();">Send Message</button>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
可以看到客户端JS,很容易的就通过WebSocket函数建立了一个与服务器的连接sock,当握手成功后,会触发WebScoket对象的onopen事件,告诉客户端连接已经成功建立。客户端一共绑定了四个事件。
|
||||
|
||||
- 1)onopen 建立连接后触发
|
||||
- 2)onmessage 收到消息后触发
|
||||
- 3)onerror 发生错误时触发
|
||||
- 4)onclose 关闭连接时触发
|
||||
|
||||
我们服务器端的实现如下:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/net/websocket"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Echo(ws *websocket.Conn) {
|
||||
var err error
|
||||
|
||||
for {
|
||||
var reply string
|
||||
|
||||
if err = websocket.Message.Receive(ws, &reply); err != nil {
|
||||
fmt.Println("Can't receive")
|
||||
break
|
||||
}
|
||||
|
||||
fmt.Println("Received back from client: " + reply)
|
||||
|
||||
msg := "Received: " + reply
|
||||
fmt.Println("Sending to client: " + msg)
|
||||
|
||||
if err = websocket.Message.Send(ws, msg); err != nil {
|
||||
fmt.Println("Can't send")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.Handle("/", websocket.Handler(Echo))
|
||||
|
||||
if err := http.ListenAndServe(":1234", nil); err != nil {
|
||||
log.Fatal("ListenAndServe:", err)
|
||||
}
|
||||
}
|
||||
|
||||
当客户端将用户输入的信息Send之后,服务器端通过Receive接收到了相应信息,然后通过Send发送了应答信息。
|
||||
|
||||

|
||||
|
||||
图8.4 WebSocket服务器端接收到的信息
|
||||
|
||||
通过上面的例子我们看到客户端和服务器端实现WebSocket非常的方便,Go的源码net分支中已经实现了这个的协议,我们可以直接拿来用,目前随着HTML5的发展,我想未来WebSocket会是Web开发的一个重点,我们需要储备这方面的知识。
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [Socket编程](<08.1.md>)
|
||||
* 下一节: [REST](<08.3.md>)
|
||||
@@ -1,114 +1,114 @@
|
||||
# 8.3 REST
|
||||
RESTful,是目前最为流行的一种互联网软件架构。因为它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用。本小节我们将来学习它到底是一种什么样的架构?以及在Go里面如何来实现它。
|
||||
## 什么是REST
|
||||
REST(REpresentational State Transfer)这个概念,首次出现是在 2000年Roy Thomas Fielding(他是HTTP规范的主要编写者之一)的博士论文中,它指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是RESTful的。
|
||||
|
||||
要理解什么是REST,我们需要理解下面几个概念:
|
||||
|
||||
- 资源(Resources)
|
||||
REST是"表现层状态转化",其实它省略了主语。"表现层"其实指的是"资源"的"表现层"。
|
||||
|
||||
那么什么是资源呢?就是我们平常上网访问的一张图片、一个文档、一个视频等。这些资源我们通过URI来定位,也就是一个URI表示一个资源。
|
||||
|
||||
- 表现层(Representation)
|
||||
|
||||
资源是做一个具体的实体信息,他可以有多种的展现方式。而把实体展现出来就是表现层,例如一个txt文本信息,他可以输出成html、json、xml等格式,一个图片他可以jpg、png等方式展现,这个就是表现层的意思。
|
||||
|
||||
URI确定一个资源,但是如何确定它的具体表现形式呢?应该在HTTP请求的头信息中用Accept和Content-Type字段指定,这两个字段才是对"表现层"的描述。
|
||||
|
||||
- 状态转化(State Transfer)
|
||||
|
||||
访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,肯定涉及到数据和状态的变化。而HTTP协议是无状态的,那么这些状态肯定保存在服务器端,所以如果客户端想要通知服务器端改变数据和状态的变化,肯定要通过某种方式来通知它。
|
||||
|
||||
客户端能通知服务器端的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。
|
||||
|
||||
综合上面的解释,我们总结一下什么是RESTful架构:
|
||||
|
||||
- (1)每一个URI代表一种资源;
|
||||
- (2)客户端和服务器之间,传递这种资源的某种表现层;
|
||||
- (3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。
|
||||
|
||||
|
||||
Web应用要满足REST最重要的原则是:客户端和服务器之间的交互在请求之间是无状态的,即从客户端到服务器的每个请求都必须包含理解请求所必需的信息。如果服务器在请求之间的任何时间点重启,客户端不会得到通知。此外此请求可以由任何可用服务器回答,这十分适合云计算之类的环境。因为是无状态的,所以客户端可以缓存数据以改进性能。
|
||||
|
||||
另一个重要的REST原则是系统分层,这表示组件无法了解除了与它直接交互的层次以外的组件。通过将系统知识限制在单个层,可以限制整个系统的复杂性,从而促进了底层的独立性。
|
||||
|
||||
下图即是REST的架构图:
|
||||
|
||||

|
||||
|
||||
图8.5 REST架构图
|
||||
|
||||
当REST架构的约束条件作为一个整体应用时,将生成一个可以扩展到大量客户端的应用程序。它还降低了客户端和服务器之间的交互延迟。统一界面简化了整个系统架构,改进了子系统之间交互的可见性。REST简化了客户端和服务器的实现,而且对于使用REST开发的应用程序更加容易扩展。
|
||||
|
||||
下图展示了REST的扩展性:
|
||||
|
||||

|
||||
|
||||
图8.6 REST的扩展性
|
||||
|
||||
## RESTful的实现
|
||||
Go没有为REST提供直接支持,但是因为RESTful是基于HTTP协议实现的,所以我们可以利用`net/http`包来自己实现,当然需要针对REST做一些改造,REST是根据不同的method来处理相应的资源,目前已经存在的很多自称是REST的应用,其实并没有真正的实现REST,我暂且把这些应用根据实现的method分成几个级别,请看下图:
|
||||
|
||||

|
||||
|
||||
图8.7 REST的level分级
|
||||
|
||||
上图展示了我们目前实现REST的三个level,我们在应用开发的时候也不一定全部按照RESTful的规则全部实现他的方式,因为有些时候完全按照RESTful的方式未必是可行的,RESTful服务充分利用每一个HTTP方法,包括`DELETE`和`PUT`。可有时,HTTP客户端只能发出`GET`和`POST`请求:
|
||||
|
||||
- HTML标准只能通过链接和表单支持`GET`和`POST`。在没有Ajax支持的网页浏览器中不能发出`PUT`或`DELETE`命令
|
||||
|
||||
- 有些防火墙会挡住HTTP `PUT`和`DELETE`请求要绕过这个限制,客户端需要把实际的`PUT`和`DELETE`请求通过 POST 请求穿透过来。RESTful 服务则要负责在收到的 POST 请求中找到原始的 HTTP 方法并还原。
|
||||
|
||||
我们现在可以通过`POST`里面增加隐藏字段`_method`这种方式可以来模拟`PUT`、`DELETE`等方式,但是服务器端需要做转换。我现在的项目里面就按照这种方式来做的REST接口。当然Go语言里面完全按照RESTful来实现是很容易的,我们通过下面的例子来说明如何实现RESTful的应用设计。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/drone/routes"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func getuser(w http.ResponseWriter, r *http.Request) {
|
||||
params := r.URL.Query()
|
||||
uid := params.Get(":uid")
|
||||
fmt.Fprintf(w, "you are get user %s", uid)
|
||||
}
|
||||
|
||||
func modifyuser(w http.ResponseWriter, r *http.Request) {
|
||||
params := r.URL.Query()
|
||||
uid := params.Get(":uid")
|
||||
fmt.Fprintf(w, "you are modify user %s", uid)
|
||||
}
|
||||
|
||||
func deleteuser(w http.ResponseWriter, r *http.Request) {
|
||||
params := r.URL.Query()
|
||||
uid := params.Get(":uid")
|
||||
fmt.Fprintf(w, "you are delete user %s", uid)
|
||||
}
|
||||
|
||||
func adduser(w http.ResponseWriter, r *http.Request) {
|
||||
uid := r.FormValue("uid")
|
||||
fmt.Fprint(w, "you are add user %s", uid)
|
||||
}
|
||||
|
||||
func main() {
|
||||
mux := routes.New()
|
||||
mux.Get("/user/:uid", getuser)
|
||||
mux.Post("/user/", adduser)
|
||||
mux.Del("/user/:uid", deleteuser)
|
||||
mux.Put("/user/:uid", modifyuser)
|
||||
http.Handle("/", mux)
|
||||
http.ListenAndServe(":8088", nil)
|
||||
}
|
||||
|
||||
上面的代码演示了如何编写一个REST的应用,我们访问的资源是用户,我们通过不同的method来访问不同的函数,这里使用了第三方库`github.com/drone/routes`,在前面章节我们介绍过如何实现自定义的路由器,这个库实现了自定义路由和方便的路由规则映射,通过它,我们可以很方便的实现REST的架构。通过上面的代码可知,REST就是根据不同的method访问同一个资源的时候实现不同的逻辑处理。
|
||||
|
||||
## 总结
|
||||
REST是一种架构风格,汲取了WWW的成功经验:无状态,以资源为中心,充分利用HTTP协议和URI协议,提供统一的接口定义,使得它作为一种设计Web服务的方法而变得流行。在某种意义上,通过强调URI和HTTP等早期Internet标准,REST是对大型应用程序服务器时代之前的Web方式的回归。目前Go对于REST的支持还是很简单的,通过实现自定义的路由规则,我们就可以为不同的method实现不同的handle,这样就实现了REST的架构。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [WebSocket](<08.2.md>)
|
||||
* 下一节: [RPC](<08.4.md>)
|
||||
# 8.3 REST
|
||||
RESTful,是目前最为流行的一种互联网软件架构。因为它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用。本小节我们将来学习它到底是一种什么样的架构?以及在Go里面如何来实现它。
|
||||
## 什么是REST
|
||||
REST(REpresentational State Transfer)这个概念,首次出现是在 2000年Roy Thomas Fielding(他是HTTP规范的主要编写者之一)的博士论文中,它指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是RESTful的。
|
||||
|
||||
要理解什么是REST,我们需要理解下面几个概念:
|
||||
|
||||
- 资源(Resources)
|
||||
REST是"表现层状态转化",其实它省略了主语。"表现层"其实指的是"资源"的"表现层"。
|
||||
|
||||
那么什么是资源呢?就是我们平常上网访问的一张图片、一个文档、一个视频等。这些资源我们通过URI来定位,也就是一个URI表示一个资源。
|
||||
|
||||
- 表现层(Representation)
|
||||
|
||||
资源是做一个具体的实体信息,他可以有多种的展现方式。而把实体展现出来就是表现层,例如一个txt文本信息,他可以输出成html、json、xml等格式,一个图片他可以jpg、png等方式展现,这个就是表现层的意思。
|
||||
|
||||
URI确定一个资源,但是如何确定它的具体表现形式呢?应该在HTTP请求的头信息中用Accept和Content-Type字段指定,这两个字段才是对"表现层"的描述。
|
||||
|
||||
- 状态转化(State Transfer)
|
||||
|
||||
访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,肯定涉及到数据和状态的变化。而HTTP协议是无状态的,那么这些状态肯定保存在服务器端,所以如果客户端想要通知服务器端改变数据和状态的变化,肯定要通过某种方式来通知它。
|
||||
|
||||
客户端能通知服务器端的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。
|
||||
|
||||
综合上面的解释,我们总结一下什么是RESTful架构:
|
||||
|
||||
- (1)每一个URI代表一种资源;
|
||||
- (2)客户端和服务器之间,传递这种资源的某种表现层;
|
||||
- (3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。
|
||||
|
||||
|
||||
Web应用要满足REST最重要的原则是:客户端和服务器之间的交互在请求之间是无状态的,即从客户端到服务器的每个请求都必须包含理解请求所必需的信息。如果服务器在请求之间的任何时间点重启,客户端不会得到通知。此外此请求可以由任何可用服务器回答,这十分适合云计算之类的环境。因为是无状态的,所以客户端可以缓存数据以改进性能。
|
||||
|
||||
另一个重要的REST原则是系统分层,这表示组件无法了解除了与它直接交互的层次以外的组件。通过将系统知识限制在单个层,可以限制整个系统的复杂性,从而促进了底层的独立性。
|
||||
|
||||
下图即是REST的架构图:
|
||||
|
||||

|
||||
|
||||
图8.5 REST架构图
|
||||
|
||||
当REST架构的约束条件作为一个整体应用时,将生成一个可以扩展到大量客户端的应用程序。它还降低了客户端和服务器之间的交互延迟。统一界面简化了整个系统架构,改进了子系统之间交互的可见性。REST简化了客户端和服务器的实现,而且对于使用REST开发的应用程序更加容易扩展。
|
||||
|
||||
下图展示了REST的扩展性:
|
||||
|
||||

|
||||
|
||||
图8.6 REST的扩展性
|
||||
|
||||
## RESTful的实现
|
||||
Go没有为REST提供直接支持,但是因为RESTful是基于HTTP协议实现的,所以我们可以利用`net/http`包来自己实现,当然需要针对REST做一些改造,REST是根据不同的method来处理相应的资源,目前已经存在的很多自称是REST的应用,其实并没有真正的实现REST,我暂且把这些应用根据实现的method分成几个级别,请看下图:
|
||||
|
||||

|
||||
|
||||
图8.7 REST的level分级
|
||||
|
||||
上图展示了我们目前实现REST的三个level,我们在应用开发的时候也不一定全部按照RESTful的规则全部实现他的方式,因为有些时候完全按照RESTful的方式未必是可行的,RESTful服务充分利用每一个HTTP方法,包括`DELETE`和`PUT`。可有时,HTTP客户端只能发出`GET`和`POST`请求:
|
||||
|
||||
- HTML标准只能通过链接和表单支持`GET`和`POST`。在没有Ajax支持的网页浏览器中不能发出`PUT`或`DELETE`命令
|
||||
|
||||
- 有些防火墙会挡住HTTP `PUT`和`DELETE`请求要绕过这个限制,客户端需要把实际的`PUT`和`DELETE`请求通过 POST 请求穿透过来。RESTful 服务则要负责在收到的 POST 请求中找到原始的 HTTP 方法并还原。
|
||||
|
||||
我们现在可以通过`POST`里面增加隐藏字段`_method`这种方式可以来模拟`PUT`、`DELETE`等方式,但是服务器端需要做转换。我现在的项目里面就按照这种方式来做的REST接口。当然Go语言里面完全按照RESTful来实现是很容易的,我们通过下面的例子来说明如何实现RESTful的应用设计。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/drone/routes"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func getuser(w http.ResponseWriter, r *http.Request) {
|
||||
params := r.URL.Query()
|
||||
uid := params.Get(":uid")
|
||||
fmt.Fprintf(w, "you are get user %s", uid)
|
||||
}
|
||||
|
||||
func modifyuser(w http.ResponseWriter, r *http.Request) {
|
||||
params := r.URL.Query()
|
||||
uid := params.Get(":uid")
|
||||
fmt.Fprintf(w, "you are modify user %s", uid)
|
||||
}
|
||||
|
||||
func deleteuser(w http.ResponseWriter, r *http.Request) {
|
||||
params := r.URL.Query()
|
||||
uid := params.Get(":uid")
|
||||
fmt.Fprintf(w, "you are delete user %s", uid)
|
||||
}
|
||||
|
||||
func adduser(w http.ResponseWriter, r *http.Request) {
|
||||
uid := r.FormValue("uid")
|
||||
fmt.Fprint(w, "you are add user %s", uid)
|
||||
}
|
||||
|
||||
func main() {
|
||||
mux := routes.New()
|
||||
mux.Get("/user/:uid", getuser)
|
||||
mux.Post("/user/", adduser)
|
||||
mux.Del("/user/:uid", deleteuser)
|
||||
mux.Put("/user/:uid", modifyuser)
|
||||
http.Handle("/", mux)
|
||||
http.ListenAndServe(":8088", nil)
|
||||
}
|
||||
|
||||
上面的代码演示了如何编写一个REST的应用,我们访问的资源是用户,我们通过不同的method来访问不同的函数,这里使用了第三方库`github.com/drone/routes`,在前面章节我们介绍过如何实现自定义的路由器,这个库实现了自定义路由和方便的路由规则映射,通过它,我们可以很方便的实现REST的架构。通过上面的代码可知,REST就是根据不同的method访问同一个资源的时候实现不同的逻辑处理。
|
||||
|
||||
## 总结
|
||||
REST是一种架构风格,汲取了WWW的成功经验:无状态,以资源为中心,充分利用HTTP协议和URI协议,提供统一的接口定义,使得它作为一种设计Web服务的方法而变得流行。在某种意义上,通过强调URI和HTTP等早期Internet标准,REST是对大型应用程序服务器时代之前的Web方式的回归。目前Go对于REST的支持还是很简单的,通过实现自定义的路由规则,我们就可以为不同的method实现不同的handle,这样就实现了REST的架构。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [WebSocket](<08.2.md>)
|
||||
* 下一节: [RPC](<08.4.md>)
|
||||
114
zh/08.3.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
114
zh/08.3.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,114 @@
|
||||
# 8.3 REST
|
||||
RESTful,是目前最为流行的一种互联网软件架构。因为它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用。本小节我们将来学习它到底是一种什么样的架构?以及在Go里面如何来实现它。
|
||||
## 什么是REST
|
||||
REST(REpresentational State Transfer)这个概念,首次出现是在 2000年Roy Thomas Fielding(他是HTTP规范的主要编写者之一)的博士论文中,它指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是RESTful的。
|
||||
|
||||
要理解什么是REST,我们需要理解下面几个概念:
|
||||
|
||||
- 资源(Resources)
|
||||
REST是"表现层状态转化",其实它省略了主语。"表现层"其实指的是"资源"的"表现层"。
|
||||
|
||||
那么什么是资源呢?就是我们平常上网访问的一张图片、一个文档、一个视频等。这些资源我们通过URI来定位,也就是一个URI表示一个资源。
|
||||
|
||||
- 表现层(Representation)
|
||||
|
||||
资源是做一个具体的实体信息,他可以有多种的展现方式。而把实体展现出来就是表现层,例如一个txt文本信息,他可以输出成html、json、xml等格式,一个图片他可以jpg、png等方式展现,这个就是表现层的意思。
|
||||
|
||||
URI确定一个资源,但是如何确定它的具体表现形式呢?应该在HTTP请求的头信息中用Accept和Content-Type字段指定,这两个字段才是对"表现层"的描述。
|
||||
|
||||
- 状态转化(State Transfer)
|
||||
|
||||
访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,肯定涉及到数据和状态的变化。而HTTP协议是无状态的,那么这些状态肯定保存在服务器端,所以如果客户端想要通知服务器端改变数据和状态的变化,肯定要通过某种方式来通知它。
|
||||
|
||||
客户端能通知服务器端的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。
|
||||
|
||||
综合上面的解释,我们总结一下什么是RESTful架构:
|
||||
|
||||
- (1)每一个URI代表一种资源;
|
||||
- (2)客户端和服务器之间,传递这种资源的某种表现层;
|
||||
- (3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。
|
||||
|
||||
|
||||
Web应用要满足REST最重要的原则是:客户端和服务器之间的交互在请求之间是无状态的,即从客户端到服务器的每个请求都必须包含理解请求所必需的信息。如果服务器在请求之间的任何时间点重启,客户端不会得到通知。此外此请求可以由任何可用服务器回答,这十分适合云计算之类的环境。因为是无状态的,所以客户端可以缓存数据以改进性能。
|
||||
|
||||
另一个重要的REST原则是系统分层,这表示组件无法了解除了与它直接交互的层次以外的组件。通过将系统知识限制在单个层,可以限制整个系统的复杂性,从而促进了底层的独立性。
|
||||
|
||||
下图即是REST的架构图:
|
||||
|
||||

|
||||
|
||||
图8.5 REST架构图
|
||||
|
||||
当REST架构的约束条件作为一个整体应用时,将生成一个可以扩展到大量客户端的应用程序。它还降低了客户端和服务器之间的交互延迟。统一界面简化了整个系统架构,改进了子系统之间交互的可见性。REST简化了客户端和服务器的实现,而且对于使用REST开发的应用程序更加容易扩展。
|
||||
|
||||
下图展示了REST的扩展性:
|
||||
|
||||

|
||||
|
||||
图8.6 REST的扩展性
|
||||
|
||||
## RESTful的实现
|
||||
Go没有为REST提供直接支持,但是因为RESTful是基于HTTP协议实现的,所以我们可以利用`net/http`包来自己实现,当然需要针对REST做一些改造,REST是根据不同的method来处理相应的资源,目前已经存在的很多自称是REST的应用,其实并没有真正的实现REST,我暂且把这些应用根据实现的method分成几个级别,请看下图:
|
||||
|
||||

|
||||
|
||||
图8.7 REST的level分级
|
||||
|
||||
上图展示了我们目前实现REST的三个level,我们在应用开发的时候也不一定全部按照RESTful的规则全部实现他的方式,因为有些时候完全按照RESTful的方式未必是可行的,RESTful服务充分利用每一个HTTP方法,包括`DELETE`和`PUT`。可有时,HTTP客户端只能发出`GET`和`POST`请求:
|
||||
|
||||
- HTML标准只能通过链接和表单支持`GET`和`POST`。在没有Ajax支持的网页浏览器中不能发出`PUT`或`DELETE`命令
|
||||
|
||||
- 有些防火墙会挡住HTTP `PUT`和`DELETE`请求要绕过这个限制,客户端需要把实际的`PUT`和`DELETE`请求通过 POST 请求穿透过来。RESTful 服务则要负责在收到的 POST 请求中找到原始的 HTTP 方法并还原。
|
||||
|
||||
我们现在可以通过`POST`里面增加隐藏字段`_method`这种方式可以来模拟`PUT`、`DELETE`等方式,但是服务器端需要做转换。我现在的项目里面就按照这种方式来做的REST接口。当然Go语言里面完全按照RESTful来实现是很容易的,我们通过下面的例子来说明如何实现RESTful的应用设计。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/drone/routes"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func getuser(w http.ResponseWriter, r *http.Request) {
|
||||
params := r.URL.Query()
|
||||
uid := params.Get(":uid")
|
||||
fmt.Fprintf(w, "you are get user %s", uid)
|
||||
}
|
||||
|
||||
func modifyuser(w http.ResponseWriter, r *http.Request) {
|
||||
params := r.URL.Query()
|
||||
uid := params.Get(":uid")
|
||||
fmt.Fprintf(w, "you are modify user %s", uid)
|
||||
}
|
||||
|
||||
func deleteuser(w http.ResponseWriter, r *http.Request) {
|
||||
params := r.URL.Query()
|
||||
uid := params.Get(":uid")
|
||||
fmt.Fprintf(w, "you are delete user %s", uid)
|
||||
}
|
||||
|
||||
func adduser(w http.ResponseWriter, r *http.Request) {
|
||||
uid := r.FormValue("uid")
|
||||
fmt.Fprint(w, "you are add user %s", uid)
|
||||
}
|
||||
|
||||
func main() {
|
||||
mux := routes.New()
|
||||
mux.Get("/user/:uid", getuser)
|
||||
mux.Post("/user/", adduser)
|
||||
mux.Del("/user/:uid", deleteuser)
|
||||
mux.Put("/user/:uid", modifyuser)
|
||||
http.Handle("/", mux)
|
||||
http.ListenAndServe(":8088", nil)
|
||||
}
|
||||
|
||||
上面的代码演示了如何编写一个REST的应用,我们访问的资源是用户,我们通过不同的method来访问不同的函数,这里使用了第三方库`github.com/drone/routes`,在前面章节我们介绍过如何实现自定义的路由器,这个库实现了自定义路由和方便的路由规则映射,通过它,我们可以很方便的实现REST的架构。通过上面的代码可知,REST就是根据不同的method访问同一个资源的时候实现不同的逻辑处理。
|
||||
|
||||
## 总结
|
||||
REST是一种架构风格,汲取了WWW的成功经验:无状态,以资源为中心,充分利用HTTP协议和URI协议,提供统一的接口定义,使得它作为一种设计Web服务的方法而变得流行。在某种意义上,通过强调URI和HTTP等早期Internet标准,REST是对大型应用程序服务器时代之前的Web方式的回归。目前Go对于REST的支持还是很简单的,通过实现自定义的路由规则,我们就可以为不同的method实现不同的handle,这样就实现了REST的架构。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [WebSocket](<08.2.md>)
|
||||
* 下一节: [RPC](<08.4.md>)
|
||||
@@ -1,392 +1,392 @@
|
||||
# 8.4 RPC
|
||||
前面几个小节我们介绍了如何基于Socket和HTTP来编写网络应用,通过学习我们了解了Socket和HTTP采用的是类似"信息交换"模式,即客户端发送一条信息到服务端,然后(一般来说)服务器端都会返回一定的信息以表示响应。客户端和服务端之间约定了交互信息的格式,以便双方都能够解析交互所产生的信息。但是很多独立的应用并没有采用这种模式,而是采用类似常规的函数调用的方式来完成想要的功能。
|
||||
|
||||
RPC就是想实现函数调用模式的网络化。客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传递到服务端,服务端解包到处理过程中执行,然后执行的结果反馈给客户端。
|
||||
|
||||
RPC(Remote Procedure Call Protocol)——远程过程调用协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。它假定某些传输协议的存在,如TCP或UDP,以便为通信程序之间携带信息数据。通过它可以使函数调用模式网络化。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
|
||||
|
||||
## RPC工作原理
|
||||
|
||||

|
||||
|
||||
图8.8 RPC工作流程图
|
||||
|
||||
运行时,一次客户机对服务器的RPC调用,其内部操作大致有如下十步:
|
||||
|
||||
- 1.调用客户端句柄;执行传送参数
|
||||
- 2.调用本地系统内核发送网络消息
|
||||
- 3.消息传送到远程主机
|
||||
- 4.服务器句柄得到消息并取得参数
|
||||
- 5.执行远程过程
|
||||
- 6.执行的过程将结果返回服务器句柄
|
||||
- 7.服务器句柄返回结果,调用远程系统内核
|
||||
- 8.消息传回本地主机
|
||||
- 9.客户句柄由内核接收消息
|
||||
- 10.客户接收句柄返回的数据
|
||||
|
||||
## Go RPC
|
||||
Go标准包中已经提供了对RPC的支持,而且支持三个级别的RPC:TCP、HTTP、JSONRPC。但Go的RPC包是独一无二的RPC,它和传统的RPC系统不同,它只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码。
|
||||
|
||||
Go RPC的函数只有符合下面的条件才能被远程访问,不然会被忽略,详细的要求如下:
|
||||
|
||||
- 函数必须是导出的(首字母大写)
|
||||
- 必须有两个导出类型的参数,
|
||||
- 第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的
|
||||
- 函数还要有一个返回值error
|
||||
|
||||
举个例子,正确的RPC函数格式如下:
|
||||
|
||||
func (t *T) MethodName(argType T1, replyType *T2) error
|
||||
|
||||
T、T1和T2类型必须能被`encoding/gob`包编解码。
|
||||
|
||||
任何的RPC都需要通过网络来传递数据,Go RPC可以利用HTTP和TCP来传递数据,利用HTTP的好处是可以直接复用`net/http`里面的一些函数。详细的例子请看下面的实现
|
||||
|
||||
### HTTP RPC
|
||||
http的服务端代码实现如下:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
type Arith int
|
||||
|
||||
func (t *Arith) Multiply(args *Args, reply *int) error {
|
||||
*reply = args.A * args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Arith) Divide(args *Args, quo *Quotient) error {
|
||||
if args.B == 0 {
|
||||
return errors.New("divide by zero")
|
||||
}
|
||||
quo.Quo = args.A / args.B
|
||||
quo.Rem = args.A % args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
arith := new(Arith)
|
||||
rpc.Register(arith)
|
||||
rpc.HandleHTTP()
|
||||
|
||||
err := http.ListenAndServe(":1234", nil)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
通过上面的例子可以看到,我们注册了一个Arith的RPC服务,然后通过`rpc.HandleHTTP`函数把该服务注册到了HTTP协议上,然后我们就可以利用http的方式来传递数据了。
|
||||
|
||||
请看下面的客户端代码:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/rpc"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Println("Usage: ", os.Args[0], "server")
|
||||
os.Exit(1)
|
||||
}
|
||||
serverAddress := os.Args[1]
|
||||
|
||||
client, err := rpc.DialHTTP("tcp", serverAddress+":1234")
|
||||
if err != nil {
|
||||
log.Fatal("dialing:", err)
|
||||
}
|
||||
// Synchronous call
|
||||
args := Args{17, 8}
|
||||
var reply int
|
||||
err = client.Call("Arith.Multiply", args, &reply)
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
|
||||
|
||||
var quot Quotient
|
||||
err = client.Call("Arith.Divide", args, ")
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
|
||||
|
||||
}
|
||||
|
||||
我们把上面的服务端和客户端的代码分别编译,然后先把服务端开启,然后开启客户端,输入代码,就会输出如下信息:
|
||||
|
||||
$ ./http_c localhost
|
||||
Arith: 17*8=136
|
||||
Arith: 17/8=2 remainder 1
|
||||
|
||||
通过上面的调用可以看到参数和返回值是我们定义的struct类型,在服务端我们把它们当做调用函数的参数的类型,在客户端作为`client.Call`的第2,3两个参数的类型。客户端最重要的就是这个Call函数,它有3个参数,第1个要调用的函数的名字,第2个是要传递的参数,第3个要返回的参数(注意是指针类型),通过上面的代码例子我们可以发现,使用Go的RPC实现相当的简单,方便。
|
||||
### TCP RPC
|
||||
上面我们实现了基于HTTP协议的RPC,接下来我们要实现基于TCP协议的RPC,服务端的实现代码如下所示:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/rpc"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
type Arith int
|
||||
|
||||
func (t *Arith) Multiply(args *Args, reply *int) error {
|
||||
*reply = args.A * args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Arith) Divide(args *Args, quo *Quotient) error {
|
||||
if args.B == 0 {
|
||||
return errors.New("divide by zero")
|
||||
}
|
||||
quo.Quo = args.A / args.B
|
||||
quo.Rem = args.A % args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
arith := new(Arith)
|
||||
rpc.Register(arith)
|
||||
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
|
||||
checkError(err)
|
||||
|
||||
listener, err := net.ListenTCP("tcp", tcpAddr)
|
||||
checkError(err)
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
rpc.ServeConn(conn)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func checkError(err error) {
|
||||
if err != nil {
|
||||
fmt.Println("Fatal error ", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
上面这个代码和http的服务器相比,不同在于:在此处我们采用了TCP协议,然后需要自己控制连接,当有客户端连接上来后,我们需要把这个连接交给rpc来处理。
|
||||
|
||||
如果你留心了,你会发现这它是一个阻塞型的单用户的程序,如果想要实现多并发,那么可以使用goroutine来实现,我们前面在socket小节的时候已经介绍过如何处理goroutine。
|
||||
下面展现了TCP实现的RPC客户端:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/rpc"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Println("Usage: ", os.Args[0], "server:port")
|
||||
os.Exit(1)
|
||||
}
|
||||
service := os.Args[1]
|
||||
|
||||
client, err := rpc.Dial("tcp", service)
|
||||
if err != nil {
|
||||
log.Fatal("dialing:", err)
|
||||
}
|
||||
// Synchronous call
|
||||
args := Args{17, 8}
|
||||
var reply int
|
||||
err = client.Call("Arith.Multiply", args, &reply)
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
|
||||
|
||||
var quot Quotient
|
||||
err = client.Call("Arith.Divide", args, ")
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
|
||||
|
||||
}
|
||||
|
||||
这个客户端代码和http的客户端代码对比,唯一的区别一个是DialHTTP,一个是Dial(tcp),其他处理一模一样。
|
||||
|
||||
### JSON RPC
|
||||
JSON RPC是数据编码采用了JSON,而不是gob编码,其他和上面介绍的RPC概念一模一样,下面我们来演示一下,如何使用Go提供的json-rpc标准包,请看服务端代码的实现:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/rpc"
|
||||
"net/rpc/jsonrpc"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
type Arith int
|
||||
|
||||
func (t *Arith) Multiply(args *Args, reply *int) error {
|
||||
*reply = args.A * args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Arith) Divide(args *Args, quo *Quotient) error {
|
||||
if args.B == 0 {
|
||||
return errors.New("divide by zero")
|
||||
}
|
||||
quo.Quo = args.A / args.B
|
||||
quo.Rem = args.A % args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
arith := new(Arith)
|
||||
rpc.Register(arith)
|
||||
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
|
||||
checkError(err)
|
||||
|
||||
listener, err := net.ListenTCP("tcp", tcpAddr)
|
||||
checkError(err)
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
jsonrpc.ServeConn(conn)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func checkError(err error) {
|
||||
if err != nil {
|
||||
fmt.Println("Fatal error ", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
通过示例我们可以看出 json-rpc是基于TCP协议实现的,目前它还不支持HTTP方式。
|
||||
|
||||
请看客户端的实现代码:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/rpc/jsonrpc"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Println("Usage: ", os.Args[0], "server:port")
|
||||
log.Fatal(1)
|
||||
}
|
||||
service := os.Args[1]
|
||||
|
||||
client, err := jsonrpc.Dial("tcp", service)
|
||||
if err != nil {
|
||||
log.Fatal("dialing:", err)
|
||||
}
|
||||
// Synchronous call
|
||||
args := Args{17, 8}
|
||||
var reply int
|
||||
err = client.Call("Arith.Multiply", args, &reply)
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
|
||||
|
||||
var quot Quotient
|
||||
err = client.Call("Arith.Divide", args, ")
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
|
||||
|
||||
}
|
||||
|
||||
## 总结
|
||||
Go已经提供了对RPC的良好支持,通过上面HTTP、TCP、JSON RPC的实现,我们就可以很方便的开发很多分布式的Web应用,我想作为读者的你已经领会到这一点。但遗憾的是目前Go尚未提供对SOAP RPC的支持,欣慰的是现在已经有第三方的开源实现了。
|
||||
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [REST](<08.3.md>)
|
||||
* 下一节: [小结](<08.5.md>)
|
||||
# 8.4 RPC
|
||||
前面几个小节我们介绍了如何基于Socket和HTTP来编写网络应用,通过学习我们了解了Socket和HTTP采用的是类似"信息交换"模式,即客户端发送一条信息到服务端,然后(一般来说)服务器端都会返回一定的信息以表示响应。客户端和服务端之间约定了交互信息的格式,以便双方都能够解析交互所产生的信息。但是很多独立的应用并没有采用这种模式,而是采用类似常规的函数调用的方式来完成想要的功能。
|
||||
|
||||
RPC就是想实现函数调用模式的网络化。客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传递到服务端,服务端解包到处理过程中执行,然后执行的结果反馈给客户端。
|
||||
|
||||
RPC(Remote Procedure Call Protocol)——远程过程调用协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。它假定某些传输协议的存在,如TCP或UDP,以便为通信程序之间携带信息数据。通过它可以使函数调用模式网络化。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
|
||||
|
||||
## RPC工作原理
|
||||
|
||||

|
||||
|
||||
图8.8 RPC工作流程图
|
||||
|
||||
运行时,一次客户机对服务器的RPC调用,其内部操作大致有如下十步:
|
||||
|
||||
- 1.调用客户端句柄;执行传送参数
|
||||
- 2.调用本地系统内核发送网络消息
|
||||
- 3.消息传送到远程主机
|
||||
- 4.服务器句柄得到消息并取得参数
|
||||
- 5.执行远程过程
|
||||
- 6.执行的过程将结果返回服务器句柄
|
||||
- 7.服务器句柄返回结果,调用远程系统内核
|
||||
- 8.消息传回本地主机
|
||||
- 9.客户句柄由内核接收消息
|
||||
- 10.客户接收句柄返回的数据
|
||||
|
||||
## Go RPC
|
||||
Go标准包中已经提供了对RPC的支持,而且支持三个级别的RPC:TCP、HTTP、JSONRPC。但Go的RPC包是独一无二的RPC,它和传统的RPC系统不同,它只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码。
|
||||
|
||||
Go RPC的函数只有符合下面的条件才能被远程访问,不然会被忽略,详细的要求如下:
|
||||
|
||||
- 函数必须是导出的(首字母大写)
|
||||
- 必须有两个导出类型的参数,
|
||||
- 第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的
|
||||
- 函数还要有一个返回值error
|
||||
|
||||
举个例子,正确的RPC函数格式如下:
|
||||
|
||||
func (t *T) MethodName(argType T1, replyType *T2) error
|
||||
|
||||
T、T1和T2类型必须能被`encoding/gob`包编解码。
|
||||
|
||||
任何的RPC都需要通过网络来传递数据,Go RPC可以利用HTTP和TCP来传递数据,利用HTTP的好处是可以直接复用`net/http`里面的一些函数。详细的例子请看下面的实现
|
||||
|
||||
### HTTP RPC
|
||||
http的服务端代码实现如下:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
type Arith int
|
||||
|
||||
func (t *Arith) Multiply(args *Args, reply *int) error {
|
||||
*reply = args.A * args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Arith) Divide(args *Args, quo *Quotient) error {
|
||||
if args.B == 0 {
|
||||
return errors.New("divide by zero")
|
||||
}
|
||||
quo.Quo = args.A / args.B
|
||||
quo.Rem = args.A % args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
arith := new(Arith)
|
||||
rpc.Register(arith)
|
||||
rpc.HandleHTTP()
|
||||
|
||||
err := http.ListenAndServe(":1234", nil)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
通过上面的例子可以看到,我们注册了一个Arith的RPC服务,然后通过`rpc.HandleHTTP`函数把该服务注册到了HTTP协议上,然后我们就可以利用http的方式来传递数据了。
|
||||
|
||||
请看下面的客户端代码:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/rpc"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Println("Usage: ", os.Args[0], "server")
|
||||
os.Exit(1)
|
||||
}
|
||||
serverAddress := os.Args[1]
|
||||
|
||||
client, err := rpc.DialHTTP("tcp", serverAddress+":1234")
|
||||
if err != nil {
|
||||
log.Fatal("dialing:", err)
|
||||
}
|
||||
// Synchronous call
|
||||
args := Args{17, 8}
|
||||
var reply int
|
||||
err = client.Call("Arith.Multiply", args, &reply)
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
|
||||
|
||||
var quot Quotient
|
||||
err = client.Call("Arith.Divide", args, ")
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
|
||||
|
||||
}
|
||||
|
||||
我们把上面的服务端和客户端的代码分别编译,然后先把服务端开启,然后开启客户端,输入代码,就会输出如下信息:
|
||||
|
||||
$ ./http_c localhost
|
||||
Arith: 17*8=136
|
||||
Arith: 17/8=2 remainder 1
|
||||
|
||||
通过上面的调用可以看到参数和返回值是我们定义的struct类型,在服务端我们把它们当做调用函数的参数的类型,在客户端作为`client.Call`的第2,3两个参数的类型。客户端最重要的就是这个Call函数,它有3个参数,第1个要调用的函数的名字,第2个是要传递的参数,第3个要返回的参数(注意是指针类型),通过上面的代码例子我们可以发现,使用Go的RPC实现相当的简单,方便。
|
||||
### TCP RPC
|
||||
上面我们实现了基于HTTP协议的RPC,接下来我们要实现基于TCP协议的RPC,服务端的实现代码如下所示:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/rpc"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
type Arith int
|
||||
|
||||
func (t *Arith) Multiply(args *Args, reply *int) error {
|
||||
*reply = args.A * args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Arith) Divide(args *Args, quo *Quotient) error {
|
||||
if args.B == 0 {
|
||||
return errors.New("divide by zero")
|
||||
}
|
||||
quo.Quo = args.A / args.B
|
||||
quo.Rem = args.A % args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
arith := new(Arith)
|
||||
rpc.Register(arith)
|
||||
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
|
||||
checkError(err)
|
||||
|
||||
listener, err := net.ListenTCP("tcp", tcpAddr)
|
||||
checkError(err)
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
rpc.ServeConn(conn)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func checkError(err error) {
|
||||
if err != nil {
|
||||
fmt.Println("Fatal error ", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
上面这个代码和http的服务器相比,不同在于:在此处我们采用了TCP协议,然后需要自己控制连接,当有客户端连接上来后,我们需要把这个连接交给rpc来处理。
|
||||
|
||||
如果你留心了,你会发现这它是一个阻塞型的单用户的程序,如果想要实现多并发,那么可以使用goroutine来实现,我们前面在socket小节的时候已经介绍过如何处理goroutine。
|
||||
下面展现了TCP实现的RPC客户端:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/rpc"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Println("Usage: ", os.Args[0], "server:port")
|
||||
os.Exit(1)
|
||||
}
|
||||
service := os.Args[1]
|
||||
|
||||
client, err := rpc.Dial("tcp", service)
|
||||
if err != nil {
|
||||
log.Fatal("dialing:", err)
|
||||
}
|
||||
// Synchronous call
|
||||
args := Args{17, 8}
|
||||
var reply int
|
||||
err = client.Call("Arith.Multiply", args, &reply)
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
|
||||
|
||||
var quot Quotient
|
||||
err = client.Call("Arith.Divide", args, ")
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
|
||||
|
||||
}
|
||||
|
||||
这个客户端代码和http的客户端代码对比,唯一的区别一个是DialHTTP,一个是Dial(tcp),其他处理一模一样。
|
||||
|
||||
### JSON RPC
|
||||
JSON RPC是数据编码采用了JSON,而不是gob编码,其他和上面介绍的RPC概念一模一样,下面我们来演示一下,如何使用Go提供的json-rpc标准包,请看服务端代码的实现:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/rpc"
|
||||
"net/rpc/jsonrpc"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
type Arith int
|
||||
|
||||
func (t *Arith) Multiply(args *Args, reply *int) error {
|
||||
*reply = args.A * args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Arith) Divide(args *Args, quo *Quotient) error {
|
||||
if args.B == 0 {
|
||||
return errors.New("divide by zero")
|
||||
}
|
||||
quo.Quo = args.A / args.B
|
||||
quo.Rem = args.A % args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
arith := new(Arith)
|
||||
rpc.Register(arith)
|
||||
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
|
||||
checkError(err)
|
||||
|
||||
listener, err := net.ListenTCP("tcp", tcpAddr)
|
||||
checkError(err)
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
jsonrpc.ServeConn(conn)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func checkError(err error) {
|
||||
if err != nil {
|
||||
fmt.Println("Fatal error ", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
通过示例我们可以看出 json-rpc是基于TCP协议实现的,目前它还不支持HTTP方式。
|
||||
|
||||
请看客户端的实现代码:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/rpc/jsonrpc"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Println("Usage: ", os.Args[0], "server:port")
|
||||
log.Fatal(1)
|
||||
}
|
||||
service := os.Args[1]
|
||||
|
||||
client, err := jsonrpc.Dial("tcp", service)
|
||||
if err != nil {
|
||||
log.Fatal("dialing:", err)
|
||||
}
|
||||
// Synchronous call
|
||||
args := Args{17, 8}
|
||||
var reply int
|
||||
err = client.Call("Arith.Multiply", args, &reply)
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
|
||||
|
||||
var quot Quotient
|
||||
err = client.Call("Arith.Divide", args, ")
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
|
||||
|
||||
}
|
||||
|
||||
## 总结
|
||||
Go已经提供了对RPC的良好支持,通过上面HTTP、TCP、JSON RPC的实现,我们就可以很方便的开发很多分布式的Web应用,我想作为读者的你已经领会到这一点。但遗憾的是目前Go尚未提供对SOAP RPC的支持,欣慰的是现在已经有第三方的开源实现了。
|
||||
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [REST](<08.3.md>)
|
||||
* 下一节: [小结](<08.5.md>)
|
||||
392
zh/08.4.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
392
zh/08.4.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,392 @@
|
||||
# 8.4 RPC
|
||||
前面几个小节我们介绍了如何基于Socket和HTTP来编写网络应用,通过学习我们了解了Socket和HTTP采用的是类似"信息交换"模式,即客户端发送一条信息到服务端,然后(一般来说)服务器端都会返回一定的信息以表示响应。客户端和服务端之间约定了交互信息的格式,以便双方都能够解析交互所产生的信息。但是很多独立的应用并没有采用这种模式,而是采用类似常规的函数调用的方式来完成想要的功能。
|
||||
|
||||
RPC就是想实现函数调用模式的网络化。客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传递到服务端,服务端解包到处理过程中执行,然后执行的结果反馈给客户端。
|
||||
|
||||
RPC(Remote Procedure Call Protocol)——远程过程调用协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。它假定某些传输协议的存在,如TCP或UDP,以便为通信程序之间携带信息数据。通过它可以使函数调用模式网络化。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
|
||||
|
||||
## RPC工作原理
|
||||
|
||||

|
||||
|
||||
图8.8 RPC工作流程图
|
||||
|
||||
运行时,一次客户机对服务器的RPC调用,其内部操作大致有如下十步:
|
||||
|
||||
- 1.调用客户端句柄;执行传送参数
|
||||
- 2.调用本地系统内核发送网络消息
|
||||
- 3.消息传送到远程主机
|
||||
- 4.服务器句柄得到消息并取得参数
|
||||
- 5.执行远程过程
|
||||
- 6.执行的过程将结果返回服务器句柄
|
||||
- 7.服务器句柄返回结果,调用远程系统内核
|
||||
- 8.消息传回本地主机
|
||||
- 9.客户句柄由内核接收消息
|
||||
- 10.客户接收句柄返回的数据
|
||||
|
||||
## Go RPC
|
||||
Go标准包中已经提供了对RPC的支持,而且支持三个级别的RPC:TCP、HTTP、JSONRPC。但Go的RPC包是独一无二的RPC,它和传统的RPC系统不同,它只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码。
|
||||
|
||||
Go RPC的函数只有符合下面的条件才能被远程访问,不然会被忽略,详细的要求如下:
|
||||
|
||||
- 函数必须是导出的(首字母大写)
|
||||
- 必须有两个导出类型的参数,
|
||||
- 第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的
|
||||
- 函数还要有一个返回值error
|
||||
|
||||
举个例子,正确的RPC函数格式如下:
|
||||
|
||||
func (t *T) MethodName(argType T1, replyType *T2) error
|
||||
|
||||
T、T1和T2类型必须能被`encoding/gob`包编解码。
|
||||
|
||||
任何的RPC都需要通过网络来传递数据,Go RPC可以利用HTTP和TCP来传递数据,利用HTTP的好处是可以直接复用`net/http`里面的一些函数。详细的例子请看下面的实现
|
||||
|
||||
### HTTP RPC
|
||||
http的服务端代码实现如下:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
type Arith int
|
||||
|
||||
func (t *Arith) Multiply(args *Args, reply *int) error {
|
||||
*reply = args.A * args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Arith) Divide(args *Args, quo *Quotient) error {
|
||||
if args.B == 0 {
|
||||
return errors.New("divide by zero")
|
||||
}
|
||||
quo.Quo = args.A / args.B
|
||||
quo.Rem = args.A % args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
arith := new(Arith)
|
||||
rpc.Register(arith)
|
||||
rpc.HandleHTTP()
|
||||
|
||||
err := http.ListenAndServe(":1234", nil)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
通过上面的例子可以看到,我们注册了一个Arith的RPC服务,然后通过`rpc.HandleHTTP`函数把该服务注册到了HTTP协议上,然后我们就可以利用http的方式来传递数据了。
|
||||
|
||||
请看下面的客户端代码:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/rpc"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Println("Usage: ", os.Args[0], "server")
|
||||
os.Exit(1)
|
||||
}
|
||||
serverAddress := os.Args[1]
|
||||
|
||||
client, err := rpc.DialHTTP("tcp", serverAddress+":1234")
|
||||
if err != nil {
|
||||
log.Fatal("dialing:", err)
|
||||
}
|
||||
// Synchronous call
|
||||
args := Args{17, 8}
|
||||
var reply int
|
||||
err = client.Call("Arith.Multiply", args, &reply)
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
|
||||
|
||||
var quot Quotient
|
||||
err = client.Call("Arith.Divide", args, ")
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
|
||||
|
||||
}
|
||||
|
||||
我们把上面的服务端和客户端的代码分别编译,然后先把服务端开启,然后开启客户端,输入代码,就会输出如下信息:
|
||||
|
||||
$ ./http_c localhost
|
||||
Arith: 17*8=136
|
||||
Arith: 17/8=2 remainder 1
|
||||
|
||||
通过上面的调用可以看到参数和返回值是我们定义的struct类型,在服务端我们把它们当做调用函数的参数的类型,在客户端作为`client.Call`的第2,3两个参数的类型。客户端最重要的就是这个Call函数,它有3个参数,第1个要调用的函数的名字,第2个是要传递的参数,第3个要返回的参数(注意是指针类型),通过上面的代码例子我们可以发现,使用Go的RPC实现相当的简单,方便。
|
||||
### TCP RPC
|
||||
上面我们实现了基于HTTP协议的RPC,接下来我们要实现基于TCP协议的RPC,服务端的实现代码如下所示:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/rpc"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
type Arith int
|
||||
|
||||
func (t *Arith) Multiply(args *Args, reply *int) error {
|
||||
*reply = args.A * args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Arith) Divide(args *Args, quo *Quotient) error {
|
||||
if args.B == 0 {
|
||||
return errors.New("divide by zero")
|
||||
}
|
||||
quo.Quo = args.A / args.B
|
||||
quo.Rem = args.A % args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
arith := new(Arith)
|
||||
rpc.Register(arith)
|
||||
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
|
||||
checkError(err)
|
||||
|
||||
listener, err := net.ListenTCP("tcp", tcpAddr)
|
||||
checkError(err)
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
rpc.ServeConn(conn)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func checkError(err error) {
|
||||
if err != nil {
|
||||
fmt.Println("Fatal error ", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
上面这个代码和http的服务器相比,不同在于:在此处我们采用了TCP协议,然后需要自己控制连接,当有客户端连接上来后,我们需要把这个连接交给rpc来处理。
|
||||
|
||||
如果你留心了,你会发现这它是一个阻塞型的单用户的程序,如果想要实现多并发,那么可以使用goroutine来实现,我们前面在socket小节的时候已经介绍过如何处理goroutine。
|
||||
下面展现了TCP实现的RPC客户端:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/rpc"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Println("Usage: ", os.Args[0], "server:port")
|
||||
os.Exit(1)
|
||||
}
|
||||
service := os.Args[1]
|
||||
|
||||
client, err := rpc.Dial("tcp", service)
|
||||
if err != nil {
|
||||
log.Fatal("dialing:", err)
|
||||
}
|
||||
// Synchronous call
|
||||
args := Args{17, 8}
|
||||
var reply int
|
||||
err = client.Call("Arith.Multiply", args, &reply)
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
|
||||
|
||||
var quot Quotient
|
||||
err = client.Call("Arith.Divide", args, ")
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
|
||||
|
||||
}
|
||||
|
||||
这个客户端代码和http的客户端代码对比,唯一的区别一个是DialHTTP,一个是Dial(tcp),其他处理一模一样。
|
||||
|
||||
### JSON RPC
|
||||
JSON RPC是数据编码采用了JSON,而不是gob编码,其他和上面介绍的RPC概念一模一样,下面我们来演示一下,如何使用Go提供的json-rpc标准包,请看服务端代码的实现:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/rpc"
|
||||
"net/rpc/jsonrpc"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
type Arith int
|
||||
|
||||
func (t *Arith) Multiply(args *Args, reply *int) error {
|
||||
*reply = args.A * args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Arith) Divide(args *Args, quo *Quotient) error {
|
||||
if args.B == 0 {
|
||||
return errors.New("divide by zero")
|
||||
}
|
||||
quo.Quo = args.A / args.B
|
||||
quo.Rem = args.A % args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
arith := new(Arith)
|
||||
rpc.Register(arith)
|
||||
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
|
||||
checkError(err)
|
||||
|
||||
listener, err := net.ListenTCP("tcp", tcpAddr)
|
||||
checkError(err)
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
jsonrpc.ServeConn(conn)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func checkError(err error) {
|
||||
if err != nil {
|
||||
fmt.Println("Fatal error ", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
通过示例我们可以看出 json-rpc是基于TCP协议实现的,目前它还不支持HTTP方式。
|
||||
|
||||
请看客户端的实现代码:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/rpc/jsonrpc"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Println("Usage: ", os.Args[0], "server:port")
|
||||
log.Fatal(1)
|
||||
}
|
||||
service := os.Args[1]
|
||||
|
||||
client, err := jsonrpc.Dial("tcp", service)
|
||||
if err != nil {
|
||||
log.Fatal("dialing:", err)
|
||||
}
|
||||
// Synchronous call
|
||||
args := Args{17, 8}
|
||||
var reply int
|
||||
err = client.Call("Arith.Multiply", args, &reply)
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
|
||||
|
||||
var quot Quotient
|
||||
err = client.Call("Arith.Divide", args, ")
|
||||
if err != nil {
|
||||
log.Fatal("arith error:", err)
|
||||
}
|
||||
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
|
||||
|
||||
}
|
||||
|
||||
## 总结
|
||||
Go已经提供了对RPC的良好支持,通过上面HTTP、TCP、JSON RPC的实现,我们就可以很方便的开发很多分布式的Web应用,我想作为读者的你已经领会到这一点。但遗憾的是目前Go尚未提供对SOAP RPC的支持,欣慰的是现在已经有第三方的开源实现了。
|
||||
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [REST](<08.3.md>)
|
||||
* 下一节: [小结](<08.5.md>)
|
||||
@@ -1,6 +1,6 @@
|
||||
# 8.5 小结
|
||||
这一章我们介绍了目前流行的几种主要的网络应用开发方式,第一小节介绍了网络编程中的基础:Socket编程,因为现在网络正在朝云的方向快速进化,作为这一技术演进的基石的的socket知识,作为开发者的你,是必须要掌握的。第二小节介绍了正愈发流行的HTML5中一个重要的特性WebSocket,通过它,服务器可以实现主动的push消息,以简化以前ajax轮询的模式。第三小节介绍了REST编写模式,这种模式特别适合来开发网络应用API,目前移动应用的快速发展,我觉得将来会是一个潮流。第四小节介绍了Go实现的RPC相关知识,对于上面四种开发方式,Go都已经提供了良好的支持,net包及其子包,是所有涉及到网络编程的工具的所在地。如果你想更加深入的了解相关实现细节,可以尝试阅读这个包下面的源码。
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [RPC](<08.4.md>)
|
||||
* 下一章: [安全与加密](<09.0.md>)
|
||||
# 8.5 小结
|
||||
这一章我们介绍了目前流行的几种主要的网络应用开发方式,第一小节介绍了网络编程中的基础:Socket编程,因为现在网络正在朝云的方向快速进化,作为这一技术演进的基石的的socket知识,作为开发者的你,是必须要掌握的。第二小节介绍了正愈发流行的HTML5中一个重要的特性WebSocket,通过它,服务器可以实现主动的push消息,以简化以前ajax轮询的模式。第三小节介绍了REST编写模式,这种模式特别适合来开发网络应用API,目前移动应用的快速发展,我觉得将来会是一个潮流。第四小节介绍了Go实现的RPC相关知识,对于上面四种开发方式,Go都已经提供了良好的支持,net包及其子包,是所有涉及到网络编程的工具的所在地。如果你想更加深入的了解相关实现细节,可以尝试阅读这个包下面的源码。
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [RPC](<08.4.md>)
|
||||
* 下一章: [安全与加密](<09.0.md>)
|
||||
6
zh/08.5.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
6
zh/08.5.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,6 @@
|
||||
# 8.5 小结
|
||||
这一章我们介绍了目前流行的几种主要的网络应用开发方式,第一小节介绍了网络编程中的基础:Socket编程,因为现在网络正在朝云的方向快速进化,作为这一技术演进的基石的的socket知识,作为开发者的你,是必须要掌握的。第二小节介绍了正愈发流行的HTML5中一个重要的特性WebSocket,通过它,服务器可以实现主动的push消息,以简化以前ajax轮询的模式。第三小节介绍了REST编写模式,这种模式特别适合来开发网络应用API,目前移动应用的快速发展,我觉得将来会是一个潮流。第四小节介绍了Go实现的RPC相关知识,对于上面四种开发方式,Go都已经提供了良好的支持,net包及其子包,是所有涉及到网络编程的工具的所在地。如果你想更加深入的了解相关实现细节,可以尝试阅读这个包下面的源码。
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [RPC](<08.4.md>)
|
||||
* 下一章: [安全与加密](<09.0.md>)
|
||||
@@ -1,20 +1,20 @@
|
||||
# 9 安全与加密
|
||||
无论是开发Web应用的开发者还是企图利用Web应用漏洞的攻击者,对于Web程序安全这个话题都给予了越来越多的关注。特别是最近CSDN密码泄露事件,更是让我们对Web安全这个话题更加重视,所有人都谈密码色变,都开始检测自己的系统是否存在漏洞。那么我们作为一名Go程序的开发者,一定也需要知道我们的应用程序随时会成为众多攻击者的目标,并提前做好防范的准备。
|
||||
|
||||
很多Web应用程序中的安全问题都是由于轻信了第三方提供的数据造成的。比如对于用户的输入数据,在对其进行验证之前都应该将其视为不安全的数据。如果直接把这些不安全的数据输出到客户端,就可能造成跨站脚本攻击(XSS)的问题。如果把不安全的数据用于数据库查询,那么就可能造成SQL注入问题,我们将会在9.3、9.4小节介绍如何避免这些问题。
|
||||
|
||||
在使用第三方提供的数据,包括用户提供的数据时,首先检验这些数据的合法性非常重要,这个过程叫做过滤,我们将在9.2小节介绍如何保证对所有输入的数据进行过滤处理。
|
||||
|
||||
过滤输入和转义输出并不能解决所有的安全问题,我们将会在9.1讲解的CSRF攻击,会导致受骗者发送攻击者指定的请求从而造成一些破坏。
|
||||
|
||||
与安全加密相关的,能够增强我们的Web应用程序的强大手段就是加密,CSDN泄密事件就是因为密码保存的是明文,使得攻击拿手库之后就可以直接实施一些破坏行为了。不过,和其他工具一样,加密手段也必须运用得当。我们将在9.5小节介绍如何存储密码,如何让密码存储的安全。
|
||||
|
||||
加密的本质就是扰乱数据,某些不可恢复的数据扰乱我们称为单向加密或者散列算法。另外还有一种双向加密方式,也就是可以对加密后的数据进行解密。我们将会在9.6小节介绍如何实现这种双向加密方式。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第八章总结](<08.5.md>)
|
||||
* 下一节: [预防CSRF攻击](<09.1.md>)
|
||||
# 9 安全与加密
|
||||
无论是开发Web应用的开发者还是企图利用Web应用漏洞的攻击者,对于Web程序安全这个话题都给予了越来越多的关注。特别是最近CSDN密码泄露事件,更是让我们对Web安全这个话题更加重视,所有人都谈密码色变,都开始检测自己的系统是否存在漏洞。那么我们作为一名Go程序的开发者,一定也需要知道我们的应用程序随时会成为众多攻击者的目标,并提前做好防范的准备。
|
||||
|
||||
很多Web应用程序中的安全问题都是由于轻信了第三方提供的数据造成的。比如对于用户的输入数据,在对其进行验证之前都应该将其视为不安全的数据。如果直接把这些不安全的数据输出到客户端,就可能造成跨站脚本攻击(XSS)的问题。如果把不安全的数据用于数据库查询,那么就可能造成SQL注入问题,我们将会在9.3、9.4小节介绍如何避免这些问题。
|
||||
|
||||
在使用第三方提供的数据,包括用户提供的数据时,首先检验这些数据的合法性非常重要,这个过程叫做过滤,我们将在9.2小节介绍如何保证对所有输入的数据进行过滤处理。
|
||||
|
||||
过滤输入和转义输出并不能解决所有的安全问题,我们将会在9.1讲解的CSRF攻击,会导致受骗者发送攻击者指定的请求从而造成一些破坏。
|
||||
|
||||
与安全加密相关的,能够增强我们的Web应用程序的强大手段就是加密,CSDN泄密事件就是因为密码保存的是明文,使得攻击拿手库之后就可以直接实施一些破坏行为了。不过,和其他工具一样,加密手段也必须运用得当。我们将在9.5小节介绍如何存储密码,如何让密码存储的安全。
|
||||
|
||||
加密的本质就是扰乱数据,某些不可恢复的数据扰乱我们称为单向加密或者散列算法。另外还有一种双向加密方式,也就是可以对加密后的数据进行解密。我们将会在9.6小节介绍如何实现这种双向加密方式。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第八章总结](<08.5.md>)
|
||||
* 下一节: [预防CSRF攻击](<09.1.md>)
|
||||
20
zh/09.0.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
20
zh/09.0.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,20 @@
|
||||
# 9 安全与加密
|
||||
无论是开发Web应用的开发者还是企图利用Web应用漏洞的攻击者,对于Web程序安全这个话题都给予了越来越多的关注。特别是最近CSDN密码泄露事件,更是让我们对Web安全这个话题更加重视,所有人都谈密码色变,都开始检测自己的系统是否存在漏洞。那么我们作为一名Go程序的开发者,一定也需要知道我们的应用程序随时会成为众多攻击者的目标,并提前做好防范的准备。
|
||||
|
||||
很多Web应用程序中的安全问题都是由于轻信了第三方提供的数据造成的。比如对于用户的输入数据,在对其进行验证之前都应该将其视为不安全的数据。如果直接把这些不安全的数据输出到客户端,就可能造成跨站脚本攻击(XSS)的问题。如果把不安全的数据用于数据库查询,那么就可能造成SQL注入问题,我们将会在9.3、9.4小节介绍如何避免这些问题。
|
||||
|
||||
在使用第三方提供的数据,包括用户提供的数据时,首先检验这些数据的合法性非常重要,这个过程叫做过滤,我们将在9.2小节介绍如何保证对所有输入的数据进行过滤处理。
|
||||
|
||||
过滤输入和转义输出并不能解决所有的安全问题,我们将会在9.1讲解的CSRF攻击,会导致受骗者发送攻击者指定的请求从而造成一些破坏。
|
||||
|
||||
与安全加密相关的,能够增强我们的Web应用程序的强大手段就是加密,CSDN泄密事件就是因为密码保存的是明文,使得攻击拿手库之后就可以直接实施一些破坏行为了。不过,和其他工具一样,加密手段也必须运用得当。我们将在9.5小节介绍如何存储密码,如何让密码存储的安全。
|
||||
|
||||
加密的本质就是扰乱数据,某些不可恢复的数据扰乱我们称为单向加密或者散列算法。另外还有一种双向加密方式,也就是可以对加密后的数据进行解密。我们将会在9.6小节介绍如何实现这种双向加密方式。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第八章总结](<08.5.md>)
|
||||
* 下一节: [预防CSRF攻击](<09.1.md>)
|
||||
@@ -1,93 +1,93 @@
|
||||
# 9.1 预防CSRF攻击
|
||||
|
||||
## 什么是CSRF
|
||||
CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。
|
||||
|
||||
那么CSRF到底能够干嘛呢?你可以这样简单的理解:攻击者可以盗用你的登陆信息,以你的身份模拟发送各种请求。攻击者只要借助少许的社会工程学的诡计,例如通过QQ等聊天软件发送的链接(有些还伪装成短域名,用户无法分辨),攻击者就能迫使Web应用的用户去执行攻击者预设的操作。例如,当用户登录网络银行去查看其存款余额,在他没有退出时,就点击了一个QQ好友发来的链接,那么该用户银行帐户中的资金就有可能被转移到攻击者指定的帐户中。
|
||||
|
||||
所以遇到CSRF攻击时,将对终端用户的数据和操作指令构成严重的威胁;当受攻击的终端用户具有管理员帐户的时候,CSRF攻击将危及整个Web应用程序。
|
||||
|
||||
## CSRF的原理
|
||||
下图简单阐述了CSRF攻击的思想
|
||||
|
||||

|
||||
|
||||
图9.1 CSRF的攻击过程
|
||||
|
||||
从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤 :
|
||||
|
||||
- 1.登录受信任网站A,并在本地生成Cookie 。
|
||||
- 2.在不退出A的情况下,访问危险网站B。
|
||||
|
||||
看到这里,读者也许会问:“如果我不满足以上两个条件中的任意一个,就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生:
|
||||
|
||||
- 你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站,特别现在浏览器都是支持多tab的。
|
||||
- 你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。
|
||||
- 上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。
|
||||
|
||||
因此对于用户来说很难避免在登陆一个网站之后不点击一些链接进行其他操作,所以随时可能成为CSRF的受害者。
|
||||
|
||||
CSRF攻击主要是因为Web的隐式身份验证机制,Web的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的。
|
||||
|
||||
## 如何预防CSRF
|
||||
过上面的介绍,读者是否觉得这种攻击很恐怖,意识到恐怖是个好事情,这样会促使你接着往下看如何改进和防止类似的漏洞出现。
|
||||
|
||||
CSRF的防御可以从服务端和客户端两方面着手,防御效果是从服务端着手效果比较好,现在一般的CSRF防御也都在服务端进行。
|
||||
|
||||
服务端的预防CSRF攻击的方式方法有多种,但思想上都是差不多的,主要从以下2个方面入手:
|
||||
|
||||
- 1、正确使用GET,POST和Cookie;
|
||||
- 2、在非GET请求中增加伪随机数;
|
||||
|
||||
我们上一章介绍过REST方式的Web应用,一般而言,普通的Web应用都是以GET、POST为主,还有一种请求是Cookie方式。我们一般都是按照如下方式设计应用:
|
||||
|
||||
1、GET常用在查看,列举,展示等不需要改变资源属性的时候;
|
||||
|
||||
2、POST常用在下达订单,改变一个资源的属性或者做其他一些事情;
|
||||
|
||||
接下来我就以Go语言来举例说明,如何限制对资源的访问方法:
|
||||
|
||||
mux.Get("/user/:uid", getuser)
|
||||
mux.Post("/user/:uid", modifyuser)
|
||||
|
||||
这样处理后,因为我们限定了修改只能使用POST,当GET方式请求时就拒绝响应,所以上面图示中GET方式的CSRF攻击就可以防止了,但这样就能全部解决问题了吗?当然不是,因为POST也是可以模拟的。
|
||||
|
||||
因此我们需要实施第二步,在非GET方式的请求中增加随机数,这个大概有三种方式来进行:
|
||||
|
||||
- 为每个用户生成一个唯一的cookie token,所有表单都包含同一个伪随机值,这种方案最简单,因为攻击者不能获得第三方的Cookie(理论上),所以表单中的数据也就构造失败,但是由于用户的Cookie很容易由于网站的XSS漏洞而被盗取,所以这个方案必须要在没有XSS的情况下才安全。
|
||||
- 每个请求使用验证码,这个方案是完美的,因为要多次输入验证码,所以用户友好性很差,所以不适合实际运用。
|
||||
- 不同的表单包含一个不同的伪随机值,我们在4.4小节介绍“如何防止表单多次递交”时介绍过此方案,复用相关代码,实现如下:
|
||||
|
||||
生成随机数token
|
||||
|
||||
h := md5.New()
|
||||
io.WriteString(h, strconv.FormatInt(crutime, 10))
|
||||
io.WriteString(h, "ganraomaxxxxxxxxx")
|
||||
token := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
t, _ := template.ParseFiles("login.gtpl")
|
||||
t.Execute(w, token)
|
||||
|
||||
输出token
|
||||
|
||||
<input type="hidden" name="token" value="{{.}}">
|
||||
|
||||
验证token
|
||||
|
||||
r.ParseForm()
|
||||
token := r.Form.Get("token")
|
||||
if token != "" {
|
||||
//验证token的合法性
|
||||
} else {
|
||||
//不存在token报错
|
||||
}
|
||||
|
||||
这样基本就实现了安全的POST,但是也许你会说如果破解了token的算法呢,按照理论上是,但是实际上破解是基本不可能的,因为有人曾计算过,暴力破解该串大概需要2的11次方时间。
|
||||
|
||||
## 总结
|
||||
跨站请求伪造,即CSRF,是一种非常危险的Web安全威胁,它被Web安全界称为“沉睡的巨人”,其威胁程度由此“美誉”便可见一斑。本小节不仅对跨站请求伪造本身进行了简单介绍,还详细说明造成这种漏洞的原因所在,然后以此提了一些防范该攻击的建议,希望对读者编写安全的Web应用能够有所启发。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [安全与加密](<09.0.md>)
|
||||
* 下一节: [确保输入过滤](<09.2.md>)
|
||||
# 9.1 预防CSRF攻击
|
||||
|
||||
## 什么是CSRF
|
||||
CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。
|
||||
|
||||
那么CSRF到底能够干嘛呢?你可以这样简单的理解:攻击者可以盗用你的登陆信息,以你的身份模拟发送各种请求。攻击者只要借助少许的社会工程学的诡计,例如通过QQ等聊天软件发送的链接(有些还伪装成短域名,用户无法分辨),攻击者就能迫使Web应用的用户去执行攻击者预设的操作。例如,当用户登录网络银行去查看其存款余额,在他没有退出时,就点击了一个QQ好友发来的链接,那么该用户银行帐户中的资金就有可能被转移到攻击者指定的帐户中。
|
||||
|
||||
所以遇到CSRF攻击时,将对终端用户的数据和操作指令构成严重的威胁;当受攻击的终端用户具有管理员帐户的时候,CSRF攻击将危及整个Web应用程序。
|
||||
|
||||
## CSRF的原理
|
||||
下图简单阐述了CSRF攻击的思想
|
||||
|
||||

|
||||
|
||||
图9.1 CSRF的攻击过程
|
||||
|
||||
从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤 :
|
||||
|
||||
- 1.登录受信任网站A,并在本地生成Cookie 。
|
||||
- 2.在不退出A的情况下,访问危险网站B。
|
||||
|
||||
看到这里,读者也许会问:“如果我不满足以上两个条件中的任意一个,就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生:
|
||||
|
||||
- 你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站,特别现在浏览器都是支持多tab的。
|
||||
- 你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。
|
||||
- 上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。
|
||||
|
||||
因此对于用户来说很难避免在登陆一个网站之后不点击一些链接进行其他操作,所以随时可能成为CSRF的受害者。
|
||||
|
||||
CSRF攻击主要是因为Web的隐式身份验证机制,Web的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的。
|
||||
|
||||
## 如何预防CSRF
|
||||
过上面的介绍,读者是否觉得这种攻击很恐怖,意识到恐怖是个好事情,这样会促使你接着往下看如何改进和防止类似的漏洞出现。
|
||||
|
||||
CSRF的防御可以从服务端和客户端两方面着手,防御效果是从服务端着手效果比较好,现在一般的CSRF防御也都在服务端进行。
|
||||
|
||||
服务端的预防CSRF攻击的方式方法有多种,但思想上都是差不多的,主要从以下2个方面入手:
|
||||
|
||||
- 1、正确使用GET,POST和Cookie;
|
||||
- 2、在非GET请求中增加伪随机数;
|
||||
|
||||
我们上一章介绍过REST方式的Web应用,一般而言,普通的Web应用都是以GET、POST为主,还有一种请求是Cookie方式。我们一般都是按照如下方式设计应用:
|
||||
|
||||
1、GET常用在查看,列举,展示等不需要改变资源属性的时候;
|
||||
|
||||
2、POST常用在下达订单,改变一个资源的属性或者做其他一些事情;
|
||||
|
||||
接下来我就以Go语言来举例说明,如何限制对资源的访问方法:
|
||||
|
||||
mux.Get("/user/:uid", getuser)
|
||||
mux.Post("/user/:uid", modifyuser)
|
||||
|
||||
这样处理后,因为我们限定了修改只能使用POST,当GET方式请求时就拒绝响应,所以上面图示中GET方式的CSRF攻击就可以防止了,但这样就能全部解决问题了吗?当然不是,因为POST也是可以模拟的。
|
||||
|
||||
因此我们需要实施第二步,在非GET方式的请求中增加随机数,这个大概有三种方式来进行:
|
||||
|
||||
- 为每个用户生成一个唯一的cookie token,所有表单都包含同一个伪随机值,这种方案最简单,因为攻击者不能获得第三方的Cookie(理论上),所以表单中的数据也就构造失败,但是由于用户的Cookie很容易由于网站的XSS漏洞而被盗取,所以这个方案必须要在没有XSS的情况下才安全。
|
||||
- 每个请求使用验证码,这个方案是完美的,因为要多次输入验证码,所以用户友好性很差,所以不适合实际运用。
|
||||
- 不同的表单包含一个不同的伪随机值,我们在4.4小节介绍“如何防止表单多次递交”时介绍过此方案,复用相关代码,实现如下:
|
||||
|
||||
生成随机数token
|
||||
|
||||
h := md5.New()
|
||||
io.WriteString(h, strconv.FormatInt(crutime, 10))
|
||||
io.WriteString(h, "ganraomaxxxxxxxxx")
|
||||
token := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
t, _ := template.ParseFiles("login.gtpl")
|
||||
t.Execute(w, token)
|
||||
|
||||
输出token
|
||||
|
||||
<input type="hidden" name="token" value="{{.}}">
|
||||
|
||||
验证token
|
||||
|
||||
r.ParseForm()
|
||||
token := r.Form.Get("token")
|
||||
if token != "" {
|
||||
//验证token的合法性
|
||||
} else {
|
||||
//不存在token报错
|
||||
}
|
||||
|
||||
这样基本就实现了安全的POST,但是也许你会说如果破解了token的算法呢,按照理论上是,但是实际上破解是基本不可能的,因为有人曾计算过,暴力破解该串大概需要2的11次方时间。
|
||||
|
||||
## 总结
|
||||
跨站请求伪造,即CSRF,是一种非常危险的Web安全威胁,它被Web安全界称为“沉睡的巨人”,其威胁程度由此“美誉”便可见一斑。本小节不仅对跨站请求伪造本身进行了简单介绍,还详细说明造成这种漏洞的原因所在,然后以此提了一些防范该攻击的建议,希望对读者编写安全的Web应用能够有所启发。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [安全与加密](<09.0.md>)
|
||||
* 下一节: [确保输入过滤](<09.2.md>)
|
||||
93
zh/09.1.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
93
zh/09.1.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,93 @@
|
||||
# 9.1 预防CSRF攻击
|
||||
|
||||
## 什么是CSRF
|
||||
CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。
|
||||
|
||||
那么CSRF到底能够干嘛呢?你可以这样简单的理解:攻击者可以盗用你的登陆信息,以你的身份模拟发送各种请求。攻击者只要借助少许的社会工程学的诡计,例如通过QQ等聊天软件发送的链接(有些还伪装成短域名,用户无法分辨),攻击者就能迫使Web应用的用户去执行攻击者预设的操作。例如,当用户登录网络银行去查看其存款余额,在他没有退出时,就点击了一个QQ好友发来的链接,那么该用户银行帐户中的资金就有可能被转移到攻击者指定的帐户中。
|
||||
|
||||
所以遇到CSRF攻击时,将对终端用户的数据和操作指令构成严重的威胁;当受攻击的终端用户具有管理员帐户的时候,CSRF攻击将危及整个Web应用程序。
|
||||
|
||||
## CSRF的原理
|
||||
下图简单阐述了CSRF攻击的思想
|
||||
|
||||

|
||||
|
||||
图9.1 CSRF的攻击过程
|
||||
|
||||
从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤 :
|
||||
|
||||
- 1.登录受信任网站A,并在本地生成Cookie 。
|
||||
- 2.在不退出A的情况下,访问危险网站B。
|
||||
|
||||
看到这里,读者也许会问:“如果我不满足以上两个条件中的任意一个,就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生:
|
||||
|
||||
- 你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站,特别现在浏览器都是支持多tab的。
|
||||
- 你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。
|
||||
- 上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。
|
||||
|
||||
因此对于用户来说很难避免在登陆一个网站之后不点击一些链接进行其他操作,所以随时可能成为CSRF的受害者。
|
||||
|
||||
CSRF攻击主要是因为Web的隐式身份验证机制,Web的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的。
|
||||
|
||||
## 如何预防CSRF
|
||||
过上面的介绍,读者是否觉得这种攻击很恐怖,意识到恐怖是个好事情,这样会促使你接着往下看如何改进和防止类似的漏洞出现。
|
||||
|
||||
CSRF的防御可以从服务端和客户端两方面着手,防御效果是从服务端着手效果比较好,现在一般的CSRF防御也都在服务端进行。
|
||||
|
||||
服务端的预防CSRF攻击的方式方法有多种,但思想上都是差不多的,主要从以下2个方面入手:
|
||||
|
||||
- 1、正确使用GET,POST和Cookie;
|
||||
- 2、在非GET请求中增加伪随机数;
|
||||
|
||||
我们上一章介绍过REST方式的Web应用,一般而言,普通的Web应用都是以GET、POST为主,还有一种请求是Cookie方式。我们一般都是按照如下方式设计应用:
|
||||
|
||||
1、GET常用在查看,列举,展示等不需要改变资源属性的时候;
|
||||
|
||||
2、POST常用在下达订单,改变一个资源的属性或者做其他一些事情;
|
||||
|
||||
接下来我就以Go语言来举例说明,如何限制对资源的访问方法:
|
||||
|
||||
mux.Get("/user/:uid", getuser)
|
||||
mux.Post("/user/:uid", modifyuser)
|
||||
|
||||
这样处理后,因为我们限定了修改只能使用POST,当GET方式请求时就拒绝响应,所以上面图示中GET方式的CSRF攻击就可以防止了,但这样就能全部解决问题了吗?当然不是,因为POST也是可以模拟的。
|
||||
|
||||
因此我们需要实施第二步,在非GET方式的请求中增加随机数,这个大概有三种方式来进行:
|
||||
|
||||
- 为每个用户生成一个唯一的cookie token,所有表单都包含同一个伪随机值,这种方案最简单,因为攻击者不能获得第三方的Cookie(理论上),所以表单中的数据也就构造失败,但是由于用户的Cookie很容易由于网站的XSS漏洞而被盗取,所以这个方案必须要在没有XSS的情况下才安全。
|
||||
- 每个请求使用验证码,这个方案是完美的,因为要多次输入验证码,所以用户友好性很差,所以不适合实际运用。
|
||||
- 不同的表单包含一个不同的伪随机值,我们在4.4小节介绍“如何防止表单多次递交”时介绍过此方案,复用相关代码,实现如下:
|
||||
|
||||
生成随机数token
|
||||
|
||||
h := md5.New()
|
||||
io.WriteString(h, strconv.FormatInt(crutime, 10))
|
||||
io.WriteString(h, "ganraomaxxxxxxxxx")
|
||||
token := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
t, _ := template.ParseFiles("login.gtpl")
|
||||
t.Execute(w, token)
|
||||
|
||||
输出token
|
||||
|
||||
<input type="hidden" name="token" value="{{.}}">
|
||||
|
||||
验证token
|
||||
|
||||
r.ParseForm()
|
||||
token := r.Form.Get("token")
|
||||
if token != "" {
|
||||
//验证token的合法性
|
||||
} else {
|
||||
//不存在token报错
|
||||
}
|
||||
|
||||
这样基本就实现了安全的POST,但是也许你会说如果破解了token的算法呢,按照理论上是,但是实际上破解是基本不可能的,因为有人曾计算过,暴力破解该串大概需要2的11次方时间。
|
||||
|
||||
## 总结
|
||||
跨站请求伪造,即CSRF,是一种非常危险的Web安全威胁,它被Web安全界称为“沉睡的巨人”,其威胁程度由此“美誉”便可见一斑。本小节不仅对跨站请求伪造本身进行了简单介绍,还详细说明造成这种漏洞的原因所在,然后以此提了一些防范该攻击的建议,希望对读者编写安全的Web应用能够有所启发。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [安全与加密](<09.0.md>)
|
||||
* 下一节: [确保输入过滤](<09.2.md>)
|
||||
@@ -1,72 +1,72 @@
|
||||
# 9.2 确保输入过滤
|
||||
过滤用户数据是Web应用安全的基础。它是验证数据合法性的过程。通过对所有的输入数据进行过滤,可以避免恶意数据在程序中被误信或误用。大多数Web应用的漏洞都是因为没有对用户输入的数据进行恰当过滤所引起的。
|
||||
|
||||
我们介绍的过滤数据分成三个步骤:
|
||||
|
||||
- 1、识别数据,搞清楚需要过滤的数据来自于哪里
|
||||
- 2、过滤数据,弄明白我们需要什么样的数据
|
||||
- 3、区分已过滤及被污染数据,如果存在攻击数据那么保证过滤之后可以让我们使用更安全的数据
|
||||
|
||||
## 识别数据
|
||||
“识别数据”作为第一步是因为在你不知道“数据是什么,它来自于哪里”的前提下,你也就不能正确地过滤它。这里的数据是指所有源自非代码内部提供的数据。例如:所有来自客户端的数据,但客户端并不是唯一的外部数据源,数据库和第三方提供的接口数据等也可以是外部数据源。
|
||||
|
||||
由用户输入的数据我们通过Go非常容易识别,Go通过`r.ParseForm`之后,把用户POST和GET的数据全部放在了`r.Form`里面。其它的输入要难识别得多,例如,`r.Header`中的很多元素是由客户端所操纵的。常常很难确认其中的哪些元素组成了输入,所以,最好的方法是把里面所有的数据都看成是用户输入。(例如`r.Header.Get("Accept-Charset")`这样的也看做是用户输入,虽然这些大多数是浏览器操纵的)
|
||||
|
||||
## 过滤数据
|
||||
在知道数据来源之后,就可以过滤它了。过滤是一个有点正式的术语,它在平时表述中有很多同义词,如验证、清洁及净化。尽管这些术语表面意义不同,但它们都是指的同一个处理:防止非法数据进入你的应用。
|
||||
|
||||
过滤数据有很多种方法,其中有一些安全性较差。最好的方法是把过滤看成是一个检查的过程,在你使用数据之前都检查一下看它们是否是符合合法数据的要求。而且不要试图好心地去纠正非法数据,而要让用户按你制定的规则去输入数据。历史证明了试图纠正非法数据往往会导致安全漏洞。这里举个例子:“最近建设银行系统升级之后,如果密码后面两位是0,只要输入前面四位就能登录系统”,这是一个非常严重的漏洞。
|
||||
|
||||
过滤数据主要采用如下一些库来操作:
|
||||
|
||||
- strconv包下面的字符串转化相关函数,因为从Request中的`r.Form`返回的是字符串,而有些时候我们需要将之转化成整/浮点数,`Atoi`、`ParseBool`、`ParseFloat`、`ParseInt`等函数就可以派上用场了。
|
||||
- string包下面的一些过滤函数`Trim`、`ToLower`、`ToTitle`等函数,能够帮助我们按照指定的格式获取信息。
|
||||
- regexp包用来处理一些复杂的需求,例如判定输入是否是Email、生日之类。
|
||||
|
||||
过滤数据除了检查验证之外,在特殊时候,还可以采用白名单。即假定你正在检查的数据都是非法的,除非能证明它是合法的。使用这个方法,如果出现错误,只会导致把合法的数据当成是非法的,而不会是相反,尽管我们不想犯任何错误,但这样总比把非法数据当成合法数据要安全得多。
|
||||
|
||||
## 区分过滤数据
|
||||
如果完成了上面的两步,数据过滤的工作就基本完成了,但是在编写Web应用的时候我们还需要区分已过滤和被污染数据,因为这样可以保证过滤数据的完整性,而不影响输入的数据。我们约定把所有经过过滤的数据放入一个叫全局的Map变量中(CleanMap)。这时需要用两个重要的步骤来防止被污染数据的注入:
|
||||
- 每个请求都要初始化CleanMap为一个空Map。
|
||||
- 加入检查及阻止来自外部数据源的变量命名为CleanMap。
|
||||
|
||||
接下来,让我们通过一个例子来巩固这些概念,请看下面这个表单
|
||||
|
||||
<form action="/whoami" method="POST">
|
||||
我是谁:
|
||||
<select name="name">
|
||||
<option value="astaxie">astaxie</option>
|
||||
<option value="herry">herry</option>
|
||||
<option value="marry">marry</option>
|
||||
</select>
|
||||
<input type="submit" />
|
||||
</form>
|
||||
|
||||
在处理这个表单的编程逻辑中,非常容易犯的错误是认为只能提交三个选择中的一个。其实攻击者可以模拟POST操作,递交`name=attack`这样的数据,所以在此时我们需要做类似白名单的处理
|
||||
|
||||
r.ParseForm()
|
||||
name := r.Form.Get("name")
|
||||
CleanMap := make(map[string]interface{}, 0)
|
||||
if name == "astaxie" || name == "herry" || name == "marry" {
|
||||
CleanMap["name"] = name
|
||||
}
|
||||
|
||||
上面代码中我们初始化了一个CleanMap的变量,当判断获取的name是`astaxie`、`herry`、`marry`三个中的一个之后
|
||||
,我们把数据存储到了CleanMap之中,这样就可以确保CleanMap["name"]中的数据是合法的,从而在代码的其它部分使用它。当然我们还可以在else部分增加非法数据的处理,一种可能是再次显示表单并提示错误。但是不要试图为了友好而输出被污染的数据。
|
||||
|
||||
上面的方法对于过滤一组已知的合法值的数据很有效,但是对于过滤有一组已知合法字符组成的数据时就没有什么帮助。例如,你可能需要一个用户名只能由字母及数字组成:
|
||||
|
||||
r.ParseForm()
|
||||
username := r.Form.Get("username")
|
||||
CleanMap := make(map[string]interface{}, 0)
|
||||
if ok, _ := regexp.MatchString("^[a-zA-Z0-9].$", username); ok {
|
||||
CleanMap["username"] = username
|
||||
}
|
||||
|
||||
## 总结
|
||||
数据过滤在Web安全中起到一个基石的作用,大多数的安全问题都是由于没有过滤数据和验证数据引起的,例如前面小节的CSRF攻击,以及接下来将要介绍的XSS攻击、SQL注入等都是没有认真地过滤数据引起的,因此我们需要特别重视这部分的内容。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [预防CSRF攻击](<09.1.md>)
|
||||
* 下一节: [避免XSS攻击](<09.3.md>)
|
||||
# 9.2 确保输入过滤
|
||||
过滤用户数据是Web应用安全的基础。它是验证数据合法性的过程。通过对所有的输入数据进行过滤,可以避免恶意数据在程序中被误信或误用。大多数Web应用的漏洞都是因为没有对用户输入的数据进行恰当过滤所引起的。
|
||||
|
||||
我们介绍的过滤数据分成三个步骤:
|
||||
|
||||
- 1、识别数据,搞清楚需要过滤的数据来自于哪里
|
||||
- 2、过滤数据,弄明白我们需要什么样的数据
|
||||
- 3、区分已过滤及被污染数据,如果存在攻击数据那么保证过滤之后可以让我们使用更安全的数据
|
||||
|
||||
## 识别数据
|
||||
“识别数据”作为第一步是因为在你不知道“数据是什么,它来自于哪里”的前提下,你也就不能正确地过滤它。这里的数据是指所有源自非代码内部提供的数据。例如:所有来自客户端的数据,但客户端并不是唯一的外部数据源,数据库和第三方提供的接口数据等也可以是外部数据源。
|
||||
|
||||
由用户输入的数据我们通过Go非常容易识别,Go通过`r.ParseForm`之后,把用户POST和GET的数据全部放在了`r.Form`里面。其它的输入要难识别得多,例如,`r.Header`中的很多元素是由客户端所操纵的。常常很难确认其中的哪些元素组成了输入,所以,最好的方法是把里面所有的数据都看成是用户输入。(例如`r.Header.Get("Accept-Charset")`这样的也看做是用户输入,虽然这些大多数是浏览器操纵的)
|
||||
|
||||
## 过滤数据
|
||||
在知道数据来源之后,就可以过滤它了。过滤是一个有点正式的术语,它在平时表述中有很多同义词,如验证、清洁及净化。尽管这些术语表面意义不同,但它们都是指的同一个处理:防止非法数据进入你的应用。
|
||||
|
||||
过滤数据有很多种方法,其中有一些安全性较差。最好的方法是把过滤看成是一个检查的过程,在你使用数据之前都检查一下看它们是否是符合合法数据的要求。而且不要试图好心地去纠正非法数据,而要让用户按你制定的规则去输入数据。历史证明了试图纠正非法数据往往会导致安全漏洞。这里举个例子:“最近建设银行系统升级之后,如果密码后面两位是0,只要输入前面四位就能登录系统”,这是一个非常严重的漏洞。
|
||||
|
||||
过滤数据主要采用如下一些库来操作:
|
||||
|
||||
- strconv包下面的字符串转化相关函数,因为从Request中的`r.Form`返回的是字符串,而有些时候我们需要将之转化成整/浮点数,`Atoi`、`ParseBool`、`ParseFloat`、`ParseInt`等函数就可以派上用场了。
|
||||
- string包下面的一些过滤函数`Trim`、`ToLower`、`ToTitle`等函数,能够帮助我们按照指定的格式获取信息。
|
||||
- regexp包用来处理一些复杂的需求,例如判定输入是否是Email、生日之类。
|
||||
|
||||
过滤数据除了检查验证之外,在特殊时候,还可以采用白名单。即假定你正在检查的数据都是非法的,除非能证明它是合法的。使用这个方法,如果出现错误,只会导致把合法的数据当成是非法的,而不会是相反,尽管我们不想犯任何错误,但这样总比把非法数据当成合法数据要安全得多。
|
||||
|
||||
## 区分过滤数据
|
||||
如果完成了上面的两步,数据过滤的工作就基本完成了,但是在编写Web应用的时候我们还需要区分已过滤和被污染数据,因为这样可以保证过滤数据的完整性,而不影响输入的数据。我们约定把所有经过过滤的数据放入一个叫全局的Map变量中(CleanMap)。这时需要用两个重要的步骤来防止被污染数据的注入:
|
||||
- 每个请求都要初始化CleanMap为一个空Map。
|
||||
- 加入检查及阻止来自外部数据源的变量命名为CleanMap。
|
||||
|
||||
接下来,让我们通过一个例子来巩固这些概念,请看下面这个表单
|
||||
|
||||
<form action="/whoami" method="POST">
|
||||
我是谁:
|
||||
<select name="name">
|
||||
<option value="astaxie">astaxie</option>
|
||||
<option value="herry">herry</option>
|
||||
<option value="marry">marry</option>
|
||||
</select>
|
||||
<input type="submit" />
|
||||
</form>
|
||||
|
||||
在处理这个表单的编程逻辑中,非常容易犯的错误是认为只能提交三个选择中的一个。其实攻击者可以模拟POST操作,递交`name=attack`这样的数据,所以在此时我们需要做类似白名单的处理
|
||||
|
||||
r.ParseForm()
|
||||
name := r.Form.Get("name")
|
||||
CleanMap := make(map[string]interface{}, 0)
|
||||
if name == "astaxie" || name == "herry" || name == "marry" {
|
||||
CleanMap["name"] = name
|
||||
}
|
||||
|
||||
上面代码中我们初始化了一个CleanMap的变量,当判断获取的name是`astaxie`、`herry`、`marry`三个中的一个之后
|
||||
,我们把数据存储到了CleanMap之中,这样就可以确保CleanMap["name"]中的数据是合法的,从而在代码的其它部分使用它。当然我们还可以在else部分增加非法数据的处理,一种可能是再次显示表单并提示错误。但是不要试图为了友好而输出被污染的数据。
|
||||
|
||||
上面的方法对于过滤一组已知的合法值的数据很有效,但是对于过滤有一组已知合法字符组成的数据时就没有什么帮助。例如,你可能需要一个用户名只能由字母及数字组成:
|
||||
|
||||
r.ParseForm()
|
||||
username := r.Form.Get("username")
|
||||
CleanMap := make(map[string]interface{}, 0)
|
||||
if ok, _ := regexp.MatchString("^[a-zA-Z0-9].$", username); ok {
|
||||
CleanMap["username"] = username
|
||||
}
|
||||
|
||||
## 总结
|
||||
数据过滤在Web安全中起到一个基石的作用,大多数的安全问题都是由于没有过滤数据和验证数据引起的,例如前面小节的CSRF攻击,以及接下来将要介绍的XSS攻击、SQL注入等都是没有认真地过滤数据引起的,因此我们需要特别重视这部分的内容。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [预防CSRF攻击](<09.1.md>)
|
||||
* 下一节: [避免XSS攻击](<09.3.md>)
|
||||
72
zh/09.2.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
72
zh/09.2.md~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,72 @@
|
||||
# 9.2 确保输入过滤
|
||||
过滤用户数据是Web应用安全的基础。它是验证数据合法性的过程。通过对所有的输入数据进行过滤,可以避免恶意数据在程序中被误信或误用。大多数Web应用的漏洞都是因为没有对用户输入的数据进行恰当过滤所引起的。
|
||||
|
||||
我们介绍的过滤数据分成三个步骤:
|
||||
|
||||
- 1、识别数据,搞清楚需要过滤的数据来自于哪里
|
||||
- 2、过滤数据,弄明白我们需要什么样的数据
|
||||
- 3、区分已过滤及被污染数据,如果存在攻击数据那么保证过滤之后可以让我们使用更安全的数据
|
||||
|
||||
## 识别数据
|
||||
“识别数据”作为第一步是因为在你不知道“数据是什么,它来自于哪里”的前提下,你也就不能正确地过滤它。这里的数据是指所有源自非代码内部提供的数据。例如:所有来自客户端的数据,但客户端并不是唯一的外部数据源,数据库和第三方提供的接口数据等也可以是外部数据源。
|
||||
|
||||
由用户输入的数据我们通过Go非常容易识别,Go通过`r.ParseForm`之后,把用户POST和GET的数据全部放在了`r.Form`里面。其它的输入要难识别得多,例如,`r.Header`中的很多元素是由客户端所操纵的。常常很难确认其中的哪些元素组成了输入,所以,最好的方法是把里面所有的数据都看成是用户输入。(例如`r.Header.Get("Accept-Charset")`这样的也看做是用户输入,虽然这些大多数是浏览器操纵的)
|
||||
|
||||
## 过滤数据
|
||||
在知道数据来源之后,就可以过滤它了。过滤是一个有点正式的术语,它在平时表述中有很多同义词,如验证、清洁及净化。尽管这些术语表面意义不同,但它们都是指的同一个处理:防止非法数据进入你的应用。
|
||||
|
||||
过滤数据有很多种方法,其中有一些安全性较差。最好的方法是把过滤看成是一个检查的过程,在你使用数据之前都检查一下看它们是否是符合合法数据的要求。而且不要试图好心地去纠正非法数据,而要让用户按你制定的规则去输入数据。历史证明了试图纠正非法数据往往会导致安全漏洞。这里举个例子:“最近建设银行系统升级之后,如果密码后面两位是0,只要输入前面四位就能登录系统”,这是一个非常严重的漏洞。
|
||||
|
||||
过滤数据主要采用如下一些库来操作:
|
||||
|
||||
- strconv包下面的字符串转化相关函数,因为从Request中的`r.Form`返回的是字符串,而有些时候我们需要将之转化成整/浮点数,`Atoi`、`ParseBool`、`ParseFloat`、`ParseInt`等函数就可以派上用场了。
|
||||
- string包下面的一些过滤函数`Trim`、`ToLower`、`ToTitle`等函数,能够帮助我们按照指定的格式获取信息。
|
||||
- regexp包用来处理一些复杂的需求,例如判定输入是否是Email、生日之类。
|
||||
|
||||
过滤数据除了检查验证之外,在特殊时候,还可以采用白名单。即假定你正在检查的数据都是非法的,除非能证明它是合法的。使用这个方法,如果出现错误,只会导致把合法的数据当成是非法的,而不会是相反,尽管我们不想犯任何错误,但这样总比把非法数据当成合法数据要安全得多。
|
||||
|
||||
## 区分过滤数据
|
||||
如果完成了上面的两步,数据过滤的工作就基本完成了,但是在编写Web应用的时候我们还需要区分已过滤和被污染数据,因为这样可以保证过滤数据的完整性,而不影响输入的数据。我们约定把所有经过过滤的数据放入一个叫全局的Map变量中(CleanMap)。这时需要用两个重要的步骤来防止被污染数据的注入:
|
||||
- 每个请求都要初始化CleanMap为一个空Map。
|
||||
- 加入检查及阻止来自外部数据源的变量命名为CleanMap。
|
||||
|
||||
接下来,让我们通过一个例子来巩固这些概念,请看下面这个表单
|
||||
|
||||
<form action="/whoami" method="POST">
|
||||
我是谁:
|
||||
<select name="name">
|
||||
<option value="astaxie">astaxie</option>
|
||||
<option value="herry">herry</option>
|
||||
<option value="marry">marry</option>
|
||||
</select>
|
||||
<input type="submit" />
|
||||
</form>
|
||||
|
||||
在处理这个表单的编程逻辑中,非常容易犯的错误是认为只能提交三个选择中的一个。其实攻击者可以模拟POST操作,递交`name=attack`这样的数据,所以在此时我们需要做类似白名单的处理
|
||||
|
||||
r.ParseForm()
|
||||
name := r.Form.Get("name")
|
||||
CleanMap := make(map[string]interface{}, 0)
|
||||
if name == "astaxie" || name == "herry" || name == "marry" {
|
||||
CleanMap["name"] = name
|
||||
}
|
||||
|
||||
上面代码中我们初始化了一个CleanMap的变量,当判断获取的name是`astaxie`、`herry`、`marry`三个中的一个之后
|
||||
,我们把数据存储到了CleanMap之中,这样就可以确保CleanMap["name"]中的数据是合法的,从而在代码的其它部分使用它。当然我们还可以在else部分增加非法数据的处理,一种可能是再次显示表单并提示错误。但是不要试图为了友好而输出被污染的数据。
|
||||
|
||||
上面的方法对于过滤一组已知的合法值的数据很有效,但是对于过滤有一组已知合法字符组成的数据时就没有什么帮助。例如,你可能需要一个用户名只能由字母及数字组成:
|
||||
|
||||
r.ParseForm()
|
||||
username := r.Form.Get("username")
|
||||
CleanMap := make(map[string]interface{}, 0)
|
||||
if ok, _ := regexp.MatchString("^[a-zA-Z0-9].$", username); ok {
|
||||
CleanMap["username"] = username
|
||||
}
|
||||
|
||||
## 总结
|
||||
数据过滤在Web安全中起到一个基石的作用,大多数的安全问题都是由于没有过滤数据和验证数据引起的,例如前面小节的CSRF攻击,以及接下来将要介绍的XSS攻击、SQL注入等都是没有认真地过滤数据引起的,因此我们需要特别重视这部分的内容。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [预防CSRF攻击](<09.1.md>)
|
||||
* 下一节: [避免XSS攻击](<09.3.md>)
|
||||
@@ -11,9 +11,14 @@
|
||||
7. [Network programming with Go](http://jan.newmarch.name/go/)
|
||||
8. [setup-the-rails-application-for-internationalization](http://guides.rubyonrails.org/i18n.html#setup-the-rails-application-for-internationalization)
|
||||
9. [The Cross-Site Scripting (XSS) FAQ](http://www.cgisecurity.com/xss-faq.html)
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< fa439f692f31fa3d054eac849ea958f29c613b6e
|
||||
10. [Network programming with Go](http://jan.newmarch.name/go)
|
||||
11. [RESTFul](http://www.ruanyifeng.com/blog/2011/09/restful.html)
|
||||
=======
|
||||
10. [Network programming with Go](http://jan.newmarch.name/go)
|
||||
>>>>>>> fix #378
|
||||
=======
|
||||
10. [Network programming with Go](http://jan.newmarch.name/go)
|
||||
11. [RESTFul](http://www.ruanyifeng.com/blog/2011/09/restful.html)
|
||||
>>>>>>> eead24cf064976b648de5826eab51880c803b0fa
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// 章节 1.2
|
||||
// $GOPATH/src/mathapp/main.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mymath"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Printf("Hello, world. Sqrt(2) = %v\n", mymath.Sqrt(2))
|
||||
}
|
||||
// 章节 1.2
|
||||
// $GOPATH/src/mathapp/main.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mymath"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Printf("Hello, world. Sqrt(2) = %v\n", mymath.Sqrt(2))
|
||||
}
|
||||
13
zh/src/1.2/main.go~eead24cf064976b648de5826eab51880c803b0fa
Normal file
13
zh/src/1.2/main.go~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,13 @@
|
||||
// 章节 1.2
|
||||
// $GOPATH/src/mathapp/main.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mymath"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Printf("Hello, world. Sqrt(2) = %v\n", mymath.Sqrt(2))
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
// 章节 1.2
|
||||
// $GOPATH/src/mymath/sqrt.go
|
||||
package mymath
|
||||
|
||||
func Sqrt(x float64) float64 {
|
||||
z := 0.0
|
||||
for i := 0; i < 1000; i++ {
|
||||
z -= (z*z - x) / (2 * x)
|
||||
}
|
||||
return z
|
||||
}
|
||||
// 章节 1.2
|
||||
// $GOPATH/src/mymath/sqrt.go
|
||||
package mymath
|
||||
|
||||
func Sqrt(x float64) float64 {
|
||||
z := 0.0
|
||||
for i := 0; i < 1000; i++ {
|
||||
z -= (z*z - x) / (2 * x)
|
||||
}
|
||||
return z
|
||||
}
|
||||
11
zh/src/1.2/sqrt.go~eead24cf064976b648de5826eab51880c803b0fa
Normal file
11
zh/src/1.2/sqrt.go~eead24cf064976b648de5826eab51880c803b0fa
Normal file
@@ -0,0 +1,11 @@
|
||||
// 章节 1.2
|
||||
// $GOPATH/src/mymath/sqrt.go
|
||||
package mymath
|
||||
|
||||
func Sqrt(x float64) float64 {
|
||||
z := 0.0
|
||||
for i := 0; i < 1000; i++ {
|
||||
z -= (z*z - x) / (2 * x)
|
||||
}
|
||||
return z
|
||||
}
|
||||
Reference in New Issue
Block a user