Kyle's Notebook

Go Cheat Sheet

Word count: 22.2kReading time: 107 min
2021/06/13

Go Cheat Sheet

参考:An overview of Go syntax and features.

文中大量的代码都是取自 A Tour of Go(非常好的 Go 入门教程)。

如果是刚入门,非常建议先从这份教程上手。

简而言之,就是:

  • 命令式语言

  • 静态类型

  • 类似 C 语言的语法标记(更少的括号,没有分号)和 Oberon-2 的结构

  • 编译为本地代码(区别于 JVM)

  • 没有类,但有带方法的结构体

  • 接口

  • 没有实现继承,但有 类型嵌入

  • 函数是一等公民

  • 函数可以返回多个值

  • 闭包

  • 指针(不支持算术运算)

  • 内置并发原语:Goroutines 和 Channels

安装

1
2
3
4
5
6
7
8
9
10
11
12
wget -c https://dl.google.com/go/go1.17.4.linux-amd64.tar.gz -O /usr/local/src/go1.17.4.linux-amd64.tar.gz
tar -zxvf /usr/local/src/go1.17.4.linux-amd64.tar.gz -C /usr/local/

/usr/local/go/bin/go version

vim ~/.zshrc
export GOPATH=/opt/go
export GOROOT=/usr/local/go
export GOARCH=386
export GOBIN=$GOROOT/bin/
export GOTOOLS=$GOROOT/pkg/tool/
export PATH=$PATH:$GOBIN:$GOTOOLS

其它常用的环境变量:

img

测试运行:test/hello.go

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("Hello, World!")
}
1
go run hello.go

测试 Go module:test-module/main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"github.com/valyala/fasthttp"
"go.uber.org/zap"
)

var logger *zap.Logger

func init() {
logger, _ = zap.NewProduction()
}

func fastHTTPHandler(ctx *fasthttp.RequestCtx) {
logger.Info("hello, go module", zap.ByteString("uri", ctx.RequestURI()))
}

func main() {
fasthttp.ListenAndServe(":8081", fastHTTPHandler)
}
1
2
3
4
go mod init github.com/yipwinghong/test-module
go mod tidy
go build main.go
./main

远程开发插件

打开 Visual Studio Code,在 Extentions 搜索并安装 Remote - SSH

配置服务器连接信息 ~/.ssh/config

1
2
3
4
5
6
# Read more about SSH config files: https://linux.die.net/man/5/ssh_config
Host wsl
HostName 127.0.0.1
Port 25
User root
IdentityFile ~/.ssh/id_rsa

配置好本地到服务器的 SSH public key,并尝试连接 Connect to Host in Current Window

Extention 可安装用于远程项目的插件。

此时在 Explorer 可以打开 target server 上的项目目录 Open Folder。或直接 clone GitHub 上的项目到本地(Clone Repository, 需要授权)。

代码格式

建议:

  • 代码都必须用 gofmt 进行格式化。

  • 运算符和操作数之间要留空格。

  • 建议一行代码不超过 120 个字符,超过部分,请采用合适的换行方式换行。例外场景:import 行、工具自动生成的代码、带 tag 的 struct 字段。

  • 文件长度不能超过 800 行。

  • 函数长度不能超过 80 行。

  • import 规范代码都必须用 goimports 进行格式化(建议将代码 Go 代码编辑器设置为:保存时运行 goimports)。不要使用相对路径引入包,例如 import …/util/net ;包名称与导入路径的最后一个目录名不匹配时,或者多个相同包名冲突时,则必须使用导入别名。

  • 导入的包建议进行分组,匿名包的引用使用一个新的分组,并对匿名包引用进行说明。

操作符

不支持前置的自增(++)自减(–)。

算术

操作符 描述
+
-
*
/
% 求余
& 按位与
| 按位或
^ 按位异或
&^ 按位清零(且非)
<< 左移
>> 右移

比较

操作符 描述
== 等于
!= 不等于
< 小于
<= 小于等于
> 大于
>= 大于等于

逻辑

操作符 描述
&& 逻辑且
|| 逻辑或
! 逻辑非

其他

操作符 描述
& 取址 / 创建指针
* 取值
<- 发送 / 接收 (见下文“Channels”)

声明

变量

类型在标识符后面,且赋值时可自动推断类型。

1
2
3
4
5
6
var foo int                     // 无初始化的声明
var foo int = 42 // 带初始化的声明
var foo, bar int = 42, 1302 // 一次声明并初始化多个变量
var foo = 42 // 类型省略(推断)
foo := 42 // 初始化且赋值,只能用在 func 主体中,省略 var 关键字,类型是隐式推断的
const constant = "This is a constant"

交换两个变量的值(在同一个语句中可对多个变量同时赋值)。

1
2
3
a := 1
b := 2
a, b = b, a

变量声明尽量放在变量第一次使用的前面,遵循就近原则。

变量名称首字母为大写时表示该变量为可导出的全局变量,应尽量少用。

通过参数传递,使每个函数都是“无状态”的,可以减少耦合,也方便分工和单元测试。

常量

如果魔法数字出现超过两次,则禁止使用,改用常量代替。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const Pi = 3.14

// 未指定类型的常量由上下文来决定其类型。
const (
// 将 1 左移 100 位来创建一个非常大的数字
// 即这个数的二进制是 1 后面跟着 100 个 0
Big = 1 << 100
// 再往右移 99 位,即 Small = 1 << 1,或者说 Small = 2
Small = Big >> 99
)

func needInt(x int) int {
return x*10 + 1
}

func needFloat(x float64) float64 {
return x * 0.1
}

func TestConst() {
fmt.Println(needInt(Small))
fmt.Println(needFloat(Small))
fmt.Println(needFloat(Big))
}

快速设置连续的值:通常用于系统内部变量,不建议在业务代码中使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// iota 可用于递增数字,从 0 开始 
const (
Monday = 1 + iota // 1
Tuesday // 2
Wednesday // 3
)

const (
Readable = 1 << iota // 1
Writable // 2
Executable // 4
)

func TestConstantTry(t *testing.T) {
t.Log(Monday, Tuesday)
}

func TestConstantTry1(t *testing.T) {
a := 1 //0001
t.Log(a&Readable == Readable, a&Writable == Writable, a&Executable == Executable)
}

对于未导出的顶层常量和变量,使用 _ 作为前缀。

1
2
3
4
5
6
7
8
9
10
11
// bad
const (
defaultHost = "127.0.0.1"
defaultPort = 8080
)

// good
const (
_defaultHost = "127.0.0.1"
_defaultPort = 8080
)

初始化

内建函数 newmake 可用于分配内存。

new

不会执行初始化,只是将内存置零。

new(T) 为类型为 T 的项分配已置零的内存空间并返回地址,即类型为 *T 的值。

由于内存已置零,在设计数据结构时就不需要初始化即可工作。

1
2
3
4
5
6
7
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}

p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer

当初始化工作比较复杂,可考虑创建初始化构造函数、对外提供结构的创建服务。

make

make(T, args) 只用于创建切片、映射和信道,并返回类型为 T(而非 *T)的已初始化(而非置零)的值(而非指针)。比如:

1
make([]int, 10, 100)

即创建长度为 100 的数组空间,并创建长度为 10、容量为 100 的、指向该数组前 10 个元素的切片。

new 的区别:

1
2
3
4
5
6
7
8
9
var p *[]int = new([]int)       // 分配切片结构;*p == nil;基本没用
var v []int = make([]int, 100) // 切片 v 现在引用了一个具有 100 个 int 元素的新数组

// 没必要的复杂:
var p *[]int = new([]int)
*p = make([]int, 100, 100)

// 习惯用法:
v := make([]int, 100)

函数

参数总是值传递,对于引用类型(切片、字典、信道),都会复制其值再传入函数,不会复制它们引用的底层数据(浅拷贝)。

如要修改传入参数的值,应以复制指针传入函数(指向同一块内存)。

函数本身可作为变量的值,也可作为参数和返回值(可返回多个值)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func f() {}

func f(param1 string, param2 int) {}

func f(param1, param2 int) {}

func f() int {
return 42
}

func f() (int, string) {
return 42, "foobar"
}

func f() (n int, s string) {
n = 42
s = "foobar"
return
}

var x, str = f()

当函数中需要使用到多个变量时,可在函数开始处使用 var 声明。

在函数外部声明必须使用 var,不要采用 := ,容易踩到变量的作用域的问题。

1
2
3
4
var (
Width int
Height int
)

使用建议:

  • 传入变量和返回变量以小写字母开头。

  • 函数参数个数不超过 5 个、返回值不超过 3 个,否则使用结构体。

  • 函数应按粗略的调用顺序排序。同一文件中的函数应按接收者分组。

  • 尽量采用值传递,而非指针传递(对于较大的结构体可使用指针)。

  • 传入参数是 map、slice、chan、interface ,不要传递指针。

高阶函数

函数作为参数:

1
2
3
4
5
6
7
8
9
type operate func(x, y int) int


func calculate(x int, y int, op operate) (int, error) {
if op == nil {
return 0, errors.New("invalid operation")
}
return op(x, y), nil
}

函数作为返回值:

1
2
3
4
5
6
7
8
9
10
type calculateFunc func(x int, y int) (int, error)

func genCalculator(op operate) calculateFunc {
return func(x int, y int) (int, error) {
if op == nil {
return 0, errors.New("invalid operation")
}
return op(x, y), nil
}
}

闭包

作为参数的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
// 把一个函数赋给 add
add := func(a, b int) int {
return a + b
}
// 通过 add 调用函数
fmt.Println(add(3, 4))
}

// 闭包,词法作用域:函数可以访问定义函数作用域内的值
func scope() func() int{
outer_var := 2
return func() int {
return outer_var
}
}

func another_scope() func() int{
// 无法通过编译,因为在这个作用域内没有定义 outer_var 和 foo
outer_var = 444
return foo
}

闭包:其内部逻辑不完整、其中一部分由自由变量构成。在闭包函数定义时状态未知,即动态生成部分程序逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func intSeq() func() int {
// 由于被返回的匿名函数引用,i 的值会一直保持。
i := 0
return func() int {
i++
return i
}
}


func genCalculator(op operate) calculateFunc {
return func(x int, y int) (int, error) {
if op == nil {
return 0, errors.New("invalid operation")
}
return op(x, y), nil
}
}

可变参数

即在最后一个参数的类型名称之前加上 ...,可指示接收零或多个参数。该函数的调用方式与其他函数一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
fmt.Println(adder(1, 2, 3)) // 6
fmt.Println(adder(9, 9)) // 18

nums := []int{10, 20, 30}
fmt.Println(adder(nums...)) // 60
}

func adder(args ...int) int {
total := 0
for _, v := range args { // 迭代访问参数
total += v
}
return total
}

递归

1
2
3
4
5
6
func fact(n int) int {
if n == 0 {
return 1
}
return n * fact(n-1)
}

处理集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
func Index(vs []string, t string) int {
for i, v := range vs {
if v == t {
return i
}
}
return -1
}
// Index(strs, "pear")

func Include(vs []string, t string) bool {
return Index(vs, t) >= 0
}
// Include(strs, "grape")

func Any(vs []string, f func(string) bool) bool {
for _, v := range vs {
if f(v) {
return true
}
}
return false
}
// Any(strs, func(v string) bool {
// return strings.HasPrefix(v, "p")
// })

func All(vs []string, f func(string) bool) bool {
for _, v := range vs {
if !f(v) {
return false
}
}
return true
}
// All(strs, func(v string) bool {
// return strings.HasPrefix(v, "p")
// })

func Filter(vs []string, f func(string) bool) []string {
vsf := make([]string, 0)
for _, v := range vs {
if f(v) {
vsf = append(vsf, v)
}
}
return vsf
}
// Filter(strs, func(v string) bool {
// return strings.Contains(v, "e")
// })

func Map(vs []string, f func(string) string) []string {
vsm := make([]string, len(vs))
for i, v := range vs {
vsm[i] = f(v)
}
return vsm
}
// Map(strs, strings.ToUpper)

控制结构

if

1
2
3
4
5
6
7
8
9
10
11
12
if x > 10 {
return x
} else if x == 10 {
return 10
} else {
return -x

// 可以在条件前放置一个语句:如果 a < 42,则把 b + c 赋值 a
if a := b + c; a < 42 {
return a
} else {
return a - 42
1
2
3
4
5
6
// 在 if 中输入断言 
var val interface{} = "foo"

if str, ok := val.(string); ok {
fmt.Println(str)
}

if 接受初始化语句,约定如下方式建立局部变量。

1
2
3
4
if err := loadConfig(); err != nil {
// error handling
return err
}

for

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
for i := 1; i < 10; i++ {
}

// 相当于 while 循环,也可以省略分号。
for ; i < 10; {
}

for i < 10 {
}

// 省略条件 ~ while (true)
for {
}

// 在当前循环中使用 break/continue
// 在外循环上使用带标签的 break/continue

for i := 0; i < 2; i++ {
for j := i + 1; j < 3; j++ {
if i == 0 {
continue here
}
fmt.Println(j)
if j == 2 {
break
}
}
}

for i := 0; i < 2; i++ {
for j := i + 1; j < 3; j++ {
if j == 1 {
continue
}
fmt.Println(j)
if j == 2 {
break there
}
}
}

range

range 表达式可用于切片/数组、字符串、字典、信道。

除了访问信道时可能会阻塞,其它情况下只会在 for 语句开始执行时被求值一次,无论后面有多少次迭代。

range 表达式求值结果会被复制,即被迭代的对象是表达式结果值的副本,实际迭代时不会使用原值(具体影响视乎结果值是值类型或引用类型)。

1
2
3
4
5
6
7
8
9
10
11
numbers2 := [...]int{1, 2, 3, 4, 5, 6}
maxIndex2 := len(numbers2) - 1
for i, e := range numbers2 {
if i == maxIndex2 {
numbers2[0] += e
} else {
numbers2[i+1] += e
}
}
fmt.Println(numbers2)
// 7, 3, 5, 7, 9, 11

每次迭代都会执行赋值,当切片元素为 string 等类型,赋值会涉及内存拷贝、开销较大,可使用下标访问来优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// bad
for _, v := range s{
if v == "xxx" {
// ...
}
}


// good
for i := range s {
if s[i] == "xxx" {
// ...
}
}

switch

每个分支不需要加上 break,但必须有 default 分支。

1
2
3
4
5
6
7
8
9
10
11
// switch 声明
switch operatingSystem {
case "darwin":
fmt.Println("Mac OS Hipster")
// case 自动中断,默认情况下没有失败(不需要 break)
case "linux":
fmt.Println("Linux Geek")
default:
// Windows, BSD, ...
fmt.Println("Other")
}

基本用法:

1
2
3
4
5
6
7
8
9
10
11
// 与 for 和 if 一样,可以在 switch 值之前加一个赋值语句,并判断执行结果。 
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
1
2
3
4
5
6
7
8
9
10
// 还可以在 switch case 中进行比较(if...else...的优雅写法)
number := 42
switch {
case number < 42:
fmt.Println("Smaller")
case number == 42:
fmt.Println("Equal")
case number > 42:
fmt.Println("Greater")
}
1
2
3
4
5
6
// case 可以用逗号分隔的列表显示 
var char byte = '?'
switch char {
case ' ', '?', '&', '=', '#', '+', '%':
fmt.Println("Should escape")
}
1
2
3
4
5
6
7
8
9
10
11
// 类型判断
whatAmI := func(i interface{}) {
switch t := i.(type) {
case bool:
fmt.Println("I'm a bool")
case int:
fmt.Println("I'm an int")
default:
fmt.Printf("Don't know type %T\n", t)
}
}

defer

延迟执行(函数返回时逆序取出执行),且确保不受 panic 影响。

被推迟函数的实参在出现 defer 语句时就会求值,而不是 defer 调用时才求值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func trace(s string) string {
fmt.Println("entering:", s)
return s
}

func un(s string) {
fmt.Println("leaving:", s)
}

func a() {
defer un(trace("a"))
fmt.Println("in a")
}

func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}

func main() {
b()
}

// entering: b
// in b
// entering: a
// in a
// leaving: a
// leaving: b

常用于解锁,释放资源等(可避免在多个分支都需要释放时忘记操作)。

当创建资源时,应紧跟 defer 释放资源。

defer 在 Go1.14 版本中性能大幅提升,即使在性能敏感型的业务中损耗也可忽略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func createFile(p string) *os.File {
fmt.Println("creating")
f, err := os.Create(p)
if err != nil {
panic(err)
}
return f
}

func writeFile(f *os.File) {
fmt.Println("writing")
fmt.Fprintln(f, "data")

}

func closeFile(f *os.File) {
fmt.Println("closing")
err := f.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}

func TestDefer(t *testing.T) {
f := createFile("/tmp/defer.txt")
defer closeFile(f)
writeFile(f)
}

不要在 for 循环里面使用 deferdefer 只有在函数退出时才会执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// bad
for file := range files {
fd, err := os.Open(file)
if err != nil {
return err
}
defer fd.Close()
// normal code
}

// good
for file := range files {
func() {
fd, err := os.Open(file)
if err != nil {
return err
}
defer fd.Close()
// normal code
}()
}

先判断是否错误,再 defer 释放资源,比如:

1
2
3
4
5
6
rep, err := http.Get(url)
if err != nil {
return err
}

defer resp.Body.Close()

goto

业务代码禁止使用 goto,框架或其他底层源码尽量不用。

select

用于操作多个信道,具体用法参考 信道

只要存在 default 就不会阻塞。

永久阻塞:

1
select {}

快速检错:利用 default 优先级较低的特性,只有在其它 case 不执行时才会被只需。

1
2
3
4
5
select {
case err = <-errCh:
// 处理错误
default:
}

数据类型

Go 是静态强类型语言,不支持隐式类型转换(包括别名与原类型之间)。

所有 Go 预先声明的标识符都在 builtin 包中定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool        // 默认 false

string // 默认 ""

int int8 int16 int32 int64 // 默认 0
uint uint8 uint16 uint32 uint64 uintptr

byte // uint8 的别名

rune // alias int32 ~= a character (Unicode code point) - very Viking

float32 float64

complex64 complex128

func TestZeorValue(t *testing.T) {
var i int
var f float64
var b bool
var s string
fmt.Printf("%v %v %v %q\n", i, f, b, s)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type MyInt int64

func TestImplicit(t *testing.T) {
var a int32 = 1
var b int64
b = int64(a)
var c MyInt
c = MyInt(b)
t.Log(a, b, c)
}

func TestPoint(t *testing.T) {
a := 1
aPtr := &a
//aPtr = aPtr + 1
t.Log(a, aPtr)
t.Logf("%T %T", a, aPtr)
}

func TestString(t *testing.T) {
var s string
t.Log("*" + s + "*") // 初始化零值是“”
t.Log(len(s))
}

字符串

string 是基本数据类型(不是引⽤或指针)。

  • string 是只读的 byte 切片,len 函数可以求它所包含的 byte 数。

  • stringbyte 数组可以存放任何数据。

  • 常用的字符串包见:strings, strcov

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
func TestString(t *testing.T) {
var s string
t.Log(s) //初始化为默认零值“”
s = "hello"
t.Log(len(s))
// s[1] = '3' // string是不可变的byte slice
// s = "\xE4\xB8\xA5" // 可以存储任何二进制数据
s = "\xE4\xBA\xBB\xFF"
t.Log(s)
t.Log(len(s))
s = "中"
t.Log(len(s)) // 是 byte 数

c := []rune(s)
t.Log(len(c))
// t.Log("rune size:", unsafe.Sizeof(c[0]))
t.Logf("中 unicode %x", c[0])
t.Logf("中 UTF8 %x", s)
}

func TestStringToRune(t *testing.T) {
s := "中华人民共和国"
for _, c := range s {
t.Logf("%[1]c %[1]x", c)
}
}

func TestStringFn(t *testing.T) {
s := "A,B,C"
parts := strings.Split(s, ",")
for _, part := range parts {
t.Log(part)
}
t.Log(strings.Join(parts, "-"))
}

func TestConv(t *testing.T) {
s := strconv.Itoa(10)
t.Log("str" + s)
if i, err := strconv.Atoi("10"); err == nil {
t.Log(10 + i)
}
}

判空:字符串可以为 "",但不会为 nil

1
2
3
4
5
6
7
8
9
// bad
if s == "" {
// normal code
}

// good
if len(s) == 0 {
// normal code
}

[]byte/string 相等比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
// bad
var s1 []byte
var s2 []byte
...
bytes.Equal(s1, s2) == 0
bytes.Equal(s1, s2) != 0

// good
var s1 []byte
var s2 []byte
...
bytes.Compare(s1, s2) == 0
bytes.Compare(s1, s2) != 0

string 常用于需要比较、不需要 nil 串的场景。

[]byte 则适用于需要修改字符串(支持粒度为字节),需要使用 nil 表示的场景,而且 [] 支持切片操作。

1
2
3
4
5
6
// string to []byte
s1 := "hello"
b := []byte(s1)

// []byte to string
s2 := string(b)
1
2
3
4
5
6
7
8
9
10
11
12
13
func String2Bytes(s string) []byte {
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}

func Bytes2String(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}

要复制数据,使用 copy 性能比 append 高:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var (
src = make([]byte, 512)
dst = make([]byte, 512)
)

// bad
for n := 0; n < b.N; n++ {
rand.Read(src)
copy(dst, src)
}

//good
for n := 0; n < b.N; n++ {
rand.Read(src)
dst = append(dst, src...)
}

格式化(优先使用 strconv 而不是 fmt):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
p := point{1, 2}
fmt.Printf("%v\n", p)
fmt.Printf("%+v\n", p)
fmt.Printf("%#v\n", p)
fmt.Printf("%T\n", p)
fmt.Printf("%t\n", true)
fmt.Printf("%d\n", 123)
fmt.Printf("%b\n", 14)
fmt.Printf("%c\n", 33)
fmt.Printf("%x\n", 456)
fmt.Printf("%f\n", 78.9)
fmt.Printf("%e\n", 123400000.0)
fmt.Printf("%E\n", 123400000.0)
fmt.Printf("%s\n", "\"string\"")
fmt.Printf("%q\n", "\"string\"")
fmt.Printf("%x\n", "hex this")
fmt.Printf("%p\n", &p)
fmt.Printf("|%6d|%6d|\n", 12, 345)
fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45)
fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45)
fmt.Printf("|%6s|%6s|\n", "foo", "b")
fmt.Printf("|%-6s|%-6s|\n", "foo", "b")
s := fmt.Sprintf("a %s", "string")
fmt.Println(s)
fmt.Fprintf(os.Stderr, "an %s\n", "error")

string 表示的是不可变的字符串变量,对 string 的修改是比较重的操作,基本上都需要重新申请内存。

在需要修改时多使用 []byte(JSON 序列化除外,[]byte 序列化会被 base64,引起跨语言解析失败)。

类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

// 替代语法
i := 42
f := float64(i)
u := uint(f)

f, _ := strconv.ParseFloat("1.234", 64)
i, _ := strconv.ParseInt("123", 0, 64)
d, _ := strconv.ParseInt("0x1c8", 0, 64)
u, _ := strconv.ParseUint("789", 0, 64)
k, _ := strconv.Atoi("135")
_, e := strconv.Atoi("wat")

对于未知的、不被认可的、无法展示的字符,会被替换为 ‘�’(Unicode 码点为 ‘U+FFFD’)。

类型声明

声明语句创建了新的类型名称,和现有类型底层结构相同。

新类型提供了一个方法用来分隔不同概念的类型,即使底层类型相同也不兼容。

1
2
type Celsius float64    // 摄氏温度
type Fahrenheit float64 // 华氏温度

对于类型别名 type Celsius = float64,等号两边为相同的类型。

两者不可以被相互比较或混在一个表达式运算。刻意区分类型可避免无意中使用不同单位的温度混合计算导致的错误。

而需要类似 Celsius(t)Fahrenheit(t) 形式的显式转型操作才能将 float64 转为对应的类型。

类型断言

类型断言表达式 v, ok := x.(T)x 表示要被判断类型的值,当下的类型必须是任意的接口类型(通常是空接口 interface{})。

1
2
3
4
5
6
7
8
9
var val interface{} = "foo"

// 判断 val 是否为 string 类型,是则赋值给 string 类型变量 str,否则 panic
str := val.(string)

// 判断 val 是否为 string 类型,是则赋值给 string 类型变量 str,否则 ok 为 false,不会 panic。
if str, ok := val.(string); ok {
fmt.Println(str)
}

由于其单个返回值针对不正确的类型将产生 panic,建议始终使用 “comma ok”的惯用法。

指针

指针保存值的内存地址,不支持运算。

1
2
3
4
5
6
7
p := Vertex{1, 2}       // p 是一个 Vertex
q := &p // q 是一个指向 Vertex 的指针
r := &Vertex{1, 2} // r 是一个指向 Vertex 的指针

// 指向顶点的指针的类型:*Vertex

var s *Vertex = new(Vertex) // new 创建一个指向新结构体实例的指针

带指针参数的函数必须接受一个指针:

1
2
3
4
5
6
7
8
func ScaleFunc(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

var v Vertex
ScaleFunc(v, 5) // 编译错误!
ScaleFunc(&v, 5) // OK

以指针为接收者的方法被调用时,接收者既能为值又能为指针:

1
2
3
4
5
6
7
8
9
10
11
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

var v Vertex
v.Scale(5) // OK
p := &v
p.Scale(10) // OK

// Go 会将语句 v.Scale(5) 解释为 (&v).Scale(5)。

使用指针接收者:

  • 方法可修改其接收者指向的值。

  • 避免在每次调用方法时复制该值,若值的类型为大型结构体时会更高效。

指针用于寻址,但以下的值不可寻址(即不可变、不安全的临时结果):

  • 常量的值。

  • 基本类型值的字面量。

  • 算术操作的结果值。

  • 对各种字面量的索引表达式和切片表达式的结果值(例外:对切片字面量的索引结果值可寻址)。

  • 对字符串变量的索引表达式和切片表达式的结果值。

  • 对字典变量的索引表达式的结果值。

  • 函数字面量和方法字面量,以及对它们的调用表达式的结果值。

  • 结构体字面量的字段值,也就是对结构体字面量的选择表达式的结果值。

  • 类型转换表达式的结果值。

  • 类型断言表达式的结果值。

  • 接收表达式的结果值。

结构

结构是一种类型,也是字段的集合。可附带方法,不支持继承。

对于需要序列化的字段,请以大写字母开头,否则 json.Marshal 时会被忽略。

1
2
3
4
5
6
7
type Vertex struct {
X, Y int
}

var v = Vertex{1, 2}
var v = Vertex{X: 1, Y: 2}
var vs = []Vertex{{1,2},{5,2},{5,5}} // 初始化结构切片

声明和初始化格式建议采用多行。

1
2
3
4
5
6
7
8
9
type User struct{
Username string
Email string
}

user := User{
Username: "colin",
Email: "colin404@foxmail.com",
}

在初始化结构引用时,请使用 &T{} 代替 new(T),以使其与结构体初始化一致。

1
2
3
4
5
6
// bad
sptr := new(T)
sptr.Name = "bar"

// good
sptr := &T{Name: "bar"}

方法

支持值方法与指针方法。

  • 值方法的接收者是该方法所属的类型值的一个副本(修改不会体现在原值上,除非使用切片或字典等引用类型的别名类型);指针方法的接收者是该方法所属的基本类型值的指针值的一个副本(在方法内可修改该副本指向的值)。

  • 一个自定义数据类型的方法集合中仅包含它的所有值方法,而该类型的指针类型的方法集合包含前者的所有方法,包括所有值方法和所有指针方法。

  • 如果基本类型和它的指针类型的方法集合不同,则其具体实现的接口类型的数量就也会有差异(可能两者都是 0,或一个指针类型实现了某接口类型,但其基本类型却不一定能作为该接口的实现类型)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// 对于修改结构字段的方法,参数类型为指向 Struct 的指针(不会为方法调用复制整个结构的值)。
func (v *Vertex) Add(n float64) {
v.X += n
v.Y += n
}

// 通过指针访问:
v := Vertex{1, 2}
v.Abs()
p := &v
p.X = 1e9
fmt.Println(v)

除了结构以外,几乎任何(自定义的)类型都能添加方法:

1
2
3
4
5
6
7
8
9
10
11
// counter b
// type Counter struct {
// n int
// }

type Counter int

func (c *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c.n++
fmt.Fprintf(w, "counter = %d\n", c.n)
}

建议方法接收器以类名第一个英文首字母的小写命名(不能采用 me、this、self 这类易混淆名称),且在函数超过 20 行时不要用单字符。

对于需要外部访问的字段,可提供对外暴露的方法(Setter、Getter 方法,不建议以 GetXxxSetXxx 命名):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type User struct {
name string
age int
}

// bad
func (u User) GetName string {
return u.name
}

// good
func (u User) Name string {
return u.name
}

匿名结构

比使用 map[string]interface{} 开销更低、更安全.

1
2
3
point := struct {
X, Y int
}{1, 2}

扩展(不支持访问子类的字段、方法,无法重载等):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Pet struct {}

func (p *Pet) Speak() {
fmt.Print("...")
}

func (p *Pet) SpeakTo(host string) {
p.Speak()
fmt.Println(" ", host)
}

type Dog struct {
Pet
}

func (d *Dog) Speak() {
fmt.Print("Wang!")
}

func TestDog(t *testing.T) {
dog := new(Dog)
dog.SpeakTo("Chao")
}

嵌入

Go 中没有子类化,但有接口和结构嵌入。特点是更灵活,组合相比继承的优势是非侵入性,不会破坏类型的封装、加重类型之间的耦合。

其中嵌入结构的同名方法会被隐藏:类似继承父类的子类会重写父类方法,但仍可以通过链式调用找到目标方法。

如果处于同一个层级的多个嵌入字段拥有同名的字段或方法,在被嵌入类型的值处,选择此名称时就会引发编译错误,因为编译器无法确定被选择的成员。

嵌入式类型应位于结构体内的字段列表的顶部,并且建议有一个空行将嵌入式字段与常规字段分隔开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ReadWriter 的实现必须同时满足 Reader 和 Writer 
type ReadWriter interface {
Reader
Writer
}

// Server 暴露了 Logger 拥有的所有方法
type Server struct {
*log.Logger

Host string
Port int
}

// 初始化嵌入类型 。
server := &Server{"localhost", 80, log.New(...)}

// 在嵌入式结构上实现的方法被传递 。
server.Log(...) // calls server.Logger.Log(...)

// 嵌入类型的字段名称是其类型名称(在本例中为 Logger)。
var logger *log.Logger = server.Logger

接口

接口类型无法被值化,在赋予它实际的值(实现)之前,其值一定是 nil,即接口类型的零值。

接口是非侵入性的,实现不依赖于接口定义,因此接口的定义可以包含在接口使用者包内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Programmer interface {
WriteHelloWorld() string
}

type GoProgrammer struct {
name string
}

// 如果类型实现了所有必需的方法,则类型隐式地满足接口
func (g *GoProgrammer) WriteHelloWorld() string {
return "fmt.Println(\"Hello World\")"
}

func TestClient(t *testing.T) {
var p Programmer
p = new(GoProgrammer)
t.Log(p.WriteHelloWorld())
}

可在结构中直接嵌套显式写入该接口,表示该接口实现了该接口。但只是起提示作用,非必须。

在 Go 中数据结构只需要实现了接口的一个方法,就隐式地实现了该接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
type Service interface { 
GetFoo() Foo
}

type DemoService struct {
Service
}

func (s *DemoService) GetFoo() Foo {
return Foo{
Name: "i am foo",
}
}

接口的嵌入不允许方法间的隐藏,组合的方法有同名的方法则会发生编译错误。

建议使用更小的接口,并通过小接口之间的组合扩展程序、增加灵活性。比如 io.ReadWriteCloser 就是由 io.Readerio.Writerio.Closer 组成(都只有一个方法)。

多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type Code string

type Programmer interface {
WriteHelloWorld() Code
}

type GoProgrammer struct {
}

func (p *GoProgrammer) WriteHelloWorld() Code {
return "fmt.Println(\"Hello World!\")"
}

type JavaProgrammer struct {
}

func (p *JavaProgrammer) WriteHelloWorld() Code {
return "System.out.Println(\"Hello World!\")"
}

func writeFirstProgram(p Programmer) {
fmt.Printf("%T %v\n", p, p.WriteHelloWorld())
}

func TestPolymorphism(t *testing.T) {
goProg := &GoProgrammer{}
javaProg := new(JavaProgrammer)
writeFirstProgram(goProg)
writeFirstProgram(javaProg)
}

空接口

空接口可表示任何类型,通过断言可将空接口转换为指定类型:v, ok := p.(int)(可类比 Java 的 Object、C/C++ 的 void * 指针)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func DoSomething(p interface{}) {
switch v := p.(type) {
case int:
fmt.Println("Integer", v)
case string:
fmt.Println("String", v)
default:
fmt.Println("Unknow Type")
}
}

func TestEmptyInterfaceAssertion(t *testing.T) {
DoSomething(10)
DoSomething("10")
}

由于编译过程无法检查 interface{} 的转换,只能在运行时检查,小心引起 panic

完整性检查

编译器没有严格检查其是否实现了 interface 包含的所有方法。

要实现强验证,可声明 _ 变量把 nil 的空指针转换成接口类型,如果没有实现其所有的方法,在编译阶段会报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Shape interface {
Sides() int
Area() int
}

type Square struct {
len int
}

func (s *Square) Sides() int {
return 4
}

// func (s *Square) Area() int {
// panic("implement me")
// }

// 要求实现 Shape 实现 Sides、Area 方法,但由于没有实现 Area,该行会报错:
// cannot use (*Square)(nil) (type *Square) as type Shape in assignment:
// *Square does not implement Shape (missing Area method)
var _ Shape = (*Square)(nil)

容器

对于切片和字典,都建议指定容器容量,以便为容器预先分配内存:

1
2
v := make(map[int]string, 4)
v := make([]string, 0, 4)

判空操作(适用于 slice、map、channel):

1
2
3
4
5
6
7
8
9
// bad
if len(slice) = 0 {
// normal code
}

// good
if slice != nil && len(slice) == 0 {
// normal code
}

数组

数组长度是其类型的一部分,因此不能改变大小。

1
2
3
4
5
6
7
8
var a [10]int           // 声明一个长度为 10 的 int 数组。数组长度是类型的一部分
a[3] = 42 // 设值
i := a[3] // 取值

// 声明和初始化
var a = [2]int{1, 2}
a := [2]int{1, 2} // 简写
a := [...]int{1, 2} // elipsis -> 编译器自动计算数组长度

只有相同维数且含有相同元素个数的数组才可以进行比较:所有元素相等才相等。

切片

切片为底层数组的视图(零值为 nil),提供动态大小、灵活的访问方式。

函数传参是值传递,数组参数传入的是整个数组的内容,而切片传入的则是对底层数组的引用。

1
var a []int                              // 声明一个切片 - 类似于数组,但长度未指定(由隐式给出的数组支持) 
1
2
3
4
5
6
7
a := []int{1,2,3,4}
chars := []string{0:"a", 2:"c", 1: "b"} // ["a", "b", "c"]

var b = a[lo:hi] // 创建从索引 lo 到 hi-1 的切片
var b = a[1:4] // 从索引 1 到 3 的切片
var b = a[:3] // 低索引即 0
var b = a[3:] // 高索引即 len(a)
1
2
3
4
5
6
7
// 创建一个切片:第一个参数是长度,第二个是容量(可选)
a = make([]byte, 5, 5)
a = make([]byte, 5)

// 或从数组创建切片
x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // 一个引用 x 存储的切片

append 函数可为切片添加元素,要小心自动分配内存,其返回的可能是新分配的地址。

1
2
a = append(a, 17, 3)                     // 把值附加到切片 a
c := append(a, b...) // 连接切片 a 和 b

从数组中创建的切片的容量等于数组的 容量 - 切片的起始下标len(a) 返回数组/切片的长度,cap(a) 返回数组/切片的容量。

1
2
3
4
b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:] // len(b)=4, cap(b)=4

切片容量可视作透过窗口最多可以看到底层数组中元素的个数,其窗口可以向右扩展直至底层数组的末尾,但无法向左扩展。

复制元素:

1
2
3
4
5
6
7
8
9
10
11
// bad
var b1, b2 []byte
for i, v := range b1 {
b2[i] = v
}
for i := range b1 {
b2[i] = b1[i]
}

// good
copy(b2, b1)

range 遍历:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// i 索引, e 元素
for i, e := range a {
}

for _, e := range a {
}

for i := range a {
}

// Go 1.4 引入了无变量形式,此前如果不使用 i 和 e,则会出现编译错误:
for range time.Tick(time.Second) {
// 每秒执行一次
}

二维切片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建一个井字板(经典游戏)
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}

// 两个玩家轮流打上 X 和 O
board[0][0] = "X"
board[2][2] = "O"
board[1][2] = "X"
board[1][0] = "O"
board[0][2] = "X"

for i := 0; i < len(board); i++ {
fmt.Printf("%s\n", strings.Join(board[i], " "))
}

使用建议:

  • 创建切片时预估容量,避免追加时扩容。

  • 切片拷贝时需要判断时机拷贝元素个数。

  • 谨慎使用多个切片操作同一个数组,以防读写冲突。

字典

非线程安全,在并发中需要加锁,编译时加上竞态检查(-race)。

key 之间通过 ==!= 进行比较,不能以函数、切片、字典类型(包括接口类型 interface{})为 key,

key 类型长度越小,比较效率越高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
m := make(map[string]int, 10)    // cap 为 10,不需要初始化 len

var m = map[interface{}]int{
"1": 1,
[]int{2}: 2, // panic
3: 3,
}

var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967
},
"Google": { // 可省略类型名:
37.42202,
-122.08408
},
}
1
2
3
4
// 访问:m["key"] 不存在则返回零值。
if elem, ok := m["key"]; ok { // 判断键“key”是否存在并取值
fmt.Println(m["key"])
}

如果要直接修改 map 的 value 值,则 value 只能是指针,否则要覆盖原来的值。

1
2
m["key"] = 42
delete(m, "key")

遍历:

1
2
for key, value := range m {
}

函数字典 map[int]func(){} 实现工厂模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func TestMapWithFunValue(t *testing.T) {
m := map[int]func(op int) int{}
m[1] = func(op int) int { return op }
m[2] = func(op int) int { return op * op }
m[3] = func(op int) int { return op * op * op }
t.Log(m[1](2), m[2](2), m[3](2))
}

func TestMapForSet(t *testing.T) {
mySet := map[int]bool{}
mySet[1] = true
n := 3
if mySet[n] {
t.Logf("%d is existing", n)
} else {
t.Logf("%d is not existing", n)
}
mySet[3] = true
t.Log(len(mySet))
delete(mySet, 1)
n = 1
if mySet[n] {
t.Logf("%d is existing", n)
} else {
t.Logf("%d is not existing", n)
}
}

Go 的内置集合中没有 Set,但可以利用 map[type]bool 实现。

错误

error 是可恢复的错误,panic 为不可恢复错误(退出前会执行 defer 的内容)。

可能产生错误的函数通常会声明一个额外的 error 类型返回值(参考 error),一般是最后一个参数。

1
2
3
4
5
6
7
8
9
// bad
func load() (error, int) {
// normal code
}

// good
func load() (int, error) {
// normal code
}

其中 error 类型实现了 error 接口,提供 Error 方法,并可以通过 errors.New("xxx") 创建错误实例。

1
2
3
4
// 内置接口类型 error 是表示错误条件的常规接口,nil 值表示没有错误。 
type error interface {
Error() string
}
1
2
3
var LessThanTwoError = errors.New("n should be not less than 2")

var LargerThenHundredError = errors.New("n should be not larger than 100")

建议应尽早进行错误处理,并尽早返回,减少嵌套。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func GetFibonacci(n int) ([]int, error) {
if n < 2 {
return nil, LessThanTwoError
}
if n > 100 {
return nil, LargerThenHundredError
}
fibList := []int{1, 1}

for i := 2; /*短变量声明 := */ i < n; i++ {
fibList = append(fibList, fibList[i-2]+fibList[i-1])
}
return fibList, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func GetFibonacci1(str string) {
var (
i int
err error
list []int
)
if i, err = strconv.Atoi(str); err != nil {
fmt.Println("Error", err)
return
}
if list, err = GetFibonacci(i); err != nil {
fmt.Println("Error", err)
return
}
fmt.Println(list)
}
1
2
3
4
5
6
7
8
9
10
func TestGetFibonacci(t *testing.T) {
if v, err := GetFibonacci(1); err != nil {
if err == LessThanTwoError {
fmt.Println("It is less.")
}
t.Error(err)
} else {
t.Log(v)
}
}

error 作为函数的值返回,必须对其处理,或将返回值赋值给明确忽略。对于 defer xx.Close() 可以不用显式处理。

1
2
3
4
5
6
7
8
9
func load() error {
// normal code
}

// bad
load()

// good
_ = load()

错误要单独判断,不与其他逻辑组合判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// bad
v, err := foo()
if err != nil || v == nil {
// error handling
return err
}

// good
v, err := foo()
if err != nil {
// error handling
return err
}

if v == nil {
// error handling
return errors.New("invalid value v")
}

判断某种类型的错误:应该使用 Is 方法,可判定错误链上的所有错误是否含有特定错误。

1
2
3
4
5
6
7
8
9
// bad
if err == os.ErrNotExist {

}

// good
if errors.Is(err, os.ErrorExist) {
// ...
}

获取错误链上特定种类的错误:

1
2
3
4
var pathError *os.PathError
if errors.As(err, &pathError) {
// ...
}

错误描述

  • 告诉用户他们可以做什么,而非不能做什么。

  • 声明需求时用 must 而不是 should。比如 must be greater than 0、must match regex ‘[a-z]+’

  • 当声明格式错误时用 must not。比如 must not contain

  • 当声明动作时用 may not。比如 may not be specified when otherField is empty、only name may be specified

  • 引用文字字符串值时在单引号中指示文字。比如 ust not contain ‘…’

  • 当引用另一个字段名称时在反引号中指定该名称。比如 must be greater than request

  • 指定不等时使用单词而不是符号。比如 must be less than 256、must be greater than or equal to 0 (不要用 larger than、bigger than、more than、higher than)。

  • 指定数字范围时尽可能使用包含范围。建议 Go 1.13 以上,error 生成方式为 fmt.Errorf("module xxx: %w", err)

  • 错误描述用小写字母开头,结尾不要加标点符号。

封装异常

实现 error 接口的 Error 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func f1(arg int) (int, error) {
if arg == 42 {
return -1, errors.New("can't work with 42")
}
return arg + 3, nil
}

type argError struct {
arg int
prob string
}

func (e *argError) Error() string {
return fmt.Sprintf("%d - %s", e.arg, e.prob)
}

func f2(arg int) (int, error) {
if arg == 42 {
// 通过 &argError 创建一个 error 结构
return -1, &argError{arg, "can't work with it"}
}
return arg + 3, nil
}

可使用第三方库 errors

panic

panic 是不可恢复的错误,会导致程序终止。比如切片检索越界或类型断言失败等。

panic 被调用后,程序将终止当前函数执行,并开始回溯 goroutine 调用栈、运行任何被 defer 的函数。直到执行至栈顶、程序终止。

由于 defer 是延迟到该语句所在函数执行结束时才执行(而无论结束原因是什么),在 defer 中调用 recovery 将停止回溯过程,并返回传入 panic 的实参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func TestPanicVxExit(t *testing.T) {

defer func() {
// 回溯时只有被推迟函数中的代码在运行,因此 `recover` 只能在被推迟的函数中才有效。
if err := recover(); err != nil {
fmt.Println("recovered from ", err)
}
}()
fmt.Println("Start")

// 退出返回一个非 0 值
panic(errors.New("Something wrong!"))
// os.Exit(-1)
// fmt.Println("End")
}

recover 要慎用:

  • 避免形成僵尸进程,导致 health check 失效。

  • 处理不确定性错误时,让其 crash 是最好的办法(fail fast)。

在 Go 中每个 Goroutine 独立存在,父 Goroutine 一旦开启子 Goroutine,两者就是平等存在,互相不能干扰。

所有 Goroutine 异常需要自己管理,不存在父 Goroutine 捕获子 Goroutine 异常的操作。每个 Goroutine 创建时可使用 defer 和 recover 关键字为其捕获 panic 异常并进行处理,否则任意一个 Goroutine 发生 panic 都会导致整个进程崩溃!

使用建议:

  • 在业务逻辑处理中禁止使用 panic

  • main 包中,只有当程序完全不可运行时使用 panic,例如无法打开文件、无法连接数据库导致程序无法正常运行。

  • main 包中,使用 log.Fatal 来记录错误,由 log 来结束程序,或者将 panic 抛出的异常记录到日志文件中,方便排查问题。

  • 可导出的接口一定不能有 panic

  • 包内建议采用 error 而不是 panic 来传递错误。

进程

调用 Shell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
dateCmd := exec.Command("date")

dateOut, err := dateCmd.Output()
if err != nil {
panic(err)
}
fmt.Println("> date")
fmt.Println(string(dateOut))

grepCmd := exec.Command("grep", "hello")

grepIn, _ := grepCmd.StdinPipe()
grepOut, _ := grepCmd.StdoutPipe()
grepCmd.Start()
grepIn.Write([]byte("hello grep\ngoodbye grep"))
grepIn.Close()
grepBytes, _ := ioutil.ReadAll(grepOut)
grepCmd.Wait()

fmt.Println("> grep hello")
fmt.Println(string(grepBytes))

lsCmd := exec.Command("bash", "-c", "ls -a -l -h")
lsOut, err := lsCmd.Output()
if err != nil {
panic(err)
}
fmt.Println("> ls -a -l -h")
fmt.Println(string(lsOut))

// go run spawning-processes.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
binary, lookErr := exec.LookPath("ls")
if lookErr != nil {
panic(lookErr)
}

args := []string{"ls", "-a", "-l", "-h"}

env := os.Environ()

execErr := syscall.Exec(binary, args, env)
if execErr != nil {
panic(execErr)
}
// go run execing-processes.go
// total 16
// drwxr-xr-x 4 mark 136B Oct 3 16:29 .
// drwxr-xr-x 91 mark 3.0K Oct 3 12:50 ..
// -rw-r--r-- 1 mark 1.3K Oct 3 16:28 execing-processes.go

信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sigs := make(chan os.Signal, 1)
done := make(chan bool, 1)

signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

go func() {
sig := <-sigs
fmt.Println()
fmt.Println(sig)
done <- true
}()

fmt.Println("awaiting signal")
<-done
fmt.Println("exiting")

// go run signals.go
// awaiting signal
// ^C
// interrupt
// exiting

协程

协程是由 Go 管理的轻量级线程(stack 初始化为 2K,远远小于 Java Thread Stack 的 1M)。

go f()() 启动一个运行 f 函数的 goroutine。

1
2
3
4
5
6
7
8
9
10
11
12
// 作为 goroutine 启动的函数 
func doStuff(s string) {
}

func main() {
// 在 goroutine 中使用命名函数
go doStuff("foobar")

// 在 goroutine 中使用匿名内部函数
go func (x int) {
}(42)
}

状态协程

即数据状态由单个协程拥有,保证数据不会因并发访问而破坏。

有时要解决同步问题,基于状态协程的方法比基于互斥锁的方法复杂。

在某些情况下可能很有用,例如当涉及其他信道时,或者管理多个此类互斥体时容易出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
type readOp struct {
key int
resp chan int
}

type writeOp struct {
key int
val int
resp chan bool
}

func TestStatefulGoroutines(t *t.Testing) {
// 统计操作次数。
var readOps uint64
var writeOps uint64

reads := make(chan readOp)
writes := make(chan writeOp)

// 有状态协程:在读和写的 channel 上选择,在读/写请求到达时发送响应。
go func() {
var state = make(map[int]int)
for {
select {
case read := <-reads:
read.resp <- state[read.key]
case write := <-writes:
state[write.key] = write.val
write.resp <- true
}
}
}()

// 创建 100 个读协程,10 个写协程。
for r := 0; r < 100; r++ {
go func() {
for {
read := readOp{key: rand.Intn(5), resp: make(chan int)}
// 读取数据,接收响应(表示读取到 xxx 值),操作次数 +1。
reads <- read
<-read.resp
atomic.AddUint64(&readOps, 1)
time.Sleep(time.Millisecond)
}
}()
}
for w := 0; w < 10; w++ {
go func() {
for {
write := writeOp{key: rand.Intn(5), val: rand.Intn(100), resp: make(chan bool)}
// 写入数据,接收响应(表示写入成功),操作次数 +1。
writes <- write
<-write.resp
atomic.AddUint64(&writeOps, 1)
time.Sleep(time.Millisecond)
}
}()
}

time.Sleep(time.Second)

readOpsFinal := atomic.LoadUint64(&readOps)
fmt.Println("readOps:", readOpsFinal)
writeOpsFinal := atomic.LoadUint64(&writeOps)
fmt.Println("writeOps:", writeOpsFinal)
}

信道

Channel 用于多个 goroutines 之间的通信,每个发往 Channel 的元素都会被复制一份、再放入 Channel 中;而从 Channel 中读取元素,也是从中复制出一份,再把该元素从 Channel 中删除(不会重复读取)。

如使用 Buffer Channel,则 Channel 中会缓存发送者数条消息(不超过容量),接收者随时都可以从中获取。

阻塞条件:信道缓冲区为空,或读取时无数据/写入时满数据,或信道为 nil。

1
2
3
4
5
6
7
8
ch := make(chan int)    // 创建一个 int 类型的 channel。
ch <- 42 // 向 ch 发送值。
v := <-ch // 从 ch 接收值。

// 非缓冲 channel 块,当没有可用值时读取块,写入块直到可读取。

// 创建一个缓冲 channel。如果写入的值小于 buffer size,则写入缓冲 channel 不会阻塞。
ch := make(chan int, 100)

关闭写入:

1
2
3
4
5
6
7
8
9
10
11
12
// 关闭 channel(发送者),不可再写入。 
close(ch)

// ok 为 false 表示信道关闭且缓冲区没有数据。
v, ok := <-ch

// 从 channel 读取(channel 为空则阻塞),直到其中的数据被读取完。
for i := range ch {
fmt.Println(i)
}

// 如果信道为 nil,则会永久阻塞。

不要通过共享内存来通信,而应通过通信来共享内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// bad
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func() {
// 循环变量 req 在每次迭代时会被重用,会在所有的 goroutine 间共享。
process(req)
<-sem
}()
}
}

// good
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func(req *Request) {
process(req)
<-sem
}(req)
}
}

互斥锁

使用缓冲区长度为 1 的 channel,写入数据相当于加锁,读取数据相当于解锁。

1
2
3
4
5
6
7
8
var counter int = 0
var ch = make(chan int, 1)

func Worker() {
ch <- 1
counter++
<-ch
}

单向信道

只能接收或发送,由其类型字面量决定。通常用于约束函数的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 信道接收符 <- 用在通道的类型字面量中,表示其方向:只能发送,不能接收。
var ch = make(chan<- int, 1)


// Notifier 接口定义了 SendInt 方法,其 ch 参数表示对于所接收的信道、内部只会向其发送元素,而不能从中接收元素。
// 在 SendInt 方法中定义了单向信道的参数,表示该接口的所有实现都满足该条件。
type Notifier interface {
SendInt(ch chan<- int)
}

// 只写
func SendInt(ch chan<- int) {
ch <- rand.Intn(1000)
}

// 只读
func RecvInt(ch <-chan int) {
<-ch
}

// 在调用时只需传入双向信道,SendInt 方法会自动转换成单向信道。
intChan1 := make(chan int, 3)
SendInt(intChan1)

在函数声明的返回值列表中使用单向信道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 对函数调用方的约束:只能从通道中接收元素值。
func getIntChan() <-chan int {
num := 5
ch := make(chan int, num)
for i := 0; i < num; i++ {
ch <- i
}
close(ch)
return ch
}

intChan2 := getIntChan()
for elem := range intChan2 {
fmt.Printf("The element in intChan2: %v\n", elem)
}

异步任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
func service() string {
time.Sleep(time.Millisecond * 50)
return "Done"
}

func otherTask() {
fmt.Println("working on something else")
time.Sleep(time.Millisecond * 100)
fmt.Println("Task is done.")
}

func AsyncService() chan string {
// 创建一个 string 类型的 channel。
retCh := make(chan string, 1)
// 创建协程,执行异步任务(非阻塞)。
go func() {
// 协程阻塞,直到执行完成、把结果写入 channel。
ret := service()
fmt.Println("returned result.")
retCh <- ret
fmt.Println("service exited.")
}()
// 返回 channel。
return retCh
}

func TestService(t *testing.T) {
fmt.Println(service())
otherTask()
}

func TestAsyncService(t *testing.T) {
// 执行异步任务,返回一个 channel(类似 Java 的 Future)。
retCh := AsyncService()

// 执行其他任务。
otherTask()

// 从 channel 读取结果。
fmt.Println(<-retCh)
time.Sleep(time.Second * 1)
}

多信道选择

只要任何一个信道消息就绪,即执行对应的 case。如果所有信道都被阻塞,则会走 default 分支。

1
2
3
4
5
6
7
8
select {
case ret := <-retCh1:
t.Logf("result %s", ret)
case ret :=<-retCh2:
t.Logf("result %s", ret)
default:
t.Error("No one returned")
}

超时控制:

1
2
3
4
5
6
7
8
select {
// 等待从 retCh 接收道值、并赋值给 ret。
case ret := <-retCh:
t.Logf("result %s", ret)
// 同时等待 time.Second * 1,执行更早返回的一个分支。
case <-time.After(time.Second * 1):
t.Error("time out")
}

非阻塞:利用 default 实现非阻塞等待。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
messages := make(chan string)
signals := make(chan bool)

select {
case msg := <-messages:
fmt.Println("received message", msg)
default:
fmt.Println("no message received")
}

msg := "hi"
select {
case messages <- msg:
fmt.Println("sent message", msg)
default:
fmt.Println("no message sent")
}

select {
case msg := <-messages:
fmt.Println("received message", msg)
case sig := <-signals:
fmt.Println("received signal", sig)
default:
fmt.Println("no activity")
}

基本规则:

  • 对于每个 case,都至少会包含一个发送表达式或一个接收表达式,同时也可能会包含其他的表达式。

  • 候选分支中的 case 都会在该语句执行开始时先被求值,顺序是依从代码编写的顺序从上到下。

  • 对于每个 case,如其中的发送表达式或接收表达式在被求值时,相应的操作正阻塞,则对该 case 的求值失败。

  • 仅当所有 case 表达式都被求值完毕后,才会开始选择满足条件的候选分支。如果所有条件都不满足,则选择 default 分支。default 分支不存在,则 select 进入阻塞、直到至少一个分支满足选择条件。

  • 如果发现同时有多个候选分支满足条件,会用一种伪随机算法从中选择一个执行。即使 select 语句是在被唤醒时发现的这种情况也会这样做。

  • select 语句每次执行(比如在循环中),包括 case 表达式求值和分支选择都是独立的。

定时任务

任务延迟执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建一个 2s 后唤醒执行的 channel。
timer1 := time.NewTimer(2 * time.Second)
<-timer1.C
fmt.Println("Timer 1 fired")

timer2 := time.NewTimer(time.Second)
go func() {
<-timer2.C
fmt.Println("Timer 2 fired")
}()

// 也可以取消等待。
stop2 := timer2.Stop()
if stop2 {
fmt.Println("Timer 2 stopped")
}

time.Sleep(2 * time.Second)

周期任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建一个每 500ms 执行一次的 ticker(直到手动把它 stop)。 
ticker := time.NewTicker(500 * time.Millisecond)
done := make(chan bool)

go func() {
for {
select {
case <-done:
return
case t := <-ticker.C:
fmt.Println("Tick at", t)
}
}
}()

time.Sleep(1600 * time.Millisecond)
// 关闭 ticker,并把 done 修改为 true 以退出协程函数。
ticker.Stop()
done <- true
fmt.Println("Ticker stopped")

任务取消

通过关闭 channel 取消:

1
2
3
func cancel_2(cancelChan chan struct{}) {
close(cancelChan)
}

发送取消消息:

1
2
3
func cancel_1(cancelChan chan struct{}) {
cancelChan <- struct{}{}
}

获取取消通知:

1
2
3
4
5
6
7
8
func isCancelled(cancelChan chan struct{}) bool {
select {
case <-cancelChan:
return true
default:
return false
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func TestCancel(t *testing.T) {
cancelChan := make(chan struct{}, 0)
for i := 0; i < 5; i++ {
go func(i int, cancelCh chan struct{}) {
for {
if isCancelled(cancelCh) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(i, cancelChan)
}
cancel_2(cancelChan)
time.Sleep(time.Second * 1)
}

Channel 规则

发送数据到 nil channel 会一直阻塞:

1
2
3
var c chan string
c <- "Hello, World!"
// fatal error: 所有 goroutine 都处于睡眠状态 - 死锁!

nil channel 读取数据会一直阻塞:

1
2
3
var c chan string
fmt.Println(<-c)
// fatal error: 所有 goroutine 都处于睡眠状态 - 死锁!

发送数据到已关闭的 channel 发生 panic

1
2
3
4
5
var c = make(chan string, 1)
c <- "Hello, World!"
close(c)
c <- "Hello, Panic!"
// panic: 在关闭的 channel 上发送

在关闭的 channel 上接收会立即返回零值:

1
2
3
4
5
6
7
8
var c = make(chan int, 2)
c <- 1
c <- 2
close(c)
for i := 0; i < 3; i++ {
fmt.Printf("%d ", <-c)
}
// 1 2 0

在 channel 关闭时,所有的接收者都会立即从阻塞等待中返回,且 v, ok <-ch 的 ok 值为 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func dataProducer(ch chan int, wg *sync.WaitGroup) {
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
wg.Done()
}()
}

func dataReceiver(ch chan int, wg *sync.WaitGroup) {
go func() {
// 循环读取,直到 ok 为 false(信道关闭)
for {
if data, ok := <-ch; ok {
fmt.Println(data)
} else {
break
}
}
wg.Done()
}()
}

func TestCloseChannel(t *testing.T) {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(1)
dataProducer(ch, &wg)
wg.Add(1)
dataReceiver(ch, &wg)
// wg.Add(1)
// dataReceiver(ch, &wg)
wg.Wait()
}

这个⼴播机制常被利⽤,进⾏向多个订阅者同时发送信号,如退出信号。

关于上下文、互斥锁、等待组、原子操作等并发工具,可参考 Go 并发原语

并发执行模式

顺序执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var count uint32
trigger := func(i uint32, fn func()) {
for {
if n := atomic.LoadUint32(&count); n == i {
fn()
atomic.AddUint32(&count, 1)
break
}
time.Sleep(time.Nanosecond)
}
}
for i := uint32(0); i < 10; i++ {
go func(i uint32) {
fn := func() {
fmt.Println(i)
}
trigger(i, fn)
}(i)
}
trigger(10, func() {})

单例模式(懒汉式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type Singleton struct {
data string
}

var singleInstance *Singleton

var once sync.Once

func GetSingletonObj() *Singleton {
// 确保只执行一次。
once.Do(func() {
fmt.Println("Create Obj")
singleInstance = new(Singleton)
})
return singleInstance
}

func TestGetSingletonObj(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
obj := GetSingletonObj()
fmt.Printf("%X\n", unsafe.Pointer(obj))
wg.Done()
}()
}
wg.Wait()
}

任意任务完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func runTask(id int) string {
time.Sleep(10 * time.Millisecond)
return fmt.Sprintf("The result is from %d", id)
}

func FirstResponse() string {
numOfRunner := 10
// 容量为 10 的 buffer channel。
ch := make(chan string, numOfRunner)
// 10 个协程执行任务。
for i := 0; i < numOfRunner; i++ {
go func(i int) {
ret := runTask(i)
ch <- ret
}(i)
}
// 当 channel 中读取到数据即返回。
return <-ch
}

func TestFirstResponse(t *testing.T) {
t.Log("Before:", runtime.NumGoroutine())
t.Log(FirstResponse())
time.Sleep(time.Second * 1)
t.Log("After:", runtime.NumGoroutine())
}

所有任务完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func runTask(id int) string {
time.Sleep(10 * time.Millisecond)
return fmt.Sprintf("The result is from %d", id)
}

func FirstResponse() string {
numOfRunner := 10
ch := make(chan string, numOfRunner)
for i := 0; i < numOfRunner; i++ {
go func(i int) {
ret := runTask(i)
ch <- ret
}(i)
}
return <-ch
}

func AllResponse() string {
// 创建 10 个协程执行任务
numOfRunner := 10
ch := make(chan string, numOfRunner)
for i := 0; i < numOfRunner; i++ {
go func(i int) {
ret := runTask(i)
ch <- ret
}(i)
}
// 等待 10 个结果(10 个协程都执行完成返回)
finalRet := ""
for j := 0; j < numOfRunner; j++ {
finalRet += <-ch + "\n"
}
return finalRet
}

func TestFirstResponse(t *testing.T) {
t.Log("Before:", runtime.NumGoroutine())
t.Log(AllResponse())
time.Sleep(time.Second * 1)
t.Log("After:", runtime.NumGoroutine())

}

对象池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
type ReusableObj struct {
}

type ObjPool struct {
// 用于缓冲可重用对象
bufChan chan *ReusableObj
}

func NewObjPool(numOfObj int) *ObjPool {
objPool := ObjPool{}
objPool.bufChan = make(chan *ReusableObj, numOfObj)
for i := 0; i < numOfObj; i++ {
objPool.bufChan <- &ReusableObj{}
}
return &objPool
}

func (p *ObjPool) GetObj(timeout time.Duration) (*ReusableObj, error) {
select {
case ret := <-p.bufChan:
return ret, nil
// 超时控制
case <-time.After(timeout):
return nil, errors.New("time out")
}

}

func (p *ObjPool) ReleaseObj(obj *ReusableObj) error {
select {
case p.bufChan <- obj:
return nil
default:
return errors.New("overflow")
}
}

// 测试
func TestObjPool(t *testing.T) {
pool := NewObjPool(10)
// if err := pool.ReleaseObj(&ReusableObj{}); err != nil { //尝试放置超出池大小的对象
// t.Error(err)
// }
for i := 0; i < 11; i++ {
if v, err := pool.GetObj(time.Second * 1); err != nil {
t.Error(err)
} else {
fmt.Printf("%T\n", v)
if err := pool.ReleaseObj(v); err != nil {
t.Error(err)
}
}
}
fmt.Println("Done")
}

对象缓存(sync.Pool)

适用于通过复用降低复杂对象创建和 GC 代价。

协程安全,但会有锁的开销,因此需要权衡维护对象的开销与锁的开销。

取用:

  • 尝试从私有对象获取,不存在则尝试从当前 Processor 的共享池获取。

  • 如果当前 Processor 共享池也为空,则尝试从其他 Processor 的共享池获取。

  • 如果所有⼦池都是空的,则由⽤户指定的 New 函数产⽣⼀个新的对象返回。

放回:如果私有对象不存在则保存为私有对象,否则放⼊当前 Processor ⼦池的共享池中。

Go GC 会清除 sync.pool 缓存的对象,因此对象的缓存有效期为下⼀次 GC 之前。由于生命周期受 GC 影响,不适用于连接池等,需要自己管理生命周期的资源池化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func TestSyncPool(t *testing.T) {
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("Create a new object.")
return 100
},
}
v := pool.Get().(int)
fmt.Println(v)
pool.Put(3)
// GC 会清除 sync.pool 中缓存的对象
runtime.GC()
v1, _ := pool.Get().(int)
fmt.Println(v1)
}

func TestSyncPoolInMultiGroutine(t *testing.T) {
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("Create a new object.")
return 10
},
}
pool.Put(100)
pool.Put(100)
pool.Put(100)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
fmt.Println(pool.Get())
wg.Done()
}(i)
}
wg.Wait()
}

限流器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func TestRateLimit(t *testing.T) {
// 模拟五个请求。
requests := make(chan int, 5)
for i := 1; i <= 5; i++ {
requests <- i
}
close(requests)

// limiter 限制每 200ms 接收 1 个值(超出以上频率写入的值会被阻塞)。
limiter := time.Tick(200 * time.Millisecond)
for req := range requests {
<-limiter
fmt.Println("request", req, time.Now())
}

// 模拟五个请求。
burstyRequests := make(chan int, 5)
for i := 1; i <= 5; i++ {
burstyRequests <- i
}
close(burstyRequests)

// burstyLimiter 限制每 200ms 接收 3 个值。
burstyLimiter := make(chan time.Time, 3)
for i := 0; i < 3; i++ {
burstyLimiter <- time.Now()
}
go func() {
for t := range time.Tick(200 * time.Millisecond) {
burstyLimiter <- t
}
}()
for req := range burstyRequests {
<-burstyLimiter
fmt.Println("request", req, time.Now())
}
}

反射

  • reflect.TypeOf 返回类型 reflct.Type
  • reflect.ValueOf 返回值 reflect.Value(可以从中获取类型)

类型分支

类型 switchcase 指定变量的类型,这些值与给定接口值持有的值的类型比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}

func main() {
do(21)
do("hello")
do(true)
}

深度比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func TestDeepEqual(t *testing.T) {
a := map[int]string{1: "one", 2: "two", 3: "three"}
b := map[int]string{1: "one", 2: "two", 3: "three"}
// t.Log(a == b)
t.Log("a==b?", reflect.DeepEqual(a, b))

s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
s3 := []int{2, 3, 1}

t.Log("s1 == s2?", reflect.DeepEqual(s1, s2))
t.Log("s1 == s3?", reflect.DeepEqual(s1, s3))

c1 := Customer{"1", "Mike", 40}
c2 := Customer{"1", "Mike", 40}
fmt.Println(c1 == c2)
fmt.Println(reflect.DeepEqual(c1, c2))
}

kind 判断类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// const ( 
// Invalid Kind = iota
// Bool
// Int
// Int8
// Int16
// Int32
// Int64
// Uint
// Uint8
// Uint16
// Uint32
// Uint64
// // ...
// )

func TestBasicType(t *testing.T) {
var f float64 = 12
t := reflect.TypeOf(&f)
switch t.Kind() {
case reflect.Float32, reflect.Float64:
fmt.Println("Float")
case reflect.Int, reflect.Int32, reflect.Int64:
fmt.Println("Integer")
default:
fmt.Println("Unknown", t)
}
}

根据名称调用

1
2
3
4
5
6
7
8
9
10
11
12
13
func TestInvokeByName(t *testing.T) {
e := &Employee{"1", "Mike", 30}
// 按名称获取成员
t.Logf("Name: value(%[1]v), Type(%[1]T) ", reflect.ValueOf(*e).FieldByName("Name"))
if nameField, ok := reflect.TypeOf(*e).FieldByName("Name"); !ok {
t.Error("Failed to get 'Name' field.")
} else {
t.Log("Tag:format", nameField.Tag.Get("format"))
}
// 按名称调用方法
reflect.ValueOf(e).MethodByName("UpdateAge").Call([]reflect.Value{reflect.ValueOf(1)})
t.Log("Updated Age:", e)
}

Struct Tag

1
2
3
4
5
6
7
8
9
10
type BasicInfo struct {
Name string `json:"name"`
Age int `json:"age"`
}

if nameField, ok := reflect.TypeOf(*e).FieldByName("Name"); !ok {
t.Error("Failed to get 'Name' field.")
} else {
t.Log("Tag:format", nameField.Tag.Get("format"))
}

万能设值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Employee struct {
EmployeeID string
Name string `format:"normal"`
Age int
}

func (e *Employee) UpdateAge(newVal int) {
e.Age = newVal
}

type Customer struct {
CookieID string
Name string
Age int
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func fillBySettings(st interface{}, settings map[string]interface{}) error {
if reflect.TypeOf(st).Kind() != reflect.Ptr {
return errors.New("the first param should be a pointer to the struct type.")
}
// Elem() 获取指针指向的值
if reflect.TypeOf(st).Elem().Kind() != reflect.Struct {
return errors.New("the first param should be a pointer to the struct type.")
}
if settings == nil {
return errors.New("settings is nil.")
}
var (
field reflect.StructField
ok bool
)
for k, v := range settings {
if field, ok = (reflect.ValueOf(st)).Elem().Type().FieldByName(k); !ok {
continue
}
if field.Type == reflect.TypeOf(v) {
vstr := reflect.ValueOf(st)
vstr = vstr.Elem()
vstr.FieldByName(k).Set(reflect.ValueOf(v))
}
}
return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
func TestFillNameAndAge(t *testing.T) {
settings := map[string]interface{}{"Name": "Mike", "Age": 30}
e := Employee{}
if err := fillBySettings(&e, settings); err != nil {
t.Fatal(err)
}
t.Log(e)
c := new(Customer)
if err := fillBySettings(c, settings); err != nil {
t.Fatal(err)
}
t.Log(*c)
}

Unsafe

类型转换

Go 本身不支持强制转换,利用 Unsafe 可实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Customer struct {
Name string
Age int
}

func TestUnsafe(t *testing.T) {
i := 10
f := *(*float64)(unsafe.Pointer(&i))
t.Log(unsafe.Pointer(&i))
t.Log(f)
}

type MyInt int

// 合理的类型转换
func TestConvert(t *testing.T) {
a := []int{1, 2, 3, 4}
b := *(*[]MyInt)(unsafe.Pointer(&a))
t.Log(b)
}

原子操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func TestAtomic(t *testing.T) {
var shareBufPtr unsafe.Pointer
writeDataFn := func() {
data := []int{}
for i := 0; i < 100; i++ {
data = append(data, i)
}
// sync/atomic
atomic.StorePointer(&shareBufPtr, unsafe.Pointer(&data))
}
readDataFn := func() {
data := atomic.LoadPointer(&shareBufPtr)
fmt.Println(data, *(*[]int)(data))
}
var wg sync.WaitGroup
writeDataFn()
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
for i := 0; i < 10; i++ {
writeDataFn()
time.Sleep(time.Microsecond * 100)
}
wg.Done()
}()
wg.Add(1)
go func() {
for i := 0; i < 10; i++ {
readDataFn()
time.Sleep(time.Microsecond * 100)
}
wg.Done()
}()
}
wg.Wait()
}

package 是基本复用模块单元,在每个源文件顶部声明。

  • 可执行文件放在 main 包中。

  • 代码的 package 可以和所在的目录不一致,但同一目录中代码的 package 要保持一致。

  • 包名 == 导入路径的最后一层(import math/rand => package rand),导入时从 src 后开始。

  • 大写标识符:导出(从其他包中可见);小写标识符:私有(在其他包中不可见)。

  • go get [-u] github.com/stretchr/testify/assert 可(强制)从远程获取/更新依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
import (
"testing"
// go get -u github.com/easierway/concurrent_map
// 引入别名:调用该包的内容 cm.xxx 即可。
cm "github.com/easierway/concurrent_map"
)

func TestConcurrentMap(t *testing.T) {
m := cm.CreateConcurrentMap(99)
m.Set(cm.StrKey("key"), 10)
t.Log(m.Get(cm.StrKey("key")))
}

如果要把自己的代码提交到 GitHub,应注意适应 go get:直接以代码路径开始,不要有 src

参考:https://github.com/easierway/concurrent_map

init 函数

  • main 被执⾏前,所有依赖的 package 的 init 函数都会被执⾏。

  • 不同包的 init 函数按照包导⼊的依赖关系决定执⾏顺序(深度优先导入)。

  • 每个包可以有多个 init 函数。

  • 包的每个源⽂件也可以有多个 init 函数。

1
2
3
4
5
6
7
8
9
10
11
package series

func init() {
fmt.Println("init1")
}

func init() {
fmt.Println("init2")
}

// 在别处 import series 时,init 函数即被执行。

init 函数不允许被手动显式调用。

重置包级变量值

参考 flag 包:其定义了一个导出的包级变量 CommandLine,如果用户没有通过 flag.NewFlagSet 创建新的代表命令行标志集合的实例,CommandLine 作为 flag 包各种导出函数背后,默认的代表命令行标志集合的实例。在 flag 包初始化时,init 函数初始化次序在包级变量之后,包级变量 CommandLine 会在 init 函数之前被初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

func NewFlagSet(name string, errorHandling ErrorHandling) *FlagSet {
f := &FlagSet{
name: name,
errorHandling: errorHandling,
}
f.Usage = f.defaultUsage
return f
}

func (f *FlagSet) defaultUsage() {
if f.name == "" {
fmt.Fprintf(f.Output(), "Usage:\n")
} else {
fmt.Fprintf(f.Output(), "Usage of %s:\n", f.name)
}
f.PrintDefaults()
}

flag 包在 init 函数中重置了 CommandLineUsage 字段:

1
2
3
4
5
6
7
8
9
10
11
12
func init() {
CommandLine.Usage = commandLineUsage // 重置CommandLine的Usage字段
}

func commandLineUsage() {
Usage()
}

var Usage = func() {
fmt.Fprintf(CommandLine.Output(), "Usage of %s:\n", os.Args[0])
PrintDefaults()
}

CommandLineUsage 字段设置为了一个 flag 包内的未导出函数 commandLineUsage,后者直接使用了 flag 包的另外一个导出包变量 Usage。通过 init 函数将 CommandLine 与包变量 Usage 关联在一起。

当用户将自定义的 usage 赋值给了 flag.Usage 后,改变了默认代表命令行标志集合的 CommandLine 变量的 Usage

当 flag 包完成初始化后,CommandLine 变量便处于一个合理可用的状态了。

实现对包级变量的复杂初始化

有些包级变量需要比较复杂的初始化过程,使用类型零值或通过简单初始化表达式不能满足业务逻辑要求,init 函数非常适合完成该项工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var (
http2VerboseLogs bool // 初始化时默认值为false
http2logFrameWrites bool // 初始化时默认值为false
http2logFrameReads bool // 初始化时默认值为false
http2inTests bool // 初始化时默认值为false
)

func init() {
e := os.Getenv("GODEBUG")
if strings.Contains(e, "http2debug=1") {
http2VerboseLogs = true // 在init中对http2VerboseLogs的值进行重置
}
if strings.Contains(e, "http2debug=2") {
http2VerboseLogs = true // 在init中对http2VerboseLogs的值进行重置
http2logFrameWrites = true // 在init中对http2logFrameWrites的值进行重置
http2logFrameReads = true // 在init中对http2logFrameReads的值进行重置
}
}

其中定义了一系列布尔类型的特性开关变量,它们默认处于关闭状态(即值为 false),可以通过 GODEBUG 环境变量开启相关特性开关。

但将这些变量初始化为类型零值不能满足要求了,所以 http 包在 init 函数中根据环境变量 GODEBUG 的值对包级开关变量进行了复杂的初始化,保证了在 http 包完成初始化后,这些遍历处于合理状态。

实现注册模式

使用 lib/pq 包访问 PostgreSQL 数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import (
"database/sql"
_ "github.com/lib/pq"
)

func main() {
db, err := sql.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=verify-full")
if err != nil {
log.Fatal(err)
}

age := 21
rows, err := db.Query("SELECT name FROM users WHERE age = $1", age)
...
}

示例代码以空导入方式导入 lib/pq 包,main 函数中没有使用 pq 包的任何变量、函数或方法,即可实现对 PostgreSQL 数据库的访问。

init 函数:

1
2
3
func init() {
sql.Register("postgres", &Driver{})
}

lib/pq 包作为 main 包的依赖包,init 函数会在 pq 包初始化时执行:pq 包将自己实现的 sql 驱动注册到了 sql 包中;通过 sql.Open 函数,返回的数据库实例句柄对数据库进行的操作,实际上是调用 pq 包中相应的驱动实现。

通过在 init 函数中注册自己的实现的模式,就有效降低了 Go 包对外的直接暴露,尤其是包级变量的暴露,从而避免了外部通过包级变量对包状态的改动。

包管理

Go 未解决依赖管理问题:

  • 同⼀环境下,不同项⽬无法使⽤同⼀包的不同版本。

  • ⽆法管理对包的特定版本的依赖。

Vendor 路径:Go 1.5 release 后,vendor ⽬录被添加到除了 GOPATH 和 GOROOT 之外的依赖⽬录查找的解决⽅案(在 Go 1.6 前需要⼿动设置环境变量)。查找依赖包路径的解决⽅案:

  • 当前包下的 vendor ⽬录。

  • 向上级⽬录查找,直到找到 src 下的 vendor ⽬录。

  • 在 GOPATH 下⾯查找依赖包。

  • 在 GOROOT ⽬录下查找。

第三方依赖管理工具:

1
2
3
4
5
mkdir module_package && cd !$
glide init
vim glide.yaml
# ...
glide install

Go Module

Go 1.16+ 默认使用 Go Module 构建模式。

Go Module 是 Go 包的集合,并附带版本属性:module 与其下的包会组成独立的版本单元。

每个代码仓库对应一个 Go Module,顶层以一个 go.mod 文件唯一标识。

使用 Go Module:

  • 项目根目录下 go mod init 生成 go.mod 文件,使之成为 Go Module(比如 go mod init github.com/yipwinghong/module-test)。

  • 使用 go mod tidy 自动更新当前 module 的依赖信息(比如 require github.com/sirupsen/logrus v1.8.1)。

  • 使用 go build 构建 module。

模式切换

在 Go 1.11 时,GOPATH 与 Go Modules 两种构建模式各自独立工作,通过设置环境变量 GO111MODULE 在两种构建模式间切换。直到 Go 1.16,Go Module 成为默认模式。

img

版本机制

img

主版本号不同的两个版本相互不兼容。在主版本号相同的情况下,次版本号大都是向后兼容次版本号小的版本。补丁版本号也不影响兼容性。

已知同一个包的新旧版本是兼容的,它们的包导入路径相同,否则不相同。

引入互不兼容的同一个包的不同版本:

1
2
3
4
import (
"github.com/sirupsen/logrus"
logv2 "github.com/sirupsen/logrus/v2"
)

最小选择原则:在该项目依赖项的所有版本中,选出符合项目整体要求的最小版本。

引入依赖

一个 module 项目(go mod tidy):

1
2
3
4
5
module github.com/yipwinghong/module-test

go 1.16

require github.com/sirupsen/logrus v1.8.1

添加 uuid 包时,在代码中导入:

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)

func main() {
logrus.Println("hello, go module mode")
logrus.Println(uuid.NewString())
}

拉取 uuid 包:将新增的依赖包下载到了本地 module 缓存,并在 go.mod 文件的 require 段中新增一行内容。

1
go get github.com/google/uuid
1
2
3
4
require (
github.com/google/uuid v1.3.0 //新增的依赖
github.com/sirupsen/logrus v1.8.1
)

也可以在执行构建前自动分析源码中的依赖变化,识别新增依赖项并下载(在管理大量依赖时显然更高效):

1
go mod tidy

升 / 降级依赖的版本

查询包的版本:

1
2
3
go list -m -versions github.com/sirupsen/logrus

# github.com/sirupsen/logrus v0.1.0 v0.1.1 v0.2.0 v0.3.0 v0.4.0 v0.4.1 v0.5.0 v0.5.1 v0.6.0 v0.6.1 v0.6.2 v0.6.3 v0.6.4 v0.6.5 v0.6.6 v0.7.0 v0.7.1 v0.7.2 v0.7.3 v0.8.0 v0.8.1 v0.8.2 v0.8.3 v0.8.4 v0.8.5 v0.8.6 v0.8.7 v0.9.0 v0.10.0 v0.11.0 v0.11.1 v0.11.2 v0.11.3 v0.11.4 v0.11.5 v1.0.0 v1.0.1 v1.0.3 v1.0.4 v1.0.5 v1.0.6 v1.1.0 v1.1.1 v1.2.0 v1.3.0 v1.4.0 v1.4.1 v1.4.2 v1.5.0 v1.6.0 v1.7.0 v1.7.1 v1.8.0 v1.8.1

比如要降级到 v1.7.0:

1
2
3
go mod edit -require=github.com/sirupsen/logrus@v1.7.0
go mod tidy
# go: downloading github.com/sirupsen/logrus v1.7.0

添加主版本号大于 1 的包

主版本号为 0 表示处于开发阶段,与主版本号 v1 做同等对待,采用不带主版本号的包导入路径:

1
2
3
4
import github.com/user/repo/v0
// 等价于 import github.com/user/repo
import github.com/user/repo/v1
// 等价于 import github.com/user/repo

对于主版本号大于 1 的依赖,需要在声明的导入路径上加上版本号信息。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
_ "github.com/go-redis/redis/v7" // “_”为空导入
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)

func main() {
logrus.Println("hello, go module mode")
logrus.Println(uuid.NewString())
}

升级到不兼容版本

按照语义导入版本的原则,不同主版本的包的导入路径是不同的。因此直接修改版本号、使用 go get 下载即可:

1
2
3
4
5
import (
_ "github.com/go-redis/redis/v8"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
1
go get github.com/go-redis/redis/v8

移除依赖

除了删除使用到该依赖的代码(包括 import)以外、还需要执行 go mod tidy 分析源码依赖,将不再使用的依赖从 go.mod 和 go.sum 中移除。

结合 Vendor 使用

在 Go Module 构建模式下,无需手动维护 vendor 目录下的依赖包,Go 提供可以快速建立和更新 vendor 的命令:

1
2
3
4
5
6
7
8
9
10
go mod vendor
tree -LF 2 vendor
# vendor
# ├── github.com/
# │ ├── google/
# │ ├── magefile/
# │ └── sirupsen/
# ├── golang.org/
# │ └── x/
# └── modules.txt

在 vendor 目录下创建一份该项目的依赖包的副本,并且通过 vendor/modules.txt 记录了 vendor 下的 module 以及版本。

如果要基于 vendor 而不是基于本地缓存的 Go Module 构建,需要在 go build 后面加上 -mod=vendor 参数。

在 Go 1.14+,如果 Go 项目的顶层目录下存在 vendor 目录,go build 默认优先基于 vendor 构建,除非执行 go build -mod=mod

测试

  • 源码文件以 _test 结尾。

  • 测试方法名以 Test 开头:func TestXxx(t *testing.T) {...}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const (
Monday = 1 + iota
Tuesday
Wednesday
)

const (
Readable = 1 << iota
Writable
Executable
)

func TestConstantTry(t *testing.T) {
t.Log(Monday, Tuesday)
}

func TestConstantTry1(t *testing.T) {
a := 1 // 0001
t.Log(a&Readable == Readable, a&Writable == Writable, a&Executable == Executable)
}

测试:go test test.go

单元测试

Go 内置单元测试框架:

  • Fail, Error: 该测试最终失败,但后续代码继续执行,且其他测试继续执行。

  • FailNow, Fatal: 该测试失败且马上中止,其他测试继续执行。

  • 测试覆盖率:go test -v -cover

要使用断言可引入第三方包 testfy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)

// 待测试函数
func square(op int) int {
return op * op
}

func TestSquare(t *testing.T) {
inputs := [...]int{1, 2, 3}
expected := [...]int{1, 4, 9}
for i := 0; i < len(inputs); i++ {
ret := square(inputs[i])
if ret != expected[i] {
t.Errorf("input is %d, the expected is %d, the actual %d", inputs[i], expected[i], ret)
}
}
}

func TestErrorInCode(t *testing.T) {
fmt.Println("Start")
t.Error("Error")
fmt.Println("End")
}

func TestFailInCode(t *testing.T) {
fmt.Println("Start")
t.Fatal("Error")
fmt.Println("End")
}

func TestSquareWithAssert(t *testing.T) {
inputs := [...]int{1, 2, 3}
expected := [...]int{1, 4, 9}
for i := 0; i < len(inputs); i++ {
ret := square(inputs[i])
assert.Equal(t, expected[i], ret)
}
}

使用建议:

  • 单元测试文件名命名规范为 example_test.go。

  • 每个重要的可导出函数都要编写测试用例。

  • 因为单元测试文件内的函数都是不对外的,所以可导出的结构体、函数等可以不带注释。

  • 如果存在 func (b *Bar) Foo,单测函数可以为 func TestBar_Foo

基准测试

基准测试:go test -bench=. -benchmem-bench=<相关benchmark测试>

Windows 下使⽤ go test 命令⾏时应写为 go test -bench="."

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
func TestConcatStringByAdd(t *testing.T) {
assert := assert.New(t)
elems := []string{"1", "2", "3", "4", "5"}
ret := ""
for _, elem := range elems {
ret += elem
}
assert.Equal("12345", ret)
}

func TestConcatStringByBytesBuffer(t *testing.T) {
assert := assert.New(t)
var buf bytes.Buffer
elems := []string{"1", "2", "3", "4", "5"}
for _, elem := range elems {
buf.WriteString(elem)
}
assert.Equal("12345", buf.String())
}

func BenchmarkConcatStringByAdd(b *testing.B) {
elems := []string{"1", "2", "3", "4", "5"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
ret := ""
for _, elem := range elems {
ret += elem
}
}
b.StopTimer()
}

func BenchmarkConcatStringByBytesBuffer(b *testing.B) {
elems := []string{"1", "2", "3", "4", "5"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
var buf bytes.Buffer
for _, elem := range elems {
buf.WriteString(elem)
}
}
b.StopTimer()
}

性能分析

第三方工具

可下载并安装以下工具:

通过⽂件⽅式输出 Profile:

通过 HTTP ⽅式输出 Profile:

  • 简单,适合于持续性运⾏的应⽤:在应⽤程序中导⼊ import _ "net/http/pprof",并启动 http server 即可(http://<host>:<port>/debug/pprof/)。

  • go tool pprof http://<host>:<port>/debug/pprof/profile?seconds=10

  • go-torch -seconds 10 http://<host>:<port>/debug/pprof/profifile

Go 支持的多种 Profile:https://golang.org/src/runtime/pprof/pprof.go

BDD (Behavior Driven Development)

https://github.com/smartystreets/goconvey

安装:go get -u github.com/smartystreets/goconvey/convey

启动:$GOPATH/bin/goconvey

其它模块

打印文本

1
2
3
4
5
6
7
8
9
10
11
12
13
fmt.Println("Hello, 你好, नमस्ते, Привет, ᎣᏏᏲ")
p := struct { X, Y int }{ 17, 2 }
fmt.Println( "My point:", p, "x coord=", p.X ) // 打印结构, ints, 等等
s := fmt.Sprintln( "My point:", p, "x coord=", p.X ) // 打印字符串

fmt.Printf("%d hex:%x bin:%b fp:%f sci:%e",17,17,17,17.0,17.0) // c-ish 格式
s2 := fmt.Sprintf( "%d %f", 17, 17.0 ) // 格式化打印到字符串变量

hellomsg := `
"Hello" in Chinese is 你好 ('Ni Hao')
"Hello" in Hindi is नमस्ते ('Namaste')
`
// 多行字符串文字,在开头和结尾使用反引号

时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
p := fmt.Println

now := time.Now()
p(now) // 2012-10-31 15:50:13.793654 +0000 UTC

then := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
p(then) // 2009-11-17 20:34:58.651387237 +0000 UTC

p(then.Year()) // 2009
p(then.Month()) // November
p(then.Day()) // 17
p(then.Hour()) // 20
p(then.Minute()) // 34
p(then.Second()) // 58
p(then.Nanosecond()) // 651387237
p(then.Location()) // UTC

p(then.Weekday()) // UTC

p(then.Before(now)) // Tuesday
p(then.After(now)) // true
p(then.Equal(now)) // false

diff := now.Sub(then) // false
p(diff) // 25891h15m15.142266763s

p(diff.Hours()) // 25891.25420618521
p(diff.Minutes()) // 1.5534752523711128e+06
p(diff.Seconds()) // 9.320851514226677e+07
p(diff.Nanoseconds()) // 93208515142266763

p(then.Add(diff)) // 2012-10-31 15:50:13.793654 +0000 UTC
p(then.Add(-diff)) // 2006-12-05 01:19:43.509120474 +0000 UTC

Epoch

1
2
3
4
5
6
7
8
9
10
11
12
now := time.Now()
secs := now.Unix()
nanos := now.UnixNano()
fmt.Println(now) // 2012-10-31 16:13:58.292387 +0000 UTC

millis := nanos / 1000000
fmt.Println(secs) // 1351700038
fmt.Println(millis) // 1351700038292
fmt.Println(nanos) // 1351700038292387000

fmt.Println(time.Unix(secs, 0)) // 2012-10-31 16:13:58 +0000 UTC
fmt.Println(time.Unix(0, nanos)) // 2012-10-31 16:13:58.292387 +0000 UTC

格式化时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
p := fmt.Println

t := time.Now()
p(t.Format(time.RFC3339))

t1, e := time.Parse(time.RFC3339, "2012-11-01T22:08:41+00:00")
p(t1)

p(t.Format("3:04PM"))
p(t.Format("Mon Jan _2 15:04:05 2006"))
p(t.Format("2006-01-02T15:04:05.999999-07:00"))
form := "3 04 PM"
t2, e := time.Parse(form, "8 41 PM")
p(t2)

fmt.Printf("%d-%02d-%02dT%02d:%02d:%02d-00:00\n", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())

ansic := "Mon Jan _2 15:04:05 2006"
_, e = time.Parse(ansic, "8:41PM")
p(e)

随机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fmt.Print(rand.Intn(100), ",")
fmt.Print(rand.Intn(100))
fmt.Println()

fmt.Println(rand.Float64())

fmt.Print((rand.Float64()*5)+5, ",")
fmt.Print((rand.Float64() * 5) + 5)
fmt.Println()

s1 := rand.NewSource(time.Now().UnixNano())
r1 := rand.New(s1)

fmt.Print(r1.Intn(100), ",")
fmt.Print(r1.Intn(100))
fmt.Println()

s2 := rand.NewSource(42)
r2 := rand.New(s2)
fmt.Print(r2.Intn(100), ",")
fmt.Print(r2.Intn(100))
fmt.Println()
s3 := rand.NewSource(42)
r3 := rand.New(s3)
fmt.Print(r3.Intn(100), ",")
fmt.Print(r3.Intn(100))

文件嵌入

Go 程序可以使用 embed 包嵌入静态文件,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"embed"
"log"
"net/http"
)

// content 包含静态内容(2 个文件)或 Web 服务器。
// go:embed a.txt b.txt
var content embed.FS

func main() {
http.Handle("/", http.FileServer(http.FS(content)))
log.Fatal(http.ListenAndServe(":8080", nil))
}

完整示例

HTTP Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func hello(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "hello\n")
}

func headers(w http.ResponseWriter, req *http.Request) {
for name, headers := range req.Header {
for _, h := range headers {
fmt.Fprintf(w, "%v: %v\n", name, h)
}
}
}

func main() {
http.HandleFunc("/hello", hello)
http.HandleFunc("/headers", headers)
http.ListenAndServe(":8090", nil)
}

路由规则

  • URL 分为两种,末尾是 /:表示⼀个⼦树,后⾯可以跟其他⼦路径; 末尾不是 /,表示⼀个叶⼦,固定的路径。

  • 以 / 结尾的 URL 可以匹配它的任何⼦路径,⽐如 /images 会匹配 /images/cute-cat.jpg。

  • 它采⽤最⻓匹配原则,如果有多个匹配,⼀定采⽤匹配路径最⻓的那个进⾏处理。

  • 如果没有找到任何匹配项,会返回 404 错误。

Default Router

1
2
3
4
5
6
7
8
9
10
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux //使⽤缺省的Router
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}

更好的 Router

HttpRouter: https://github.com/julienschmidt/httprouter

1
2
3
4
5
6
7
8
9
func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}
func main() {
router := httprouter.New()
router.GET("/", Index)
router.GET("/hello/:name", Hello)
log.Fatal(http.ListenAndServe(":8080", router))
}

Restful 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/julienschmidt/httprouter"
)

type Employee struct {
ID string `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}

var employeeDB map[string]*Employee

func init() {
employeeDB = map[string]*Employee{}
employeeDB["Mike"] = &Employee{"e-1", "Mike", 35}
employeeDB["Rose"] = &Employee{"e-2", "Rose", 45}
}

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Welcome!\n")
}

func GetEmployeeByName(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
qName := ps.ByName("name")
var (
ok bool
info *Employee
infoJson []byte
err error
)
if info, ok = employeeDB[qName]; !ok {
w.Write([]byte("{\"error\":\"Not Found\"}"))
return
}
if infoJson, err = json.Marshal(info); err != nil {
w.Write([]byte(fmt.Sprintf("{\"error\":,\"%s\"}", err)))
return
}

w.Write(infoJson)
}

func main() {
router := httprouter.New()
router.GET("/", Index)
router.GET("/employee/:name", GetEmployeeByName)

log.Fatal(http.ListenAndServe(":8080", router))
}

HTTP Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
resp, err := http.Get("http://gobyexample.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()

fmt.Println("Response status:", resp.Status)

scanner := bufio.NewScanner(resp.Body)
for i := 0; scanner.Scan() && i < 5; i++ {
fmt.Println(scanner.Text())
}

if err := scanner.Err(); err != nil {
panic(err)
}

JSON

Struct Tag 解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
type BasicInfo struct {
Name string `json:"name"`
Age int `json:"age"`
}
type JobInfo struct {
Skills []string `json:"skills"`
}
type Employee struct {
BasicInfo BasicInfo `json:"basic_info"`
JobInfo JobInfo `json:"job_info"`
}

var jsonStr = `{
"basic_info":{
"name":"Mike",
"age":30
},
"job_info":{
"skills":["Java","Go","C"]
}
}`

func TestEmbeddedJson(t *testing.T) {
e := new(Employee)
err := json.Unmarshal([]byte(jsonStr), e)
if err != nil {
t.Error(err)
}
fmt.Println(*e)
if v, err := json.Marshal(e); err == nil {
fmt.Println(string(v))
} else {
t.Error(err)
}

}

第三方库

EasyJSON: go get -u github.com/mailru/easyjson/...

XML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
type Plant struct {
XMLName xml.Name `xml:"plant"`
Id int `xml:"id,attr"`
Name string `xml:"name"`
Origin []string `xml:"origin"`
}

func (p Plant) String() string {
return fmt.Sprintf("Plant id=%v, name=%v, origin=%v",
p.Id, p.Name, p.Origin)
}

func TestXML(t *t.Testing) {
coffee := &Plant{Id: 27, Name: "Coffee"}
coffee.Origin = []string{"Ethiopia", "Brazil"}

out, _ := xml.MarshalIndent(coffee, " ", " ")
fmt.Println(string(out))

fmt.Println(xml.Header + string(out))

var p Plant
if err := xml.Unmarshal(out, &p); err != nil {
panic(err)
}
fmt.Println(p)

tomato := &Plant{Id: 81, Name: "Tomato"}
tomato.Origin = []string{"Mexico", "California"}

type Nesting struct {
XMLName xml.Name `xml:"nesting"`
Plants []*Plant `xml:"parent>child>plant"`
}

nesting := &Nesting{}
nesting.Plants = []*Plant{coffee, tomato}

out, _ = xml.MarshalIndent(nesting, " ", " ")
fmt.Println(string(out))
}

编码

SHA1

1
2
3
4
5
6
7
8
9
10
11
12
13
import (
"crypto/sha1"
"fmt"
)

func TestSHA1(t *t.Testing) {
s := "sha1 this string"
h := sha1.New()
h.Write([]byte(s))
bs := h.Sum(nil)
fmt.Println(s)
fmt.Printf("%x\n", bs)
}

Base64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import (
b64 "encoding/base64"
"fmt"
)

func TestBase64(t *t.Testing) {

data := "abc123!?$*&()'-=@~"

sEnc := b64.StdEncoding.EncodeToString([]byte(data))
fmt.Println(sEnc)

sDec, _ := b64.StdEncoding.DecodeString(sEnc)
fmt.Println(string(sDec))
fmt.Println()

uEnc := b64.URLEncoding.EncodeToString([]byte(data))
fmt.Println(uEnc)
uDec, _ := b64.URLEncoding.DecodeString(uEnc)
fmt.Println(string(uDec))
}

读写文件

列出文件与目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
dir := os.Args[1]
listAll(dir,0)
}

func listAll(path string, curHier int){
fileInfos, err := ioutil.ReadDir(path)
if err != nil{fmt.Println(err); return}

for _, info := range fileInfos{
if info.IsDir(){
for tmpHier := curHier; tmpHier > 0; tmpHier--{
fmt.Printf("|\t")
}
fmt.Println(info.Name(),"\\")
listAll(path + "/" + info.Name(),curHier + 1)
}else{
for tmpHier := curHier; tmpHier > 0; tmpHier--{
fmt.Printf("|\t")
}
fmt.Println(info.Name())
}
}
}

整个读取:针对普通的小文件,并且对内容没有太多操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func readAll(filePath string) {
ioutil.ReadFile(filePath)
}

func readAllBuff(filePath string) {
FileHandle, err := os.Open(filePath)
if err != nil {
log.Println(err)
return
}
defer FileHandle.Close()

fileInfo, err := FileHandle.Stat()
if err != nil {
log.Println(err)
return
}
buffer := make([]byte, fileInfo.Size())
// 读取文件内容,并写入buffer中
n, err := FileHandle.Read(buffer)
if err != nil {
log.Println(err)
}
// 打印所有切片中的内容
fmt.Println(string(buffer[:n]))
}

分片读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func readBlock(filePath string) {
FileHandle, err := os.Open(filePath)
if err != nil {
log.Println(err)
return
}
defer FileHandle.Close()
// 设置每次读取字节数
buffer := make([]byte, 1024)
for {
n, err := FileHandle.Read(buffer)
// 控制条件,根据实际调整
if err != nil && err != io.EOF {
log.Println(err)
}
if n == 0 {
break
}
// 如下代码打印出每次读取的文件块(字节数)
//fmt.Println(string(buffer[:n]))
}
}

逐行读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func readEachLineReader(filePath string) {
FileHandle, err := os.Open(filePath)
if err != nil {
log.Println(err)
return
}
defer FileHandle.Close()
lineReader := bufio.NewReader(FileHandle)
for {
// 相同使用场景下可以采用的方法
// func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)
// func (b *Reader) ReadBytes(delim byte) (line []byte, err error)
// func (b *Reader) ReadString(delim byte) (line string, err error)
line, _, err := lineReader.ReadLine()
if err == io.EOF {
break
}
// 如下是某些业务逻辑操作
// 如下代码打印每次读取的文件行内容
fmt.Println(string(line))
}
}

func readEachLineScanner(filePath string) {
FileHandle, err := os.Open(filePath)
if err != nil {
log.Println(err)
return
}
defer FileHandle.Close()
lineScanner := bufio.NewScanner(FileHandle)
for lineScanner.Scan() {
// 相同使用场景下可以使用如下方法
// func (s *Scanner) Bytes() []byte
// func (s *Scanner) Text() string
// 实际逻辑 : 对读取的内容进行某些业务操作
// 如下代码打印每次读取的文件行内容
fmt.Println(lineScanner.Text())
}
}

写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// File Handler
func TestFileHandler() {
// 以指定的权限打开文件
file, _ := os.OpenFile("test2.txt", os.O_RDWR | os.O_APPEND | os.O_CREATE, 0664)

defer file.Close()

data := []byte("hello go\n")

// 写入字节
count, _ := file.Write(data)

printWroteBytes(count)

// 写入字符串
count, _ = file.WriteString("hello world\n")

printWroteBytes(count)

// 确保写入到磁盘
file.Sync()
}

// ioutil
func TestIoutil() {
data := []byte("hello goo\n")

// 覆盖式写入
ioutil.WriteFile("test.txt", data, 0664)
}

// bufio
func TestBufio() {
file, _ := os.OpenFile("test.txt", os.O_RDWR | os.O_APPEND | os.O_CREATE, 0664)

defer file.Close()

// 获取bufio.Writer实例
writer := bufio.NewWriter(file)

// 写入字符串
count, _ := writer.Write([]byte("hello go\n"))

fmt.Printf("wrote %d bytes\n", count)

// 写入字符串
count, _ = writer.WriteString("hello world\n")

fmt.Printf("wrote %d bytes\n", count)

// 清空缓存 确保写入磁盘
writer.Flush()
}

行过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scanner := bufio.NewScanner(os.Stdin)

for scanner.Scan() {

ucl := strings.ToUpper(scanner.Text())

fmt.Println(ucl)
}

if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}

// cat /tmp/lines | go run line-filters.go

命令行参数

1
2
3
4
5
6
7
8
9
10
11
12
13
// go build command-line-arguments.go
// ./command-line-arguments a b c d
func main() {

argsWithProg := os.Args
argsWithoutProg := os.Args[1:]

arg := os.Args[3]

fmt.Println(argsWithProg)
fmt.Println(argsWithoutProg)
fmt.Println(arg)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// go build command-line-flags.go
// ./command-line-flags -word=opt -numb=7 -fork -svar=flag
// ./command-line-flags -word=opt a1 a2 a3 -numb=7
func main() {
wordPtr := flag.String("word", "foo", "a string")

numbPtr := flag.Int("numb", 42, "an int")
boolPtr := flag.Bool("fork", false, "a bool")

var svar string
flag.StringVar(&svar, "svar", "bar", "a string var")

flag.Parse()

fmt.Println("word:", *wordPtr)
fmt.Println("numb:", *numbPtr)
fmt.Println("fork:", *boolPtr)
fmt.Println("svar:", svar)
fmt.Println("tail:", flag.Args())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// go build command-line-subcommands.go 
// ./command-line-subcommands foo -enable -name=joe a1 a2
func main() {
fooCmd := flag.NewFlagSet("foo", flag.ExitOnError)
fooEnable := fooCmd.Bool("enable", false, "enable")
fooName := fooCmd.String("name", "", "name")

barCmd := flag.NewFlagSet("bar", flag.ExitOnError)
barLevel := barCmd.Int("level", 0, "level")

if len(os.Args) < 2 {
fmt.Println("expected 'foo' or 'bar' subcommands")
os.Exit(1)
}

switch os.Args[1] {
case "foo":
fooCmd.Parse(os.Args[2:])
fmt.Println("subcommand 'foo'")
fmt.Println(" enable:", *fooEnable)
fmt.Println(" name:", *fooName)
fmt.Println(" tail:", fooCmd.Args())
case "bar":
barCmd.Parse(os.Args[2:])
fmt.Println("subcommand 'bar'")
fmt.Println(" level:", *barLevel)
fmt.Println(" tail:", barCmd.Args())
default:
fmt.Println("expected 'foo' or 'bar' subcommands")
os.Exit(1)
}
}

环境变量

1
2
3
4
5
6
7
8
9
10
11
12
func main() {

os.Setenv("FOO", "1")
fmt.Println("FOO:", os.Getenv("FOO"))
fmt.Println("BAR:", os.Getenv("BAR"))

fmt.Println()
for _, e := range os.Environ() {
pair := strings.SplitN(e, "=", 2)
fmt.Println(pair[0])
}
}

正则表达式

使用正则表达式必须预编译,避免每次使用时构建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
match, _ := regexp.MatchString("p([a-z]+)ch", "peach")
fmt.Println(match)

r, _ := regexp.Compile("p([a-z]+)ch")

fmt.Println(r.MatchString("peach"))
fmt.Println(r.FindString("peach punch"))
fmt.Println(r.FindStringIndex("peach punch"))
fmt.Println(r.FindStringSubmatch("peach punch"))
fmt.Println(r.FindStringSubmatchIndex("peach punch"))
fmt.Println(r.FindAllString("peach punch pinch", -1))
fmt.Println(r.FindAllStringSubmatchIndex("peach punch pinch", -1))
fmt.Println(r.FindAllString("peach punch pinch", 2))
fmt.Println(r.Match([]byte("peach")))

r = regexp.MustCompile("p([a-z]+)ch")
fmt.Println(r)

fmt.Println(r.ReplaceAllString("a peach", "<fruit>"))

in := []byte("a peach")
out := r.ReplaceAllFunc(in, bytes.ToUpper)
fmt.Println(string(out))

技巧总结

  • package 的名字和目录名一样,main 除外。

  • string 表示的是不可变的字符串变量,对 string 的修改是比较重的操作,基本上都需要重新申请内存,如果没有特殊需要,需要修改时多使用 []byte

  • 尽量使用 strings 库操作 string 可以提高性能。

  • append 要小心自动分配内存,append 返回的可能是新分配的地址。

  • 如果要直接修改 map 的 value 值,则 value 只能是指针,否则要覆盖原来的值。

  • map 在并发中需要加锁。

  • 编译过程无法检查 interface{} 的转换,只有运行时检查,小心引起 panic

  • 使用 defer,保证退出函数时释放资源。

  • 尽量少用全局变量,通过参数传递,使每个函数都是“无状态”的,可减少耦合,也方便分工和单元测试。

  • 参数如果比较多,将相关参数定义成结构体传递。

CATALOG
  1. 1. Go Cheat Sheet
    1. 1.1. 安装
      1. 1.1.1. 远程开发插件
    2. 1.2. 代码格式
    3. 1.3. 操作符
      1. 1.3.1. 算术
      2. 1.3.2. 比较
      3. 1.3.3. 逻辑
      4. 1.3.4. 其他
    4. 1.4. 声明
      1. 1.4.1. 变量
      2. 1.4.2. 常量
    5. 1.5. 初始化
      1. 1.5.1. new
      2. 1.5.2. make
    6. 1.6. 函数
      1. 1.6.1. 高阶函数
      2. 1.6.2. 闭包
      3. 1.6.3. 可变参数
      4. 1.6.4. 递归
      5. 1.6.5. 处理集合
    7. 1.7. 控制结构
      1. 1.7.1. if
      2. 1.7.2. for
      3. 1.7.3. range
      4. 1.7.4. switch
      5. 1.7.5. defer
      6. 1.7.6. goto
      7. 1.7.7. select
    8. 1.8. 数据类型
      1. 1.8.1. 字符串
      2. 1.8.2. 类型转换
      3. 1.8.3. 类型声明
      4. 1.8.4. 类型断言
      5. 1.8.5. 指针
    9. 1.9. 结构
      1. 1.9.1. 方法
      2. 1.9.2. 匿名结构
      3. 1.9.3. 嵌入
    10. 1.10. 接口
      1. 1.10.1. 多态
      2. 1.10.2. 空接口
      3. 1.10.3. 完整性检查
    11. 1.11. 容器
      1. 1.11.1. 数组
      2. 1.11.2. 切片
      3. 1.11.3. 字典
    12. 1.12. 错误
      1. 1.12.1. 错误描述
      2. 1.12.2. 封装异常
      3. 1.12.3. panic
    13. 1.13. 进程
      1. 1.13.1. 信号
    14. 1.14. 协程
      1. 1.14.1. 状态协程
    15. 1.15. 信道
      1. 1.15.1. 互斥锁
      2. 1.15.2. 单向信道
      3. 1.15.3. 异步任务
      4. 1.15.4. 多信道选择
      5. 1.15.5. 定时任务
      6. 1.15.6. 周期任务
      7. 1.15.7. 任务取消
      8. 1.15.8. Channel 规则
    16. 1.16. 并发执行模式
      1. 1.16.1. 顺序执行
      2. 1.16.2. 单例模式(懒汉式)
      3. 1.16.3. 任意任务完成
      4. 1.16.4. 所有任务完成
      5. 1.16.5. 对象池
      6. 1.16.6. 对象缓存(sync.Pool)
      7. 1.16.7. 限流器
    17. 1.17. 反射
      1. 1.17.1. 类型分支
      2. 1.17.2. 深度比较
      3. 1.17.3. kind 判断类型
      4. 1.17.4. 根据名称调用
      5. 1.17.5. Struct Tag
      6. 1.17.6. 万能设值
    18. 1.18. Unsafe
      1. 1.18.1. 类型转换
      2. 1.18.2. 原子操作
    19. 1.19.
      1. 1.19.1. init 函数
        1. 1.19.1.1. 重置包级变量值
        2. 1.19.1.2. 实现对包级变量的复杂初始化
        3. 1.19.1.3. 实现注册模式
      2. 1.19.2. 包管理
    20. 1.20. Go Module
      1. 1.20.1. 模式切换
      2. 1.20.2. 版本机制
      3. 1.20.3. 引入依赖
      4. 1.20.4. 升 / 降级依赖的版本
      5. 1.20.5. 添加主版本号大于 1 的包
      6. 1.20.6. 升级到不兼容版本
      7. 1.20.7. 移除依赖
      8. 1.20.8. 结合 Vendor 使用
    21. 1.21. 测试
      1. 1.21.1. 单元测试
      2. 1.21.2. 基准测试
      3. 1.21.3. 性能分析
      4. 1.21.4. 第三方工具
      5. 1.21.5. BDD (Behavior Driven Development)
    22. 1.22. 其它模块
      1. 1.22.1. 打印文本
      2. 1.22.2. 时间
        1. 1.22.2.1. Epoch
        2. 1.22.2.2. 格式化时间
      3. 1.22.3. 随机
      4. 1.22.4. 文件嵌入
      5. 1.22.5. HTTP Server
        1. 1.22.5.1. 路由规则
        2. 1.22.5.2. Default Router
        3. 1.22.5.3. 更好的 Router
        4. 1.22.5.4. Restful 服务
      6. 1.22.6. HTTP Client
      7. 1.22.7. JSON
        1. 1.22.7.1. Struct Tag 解析
        2. 1.22.7.2. 第三方库
      8. 1.22.8. XML
      9. 1.22.9. 编码
        1. 1.22.9.1. SHA1
        2. 1.22.9.2. Base64
      10. 1.22.10. 读写文件
      11. 1.22.11. 行过滤器
      12. 1.22.12. 命令行参数
      13. 1.22.13. 环境变量
      14. 1.22.14. 正则表达式
    23. 1.23. 技巧总结