go知识重新整理

This commit is contained in:
Estom
2021-09-03 05:34:34 +08:00
parent 62309f856a
commit 1bad082e49
291 changed files with 29345 additions and 2 deletions

View File

@@ -0,0 +1,219 @@
该笔记基于 [Go语言基础+进阶+就业)](https://www.bilibili.com/video/BV17Q4y1P7n9?p=3) 进行整理
该视频作者的播客 [李文周的博客](https://www.liwenzhou.com/)
## 1.1 安装 Go 开发包
[环境搭建内容参考视频老师的文章地址](https://www.liwenzhou.com/posts/Go/go_menu/)
* 下载地址
[Go官网下载地址](https://golang.org/dl/)
[Go官方镜像站推荐](https://golang.google.cn/dl/)
## 1.2 配置环境变量及 GOPROXY
### 1.2.1 环境变量
`GOROOT``GOPATH` 都是环境变量,其中 `GOROOT` 是我们安装 go 开发包的路径,而从 `Go 1.8` 版本开始Go 开发包在安装完成后会为 `GOPATH` 设置一个默认目录,参见下表。
GOPATH 在不同操作系统平台上的默认值
平台 | GOPATH默认值 | 举例
---|---|---
Windows | `%USERPROFILE%/go` | `C:\Users\用户名\go`
Unix | `$HOME/go` | `/home/用户名/go`
> 配置方式可以参考 [从零开始搭建Go语言开发环境](https://www.liwenzhou.com/posts/Go/install_go_dev/)。MAC 下的配置方式可以参考本套笔记 Android 目录下的环境变量配置。
### 1.2.2 GOPROXY
Go 1.11 版本开始,官方支持了 `go module` 包依赖管理工具和 `GOPROXY` 变量。
设置 `GOPROXY` 环境变量后下载源代码时将会走该环境变量设置的代理地址,不再直接从默认代码库下载。
默认 `GOPROXY` 配置是:`GOPROXY=https://proxy.golang.org,direct`,由于国内访问不到 `https://proxy.golang.org`,所以我们需要换一个 PROXY ,这里推荐使用 `https://goproxy.io``https://goproxy.cn`
可以执行下面的命令修改 `GOPROXY`
```go
go env -w GOPROXY=https://goproxy.cn,direct
```
### 1.2.3 `go mod`
从 Go 1.11 版本开始,官方支持了 `go module` 包依赖管理工具,这样我们就不需要非得把项目源码放到 GOPATH 指定的 src 目录下了。
> 使用方式参考 1.4 中的内容
也可以参考:[告别GOPATH快速使用 go modGolang包管理工具](https://www.jianshu.com/p/bbed916d16ea)
## 1.3 编辑器
Go 采用的是 `UTF-8` 编码的文本文件存放源代码,理论上使用任何一款文本编辑器都可以做 Go 语言开发,这里推荐使用 `VS Code``Goland``VS Code` 是微软开源的编辑器,而 Goland 是 jetbrains 出品的付费 IDE。
我们这里使用 `VS Code` 加插件做为 go 语言的开发工具。
### 1.3.1 下载 VS
[VS Code 官方下载地址](https://code.visualstudio.com/Download)
### 1.3.2 安装插件
* 安装中文插件
VS 默认英文界面,安装中文插件并重启后即可变成中文界面
![](pics/1-1-安装中文插件.png)
* 安装 go 扩展插件
![](pics/1-2-安装go插件.png)
## 1.4 第一个 go 程序
### 1.4.1 第一个 go 程序
在本机任意目录新建一个名称为 `hello` 的目录, 然后在该目录下新建一个名称为 `hello.go` 的文件, 并编辑该文件内容,内容如下:
```go
package main
// 这是导包语句
import "fmt"
// 函数外面只能放置变量、常量、函数的声明语句
// main 是程序的入口
func main() {
fmt.Println("hello go")
}
```
### 1.4.2 通过 `go mod` 管理项目
通过命令行/终端进入到我们新建的 `hello` 目录下,然后在终端中执行如下命令:
```go
// 末尾的 hello 可以自定义,通常为项目名
go mod init hello
```
执行命令:
![](pics/1-3-gomod.png)
初始化成功后,`hello` 目录下会多出来一个 `go.mod` 文件。
### 1.4.3 编译并运行项目
通过命令行/终端进入到我们新建的 `hello` 目录下,然后在终端中执行如下命令:
```go
// 此处的 hello 表示 hello.go 的文件名
go build hello
```
执行命令:
![](pics/1-5-gobuild.png)
编译成功后会得到一个可执行的二进制程序:
![](pics/1-6-build结果.png)
> 因为我是使用 MAC 编译的,所以,编译后的文件没有 `.exe` 后缀。Windows 下会有 `.exe` 后缀。
直接在命令行中输入编译后的文件名 `hello` ,然后回车即可运行二进制程序:
![](pics/1-7-运行二进制程序.png)
## 1.5 补充
以下内容都是基于 GOPATH 来管理项目的。
### 1.5.1 使用 GOPATH 管理项目
如果不使用 `go mod` 管理项目,而是依旧使用 GOPATH 管理项目,那么我们就需要在 GOPATH 指定的目录下新建三个子目录:`src``pkg``bin`。其中src 目录是我们的源代码pkg 是编译的中间状态的包bin 中是一些可执行文件。
另外,通过 `go env` 可以查看 go 相关的信息,如下:
![](pics/1-8-goenv.png)
### 1.5.2 Go 项目结构
目录|作用
---|---
`$GOPATH/src` | 存放源代码 ,或存储执行 `go build``go install``go get` 等指令后的三方库
`$GOPATH/bin` | 存储执行 `go build``go install``go get` 等指令后产生的二进制文件
`$GOPATH/pkg`| 存储执行 `go build``go install``go get` 等指令后产生的中间缓存文件
在使用版本管理工具管理项目代码时,只需要控制 src 目录中的内容即可。
适合个人开发者的目录结构:
![](pics/1-9-目录结构1.png)
Go 语言中也是通过包来组织代码文件,我们可以引用别人的包,也可以发布自己的包,但为了防止不同包的项目名冲突,所以,我们通常使用 `顶级域名` 来作为包名的前缀,这样就不用担心项目名称冲突的问题了。如下:
![](pics/1-10-目录结构2.png)
由于企业中的业务线比较多,所以适合企业的目录结构如下:
![](pics/1-11-目录结构3.png)
### 1.5.3 相关命令
* 执行 `go build` 时如果 go 文件就在当前目录下,直接执行 `go build xxx` , xxx 表示文件名。
* 执行 `go build` 时如果 go 文件不在当前目录下,则需要写明其路径,该路径从 src 后面的路径开始写;编译完成后的文件存放在当前目录下。
* `go build -o xxx` 指定编译后的文件名称为 `xxx` 。Windows 中需要添加 `.exe` 后缀,如:`go build -o xxx.exe`
* `go run xx.go` 直接编译 xx.go 文件并运行编译后的结果
* `go install` 先执行 build ,然后将编译后的可执行文件拷贝到环境变量下的 bin 目录中
### 1.5.4 跨平台编译
跨平台编译参考自 [从零开始搭建Go语言开发环境](https://www.liwenzhou.com/posts/Go/install_go_dev/) 中的跨平台部分。
默认我们 `go build` 的可执行文件都是当前操作系统可执行的文件,如果我想在 windows 下编译一个 linux 下可执行文件,那需要怎么做呢?
只需要指定目标操作系统的平台和处理器架构即可:
```go
SET CGO_ENABLED=0 // 禁用CGO
SET GOOS=linux // 目标平台是linux
SET GOARCH=amd64 // 目标处理器架构是amd64
```
使用了 cgo 的代码是不支持跨平台编译的, 所以需要禁用。
然后再执行 `go build` 命令,得到的就是能够在 Linux 平台运行的可执行文件了。
Mac 下编译 Linux 和 Windows平台 64位 可执行程序:
```go
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build
```
Linux 下编译 Mac 和 Windows 平台64位可执行程序
```go
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build
```
Windows下编译Mac平台64位可执行程序
```go
SET CGO_ENABLED=0
SET GOOS=darwin
SET GOARCH=amd64
go build
```

View File

@@ -0,0 +1,132 @@
# 10 函数
## 10.1 函数的定义
### 10.1.1 函数的定义
```go
// 有参数有返回值的
func sum(x int, y int) (ret int) {
return x + y
}
// 有参数但无返回值
func f1(x int, y int) {
fmt.Println(x + y)
}
// 无参数无返回值
func f2() {
fmt.Println("f2")
}
//无参有返回值
func f3() int {
return 3
}
```
### 10.1.2 返回值
#### 10.1.2.1 为返回值命名
可以为返回值指定一个名字,这就相当于提前声明了一个变量,并将该变量作为返回值。
```go
/* 声明函数时为返回值指定了名称为 ret, 在函数内部可以直接使用 ret,
* 并且函数体最后只写 return 即可,其他不需要添加 ret.
*/
func sum(x int, y int) (ret int) {
ret = x + y
return
}
/*
* 声明函数时仅声明了返回值类型,没有为返回值命名。所以,函数体内部需要自定义
* 一个变量来接收结果。函数的最后需要在 return 后面添加结果。
*/
func sum2(x int, y int) int {
ret := x + y
return ret
}
```
#### 10.1.2.2 多个返回值
```go
package main
import "fmt"
func main() {
// 定义两个变量分别接收 f1 的两个返回值
m, n := f1()
fmt.Println(m, n)
// 我们仅需要其中一个返回值时,另一个不需要的返回值使用 _ 表示
_, b := f1()
fmt.Println(b)
}
// 该函数有两个返回值, int 和 string
func f1() (int, string) {
return 1, "济南"
}
```
### 10.1.3 参数
#### 10.1.3.1 参数类型的简写
```go
func f1(x int, y int) {
fmt.Println("这是普通的参数声明方式")
}
func f2(x, y int) {
fmt.Println("参数中多个连续的参数类型一致时,可以省略非最后一个参数的类型")
}
func f3(x, y int, m, n string, a, b bool) {
fmt.Println("参数中多个连续的参数类型一致时,可以省略非最后一个参数的类型")
}
```
#### 10.1.3.2 可变长度参数
* **可变长参数必须写在函数最后**
* go 语言中没有默认参数的概念
```go
package main
import "fmt"
func main() {
f4("济南", 1, 2, 3, 4, 5, 6)
}
// ...表示可变长度参数,...int 表示可以传入多个 int 值, 其本质是切片
func f4(x string, y ...int) {
fmt.Println(x)
fmt.Println(y)
fmt.Printf("%T\n", y)
}
```
运行结果如下:
```go
济南
[1 2 3 4 5 6]
[]int
```
## 10.2
P35

View File

View File

View File

@@ -0,0 +1,339 @@
## 2.1 变量的声明
变量名、常量名、函数名统称为标识符。标识符由字母、数字和下划线组成,只能以字母或下划线开头。
Go 语言中推荐使用驼峰命名方式。
> 命名方式stu_name 下划线方式stuName 小驼峰方式StuName 大驼峰方式
**Go 语言中的变量必须先声明再使用;非全局变量(函数内的变量)声明之后必须使用,否则报错**
同一个作用域内不能有重名的变量。
### 2.1.1 标准声明方式
`var 变量名 变量类型`
如:
```go
var s1 string
var num int
var isok bool
```
### 2.1.2 批量声明
```go
var (
a string
b int
c bool
d float32
)
```
### 2.1.4 类型推导方式声明
即在声明变量时直接赋值,且并不需要声明其类型,系统会根据变量值自动推断变量的类型,推荐使用这种方式:
```go
var age = 28
```
### 2.1.5 短变量声明
**在函数内部时**,可以直接使用简短的声明方式——`:=`
```go
package main
import "fmt"
func main() {
// 简短声明方式,省略了 var ,仅能用于函数内部
s3 := 13
fmt.Printf("%d \n", s3)
}
```
### 2.1.6 匿名变量
在使用多重赋值时,如果想要忽略某个值,可以使用 `匿名变量anonymous variable` 。匿名变量用下划线 `_` 表示。
匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。
```go
package main
import "fmt"
func main() {
x, _ := foo()
_, y := foo()
// x= 10
fmt.Println("x=", x)
// y= 张三
fmt.Println("y=", y)
}
func foo() (int, string) {
return 10, "张三"
}
```
## 2.2 变量的初始化
### 2.2.1 声明时初始化
可以在声明的同时初始化,格式为:`var 变量名 类型 = 表达式`,如:
```go
var name string = "张三"
var age int = 18
```
### 2.2.2 先声明后初始化
也可以先声明再初始化,声明后系统会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化为其类型的默认值。如:整型和浮点型变量的默认值为 0字符串变量的默认值为空字符串布尔类型的变量默认为 flase ,切片、函数、指针变量的默认值为 nil。如下
```go
package main
import "fmt"
var age int
func main() {
//输出 0
fmt.Printf("%d \n", age)
age = 18
//输出 18
fmt.Printf("%d \n", age)
}
```
> fmt 包中有三种 print : Print——普通打印Printf——可以使用占位符的格式化打印Println——带有换行符的打印
### 2.2.3 批量初始化
```go
var name, age = "张三", 18
```
## 2.3 常量
常量是恒定不变的值,多用于定义程序运行期间不会改变的一些值。常量关键字 `const`,在定义常量时必须赋值。如:
```go
const pi = 3.1415
const e = 2.7182
```
多个常量也可以一起声明
```go
const (
pi = 3.1415
e = 2.7182
)
```
**const 同时声明多个常量时,如果省略了值则表示和上面一行的值相同**,如:
```go
package main
import "fmt"
const (
a = 100
b
c = 200
d
)
func main() {
// 100
fmt.Println("a=", a)
// 100
fmt.Println("b=", b)
//200
fmt.Println("c=", c)
//200
fmt.Println("d=", d)
}
```
## 2.4 iota
### 2.4.1 iota 的基本定义和使用
`iota` 是 go 语言中的常量计数器,只能在常量的表达式中使用。
**`iota``const` 关键字出现时将被重置为 0`const` 中每新增一行常量声明都将使 `iota` 计数一次。**
使用 `iota` 能简化定义,在定义枚举时很有用。
```go
package main
import "fmt"
const (
a = iota
b
c
)
func main() {
// 0
fmt.Println("a=", a)
// 1
fmt.Println("b=", b)
//2
fmt.Println("c=", c)
}
```
### 2.4.2 常见示例
* 使用 `_` 跳过某些值:
```go
package main
import "fmt"
const (
a = iota
b
_
c
)
func main() {
// 0
fmt.Println("a=", a)
// 1
fmt.Println("b=", b)
// 3
fmt.Println("c=", c)
}
```
* `iota` 声明中间插队
```go
package main
import "fmt"
const (
a = iota
b = 100
c
d = iota
e
)
func main() {
// 0
fmt.Println("a=", a)
// 100
fmt.Println("b=", b)
// 100
fmt.Println("c=", c)
// 3
fmt.Println("d=", d)
// 4
fmt.Println("e=", e)
}
```
* 多个常量声明在一行
```go
package main
import "fmt"
const (
a, b = iota + 1, iota + 2
c, d = iota + 1, iota + 2
e, f
)
func main() {
// 1
fmt.Println("a=", a)
// 2
fmt.Println("b=", b)
// 2
fmt.Println("c=", c)
// 3
fmt.Println("d=", d)
// 3
fmt.Println("e=", e)
// 4
fmt.Println("f=", f)
}
```
```go
package main
import "fmt"
const (
a, b = iota + 1, iota + 2
c, d
e, f
)
func main() {
// 1
fmt.Println("a=", a)
// 2
fmt.Println("b=", b)
// 2
fmt.Println("c=", c)
// 3
fmt.Println("d=", d)
// 3
fmt.Println("e=", e)
// 4
fmt.Println("f=", f)
}
```
* 定义数量级
```go
package main
import "fmt"
const (
_ = iota
KB = 1 << (10 * iota)
MB = 1 << (10 * iota)
GB = 1 << (10 * iota)
TB = 1 << (10 * iota)
PB = 1 << (10 * iota)
)
func main() {
// KB = 1024
fmt.Println("KB =", KB)
// MB = 1048576
fmt.Println("MB =", MB)
// GB = 1073741824
fmt.Println("GB =", GB)
// TB = 1099511627776
fmt.Println("TB =", TB)
// PB = 1125899906842624
fmt.Println("PB =", PB)
}
```

View File

@@ -0,0 +1,689 @@
# 3 基本数据类型
基本数据类型包括:整型、浮点型、布尔型、字符串。
除以上基本类型之外,还有 数组、切片、结构体、函数、map、通道 等。
## 3.1 整型
### 3.1.1 整型
整型分为:`int8``int16``int32``int64` ,以及对应的无符号整型:`uint8``uint16``uint32``uint64`
其中 `unit8` 对应其他语言中的 `byte` 类型;`int16` 对应 C 语言中的 `short` 类型;`int64` 对应 C 语言中的 `long` 类型。
各数据类型及对应的取值关系如下:
![](pics/3-1-整型及对应的数据类型.png)
特殊整型:
![](pics/3-2-特殊整型.png)
>注意: 获取对象的长度时,内建的 `len()` 函数返回的长度可以根据不同平台的字节长度进行变化。实际使用中,切片或 map 的元素数量等都可以直接使用 int 来表示 。
### 3.1.2 八进制和十六进制
```go
package main
import (
"fmt"
)
func main() {
// 声明十进制数.
var a = 10
// %d 为十进制数的占位符表示输出十进制数——10
fmt.Printf("%d \n", a)
// %b 为二进制数的占位符输出二进制数——1010
fmt.Printf("%b \n", a)
// %o 为八进制数的占位符输出八进制数——12
fmt.Printf("%o \n", a)
// %x 为十六进制数的占位符输出十六进制数——a
fmt.Printf("%x \n", a)
// %T 表示获取某个变量的类型_int
fmt.Printf("%T \n", a)
// 声明八进制数,以 0 开头
b := 077
// 63
fmt.Printf("%d \n", b)
//111111
fmt.Printf("%b \n", b)
// 77
fmt.Printf("%o \n", b)
// 3f
fmt.Printf("%x \n", b)
// int
fmt.Printf("%T \n", b)
// 声明 16 进制数,以 0x开头
c := 0xff
// 255
fmt.Printf("%d \n", c)
// 11111111
fmt.Printf("%b \n", c)
// 377
fmt.Printf("%o \n", c)
// ff
fmt.Printf("%x \n", c)
// int
fmt.Printf("%T \n", c)
// 显示声明为 int8 类型,否则,默认 int 类型
d := int8(9)
// 9
fmt.Printf("%d \n", d)
// 1001
fmt.Printf("%b \n", d)
// 11
fmt.Printf("%o \n", d)
// 9
fmt.Printf("%x \n", d)
// int8
fmt.Printf("%T \n", d)
// %v 表示获取某个变量的值——int8,9
fmt.Printf("d 的类型为:%T值为: %v", d, d)
}
```
## 3.2 浮点型
go 语言支持两种浮点型数,`float32``float64`.
这两种浮点型数据格式遵循 `IEEE 754` 标准, float32 的浮点数的最大范围约为 `3.4e38` ,可以使用常量定义:`math.MaxFloat32`。float64 的浮点数的最大范围约为 `1.8e308`, 可以使用常量定义 `math.MaxFloat64`
```go
package main
import (
"fmt"
"math"
)
func main() {
// %f 为浮点数的占位符,
fmt.Printf("%f \n", math.MaxFloat32)
fmt.Printf("%f \n", math.MaxFloat64)
var a = 1.2345
// 默认 floa64
fmt.Printf("%T \n", a)
var b = float32(1.2345)
// 显示声明为 float32
fmt.Printf("%T \n", b)
}
```
## 3.3 复数
* 复数实际上是有两个实数浮点数的组合一个表示实部real一个表示虚部imag
* 虚部需要添加 `i` 作为后缀。
* `real(复数)`可以获取实部 `imag(复数)` 可以获取虚部
*`complex64``complex128` 两种类型complex64 的实部和虚部为 32 位complex128 的实部和虚部为 64 位。
```go
package main
import "fmt"
func main() {
// 1、声明一个复数
var v1 complex64
// 虚部数据需要后缀 i
v1 = 3.2 + 12i
fmt.Println("v1=", v1)
// 2、自动推导类型的复数
v2 := 3.3 + 2.0i
fmt.Printf("v2的类型为%T \n", v2)
fmt.Println("v2的值为", v2)
// 3、获取复数的实部和虚部: real(复数)imag(复数)
fmt.Println("v2的实部为", real(v2), "v2的虚部为", imag(v2))
}
```
运行结果:
```
cnpeng$ go run Day1.go
v1= (3.2+12i)
v2的类型为complex128
v2的值为 (3.3+2i)
v2的实部为 3.3 v2的虚部为 2
```
## 3.4 布尔类型
### 3.4.1 布尔类型
go 语言中以 `bool` 声明布尔类型数据。布尔类型仅有 `true``false` 两种值。
注意:
* 布尔类型变量的默认值为 false
* go 语言中不允许将整型强制转换为布尔类型
* 布尔型数据无法参与数值运算,与无法与其他类型进行转换。
### 3.4.2 补充:格式化输出的总结
fmt 包的 `Printf()` 格式化输出可以使用下面的格式化符号:
格式|含义
---|---
`%%`| `%` 字面量
`%b` | 二进制整数值(基数为2),或者是(高级的)用科学计数法表示的指数为2的浮点数
`%c` | 字符型。可以把输入的数字按照 ASCII 码转换为对应的字符。
`%d` | 十进制数值(基数为10)
`%e` | 以科学计数法 e 表示的浮点数或者复数值
`%E` | 以科学计数法 E 表示的浮点数或者复数值
`%f` | 以标准计数法表示的浮点数或者复数值
`%g` | 以`%e`或者`%f`表示的浮点数或者复数,任何一个都以最为紧凑的方式输出
`%G` | 以 `%E`或者`%f`表示的浮点数或者复数,任何一个都以最为紧凑的方式输出
`%o`| 以八进制表示的数字
`%p`| 以十六进制表示的值的地址,前缀为 0x, 字母使用小小的 a-f 表示
`%s`| 字符串
`%t`| 以 true 或者 false 输出布尔值
`%T`| 获取数据类型。
`%U`| 用 Unicode 表示法表示的整型码点。默认值为四个数字字符
`%v`| 使用默认格式输出的内置或自定义类型的值。或者时使用期望类型的 `String()` 方式输出的自定义值。
`%#v` | 如果输出的字符串,该字符串会被双引号包裹起来
`%x`| 以十六进制表示的整型值, a-f 使用小写
`%X`| 以十六进制表示的整型值, A-F 使用大写
```go
package main
import (
"fmt"
)
func main() {
var str = "abc"
// "abc"
fmt.Printf("%#v \n", str)
}
```
## 3.4 字符串
go 语言中的字符串以原生数据类型出现使用字符串就想使用其他原生数据类型int、bool、float32、float64 等)一样。
go 语言中字符串的内部实现使用了 `UTF-8` 编码。**字符串的值使用双引号 `" "` 包裹** go 语言中,只有使用单引号 `' '` 包裹的是字符)。
### 3.4.1 字符串转义符
转义符|含义
---|---
`\r` | 回车符(返回行首)
`\n` | 换行符
`\t` | 制表符
`\'` | 单引号
`\"` | 双引号
`\\` | 反斜杠
> 1字节=8Bit 即1字节表示八个二进制位也就是 八个 01——01010101
> 一个 UTF-8 编码的汉字通常占用三个字节,偏僻字可能会占四个字节。
```go
package main
import (
"fmt"
)
func main() {
// 打印 windows 下的一个文件目录,需要使用转义字符
// 输出结果:"D:\Go\src\code.github.cnoeng\stu"
fmt.Printf("\"D:\\Go\\src\\code.github.cnoeng\\stu\"")
}
```
### 3.4.2 多行字符串(原样字符串)
go 语言中要定义一个多行字符串时,使用 `反引号` (即键盘左上角数字一左侧的那个按键) 包裹。也叫原样字符串,输入啥样,输出就啥样。
```go
package main
import (
"fmt"
)
func main() {
s := `
床前明月光,
疑是地上霜。
举头望明月,
低头思故乡。
`
fmt.Printf("%s \n", s)
}
```
![](pics/3-3-多行字符串.png)
```go
package main
import (
"fmt"
)
func main() {
s := `D:\Go\src\code.github.cnoeng\stu`
// D:\Go\src\code.github.cnoeng\stu
fmt.Printf("%s \n", s)
}
```
### 3.4.3 字符串的常用操作
方法|含义
---|---
`len(str)` | 获取字符串的长度
`+ 或 fmt.Sprintf()` | 拼接字符串
`strings.Split` | 分割
`strings.contains` | 判断是否包含
`strings.HasPrefix``strings.HasSuffix` | 前缀、后缀的判断
`strings.Index()``strings.LastIndex()` | 子串出现的位置
`strings.Join(a[]string,sep string)` | join 操作 (拼接)
>字符串是由 byte 字节组成,所以字符串的长度就是 byte 字节的长度。
```go
package main
import (
"fmt"
"strings"
)
func main() {
s := `张三`
// 6
fmt.Println(len(s))
// 字符串拼接
s1 := "李四"
// fmt.Printf 只能输出到终端
// 张三李四
fmt.Printf("%s \n", s+s1)
// 张三李四
fmt.Printf("%s%s \n", s, s1)
// fmt.Sprintf 可以将拼接后的值返回给变量
s2 := fmt.Sprintf("%s%s \n", s, s1)
// 张三李四
fmt.Printf("%s \n", s2)
// 字符串分割
s3 := "/Users/cnpeng/CnPeng/04_Demos/12_Go"
splitS := strings.Split(s3, "/")
// [ Users cnpeng CnPeng 04_Demos 12_Go]
fmt.Printf("%s \n", splitS)
// 是否包含——true
fmt.Println(strings.Contains(s2, "张三"))
// 前缀和后缀——true
fmt.Println(strings.HasPrefix(s2, "张"))
// true
fmt.Println(strings.HasSuffix(s2, "四"))
// 子串位置——0
fmt.Println(strings.Index(s2, "张"))
// 9
fmt.Println(strings.LastIndex(s2, "四"))
// Join 拼接——+Users+cnpeng+CnPeng+04_Demos+12_Go
fmt.Println(strings.Join(splitS, "+"))
}
```
### 3.4.4 byte 和 rune 类型
组成每个字符串的元素叫做字符。可以通过遍历或者单个获取字符串元素获得字符。字符使用单引号包裹,如:
```go
var a := '中'
var b := 'x'
```
go 语言中的字符有如下两种:
* `uint8` 类型,或者叫 `byte` 类型,代表了 ASCII 码的一个字符。
* `rune` 类型,代表一个 `UTF-8` 字符。一个 `rune` 字符由一个或多个 byte 组成。
当需要处理中文、日文或其他复合字符时,需要使用 `rune` 类型。`rune` 类型实际是一个 `int32`
```go
package main
import (
"fmt"
)
func main() {
s := "中国"
// 普通遍历方式得到的是 byte 最终输出结果为228——ä184——¸173——­229——å155——›189——½
for i := 0; i < len(s); i++ {
fmt.Printf("%v——%c", s[i], s[i])
}
fmt.Println()
// range 遍历得到的数 rune 最终结果为20013——中22269——国
for _, c := range s {
fmt.Printf("%v——%c", c, c)
}
fmt.Println()
}
```
因为 `UTF-8` 编码下一个中文汉字由 3-4 个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的字符串,否则就会出现上面第一个遍历中的结果。
### 3.4.5 修改字符串
字符串是不能直接修改的。
字符串底层是一个 byte 数组,所以可以和 `[]byte` 类型相互转换。
修改字符串时,需要先将其转换成 `[]rune``[]byte`, 修改完成后再转换为 string。无论哪种转换都会重新分配内存并复制字节数组。
```go
package main
import (
"fmt"
)
func main() {
s1 := "big"
byteS1 := []byte(s1)
byteS1[0] = 'p'
// pig
fmt.Println(string(byteS1))
s2 := "白萝卜"
runeS2 := []rune(s2)
runeS2[0] = '红'
//红萝卜
fmt.Println(string(runeS2))
}
```
## 3.5 类型转换和类型别名
### 3.5.1 类型转换
Go 语言中不允许隐式转换,所有类型转换必须显示声明,而且转换只能发生在两种相互兼容的类型之间。
```go
package main
func main() {
var a byte = 97
//显示类型转换
var b int = int(a)
//隐式类型转换,报错
// var c int = a
//类型不兼容int 不能转为 bool
//var d bool = bool(b)
fmt.Println(d)
}
```
### 3.5.2 类型别名
* 为现有的类型定义别名,方便调用。关键字 `type`
( 在 Swift 中也有这种类型别名,特别是对函数类型定义别名很有用)
```go
package main
import "fmt"
func main() {
//1、为 int64 起一个类型别名为bigint
type bigint int64
var x bigint = 100
//2、同时定义多个类型别名
type (
myint int
mystr string
)
var y myint = 11
var z mystr = "12"
fmt.Println(x, y, z)
fmt.Printf("x,y,z 的类型分别为:%T , %T , %T \n ", x, y, z)
}
```
运行结果:
```
cnpeng$ go run Day1.go
100 11 12
x,y,z 的类型分别为main.bigint , main.myint , main.mystr
```
## 3.6 strconv 包
Go 语言中 strconv 包实现了基本数据类型和其字符串表示的相互转换。主要有以下常用函数: `Atoi()``Itoa()``parse` 系列、`format` 系列、`append` 系列。
[更多函数请查看官方文档。](http://docscn.studygolang.com/pkg/strconv/)
### 3.6.1 string与int类型转换
这一组函数是我们平时编程中用的最多的。
#### 3.6.1.1 Atoi()
`Atoi()` 函数用于将字符串类型的整数转换为int类型函数签名如下。
```go
func Atoi(s string) (i int, err error)
```
如果传入的字符串参数无法转换为int类型就会返回错误。
```go
s1 := "100"
i1, err := strconv.Atoi(s1)
if err != nil {
fmt.Println("can't convert to int")
} else {
fmt.Printf("type:%T value:%#v\n", i1, i1) //type:int value:100
}
```
#### 3.6.1.2 Itoa()
`Itoa()` 函数用于将 int 类型数据转换为对应的字符串表示,具体的函数签名如下。
```go
func Itoa(i int) string
```
示例代码如下:
```go
i2 := 200
s2 := strconv.Itoa(i2)
fmt.Printf("type:%T value:%#v\n", s2, s2) //type:string value:"200"
```
#### 3.6.1.3 a的典故
这是 C 语言遗留下的典故。**C 语言中没有 string 类型而是用字符数组(array)表示字符串**,所以 Itoa 对很多 C 系的程序员很好理解。
### 3.6.2 Parse系列函数
Parse 类函数用于转换字符串为给定类型的值:`ParseBool()``ParseFloat()``ParseInt()``ParseUint()`
#### 3.6.2.1 ParseBool()
```go
func ParseBool(str string) (value bool, err error)
```
返回字符串表示的 bool 值。
* 当 str 为1tTTRUEtrueTrue 中的一种时为真值
* 当 str 为0fFFALSEfalseFalse 中的一种时为假值
* 其他输入内容一律返回 false
```go
fmt.Println(strconv.ParseBool("t"))
fmt.Println(strconv.ParseBool("TRUE"))
fmt.Println(strconv.ParseBool("true"))
fmt.Println(strconv.ParseBool("True"))
fmt.Println(strconv.ParseBool("0"))
fmt.Println(strconv.ParseBool("f"))
```
#### 3.6.2.2 ParseInt()
```go
func ParseInt(s string, base int, bitSize int) (i int64, err error)
```
返回字符串表示的整数值,接受正负号。
* `base` 指定进制2到36如果 base 为0则会从字符串前置判断`0x` 是16进制`0` 是 8 进制,否则是 10 进制;
* `bitSize` 指定结果必须能无溢出赋值的整数类型0、8、16、32、64 分别代表 int、int8、int16、int32、int64
* 返回的 err 是 `*NumErr` 类型的,如果语法有误,`err.Error = ErrSyntax`;如果结果超出类型范围 `err.Error = ErrRange`
#### 3.6.2.3 ParseUnit()
```go
func ParseUint(s string, base int, bitSize int) (n uint64, err error)
```
ParseUint 类似 ParseInt 但不接受正负号,用于无符号整型。
#### 3.6.2.4 ParseFloat()
```go
func ParseFloat(s string, bitSize int) (f float64, err error)
```
解析一个表示浮点数的字符串并返回其值。
如果 s 合乎语法规则,函数会返回**最为接近 s 表示值的一个浮点数**(使用 IEEE754 规范舍入)。
* `bitSize` 指定了期望的接收类型32 是 float32返回值可以不改变精确值的赋值给float3264 是 float64
* 返回值 err 是 `*NumErr` 类型的,语法有误的,`err.Error=ErrSyntax`;结果超出表示范围的,返回值 f 为 `±Inf``err.Error= ErrRange`
#### 3.6.2.5 代码示例
```go
b, err := strconv.ParseBool("true")
f, err := strconv.ParseFloat("3.1415", 64)
i, err := strconv.ParseInt("-2", 10, 64)
u, err := strconv.ParseUint("2", 10, 64)
```
这些函数都有两个返回值,第一个返回值是转换后的值,第二个返回值为转化失败的错误信息。
### 3.6.3 Format系列函数
Format 系列函数实现了将给定类型数据格式化为 string 类型数据的功能。
#### 3.6.3.1 FormatBool()
```go
func FormatBool(b bool) string
```
根据 b 的值返回 `"true"``"false"`
#### 3.6.3.2 FormatInt()
```go
func FormatInt(i int64, base int) string
```
返回 i 的 base 进制的字符串表示。`base` 必须在2到36之间当 base 大于 10 时,结果中会使用小写字母 `a``z` 表示大于 10 的数字。
#### 3.6.3.3 FormatUint()
```go
func FormatUint(i uint64, base int) string
```
是 FormatInt 的无符号整数版本。
#### 3.6.3.4 FormatFloat()
```go
func FormatFloat(f float64, fmt byte, prec, bitSize int) string
```
函数将浮点数表示为字符串并返回。
* `bitSize` 表示 f 的来源类型32float32、64float64会据此进行舍入。
* `fmt` 表示格式:`f`-ddd.dddd`b`-ddddp±ddd指数为二进制`e`-d.dddde±dd十进制指数`E`-d.ddddE±dd十进制指数`g`(指数很大时用`e`格式,否则`f`格式)、`G`(指数很大时用`E`格式,否则`f`格式)。
* `prec` 控制精度(排除指数部分):**对 `f``e``E`,它表示小数点后的数字个数;对 `g``G`,它控制总的数字个数。如果 prec 为 -1则代表使用最少数量的、但又必需的数字来表示 f 。**
#### 3.6.3.5 代码示例
```go
s1 := strconv.FormatBool(true)
s2 := strconv.FormatFloat(3.1415, 'E', -1, 64)
s3 := strconv.FormatInt(-2, 16)
s4 := strconv.FormatUint(2, 16)
```
### 3.6.4 其他
#### 3.6.4.1 isPrint()
```go
func IsPrint(r rune) bool
```
返回一个字符是否是可打印的,和 `unicode.IsPrint` 一样r 必须是:`字母(广义)``数字``标点``符号``ASCII空格`
#### 3.6.4.2 CanBackquote()
```go
func CanBackquote(s string) bool
```
返回字符串 s 是否可以不被修改的表示为一个`单行的、没有空格``tab之外控制字符`的**反引号字符串**。
#### 3.6.4.3 其他
除上文列出的函数外strconv 包中还有 `Append`系列、`Quote`系列等函数。具体用法可查看官方文档。
[更多函数请查看官方文档。](http://docscn.studygolang.com/pkg/strconv/)

View File

@@ -0,0 +1,363 @@
# 4 流程控制和运算符
Go 语言中常用的流程控制有 `if``for`。而 `switch``goto` 主要是为了简化代码、降低重复代码而生的结构,属于扩展类的流程控制。
## 4.1 if 语句
### 4.1.1 if 语句的基本格式
```go
if 表达式1 {
分支1
} else if 表达式2 {
分支2
} else {
分支3
}
```
### 4.1.2 if 的特殊写法
```go
// 此处声明的变量 score 只在 if 语句中有效
if score := 65; score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}
```
## 4.2 for 语句
go 语言中所有的循环类型都可以使用 `for` 关键字来完成。
### 4.2.1 for 基本格式
```
for 初始语句; 条件表达式; 结束语句 {
循环体语句
}
```
```go
for i := 0; i < 10; i++ {
fmt.Println(i)
}
```
### 4.2.2 省略初始值的样式
初始语句可以被省略,但初始语句后的分号不能被省略
```go
i := 0
for ; i < 5; i++ {
fmt.Println(i)
}
```
### 4.2.3 省略初始语句和结束语句
```go
i := 0
for i < 5 {
fmt.Println(i)
i++
}
```
### 4.2.4 死循环
```go
for {
// 循环语句
}
```
### 4.2.5 `for range`
Go 语言中可以使用 `for range` 遍历数组、切片、字符串、map 以及通道channel.通过 `for range` 遍历的返回值有如下规律:
* 数组、切片、字符串返回索引和值
* map 返回键和值
* 通道channel只返回通道中的值
```go
package main
import (
"fmt"
)
func main() {
str := "hello,中国"
for index, value := range str {
fmt.Printf("%d %c\n", index, value)
}
}
```
输出结果如下:
```go
0 h
1 e
2 l
3 l
4 o
5 ,
6
9
```
上述示例中,由于`中``国`占3个字节所以`国`的索引从 9 开始。
### 4.2.6 跳出 for 循环——break
```go
package main
import (
"fmt"
)
func main() {
for i := 0; i < 10; i++ {
if i == 5 {
// 跳出 for 循环
break
}
fmt.Println(i)
}
fmt.Println("循环结束")
}
```
输出结果为:
```go
0
1
2
3
4
循环结束
```
### 4.2.7 跳过某次循环——continue
```go
package main
import (
"fmt"
)
func main() {
for i := 0; i < 10; i++ {
if i == 5 {
// 跳出 for 循环
continue
}
fmt.Println(i)
}
fmt.Println("循环结束")
}
```
输出结果为:
```go
0
1
2
3
4
6
7
8
9
循环结束
```
## 4.3 switch 语句
### 4.3.1 switch 基本写法
```go
package main
import (
"fmt"
)
func main() {
finger := 3
switch finger {
case 1:
fmt.Println("大拇指")
case 2:
fmt.Println("食指")
case 3:
fmt.Println("中指")
case 4:
fmt.Println("无名指")
case 5:
fmt.Println("小拇指")
default:
fmt.Println("输入无效")
}
}
```
### 4.3.2 一个分支有多个值
```go
package main
import (
"fmt"
)
func main() {
// 声明仅在 switch 中有效的变量 n
switch n := 3; n {
case 1, 3, 5, 7, 9:
fmt.Println("奇数")
case 2, 4, 6, 8:
fmt.Println("偶数")
default:
fmt.Println("其他数值")
}
}
```
### 4.3.3 case 语句可以是表达式
```go
package main
import (
"fmt"
)
func main() {
age := 3
switch {
case age < 18:
fmt.Println("未成年人")
case age > 50:
fmt.Println("老年人")
default:
fmt.Println("中年人")
}
}
```
### 4.3.3 case 语句穿透--fallthrough
```go
package main
import (
"fmt"
)
func main() {
s := "a"
switch {
case s == "a":
fmt.Println("a")
fallthrough
case s == "b":
fmt.Println("b")
case s == "c":
fmt.Println("c")
default:
fmt.Println("其他字符")
}
}
```
输出结果:
```go
a
b
```
因为语句中使用了 `fallthrough`,所以打印完 a 之后还会打印一个 b
## 4.4 goto
`goto` 语句通过标签进行代码间的无条件跳转。`goto` 常用于快速跳出循环、避免重复退出。
### 4.4.1 快速跳出双层循环
* 未使用 goto 语句时跳出双层循环的写法
```go
package main
import (
"fmt"
)
func main() {
var breakFlag bool
for i := 0; i < 10; i++ {
for j := 0; j < 5; j++ {
if j == 3 {
breakFlag = true
break
}
fmt.Printf("内存循环的值 %d", j)
}
if breakFlag {
break
}
fmt.Printf("外层循环的值 %d", i)
}
}
```
* 使用 goto 后的写法
```go
package main
import (
"fmt"
)
func main() {
for i := 0; i < 10; i++ {
for j := 0; j < 5; j++ {
if j == 3 {
goto myBreakTag
}
fmt.Printf("内存循环的值 %d \n", j)
}
fmt.Printf("外层循环的值 %d \n", i)
}
myBreakTag:
fmt.Println("使用 goto 快速跳出双层循环,并指向自定义的 TAG")
}
```
运行结果:
```go
内存循环的值 0
内存循环的值 1
内存循环的值 2
使用 goto 快速跳出双层循环并指向自定义的 TAG
```

View File

@@ -0,0 +1,44 @@
# 5 运算符
go 语言中内置的运算符有:
* 算术运算符
* 关系运算符
* 逻辑运算符
* 位运算符
* 赋值运算符
## 5.1 算数运算符
+、-、*、/、%
## 5.2 关系运算符
==、!=、>、<、>=、<=
## 5.3 逻辑运算符
&&、||、!
## 5.4 位运算符
位运算符对证书在内存中的二进制位进行操作。
运算符|描述
---|---
`&` | 参与运算的两数各对应的二进位相与两位均为1才为1
`|` | 参与运算的两数各对应的二进制位相或两位有一个1就为1)
`^` | 参与运算的两数各对应的二进制位相异或两位不一样时为1
`<<` | 左移 n 位就是乘以 2 的 n 次方。`a<<b` 是把 a 的各二进制位全部左移 b 位,高位丢弃,低位补 0
`>>` | 右移 n 位就是除以 2 的 n 次方。`a>>b` 是把 a 的各二进制位全部右移 b 位。
```go
func main() {
// 5 的二进制数101 2 的二进制 010相与之后得到 000
fmt.Println(5 & 2)
}
```
## 5.5 赋值运算符
=、+=、=+、*=、/=、%=、<<=、>>=、&=、!=、^=、

View File

@@ -0,0 +1,302 @@
# 6 数组
## 6.1 数组的定义
数组在定义时就需要声明其元素数量和类型:
```go
// T 即元素类型
var 数组变量名 [元素数量] T
```
如:`var a [5]int`.
**数组的长度必须是常量,并且长度是数组类型的一部分**,一旦定义,长度不能变。所以,`[5]int``[10]int` 是不同的类型。
```go
package main
import "fmt"
func main() {
var a [3]bool
var b [6]bool
// a的类型是-[3]bool,b 的类型是-[6]bool
fmt.Printf("a的类型是-%T,b 的类型是-%T", a, b)
}
```
数组可以通过下表进行访问,下标从 `0` 开始,最后一个元素下标为:`len-1`
## 6.2 数组的初始化
### 6.2.1 方式1——通过初始化列表设置值
```go
package main
import "fmt"
func main() {
// 未初始化时元素取类型默认值零值bool 零值为 false; 整型和浮点型的零值为 0字符串的零值为""
var arr1 [3]int
var arr2 = [3]int{1, 2, 3}
var strArr = [3]string{"北京", "上海", "广州"}
// [0 0 0]
fmt.Println(arr1)
// [1 2 3]
fmt.Println(arr2)
// [北京 上海 广州]
fmt.Println(strArr)
}
```
### 6.2.2 初始化方式2——`...`
```go
package main
import "fmt"
func main() {
// ... 表示让系统根据初始值自己去数元素数量
arr1 := [...]int{1, 2, 3, 4, 5, 6, 7, 8}
fmt.Println(arr1)
}
```
### 6.2.3 初始化方式3——使用零值补足元素
```go
package main
import "fmt"
func main() {
// 初始时元素真实数量小于声明数量时,使用元素类型对应的默认值补足
var arr1 = [5]int{1, 2, 3}
// [1 2 3 0 0]
fmt.Println(arr1)
}
```
### 6.2.4 初始化方式4——指定索引对应的值
```go
package main
import "fmt"
func main() {
// 初始时元素真实数量小于声明数量时,可以指定某个索引对应的值,未指定的部分使用默认值补足
var arr1 = [5]int{0: 1, 4: 2}
// [1 0 0 0 2]
fmt.Println(arr1)
}
```
## 6.3 数组的遍历
可以使用 `for``for-range` 两种方式
```go
package main
import (
"fmt"
)
func main() {
arr1 := [...]int{1, 2, 3, 4, 5, 6, 7}
for i := 0; i < len(arr1); i++ {
// 获取数组指定索引位置对应的元素并打印
fmt.Println(arr1[i])
}
for index, intValue := range arr1 {
fmt.Printf("索引 %d 对应的值为 %d \n", index, intValue)
}
}
```
## 6.4 多维数组
### 6.4.1 多维数组的声明和初始化
`var arr [3][2]int` 表示声明一个二维数组,该二维数组有三个元素,每个元素都是一个有两个 int 元素的数组。
```go
package main
import "fmt"
func main() {
var arr [3][2]int
arr = [3][2]int{
// 二维数组中,每个一维数组后面都必须跟一个逗号,否则报错。
[2]int{1, 2},
// 可以省略以为数组类型的声明,直接写值
{3, 4},
{5, 6},
}
// [[1 2] [3 4] [5 6]]
fmt.Println(arr)
}
```
在声明多维数组时,只有最外层可以使用 `...`.如:
```go
package main
import "fmt"
func main() {
arr := [...][2]int{
{1, 2},
{3, 4},
{5, 6},
}
// [[1 2] [3 4] [5 6]]
fmt.Println(arr)
}
```
### 6.4.2 多维数组的遍历
嵌套 for 循环
```go
package main
import "fmt"
func main() {
var arr [3][2]int
arr = [3][2]int{
{1, 2},
{3, 4},
{5, 6},
}
for _, v1 := range arr {
for _, v2 := range v1 {
fmt.Printf("%d \t", v2)
}
fmt.Println()
}
}
```
输出结果如下:
```go
1 2
3 4
5 6
```
### 6.4.3 数组是值类型(值传递)
数组是值类型(值传递),赋值和传参会复制整个数组。因此改变副本的值并不会改变其本身。
```go
package main
import "fmt"
func main() {
a := [...]int{1, 2, 3}
modifyArr1(a)
// [1 2 3]
fmt.Println(a)
b := [...][2]int{
{1, 2}, {3, 4}, {5, 6},
}
modifyArr2(b)
// [[1 2] [3 4] [5 6]]
fmt.Println(b)
}
func modifyArr1(x [3]int) {
x[0] = 100
}
func modifyArr2(y [3][2]int) {
y[2][0] = 100
}
```
```go
package main
import "fmt"
func main() {
a := [...]int{1, 2, 3}
b := a
b[0] = 100
// [1 2 3]
fmt.Println(a)
// [100 2 3]
fmt.Println(b)
}
```
## 6.5 数组比较
* 数组支持 `==``!=` 操作符,因为内存总是被初始化过的
* `[n]*T` 表示元素为指针的数组;`*[n]T` 表示数组的指针
## 6.6 练习题
### 6.6.1 求数组中所有元素的和
`[1,3,5,7,8]` 中元素的和
```go
package main
import "fmt"
func main() {
a := [...]int{1, 3, 5, 7, 8}
sum := 0
for _, v := range a {
sum += v
}
fmt.Println(sum)
}
```
### 6.6.2 找出数组中和为指定值的两个元素的下标
`[1,3,5,7,8]` 中找出和为8的两个元素的下标
```go
package main
import "fmt"
func main() {
a := [...]int{1, 3, 5, 7, 8}
for outIndex, v1 := range a {
for i := outIndex + 1; i < len(a); i++ {
v2 := a[i]
if v1+v2 == 8 {
fmt.Printf("索引值为 %d%d \n", outIndex, i)
}
}
}
}
```

View File

@@ -0,0 +1,520 @@
# 7 切片
由于数组的长度时固定的且数组的长度属于类型的一部分,所以数组有很多局限性。如:
```go
func arrSum(x [3]int)int{
sum :=0
for _, v := range x {
sum += v
}
return sum
}
```
上述代码中,只能接受 `[3]int` 类型,其他的都不支持。再比如:
```go
x := [3]int{1,2,3}
```
上述代码中,数组 x 中已经有 3 个值了,我们无法再继续向其中添加新的元素了。
基于数组的如上缺陷,我们就需要使用 `切片(slice)`
**切片是一个相同类型元素的可变长度的序列。它是基于数组类型的一种封装。**
**切片是一个引用类型,它的内部结构包括:`地址`、`长度`、`容量`**。切片多用于快速操作一块数据集合。
## 7.1 切片的定义
### 7.1.1 基本定义格式
声明切片类型的基本语法如下:
```go
var name [] T
```
其中:
* name 是变量名
* T 表示切片中的元素类型
* `[ ]` 内不需要声明长度
* 切片是引用类型,所以两个切片无法直接进行比较,切片只能和 `nil` 做比较。( nil 表示未初始化的引用类型变量
* **切片有自己的长度和容量,我们可以通过使用内置的 `len(切片名)` 来获取长度,通过 `cap(切片名)` 获取切片的容量。**
示例如下:
```go
package main
import "fmt"
func main() {
// 声明一个字符串类型的切片
var a []string
// 声明一个 int 类型的切片并执行初始化
var b = []int{}
// 声明一个 bool 类型的切片并执行初始化
c := []bool{false, true}
// [ ]
fmt.Println(a)
// [ ]
fmt.Println(b)
//[false true]
fmt.Println(c)
// true —— 因为 a 没有初始化,所以为 nil
fmt.Println(nil == a)
// false —— 因为 b 虽然没有值,但已经初始化了
fmt.Println(nil == b)
//a 的长度为0,容量为0
fmt.Printf("a 的长度为:%d,容量为:%d \n", len(a), cap(a))
//b 的长度为0容量为0
fmt.Printf("b 的长度为:%d容量为%d \n", len(b), cap(b))
//c 的长度为2容量为2
fmt.Printf("c 的长度为:%d容量为%d \n", len(c), cap(c))
}
```
### 7.1.2 基于数组定义切片
基于数组定义切片的格式为:**`切片名 := 数组名[起始索引:结束索引]`,**
其中的 `起始索引:结束索引` 遵循前闭后开的原则,也就是说,实际取的是`起始索引``结束索引` 之间的值,包含`起始索引`,但不包含`结束索引`
```go
package main
import "fmt"
func main() {
// 定义并初始化一个 [5]int 类型的数组
a := [5]int{1, 2, 3, 4, 5}
// 基于数组 a 创建切片,取索引 1-4 的元素,该索引前闭后开,所以,实际取的是索引 123 对应的元素
b := a[1:4]
// [2 3 4]
fmt.Println(b)
// b 的类型是:[]int
fmt.Printf("b 的类型是:%T\n", b)
// 还支持如下切割方式:
// 从索引 1 开始切割到最后
c := a[1:]
// [2 3 4 5]
fmt.Println(c)
// 截止到索引为4的元素——不包含该元素
d := a[:4]
//[1 2 3 4]
fmt.Println(d)
// 截取全部
e := a[:]
// [1 2 3 4 5]
fmt.Println(e)
}
```
基于上述代码,我们再分别来获取不同切片的长度和容量:
```go
// b 的长度和容量分别为34
fmt.Printf("b 的长度和容量分别为:%d%d \n", len(b), cap(b))
// c 的长度和容量分别为44
fmt.Printf("c 的长度和容量分别为:%d%d \n", len(c), cap(c))
// d 的长度和容量分别为45
fmt.Printf("d 的长度和容量分别为:%d%d \n", len(d), cap(d))
// e 的长度和容量分别为55
fmt.Printf("e 的长度和容量分别为:%d%d \n", len(e), cap(e))
```
通过上述代码我们可以分析得知:
* **切片的长度 = 结束索引 - 起始索引**
* 切片的底层实际是指向了一个数组
* 切片的容量取决于其底层依赖的数组,**切片的容量 = 数组的长度 - 起始索引**
切片的长度和容量与底层数组的关系可以参考如下两个图:
![](pics/7-1-切片的长度和容量与底层数组的关系.png)
![](pics/7-2-切片的长度和容量与底层数组的关系2.png)
### 7.1.3 切片再切片
```go
package main
import "fmt"
func main() {
// 定义并初始化一个数组,此处 ... 的意思是让数组自己计算长度
a := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
b := a[1:3]
c := b[1:5]
// a 的类型:[9]int,长度9,容量9,元素内容:[1 2 3 4 5 6 7 8 9]
fmt.Printf("a 的类型:%T,长度:%d,容量:%d,元素内容:%v \n", a, len(a), cap(a), a)
// b 的类型:[]int,长度2,容量8,元素内容:[2 3]
fmt.Printf("b 的类型:%T,长度:%d,容量:%d,元素内容:%v \n", b, len(b), cap(b), b)
// c 的类型:[]int,长度4,容量7,元素内容:[3 4 5 6]
fmt.Printf("c 的类型:%T,长度:%d,容量:%d,元素内容:%v \n", c, len(c), cap(c), c)
}
```
上述代码中,`b := a[1:3]` 得到切片之后b 对应的底层数组此时就相应的变成了 `[...]int{2,3,4,5,6,7,8,9}``c := b[1:5]` 表示基于 b 切片底层的数组再做切片,所以 c 切片的数据为 `[3,4,5,6]` 其底层对应的数组为 `[...]int{3,4,5,6,7,8,9}`
需要注意的是:对切片再做切片时,索引不能超过原切片底层数组的长度,否则会出现索引越界的错误。
基于上面的代码我们再看下面的例子:
```go
b[1] = 20
// [1 2 20 4 5 6 7 8 9]
fmt.Println(a)
// [2 20]
fmt.Println(b)
// [20 4 5 6]
fmt.Println(c)
```
我们通过 `b[1] = 20` 修改了切片 b 中的第一个元素值,通过打印发现其底层数组 a、切片 b 、切片 c 都发生了变化,这是因为,**切片是引用类型数据**。
### 7.1.4 使用 `make()` 构造切片
通过 `make()` 函数可以动态的构建一个切片,其格式如下下:
```go
make( []T, size, cap)
```
其中:
* T 是切片元素的类型
* size 是切片中元素的数量
* cap 是切片的容量
```go
package main
import "fmt"
func main() {
a := make([]int, 2, 10)
// [0 0]
fmt.Println(a)
// 2
fmt.Println(len(a))
// 10
fmt.Println(cap(a))
b := make([]int, 0, 10)
// 长度0,容量10,元素:[]
fmt.Printf("长度:%d,容量:%d,元素:%v", len(b), cap(b), b)
}
```
上述代码中,`a` 的内部存储空间已经分配了 10 个,但实际上只用了 2 个。容量并不会影响当前元素的个数,所以,`len(a)` 的值为 2`cap(a)` 的值为 10 。
## 7.2 切片的本质
切片的本质是对底层数组的封装它包含了三个信息底层数组的指针、切片的长度len和切片的容量cap。真正的数据存储还是操作的底层数组。
### 7.2.1 切片赋值
```go
package main
import "fmt"
func main() {
s3 := []int{1, 3, 5}
s4 := s3
// [1 3 5] [1 3 5]
fmt.Println(s3, s4)
s3[0] = 100
// [100 3 5] [100 3 5]——修改切片元素时本质是修改了其底层数组s3 和 s4 都指向同一个数组,所以,切片 s3 和 s4 都会发生变化
fmt.Println(s3, s4)
}
```
### 7.2.2 切片的遍历
#### 7.2.2.1 索引遍历
```go
package main
import "fmt"
func main() {
s3 := []int{1, 3, 5}
for i := 0; i < len(s3); i++ {
fmt.Println(s3[i])
}
}
```
#### 7.2.2.2 `for-range` 遍历
```go
package main
import "fmt"
func main() {
s3 := []int{1, 3, 5}
for _, v := range s3 {
fmt.Println(v)
}
}
```
## 7.3 通过 `append()` 为切片追加元素
### 7.3.1 追加元素的基本使用
Go 语言的内建函数 `切片A = append(切片A,被追加的数据)` 可以为切片动态添加元素。
> 上述格式的含义是,将数据追加到切片 A 中,这样会产生一个新的切片,继续使用原切片 A 来接收新的切片。
每个切片都会指向一个底层数组,该数组能容纳一定数量的元素。
当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略**进行“扩容”,此时该切片执行的底层数组就会更换。**
```go
package main
import "fmt"
func main() {
var numSlice []int
for i := 0; i < 10; i++ {
// 必须使用原切片来接收追加后得到的新切片
numSlice = append(numSlice, i)
fmt.Printf("长度:%d, 容量:%d,指针地址:%p,元素内容:%v\n", len(numSlice), cap(numSlice), numSlice, numSlice)
}
}
```
输出结果:
```go
长度1, 容量1,指针地址0xc0000b4008,元素内容[0]
长度2, 容量2,指针地址0xc0000b4030,元素内容[0 1]
长度3, 容量4,指针地址0xc0000b8020,元素内容[0 1 2]
长度4, 容量4,指针地址0xc0000b8020,元素内容[0 1 2 3]
长度5, 容量8,指针地址0xc0000ba040,元素内容[0 1 2 3 4]
长度6, 容量8,指针地址0xc0000ba040,元素内容[0 1 2 3 4 5]
长度7, 容量8,指针地址0xc0000ba040,元素内容[0 1 2 3 4 5 6]
长度8, 容量8,指针地址0xc0000ba040,元素内容[0 1 2 3 4 5 6 7]
长度9, 容量16,指针地址0xc0000bc080,元素内容[0 1 2 3 4 5 6 7 8]
长度10, 容量16,指针地址0xc0000bc080,元素内容[0 1 2 3 4 5 6 7 8 9]
```
切片扩容的规则大致如下:
* 如果新申请的容量大于旧有容量的 2 倍,则新申请的容量即为该切片的容量。否则,
* 如果旧切片的长度小于 1024则最终容量是为旧有容量的 2 倍。否则,
* 如果旧切片的长度大于等于 1024 则最终容量newcap) 从旧容量old.cap开始循环增加原来的 1/4`newcap = old.cap, for{ newcap += newcap/4}`, 直到最终容量newcap大于等于新申请的容量cap`newcap>=cap`.
* 如果最终容量计算值溢出,则最终容量就是新申请的容量。
> 切片扩容时还会根据切片中元素的类型不同而做不同的处理。
### 7.3.2 一次追加多个元素到切片
```go
package main
import "fmt"
func main() {
var numSlice []int
// 一次追加单个元素
numSlice = append(numSlice, 1)
// [1]
fmt.Println(numSlice)
// 一次追加多个元素
numSlice = append(numSlice, 2, 3, 4)
//[1 2 3 4]
fmt.Println(numSlice)
// 将一个切片追击到另一个切片中注意numSlice1... 后面的三个点表示解构。
numSlice1 := []int{5, 6, 7}
numSlice = append(numSlice, numSlice1...)
//[1 2 3 4 5 6 7]
fmt.Println(numSlice)
}
```
## 7.4 使用 `copy()` 函数复制切片
先看一个例子:
```go
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4, 5}
b := a
// [1 2 3 4 5]
fmt.Println(a)
// [1 2 3 4 5]
fmt.Println(b)
b[0] = 100
// [100 2 3 4 5]
fmt.Println(a)
// [100 2 3 4 5]
fmt.Println(b)
}
```
在上述代码中,由于切片是引用类型的,所以 a 和 b 其实都指向了同一块内存地址,因此,修改 b 的同时 a 的值也会发生变化。
Go 语言内建的 `copy()` 函数可以快速的将一个切片的数据赋值到另外一个切片空间中,`cppy()` 函数的使用格式如下:
```go
copy(destSlice , srcSlice)
```
其中:
* destSlice 表示目标切片(即要复制到哪里去)
* srcSlice 数据来源切片(即从哪里复制)
* destSlice 作为 copy 的目标切片时,必须初始化,且长度必须大于等于被拷贝的切片,否则,拷贝出来的内容会被裁剪。
```go
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4, 5}
b := a
// c 作为 copy 的目标切片时,必须初始化,且长度必须大于等于被拷贝的切片,否则,拷贝出来的内容会被裁剪
c := make([]int, 5, 5)
copy(c, b)
// [1 2 3 4 5] [1 2 3 4 5] [1 2 3 4 5]
fmt.Println(a, b, c)
b[0] = 100
// [100 2 3 4 5] [100 2 3 4 5] [1 2 3 4 5]
fmt.Println(a, b, c)
}
```
## 7.5 从切片中删除元素
Go 语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素,代码如下:
```go
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4, 5}
// 通过切片再切片以及追加的方式删除索引为 2 的元素
a = append(a[:2], a[3:]...)
// [1 2 4 5]
fmt.Println(a)
// 长度4容量5
fmt.Printf("长度:%d容量%d\n", len(a), cap(a))
}
```
示例2
```go
package main
import "fmt"
func main() {
arrA := [...]int{1, 2, 3, 4, 5}
sliceA := arrA[:]
// 通过切片再切片以及追加的方式删除索引为 2 的元素
sliceA = append(sliceA[:2], sliceA[3:]...)
//[1 2 4 5 5]
fmt.Println(arrA)
// 长度4容量5, 元素:[1 2 4 5]
fmt.Printf("长度:%d容量%d,元素:%v \n", len(sliceA), cap(sliceA), sliceA)
}
```
上述代码中,先构建了一个数组 arrA , 然后基于该数组得到切片 sliceA . 由于切片的底层是一个数组,所以在 `sliceA = append(sliceA[:2], sliceA[3:]...)` 这一句代码中,通过 `sliceA[:2]` 取到了数组前两个元素,然后通过 `sliceA[3:]...` 取到了索引为 3 和 4 的元素,`append` 操作时,先在数组的 0 和 1 索引处放置 `sliceA[:2]`, 然后追加解构出来的 `sliceA[3:]...` 中的元素,也就是说 `sliceA[3:]...` 中的元素占用了索引 2、3 , 数组索引 4 处的数据没有发生改变,依旧是原数组中的数据。所以,最终 arrA 的数据为:`[1 2 4 5 5]`
## 7.6 练习题
### 7.6.1 计算下面代码的输出值
```go
package main
import "fmt"
func main() {
a := make([]int, 5, 10)
for i := 0; i < 10; i++ {
a = append(a, i)
}
fmt.Println(a)
}
```
上述代码中,先通过 `a := make([]int, 5, 10)` 初始化了一个切片,其长度为 5 ,容量为 10 此时a 中的元素为 `[0,0,0,0,0]` , 然后在 for 循环中做了的操作是向 a 中追加数据,所以,最终得到的结果为:
```go
[0 0 0 0 0 0 1 2 3 4 5 6 7 8 9]
```
### 7.6.2 使用内置的 `sort` 对数组进行排序
数组:`var a=[...]int{3,7,8,9,1}`
```go
package main
import (
"fmt"
"sort"
)
func main() {
a1 := [...]int{3, 7, 8, 9, 1}
// 先将数组转换成切片,然后使用 sort 排序,因为数组中的元素为 int ,所以此处使用了 sort.Ints
sort.Ints(a1[:])
// [1 3 7 8 9]
fmt.Println(a1)
}
```

View File

@@ -0,0 +1,135 @@
# 8 指针、make、new
## 8.1 指针pointer
Go 语言中没有指针操作,只需要记住两个符号即可:
* `&` 取内存地址
* `*` 根据地址取值
```go
package main
import "fmt"
func main() {
a := 18
// 获取 a 的地址值并复制给 p
p := &a
// p 的类型:*intp 的取值0xc000018078
fmt.Printf("p 的类型:%Tp 的取值:%v\n", p, p)
b := *p
// b 的类型intb 的取值18
fmt.Printf("b 的类型:%Tb 的取值:%v\n", b, b)
}
```
指针传值示例:
```go
package main
import "fmt"
func main() {
a := 10
modify1(a)
// 10
fmt.Println(a)
modify2(&a)
// 100
fmt.Println(a)
}
func modify1(x int) {
x = 100
}
// 需要传入一个 int 变量的地址值
func modify2(x *int) {
*x = 100
}
```
## 8.2 make 和 new
先看一段代码,
```go
package main
import "fmt"
func main() {
var a *int
*a = 100
fmt.Println(a)
var b map[string]int
b["xxx"] = 100
fmt.Println(b)
}
```
执行上面的代码会触发 panic 即异常。错误信息为panic: runtime error: invalid memory address or nil pointer dereference.——非法的内存地址或者空指针引用。这是因为:
在 Go 语言中,引用类型数据声明之后还必须初始化,初始化的操作就是为其分配内存空间。**`new``make` 的作用就是为引用类型数据分配内存空间。**
> 值类型数据声明之后系统会默认为其分配内存。
### 8.2.1 new
`new` 用来构建内存地址类型数据。
```go
package main
import "fmt"
func main() {
// 构建一个 int 类型的内存地址
a := new(int)
*a = 100
// a 表示的内存地址0xc0000b4008 , a 的值100
fmt.Printf("a 表示的内存地址:%p , a 的值:%d\n", a, *a)
}
```
### 8.2.2 make
make 也是用于分配内存的, 只用于 `slice``map` 以及 `channel` 的内存创建而且他的返回值就是这三种类型本身而不是他们的指针。make 函数的函数签名如下:
```go
func make ( t Type, size ...IntegerType) Type
```
我们在使用 `slice``map` 以及 `channel` 时都需要使用 make 进行初始化,然后才可以对他们进行操作。
```go
package main
import "fmt"
func main() {
// 声明一个键为 string 类型,值为 int 类型的 map
var b map[string]int
// 通过 make 初始化 map, 并指定其容量为 10
b = make(map[string]int, 10)
b["xxx"] = 100
// map[xxx:100]
fmt.Println(b)
}
```
### 8.2.3 new 和 make 的区别
* make 和 new 都是用来申请内存地址的
* new 通常用于给基本数据类型申请内存,返回的是对应类型的指针,如 `*string``*int`
* make 专用于给 map、slice、channe 申请内存空间,返回的是对应的类型本身

View File

@@ -0,0 +1,389 @@
# 9 map
Go 语言中提供的映射关系容器为 `map` ,其内部使用 `散列表hash` 实现。它是一种无序的基于 `key-value` 的数据结构。
Go 语言中的 map 是引用类型,必须初始化之后才能使用。
## 9.1 map 定义
Go 语言中 map 的定义语法为:`map[keyType]valueType`,其中:
* keyType 表示键的类型
* valueType 表示值的类型
map 类型变量默认初始值为 nil (引用类型的默认初始值都为 nil), 需要使用 `make()` 函数来分配内存,语法格式为:
```go
make(map[keyType]valueType , cap )
```
上述格式中,`cap` 表示 map 的容量不是必须的map 可以动态扩容。但我们通常会在初始化的时候就指定一个合适的容量,因为这样会比动态扩容的执行效率高。
```go
package main
import "fmt"
func main() {
// 声明一个键为 string 类型,值为 int 类型的 map
var b map[string]int
// true
fmt.Println(nil == b)
// 通过 make 初始化 map, 并指定其长度为 10。 map 可以自动扩容,但不如声明时指定容量的执行效率高。
b = make(map[string]int, 10)
b["aa"] = 100
b["bb"] = 100
//map[aa:100 bb:100]
fmt.Println(b)
}
```
## 9.2 map 的基本使用
### 9.2.1 增值和取值
```go
package main
import "fmt"
func main() {
var b map[string]int
// true
fmt.Println(nil == b)
b = make(map[string]int, 10)
b["aa"] = 100
b["bb"] = 100
// 获取键对应的值时,使用 map名称[键名] 的格式
fmt.Println(b["aa"])
// 不确定是否存在某个键时使用这种方式获取其值。ok 表示是否有该键v 表示如果有该键时的值
v, ok := b["cc"]
if !ok {
fmt.Println("b 中不存在键 cc")
} else {
fmt.Println("cc对应的值为", v)
}
}
```
### 9.2.1 删除某个键值对
删除时使用内置函数 `delete`, 该函数的定义如下:
```go
func delete(m map[Type]Type1, key Type)
```
如果被删除的键存在,直接删除,不存在,则不执行任何操作。
```go
package main
import (
"fmt"
)
func main() {
scoreMap := make(map[string]int, 10)
scoreMap["张三"] = 93
scoreMap["李四"] = 94
scoreMap["王五"] = 95
delete(scoreMap, "张三")
}
```
## 9.3 map 的遍历
### 9.3.1 `for-range` 遍历
```go
package main
import "fmt"
func main() {
scoreMap := make(map[string]int, 10)
scoreMap["张三"] = 93
scoreMap["李四"] = 94
scoreMap["王五"] = 95
for k, v := range scoreMap {
fmt.Println(k, v)
}
}
```
### 9.3.2 只遍历 key
```go
package main
import (
"fmt"
)
func main() {
scoreMap := make(map[string]int, 10)
scoreMap["张三"] = 93
scoreMap["李四"] = 94
scoreMap["王五"] = 95
for k := range scoreMap {
fmt.Println(k, scoreMap[k])
}
}
```
### 9.3.3 只遍历 value
```go
package main
import (
"fmt"
)
func main() {
scoreMap := make(map[string]int, 10)
scoreMap["张三"] = 93
scoreMap["李四"] = 94
scoreMap["王五"] = 95
for _, v := range scoreMap {
fmt.Println(v)
}
}
```
### 9.3.4 按照指定顺序遍历
Go 语言中没有 map 专用的排序,需要借助切片的排序实现。
```go
package main
import (
"fmt"
"math/rand"
"sort"
"time"
)
func main() {
//初始化随机种子
rand.Seed(time.Now().UnixNano())
scoreMap := make(map[string]int, 150)
for i := 0; i < 100; i++ {
// 生成 stu 开头的字符串.此处的 %2d 表示使用两位数表示不足两位则左边补0
key := fmt.Sprintf("stu%02d", i)
// 生成 0-99 的随机整数
value := rand.Intn(100)
scoreMap[key] = value
}
// 取出 map 中的所有 key 存入切片
keys := make([]string, 0, 200)
for k := range scoreMap {
keys = append(keys, k)
}
// 对切片进行排序
sort.Strings(keys)
// 对排序后的切片进行遍历,并取 map 中的值
for _, k := range keys {
fmt.Println(k, scoreMap[k])
}
}
```
## 9.4 其他相关
### 9.4.1 元素为 map 的切片
```go
package main
import (
"fmt"
)
func main() {
// 构建一个切片,容量为 3元素为 map[string]string
var mapSlice = make([]map[string]string, 3)
for index, v := range mapSlice {
fmt.Printf("index:%d, value:%v \n", index, v)
}
fmt.Println()
// 对切片中的元素进行初始化, 不初始化会报错——map、slice、channel 使用前必须初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "张三"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "济南"
for index, v := range mapSlice {
fmt.Printf("index:%d, value:%v\n", index, v)
}
}
```
运行结果如下:
```go
index:0, value:map[]
index:1, value:map[]
index:2, value:map[]
index:0, value:map[address:济南 name:张三 password:123456]
index:1, value:map[]
index:2, value:map[]
```
### 9.4.2 值为切片类型的 map
```go
package main
import "fmt"
func main() {
// 构建一个 map, 容量为 3元素类型为 []string 切片
var sliceMap = make(map[string][]string, 3)
// map[]
fmt.Println(sliceMap)
k := "中国"
value, ok := sliceMap[k]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海")
sliceMap[k] = value
// map[中国:[北京 上海]]
fmt.Println(sliceMap)
}
```
```go
package main
import "fmt"
func main() {
// 构建一个 map, 容量为 3元素类型为 []string 切片
var sliceMap = make(map[string][]int, 3)
sliceMap["北京"] = []int{1, 2, 3, 4, 5}
// map[北京:[1 2 3 4 5]]
fmt.Println(sliceMap)
}
```
## 9.5 作业
### 9.5.1 判断字符串中汉字的数量
思路:
* 依次获取每个字符
* 判断字符是不是汉字
* 把汉字出现的次数累加
```go
package main
import (
"fmt"
"unicode"
)
func main() {
s1 := "我是 CnPeng,我在济南"
var count int
for _, c := range s1 {
// 判断是不是汉字
if unicode.Is(unicode.Han, c) {
count++
}
}
fmt.Println(count)
}
```
### 9.5.2 统计单词出现的次数:
```go
package main
import (
"fmt"
"strings"
)
func main() {
s1 := "how do you do "
strSlice := strings.Split(s1, " ")
strMap := make(map[string]int, 10)
for _, w := range strSlice {
if _, ok := strMap[w]; !ok {
strMap[w] = 1
} else {
strMap[w]++
}
}
for k, v := range strMap {
fmt.Println(k, v)
}
}
```
### 9.5.2 回文判断
一个字符串从左向右读和从右向左读含义一致,就称为回文。如:
“上海自来水来自海上”、“山西运煤车煤运西山”、“黄山落叶松叶落山黄”
```go
package main
import "fmt"
func main() {
s1 := "山西运煤车煤运西山"
// 规律s1[0]==s[len(ss)-1]
// s1[1]==s[len(ss)-1-1]
// s1[2]==s[len(ss)-1-2]
// s1[3]==s[len(ss)-1-3]
// 。。。s1[i]==s[len(ss)-1-i]
// 将字符串转换成 rune 切片
r := make([]rune, 0, len(s1))
for _, c := range s1 {
r = append(r, c)
}
// 只比较前面一半和后面一个就可以
for i := 0; i < len(r)/2; i++ {
if r[i] != r[len(r)-1-i] {
fmt.Println("不是回文")
return
}
}
}
```

View File

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 291 KiB

View File

Before

Width:  |  Height:  |  Size: 267 KiB

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -0,0 +1,161 @@
# 概述
bufio模块通过对io模块的封装提供了数据缓冲功能能够一定程度减少大块数据读写带来的开销。
实际上在bufio各个组件内部都维护了一个缓冲区数据读写操作都直接通过缓存区进行。当发起一次读写操作时会首先尝试从缓冲区获取数据只有当缓冲区没有数据时才会从数据源获取数据更新缓冲。
# Reader
可以通过`NewReader`函数创建bufio.Reader对象函数接收一个io.Reader作为参数也就是说bufio.Reader不能直接使用需要绑定到某个io.Reader上。函数声明如下
```
func NewReader(rd io.Reader) *Reader
func NewReaderSize(rd io.Reader, size int) *Reader // 可以配置缓冲区的大小
```
相较于io.Readerbufio.Reader提供了很多实用的方法能够更有效的对数据进行读取。首先是几个基础方法它们能够对Reader进行细粒度的操作
- Read读取n个byte数据
- Discard丢弃接下来n个byte数据
- Peek获取当前缓冲区内接下来的n个byte但是不移动指针
- Reset清空整个缓冲区
具体的方法声明如下:
```
func (b *Reader) Read(p []byte) (n int, err error)
func (b *Reader) Discard(n int) (discarded int, err error)
func (b *Reader) Peek(n int) ([]byte, error)
func (b *Reader) Reset(r io.Reader)
```
除了上面的基础操作之外bufio.Reader还提供了多个更高抽象层次的方法对数据进行简单的结构化读取。主要包括如下几个方法
- ReadByte读取一个byte
- ReadRune读取一个utf-8字符
- ReadLine读取一行数据由'\n'分隔
- ReadBytes读取一个byte列表
- ReadString读取一个字符串
其中前三个函数都没有参数会从缓冲区读取一个满足需求的数据。后面两个函数接收一个参数delim用于做数据拆分持续读取数据直到当前字节的值等于delim然后返回这些数据实际上这两个函数功能相同只是在函数返回值的类型上有所区别。具体的方法声明如下
```
func (b *Reader) ReadByte() (byte, error)
func (b *Reader) ReadRune() (r rune, size int, err error)
func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)
func (b *Reader) ReadBytes(delim byte) ([]byte, error)
func (b *Reader) ReadString(delim byte) (string, error)
```
下面是一个简单的示例使用ReadString方法获取用 ’分隔的字符串。
```
package main
import (
"bufio"
"fmt"
"strings"
)
func main() {
r := strings.NewReader("hello world !")
reader := bufio.NewReader(r)
for {
str, err := reader.ReadString(byte(' '))
fmt.Println(str)
if err != nil {
return
}
}
}
```
# Scanner
实际使用中更推荐使用Scanner对数据进行读取而非直接使用Reader类。Scanner可以通过splitFunc将输入数据拆分为多个token然后依次进行读取。
和Reader类似Scanner需要绑定到某个io.Reader上通过NewScannner进行创建函数声明如下
```
func NewScanner(r io.Reader) *Scanner
```
在使用之前还需要设置splitFunc默认为ScanLinessplitFunc用于将输入数据拆分为多个token。bufio模块提供了几个默认splitFunc能够满足大部分场景的需求包括
- ScanBytes按照byte进行拆分
- ScanLines按照行("\n")进行拆分
- ScanRunes按照utf-8字符进行拆分
- ScanWords按照单词(" ")进行拆分
通过Scanner的Split方法可以为Scanner指定splitFunc。使用方法如下
```
scanner := bufio.NewScanner(os.StdIn)
scanner.split(bufio.ScanWords
```
除此了默认的splitFunc之外也可以定义自己的splitFunc函数需要满足如下声明
```
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
```
函数接收两个参数第一个参数是输入数据第二个参数是一个标识位用于标识当前数据是否为结束。函数返回三个参数第一个是本次split操作的指针偏移第二个是当前读取到的token第三个是返回的错误信息。
在完成了Scanner初始化之后通过Scan方法可以在输入中向前读取一个token读取成功返回True使用Text和Bytes方法获取这个tokenText返回一个字符串Bytes返回字节数组。方法声明如下
```
func (s *Scanner) Scan() bool
func (s *Scanner) Text() string
func (s *Scanner) Text() []byte
```
下面的示例使用Scanner对上面的示例进行了重现可以看到和Reader相比Scanner的使用更加便捷。
```
package main
import (
"bufio"
"strings"
"fmt"
)
func main() {
scanner := bufio.NewScanner(strings.NewReader("hello world !"))
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}
```
# Writer
和Reader类似Writer也对应的提供了多组方法。基础方法包括如下几个
```
func (b *Writer) Write(p []byte) (nn int, err error) // 写入n byte数据
func (b *Writer) Reset(w io.Writer) // 重置当前缓冲区
func (b *Writer) Flush() error // 清空当前缓冲区,将数据写入输出
```
此外Writer也提供了多个方法方便我们进行数据写入操作
```
func (b *Writer) WriteByte(c byte) error // 写入一个字节
func (b *Writer) WriteRune(r rune) (size int, err error // 写入一个字符
func (b *Writer) WriteString(s string) (int, error) // 写入一个字符串
```

View File

@@ -0,0 +1,119 @@
# 概述
container/heap包对通用堆进行了定义并实现了标准堆操作函数以此为基础可以很容易对各类堆和优先队列进行实现。
# 类型接口
heap包中最核心的就是heap.Interface接口堆的基础存储是一个树形结构可以用数组或是链表实现。通过heap的函数可以建立堆并在堆上进行操作要使用heap包的函数你的类需要实现heap.Interface接口定义如下
```
// heap.Interface
type Interface interface {
sort.Interface
Push(x interface{}) // 在Len()位置插入一个元素
Pop() interface{} // 删除并返回Len()-1位置的元素
}
// sort.Interface
type Interface interface {
Len() // 获取当前对象的长度
Swap(i, j interface{}) // 交换i,j位置两个元素的位置
Less(i, j interface{}) // 比较i位置元素的值是否小于j位置元素
}
```
在实现了这些接口之后就可以被heap包提供的各个函数进行操作从而实现一个堆。
# 成员函数
heap包中提供了几个最基本的堆操作函数包括InitFixPushPop和Remove。这些函数都通过调用前面实现接口里的方法对堆进行操作。
## Init
Init函数用于堆初始化接受一个实现了heap.Interface的对象并初始化为一个堆所有的堆在使用之前都需要进行初始化Init函数定义为
```
func Init(h Interface)
```
## Fix
Fix函数用于单次对堆进行调整接收一个堆对象以及一个位置参数i其函数定义如下
```
func Fix(h Interface, i int)
```
如果你还记得如何维护一个堆那么应该可以很容易理解这个函数的作用。实际上每次在堆上插入一个元素后堆结构会被破坏需要通过Fix函数将这个元素交换到合适的位置以保证堆的正确性。
## Push&Pop
Push和Pop是一对标准堆操作Push向堆添加一个新元素Pop弹出并返回堆顶元素而在push和pop操作不会破坏堆的结构具体函数定义如下
```
func Pop(h Interface) interface{}
func Pop(h Interface) interface{}
```
## Remove
Remove函数用于删除堆上特定位置的元素这个位置是指元素在堆上的排序其函数定义如下
```
func Remove(h Interface, i int) interface{}
```
# 使用实例
下面是一个简单的例子对上面的内容进行回顾代码实现了一个小顶堆堆中元素为长方形类按照面积大小进行排序使用slice作为基础存储。首先是类定义和接口实现需要实现前面说到的五个接口。
```
type Rectangle struct {
height int
width int
}
func (rec *Rectangle) Area() {
return rec.height * rec.width
type RecHeap []Rectangle
func (h RecHeap) Len() {
return len(h)
}
func (h RecHeap) Swap(i, j interface{}) {
h[i], h[j] = h[j], h[i]
}
func (h RecHeap) Less(i, j interface{}) {
return h[i].Area() < h[j].Area()
}
func (h *RecHeap) Push(h interface{}) {
*h = append(*h, h.(Rectangle)
}
func (h *RecHeap) Pop(h interface{}) {
n := len(*h)
x := *h[n-1]
*h = *h[:n-1]
return x
}
```
完成了接口定义之后就可以通过heap包提供的函数进行堆的操作了首先使用Init进行初始化然后通过Push进行元素的插入Pop进行元素的删除需要注意的一点是heap包并没有提供Top这样一个函数获取当前堆顶元素你可以通过获取slice[0]来获取。
```
import (
"fmt"
"container/heap"
)
func main() {
hp := &[]RecHeap{}
for i := 2; i <= 5; i++ {
*hp = append(*hp, Rectangle{i, i})
// {2, 2}, {3, 3}, {4, 4}, {5, 5}
}
heap.Init(hp) // 初始化
heap.Push(hp, Rectangle{1, 1})
fmt.Printf("minimum: %d\n", (*hp)[0]) // Rectangle{1, 1}
res := heap.Pop(hp)
fmt.Printf("minimum: %d\n", (*hp)[0]) // Rectangle{2, 2}
}
```
你也可以通过修改Less方法将其变为一个大顶堆。
# 参考文献
- [container/heap](https://golang.org/pkg/container/heap/#Interface)

View File

@@ -0,0 +1,81 @@
# 概述
container/list包实现了基本的双向链表功能包括元素的插入、删除、移动功能
# 链表元素
链表中元素定义如下:
```
type Element struct {
Value interface{}
}
func (e *Element) Next() *Element
func (e *Element) Prev() *Element
```
通过Value属性来获取元素的值此外Element还有两个方法Next和Prev分别获取当前元素的前一个元素和后一个元素。
# 成员函数
## 初始化
可以通过调用New函数或者Init方法来初始化一个空的list此外Init也可以重置一个list。函数声明如下
```
func New() *List
func (l *List) Init() *List
```
## 遍历
对于链表来说,遍历是最常用的操作,遍历操作一共三步:
- 第一步获取一个遍历起始点使用Front或Back获取一个链表的头和尾其函数声明如下
```
func (l *List) Front() *List
func (l *List) Back() *List
```
- 第二步从当前元素转到下一个元素使用Element上的Prev和Next方法向前或向后移动一个元素。
- 第三步,遍历结束条件;遍历结束条件需要人为判断,一般比较当前元素是否为结束元素。
## 插入
container/list中提供了两种插入方法InsertAfter和InsertBefore分别用于在一个元素前或后插入元素方法声明如下
```
func (l *List) InsertAfter(v interface{}, mark *Element) *Element
func (l *List) InsertBefore(v interface{}, mark *Element) *Element
```
## 添加
PushBack和PushFront用于在一个链表的头和尾添加元素此外还有一次性添加一个list的PushBackList和PushFrontList方法声明如下
```
func (l *List) PushBack(v interface{}) *Element
func (l *List) PushFront(v interface{}) *Element
```
## 删除
可以通过Remove方法删除链表上指定元素方法声明如下
```
func (l *List) Remove(e *Element) interface{}
```
# 使用实例
实际上,将前面的内容整合起来,就可以实现一个简单的遍历链表的功能。下面是一个简单的遍历实现
```
package main
import (
"fmt"
"container/list"
)
func main() {
link := list.New()
for i := 0; i <= 10; i++ {
link.PushBack(i)
}//
for p := link.Front(); p != link.Back(); p = p.Next() {
fmt.Println("Number", p.Value)
}
}
```
# 参考文献
- [container/list](https://golang.org/pkg/container/list/#List.MoveAfter)

View File

@@ -0,0 +1,104 @@
# 概述
Ring是一种循环链表结构没有头尾从任意一个节点出发都可以遍历整个链。其定义如下Value表示当前节点的值
```
type Ring struct {
Value interface{}
}
```
# 类型方法
## New
Ring.New用于创建一个新的Ring接收一个整形参数用于初始化Ring的长度其方法定义如下
```
func New(n int) *Ring
```
## Next & Prev
作为一个链表最重要的操作进行遍历可以通过Next和Prev方法获取当前节点的上一个节点和下一个节点方法定义如下
```
func (r *Ring) Next() *Ring
func (r *Ring) Prev() *Ring
```
通过这两个方法可以对一个ring进行遍历首先保存当前节点然后依次访问下一个节点直到回到起始节点代码实现如下
```
p := ring.Next()
// do something with first element
for p != ring {
// do something with current element
p = p.Next()
}
```
## Link & Unlink
Link将两个ring连接到一起而Unlink将一个ring拆分为两个移除n个元素并组成一个新的ring这两个操作组合起来可以对多个链表进行管理方法声明如下
```
func (r *Ring) Link(s *Ring) *Ring
func (r *Ring) Unlink(n int) *Ring
```
## Do
前面通过Next方法对ring进行了遍历由于这类操作的广泛存在所以Ring包中还提供了一个额外的方法Do方法接收一个函数作为参数方法声明如下
```
func (r *Ring) Do(f func(interface{}))
```
在调用Ring.Do时会依次将每个节点的Value当做参数调用这个函数实际上这是策略方法的应用通过传递不同的函数可以在同一个ring上实现多种不同的操作。下面展示一个简单的遍历打印程序。
```
package main
import (
"container/ring"
"fmt"
)
func main() {
r := ring.New(10)
for i := 0; i < 10; i++ {
r.Value = i
r = r.Next()
}
sum := SumInt{}
r.Do(func(i interface{}) {
fmt.Println(i)
})
}
```
除了简单的无状态程序外也可以通过结构体保存状态例如下面是一个对ring上值求和的程序。
```
package main
import (
"container/ring"
"fmt"
)
type SumInt struct {
Value int
}
func (s *SumInt) add(i interface{}) {
s.Value += i.(int)
}
func main() {
r := ring.New(10)
for i := 0; i < 10; i++ {
r.Value = i
r = r.Next()
}
sum := SumInt{}
r.Do(sum.add)
fmt.Println(sum.Value)
}
```

View File

@@ -0,0 +1,75 @@
# 概述
context也是并发环境的一个常用标准库它用于在并发环境下在协程之间安全的传递某些上下文信息。
一个经典的应用场景是服务器模型当服务器处理接收到的请求时通常需要并发的运行多个子任务例如访问服务器请求授权等。而这些任务都会以子协程的方式运行也就是说一个请求绑定了多个协程这些协程需要共享或传递某些请求相关的数据此外当请求被撤销时也需要有一种机制保证每个子协程能够安全的退出。而context包就给提供了上面说到的这些功能。
# Context
Context是一个上下文对象其声明如下
```
type Context interface {
Deadline() (deadline time.Time, ok bool)
// 获取deadline
Done() <-chan struct{}
//
Err() error
Value(key interface{}) interface{}
}
```
## 创建Context
在context中有两种基础的Context分别通过Backgroud和TODO函数创建下面是具体的函数声明
```
func Background() Context
func TODO() Context
```
通常情况下使用Backgroud函数即可调用函数可以得到一个Context但是这个Context不能够直接使用只是作为一个基础的根Context使用所有的Context都需要从这个Context上衍生。
# 衍生 Context
要创建一个可使用的Context你需要使用下面的三个函数在根Context衍生出新的Context。当然由于Context是以树状结构存在的你也可以通过调用这些函数在任何一个Context上创建子Context。
## WithCancel
WithCancel会返回一个可以取消的Context函数声明如下
```
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
```
函数接收一个Contex作为参数返回两个值第一个是新创建的Context结构上来看这个Context是输入Context的子节点第二个参数是cancel函数用于向这个Context发送cancel信号。由于Context存在继承关系当父节点调用cancel子节点的cancel也会被调用。
### CancelFunc & Done
这里介绍一下CancelFuncDone这一对函数类似于signalwaitCancelFunc函数会向Context发送cancel信号而Done方法返回一个通道若当前Context被cancel那么这个通道会被关闭也就是说通过CancelFunc和Done的协作可以对子协程传递cancel信号一个常用的代码段如下
```
func Stream(ctx context.Context, out chan<- Value) error {
for {
v, err := DoSomething(ctx)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case out <- v:
}
}
}
```
子协程不停地运行并检查当前任务是否被取消,若被取消则结束当前任务并返回。
## WithDeadLine & WithTimeout
和WithCancel类似WithDeadLine和WithTimeout额外接收一个参数分别是消亡时间和超时时间。也就是说对于这两类Context即使不主动取消当发生超时时该Context也会接收到cancel信号。函数声明如下
```
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
```
同样的即使设置了很大的值但是子Context的deadline和timeout也不会超过父Context的值。
## WithValue
这类Context用于在同一个上下文中传递数据这个Context是不可取消的其函数声明如下
```
func WithValue(parent Context, key, val interface{}) Context
```
除了Context参数外还接收key和val参数用于保存数据数据以键值对的方式存储然后可以通过Context.Value(key)来获取对应的值。
# 一些建议
- 子协程不能cancel父协程的Context
- Context需要显式的传递而不是作为某个类型的一个字段

View File

@@ -0,0 +1,43 @@
# 概述
在go中没有异常捕获机制而是通过一个单独的函数返回值来表示错误信息。
# Error
错误类型的接口定义如下:
```
type error interface {
Error() string
}
```
这个接口只有一个方法`Error`,这个方法返回一个字符串,描述错误的详情。在使用中,通常会通过直接实现`Error`方法来自定义错误类型,同时也可以传递不同的参数对错误状态进行更详细的说明,例如:
```
type AError struct {
MoreInfo string
}
func (err *AError) Error() string {
return fmt.Sprintf("Basic Error Info: %s", err.MoreInfo)
}
```
此外,当使用`fmt.Print`函数打印时,会自动的调用`Error`方法。
# errors
但是对于大多数的自定义错误只需要简单的错误描述上面的声明方法过于繁琐。所以在errors包中提供了一种更简单的方法对错误进行自定义。
`errors.New`接收一个字符串,并返回一个错误对象,该错误对象的`Error`方法返回该字符串,函数声明如下:
```
func New(text string) error
```
有了这个函数,就可以像这样自定义错误对象了:
```
var (
AError = errors.New("AError")
BError = errors.New("BError")
CError = errors.New("CError")
)
```
# 更多内容
- [[1] Error and Go](https://blog.golang.org/error-handling-and-go)

View File

@@ -0,0 +1,198 @@
* [原文链接](https://www.liwenzhou.com/posts/Go/go_flag/)
* [视频 126 ](https://www.bilibili.com/video/BV17Q4y1P7n9?p=126)
Go 语言内置的 flag 包实现了命令行参数的解析flag 包使得开发命令行工具更为简单。
## 22.1 `os.Args`
如果你只是简单的想要获取命令行参数,可以像下面的代码示例一样使用 `os.Args` 来获取命令行参数。
```go
package main
import (
"fmt"
"os"
)
//os.Args demo
func main() {
//os.Args是一个[]string
if len(os.Args) > 0 {
for index, arg := range os.Args {
fmt.Printf("args[%d]=%v\n", index, arg)
}
}
}
```
将上面的代码执行 `go build -o "args_demo"` 编译之后,执行:
```
$ ./args_demo a b c d
args[0]=./args_demo
args[1]=a
args[2]=b
args[3]=c
args[4]=d
```
`os.Args` 是一个存储命令行参数的字符串切片,它的第一个元素是执行文件的名称。
## 22.2 flag 包基本使用
本文介绍了 flag 包的常用函数和基本用法,更详细的内容请查看 [官方文档](https://studygolang.com/pkgdoc)。
### 22.2.1 导入 flag 包
```go
import flag
```
### 22.2.2 flag 参数类型
flag 包支持的命令行参数类型有 `bool``int``int64``uint``uint64``float``float64``string``duration`
flag 参数 | 有效值
---|---
字符串flag | 合法字符串
整数flag | 1234、0664、0x1234等类型也可以是负数。
浮点数flag | 合法浮点数
bool类型flag | 1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False。
时间段flag | 任何合法的时间段字符串。如”300ms”、”-1.5h”、”2h45m”。
合法的单位有”ns”、”us” /“µs”、”ms”、”s”、”m”、”h”。
## 22.3 定义命令行flag参数
有以下两种常用的定义命令行 flag 参数的方法。
### 22.3.1 `flag.Type()`
基本格式如下:
```go
flag.Type(flag名, 默认值, 帮助信息)*Type
```
例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:
```go
name := flag.String("name", "张三", "姓名")
age := flag.Int("age", 18, "年龄")
married := flag.Bool("married", false, "婚否")
delay := flag.Duration("d", 0, "时间间隔")
```
需要注意的是,此时 name、age、married、delay 均为对应类型的指针。
### 22.3.2 `flag.TypeVar()`
基本格式如下:
```go
flag.TypeVar(Type指针, flag名, 默认值, 帮助信息)
```
例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:
```go
var name string
var age int
var married bool
var delay time.Duration
flag.StringVar(&name, "name", "张三", "姓名")
flag.IntVar(&age, "age", 18, "年龄")
flag.BoolVar(&married, "married", false, "婚否")
flag.DurationVar(&delay, "d", 0, "时间间隔")
```
## 22.4 `flag.Parse()`
通过以上两种方法定义好命令行 flag 参数后,需要通过调用 `flag.Parse()` 来对命令行参数进行解析。
支持的命令行参数格式有以下几种:
* `-flag xxx` (使用空格,一个`-`符号)
* `--flag xxx` (使用空格,两个`-`符号)
* `-flag=xxx` (使用等号,一个`-`符号)
* `--flag=xxx` (使用等号,两个`-`符号)
其中,布尔类型的参数必须使用等号的方式指定。
Flag 解析在第一个非 flag 参数(单个 `-` 不是 flag 参数)之前停止,或者在终止符 ``之后停止。
## 22.5 flag 其他函数
```go
flag.Args() ////返回命令行参数后的其他参数,以[]string类型
flag.NArg() //返回命令行参数后的其他参数个数
flag.NFlag() //返回使用的命令行参数个数
```
## 22.6 完整示例
### 22.6.1 定义
```go
func main() {
//定义命令行参数方式1
var name string
var age int
var married bool
var delay time.Duration
flag.StringVar(&name, "name", "张三", "姓名")
flag.IntVar(&age, "age", 18, "年龄")
flag.BoolVar(&married, "married", false, "婚否")
flag.DurationVar(&delay, "d", 0, "延迟的时间间隔")
//解析命令行参数
flag.Parse()
fmt.Println(name, age, married, delay)
//返回命令行参数后的其他参数
fmt.Println(flag.Args())
//返回命令行参数后的其他参数个数
fmt.Println(flag.NArg())
//返回使用的命令行参数个数
fmt.Println(flag.NFlag())
}
```
### 22.6.2 使用
命令行参数使用提示:
```
$ ./flag_demo -help
Usage of ./flag_demo:
-age int
年龄 (default 18)
-d duration
时间间隔
-married
婚否
-name string
姓名 (default "张三")
```
正常使用命令行 flag 参数:
```
$ ./flag_demo -name 沙河娜扎 --age 28 -married=false -d=1h30m
沙河娜扎 28 false 1h30m0s
[]
0
4
```
使用非 flag 命令行参数:
```
$ ./flag_demo a b c
张三 18 false 0s
[a b c]
3
0
```

View File

@@ -0,0 +1,66 @@
# 概述
在运行命令行程序时通常通过命令行参数对程序运行进行配置。在go程序中使用flag包可以快速构建命令行程序对于程序使用者只需要声明所需命令行参数。
# 使用示例
创建命令行程序可以分为两步:
- 声明命令行参数
- 运行`flag.Parse`,对参数进行解析
然后就可以读取命令行参数了。例如如下程序可以创建一个命令行程序`demo --foo hello --bar world`
```
package main
import (
"fmt"
"flag"
)
func main() {
foo := flag.String("foo", false, "Foo")
bar := flag.String("bar", "", "Bar")
flag.Parse()
fmt.Print(*foo, *bar)
}
```
# VarXXX 和 XXX
flag包中对多中提供了两类参数声明函数下面以String类型为例两个函数声明如下
```
func String(name string, value string, usage string) *string
func StringVar(p *string, name string, value string, usage string)
```
String函数接收三个参数分别是命令行参数名`--<name>`参数默认值参数描述也就是help命令显示的帮助描述并返回对应参数值的指针。StringVar与之不同在于将返回值改为函数参数。
除了对string类型的支持外flag包还提供了多个类似的函数用于解析不同类型的参数函数名进行对应的替换即可包括
- IntInt64Uint
- Bool
- Float
- Duration
# 其他
以上几乎就是使用flag包的全部了flag包中的其他函数可以直接对其底层实现进行操作普通命令行程序中不会使用。
在实际使用中建议将参数声明部分放到var代码段中对上面的代码进行修改后如下所示。
```
package main
import (
"fmt"
"flag"
)
var (
foo = flag.Bool("foo", false, "Foo")
bar = flag.String("bar", "", "Bar")
)
func main() {
flag.Parse()
fmt.Print(*foo, *bar)
}
```

View File

@@ -0,0 +1,261 @@
基于原文:[Go语言基础之net/http](https://www.liwenzhou.com/posts/Go/go_http/) 和视频 [120-121](https://www.bilibili.com/video/BV17Q4y1P7n9?p=120) 整理
Go 语言内置的 net/http 包提供了 HTTP 客户端和服务端的实现。
## 19.1 HTTP 协议
`HTTP 协议`即超文本传输协议HTTPHyperText Transfer Protocol)。是互联网上应用最为广泛的一种网络传输协议,所有的 WWW 文件都必须遵守这个标准。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。
## 19.2 HTTP客户端
### 19.2.1 基本的 HTTP/HTTPS 请求
Get、Head、Post 和 PostForm 函数发出 HTTP/HTTPS 请求。
```go
resp, err := http.Get("http://example.com/")
...
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
...
resp, err := http.PostForm("http://example.com/form",
url.Values{"key": {"Value"}, "id": {"123"}})
```
程序在使用完 response 后必须关闭回复的主体。
```go
resp, err := http.Get("http://example.com/")
if err != nil {
// handle error
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
// ...
```
### 19.2.2 GET 请求示例
使用 net/http 包编写一个简单的发送 HTTP 请求的 Client 端,代码如下:
```go
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
resp, err := http.Get("https://www.liwenzhou.com/")
if err != nil {
fmt.Printf("get failed, err:%v\n", err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("read from resp.Body failed, err:%v\n", err)
return
}
fmt.Print(string(body))
}
```
将上面的代码保存之后编译成可执行文件,执行之后就能在终端打印 liwenzhou.com 网站首页的内容了.
我们的浏览器其实就是一个发送和接收 HTTP 协议数据的客户端,我们平时通过浏览器访问网页其实就是从网站的服务器接收 HTTP 数据,然后浏览器会按照 HTML、CSS 等规则将网页渲染展示出来。
### 19.2.3 带参数的 GET 请求示例
关于 GET 请求的参数需要使用 Go 语言内置的 `net/url` 这个标准库来处理。
```go
func main() {
apiUrl := "http://127.0.0.1:9090/get"
// URL param
data := url.Values{}
data.Set("name", "小王子")
data.Set("age", "18")
u, err := url.ParseRequestURI(apiUrl)
if err != nil {
fmt.Printf("parse url requestUrl failed, err:%v\n", err)
}
u.RawQuery = data.Encode() // URL encode
fmt.Println(u.String())
resp, err := http.Get(u.String())
if err != nil {
fmt.Printf("post failed, err:%v\n", err)
return
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("get resp failed, err:%v\n", err)
return
}
fmt.Println(string(b))
}
```
对应的 Server 端 HandlerFunc 如下:
```go
func getHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
data := r.URL.Query()
fmt.Println(data.Get("name"))
fmt.Println(data.Get("age"))
answer := `{"status": "ok"}`
w.Write([]byte(answer))
}
```
### 19.2.4 Post 请求示例
上面演示了使用 net/http 包发送 GET 请求的示例,发送 POST 请求的示例代码如下:
```go
package main
import (
"fmt"
"io/ioutil"
"net/http"
"strings"
)
// net/http post demo
func main() {
url := "http://127.0.0.1:9090/post"
// 表单数据
//contentType := "application/x-www-form-urlencoded"
//data := "name=小王子&age=18"
// json
contentType := "application/json"
data := `{"name":"小王子","age":18}`
resp, err := http.Post(url, contentType, strings.NewReader(data))
if err != nil {
fmt.Printf("post failed, err:%v\n", err)
return
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("get resp failed, err:%v\n", err)
return
}
fmt.Println(string(b))
}
```
对应的 Server 端 HandlerFunc 如下:
```go
func postHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
// 1. 请求类型是application/x-www-form-urlencoded时解析form数据
r.ParseForm()
fmt.Println(r.PostForm) // 打印form数据
fmt.Println(r.PostForm.Get("name"), r.PostForm.Get("age"))
// 2. 请求类型是application/json时从r.Body读取数据
b, err := ioutil.ReadAll(r.Body)
if err != nil {
fmt.Printf("read request.Body failed, err:%v\n", err)
return
}
fmt.Println(string(b))
answer := `{"status": "ok"}`
w.Write([]byte(answer))
}
```
### 19.2.5 自定义Client
**要管理 HTTP 客户端的头域、重定向策略和其他设置,创建一个 Client**
```go
client := &http.Client{
CheckRedirect: redirectPolicyFunc,
}
resp, err := client.Get("http://example.com")
// ...
req, err := http.NewRequest("GET", "http://example.com", nil)
// ...
req.Header.Add("If-None-Match", `W/"wyzzy"`)
resp, err := client.Do(req)
// ...
```
### 19.2.6 自定义Transport
**要管理代理、TLS配置、`keep-alive`、压缩和其他设置,创建一个 Transport**
```go
tr := &http.Transport{
TLSClientConfig: &tls.Config{RootCAs: pool},
DisableCompression: true,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://example.com")
```
Client 和 Transport 类型都可以安全的被多个 goroutine 同时使用。出于效率考虑,应该一次建立、尽量重用。
## 19.3 服务端
### 19.3.1 默认的Server
ListenAndServe 使用指定的监听地址和处理器启动一个 HTTP 服务端。处理器参数通常是 nil这表示采用包变量 DefaultServeMux 作为处理器。
Handle 和 HandleFunc 函数可以向 DefaultServeMux 添加处理器。
```go
http.Handle("/foo", fooHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServe(":8080", nil))
```
### 19.3.2 默认的Server示例
使用 Go 语言中的 net/http 包来编写一个简单的接收 HTTP 请求的 Server 端示例net/http 包是对 net 包的进一步封装,专门用来处理 HTTP 协议的数据。具体的代码如下:
```go
// http server
func sayHello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello 沙河!")
}
func main() {
http.HandleFunc("/", sayHello)
err := http.ListenAndServe(":9090", nil)
if err != nil {
fmt.Printf("http server failed, err:%v\n", err)
return
}
}
```
将上面的代码编译之后执行,打开你电脑上的浏览器在地址栏输入`127.0.0.1:9090` 回车,此时就能够看到如下页面了。
![](pics/19-1-默认server示例结果.png)
### 19.3.3 自定义 Server
要管理服务端的行为,可以创建一个自定义的 Server
```go
s := &http.Server{
Addr: ":8080",
Handler: myHandler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())
```

View File

@@ -0,0 +1,55 @@
# 概述
suffixarray模块提供了基于前缀数组的子串检索功能能够在byte数组中检索指定子串并获得其索引下标。
# 创建前缀数组
可用通过New方法创建一个前缀数组方法声明如下
```
func New(data []byte) *Index
```
此外可以通过其Bytes方法获取原始byte数组方法声明如下
```
func (x *Index) Bytes() []byte
```
# 数据检索
Index对象上提供了两种检索方法FindAllIndex和Lookup。
其中FindAllIndex接收一个正则表达式并返回长度不超过n的匹配索引列表n<0时返回全部结果方法声明如下
```
func (x *Index) FindAllIndex(r *regexp.Regexp, n int) (result [][]int)
```
而Lookup方法接收一个byte列表返回长度不超过n的匹配索引列表n<0时返回全部结果方法声明如下
```
func (x *Index) Lookup(s []byte, n int) (result []int)
```
下面是一个简单的使用实例
```
package main
import (
"index/suffixarray"
"fmt"
"sort"
)
func main() {
source := []byte("hello world, hello china")
index := suffixarray.New(source)
offsets := index.Lookup([]byte("hello"), -1)
sort.Ints(offsets)
fmt.Printf("%v", offsets)
}
```

View File

@@ -0,0 +1,87 @@
io包一共分为两块主目录和ioutil目录。总得来说还是比较干净的。
在整个io包中有以下几个文件
>io.go
>multi.go
>pipe.go
>ioutil
>>ioutil.go
>>tempfile.go
比起原来的reflect包来说内容还是多了不少。
先看一下io.go的代码内容不多一共才530行。multi.go更少一共100行。pipe.go则是200行。
这样的代码倒是非常方便阅读。
至于ioutil包里的两个代码也分别在200行左右。
在io.go的代码段中有一段都是用来定义接口的。
```go
1type Reader interface {}
2type Writer interface {}
3type Closer interface {}
4type Seeker interface {}
5type ReadWriter interface {}
6type ReadCloser interface {}
7type WriteCloser interface {}
8type ReadWriteCloser interface {}
9type ReadSeeker interface {}
10type WriteSeeker interface {}
11type ReadWriteSeeker interface {}
12type ReaderFrom interface {}
13type WriterTo interface {}
14type ReaderAt interface {}
15type WriterAt interface {}
16type ByteReader interface {}
17type ByteScanner interface {}
18type ByteWriter interface {}
19type RuneReader interface {}
20type RuneScanner interface {}
21type stringWriter interface {} //私有
```
一共21个……这么多。目前来说我并不知道他们的实际用处。
接下来,又定义了几组对外的公有函数:
```go
1func WriteString(w Writer, s string) (n int, err error) {}
2func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) {}
3func ReadFull(r Reader, buf []byte) (n int, err error) {}
4func CopyN(dst Writer, src Reader, n int64) (written int64, err error) {}
5func Copy(dst Writer, src Reader) (written int64, err error) {}
6func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {}
7func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {} //私有
8func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }
9func TeeReader(r Reader, w Writer) Reader {}
```
最后,定义了几组对象:
```go
type LimitedReader struct {
R Reader // underlying reader
N int64 // max bytes remaining
}
func (l *LimitedReader) Read(p []byte) (n int, err error) {}
func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader {}
```
```go
type SectionReader struct {
r ReaderAt
base int64
off int64
limit int64
}
func (s *SectionReader) Read(p []byte) (n int, err error) {}
func (s *SectionReader) Seek(offset int64, whence int) (int64, error) {}
func (s *SectionReader) ReadAt(p []byte, off int64) (n int, err error) {}
func (s *SectionReader) Size() int64 { return s.limit - s.base }
```
```go
type teeReader struct { //私有
r Reader
w Writer
}
func (t *teeReader) Read(p []byte) (n int, err error) {
```
以上就是io.go的全貌了。

View File

@@ -0,0 +1,370 @@
# 13 文件操作
原文:[文件操作](https://www.liwenzhou.com/posts/Go/go_file/)
本文主要介绍了Go语言中文件读写的相关操作。
文件是什么?
计算机中的文件是存储在外部介质(通常是磁盘)上的数据集合,文件分为文本文件和二进制文件。
## 13.1 打开和关闭文件
`os.Open()` 函数能够打开一个文件,返回一个 `*File` 和一个 `err`。对得到的文件实例调用`close()` 方法能够关闭文件。
```go
package main
import (
"fmt"
"os"
)
func main() {
// 只读方式打开当前目录下的main.go文件
file, err := os.Open("./main.go")
if err != nil {
fmt.Println("open file failed!, err:", err)
return
}
// 关闭文件
file.Close()
}
```
为了防止文件忘记关闭,我们通常使用 `defer` 注册文件关闭语句。
## 13.2 读取文件 `file.Read()`
### 13.2.1 基本使用
Read方法定义如下
```go
func (f *File) Read(b []byte) (n int, err error)
```
它接收一个字节切片,返回读取的字节数和可能的具体错误,读到文件末尾时会返回 `0``io.EOF`。 举个例子:
```go
func main() {
// 只读方式打开当前目录下的main.go文件
file, err := os.Open("./main.go")
if err != nil {
fmt.Println("open file failed!, err:", err)
return
}
defer file.Close()
// 使用Read方法读取数据
var tmp = make([]byte, 128)
n, err := file.Read(tmp)
if err == io.EOF {
fmt.Println("文件读完了")
return
}
if err != nil {
fmt.Println("read file failed, err:", err)
return
}
fmt.Printf("读取了%d字节数据\n", n)
fmt.Println(string(tmp[:n]))
}
```
### 13.2.2 循环读取
使用for循环读取文件中的所有数据。
```go
func main() {
// 只读方式打开当前目录下的main.go文件
file, err := os.Open("./main.go")
if err != nil {
fmt.Println("open file failed!, err:", err)
return
}
defer file.Close()
// 循环读取文件
var content []byte
var tmp = make([]byte, 128)
for {
n, err := file.Read(tmp)
if err == io.EOF {
fmt.Println("文件读完了")
break
}
if err != nil {
fmt.Println("read file failed, err:", err)
return
}
content = append(content, tmp[:n]...)
}
fmt.Println(string(content))
}
```
### 13.2.3 bufio 读取文件
`bufio` 是在 file 的基础上封装了一层 API支持更多的功能。
```go
package main
import (
"bufio"
"fmt"
"io"
"os"
)
// bufio按行读取示例
func main() {
file, err := os.Open("./xx.txt")
if err != nil {
fmt.Println("open file failed, err:", err)
return
}
defer file.Close()
reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n') //注意是字符
if err == io.EOF {
if len(line) != 0 {
fmt.Println(line)
}
fmt.Println("文件读完了")
break
}
if err != nil {
fmt.Println("read file failed, err:", err)
return
}
fmt.Print(line)
}
}
```
### 13.2.4 ioutil读取整个文件
`io/ioutil` 包的 ReadFile 方法能够读取完整的文件,只需要将文件名作为参数传入。
```go
package main
import (
"fmt"
"io/ioutil"
)
// ioutil.ReadFile读取整个文件
func main() {
content, err := ioutil.ReadFile("./main.go")
if err != nil {
fmt.Println("read file failed, err:", err)
return
}
fmt.Println(string(content))
}
```
## 13.3 文件写入操作
`os.OpenFile()` 函数能够以指定模式打开文件,从而实现文件写入相关功能。
```go
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
...
}
```
其中:
* name要打开的文件名
* flag打开文件的模式。
* perm文件权限一个八进制数。r04w02x执行01。
文件打开模式有以下几种:
模式 |含义
---|---
`os.O_WRONLY` |只写
`os.O_CREATE` |创建文件
`os.O_RDONLY` |只读
`os.O_RDWR` |读写
`os.O_TRUNC` |清空
`os.O_APPEND` |追加
### 13.3.1 Write 和 WriteString
```go
func main() {
file, err := os.OpenFile("xx.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
if err != nil {
fmt.Println("open file failed, err:", err)
return
}
defer file.Close()
str := "hello 沙河"
file.Write([]byte(str)) //写入字节切片数据
file.WriteString("hello 小王子") //直接写入字符串数据
}
```
### 13.3.2 bufio.NewWriter
```go
func main() {
file, err := os.OpenFile("xx.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
if err != nil {
fmt.Println("open file failed, err:", err)
return
}
defer file.Close()
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
//将数据先写入缓存
writer.WriteString("hello沙河\n")
}
//将缓存中的内容写入文件
writer.Flush()
}
```
千万不要忘了最后一句 `writer.Flush() ` ,否则文件中没有内容。
### 13.3.3 ioutil.WriteFile
```go
func main() {
str := "hello 沙河"
err := ioutil.WriteFile("./xx.txt", []byte(str), 0666)
if err != nil {
fmt.Println("write file failed, err:", err)
return
}
}
```
### 13.3.4 使用 bufio 获取用户输入
```go
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// useScan()
useBufio()
}
func useScan() {
var s string
fmt.Print("请输入内容-1")
// Scanln 读取内容时,遇到空格或者换行即终止
fmt.Scanln(&s)
fmt.Printf("输入的内容是-1 %s \n", s)
}
func useBufio() {
var s string
reader := bufio.NewReader(os.Stdin)
fmt.Print("请输入内容2")
s, _ = reader.ReadString('\n')
fmt.Printf("输入的内容是-2%s \n", s)
}
```
上述代码中,执行 `useScan()` 时,在控制台输入带有空格的 `a b c` , 打印时仅会打印 `a`;在执行 `useBufio() ` 时,在控制台输入带有空格的 `a b c` , 打印时仅会打印 `a b c `
## 13.4 练习
### 13.4.1 copyFile
借助 `io.Copy()` 实现一个拷贝文件函数。
```go
// CopyFile 拷贝文件函数
func CopyFile(dstName, srcName string) (written int64, err error) {
// 以读方式打开源文件
src, err := os.Open(srcName)
if err != nil {
fmt.Printf("open %s failed, err:%v.\n", srcName, err)
return
}
defer src.Close()
// 以写|创建的方式打开目标文件
dst, err := os.OpenFile(dstName, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
fmt.Printf("open %s failed, err:%v.\n", dstName, err)
return
}
defer dst.Close()
return io.Copy(dst, src) //调用io.Copy()拷贝内容
}
func main() {
_, err := CopyFile("dst.txt", "src.txt")
if err != nil {
fmt.Println("copy file failed, err:", err)
return
}
fmt.Println("copy done!")
}
```
### 13.4.2 实现一个cat命令
使用文件操作相关知识模拟实现linux平台cat命令的功能。
```go
package main
import (
"bufio"
"flag"
"fmt"
"io"
"os"
)
// cat命令实现
func cat(r *bufio.Reader) {
for {
buf, err := r.ReadBytes('\n') //注意是字符
if err == io.EOF {
break
}
fmt.Fprintf(os.Stdout, "%s", buf)
}
}
func main() {
flag.Parse() // 解析命令行参数
if flag.NArg() == 0 {
// 如果没有参数默认从标准输入读取内容
cat(bufio.NewReader(os.Stdin))
}
// 依次读取每个指定文件的内容并打印到终端
for i := 0; i < flag.NArg(); i++ {
f, err := os.Open(flag.Arg(i))
if err != nil {
fmt.Fprintf(os.Stdout, "reading from %s failed, err:%v\n", flag.Arg(i), err)
continue
}
cat(bufio.NewReader(f))
}
}
```

View File

@@ -0,0 +1,84 @@
按照惯例,还是先从第一个方法(接口)看起吧。
```go
type Reader interface {
Read(p []byte) (n int, err error)
}
```
第一个是接口其实在io的操作里面Reader和Write是必不可少的东西可是如此一来这个接口是在哪里实现的呢
在网上,我找到一个例子。
```go
func main() {
data, err := ReadFrom(strings.NewReader("from string"), 12)
fmt.Println(data)
fmt.Println(err)
}
func ReadFrom(reader io.Reader, num int) ([]byte, error) {
p := make([]byte, num)
n, err := reader.Read(p)
if n > 0 {
return p[:n], nil
}
return p, err
}
result:
[102 114 111 109 32 115 116 114 105 110 103]
<nil>
```
可以看到传入的是一个io.Reader类型而这个类型经由strings.NewReader("from string")转化。
于是我又转到string包中发现了对Reader接口的实行。
```go
func (r *Reader) Read(b []byte) (n int, err error) {
if r.i >= int64(len(r.s)) {
return 0, io.EOF
}
r.prevRune = -1
n = copy(b, r.s[r.i:])
r.i += int64(n)
return
}
```
它是继承自
```go
// A Reader implements the io.Reader, io.ReaderAt, io.Seeker, io.WriterTo,
// io.ByteScanner, and io.RuneScanner interfaces by reading
// from a string.
type Reader struct {
s string
i int64 // current reading index
prevRune int // index of previous rune; or < 0
}
```
从注释上就可以看出这个结构体又实现了io.Reader, io.ReaderAt, io.Seeker, io.WriterToio.ByteScanner, and io.RuneScanner这么多的接口。。
先撸一遍代码的顺序
step.1:`strings.NewReader("from string")`
step.2:转入string包的reader.go
step.3:返回`func NewReader(s string) *Reader { return &Reader{s, 0, -1} }`把Reader结构体其实是对象返回。
step.4:`p := make([]byte, num)`初始化一个比特变量
step.5:`n, err := reader.Read(p)`调用reader对象的Read方法返回比特数据。
总得来说Reader这个接口是交给其它的方法去自行实现的接口他本身不作任何数据处理。除了上面的读取字符串以外。还有
os.Stdin读取输入的流
位于os包的file.go文件中
```go
func (f *File) Read(b []byte) (n int, err error) {
if err := f.checkValid("read"); err != nil {
return 0, err
}
n, e := f.read(b)
if n == 0 && len(b) > 0 && e == nil {
return 0, io.EOF
}
if e != nil {
err = &PathError{"read", f.name, e}
}
return n, err
}
```
还有位于读取文件os.Open同上
其实我最想了解的是网络io一块的内容在net包中不过暂时先放一下了等下次读到net的时候再深入一下。

View File

@@ -0,0 +1,161 @@
首先strings包里面的reader.go文件并没有write有WriteTo所以我们需要到别的地方去找这个接口的实现。
最简单的就是文件操作的代码里面了,因为涉及到文件必然有写的操作。
所以我们在os包的file.go文件里找到了它的实现
```go
func (f *File) Write(b []byte) (n int, err error) {
if err := f.checkValid("write"); err != nil {
return 0, err
}
n, e := f.write(b)
if n < 0 {
n = 0
}
if n != len(b) {
err = io.ErrShortWrite
}
epipecheck(f, e)
if e != nil {
err = &PathError{"write", f.name, e}
}
return n, err
}
```
接下来,我们写一段例子来实现它。
```go
func main() {
f, err := os.OpenFile("test.txt",os.O_RDWR|os.O_CREATE|os.O_APPEND,os.ModePerm)
if(err!=nil){
fmt.Println(err)
}
f.Write([]byte(string("hello mama miya")))
fmt.Println(f)
}
result:
创建了一个test.txt并成功将hello mama miya写入
```
注意的是在Write内部还调用了writes这个私有方法。
```go
// write writes len(b) bytes to the File.
// It returns the number of bytes written and an error, if any.
func (f *File) write(b []byte) (n int, err error) {
f.l.Lock()
defer f.l.Unlock()
if f.isConsole {
return f.writeConsole(b)
}
return fixCount(syscall.Write(f.fd, b))
}
```
虽然已经超出了io包的范畴不过也可以简单得说下。
*File也是一个结构
```go
type file struct {
fd syscall.Handle
name string
dirinfo *dirInfo // nil unless directory being read
l sync.Mutex // used to implement windows pread/pwrite
// only for console io
isConsole bool
lastbits []byte // first few bytes of the last incomplete rune in last write
readuint16 []uint16 // buffer to hold uint16s obtained with ReadConsole
readbyte []byte // buffer to hold decoding of readuint16 from utf16 to utf8
readbyteOffset int // readbyte[readOffset:] is yet to be consumed with file.Read
}
```
这是windows版的定义linux版还不知道。这里的l是一个锁。
在写文件的时候,会上锁,写完,会解锁。
## type Closer interface {}
这个真没什么好说的了io读和写必然涉及到关闭所以Close也是必须的方法写代码的时候千万不能漏。
## type Seeker interface {}
继续看文件读写的方法。Seek同样在其中被实现了
```go
// Seek sets the offset for the next Read or Write on file to offset, interpreted
// according to whence: 0 means relative to the origin of the file, 1 means
// relative to the current offset, and 2 means relative to the end.
// It returns the new offset and an error, if any.
// The behavior of Seek on a file opened with O_APPEND is not specified.
func (f *File) Seek(offset int64, whence int) (ret int64, err error) {
if err := f.checkValid("seek"); err != nil {
return 0, err
}
r, e := f.seek(offset, whence)
if e == nil && f.dirinfo != nil && r != 0 {
e = syscall.EISDIR
}
if e != nil {
return 0, &PathError{"seek", f.name, e}
}
return r, nil
}
```
查看注释Seek就设置下一次读或者写的偏移量。根据这个whence0意味着相对于文件的起始1意味着当前的偏移位置2意味着相对于文件的末尾。他会返回新的偏移量和一个错误。
在io.go里面针对whence定义了一组常量可以拿来使用。
```go
// Seek whence values.
const (
SeekStart = 0 // seek relative to the origin of the file
SeekCurrent = 1 // seek relative to the current offset
SeekEnd = 2 // seek relative to the end
)
```
其实在file.go里面也有定义
```go
// Deprecated: Use io.SeekStart, io.SeekCurrent, and io.SeekEnd.
const (
SEEK_SET int = 0 // seek relative to the origin of the file
SEEK_CUR int = 1 // seek relative to the current offset
SEEK_END int = 2 // seek relative to the end
)
```
不过看一下注释已经被Deprecated掉了所以不要再去使用了。
接着,我们来试一下怎么用吧。
```go
func main() {
f, err := os.OpenFile("test.txt",os.O_RDWR|os.O_CREATE|os.O_APPEND,os.ModePerm)
if(err!=nil){
fmt.Println(err)
}
//f.Write([]byte(string("hello mama miya")))
rs,err := f.Seek(2,io.SeekStart)
if(err!=nil){
//报错
}
defer f.Close()
fmt.Println(rs)
fd,err := ioutil.ReadAll(f)
fmt.Println(string(fd))
}
result:
2
llo mama miya
```
把代码改成
```go
rs,err := f.Seek(2,io.SeekCurrent)
result:
2
llo mama miya
```
把代码改成
```go
rs,err := f.Seek(2,io.SeekEnd)
result:
17
```
看上去SeekCurrent和SeekEnd的用法似乎不大可能多还是用在写上面吧。

View File

@@ -0,0 +1,51 @@
```go
// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
Reader
Writer
}
// ReadCloser is the interface that groups the basic Read and Close methods.
type ReadCloser interface {
Reader
Closer
}
// WriteCloser is the interface that groups the basic Write and Close methods.
type WriteCloser interface {
Writer
Closer
}
// ReadWriteCloser is the interface that groups the basic Read, Write and Close methods.
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// ReadSeeker is the interface that groups the basic Read and Seek methods.
type ReadSeeker interface {
Reader
Seeker
}
// WriteSeeker is the interface that groups the basic Write and Seek methods.
type WriteSeeker interface {
Writer
Seeker
}
// ReadWriteSeeker is the interface that groups the basic Read, Write and Seek methods.
type ReadWriteSeeker interface {
Reader
Writer
Seeker
}
```
这一套接口看上去琳琅满目的,其实仔细一看的话,无非就是
Reader
Writer
Closer
Seeker
四个的排列组合,是不是很简单,这一课就学完了。

View File

@@ -0,0 +1,228 @@
*这是在生病前更新的最后一篇时隔近2月有余已经忘得差不多了现在重新从这一节开始整理边看边补吧。
2017-9-23
ReaderFrom接口在bufio包的bufio.go中有实现bufio是什么意思一看名字可以猜得出是带了buffer缓存的io包。
通俗地来讲就是io的威力加强版。
可见io就是一个简单的小骨架他是开枝散叶的散布于各方法之中由其调用。
先看一段实现的方法:
```go
func main() {
b := bytes.NewBuffer(make([]byte, 10))
s := strings.NewReader("Hello world")
bw := bufio.NewWriter(b)
bw.ReadFrom(s)
fmt.Println(b)
}
```
输出:
```go
Hello world
```
这里我们先不用深究bufio是怎么用的strings.NewReader是将“hello world”这个字符串转换成Reader的结构体。
```go
type Reader struct {
s string
i int64 // current reading index
prevRune int // index of previous rune; or < 0
}
```
bufio.NewWriter就确定buffer的尺寸。
最后用ReadFrom将s读入到bw中。
```go
// 同样实现了io.ReaderFrom
// ReadFrom implements io.ReaderFrom.
func (b *Writer) ReadFrom(r io.Reader) (n int64, err error) {
if b.Buffered() == 0 {
if w, ok := b.wr.(io.ReaderFrom); ok {
return w.ReadFrom(r)
}
}
var m int
for {
if b.Available() == 0 {
if err1 := b.Flush(); err1 != nil {
return n, err1
}
}
nr := 0
for nr < maxConsecutiveEmptyReads {
m, err = r.Read(b.buf[b.n:])
if m != 0 || err != nil {
break
}
nr++
}
if nr == maxConsecutiveEmptyReads {
return n, io.ErrNoProgress
}
b.n += m
n += int64(m)
if err != nil {
break
}
}
if err == io.EOF {
// If we filled the buffer exactly, flush preemptively.
if b.Available() == 0 {
err = b.Flush()
} else {
err = nil
}
}
return n, err
}
```
在ReadFrom中有一个判断if b.Buffered() == 0 {}
翻到buffered()方法的代码中
```go
// Buffered returns the number of bytes that have been written into the current buffer.
func (b *Writer) Buffered() int { return b.n }
```
会发现有一个b.n
这其实是一个Writer结构里的一个字段。
```go
type Writer struct {
err error
buf []byte
n int
wr io.Writer
}
```
(注意Writer implements buffering for an io.Writer object.)
这个Writer也是实现了io.Writer的。
不过我没找到n的用法在我上面的例子中。
```go
if b.Buffered() == 0 {
if w, ok := b.wr.(io.ReaderFrom); ok {
return w.ReadFrom(r)
}
}
```
b.wr是什么b.wr就是`b := bytes.NewBuffer(make([]byte, 10))`
在这一段是必然会return了因为b.Buffered()为0。到b.wr.(io.ReaderFrom)的时候即强行把b.wr转换成了io.ReaderFrom(这个的意思是现在w只拥有ReadFrom这一个方法)并赋值给了w。
也就是说,这里的`return w.ReadFrom(r)`,就是b.ReadFrom(r),是否如此,还需验证一下。
```go
func main() {
b := bytes.NewBuffer(make([]byte, 10))
s := strings.NewReader("Hello world")
bw := bufio.NewWriter(b)
val1,err := b.ReadFrom(s)
fmt.Println(val1)
val2,err := bw.ReadFrom(s)
fmt.Println(val2)
fmt.Println(err)
}
```
输出:
```go
14
0
<nil>
```
奇怪怎么val1是14val2是0了实际上原因就在于ReadFrom()只会读一次就把buff给清了。
```go
// If buffer is empty, reset to recover space.
if b.off >= len(b.buf) {
b.Truncate(0)
}
```
b.off在第一次调用的时候会置为0。
如果我们把第一次的ReadFrom()去掉,即删除`val1,err := b.ReadFrom(s)`再打印的时候val2就是14了。
接下来,再做个简单的回顾:
首先bytes.NewBuffer返回了一个{ return &Buffer{buf: buf} }Buffer的对象。赋值给b。
```go
type Buffer struct {
buf []byte // contents are the bytes buf[off : len(buf)]
off int // read at &buf[off], write at &buf[len(buf)]
bootstrap [64]byte // memory to hold first slice; helps small buffers avoid allocation.
lastRead readOp // last read operation, so that Unread* can work correctly.
}
```
此位于bytes包的buffer.go文件。
接下来strings.NewReader{ return &Reader{s, 0, -1} }返回了一个Reader对象。赋值给s。
```go
type Reader struct {
s string
i int64 // current reading index
prevRune int // index of previous rune; or < 0
}
```
此位于strings包的reader.go文件。
再接下来bufio.NewWriter(b)把b带入最后返回的是一个Writer对象。赋值给bw
```go
return &Writer{
buf: make([]byte, size),
wr: w,
}
```
这里就是关键这个Writer对象里初始化了两个值buf和wr不过这两个值都是私有的其中buf是大小调用了系统的固定值一个常量defaultBufSize为4096。`size = defaultBufSize`
而w就是传入的b,也即Buffer的对象。传入的过程中已经被转换成了io.Writer对象。
现在的问题就是,这个转换过程,到底做了什么?
事实上我写了一段例子发现,传过去并没有做任何转换。
```go
func main() {
b := bytes.NewBuffer(make([]byte, 10))
fmt.Println(reflect.TypeOf(b).String())
NewWriter(b)
}
type WriterTest interface {
Write(p []byte) (n int, err error)
}
// NewWriter returns a new Writer whose buffer has the default size.
func NewWriter(w WriterTest) { //io.Write只是一个接口
fmt.Println(reflect.TypeOf(w).String())
}
result:
*bytes.Buffer
*bytes.Buffer
```
这突然让我想起一件事情io.Writer是个接口而所有的变量的根都是继承自一个空的接口interface{})
所以根本就没做什么转换,直接转过去就行了。
所以wr就是b本身。
最后:
`bw.ReadFrom(s)`
在目录的例子子,这段程序只执行到:
```go
if b.Buffered() == 0 {
if w, ok := b.wr.(io.ReaderFrom); ok {
return w.ReadFrom(r)
}
}
```
就结束了,关键是`w.ReadFrom(r)`这一段目前已经知道w就是b。实际上就也就是让b操作了一下ReadFrom(r)。
要注意的是这个ReadFrom(r)是buffer包里的ReadFrom(),位于`src\bytes\buffer.go`文件里面。
`bw.ReadFrom(s)`调用的是bufio包里的ReadFrom(),位于`src\bufio\bufio.go`文件里面,是不一样的!
`bw.ReadFrom(s)`里面,就把"Hello world"这串string给了b。
所以就可以打印出来了
bufio的应用场景目前还不太清楚这一段主要就是大概了解一下ReadFrom的使用情况。

100
Go/1 Go标准库/io/io.md Normal file
View File

@@ -0,0 +1,100 @@
# 概述
IO是操作系统的基础概念是对输入输出设备的抽象。Go语言的io库对这些功能进行了抽象通过统一的接口对输入输出设备进行操作。
# Reader
Reader对象是对输入设备的抽象一个Reader可以绑定到一个输入对象并在这个输入设备上读取数据其声明如下
```
type Reader interface {
Read(p []byte) (n int, err error)
}
```
除了基础的Reader类之外io包中还有LimitReaderMultiReader和TeeReader。其中LimitReader只读取指定长度的数据MultiReader用于聚合多个Reader并依次进行读取TeeReader将一个输入绑定到一个输出。具体声明如下
```
func LimitReader(r Reader, n int64) Reader
func MultiReader(readers ...Reader) Reader
func TeeReader(r Reader, w Writer) Reader
```
这些衍生Reader都以包装的方式进行使用也就是传入一个Reader在这个Reader上增加额外功能然后返回这个新Reader。下面是一个简单的使用实例。
```
package main
import (
"io"
"strings"
"os"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
lr := io.LimitReader(r, 4)
io.Copy(os.Stdout, lr)
}
```
## ReadAtLeast & ReadFull
这两个函数用于从Reader里面读取数据到指定缓冲区ReadAtLeast会读取至少n个字节的数据ReadFull会读取直到数据填满整个缓冲区。其函数声明如下
```
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)
func ReadFull(r Reader, buf []byte) (n int, err error)
```
# Writer
Writer对象是对输出设备的抽象一个Writer可以绑定到一个输出对象并在这个输出设备上写入数据其声明如下
```
type Writer interface {
Write(p []byte) (n int, err error)
}
```
和Reader类似Writer也有MultiWriter可以同步输出到多个Writer声明如下
```
func MultiWriter(writers ...Writer) Writer
```
## WriteString
WriteString函数用于向某个Writer写入一个字符串其声明如下
```
func WriteString(w Writer, s string) (n int, err error)
```
# ReadWriter
整合了Reader和Writer可以同时进行读取和写入操作声明如下
```
type ReadWriter interface {
Reader
Writer
}
```
# Copy
io的一个常用操作就是数据的复制io包中提供了多个复制函数直接将数据从Writer复制到Reader。
## Copy
Copy是最基础的复制函数读取Writer中的数据直到EOF并写入Reader函数声明如下
```
func Copy(dst Writer, src Reader) (written int64, err error)
```
## CopyBuffer
CopyBuffer函数在Copy的基础上可以指定数据缓冲区。每次调用Copy函数时都会生成一块临时的缓冲区会带来一定的分配开销CopyBuffer可以多次复用同一块缓冲区其函数声明如下
```
func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error)
```
## CopyN
CopyN在Copy的基础上可以额外指定拷贝制定字节的数据其函数声明如下
```
func CopyN(dst Writer, src Reader, n int64) (written int64, err error)
```
# 更多内容
io库中还有许多本文未涉及的内容包括PipeReaderPipeWriterByteReaderByteWriter等针对具体类型的实例和一些辅助函数。详见 [golang/io](https://golang.org/pkg/io/)

View File

@@ -0,0 +1,62 @@
# 概述
前面的io包提供了对输入输出设备最基本的抽象而ioutil在io包的基础上提供了一系列的函数来应对具体的场景。
# 数据读取
ioutil一共提供了三个数据读取的函数分别是
- ReadAll从一个io.Reader读取所有数据并返回一个字节数组
- ReadllDir从一个目录读取数据并得到这个目录里的文件对象列表
- ReadFile读取指定文件的内容并返回一个字节数组
其函数声明如下:
```
func ReadAll(r io.Reader) ([]byte, error)
func ReadDir(dirname string) ([]os.FileInfo, error)
func ReadFile(filename string) ([]byte, error)
```
可以看到上面的三个函数分别对应于三个特定的场景下面以ReadFile为例对其使用进行说明
```
package main
import (
"fmt"
"io/ioutil"
)
func main() {
content, err := ioutil.ReadFile("demo.txt")
if err != nil {
fmt.Fatal(err)
}
fmt.Print(content)
}
```
# 临时文件
ioutil也支持创建临时目录和文件分别通过TempDir和TempFile函数实现文件和目录不会自动销毁需要使用者自行对创建的临时文件进行处理可以使用`os.Remove()`删除文件。
使用TempDir可以创建一个临时的目录函数接收父目录名和目录前缀名作为参数创建一个临时目录并返回它的名字具体函数声明如下
```
func TempDir(dir, prefix string) (name string, err error)
```
使用TempFile可以创建一个临时的文件同样可以指定路径和文件名前缀函数返回这个文件对象可以直接对文件进行读写。具体函数声明如下
```
func TempFile(dir, prefix string) (f *os.File, err error)
```
# 文件写入
和文件读取类似WriteFile可以对文件进行写入函数接收三个参数分别是要写入的文件名写入的数据以及一个文件信息标识位。具体的标识位可以见文档 [FileMode](https://golang.org/pkg/os/#FileMode)。WriteFile函数声明如下
```
func WriteFile(filename string, data []byte, perm os.FileMode) error
```

View File

@@ -0,0 +1,598 @@
# 15 日志库
## 15.1 标准日志库 log
原文链接 《[Go语言标准库log介绍](https://www.liwenzhou.com/posts/Go/go_log/)》
Go 语言内置的 log 包实现了简单的日志服务。
### 15.1.1 使用 Logger
log 包定义了 Logger 类型该类型提供了一些格式化输出的方法。本包也提供了一个预定义的“标准”logger可以通过调用函数 `Print系列(Print|Printf|Println``Fatal系列Fatal|Fatalf|Fatalln`、和 `Panic系列Panic|Panicf|Panicln`来使用,比自行创建一个 logger 对象更容易使用。
例如,我们可以像下面的代码一样直接通过 log 包来调用上面提到的方法,**默认会将日志信息打印到终端界面**
```go
package main
import (
"log"
)
func main() {
log.Println("普通的日志打印")
str := "日志打印"
log.Printf("格式化的 %s", str)
log.Fatalln("Fatal 日志打印")
log.Panicln("Panic 日志打印")
}
```
编译并执行上面的代码会得到如下输出:
```
2020/09/17 08:21:23 普通的日志打印
2020/09/17 08:21:23 格式化的 日志打印
2020/09/17 08:21:23 Fatal 日志打印
exit status 1
```
* logger 会打印每条日志信息的日期、时间,默认输出到系统的终端。
* Fatal 系列函数会在写入日志信息后调用 `os.Exit(1)`
* Panic系列函数会在写入日志信息后 panic。
### 15.1.2 配置 logger
#### 15.1.2.1 标准 logger 的配置
默认情况下的 logger 只会提供日志的时间信息,但是很多情况下我们希望得到更多信息,比如**记录该日志的文件名和行号**等。log 标准库中为我们提供了定制这些设置的方法。
log 标准库中的 `Flags 函数` 会返回标准 logger 的输出配置,而 `SetFlags函数` 用来设置标准 logger 的输出配置。
```go
func Flags() int
func SetFlags(flag int)
```
#### 15.1.2.2 flag 选项
log 标准库提供了如下的 flag 选项,它们是一系列定义好的常量。
```go
const (
// 控制输出日志信息的细节,不能控制输出的顺序和格式。
// 输出的日志在每一项后会有一个冒号分隔例如2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
Ldate = 1 << iota // 日期2009/01/23
Ltime // 时间01:23:23
Lmicroseconds // 微秒级别的时间01:23:23.123123用于增强Ltime位
Llongfile // 文件全路径名+行号: /a/b/c/d.go:23
Lshortfile // 文件名+行号d.go:23会覆盖掉Llongfile
LUTC // 使用UTC时间
LstdFlags = Ldate | Ltime // 标准logger的初始值
)
```
下面我们在记录日志之前先设置一下标准logger的输出选项如下
```go
package main
import (
"log"
)
func main() {
log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
log.Println("定制 flag 之后的日志打印")
}
```
编译执行后得到的输出结果如下:
```go
2020/09/17 08:29:05.719377 /Users/cnpeng/CnPeng/04_Demos/12_Go/oldBoy/varAndConst/const.go:9: 定制 flag 之后的日志打印
```
#### 15.1.2.3 配置日志前缀
log标准库中还提供了关于日志信息前缀的两个方法
```go
func Prefix() string
func SetPrefix(prefix string)
```
其中 `Prefix函数` 用来查看标准 logger 的输出前缀,`SetPrefix函数` 用来设置输出前缀。
```go
package main
import (
"log"
)
func main() {
log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
log.Println("定制 flag 之后的日志打印")
log.SetPrefix("[CnPeng]")
log.Println("定制 flag 和 prefix 之后的日志打印")
}
```
上面的代码输出如下:
```go
2020/09/17 08:35:08.293025 /Users/cnpeng/CnPeng/04_Demos/12_Go/oldBoy/varAndConst/const.go:9: 定制 flag 之后的日志打印
[CnPeng]2020/09/17 08:35:08.293197 /Users/cnpeng/CnPeng/04_Demos/12_Go/oldBoy/varAndConst/const.go:11: 定制 flag prefix 之后的日志打印
```
这样我们就能够在代码中为我们的日志信息添加指定的前缀,方便之后对日志信息进行检索和处理。
#### 15.1.2.4 配置日志输出位置
```go
func SetOutput(w io.Writer)
```
`SetOutput函数` 用来设置标准 logger 的输出目的地,默认是标准错误输出。
例如,下面的代码会把日志输出到同目录下的 `xx.log` 文件中。
```go
package main
import (
"fmt"
"log"
"os"
)
func main() {
// 创建或者打开 log 文件
logFile, err := os.OpenFile("./日志信息.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if nil != err {
fmt.Println("打开日志文件出错", err)
return
}
// 指定日志输出文件
log.SetOutput(logFile)
log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
log.Println("定制 flag 之后的日志打印")
log.SetPrefix("[CnPeng]")
log.Println("定制 flag 和 prefix 之后的日志打印")
}
```
![](pics/15-1-指定日志输出文件.png)
`日志信息.log` 文件中的内容如下:
```go
2020/09/17 08:40:57.335316 /Users/cnpeng/CnPeng/04_Demos/12_Go/oldBoy/varAndConst/const.go:22: 定制 flag 之后的日志打印
[CnPeng]2020/09/17 08:40:57.335490 /Users/cnpeng/CnPeng/04_Demos/12_Go/oldBoy/varAndConst/const.go:24: 定制 flag prefix 之后的日志打印
```
如果要使用标准的 logger我们通常会把上面的配置操作写到 init 函数中。
```go
func init() {
logFile, err := os.OpenFile("./xx.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Println("open log file failed, err:", err)
return
}
log.SetOutput(logFile)
log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
}
```
### 15.1.3 创建logger
log 标准库中还提供了一个创建新 logger 对象的构造函数–`New`,支持我们创建自己的 logger 示例。New函数的签名如下
```go
func New(out io.Writer, prefix string, flag int) *Logger
```
New 创建一个 Logger 对象。其中,
* 参数 `out` 设置日志信息写入的目的地。
* 参数 `prefix` 会添加到生成的每一条日志前面。
* 参数 `flag` 定义日志的属性(时间、文件等等)。
举个例子:
```go
package main
import (
"log"
"os"
)
func main() {
// Stdout 表示终端
cusLog := log.New(os.Stdout, "[CnPeng]", log.Lshortfile|log.Ldate|log.Ltime)
cusLog.Println("自定义的log对象输出日志")
}
```
将上面的代码编译执行之后,得到结果如下:
```go
[CnPeng]2020/09/17 08:48:34 const.go:10: 自定义的log对象输出日志
```
### 15.1.4 总结
Go 内置的 log 库功能有限,例如无法满足记录不同级别日志的情况,我们在实际的项目中根据自己的需要选择使用第三方的日志库,如 `logrus``zap` 等。
## 15.2 第三方日志库 logrus 使用
日志是程序中必不可少的一个环节,由于 Go 语言内置的日志库功能比较简洁,我们在实际开发中通常会选择使用第三方的日志库来进行开发。本文介绍了 logrus 这个日志库的基本使用。
### 15.2.1 logrus介绍
Logrus 是 Gogolang的结构化 logger与标准库 logger 的 API 完全兼容。
它有以下特点:
* 完全兼容标准日志库,拥有七种日志级别:`Trace`, `Debug`, `Info`, `Warning`, `Error`, `Fataland`,` Panic`
* 可扩展的 Hook 机制,允许使用者通过 Hook 的方式将日志分发到任意地方,如本地文件系统,`logstash``elasticsearch`或者`mq`等,或者通过 Hook 定义日志内容和格式等
* 可选的日志输出格式,内置了两种日志格式 `JSONFormater``TextFormatter`,还可以自定义日志格式
* `Field` 机制,通过 `Filed` 机制进行结构化的日志记录
* 线程安全
### 15.2.2 安装
> CnPeng 不执行该安装也可以。不安装时直接在使用的地方导入,运行之后就会主动的安装。
```go
go get github.com/sirupsen/logrus
```
### 15.2.3 基本示例
使用 Logrus 最简单的方法是简单的包级导出日志程序:
```go
package main
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.WithFields(log.Fields{"animal": "dog"}).Info("引入三方日志库")
}
```
运行结果如下:
```go
CnPeng:varAndConst cnpeng$ go run const.go
go: finding module for package github.com/sirupsen/logrus
go: found github.com/sirupsen/logrus in github.com/sirupsen/logrus v1.6.0
INFO[0000] 引入三方日志库 animal=dog
```
### 15.2.4 进阶示例
对于更高级的用法,例如在同一应用程序记录到多个位置,你还可以创建 logrus Logger 的实例:
```go
package main
import (
"os"
"github.com/sirupsen/logrus"
)
func main() {
log := logrus.New()
log.Out = os.Stdout
log.WithFields(logrus.Fields{"animal": "dog"}).Info("引入三方日志库")
log.Info("logrus.Fields 是可选的")
fileObj, err := os.OpenFile("logrus.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if nil != err {
log.Info("创建日志文件出错了")
return
}
log.Out = fileObj
log.Info("这一句日志信息将写入到 logrus.log 文件中")
}
```
上述代码运行之后,控制台的输出信息为:
```go
CnPeng:varAndConst cnpeng$ go run const.go
INFO[0000] 引入三方日志库 animal=dog
INFO[0000] logrus.Fields 是可选的
```
另外,还会在当前 go 文件的同级目录下创建一个 `logrus.log` 文件,如下:
![](pics/15-2-logrus进阶用法.png)
### 15.2.5 日志级别
#### 15.2.5.1 日志级别
Logrus 有七个日志级别:`Trace`, `Debug`, `Info`, `Warning`, `Error`, `Fataland `,`Panic`
```go
log.Trace("Something very low level.")
log.Debug("Useful debugging information.")
log.Info("Something noteworthy happened!")
log.Warn("You should probably take a look at this.")
log.Error("Something failed but I'm not quitting.")
// 记完日志后会调用os.Exit(1)
log.Fatal("Bye.")
// 记完日志后会调用 panic()
log.Panic("I'm bailing.")
```
#### 15.2.5.2 设置日志级别
你可以在 Logger 上设置日志记录级别,然后它只会记录具有该级别或以上级别任何内容的条目:
```go
// 会记录info及以上级别 (warn, error, fatal, panic)
log.SetLevel(log.InfoLevel)
```
如果你的程序支持 debug 或环境变量模式,设置 `log.Level = logrus.DebugLevel` 会很有帮助。
### 15.2.6 字段
Logrus 鼓励通过日志字段进行谨慎的结构化日志记录,而不是冗长的、不可解析的错误消息。
例如,区别于使用 `log.Fatalf("Failed to send event %s to topic %s with key %d")`,你应该使用如下方式记录更容易发现的内容:
```go
log.WithFields(log.Fields{
"event": event,
"topic": topic,
"key": key,
}).Fatal("Failed to send event")
```
**WithFields 的调用是可选的。**
### 15.2.7 默认字段
通常,将一些字段始终附加到应用程序的全部或部分的日志语句中会很有帮助。例如,你可能希望始终在请求的上下文中记录 `request_id``user_ip`
区别于在每一行日志中写上 `log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})`,你可以向下面的示例代码一样创建一个 `logrus.Entry` 去传递这些字段。
```go
requestLogger := log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})
# will log request_id and user_ip
requestLogger.Info("something happened on that request")
requestLogger.Warn("something not great happened")
```
### 15.2.8 日志条目
除了使用 `WithField``WithFields` 添加的字段外,一些字段会自动添加到所有日志记录事中:
* time记录日志时的时间戳
* msg记录的日志信息
* level记录的日志级别
### 15.2.9 Hooks
你可以添加日志级别的钩子Hook。例如向异常跟踪服务发送 `Error``Fatal ``Panic`、信息到StatsD 或同时将日志发送到多个位置,例如 syslog。
Logrus 配有内置钩子。在 init 中添加这些内置钩子或你自定义的钩子:
```go
import (
log "github.com/sirupsen/logrus"
"gopkg.in/gemnasium/logrus-airbrake-hook.v2" // the package is named "airbrake"
logrus_syslog "github.com/sirupsen/logrus/hooks/syslog"
"log/syslog"
)
func init() {
// Use the Airbrake hook to report errors that have Error severity or above to
// an exception tracker. You can create custom hooks, see the Hooks section.
log.AddHook(airbrake.NewHook(123, "xyz", "production"))
hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
if err != nil {
log.Error("Unable to connect to local syslog daemon")
} else {
log.AddHook(hook)
}
}
```
注意Syslog 钩子还支持连接到本地 syslog例如. `/dev/log` or `/var/run/syslog` or `/var/run/log`)。有关详细信息,请查看 syslog hook README。
### 15.2.10 格式化
logrus 内置以下两种日志格式化程序:
`logrus.TextFormatter``logrus.JSONFormatter`
还支持一些第三方的格式化程序,详见项目首页。
### 15.2.11 记录函数名
> CnPeng 实际操作时,没看明白怎么得到的示例中的结果
如果你希望将调用的函数名添加为字段,请通过以下方式设置:
```go
log.SetReportCaller(true)
```
这会将调用者添加为 ”method”如下所示
```go
{"animal":"penguin","level":"fatal","method":"github.com/sirupsen/arcticcreatures.migrate","msg":"a penguin swims by",
"time":"2014-03-10 19:57:38.562543129 -0400 EDT"}
```
**注意:开启这个模式会增加性能开销。**
### 15.2.12 线程安全
默认的 logger 在并发写的时候是被 mutex 保护的,比如当同时调用 hook 和写 log 时 mutex 就会被请求,有另外一种情况,文件是以 `appending mode` 打开的, 此时的并发操作就是安全的,可以用`logger.SetNoLock()` 来关闭它。
### 15.2.13 gin 框架使用 logrus
Gin 是一个 go 写的 web 框架具有高性能的优点。官方地址https://github.com/gin-gonic/gin
> CnPeng 下面的代码运行之后并没有得到想要的结果。。。 gin.log 文件中没有内容,不知道为啥
```go
// a gin with logrus demo
package main
import (
"os"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
var log = logrus.New()
func init() {
// Log as JSON instead of the default ASCII formatter.
log.Formatter = &logrus.JSONFormatter{}
// Output to stdout instead of the default stderr
// Can be any io.Writer, see below for File example
f, _ := os.Create("./gin.log")
log.Out = f
gin.SetMode(gin.ReleaseMode)
gin.DefaultWriter = log.Out
// Only log the warning severity or above.
log.Level = logrus.InfoLevel
}
func main() {
// 创建一个默认的路由引擎
r := gin.Default()
// GET请求方式/hello请求的路径
// 当客户端以GET方法请求/hello路径时会执行后面的匿名函数
r.GET("/hello", func(c *gin.Context) {
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Warn("A group of walrus emerges from the ocean")
// c.JSON返回JSON格式的数据
c.JSON(200, gin.H{
"message": "Hello world!",
})
})
// 启动HTTP服务默认在0.0.0.0:8080启动服务
r.Run()
}
```
## 15.3 自定义实现日志库
视频 81-86, 重点是视频 85。
### 15.3.1 需求分析
一个日志库需要支持如下内容:
* 1. 支持向不同的地方输出
* `fmt.Fprintf` 指定内容写到哪里去
* `path.Join( filePath, fileName)` 将路径和名字进行拼接,得到完整的路径
* `os.OpenFile(fullFileName, ,)` 打开文件,后面两个参数为写入模式,
* 2. 日志需要分级
* Debug
* Trace
* Info
* Waring
* Error
* Fatal
* 3. 支持开关控制(开发接口可以打印任意级别的日志,线上环境则不允许打印日志)
* 定义 int 常量用于比较级别
* 4. 完整的日志记录要包含有时间、行号、文件名、日志级别、日志信息
* `time.Now()` 获取时间
* `runtime.Caller(int) `获取文件信息fileInfo、行号、函数信息(funcInfo)。其中的 int 表示从该处上溯几层,获取的是上溯到层数的文件、行数、函数信息。
* 然后通过 `path.Base(fileInfo)` 可以得到文件名
* 通过 `runtime.FuncForPc(funcInfo).NAme()` 可以获取 `包名.函数名` 然后再通过 `strings.Split(,)` 得到具体的函数名。
* `fmt.Sprintf(a,b...)` 可以将 a 和 可变长参数 b 拼接成一个字符串。
* 5. 日志文件需要切割
* 可以按日期切割
* 可以按文件大小切割,
* 预定义一个单文件最大值,每次写日志时判断日志文件是否超出该值
* 切割时,
* 先关闭当前的日志文件——`fileOj.close()`
* 然后通过 `os.Rename()` 对文件进行备份
* 打开一个新的日志文件
* 将新打开的日志文件对象作为删除目标
获取文件大小的操作如下:
```go
// 获取文件大小
fileObj , err := os.Open("./main.go")
if err != nil {
fmt.Printf("文件打开失败,错误信息为:%v",err)
return
}
// 检查得到的文件对象类型,输出为:`*os.File`
fmt.Printf("%T\n",fileObj)
// 获取文件对象的详细信息
fileInfo,err := fileObj.Stat()
if err != nil {
fmt.Printf("获取文件信息时出错,错误为:%v\n",err)
return
}
// 得到的是字节
fmt.Printf("文件大小是:%dB\n",fileInfo.Size())
```
切割日志文件:
```go
// 1 将原名字 xx.log 备份为 xx.log.bak20200927084105
timeStr := time.Now().Format("20200927084105")
// 使用这种方式拼接不需要区分是 Win 还是 Linux
preLogName := path.Join(filePath,fileName)
newLogName := fmt.Sprintf("%s.bak%s",preLogName,timeStr)
os.Rename(preLogName,newLogName)
//2 关闭当前日志文件——假设当前日志文件对象为 fileObj
fileObj.Close()
//3 打开一个新的日志文件
newFileObj,err := os.OpenFile(newLogName, os.O_CREATE|os.O_APPEND|os.O_WRONLY,0644)
if err != nil {
fmt.Prinftf("文件打开出错了,%v\n",err)
return
}
//4 将新打开的文件对象赋值给日志对象中的文件对象字段。然后继续执行写操作即可。
```
### 15.3.2 自定义日志库
首字母大写的标识符是对外暴露的标识符,必须要添加注释信息。

View File

@@ -0,0 +1,89 @@
# 概述
log 模块用于在程序中输出日志它的使用十分简单类似于fmt中的Print一个最简单的示例如下
```
package main
import "log"
func main() {
log.Print("Hello World")
}
```
上面的程序会在命令行打印一条日志:
```
>>> 2018/05/16 16:48:06 Hello World
```
# Logger
Logger是写入日志的基本组件log模块中存在一个标准Logger可以直接通过log进行访问所以在上一节的例子中可以直接使用log.Print进行日志进行输出。但是在实际使用中不同类型的日志可能拥有需求仅标准Logger不能满足日志记录的需求通过创建不同的Logger可以将不同类型的日志分类输出。使用logger前需要首先通过New函数创建一个Logger对象函数声明如下
```
func New(out io.Writer, prefix string, flag int) *Logger
```
函数接收三个参数分别是日志输出的IO对象日志前缀和日志包含的通用信息标识位通过对它们进行设置可以对Logger进行定制。其中IO对象通常是标准输出os.Stdoutos.Stderr或者绑定到文件的IO。日志前缀和信息标识位可以对日志的格式进行设置。
一条日志由三个部分组成,其结构如下:
```
{日志前缀} {标识1} {标识2} ... {标识n} {日志内容}
```
- 日志前缀通过prefix参数设置可以是任意字符串
- 标识通过flags参数设置当某个标识被设置会在日志中进行显示log模块中定义了如下标识多个标识通过按位或进行组合
- Ldate 显示当前日期(当前时区)
- Ltime 显示当前时间(当前时区)
- Lmicroseconds 显示当前时间(微秒)
- Llongfile 包含路径的完整文件名
- Lshortfile 不包含路径的文件名
- LUTC Ldata和Ltime使用UTC时间
- LstdFlags 标准Logger的标识等价于 Ldate | Ltime
```
package main
import (
"os"
"log"
)
func main() {
prefix := "[THIS IS THE LOG]"
logger := log.New(os.Stdout, prefix, log.LstdFlags | log.Lshortfile)
logger.Print("Hello World")
}
```
上面的程序将会输出如下内容,可以看到日志由上述三个部分组成。
```
[THIS IS THE LOG]2018/05/16 17:12:19 log.go:11: Hello World
```
# 更多的输出方式
log模块中日志输出分为三类PrintFatalPanic。Print是普通输出Fatal是在执行完Print之后执行 os.Exit(1)Panic是在执行完Print之后调用panic()方法。
除了基础的Print之外还有Printf和Println方法对输出进格式化对于Fatal和Panic也是类似具体的函数声明 [Log Index](https://www.godoc.org/log#pkg-index)
## 日志分级
Go的log模块没有对日志进行分级的功能对于这部分需求可以在log的基础上进行实现下面是一个简单的INFO方法实现。
```
package main
import (
"os"
"log"
)
func main() {
var (
logger = log.New(os.Stdout, "INFO: ", log.Lshortfile)
infof = func(info string) {
logger.Print(info)
}
)
infof("Hello world")
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -0,0 +1,180 @@
# 概述
net/http可以用来处理HTTP协议包括HTTP服务器和HTTP客户端http包主要由五个部分组成
- RequestHTTP请求对象
- ResponseHTTP响应对象
- ClientHTTP客户端
- ServerHTTP服务端
# 最简单的使用
http包提供了对应于每个HTTP动词的函数来发送HTTP请求当你不需要对请求进行详细的定制时可以直接使用它们。
```
resp, err := http.Get("http://example.com/") // GET
resp, err := http.Post("http://example.com/") // POST
resp, err := http.PostForm("http://example.com/", url.Values{"foo": "bar"}) // 提交表单
```
# HTTP请求和响应
HTTP作为一个通信协议通过报文传递信息报文分为请求报文和响应报文在http包中分别用Reqeust和Response对象进行了抽象。
## Request
可以通过NewRequest创建一个Request对象方法声明如下需要传入HTTP方法URL以及报文体进行初始化
```
func NewRequest(method, url string, body io.Reader) (*Request, error)
```
Request对象主要用于数据的存储结构如下
```
type Request struct {
Method string // HTTP方法
URL *url.URL // URL
Proto string // "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
Header Header // 报文头
Body io.ReadCloser // 报文体
GetBody func() (io.ReadCloser, error)
ContentLength int64 // 报文长度
TransferEncoding []string // 传输编码
Close bool // 关闭连接
Host string // 主机名
Form url.Values //
PostForm url.Values // POST表单信息
MultipartForm *multipart.Form // multipart
Trailer Header
RemoteAddr string
RequestURI string
TLS *tls.ConnectionState
Cancel <-chan struct{}
Response *Response
}
```
可以看到Request对象可以对请求报文的各个方面进行设置除了上述属性之外Request也提供了一些方法对这些属性进行访问和修改这里不具体展开详细文档可见[Request](https://golang.org/pkg/net/http/#Request)。
## Response
和Request对象类似Response也是一个数据对象拥有多个字段来描述HTTP响应需要注意的是Reponse对象拥有了当前Request对象的引用对象的具体声明如下
```
type Response struct {
Status string // HTTP 状态 "200 OK"
StatusCode int // 状态码 200
Proto string // 版本号 "HTTP/1.0"
ProtoMajor int // 主版本号
ProtoMinor int // 次版本号
Header Header // 响应报文头
Body io.ReadCloser // 响应报文体
ContentLength int64 // 报文长度
TransferEncoding []string // 报文编码
Close bool
Trailer Header
Request *Request // 请求对象
TLS *tls.ConnectionState
}
```
# Client
实际上第一节中的GETPOST等函数就是通过绑定到默认Client实现的。我们也可以创建自己的client对象要通过Client发出HTTP请求你需要首先初始化一个Client对象然后发出请求例如下面的程序可以访问Google
```
package main
import "net/http"
func main() {
client := http.Client()
res, err := client.Get("http://www.google.com")
}
```
对于常用HTTP动词Client对象都有对应的函数进行处理
```
func (c *Client) Get(url string) (resp *Response, err error)
func (c *Client) Head(url string) (resp *Response, err error)
func (c *Client) Post(url string, contentType string, body io.Reader) (resp *Response, err error)
func (c *Client) PostForm(url string, data url.Values) (resp *Response, err error)
```
但是在很多情况下需要支持对报文头Cookies等的定制上面提供的方法就不能满足需求了。所以Client对象还提供了一个Do方法通过传入一个Request对象达到请求的定制化具体方法声明如下
```
func (c *Client) Get(url string) (resp *Response, err error)
```
下面一个简单的配置实例:
```
package main
import (
"net/http"
"fmt"
"io/ioutil"
)
func main() {
req, err := http.NewRequest(http.MethodGet, "http://www.baidu.com", nil)
if err != nil {
fmt.Println(err.Error())
return
}
req.Header.Set("Cookie", "name=foo")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := http.Client{}
res, err := client.Do(req)
// defer res.Body.Close()
if err != nil {
fmt.Println(err.Error())
return
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Println(string(body))
}
```
# Server
http包除了可以发送HTTP请求之外也可以创建HTTP服务器对外提供访问。可以通过ListenandServe方法创建一个HTTP服务。
```
package main
import (
"net/http"
"io/ioutil"
"log"
)
func EchoServer(w http.ResponseWriter, req *http.Request) {
body, err := ioutil.ReadAll(req.Body)
io.WriteString(w, body)
}
func main() {
handleFunc := http.HandleFunc("/echo/", EchoServer)
log.Fatal(http.ListenAndServe(":8080", nil))
}
```
在Server模块中有两个概念一个是URL一个是Handler前者是访问的URL后者是对应的处理函数。Server需要完成从URL到Handler映射在http包中的默认实现是DefaultServerMux每个Handler需要通过HandleFunc进行注册。
除了上面的默认方式之外和Client一样可以通过创建Server实例对服务进行定制整体的流程差别不大这里就不再展开。
# HTTP方法和状态码
除了上面的内容以外http包还定义了一系列常量用于表示HTTP动词和返回状态码详见[constants](https://golang.org/pkg/net/http/#pkg-constants)

View File

@@ -0,0 +1,48 @@
# 概述
`filepath`包的功能和`path`包类似但是对于不同操作系统提供了更好的支持。filepath包能够自动的根据不同的操作系统文件路径进行转换所以如果你有跨平台的需求你需要使用`filepath`
# 与`path`包相同的函数
`filepath`包中的函数和path包很类似其中对应函数的功能相同只是一个可以跨平台一个不能所以这里不详细展开可以从 [path](https://github.com/preytaren/go-doc-zh/blob/master/path/path.md) 中获取这些函数的详细说明。主要函数如下:
- func Base(path string) string
- func Dir(path string) string
- func Ext(path string) string
- func Join(elem ...string) string
- func Split(path string) (dir, file string)
# 其他函数
剩下的还有两个函数值得一说,一个是`Abs`函数,可以将一个文件路径转换为绝对路径。函数声明如下:
```
func Abs(path string) (string, error)
```
另一个是`Walk`函数,和`filepath`包中的其他函数不同,它并不对文件路径字符串进行操作,而可以访问更多文件信息。它通过遍历的方式对目录中的每个子路径进行访问,函数接收两个参数,一个是路径名,另一个是遍历函数`WalkFunc`,函数声明如下:
```
func Walk(root string, walkFn WalkFunc) error
type WalkFunc func(path string, info os.FileInfo, err error) error
```
WalkFunc接收三个参数分别是当前子路径路径的`FileInfo`对象,以及一个可能的访问错误信息。下面是一个简单的示例,打印了当前目录的所有文件。
```
package main
import (
"path/filepath"
"os"
"fmt"
)
func main() {
filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
fmt.Println(info.Name())
return nil
})
}
```

View File

@@ -0,0 +1,52 @@
# 概述
path包提供了许多辅助函数来处理UNIX系统文件路径
# 辅助函数
一个unix文件路径有如下格式`<DirName>/<BaseName>`分别对应于目录路径和基础路径当这个路径表示一个文件时BaseName就对应于文件名。
其中Base函数获取一个路径的BaseNameDir函数获取一个路径的DirName具体函数声明如下
```
func Base(path string) string
func Dir(path string) string
```
在UNIX文件系统中一个完整文件名由文件名和文件后缀组成比如.go.cExt函数可以用于获取路径中的后缀名。
```
func Ext(path string) string
```
对于一个目录和文件还有绝对路径和相对路径的概念,绝对路径就是从根目录开始的完整路径,比如`/a/b/c`;相对路径就是相对于当前目录的路径,比如`a/b/c``../a/b/c`。使用IsAbs可以判断一个路径是否是绝对路径具体函数声明如下
```
func IsAbs(path string) bool
```
最后path包还提供了两个函数用于组合和拆分一个文件路径。Split函数将路径拆分为目录名和文件名Join函数以`/`为分隔符将多个字符串进行连接;函数声明如下:
```
func Split(path string) (dir, file string)
func Join(elem ...string) string
```
# 实例
```
package main
import (
"path"
"fmt"
)
func main() {
p := "foo/bar.tar"
sDir, sBase := path.Split(p)
fmt.Println(sDir)
fmt.Println(sBase)
fmt.Println(path.Ext(p))
}
```

View File

@@ -0,0 +1,383 @@
# 16 反射
基于 [Go语言基础之反射](https://www.liwenzhou.com/posts/Go/13_reflect/) 和视频内容整理
## 16.1 变量的内在机制
Go语言中的变量是分为两部分的:
* 类型信息:预先定义好的元信息。
* 值信息:程序运行过程中可动态变化的。
## 16.2 反射介绍
反射是指**在程序运行期对程序本身进行访问和修改的能力**。
程序在编译时,**变量被转换为内存地址,变量名不会被编译器写入到可执行部分**。在运行程序时,程序无法获取自身的信息。
支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
Go程序在运行期使用 `reflect` 包访问程序的反射信息。
在之前章节中我们介绍了空接口。 空接口可以存储任意类型的变量,那我们如何知道这个空接口保存的数据是什么呢? 反射就是在运行时动态的获取一个变量的类型信息和值信息。
## 16.3 reflect包
在 Go 语言的反射机制中,任何接口值都由是一个**具体类型和具体类型的值**两部分组成的(我们在上一篇接口的博客中有介绍相关概念)。
在 Go 语言中反射的相关功能由内置的 reflect 包提供,任意接口值在反射中都可以理解为由`reflect.Type``reflect.Value` 两部分组成,并且 reflect 包提供了 `reflect.TypeOf``reflect.ValueOf` 两个函数来获取任意对象的 Value 和 Type。
### 16.3.1 TypeOf
#### 16.3.1.1 TypeOf
在 Go 语言中,使用 `reflect.TypeOf()` 函数可以获得任意值的类型对象——`reflect.Type`,程序通过类型对象可以访问任意值的类型信息。
```go
package main
import (
"fmt"
"reflect"
)
func reflectType(x interface{}) {
v := reflect.TypeOf(x)
fmt.Printf("type:%v\n", v)
}
func main() {
var a float32 = 3.14
reflectType(a) // type:float32
var b int64 = 100
reflectType(b) // type:int64
}
```
#### 16.3.1.2 type name 和 type kind
在反射中关于类型还划分为两种:`类型Type``种类Kind`。因为在 Go 语言中我们**可以使用 type 关键字构造很多自定义类型而种类Kind就是指底层的类型**.
**在反射中当需要区分指针、结构体等大品种的类型时就会用到种类Kind**。 举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。
```go
package main
import (
"fmt"
"reflect"
)
type myInt int64
func reflectType(x interface{}) {
t := reflect.TypeOf(x)
fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}
func main() {
var a *float32 // 指针
var b myInt // 自定义类型
var c rune // int32 的类型别名
reflectType(a) // type: kind:ptr
reflectType(b) // type:myInt kind:int64
reflectType(c) // type:int32 kind:int32
type person struct {
name string
age int
}
type book struct{ title string }
var d = person{
name: "沙河小王子",
age: 18,
}
var e = book{title: "《跟小王子学Go语言》"}
reflectType(d) // type:person kind:struct
reflectType(e) // type:book kind:struct
}
```
**Go语言的反射中像数组、切片、Map、指针等类型的变量它们的.Name()都是返回空。**
在 reflect 包中定义的 Kind 类型如下:
```go
type Kind uint
const (
Invalid Kind = iota // 非法类型
Bool // 布尔型
Int // 有符号整型
Int8 // 有符号8位整型
Int16 // 有符号16位整型
Int32 // 有符号32位整型
Int64 // 有符号64位整型
Uint // 无符号整型
Uint8 // 无符号8位整型
Uint16 // 无符号16位整型
Uint32 // 无符号32位整型
Uint64 // 无符号64位整型
Uintptr // 指针
Float32 // 单精度浮点数
Float64 // 双精度浮点数
Complex64 // 64位复数类型
Complex128 // 128位复数类型
Array // 数组
Chan // 通道
Func // 函数
Interface // 接口
Map // 映射
Ptr // 指针
Slice // 切片
String // 字符串
Struct // 结构体
UnsafePointer // 底层指针
)
```
## 16.3.2 ValueOf
`reflect.ValueOf()` 返回的是 `reflect.Value` 类型,其中包含了原始值的值信息。
`reflect.Value`与原始值之间可以互相转换。
`reflect.Value` 类型提供的获取原始值的方法如下:
方法 | 说明
---|---
`Interface() interface {}` | 将值以 `interface{}` 类型返回,可以通过类型断言转换为指定类型
`Int() int64` | 将值以 int 类型返回,所有有符号整型均可以此方式返回
`Uint() uint64` | 将值以 uint 类型返回,所有无符号整型均可以此方式返回
`Float() float64` | 将值以双精度float64类型返回所有浮点数float32、float64均可以此方式返回
`Bool() bool` | 将值以 bool 类型返回
`Bytes() []bytes` | 将值以字节数组 `[]bytes` 类型返回
`String() string` | 将值以字符串类型返回
#### 16.3.2.1 通过反射获取值
```go
func reflectValue(x interface{}) {
v := reflect.ValueOf(x)
k := v.Kind()
switch k {
case reflect.Int64:
// v.Int()从反射中获取整型的原始值然后通过int64()强制类型转换
fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
case reflect.Float32:
// v.Float()从反射中获取浮点型的原始值然后通过float32()强制类型转换
fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
case reflect.Float64:
// v.Float()从反射中获取浮点型的原始值然后通过float64()强制类型转换
fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
}
}
func main() {
var a float32 = 3.14
var b int64 = 100
reflectValue(a) // type is float32, value is 3.140000
reflectValue(b) // type is int64, value is 100
// 将int类型的原始值转换为reflect.Value类型
c := reflect.ValueOf(10)
fmt.Printf("type c :%T\n", c) // type c :reflect.Value
}
```
#### 16.3.2.2 通过反射设置变量的值
想要在函数中通过反射修改变量的值,需要注意**函数参数传递的是值拷贝,必须传递变量地址才能修改变量值**。而 **反射中使用专有的 `Elem()` 方法来获取指针对应的值**
```go
package main
import (
"fmt"
"reflect"
)
func reflectSetValue1(x interface{}) {
v := reflect.ValueOf(x)
if v.Kind() == reflect.Int64 {
v.SetInt(200) //修改的是副本reflect包会引发panic
}
}
func reflectSetValue2(x interface{}) {
v := reflect.ValueOf(x)
// 反射中使用 Elem()方法获取指针对应的值
if v.Elem().Kind() == reflect.Int64 {
v.Elem().SetInt(200)
}
}
func main() {
var a int64 = 100
// reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
reflectSetValue2(&a)
fmt.Println(a)
}
```
#### 16.3.2.3 isNil() 和 isValid()
* `isNil()`
```go
func (v Value) IsNil() bool
```
`IsNil()` 报告 v 持有的值是否为 nil 。v 持有的值的分类**必须是通道、函数、接口、映射、指针、切片**之一;否则 IsNil 函数会导致 panic 。
* `isValid()`
```go
func (v Value) IsValid() bool
```
`IsValid()` 返回 v 是否持有一个值。如果 v 是 Value **零值会返回假**,此时 v 除了 `IsValid``String``Kind` 之外的方法都会导致 panic。
`IsNil()` 常被用于判断指针是否为空;`IsValid()`常被用于判定返回值是否有效。
举个例子
```go
func main() {
// *int类型空指针
var a *int
fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil())
// nil值
fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid())
// 实例化一个匿名结构体
b := struct{}{}
// 尝试从结构体中查找"abc"字段
fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid())
// 尝试从结构体中查找"abc"方法
fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid())
// map
c := map[string]int{}
// 尝试从map中查找一个不存在的键
fmt.Println("map中不存在的键", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid())
}
```
## 16.4 结构体反射
### 16.4.1 与结构体相关的方法
任意值通过 `reflect.TypeOf()` 获得反射对象信息后,**如果它的类型是结构体可以通过反射值对象reflect.Type`NumField()``Field()` 方法获得结构体成员的详细信息**。
`reflect.Type` 中与获取结构体成员相关的的方法如下表所示。
方法 | 说明
---|---
`Field(i int) StructField` | 根据索引,返回索引对应的结构体字段的信息。
`NumField() int` | 返回结构体成员字段数量。
`FieldByName(name string) (StructField, bool)` |根据给定字符串返回字符串对应的结构体字段的信息。
`FieldByIndex(index []int) StructField` | 多层成员访问时,根据 `[]int` 提供的每个结构体的字段索引,返回字段的信息。
`FieldByNameFunc(match func(string) bool) (StructField,bool)` | 根据传入的匹配函数匹配需要的字段。
`NumMethod() int` | 返回该类型的方法集中方法的数目
`Method(int) Method` | 返回该类型方法集中的第 i 个方法
`MethodByName(string)(Method, bool)` | 根据方法名返回该类型方法集 中的方法
### 16.4.2 StructField 类型
StructField 类型用来描述结构体中的一个字段的信息。
StructField 的定义如下:
```go
type StructField struct {
// Name是字段的名字。PkgPath是非导出字段的包路径对导出字段该字段为""。
// 参见http://golang.org/ref/spec#Uniqueness_of_identifiers
Name string
PkgPath string
Type Type // 字段的类型
Tag StructTag // 字段的标签
Offset uintptr // 字段在结构体中的字节偏移量
Index []int // 用于 Type.FieldByIndex 时的索引切片
Anonymous bool // 是否匿名字段
}
```
### 16.4.3 结构体反射示例
当我们使用反射得到一个结构体数据之后可以通过索引依次获取其字段信息,也可以通过字段名去获取指定的字段信息。
```go
type student struct {
Name string `json:"name"`
Score int `json:"score"`
}
func main() {
stu1 := student{
Name: "小王子",
Score: 90,
}
t := reflect.TypeOf(stu1)
fmt.Println(t.Name(), t.Kind()) // student struct
// 通过for循环遍历结构体的所有字段信息
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
}
// 通过字段名获取指定结构体字段信息
if scoreField, ok := t.FieldByName("Score"); ok {
fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
}
}
```
接下来编写一个函数 `printMethod(s interface{})` 来遍历打印s包含的方法。
```go
// 给student添加两个方法 Study和Sleep(注意首字母大写)
func (s student) Study() string {
msg := "好好学习,天天向上。"
fmt.Println(msg)
return msg
}
func (s student) Sleep() string {
msg := "好好睡觉,快快长大。"
fmt.Println(msg)
return msg
}
func printMethod(x interface{}) {
t := reflect.TypeOf(x)
v := reflect.ValueOf(x)
fmt.Println(t.NumMethod())
for i := 0; i < v.NumMethod(); i++ {
methodType := v.Method(i).Type()
fmt.Printf("method name:%s\n", t.Method(i).Name)
fmt.Printf("method:%s\n", methodType)
// 通过反射调用方法传递的参数必须是 []reflect.Value 类型
var args = []reflect.Value{}
v.Method(i).Call(args)
}
}
```
通过反射调用方法的同时传参(摘自原文的评论):
```go
v.Method(index).Call([]reflect.Value{
reflect.ValueOf("hi~"),
})
```
## 16.5 反射是把双刃剑
反射是一个强大并富有表现力的工具,能让我们写出更灵活的代码。但是反射不应该被滥用,原因有以下三个。
* 基于反射的代码是极其脆弱的反射中的类型错误会在真正运行的时候才会引发panic那很可能是在代码写完的很长时间之后。
* 大量使用反射的代码通常难以理解。
* 反射的性能低下,基于反射实现的代码通常比正常代码运行速度慢一到两个数量级。
## 16.6练习题
编写代码利用反射实现一个 ini 文件的解析器程序。
**TODO : V87-V91 还没有观看**

View File

@@ -0,0 +1,722 @@
## 前言
这篇文章大概2个月前就开始在边看边翻译了可惜生了场病然后不断地出问题工作也没了人比较焦虑。就耽搁了。
在我的初衷里是准备对Go语言进行一个全面的系统的学习和研究的但是目前看下来是坚持不下去了。只得暂摆不过这篇文章弄到一半放在这里心里也是一个梗就趁着最近的时间把它全部翻完说实话这工作好累一方面是英文的基础不扎实再碰到大量的计算机术语更是一道壁垒。不过总算也是坚持子下来翻完之后发现网上其实已经有别人做了同样的事情撞车了。好在自己也算是学习一下知识并不会觉得是无用功。  
之后又花了一点时间,对照着别人的成果,将我自己的成果重新润了一下色,让句子读起来更通顺一点,并加了一些自己的理解。  
应该说这一篇是自己最用心翻译的文章虽然说谈不上完美算是对我Go语言学习reflect部分的一个总结。当然这篇文章在整个Go的reflect领域里面还是太浅有兴趣可以看一下深入的知识。
## Go语言反射之定律
## The Laws of Reflection
### 介绍
### Introduction
在计算机领域中反射是一种主要通过types`[类型]`,检查自身结构的编程手段。它是<b>元编程</b>的一种形式,同时也是引发混乱的根源`[相对于程序本身而言]``注用来生成代码的程序有时被称为元程序metaprogram编写这种程序就称为元编程metaprogramming。`
Reflection in computing is the ability of a program to examine its own structure, particularly through types; it's a form of metaprogramming. It's also a great source of confusion.
在这篇文章中我们试图通过阐释GO语言中反射的运作机制从而理清孕育于其中的概念。每个语言的反射模式都是不尽相同的当然也有很多语言并不支持反射由于我们这篇文章只涉及到Go语言的内容所以后面所指的“reflection”就仅仅意味着“reflection in Go”即Go语言中的反射。
In this article we attempt to clarify things by explaining how reflection works in Go. Each language's reflection model is different (and many languages don't support it at all), but this article is about Go, so for the rest of this article the word "reflection" should be taken to mean "reflection in Go".
### 类型和接口
### Types and interfaces
因为反射是建立在类型系统上的所以我们需要整理一下Go语言中的types这个概念。
Because reflection builds on the type system, let's start with a refresher about types in Go.
Go是一种基于<b>静态类型化</b>的的语言。每一个变量都有一个静态类型也即而言在编译周期中将会确定其变量的类型int整形float32浮点*MyType指针 []byte比特诸如此类。如果我们声明
Go is statically typed. Every variable has a static type, that is, exactly one type known and fixed at compile time: int, float32, *MyType, []byte, and so on. If we declare
```go
type MyInt int //声明一个新的类型名为MyInt其基本类型是int类型
var i int //声明一个int变量
var j MyInt //声明一个MyInt变量
```
那么变量i是整数类型变量j则是自定义的MyInt`[本质上还是int]`类型。变量i和j都有着一个很清晰的静态类型以及虽然它们有一个同样的基本类型`[也就是本质上都是int类型]`,但它们并不能在未经过转换的情况下进行互相赋值的操作。
then i has type int and j has type MyInt. The variables i and j have distinct static types and, although they have the same underlying type, they cannot be assigned to one another without a conversion.
在众多变量类型的种类中,接口类型无疑是非常重要的一种,它能用来表示方法的固定集合。一个接口变量能存储任意的具体(非接口)数值,只要这个数值实现了接口方法。一个众所周知的双组类型的例子是`io.Reader``io.Writer`此读和写两个类型源自于io包
`pair 是一种模版类型。每个pair 可以存储两个值。这个单词真不好翻译,原意是对、双的意思,我这里就称之为双组类型,后面会有更多的提到`
One important category of type is interface types, which represent fixed sets of methods. An interface variable can store any concrete (non-interface) value as long as that value implements the interface's methods. A well-known pair of examples is io.Reader and io.Writer, the types Reader and Writer from the io package:
```go
// Reader is the interface that wraps the basic Read method.
type Reader interface {
Read(p []byte) (n int, err error)
}
// Writer is the interface that wraps the basic Write method.
type Writer interface {
Write(p []byte) (n int, err error)
}
```
任何类型只要是实现了读或写的声明方法就意味着实现了io.Reader (或io.Writer)。此间讨论的目的主要是昭示类型io.Reader中的变量能储存任何一个类型中含有Read方法的数值
`signature直译是签名的意思而在计算机中也多用于签名的含义可是在这里感觉签名又有些不合文意但找不到更好的翻译或者可以理解为含有Reader和Writer方法的接口`
`补充:网上有翻译成声明的,或许可以更贴近一点这里的意思,姑且就用声明来表示吧,后同。`
Any type that implements a Read (or Write) method with this signature is said to implement io.Reader (or io.Writer). For the purposes of this discussion, that means that a variable of type io.Reader can hold any value whose type has a Read method:
【个人理解】
---
上面这一段翻出来很拗口好在下面有一个例子可以说明上面一段话的含义首先先声明一个变量rr的类型是io.Reader。这就意味着只要是实现了Read()方法的对象都可以赋值给r。比如这个os.Stdin其代码在GO的代码库`src\os\file.go`文件中在这个文件中我们可以找到一个实现了Read()的方法:
```go
func (f *File) Read(b []byte) (n int, err error) {}
```
因此`r = os.Stdin`就是合法的。
而看bufio.NewReader(r)的代码:
```go
// NewReader returns a new Reader whose buffer has the default size.
func NewReader(rd io.Reader) *Reader {
return NewReaderSize(rd, defaultBufSize)
}
```
这个返回值的`*Reader`同样在bufio的代码之中
```go
// Reader implements buffering for an io.Reader object.
type Reader struct {
buf []byte
rd io.Reader // reader provided by the client
r, w int // buf read and write positions
err error
lastByte int
lastRuneSize int
}
```
看注释也知道这是基于io.Reader对象的。大抵的意思就是张三生了儿子张五李四生了儿子李五他们从本质上来说都是人类。
---
```go
var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on
```
这里非常重要的一点是你需要明白无论具体数值r如何被保存r的类型始终是io.ReaderGo是一种静态类型化的语言而r的静态的类型就是io.Reader。
It's important to be clear that whatever concrete value r may hold, r's type is always io.Reader: Go is statically typed and the static type of r is io.Reader.
一个关于接口类型的非常重要的例子就是空接口:
An extremely important example of an interface type is the empty interface:
```go
interface{}
```
它表示空的方法集合,并且完全可以适用于任意数值,因为任何值都有零值或是多个方法。
It represents the empty set of methods and is satisfied by any value at all, since any value has zero or more methods.
【个人理解】
---
补充一个例子:
```go
var r interface{}
r = "hello"
fmt.Println(r)
```
输出为hello
---
一些人说Go接口是动态类型化的那其实是具有严重误导性的、错误的。Go接口始终是静态类型化的接口类型的变量总是不变的静态类型即使是在整个运行周期存储在接口变量中的数值会改变其类型但是这些数值再如何改变都还是切合于接口的。
【个人理解】
---
此段有些拗口,其本意就是被赋在接口变量中的值的类型永远是接口类型吧
---
Some people say that Go's interfaces are dynamically typed, but that is misleading. They are statically typed: a variable of interface type always has the same static type, and even though at run time the value stored in the interface variable may change type, that value will always satisfy the interface.
我们需要对这些内容非常的清晰,因为反射和接口两者之间的关联相当紧密。
We need to be precise about all this because reflection and interfaces are closely related.
### 接口的陈述
### The representation of an interface
外国的一个叫Russ Cox的程序员写了一篇关于Go语言中的接口数值的博客文章非常详尽。我们并不需要再去完整地在此重复一遍但是还是需要有条理地简单的归纳一下。
Russ Cox has written a detailed blog post about the representation of interface values in Go. It's not necessary to repeat the full story here, but a simplified summary is in order.
```
这篇文章的地址是http://research.swtch.com/2009/12/go-data-structures-interfaces.html
不过已经打不开了。
```
接口类型的一个变量会存储两份数据`就是上面提到的双组数据`:变量的具体数值,和这个数值的类型的具体描述符。更确切一些的讲,这个数值是一个实现了接口的基础性具体数据项,并且这类型是描述了此数据项的全类型。后面举个例子
A variable of interface type stores a pair: the concrete value assigned to the variable, and that value's type descriptor. To be more precise, the value is the underlying concrete data item that implements the interface and the type describes the full type of that item. For instance, after
```go
var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, err
}
r = tty
```
简要性地来说r包含了一份类型形式的双组数据也即`tty,*os.File`。注意类型`*os.File`实现了Read以外的方法即使接口数值只提供了读方法的入口这个数值的内部携带了它的类型的所有信息。这就是为什么我们可以做下面这些事情
r contains, schematically, the (value, type) pair, (tty, *os.File). Notice that the type *os.File implements methods other than Read; even though the interface value provides access only to the Read method, the value inside carries all the type information about that value. That's why we can do things like this:
```go
var w io.Writer
w = r.(io.Writer)
```
这个表达的语句是一个类型断言它断言了变量r中的数据项实现了io.Writer接口所以我们也能将它赋派给w。在赋派之后w也将包含一份双组数据`tty,*os.File)`。这和r所持有的双组数据是相同的。这个接口的静态类型决定接口变量中的哪个方法可以被调用即使这个具体的数值内部有更多的方法集合。
The expression in this assignment is a type assertion; what it asserts is that the item inside r also implements io.Writer, and so we can assign it to w. After the assignment, w will contain the pair (tty, *os.File). That's the same pair as was held in r. The static type of the interface determines what methods may be invoked with an interface variable, even though the concrete value inside may have a larger set of methods.
继续下去,我们能这样做:
Continuing, we can do this:
```go
var empty interface{}
empty = w
```
我们的空接口的数值是空的,将也包含同样的双组数据`(tty, *os.File)`。这样的方便之处在于:一个空接口能持有任何数值,能包含我们所需要的关于这个数值的任何信息。
and our empty interface value empty will again contain that same pair, (tty, *os.File). That's handy: an empty interface can hold any value and contains all the information we could ever need about that value.
这里我们可以不需要类型断言因为w是已知的静态的w完全可以满足空接口的规则。在上上个例子中我们移动a Reader到a Writer,我们需要明确地使用类型断言因为Writer这个方法不是Reader的子集合。
(We don't need a type assertion here because it's known statically that w satisfies the empty interface. In the example where we moved a value from a Reader to a Writer, we needed to be explicit and use a type assertion because Writer's methods are not a subset of Reader's.)
一个重要的细节是接口中的双组数据总是以(值,具体实现类型)这样的形式呈现,而不能以(值,接口类型)的形式呈现。接口不能持有接口数值。
One important detail is that the pair inside an interface always has the form (value, concrete type) and cannot have the form (value, interface type). Interfaces do not hold interface values.
【个人理解】
---
这段不知道是否可以这样理解:
```go
var t interface{}
var i io.Reader
i = os.Stdin
t = "hello"
i = t //这里是错的编译会报错cannot use t (type interface {}) as type io.Reader in assignment:
fmt.Println(t)
fmt.Println(i)
```
---
现在我们可以开始学习反射了。
Now we're ready to reflect.
### 反射的第一定律
### The first law of reflection
#### 1.反射从接口数值到接口对象
#### 1. Reflection goes from interface value to reflection object.
从浅的地方说起反射仅是一种检查类型和接口变量内部数值双组数据形式存诸在接口变量中的机制。开始的时候我们知道在反射包中有两种类型Type和Value。借助于这两个类型我们可以访问接口变量中的内容此外还有两个简单的方法称之为reflect.TypeOf和reflect.ValueOf用于从一个接口数值中分别抽取到reflect.Type和reflect.Value这两个自定义类型的数值。同样从reflect.Value中得到reflect.Type也是相当容易的但是我们现在要区别并保有Value和Type两个不同的概念
```go
//两个自定义类型位于reflect库中
type Type interface {}
type Value struct {}
```
At the basic level, reflection is just a mechanism to examine the type and value pair stored inside an interface variable. To get started, there are two types we need to know about in package reflect: Type and Value. Those two types give access to the contents of an interface variable, and two simple functions, called reflect.TypeOf and reflect.ValueOf, retrieve reflect.Type and reflect.Value pieces out of an interface value. (Also, from the reflect.Value it's easy to get to the reflect.Type, but let's keep the Value and Type concepts separate for now.)
让我们开始写一段关于TypeOf代码的例子
Let's start with TypeOf:
```go
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}
```
这段程序打印出
This program prints
```
type: float64
```
你可能惊异于此处的接口在哪里从这段程序中看传入reflect.TypeOf的是类型为64位浮点数的变量x而并非是接口数值。其实需要的接口确实是在这里;参考go的文档说明reflect.TypeOf的声明中包含着一个空接口
You might be wondering where the interface is here, since the program looks like it's passing the float64 variable x, not an interface value, to reflect.TypeOf. But it's there; as godoc reports[https://golang.org/pkg/reflect/#TypeOf], the signature of reflect.TypeOf includes an empty interface:
```go
// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type
```
【个人理解】
---
表示传入的i是一个接口类型接口类型是万能类型所以传任意值都可以
---
当我们调用reflect.TypeOf(x)方法的时候x首先存储在一个空接口中且后作为参数进行传递reflect.TypeOf解包空接口并恢复其所含的类型信息。
When we call reflect.TypeOf(x), x is first stored in an empty interface, which is then passed as the argument; reflect.TypeOf unpacks that empty interface to recover the type information.
reflect.ValueOf方法当然就是把这个数值给恢复出来这里我们省略了一些不必要的代码部分将专注于执行的关键代码
The reflect.ValueOf function, of course, recovers the value (from here on we'll elide the boilerplate and focus just on the executable code):
```go
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())
```
打印出
prints
```go
value: <float64 Value>
```
我们很明显地调用了这个String方法因为默认情况下fmt包会直接在reflect.Value中调用并显示其中的具体数值字符串方法并非如此。
(We call the String method explicitly because by default the fmt package digs into a reflect.Value to show the concrete value inside. The String method does not.)
【个人理解】
---
不是很理解这段话的意思,不知道是不是表示
```go
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x))
```
如果是这样的话,输入的结果直接就是
value: 3.4
---
reflect.Type和reflect.Value都有一些方法让我们可以检查和操作它们。一个很重要的例子是在Value中有一个Type的方法用来返回reflect.Value中的Type。另一个例子是在Type和Value中都有一个Kind方法用来返回存储于常量中的项是属于何种类型Uint, Float64, Slice, 等等。同时Value中的方法如Int和Float让我们获取被存储于其中的值如int64和float64
`sort在这里也应该是作为“种类”的意思来解释的如同type和kind在网上看到有种解释是排列序列感觉完全解释不通。`
Both reflect.Type and reflect.Value have lots of methods to let us examine and manipulate them. One important example is that Value has a Type method that returns the Type of a reflect.Value. Another is that both Type and Value have a Kind method that returns a constant indicating what sort of item is stored: Uint, Float64, Slice, and so on. Also methods on Value with names like Int and Float let us grab values (as int64 and float64) stored inside:
```go
var x float64 = 3.4
v := reflect.ValueOf(x) //x被反射解包可以得到x的相关属性信息
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
```
打印出
prints
```go
type: float64
kind is float64: true
value: 3.4
```
这里也有如SetInt和SetFloat这样的Set方法但是我们在使用他们之前需要明白`可设置性`的含义,这是反射的第三定律中的内容,将在后面讨论。
There are also methods like SetInt and SetFloat but to use them we need to understand settability, the subject of the third law of reflection, discussed below.
在反射库里有一组特性值得重点对待。首先为保持API的简洁性在“getter”和“setter”两个操作方法中都是以最大的类型定义去持有或是操作数值的如int64会应用在所有的有符号整型int8,int32中。所以Value中的Int方法一律返回int64类型并且在SetInt进行赋值时也采用int64类型进行操作这在转换成实际类型时是非常有必要的。
The reflection library has a couple of properties worth singling out. First, to keep the API simple, the "getter" and "setter" methods of Value operate on the largest type that can hold the value: int64 for all the signed integers, for instance. That is, the Int method of Value returns an int64 and the SetInt value takes an int64; it may be necessary to convert to the actual type involved:
```go
var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type()) // uint8.
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
x = uint8(v.Uint()) // v.Uint returns a uint64.
```
第二个特性是反射对象的Kind是用来描述基础类型的而不是静态类型。如果反射对象包含一个用户定义的整形类型的值
The second property is that the Kind of a reflection object describes the underlying type, not the static type. If a reflection object contains a value of a user-defined integer type, as in
```go
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
```
这个变量v的Kind仍旧是reflect.Int尽管x的静态类型是MyInt而不是int。换种说法这Kind不能区分MyInt和int哪怕Type是可以区分的。
the Kind of v is still reflect.Int, even though the static type of x is MyInt, not int. In other words, the Kind cannot discriminate an int from a MyInt even though the Type can.
### 反射的第二定律
### The second law of reflection
#### 2. 反射的进行是从反射对象到值的
#### 2. Reflection goes from reflection object to interface value.
如同物理反射一样Go的反射是可逆的。
Like physical reflection, reflection in Go generates its own inverse.
通过reflect.Value我们可以使用接口方法来恢复接口的数值;事实上此方法将类型和数值的信息打包成一个以接口形式展呈的包数据,并作为结果返回:
Given a reflect.Value we can recover an interface value using the Interface method; in effect the method packs the type and value information back into an interface representation and returns the result:
```go
// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}
```
因此我们可以说
As a consequence we can say
```go
y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)
```
用来打印出通过反射对象呈现出的float64的值。
to print the float64 value represented by the reflection object v.
【个人理解】
---
上面那个例子不太好。
```go
var x float64 = 3.4
v:= reflect.ValueOf(x)
y := v.Interface().(float64) // y will have type float64.
fmt.Println(v.Kind())
fmt.Println(y)
```
这样就可以更好的理解了,`fmt.Println(y)`的时候这个y已经不再具体.kind()方法了。
---
其实我们还可以更进一步传入到fmt.Println,fmt.Printf等的参数都可以视作为空接口类型数值之后它们会在fmt包的内部被解开正如我们在上一个例子中做的一样。它可以正确地打印reflect.Value的内容并做为接口方法的结果返回给格式化打印程序。
`routine在某些特定词组中有程序的意思不知道这里是不是否则不太好翻译了。`
We can do even better, though. The arguments to fmt.Println, fmt.Printf and so on are all passed as empty interface values, which are then unpacked by the fmt package internally just as we have been doing in the previous examples. Therefore all it takes to print the contents of a reflect.Value correctly is to pass the result of the Interface method to the formatted print routine:
```go
fmt.Println(v.Interface())
```
(为什么不能写成fmt.Println(v)因为v是一个reflect.Value;我们想持存的则是具体数值。)也因此我们的值是一个float64的类型我们甚至能使用浮点输出格式如果我们想的话
(Why not fmt.Println(v)? Because v is a reflect.Value; we want the concrete value it holds.) Since our value is a float64, we can even use a floating-point format if we want:
```go
fmt.Printf("value is %7.1e\n", v.Interface())
```
会得到下面的情形
and get in this case
```
3.4e+00
```
再次说明这里不需要做类型断言来判断v.Interface()的结果为float64空接口数值内有具体数值的类型信息Printf将会恢复它。
Again, there's no need to type-assert the result of v.Interface() to float64; the empty interface value has the concrete value's type information inside and Printf will recover it.
简而言之这接口方法是函数ValueOf的反逆除了ValueOf的结果永远是静态类型interface{}以外
In short, the Interface method is the inverse of the ValueOf function, except that its result is always of static type interface{}.
重申一次:反射是从接口数值到反射对象,然后再次回到接口数值中。
Reiterating: Reflection goes from interface values to reflection objects and back again.
### 反射的第三定律
### The third law of reflection
#### 3. 如要修改反射对象,他的数值必须可设置状态。
#### 3. To modify a reflection object, the value must be settable.
这第三定律非常微妙和混乱,但是如果我们从它的基本原理开始探研的话,它仍然可以很好地被理解
The third law is the most subtle and confusing, but it's easy enough to understand if we start from first principles.
这儿有一些无法正常运行的代码,但是值得研究一下。
Here is some code that does not work, but is worth studying.
```go
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.
```
如果你运行这段代码,它将抛出一个带有隐晦的异常消息的错误
If you run this code, it will panic with the cryptic message
```go
panic: reflect.Value.SetFloat using unaddressable value
```
这个问题在于7.1这个数值并没有可设定的地址也就是说目前的v是不可设置的。Settability`可设置性`是反射数值的一个特性,但并非所有的反射数值都具有这个特性。
The problem is not that the value 7.1 is not addressable; it's that v is not settable. Settability is a property of a reflection Value, and not all reflection Values have it.
CanSet这个操作数值的方法用于表示值是否可设置在我们的例子中
The CanSet method of Value reports the settability of a Value; in our case,
```go
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
```
打印
prints
```go
settability of v: false
```
在一个非可设置的数值中调用Set方法是错误的。但是……说了半天到底什么是settability呢
It is an error to call a Set method on an non-settable Value. But what is settability?
settability有些像addressability`可寻址能力,寻址率`但是更加严格。这是一种特性一种反射对象能修改创建这个反射对象的实际的存储内容的特性。Settability是由反射对象是否持存原始项来决定的。当我们说
Settability is a bit like addressability, but stricter. It's the property that a reflection object can modify the actual storage that was used to create the reflection object. Settability is determined by whether the reflection object holds the original item. When we say
```go
var x float64 = 3.4
v := reflect.ValueOf(x)
```
我们传入x的拷贝到reflect.ValueOf中也就是x的一份拷贝传入到reflect.ValueOf中而创建了接口数值而不是x自身。于是假如这个陈述
we pass a copy of x to reflect.ValueOf, so the interface value created as the argument to reflect.ValueOf is a copy of x, not x itself. Thus, if the statement
```go
v.SetFloat(7.1)
```
是被允许并且是成功执行的它并非更新变量x即使v看上去像是从变量x那里被创建的它将更新存储在反射数值内的x的拷贝而x自身不会受到任何影响。这将非常的混乱和无用因此他是不合法的settability则正是为了避免这种问题而产生的一种特性。
were allowed to succeed, it would not update x, even though v looks like it was created from x. Instead, it would update the copy of x stored inside the reflection value and x itself would be unaffected. That would be confusing and useless, so it is illegal, and settability is the property used to avoid this issue.
虽然这看上去非常奇怪实际并非这样。这只是包了一层使人感到迷惑的外衣而已事实上其本质还是我们所熟悉的一种程序手段。想一下在普通的函数中传入参数x
If this seems bizarre, it's not. It's actually a familiar situation in unusual garb. Think of passing x to a function:
```go
f(x)
```
我们将不期望函数f能修改x因为我们传入的是x的值的一个拷贝而不是x自己。如果我们想让f直接修改x我们必须把x的地址传入到我们的函数中去。其实也就是传入x的指针
We would not expect f to be able to modify x because we passed a copy of x's value, not x itself. If we want f to modify x directly we must pass our function the address of x (that is, a pointer to x):
```go
f(&x)
```
这种原理是多么地直截了当和令人熟悉反射亦是同样的工作原理。如果我们想通过反射去修改x我们必须传给反射库我们想修改的值的指针。
This is straightforward and familiar, and reflection works the same way. If we want to modify x by reflection, we must give the reflection library a pointer to the value we want to modify.
让我们按部就班吧。首先我们如平常一样始初化x并创建一个反射值指向它称之为p。
Let's do that. First we initialize x as usual and then create a reflection value that points to it, called p.
```go
var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())
```
输出则是
The output so far is
```go
type of p: *float64
settability of p: false
```
反射对象p是不可设置的但是我们想设置的并不是p而是p的指针。我们调用值中的<b>Elem方法</b>来得到p所指向的地址它直接通过指针并在称为v的反射数值中保留结果
The reflection object p isn't settable, but it's not p we want to set, it's (in effect) *p. To get to what p points to, we call the Elem method of Value, which indirects through the pointer, and save the result in a reflection Value called v:
```go
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
```
现在v是可设置的反射对象了作为输出论证
Now v is a settable reflection object, as the output demonstrates,
```go
settability of v: true
```
既然它代表了x我们最终能使用v.SetFloat来修改x的值
and since it represents x, we are finally able to use v.SetFloat to modify the value of x:
```go
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
```
输出,如所期望的
The output, as expected, is
```go
7.1
7.1
```
反射不容易被理解但是它所做的事情切实是程序语言所能做的事情虽然通过反射的Types和Values能掩饰所发生的过程不过只要记住反射的Values
需要它的地址,这样才可去修改它所显示出来的内容。
Reflection can be hard to understand but it's doing exactly what the language does, albeit through reflection Types and Values that can disguise what's going on. Just keep in mind that reflection Values need the address of something in order to modify what they represent.
结构体
Structs
在我们上一个例子中v并不是指针本身它只是从指针中衍生出来的。通常我们在使用反射去修改结构体中的字段时会出现这种情况。只要我们有结构体的地址我们能修改它里面的字段。
In our previous example v wasn't a pointer itself, it was just derived from one. A common way for this situation to arise is when using reflection to modify the fields of a structure. As long as we have the address of the structure, we can modify its fields.
这里有一个简单的例子来解析一下结构体数值t。我们创建了这个结构体地址的反射对象因为我们想稍候去修改它。然后我们设置typeOfT为它`即t`的类型并用最直接的方法调用来迭代字段详细可见反射包。注意我们从结构的类型中提取字段的名称但是字段本身是规则的reflect.Value对象。
Here's a simple example that analyzes a struct value, t. We create the reflection object with the address of the struct because we'll want to modify it later. Then we set typeOfT to its type and iterate over the fields using straightforward method calls (see package reflect for details). Note that we extract the names of the fields from the struct type, but the fields themselves are regular reflect.Value objects.
```go
type T struct {
A int
B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem() //s其实就代表着t
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {//迭代s这个结构体中的每个字段
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}
```
这段代码输出
The output of this program is
```go
0: A int = 23
1: B string = skidoo
```
关于settability这里还有好几个要介绍的点T的字段名是大写的可被导出因为只有可导出的字段才可设置。
There's one more point about settability introduced in passing here: the field names of T are upper case (exported) because only exported fields of a struct are settable.
【个人理解】
---
其实很好理解,把上面的
```go
type T struct {
A int
B string
}
```
改成
```go
type T struct {
a int
b string
}
```
直接报错:`panic: reflect.Value.Interface: cannot return value obtained from unexported field or method`
---
因为s包含可设置的反射对象我们能修改结构体中的字段。
Because s contains a settable reflection object, we can modify the fields of the structure.
```go
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)
```
这里是结果:
And here's the result:
```go
t is now {77 Sunset Strip}
```
如果我们修改程序使s是通过t被创建的而不是&t然后调用SetInt和SetString将会报错t的字段无法被设置。
If we modified the program so that s was created from t, not &t, the calls to SetInt and SetString would fail as the fields of t would not be settable.
结论
Conclusion
这里再次强调反射的定律:
Here again are the laws of reflection:
反射从接口数值到接口对象
Reflection goes from interface value to reflection object.
反射从反射对象到接口数值
Reflection goes from reflection object to interface value.
若修改反射对象,其值一定要是可设置的。
To modify a reflection object, the value must be settable.
一旦你理解了Go语言中的反射定律Go语言将变得很容易使用虽然它保持着一些微妙之处。这是非常强力的工具它应该被小心使用并且不到必要时刻仍然要避开对它的使用。
Once you understand these laws reflection in Go becomes much easier to use, although it remains subtle. It's a powerful tool that should be used with care and avoided unless strictly necessary.
写到这里我们其实还有大量的反射的内容未提及channels的发送和接收内存分配使用slices和maps调用方法和函数——但是这篇文章已经足够长了。我们将在以后的文章再研究这些内容。
There's plenty more to reflection that we haven't covered — sending and receiving on channels, allocating memory, using slices and maps, calling methods and functions — but this post is long enough. We'll cover some of those topics in a later article.
作者 Rob Pike
By Rob Pike
后记:
反射的内容我已经都看过一遍,并且记下笔记,在这里分享一下,如果有兴趣可以看一下,内容比较多:  
https://github.com/gundamzaku/golang_study_note/tree/master/%E5%8F%8D%E5%B0%84reflect

View File

@@ -0,0 +1,92 @@
# 概述
sort包实现了对列表的排序以及在有序列表上的二分查找等操作
## 通用排序函数
### 接口实现
要使用sort包的各个函数需要实现sort.Interface定义如下
```
type Interface interface {
Len() int // 返回当前元素个数
Less(i, j int) bool. // 判断第i个元素是小于第j个元素
Swap(i, j int) // 交换两个元素
}
```
### Sort
sort包最核心的函数Sort用于对一个列表上的元素进行排序Sort函数会在原有列表上进行排序函数声明如下
```
func Sort(data Interface)
```
### Stable
相较于Sort函数Stable函数也用于对一个列表进行排序但是它额外提供保证排序算法是稳定的也就是排序前后值相同的两个元素相对位置不发生变化函数声明和Sort类似。
```
func Stable(data Interface)
```
### Slice
Slice函数用于对一个Slice进行排序这是实际使用中更为常用的一个函数函数接收两个参数。第一个是需要排序的Slice第二个是Slice元素比较函数它类似于前面sort.Interface里的Less方法。函数声明如下
```
func Slice(slice interface{}, less func(i, j int) bool)
```
### Reverse
Reverse函数用于翻转一个列表并返回翻转后的列表函数声明如下
```
func Reverse(data Interface) Interface
```
### IsSorted
IsSorted函数用于判断一个列表是否有序函数声明如下
```
func IsSorted(data Interface) bool
```
### Search
Search函数可以在一个有序列表上进行二分查找操作它接收两个参数第一个为从第一个元素开始搜索的元素个数第二个参数是一个函数通过接收一个函数f作为参数找到使得f(x)==true的元素函数声明如下
```
func Search(n int, f func(int) bool) int
```
## 特定类型方法
除了上面的通用函数之外sort包还对几个常用基础类型intfloatstring和slice的排序提供了支持对于每个类型分别实现了上一节的各个函数。具体函数定义见 [Package sort](https://golang.org/pkg/sort/)
# 使用示例
下面以Slice排序为例进行说明示例中声明了一个类型Person根据Person.Age字段对数据进行排序。
```
package main
import (
"sort"
"fmt"
)
type Person struct {
Name string
Age int
}
func main() {
data := []Person{
{"Alice", 20},
{"Bob", 15},
{"Jane", 30},
}
sort.Slice(data, func(i, j int) bool {
return data[i].Age < data[j].Age
})
for _, each := range data {
fmt.Println("Name:", each.Name, "Age:", each.Age)
}
}
```

View File

@@ -0,0 +1,45 @@
# 概述
strconv包中包含了一系列辅助函数用于字符串类型变量和其他类型变量之间的转换。
# Atoi & Itoa
其中最常用的就是字符串和整型变量的相互转换。Atoistring to int,Itoaint to string分别是字符串转整型和整型转字符串注意这个两个函数中的整型变量都是十进制整数。函数声明如下
```
func Atoi(s string) (int, error)
// 当字符串格式错误时会返回strcov.NumError
func Itoa(i int) string
```
# ParseX
当需要将字符串转换为其他类型变量时就需要使用到strconv中的ParseX系列函数一共有四个函数`ParseBoolParseIntParseFloatParseUint`,下面是详细的函数说明:
- `func ParseBool(str string) (bool, error)`
函数接收一个字符串作为参数返回转换后的布尔值如输入格式错误返回NumError它接受真值`1, t, T, TRUE, true`, True假值`0, f, F, FALSE, false, False`
- `func ParseInt(s string, base int, bitSize int) (i int64, err error)`
函数接收三个参数第一个是需要转换的字符串第二个是转换后整型变量的底数02-36一般取值是02816第三个是整型变量的大小0-64)一般来说0、8、16、32 和 64 分别代表 int、int8、int16、int32 和 int64如果实际数据超出了bitSize会产生数据溢出和截断。函数返回值和ParseBool类似返回转换后的整型变量如输入格式错误返回NumError。
实际上Atoi等价于`ParseInt(s, 10, 0)`
- `func ParseUint(s string, base int, bitSize int) (n uint64, err error)`
ParseUint和ParseInt类似针对无符号整型。
- `func ParseFloat(s string, bitSize int) (float64, error)`
函数接收两个参数第一个是输入字符串第二个是转换后浮点数的二进制长度两个典型值是32和64无论bitSize取值如何函数返回值类型都是float64。
# FormatX
与ParseX相对于可以使用FormatX系列函数可以将其他类型变量转换为字符串类型。与上面相对应也有四个函数
- `func FormatBool(b bool) string`
- `func FormatInt(i int64, base int) string`
- `func FormatUint(i uint64, base int) string`
前面三个函数可以和ParseX函数相对应这里不再具体阐述与ParseX函数不同从其他类型转换为字符串总会成功而不会返回错误。
- `func FormatFloat(f float64, fmt byte, prec, bitSize int) string`
FormatFloat函数接收四个参数第一个参数是输入浮点数第二个是浮点数的显示格式可以是`b, e, E, f, g, G`第三个是浮点数精度对于不同的fmt参数具有不同的含义最后一个表示浮点数的大小。对于FormatFloat参数的详细介绍见 [GoDoc FormatFloat](https://golang.org/pkg/strconv/#FormatFloat)

View File

@@ -0,0 +1,131 @@
# 概述
字符串是一个十分常用的基础类型strings包提供了很多函数对string类型变量的操作。这些函数的调用方式大多类似通过传入一个字符串为参数在字符串上进行相应的处理。这些函数主要可以分为下面几类
- 字符串搜索和匹配
- 字符串拆分
- 字符串修改
- 其他独立的函数
# 字符串搜索与匹配
strings.Contains可以检测字符串是否包含某个子串strings.ContainsRune可以检测字符串是否包含某个字符strings.ContainsAny可以检测字符串是否包含字符集中的某个字符。详细函数声明如下
```
func Contains(s, substr string) bool
func ContainsRune(s string, r rune) bool
func ContainsAny(s, chars string) bool
```
除了简单的判断字符串包含使用strings.Index可以在字符串中搜索某个子串并得到对应子串起始索引下标若不存在对应子串则返回-1。函数声明如下
```
func Index(s, substr string) int
```
除了对子串进行搜索之外,也可以对某个字节,字符,字符集合进行搜索。具体声明如下:
```
func IndexByte(s string, c byte) int // 字节搜索
func IndexRune(s string, r rune) int。 // 字符搜索
func IndexAny(s, chars string) int。 // 字符集合搜索匹配chars中的任何一个字符
```
上述的所有搜索操作都返回第一个匹配的索引除此之外strings包也提供了一系列函数获取对应元素的最后一个匹配项的索引下标。对应于每个Index函数都有一个LastIndex函数。例如Index返回第一个匹配的子串的起始索引LastIndex返回最后一个匹配子串的起始索引。详细函数声明如下
```
func LastIndex(s, substr string) int
func LastIndexByte(s string, c byte) int
func IndexAny(s, chars string) int。
```
# 字符串拆分
字符串拆分是字符串的常见操作。strings支持两类拆分操作Split和SplitAfter。两者的区别在于Split拆分后的结果中不包含分隔符而SplitAfter包含分隔符。例如`strings.Split("a,b,c", ",")`结果为`["a", "b", "c"]`;`strings.SplitAfter("a,b,c", ",")`结果为`["a,", "b,", "c"]`。函数声明如下:
```
func Split(s, sep string) []string
func SplitAfter(s, sep string) []string
```
前面的Split和SplitAfter都只能指定一个分隔符那么如果希望指定一类分隔符应该怎么做呢。在strings模块中提供了FieldsFunc函数通过传递一个函数来确定一个字符是否为分隔符。下面来看一个例子所有非数字和非字母的字符都被认为是分隔符而被跳过。
```
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
str := " hello&$ world"
f := func(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}
fmt.Printf("%q", strings.FieldsFunc(str, f))
}
```
函数声明如下:
```
func FieldsFunc(s string, f func(rune) bool) []string
```
实际使用中也可以使用Fields函数对字符串中空格进行删除相当于FieldsFunc(s, unicode.IsSpace)
```
func Fields(s string) []string
```
# 字符串修改
Trim系列函数可以删除字符串首尾的连续多余字符包括
- Trim删除字符串首尾的多余字符
- TrimLeft删除字符串首的多余字符
- TrimRight删除字符串尾部的多余字符
- TrimSpace删除字符串首尾的空格
函数声明如下:
```
func Trim(s string, cutset string) string
func TrimLeft(s string, cutset string) string
func TrimRight(s string, cutset string) string
func TrimSpace(s string) string
```
除此之外,还可以通过传递函数的方式对删除字符进行更精确的选择,这里不再展开,具体见[TrimFunc](https://golang.org/pkg/strings/#TrimFunc)。
除了对字符进行删除之外strings包也可以字符串进行格式化通过一系列函数提供了支持其中最为常用的是`ToLower``ToUpper`分别用于将字符串转化为小写和大写字母,函数声明如下:
```
func ToLower(s string) string
func ToUpper(s string) string
```
# 其他函数
除了上述几类函数之外strings包还提供了下面几个实用函数
- Join将多个字符串组装成一个字符串子串间通过分隔符连接split的逆操作
- Compare通过字典序比较两个字符串的大小等价于>,<,==运算;
- Count统计字符串中指定子串的数量
- Replace替换字符串中的对应子串
函数声明如下:
```
func Join(a []string, sep string) string
func Compare(a, b string) int
func Count(s, substr string) int
```

View File

@@ -0,0 +1,72 @@
# 概述
为了保证并发安全,除了使用临界区之外,还可以使用原子操作。顾名思义这类操作满足原子性,其执行过程不能被中断,这也就保证了同一时刻一个线程的执行不会被其他线程中断,也保证了多线程下数据操作的一致性。
在atomic包中对几种基础类型提供了原子操作包括int32int64uint32uint64uintptrunsafe.Pointer。对于每一种类型提供了五类原子操作分别是
- Add, 增加和减少
- CompareAndSwap, 比较并交换
- Swap, 交换
- Load , 读取
- Store, 存储
具体函数名由原子操作名和类型关键字组成例如对于int32的Add操作函数名为AddInt32其他函数名以此类推后文中仅以int32类型系列函数为例进行说明其他类型函数功能类似。
# AddInt32
AddInt32可以实现对元素的原子增加或减少其函数定义如下函数接收两个参数分别是需要修改的变量的地址和修改的差值函数会直接在传递的地址上进行修改操作此外函数会返回修改之后的新值
```
func AddInt32(addr *int32, delta int32) (new int32)
```
传递一个正整数增加值负整数减少值函数的通常使用方法如下。需要注意的是当你处理unint32和unint64时由于delta参数类型被限定不能直接传输负数所以需要利用二进制补码机制其中N为需要减少的正整数值。
```
package main
import (
"sync/atomic"
"fmt"
)
func main() {
var a int32
a += 10
atomic.AddInt32(&a, 10)
fmt.Println(a == 20) // true
var b uint32
b += 20
atomic.AddUint32(&b, ^uint32(10-1))
// 等价于 b -= 10
// atomic.Adduint32(&b, ^uint32(N-1))
fmt.Println(b == 10) // true
}
```
# CompareAndSwapInt32
CompareAndSwapInt32函数接收三个参数一个是需要交换赋值的变量地址然后是一个待比较的值旧值最后是需要交换的值新值函数返回一个布尔值表示交换结果。函数声明如下
```
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
```
比较并交换Compare and SwapCAS是一个常用的原子操作首先判断当前变量的值和旧值是否相等也就是变量值是否被其他线程所修改如果相等则用新值替换掉原来的值否则就不进行替换操作。
# SwapInt32
类似于CASSwap也是将一个新值赋给某变量区别在于CAS会比较当前变量与旧值来决定赋值与否Swap会直接执行赋值操作并将原值作为返回值返回其函数声明如下
```
func SwapInt32(addr *int32, new int32) (old int32)
```
# LoadInt32 & StoreInt32
Load和Store操作对应与变量的原子性读写许多变量的读写无法在一个时钟周期内完成而此时执行可能会被调度到其他线程无法保证并发安全。函数的类型声明如下
```
func LoadInt32(addr *int32) (val int32)
func StoreInt32(addr *int32, val int32)
```
Load函数参数为需要读取的变量地址返回值为读取的值Store函数参数为需要存储的变量地址以及需要写入的值不同于CASStore操作不关心变量的原始值是否被修改只是简单的执行写入所以Store函数总能成功返回。

View File

@@ -0,0 +1,157 @@
# 概述
sync包对并发和同步机制进行了实现但显然并发编程这样一个话题过于庞大无法在一篇博客里面详细展开所以本文的重点放在sync包的使用。
不过这里首先对并发的背景进行简单的介绍在单线程的程序中同一个时刻只存在一个线程对数据进行访问访问永远是线性的不需要额外的机制保障但是当同时存在多个线程可能同时访问一个数据时由于线程调度的特性会带来难以预料的结果。试想如下代码会输出什么结果正常情况会是3但实际上可能的结果是2或者3这是由于函数或者代码的运行没有原子性比如在线程1执行Add1操作时被暂停了已经读取data还未写回然后开始运行线程2并读取数据然后两个线程依次执行到结束由于两个线程读取的值都是data=1执行Add1后都得到2而非3。
```
import (
"fmt"
"time"
)
func Add1(data *int) {
tmp = *data
*data = tmp + 1
}
func main(){
data = 1
go Add1(&data)
go Add1(&data)
time.Sleep(10)
fmt.Print(data)
}
```
并发编程的本质就是在乱序执行的代码中创建小块的临界区,在临界区中程序线性执行,保证代码的执行结果符合预期。
# 互斥锁
sync.Mutex是互斥锁的实现这是同步中最经典的模型Mutex有两个方法Lock和Unlock分别用于锁定和解锁一个锁每个互斥锁只能被Lock一次在一个已锁定的锁上执行Lock操作会阻塞直到这个锁被Unlock。Mutex的声明如下
```
type Mutex struct {
// contains filtered or unexported fields
}
func (m *Mutex) Lock()
func (m *Mutex) Unlock()
```
对于前面那个程序可以加入锁保证并发安全同时推荐使用defer保证解锁
```
import (
"fmt"
"time"
"sync"
)
func Add1(data *int, mu &sync.Mutex) {
mu.Lock()
defer mu.Unlock()
tmp = *data
*data = tmp + 1
}
func main(){
mu := sync.Mutex()
data = 1
go Add1(&data, &mu)
go Add1(&data, &mu)
time.Sleep(10)
fmt.Print(data)
}
```
## 读写锁
实际上单纯对数据的并发读取是不会带来数据不一致的而这种情况下使用互斥锁会带来额外的等待开销所以除了基础的互斥锁之外sync包还提供了RWMutex与Mutex不同在于Mutex只能同时被一个线程锁定而RWMutex可以多次读锁定也就是可以进行并发读取具体见[sync.RWMutex](https://golang.org/pkg/sync/#RWMutex)
# 信号量
sync.Cond是信号量的实现这也是一个经典的同步模型从功能的角度来看你只能等待一个信号量或者向信号量发送一个信号经典的应用场景就是生产者消费者模型当生产者结束一个生产则发起一个信号通知消费者而消费者只需要等待这个信号。
## NewCond
Cond对象通过NewCond初始化并且绑定到一个Locker。
```
type Cond struct {
L Locker
}
func NewCond(l Locker) *Cond
```
## Signal&Broadcast
Signal和Broadcast用于唤醒一个信号量区别在于Signal只会随机的唤醒一个线程而Broadcast会唤醒所有在等待的线程。
```
func (c *Cond) Signal()
func (c *Cond) Broadcast()
```
## Wait
阻塞等待当Signal或Broadcast被调用时唤醒声明如下
```
func (c *Cond) Wait()
```
# WaitGroup
WaitGroup的功能和信号量类似不过信号量只等待单个信号的到来WaitGroup等待一组任务的结束。
## Add
在一个WaitGroup上增加某个值方法声明如下
```
func (wg *WaitGroup) Add(delta int)
```
## Done
每次调用Done方法会使WaitGroup的值减少 1
```
func (wg *WaitGroup) Done()
```
## Wait
阻塞等待当WaitGroup的值为 0 时唤醒
```
func (wg *WaitGroup) Wait()
```
## 示例
下面用一个简单的例子对WaitGroup的使用进行说明创建了一个WaitGroup并将其值增加3然后并发的执行handle函数最后调用Wait等待执行结束再退出主线程。
```
package main
import (
"sync"
"fmt"
)
func handle(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("Done Once")
}
func main() {
wg := sync.WaitGroup{}
wg.Add(3)
for i := 0; i < 3; i++ {
go handle(&wg)
}
wg.Wait()
}
```
# Once
sync.Once保证某个函数有且仅有一次执行只有一个方法Do接受一个无参函数作为参数当你调用Do时会执行这个函数其方法声明如下
```
func (o *Once) Do(f func())
```
下面的代码是对Once的简单使用由于Once的使用只会打印出一次“Do Once”而不是10次
```
for i := 0; i < 10; i++ {
sync.Once.Do(func () { fmt.Println("Do Once") }
}
```

View File

@@ -0,0 +1,249 @@
# 14 time
基于 原文[《Go语言基础之time包》](https://www.liwenzhou.com/posts/Go/go_time/) 和视频内容整理。
time 包提供了时间的显示和测量用的函数。日历的计算采用的是公历。
## 14.1 时间类型
`time.Time` 类型表示时间。我们可以通过 `time.Now()` 函数获取当前的时间对象,然后获取时间对象的年月日时分秒等信息。示例代码如下:
```go
func timeDemo() {
//获取当前时区的当前时间
now := time.Now()
fmt.Printf("current time:%v\n", now)
year := now.Year() //年
month := now.Month() //月
day := now.Day() //日
hour := now.Hour() //小时
minute := now.Minute() //分钟
second := now.Second() //秒
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
}
```
## 14.2 时间戳
时间戳是自1970年1月1日08:00:00GMT至当前时间的总**毫秒数**。它也被称为 Unix 时间戳UnixTimestamp
* 基于时间对象获取时间戳的示例代码如下:
```go
func timestampDemo() {
now := time.Now() //获取当前时间
timestamp1 := now.Unix() //时间戳
timestamp2 := now.UnixNano() //纳秒时间戳
fmt.Printf("current timestamp1:%v\n", timestamp1)
fmt.Printf("current timestamp2:%v\n", timestamp2)
}
func timeStamp2() {
fmt.Printf("时间戳(秒):%v;\n", time.Now().Unix())
fmt.Printf("时间戳(纳秒):%v;\n",time.Now().UnixNano())
fmt.Printf("时间戳(毫秒):%v;\n",time.Now().UnixNano() / 1e6)
fmt.Printf("时间戳(纳秒转换为秒):%v;\n",time.Now().UnixNano() / 1e9)
}
```
* 使用 `time.Unix()`函数可以将时间戳转为时间格式。
```go
func timestampDemo2(timestamp int64) {
timeObj := time.Unix(timestamp, 0) //将时间戳转为时间格式
fmt.Println(timeObj)
year := timeObj.Year() //年
month := timeObj.Month() //月
day := timeObj.Day() //日
hour := timeObj.Hour() //小时
minute := timeObj.Minute() //分钟
second := timeObj.Second() //秒
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
}
```
## 14.3 时间间隔
`time.Duration` 是 time 包定义的一个类型,它代表两个时间点之间经过的时间,以**纳秒**为单位。
`time.Duration` 表示一段时间间隔可表示的最长时间段大约290年。
time 包中定义的时间间隔类型的常量如下:
```go
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
```
例如:`time.Duration` 表示1纳秒`time.Second` 表示1秒。
## 14.4 时间操作
### 1.4.1 Add
我们在日常的编码过程中可能会遇到要求 `时间+时间间隔` 的需求Go语言的时间对象有提供 Add 方法如下:
```go
func (t Time) Add(d Duration) Time
```
举个例子,求一个小时之后的时间:
```go
func main() {
now := time.Now()
later := now.Add(24 * time.Hour) // 当前时间加24小时后的时间
fmt.Println(later)
}
```
### 14.4.2 Sub
求两个时间之间的差值:
```go
func (t Time) Sub(u Time) Duration
```
返回一个**时间段** `t-u` 。如果结果超出了 Duration 可以表示的最大值/最小值,将返回最大值/最小值。
要获取**时间点** `t-d`d为Duration可以使用 `t.Add(-d)`
### 14.4.3 Equal
```go
func (t Time) Equal(u Time) bool
```
判断两个时间是否相同,会考虑时区的影响,因此不同时区标准的时间也可以正确比较。
本方法和用 `t==u` 不同,这种方法还会比较地点和时区信息。
### 14.4.4 Before
```go
func (t Time) Before(u Time) bool
```
如果 t 代表的时间点在 u 之前,返回真;否则返回假。
### 14.4.5 After
```go
func (t Time) After(u Time) bool
```
如果 t 代表的时间点在 u 之后,返回真;否则返回假。
## 14.5 定时器
使用 `time.Tick(时间间隔)` 来设置定时器,**定时器本质上是一个通道channel**。
```go
func tickDemo() {
ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器
for i := range ticker {
//因为上面的时间间隔定义为 time.Second 所以每秒都会执行的任务
fmt.Println(i)
}
}
```
运行之后,将输出如下格式的内容:
```go
2020-09-15 08:22:09.906515 +0800 CST m=+1.001907219
2020-09-15 08:22:10.905672 +0800 CST m=+2.001090754
2020-09-15 08:22:11.905355 +0800 CST m=+3.000801291
2020-09-15 08:22:12.904676 +0800 CST m=+4.000149566
2020-09-15 08:22:13.905339 +0800 CST m=+5.000839685
```
## 14.6 时间格式化
时间类型有一个自带的方法 `Format` 进行格式化,需要注意的是 Go 语言中格式化时间模板不是常见的 `Y-m-d H:M:S` 而是使用 Go 的诞生时间 `2006年1月2号15点04分`记忆口诀为2006 1 2 3 4作为格式化模板。
### 14.6.1 时间格式化
如果想格式化为12小时方式需指定PM。
```go
func formatDemo() {
now := time.Now()
// 格式化的模板为Go的出生时间2006年1月2号15点04分 Mon Jan
// 24小时制秒数后面的 .000 表示毫秒数
fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan"))
// 12小时制需要指定 PM
fmt.Println(now.Format("2006-01-02 03:04:05.000 PM Mon Jan"))
fmt.Println(now.Format("2006/01/02 15:04"))
fmt.Println(now.Format("15:04 2006/01/02"))
fmt.Println(now.Format("2006/01/02"))
}
```
### 14.6.2 解析字符串格式的时间
```go
// 获取当前时区的当前时间
now := time.Now()
fmt.Println(now)
// 因为我们在东八区,所以需要加载 Shanghai 时区
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
fmt.Println(err)
return
}
// 按照指定时区和指定格式解析字符串时间. 传入的时间串格式必须和要解析成的模板格式一致
timeObj, err := time.ParseInLocation("2006/01/02 15:04:05", "2019/08/04 14:15:20", loc)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(timeObj)
fmt.Println(timeObj.Sub(now))
```
## 14.7 sleep
```go
n := 100
// n 是 int 类型Sleep 接收 Duration 类型, 所以需要强转。——Duration 本质是 int64 的别名
time.Sleep(time.Duration(n))
// 直接传入一个数值会自动转换为 Duration 类型
time.Sleep(100)
```
## 14.8 其他
### 14.8.1 获取月份的第一天和最后一天
```go
func getDate() {
now := time.Now()
currentYear, currentMonth, _ := now.Date()
currentLocation := now.Location()
// 获取某月的第一天和最后一天
firstOfMonth := time.Date(currentYear, currentMonth, 1, 0, 0, 0, 0, currentLocation)
lastOfMonth := firstOfMonth.AddDate(0, 1, -1)
// 1609430400
fmt.Println(firstOfMonth.Unix())
// 1612022400
fmt.Println(lastOfMonth.Unix())
}
```
## 练习题:
* 获取当前时间,格式化输出为 2017/06/19 20:30:05`格式。
* 编写程序统计一段代码的执行耗时时间,单位精确到微秒

View File

@@ -0,0 +1,220 @@
* [原文链接如何使用go module导入本地包](https://www.liwenzhou.com/posts/Go/import_local_package_in_go_module/)
* [视频链接](https://www.bilibili.com/video/BV17Q4y1P7n9?p=146)
`go module` 是 Go1.11 版本之后官方推出的版本管理工具,并且从 Go1.13 版本开始,`go module` 将是 Go 语言默认的**依赖管理工具**。到今天 Go1.14 版本推出之后 Go modules 功能已经被正式推荐在生产环境下使用了。
这几天已经有很多教程讲解如何使用 `go module`,以及如何使用 `go module` 导入 gitlab 私有仓库,我这里就不再啰嗦了。但是最近我发现很多小伙伴在群里问如何使用 `go module` 导入本地包,作为初学者大家刚开始接触 package 的时候肯定都是先在本地创建一个包,然后本地调用一下,然后就被卡住了。。。
## 28.1 使用 `go module` 导入本地包
### 28.1.1 前提
假设我们现在有 `moduledemo``mypackage` 两个包,其中 `moduledemo` 包中会导入 `mypackage` 包并使用它的 New 方法。
`mypackage/mypackage.go` 内容如下:
```go
package mypackage
import "fmt"
func New(){
fmt.Println("mypackage.New")
}
```
我们现在分两种情况讨论:
### 28.1.2 在同一个项目下
**注意:**在一个项目project下我们是可以定义多个包package的。
#### 28.1.2.1 目录结构
现在的情况是,我们在 `moduledemo/main.go` 中调用了 `mypackage` 这个包。
```
moduledemo
├── go.mod
├── main.go
└── mypackage
└── mypackage.go
```
#### 28.1.2.2 导入包
这个时候,我们需要在 `moduledemo/go.mod` 中按如下定义:
```
module moduledemo
go 1.14
```
然后在 `moduledemo/main.go` 中按如下方式导入 `mypackage`:
```go
package main
import (
"fmt"
"moduledemo/mypackage" // 导入同一项目下的mypackage包
)
func main() {
mypackage.New()
fmt.Println("main")
}
```
#### 28.1.2.3 举个例子
举一反三,假设我们现在有文件目录结构如下:
```
└── bubble
├── dao
│ └── mysql.go
├── go.mod
└── main.go
```
其中 `bubble/go.mod` 内容如下:
```go
module github.com/q1mi/bubble
go 1.14
```
`bubble/dao/mysql.go` 内容如下:
```go
package dao
import "fmt"
func New(){
fmt.Println("mypackage.New")
}
```
`bubble/main.go` 内容如下:
```go
package main
import (
"fmt"
"github.com/q1mi/bubble/dao"
)
func main() {
dao.New()
fmt.Println("main")
}
```
### 28.1.3 不在同一个项目下
#### 28.1.3.1 目录结构
```
├── moduledemo
│ ├── go.mod
│ └── main.go
└── mypackage
├── go.mod
└── mypackage.go
```
#### 28.1.3.2 导入包
这个时候,`mypackage` 也需要进行 `module` 初始化,即拥有一个属于自己的 `go.mod` 文件,内容如下:
```
module mypackage
go 1.14
```
然后我们在 `moduledemo/main.go` 中按如下方式导入:
```go
import (
"fmt"
"mypackage"
)
func main() {
mypackage.New()
fmt.Println("main")
}
```
因为这两个包不在同一个项目路径下,你想要导入本地包,并且这些包也没有发布到远程的 github 或其他代码仓库地址。这个时候我们就需要在 `go.mod` 文件中使用 `replace` 指令。
在调用方也就是 `moduledemo/go.mod` 中按如下方式指定使用相对路径来寻找 `mypackage` 这个包。
```go
module moduledemo
go 1.14
require "mypackage" v0.0.0
replace "mypackage" => "../mypackage"
```
#### 28.1.3.3 举个例子
最后我们再举个例子巩固下上面的内容。
我们现在有文件目录结构如下:
```
├── p1
│ ├── go.mod
│ └── main.go
└── p2
├── go.mod
└── p2.go
```
`p1/main.go` 中想要导入 `p2.go` 中定义的函数。
`p2/go.mod` 内容如下:
```go
module liwenzhou.com/q1mi/p2
go 1.14
```
`p1/main.go` 中按如下方式导入
```go
import (
"fmt"
"liwenzhou.com/q1mi/p2"
)
func main() {
p2.New()
fmt.Println("main")
}
```
因为我并没有把 `liwenzhou.com/q1mi/p2` 这个包上传到 `liwenzhou.com` 这个网站,我们只是想导入本地的包,这个时候就需要用到 `replace` 这个指令了。
`p1/go.mod` 内容如下:
```go
module github.com/q1mi/p1
go 1.14
require "liwenzhou.com/q1mi/p2" v0.0.0
replace "liwenzhou.com/q1mi/p2" => "../p2"
```
此时,我们就可以正常编译 p1 这个项目了。
说再多也没用,自己动手试试吧。

View File

@@ -0,0 +1,996 @@
基于原文 [Go语言基础之并发](https://www.liwenzhou.com/posts/Go/14_concurrence/) 和 [视频 93-111](https://www.bilibili.com/video/BV17Q4y1P7n9?p=90) 整理。
# 17 Go语言中的并发编程
并发是编程里面一个非常重要的概念,**Go语言在语言层面天生支持并发**这也是Go语言流行的一个很重要的原因。
## 17.1 并发与并行
* 并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天,一个主体同一时间做多个任务)。
* 并行:同一时刻执行多个任务(你和你朋友都在用微信和女朋友聊天,多个主体同一时间做多个任务)。
Go 语言的并发通过 `goroutine` 实现。`goroutine` 类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个 goroutine 并发工作。**`goroutine` 是由 Go 语言的运行时runtime调度完成而线程是由操作系统调度完成。**
**Go 语言还提供 `channel` 在多个 `goroutine` 间进行通信**
`goroutine``channel` 是 Go 语言秉承的 `CSPCommunicating Sequential Process`并发模式的重要实现基础。
## 17.2 goroutine
在 java/c++ 中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。那么能不能有一种机制,**程序员只需要定义很多个任务让系统去帮助我们把这些任务分配到CPU上实现并发**执行呢?
Go 语言中的 `goroutine` 就是这样一种机制,`goroutine` 的概念类似于线程,但 **`goroutine`是由 Go 的运行时runtime调度和管理的。Go 程序会智能地将 `goroutine` 中的任务合理地分配给每个CPU**。Go 语言之所以被称为现代化的编程语言,就是因为它在语言层面已经**内置了调度和上下文切换的机制**。
在 Go 语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能——`goroutine`,当你**需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个 goroutine 去执行这个函数就可以了**,就是这么简单粗暴。
### 17.2.1 使用goroutine
Go 语言中使用 `goroutine` 非常简单,只需要**在调用函数的时候在前面加上 `go` 关键字,就可以为一个函数创建一个 `goroutine`**。
**一个 `goroutine` 必定对应一个函数,可以创建多个 `goroutine` 去执行相同的函数。**
### 17.2.2 启动单个goroutine
启动 `goroutine` 的方式非常简单只需要在调用的函数普通函数和匿名函数前面加上一个go关键字。
举个例子如下:
```go
func hello() {
fmt.Println("Hello Goroutine!")
}
func main() {
hello()
fmt.Println("main goroutine done!")
}
```
这个示例中 hello 函数和下面的语句是串行的,执行的结果是打印完 `Hello Goroutine!` 后打印 `main goroutine done!`
接下来我们在调用 hello 函数前面加上关键字 `go`,也就是启动一个 goroutine 去执行 hello 这个函数。
```go
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
}
```
这一次的执行结果只打印了 `main goroutine done!`,并没有打印 `Hello Goroutine!` 。为什么呢?
在程序启动时Go 程序就会为 `main()`函数创建一个默认的 goroutine。
**当 `main()` 函数返回的时候该 goroutine 就结束了,所有在 `main()` 函数中启动的 goroutine 会一同结束**main 函数所在的 goroutine 就像是权利的游戏中的夜王,其他的 goroutine 都是异鬼,夜王一死它转化的那些异鬼也就全部 GG 了。
所以我们要想办法让 main 函数等一等 hello 函数,最简单粗暴的方式就是 `time.Sleep` 了。
```go
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
time.Sleep(time.Second)
}
```
执行上面的代码你会发现,这一次先打印 `main goroutine done!`,然后紧接着打印 `Hello Goroutine!`
首先为什么会先打印 `main goroutine done!` 是因为我们**在创建新的 goroutine 的时候需要花费一些时间**,而此时 main 函数所在的 goroutine 是继续执行的。
### 17.2.3 启动多个 goroutine
在 Go 语言中实现并发就是这样简单,我们还可以启动多个 goroutine。让我们再来一个例子 (这里使用了`sync.WaitGroup` 来实现 goroutine 的同步)
```go
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}
```
多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为 10 个 goroutine 是并发执行的,而 **goroutine 的调度是随机的**
## 17.3 goroutine与线程
### 17.3.1 可增长的栈
`OS线程`操作系统线程一般都有固定的栈内存通常为2MB, **一个 goroutine 的栈在其生命周期开始时只有很小的栈典型情况下2KBgoroutine 的栈不是固定的他可以按需增大和缩小goroutine 的栈大小限制可以达到 1GB**,虽然极少会用到这么大。所以在 Go 语言中一次创建十万左右的 goroutine 也是可以的。
### 17.3.2 goroutine 调度
`GPM` 是 Go 语言运行时runtime层面的实现是 go 语言自己实现的一套**调度系统**。区别于操作系统调度 OS 线程。
* `G` Goroutine ,里面除了存放本 goroutine 堆栈等信息外,还有与所在 P 的绑定等信息。
* `P` Processor管理着一组 goroutine 队列P 里面会存储当前 goroutine 运行的上下文环境函数指针堆栈地址及地址边界P 会对自己管理的 goroutine 队列做一些调度(比如把占用 CPU 时间较长的 goroutine 暂停、运行后续的 goroutine 等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他 P 的队列里抢任务。
* `M` Machine是 Go 运行时runtime对操作系统内核线程的虚拟 M 与内核线程一般是一一映射的关系, 一个 groutine 最终是要放到M上执行的
P 与 M 一般也是一一对应的。他们关系是: **P 管理着一组 G 挂载在 M 上运行**。**当一个 G 长久阻塞在一个 M 上时runtime 会新建一个 M阻塞 G 所在的 P 会把其他的 G 挂载在新建的 M上。当旧的 G 阻塞完成或者认为其已经死掉时回收旧的 M。该调度模式其实是一种抢占式的调度**
**P 的个数是通过 `runtime.GOMAXPROCS` 设定最大256Go1.5 版本之后默认为物理线程数**。 在并发量大的时候会增加一些 P 和 M但不会太多切换太频繁的话得不偿失。
单从线程调度讲Go 语言相比起其他语言的优势在于: OS 线程是由 OS 内核来调度的goroutine 则是由Go运行时runtime自己的调度器调度的**这个调度器使用一个称为 m:n 调度的技术(复用/调度 m 个 goroutine 到 n 个 OS 线程)**。 其一大特点是 goroutine 的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的 `malloc` 函数(除非内存池需要改变),成本比调度 OS 线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干 goroutine 均分在物理线程上, 再加上本身 goroutine 的超轻量以上种种保证了go调度方面的性能。
其他相关参考:
* [深入Golang调度器之GMP模型](https://www.cnblogs.com/sunsky303/p/9705727.html)
### 17.3.3 GOMAXPROCS
Go 运行时的调度器使用 `GOMAXPROCS` 参数来确定需要使用多少个 OS 线程来同时执行 Go 代码。默认值是机器上的 CPU 核心数。例如在一个 8 核心的机器上,调度器会把 Go 代码同时调度到 8 个 OS 线程上( **GOMAXPROCS 是 m:n 调度中的 n**)。
Go 语言中可以通过 `runtime.GOMAXPROCS()` 函数**设置当前程序并发时占用的 CPU 逻辑核心数**。
Go1.5 版本之前默认使用的是单核心执行。Go1.5版本之后,默认使用全部的 CPU 逻辑核心数。
我们可以通过将任务分配到不同的 CPU 逻辑核心上实现并行的效果,这里举个例子:
```go
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
runtime.GOMAXPROCS(1)
go a()
go b()
time.Sleep(time.Second)
}
```
两个任务只有一个逻辑核心,此时是做完一个任务再做另一个任务。 将逻辑核心数设为 2此时两个任务并行执行代码如下。
```go
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
runtime.GOMAXPROCS(2)
go a()
go b()
time.Sleep(time.Second)
}
```
Go 语言中的操作系统线程和 goroutine 的关系如下:
* 一个操作系统线程对应用户态多个 goroutine。
* go 程序可以同时使用多个操作系统线程。
* **goroutine 和 OS 线程是多对多的关系,即 m:n**。
## 17.4 channel
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的 goroutine 中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go 语言的并发模型是 `CSPCommunicating Sequential Processes`,提倡**通过通信共享内存**而不是通过共享内存而实现通信。
如果说 `goroutine` 是 Go 程序并发的执行体,`channel` 就是它们之间的连接。**`channel` 是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。**
Go 语言中的通道channel是一种特殊的类型。**通道像一个传送带或者队列,总是遵循先入先出(`First In First Out`)的规则,保证收发数据的顺序**。每一个通道都是一个具体类型的导管,也就是**声明 channel 的时候需要为其指定元素类型**。
### 17.4.1 channel类型
channel 是一种类型,一种 `引用类型`。声明通道类型的格式如下:
```go
var 变量 chan 元素类型
```
举几个例子:
```go
var ch1 chan int // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道
```
### 17.4.2 创建 channel
通道是引用类型,通道类型的空值是 `nil`
```go
var ch chan int
fmt.Println(ch) // <nil>
```
声明的通道需要使**用 `make` 函数初始化之后才能使用**。
创建 channel 的格式如下:
```go
make(chan 元素类型, [缓冲大小])
```
channel 的缓冲大小是可选的。
举几个例子:
```go
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)
```
### 17.4.3 channel 操作
通道有发送(`send`)、接收(`receive`)和关闭(`close`)三种操作。
发送和接收都使用 **`<-`** 符号。
现在我们先使用以下语句定义一个通道:
```go
ch := make(chan int)
```
#### 17.4.3.1 发送
将一个值发送到通道中。
```go
ch <- 10 // 把10发送到ch中
```
#### 17.4.3.2 接收
从一个通道中接收值。
```go
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值忽略结果
```
#### 17.4.3.3 关闭
我们通过调用内置的 `close` 函数来关闭通道。
```go
close(ch)
```
关于关闭通道需要注意的事情是只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。
**通道是可以被垃圾回收机制回收的**,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
* 对一个关闭的通道再发送值就会导致 panic。
* 对一个关闭的通道进行接收会一直获取值直到通道为空。
* 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
* 关闭一个已经关闭的通道会导致 panic。
### 17.4.4 无缓冲的通道
无缓冲的通道又称为 `阻塞的通道`。我们来看一下下面的代码:
```go
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}
```
上面这段代码能够通过编译,但是执行的时候会出现以下错误:
```go
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
.../src/github.com/Q1mi/studygo/day06/channel02/main.go:8 +0x54
```
为什么会出现 deadlock 错误呢?
因为我们使用 `ch := make(chan int)` 创建的是无缓冲的通道,**无缓冲的通道只有在有人接收值的时候才能发送值**。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。
上面的代码会阻塞在 `ch <- 10` 这一行代码形成死锁,那如何解决这个问题呢?
一种方法是**启用一个 goroutine 去接收值**,例如:
```go
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 启用goroutine从通道接收值
ch <- 10
fmt.Println("发送成功")
}
```
无缓冲通道上的发送操作会阻塞,直到另一个 goroutine 在该通道上执行接收操作,这时值才能发送成功,两个 goroutine 将继续执行。相反,如果接收操作先执行,接收方的 goroutine 将阻塞,直到另一个 goroutine 在该通道上发送一个值。
使用无缓冲通道进行通信将导致发送和接收的 goroutine 同步化。因此,**无缓冲通道也被称为同步通道**。
### 17.4.5 有缓冲的通道
解决上面问题的方法还有一种就是使用有缓冲区的通道。我们可以在使用 make 函数初始化通道的时候为其指定通道的容量,例如:
```go
func main() {
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
ch <- 10
fmt.Println("发送成功")
}
```
只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
我们**可以使用内置的 `len` 函数获取通道内元素的数量,使用 `cap` 函数获取通道的容量**,虽然我们很少会这么做。
### 17.4.6 for range从通道循环取值
当向通道中发送完数据时,我们可以通过 `close` 函数来关闭通道。
当通道被关闭时,再往该通道发送值会引发 panic从该通道取值的操作会先取完通道中的值再然后取到的值一直都是对应类型的零值。那如何判断一个通道是否被关闭了呢
我们来看下面这个例子:
```go
// channel 练习
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// 开启goroutine将0~100的数发送到ch1中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
// 开启goroutine从ch1中接收值并将该值的平方发送到ch2中
go func() {
for {
i, ok := <-ch1 // 通道关闭后再取值ok=false
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}()
// 在主goroutine中从ch2中接收值打印
for i := range ch2 { // 通道关闭后会退出for range循环
fmt.Println(i)
}
}
```
从上面的例子中我们看到有两种方式在接收值的时候判断该通道是否被关闭,不过我们通常使用的是`for range` 的方式。使用 `for range` 遍历通道当通道被关闭的时候就会退出for range。
### 17.4.7 单向通道
有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。
Go语言中提供了单向通道来处理这种情况。例如我们把上面的例子改造如下
```go
func counter(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go squarer(ch2, ch1)
printer(ch2)
}
```
其中,
* `chan<- int` 是一个只写单向通道只能对其写入int类型值可以对其执行发送操作但是不能执行接收操作
* `<-chan int` 是一个只读单向通道只能从其读取int类型值可以对其执行接收操作但是不能执行发送操作。
在函数传参及任何赋值操作中可以将双向通道转换为单向通道,但反过来是不可以的。
### 17.4.8 通道总结
channel 常见的异常总结,如下图:
![](pics/17-1-channel常见的异常.png)
关闭已经关闭的 channel 也会引发 panic。
## 17.5 worker poolgoroutine池
在工作中我们通常会使用可以指定启动的 goroutine 数量 `worker pool`模式,控制 goroutine 的数量,防止 goroutine 泄漏和暴涨。
一个简易的 work pool 示例代码如下:
```go
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("worker:%d start job:%d\n", id, j)
time.Sleep(time.Second)
fmt.Printf("worker:%d end job:%d\n", id, j)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// 开启3个goroutine
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 5个任务
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// 输出结果
for a := 1; a <= 5; a++ {
<-results
}
}
```
## 17.6 select多路复用
在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。你也许会写出如下代码使用遍历的方式来实现:
```go
for{
// 尝试从ch1接收值
data, ok := <-ch1
// 尝试从ch2接收值
data, ok := <-ch2
}
```
这种方式虽然可以实现从多个通道接收值的需求但是运行性能会差很多。为了应对这种场景Go 内置了**`select` 关键字,可以同时响应多个通道的操作**。
select 的使用类似于 switch 语句,它有一系列 case 分支和一个默认的分支。每个 case 会对应一个通道的通信接收或发送过程。select 会一直等待,直到某个 case 的通信操作完成时,就会执行 case 分支对应的语句。具体格式如下:
```go
select{
case <-ch1:
...
case data := <-ch2:
...
case ch3<-data:
...
default:
默认操作
}
```
举个小例子来演示下 select 的使用:
```go
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
}
}
}
```
使用 select 语句能提高代码的可读性。
* 可处理一个或多个 channel 的发送/接收操作。
* 如果多个 case 同时满足select 会随机选择一个。
* 对于没有 case 的 select{} 会一直等待,可用于阻塞 main 函数。
## 17.7 并发安全和锁
有时候在 Go 代码中**可能会存在多个 goroutine 同时操作一个资源(临界区),这种情况会发生 `竞态问题(数据竞态)`**。类比现实生活中的例子有十字路口被各个方向的的汽车竞争;还有火车上的卫生间被车厢里的人竞争。
举个例子:
```go
var x int64
var wg sync.WaitGroup
func add() {
for i := 0; i < 5000; i++ {
x = x + 1
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
```
上面的代码中我们开启了两个 goroutine 去累加变量 x 的值,这两个 goroutine 在访问和修改 x 变量的时候就会存在数据竞争,导致最后的结果与期待的不符。
### 17.7.1 互斥锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个 goroutine 可以访问共享资源。Go 语言中使用 sync 包的 `Mutex` 类型来实现互斥锁。 使用互斥锁来修复上面代码的问题:
```go
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
for i := 0; i < 5000; i++ {
lock.Lock() // 加锁
x = x + 1
lock.Unlock() // 解锁
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
```
* 使用互斥锁能够保证同一时间有且只有一个 goroutine 进入临界区,其他的 goroutine 则在等待锁;
* 当互斥锁释放后,等待的 goroutine 才可以获取锁进入临界区,
* 多个 goroutine 同时等待一个锁时,唤醒的策略是随机的
### 17.7.2 读写互斥锁
互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,**当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择**。
读写锁在 Go 语言中使用 sync 包中的 **`RWMutex`** 类型。
读写锁分为两种:**读锁** 和 **写锁**
* 当一个 goroutine 获取读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待;
* 当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待。
读写锁示例:
```go
var (
x int64
wg sync.WaitGroup
lock sync.Mutex
rwlock sync.RWMutex
)
func write() {
// lock.Lock() // 加互斥锁
rwlock.Lock() // 加写锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
rwlock.Unlock() // 解写锁
// lock.Unlock() // 解互斥锁
wg.Done()
}
func read() {
// lock.Lock() // 加互斥锁
rwlock.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwlock.RUnlock() // 解读锁
// lock.Unlock() // 解互斥锁
wg.Done()
}
func main() {
start := time.Now()
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 1000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}
```
需要注意的是 **读写锁非常适合读多写少的场景** ,如果读和写的操作差别不大,读写锁的优势就发挥不出来。
### 17.7.3 sync.WaitGroup
在代码中生硬的使用 time.Sleep 肯定是不合适的Go 语言中可以使用 `sync.WaitGroup` 来实现并发任务的同步。 `sync.WaitGroup` 有以下几个方法:
方法名 | 功能
---|---
`(wg * WaitGroup) Add(delta int)` | 计数器+delta
`(wg *WaitGroup) Done()` | 计数器-1
`(wg *WaitGroup) Wait()` | 阻塞直到计数器变为0
`sync.WaitGroup` 内部维护着一个计数器,计数器的值可以增加和减少。
例如当我们启动了 N 个并发任务时,就将计数器值增加 N。每个任务完成时通过调用 `Done()`方法将计数器减1。通过调用 `Wait()`来等待并发任务执行完,当计数器值为 0 时,表示所有并发任务已经完成。
我们利用 `sync.WaitGroup` 将上面的代码优化一下:
```go
var wg sync.WaitGroup
func hello() {
defer wg.Done()
fmt.Println("Hello Goroutine!")
}
func main() {
wg.Add(1)
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
wg.Wait()
}
```
需要注意 **`sync.WaitGroup` 是一个结构体,传递的时候要传递指针**。
### 17.7.4 sync.Once
说在前面的话:这是一个进阶知识点。
**在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次**,例如只加载一次配置文件、只关闭一次通道等。
Go 语言中的 sync 包中提供了一个针对只执行一次场景的解决方案–**`sync.Once`**。
`sync.Once` 只有一个 `Do` 方法,其签名如下:
```go
func (o *Once) Do(f func()) {}
```
>备注:如果要执行的函数 f 需要传递参数就需要搭配闭包来使用。
#### 17.7.4.1 加载配置文件示例
延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量比如在init函数中完成初始化会增加程序的启动耗时而且有可能实际执行过程中这个变量没有用上那么这个初始化操作就不是必须要做的。我们来看一个例子
```go
var icons map[string]image.Image
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 被多个goroutine调用时不是并发安全的
func Icon(name string) image.Image {
if icons == nil {
loadIcons()
}
return icons[name]
}
```
多个 goroutine 并发调用 Icon 函数时不是并发安全的,现代的编译器和 CPU 可能会在保证每个 goroutine 都满足串行一致的基础上自由地重排访问内存的顺序。loadIcons 函数可能会被重排为以下结果:
```go
func loadIcons() {
icons = make(map[string]image.Image)
icons["left"] = loadIcon("left.png")
icons["up"] = loadIcon("up.png")
icons["right"] = loadIcon("right.png")
icons["down"] = loadIcon("down.png")
}
```
在这种情况下就会出现即使判断了 icons 不是 nil 也不意味着变量初始化完成了。考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化 icons 的时候不会被其他的 goroutine 操作,但是这样做又会引发性能问题。
使用 `sync.Once` 改造的示例代码如下:
```go
var icons map[string]image.Image
var loadIconsOnce sync.Once
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 是并发安全的
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}
```
#### 17.7.4.2 并发安全的单例模式
下面是借助 `sync.Once` 实现的并发安全的单例模式:
```go
package singleton
import (
"sync"
)
type singleton struct {}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
```
`sync.Once` 其实**内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成**。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。
### 17.7.5 sync.Map
**Go 语言中内置的 map 不是并发安全的**。请看下面的示例:
```go
var m = make(map[string]int)
func get(key string) int {
return m[key]
}
func set(key string, value int) {
m[key] = value
}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
set(key, n)
fmt.Printf("k=:%v,v:=%v\n", key, get(key))
wg.Done()
}(i)
}
wg.Wait()
}
```
上面的代码开启少量几个 goroutine 的时候可能没什么问题,当并发多了之后执行上面的代码就会报 `fatal error: concurrent map writes` 错误。
像这种场景下就需要为 map 加锁来保证并发的安全性了Go 语言的 sync 包中提供了一个开箱即用的并发安全版 `mapsync.Map`。**开箱即用表示**不用像内置的 map 一样使用 make 函数初始化就能直接使用(即**无需使用 make 初始化就可以直接使用**)。同时 `sync.Map` 内置了诸如 `Store``Load``LoadOrStore``Delete``Range`等操作方法。
```go
var m = sync.Map{}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n)
value, _ := m.Load(key)
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}
```
## 17.8 原子操作
代码中的**加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高**。
所以,**针对基本数据类型我们还可以使用原子操作来保证并发安全**。
原子操作是 Go 语言提供的方法它在用户态就可以完成因此性能比加锁操作更好。Go 语言中原子操作由内置的标准库 `sync/atomic` 提供。
### 17.8.1 atomic包
读取操作
* `func LoadInt32(addr *int32) (val int32)`
* `func LoadInt64(addr *int64) (val int64)`
* `func LoadUint32(addr *uint32) (val uint32)`
* `func LoadUint64(addr *uint64) (val uint64)`
* `func LoadUintptr(addr *uintptr) (val uintptr)`
* `func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)`
写入操作
* `func StoreInt32(addr *int32, val int32)`
* `func StoreInt64(addr *int64, val int64)`
* `func StoreUint32(addr *uint32, val uint32)`
* `func StoreUint64(addr *uint64, val uint64)`
* `func StoreUintptr(addr *uintptr, val uintptr)`
* `func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)`
修改操作
* `func AddInt32(addr *int32, delta int32) (new int32)`
* `func AddInt64(addr *int64, delta int64) (new int64)`
* `func AddUint32(addr *uint32, delta uint32) (new uint32)`
* `func AddUint64(addr *uint64, delta uint64) (new uint64)`
* `func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)`
交换操作
* `func SwapInt32(addr *int32, new int32) (old int32)`
* `func SwapInt64(addr *int64, new int64) (old int64)`
* `func SwapUint32(addr *uint32, new uint32) (old uint32)`
* `func SwapUint64(addr *uint64, new uint64) (old uint64)`
* `func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)`
* `func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)`
比较并交换操作
* `func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)`
* `func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)`
* `func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)`
* `func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)`
* `func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)`
* `func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)`
### 17.8.2 示例
我们填写一个示例来比较下互斥锁和原子操作的性能。
```go
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
type Counter interface {
Inc()
Load() int64
}
// 普通版
type CommonCounter struct {
counter int64
}
func (c CommonCounter) Inc() {
c.counter++
}
func (c CommonCounter) Load() int64 {
return c.counter
}
// 互斥锁版
type MutexCounter struct {
counter int64
lock sync.Mutex
}
func (m *MutexCounter) Inc() {
m.lock.Lock()
defer m.lock.Unlock()
m.counter++
}
func (m *MutexCounter) Load() int64 {
m.lock.Lock()
defer m.lock.Unlock()
return m.counter
}
// 原子操作版
type AtomicCounter struct {
counter int64
}
func (a *AtomicCounter) Inc() {
atomic.AddInt64(&a.counter, 1)
}
func (a *AtomicCounter) Load() int64 {
return atomic.LoadInt64(&a.counter)
}
func test(c Counter) {
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
c.Inc()
wg.Done()
}()
}
wg.Wait()
end := time.Now()
fmt.Println(c.Load(), end.Sub(start))
}
func main() {
c1 := CommonCounter{} // 非并发安全
test(c1)
c2 := MutexCounter{} // 使用互斥锁实现并发安全
test(&c2)
c3 := AtomicCounter{} // 并发安全且比互斥锁效率更高
test(&c3)
}
```
atomic 包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者 sync 包的函数/类型实现同步更好。
## 17.9 练习题
* 使用 goroutine 和 channel 实现一个计算 int64 随机数各位数和的程序。
* 开启一个 goroutine 循环生成 int64 类型的随机数,发送到 jobChan
* 开启 24 个 goroutine 从 jobChan 中取出随机数计算各位数的和,将结果发送到 resultChan
* 主 goroutine 从 resultChan 取出结果并打印到终端输出
* 为了保证业务代码的执行性能将之前写的日志库改写为异步记录日志方式。

View File

@@ -0,0 +1,578 @@
* [原文链接](https://www.liwenzhou.com/posts/Go/go_context/)
* [视频链接:147-148](https://www.bilibili.com/video/BV17Q4y1P7n9?p=147)
在 Go http 包的 Server 中,每一个请求都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和 RPC 服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的 token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。
## 1. 为什么需要Context
### 1.1. 基本示例
```go
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
// 初始的例子
func worker() {
for {
fmt.Println("worker")
time.Sleep(time.Second)
}
// 如何接收外部命令实现退出
wg.Done()
}
func main() {
wg.Add(1)
go worker()
// 如何优雅的实现结束子goroutine
wg.Wait()
fmt.Println("over")
}
```
### 1.2. 全局变量方式
```go
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
var exit bool
// 全局变量方式存在的问题:
// 1. 使用全局变量在跨包调用时不容易统一
// 2. 如果worker中再启动goroutine就不太好控制了。
func worker() {
for {
fmt.Println("worker")
time.Sleep(time.Second)
if exit {
break
}
}
wg.Done()
}
func main() {
wg.Add(1)
go worker()
time.Sleep(time.Second * 3) // sleep3秒以免程序过快退出
exit = true // 修改全局变量实现子goroutine的退出
wg.Wait()
fmt.Println("over")
}
```
### 1.3. 通道方式
```go
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
// 管道方式存在的问题:
// 1. 使用全局变量在跨包调用时不容易实现规范和统一需要维护一个共用的channel
func worker(exitChan chan struct{}) {
LOOP:
for {
fmt.Println("worker")
time.Sleep(time.Second)
select {
case <-exitChan: // 等待接收上级通知
break LOOP
default:
}
}
wg.Done()
}
func main() {
var exitChan = make(chan struct{})
wg.Add(1)
go worker(exitChan)
time.Sleep(time.Second * 3) // sleep3秒以免程序过快退出
exitChan <- struct{}{} // 给子goroutine发送退出信号
close(exitChan)
wg.Wait()
fmt.Println("over")
}
```
### 1.4. 官方版的方案
```go
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func worker(ctx context.Context) {
LOOP:
for {
fmt.Println("worker")
time.Sleep(time.Second)
select {
case <-ctx.Done(): // 等待上级通知
break LOOP
default:
}
}
wg.Done()
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 3)
cancel() // 通知子goroutine结束
wg.Wait()
fmt.Println("over")
}
```
当子 goroutine 又开启另外一个 goroutine 时,只需要将 ctx 传入即可:
```go
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func worker(ctx context.Context) {
go worker2(ctx)
LOOP:
for {
fmt.Println("worker")
time.Sleep(time.Second)
select {
case <-ctx.Done(): // 等待上级通知
break LOOP
default:
}
}
wg.Done()
}
func worker2(ctx context.Context) {
LOOP:
for {
fmt.Println("worker2")
time.Sleep(time.Second)
select {
case <-ctx.Done(): // 等待上级通知
break LOOP
default:
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 3)
cancel() // 通知子goroutine结束
wg.Wait()
fmt.Println("over")
}
```
## 2. Context 初识
Go1.7 加入了一个新的标准库 `context` ,它定义了 `Context` 类型,专门**用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作**,这些操作可能涉及多个 API 调用。
对服务器传入的请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传递上下文,或者可以使用 `WithCancel``WithDeadline``WithTimeout``WithValue` 创建的派生上下文。当一个上下文被取消时,它派生的所有上下文也被取消。
## 3. Context 接口
### 3.1. Context 接口
`context.Context` 是一个接口,该接口定义了四个需要实现的方法。具体签名如下:
```go
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
```
其中:
* `Deadline` 方法需要返回当前 `Context` 被取消的时间也就是完成工作的截止时间deadline
* `Done` 方法需要返回一个 `Channel`,这个 `Channel` 会在当前工作完成或者上下文被取消之后关闭,多次调用 Done 方法会返回同一个 `Channel`
* `Err` 方法会返回当前 `Context` 结束的原因,它只会在 Done 返回的 Channel 被关闭时才会返回非空的值;
* 如果当前 `Context` 被取消就会返回 `Canceled` 错误;
* 如果当前 `Context` 超时就会返回 `DeadlineExceeded` 错误;
* `Value` 方法会从 `Context` 中返回键对应的值,对于同一个上下文来说,多次调用 `Value` 并传入相同的 `Key` 会返回相同的结果,该方法仅用于传递跨 API 和进程间跟请求域的数据;
### 3.2. `Background()` 和 `TODO()`
Go 内置两个函数:`Background()``TODO()`,这两个函数分别返回一个实现了 `Context` 接口的`background``todo`。我们代码中最开始都是以这两个内置的上下文对象作为最顶层的 `partent context`,衍生出更多的子上下文对象。
`Background()` 主要用于 `main` 函数、初始化以及测试代码中,作为 Context 这个树结构的最顶层的 Context也就是 **根Context**
`TODO()`,它目前还不知道具体的使用场景,如果我们不知道该使用什么 Context 的时候,可以使用这个。
`background``todo` 本质上都是 `emptyCtx` 结构体类型,是一个**不可取消,没有设置截止时间,没有携带任何值的 `Context`**。
## 4. With系列函数
此外context 包中还定义了四个 With 系列函数。
### 4.1. WithCancel
`WithCancel` 的函数签名如下:
```go
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
```
`WithCancel` 返回带有新 Done 通道的父节点的副本。当调用返回的 cancel 函数或当关闭父上下文的Done 通道时,将关闭返回上下文的 Done 通道,无论先发生什么情况。
取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用 cancel。
```go
func gen(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // return结束该goroutine防止泄露
case dst <- n:
n++
}
}
}()
return dst
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 当我们取完需要的整数后调用cancel
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
}
```
上面的示例代码中gen 函数在单独的 goroutine 中生成整数并将它们发送到返回的通道。 gen 的调用者在使用生成的整数之后需要取消上下文,以免 gen 启动的内部 goroutine 发生泄漏。
### 4.2. WithDeadline
`WithDeadline` 的函数签名如下:
```go
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
```
返回父上下文的副本,并将 deadline 调整为不迟于 d。如果父上下文的 deadline 已经早于 d`WithDeadline(parent, d)` 在语义上等同于父上下文。当截止日过期时,当调用返回的 cancel 函数时,或者当父上下文的 Done 通道关闭时,返回上下文的 Done 通道将被关闭,以最先发生的情况为准。
取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用 cancel。
```go
func main() {
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
// 尽管ctx会过期但在任何情况下调用它的cancel函数都是很好的实践。
// 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
```
上面的代码中,定义了一个 50 毫秒之后过期的 deadline然后我们调用 `context.WithDeadline(context.Background(), d)` 得到一个上下文ctx和一个取消函数cancel然后使用一个 select 让主程序陷入等待:等待 1 秒后打印 overslept 退出或者等待 ctx 过期后退出。 因为 ctx 50秒后就过期所以 `ctx.Done()` 会先接收到值,上面的代码会打印`ctx.Err()` 取消原因。
### 4.3. WithTimeout
`WithTimeout` 的函数签名如下:
```go
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
```
`WithTimeout` 返回 `WithDeadline(parent, time.Now().Add(timeout))`
取消此上下文将释放与其相关的资源,因此代码应该在此上下文中运行的操作完成后立即调用 cancel通常用于数据库或者网络连接的超时控制。具体示例如下
```go
package main
import (
"context"
"fmt"
"sync"
"time"
)
// context.WithTimeout
var wg sync.WaitGroup
func worker(ctx context.Context) {
LOOP:
for {
fmt.Println("db connecting ...")
time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
select {
case <-ctx.Done(): // 50毫秒后自动调用
break LOOP
default:
}
}
fmt.Println("worker done!")
wg.Done()
}
func main() {
// 设置一个50毫秒的超时
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 5)
cancel() // 通知子goroutine结束
wg.Wait()
fmt.Println("over")
}
```
### 4.4. WithValue
`WithValue` 函数能够将请求作用域的数据与 Context 对象建立关系。声明如下:
```go
func WithValue(parent Context, key, val interface{}) Context
```
`WithValue` 返回父节点的副本,其中与 key 关联的值为 val。
仅对 API 和进程间传递请求域的数据使用上下文值,而不是使用它来传递可选参数给函数。
**所提供的键必须是可比较的,并且不应该是 string 类型或任何其他内置类型**,以避免使用上下文在包之间发生冲突。`WithValue` 的用户应该为键定义自己的类型。为了避免在分配给 `interface{}` 时进行分配,上下文键通常具有具体类型 `struct{}`。或者,导出的上下文关键变量的静态类型应该是指针或接口。
```go
package main
import (
"context"
"fmt"
"sync"
"time"
)
// context.WithValue
type TraceCode string
var wg sync.WaitGroup
func worker(ctx context.Context) {
key := TraceCode("TRACE_CODE")
traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取trace code
if !ok {
fmt.Println("invalid trace code")
}
LOOP:
for {
fmt.Printf("worker, trace code:%s\n", traceCode)
time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
select {
case <-ctx.Done(): // 50毫秒后自动调用
break LOOP
default:
}
}
fmt.Println("worker done!")
wg.Done()
}
func main() {
// 设置一个50毫秒的超时
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
// 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合
ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234")
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 5)
cancel() // 通知子goroutine结束
wg.Wait()
fmt.Println("over")
}
```
## 5. 使用Context的注意事项
* 推荐以参数的方式显示传递 Context
* 以 Context 作为参数的函数方法,应该把 **Context 作为第一个参数**
* 给一个函数方法传递 Context 的时候,**不要传递nil如果不知道传递什么就使用 `context.TODO()`**
* Context 的 Value 相关方法应该传递请求域的必要数据,不应该用于传递可选参数
* **Context 是线程安全的,可以放心的在多个 goroutine 中传递**
## 6. 客户端超时取消示例
调用服务端API时如何在客户端实现超时控制
### 6.1. server端
```go
// context_timeout/server/main.go
package main
import (
"fmt"
"math/rand"
"net/http"
"time"
)
// server端随机出现慢响应
func indexHandler(w http.ResponseWriter, r *http.Request) {
number := rand.Intn(2)
if number == 0 {
time.Sleep(time.Second * 10) // 耗时10秒的慢响应
fmt.Fprintf(w, "slow response")
return
}
fmt.Fprint(w, "quick response")
}
func main() {
http.HandleFunc("/", indexHandler)
err := http.ListenAndServe(":8000", nil)
if err != nil {
panic(err)
}
}
```
### 6.2. client端
```go
// context_timeout/client/main.go
package main
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
)
// 客户端
type respData struct {
resp *http.Response
err error
}
func doCall(ctx context.Context) {
transport := http.Transport{
// 请求频繁可定义全局的client对象并启用长链接
// 请求不频繁使用短链接
DisableKeepAlives: true, }
client := http.Client{
Transport: &transport,
}
respChan := make(chan *respData, 1)
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil)
if err != nil {
fmt.Printf("new requestg failed, err:%v\n", err)
return
}
req = req.WithContext(ctx) // 使用带超时的ctx创建一个新的client request
var wg sync.WaitGroup
wg.Add(1)
defer wg.Wait()
go func() {
resp, err := client.Do(req)
fmt.Printf("client.do resp:%v, err:%v\n", resp, err)
rd := &respData{
resp: resp,
err: err,
}
respChan <- rd
wg.Done()
}()
select {
case <-ctx.Done():
//transport.CancelRequest(req)
fmt.Println("call api timeout")
case result := <-respChan:
fmt.Println("call server api success")
if result.err != nil {
fmt.Printf("call server api failed, err:%v\n", result.err)
return
}
defer result.resp.Body.Close()
data, _ := ioutil.ReadAll(result.resp.Body)
fmt.Printf("resp:%v\n", string(data))
}
}
func main() {
// 定义一个100毫秒的超时
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
defer cancel() // 调用cancel释放子goroutine资源
doCall(ctx)
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

View File

@@ -0,0 +1,481 @@
基于原文:[Go语言基础之网络编程](https://www.liwenzhou.com/posts/Go/15_socket/#autoid-2-3-2) 和 视频 [112-115](https://www.bilibili.com/video/BV17Q4y1P7n9?p=112) 整理
现在我们几乎每天都在使用互联网,我们前面已经学习了如何编写 Go 语言程序但是如何才能让我们的程序通过网络互相通信呢本章我们就一起来学习下Go语言中的网络编程。
关于网络编程其实是一个很庞大的领域,本文只是简单的演示了如何使用 net 包进行 `TCP``UDP` 通信。如需了解更详细的网络编程请自行检索和阅读专业资料。
## 18.1 互联网协议介绍
互联网的核心是一系列协议,总称为 `互联网协议Internet Protocol Suite`,正是这一些协议规定了电脑如何连接和组网。我们理解了这些协议,就理解了互联网的原理。由于这些协议太过庞大和复杂,没有办法在这里一概而全,只能介绍一下我们日常开发中接触较多的几个协议。
### 18.1.1 互联网分层模型
互联网的逻辑实现被分为好几层。每一层都有自己的功能,就像建筑物一样,每一层都靠下一层支持。用户接触到的只是最上面的那一层,根本不会感觉到下面的几层。要理解互联网就需要自下而上理解每一层的实现的功能。
![](pics/18-1-osi模型.png)
如上图所示,互联网按照不同的模型划分会有不用的分层,但是不论按照什么模型去划分,越往上的层越靠近用户,越往下的层越靠近硬件。在软件开发中我们使用最多的是上图中将互联网划分为五个分层的模型。
接下来我们一层一层的自底向上介绍一下每一层。
#### 18.1.1.1 物理层
我们的电脑要与外界互联网通信,需要先把电脑连接网络,我们可以用`双绞线``光纤``无线`电波等方式。这就叫做 **`实物理层`**,它就是把电脑连接起来的物理手段。它主要规定了网络的一些电气特性,作用是负责传送 0 和 1 的电信号。
#### 18.1.1.2 数据链路层
单纯的 0 和 1 没有任何意义,所以我们使用者会为其赋予一些特定的含义,规定解读电信号的方式:例如:多少个电信号算一组?每个信号位有何意义?这就是 `数据链接层` 的功能,它在 `物理层` 的上方,**确定了物理层传输的 0 和 1 的分组方式及代表的意义**。早期的时候,每家公司都有自己的电信号分组方式。逐渐地,一种叫做 **`以太网Ethernet`**的协议,占据了主导地位。
以太网规定:
* 一组电信号构成一个数据包,叫做 `帧`Frame
* 每一帧分成两个部分:`标头Head`)和`数据Data`。其中,
* `标头` 包含数据包的一些说明项,比如发送者、接受者、数据类型等等;
* `数据` 则是数据包的具体内容。
* `标头` 的长度固定为18字节。
* `数据` 的长度最短为46字节最长为1500字节。
* 因此,整个 `帧` 最短为64字节最长为1518字节。如果数据很长就必须分割成多个帧进行发送。
那么,发送者和接受者是如何标识呢?
以太网规定,连入网络的所有设备都必须具有 `网卡` 接口。数据包必须是从一块网卡,传送到另一块网卡。网卡的地址,就是数据包的发送地址和接收地址,这叫做 **`MAC地址`**。
> 每块网卡出厂的时候,都有一个全世界独一无二的 MAC 地址,长度是 48 个二进制位,通常用 12 个十六进制数表示。前 6 个十六进制数是厂商编号后6个是该厂商的网卡流水号。有了MAC地址就可以定位网卡和数据包的路径了。
我们会通过 ARP 协议来获取接受方的 MAC 地址,有了 MAC 地址之后,如何把数据准确的发送给接收方呢?
其实这里以太网采用了一种很`原始`的方式,它不是把数据包准确送到接收方,而是向本网络内所有计算机都发送,让每台计算机读取这个包的 `标头`,找到接收方的 MAC 地址,然后与自身的 MAC 地址相比较,如果两者相同,就接受这个包,做进一步处理,否则就丢弃这个包。这种发送方式就叫做 **`广播broadcasting`**。
#### 18.1.1.3 网络层
按照以太网协议的规则我们可以依靠 MAC 地址来向外发送数据。理论上依靠 MAC 地址,你电脑的网卡就可以找到身在世界另一个角落的某台电脑的网卡了,但是这种做法有一个重大缺陷就是以太网采用广播方式发送数据包,所有成员人手一 `包` ,不仅效率低,而且发送的数据只能局限在发送者所在的子网络。也就是说如果两台计算机不在同一个子网络,广播是传不过去的。这种设计是合理且必要的,因为如果互联网上每一台计算机都会收到互联网上收发的所有数据包,那是不现实的。
因此,必须找到一种方法区分哪些 MAC 地址属于同一个子网络,哪些不是。**如果是同一个子网络,就采用广播方式发送,否则就采用 `路由` 方式发送**。这就导致了 **`网络层`** 的诞生。它的**作用是引进一套新的地址,使得我们能够区分不同的计算机是否属于同一个子网络。这套地址就叫做 `网络地址`,简称 `网址`**。
**`网络层` 出现以后,每台计算机有了两种地址,一种是 MAC 地址,另一种是网络地址。**两种地址之间没有任何联系MAC地址是绑定在网卡上的网络地址则是网络管理员分配的。**网络地址帮助我们确定计算机所在的子网络MAC 地址则将数据包送到该子网络中的目标网卡**。因此,从逻辑上可以推断,必定是先处理网络地址,然后再处理 MAC 地址。
**规定网络地址的协议,叫做 `IP 协议`**。它所定义的地址,就被称为 `IP地址`。目前,广泛采用的是 IP 协议第四版,简称 `IPv4`。IPv4 这个版本规定,网络地址由 32 个二进制位组成,我们通常习惯用分成四段的十进制数表示 IP 地址,从 `0.0.0.0` 一直到 `255.255.255.255`
**根据 IP 协议发送的数据,就叫做 `IP 数据包`**。IP 数据包也分为 `标头``数据` 两个部分:`标头`部分主要包括 `版本``长度``IP地址`等信息,`数据` 部分则是 IP 数据包的具体内容。IP数据包的 `标头`部分的长度为 20 到 60 字节,整个数据包的总长度最大为 65535 字节。
#### 18.1.1.4 传输层
有了 MAC 地址和 IP 地址,我们已经可以在互联网上任意两台主机上建立通信。但问题是同一台主机上会有许多程序都需要用网络收发数据,比如 QQ 和浏览器这两个程序都需要连接互联网并收发数据,我们如何区分某个数据包到底是归哪个程序的呢?也就是说,我们还需要一个参数,**表示这个数据包到底供哪个程序(进程)使用。这个参数就叫做 `端口`port**,它其实是每一个使用网卡的程序的编号。每个数据包都发到主机的特定端口,所以不同的程序就能取到自己所需要的数据。
**`端口` 是 0 到 65535 之间的一个整数,正好 16 个二进制位**。0 到1023的端口被系统占用用户只能选用大于 1023 的端口。有了 IP 和 端口 我们就能实现唯一确定互联网上一个程序,进而实现网络间的程序通信。
我们必须**在数据包中加入端口信息,这就需要新的协议。最简单的实现叫做 `UDP协议`**它的格式几乎就是在数据前面加上端口号。UDP 数据包,也是由 `标头``数据` 两部分组成:`标头`部分主要定义了发出端口和接收端口,`数据`部分就是具体的内容。UDP 数据包非常简单,`标头` 部分一共只有 8 个字节,总长度不超过 65,535 字节,正好放进一个 IP 数据包。
**UDP 协议的优点是比较简单容易实现但是缺点是可靠性较差一旦数据包发出无法知道对方是否收到。为了解决这个问题提高网络可靠性TCP 协议就诞生了。TCP 协议能够确保数据不会遗失。它的缺点是过程复杂、实现困难、消耗较多的资源**。TCP 数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常 TCP 数据包的长度不会超过 IP 数据包的长度,以确保单个 TCP 数据包不必再分割。
#### 18.1.1.5 应用层
应用程序收到 `传输层` 的数据,接下来就要对数据进行解包。由于互联网是开放架构,数据来源五花八门,必须事先规定好通信的数据格式,否则接收方根本无法获得真正发送的数据内容。**`应用层`的作用就是规定应用程序使用的数据格式**,例如我们 TCP 协议之上常见的 Email、HTTP、FTP 等协议,这些协议就组成了互联网协议的应用层。
如下图所示,发送方的 HTTP 数据经过互联网的传输过程中会依次添加各层协议的标头信息,接收方收到数据包之后再依次根据协议解包得到数据。
![](pics/18-2-HTTP数据传输图解.png)
## 18.2 socket编程
**Socket 是 BSD UNIX 的进程通信机制,通常也称作 `套接字`**,用于描述 IP 地址和端口,是一个通信链的句柄。
**Socket 可以理解为 `TCP/IP` 网络的 API**,它定义了许多函数或例程,程序员可以用它们来开发 TCP/IP 网络上的应用程序。电脑上运行的应用程序通常通过 `套接字` 向网络发出请求或者应答网络请求。
### 18.2.1 socket图解
Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层。在设计模式中Socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 后面,对用户来说只需要调用 Socket 规定的相关函数,让 Socket 去组织符合指定的协议数据然后进行通信。
![](pics/18-3-socket图解.png)
### 18.2.2 Go语言实现TCP通信
#### 18.2.2.1 TCP 协议
`TCP/IP(Transmission Control Protocol/Internet Protocol)` 即传输控制协议/网间协议是一种面向连接连接导向的、可靠的、基于字节流的传输层Transport layer通信协议因为是面向连接的协议数据像水流一样传输会存在 `黏包` 问题。
#### 18.2.2.2 TCP 服务端
一个 TCP 服务端可以同时连接很多个客户端,例如世界各地的用户使用自己电脑上的浏览器访问淘宝网。因为 Go 语言中创建多个 goroutine 实现并发非常方便和高效,所以我们可以每建立一次链接就创建一个 goroutine 去处理。
TCP服务端程序的处理流程
* 监听端口
* 接收客户端请求建立链接
* 创建 goroutine 处理链接。
我们使用 Go 语言的 `net` 包实现的 TCP 服务端代码如下:
```go
// tcp/server/main.go
// TCP server端
// 处理函数
func process(conn net.Conn) {
defer conn.Close() // 关闭连接
for {
reader := bufio.NewReader(conn)
var buf [128]byte
n, err := reader.Read(buf[:]) // 读取数据
if err != nil {
fmt.Println("read from client failed, err:", err)
break
}
recvStr := string(buf[:n])
fmt.Println("收到client端发来的数据", recvStr)
conn.Write([]byte(recvStr)) // 发送数据
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
for {
conn, err := listen.Accept() // 建立连接
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn) // 启动一个goroutine处理连接
}
}
```
将上面的代码保存之后编译成 server 或 server.exe 可执行文件。
#### 18.2.2.3 TCP 客户端
一个 TCP 客户端进行 TCP 通信的流程如下:
* 建立与服务端的链接
* 进行数据收发
* 关闭链接
使用 Go 语言的 net 包实现的 TCP 客户端代码如下:
```go
// tcp/client/main.go
// 客户端
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Println("err :", err)
return
}
defer conn.Close() // 关闭连接
inputReader := bufio.NewReader(os.Stdin)
for {
input, _ := inputReader.ReadString('\n') // 读取用户输入
inputInfo := strings.Trim(input, "\r\n")
if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出
return
}
_, err = conn.Write([]byte(inputInfo)) // 发送数据
if err != nil {
return
}
buf := [512]byte{}
n, err := conn.Read(buf[:])
if err != nil {
fmt.Println("recv failed, err:", err)
return
}
fmt.Println(string(buf[:n]))
}
}
```
将上面的代码编译成 client 或 client.exe 可执行文件,先启动 server 端再启动 client 端,在 client 端输入任意内容回车之后就能够在 server 端看到 client 端发送的数据,从而实现 TCP 通信。
### 18.2.3 TCP黏包
#### 18.2.3.1 黏包示例
服务端代码如下:
```go
// socket_stick/server/main.go
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
var buf [1024]byte
for {
n, err := reader.Read(buf[:])
if err == io.EOF {
break
}
if err != nil {
fmt.Println("read from client failed, err:", err)
break
}
recvStr := string(buf[:n])
fmt.Println("收到client发来的数据", recvStr)
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn)
}
}
```
客户端代码如下:
```go
// socket_stick/client/main.go
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("dial failed, err", err)
return
}
defer conn.Close()
for i := 0; i < 20; i++ {
msg := `Hello, Hello. How are you?`
conn.Write([]byte(msg))
}
}
```
将上面的代码保存后,分别编译。先启动服务端再启动客户端,可以看到服务端输出结果如下:
```
收到client发来的数据 Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据 Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据 Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据 Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据 Hello, Hello. How are you?Hello, Hello. How are you?
```
客户端分10次发送的数据在服务端并没有成功的输出10次而是多条数据“粘”到了一起。
#### 18.2.3.2 为什么会出现粘包
主要原因就是 tcp 数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。
“粘包”可发生在发送端也可发生在接收端:
* `由 Nagle 算法造成的发送端的粘包`Nagle 算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给 TCP 发送时TCP 并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
* `接收端接收不及时造成的接收端粘包`TCP 会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把 TCP 的数据取出来,就会造成 TCP 缓冲区中存放了几段数据。
#### 18.2.3.3 解决办法
出现 `粘包` 的关键在于接收方不确定将要传输的数据包的大小,因此我们可以**对数据包进行`封包``拆包`**的操作。
`封包`:封包就是给一段数据加上包头,这样一来数据包就分为 **`包头`**和**`包体`**两部分内容了(过滤非法包时封包会加入**`包尾`**内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
我们可以自己定义一个协议比如数据包的前4个字节为包头里面存储的是发送的数据的长度。
```go
// socket_stick/proto/proto.go
package proto
import (
"bufio"
"bytes"
"encoding/binary"
)
// Encode 将消息编码
func Encode(message string) ([]byte, error) {
// 读取消息的长度转换成int32类型占4个字节
var length = int32(len(message))
var pkg = new(bytes.Buffer)
// 写入消息头
err := binary.Write(pkg, binary.LittleEndian, length)
if err != nil {
return nil, err
}
// 写入消息实体
err = binary.Write(pkg, binary.LittleEndian, []byte(message))
if err != nil {
return nil, err
}
return pkg.Bytes(), nil
}
// Decode 解码消息
func Decode(reader *bufio.Reader) (string, error) {
// 读取消息的长度
lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据
lengthBuff := bytes.NewBuffer(lengthByte)
var length int32
err := binary.Read(lengthBuff, binary.LittleEndian, &length)
if err != nil {
return "", err
}
// Buffered返回缓冲中现有的可读取的字节数。
if int32(reader.Buffered()) < length+4 {
return "", err
}
// 读取真正的消息数据
pack := make([]byte, int(4+length))
_, err = reader.Read(pack)
if err != nil {
return "", err
}
return string(pack[4:]), nil
}
```
接下来在服务端和客户端分别使用上面定义的 proto 包的 Decode 和 Encode 函数处理数据。
服务端代码如下:
```go
// socket_stick/server2/main.go
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
msg, err := proto.Decode(reader)
if err == io.EOF {
return
}
if err != nil {
fmt.Println("decode msg failed, err:", err)
return
}
fmt.Println("收到client发来的数据", msg)
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn)
}
}
```
客户端代码如下:
```go
// socket_stick/client2/main.go
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("dial failed, err", err)
return
}
defer conn.Close()
for i := 0; i < 20; i++ {
msg := `Hello, Hello. How are you?`
data, err := proto.Encode(msg)
if err != nil {
fmt.Println("encode msg failed, err:", err)
return
}
conn.Write(data)
}
}
```
### 18.2.4 Go 语言实现 UDP 通信
#### 18.2.4.1 UDP 协议
`UDP协议User Datagram Protocol`)中文名称是**用户数据报协议**,是 OSIOpen System Interconnection开放式系统互联参考模型中一种无连接的传输层协议不需要建立连接就能直接进行数据发送和接收属于**不可靠的、没有时序的**通信,**但是UDP协议的实时性比较好通常用于视频直播相关领域。**
#### 18.2.4.2 UDP服务端
使用 Go 语言的 net 包实现的 UDP 服务端代码如下:
```go
// UDP/server/main.go
// UDP server端
func main() {
listen, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
var data [1024]byte
n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据
if err != nil {
fmt.Println("read udp failed, err:", err)
continue
}
fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
_, err = listen.WriteToUDP(data[:n], addr) // 发送数据
if err != nil {
fmt.Println("write to udp failed, err:", err)
continue
}
}
}
```
#### 18.2.4.3 UDP客户端
使用 Go 语言的 net 包实现的 UDP 客户端代码如下:
```go
// UDP 客户端
func main() {
socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("连接服务端失败err:", err)
return
}
defer socket.Close()
sendData := []byte("Hello server")
_, err = socket.Write(sendData) // 发送数据
if err != nil {
fmt.Println("发送数据失败err:", err)
return
}
data := make([]byte, 4096)
n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据
if err != nil {
fmt.Println("接收数据失败err:", err)
return
}
fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

View File

@@ -0,0 +1,461 @@
[原文《Docker 入门教程》——阮一峰](http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html)
---
> 注意:根据评论描述 `$ docker container ls` 可以查看运行中的 docker 容器 `$ docker container ls -l` 表示查看最近创建的容器 也有说使用 `docker images` 可以查看容器; 也有评论说官方推荐使用 `docker ps` 和 `docker ps -a ` 查看容器;
2013 年发布至今, [Docker](https://www.docker.com/) 一直广受瞩目,被认为可能会改变软件行业。
但是,许多人并不清楚 Docker 到底是什么,要解决什么问题,好处又在哪里?本文就来详细解释,帮助大家理解它,还带有简单易懂的实例,教你如何将它用于日常开发。
![](pics/docker入门-1.png )
## .1 环境配置的难题
软件开发最大的麻烦事之一,就是环境配置。用户计算机的环境都不相同,你怎么知道自家的软件,能在那些机器跑起来?
**用户必须保证两件事:操作系统的设置,各种库和组件的安装**。只有它们都正确,软件才能运行。举例来说,安装一个 Python 应用,计算机必须有 Python 引擎,还必须有各种依赖,可能还要配置环境变量。
如果某些老旧的模块与当前环境不兼容,那就麻烦了。开发者常常会说:"它在我的机器可以跑了"It works on my machine言下之意就是其他机器很可能跑不了。
环境配置如此麻烦,换一台机器,就要重来一次,旷日费时。很多人想到,能不能从根本上解决问题,**软件可以带环境安装?也就是说,安装的时候,把原始环境一模一样地复制过来。**
## .2 虚拟机
**`虚拟机virtual machine` 就是带环境安装的一种解决方案**。它可以在一种操作系统里面运行另一种操作系统,比如在 Windows 系统里面运行 Linux 系统。应用程序对此毫无感知,因为虚拟机看上去跟真实系统一模一样,而**对于底层系统来说,虚拟机就是一个普通文件,不需要了就删掉,对其他部分毫无影响**。
虽然用户可以通过虚拟机还原软件的原始环境。但是,这个方案有几个缺点:
* 资源占用多
虚拟机会独占一部分内存和硬盘空间。它运行的时候,其他程序就不能使用这些资源了。哪怕虚拟机里面的应用程序,真正使用的内存只有 1MB虚拟机依然需要几百 MB 的内存才能运行。
* 冗余步骤多
虚拟机是完整的操作系统,一些系统级别的操作步骤,往往无法跳过,比如用户登录。
* 启动慢
启动操作系统需要多久,启动虚拟机就需要多久。可能要等几分钟,应用程序才能真正运行。
## .3 Linux 容器
由于虚拟机存在前述三个缺点,所以 Linux 发展出了另一种虚拟化技术:**Linux 容器Linux Containers缩写为 LXC**。
**Linux 容器不是模拟一个完整的操作系统,而是对进程进行隔离**。或者说,在正常进程的外面套了一个保护层。**对于容器里面的进程来说,它接触到的各种资源都是虚拟的,从而实现与底层系统的隔离。**
由于容器是进程级别的,相比虚拟机有很多优势:
* 启动快
容器里面的应用,直接就是底层系统的一个进程,而不是虚拟机内部的进程。所以,启动容器相当于启动本机的一个进程,而不是启动一个操作系统,速度就快很多。
* 资源占用少
容器只占用需要的资源,不占用那些没有用到的资源;虚拟机由于是完整的操作系统,不可避免要占用所有资源。另外,**多个容器可以共享资源,虚拟机都是独享资源**。
* 体积小
容器只要包含用到的组件即可,而虚拟机是整个操作系统的打包,所以容器文件比虚拟机文件要小很多。
总之,**容器有点像轻量级的虚拟机,能够提供虚拟化的环境,但是成本开销小得多**。
## .3 Docker 是什么?
**`Docker` 属于 `Linux` 容器的一种封装,提供简单易用的容器使用接口。**它是目前最流行的 Linux 容器解决方案。
**`Docker` 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。**有了 Docker就不用担心环境问题。
总体来说,`Docker` 的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。**容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。**
## .5 Docker 的用途
Docker 的主要用途,目前有三大类。
* 提供一次性的环境。比如,本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。
* 提供弹性的云服务。因为 Docker 容器可以随开随关,很**适合动态扩容和缩容**。
* 组建微服务架构。通过多个容器,一台机器可以跑多个服务,因此在本机就可以模拟出微服务架构。
## .6 Docker 的安装
Docker 是一个开源的商业产品,有两个版本:**社区版Community Edition缩写为 CE**和**企业版Enterprise Edition缩写为 EE**。企业版包含了一些收费服务,个人开发者一般用不到。下面的介绍都针对社区版。
Docker CE 在不同操作系统上的安装请参考如下链接对应的官方文档:
* [Mac](https://docs.docker.com/docker-for-mac/install/)
* [Windows](https://docs.docker.com/docker-for-windows/install/)
* [Ubuntu](https://docs.docker.com/engine/install/ubuntu/)
* [Debian](https://docs.docker.com/engine/install/debian/)
* [CentOS](https://docs.docker.com/engine/install/centos/)
* [Fedora](https://docs.docker.com/engine/install/fedora/)
* [其他 Linux 发行版](https://docs.docker.com/engine/install/binaries/)
安装完成后,运行下面的命令,验证是否安装成功。
```
$ docker version
# 或者
$ docker info
```
Docker 需要用户具有 `sudo` 权限,为了避免每次命令都输入 `sudo`,可以把用户加入 [`Docker 用户组`(官方文档)](https://docs.docker.com/engine/install/linux-postinstall/)。
```
$ sudo usermod -aG docker $USER
```
Docker 是 `服务器--客户端` 架构。命令行运行 `docker` 命令的时候,需要本机有 Docker 服务。如果这项服务没有启动,可以用下面的命令启动([官方文档](https://docs.docker.com/config/daemon/systemd/))。
```
# service 命令的用法
$ sudo service docker start
# systemctl 命令的用法
$ sudo systemctl start docker
```
## .6 image 文件
**Docker 把应用程序及其依赖,打包在 image 文件里面**
* 只有通过这个文件,才能生成 Docker 容器。
* image 文件可以看作是容器的模板。
* Docker 根据 image 文件生成容器的实例。
* 同一个 image 文件,可以生成多个同时运行的容器实例。
`image` 是二进制文件。实际开发中,一个 `image` 文件往往通过继承另一个 `image` 文件,加上一些个性化设置而生成。举例来说,你可以在 Ubuntu 的 image 基础上,往里面加入 Apache 服务器,形成你的 image。
```
# 列出本机的所有 image 文件。
$ docker image ls
# 删除 image 文件
$ docker image rm [imageName]
```
`image` 文件是通用的,一台机器的 image 文件拷贝到另一台机器,照样可以使用。一般来说,为了节省时间,我们应该尽量使用别人制作好的 image 文件,而不是自己制作。即使要定制,也应该基于别人的 image 文件进行加工,而不是从零开始制作。
为了方便共享image 文件制作完成后,可以上传到网上的仓库。[Docker 的官方仓库 Docker Hub 是最重要、最常用的 image 仓库](https://hub.docker.com/)。此外,出售自己制作的 image 文件也是可以的。
## .7 实例hello world
下面,我们通过最简单的 image 文件 ["hello world"](https://hub.docker.com/_/hello-world),感受一下 Docker。
需要说明的是,国内连接 Docker 的官方仓库很慢,还会断线,需要将默认仓库改成国内的镜像网站,具体的修改方法在[下一篇文章《Docker 微服务教程》](http://www.ruanyifeng.com/blog/2018/02/docker-wordpress-tutorial.html)的第一节。有需要的朋友,可以先看一下。
首先,运行下面的命令,将 image 文件从仓库抓取到本地。
```
$ docker image pull library/hello-world
```
上面代码中,`docker image pull` 是抓取 image 文件的命令。`library/hello-world` 是 image 文件在仓库里面的位置,其中 `library` 是 image 文件所在的组,`hello-world` 是 image 文件的名字。
由于 Docker 官方提供的 image 文件,都放在 `library` 组里面,所以它的是默认组,可以省略。因此,上面的命令可以写成下面这样。
```
$ docker image pull hello-world
```
抓取成功以后,就可以在本机看到这个 image 文件了。
```
$ docker image ls
```
现在,运行这个 image 文件。
```
$ docker container run hello-world
```
**`docker container run` 命令会从 image 文件,生成一个正在运行的容器实例。**
注意,**`docker container run` 命令具有自动抓取 image 文件的功能。如果发现本地没有指定的 image 文件,就会从仓库自动抓取**。因此,前面的 `docker image pull` 命令并不是必需的步骤。
如果运行成功,你会在屏幕上读到下面的输出:
```
$ docker container run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
... ...
```
输出这段提示以后,`hello world` 就会停止运行,容器自动终止。
有些容器不会自动终止,因为提供的是服务。比如,安装运行 Ubuntu 的 image就可以在命令行体验 Ubuntu 系统。
```
$ docker container run -it ubuntu bash
```
对于那些不会自动终止的容器,必须使用 `docker container kill` 命令手动终止。
```
$ docker container kill [containID]
```
## .8 容器文件
**image 文件生成的容器实例,本身也是一个文件,称为 `容器文件`**。也就是说,一旦容器生成,就会同时存在两个文件: image 文件和容器文件。而且**关闭容器并不会删除容器文件**,只是容器停止运行而已。
```
# 列出本机正在运行的容器
$ docker container ls
# 列出本机所有容器,包括终止运行的容器
$ docker container ls --all
```
上面命令的输出结果之中,包括容器的 `ID`。很多地方都需要提供这个 `ID`,比如上一节终止容器运行的 `docker container kill`命令。
终止运行的容器文件,依然会占据硬盘空间,可以使用 `docker container rm` 命令删除。
```
$ docker container rm [containerID]
```
运行上面的命令之后,再使用 `docker container ls --all` 命令,就会发现被删除的容器文件已经消失了。
## .9 Dockerfile 文件
学会使用 image 文件以后,接下来的问题就是,如何可以生成 image 文件?如果你要推广自己的软件,势必要自己制作 image 文件。
这就需要用到 `Dockerfile` 文件。**它是一个文本文件,用来配置 image。Docker 根据 该文件生成二进制的 image 文件**。
下面通过一个实例,演示如何编写 `Dockerfile` 文件。
## .10 实例:制作自己的 Docker 容器
下面我以 [koa-demos](http://www.ruanyifeng.com/blog/2017/08/koa.html) 项目为例,介绍怎么写 `Dockerfile` 文件,实现让用户在 Docker 容器里面运行 Koa 框架。
作为准备工作,请先 [下载源码](https://github.com/ruanyf/koa-demos/archive/master.zip):
```
$ git clone https://github.com/ruanyf/koa-demos.git
$ cd koa-demos
```
### .10.1 编写 Dockerfile 文件
首先,在项目的根目录下,新建一个文本文件 `.dockerignore`,写入下面的内容。
```git
.git
node_modules
npm-debug.log
```
上面代码表示,这三个路径要排除,不要打包进入 image 文件。如果你没有路径要排除,这个文件可以不新建。
然后,在项目的根目录下,新建一个文本文件 `Dockerfile`,写入下面的内容。
```
FROM node:8.4
COPY . /app
WORKDIR /app
RUN npm install --registry=https://registry.npm.taobao.org
EXPOSE 3000
```
上面代码一共五行,含义如下。
* `FROM node:8.4` :该 image 文件继承官方的 node image冒号表示标签这里标签是8.4,即 8.4 版本的 node。
* `COPY . /app` :将当前目录下的所有文件(除了 `.dockerignore` 排除的路径),都拷贝进入 image 文件的 `/app`目录。
* `WORKDIR /app` :指定接下来的工作路径为 `/app`
* `RUN npm install`:在 `/app` 目录下,运行 `npm install` 命令安装依赖。注意,安装后所有的依赖,都将打包进入 image 文件。
* `EXPOSE 3000` :将容器 3000 端口暴露出来, 允许外部连接这个端口。
### .10.2 创建 image 文件
有了 `Dockerfile` 文件以后,就可以使用 `docker image build` 命令创建 image 文件了。
```
$ docker image build -t koa-demo .
# 或者
$ docker image build -t koa-demo:0.0.1 .
```
上面代码中,
* `-t` 参数用来指定 image 文件的名字,后面还可以用冒号指定标签。如果不指定,默认的标签就是 `latest`
* 最后的那个点表示 `Dockerfile` 文件所在的路径,**上例是当前路径,所以是一个点**。
如果运行成功,就可以看到新生成的 image 文件 `koa-demo` 了。
```
$ docker image ls
```
### .10.3 生成容器
`docker container run` 命令会从 image 文件生成容器。
```
$ docker container run -p 8000:3000 -it koa-demo /bin/bash
# 或者
$ docker container run -p 8000:3000 -it koa-demo:0.0.1 /bin/bash
```
上面命令的各个参数含义如下:
* `-p` 参数:容器的 3000 端口映射到本机的 8000 端口。
* `-it` 参数:容器的 Shell 映射到当前的 Shell然后你在本机窗口输入的命令就会传入容器。
* `koa-demo:0.0.1`image 文件的名字(如果有标签,还需要提供标签,默认是 `latest` 标签)。
* `/bin/bash`:容器启动以后,内部第一个执行的命令。这里是启动 Bash保证用户可以使用 Shell。
如果一切正常,运行上面的命令以后,就会返回一个命令行提示符。
```
root@66d80f4aaf1e:/app#
```
这表示你已经在容器里面了,**返回的提示符就是容器内部的 Shell 提示符**。执行下面的命令:
```
root@66d80f4aaf1e:/app# node demos/01.js
```
这时Koa 框架已经运行起来了。打开本机的浏览器,访问 `http://127.0.0.1:8000`,网页显示 "Not Found" ,这是因为这个 [demo](https://github.com/ruanyf/koa-demos/blob/master/demos/01.js) 没有写路由。
这个例子中Node 进程运行在 Docker 容器的虚拟环境里面,进程接触到的文件系统和网络接口都是虚拟的,与本机的文件系统和网络接口是隔离的,因此**需要定义容器与物理机的端口映射map**。
现在,在容器的命令行,按下 `Ctrl + c`(Windows 环境中) 停止 Node 进程,然后按下 `Ctrl + d` (或者输入 `exit` )退出容器。此外,也可以用 `docker container kill` 终止容器运行。
```
# 在本机的另一个终端窗口,查出容器的 ID
$ docker container ls
# 停止指定的容器运行
$ docker container kill [containerID]
```
容器停止运行之后,并不会消失,用下面的命令删除容器文件。
```
# 查出容器的 ID
$ docker container ls --all
# 删除指定的容器文件
$ docker container rm [containerID]
```
也可以使用 `docker container run` 命令的 `--rm` 参数,**在容器终止运行后自动删除容器文件**。
```
$ docker container run --rm -p 8000:3000 -it koa-demo /bin/bash
```
### .10.4 CMD 命令
上一节的例子里面,容器启动以后,需要手动输入命令 `node demos/01.js` 。我们可以把这个命令写在 `Dockerfile` 里面,这样容器启动以后,这个命令就已经执行了,不用再手动输入了。
```
FROM node:8.4
COPY . /app
WORKDIR /app
RUN npm install --registry=https://registry.npm.taobao.org
EXPOSE 3000
CMD node demos/01.js
```
上面的 Dockerfile 里面,多了最后一行 `CMD node demos/01.js`,它表示容器启动后自动执行 `node demos/01.js`
你可能会问,`RUN` 命令与 `CMD` 命令的区别在哪里?简单说:
* `RUN` 命令在 image 文件的构建阶段执行,执行结果都会打包进入 image 文件;`CMD` 命令则是在容器启动后执行。
* 另外,一个 Dockerfile 可以包含多个 `RUN` 命令,但是只能有一个 `CMD` 命令。
注意,指定了 `CMD` 命令以后,`docker container run` 命令就不能附加命令了(比如前面的 `/bin/bash`),否则它会覆盖 `CMD`命令。现在,启动容器可以使用下面的命令。
```
$ docker container run --rm -p 8000:3000 -it koa-demo:0.0.1
```
### .10.5 发布 image 文件
**容器运行成功后,就确认了 image 文件的有效性**。这时,我们就可以考虑把 image 文件分享到网上,让其他人使用。
首先,去 [`hub.docker.com`](https://hub.docker.com/) 或 `cloud.docker.com` 注册一个账户。然后,用下面的命令登录。
```
$ docker login
```
接着,为本地的 image 标注用户名和版本。
```
$ docker image tag [imageName] [username]/[repository]:[tag]
# 实例
$ docker image tag koa-demos:0.0.1 ruanyf/koa-demos:0.0.1
```
也可以不标注用户名,重新构建一下 image 文件。
```
$ docker image build -t [username]/[repository]:[tag] .
```
最后,发布 image 文件。
```
$ docker image push [username]/[repository]:[tag]
```
发布成功以后,登录 `hub.docker.com`,就可以看到已经发布的 image 文件。
## .11 其他有用的命令
docker 的主要用法就是上面这些,此外还有几个命令,也非常有用。
### .11.1 `docker container start`
前面的 `docker container run` 命令是**新建容器,每运行一次,就会新建一个容器**。同样的命令运行两次,就会生成两个一模一样的容器文件。如果希望重复使用容器,就要使用 `docker container start` 命令,它用来**启动已经生成、已经停止运行的容器文件**。
```
$ docker container start [containerID]
```
### .11.2 `docker container stop`
前面的 `docker container kill` 命令终止容器运行,相当于向容器里面的主进程发出 `SIGKILL` 信号。而 `docker container stop` 命令也是用来终止容器运行,相当于向容器里面的主进程发出 `SIGTERM` 信号,然后过一段时间再发出 `SIGKILL` 信号。
```
$ bash container stop [containerID]
```
这两个信号的差别是,
* 应用程序收到 `SIGTERM` 信号以后,可以自行进行收尾清理工作,但也可以不理会这个信号。
* 如果收到 `SIGKILL` 信号,就会强行立即终止,那些正在进行中的操作会全部丢失。
### .11.3 `docker container logs`
`docker container logs` 命令用来查看 docker 容器的输出,即容器里面 `Shell` 的标准输出。如果 `docker run` 命令运行容器的时候,没有使用 `-it` 参数,就要用这个命令查看输出。
```
$ docker container logs [containerID]
```
### .11.4 `docker container exec`
`docker container exec` 命令用于**进入一个正在运行的 docker 容器**。如果 `docker run` 命令运行容器的时候,没有使用 `-it` 参数,就要用这个命令进入容器。一旦进入了容器,就可以在容器的 Shell 执行命令了。
```
$ docker container exec -it [containerID] /bin/bash
```
### .11.5 `docker container cp`
`docker container cp` 命令用于**从正在运行的 Docker 容器里面,将文件拷贝到本机**。下面是拷贝到当前目录的写法。
```
$ docker container cp [containID]:[/path/to/file] .
```
非常感谢你一直读到了这里,这个系列还有[下一篇——《Docker 微服务教程》](http://www.ruanyifeng.com/blog/2018/02/docker-wordpress-tutorial.html)。

View File

@@ -0,0 +1,423 @@
[原文《如何使用Docker部署Go Web应用》——李文周](https://www.liwenzhou.com/posts/Go/how_to_deploy_go_app_using_docker/)
其他相关参考:
[《Docker 入门教程》——阮一峰](http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html)
> 建议先看 [阮一峰 老师的入门教程](http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html),先对 Docker 有一个基本了解,然后再来看本篇文章。
---
本文介绍了如何使用 Docker 以及 Docker Compose 部署我们的 Go Web 程序。
## .1 为什么需要Docker
使用 Docker 的主要目标是 **容器化**。也就是**为你的应用程序提供一致的环境,而不依赖于它运行的主机**。
想象一下你是否也会遇到下面这个场景:你在本地开发了你的应用程序,它很可能有很多的依赖环境或包,甚至对依赖的具体版本都有严格的要求,当开发过程完成后,你希望将应用程序部署到 web 服务器。这个时候你必须确保所有依赖项都安装正确并且版本也完全相同,否则应用程序可能会崩溃并无法运行。如果你想在另一个 web 服务器上也部署该应用程序,那么你必须从头开始重复这个过程。这种场景就是 Docker 发挥作用的地方。
对于运行我们应用程序的主机,不管是笔记本电脑还是 web 服务器,我们唯一需要做的就是运行一个 docker 容器平台。然后,你就不需要担心你使用的是 MacOSUbuntuArch 还是其他。你只需定义一次应用,即可随时随地运行。
## .2 Docker 部署示例
### .2.1 准备代码
这里我先用一段使用 `net/http` 库编写的简单代码为例讲解如何使用 Docker 进行部署,后面再讲解稍微复杂一点的项目部署案例。
```go
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", hello)
server := &http.Server{
Addr: ":8888",
}
fmt.Println("server startup...")
if err := server.ListenAndServe(); err != nil {
fmt.Printf("server startup failed, err:%v\n", err)
}
}
func hello(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte("hello liwenzhou.com!"))
}
```
上面的代码通过 `8888` 端口对外提供服务,返回一个字符串响应:`hello liwenzhou.com!`
### .2.2 创建 Docker 镜像
`镜像image` 包含运行应用程序所需的所有东西——**代码或二进制文件、运行时、依赖项以及所需的任何其他文件系统对象**。
或者简单地说,**`镜像image`是定义应用程序及其运行所需的一切**。
### .2.3 编写 Dockerfile
要创建 `Docker镜像image`必须在配置文件中指定步骤。这个文件默认我们通常称之为`Dockerfile`。(虽然这个文件名可以随意命名它,但最好还是使用默认的 Dockerfile。
#### .2.3.1 编写 Dockerfile
现在我们开始编写 `Dockerfile`,某些步骤不是唯一的,可以根据自己的需要修改诸如文件路径、最终可执行文件的名称等。具体内容如下:
```
FROM golang:alpine
# 为我们的镜像设置必要的环境变量
ENV GO111MODULE=on \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64
# 移动到工作目录:/build
WORKDIR /build
# 将代码复制到容器中
COPY . .
# 将我们的代码编译成二进制可执行文件app
RUN go build -o app .
# 移动到用于存放生成的二进制文件的 /dist 目录
WORKDIR /dist
# 将二进制文件从 /build 目录复制到这里
RUN cp /build/app .
# 声明服务端口(暴露接口供外部访问)
EXPOSE 8888
# 启动容器时运行的命令
CMD ["/dist/app"]
```
#### .2.3.2 Dockerfile 解析
* **From** 我们正在使用基础镜像 `golang:alpine` 来创建我们的镜像。这和我们要创建的镜像一样是一个我们能够访问的存储在 Docker 仓库的基础镜像。这个镜像运行的是 `alpine Linux`发行版,该发行版的大小很小并且内置了 Go非常适合我们的用例。有大量公开可用的 Docker 镜像,具体请查看 [`https://hub.docker.com/_/golang`](https://hub.docker.com/_/golang)
* **Env**:用来设置我们编译阶段需要用的环境变量。
* `WORKDIR`: 用来指定/切换工作目录
* `COPY` : 复制内容
* `RUN`: 运行某项内容,运行结果会被打包进 image 中 (可以执行多个 RUN
* `EXPOSE `: 声明对外暴露的接口。我们的应用程序监听该端口并通过该端口对外提供服务。
* `CMD`: 指定容器启动成功之后立即执行的终端命令。(只能指定一个 CMD
### .2.4 构建镜像
在项目目录下,执行下面的命令创建镜像,并指定镜像名称为 `goweb_app`
```
docker build . -t goweb_app
```
等待构建过程结束,输出如下提示:
```
...
Successfully built 90d9283286b7
Successfully tagged goweb_app:latest
```
现在我们已经准备好了镜像,但是目前它什么也没做。我们接下来要做的是运行我们的镜像,以便它能够处理我们的请求。**运行中的镜像称为容器**。
执行下面的命令来运行镜像:
```
docker run -p 8888:8888 goweb_app
```
标志位 `-p` 用来定义端口绑定。由于容器中的应用程序在端口 8888 上运行,我们将其绑定到主机端口也是 8888 。如果要绑定到另一个端口,则可以使用 **`-p $HOST_PORT:8888`**。例如 `-p 5000:8888`
现在就可以测试下我们的 web 程序是否工作正常,打开浏览器输入 `http://127.0.0.1:8888` 就能看到我们事先定义的响应内容如下:
```
hello liwenzhou.com!
```
## .3 分阶段构建示例
我们的 Go 程序编译之后会得到一个可执行的二进制文件,其实在最终的镜像中是不需要 go 编译器的,也就是说我们只需要一个运行最终二进制文件的容器即可。
`Docker` 的最佳实践之一是通过仅保留二进制文件来减小镜像大小,为此,我们将使用一种称为**多阶段构建的技术**,这意味着我们将通过多个步骤构建镜像。
```
FROM golang:alpine AS builder
# 为我们的镜像设置必要的环境变量
ENV GO111MODULE=on \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64
# 移动到工作目录:/build
WORKDIR /build
# 将代码复制到容器中
COPY . .
# 将我们的代码编译成二进制可执行文件 app
RUN go build -o app .
###################
# 接下来创建一个小镜像
###################
FROM scratch
# 从 builder 镜像中把/dist/app 拷贝到当前目录
COPY --from=builder /build/app /
# 需要运行的命令
ENTRYPOINT ["/app"]
```
使用这种技术,我们剥离了使用 `golang:alpine` 作为编译镜像来编译得到二进制可执行文件的过程,并基于 `scratch` 生成一个简单的、非常小的新镜像。我们将二进制文件从命名为 builder的第一个镜像中复制到新创建的 `scratch` 镜像中。有关 scratch 镜像的更多信息,请查看 [https://hub.docker.com/_/scratch](https://hub.docker.com/_/scratch)
## .4 附带其他文件的部署示例
这里以我之前《Go Web视频教程》中的小清单项目为例项目的 Github 仓库地址为:[https://github.com/Q1mi/bubble](https://github.com/Q1mi/bubble)。
如果项目中带有静态文件或配置文件,需要将其拷贝到最终的镜像文件中。
我们的 bubble 项目用到了静态文件和配置文件,具体目录结构如下:
```
bubble
├── README.md
├── bubble
├── conf
│ └── config.ini
├── controller
│ └── controller.go
├── dao
│ └── mysql.go
├── example.png
├── go.mod
├── go.sum
├── main.go
├── models
│ └── todo.go
├── routers
│ └── routers.go
├── setting
│ └── setting.go
├── static
│ ├── css
│ │ ├── app.8eeeaf31.css
│ │ └── chunk-vendors.57db8905.css
│ ├── fonts
│ │ ├── element-icons.535877f5.woff
│ │ └── element-icons.732389de.ttf
│ └── js
│ ├── app.007f9690.js
│ └── chunk-vendors.ddcb6f91.js
└── templates
├── favicon.ico
└── index.html
```
我们需要将 `templates``static``conf` 三个文件夹中的内容拷贝到最终的镜像文件中。更新后的 `Dockerfile` 如下
```
FROM golang:alpine AS builder
# 为我们的镜像设置必要的环境变量
ENV GO111MODULE=on \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64
# 移动到工作目录:/build
WORKDIR /build
# 复制项目中的 go.mod 和 go.sum 文件并下载依赖信息
COPY go.mod .
COPY go.sum .
RUN go mod download
# 将代码复制到容器中
COPY . .
# 将我们的代码编译成二进制可执行文件 bubble
RUN go build -o bubble .
###################
# 接下来创建一个小镜像
###################
FROM scratch
COPY ./templates /templates
COPY ./static /static
COPY ./conf /conf
# 从builder镜像中把 /dist/app 拷贝到当前目录
COPY --from=builder /build/bubble /
# 需要运行的命令
ENTRYPOINT ["/bubble", "conf/config.ini"]
```
简单来说就是多了几步 `COPY` 的步骤,大家看一下 `Dockerfile` 中的注释即可。
Tips 这里把 `COPY` 静态文件的步骤放在上层,把 `COPY` 二进制可执行文件放在下层,争取多使用缓存。
### .4.1 关联其他容器
又因为我们的项目中使用了 `MySQL`,我们可以选择使用如下命令启动一个 `MySQL` 容器,它的别名为 `mysql8019``root` 用户的密码为 `root1234`;挂载容器中的 `/var/lib/mysql` 到本地的 `/Users/q1mi/docker/mysql` 目录;内部服务端口为 `3306`,映射到外部的 `13306` 端口。
```
docker run --name mysql8019 -p 13306:3306 -e MYSQL_ROOT_PASSWORD=root1234 -v /Users/q1mi/docker/mysql:/var/lib/mysql -d mysql:8.0.19
```
这里需要修改一下我们程序中配置的 `MySQL``host` 地址为容器别名,使它们在内部通过别名(此处为 `mysql8019`)联通。
```
[mysql]
user = root
password = root1234
host = mysql8019
port = 3306
db = bubble
```
修改后记得重新构建 `bubble_app` 镜像:
```
docker build . -t bubble_app
```
我们这里运行 `bubble_app` 容器的时候需要使用 `--link` 的方式与上面的 `mysql8019` 容器关联起来,具体命令如下:
```
docker run --link=mysql8019:mysql8019 -p 8888:8888 bubble_app
```
## .5 Docker Compose 模式
除了像上面一样使用 `--link` 的方式来关联两个容器之外,我们还可以使用 `Docker Compose` 来定义和运行多个容器。
`Compose` 是用于定义和运行多容器 `Docker` 应用程序的工具。通过 `Compose`,你可以使用 `YML` 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 `YML` 文件配置中创建并启动所有服务。
使用 `Compose` 基本上是一个三步过程:
* 使用 `Dockerfile` 定义你的应用环境以便可以在任何地方复制。
* 定义组成应用程序的服务,`docker-compose.yml` 以便它们可以在隔离的环境中一起运行。
* 执行 `docker-compose up` 命令来启动并运行整个应用程序。
我们的项目需要两个容器分别运行 `mysql``bubble_app` ,我们编写的 `docker-compose.yml` 文件内容如下:
```yaml
# yaml 配置
version: "3.7"
services:
mysql8019:
image: "mysql:8.0.19"
ports:
- "33061:3306"
command: "--default-authentication-plugin=mysql_native_password --init-file /data/application/init.sql"
environment:
MYSQL_ROOT_PASSWORD: "root1234"
MYSQL_DATABASE: "bubble"
MYSQL_PASSWORD: "root1234"
volumes:
- ./init.sql:/data/application/init.sql
bubble_app:
build: .
command: sh -c "./wait-for.sh mysql8019:3306 -- ./bubble ./conf/config.ini"
depends_on:
- mysql8019
ports:
- "8888:8888"
```
这个 `Compose` 文件定义了两个服务:`bubble_app``mysql8019`。其中:
* `bubble_app`: 使用当前目录下的 `Dockerfile` 文件构建镜像,并通过 `depends_on` 指定依赖 `mysql8019` 服务,声明服务端口 8888 并绑定对外 8888 端口。
* `mysql8019`: mysql8019 服务使用 `Docker Hub` 的公共 `mysql:8.0.19` 镜像,内部端口 3306外部端口 33061。
这里需要注意一个问题就是,我们的 `bubble_app` 容器需要等待 mysql8019 容器正常启动之后再尝试启动,因为我们的 web 程序在启动的时候会初始化 MySQL 连接。这里共有两个地方要更改,第一个就是我们 Dockerfile 中要把最后一句注释掉:
```
# Dockerfile
...
# 需要运行的命令(注释掉这一句,因为需要等 MySQL 启动之后再启动我们的 Web 程序)
# ENTRYPOINT ["/bubble", "conf/config.ini"]
```
第二个地方是在 `bubble_app` 下面添加如下命令,使用提前编写的 `wait-for.sh` 脚本检测 `mysql8019:3306` 正常后再执行后续启动 Web 应用程序的命令:
```
command: sh -c "./wait-for.sh mysql8019:3306 -- ./bubble ./conf/config.ini"
```
当然,**因为我们现在要在 `bubble_app` 镜像中执行 `sh` 命令,所以不能再使用 `scratch` 镜像构建了,这里改为使用 `debian:stretch-slim`,同时还要安装 `wait-for.sh` 脚本用到的`netcat`,最后不要忘了把 `wait-for.sh` 脚本文件 `COPY` 到最终的镜像中,并赋予可执行权限**。更新后的 `Dockerfile` 内容如下:
```
FROM golang:alpine AS builder
# 为我们的镜像设置必要的环境变量
ENV GO111MODULE=on \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64
# 移动到工作目录:/build
WORKDIR /build
# 复制项目中的 go.mod 和 go.sum文件并下载依赖信息
COPY go.mod .
COPY go.sum .
RUN go mod download
# 将代码复制到容器中
COPY . .
# 将我们的代码编译成二进制可执行文件 bubble
RUN go build -o bubble .
###################
# 接下来创建一个小镜像
###################
FROM debian:stretch-slim
COPY ./wait-for.sh /
COPY ./templates /templates
COPY ./static /static
COPY ./conf /conf
# 从builder镜像中把/dist/app 拷贝到当前目录
COPY --from=builder /build/bubble /
RUN set -eux; \
apt-get update; \
apt-get install -y \
--no-install-recommends \
netcat; \
chmod 755 wait-for.sh
# 需要运行的命令
# ENTRYPOINT ["/bubble", "conf/config.ini"]
```
所有的条件都准备就绪后,就可以执行下面的命令跑起来了:
```
docker-compose up
```
完整版代码示例,请查看我的 github 仓库:[https://github.com/Q1mi/deploy_bubble_using_docker](https://github.com/Q1mi/deploy_bubble_using_docker)。
## .6 总结
使用 Docker 容器能够极大简化我们在配置依赖环境方面的操作,但同时也对我们的技术储备提了更高的要求。对于 Docker 不管你是熟悉抑或是不熟悉,技术发展的车轮都滚滚向前。
参考链接:
* [https://levelup.gitconnected.com/complete-guide-to-create-docker-container-for-your-golang-application-80f3fb59a15e](https://levelup.gitconnected.com/complete-guide-to-create-docker-container-for-your-golang-application-80f3fb59a15e)

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

View File

@@ -0,0 +1,724 @@
[原文: 《etcd 快速入门》](https://zhuanlan.zhihu.com/p/96428375?from_voters_page=true)
## .1 认识etcd
### .1.1 etcd 概念
从哪里说起呢?官网第一个页面,有那么一句话:"A distributed, reliable key-value store for the most critical data of a distributed system"。也就是说 **`etcd` 是一个分布式、可靠 `key-value` 存储的分布式系统**。当然,**它不仅仅用于存储,还提供共享配置及服务发现**。
### .1.2 etcd vs Zookeeper
提供配置共享和服务发现的系统比较多,其中最为大家熟知的是 `Zookeeper`,而 etcd 可以算得上是后起之秀了。在项目实现、一致性协议易理解性、运维、安全等多个维度上,`etcd` 相比 `zookeeper` 都占据优势。
本文选取 `Zookeeper` 作为典型代表与 `etcd` 进行比较,而不考虑 `Consul` 项目作为比较对象,因为 `Consul` 的可靠性和稳定性还需要时间来验证(项目发起方自身服务并未使用 Consul自己都不用)。
* 一致性协议: `etcd` 使用 `Raft` 协议,`Zookeeper` 使用 `ZAB`(类 `PAXOS` 协议),前者容易理解,方便工程实现;
* 运维方面:`etcd` 方便运维,`Zookeeper` 难以运维;
* 数据存储:`etcd` 多版本并发控制(`MVCC`)数据模型 支持查询先前版本的键值对
* 项目活跃度:`etcd` 社区与开发活跃Zookeeper 感觉已经快死了;
* API`etcd` 提供 `HTTP+JSON`, `gRPC 接口`,跨平台跨语言;`Zookeeper` 需要使用其客户端;
* 访问安全方面:`etcd` 支持 `HTTPS` 访问,`Zookeeper` 在这方面缺失;
...
### .1.3 etcd 应用场景
`etcd` 比较多的应用场景是用于服务发现。
**服务发现 (Service Discovery)** 要解决的是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务如何才能找到对方并建立连接。和 `Zookeeper` 类似,`etcd` 有很多使用场景,包括:
* 配置管理
* 服务注册发现
* 选主
* 应用调度
* 分布式队列
* 分布式锁
### .1.4 etcd 工作原理
#### .1.4.1 如何保证一致性
`etcd` 使用 `raft` 协议来维护集群内各个节点状态的一致性。简单说,`etcd` 集群是一个分布式系统,由多个节点相互通信构成整体对外服务,每个节点都存储了完整的数据,并且通过 `Raft` 协议保证每个节点维护的数据是一致的。
每个 `etcd` 节点都维护了一个状态机,并且,任意时刻至多存在一个有效的主节点。主节点处理所有来自客户端写操作,通过 `Raft` 协议保证写操作对状态机的改动会可靠的同步到其他节点。
#### .1.4.2 数据模型
`etcd` 的设计目标是用来**存放非频繁更新的数据**,提供可靠的 `Watch` 插件,它暴露了键值对的历史版本,以支持低成本的快照、监控历史事件。这些设计目标要求它使用一个持久化的、多版本的、支持并发的数据数据模型。
`etcd` 键值对的新版本保存后,先前的版本依然存在。从效果上来说,键值对是不可变的,`etcd` 不会对其进行 `in-place` 的更新操作,而总是生成一个新的数据结构。为了防止历史版本无限增加,`etcd` 的存储支持压缩Compact以及删除老旧版本。
##### .1.4.2.1 逻辑视图
从逻辑角度看,**`etcd` 的存储是一个扁平的二进制键空间,键空间有一个针对键(字节字符串)的词典序索引**,因此范围查询的成本较低。
键空间维护了多个修订版本Revisions每一个原子变动操作一个事务可由多个子操作组成都会产生一个新的修订版本。在集群的整个生命周期中修订版都是单调递增的。修订版同样支持索引因此基于修订版的范围扫描也是高效的。压缩操作需要指定一个修订版本号小于它的修订版会被移除。
一个键的一次生命周期(从创建到删除)叫做 **`代 (Generation)`**每个键可以有多个代。创建一个键时会增加键的版本version如果在当前修订版中键不存在则版本设置为1。删除一个键会创建一个**`墓碑Tombstone`**将版本设置为0结束当前代。每次对键的值进行修改都会增加其版本号 — 在同一代中版本号是单调递增的。
当压缩时,任何在压缩修订版之前结束的代,都会被移除。值在修订版之前的修改记录(仅仅保留最后一个)都会被移除。
##### .1.4.2.2 物理视图
`etcd` 将数据存放在一个持久化的 B+ 树中出于效率的考虑每个修订版仅仅存储相对前一个修订版的数据状态变化Delta。单个修订版中可能包含了 B+ 树中的多个键。
键值对的键是三元组majorsubtype
* major存储键值的修订版
* sub用于区分相同修订版中的不同键
* type用于特殊值的可选后缀例如 t 表示值包含墓碑
键值对的值,包含从上一个修订版的 Delta。B+ 树 —— 键的词法字节序排列,基于修订版的范围扫描速度快,可以方便的从一个修改版到另外一个的值变更情况查找。
`etcd` 同时在内存中维护了一个 B 树索引,用于加速针对键的范围扫描。索引的键是物理存储的键面向用户的映射,索引的值则是指向 B+ 树修该点的指针。
### .1.5 etcd 读写性能
按照官网给出的数据, 在 2CPU1.8G 内存SSD 磁盘这样的配置下,单节点的写性能可以达到 16K QPS, 而先写后读也能达到 12K QPS。这个性能还是相当可观。
### .1.6 etcd 术语
![](pics/etcd快速入门1.jpg)
## .2 安装和运行
### .2.1 构建
需要 Go 1.9 以上版本:
```
cd $GOPATH/src
mkdir go.etcd.io && cd go.etcd.io
git clone https://github.com/etcd-io/etcd.git
cd etcd
./build
```
使用 `build` 脚本构建会在当前项目的 `bin` 目录生产 `etcd``etcdctl` 可执行程序。
* `etcd` 就是 `etcd server` 了,
* `etcdctl` 主要为 `etcd server` 提供了命令行操作。
### .2.2 静态集群
如果 Etcd 集群成员是已知的,具有固定的 IP 地址,则可以静态的初始化一个集群。
每个节点都可以使用如下环境变量:
```
ETCD_INITIAL_CLUSTER="radon=http://10.0.2.1:2380,neon=http://10.0.3.1:2380"
ETCD_INITIAL_CLUSTER_STATE=new
```
或者如下命令行参数
```
--initial-cluster radon=http://10.0.2.1:2380,neon=http://10.0.3.1:2380
--initial-cluster-state new
```
来指定集群成员。
### .2.3 初始化集群
完整的命令行示例:
```
etcd --name radon --initial-advertise-peer-urls http://10.0.2.1:2380
--listen-peer-urls http://10.0.2.1:2380
--listen-client-urls http://10.0.2.1:2379,http://127.0.0.1:2379
--advertise-client-urls http://10.0.2.1:2380
# 所有以-initial-cluster开头的选项在第一次运行Bootstrap后都被忽略
--initial-cluster-token etcd.gmem.cc
--initial-cluster radon=http://10.0.2.1:2380,neon=http://10.0.3.1:2380
--initial-cluster-state new
```
### .2.4 使用TLS
Etcd 支持基于 TLS 加密的集群内部、客户端-集群通信。每个集群节点都应该拥有被共享 CA 签名的证书:
```
# 密钥对、证书签名请求
openssl genrsa -out radon.key 2048
export SAN_CFG=$(printf "\n[SAN]\nsubjectAltName=IP:127.0.0.1,IP:10.0.2.1,DNS:radon.gmem.cc")
openssl req -new -sha256 -key radon.key -out radon.csr \
-subj "/C=CN/ST=BeiJing/O=Gmem Studio/CN=Server Radon" \
-reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(echo $SAN_CFG))
# 执行签名
openssl x509 -req -sha256 -in radon.csr -out radon.crt -CA ../ca.crt -CAkey ../ca.key -CAcreateserial -days 3650 \
-extensions SAN -extfile <(echo "${SAN_CFG}")
```
初始化集群命令需要修改为:
```
etcd --name radon --initial-advertise-peer-urls https://10.0.2.1:2380
--listen-peer-urls https://10.0.2.1:2380
--listen-client-urls https://10.0.2.1:2379,https://127.0.0.1:2379
--advertise-client-urls https://10.0.2.1:2380
# 所有以-initial-cluster开头的选项在第一次运行Bootstrap后都被忽略
--initial-cluster-token etcd.gmem.cc
--initial-cluster radon=https://10.0.2.1:2380,neon=https://10.0.3.1:2380 # 指定集群成员列表
--initial-cluster-state new # 初始化新集群时使用
--initial-cluster-state existing # 加入已有集群时使用
# 客户端TLS相关参数
--client-cert-auth
--trusted-ca-file=/usr/share/ca-certificates/GmemCA.crt
--cert-file=/opt/etcd/cert/radon.crt
--key-file=/opt/etcd/cert/radon.key
# 集群内部TLS相关参数
--peer-client-cert-auth
--peer-trusted-ca-file=/usr/share/ca-certificates/GmemCA.crt
--peer-cert-file=/opt/etcd/cert/radon.crt
--peer-key-file=/opt/etcd/cert/radon.key
```
## .3 与 etcd 交互
etcd 提供了 `etcdctl 命令行工具` 和 `HTTP API` 两种交互方法。etcdctl 命令行工具用 go 语言编写,也是对 HTTP API 的封装,日常使用起来也更容易。所以这里我们主要使用 etcdctl 命令行工具演示。
### .3.1 put
应用程序通过 `put` 将 key 和 value 存储到 etcd 集群中。每个存储的密钥都通过 Raft 协议复制到所有 etcd 集群成员,以实现一致性和可靠性。
这里是设置键的值的命令 foo 到 bar
```
$ etcdctl put foo bar
OK
```
### .3.2 get
应用程序可以从一个 etcd 集群中读取 key 的值。
假设 etcd 集群已经存储了以下密钥:
```
foo = bar
foo1 = bar1
foo2 = bar2
foo3 = bar3
a = 123
b = 456
z = 789
```
* 读取键为 foo 的命令:
```
$ etcdctl get foo
foo // key
bar // value
```
上面同时返回了 key 和 value怎么只读取 key 对应的值呢:
```
$ etcdctl get foo --print-value-only
bar
```
* 以十六进制格式读取键为 foo 的命令:
```
$ etcdctl get foo --hex
\x66\x6f\x6f
\x62\x61\x72
```
* 查询可以读取单个 key ,也可以读取一系列 key
```
$ etcdctl get foo foo3
foo
bar
foo1
bar1
foo2
bar2
```
请注意foo3 由于范围超过了半开放区间间隔 `[foo, foo3)`,因此不包括在内 foo3。
* 按前缀读取:
```
$ etcdctl get --prefix foo
foo
bar
foo1
bar1
foo2
bar2
foo3
bar3
```
* 按结果数量限制读取
```
$ etcdctl get --limit=2 --prefix foo
foo
bar
foo1
bar1
```
* 读取大于或等于指定键的字节值的键:
```
$ etcdctl get --from-key b
b
456
z
789
```
应用程序可能希望通过访问早期版本的 key 来回滚到旧配置。由于对 etcd 集群键值存储区的每次修改都会增加一个 etcd 集群的全局修订版本,因此应用程序可以通过提供旧的 etcd 修订版来读取被取代的键。
假设一个 etcd 集群已经有以下 key
```
foo = bar # revision = 2
foo1 = bar1 # revision = 3
foo = bar_new # revision = 4
foo1 = bar1_new # revision = 5
```
以下是访问以前版本 key 的示例:
```
$ etcdctl get --prefix foo # 访问最新版本的key
foo
bar_new
foo1
bar1_new
$ etcdctl get --prefix --rev=4 foo # 访问第4个版本的key
foo
bar_new
foo1
bar1
$ etcdctl get --prefix --rev=3 foo # 访问第3个版本的key
foo
bar
foo1
bar1
$ etcdctl get --prefix --rev=2 foo # 访问第3个版本的key
foo
bar
$ etcdctl get --prefix --rev=1 foo # 访问第1个版本的key
```
### .3.3 del
应用程序可以从一个 etcd 集群中删除一个 key 或一系列 key。
假设一个 etcd 集群已经有以下key
```
foo = bar
foo1 = bar1
foo3 = bar3
zoo = val
zoo1 = val1
zoo2 = val2
a = 123
b = 456
z = 789
```
* 删除 key 为 foo 的命令:
```
$ etcdctl del foo
1
```
* 删除键值对的命令:
```
$ etcdctl del --prev-kv zoo
1
zoo
val
```
* 删除从 foo 到 foo9 的命令:
```
$ etcdctl del foo foo9
2
```
* 删除具有前缀的键的命令:
```
$ etcdctl del --prefix zoo
2
```
* 删除大于或等于键的字节值的键的命令:
```
$ etcdctl del --from-key b
2
```
### .3.4 watch
应用程序可以使用 watch 观察一个键或一系列键来监视任何更新。
打开第一个终端,监听 foo 的变化,我们输入如下命令:
```
$ etcdctl watch foo
```
再打开另外一个终端来对 foo 进行操作:
```
$ etcdctl put foo 123
OK
$ etcdctl put foo 456
OK
$ ./etcdctl del foo
1
```
第一个终端结果如下:
```
$ etcdctl watch foo
PUT
foo
123
PUT
foo
456
DELETE
foo
```
除了以上基本操作watch 也可以像 get、del 操作那样使用 prefix、rev、 hex 等参数,这里就不一一列举了。
### .3.5 lock
>`Distributed locks`: 分布式锁,一个人操作的时候,另外一个人只能看,不能操作
`lock` 可以通过指定的名字加锁。注意,只有当**正常退出且释放锁后lock 命令的退出码是 `0`否则这个锁会一直被占用直到过期默认60秒**
在第一个终端输入如下命令:
```
$ etcdctl lock mutex1
mutex1/326963a02758b52d
```
在第二个终端输入同样的命令:
```
$ etcdctl lock mutex1
```
从上可以发现第二个终端发生了阻塞,并未返回像 `mutex1/326963a02758b52d` 的字样。此时我们需要结束第一个终端的 lock ,可以使用 `Ctrl+C` 正常退出 lock 命令。第一个终端 lock 退出后,第二个终端的显示如下:
```
$ etcdctl lock mutex1
mutex1/694d6ee9ac069436
```
### .3.6 txn
`txn` 从标准输入中读取多个请求,将它们看做一个原子性的事务执行。
**`事务`**是由条件列表,条件判断成功时的执行列表(条件列表中全部条件为真表示成功)和条件判断失败时的执行列表(条件列表中有一个为假即为失败)组成的。
```
$ etcdctl put user frank
OK
$ ./etcdctl txn -i
compares:
value("user") = "frank"
success requests (get, put, del):
put result ok
failure requests (get, put, del):
put result failed
SUCCESS
OK
$ etcdctl get result
result
ok
```
解释如下:
* 先使用 `etcdctl put user frank` 设置 user 为 frank
* `etcdctl txn -i` 开启事务(`-i`表示交互模式)
* 第2步输入命令后回车终端显示出 `compares`
* 输入 `value("user") = "frank"`,此命令是比较 user 的值与 frank 是否相等
* 第 4 步完成后输入回车,终端会换行显示,此时可以继续输入判断条件(前面说过事务由条件列表组成),**再次输入回车表示判断条件输入完毕**
* 第 5 步连续输入两个回车后,终端显示出 `success requests (get, put, delete):`,表示下面输入判断条件为真时要执行的命令
* 与输入判断条件相同,连续两个回车表示成功时的执行列表输入完成
* 终端显示 `failure requests (get, put, delete)`后输入条件判断失败时的执行列表
为了看起来简洁,此实例中条件列表和执行列表只写了一行命令,实际可以输入多行。
总结上面的事务,要做的事情就是 user 为 frank 时设置 result 为 ok否则设置 result 为 failed
上述事务执行完成后查看 result 值为 ok。
### .3.7 compact
正如我们所提到的etcd 保持修改,以便应用程序可以读取以前版本的 key。但是为了避免累积无限的历史重要的是要压缩过去的修订版本。**压缩后etcd 删除历史版本,释放资源供将来使用**。**在压缩版本之前的所有被修改的数据都将不可用**(即无法访问压缩版本之前的数据)。
```
$ etcdctl compact 5
compacted revision 5
$ etcdctl get --rev=4 foo
Error: etcdserver: mvcc: required revision has been compacted
```
### .3.8 lease 与 TTL
etcd 也能为 key 设置超时时间,但与 redis 不同etcd 需要先创建 `lease`,然后 `put` 命令加上参数 `lease=` 来设置。
`lease` 又由生存时间TTL管理每个租约都有一个在授予时间由应用程序指定的最小生存时间TTL值。
以下是授予租约的命令:
```
$ etcdctl lease grant 30
lease 694d6ee9ac06945d granted with TTL(30s)
$ etcdctl put --lease=694d6ee9ac06945d foo bar
OK
```
以下是撤销同一租约的命令:
```
$ etcdctl lease revoke 694d6ee9ac06945d
lease 694d6ee9ac06945d revoked
$ etcdctl get foo
```
应用程序**可以通过刷新其 TTL 来保持租约活着,因此不会过期**。
假设我们完成了以下一系列操作:
```
$ etcdctl lease grant 10
lease 32695410dcc0ca06 granted with TTL(10s)
```
以下是保持同一租约有效的命令:
```
$ etcdctl lease keep-alive 32695410dcc0ca06
lease 32695410dcc0ca06 keepalived with TTL(10)
lease 32695410dcc0ca06 keepalived with TTL(10)
lease 32695410dcc0ca06 keepalived with TTL(10)
...
```
应用程序可能想要了解租赁信息,以便它们可以续订或检查租赁是否仍然存在或已过期。应用程序也可能想知道特定租约所附的 key。
假设我们完成了以下一系列操作:
```
$ etcdctl lease grant 200
lease 694d6ee9ac06946a granted with TTL(200s)
$ etcdctl put demo1 val1 --lease=694d6ee9ac06946a
OK
$ etcdctl put demo2 val2 --lease=694d6ee9ac06946a
OK
```
以下是获取有关租赁信息的命令:
```
$ etcdctl lease timetolive 694d6ee9ac06946a
lease 694d6ee9ac06946a granted with TTL(200s), remaining(178s)
```
以下是获取哪些 key 使用了租赁信息的命令:
```
$ etcdctl lease timetolive --keys 694d6ee9ac06946a
lease 694d6ee9ac06946a granted with TTL(200s), remaining(129s), attached keys([demo1 demo2])
```
## .4 服务发现实战
如果有一个**让系统可以动态调整集群大小的需求,那么首先就要支持服务发现**。就是说当一个新的节点启动时,可以将自己的信息注册到 `master`,让 `master` 把它加入到集群里,关闭之后也可以把自己从集群中删除。这个情况,其实就是一个 `membership protocol`,用来维护集群成员的信息。
整个代码的逻辑很简单,`worker` 启动时向 `etcd` 注册自己的信息,并设置一个带 TTL 的租约,每隔一段时间更新这个 TTL如果该 `worker` 挂掉了,这个 TTL 就会 `expire` 并删除相应的 key。发现服务监听` workers/` 这个 etcd directory根据检测到的不同 `action` 来增加,更新,或删除 `worker`。
首先我们要建立一个 `etcd client`
```go
func NewMaster(endpoints []string) *Master {
// etcd 配置
cfg := client.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
}
// 创建 etcd 客户端
etcdClient, err := client.New(cfg)
if err != nil {
log.Fatal("Error: cannot connect to etcd: ", err)
}
// 创建 master
master := &Master{
members: make(map[string]*Member),
API: etcdClient,
}
return master
}
```
这里我们先建立一个 `etcd client`,然后把它的 key API 放进 `master` 里面,这样我们以后只需要通过这个 API 来跟 etcd 进行交互。 `Endpoints` 是指 etcd 服务器们的地址,如 ”http://127.0.0.1:2379“ 等。
`go master.WatchWorkers()` 这一行启动一个 Goroutine 来监控节点的情况。下面是 WatchWorkers 的代码:
```
func (master *Master) WatchWorkers() {
// 创建 watcher channel
watcherCh := master.API.Watch(context.TODO(), "workers", client.WithPrefix())
// 从 chanel 取数据
for wresp := range watcherCh {
for _, ev := range wresp.Events {
key := string(ev.Kv.Key)
if ev.Type.String() == "PUT" { // put 方法
info := NodeToWorkerInfo(ev.Kv.Value)
if _, ok := master.members[key]; ok {
log.Println("Update worker ", info.Name)
master.UpdateWorker(key,info)
} else {
log.Println("Add worker ", info.Name)
master.AddWorker(key, info)
}
} else if ev.Type.String() == "DELETE" { // del 方法
log.Println("Delete worker ", key)
delete(master.members, key)
}
}
}
}
```
worker 这边也跟 master 类似,保存一个 etcd KeysAPI通过它与 etcd 交互,然后用 `heartbeat` 来保持自己的状态,在 `heartbeat` 定时创建租约如果租用失效master 将会收到 delete 事件。代码如下:
```
func NewWorker(name, IP string, endpoints []string) *Worker {
// etcd 配置
cfg := client.Config {
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
}
// 创建 etcd 客户端
etcdClient, err := client.New(cfg)
if err != nil {
log.Fatal("Error: cannot connect to etcd: ", err)
}
// 创建 worker
worker := &Worker {
Name: name,
IP: IP,
API: etcdClient,
}
return worker
}
func (worker *Worker) HeartBeat() {
for {
// worker info
info := &WorkerInfo{
Name: worker.Name,
IP: worker.IP,
CPU: runtime.NumCPU(),
}
key := "workers/" + worker.Name
value, _ := json.Marshal(info)
// 创建 lease
leaseResp, err := worker.API.Lease.Grant(context.TODO(), 10)
if err != nil {
log.Fatalf("设置租约时间失败:%s\n", err.Error())
}
// 创建 watcher channel
_, err = worker.API.Put(context.TODO(), key, string(value), client.WithLease(leaseResp.ID))
if err != nil {
log.Println("Error update workerInfo:", err)
}
time.Sleep(time.Second * 3)
}
}
```
启动的时候需要有多个 worker 节点(至少一个)和一个 master 节点,所以我们在启动程序的时候,可以传递一个 “role” 参数。代码如下:
```
var role = flag.String("role", "", "master | worker")
flag.Parse()
endpoints := []string{"http://127.0.0.1:2379"}
if *role == "master" {
master := discovery.NewMaster(endpoints)
master.WatchWorkers()
} else if *role == "worker" {
worker := discovery.NewWorker("localhost", "127.0.0.1", endpoints)
worker.HeartBeat()
} else {
...
}
```
项目地址: [https://github.com/chapin666/etcd-service-discovery](https://github.com/chapin666/etcd-service-discovery)
## .5 总结
* etcd **默认只保存 1000 个历史事件**,所以不适合有大量更新操作的场景,这样会导致数据的丢失。
* **etcd 典型的应用场景是配置管理和服务发现,这些场景都是读多写少的**。
* 相比于 `zookeeper``etcd` 使用起来要简单很多。不过要实现真正的服务发现功能,`etcd` 还需要和其他工具(比如 `registrator``confd` 等)一起使用来实现服务的自动注册和更新。
## .6 其他参考
* [《ETCD 简介 + 使用》——还没看](https://blog.csdn.net/bbwangj/article/details/82584988)
* [《Etcd 使用入门》——还没看](https://www.jianshu.com/p/f68028682192)

View File

@@ -0,0 +1,460 @@
[原文链接《go操作etcd》--李文周](https://www.liwenzhou.com/posts/Go/go_etcd/)
其他参考:
* [《etcd 快速入门》](https://zhuanlan.zhihu.com/p/96428375?from_voters_page=true)
---
[`etcd`](https://etcd.io/) 是近几年比较火热的一个开源的、分布式的键值对数据存储系统,提供共享配置、服务的注册和发现,本文主要介绍 etcd 的安装和使用。
## .1 etcd介绍
[etcd](https://etcd.io/) 是使用 Go 语言开发的一个**开源的、高可用的分布式 `key-value` 存储系统**,可以用于配置共享和服务的注册和发现。
类似项目有 `zookeeper``consul`
etcd 具有以下特点:
* 完全复制:集群中的每个节点都可以使用完整的存档
* 高可用性Etcd 可用于避免硬件的单点故障或网络问题
* 一致性:每次读取都会返回跨多主机的最新写入
* 简单:包括一个定义良好、面向用户的 APIgRPC
* 安全:实现了带有可选的客户端证书身份验证的自动化 TLS
* 快速:每秒 10000 次写入的基准速度
* 可靠:使用 Raft 算法实现了强一致、高可用的服务存储目录
## .2 etcd 应用场景
### .2.1 服务发现
服务发现要解决的也是分布式系统中最常见的问题之一,即在**同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接**。本质上来说,服务发现就是想要了解集群中是否有进程在监听 udp 或 tcp 端口,并且通过名字就可以查找和连接。
![](pics/etcd_01.png)
### .2.2 配置中心
将一些配置信息放到 etcd 上进行集中管理。
这类场景的使用方式通常是这样:应用在启动的时候主动从 etcd 获取一次配置信息,同时,在 etcd 节点上注册一个 Watcher 并等待以后每次配置有更新的时候etcd 都会实时通知订阅者,以此达到获取最新配置信息的目的。
### .2.3 分布式锁
因为 etcd 使用 Raft 算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易实现分布式锁。
**锁服务有两种使用方式,一是保持独占,二是控制时序。**
* `保持独占` : 即所有获取锁的用户最终只有一个可以得到。etcd 为此提供了一套实现**分布式锁原子操作 `CASCompareAndSwap`**的 API。通过设置 `prevExist` 值,可以保证在多个节点同时去创建某个目录时,只有一个成功。而创建成功的用户就可以认为是获得了锁。
* `控制时序`: 即所有想要获得锁的用户都会被安排执行但是获得锁的顺序也是全局唯一的同时决定了执行顺序。etcd 为此也提供了一套 API自动创建有序键对一个目录建值时指定为 POST 动作,这样 etcd 会自动在目录下生成一个当前最大的值为键,存储这个新的值(客户端编号)。同时还可以使用 API 按顺序列出所有当前目录下的键值。此时这些键的值就是客户端的时序,而这些键中存储的值可以是代表客户端的编号。
![](pics/etcd_02.png)
## .3 为什么用 etcd 而不用 ZooKeeper
etcd 实现的这些功能ZooKeeper 都能实现。那么为什么要用 etcd 而非直接使用 ZooKeeper呢
### .3.1 为什么不选择 ZooKeeper
* 部署维护复杂,其使用的 Paxos 强一致性算法复杂难懂。官方只提供了 Java 和 C 两种语言的接口。
* 使用 Java 编写引入大量的依赖。运维人员维护起来比较麻烦。
* 最近几年发展缓慢,不如 etcd 和 consul 等后起之秀。
### .3.2 为什么选择 etcd
* 简单。使用 Go 语言编写部署简单;支持 HTTP/JSON API,使用简单;使用 Raft 算法保证强一致性让用户易于理解。
* etcd 默认数据一更新就进行持久化。
* etcd 支持 SSL 客户端安全认证。
最后etcd 作为一个年轻的项目,正在高速迭代和开发中,这既是一个优点,也是一个缺点。优点是它的未来具有无限的可能性,缺点是无法得到大项目长时间使用的检验。然而,目前 `CoreOS``Kubernetes``CloudFoundry` 等知名项目均在生产环境中使用了 etcd所以总的来说etcd 值得你去尝试。
## .4 etcd 集群
etcd 作为一个高可用键值存储系统,天生就是为集群化而设计的。由于 Raft 算法在做决策时需要多数节点的投票,所以 **etcd 一般部署集群推荐奇数个节点,推荐的数量为 3、5 或者 7 个节点构成一个集群**
### .4.1 搭建一个3节点集群示例
在每个 etcd 节点指定集群成员,为了区分不同的集群最好同时配置一个独一无二的 token。
下面是提前定义好的集群信息,其中 n1、n2 和 n3 表示 3 个不同的 etcd 节点。
```java
TOKEN=token-01
CLUSTER_STATE=new
CLUSTER=n1=http://10.240.0.17:2380,n2=http://10.240.0.18:2380,n3=http://10.240.0.19:2380
```
在 n1 这台机器上执行以下命令来启动 etcd
```java
etcd --data-dir=data.etcd --name n1 \
--initial-advertise-peer-urls http://10.240.0.17:2380 --listen-peer-urls http://10.240.0.17:2380 \
--advertise-client-urls http://10.240.0.17:2379 --listen-client-urls http://10.240.0.17:2379 \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
```
n2 这台机器上执行以下命令启动 etcd
```java
etcd --data-dir=data.etcd --name n2 \
--initial-advertise-peer-urls http://10.240.0.18:2380 --listen-peer-urls http://10.240.0.18:2380 \
--advertise-client-urls http://10.240.0.18:2379 --listen-client-urls http://10.240.0.18:2379 \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
```
在 n3 这台机器上执行以下命令启动 etcd
```java
etcd --data-dir=data.etcd --name n3 \
--initial-advertise-peer-urls http://10.240.0.19:2380 --listen-peer-urls http://10.240.0.19:2380 \
--advertise-client-urls http://10.240.0.19:2379 --listen-client-urls http://10.240.0.19:2379 \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
```
etcd 官网提供了一个可以公网访问的 etcd 存储地址。你可以通过如下命令得到 etcd 服务的目录,并把它作为 `-discovery` 参数使用。
```
curl https://discovery.etcd.io/new?size=3
https://discovery.etcd.io/a81b5818e67a6ea83e9d4daea5ecbc92
# grab this token
TOKEN=token-01
CLUSTER_STATE=new
DISCOVERY=https://discovery.etcd.io/a81b5818e67a6ea83e9d4daea5ecbc92
etcd --data-dir=data.etcd --name n1 \
--initial-advertise-peer-urls http://10.240.0.17:2380 --listen-peer-urls http://10.240.0.17:2380 \
--advertise-client-urls http://10.240.0.17:2379 --listen-client-urls http://10.240.0.17:2379 \
--discovery ${DISCOVERY} \
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
etcd --data-dir=data.etcd --name n2 \
--initial-advertise-peer-urls http://10.240.0.18:2380 --listen-peer-urls http://10.240.0.18:2380 \
--advertise-client-urls http://10.240.0.18:2379 --listen-client-urls http://10.240.0.18:2379 \
--discovery ${DISCOVERY} \
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
etcd --data-dir=data.etcd --name n3 \
--initial-advertise-peer-urls http://10.240.0.19:2380 --listen-peer-urls http://10.240.0.19:2380 \
--advertise-client-urls http://10.240.0.19:2379 --listen-client-urls http:/10.240.0.19:2379 \
--discovery ${DISCOVERY} \
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
```
到此 etcd 集群就搭建起来了,可以使用 etcdctl 来连接 etcd。
```
export ETCDCTL_API=3
HOST_1=10.240.0.17
HOST_2=10.240.0.18
HOST_3=10.240.0.19
ENDPOINTS=$HOST_1:2379,$HOST_2:2379,$HOST_3:2379
etcdctl --endpoints=$ENDPOINTS member list
```
## .5 Go 语言操作 etcd
这里使用官方的 [etcd/clientv3](https://github.com/etcd-io/etcd/tree/master/client/v3) 包来连接 etcd 并进行相关操作。
### .5.1 安装
```
go get go.etcd.io/etcd/clientv3
```
### .5.2 put 和 get 操作
`put` 命令用来设置键值对数据,`get` 命令用来根据 key 获取值。
```go
package main
import (
"context"
"fmt"
"time"
"go.etcd.io/etcd/clientv3"
)
// etcd client put/get demo
// use etcd/clientv3
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
// handle error!
fmt.Printf("connect to etcd failed, err:%v\n", err)
return
}
fmt.Println("connect to etcd success")
defer cli.Close()
// put
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
_, err = cli.Put(ctx, "q1mi", "dsb")
cancel()
if err != nil {
fmt.Printf("put to etcd failed, err:%v\n", err)
return
}
// get
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
resp, err := cli.Get(ctx, "q1mi")
cancel()
if err != nil {
fmt.Printf("get from etcd failed, err:%v\n", err)
return
}
for _, ev := range resp.Kvs {
fmt.Printf("%s:%s\n", ev.Key, ev.Value)
}
}
```
### .5.3 watch 操作
watch 用来获取未来更改的通知。
```go
package main
import (
"context"
"fmt"
"time"
"go.etcd.io/etcd/clientv3"
)
// watch demo
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
fmt.Printf("connect to etcd failed, err:%v\n", err)
return
}
fmt.Println("connect to etcd success")
defer cli.Close()
// watch key:q1mi change
rch := cli.Watch(context.Background(), "q1mi") // <-chan WatchResponse
for wresp := range rch {
for _, ev := range wresp.Events {
fmt.Printf("Type: %s Key:%s Value:%s\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
}
}
}
```
将上面的代码保存编译执行,此时程序就会等待 etcd 中 q1mi 这个 key 的变化。
例如:我们打开终端执行以下命令修改、删除、设置 q1mi 这个 key。
```
etcd> etcdctl.exe --endpoints=http://127.0.0.1:2379 put q1mi "dsb2"
OK
etcd> etcdctl.exe --endpoints=http://127.0.0.1:2379 del q1mi
1
etcd> etcdctl.exe --endpoints=http://127.0.0.1:2379 put q1mi "dsb3"
OK
```
上面的程序都能收到如下通知。
```
watch>watch.exe
connect to etcd success
Type: PUT Key:q1mi Value:dsb2
Type: DELETE Key:q1mi Value:
Type: PUT Key:q1mi Value:dsb3
```
### .5.4 lease 租约
```go
package main
import (
"fmt"
"time"
)
// etcd lease
import (
"context"
"log"
"go.etcd.io/etcd/clientv3"
)
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: time.Second * 5,
})
if err != nil {
log.Fatal(err)
}
fmt.Println("connect to etcd success.")
defer cli.Close()
// 创建一个5秒的租约
resp, err := cli.Grant(context.TODO(), 5)
if err != nil {
log.Fatal(err)
}
// 5秒钟之后, /nazha/ 这个key就会被移除
_, err = cli.Put(context.TODO(), "/nazha/", "dsb", clientv3.WithLease(resp.ID))
if err != nil {
log.Fatal(err)
}
}
```
### .5.5 keepAlive
```go
package main
import (
"context"
"fmt"
"log"
"time"
"go.etcd.io/etcd/clientv3"
)
// etcd keepAlive
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: time.Second * 5,
})
if err != nil {
log.Fatal(err)
}
fmt.Println("connect to etcd success.")
defer cli.Close()
resp, err := cli.Grant(context.TODO(), 5)
if err != nil {
log.Fatal(err)
}
_, err = cli.Put(context.TODO(), "/nazha/", "dsb", clientv3.WithLease(resp.ID))
if err != nil {
log.Fatal(err)
}
// the key 'foo' will be kept forever
ch, kaerr := cli.KeepAlive(context.TODO(), resp.ID)
if kaerr != nil {
log.Fatal(kaerr)
}
for {
ka := <-ch
fmt.Println("ttl:", ka.TTL)
}
}
```
### .5.6 基于 etcd 实现分布式锁
`go.etcd.io/etcd/clientv3/concurrency` 在 etcd 之上实现并发操作,如分布式锁、屏障和选举。
导入该包:
```go
import "go.etcd.io/etcd/clientv3/concurrency"
```
基于 etcd 实现的分布式锁示例:
```go
cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
// 创建两个单独的会话用来演示锁竞争
s1, err := concurrency.NewSession(cli)
if err != nil {
log.Fatal(err)
}
defer s1.Close()
m1 := concurrency.NewMutex(s1, "/my-lock/")
s2, err := concurrency.NewSession(cli)
if err != nil {
log.Fatal(err)
}
defer s2.Close()
m2 := concurrency.NewMutex(s2, "/my-lock/")
// 会话s1获取锁
if err := m1.Lock(context.TODO()); err != nil {
log.Fatal(err)
}
fmt.Println("acquired lock for s1")
m2Locked := make(chan struct{})
go func() {
defer close(m2Locked)
// 等待直到会话s1释放了/my-lock/的锁
if err := m2.Lock(context.TODO()); err != nil {
log.Fatal(err)
}
}()
if err := m1.Unlock(context.TODO()); err != nil {
log.Fatal(err)
}
fmt.Println("released lock for s1")
<-m2Locked
fmt.Println("acquired lock for s2")
```
输出:
```
acquired lock for s1
released lock for s1
acquired lock for s2
```
[查看文档了解更多](https://godoc.org/go.etcd.io/etcd/clientv3/concurrency)
### .5.7 其他操作
其他操作请查看 [etcd/clientv3 官方文档](https://github.com/etcd-io/etcd/tree/master/client/v3)。
参考链接:
* [https://etcd.io/docs/v3.3.12/demo/](https://etcd.io/docs/v3.3.12/demo/)
* [https://www.infoq.cn/article/etcd-interpretation-application-scenario-implement-principle/](https://www.infoq.cn/article/etcd-interpretation-application-scenario-implement-principle/)

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -0,0 +1,111 @@
## 概述
看下 Gin 框架的官方介绍:
> Gin 是一个用 Go (Golang) 编写的 web 框架。 它是一个类似于 martini 但拥有更好性能的 API 框架, 由于 httprouter速度提高了近 40 倍。 如果你是性能和高效的追求者, 你会爱上 Gin。
是的,就是用 Gin 来写 API 接口。
## Gin 安装
必须要先安装 GoGo 的安装可以参考:[Go - 环境安装](https://mp.weixin.qq.com/s/ByhEuCncxcXvq7am7D4IPg)。
框架安装可以参考官网:
https://gin-gonic.com/zh-cn/docs/quickstart/
我在安装时,用的是 dep 安装,给大家分享下。
**dep 是啥 **
它是 Golang 官方依赖管理工具,可以认为它与 PHP 中的 composer 类似。
在这就不多做介绍了,可以自己去了解,安装也比较简单。
我本机是 Mac安装只需一个命令
```
brew install dep
```
咱们接下来创建一个新项目ginDemo。
在 ginDemo 目录下执行:
```
dep init
```
执行完毕,会生成如下三个文件:
```
├─ ginDemo
│ ├─ vendor
│ ├─ Gopkg.toml
│ ├─ Gopkg.lock
```
- 依赖包都会下载到 `vendor` 目录。
- 需要的依赖配置写在 `Gopkg.toml` 文件。
- `Gopkg.lock` 暂时可以不用管。
`Gopkg.toml` 文件中增加依赖:
```
[[constraint]]
name = "github.com/gin-gonic/gin"
version = "1.4.0"
```
新增一个 main.go 文件:
```
// 官方 Demo
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}
```
ginDemo 目录下执行:
```
dep ensure
```
执行完毕,`vendor` 目录会存在安装包,这时整体目录结构如下:
```
├─ ginDemo
│ ├─ vendor
│ ├── github.com
│ ├── ...
│ ├── golang.org
│ ├── ...
│ ├── gopkg.in
│ ├── ...
│ ├─ Gopkg.toml
│ ├─ Gopkg.lock
│ ├─ main.go
```
ginDemo 目录下执行:
```
go run main.go
```
浏览器访问:`http://localhost:8080/ping`
![](https://github.com/xinliangnote/Go/blob/master/01-Gin框架/images/01-框架安装/1_go_1.png)
大功告成!

View File

@@ -0,0 +1,263 @@
## 概述
这篇文章分享 Gin 的路由配置,主要包含的功能点如下:
- 实现了,路由分组 v1版本、v2版本。
- 实现了,生成签名和验证验证。
- 实现了,在配置文件中读取配置。
## 路由配置
比如我们的接口地址是这样的:
- `/v1/product/add`
- `/v1/member/add`
- `/v2/product/add`
- `/v2/member/add`
假设需求是这样的接口支持多种请求方式v1 不需签名验证v2 需要签名验证,路由文件应该这样写:
```
package router
import (
"ginDemo/common"
"ginDemo/controller/v1"
"ginDemo/controller/v2"
"github.com/gin-gonic/gin"
"net/url"
"strconv"
)
func InitRouter(r *gin.Engine) {
r.GET("/sn", SignDemo)
// v1 版本
GroupV1 := r.Group("/v1")
{
GroupV1.Any("/product/add", v1.AddProduct)
GroupV1.Any("/member/add", v1.AddMember)
}
// v2 版本
GroupV2 := r.Group("/v2", common.VerifySign)
{
GroupV2.Any("/product/add", v2.AddProduct)
GroupV2.Any("/member/add", v2.AddMember)
}
}
func SignDemo(c *gin.Context) {
ts := strconv.FormatInt(common.GetTimeUnix(), 10)
res := map[string]interface{}{}
params := url.Values{
"name" : []string{"a"},
"price" : []string{"10"},
"ts" : []string{ts},
}
res["sn"] = common.CreateSign(params)
res["ts"] = ts
common.RetJson("200", "", res, c)
}
```
`.Any` 表示支持多种请求方式。
`controller/v1` 表示 v1 版本的文件。
`controller/v2` 表示 v2 版本的文件。
`SignDemo` 表示生成签名的Demo。
接下来,给出一些代码片段:
验证签名方法:
```
// 验证签名
func VerifySign(c *gin.Context) {
var method = c.Request.Method
var ts int64
var sn string
var req url.Values
if method == "GET" {
req = c.Request.URL.Query()
sn = c.Query("sn")
ts, _ = strconv.ParseInt(c.Query("ts"), 10, 64)
} else if method == "POST" {
req = c.Request.PostForm
sn = c.PostForm("sn")
ts, _ = strconv.ParseInt(c.PostForm("ts"), 10, 64)
} else {
RetJson("500", "Illegal requests", "", c)
return
}
exp, _ := strconv.ParseInt(config.API_EXPIRY, 10, 64)
// 验证过期时间
if ts > GetTimeUnix() || GetTimeUnix() - ts >= exp {
RetJson("500", "Ts Error", "", c)
return
}
// 验证签名
if sn == "" || sn != CreateSign(req) {
RetJson("500", "Sn Error", "", c)
return
}
}
```
生成签名的方法:
```
// 生成签名
func CreateSign(params url.Values) string {
var key []string
var str = ""
for k := range params {
if k != "sn" {
key = append(key, k)
}
}
sort.Strings(key)
for i := 0; i < len(key); i++ {
if i == 0 {
str = fmt.Sprintf("%v=%v", key[i], params.Get(key[i]))
} else {
str = str + fmt.Sprintf("&%v=%v", key[i], params.Get(key[i]))
}
}
// 自定义签名算法
sign := MD5(MD5(str) + MD5(config.APP_NAME + config.APP_SECRET))
return sign
}
```
获取参数的方法:
```
// 获取 Get 参数
name := c.Query("name")
price := c.DefaultQuery("price", "100")
// 获取 Post 参数
name := c.PostForm("name")
price := c.DefaultPostForm("price", "100")
// 获取 Get 所有参数
ReqGet = c.Request.URL.Query()
//获取 Post 所有参数
ReqPost = c.Request.PostForm
```
v1 业务代码:
```
package v1
import "github.com/gin-gonic/gin"
func AddProduct(c *gin.Context) {
// 获取 Get 参数
name := c.Query("name")
price := c.DefaultQuery("price", "100")
c.JSON(200, gin.H{
"v1" : "AddProduct",
"name" : name,
"price" : price,
})
}
```
v2 业务代码:
```
package v2
import (
"github.com/gin-gonic/gin"
)
func AddProduct(c *gin.Context) {
// 获取 Get 参数
name := c.Query("name")
price := c.DefaultQuery("price", "100")
c.JSON(200, gin.H{
"v1" : "AddProduct",
"name" : name,
"price" : price,
})
}
```
接下来,直接看效果吧。
访问 v1 接口:
![](https://github.com/xinliangnote/Go/blob/master/01-Gin框架/images/02-路由配置/2_go_1.png)
访问后,直接返回数据,不走签名验证。
访问 v2 接口:
![](https://github.com/xinliangnote/Go/blob/master/01-Gin框架/images/02-路由配置/2_go_2.png)
进入了这段验证:
```
// 验证过期时间
if ts > GetTimeUnix() || GetTimeUnix() - ts >= exp {
RetJson("500", "Ts Error", "", c)
return
}
```
修改为合法的时间戳后:
![](https://github.com/xinliangnote/Go/blob/master/01-Gin框架/images/02-路由配置/2_go_3.png)
进入了这段验证:
```
// 验证签名
if sn == "" || sn != CreateSign(req) {
RetJson("500", "Sn Error", "", c)
return
}
```
修改为合法的签名后:
![](https://github.com/xinliangnote/Go/blob/master/01-Gin框架/images/02-路由配置/2_go_4.png)
至此,简单的路由配置已经实现了。
对了,还有一个点没说,就是如何读取配置文件中的配置,我是这样做的:
```
package config
const (
PORT = ":8080"
APP_NAME = "ginDemo"
APP_SECRET = "6YJSuc50uJ18zj45"
API_EXPIRY = "120"
)
```
引入 config 包,直接 `config.xx` 即可。
## 源码
**下载源码后,请先执行 `dep ensure` 下载依赖包!**
[查看源码](https://github.com/xinliangnote/Go/blob/master/01-Gin框架/codes/02-路由配置)

View File

@@ -0,0 +1,395 @@
## 概述
上篇文章分享了 Gin 框架的路由配置,这篇文章分享日志记录。
查了很多资料Go 的日志记录用的最多的还是 `github.com/sirupsen/logrus`
> Logrus is a structured logger for Go (golang), completely API compatible with the standard library logger.
Gin 框架的日志默认只会在控制台输出,咱们利用 `Logrus` 封装一个中间件,将日志记录到文件中。
这篇文章就是学习和使用 `Logrus`
## 日志格式
比如,我们约定日志格式为 Text包含字段如下
`请求时间``日志级别``状态码``执行时间``请求IP``请求方式``请求路由`
接下来,咱们利用 `Logrus` 实现它。
## Logrus 使用
`dep` 方式进行安装。
`Gopkg.toml` 文件新增:
```
[[constraint]]
name = "github.com/sirupsen/logrus"
version = "1.4.2"
```
在项目中导入:
```
import "github.com/sirupsen/logrus"
```
在项目命令行执行:
```
dep ensure
```
这时,在 `vendor/github.com/` 目录中就会看到 `sirupsen` 目录。
准备上手用了,上手之前咱们先规划一下,将这个功能设置成一个中间件,比如:`logger.go`
日志可以记录到 File 中,定义一个 `LoggerToFile` 方法。
日志可以记录到 MongoDB 中,定义一个 `LoggerToMongo` 方法。
日志可以记录到 ES 中,定义一个 `LoggerToES` 方法。
日志可以记录到 MQ 中,定义一个 `LoggerToMQ` 方法。
...
这次咱们先实现记录到文件, 实现 `LoggerToFile` 方法,其他的可以根据自己的需求进行实现。
这个 `logger` 中间件,创建好了,可以任意在其他项目中进行迁移使用。
废话不多说,直接看代码。
```
package middleware
import (
"fmt"
"ginDemo/config"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"os"
"path"
"time"
)
// 日志记录到文件
func LoggerToFile() gin.HandlerFunc {
logFilePath := config.Log_FILE_PATH
logFileName := config.LOG_FILE_NAME
//日志文件
fileName := path.Join(logFilePath, logFileName)
//写入文件
src, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
if err != nil {
fmt.Println("err", err)
}
//实例化
logger := logrus.New()
//设置输出
logger.Out = src
//设置日志级别
logger.SetLevel(logrus.DebugLevel)
//设置日志格式
logger.SetFormatter(&logrus.TextFormatter{})
return func(c *gin.Context) {
// 开始时间
startTime := time.Now()
// 处理请求
c.Next()
// 结束时间
endTime := time.Now()
// 执行时间
latencyTime := endTime.Sub(startTime)
// 请求方式
reqMethod := c.Request.Method
// 请求路由
reqUri := c.Request.RequestURI
// 状态码
statusCode := c.Writer.Status()
// 请求IP
clientIP := c.ClientIP()
// 日志格式
logger.Infof("| %3d | %13v | %15s | %s | %s |",
statusCode,
latencyTime,
clientIP,
reqMethod,
reqUri,
)
}
}
// 日志记录到 MongoDB
func LoggerToMongo() gin.HandlerFunc {
return func(c *gin.Context) {
}
}
// 日志记录到 ES
func LoggerToES() gin.HandlerFunc {
return func(c *gin.Context) {
}
}
// 日志记录到 MQ
func LoggerToMQ() gin.HandlerFunc {
return func(c *gin.Context) {
}
}
```
日志中间件写好了,怎么调用呢?
只需在 main.go 中新增:
```
engine := gin.Default() //在这行后新增
engine.Use(middleware.LoggerToFile())
```
运行一下,看看日志:
```
time="2019-07-17T22:10:45+08:00" level=info msg="| 200 | 27.698µs | ::1 | GET | /v1/product/add?name=a&price=10 |"
time="2019-07-17T22:10:46+08:00" level=info msg="| 200 | 27.239µs | ::1 | GET | /v1/product/add?name=a&price=10 |"
```
**这个 `time="2019-07-17T22:10:45+08:00"` ,这个时间格式不是咱们想要的,怎么办?**
时间需要格式化一下,修改 `logger.SetFormatter`
```
//设置日志格式
logger.SetFormatter(&logrus.TextFormatter{
TimestampFormat:"2006-01-02 15:04:05",
})
```
执行以下,再看日志:
```
time="2019-07-17 22:15:57" level=info msg="| 200 | 185.027µs | ::1 | GET | /v1/product/add?name=a&price=10 |"
time="2019-07-17 22:15:58" level=info msg="| 200 | 56.989µs | ::1 | GET | /v1/product/add?name=a&price=10 |"
```
时间变得正常了。
**我不喜欢文本格式,喜欢 JSON 格式,怎么办?**
```
//设置日志格式
logger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat:"2006-01-02 15:04:05",
})
```
执行以下,再看日志:
```
{"level":"info","msg":"| 200 | 24.78µs | ::1 | GET | /v1/product/add?name=a\u0026price=10 |","time":"2019-07-17 22:23:55"}
{"level":"info","msg":"| 200 | 26.946µs | ::1 | GET | /v1/product/add?name=a\u0026price=10 |","time":"2019-07-17 22:23:56"}
```
**msg 信息太多,不方便看,怎么办?**
```
// 日志格式
logger.WithFields(logrus.Fields{
"status_code" : statusCode,
"latency_time" : latencyTime,
"client_ip" : clientIP,
"req_method" : reqMethod,
"req_uri" : reqUri,
}).Info()
```
执行以下,再看日志:
```
{"client_ip":"::1","latency_time":26681,"level":"info","msg":"","req_method":"GET","req_uri":"/v1/product/add?name=a\u0026price=10","status_code":200,"time":"2019-07-17 22:37:54"}
{"client_ip":"::1","latency_time":24315,"level":"info","msg":"","req_method":"GET","req_uri":"/v1/product/add?name=a\u0026price=10","status_code":200,"time":"2019-07-17 22:37:55"}
```
说明一下:`time``msg``level` 这些参数是 logrus 自动加上的。
**logrus 支持输出文件名和行号吗?**
不支持,作者的回复是太耗性能。
不过网上也有人通过 Hook 的方式实现了,选择在生产环境使用的时候,记得做性能测试。
**logrus 支持日志分割吗?**
不支持,但有办法实现它。
1、可以利用 `Linux logrotate`,统一由运维进行处理。
2、可以利用 `file-rotatelogs` 实现。
需要导入包:
`github.com/lestrrat-go/file-rotatelogs`
`github.com/rifflock/lfshook`
奉上完整代码:
```
package middleware
import (
"fmt"
"ginDemo/config"
"github.com/gin-gonic/gin"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
"github.com/rifflock/lfshook"
"github.com/sirupsen/logrus"
"os"
"path"
"time"
)
// 日志记录到文件
func LoggerToFile() gin.HandlerFunc {
logFilePath := config.Log_FILE_PATH
logFileName := config.LOG_FILE_NAME
// 日志文件
fileName := path.Join(logFilePath, logFileName)
// 写入文件
src, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
if err != nil {
fmt.Println("err", err)
}
// 实例化
logger := logrus.New()
// 设置输出
logger.Out = src
// 设置日志级别
logger.SetLevel(logrus.DebugLevel)
// 设置 rotatelogs
logWriter, err := rotatelogs.New(
// 分割后的文件名称
fileName + ".%Y%m%d.log",
// 生成软链,指向最新日志文件
rotatelogs.WithLinkName(fileName),
// 设置最大保存时间(7天)
rotatelogs.WithMaxAge(7*24*time.Hour),
// 设置日志切割时间间隔(1天)
rotatelogs.WithRotationTime(24*time.Hour),
)
writeMap := lfshook.WriterMap{
logrus.InfoLevel: logWriter,
logrus.FatalLevel: logWriter,
logrus.DebugLevel: logWriter,
logrus.WarnLevel: logWriter,
logrus.ErrorLevel: logWriter,
logrus.PanicLevel: logWriter,
}
lfHook := lfshook.NewHook(writeMap, &logrus.JSONFormatter{
TimestampFormat:"2006-01-02 15:04:05",
})
// 新增 Hook
logger.AddHook(lfHook)
return func(c *gin.Context) {
// 开始时间
startTime := time.Now()
// 处理请求
c.Next()
// 结束时间
endTime := time.Now()
// 执行时间
latencyTime := endTime.Sub(startTime)
// 请求方式
reqMethod := c.Request.Method
// 请求路由
reqUri := c.Request.RequestURI
// 状态码
statusCode := c.Writer.Status()
// 请求IP
clientIP := c.ClientIP()
// 日志格式
logger.WithFields(logrus.Fields{
"status_code" : statusCode,
"latency_time" : latencyTime,
"client_ip" : clientIP,
"req_method" : reqMethod,
"req_uri" : reqUri,
}).Info()
}
}
// 日志记录到 MongoDB
func LoggerToMongo() gin.HandlerFunc {
return func(c *gin.Context) {
}
}
// 日志记录到 ES
func LoggerToES() gin.HandlerFunc {
return func(c *gin.Context) {
}
}
// 日志记录到 MQ
func LoggerToMQ() gin.HandlerFunc {
return func(c *gin.Context) {
}
}
```
这时会新生成一个文件 `system.log.20190717.log`,日志内容与上面的格式一致。
最后,`logrus` 可扩展的 Hook 很多,大家可以去网上查找。
## 源码
**下载源码后,请先执行 `dep ensure` 下载依赖包!**
[查看源码](https://github.com/xinliangnote/Go/blob/master/01-Gin框架/codes/03-日志记录)

View File

@@ -0,0 +1,308 @@
## 概述
上篇文章分享了 Gin 框架使用 Logrus 进行日志记录,这篇文章分享 Gin 框架的数据绑定与验证。
有读者咨询我一个问题,如何让框架的运行日志不输出控制台?
解决方案:
```
engine := gin.Default() //修改成如下
engine := gin.New()
```
我是怎么知道的?看框架代码。
`Default()`
```
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
```
`New()` 代码我就不贴了。
我们看到 `Default()` 使用了两个中间件 `Logger(), Recovery()`,如果不想使用,那就直接使用 `New()` 就可以了。
开始今天的文章。
比如,请求 `v1/member/add` 新增会员方法,`name``age` 为必填,同时 `name` 不能等于 admin 字符串10 <= age <= 120。
直接看代码吧。
首先,先定义一个结构体。
**entity/member.go**
```
package entity
// 定义 Member 结构体
type Member struct {
Name string `form:"name" json:"name" binding:"required,NameValid"`
Age int `form:"age" json:"age" binding:"required,gt=10,lt=120"`
}
```
binding 中 `required`,这个是框架自带的,`NameValid`,这个是自己定义的。
问题一:框架自带的 binding 参数还有哪些?
问题二:自定义验证方法,怎么写?
接下来要说的就是问题二,写一个验证方法。
**validator/member/member.go**
```
package member
import (
"gopkg.in/go-playground/validator.v8"
"reflect"
)
func NameValid(
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
) bool {
if s, ok := field.Interface().(string); ok {
if s == "admin" {
return false
}
}
return true
}
```
接下来,在路由中绑定:
**router/router.go**
```
package router
import (
"ginDemo/middleware/logger"
"ginDemo/middleware/sign"
"ginDemo/router/v1"
"ginDemo/router/v2"
"ginDemo/validator/member"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v8"
)
func InitRouter(r *gin.Engine) {
r.Use(logger.LoggerToFile())
// v1 版本
GroupV1 := r.Group("/v1")
{
GroupV1.Any("/product/add", v1.AddProduct)
GroupV1.Any("/member/add", v1.AddMember)
}
// v2 版本
GroupV2 := r.Group("/v2").Use(sign.Sign())
{
GroupV2.Any("/product/add", v2.AddProduct)
GroupV2.Any("/member/add", v2.AddMember)
}
// 绑定验证器
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("NameValid", member.NameValid)
}
}
```
最后,看一下调用的代码。
**router/v1/member.go**
```
package v1
import (
"ginDemo/entity"
"github.com/gin-gonic/gin"
"net/http"
)
func AddMember(c *gin.Context) {
res := entity.Result{}
mem := entity.Member{}
if err := c.ShouldBind(&mem); err != nil {
res.SetCode(entity.CODE_ERROR)
res.SetMessage(err.Error())
c.JSON(http.StatusForbidden, res)
c.Abort()
return
}
// 处理业务(下次再分享)
data := map[string]interface{}{
"name" : mem.Name,
"age" : mem.Age,
}
res.SetCode(entity.CODE_ERROR)
res.SetData(data)
c.JSON(http.StatusOK, res)
}
```
访问看看效果吧。
访问:`http://localhost:8080/v1/member/add`
```
{
"code": -1,
"msg": "Key: 'Member.Name' Error:Field validation for 'Name' failed on the 'required' tag\nKey: 'Member.Age' Error:Field validation for 'Age' failed on the 'required' tag",
"data": null
}
```
访问:`http://localhost:8080/v1/member/add?name=1`
```
{
"code": -1,
"msg": "Key: 'Member.Age' Error:Field validation for 'Age' failed on the 'required' tag",
"data": null
}
```
访问:`http://localhost:8080/v1/member/add?age=1`
```
{
"code": -1,
"msg": "Key: 'Member.Age' Error:Field validation for 'Age' failed on the 'required' tag",
"data": null
}
```
访问:`http://localhost:8080/v1/member/add?name=admin&age=1`
```
{
"code": -1,
"msg": "Key: 'Member.Name' Error:Field validation for 'Name' failed on the 'NameValid' tag",
"data": null
}
```
访问:`http://localhost:8080/v1/member/add?name=1&age=1`
```
{
"code": -1,
"msg": "Key: 'Member.Age' Error:Field validation for 'Age' failed on the 'gt' tag",
"data": null
}
```
访问:`http://localhost:8080/v1/member/add?name=1&age=121`
```
{
"code": -1,
"msg": "Key: 'Member.Age' Error:Field validation for 'Age' failed on the 'lt' tag",
"data": null
}
```
访问:`http://localhost:8080/v1/member/add?name=Tom&age=30`
```
{
"code": 1,
"msg": "",
"data": {
"age": 30,
"name": "Tom"
}
}
```
未避免返回信息过多,错误提示咱们也可以统一。
```
if err := c.ShouldBind(&mem); err != nil {
res.SetCode(entity.CODE_ERROR)
res.SetMessage("参数验证错误")
c.JSON(http.StatusForbidden, res)
c.Abort()
return
}
```
这一次目录结构调整了一些,在这里说一下:
```
├─ ginDemo
│ ├─ common //公共方法
│ ├── common.go
│ ├─ config //配置文件
│ ├── config.go
│ ├─ entity //实体
│ ├── ...
│ ├─ middleware //中间件
│ ├── logger
│ ├── ...
│ ├── sign
│ ├── ...
│ ├─ router //路由
│ ├── ...
│ ├─ validator //验证器
│ ├── ...
│ ├─ vendor //扩展包
│ ├── github.com
│ ├── ...
│ ├── golang.org
│ ├── ...
│ ├── gopkg.in
│ ├── ...
│ ├─ Gopkg.toml
│ ├─ Gopkg.lock
│ ├─ main.go
```
`sign``logger` 调整为中间件,并放到 `middleware` 中间件 目录。
新增了 `common` 公共方法目录。
新增了 `validator` 验证器目录。
新增了 `entity` 实体目录。
具体代码我会放到 `GitHub`有感兴趣的可以去看https://github.com/xinliangnote/Go
上面还遗漏了问题一没解决,框架自带的 binding 参数还有哪些?
从框架源码了解到验证使用的是:
`gopkg.in/go-playground/validator.v8`
文档地址为:
https://godoc.org/gopkg.in/go-playground/validator.v8
去探索文档吧,里面有很多验证规则。
## 源码
**下载源码后,请先执行 `dep ensure` 下载依赖包!**
[查看源码](https://github.com/xinliangnote/Go/blob/master/01-Gin框架/codes/04-数据绑定和验证)

View File

@@ -0,0 +1,600 @@
## 概述
开始今天的文章,为什么要自定义错误处理?默认的错误处理方式是什么?
那好,咱们就先说下默认的错误处理。
默认的错误处理是 `errors.New("错误信息")`,这个信息通过 error 类型的返回值进行返回。
举个简单的例子:
```
func hello(name string) (str string, err error) {
if name == "" {
err = errors.New("name 不能为空")
return
}
str = fmt.Sprintf("hello: %s", name)
return
}
```
当调用这个方法时:
```
var name = ""
str, err := hello(name)
if err != nil {
fmt.Println(err.Error())
return
}
```
这就是默认的错误处理,下面还会用这个例子进行说。
这个默认的错误处理,只是得到了一个错误信息的字符串。
然而...
我还想得到发生错误时的 `时间``文件名``方法名``行号` 等信息。
我还想得到错误时进行告警,比如 `短信告警``邮件告警``微信告警` 等。
我还想调用的时候,不那么复杂,就和默认错误处理类似,比如:
```
alarm.WeChat("错误信息")
return
```
这样,我们就得到了我们想要的信息(`时间``文件名``方法名``行号`),并通过 `微信` 的方式进行告警通知我们。
同理,`alarm.Email("错误信息")``alarm.Sms("错误信息")` 我们得到的信息是一样的,只是告警方式不同而已。
还要保证,我们业务逻辑中,获取错误的时候,只获取错误信息即可。
上面这些想出来的,就是今天要实现的,自定义错误处理,我们就实现之前,先说下 Go 的错误处理。
## 错误处理
```
package main
import (
"errors"
"fmt"
)
func hello(name string) (str string, err error) {
if name == "" {
err = errors.New("name 不能为空")
return
}
str = fmt.Sprintf("hello: %s", name)
return
}
func main() {
var name = ""
fmt.Println("param:", name)
str, err := hello(name)
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Println(str)
}
```
输出:
```
param: Tom
hello: Tom
```
当 name = "" 时,输出:
```
param:
name 不能为空
```
建议每个函数都要有错误处理error 应该为最后一个返回值。
咱们一起看下官方 errors.go
```
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package errors implements functions to manipulate errors.
package errors
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
```
上面的代码,并不复杂,参照上面的,咱们进行写一个自定义错误处理。
## 自定义错误处理
咱们定义一个 alarm.go用于处理告警。
废话不多说,直接看代码。
```
package alarm
import (
"encoding/json"
"fmt"
"ginDemo/common/function"
"path/filepath"
"runtime"
"strings"
)
type errorString struct {
s string
}
type errorInfo struct {
Time string `json:"time"`
Alarm string `json:"alarm"`
Message string `json:"message"`
Filename string `json:"filename"`
Line int `json:"line"`
Funcname string `json:"funcname"`
}
func (e *errorString) Error() string {
return e.s
}
func New (text string) error {
alarm("INFO", text)
return &errorString{text}
}
// 发邮件
func Email (text string) error {
alarm("EMAIL", text)
return &errorString{text}
}
// 发短信
func Sms (text string) error {
alarm("SMS", text)
return &errorString{text}
}
// 发微信
func WeChat (text string) error {
alarm("WX", text)
return &errorString{text}
}
// 告警方法
func alarm(level string, str string) {
// 当前时间
currentTime := function.GetTimeStr()
// 定义 文件名、行号、方法名
fileName, line, functionName := "?", 0 , "?"
pc, fileName, line, ok := runtime.Caller(2)
if ok {
functionName = runtime.FuncForPC(pc).Name()
functionName = filepath.Ext(functionName)
functionName = strings.TrimPrefix(functionName, ".")
}
var msg = errorInfo {
Time : currentTime,
Alarm : level,
Message : str,
Filename : fileName,
Line : line,
Funcname : functionName,
}
jsons, errs := json.Marshal(msg)
if errs != nil {
fmt.Println("json marshal error:", errs)
}
errorJsonInfo := string(jsons)
fmt.Println(errorJsonInfo)
if level == "EMAIL" {
// 执行发邮件
} else if level == "SMS" {
// 执行发短信
} else if level == "WX" {
// 执行发微信
} else if level == "INFO" {
// 执行记日志
}
}
```
看下如何调用:
```
package v1
import (
"fmt"
"ginDemo/common/alarm"
"ginDemo/entity"
"github.com/gin-gonic/gin"
"net/http"
)
func AddProduct(c *gin.Context) {
// 获取 Get 参数
name := c.Query("name")
var res = entity.Result{}
str, err := hello(name)
if err != nil {
res.SetCode(entity.CODE_ERROR)
res.SetMessage(err.Error())
c.JSON(http.StatusOK, res)
c.Abort()
return
}
res.SetCode(entity.CODE_SUCCESS)
res.SetMessage(str)
c.JSON(http.StatusOK, res)
}
func hello(name string) (str string, err error) {
if name == "" {
err = alarm.WeChat("name 不能为空")
return
}
str = fmt.Sprintf("hello: %s", name)
return
}
```
访问:`http://localhost:8080/v1/product/add?name=a`
```
{
"code": 1,
"msg": "hello: a",
"data": null
}
```
未抛出错误,不会输出信息。
访问:`http://localhost:8080/v1/product/add`
```
{
"code": -1,
"msg": "name 不能为空",
"data": null
}
```
抛出了错误,输出信息如下:
```
{"time":"2019-07-23 22:19:17","alarm":"WX","message":"name 不能为空","filename":"绝对路径/ginDemo/router/v1/product.go","line":33,"funcname":"hello"}
```
可能这会有同学说:“用上一篇分享的数据绑定和验证,将传入的参数进行 binding:"required" 也可以实现呀”。
我只能说:“同学呀,你不理解我的良苦用心,这只是个例子,大家可以在一些复杂的业务逻辑判断场景中使用自定义错误处理”。
到这里,报错时我们收到了 `时间``错误信息``文件名``行号``方法名` 了。
调用起来,也比较简单。
虽然标记了告警方式,还是没有进行告警通知呀。
我想说,在这里存储数据到队列中,再执行异步任务具体去消耗,这块就不实现了,大家可以去完善。
读取 `文件名``方法名``行号` 使用的是 `runtime.Caller()`
我们还知道Go 有 `panic``recover`,它们是干什么的呢,接下来咱们就说说。
## panic 和 recover
当程序不能继续运行的时候,才应该使用 panic 抛出错误。
当程序发生 panic 后,在 defer(延迟函数) 内部可以调用 recover 进行控制,不过有个前提条件,只有在相同的 Go 协程中才可以。
panic 分两个,一种是有意抛出的,一种是无意的写程序马虎造成的,咱们一个个说。
有意抛出的 panic
```
package main
import (
"fmt"
)
func main() {
fmt.Println("-- 1 --")
defer func() {
if r := recover(); r != nil {
fmt.Printf("panic: %s\n", r)
}
fmt.Println("-- 2 --")
}()
panic("i am panic")
}
```
输出:
```
-- 1 --
panic: i am panic
-- 2 --
```
无意抛出的 panic
```
package main
import (
"fmt"
)
func main() {
fmt.Println("-- 1 --")
defer func() {
if r := recover(); r != nil {
fmt.Printf("panic: %s\n", r)
}
fmt.Println("-- 2 --")
}()
var slice = [] int {1, 2, 3, 4, 5}
slice[6] = 6
}
```
输出:
```
-- 1 --
panic: runtime error: index out of range
-- 2 --
```
上面的两个我们都通过 `recover` 捕获到了,那我们如何在 Gin 框架中使用呢?如果收到 `panic` 时,也想进行告警怎么实现呢?
既然想实现告警,先在 ararm.go 中定义一个 `Panic()` 方法,当项目发生 `panic` 异常时,调用这个方法,这样就实现告警了。
```
// Panic 异常
func Panic (text string) error {
alarm("PANIC", text)
return &errorString{text}
}
```
那我们怎么捕获到呢?
使用中间件进行捕获,写一个 `recover` 中间件。
```
package recover
import (
"fmt"
"ginDemo/common/alarm"
"github.com/gin-gonic/gin"
)
func Recover() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
alarm.Panic(fmt.Sprintf("%s", r))
}
}()
c.Next()
}
}
```
路由调用中间件:
```
r.Use(logger.LoggerToFile(), recover.Recover())
//Use 可以传递多个中间件。
```
验证下吧,咱们先抛出两个异常,看看能否捕获到?
还是修改 product.go 这个文件吧。
有意抛出 panic
```
package v1
import (
"fmt"
"ginDemo/entity"
"github.com/gin-gonic/gin"
"net/http"
)
func AddProduct(c *gin.Context) {
// 获取 Get 参数
name := c.Query("name")
var res = entity.Result{}
str, err := hello(name)
if err != nil {
res.SetCode(entity.CODE_ERROR)
res.SetMessage(err.Error())
c.JSON(http.StatusOK, res)
c.Abort()
return
}
res.SetCode(entity.CODE_SUCCESS)
res.SetMessage(str)
c.JSON(http.StatusOK, res)
}
func hello(name string) (str string, err error) {
if name == "" {
// 有意抛出 panic
panic("i am panic")
return
}
str = fmt.Sprintf("hello: %s", name)
return
}
```
访问:`http://localhost:8080/v1/product/add`
界面是空白的。
抛出了异常,输出信息如下:
```
{"time":"2019-07-23 22:42:37","alarm":"PANIC","message":"i am panic","filename":"绝对路径/ginDemo/middleware/recover/recover.go","line":13,"funcname":"1"}
```
很显然,定位的文件名、方法名、行号不是我们想要的。
需要调整 `runtime.Caller(2)`,这个代码在 `alarm.go 的 alarm` 方法中。
将 2 调整成 4 ,看下输出信息:
```
{"time":"2019-07-23 22:45:24","alarm":"PANIC","message":"i am panic","filename":"绝对路径/ginDemo/router/v1/product.go","line":33,"funcname":"hello"}
```
这就对了。
无意抛出 panic
```
// 上面代码不变
func hello(name string) (str string, err error) {
if name == "" {
// 无意抛出 panic
var slice = [] int {1, 2, 3, 4, 5}
slice[6] = 6
return
}
str = fmt.Sprintf("hello: %s", name)
return
}
```
访问:`http://localhost:8080/v1/product/add`
界面是空白的。
抛出了异常,输出信息如下:
```
{"time":"2019-07-23 22:50:06","alarm":"PANIC","message":"runtime error: index out of range","filename":"绝对路径/runtime/panic.go","line":44,"funcname":"panicindex"}
```
很显然,定位的文件名、方法名、行号也不是我们想要的。
将 4 调整成 5 ,看下输出信息:
```
{"time":"2019-07-23 22:55:27","alarm":"PANIC","message":"runtime error: index out of range","filename":"绝对路径/ginDemo/router/v1/product.go","line":34,"funcname":"hello"}
```
这就对了。
奇怪了,这是为什么?
在这里,有必要说下 `runtime.Caller(skip)` 了。
skip 指的调用的深度。
为 0 时,打印当前调用文件及行数。
为 1 时,打印上级调用的文件及行数。
依次类推...
在这块,调用的时候需要注意下,我现在还没有好的解决方案。
我是将 skip调用深度当一个参数传递进去。
比如:
```
// 发微信
func WeChat (text string) error {
alarm("WX", text, 2)
return &errorString{text}
}
// Panic 异常
func Panic (text string) error {
alarm("PANIC", text, 5)
return &errorString{text}
}
```
具体的代码就不贴了。
但是,有意抛出 Panic 和 无意抛出 Panic 的调用深度又不同,怎么办?
1、尽量将有意抛出的 Panic 改成抛出错误的方式。
2、想其他办法搞定它。
就到这吧。
## 源码
**下载源码后,请先执行 `dep ensure` 下载依赖包!**
[查看源码](https://github.com/xinliangnote/Go/blob/master/01-Gin框架/codes/05-自定义错误处理)

View File

@@ -0,0 +1,147 @@
## 改之前
在使用 `gin` 开发接口的时候,返回接口数据是这样写的。
```
type response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
// always return http.StatusOK
c.JSON(http.StatusOK, response{
Code: 20101,
Msg: "用户手机号不合法",
Data: nil,
})
```
这种写法 `code``msg` 都是在哪需要返回在哪定义,没有进行统一管理。
## 改之后
```
// 比如,返回“用户手机号不合法”错误
c.JSON(http.StatusOK, errno.ErrUserPhone.WithID(c.GetString("trace-id")))
// 正确返回
c.JSON(http.StatusOK, errno.OK.WithData(data).WithID(c.GetString("trace-id")))
```
`errno.ErrUserPhone``errno.OK` 表示自定义的错误码,下面会看到定义的地方。
`.WithID()` 设置当前请求的唯一ID也可以理解为链路ID忽略也可以。
`.WithData()` 设置成功时返回的数据。
下面分享下编写的 `errno` 包源码,非常简单,希望大家不要介意。
## errno 包源码
```
// errno/errno.go
package errno
import (
"encoding/json"
)
var _ Error = (*err)(nil)
type Error interface {
// i 为了避免被其他包实现
i()
// WithData 设置成功时返回的数据
WithData(data interface{}) Error
// WithID 设置当前请求的唯一ID
WithID(id string) Error
// ToString 返回 JSON 格式的错误详情
ToString() string
}
type err struct {
Code int `json:"code"` // 业务编码
Msg string `json:"msg"` // 错误描述
Data interface{} `json:"data"` // 成功时返回的数据
ID string `json:"id,omitempty"` // 当前请求的唯一ID便于问题定位忽略也可以
}
func NewError(code int, msg string) Error {
return &err{
Code: code,
Msg: msg,
Data: nil,
}
}
func (e *err) i() {}
func (e *err) WithData(data interface{}) Error {
e.Data = data
return e
}
func (e *err) WithID(id string) Error {
e.ID = id
return e
}
// ToString 返回 JSON 格式的错误详情
func (e *err) ToString() string {
err := &struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
ID string `json:"id,omitempty"`
}{
Code: e.Code,
Msg: e.Msg,
Data: e.Data,
ID: e.ID,
}
raw, _ := json.Marshal(err)
return string(raw)
}
```
```
// errno/code.go
package errno
var (
// OK
OK = NewError(0, "OK")
// 服务级错误码
ErrServer = NewError(10001, "服务异常,请联系管理员")
ErrParam = NewError(10002, "参数有误")
ErrSignParam = NewError(10003, "签名参数有误")
// 模块级错误码 - 用户模块
ErrUserPhone = NewError(20101, "用户手机号不合法")
ErrUserCaptcha = NewError(20102, "用户验证码有误")
// ...
)
```
## 错误码规则
- 错误码需在 `code.go` 文件中定义。
- 错误码需为 > 0 的数,反之表示正确。
#### 错误码为 5 位数
| 1 | 01 | 01 |
| :------ | :------ | :------ |
| 服务级错误码 | 模块级错误码 | 具体错误码 |
- 服务级别错误码1 位数进行表示,比如 1 为系统级错误2 为普通错误,通常是由用户非法操作引起。
- 模块级错误码2 位数进行表示,比如 01 为用户模块02 为订单模块。
- 具体错误码2 位数进行表示,比如 01 为手机号不合法02 为验证码输入错误。

View File

@@ -0,0 +1,40 @@
## 项目介绍
[Gin 路由配置](https://github.com/xinliangnote/Go/blob/master/01-Gin框架/02-路由配置.md)
## 配置
```
func InitRouter(r *gin.Engine) {
r.GET("/sn", SignDemo)
// v1 版本
GroupV1 := r.Group("/v1")
{
GroupV1.Any("/product/add", v1.AddProduct)
GroupV1.Any("/member/add", v1.AddMember)
}
// v2 版本
GroupV2 := r.Group("/v2", common.VerifySign)
{
GroupV2.Any("/product/add", v2.AddProduct)
GroupV2.Any("/member/add", v2.AddMember)
}
}
```
## 运行
**下载源码后,请先执行 `dep ensure` 下载依赖包!**
## 效果图
![](https://github.com/xinliangnote/Go/blob/master/01-Gin框架/images/02-路由配置/2_go_1.png)
![](https://github.com/xinliangnote/Go/blob/master/01-Gin框架/images/02-路由配置/2_go_2.png)
![](https://github.com/xinliangnote/Go/blob/master/01-Gin框架/images/02-路由配置/2_go_3.png)
![](https://github.com/xinliangnote/Go/blob/master/01-Gin框架/images/02-路由配置/2_go_4.png)

View File

@@ -0,0 +1,128 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:3ee1d175a75b911a659fbd860060874c4f503e793c5870d13e5a0ede529a63cf"
name = "github.com/gin-contrib/sse"
packages = ["."]
pruneopts = "UT"
revision = "54d8467d122d380a14768b6b4e5cd7ca4755938f"
version = "v0.1.0"
[[projects]]
digest = "1:d8bd2a337f6ff2188e08f72c614f2f3f0fd48e6a7b37a071b197e427d77d3a47"
name = "github.com/gin-gonic/gin"
packages = [
".",
"binding",
"internal/json",
"render",
]
pruneopts = "UT"
revision = "b75d67cd51eb53c3c3a2fc406524c940021ffbda"
version = "v1.4.0"
[[projects]]
digest = "1:573ca21d3669500ff845bdebee890eb7fc7f0f50c59f2132f2a0c6b03d85086a"
name = "github.com/golang/protobuf"
packages = ["proto"]
pruneopts = "UT"
revision = "6c65a5562fc06764971b7c5d05c76c75e84bdbf7"
version = "v1.3.2"
[[projects]]
digest = "1:f5a2051c55d05548d2d4fd23d244027b59fbd943217df8aa3b5e170ac2fd6e1b"
name = "github.com/json-iterator/go"
packages = ["."]
pruneopts = "UT"
revision = "0ff49de124c6f76f8494e194af75bde0f1a49a29"
version = "v1.1.6"
[[projects]]
digest = "1:31e761d97c76151dde79e9d28964a812c46efc5baee4085b86f68f0c654450de"
name = "github.com/konsorten/go-windows-terminal-sequences"
packages = ["."]
pruneopts = "UT"
revision = "f55edac94c9bbba5d6182a4be46d86a2c9b5b50e"
version = "v1.0.2"
[[projects]]
branch = "master"
digest = "1:8654122ef85f2bc03859b0a7ea2d36c9965888689cdac7640cceaa6edb11cff6"
name = "github.com/lestrrat-go/strftime"
packages = ["."]
pruneopts = "UT"
revision = "8b31f9c59b0feb56c456ce49a7b3d2b2e93a6f18"
[[projects]]
digest = "1:9b90c7639a41697f3d4ad12d7d67dfacc9a7a4a6e0bbfae4fc72d0da57c28871"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = "UT"
revision = "1311e847b0cb909da63b5fecfb5370aa66236465"
version = "v0.0.8"
[[projects]]
digest = "1:33422d238f147d247752996a26574ac48dcf472976eda7f5134015f06bf16563"
name = "github.com/modern-go/concurrent"
packages = ["."]
pruneopts = "UT"
revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94"
version = "1.0.3"
[[projects]]
digest = "1:e32bdbdb7c377a07a9a46378290059822efdce5c8d96fe71940d87cb4f918855"
name = "github.com/modern-go/reflect2"
packages = ["."]
pruneopts = "UT"
revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd"
version = "1.0.1"
[[projects]]
digest = "1:cf31692c14422fa27c83a05292eb5cbe0fb2775972e8f1f8446a71549bd8980b"
name = "github.com/pkg/errors"
packages = ["."]
pruneopts = "UT"
revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4"
version = "v0.8.1"
[[projects]]
digest = "1:5a1cf4e370bc86137b58da2ae065e76526d32b11f62a7665f36dbd5f41fa95ff"
name = "github.com/ugorji/go"
packages = ["codec"]
pruneopts = "UT"
revision = "23ab95ef5dc3b70286760af84ce2327a2b64ed62"
version = "v1.1.7"
[[projects]]
branch = "master"
digest = "1:5b3f90037a9027c43bdcae488c39d41aadecb9d85e645bb62b773a0a6f6e86b8"
name = "golang.org/x/sys"
packages = ["unix"]
pruneopts = "UT"
revision = "6ec70d6a5542cba804c6d16ebe8392601a0b7b60"
[[projects]]
digest = "1:cbc72c4c4886a918d6ab4b95e347ffe259846260f99ebdd8a198c2331cf2b2e9"
name = "gopkg.in/go-playground/validator.v8"
packages = ["."]
pruneopts = "UT"
revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf"
version = "v8.18.2"
[[projects]]
digest = "1:4d2e5a73dc1500038e504a8d78b986630e3626dc027bc030ba5c75da257cdb96"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "UT"
revision = "51d6538a90f86fe93ac480b35f37b2be17fef232"
version = "v2.2.2"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/gin-gonic/gin",
]
solver-name = "gps-cdcl"
solver-version = 1

View File

@@ -0,0 +1,33 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/gin-gonic/gin"
version = "1.4.0"
[prune]
go-tests = true
unused-packages = true

View File

@@ -0,0 +1,102 @@
package common
import (
"crypto/md5"
"encoding/hex"
"fmt"
"ginDemo/config"
"github.com/gin-gonic/gin"
"net/http"
"net/url"
"sort"
"strconv"
"time"
)
// 打印
func Print(i interface{}) {
fmt.Println("---")
fmt.Println(i)
fmt.Println("---")
}
// 返回JSON
func RetJson(code, msg string, data interface{}, c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : msg,
"data" : data,
})
c.Abort()
}
// 获取当前时间戳
func GetTimeUnix() int64 {
return time.Now().Unix()
}
// MD5 方法
func MD5(str string) string {
s := md5.New()
s.Write([]byte(str))
return hex.EncodeToString(s.Sum(nil))
}
// 生成签名
func CreateSign(params url.Values) string {
var key []string
var str = ""
for k := range params {
if k != "sn" {
key = append(key, k)
}
}
sort.Strings(key)
for i := 0; i < len(key); i++ {
if i == 0 {
str = fmt.Sprintf("%v=%v", key[i], params.Get(key[i]))
} else {
str = str + fmt.Sprintf("&%v=%v", key[i], params.Get(key[i]))
}
}
// 自定义签名算法
sign := MD5(MD5(str) + MD5(config.APP_NAME + config.APP_SECRET))
return sign
}
// 验证签名
func VerifySign(c *gin.Context) {
var method = c.Request.Method
var ts int64
var sn string
var req url.Values
if method == "GET" {
req = c.Request.URL.Query()
sn = c.Query("sn")
ts, _ = strconv.ParseInt(c.Query("ts"), 10, 64)
} else if method == "POST" {
c.Request.ParseForm()
req = c.Request.PostForm
sn = c.PostForm("sn")
ts, _ = strconv.ParseInt(c.PostForm("ts"), 10, 64)
} else {
RetJson("500", "Illegal requests", "", c)
return
}
exp, _ := strconv.ParseInt(config.API_EXPIRY, 10, 64)
// 验证过期时间
if ts > GetTimeUnix() || GetTimeUnix() - ts >= exp {
RetJson("500", "Ts Error", "", c)
return
}
// 验证签名
if sn == "" || sn != CreateSign(req) {
RetJson("500", "Sn Error", "", c)
return
}
}

View File

@@ -0,0 +1,9 @@
package config
const (
PORT = ":8080"
APP_NAME = "ginDemo"
APP_SECRET = "6YJSuc50uJ18zj45"
API_EXPIRY = "120"
)

View File

@@ -0,0 +1,15 @@
package v1
import "github.com/gin-gonic/gin"
func AddMember(c *gin.Context) {
// 获取 Get 参数
name := c.Query("name")
price := c.DefaultQuery("price", "100")
c.JSON(200, gin.H{
"v1" : "AddMember",
"name" : name,
"price" : price,
})
}

View File

@@ -0,0 +1,15 @@
package v1
import "github.com/gin-gonic/gin"
func AddProduct(c *gin.Context) {
// 获取 Get 参数
name := c.Query("name")
price := c.DefaultQuery("price", "100")
c.JSON(200, gin.H{
"v1" : "AddProduct",
"name" : name,
"price" : price,
})
}

View File

@@ -0,0 +1,9 @@
package v2
import "github.com/gin-gonic/gin"
func AddMember(c *gin.Context) {
c.JSON(200, gin.H{
"v2" : "AddMember",
})
}

View File

@@ -0,0 +1,17 @@
package v2
import (
"github.com/gin-gonic/gin"
)
func AddProduct(c *gin.Context) {
// 获取 Get 参数
name := c.Query("name")
price := c.DefaultQuery("price", "100")
c.JSON(200, gin.H{
"v2" : "AddProduct",
"name" : name,
"price" : price,
})
}

View File

@@ -0,0 +1,14 @@
package main
import (
"ginDemo/config"
"ginDemo/router"
"github.com/gin-gonic/gin"
)
func main() {
gin.SetMode(gin.ReleaseMode) // 默认为 debug 模式,设置为发布模式
engine := gin.Default()
router.InitRouter(engine) // 设置路由
engine.Run(config.PORT)
}

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