Golang小技巧

下划线的妙用

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

struct User {
    ...
}

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

其中authboss.User是一个接口:

type User interface {
    GetPID() (pid string)
    PutPID(pid string)
}

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

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

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方法时,可以在包名前添加下划线,比如:

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

关于errors

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

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

借用个别人的例子

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服务时很重要的一点就是处理完当前链接后再退出程序,这里就可以使用信号机制:

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,或者叫迪米特法则。