有用的 go 语言代码片段收集。
特别是不常见,或者常见但容易出错的。
尽量
实际运行验证过
标注验证的 go 版本和相关的环境
标注使用要点
接口实现的静态类型检查 go struct 实现 interface 不需要显式的声明,只需要实现其所有方法签名。
这实际上是一种 duck typing(鸭子类型,『当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。』),而 go 很可能是唯一一个实现鸭子类型的静态语言。
鸭子类型关注行为而不是继承链,给灵活实现提供了便利。不过由于没有显式声明,如果在实现时出了差错,比较难发现(如打错方法签名,或者 go 特有的问题:方法 receiver 弄混了 值类型 和 指针类型)。所以需要加入静态检查,利用好静态语言的优势。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var (
_ Ducker = (*Duck)(nil )
)
type Ducker interface {
Quack() string
}
type Duck struct {
}
func (d *Duck) Quack () string {
return "Gagaga!"
}
这个类型转换将空值转为 Duck 指针,然后赋值给 Ducker 类型,最后丢弃。这一行最后会被编译器优化掉(因为没有任何实际意义),不会有任何实际开销,但是在静态检查中如果类型不对,还是会报错。
优雅关闭 Graceful Shutdown 是指,在接收到退出信号时,不是马上退出,而是先停止接受新请求,然后把当前请求处理完,再主动退出。
这适合守护进程 daemon 型应用(典型的是服务器),实现一个循环持续接受请求并处理,处理完并不会退出而是等待接下来的请求,直到收到退出信号。
单(主) goroutine 版 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
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main () {
loop()
fmt.Println("cleaning up" )
}
func loop () {
sigch := make (chan os.Signal, 1 )
signal.Notify(sigch, syscall.SIGINT, syscall.SIGTERM)
for {
select {
case <-sigch:
return
default :
fmt.Println("do something" )
time.Sleep(time.Second)
}
}
}
运行输出:
1
2
3
4
5
do something
do something
...
(Ctrl + C)
cleaning up
独立工作 goroutine 版 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
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main () {
sigch := make (chan os.Signal, 1 )
signal.Notify(sigch, syscall.SIGINT, syscall.SIGTERM)
done := make (chan bool , 1 )
go func () {
for {
select {
case <-sigch:
done <- true
return
default :
fmt.Println("do something" )
time.Sleep(time.Second)
}
}
}()
fmt.Println("goroutine started." )
<-done
fmt.Println("goroutine stoped. exit." )
}
运行输出:
1
2
3
4
5
6
goroutine started.
do something
do something
...
(Ctrl + C)
goroutine stoped. exit.
测试直接 os.Exit() 的函数的 exit code 单元测试有助于提高软件组件质量,及早发现错误,减少后期发现和修复问题的负担。
go 在 工具链 和 原生库 级别就给单元测试提供了大量支持,只需要引入原生库就可以写成比较完善的测试。在此基础上再引入少量第三方库(如 testify)就可以写出非常优雅的测试。
这降低了写单元测试的负担,提高了大家写单元测试的积极性。如果使用 TDD 进行开发,用运行单元测试来保证每次修改的质量,能够极大地解放开发者的心智负担。
不过最近遇到了一种情况,在做重构时使用单元测试来确保没有破坏原有的功能。结果无论怎么改,测试都是通过。一开始还以为是确实没错。直到我发现了一个 bug,但是测试依然通过,我才反应过来被测函数里有 os.Exit(int)
,测试其实被提前退出了,根本没有测。而这种情况, go test 仍然算 passed 。
子进程测试 怎么办呢?这个函数确实需要遇到错误提前退出。在参考 stackoverflow 之后,我发现了这种写法:
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
import (
"os"
"os/exec"
"runtime"
"testing"
)
func TestFoo (t *testing.T) {
if os.Getenv("TEST_RUNNER" ) == "1" {
Foo("some args" , 1 , false )
return
}
cmd := exec.Command(os.Args[0 ], "-test.run=^(TestFoo)$" )
cmd.Env = append (os.Environ(), "TEST_RUNNER=1" )
err := cmd.Run()
e, ok := err.(*exec.ExitError)
if ok && e.ExitCode() != 0 {
t.Errorf("exit code got %d, expected 0" , e.ExitCode())
}
return
}
子进程测试的工具函数 不过每个测试函数都这样写,太累,还容易出错。能不能写成通用的函数呢?可以。
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
func assertExitCode (t *testing.T, f func () , expected int ) {
if os.Getenv("TEST_RUNNER" ) == "1" {
f()
return
}
pc := make ([]uintptr , 1 )
runtime.Callers(2 , pc)
ft := runtime.FuncForPC(pc[0 ])
nn := strings.Split(ft.Name(), "." )
cmd := exec.Command(os.Args[0 ], "-test.run=^(" +nn[len (nn)-1 ]+")$" )
cmd.Env = append (os.Environ(), "TEST_RUNNER=1" )
err := cmd.Run()
e, ok := err.(*exec.ExitError)
if expected == 0 {
if ok && e.ExitCode() != 0 {
t.Errorf("exit code got %d, expected %d" , e.ExitCode(), 0 )
}
return
}
if !ok {
t.Errorf("expect ExitError with code %d, got err %v" , expected, err)
return
}
if e.ExitCode() != expected {
t.Errorf("exit code got %d, expected %d" , e.ExitCode(), expected)
}
}
func TestFoo (t *testing.T) {
assertExitCode(t, func () {
Foo("some args" , 1 , false )
}, 0 )
}
支持测试多个/次 os.Exit() 的工具函数 不过以上两种写法,都有一个 副作用 :一个单元测试里只能测试一次带有 os.Exit(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
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
func assertExitCode (t *testing.T, f func () , flag string , expected int ) {
if env := os.Getenv("TEST_RUNNER" ); env != "" {
if env == flag {
f()
}
return
}
pc := make ([]uintptr , 1 )
runtime.Callers(2 , pc)
ft := runtime.FuncForPC(pc[0 ])
nn := strings.Split(ft.Name(), "." )
cmd := exec.Command(os.Args[0 ], "-test.run=^(" +nn[len (nn)-1 ]+")$" )
cmd.Env = append (os.Environ(), "TEST_RUNNER=" +flag)
err := cmd.Run()
e, ok := err.(*exec.ExitError)
if expected == 0 {
if ok && e.ExitCode() != 0 {
t.Errorf("exit code got %d, expected %d" , e.ExitCode(), 0 )
}
return
}
if !ok {
t.Errorf("expect ExitError with code %d, got err %v" , expected, err)
return
}
if e.ExitCode() != expected {
t.Errorf("exit code got %d, expected %d" , e.ExitCode(), expected)
}
}
func TestFooBar (t *testing.T) {
assertExitCode(t, func () {
Foo("some args" , 1 , false )
}, "foo" , 0 )
assertExitCode(t, func () {
Bar("some args" , 2 )
}, "bar" , 0 )
}
对了,以上三种写法 还有一个副作用 :因为测试不是在当前进程里做的,凡是用这种方法测试的,都不计算覆盖率。
本文为本人原创,采用知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议 进行许可。 本作品可自由复制、传播及基于本作品进行演绎创作。如有以上需要,请留言告知,在文章开头明显位置加上署名(Jayce Chant)、原链接及许可协议信息,并明确指出修改(如有),不得用于商业用途。谢谢合作。 请点击查看协议 的中文摘要。