@ -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将会是一件愉快的事情。等回过头来,你就会发现这二十五个关键字是多么地亲切。 |
||||
|
|
||||
|
## 目录 |
||||
|
![](images/navi2.png?raw=true) |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一章: [第一章总结](<01.5.md>) |
||||
|
* 下一节: [你好,Go](<02.1.md>) |
@ -0,0 +1,53 @@ |
|||||
|
# 2.1 你好,Go |
||||
|
|
||||
|
在开始编写应用之前,我们先从最基本的程序开始。就像你造房子之前不知道什么是地基一样,编写程序也不知道如何开始。因此,在本节中,我们要学习用最基本的语法让Go程序运行起来。 |
||||
|
|
||||
|
## 程序 |
||||
|
|
||||
|
这就像一个传统,在学习大部分语言之前,你先学会如何编写一个可以输出`hello world`的程序。 |
||||
|
|
||||
|
准备好了吗?Let's Go! |
||||
|
```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>) |
@ -0,0 +1,337 @@ |
|||||
|
# 2.5 面向对象 |
||||
|
前面两章我们介绍了函数和struct,那你是否想过函数当作struct的字段一样来处理呢?今天我们就讲解一下函数的另一种形态,带有接收者的函数,我们称为`method` |
||||
|
|
||||
|
## method |
||||
|
现在假设有这么一个场景,你定义了一个struct叫做长方形,你现在想要计算他的面积,那么按照我们一般的思路应该会用下面的方式来实现 |
||||
|
```Go |
||||
|
|
||||
|
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的。 |
||||
|
|
||||
|
![](images/2.5.rect_func_without_receiver.png?raw=true) |
||||
|
|
||||
|
图2.8 方法和struct的关系图 |
||||
|
|
||||
|
很显然,这样的实现并不优雅,并且从概念上来说"面积"是"形状"的一个属性,它是属于这个特定的形状的,就像长方形的长和宽一样。 |
||||
|
|
||||
|
基于上面的原因所以就有了`method`的概念,`method`是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在`func`后面增加了一个receiver(也就是method所依从的主体)。 |
||||
|
|
||||
|
用上面提到的形状的例子来说,method `area()` 是依赖于某个形状(比如说Rectangle)来发生作用的。Rectangle.area()的发出者是Rectangle, area()是属于Rectangle的方法,而非一个外围函数。 |
||||
|
|
||||
|
更具体地说,Rectangle存在字段 height 和 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来实现: |
||||
|
```Go |
||||
|
|
||||
|
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里面访问字段一样 |
||||
|
|
||||
|
图示如下: |
||||
|
|
||||
|
![](images/2.5.shapes_func_with_receiver_cp.png?raw=true) |
||||
|
|
||||
|
图2.9 不同struct的method不同 |
||||
|
|
||||
|
在上例,method area() 分别属于Rectangle和Circle, 于是他们的 Receiver 就变成了Rectangle 和 Circle, 或者说,这个area()方法 是由 Rectangle/Circle 发出的。 |
||||
|
|
||||
|
>值得说明的一点是,图示中method用虚线标出,意思是此处方法的Receiver是以值传递,而非引用传递,是的,Receiver还可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。后文对此会有详细论述。 |
||||
|
|
||||
|
那是不是method只能作用在struct上面呢?当然不是咯,他可以定义在任何你自定义的类型、内置类型、struct等各种类型上面。这里你是不是有点迷糊了,什么叫自定义类型,自定义类型不就是struct嘛,不是这样的哦,struct只是自定义类型里面一种比较特殊的类型而已,还有其他自定义类型申明,可以通过如下这样的申明来实现。 |
||||
|
```Go |
||||
|
|
||||
|
type typeName typeLiteral |
||||
|
``` |
||||
|
请看下面这个申明自定义类型的代码 |
||||
|
```Go |
||||
|
|
||||
|
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`,接下来让我们看一个复杂一点的例子 |
||||
|
```Go |
||||
|
|
||||
|
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。让我们来看下面这个例子 |
||||
|
```Go |
||||
|
|
||||
|
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,重写了匿名字段的方法。请看下面的例子 |
||||
|
```Go |
||||
|
|
||||
|
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>) |
@ -0,0 +1,414 @@ |
|||||
|
# 2.6 interface |
||||
|
|
||||
|
## interface |
||||
|
Go语言里面设计最精妙的应该算interface,它让面向对象,内容组织实现非常的方便,当你看完这一章,你就会被interface的巧妙设计所折服。 |
||||
|
### 什么是interface |
||||
|
简单的说,interface是一组method签名的组合,我们通过interface来定义对象的一组行为。 |
||||
|
|
||||
|
我们前面一章最后一个例子中Student和Employee都能SayHi,虽然他们的内部实现不一样,但是那不重要,重要的是他们都能`say hi` |
||||
|
|
||||
|
让我们来继续做更多的扩展,Student和Employee实现另一个方法`Sing`,然后Student实现方法BorrowMoney而Employee实现SpendSalary。 |
||||
|
|
||||
|
这样Student实现了三个方法:SayHi、Sing、BorrowMoney;而Employee实现了SayHi、Sing、SpendSalary。 |
||||
|
|
||||
|
上面这些方法的组合称为interface(被对象Student和Employee实现)。例如Student和Employee都实现了interface:SayHi和Sing,也就是这两个对象是该interface类型。而Employee没有实现这个interface:SayHi、Sing和BorrowMoney,因为Employee没有实现BorrowMoney这个方法。 |
||||
|
### interface类型 |
||||
|
interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。详细的语法参考下面这个例子 |
||||
|
```Go |
||||
|
|
||||
|
type Human struct { |
||||
|
name string |
||||
|
age int |
||||
|
phone string |
||||
|
} |
||||
|
|
||||
|
type Student struct { |
||||
|
Human //匿名字段Human |
||||
|
school string |
||||
|
loan float32 |
||||
|
} |
||||
|
|
||||
|
type Employee struct { |
||||
|
Human //匿名字段Human |
||||
|
company string |
||||
|
money float32 |
||||
|
} |
||||
|
|
||||
|
//Human对象实现Sayhi方法 |
||||
|
func (h *Human) SayHi() { |
||||
|
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) |
||||
|
} |
||||
|
|
||||
|
// Human对象实现Sing方法 |
||||
|
func (h *Human) Sing(lyrics string) { |
||||
|
fmt.Println("La la, la la la, la la la la la...", lyrics) |
||||
|
} |
||||
|
|
||||
|
//Human对象实现Guzzle方法 |
||||
|
func (h *Human) Guzzle(beerStein string) { |
||||
|
fmt.Println("Guzzle Guzzle Guzzle...", beerStein) |
||||
|
} |
||||
|
|
||||
|
// Employee重载Human的Sayhi方法 |
||||
|
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) //此句可以分成多行 |
||||
|
} |
||||
|
|
||||
|
//Student实现BorrowMoney方法 |
||||
|
func (s *Student) BorrowMoney(amount float32) { |
||||
|
s.loan += amount // (again and again and...) |
||||
|
} |
||||
|
|
||||
|
//Employee实现SpendSalary方法 |
||||
|
func (e *Employee) SpendSalary(amount float32) { |
||||
|
e.money -= amount // More vodka please!!! Get me through the day! |
||||
|
} |
||||
|
|
||||
|
// 定义interface |
||||
|
type Men interface { |
||||
|
SayHi() |
||||
|
Sing(lyrics string) |
||||
|
Guzzle(beerStein string) |
||||
|
} |
||||
|
|
||||
|
type YoungChap interface { |
||||
|
SayHi() |
||||
|
Sing(song string) |
||||
|
BorrowMoney(amount float32) |
||||
|
} |
||||
|
|
||||
|
type ElderlyGent interface { |
||||
|
SayHi() |
||||
|
Sing(song string) |
||||
|
SpendSalary(amount float32) |
||||
|
} |
||||
|
``` |
||||
|
通过上面的代码我们可以知道,interface可以被任意的对象实现。我们看到上面的Men interface被Human、Student和Employee实现。同理,一个对象可以实现任意多个interface,例如上面的Student实现了Men和YoungChap两个interface。 |
||||
|
|
||||
|
最后,任意的类型都实现了空interface(我们这样定义:interface{}),也就是包含0个method的interface。 |
||||
|
|
||||
|
### interface值 |
||||
|
那么interface里面到底能存什么值呢?如果我们定义了一个interface的变量,那么这个变量里面可以存实现这个interface的任意类型的对象。例如上面例子中,我们定义了一个Men interface类型的变量m,那么m里面可以存Human、Student或者Employee值。 |
||||
|
|
||||
|
因为m能够持有这三种类型的对象,所以我们可以定义一个包含Men类型元素的slice,这个slice可以被赋予实现了Men接口的任意结构的对象,这个和我们传统意义上面的slice有所不同。 |
||||
|
|
||||
|
让我们来看一下下面这个例子: |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
type Human struct { |
||||
|
name string |
||||
|
age int |
||||
|
phone string |
||||
|
} |
||||
|
|
||||
|
type Student struct { |
||||
|
Human //匿名字段 |
||||
|
school string |
||||
|
loan float32 |
||||
|
} |
||||
|
|
||||
|
type Employee struct { |
||||
|
Human //匿名字段 |
||||
|
company string |
||||
|
money float32 |
||||
|
} |
||||
|
|
||||
|
//Human实现SayHi方法 |
||||
|
func (h Human) SayHi() { |
||||
|
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) |
||||
|
} |
||||
|
|
||||
|
//Human实现Sing方法 |
||||
|
func (h Human) Sing(lyrics string) { |
||||
|
fmt.Println("La la la la...", lyrics) |
||||
|
} |
||||
|
|
||||
|
//Employee重载Human的SayHi方法 |
||||
|
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) |
||||
|
} |
||||
|
|
||||
|
// Interface Men被Human,Student和Employee实现 |
||||
|
// 因为这三个类型都实现了这两个方法 |
||||
|
type Men interface { |
||||
|
SayHi() |
||||
|
Sing(lyrics string) |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00} |
||||
|
paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100} |
||||
|
sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000} |
||||
|
tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000} |
||||
|
|
||||
|
//定义Men类型的变量i |
||||
|
var i Men |
||||
|
|
||||
|
//i能存储Student |
||||
|
i = mike |
||||
|
fmt.Println("This is Mike, a Student:") |
||||
|
i.SayHi() |
||||
|
i.Sing("November rain") |
||||
|
|
||||
|
//i也能存储Employee |
||||
|
i = tom |
||||
|
fmt.Println("This is tom, an Employee:") |
||||
|
i.SayHi() |
||||
|
i.Sing("Born to be wild") |
||||
|
|
||||
|
//定义了slice Men |
||||
|
fmt.Println("Let's use a slice of Men and see what happens") |
||||
|
x := make([]Men, 3) |
||||
|
//这三个都是不同类型的元素,但是他们实现了interface同一个接口 |
||||
|
x[0], x[1], x[2] = paul, sam, mike |
||||
|
|
||||
|
for _, value := range x{ |
||||
|
value.SayHi() |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
通过上面的代码,你会发现interface就是一组抽象方法的集合,它必须由其他非interface类型实现,而不能自我实现, Go通过interface实现了duck-typing:即"当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子"。 |
||||
|
|
||||
|
### 空interface |
||||
|
空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。 |
||||
|
```Go |
||||
|
|
||||
|
// 定义a为空接口 |
||||
|
var a interface{} |
||||
|
var i int = 5 |
||||
|
s := "Hello world" |
||||
|
// a可以存储任意类型的数值 |
||||
|
a = i |
||||
|
a = s |
||||
|
``` |
||||
|
一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回interface{},那么也就可以返回任意类型的值。是不是很有用啊! |
||||
|
### interface函数参数 |
||||
|
interface的变量可以持有任意实现该interface类型的对象,这给我们编写函数(包括method)提供了一些额外的思考,我们是不是可以通过定义interface参数,让函数接受各种类型的参数。 |
||||
|
|
||||
|
举个例子:fmt.Println是我们常用的一个函数,但是你是否注意到它可以接受任意类型的数据。打开fmt的源码文件,你会看到这样一个定义: |
||||
|
```Go |
||||
|
|
||||
|
type Stringer interface { |
||||
|
String() string |
||||
|
} |
||||
|
``` |
||||
|
也就是说,任何实现了String方法的类型都能作为参数被fmt.Println调用,让我们来试一试 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
import ( |
||||
|
"fmt" |
||||
|
"strconv" |
||||
|
) |
||||
|
|
||||
|
type Human struct { |
||||
|
name string |
||||
|
age int |
||||
|
phone string |
||||
|
} |
||||
|
|
||||
|
// 通过这个方法 Human 实现了 fmt.Stringer |
||||
|
func (h Human) String() string { |
||||
|
return "❰"+h.name+" - "+strconv.Itoa(h.age)+" years - ✆ " +h.phone+"❱" |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
Bob := Human{"Bob", 39, "000-7777-XXX"} |
||||
|
fmt.Println("This Human is : ", Bob) |
||||
|
} |
||||
|
``` |
||||
|
现在我们再回顾一下前面的Box示例,你会发现Color结构也定义了一个method:String。其实这也是实现了fmt.Stringer这个interface,即如果需要某个类型能被fmt包以特殊的格式输出,你就必须实现Stringer这个接口。如果没有实现这个接口,fmt将以默认的方式输出。 |
||||
|
```Go |
||||
|
|
||||
|
//实现同样的功能 |
||||
|
fmt.Println("The biggest one is", boxes.BiggestsColor().String()) |
||||
|
fmt.Println("The biggest one is", boxes.BiggestsColor()) |
||||
|
``` |
||||
|
注:实现了error接口的对象(即实现了Error() string的对象),使用fmt输出时,会调用Error()方法,因此不必再定义String()方法了。 |
||||
|
### interface变量存储的类型 |
||||
|
|
||||
|
我们知道interface的变量里面可以存储任意类型的数值(该类型实现了interface)。那么我们怎么反向知道这个变量里面实际保存了的是哪个类型的对象呢?目前常用的有两种方法: |
||||
|
|
||||
|
- Comma-ok断言 |
||||
|
|
||||
|
Go语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。 |
||||
|
|
||||
|
如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false。 |
||||
|
|
||||
|
让我们通过一个例子来更加深入的理解。 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"strconv" |
||||
|
) |
||||
|
|
||||
|
type Element interface{} |
||||
|
type List [] Element |
||||
|
|
||||
|
type Person struct { |
||||
|
name string |
||||
|
age int |
||||
|
} |
||||
|
|
||||
|
//定义了String方法,实现了fmt.Stringer |
||||
|
func (p Person) String() string { |
||||
|
return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)" |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
list := make(List, 3) |
||||
|
list[0] = 1 // an int |
||||
|
list[1] = "Hello" // a string |
||||
|
list[2] = Person{"Dennis", 70} |
||||
|
|
||||
|
for index, element := range list { |
||||
|
if value, ok := element.(int); ok { |
||||
|
fmt.Printf("list[%d] is an int and its value is %d\n", index, value) |
||||
|
} else if value, ok := element.(string); ok { |
||||
|
fmt.Printf("list[%d] is a string and its value is %s\n", index, value) |
||||
|
} else if value, ok := element.(Person); ok { |
||||
|
fmt.Printf("list[%d] is a Person and its value is %s\n", index, value) |
||||
|
} else { |
||||
|
fmt.Printf("list[%d] is of a different type\n", index) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
是不是很简单啊,同时你是否注意到了多个if里面,还记得我前面介绍流程时讲过,if里面允许初始化变量。 |
||||
|
|
||||
|
也许你注意到了,我们断言的类型越多,那么if else也就越多,所以才引出了下面要介绍的switch。 |
||||
|
- switch测试 |
||||
|
|
||||
|
最好的讲解就是代码例子,现在让我们重写上面的这个实现 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"strconv" |
||||
|
) |
||||
|
|
||||
|
type Element interface{} |
||||
|
type List [] Element |
||||
|
|
||||
|
type Person struct { |
||||
|
name string |
||||
|
age int |
||||
|
} |
||||
|
|
||||
|
//打印 |
||||
|
func (p Person) String() string { |
||||
|
return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)" |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
list := make(List, 3) |
||||
|
list[0] = 1 //an int |
||||
|
list[1] = "Hello" //a string |
||||
|
list[2] = Person{"Dennis", 70} |
||||
|
|
||||
|
for index, element := range list{ |
||||
|
switch value := element.(type) { |
||||
|
case int: |
||||
|
fmt.Printf("list[%d] is an int and its value is %d\n", index, value) |
||||
|
case string: |
||||
|
fmt.Printf("list[%d] is a string and its value is %s\n", index, value) |
||||
|
case Person: |
||||
|
fmt.Printf("list[%d] is a Person and its value is %s\n", index, value) |
||||
|
default: |
||||
|
fmt.Println("list[%d] is of a different type", index) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
这里有一点需要强调的是:`element.(type)`语法不能在switch外的任何逻辑里面使用,如果你要在switch外面判断一个类型就使用`comma-ok`。 |
||||
|
|
||||
|
### 嵌入interface |
||||
|
Go里面真正吸引人的是它内置的逻辑语法,就像我们在学习Struct时学习的匿名字段,多么的优雅啊,那么相同的逻辑引入到interface里面,那不是更加完美了。如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。 |
||||
|
|
||||
|
我们可以看到源码包container/heap里面有这样的一个定义 |
||||
|
```Go |
||||
|
|
||||
|
type Interface interface { |
||||
|
sort.Interface //嵌入字段sort.Interface |
||||
|
Push(x interface{}) //a Push method to push elements into the heap |
||||
|
Pop() interface{} //a Pop elements that pops elements from the heap |
||||
|
} |
||||
|
``` |
||||
|
我们看到sort.Interface其实就是嵌入字段,把sort.Interface的所有method给隐式的包含进来了。也就是下面三个方法: |
||||
|
```Go |
||||
|
|
||||
|
type Interface interface { |
||||
|
// Len is the number of elements in the collection. |
||||
|
Len() int |
||||
|
// Less returns whether the element with index i should sort |
||||
|
// before the element with index j. |
||||
|
Less(i, j int) bool |
||||
|
// Swap swaps the elements with indexes i and j. |
||||
|
Swap(i, j int) |
||||
|
} |
||||
|
``` |
||||
|
另一个例子就是io包下面的 io.ReadWriter ,它包含了io包下面的Reader和Writer两个interface: |
||||
|
```Go |
||||
|
|
||||
|
// io.ReadWriter |
||||
|
type ReadWriter interface { |
||||
|
Reader |
||||
|
Writer |
||||
|
} |
||||
|
``` |
||||
|
### 反射 |
||||
|
Go语言实现了反射,所谓反射就是能检查程序在运行时的状态。我们一般用到的包是reflect包。如何运用reflect包,官方的这篇文章详细的讲解了reflect包的实现原理,[laws of reflection](http://golang.org/doc/articles/laws_of_reflection.html) |
||||
|
|
||||
|
使用reflect一般分成三步,下面简要的讲解一下:要去反射是一个类型的值(这些值都实现了空interface),首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数)。这两种获取方式如下: |
||||
|
```Go |
||||
|
|
||||
|
t := reflect.TypeOf(i) //得到类型的元数据,通过t我们能获取类型定义里面的所有元素 |
||||
|
v := reflect.ValueOf(i) //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值 |
||||
|
``` |
||||
|
转化为reflect对象之后我们就可以进行一些操作了,也就是将reflect对象转化成相应的值,例如 |
||||
|
```Go |
||||
|
|
||||
|
tag := t.Elem().Field(0).Tag //获取定义在struct里面的标签 |
||||
|
name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值 |
||||
|
``` |
||||
|
获取反射值能返回相应的类型和数值 |
||||
|
```Go |
||||
|
|
||||
|
var x float64 = 3.4 |
||||
|
v := reflect.ValueOf(x) |
||||
|
fmt.Println("type:", v.Type()) |
||||
|
fmt.Println("kind is float64:", v.Kind() == reflect.Float64) |
||||
|
fmt.Println("value:", v.Float()) |
||||
|
``` |
||||
|
最后,反射的话,那么反射的字段必须是可修改的,我们前面学习过传值和传引用,这个里面也是一样的道理。反射的字段必须是可读写的意思是,如果下面这样写,那么会发生错误 |
||||
|
```Go |
||||
|
|
||||
|
var x float64 = 3.4 |
||||
|
v := reflect.ValueOf(x) |
||||
|
v.SetFloat(7.1) |
||||
|
``` |
||||
|
如果要修改相应的值,必须这样写 |
||||
|
```Go |
||||
|
|
||||
|
var x float64 = 3.4 |
||||
|
p := reflect.ValueOf(&x) |
||||
|
v := p.Elem() |
||||
|
v.SetFloat(7.1) |
||||
|
``` |
||||
|
上面只是对反射的简单介绍,更深入的理解还需要自己在编程中不断的实践。 |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一章: [面向对象](<02.5.md>) |
||||
|
* 下一节: [并发](<02.7.md>) |
@ -0,0 +1,32 @@ |
|||||
|
# 2.8 总结 |
||||
|
|
||||
|
这一章我们主要介绍了Go语言的一些语法,通过语法我们可以发现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>) |
@ -0,0 +1,11 @@ |
|||||
|
# 3 Web基础 |
||||
|
|
||||
|
学习基于Web的编程可能正是你读本书的原因。事实上,如何通过Go来编写Web应用也是我编写这本书的初衷。前面已经介绍过,Go目前已经拥有了成熟的HTTP处理包,这使得编写能做任何事情的动态Web程序易如反掌。在接下来的各章中将要介绍的内容,都是属于Web编程的范畴。本章则集中讨论一些与Web相关的概念和Go如何运行Web程序的话题。 |
||||
|
|
||||
|
## 目录 |
||||
|
![](images/navi3.png?raw=true) |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一章: [第二章总结](<02.8.md>) |
||||
|
* 下一节: [Web工作方式](<03.1.md>) |
@ -0,0 +1,68 @@ |
|||||
|
# 3.2 Go搭建一个Web服务器 |
||||
|
|
||||
|
前面小节已经介绍了Web是基于http协议的一个服务,Go语言里面提供了一个完善的net/http包,通过http包可以很方便的搭建起来一个可以运行的Web服务。同时使用这个包能很简单地对Web的路由,静态文件,模版,cookie等数据进行设置和操作。 |
||||
|
|
||||
|
## http包建立Web服务器 |
||||
|
```Go |
||||
|
|
||||
|
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` |
||||
|
|
||||
|
看看浏览器输出的是什么,服务器输出的是什么? |
||||
|
|
||||
|
在服务器端输出的信息如下: |
||||
|
|
||||
|
![](images/3.2.goweb.png?raw=true) |
||||
|
|
||||
|
图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>) |
@ -0,0 +1,89 @@ |
|||||
|
# 3.3 Go如何使得Web工作 |
||||
|
前面小节介绍了如何通过Go搭建一个Web服务,我们可以看到简单应用一个net/http包就方便的搭建起来了。那么Go在底层到底是怎么做的呢?万变不离其宗,Go的Web服务工作也离不开我们第一小节介绍的Web工作方式。 |
||||
|
|
||||
|
## web工作方式的几个概念 |
||||
|
|
||||
|
以下均是服务器端的几个概念 |
||||
|
|
||||
|
Request:用户请求的信息,用来解析用户的请求信息,包括post、get、cookie、url等信息 |
||||
|
|
||||
|
Response:服务器需要反馈给客户端的信息 |
||||
|
|
||||
|
Conn:用户的每次请求链接 |
||||
|
|
||||
|
Handler:处理请求和生成返回信息的处理逻辑 |
||||
|
|
||||
|
## 分析http包运行机制 |
||||
|
|
||||
|
下图是Go实现Web服务的工作模式的流程图 |
||||
|
|
||||
|
![](images/3.3.http.png?raw=true) |
||||
|
|
||||
|
图3.9 http包执行流程 |
||||
|
|
||||
|
1. 创建Listen Socket, 监听指定的端口, 等待客户端请求到来。 |
||||
|
|
||||
|
2. Listen Socket接受客户端的请求, 得到Client Socket, 接下来通过Client Socket与客户端通信。 |
||||
|
|
||||
|
3. 处理客户端的请求, 首先从Client Socket读取HTTP请求的协议头, 如果是POST方法, 还可能要读取客户端提交的数据, 然后交给相应的handler处理请求, handler处理完毕准备好客户端需要的数据, 通过Client Socket写给客户端。 |
||||
|
|
||||
|
这整个的过程里面我们只要了解清楚下面三个问题,也就知道Go是如何让Web运行起来了 |
||||
|
|
||||
|
- 如何监听端口? |
||||
|
- 如何接收客户端请求? |
||||
|
- 如何分配handler? |
||||
|
|
||||
|
前面小节的代码里面我们可以看到,Go是通过一个函数`ListenAndServe`来处理这些事情的,这个底层其实这样处理的:初始化一个server对象,然后调用了`net.Listen("tcp", addr)`,也就是底层用TCP协议搭建了一个服务,然后监控我们设置的端口。 |
||||
|
|
||||
|
下面代码来自Go的http包的源码,通过下面的代码我们可以看到整个的http处理过程: |
||||
|
```Go |
||||
|
|
||||
|
func (srv *Server) Serve(l net.Listener) error { |
||||
|
defer l.Close() |
||||
|
var tempDelay time.Duration // how long to sleep on accept failure |
||||
|
for { |
||||
|
rw, e := l.Accept() |
||||
|
if e != nil { |
||||
|
if ne, ok := e.(net.Error); ok && ne.Temporary() { |
||||
|
if tempDelay == 0 { |
||||
|
tempDelay = 5 * time.Millisecond |
||||
|
} else { |
||||
|
tempDelay *= 2 |
||||
|
} |
||||
|
if max := 1 * time.Second; tempDelay > max { |
||||
|
tempDelay = max |
||||
|
} |
||||
|
log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay) |
||||
|
time.Sleep(tempDelay) |
||||
|
continue |
||||
|
} |
||||
|
return e |
||||
|
} |
||||
|
tempDelay = 0 |
||||
|
c, err := srv.newConn(rw) |
||||
|
if err != nil { |
||||
|
continue |
||||
|
} |
||||
|
go c.serve() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
监控之后如何接收客户端的请求呢?上面代码执行监控端口之后,调用了`srv.Serve(net.Listener)`函数,这个函数就是处理接收客户端的请求信息。这个函数里面起了一个`for{}`,首先通过Listener接收请求,其次创建一个Conn,最后单独开了一个goroutine,把这个请求的数据当做参数扔给这个conn去服务:`go c.serve()`。这个就是高并发体现了,用户的每一次请求都是在一个新的goroutine去服务,相互不影响。 |
||||
|
|
||||
|
那么如何具体分配到相应的函数来处理请求呢?conn首先会解析request:`c.readRequest()`,然后获取相应的handler:`handler := c.server.Handler`,也就是我们刚才在调用函数`ListenAndServe`时候的第二个参数,我们前面例子传递的是nil,也就是为空,那么默认获取`handler = DefaultServeMux`,那么这个变量用来做什么的呢?对,这个变量就是一个路由器,它用来匹配url跳转到其相应的handle函数,那么这个我们有设置过吗?有,我们调用的代码里面第一句不是调用了`http.HandleFunc("/", sayhelloName)`嘛。这个作用就是注册了请求`/`的路由规则,当请求uri为"/",路由就会转到函数sayhelloName,DefaultServeMux会调用ServeHTTP方法,这个方法内部其实就是调用sayhelloName本身,最后通过写入response的信息反馈到客户端。 |
||||
|
|
||||
|
|
||||
|
详细的整个流程如下图所示: |
||||
|
|
||||
|
![](images/3.3.illustrator.png?raw=true) |
||||
|
|
||||
|
图3.10 一个http连接处理流程 |
||||
|
|
||||
|
至此我们的三个问题已经全部得到了解答,你现在对于Go如何让Web跑起来的是否已经基本了解了呢? |
||||
|
|
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [GO搭建一个简单的web服务](<03.2.md>) |
||||
|
* 下一节: [Go的http包详解](<03.4.md>) |
@ -0,0 +1,193 @@ |
|||||
|
# 3.4 Go的http包详解 |
||||
|
前面小节介绍了Go怎么样实现了Web工作模式的一个流程,这一小节,我们将详细地解剖一下http包,看它到底是怎样实现整个过程的。 |
||||
|
|
||||
|
Go的http有两个核心功能:Conn、ServeMux |
||||
|
|
||||
|
## Conn的goroutine |
||||
|
与我们一般编写的http服务器不同, Go为了实现高并发和高性能, 使用了goroutines来处理Conn的读写事件, 这样每个请求都能保持独立,相互不会阻塞,可以高效的响应网络事件。这是Go高效的保证。 |
||||
|
|
||||
|
Go在等待客户端请求里面是这样写的: |
||||
|
```Go |
||||
|
|
||||
|
c, err := srv.newConn(rw) |
||||
|
if err != nil { |
||||
|
continue |
||||
|
} |
||||
|
go c.serve() |
||||
|
|
||||
|
``` |
||||
|
这里我们可以看到客户端的每次请求都会创建一个Conn,这个Conn里面保存了该次请求的信息,然后再传递到对应的handler,该handler中便可以读取到相应的header信息,这样保证了每个请求的独立性。 |
||||
|
|
||||
|
## ServeMux的自定义 |
||||
|
我们前面小节讲述conn.server的时候,其实内部是调用了http包默认的路由器,通过路由器把本次请求的信息传递到了后端的处理函数。那么这个路由器是怎么实现的呢? |
||||
|
|
||||
|
它的结构如下: |
||||
|
```Go |
||||
|
|
||||
|
type ServeMux struct { |
||||
|
mu sync.RWMutex //锁,由于请求涉及到并发处理,因此这里需要一个锁机制 |
||||
|
m map[string]muxEntry // 路由规则,一个string对应一个mux实体,这里的string就是注册的路由表达式 |
||||
|
hosts bool // 是否在任意的规则中带有host信息 |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
下面看一下muxEntry |
||||
|
```Go |
||||
|
|
||||
|
type muxEntry struct { |
||||
|
explicit bool // 是否精确匹配 |
||||
|
h Handler // 这个路由表达式对应哪个handler |
||||
|
pattern string //匹配字符串 |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
接着看一下Handler的定义 |
||||
|
```Go |
||||
|
|
||||
|
type Handler interface { |
||||
|
ServeHTTP(ResponseWriter, *Request) // 路由实现器 |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
Handler是一个接口,但是前一小节中的`sayhelloName`函数并没有实现ServeHTTP这个接口,为什么能添加呢?原来在http包里面还定义了一个类型`HandlerFunc`,我们定义的函数`sayhelloName`就是这个HandlerFunc调用之后的结果,这个类型默认就实现了ServeHTTP这个接口,即我们调用了HandlerFunc(f),强制类型转换f成为HandlerFunc类型,这样f就拥有了ServeHTTP方法。 |
||||
|
```Go |
||||
|
|
||||
|
type HandlerFunc func(ResponseWriter, *Request) |
||||
|
|
||||
|
// ServeHTTP calls f(w, r). |
||||
|
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { |
||||
|
f(w, r) |
||||
|
} |
||||
|
``` |
||||
|
路由器里面存储好了相应的路由规则之后,那么具体的请求又是怎么分发的呢?请看下面的代码,默认的路由器实现了`ServeHTTP`: |
||||
|
```Go |
||||
|
|
||||
|
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { |
||||
|
if r.RequestURI == "*" { |
||||
|
w.Header().Set("Connection", "close") |
||||
|
w.WriteHeader(StatusBadRequest) |
||||
|
return |
||||
|
} |
||||
|
h, _ := mux.Handler(r) |
||||
|
h.ServeHTTP(w, r) |
||||
|
} |
||||
|
``` |
||||
|
如上所示路由器接收到请求之后,如果是`*`那么关闭链接,不然调用`mux.Handler(r)`返回对应设置路由的处理Handler,然后执行`h.ServeHTTP(w, r)` |
||||
|
|
||||
|
也就是调用对应路由的handler的ServerHTTP接口,那么mux.Handler(r)怎么处理的呢? |
||||
|
```Go |
||||
|
|
||||
|
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { |
||||
|
if r.Method != "CONNECT" { |
||||
|
if p := cleanPath(r.URL.Path); p != r.URL.Path { |
||||
|
_, pattern = mux.handler(r.Host, p) |
||||
|
return RedirectHandler(p, StatusMovedPermanently), pattern |
||||
|
} |
||||
|
} |
||||
|
return mux.handler(r.Host, r.URL.Path) |
||||
|
} |
||||
|
|
||||
|
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) { |
||||
|
mux.mu.RLock() |
||||
|
defer mux.mu.RUnlock() |
||||
|
|
||||
|
// Host-specific pattern takes precedence over generic ones |
||||
|
if mux.hosts { |
||||
|
h, pattern = mux.match(host + path) |
||||
|
} |
||||
|
if h == nil { |
||||
|
h, pattern = mux.match(path) |
||||
|
} |
||||
|
if h == nil { |
||||
|
h, pattern = NotFoundHandler(), "" |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
``` |
||||
|
原来他是根据用户请求的URL和路由器里面存储的map去匹配的,当匹配到之后返回存储的handler,调用这个handler的ServeHTTP接口就可以执行到相应的函数了。 |
||||
|
|
||||
|
通过上面这个介绍,我们了解了整个路由过程,Go其实支持外部实现的路由器 `ListenAndServe`的第二个参数就是用以配置外部路由器的,它是一个Handler接口,即外部路由器只要实现了Handler接口就可以,我们可以在自己实现的路由器的ServeHTTP里面实现自定义路由功能。 |
||||
|
|
||||
|
如下代码所示,我们自己实现了一个简易的路由器 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"net/http" |
||||
|
) |
||||
|
|
||||
|
type MyMux struct { |
||||
|
} |
||||
|
|
||||
|
func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
||||
|
if r.URL.Path == "/" { |
||||
|
sayhelloName(w, r) |
||||
|
return |
||||
|
} |
||||
|
http.NotFound(w, r) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func sayhelloName(w http.ResponseWriter, r *http.Request) { |
||||
|
fmt.Fprintf(w, "Hello myroute!") |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
mux := &MyMux{} |
||||
|
http.ListenAndServe(":9090", mux) |
||||
|
} |
||||
|
``` |
||||
|
## Go代码的执行流程 |
||||
|
|
||||
|
通过对http包的分析之后,现在让我们来梳理一下整个的代码执行过程。 |
||||
|
|
||||
|
- 首先调用Http.HandleFunc |
||||
|
|
||||
|
按顺序做了几件事: |
||||
|
|
||||
|
1 调用了DefaultServeMux的HandleFunc |
||||
|
|
||||
|
2 调用了DefaultServeMux的Handle |
||||
|
|
||||
|
3 往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则 |
||||
|
|
||||
|
- 其次调用http.ListenAndServe(":9090", nil) |
||||
|
|
||||
|
按顺序做了几件事情: |
||||
|
|
||||
|
1 实例化Server |
||||
|
|
||||
|
2 调用Server的ListenAndServe() |
||||
|
|
||||
|
3 调用net.Listen("tcp", addr)监听端口 |
||||
|
|
||||
|
4 启动一个for循环,在循环体中Accept请求 |
||||
|
|
||||
|
5 对每个请求实例化一个Conn,并且开启一个goroutine为这个请求进行服务go c.serve() |
||||
|
|
||||
|
6 读取每个请求的内容w, err := c.readRequest() |
||||
|
|
||||
|
7 判断handler是否为空,如果没有设置handler(这个例子就没有设置handler),handler就设置为DefaultServeMux |
||||
|
|
||||
|
8 调用handler的ServeHttp |
||||
|
|
||||
|
9 在这个例子中,下面就进入到DefaultServeMux.ServeHttp |
||||
|
|
||||
|
10 根据request选择handler,并且进入到这个handler的ServeHTTP |
||||
|
|
||||
|
mux.handler(r).ServeHTTP(w, r) |
||||
|
|
||||
|
11 选择handler: |
||||
|
|
||||
|
A 判断是否有路由能满足这个request(循环遍历ServeMux的muxEntry) |
||||
|
|
||||
|
B 如果有路由满足,调用这个路由handler的ServeHTTP |
||||
|
|
||||
|
C 如果没有路由满足,调用NotFoundHandler的ServeHTTP |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [Go如何使得web工作](<03.3.md>) |
||||
|
* 下一节: [小结](<03.5.md>) |
@ -0,0 +1,9 @@ |
|||||
|
# 3.5 小结 |
||||
|
这一章我们介绍了HTTP协议, DNS解析的过程, 如何用go实现一个简陋的web server。并深入到net/http包的源码中为大家揭开实现此server的秘密。 |
||||
|
|
||||
|
希望通过这一章的学习,你能够对Go开发Web有了初步的了解,我们也看到相应的代码了,Go开发Web应用是很方便的,同时又是相当的灵活。 |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [Go的http包详解](<03.4.md>) |
||||
|
* 下一章: [表单](<04.0.md>) |
@ -0,0 +1,180 @@ |
|||||
|
# 4.2 验证表单的输入 |
||||
|
|
||||
|
开发Web的一个原则就是,不能信任用户输入的任何信息,所以验证和过滤用户的输入信息就变得非常重要,我们经常会在微博、新闻中听到某某网站被入侵了,存在什么漏洞,这些大多是因为网站对于用户输入的信息没有做严格的验证引起的,所以为了编写出安全可靠的Web程序,验证表单输入的意义重大。 |
||||
|
|
||||
|
我们平常编写Web应用主要有两方面的数据验证,一个是在页面端的js验证(目前在这方面有很多的插件库,比如ValidationJS插件),一个是在服务器端的验证,我们这小节讲解的是如何在服务器端验证。 |
||||
|
|
||||
|
## 必填字段 |
||||
|
你想要确保从一个表单元素中得到一个值,例如前面小节里面的用户名,我们如何处理呢?Go有一个内置函数`len`可以获取字符串的长度,这样我们就可以通过len来获取数据的长度,例如: |
||||
|
```Go |
||||
|
|
||||
|
if len(r.Form["username"][0])==0{ |
||||
|
//为空的处理 |
||||
|
} |
||||
|
``` |
||||
|
`r.Form`对不同类型的表单元素的留空有不同的处理, 对于空文本框、空文本区域以及文件上传,元素的值为空值,而如果是未选中的复选框和单选按钮,则根本不会在r.Form中产生相应条目,如果我们用上面例子中的方式去获取数据时程序就会报错。所以我们需要通过`r.Form.Get()`来获取值,因为如果字段不存在,通过该方式获取的是空值。但是通过`r.Form.Get()`只能获取单个的值,如果是map的值,必须通过上面的方式来获取。 |
||||
|
|
||||
|
## 数字 |
||||
|
你想要确保一个表单输入框中获取的只能是数字,例如,你想通过表单获取某个人的具体年龄是50岁还是10岁,而不是像“一把年纪了”或“年轻着呢”这种描述 |
||||
|
|
||||
|
如果我们是判断正整数,那么我们先转化成int类型,然后进行处理 |
||||
|
```Go |
||||
|
|
||||
|
getint,err:=strconv.Atoi(r.Form.Get("age")) |
||||
|
if err!=nil{ |
||||
|
//数字转化出错了,那么可能就不是数字 |
||||
|
} |
||||
|
|
||||
|
//接下来就可以判断这个数字的大小范围了 |
||||
|
if getint >100 { |
||||
|
//太大了 |
||||
|
} |
||||
|
``` |
||||
|
还有一种方式就是正则匹配的方式 |
||||
|
```Go |
||||
|
|
||||
|
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` 来验证,也可以使用正则方式来验证,这里使用最简单的正则方式,如下代码所示 |
||||
|
```Go |
||||
|
|
||||
|
if m, _ := regexp.MatchString("^\\p{Han}+$", r.Form.Get("realname")); !m { |
||||
|
return false |
||||
|
} |
||||
|
``` |
||||
|
## 英文 |
||||
|
我们期望通过表单元素获取一个英文值,例如我们想知道一个用户的英文名,应该是astaxie,而不是asta谢。 |
||||
|
|
||||
|
我们可以很简单的通过正则验证数据: |
||||
|
```Go |
||||
|
|
||||
|
if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
## 电子邮件地址 |
||||
|
你想知道用户输入的一个Email地址是否正确,通过如下这个方式可以验证: |
||||
|
```Go |
||||
|
|
||||
|
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") |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
## 手机号码 |
||||
|
你想要判断用户输入的手机号码是否正确,通过正则也可以验证: |
||||
|
```Go |
||||
|
|
||||
|
if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m { |
||||
|
return false |
||||
|
} |
||||
|
``` |
||||
|
## 下拉菜单 |
||||
|
如果我们想要判断表单里面`<select>`元素生成的下拉菜单中是否有被选中的项目。有些时候黑客可能会伪造这个下拉菜单不存在的值发送给你,那么如何判断这个值是否是我们预设的值呢? |
||||
|
|
||||
|
我们的select可能是这样的一些元素 |
||||
|
```html |
||||
|
|
||||
|
<select name="fruit"> |
||||
|
<option value="apple">apple</option> |
||||
|
<option value="pear">pear</option> |
||||
|
<option value="banana">banana</option> |
||||
|
</select> |
||||
|
``` |
||||
|
那么我们可以这样来验证 |
||||
|
```Go |
||||
|
|
||||
|
slice:=[]string{"apple","pear","banana"} |
||||
|
|
||||
|
v := r.Form.Get("fruit") |
||||
|
for _, item := range slice { |
||||
|
if item == v { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false |
||||
|
``` |
||||
|
## 单选按钮 |
||||
|
如果我们想要判断radio按钮是否有一个被选中了,我们页面的输出可能就是一个男、女性别的选择,但是也可能一个15岁大的无聊小孩,一手拿着http协议的书,另一只手通过telnet客户端向你的程序在发送请求呢,你设定的性别男值是1,女是2,他给你发送一个3,你的程序会出现异常吗?因此我们也需要像下拉菜单的判断方式类似,判断我们获取的值是我们预设的值,而不是额外的值。 |
||||
|
```html |
||||
|
|
||||
|
<input type="radio" name="gender" value="1">男 |
||||
|
<input type="radio" name="gender" value="2">女 |
||||
|
``` |
||||
|
那我们也可以类似下拉菜单的做法一样 |
||||
|
```Go |
||||
|
|
||||
|
slice:=[]string{"1","2"} |
||||
|
|
||||
|
for _, v := range slice { |
||||
|
if v == r.Form.Get("gender") { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
``` |
||||
|
## 复选框 |
||||
|
有一项选择兴趣的复选框,你想确定用户选中的和你提供给用户选择的是同一个类型的数据。 |
||||
|
```html |
||||
|
|
||||
|
<input type="checkbox" name="interest" value="football">足球 |
||||
|
<input type="checkbox" name="interest" value="basketball">篮球 |
||||
|
<input type="checkbox" name="interest" value="tennis">网球 |
||||
|
``` |
||||
|
对于复选框我们的验证和单选有点不一样,因为接收到的数据是一个slice |
||||
|
```Go |
||||
|
|
||||
|
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的处理包,我们可以把用户的输入年月日转化成相应的时间,然后进行逻辑判断 |
||||
|
```Go |
||||
|
|
||||
|
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位,我们两个都需要验证 |
||||
|
```Go |
||||
|
|
||||
|
//验证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>) |
@ -0,0 +1,220 @@ |
|||||
|
# 5.1 database/sql接口 |
||||
|
Go与PHP不同的地方是Go官方没有提供数据库驱动,而是为开发数据库驱动定义了一些标准接口,开发者可以根据定义的接口来开发相应的数据库驱动,这样做有一个好处,只要是按照标准接口开发的代码, 以后需要迁移数据库时,不需要任何修改。那么Go都定义了哪些标准接口呢?让我们来详细的分析一下 |
||||
|
|
||||
|
## sql.Register |
||||
|
这个存在于database/sql的函数是用来注册数据库驱动的,当第三方开发者开发数据库驱动时,都会实现init函数,在init里面会调用这个`Register(name string, driver driver.Driver)`完成本驱动的注册。 |
||||
|
|
||||
|
我们来看一下mymysql、sqlite3的驱动里面都是怎么调用的: |
||||
|
```Go |
||||
|
|
||||
|
//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来存储用户定义的相应驱动。 |
||||
|
```Go |
||||
|
|
||||
|
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接口。 |
||||
|
```Go |
||||
|
|
||||
|
type Driver interface { |
||||
|
Open(name string) (Conn, error) |
||||
|
} |
||||
|
``` |
||||
|
返回的Conn只能用来进行一次goroutine的操作,也就是说不能把这个Conn应用于Go的多个goroutine里面。如下代码会出现错误 |
||||
|
```Go |
||||
|
|
||||
|
... |
||||
|
go goroutineA (Conn) //执行查询操作 |
||||
|
go goroutineB (Conn) //执行插入操作 |
||||
|
... |
||||
|
``` |
||||
|
上面这样的代码可能会使Go不知道某个操作究竟是由哪个goroutine发起的,从而导致数据混乱,比如可能会把goroutineA里面执行的查询操作的结果返回给goroutineB从而使B错误地把此结果当成自己执行的插入数据。 |
||||
|
|
||||
|
第三方驱动都会定义这个函数,它会解析name参数来获取相关数据库的连接信息,解析完成后,它将使用此信息来初始化一个Conn并返回它。 |
||||
|
|
||||
|
## driver.Conn |
||||
|
Conn是一个数据库连接的接口定义,他定义了一系列方法,这个Conn只能应用在一个goroutine里面,不能使用在多个goroutine里面,详情请参考上面的说明。 |
||||
|
```Go |
||||
|
|
||||
|
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。 |
||||
|
```Go |
||||
|
|
||||
|
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 |
||||
|
事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以 |
||||
|
```Go |
||||
|
|
||||
|
type Tx interface { |
||||
|
Commit() error |
||||
|
Rollback() error |
||||
|
} |
||||
|
``` |
||||
|
这两个函数一个用来递交一个事务,一个用来回滚事务。 |
||||
|
|
||||
|
## driver.Execer |
||||
|
这是一个Conn可选择实现的接口 |
||||
|
```Go |
||||
|
|
||||
|
type Execer interface { |
||||
|
Exec(query string, args []Value) (Result, error) |
||||
|
} |
||||
|
``` |
||||
|
如果这个接口没有定义,那么在调用DB.Exec,就会首先调用Prepare返回Stmt,然后执行Stmt的Exec,然后关闭Stmt。 |
||||
|
|
||||
|
## driver.Result |
||||
|
这个是执行Update/Insert等操作返回的结果接口定义 |
||||
|
```Go |
||||
|
|
||||
|
type Result interface { |
||||
|
LastInsertId() (int64, error) |
||||
|
RowsAffected() (int64, error) |
||||
|
} |
||||
|
``` |
||||
|
LastInsertId函数返回由数据库执行插入操作得到的自增ID号。 |
||||
|
|
||||
|
RowsAffected函数返回执行Update/Insert等操作影响的数据条目数。 |
||||
|
|
||||
|
## driver.Rows |
||||
|
Rows是执行查询返回的结果集接口定义 |
||||
|
```Go |
||||
|
|
||||
|
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的表示方式 |
||||
|
```Go |
||||
|
|
||||
|
type RowsAffected int64 |
||||
|
|
||||
|
func (RowsAffected) LastInsertId() (int64, error) |
||||
|
|
||||
|
func (v RowsAffected) RowsAffected() (int64, error) |
||||
|
``` |
||||
|
## driver.Value |
||||
|
Value其实就是一个空接口,他可以容纳任何的数据 |
||||
|
```Go |
||||
|
|
||||
|
type Value interface{} |
||||
|
``` |
||||
|
drive的Value是驱动必须能够操作的Value,Value要么是nil,要么是下面的任意一种 |
||||
|
```Go |
||||
|
|
||||
|
int64 |
||||
|
float64 |
||||
|
bool |
||||
|
[]byte |
||||
|
string [*]除了Rows.Next返回的不能是string. |
||||
|
time.Time |
||||
|
``` |
||||
|
## driver.ValueConverter |
||||
|
ValueConverter接口定义了如何把一个普通的值转化成driver.Value的接口 |
||||
|
```Go |
||||
|
|
||||
|
type ValueConverter interface { |
||||
|
ConvertValue(v interface{}) (Value, error) |
||||
|
} |
||||
|
``` |
||||
|
在开发的数据库驱动包里面实现这个接口的函数在很多地方会使用到,这个ValueConverter有很多好处: |
||||
|
|
||||
|
- 转化driver.value到数据库表相应的字段,例如int64的数据如何转化成数据库表uint16字段 |
||||
|
- 把数据库查询结果转化成driver.Value值 |
||||
|
- 在scan函数里面如何把driver.Value值转化成用户定义的值 |
||||
|
|
||||
|
## driver.Valuer |
||||
|
Valuer接口定义了返回一个driver.Value的方式 |
||||
|
```Go |
||||
|
|
||||
|
type Valuer interface { |
||||
|
Value() (Value, error) |
||||
|
} |
||||
|
``` |
||||
|
很多类型都实现了这个Value方法,用来自身与driver.Value的转化。 |
||||
|
|
||||
|
通过上面的讲解,你应该对于驱动的开发有了一个基本的了解,一个驱动只要实现了这些接口就能完成增删查改等基本操作了,剩下的就是与相应的数据库进行数据交互等细节问题了,在此不再赘述。 |
||||
|
|
||||
|
## database/sql |
||||
|
database/sql在database/sql/driver提供的接口基础上定义了一些更高阶的方法,用以简化数据库操作,同时内部还建议性地实现一个conn pool。 |
||||
|
```Go |
||||
|
|
||||
|
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` -> `db.prepareDC`的时候会`defer dc.releaseConn`,然后调用`db.putConn`,也就是把这个连接放入连接池,每次调用`db.conn`的时候会先判断freeConn的长度是否大于0,大于0说明有可以复用的conn,直接拿出来用就是了,如果不大于0,则创建一个conn,然后再返回之。 |
||||
|
|
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [访问数据库](<05.0.md>) |
||||
|
* 下一节: [使用MySQL数据库](<05.2.md>) |
@ -0,0 +1,141 @@ |
|||||
|
# 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。 |
||||
|
```sql |
||||
|
|
||||
|
CREATE TABLE `userinfo` ( |
||||
|
`uid` INT(10) NOT NULL AUTO_INCREMENT, |
||||
|
`username` VARCHAR(64) NULL DEFAULT NULL, |
||||
|
`department` 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接口对数据库表进行增删改查操作 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"database/sql" |
||||
|
"fmt" |
||||
|
//"time" |
||||
|
|
||||
|
_ "github.com/go-sql-driver/mysql" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8") |
||||
|
checkErr(err) |
||||
|
|
||||
|
//插入数据 |
||||
|
stmt, err := db.Prepare("INSERT INTO userinfo SET username=?,department=?,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>) |
@ -0,0 +1,122 @@ |
|||||
|
# 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: |
||||
|
```sql |
||||
|
|
||||
|
CREATE TABLE `userinfo` ( |
||||
|
`uid` INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
|
`username` VARCHAR(64) NULL, |
||||
|
`department` VARCHAR(64) NULL, |
||||
|
`created` DATE NULL |
||||
|
); |
||||
|
|
||||
|
CREATE TABLE `userdetail` ( |
||||
|
`uid` INT(10) NULL, |
||||
|
`intro` TEXT NULL, |
||||
|
`profile` TEXT NULL, |
||||
|
PRIMARY KEY (`uid`) |
||||
|
); |
||||
|
``` |
||||
|
看下面Go程序是如何操作数据库表数据:增删改查 |
||||
|
```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, department, 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>) |
@ -0,0 +1,135 @@ |
|||||
|
# 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上也比较活跃。 |
||||
|
|
||||
|
## 实例代码 |
||||
|
数据库建表语句: |
||||
|
```sql |
||||
|
|
||||
|
CREATE TABLE userinfo |
||||
|
( |
||||
|
uid serial NOT NULL, |
||||
|
username character varying(100) NOT NULL, |
||||
|
department character varying(500) NOT NULL, |
||||
|
Created date, |
||||
|
CONSTRAINT userinfo_pkey PRIMARY KEY (uid) |
||||
|
) |
||||
|
WITH (OIDS=FALSE); |
||||
|
|
||||
|
CREATE TABLE userdetail |
||||
|
( |
||||
|
uid integer, |
||||
|
intro character varying(100), |
||||
|
profile character varying(100) |
||||
|
) |
||||
|
WITH(OIDS=FALSE); |
||||
|
|
||||
|
``` |
||||
|
看下面这个Go如何操作数据库表数据:增删改查 |
||||
|
|
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"database/sql" |
||||
|
"fmt" |
||||
|
|
||||
|
_ "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,department,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) |
||||
|
|
||||
|
var lastInsertId int |
||||
|
err = db.QueryRow("INSERT INTO userinfo(username,departname,created) VALUES($1,$2,$3) returning uid;", "astaxie", "研发部门", "2012-12-09").Scan(&lastInsertId) |
||||
|
checkErr(err) |
||||
|
fmt.Println("最后插入id =", lastInsertId) |
||||
|
|
||||
|
|
||||
|
//更新数据 |
||||
|
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>) |
||||
|
* 下一节: [使用Beego orm库进行ORM开发](<05.5.md>) |
@ -0,0 +1,448 @@ |
|||||
|
# 5.5 使用Beego orm库进行ORM开发 |
||||
|
beego orm是我开发的一个Go进行ORM操作的库,它采用了Go style方式对数据库进行操作,实现了struct到数据表记录的映射。beego orm是一个十分轻量级的Go ORM框架,开发这个库的本意降低复杂的ORM学习曲线,尽可能在ORM的运行效率和功能之间寻求一个平衡,beego orm是目前开源的Go ORM框架中实现比较完整的一个库,而且运行效率相当不错,功能也基本能满足需求。 |
||||
|
|
||||
|
beego orm是支持database/sql标准接口的ORM库,所以理论上来说,只要数据库驱动支持database/sql接口就可以无缝的接入beego orm。目前我测试过的驱动包括下面几个: |
||||
|
|
||||
|
|
||||
|
Mysql: [github/go-mysql-driver/mysql](https://github.com/go-sql-driver/mysql) |
||||
|
|
||||
|
PostgreSQL: [github.com/lib/pq](https://github.com/lib/pq) |
||||
|
|
||||
|
SQLite: [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) |
||||
|
|
||||
|
Mysql: [github.com/ziutek/mymysql/godrv](https://github.com/ziutek/mymysql) |
||||
|
|
||||
|
|
||||
|
暂未支持数据库: |
||||
|
|
||||
|
MsSql: [github.com/denisenkom/go-mssqldb](https://github.com/denisenkom/go-mssqldb) |
||||
|
|
||||
|
MS ADODB: [github.com/mattn/go-adodb](https://github.com/mattn/go-adodb) |
||||
|
|
||||
|
Oracle: [github.com/mattn/go-oci8](https://github.com/mattn/go-oci8) |
||||
|
|
||||
|
ODBC: [bitbucket.org/miquella/mgodbc](https://bitbucket.org/miquella/mgodbc) |
||||
|
|
||||
|
|
||||
|
## 安装 |
||||
|
|
||||
|
beego orm支持go get方式安装,是完全按照Go Style的方式来实现的。 |
||||
|
|
||||
|
go get github.com/astaxie/beego |
||||
|
|
||||
|
## 如何初始化 |
||||
|
首先你需要import相应的数据库驱动包、database/sql标准接口包以及beego orm包,如下所示: |
||||
|
```Go |
||||
|
|
||||
|
import ( |
||||
|
"database/sql" |
||||
|
"github.com/astaxie/beego/orm" |
||||
|
_ "github.com/go-sql-driver/mysql" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
//注册驱动 |
||||
|
orm.RegisterDriver("mysql", orm.DRMySQL) |
||||
|
//设置默认数据库 |
||||
|
orm.RegisterDataBase("default", "mysql", "root:root@/my_db?charset=utf8", 30) |
||||
|
//注册定义的model |
||||
|
orm.RegisterModel(new(User)) |
||||
|
|
||||
|
// 创建table |
||||
|
orm.RunSyncdb("default", false, true) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
PostgreSQL 配置: |
||||
|
|
||||
|
```Go |
||||
|
//导入驱动 |
||||
|
// _ "github.com/lib/pq" |
||||
|
|
||||
|
// 注册驱动 |
||||
|
orm.RegisterDriver("postgres", orm.DR_Postgres) |
||||
|
|
||||
|
// 设置默认数据库 |
||||
|
//PostgresQL用户:postgres ,密码:zxxx , 数据库名称:test , 数据库别名:default |
||||
|
orm.RegisterDataBase("default", "postgres", "user=postgres password=zxxx dbname=test host=127.0.0.1 port=5432 sslmode=disable") |
||||
|
``` |
||||
|
|
||||
|
MySQL 配置: |
||||
|
|
||||
|
```Go |
||||
|
//导入驱动 |
||||
|
//_ "github.com/go-sql-driver/mysql" |
||||
|
|
||||
|
//注册驱动 |
||||
|
orm.RegisterDriver("mysql", orm.DR_MySQL) |
||||
|
|
||||
|
// 设置默认数据库 |
||||
|
//mysql用户:root ,密码:zxxx , 数据库名称:test , 数据库别名:default |
||||
|
orm.RegisterDataBase("default", "mysql", "root:zxxx@/test?charset=utf8") |
||||
|
``` |
||||
|
Sqlite 配置: |
||||
|
```Go |
||||
|
//导入驱动 |
||||
|
//_ "github.com/mattn/go-sqlite3" |
||||
|
|
||||
|
//注册驱动 |
||||
|
orm.RegisterDriver("sqlite", orm.DR_Sqlite) |
||||
|
|
||||
|
// 设置默认数据库 |
||||
|
//数据库存放位置:./datas/test.db , 数据库别名:default |
||||
|
orm.RegisterDataBase("default", "sqlite3", "./datas/test.db") |
||||
|
``` |
||||
|
|
||||
|
导入必须的package之后,我们需要打开到数据库的链接,然后创建一个beego orm对象(以MySQL为例),如下所示 |
||||
|
beego orm: |
||||
|
|
||||
|
```Go |
||||
|
|
||||
|
func main() { |
||||
|
o := orm.NewOrm() |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
简单示例: |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"github.com/astaxie/beego/orm" |
||||
|
_ "github.com/go-sql-driver/mysql" // 导入数据库驱动 |
||||
|
) |
||||
|
|
||||
|
// Model Struct |
||||
|
type User struct { |
||||
|
Id int |
||||
|
Name string `orm:"size(100)"` |
||||
|
} |
||||
|
|
||||
|
func init() { |
||||
|
// 设置默认数据库 |
||||
|
orm.RegisterDataBase("default", "mysql", "root:root@/my_db?charset=utf8", 30) |
||||
|
|
||||
|
// 注册定义的 model |
||||
|
orm.RegisterModel(new(User)) |
||||
|
//RegisterModel 也可以同时注册多个 model |
||||
|
//orm.RegisterModel(new(User), new(Profile), new(Post)) |
||||
|
|
||||
|
// 创建 table |
||||
|
orm.RunSyncdb("default", false, true) |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
o := orm.NewOrm() |
||||
|
|
||||
|
user := User{Name: "slene"} |
||||
|
|
||||
|
// 插入表 |
||||
|
id, err := o.Insert(&user) |
||||
|
fmt.Printf("ID: %d, ERR: %v\n", id, err) |
||||
|
|
||||
|
// 更新表 |
||||
|
user.Name = "astaxie" |
||||
|
num, err := o.Update(&user) |
||||
|
fmt.Printf("NUM: %d, ERR: %v\n", num, err) |
||||
|
|
||||
|
// 读取 one |
||||
|
u := User{Id: user.Id} |
||||
|
err = o.Read(&u) |
||||
|
fmt.Printf("ERR: %v\n", err) |
||||
|
|
||||
|
// 删除表 |
||||
|
num, err = o.Delete(&u) |
||||
|
fmt.Printf("NUM: %d, ERR: %v\n", num, err) |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
SetMaxIdleConns |
||||
|
|
||||
|
根据数据库的别名,设置数据库的最大空闲连接 |
||||
|
```Go |
||||
|
|
||||
|
orm.SetMaxIdleConns("default", 30) |
||||
|
``` |
||||
|
SetMaxOpenConns |
||||
|
|
||||
|
根据数据库的别名,设置数据库的最大数据库连接 (go >= 1.2) |
||||
|
```Go |
||||
|
|
||||
|
orm.SetMaxOpenConns("default", 30) |
||||
|
``` |
||||
|
|
||||
|
目前beego orm支持打印调试,你可以通过如下的代码实现调试 |
||||
|
```Go |
||||
|
|
||||
|
orm.Debug = true |
||||
|
``` |
||||
|
|
||||
|
接下来我们的例子采用前面的数据库表User,现在我们建立相应的struct |
||||
|
```Go |
||||
|
|
||||
|
type Userinfo struct { |
||||
|
Uid int `orm:"PK"` //如果表的主键不是id,那么需要加上pk注释,显式的说这个字段是主键 |
||||
|
Username string |
||||
|
Departname string |
||||
|
Created time.Time |
||||
|
} |
||||
|
|
||||
|
type User struct { |
||||
|
Uid int `orm:"PK"` //如果表的主键不是id,那么需要加上pk注释,显式的说这个字段是主键 |
||||
|
Name string |
||||
|
Profile *Profile `orm:"rel(one)"` // OneToOne relation |
||||
|
Post []*Post `orm:"reverse(many)"` // 设置一对多的反向关系 |
||||
|
} |
||||
|
|
||||
|
type Profile struct { |
||||
|
Id int |
||||
|
Age int16 |
||||
|
User *User `orm:"reverse(one)"` // 设置一对一反向关系(可选) |
||||
|
} |
||||
|
|
||||
|
type Post struct { |
||||
|
Id int |
||||
|
Title string |
||||
|
User *User `orm:"rel(fk)"` |
||||
|
Tags []*Tag `orm:"rel(m2m)"` //设置一对多关系 |
||||
|
} |
||||
|
|
||||
|
type Tag struct { |
||||
|
Id int |
||||
|
Name string |
||||
|
Posts []*Post `orm:"reverse(many)"` |
||||
|
} |
||||
|
|
||||
|
func init() { |
||||
|
// 需要在init中注册定义的model |
||||
|
orm.RegisterModel(new(Userinfo),new(User), new(Profile), new(Post), new(Tag)) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
``` |
||||
|
>注意一点,beego orm针对驼峰命名会自动帮你转化成下划线字段,例如你定义了struct名字为`UserInfo`,那么转化成底层实现的时候是`user_info`,字段命名也遵循该规则。 |
||||
|
|
||||
|
## 插入数据 |
||||
|
下面的代码演示了如何插入一条记录,可以看到我们操作的是struct对象,而不是原生的sql语句,最后通过调用Insert接口将数据保存到数据库。 |
||||
|
```Go |
||||
|
|
||||
|
o := orm.NewOrm() |
||||
|
var user User |
||||
|
user.Name = "zxxx" |
||||
|
user.Departname = "zxxx" |
||||
|
|
||||
|
id, err := o.Insert(&user) |
||||
|
if err == nil { |
||||
|
fmt.Println(id) |
||||
|
} |
||||
|
``` |
||||
|
我们看到插入之后`user.Uid`就是插入成功之后的自增ID。 |
||||
|
|
||||
|
|
||||
|
同时插入多个对象:InsertMulti |
||||
|
|
||||
|
类似sql语句 |
||||
|
```Go |
||||
|
|
||||
|
insert into table (name, age) values("slene", 28),("astaxie", 30),("unknown", 20) |
||||
|
``` |
||||
|
第一个参数 bulk 为并列插入的数量,第二个为对象的slice |
||||
|
|
||||
|
返回值为成功插入的数量 |
||||
|
```Go |
||||
|
|
||||
|
users := []User{ |
||||
|
{Name: "slene"}, |
||||
|
{Name: "astaxie"}, |
||||
|
{Name: "unknown"}, |
||||
|
... |
||||
|
} |
||||
|
successNums, err := o.InsertMulti(100, users) |
||||
|
``` |
||||
|
bulk 为 1 时,将会顺序插入 slice 中的数据 |
||||
|
|
||||
|
|
||||
|
## 更新数据 |
||||
|
继续上面的例子来演示更新操作,现在user的主键已经有值了,此时调用Insert接口,beego orm内部会自动调用update以进行数据的更新而非插入操作。 |
||||
|
```Go |
||||
|
|
||||
|
o := orm.NewOrm() |
||||
|
user := User{Uid: 1} |
||||
|
if o.Read(&user) == nil { |
||||
|
user.Name = "MyName" |
||||
|
if num, err := o.Update(&user); err == nil { |
||||
|
fmt.Println(num) |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
Update 默认更新所有的字段,可以更新指定的字段: |
||||
|
```Go |
||||
|
|
||||
|
// 只更新 Name |
||||
|
o.Update(&user, "Name") |
||||
|
// 指定多个字段 |
||||
|
// o.Update(&user, "Field1", "Field2", ...) |
||||
|
``` |
||||
|
|
||||
|
Where: 用来设置条件,支持多个参数,第一个参数如果为整数,相当于调用了Where("主键=?",值)。 |
||||
|
|
||||
|
## 查询数据 |
||||
|
beego orm的查询接口比较灵活,具体使用请看下面的例子 |
||||
|
|
||||
|
例子1,根据主键获取数据: |
||||
|
```Go |
||||
|
|
||||
|
o := orm.NewOrm() |
||||
|
var user User |
||||
|
|
||||
|
user := User{Id: 1} |
||||
|
|
||||
|
err = o.Read(&user) |
||||
|
|
||||
|
if err == orm.ErrNoRows { |
||||
|
fmt.Println("查询不到") |
||||
|
} else if err == orm.ErrMissPK { |
||||
|
fmt.Println("找不到主键") |
||||
|
} else { |
||||
|
fmt.Println(user.Id, user.Name) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
例子2: |
||||
|
```Go |
||||
|
|
||||
|
o := orm.NewOrm() |
||||
|
var user User |
||||
|
|
||||
|
qs := o.QueryTable(user) // 返回 QuerySeter |
||||
|
qs.Filter("id", 1) // WHERE id = 1 |
||||
|
qs.Filter("profile__age", 18) // WHERE profile.age = 18 |
||||
|
``` |
||||
|
例子3,WHERE IN查询条件: |
||||
|
```Go |
||||
|
|
||||
|
qs.Filter("profile__age__in", 18, 20) |
||||
|
// WHERE profile.age IN (18, 20) |
||||
|
|
||||
|
``` |
||||
|
例子4,更加复杂的条件: |
||||
|
```Go |
||||
|
|
||||
|
qs.Filter("profile__age__in", 18, 20).Exclude("profile__lt", 1000) |
||||
|
// WHERE profile.age IN (18, 20) AND NOT profile_id < 1000 |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
可以通过如下接口获取多条数据,请看示例 |
||||
|
|
||||
|
例子1,根据条件age>17,获取20位置开始的10条数据的数据 |
||||
|
```Go |
||||
|
|
||||
|
var allusers []User |
||||
|
qs.Filter("profile__age__gt", 17) |
||||
|
// WHERE profile.age > 17 |
||||
|
``` |
||||
|
例子2,limit默认从10开始,获取10条数据 |
||||
|
```Go |
||||
|
|
||||
|
qs.Limit(10, 20) |
||||
|
// LIMIT 10 OFFSET 20 注意跟SQL反过来的 |
||||
|
``` |
||||
|
|
||||
|
## 删除数据 |
||||
|
beedb提供了丰富的删除数据接口,请看下面的例子 |
||||
|
|
||||
|
例子1,删除单条数据 |
||||
|
```Go |
||||
|
|
||||
|
o := orm.NewOrm() |
||||
|
if num, err := o.Delete(&User{Id: 1}); err == nil { |
||||
|
fmt.Println(num) |
||||
|
} |
||||
|
``` |
||||
|
Delete 操作会对反向关系进行操作,此例中 Post 拥有一个到 User 的外键。删除 User 的时候。如果 on_delete 设置为默认的级联操作,将删除对应的 Post |
||||
|
|
||||
|
## 关联查询 |
||||
|
有些应用却需要用到连接查询,所以现在beego orm提供了一个简陋的实现方案: |
||||
|
```Go |
||||
|
|
||||
|
type Post struct { |
||||
|
Id int `orm:"auto"` |
||||
|
Title string `orm:"size(100)"` |
||||
|
User *User `orm:"rel(fk)"` |
||||
|
} |
||||
|
|
||||
|
var posts []*Post |
||||
|
qs := o.QueryTable("post") |
||||
|
num, err := qs.Filter("User__Name", "slene").All(&posts) |
||||
|
|
||||
|
``` |
||||
|
上面代码中我们看到了一个struct关联查询 |
||||
|
|
||||
|
|
||||
|
|
||||
|
## GroupBy和Having |
||||
|
针对有些应用需要用到group by的功能,beego orm也提供了一个简陋的实现 |
||||
|
```Go |
||||
|
|
||||
|
qs.OrderBy("id", "-profile__age") |
||||
|
// ORDER BY id ASC, profile.age DESC |
||||
|
|
||||
|
qs.OrderBy("-profile__age", "profile") |
||||
|
// ORDER BY profile.age DESC, profile_id ASC |
||||
|
|
||||
|
``` |
||||
|
上面的代码中出现了两个新接口函数 |
||||
|
|
||||
|
GroupBy:用来指定进行groupby的字段 |
||||
|
|
||||
|
Having:用来指定having执行的时候的条件 |
||||
|
|
||||
|
|
||||
|
## 使用原生sql |
||||
|
|
||||
|
简单示例: |
||||
|
|
||||
|
```Go |
||||
|
|
||||
|
o := orm.NewOrm() |
||||
|
var r orm.RawSeter |
||||
|
r = o.Raw("UPDATE user SET name = ? WHERE name = ?", "testing", "slene") |
||||
|
``` |
||||
|
|
||||
|
复杂原生sql使用: |
||||
|
|
||||
|
```Go |
||||
|
func (m *User) Query(name string) user []User { |
||||
|
var o orm.Ormer |
||||
|
var rs orm.RawSeter |
||||
|
o = orm.NewOrm() |
||||
|
rs = o.Raw("SELECT * FROM user "+ |
||||
|
"WHERE name=? AND uid>10 "+ |
||||
|
"ORDER BY uid DESC "+ |
||||
|
"LIMIT 100", name) |
||||
|
//var user []User |
||||
|
num, err := rs.QueryRows(&user) |
||||
|
if err != nil { |
||||
|
fmt.Println(err) |
||||
|
} else { |
||||
|
fmt.Println(num) |
||||
|
//return user |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
更多说明,请到[beego.me](https://beego.me) |
||||
|
|
||||
|
## 进一步的发展 |
||||
|
目前beego orm已经获得了很多来自国内外用户的反馈,我目前也正在考虑支持更多数据库,接下来会在更多方面进行改进 |
||||
|
|
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [使用PostgreSQL数据库](<05.4.md>) |
||||
|
* 下一节: [NOSQL数据库操作](<05.6.md>) |
@ -0,0 +1,208 @@ |
|||||
|
# 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/garyburd/redigo (推荐) |
||||
|
- https://github.com/go-redis/redis |
||||
|
- https://github.com/hoisie/redis |
||||
|
- https://github.com/alphazero/Go-Redis |
||||
|
- https://github.com/simonz05/godis |
||||
|
|
||||
|
我以redigo驱动为例来演示如何进行数据的操作: |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"os" |
||||
|
"os/signal" |
||||
|
"syscall" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/garyburd/redigo/redis" |
||||
|
) |
||||
|
|
||||
|
var ( |
||||
|
Pool *redis.Pool |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
redisHost := ":6379" |
||||
|
Pool = newPool(redisHost) |
||||
|
close() |
||||
|
} |
||||
|
|
||||
|
func newPool(server string) *redis.Pool { |
||||
|
|
||||
|
return &redis.Pool{ |
||||
|
|
||||
|
MaxIdle: 3, |
||||
|
IdleTimeout: 240 * time.Second, |
||||
|
|
||||
|
Dial: func() (redis.Conn, error) { |
||||
|
c, err := redis.Dial("tcp", server) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
return c, err |
||||
|
}, |
||||
|
|
||||
|
TestOnBorrow: func(c redis.Conn, t time.Time) error { |
||||
|
_, err := c.Do("PING") |
||||
|
return err |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func close() { |
||||
|
c := make(chan os.Signal, 1) |
||||
|
signal.Notify(c, os.Interrupt) |
||||
|
signal.Notify(c, syscall.SIGTERM) |
||||
|
signal.Notify(c, syscall.SIGKILL) |
||||
|
go func() { |
||||
|
<-c |
||||
|
Pool.Close() |
||||
|
os.Exit(0) |
||||
|
}() |
||||
|
} |
||||
|
|
||||
|
func Get(key string) ([]byte, error) { |
||||
|
|
||||
|
conn := Pool.Get() |
||||
|
defer conn.Close() |
||||
|
|
||||
|
var data []byte |
||||
|
data, err := redis.Bytes(conn.Do("GET", key)) |
||||
|
if err != nil { |
||||
|
return data, fmt.Errorf("error get key %s: %v", key, err) |
||||
|
} |
||||
|
return data, err |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
test, err := Get("test") |
||||
|
fmt.Println(test, err) |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
另外以前我fork了最后一个驱动,修复了一些bug,目前应用在我自己的短域名服务项目中(每天200W左右的PV值) |
||||
|
|
||||
|
https://github.com/astaxie/goredis |
||||
|
|
||||
|
接下来的以我自己fork的这个redis驱动为例来演示如何进行数据的操作 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
|
||||
|
"github.com/astaxie/goredis" |
||||
|
) |
||||
|
|
||||
|
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的性能非常好。 |
||||
|
|
||||
|
![](images/5.6.mongodb.png?raw=true) |
||||
|
|
||||
|
图5.1 MongoDB和Mysql的操作对比图 |
||||
|
|
||||
|
目前Go支持mongoDB最好的驱动就是[mgo](http://labix.org/mgo),这个驱动目前最有可能成为官方的pkg。 |
||||
|
|
||||
|
安装mgo: |
||||
|
|
||||
|
```Go |
||||
|
go get gopkg.in/mgo.v2 |
||||
|
``` |
||||
|
|
||||
|
下面我将演示如何通过Go来操作mongoDB: |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"log" |
||||
|
|
||||
|
"gopkg.in/mgo.v2" |
||||
|
"gopkg.in/mgo.v2/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() |
||||
|
|
||||
|
// Optional. Switch the session to a monotonic behavior. |
||||
|
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 { |
||||
|
log.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
result := Person{} |
||||
|
err = c.Find(bson.M{"name": "Ale"}).One(&result) |
||||
|
if err != nil { |
||||
|
log.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
fmt.Println("Phone:", result.Phone) |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
我们可以看出来mgo的操作方式和beedb的操作方式几乎类似,都是基于struct的操作方式,这个就是Go Style。 |
||||
|
|
||||
|
|
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [使用Beego orm库进行ORM开发](<05.5.md>) |
||||
|
* 下一节: [小结](<05.7.md>) |
@ -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>) |
@ -0,0 +1,13 @@ |
|||||
|
# 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.4小节里面实现的session是存储在内存中的,但是如果我们的应用进一步扩展了,要实现应用的session共享,那么我们可以把session存储在数据库中(memcache或者redis),6.5小节将详细的讲解如何实现这些功能。 |
||||
|
|
||||
|
|
||||
|
## 目录 |
||||
|
![](images/navi6.png?raw=true) |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一章: [第五章总结](<05.7.md>) |
||||
|
* 下一节: [session和cookie](<06.1.md>) |
@ -0,0 +1,111 @@ |
|||||
|
# 6.1 session和cookie |
||||
|
session和cookie是网站浏览中较为常见的两个概念,也是比较难以辨析的两个概念,但它们在浏览需要认证的服务页面以及页面统计中却相当关键。我们先来了解一下session和cookie怎么来的?考虑这样一个问题: |
||||
|
|
||||
|
如何抓取一个访问受限的网页?如新浪微博好友的主页,个人微博页面等。 |
||||
|
|
||||
|
显然,通过浏览器,我们可以手动输入用户名和密码来访问页面,而所谓的“抓取”,其实就是使用程序来模拟完成同样的工作,因此我们需要了解“登录”过程中到底发生了什么。 |
||||
|
|
||||
|
当用户来到微博登录页面,输入用户名和密码之后点击“登录”后浏览器将认证信息POST给远端的服务器,服务器执行验证逻辑,如果验证通过,则浏览器会跳转到登录用户的微博首页,在登录成功后,服务器如何验证我们对其他受限制页面的访问呢?因为HTTP协议是无状态的,所以很显然服务器不可能知道我们已经在上一次的HTTP请求中通过了验证。当然,最简单的解决方案就是所有的请求里面都带上用户名和密码,这样虽然可行,但大大加重了服务器的负担(对于每个request都需要到数据库验证),也大大降低了用户体验(每个页面都需要重新输入用户名密码,每个页面都带有登录表单)。既然直接在请求中带上用户名与密码不可行,那么就只有在服务器或客户端保存一些类似的可以代表身份的信息了,所以就有了cookie与session。 |
||||
|
|
||||
|
cookie,简而言之就是在本地计算机保存一些用户操作的历史信息(当然包括登录信息),并在用户再次访问该站点时浏览器通过HTTP协议将本地cookie内容发送给服务器,从而完成验证,或继续上一步操作。 |
||||
|
|
||||
|
![](images/6.1.cookie2.png?raw=true) |
||||
|
|
||||
|
图6.1 cookie的原理图 |
||||
|
|
||||
|
session,简而言之就是在服务器上保存用户操作的历史信息。服务器使用session id来标识session,session id由服务器负责产生,保证随机性与唯一性,相当于一个随机密钥,避免在握手或传输中暴露用户真实密码。但该方式下,仍然需要将发送请求的客户端与session进行对应,所以可以借助cookie机制来获取客户端的标识(即session id),也可以通过GET方式将id提交给服务器。 |
||||
|
|
||||
|
![](images/6.1.session.png?raw=true) |
||||
|
|
||||
|
图6.2 session的原理图 |
||||
|
|
||||
|
## cookie |
||||
|
Cookie是由浏览器维持的,存储在客户端的一小段文本信息,伴随着用户请求和页面在Web服务器和浏览器之间传递。用户每次访问站点时,Web应用程序都可以读取cookie包含的信息。浏览器设置里面有cookie隐私数据选项,打开它,可以看到很多已访问网站的cookies,如下图所示: |
||||
|
|
||||
|
![](images/6.1.cookie.png?raw=true) |
||||
|
|
||||
|
图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来设置: |
||||
|
```Go |
||||
|
|
||||
|
http.SetCookie(w ResponseWriter, cookie *Cookie) |
||||
|
``` |
||||
|
w表示需要写入的response,cookie是一个struct,让我们来看一下cookie对象是怎么样的 |
||||
|
```Go |
||||
|
|
||||
|
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 |
||||
|
```Go |
||||
|
|
||||
|
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 |
||||
|
```Go |
||||
|
|
||||
|
cookie, _ := r.Cookie("username") |
||||
|
fmt.Fprint(w, cookie) |
||||
|
``` |
||||
|
还有另外一种读取方式 |
||||
|
```Go |
||||
|
|
||||
|
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>) |
@ -0,0 +1,231 @@ |
|||||
|
# 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管理器 |
||||
|
```Go |
||||
|
|
||||
|
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管理器 |
||||
|
```Go |
||||
|
|
||||
|
var globalSessions *session.Manager |
||||
|
//然后在init函数中初始化 |
||||
|
func init() { |
||||
|
globalSessions, _ = NewManager("memory", "gosessionid", 3600) |
||||
|
} |
||||
|
``` |
||||
|
我们知道session是保存在服务器端的数据,它可以以任何的方式存储,比如存储在内存、数据库或者文件中。因此我们抽象出一个Provider接口,用以表征session管理器底层存储结构。 |
||||
|
```Go |
||||
|
|
||||
|
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接口也就实现这四个操作。 |
||||
|
```Go |
||||
|
|
||||
|
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函数的实现。 |
||||
|
|
||||
|
```Go |
||||
|
|
||||
|
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 provider is nil") |
||||
|
} |
||||
|
if _, dup := provides[name]; dup { |
||||
|
panic("session: Register called twice for provider " + name) |
||||
|
} |
||||
|
provides[name] = provider |
||||
|
} |
||||
|
``` |
||||
|
### 全局唯一的Session ID |
||||
|
|
||||
|
Session ID是用来识别访问Web应用的每一个用户,因此必须保证它是全局唯一的(GUID),下面代码展示了如何满足这一需求: |
||||
|
|
||||
|
```Go |
||||
|
|
||||
|
func (manager *Manager) sessionId() string { |
||||
|
b := make([]byte, 32) |
||||
|
if _, err := rand.Read(b); err != nil { |
||||
|
return "" |
||||
|
} |
||||
|
return base64.URLEncoding.EncodeToString(b) |
||||
|
} |
||||
|
``` |
||||
|
### session创建 |
||||
|
我们需要为每个来访用户分配或获取与他相关连的Session,以便后面根据Session信息来验证操作。SessionStart这个函数就是用来检测是否已经有某个Session与当前来访用户发生了关联,如果没有则创建之。 |
||||
|
```Go |
||||
|
|
||||
|
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的运用: |
||||
|
```Go |
||||
|
|
||||
|
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")`已经展示了基本的读取数据的操作,现在我们再来看一下详细的操作: |
||||
|
```Go |
||||
|
|
||||
|
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重置操作,下面这个函数就是实现了这个功能: |
||||
|
```Go |
||||
|
|
||||
|
//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启动的时候启动: |
||||
|
```Go |
||||
|
|
||||
|
func init() { |
||||
|
go globalSessions.GC() |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
```Go |
||||
|
|
||||
|
func (manager *Manager) GC() { |
||||
|
manager.lock.Lock() |
||||
|
defer manager.lock.Unlock() |
||||
|
time.AfterFunc(time.Duration(manager.maxLifeTime), func() { manager.provider.SessionGC(manager.maxLifeTime) }) |
||||
|
} |
||||
|
``` |
||||
|
我们可以看到GC充分利用了time包中的定时器功能,当超时`maxLifeTime`之后调用GC函数,这样就可以保证`maxLifeTime`时间内的session都是可用的,类似的方案也可以用于统计在线用户数之类的。 |
||||
|
|
||||
|
## 总结 |
||||
|
至此 我们实现了一个用来在Web应用中全局管理Session的SessionManager,定义了用来提供Session存储实现Provider的接口,下一小节,我们将会通过接口定义来实现一些Provider,供大家参考学习。 |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [session和cookie](<06.1.md>) |
||||
|
* 下一节: [session存储](<06.3.md>) |
@ -0,0 +1,139 @@ |
|||||
|
# 6.3 session存储 |
||||
|
上一节我们介绍了Session管理器的实现原理,定义了存储session的接口,这小节我们将示例一个基于内存的session存储接口的实现,其他的存储方式,读者可以自行参考示例来实现,内存的实现请看下面的例子代码 |
||||
|
```Go |
||||
|
|
||||
|
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 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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管理器中。这样就可以方便的调用了。我们如何来调用该引擎呢?请看下面的代码 |
||||
|
```Go |
||||
|
|
||||
|
import ( |
||||
|
"github.com/astaxie/session" |
||||
|
_ "github.com/astaxie/session/providers/memory" |
||||
|
) |
||||
|
``` |
||||
|
当import的时候已经执行了memory函数里面的init函数,这样就已经注册到session管理器中,我们就可以使用了,通过如下方式就可以初始化一个session管理器: |
||||
|
```Go |
||||
|
|
||||
|
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>) |
@ -0,0 +1,93 @@ |
|||||
|
# 6.4 预防session劫持 |
||||
|
session劫持是一种广泛存在的比较严重的安全威胁,在session技术中,客户端和服务端通过session的标识符来维护会话, 但这个标识符很容易就能被嗅探到,从而被其他人利用。它是中间人攻击的一种类型。 |
||||
|
|
||||
|
本节将通过一个实例来演示会话劫持,希望通过这个实例,能让读者更好地理解session的本质。 |
||||
|
## session劫持过程 |
||||
|
我们写了如下的代码来展示一个count计数器: |
||||
|
```Go |
||||
|
|
||||
|
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的代码如下所示: |
||||
|
```Go |
||||
|
|
||||
|
Hi. Now count:{{.}} |
||||
|
``` |
||||
|
然后我们在浏览器里面刷新可以看到如下内容: |
||||
|
|
||||
|
![](images/6.4.hijack.png?raw=true) |
||||
|
|
||||
|
图6.4 浏览器端显示count数 |
||||
|
|
||||
|
随着刷新,数字将不断增长,当数字显示为6的时候,打开浏览器(以chrome为例)的cookie管理器,可以看到类似如下的信息: |
||||
|
|
||||
|
|
||||
|
![](images/6.4.cookie.png?raw=true) |
||||
|
|
||||
|
图6.5 获取浏览器端保存的cookie |
||||
|
|
||||
|
下面这个步骤最为关键: 打开另一个浏览器(这里我打开了firefox浏览器),复制chrome地址栏里的地址到新打开的浏览器的地址栏中。然后打开firefox的cookie模拟插件,新建一个cookie,把按上图中cookie内容原样在firefox中重建一份: |
||||
|
|
||||
|
![](images/6.4.setcookie.png?raw=true) |
||||
|
|
||||
|
图6.6 模拟cookie |
||||
|
|
||||
|
回车后,你将看到如下内容: |
||||
|
|
||||
|
![](images/6.4.hijacksuccess.png?raw=true) |
||||
|
|
||||
|
图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,从而保证用户的请求都是唯一性。 |
||||
|
```Go |
||||
|
|
||||
|
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劫持的问题。 |
||||
|
```Go |
||||
|
|
||||
|
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>) |
@ -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>) |
@ -0,0 +1,237 @@ |
|||||
|
# 7.1 XML处理 |
||||
|
XML作为一种数据交换和信息传递的格式已经十分普及。而随着Web服务日益广泛的应用,现在XML在日常的开发工作中也扮演了愈发重要的角色。这一小节, 我们将就Go语言标准包中的XML相关处理的包进行介绍。 |
||||
|
|
||||
|
这个小节不会涉及XML规范相关的内容(如需了解相关知识请参考其他文献),而是介绍如何用Go语言来编解码XML文件相关的知识。 |
||||
|
|
||||
|
假如你是一名运维人员,你为你所管理的所有服务器生成了如下内容的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`函数来达到我们的目的 |
||||
|
```Go |
||||
|
|
||||
|
func Unmarshal(data []byte, v interface{}) error |
||||
|
``` |
||||
|
data接收的是XML数据流,v是需要输出的结构,定义为interface,也就是可以把XML转换为任意的格式。我们这里主要介绍struct的转换,因为struct和XML都有类似树结构的特征。 |
||||
|
|
||||
|
示例代码如下: |
||||
|
```Go |
||||
|
|
||||
|
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对象。如上例子输出如下数据 |
||||
|
```xml |
||||
|
|
||||
|
{{ 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`的定义: |
||||
|
```Go |
||||
|
|
||||
|
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定义。最后的输出是 |
||||
|
|
||||
|
```xml |
||||
|
|
||||
|
<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`两个函数,来满足我们的需求。这两个函数主要的区别是第二个函数会增加前缀和缩进,函数的定义如下所示: |
||||
|
```Go |
||||
|
|
||||
|
func Marshal(v interface{}) ([]byte, error) |
||||
|
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) |
||||
|
``` |
||||
|
两个函数第一个参数是用来生成XML的结构定义类型数据,都是返回生成的XML数据流。 |
||||
|
|
||||
|
下面我们来看一下如何输出如上的XML: |
||||
|
```Go |
||||
|
|
||||
|
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 |
||||
|
|
||||
|
<?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,例如如下代码就会输出 |
||||
|
|
||||
|
```xml |
||||
|
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>) |
@ -0,0 +1,243 @@ |
|||||
|
# 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来表示,结果描述如下: |
||||
|
```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包中有如下函数 |
||||
|
```Go |
||||
|
|
||||
|
func Unmarshal(data []byte, v interface{}) error |
||||
|
``` |
||||
|
通过这个函数我们就可以实现解析的目的,详细的解析例子请看如下代码: |
||||
|
```Go |
||||
|
|
||||
|
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数据 |
||||
|
```Go |
||||
|
|
||||
|
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`) |
||||
|
``` |
||||
|
如果在我们不知道他的结构的情况下,我们把他解析到interface{}里面 |
||||
|
```Go |
||||
|
|
||||
|
var f interface{} |
||||
|
err := json.Unmarshal(b, &f) |
||||
|
``` |
||||
|
这个时候f里面存储了一个map类型,他们的key是string,值存储在空的interface{}里 |
||||
|
```Go |
||||
|
|
||||
|
f = map[string]interface{}{ |
||||
|
"Name": "Wednesday", |
||||
|
"Age": 6, |
||||
|
"Parents": []interface{}{ |
||||
|
"Gomez", |
||||
|
"Morticia", |
||||
|
}, |
||||
|
} |
||||
|
``` |
||||
|
那么如何来访问这些数据呢?通过断言的方式: |
||||
|
```Go |
||||
|
|
||||
|
m := f.(map[string]interface{}) |
||||
|
``` |
||||
|
通过断言之后,你就可以通过如下方式来访问里面的数据了 |
||||
|
```Go |
||||
|
|
||||
|
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时相当方便,详细例子如下所示: |
||||
|
```Go |
||||
|
|
||||
|
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`函数来处理,函数定义如下: |
||||
|
```Go |
||||
|
|
||||
|
func Marshal(v interface{}) ([]byte, error) |
||||
|
``` |
||||
|
假设我们还是需要生成上面的服务器列表信息,那么如何来处理呢?请看下面的例子: |
||||
|
```Go |
||||
|
|
||||
|
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)) |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
输出如下内容: |
||||
|
```json |
||||
|
|
||||
|
{"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]} |
||||
|
``` |
||||
|
我们看到上面的输出字段名的首字母都是大写的,如果你想用小写的首字母怎么办呢?把结构体的字段名改成首字母小写的?JSON输出的时候必须注意,只有导出的字段才会被输出,如果修改字段名,那么就会发现什么都不会输出,所以必须通过struct tag定义来实现: |
||||
|
```Go |
||||
|
|
||||
|
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字符串 |
||||
|
|
||||
|
|
||||
|
举例来说: |
||||
|
```Go |
||||
|
|
||||
|
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) |
||||
|
|
||||
|
``` |
||||
|
会输出以下内容: |
||||
|
```json |
||||
|
|
||||
|
{"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>) |
@ -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的代码。 |
||||
|
|
||||
|
## 目录 |
||||
|
![](images/navi8.png?raw=true) |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一章: [第七章总结](<07.7.md>) |
||||
|
* 下一节: [Socket编程](<08.1.md>) |
@ -0,0 +1,437 @@ |
|||||
|
# 8.1 Socket编程 |
||||
|
在很多底层网络应用开发者的眼里一切编程都是Socket,话虽然有点夸张,但却也几乎如此了,现在的网络编程几乎都是用Socket来编程。你想过这些情景么?我们每天打开浏览器浏览网页时,浏览器进程怎么和Web服务器进行通信的呢?当你用QQ聊天时,QQ进程怎么和服务器或者是你的好友所在的QQ进程进行通信的呢?当你打开PPstream观看视频时,PPstream进程如何与视频服务器进行通信的呢? 如此种种,都是靠Socket来进行通信的,以一斑窥全豹,可见Socket编程在现代编程中占据了多么重要的地位,这一节我们将介绍Go语言中如何进行Socket编程。 |
||||
|
|
||||
|
## 什么是Socket? |
||||
|
Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。 |
||||
|
|
||||
|
常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。 |
||||
|
|
||||
|
## Socket如何通信 |
||||
|
网络中的进程之间如何通过Socket通信呢?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中需要互相通信的进程,就可以利用这个标志在他们之间进行交互。请看下面这个TCP/IP协议结构图 |
||||
|
|
||||
|
![](images/8.1.socket.png?raw=true) |
||||
|
|
||||
|
图8.1 七层网络协议图 |
||||
|
|
||||
|
使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是为什么说“一切皆Socket”。 |
||||
|
|
||||
|
## Socket基础知识 |
||||
|
通过上面的介绍我们知道Socket有两种:TCP Socket和UDP Socket,TCP和UDP是协议,而要确定一个进程的需要三元组,需要IP地址和端口。 |
||||
|
|
||||
|
### IPv4地址 |
||||
|
目前的全球因特网所采用的协议族是TCP/IP协议。IP是TCP/IP协议中网络层的协议,是TCP/IP协议族的核心协议。目前主要采用的IP协议的版本号是4(简称为IPv4),发展至今已经使用了30多年。 |
||||
|
|
||||
|
IPv4的地址位数为32位,也就是最多有2的32次方的网络设备可以联到Internet上。近十年来由于互联网的蓬勃发展,IP位址的需求量愈来愈大,使得IP位址的发放愈趋紧张,前一段时间,据报道IPV4的地址已经发放完毕,我们公司目前很多服务器的IP都是一个宝贵的资源。 |
||||
|
|
||||
|
地址格式类似这样:127.0.0.1 172.122.121.111 |
||||
|
|
||||
|
### IPv6地址 |
||||
|
IPv6是下一版本的互联网协议,也可以说是下一代互联网的协议,它是为了解决IPv4在实施过程中遇到的各种问题而被提出的,IPv6采用128位地址长度,几乎可以不受限制地提供地址。按保守方法估算IPv6实际可分配的地址,整个地球的每平方米面积上仍可分配1000多个地址。在IPv6的设计过程中除了一劳永逸地解决了地址短缺问题以外,还考虑了在IPv4中解决不好的其它问题,主要有端到端IP连接、服务质量(QoS)、安全性、多播、移动性、即插即用等。 |
||||
|
|
||||
|
地址格式类似这样:2002:c0e8:82e7:0:0:0:c0e8:82e7 |
||||
|
|
||||
|
### Go支持的IP类型 |
||||
|
在Go的`net`包中定义了很多类型、函数和方法用来网络编程,其中IP的定义如下: |
||||
|
```Go |
||||
|
|
||||
|
type IP []byte |
||||
|
|
||||
|
``` |
||||
|
在`net`包中有很多函数来操作IP,但是其中比较有用的也就几个,其中`ParseIP(s string) IP`函数会把一个IPv4或者IPv6的地址转化成IP类型,请看下面的例子: |
||||
|
|
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
import ( |
||||
|
"net" |
||||
|
"os" |
||||
|
"fmt" |
||||
|
) |
||||
|
func main() { |
||||
|
if len(os.Args) != 2 { |
||||
|
fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0]) |
||||
|
os.Exit(1) |
||||
|
} |
||||
|
name := os.Args[1] |
||||
|
addr := net.ParseIP(name) |
||||
|
if addr == nil { |
||||
|
fmt.Println("Invalid address") |
||||
|
} else { |
||||
|
fmt.Println("The address is ", addr.String()) |
||||
|
} |
||||
|
os.Exit(0) |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
执行之后你就会发现只要你输入一个IP地址就会给出相应的IP格式 |
||||
|
|
||||
|
## TCP Socket |
||||
|
当我们知道如何通过网络端口访问一个服务时,那么我们能够做什么呢?作为客户端来说,我们可以通过向远端某台机器的的某个网络端口发送一个请求,然后得到在机器的此端口上监听的服务反馈的信息。作为服务端,我们需要把服务绑定到某个指定端口,并且在此端口上监听,当有客户端来访问时能够读取信息并且写入反馈信息。 |
||||
|
|
||||
|
在Go语言的`net`包中有一个类型`TCPConn`,这个类型可以用来作为客户端和服务器端交互的通道,他有两个主要的函数: |
||||
|
|
||||
|
```Go |
||||
|
|
||||
|
func (c *TCPConn) Write(b []byte) (int, error) |
||||
|
func (c *TCPConn) Read(b []byte) (int, error) |
||||
|
``` |
||||
|
|
||||
|
`TCPConn`可以用在客户端和服务器端来读写数据。 |
||||
|
|
||||
|
还有我们需要知道一个`TCPAddr`类型,他表示一个TCP的地址信息,他的定义如下: |
||||
|
|
||||
|
```Go |
||||
|
|
||||
|
type TCPAddr struct { |
||||
|
IP IP |
||||
|
Port int |
||||
|
Zone string // IPv6 scoped addressing zone |
||||
|
} |
||||
|
``` |
||||
|
在Go语言中通过`ResolveTCPAddr`获取一个`TCPAddr` |
||||
|
|
||||
|
```Go |
||||
|
|
||||
|
func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error) |
||||
|
``` |
||||
|
|
||||
|
- net参数是"tcp4"、"tcp6"、"tcp"中的任意一个,分别表示TCP(IPv4-only), TCP(IPv6-only)或者TCP(IPv4, IPv6的任意一个)。 |
||||
|
- addr表示域名或者IP地址,例如"www.google.com:80" 或者"127.0.0.1:22"。 |
||||
|
|
||||
|
|
||||
|
### TCP client |
||||
|
Go语言中通过net包中的`DialTCP`函数来建立一个TCP连接,并返回一个`TCPConn`类型的对象,当连接建立时服务器端也创建一个同类型的对象,此时客户端和服务器端通过各自拥有的`TCPConn`对象来进行数据交换。一般而言,客户端通过`TCPConn`对象将请求信息发送到服务器端,读取服务器端响应的信息。服务器端读取并解析来自客户端的请求,并返回应答信息,这个连接只有当任一端关闭了连接之后才失效,不然这连接可以一直在使用。建立连接的函数定义如下: |
||||
|
|
||||
|
```Go |
||||
|
|
||||
|
func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error) |
||||
|
``` |
||||
|
- network参数是"tcp4"、"tcp6"、"tcp"中的任意一个,分别表示TCP(IPv4-only)、TCP(IPv6-only)或者TCP(IPv4,IPv6的任意一个) |
||||
|
- laddr表示本机地址,一般设置为nil |
||||
|
- raddr表示远程的服务地址 |
||||
|
|
||||
|
接下来我们写一个简单的例子,模拟一个基于HTTP协议的客户端请求去连接一个Web服务端。我们要写一个简单的http请求头,格式类似如下: |
||||
|
|
||||
|
"HEAD / HTTP/1.0\r\n\r\n" |
||||
|
|
||||
|
从服务端接收到的响应信息格式可能如下: |
||||
|
```Go |
||||
|
|
||||
|
HTTP/1.0 200 OK |
||||
|
ETag: "-9985996" |
||||
|
Last-Modified: Thu, 25 Mar 2010 17:51:10 GMT |
||||
|
Content-Length: 18074 |
||||
|
Connection: close |
||||
|
Date: Sat, 28 Aug 2010 00:43:48 GMT |
||||
|
Server: lighttpd/1.4.23 |
||||
|
``` |
||||
|
我们的客户端代码如下所示: |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"io/ioutil" |
||||
|
"net" |
||||
|
"os" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
if len(os.Args) != 2 { |
||||
|
fmt.Fprintf(os.Stderr, "Usage: %s host:port ", os.Args[0]) |
||||
|
os.Exit(1) |
||||
|
} |
||||
|
service := os.Args[1] |
||||
|
tcpAddr, err := net.ResolveTCPAddr("tcp4", service) |
||||
|
checkError(err) |
||||
|
conn, err := net.DialTCP("tcp", nil, tcpAddr) |
||||
|
checkError(err) |
||||
|
_, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n")) |
||||
|
checkError(err) |
||||
|
// result, err := ioutil.ReadAll(conn) |
||||
|
result := make([]byte, 256) |
||||
|
_, err = conn.Read(result) |
||||
|
checkError(err) |
||||
|
fmt.Println(string(result)) |
||||
|
os.Exit(0) |
||||
|
} |
||||
|
func checkError(err error) { |
||||
|
if err != nil { |
||||
|
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) |
||||
|
os.Exit(1) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
通过上面的代码我们可以看出:首先程序将用户的输入作为参数`service`传入`net.ResolveTCPAddr`获取一个tcpAddr,然后把tcpAddr传入DialTCP后创建了一个TCP连接`conn`,通过`conn`来发送请求信息,最后通过`ioutil.ReadAll`从`conn`中读取全部的文本,也就是服务端响应反馈的信息。 |
||||
|
|
||||
|
### TCP server |
||||
|
上面我们编写了一个TCP的客户端程序,也可以通过net包来创建一个服务器端程序,在服务器端我们需要绑定服务到指定的非激活端口,并监听此端口,当有客户端请求到达的时候可以接收到来自客户端连接的请求。net包中有相应功能的函数,函数定义如下: |
||||
|
```Go |
||||
|
|
||||
|
func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error) |
||||
|
func (l *TCPListener) Accept() (Conn, error) |
||||
|
``` |
||||
|
参数说明同DialTCP的参数一样。下面我们实现一个简单的时间同步服务,监听7777端口 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"net" |
||||
|
"os" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
service := ":7777" |
||||
|
tcpAddr, err := net.ResolveTCPAddr("tcp4", service) |
||||
|
checkError(err) |
||||
|
listener, err := net.ListenTCP("tcp", tcpAddr) |
||||
|
checkError(err) |
||||
|
for { |
||||
|
conn, err := listener.Accept() |
||||
|
if err != nil { |
||||
|
continue |
||||
|
} |
||||
|
daytime := time.Now().String() |
||||
|
conn.Write([]byte(daytime)) // don't care about return value |
||||
|
conn.Close() // we're finished with this client |
||||
|
} |
||||
|
} |
||||
|
func checkError(err error) { |
||||
|
if err != nil { |
||||
|
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) |
||||
|
os.Exit(1) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
上面的服务跑起来之后,它将会一直在那里等待,直到有新的客户端请求到达。当有新的客户端请求到达并同意接受`Accept`该请求的时候他会反馈当前的时间信息。值得注意的是,在代码中`for`循环里,当有错误发生时,直接continue而不是退出,是因为在服务器端跑代码的时候,当有错误发生的情况下最好是由服务端记录错误,然后当前连接的客户端直接报错而退出,从而不会影响到当前服务端运行的整个服务。 |
||||
|
|
||||
|
上面的代码有个缺点,执行的时候是单任务的,不能同时接收多个请求,那么该如何改造以使它支持多并发呢?Go里面有一个goroutine机制,请看下面改造后的代码 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"net" |
||||
|
"os" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
service := ":1200" |
||||
|
tcpAddr, err := net.ResolveTCPAddr("tcp4", service) |
||||
|
checkError(err) |
||||
|
listener, err := net.ListenTCP("tcp", tcpAddr) |
||||
|
checkError(err) |
||||
|
for { |
||||
|
conn, err := listener.Accept() |
||||
|
if err != nil { |
||||
|
continue |
||||
|
} |
||||
|
go handleClient(conn) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func handleClient(conn net.Conn) { |
||||
|
defer conn.Close() |
||||
|
daytime := time.Now().String() |
||||
|
conn.Write([]byte(daytime)) // don't care about return value |
||||
|
// we're finished with this client |
||||
|
} |
||||
|
func checkError(err error) { |
||||
|
if err != nil { |
||||
|
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) |
||||
|
os.Exit(1) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
通过把业务处理分离到函数`handleClient`,我们就可以进一步地实现多并发执行了。看上去是不是很帅,增加`go`关键词就实现了服务端的多并发,从这个小例子也可以看出goroutine的强大之处。 |
||||
|
|
||||
|
有的朋友可能要问:这个服务端没有处理客户端实际请求的内容。如果我们需要通过从客户端发送不同的请求来获取不同的时间格式,而且需要一个长连接,该怎么做呢?请看: |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"net" |
||||
|
"os" |
||||
|
"time" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
service := ":1200" |
||||
|
tcpAddr, err := net.ResolveTCPAddr("tcp4", service) |
||||
|
checkError(err) |
||||
|
listener, err := net.ListenTCP("tcp", tcpAddr) |
||||
|
checkError(err) |
||||
|
for { |
||||
|
conn, err := listener.Accept() |
||||
|
if err != nil { |
||||
|
continue |
||||
|
} |
||||
|
go handleClient(conn) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func handleClient(conn net.Conn) { |
||||
|
conn.SetReadDeadline(time.Now().Add(2 * time.Minute)) // set 2 minutes timeout |
||||
|
request := make([]byte, 128) // set maxium request length to 128B to prevent flood attack |
||||
|
defer conn.Close() // close connection before exit |
||||
|
for { |
||||
|
read_len, err := conn.Read(request) |
||||
|
|
||||
|
if err != nil { |
||||
|
fmt.Println(err) |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
if read_len == 0 { |
||||
|
break // connection already closed by client |
||||
|
} else if strings.TrimSpace(string(request[:read_len])) == "timestamp" { |
||||
|
daytime := strconv.FormatInt(time.Now().Unix(), 10) |
||||
|
conn.Write([]byte(daytime)) |
||||
|
} else { |
||||
|
daytime := time.Now().String() |
||||
|
conn.Write([]byte(daytime)) |
||||
|
} |
||||
|
|
||||
|
request = make([]byte, 128) // clear last read content |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func checkError(err error) { |
||||
|
if err != nil { |
||||
|
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) |
||||
|
os.Exit(1) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
在上面这个例子中,我们使用`conn.Read()`不断读取客户端发来的请求。由于我们需要保持与客户端的长连接,所以不能在读取完一次请求后就关闭连接。由于`conn.SetReadDeadline()`设置了超时,当一定时间内客户端无请求发送,`conn`便会自动关闭,下面的for循环即会因为连接已关闭而跳出。需要注意的是,`request`在创建时需要指定一个最大长度以防止flood attack;每次读取到请求处理完毕后,需要清理request,因为`conn.Read()`会将新读取到的内容append到原内容之后。 |
||||
|
|
||||
|
### 控制TCP连接 |
||||
|
TCP有很多连接控制函数,我们平常用到比较多的有如下几个函数: |
||||
|
```Go |
||||
|
|
||||
|
func DialTimeout(net, addr string, timeout time.Duration) (Conn, error) |
||||
|
|
||||
|
``` |
||||
|
设置建立连接的超时时间,客户端和服务器端都适用,当超过设置时间时,连接自动关闭。 |
||||
|
```Go |
||||
|
|
||||
|
func (c *TCPConn) SetReadDeadline(t time.Time) error |
||||
|
func (c *TCPConn) SetWriteDeadline(t time.Time) error |
||||
|
|
||||
|
``` |
||||
|
用来设置写入/读取一个连接的超时时间。当超过设置时间时,连接自动关闭。 |
||||
|
```Go |
||||
|
|
||||
|
func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error |
||||
|
``` |
||||
|
设置keepAlive属性。操作系统层在tcp上没有数据和ACK的时候,会间隔性的发送keepalive包,操作系统可以通过该包来判断一个tcp连接是否已经断开,在windows上默认2个小时没有收到数据和keepalive包的时候认为tcp连接已经断开,这个功能和我们通常在应用层加的心跳包的功能类似。 |
||||
|
|
||||
|
更多的内容请查看`net`包的文档。 |
||||
|
## UDP Socket |
||||
|
Go语言包中处理UDP Socket和TCP Socket不同的地方就是在服务器端处理多个客户端请求数据包的方式不同,UDP缺少了对客户端连接请求的Accept函数。其他基本几乎一模一样,只有TCP换成了UDP而已。UDP的几个主要函数如下所示: |
||||
|
```Go |
||||
|
|
||||
|
func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error) |
||||
|
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error) |
||||
|
func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error) |
||||
|
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error) |
||||
|
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error) |
||||
|
``` |
||||
|
一个UDP的客户端代码如下所示,我们可以看到不同的就是TCP换成了UDP而已: |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"net" |
||||
|
"os" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
if len(os.Args) != 2 { |
||||
|
fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0]) |
||||
|
os.Exit(1) |
||||
|
} |
||||
|
service := os.Args[1] |
||||
|
udpAddr, err := net.ResolveUDPAddr("udp4", service) |
||||
|
checkError(err) |
||||
|
conn, err := net.DialUDP("udp", nil, udpAddr) |
||||
|
checkError(err) |
||||
|
_, err = conn.Write([]byte("anything")) |
||||
|
checkError(err) |
||||
|
var buf [512]byte |
||||
|
n, err := conn.Read(buf[0:]) |
||||
|
checkError(err) |
||||
|
fmt.Println(string(buf[0:n])) |
||||
|
os.Exit(0) |
||||
|
} |
||||
|
func checkError(err error) { |
||||
|
if err != nil { |
||||
|
fmt.Fprintf(os.Stderr, "Fatal error %s", err.Error()) |
||||
|
os.Exit(1) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
我们来看一下UDP服务器端如何来处理: |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"net" |
||||
|
"os" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
service := ":1200" |
||||
|
udpAddr, err := net.ResolveUDPAddr("udp4", service) |
||||
|
checkError(err) |
||||
|
conn, err := net.ListenUDP("udp", udpAddr) |
||||
|
checkError(err) |
||||
|
for { |
||||
|
handleClient(conn) |
||||
|
} |
||||
|
} |
||||
|
func handleClient(conn *net.UDPConn) { |
||||
|
var buf [512]byte |
||||
|
_, addr, err := conn.ReadFromUDP(buf[0:]) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
daytime := time.Now().String() |
||||
|
conn.WriteToUDP([]byte(daytime), addr) |
||||
|
} |
||||
|
func checkError(err error) { |
||||
|
if err != nil { |
||||
|
fmt.Fprintf(os.Stderr, "Fatal error %s", err.Error()) |
||||
|
os.Exit(1) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
## 总结 |
||||
|
通过对TCP和UDP Socket编程的描述和实现,可见Go已经完备地支持了Socket编程,而且使用起来相当的方便,Go提供了很多函数,通过这些函数可以很容易就编写出高性能的Socket应用。 |
||||
|
|
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [Web服务](<08.0.md>) |
||||
|
* 下一节: [WebSocket](<08.2.md>) |
@ -0,0 +1,156 @@ |
|||||
|
# 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),这一套接口可被用来通过事件句柄异步地接收数据。 |
||||
|
|
||||
|
![](images/8.2.websocket.png?raw=true) |
||||
|
|
||||
|
图8.2 WebSocket原理图 |
||||
|
|
||||
|
## WebSocket原理 |
||||
|
WebSocket的协议颇为简单,在第一次handshake通过以后,连接便建立成功,其后的通讯数据都是以”\x00″开头,以”\xFF”结尾。在客户端,这个是透明的,WebSocket组件会自动将原始数据“掐头去尾”。 |
||||
|
|
||||
|
浏览器发出WebSocket连接请求,然后服务器发出回应,然后连接建立成功,这个过程通常称为“握手” (handshaking)。请看下面的请求和反馈信息: |
||||
|
|
||||
|
![](images/8.2.websocket2.png?raw=true) |
||||
|
|
||||
|
图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 golang.org/x/net/websocket |
||||
|
|
||||
|
WebSocket分为客户端和服务端,接下来我们将实现一个简单的例子:用户输入信息,客户端通过WebSocket将信息发送给服务器端,服务器端收到信息之后主动Push信息到客户端,然后客户端将输出其收到的信息,客户端的代码如下: |
||||
|
|
||||
|
```html |
||||
|
|
||||
|
<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,当握手成功后,会触发WebSocket对象的onopen事件,告诉客户端连接已经成功建立。客户端一共绑定了四个事件。 |
||||
|
|
||||
|
- 1)onopen 建立连接后触发 |
||||
|
- 2)onmessage 收到消息后触发 |
||||
|
- 3)onerror 发生错误时触发 |
||||
|
- 4)onclose 关闭连接时触发 |
||||
|
|
||||
|
我们服务器端的实现如下: |
||||
|
|
||||
|
```Go |
||||
|
|
||||
|
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发送了应答信息。 |
||||
|
|
||||
|
![](images/8.2.websocket3.png?raw=true) |
||||
|
|
||||
|
图8.4 WebSocket服务器端接收到的信息 |
||||
|
|
||||
|
通过上面的例子我们看到客户端和服务器端实现WebSocket非常的方便,Go的源码net分支中已经实现了这个的协议,我们可以直接拿来用,目前随着HTML5的发展,我想未来WebSocket会是Web开发的一个重点,我们需要储备这方面的知识。 |
||||
|
|
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [Socket编程](<08.1.md>) |
||||
|
* 下一节: [REST](<08.3.md>) |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 243 KiB |
After Width: | Height: | Size: 100 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 69 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 84 KiB |
After Width: | Height: | Size: 41 KiB |
@ -0,0 +1,29 @@ |
|||||
|
package main |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
func main() { |
||||
|
fmt.Println("\n ================== 指针类型 方式一 = 开始 ================== ") |
||||
|
|
||||
|
var a int = 20 |
||||
|
var b *int |
||||
|
|
||||
|
b = &a |
||||
|
|
||||
|
fmt.Printf("\na的数值为%d以及指针为%x\n", a, b) |
||||
|
|
||||
|
c := &b |
||||
|
|
||||
|
fmt.Printf("\n指针b的指针值为%x\n", c) |
||||
|
|
||||
|
d := &c |
||||
|
|
||||
|
fmt.Printf("\n指针c的指针值为%x\n", d) |
||||
|
|
||||
|
e := *d |
||||
|
|
||||
|
fmt.Printf("\n指针d的指针值为%x\n", e) |
||||
|
|
||||
|
fmt.Printf("\n指针d的指针的指针的指针值为%x\n", **e) |
||||
|
|
||||
|
} |
@ -0,0 +1,60 @@ |
|||||
|
package main |
||||
|
|
||||
|
/** |
||||
|
|
||||
|
Go 语言循环语句 |
||||
|
for 循环 重复执行语句块 |
||||
|
循环嵌套 在 for 循环中嵌套一个或多个 for 循环 |
||||
|
|
||||
|
循环控制语句 |
||||
|
break 语句 经常用于中断当前 for 循环或跳出 switch 语句 |
||||
|
continue 语句 跳过当前循环的剩余语句,然后继续进行下一轮循环。 |
||||
|
goto 语句 将控制转移到被标记的语句。 |
||||
|
|
||||
|
语法 |
||||
|
Go 语言的 For 循环有 3 种形式,只有其中的一种使用分号。 |
||||
|
|
||||
|
和 C 语言的 for 一样: |
||||
|
for init; condition; post { |
||||
|
... |
||||
|
} |
||||
|
|
||||
|
和 C 的 while 一样: |
||||
|
for condition { |
||||
|
... |
||||
|
} |
||||
|
|
||||
|
和 C 的 for(;;) 一样: |
||||
|
for { |
||||
|
... |
||||
|
} |
||||
|
|
||||
|
init: 一般为赋值表达式,给控制变量赋初值; |
||||
|
condition: 关系表达式或逻辑表达式,循环控制条件; |
||||
|
post: 一般为赋值表达式,给控制变量增量或减量。 |
||||
|
|
||||
|
for语句执行过程如下: |
||||
|
|
||||
|
1、先对表达式 1 赋初值; |
||||
|
|
||||
|
2、判别赋值表达式 init 是否满足给定条件,若其值为真,满足循环条件,则执行循环体内语句,然后执行 post,进入第二次循环,再判别 condition;否则判断 condition 的值为假,不满足条件,就终止for循环,执行循环体外语句。 |
||||
|
|
||||
|
for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下: |
||||
|
|
||||
|
for key, value := range oldMap { |
||||
|
newMap[key] = value |
||||
|
} |
||||
|
|
||||
|
*/ |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
func main() { |
||||
|
for m := 1; m < 10; m++ { |
||||
|
/* fmt.Printf("第%d次:\n",m) */ |
||||
|
for n := 1; n <= m; n++ { |
||||
|
fmt.Printf("%dx%d=%d ", n, m, m*n) |
||||
|
} |
||||
|
fmt.Println("") |
||||
|
} |
||||
|
} |
@ -0,0 +1,48 @@ |
|||||
|
package main |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
func main() { |
||||
|
fmt.Println("\n ================== 切片Slice 方式一 = 开始 ================== ") |
||||
|
|
||||
|
s1 := []int{1, 2, 3} |
||||
|
printSS(s1) |
||||
|
|
||||
|
s2 := s1[:] |
||||
|
printSS(s2) |
||||
|
|
||||
|
s3 := s1[1:] |
||||
|
printSS(s3) |
||||
|
|
||||
|
s4 := s1[:2] |
||||
|
printSS(s4) |
||||
|
|
||||
|
var number = make([]int, 3, 5) |
||||
|
print(number) |
||||
|
|
||||
|
number1 := append(number, 1) |
||||
|
printSS(number1) |
||||
|
|
||||
|
number3 := append(number, 1, 2, 3, 4) |
||||
|
printSS(number3) |
||||
|
|
||||
|
number4 := make([]int, len(number3), cap(number3)*2) |
||||
|
copy(number4, number3) |
||||
|
printSS(number4) |
||||
|
|
||||
|
fmt.Println("\n自主验证...") |
||||
|
numbern := make([]int, 0) |
||||
|
printSS(numbern) |
||||
|
numbern = append(numbern, 28) |
||||
|
printSS(numbern) |
||||
|
numbern = append(numbern, 32) |
||||
|
printSS(numbern) |
||||
|
|
||||
|
} |
||||
|
func printSS(balance []int) { |
||||
|
var j int |
||||
|
i := len(balance) |
||||
|
for j = 0; j < i; j++ { |
||||
|
fmt.Printf("\n第%d个元素值为:", j, balance[j]) |
||||
|
} |
||||
|
} |
@ -0,0 +1,32 @@ |
|||||
|
package main |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
func main() { |
||||
|
fmt.Println("\n ================== 范围Range 方式一 = 开始 ================== ") |
||||
|
|
||||
|
nums := []int{2, 3, 4} |
||||
|
sum := 0 |
||||
|
for _, num := range nums { |
||||
|
sum = sum + num |
||||
|
} |
||||
|
fmt.Println(sum) |
||||
|
|
||||
|
//拆分数组
|
||||
|
for i, num := range nums { |
||||
|
if num == 3 { |
||||
|
fmt.Println("index:", i) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//拆分集合
|
||||
|
kvs := map[string]string{"a": "apple", "b": "banana"} |
||||
|
for k, v := range kvs { |
||||
|
fmt.Printf("%s -> %s\n", k, v) |
||||
|
} |
||||
|
|
||||
|
//拆分字符串
|
||||
|
for i, c := range "go" { |
||||
|
fmt.Println(i, c) |
||||
|
} |
||||
|
} |