6、slice 初始化
有时候我们明确的知道一个 slice 切片的最终长度。例如我们想要将一个 Foo 切片 convert 为 Bar 切片,这意味着两个 slice 切片的长度是相同的。
然而有些人却经常初始化 slice 切片如:
var bars []Bar bars := make([]Bar, 0)
slice 切片并不是一个神奇的结构,当没有更多可用空间时,它会进行扩容,也就是其将会自动创建一个具有更大容量的新数组并复制所有的元素。
现在,让我们想象一下如果切片需要多次扩容,即使时间复杂度保持为 O(1) ,但在实践中,它也会对性能造成影响。尽可能避免这种情况。
7、context 管理
context.Context 经常被开发者误解。官方文档的描述是:
A Context carries a deadline, a cancelation signal, and other values across API boundaries.
这个描述很宽泛,以致于一些人对为什么以及如何使用它感到困惑。
让我们试着详细说明下,一个 context 可以包含:
- 一个 deadline 。其可以是持续时间(例如 250 毫秒)或者具体某个时间点(例如 2019-01-08 01:00:00),一旦达到 deadline 则所有正在进行的活动都会取消(比如 I/O 请求,等待某个 channel 输入等等)。
- 一个取消 signal 信号(基本上是 <-chan struct{} )。这里的行为是类似的,一旦收到取消信号则必须停止正在进行中的活动。
- 一组 key/value(基于 interface{} 类型)。
需要说明的是,一个 context 是可组合的,例如既包含一个 deadline 又包含一组 key/value 。此外,多个 goroutine 可以共享同一个 context ,因此取消信号可能会导致多个 goroutine 中的活动被停止。
例如由同一个 context 引发的连环取消,我们要注意使用父子形式的 context ,以此来区分管理,避免相互影响。
8、未使用 -race
测试时未使用 -race 选项也是常见的,它是有价值的工具,我们应该在测试时始终启动它。
9、使用文件名作为输入
假设我们要实现一个函数去统计文件中的空行数,我们可能这样做:
func count(filename string) (int, error) { file, err := os.Open(filename) if err != nil { return 0, errors.Wrapf(err, "unable to open %s", filename) } defer file.Close() scanner := bufio.NewScanner(file) count := 0 for scanner.Scan() { if scanner.Text() == "" { count++ } } return count, nil }
这看起来很自然,filename 文件名作为输入,在函数内部打开文件。
然而,如果我们想要对此函数进行单元测试,输入可能是普通文件,或者空文件,或者其它不同编码类型的文件等等,此时则很容易变得难以管理。另外,如果我们想对某个 HTTP body 实现相同的逻辑,那么我们不得不创建一个另外的函数。
Go 提供了两个很棒的抽象:io.Reader 和 io.Writer 。我们可以传递 io.Reader 抽象数据源而不是 filename 。这样不管是文件也好,HTTP body 也好,byte buffer 也好,我们都只需要使用 Read 方法即可。
上述例子中,我们甚至可以缓冲输入以逐行读取,因此我们可以使用 bufio.Reader 和它的 ReadLine 方法:
func count(reader *bufio.Reader) (int, error) { count := 0 for { line, _, err := reader.ReadLine() if err != nil { switch err { default: return 0, errors.Wrapf(err, "unable to read") case io.EOF: return count, nil } } if len(line) == 0 { count++ } } }
而打开文件的操作则交由 count 的调用者去完成:
file, err := os.Open(filename) if err != nil { return errors.Wrapf(err, "unable to open %s", filename) } defer file.Close() count, err := count(bufio.NewReader(file))
这样,无论数据源如何我们都可以调用 count 函数,同时这有有利于我们进行单元测试,因为我们可以简单的从字符串中创建一个 bufio.Reader :
count, err := count(bufio.NewReader(strings.NewReader("input")))
10、Goroutines 和 Loop 循环变量
示例:
ints := []int{1, 2, 3} for _, i := range ints { go func() { fmt.Printf("%v\n", i) }() }
输出将会是什么?1 2 3 吗?当然不是。
上例中,每个 goroutine 共享同一个变量实例,因此将会输出 3 3 3(最有可能)。
这个问题有两种解决方式。第一种是将 i 变量传递给 closure 闭包( inner function ):
ints := []int{1, 2, 3} for _, i := range ints { go func(i int) { fmt.Printf("%v\n", i) }(i) }
第二种方式是在 for 循环范围内创建另一个变量:
ints := []int{1, 2, 3} for _, i := range ints { i := i go func() { fmt.Printf("%v\n", i) }() }