Go 语言避坑与代码规范

本篇记录自己在编写 go 代码时经常犯的错误,参考 Uber Go 的编码规范给出正确代码姿势。

go 多版本管理

在安装时将 usr/local/go 改为 usr/local/go1.xx

添加环境变量

1
2
3
4
5
# go version 1.xx
export GOROOT=/usr/local/go1.xx
export GOPATH=/home/{username}/go1.xx
export GOBIN=$GOPATH/bin
export PATH=$GOPATH/bin:$PATH

go module

go module 是一个 go 包的集合,在 go module 模式下,通常一个代码仓库对应一个 go module。一个 go module 的顶层目录下会放置一个 go.mod 文件,每个 go.mod 文件会定义唯一一个 module

  • go.sum :存放特定版本 module 的哈希值,当某个 module 的特定版本再次下载时,会进行哈希值对比以确保项目依赖内容不会被恶意篡改。(通常 go.mod, go.sum 两个文件同样需要使用 git 维护起来)

如何引入未发布的本地 module

使用 replace 替换 module 路径。例如:

1
2
require github.com/user/repo v1.0.0
replace github.com/user/repo v1.0.0 => module 的本地路径

这里分享一个不错的 go module 指南:

go mod 作弊码: https://encore.dev/guide/go.mod

语义导入版本 (Semantic Import Versioning)

版本号规范: vX.Y.Z

  • X: 主版本号
  • Y: 次版本号
  • Z: patch 补丁版本号

借助于语义版本规范,Go 命令可以确定同一 module 的两个版本发布的先后次序,而且可以确定它们是否兼容。按照语义版本规范,主版本号不同的两个版本是相互不兼容的。而且,在主版本号相同的情况下,次版本号大都是向后兼容次版本号小的版本。补丁版本号也不影响兼容性。
例如 v1.8.1 是兼容 v1.7.0 的导入方式相同,而 v2.0.0 与前两个不兼容,导入方式需要做改变。
Go Module 给出了一个方法:将包主版本号引入到包导入路径中,我们可以像下面这样导入 logrus v2.0.0 版本依赖包:

1
2
3
4
5
import "github.com/sirupsen/logrus/v2"


import github.com/user/repo/v0 等价于 import github.com/user/repo
import github.com/user/repo/v1 等价于 import github.com/user/repo

最小版本选择 (Minimal Version Selection)

Go 会在该项目依赖项的所有版本中,选出符合项目整体要求的“最小版本”。

mod 命令

  • 控制依赖版本:go get 直接指定版本号 go get github.com/sirupsen/logrus@v1.7.0
  • 跨版本升级需要修改大版本号:go get github.com/go-redis/redis/v8
  • 移除依赖:源码中删除导入语句,之后 go mod tidy
  • 列出所有依赖:go list -m all

空导入

1
2
3
4
5
import _ "foo"
import(
    _ "github.com/go-redis/redis/v7" // “_”为空导入
    //有时候我们导入一个包,只是为了确保它进行了初始化,而无需使用包中的任何函数或变量。例如,我们或许需要确保调用了 rectangle 包的 init 函数,而不需要在代码中使用它。这种情况也可以使用空白标识符。
)

空导入也是导入,意味着我们将依赖foo这个路径下的包。但由于是空导入,我们并没有显式使用这个包中的任何语法元素。通常实践中空导入意味着期望依赖包的init函数得到执行,这个init函数中有我们需要的逻辑。

go 项目布局

可执行项目

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
exe-layout
├── cmd/
│   ├── app1/
│   │   └── main.go
│   └── app2/
│       └── main.go
├── go.mod
├── go.sum
├── internal/
│   ├── pkga/
│   │   └── pkg_a.go
│   └── pkgb/
│       └── pkg_b.go
├── pkg1/
│   └── pkg1.go
├── pkg2/
│   └── pkg2.go
└── vendor/
  • cmd 目录:存放需要编译构建的 main 包源文件,如果有多个可执行文件,每个 main 包单独存放在一个子目录中,如果只有一个可执行程序,则可以简化删除 cmd 目录,并把 main 放到根目录下。
    而且通常来说,main 包应该很简洁。我们在 main 包中会做一些命令行参数解析、资源初始化、日志设施初始化、数据库连接初始化等工作,之后就会将程序的执行权限交给更高级的执行控制对象。另外,也有一些 Go 项目将 cmd 这个名字改为 app 或其他名字,但它的功能其实并没有变。
  • pkgN 目录:存放项目自身需要使用的库文件,同时该目录下的包可以被外部项目引用。
  • vendor 目录:在项目本地缓存特定版本依赖包的目录,用于实现可重现构建(可选目录,现包管理使用 go module)。

库项目

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
lib-layout
├── go.mod
├── internal/
│   ├── pkga/
│   │   └── pkg_a.go
│   └── pkgb/
│       └── pkg_b.go
├── pkg1/
│   └── pkg1.go
└── pkg2/
    └── pkg2.go
  • internal 目录:导出路径包含internal关键字的包,只允许internal的父级目录及父级目录的子包导入,其它包无法导入。仅限项目内部使用,不想暴露到外部的包。
    如果项目仅有一个包,则可以简化 pkg 目录,将包放到跟目下。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.
├── module1
│   ├── go.mod
│   ├── internal
│   │   └── pkga
│   ├── pkg1
│   └── pkg2
└── module2
    ├── go.mod
    └── pkg1

例如上面这个例子,internal 的父级目录为 module1,父级目录的子包有 module1/pkg1module1/pkg2 ,所以 internal 只能被这三个地方引入。
该约定由 go 编译器验证,internal 是特殊目录名。

一个非官方的布局指南: https://github.com/golang-standards/project-layout

基础类型参数传递

  1. 引用类型:切片,映射,通道,接口和函数都是引用类型。引用类型传参会传递地址的副本,实际效果等同与传递引用,外部修改该类型同样会改变接收该类型的数据。
  2. 值传递:整型、数组、结构体等作为参数传递时是通过逐位拷贝,默认传递副本因此如果需要改变原变量需要使用指针。
  3. 不支持比较的类型:函数、map、切片只支持与 nil 的比较,不支持同类型之间的比较。
  4. Go 中的函数参数传递采用值拷贝,参数为实际参数的一个副本。
  5. *T 为接受者的方法集合除了自己的方法还包括 T 类型接受者的方法。

receiver 方法接受者类型

  • 当 receiver 参数的类型为 T 时:
    当我们选择以 T 作为 receiver 参数类型时,M1 方法等价转换为 F1(t T)。我们知道,Go 函数的参数采用的是值拷贝传递,也就是说,F1 函数体中的 t 是 T 类型实例的一个副本。这样,我们在 F1 函数的实现中对参数 t 做任何修改,都只会影响副本,而不会影响到原 T 类型实例。
  • 当 receiver 参数的类型为 *T 时:
    当我们选择以 *T 作为 receiver 参数类型时,M2 方法等价转换为 F2(t *T)。同上面分析,我们传递给 F2 函数的 t 是 T 类型实例的地址,这样 F2 函数体中对参数 t 做的任何修改,都会反映到原 T 类型实例上。
 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 (t T) M1() <=> F1(t T)
func (t *T) M2() <=> F2(t *T)

// 放射得到方法集合
func dumpMethodSet(i interface{}) {
    dynTyp := reflect.TypeOf(i)

    if dynTyp == nil {
        fmt.Printf("there is no dynamic type\n")
        return
    }

    n := dynTyp.NumMethod()
    if n == 0 {
        fmt.Printf("%s's method set is empty!\n", dynTyp)
        return
    }

    fmt.Printf("%s's method set:\n", dynTyp)
    for j := 0; j < n; j++ {
        fmt.Println("-", dynTyp.Method(j).Name)
    }
    fmt.Printf("\n")
}

string

注意

Go 中 string 采用 Unicode 字符集,编码存储采用 UTF-8。而 Go 中特有的 rune 类型用于表示一个 Unicode 码点,实际 type rune = int32 rune 是 int32 类型的别名。
string 类型实际存储的是一个“描述符”,其包括两个字段 data, len data 指向底层数组。 在遍历 string 需要注意 forfor range 的区别,for 是 UTF-8 中的一个字节, for range 是 Unicode 的码点值: RuneCountInString 获取码点个数。

  • for range 遍历字符串时 v 类型是 rune

  • 直接使用 s[i] 取得的类型是 byte

  • for 循环取得的是 byte

  • len(str) 返回长度等于 len([]byte(str)),一个英文字符长度加一,一个中文字符长度加三。

  • 英文和数字可以直接使用 byte 若存在中文字符或其他则需要使用 rune

  • c := '2': 默认是 rune 类型(int32)

  • var rc rune = '2'var bc byte = '2' : 自动转换为指定类型

 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
var s = "中国人"

for i := 0; i < len(s); i++ {
  fmt.Printf("index: %d, value: 0x%x\n", i, s[i])
}

输出
index: 0, value: 0xe4
index: 1, value: 0xb8
index: 2, value: 0xad
index: 3, value: 0xe5
index: 4, value: 0x9b
index: 5, value: 0xbd
index: 6, value: 0xe4
index: 7, value: 0xba
index: 8, value: 0xba

--------------

var s = "中国人"

for i, v := range s {
    fmt.Printf("index: %d, value: 0x%x\n", i, v)
}
输出
index: 0, value: 0x4e2d
index: 3, value: 0x56fd
index: 6, value: 0x4eba

切片与字符串转换:

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

var s string = "中国人"
                      
// string -> []rune
rs := []rune(s) 
fmt.Printf("%x\n", rs) // [4e2d 56fd 4eba]
                
// string -> []byte
bs := []byte(s) 
fmt.Printf("%x\n", bs) // e4b8ade59bbde4baba
                
// []rune -> string
s1 := string(rs)
fmt.Println(s1) // 中国人
                
// []byte -> string
s2 := string(bs)
fmt.Println(s2) // 中国人

map

  • map 自身是无序的,每次遍历都会不同。(可以用一个有序结构存储 key,如 slice,然后 for 这个 slice,用 key 获取值。)
  • map 传递的同样是描述符,开销很小。
  • 将 map 传递到函数或方法内部后的修改外部可见。
  • 使用结构体作为 map 的 key,需要保证结构体字段逻辑上不可变,若改变字段则会导致 map 无法找到对应的 key。
  1. 查找操作,“comma ok” 的惯用法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
m := make(map[string]int)
v := m["key1"] // 若不存在则返回对应类型的零值

// 正确查询方式:
m := make(map[string]int)
v, ok := m["key1"]
if !ok {
    // "key1"不在map中
}
// "key1"在map中,v将被赋予"key1"键对应的value
  1. 删除操作: 需要删除的键并不存在也会成功删除。
1
2
3
4
5
6
7
8
m := map[string]int {
  "key1" : 1,
  "key2" : 2,
}

fmt.Println(m) // map[key1:1 key2:2]
delete(m, "key2") // 删除"key2"
fmt.Println(m) // map[key1:1]

数组/切片传递

  • for range 循环中迭代的是数组副本,并不是真正的数组;
  • 切片由于底层是指向数组的指针,在 for range 中复制的副本仍然指向同一个数组,因此可以做到在迭代中改变底层值;
  • 切片和 map 包含指向底层数据的指针,当作为函数参数传递或复制时最好创建副本使用 copy 函数显示复制。
  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
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
/*
    数组
*/
func main() {
    var a = [5]int{1, 2, 3, 4, 5}
    var r [5]int

    fmt.Println("original a =", a)

    for i, v := range a {
        if i == 0 {
            a[1] = 12
            a[2] = 13
        }
        r[i] = v
    }

    fmt.Println("after for range loop, r =", r)
    fmt.Println("after for range loop, a =", a)
}

// 实际输出:
original a = [1 2 3 4 5]
after for range loop, r = [1 2 3 4 5]
after for range loop, a = [1 12 13 4 5]

/*
    切片
*/

func main() {
    var a = [5]int{1, 2, 3, 4, 5}
    var r [5]int

    fmt.Println("original a =", a)

    for i, v := range a[:] { // range &a 也是可以的
        if i == 0 {
            a[1] = 12
            a[2] = 13
        }
        r[i] = v
    }

    fmt.Println("after for range loop, r =", r)
    fmt.Println("after for range loop, a =", a)
}


original a = [1 2 3 4 5]
after for range loop, r = [1 12 13 4 5]
after for range loop, a = [1 12 13 4 5]

-----------------------------------------------
// 安全复制切片
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = make([]Trip, len(trips))
  copy(d.trips, trips)
}

trips := ...
d1.SetTrips(trips)

// 这里我们修改 trips[0],但不会影响到 d1.trips
trips[0] = ...

---------------------返回 slice  map 对用户暴露内部状态-------------
// BAD
type Stats struct {
  mu sync.Mutex

  counters map[string]int
}

// Snapshot 返回当前状态。
func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock()

  return s.counters
}

// snapshot 不再受互斥锁保护
// 因此对 snapshot 的任何访问都将受到数据竞争的影响
// 影响 stats.counters
snapshot := stats.Snapshot()

// GOOD
type Stats struct {
  mu sync.Mutex

  counters map[string]int
}

func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock()

  result := make(map[string]int, len(s.counters))
  for k, v := range s.counters {
    result[k] = v
  }
  return result
}

// snapshot 现在是一个拷贝
snapshot := stats.Snapshot()

各种初始化

包初始化顺序

Go 采用深度优先原则初始化包中的常量、变量、init()。先根据包导入顺序选择第一个包初始化,之后采用深度优先方式初始化第一个包的所有依赖包,全部包初始化完成后再初始化 main

  • 常量 -> 变量 -> init
  • 被多个包依赖的包只会初始化一次
  • 包内多个 init 函数按出现次序调用

init 函数用途

  1. 包级变量检查或重置
  2. 包级变量复杂初始化
  3. 实现注册模式
注意
  1. init 在实际使用时并不受欢迎,误用会对测试造成干扰
  2. 一个包中的 init 不应依赖包外的环境,不应对包外环境造成影响
  3. 如果违反上述原则,则应该自定义 Init() 函数显式初始化
  4. 反例单元测试可以使用 init
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func TestCURDPlayer(t *testing.T) {
	// 测试 curd 玩家信息
    // balabala
}

func TestCURDStore(t *testing.T) {
	// 测试 curd 商店信息
    // balabala
}

func TestCURDMail(t *testing.T) {
	// 测试 curd 邮件信息
    // balabala
}

func init(){
    initdb()
}

func initdb(){
    // sql.Open()...
}

数组与切片

  1. 稀疏数组显示初始化:
1
2
3
4
var arr4 = [...]int{
    99: 39, // 将第100个元素(下标值为99)的值赋值为39,其余元素值均为0
}
fmt.Printf("%T\n", arr4) // [100]int
  1. 数组切片化:多个切片共享同一个底层数组。
1
2
3
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
s1 := arr[3:7:9] // 初始下标,结束下标,容量
s2 := arr[1:4] // 默认容量为起始到数组结束的长度 9

注意小陷阱:当切片扩容时会重新创建新的底层数组,与旧的数组解除绑定,这时修改就数组是无法反应到新切片上的
3. 切片初始化:

1
2
3
4
5
var sl1 []int
var sl2 = []int{}
piles := []int{3, 6, 7, 11}
s1是声明还没初始化是nil值底层没有分配内存空间(append 中会初始化空切片)
s2初始化了不是nil值底层分配了内存空间有地址

map 初始化

不同与切片可以通过 append 直接对初始为 nil 的切面操作做到“零值可用”,map 使用时必须进行初始化。

  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
m := map[int]string{}

// 完整的不省略类型初始化
m1 := map[int][]string{
    1: []string{"val1_1", "val1_2"},
    3: []string{"val3_1", "val3_2", "val3_3"},
    7: []string{"val7_1"},
}

type Position struct { 
    x float64 
    y float64
}

m2 := map[Position]string{
    Position{29.935523, 52.568915}: "school",
    Position{25.352594, 113.304361}: "shopping-mall",
    Position{73.224455, 111.804306}: "hospital",
}

// 可以省略类型初始化
m2 := map[Position]string{
    {29.935523, 52.568915}: "school",
    {25.352594, 113.304361}: "shopping-mall",
    {73.224455, 111.804306}: "hospital",
}
  1. make 显示初始化:
1
2
m1 := make(map[int]string) // 未指定初始容量
m2 := make(map[int]string, 8) // 指定初始容量为8

结构体初始化

1
2
3
4
5
6
type point struct{
    x, y int
}
a := new(point)
b := &point{}
c := point{}

ab 实际都返回的是指针,c 返回的是类型实体。

嵌套结构体初始化可以省略内部结构体的字段名,直接使用类型名即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 不省略写法
type Person struct {
    Name string
    Phone string
    Addr string
}

type Book struct {
    Title string
    Author Person
    ... ...
}

// 省略写法
type Book struct {
    Title string
    Person
    ... ...
}

嵌入字段访问是也可使用语法糖,跳过类型直接访问字段。

1
2
3
var book Book 
println(book.Person.Phone) // 将类型名当作嵌入字段的名字
println(book.Phone)        // 支持直接访问嵌入字段所属类型中字段
  1. 结构体零值初始化:
1
var book Book // book为零值结构体变量

一些结构体零值无需初始化即可用如 sync.Mutex, bytes.Buffer

1
2
3
4
5
6
7
var mu sync.Mutex
mu.Lock()
mu.Unlock()

var b bytes.Buffer
b.Write([]byte("Hello, Go"))
fmt.Println(b.String()) // 输出:Hello, Go
  1. 符合字面值初始化: 按顺序给每个字段赋值,当结构体类型定义中的字段顺序发生变化,或者字段出现增删操作时,我们就需要手动调整该结构体类型变量的显式初始化代码,让赋值顺序与调整后的字段顺序一致。 一旦结构体中包含非导出字段,那么这种逐一字段赋值的方式就不再被支持了。
1
2
3
4
5
6
type Book struct {
    Title string              // 书名
    Pages int                 // 书的页数
    Indexes map[string]int    // 书的索引
}
var book = Book{"The Go Programming Language", 700, make(map[string]int)}
  1. “field:value” 形式的复合字面值初始化: 推荐做法
1
2
3
4
5
var t = T{
    F2: "hello",
    F1: 11,
    F4: 14,
}
  1. 初始化含有非导出字段且零值不可用的结构体:
    实现专用构造函数:例如 Timer
    非导出字段在 NewT 内部初始化,返回 T 类型的指针。
 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 NewT(field1, field2, ...) *T {
    ... ...
}

// $GOROOT/src/time/sleep.go
type runtimeTimer struct {
    pp       uintptr
    when     int64
    period   int64
    f        func(interface{}, uintptr) 
    arg      interface{}
    seq      uintptr
    nextwhen int64
    status   uint32
}

type Timer struct {
    C <-chan Time
    r runtimeTimer
}

// $GOROOT/src/time/sleep.go
func NewTimer(d Duration) *Timer {
    c := make(chan Time, 1)
    t := &Timer{
        C: c,
        r: runtimeTimer{
            when: when(d),
            f:    sendTime,
            arg:  c,
        },
    }
    startTimer(&t.r)
    return t
}

结构体嵌入接口类型

实现“伪类”,简化单元测试,嵌入某接口类型的结构体类型的方法集合包含了这个接口类型的方法集合,这就意味着,这个结构体类型也是它嵌入的接口类型的一个实现。即便结构体类型自身并没有实现这个接口类型的任意一个方法,也没有关系。 如下的例子:

 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
package employee
  
type Result struct {
    Count int
}

func (r Result) Int() int { return r.Count }

type Rows []struct{}

type Stmt interface {
    Close() error
    NumInput() int
    Exec(stmt string, args ...string) (Result, error)
    Query(args []string) (Rows, error)
}

// 返回男性员工总数
func MaleCount(s Stmt) (int, error) {
    result, err := s.Exec("select count(*) from employee_tab where gender=?", "1")
    if err != nil {
        return 0, err
    }

    return result.Int(), nil
}

----------------------------
package employee
  
import "testing"

type fakeStmtForMaleCount struct {
    Stmt
}

func (fakeStmtForMaleCount) Exec(stmt string, args ...string) (Result, error) {
    return Result{Count: 5}, nil
}

func TestEmployeeMaleCount(t *testing.T) {
    f := fakeStmtForMaleCount{}
    c, _ := MaleCount(f)
    if c != 5 {
        t.Errorf("want: %d, actual: %d", 5, c)
        return
    }
}

在这个例子中,我们有一个 employee 包,这个包中的方法 MaleCount,通过传入的 Stmt 接口的实现从数据库获取男性员工的数量。现在我们的任务是要对 MaleCount 方法编写单元测试代码。对于这种依赖外部数据库操作的方法,我们的惯例是使用“伪对象(fake object)”来冒充真实的 Stmt 接口实现。不过现在有一个问题,那就是 Stmt 接口类型的方法集合中有四个方法,而 MaleCount 函数只使用了 Stmt 接口的一个方法 Exec。如果我们针对每个测试用例所用的伪对象都实现这四个方法,那么这个工作量有些大。
我们为 TestEmployeeMaleCount 测试用例建立了一个 fakeStmtForMaleCount 的伪对象类型,然后在这个类型中嵌入了 Stmt 接口类型。这样 fakeStmtForMaleCount 就实现了 Stmt 接口,我们也实现了快速建立伪对象的目的。接下来我们只需要为 fakeStmtForMaleCount 实现 MaleCount 所需的 Exec 方法,就可以满足这个测试的要求了。

结构体嵌入结构体的继承陷阱

 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 T1 struct{}

func (T1) T1M1()   { println("T1's M1") }
func (*T1) PT1M2() { println("PT1's M2") }

type T2 struct{}

func (T2) T2M1()   { println("T2's M1") }
func (*T2) PT2M2() { println("PT2's M2") }

type T struct {
    T1
    *T2
}

func main() {
    t := T{
        T1: T1{},
        T2: &T2{},
    }

    dumpMethodSet(t)
    dumpMethodSet(&t)
}
  • 类型 T 的方法集合 = T1 的方法集合 + *T2 的方法集合
  • 类型 *T 的方法集合 = *T1 的方法集合 + *T2 的方法集合
    虽然 T 中 嵌入的是 T1 类型结构体,但 *T 继承了 *T1 中的方法。

类型嵌入语法糖陷阱

 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
type T1 int
type t2 struct{
    n int
    m int
}

type I interface {
    M1()
}

type S1 struct {
    T1
    *t2
    I
    a int
    b string
}

type S2 struct { 
    T1 T1
    t2 *t2
    I  I
    a  int
    b  string
}
  • 这两个S1与S2不等价,区别是:S1结构体能调用代理嵌入类型的所有方法,S2结构体是没有代理嵌入类型方法。
  • S2 需要 S2.I.M1 调用

性能提升实践

字符串拼接

如果能知道拼接字符串的个数,那么使用 bytes.Bufferstrings.BuilderGrows 申请空间后,性能是最好的;如果不能确定长度,那么 bytes.Bufferstrings.Builder 也比“+”和 fmt.Sprintf 性能好很多。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func builderConcat(n int, str string) string {
	var builder strings.Builder
	for i := 0; i < n; i++ {
		builder.WriteString(str)
	}
	return builder.String()
}

func bufferConcat(n int, s string) string {
	buf := new(bytes.Buffer)
	for i := 0; i < n; i++ {
		buf.WriteString(s)
	}
	return buf.String()
}

不额外进行内存分配 string 与 []byte 互相转换:

常用的转换 bytes := []byte(str)str := string(bytes) 转换过程都需要进行额外的内存分配。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 通过底层数据转换
package strbytesconv

import (
	"reflect"
	"unsafe"
)

// StringToBytes 实现string 转换成 []byte, 不用额外的内存分配
func StringToBytes(str string) (bytes []byte) {
	ss := *(*reflect.StringHeader)(unsafe.Pointer(&str))
	bs := (*reflect.SliceHeader)(unsafe.Pointer(&bytes))
	bs.Data = ss.Data
	bs.Len = ss.Len
	bs.Cap = ss.Len
	return bytes
}

// BytesToString 实现 []byte 转换成 string, 不需要额外的内存分配
func BytesToString(bytes []byte) string {
	return *(*string)(unsafe.Pointer(&bytes))
}

快速填充数组或切片的 trick

使用下面的方法要比直接用 for 循环或 for range 性能高 20 倍左右。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Preload the first value into the array/slice
bigSlice[0] = 67      

// Incrementally duplicate the value into the rest of the container
for j := 1; j < len(bigSlice); j *= 2 {
    copy(bigSlice[j:], bigSlice[:j])
}

-----------------------------------------
// Define the pattern
pattern := []byte{0xde, 0xad, 0xbe, 0xef}

// Copy the pattern into the start of the container
copy(bigSlice, pattern)

// Incrementally duplicate the pattern throughout the container
for j := len(pattern); j < len(bigSlice); j *= 2 {
    copy(bigSlice[j:], bigSlice[:j])
}

具体测试数据如这篇文章:Filling an array or slice with a repeated pattern

初始化切片提供容量值

BadGood
1
2
3
4
5
6
for n := 0; n < b.N; n++ {
  data := make([]int, 0)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}
1
2
3
4
5
6
for n := 0; n < b.N; n++ {
  data := make([]int, 0, size)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}
1
BenchmarkBad-4    100000000    2.48s
1
BenchmarkGood-4   100000000    0.21s

空结构体

空结构体变量大小为 0 ,日常代码中常使用空结构体作为“事件”信息在 goroutine 中通信。

1
2
var c = make(chan Empty) // 声明一个元素类型为Empty的channel
c<-Empty{}               // 向channel写入一个“事件”

分支控制

将匹配概率高的分支放在前面,有助于提升执行效率,如 switch, if-else

使用小接口

接口方法尽量在 1-3 个之间,接口越大抽象成都越弱。

GC 相关内存优化

  1. 语义垃圾(内存泄漏)
    初始化一个长度为 5 的切片,每个元素指向一个堆上的 10MB 大小的空间,当该切片缩容时与其关联的对上的对应内存仍然无法释放。缩容前需要先将元素内容置为 nil
1
2
3
arr := make([]*MyStructOnHeap, 5)

arr = arr[:3]

函数妙用

显示转型

下面这个例子中 http.HandlerFunc(greeting) 所做的是将 greeting 函数显示转型为 http.HandlerFunc 函数。HandlerFunc 是一个基于函数类型定义的新类型,它的底层类型为函数类型func(ResponseWriter, *Request)。这个类型有一个方法 ServeHTTP,然后实现了 Handler 接口。也就是说http.HandlerFunc(greeting)这句代码的真正含义,是将函数 greeting 显式转换为 HandlerFunc 类型,后者实现了 Handler 接口,满足 ListenAndServe 函数第二个参数的要求。

 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 greeting(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome, Gopher!\n")
}                    

func main() {
    http.ListenAndServe(":8080", http.HandlerFunc(greeting))
}

----------------
// $GOROOT/src/net/http/server.go
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

// $GOROOT/src/net/http/server.go
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

// $GOROOT/src/net/http/server.go
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
        f(w, r)
}

闭包简化函数调用

下面的 partialTimes 返回了一个匿名函数形成了闭包,下面固定了高频乘数 2、3、4。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func times(x, y int) int {
  return x * y
}

func partialTimes(x int) func(int) int {
  return func(y int) int {
    return times(x, y)
  }
}

func main() {
  timesTwo := partialTimes(2)   // 以高频乘数2为固定乘数的乘法函数
  timesThree := partialTimes(3) // 以高频乘数3为固定乘数的乘法函数
  timesFour := partialTimes(4)  // 以高频乘数4为固定乘数的乘法函数
  fmt.Println(timesTwo(5))   // 10,等价于times(2, 5)
  fmt.Println(timesTwo(6))   // 12,等价于times(2, 6)
  fmt.Println(timesThree(5)) // 15,等价于times(3, 5)
  fmt.Println(timesThree(6)) // 18,等价于times(3, 6)
  fmt.Println(timesFour(5))  // 20,等价于times(4, 5)
  fmt.Println(timesFour(6))  // 24,等价于times(4, 6)
}

函数也是一种可以有方法的类型

1
2
3
4
5
6
type MyFunc func(a int, b int) int

func (m MyFunc) myname(a int, b int) int {
	c := m(a, b)
	return c
}

函数式选项模式

当初始化结构体包含多个必填字段与多个可选字段并含有默认值,为灵活初始化结构体常使用函数式选项模式。
如下面的例子:
option 函数命名常用 with 开头。

 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
type Foo struct {
    name string
    id int
    age int

    db interface{}
}

// FooOption 代表可选参数
type FooOption func(foo *Foo)

// WithName 代表Name为可选参数
func WithName(name string) FooOption {
   return func(foo *Foo) {
      foo.name = name
   }
}

// WithAge 代表age为可选参数
func WithAge(age int) FooOption {
   return func(foo *Foo) {
      foo.age = age
   }
}

// WithDB 代表db为可选参数
func WithDB(db interface{}) FooOption {
   return func(foo *Foo) {
      foo.db = db
   }
}

// NewFoo 代表初始化
func NewFoo(id int, options ...FooOption) *Foo {
   foo := &Foo{
      name: "default",
      id:   id,
      age:  10,
      db:   nil,
   }
   for _, option := range options {
      option(foo)
   }
   return foo
}


// 具体使用NewFoo的函数
func Bar() {
   foo := NewFoo(1, WithAge(15), WithName("foo"))
   fmt.Println(foo)
}

错误处理方式

透明错误处理策略

在不关心错误上下文的情况下常用该方式处理返回的错误。

1
2
3
4
5
6
7
err := doSomething()
if err != nil {
    // 不关心err变量底层错误值所携带的具体上下文信息
    // 执行简单错误处理逻辑并返回
    ... ...
    return err
}

哨兵错误处理方式

当需要对不同的错误值进行分类处理时会使用哨兵方式,“哨兵”错误值变量以 ErrXXX 格式命名。这样做意味着该包需要同时维护导出的错误。

 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
// $GOROOT/src/bufio/bufio.go
var (
    ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
    ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
    ErrBufferFull        = errors.New("bufio: buffer full")
    ErrNegativeCount     = errors.New("bufio: negative count")
)

data, err := b.Peek(1)
if err != nil {
    switch err {
    case bufio.ErrNegativeCount:
        // ... ...
        return
    case bufio.ErrBufferFull:
        // ... ...
        return
    case bufio.ErrInvalidUnreadByte:
        // ... ...
        return
    default:
        // ... ...
        return
    }
}

从 Go 1.13 版本开始,标准库 errors 包提供了 Is 函数用于错误处理方对错误值的检视。Is 函数类似于把一个 error 类型变量与“哨兵”错误值进行比较,比如下面代码:
如果 error 类型变量的底层错误值是一个包装错误(Wrapped Error),errors.Is 方法会沿着该包装错误所在错误链(Error Chain),与链上所有被包装的错误(Wrapped Error)进行比较,直至找到一个匹配的错误为止。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 类似 if err == ErrOutOfBounds{ … }
if errors.Is(err, ErrOutOfBounds) {
    // 越界的错误处理
}

var ErrSentinel = errors.New("the underlying sentinel error")

func main() {
  err1 := fmt.Errorf("wrap sentinel: %w", ErrSentinel)
  err2 := fmt.Errorf("wrap err1: %w", err1)
    println(err2 == ErrSentinel) //false
  if errors.Is(err2, ErrSentinel) {
    println("err2 is ErrSentinel")
    return
  }

  println("err2 is not ErrSentinel")
}

// 输出
false
err2 is ErrSentinel

通过比较操作符对 err2 与 ErrSentinel 进行比较后,我们发现这二者并不相同。而 errors.Is 函数则会沿着 err2 所在错误链,向下找到被包装到最底层的“哨兵”错误值 ErrSentinel。

错误值类型检视策略(自定义错误类型)

通过自定义错误类型提供更多的错误上下文信息,错误处理方需要使用 Go 提供的类型断言机制(Type Assertion)或类型选择机制(Type Switch)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// $GOROOT/src/encoding/json/decode.go
type UnmarshalTypeError struct {
    Value  string       
    Type   reflect.Type 
    Offset int64        
    Struct string       
    Field  string      
}

// $GOROOT/src/encoding/json/decode.go
func (d *decodeState) addErrorContext(err error) error {
    if d.errorContext.Struct != nil || len(d.errorContext.FieldStack) > 0 {
        switch err := err.(type) {
        case *UnmarshalTypeError:
            err.Struct = d.errorContext.Struct.Name()
            err.Field = strings.Join(d.errorContext.FieldStack, ".")
            return err
        }
    }
    return err
}

标准库 errors 包提供了As函数给错误处理方检视错误值。As函数类似于通过类型断言判断一个 error 类型变量是否为特定的自定义错误类型。如果 error 类型变量的动态错误值是一个包装错误,errors.As函数会沿着该包装错误所在错误链,与链上所有被包装的错误的类型进行比较,直至找到一个匹配的错误类型,就像 errors.Is 函数那样。如下面代码所示:

 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
// 类似 if e, ok := err.(*MyError); ok { … }
var e *MyError
if errors.As(err, &e) {
    // 如果err类型为*MyError,变量e将被设置为对应的错误值
}

type MyError struct {
    e string
}

func (e *MyError) Error() string {
    return e.e
}

func main() {
    var err = &MyError{"MyError error demo"}
    err1 := fmt.Errorf("wrap err: %w", err)
    err2 := fmt.Errorf("wrap err1: %w", err1)
    var e *MyError
    if errors.As(err2, &e) {
        println("MyError is on the chain of err2")
        println(e == err)                  
        return                             
    }                                      
    println("MyError is not on the chain of err2")
} 

错误类型检视策略

为了降低耦合将某个包中的错误类型归类,统一提取出一些公共的错误行为特征,并将这些错误行为特征放入一个公开的接口类型中。以标准库中的net包为例,它将包内的所有错误类型的公共行为特征抽象并放入net.Error这个接口中,如下面代码:

 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
// $GOROOT/src/net/net.go
type Error interface {
    error
    Timeout() bool  
    Temporary() bool
}

// $GOROOT/src/net/http/server.go
func (srv *Server) Serve(l net.Listener) error {
    ... ...
    for {
        rw, e := l.Accept()
        if e != nil {
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
            if ne, ok := e.(net.Error); ok && ne.Temporary() {
                // 注:这里对临时性(temporary)错误进行处理
                ... ...
                time.Sleep(tempDelay)
                continue
            }
            return e
        }
        ...
    }
    ... ...
}

接口类型断言

类似 v, ok := i.(T) 形式的代码表示接口断言(判断并取出接口内存储的真正类型变量)。若 T 是一个接口类型,那么类型断言的语义就会变成:断言 i 的值实现了接口类型 T。如果断言成功,变量 v 的类型为 i 的值的类型,而并非接口类型 T。如果断言失败,v 的类型信息为接口类型 T,它的值为 nil。
如下的类型断言例子:

 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
type MyInterface interface {
    M1()
}

type T int
               
func (T) M1() {
    println("T's M1")
}              
               
func main() {  
    var t T    
    var i interface{} = t
    v1, ok := i.(MyInterface)
    if !ok {   
        panic("the value of i is not MyInterface")
    }          
    v1.M1()    
    fmt.Printf("the type of v1 is %T\n", v1) // the type of v1 is main.T
               
    i = int64(13)
    v2, ok := i.(MyInterface)
    fmt.Printf("the type of v2 is %T\n", v2) // the type of v2 is <nil>
    // v2 = 13 //  cannot use 1 (type int) as type MyInterface in assignment: int does not implement MyInterface (missing M1   method) 
}

------------------------------------

var a int64 = 13
var i interface{} = a
v1, ok := i.(int64) 
fmt.Printf("v1=%d, the type of v1 is %T, ok=%t\n", v1, v1, ok) // v1=13, the type of v1 is int64, ok=true
v2, ok := i.(string)
fmt.Printf("v2=%s, the type of v2 is %T, ok=%t\n", v2, v2, ok) // v2=, the type of v2 is string, ok=false
v3 := i.(int64) 
fmt.Printf("v3=%d, the type of v3 is %T\n", v3, v3) // v3=13, the type of v3 is int64
v4 := i.([]int) // panic: interface conversion: interface {} is int64, not []int
fmt.Printf("the type of v4 is %T\n", v4) 

接口类型比较

对于空接口类型变量,只有 _type 和 data 所指数据内容一致的情况下,两个空接口类型变量之间才能划等号。另外,Go 在创建 eface 时一般会为 data 重新分配新内存空间,将动态类型变量的值复制到这块内存空间,并将 data 指针指向这块内存空间。因此我们多数情况下看到的 data 指针值都是不同的。

 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

// $GOROOT/src/runtime/print.go
func printeface(e eface) {
    print("(", e._type, ",", e.data, ")")
}

func printiface(i iface) {
    print("(", i.tab, ",", i.data, ")")
}

func printNilInterface() {
  // nil接口变量
  var i interface{} // 空接口类型
  var err error     // 非空接口类型
  println(i)
  println(err)
  println("i = nil:", i == nil)
  println("err = nil:", err == nil)
  println("i = err:", i == err)
}
//Outpute:
(0x0,0x0)
(0x0,0x0)
i = nil: true
err = nil: true
i = err: true
--------------------------------------

func printEmptyInterface() {
    var eif1 interface{} // 空接口类型
    var eif2 interface{} // 空接口类型
    var n, m int = 17, 18

    eif1 = n
    eif2 = m

    println("eif1:", eif1)
    println("eif2:", eif2)
    println("eif1 = eif2:", eif1 == eif2) // false

    eif2 = 17
    println("eif1:", eif1)
    println("eif2:", eif2)
    println("eif1 = eif2:", eif1 == eif2) // true

    eif2 = int64(17)
    println("eif1:", eif1)
    println("eif2:", eif2)
    println("eif1 = eif2:", eif1 == eif2) // false
}
 // Outpute:
eif1: (0x10ac580,0xc00007ef48)
eif2: (0x10ac580,0xc00007ef40)
eif1 = eif2: false
eif1: (0x10ac580,0xc00007ef48)
eif2: (0x10ac580,0x10eb3d0)
eif1 = eif2: true
eif1: (0x10ac580,0xc00007ef48)
eif2: (0x10ac640,0x10eb3d8)
eif1 = eif2: false

非空接口类型,和空接口类型变量一样,只有 tab 和 data 指的数据内容一致的情况下,两个非空接口类型变量之间才能划等号。err1 = (*T)(nil) 针对这种赋值,println 输出的 err1 是(0x10ed120, 0x0),也就是非空接口类型变量的类型信息并不为空,数据指针为空,因此它与 nil(0x0,0x0)之间不能划等号。

 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
type T int

func (t T) Error() string { 
    return "bad error"
}

func printNonEmptyInterface() { 
    var err1 error // 非空接口类型
    var err2 error // 非空接口类型
    err1 = (*T)(nil)
    println("err1:", err1)
    println("err1 = nil:", err1 == nil)

    err1 = T(5)
    err2 = T(6)
    println("err1:", err1)
    println("err2:", err2)
    println("err1 = err2:", err1 == err2)

    err2 = fmt.Errorf("%d\n", 5)
    println("err1:", err1)
    println("err2:", err2)
    println("err1 = err2:", err1 == err2)
}
// Output:
err1: (0x10ed120,0x0)
err1 = nil: false
err1: (0x10ed1a0,0x10eb310)
err2: (0x10ed1a0,0x10eb318)
err1 = err2: false
err1: (0x10ed1a0,0x10eb310)
err2: (0x10ed0c0,0xc000010050)
err1 = err2: false

非空接口和空接口类型比较,空接口类型变量和非空接口类型变量内部表示的结构有所不同(第一个字段:_type vs. tab),两者似乎一定不能相等。但 Go 在进行等值比较时,类型比较使用的是 eface 的 _type 和 iface 的 tab._type,像例子中看到的那样,当 eif 和 err 都被赋值为T(5)时,两者之间是划等号的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func printEmptyInterfaceAndNonEmptyInterface() {
  var eif interface{} = T(5)
  var err error = T(5)
  println("eif:", eif)
  println("err:", err)
  println("eif = err:", eif == err)

  err = T(6)
  println("eif:", eif)
  println("err:", err)
  println("eif = err:", eif == err)
}
// Output:
eif: (0x10b3b00,0x10eb4d0)
err: (0x10ed380,0x10eb4d8)
eif = err: true
eif: (0x10b3b00,0x10eb4d0)
err: (0x10ed380,0x10eb4e0)
eif = err: false

接口合理性验证

用于在编一阶段验证接口是否合法。如果 *Handler 与 http.Handler 的接口不匹配, 那么语句 var _ http.Handler = (*Handler)(nil) 将无法编译通过。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type Handler struct {
  // ...
}
// 用于触发编译期的接口的合理性检查机制
// 如果 Handler 没有实现 http.Handler,会在编译期报错
var _ http.Handler = (*Handler)(nil)
func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  // ...
}

指针

  • & 取地址符
  • * 取值符
  • go 中一种类型的指针不能显示转换为其他类型,若必须转换可以使用 unsafe.Pointer
  • 不支持指针运算如 p = p+1 非要使用可以用 var i uintptr

unsafe.Pointer

unsafe.Pointer 类似于 C 语言中的 void*,用于表示一个通用指针类型,也就是任何指针类型都可以显式转换为一个 unsafe.Pointer,而 unsafe.Pointer 也可以显式转换为任意指针类型。

1
2
3
4
5
6
7
var p *T
var p1 = unsafe.Pointer(p) // 任意指针类型显式转换为unsafe.Pointer
p = (*T)(p1)               // unsafe.Pointer也可以显式转换为任意指针类型

// 指针赋初值
var a int = 13
var p *int = &a  // 给整型指针变量p赋初值

指针地址大小固定,在 x86-64 平台上指针大小都为 8 字节。

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

import "unsafe"

type foo struct {
    id   string
    age  int8
    addr string
}

func main() {
    var p1 *int
    var p2 *bool
    var p3 *byte
    var p4 *[20]int
    var p5 *foo
    var p6 unsafe.Pointer
    println(unsafe.Sizeof(p1)) // 8 
    println(unsafe.Sizeof(p2)) // 8
    println(unsafe.Sizeof(p3)) // 8
    println(unsafe.Sizeof(p4)) // 8
    println(unsafe.Sizeof(p5)) // 8
    println(unsafe.Sizeof(p6)) // 8
}

iota

iota 是 Go 语言的一个预定义标识符,它表示的是 const 声明块(包括单行声明)中,每个常量所处位置在块中的偏移值(从零开始)。同时,每一行中的 iota 自身也是一个无类型常量,自动参与到不同类型的求值过程中来,不需要我们再对它进行显式转型操作。
每个 iota 的生命周期都始于一个 const 代码块的开始,在该 const 代码块结束时结束。

1
2
3
4
5
6
7
const ( 
    mutexLocked = 1 << iota
    mutexWoken
    mutexStarving
    mutexWaiterShift = iota
    starvationThresholdNs = 1e6
)

首先,这个 const 声明块的第一行是 mutexLocked = 1 « iota ,iota 的值是这行在 const 块中的偏移,因此 iota 的值为 0,我们得到 mutexLocked 这个常量的值为 1 « 0,也就是 1。
接着,第二行:mutexWorken 。因为这个 const 声明块中并没有显式的常量初始化表达式,所以我们根据 const 声明块里“隐式重复前一个非空表达式”的机制,这一行就等价于 mutexWorken = 1 « iota。而且,又因为这一行是 const 块中的第二行,所以它的偏移量 iota 的值为 1,我们得到 mutexWorken 这个常量的值为 1 « 1,也就是 2。
mutexWaiterShift = iota 这一行,这一行为常量 mutexWaiterShift 做了显式初始化,这样就不用再重复前一行了。由于这一行是第四行,而且作为行偏移值的 iota 的值为 3,因此 mutexWaiterShift 的值就为 3。
位于同一行的 iota 即便出现多次,多个 iota 的值也是一样的:

1
2
3
4
5
const (
    Apple, Banana = iota, iota + 10 // 0, 10 (iota = 0)
    Strawberry, Grape // 1, 11 (iota = 1)
    Pear, Watermelon  // 2, 12 (iota = 2)
)

跳过 iota = 0

1
2
3
4
5
6
7
8
const (
    _ = iota // 0
    Pin1
    Pin2
    Pin3
    _
    Pin5    // 5   
)

泛型

什么情况适合使用泛型?

注意
  1. 类型参数的一种有用的情况,就是当编写的函数的操作元素的类型为 slice、map、channel 等特定类型的时候。如果一个函数接受这些类型的形参,并且函数代码没有对参数的元素类型作出任何假设,那么使用类型参数可能会非常有用。在这种场合下,泛型方案可以替代反射方案,获得更高的性能。
  2. 编写通用数据结构。所谓的通用数据结构,指的是像切片或 map 这样,但 Go 语言又没有提供原生支持的类型。比如一个链表或一个二叉树。
  3. 在一些场合,使用类型参数替代接口类型,意味着代码可以避免进行类型断言(type assertion),并且在编译阶段还可以进行全面的类型静态检查。

什么情况不适合使用泛型?

注意

如果你要对某一类型的值进行的全部操作,仅仅是在那个值上调用一个方法,请使用 interface 类型,而不是类型参数。比如,io.Reader 易读且高效,没有必要像下面代码中这样使用一个类型参数像调用 Read 方法那样去从一个值中读取数据:

1
2
func ReadAll[reader io.Reader](r reader) ([]byte, error)  // 错误的作法
func ReadAll(r io.Reader) ([]byte, error)                 // 正确的作法

使用类型参数的原因是它们让你的代码更清晰,如果它们会让你的代码变得更复杂,就不要使用。

使用 any + 断言的缺陷

 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
// max_any.go
func maxAny(sl []any) any {
    if len(sl) == 0 {
        panic("slice is empty")
    }

    max := sl[0]
    for _, v := range sl[1:] {
        switch v.(type) {
        case int:
            if v.(int) > max.(int) {
                max = v
            }
        case string:
            if v.(string) > max.(string) {
                max = v
            }
        case float64:
            if v.(float64) > max.(float64) {
                max = v
            }
        }
    }
    return max
}

func main() {
    i := maxAny([]any{1, 2, -4, -6, 7, 0})
    m := i.(int)
    fmt.Println(m) // 输出:7
    fmt.Println(maxAny([]any{"11", "22", "44", "66", "77", "10"})) // 输出:77
    fmt.Println(maxAny([]any{1.01, 2.02, 3.03, 5.05, 7.07, 0.01})) // 输出:7.07
}

maxAny 利用 any、type switch 和类型断言(type assertion)实现了类似泛型的效果。但其存在一些缺陷:

  • 若要支持其他元素类型的切片,我们需对该函数进行修改;
  • maxAny 的返回值类型为 any(interface{}),要得到其实际类型的值还需要通过类型断言转换;
  • 使用 any(interface{})作为输入参数的元素类型和返回值的类型,由于存在装箱和拆箱操作,其性能要比直接使用原类型慢数倍。

泛型版本:

 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
// max_generics.go
type ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
        ~float32 | ~float64 |
        ~string
}

func maxGenerics[T ordered](sl []T) T {
    if len(sl) == 0 {
        panic("slice is empty")
    }

    max := sl[0]
    for _, v := range sl[1:] {
        if v > max {
            max = v
        }
    }
    return max
}

type myString string

func main() {
    var m int = maxGenerics([]int{1, 2, -4, -6, 7, 0})
    fmt.Println(m) // 输出:7
    fmt.Println(maxGenerics([]string{"11", "22", "44", "66", "77", "10"})) // 输出:77
    fmt.Println(maxGenerics([]float64{1.01, 2.02, 3.03, 5.05, 7.07, 0.01})) // 输出:7.07
    fmt.Println(maxGenerics([]int8{1, 2, -4, -6, 7, 0})) // 输出:7
    fmt.Println(maxGenerics([]myString{"11", "22", "44", "66", "77", "10"})) // 输出:77
}

类型参数(type parameters)

Go 泛型方案的实质是对类型参数(type parameter)的支持:

  • 泛型函数(generic function):带有类型参数的函数;
  • 泛型类型(generic type):带有类型参数的自定义类型;
  • 泛型方法(generic method):泛型类型的方法。

类型参数语法注意点:

  • 类型参数名的首字母通常采用大写形式,并且类型参数必须是具名的。
  • 在同一个类型参数列表中,类型参数名字也要唯一
1
2
3
func foo[M map[E]T, T any, E comparable](m M)(E, T) {
    //... ...
}

泛型函数

1
2
3
4
5
// 泛型函数声明:T为类型形参
func maxGenerics[T ordered](sl []T) T

// 调用泛型函数:int为类型实参
m := maxGenerics[int]([]int{1, 2, -4, -6, 7, 0})
  • [T ordered]就是 Go 泛型的类型参数列表(type parameters list),示例中这个列表中仅有一个类型参数 T,ordered 为类型参数的类型约束(type constraint);
  • Go 语言规范规定:函数的类型参数列表位于函数名与函数参数列表之间,由方括号括起的固定个数的、由逗号分隔的类型参数声明组成;
  • 在调用泛型函数时,除了要传递普通参数列表对应的实参之外,还要显式传递类型实参;
  • 若在函数的参数列表中使用了的类型形参,编译器会根据函数类型实参的自动推断;
  • 不能通过返回值类型来推断类型实参;

泛型类型

在类型声明中带有类型参数的 Go 类型。

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

type TypeName[T1 constraint1, T2 constraint2, ..., Tn constraintN] TypeLiteral

type Vector[T any] []T

func (v Vector[T]) Dump() {
    fmt.Printf("%#v\n", v)
}

func main() {
    var iv = Vector[int]{1,2,3,4}
    var sv Vector[string]
    sv = []string{"a","b", "c", "d"}
    iv.Dump()
    sv.Dump()
}

在使用 Vector[T]之前都显式用类型实参对泛型类型进行了具化,从而得到具化后的类型 Vector[int]和 Vector[string]。 Vector[int]的底层类型为[]int,Vector[string]的底层类型为[]string。然后再对具化后的类型进行操作。

一些具体的例子:

 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
type Set[T comparable] map[T]struct{}

type sliceFn[T any] struct {
  s   []T
  cmp func(T, T) bool
}

type Map[K, V any] struct {
  root    *node[K, V]
  compare func(K, K) int
}

type element[T any] struct {
  next *element[T]
  val  T
}

type Numeric interface {
  ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
    ~float32 | ~float64 |
    ~complex64 | ~complex128
}

type NumericAbs[T Numeric] interface {
  Abs() T
}
  • 泛型类型声明的内部引用该类型名,必须要带上类型参数,如上面的 element 结构体中的 next 字段的类型:*element[T]。 泛型类型与类型别名组合:
1
2
3
4
5
6
7
type foo[T1 any, T2 comparable] struct {
    a T1
    b T2
}
  
type fooAlias = foo // 编译器错误:cannot use generic type foo[T1 any, T2 comparable] without instantiation
type fooAlias = foo[int, string] // 合法
0%