下划线的妙用

大多数情况下,下划线_都用于忽略函数返回值的情况。最近查看authboss这个开源库的代码时,发现这样一种用法:

1
2
3
4
5
6
7
8
9
10
struct User {
...
}

var (
assertUser = &User{}
assertStorer = &MemStorer{}
_ authboss.User = assertUser
_ authboss.AuthableUser = assertUser
)

其中authboss.User是一个接口:

1
2
3
4
type User interface {
GetPID() (pid string)
PutPID(pid string)
}

这里下划线的作用就是在编译时进行检查,确保User完全实现了authboss.User接口。

此外还可以在声明结构体时使用这个技巧:

1
2
3
4
5
6
7
8
type User struct {
name string
age int
_ struct{}
}

// a := User{"bob",20} // 这种会报错too few values in SomeSturct literal
b := User{"name":"bob","age":20} // 通过检查

比如当开发阶段,接口定义、结构体定义还会经常变动时可以使用这个技巧,在编译阶段就发现问题。

另外,如果在代码里import了一个包而没使用时,编译会报错。但在某些情况下,比如仅仅需要引用某些包调用其init方法时,可以在包名前添加下划线,比如:

1
2
3
4
import (
"context"
_ "github.com/xxx/xxxx" // 这里仅需要执行其init函数而非需要使用其中的方法、结构等。
)

关于errors

使用github.com/pkg/errors替换原生的errors包,这个包有3个关键函数:

  1. Warp用于对底层错误进行包装,添加上下文以及调用栈信息。通常建议用这个包装其他人的三方库或者标准库错误。
  2. WithMessage这个函数只用于对包装过的错误添加信息,注意不要重复Warp
  3. WithStack这个函数只用于添加调用栈而不用附加额外信息的情况。
  4. Cause用于获取底层错误。

借用个别人的例子

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
import (
"database/sql"
"fmt"

"github.com/pkg/errors"
)

func foo() error {
return errors.Wrap(sql.ErrNoRows, "foo failed")
}

func bar() error {
return errors.WithMessage(foo(), "bar failed")
}

func main() {
err := bar()
if errors.Cause(err) == sql.ErrNoRows {
fmt.Printf("data not found, %v\n", err)
fmt.Printf("%+v\n", err)
return
}
if err != nil {
// unknown error
}
}
/*Output:
data not found, bar failed: foo failed: sql: no rows in result set
sql: no rows in result set
foo failed
main.foo
/usr/three/main.go:11
main.bar
/usr/three/main.go:15
main.main
/usr/three/main.go:19
runtime.main
...
*/

优雅结束任务

对外提供API服务时很重要的一点就是处理完当前链接后再退出程序,这里就可以使用信号机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var gracefulStop = make(chan os.Signal)
signal.Notify(gracefulStop, syscall.SIGTERM) // kill
signal.Notify(gracefulStop, syscall.SIGINT) // ctrl+c

// 如果是命令行
go func() {
sig := <-gracefulStop
// 一些清理工作
os.Exit(0)
}()

// 如果是http server
go func() {
sig := <-gracefulStop
server.Shutdown(ctx)
// 一些清理工作
os.Exit(0)
}()

那么问题就是,怎么知道清理完成可以退出了呢?最简单的直接sleep几秒钟,但这个时间设定多久合适呢?

这里其实可以结合channel来实现,比如这里的例子,这里就不贴代码了。

关于接口

关于接口,如果只需要有一种实现的话就别用接口了。另外定义接口时尽量分割成小的部分,保持最小知识原则LOD,或者叫迪米特法则。