《100 Go Mistakes and How to Avoid Them》之错误篇
礼物说7.1 使用panic
在Go语言中,errors被用在函数或者方法上,常作为最后一个参数返回一种error
类型的错误,很多开发者会喜欢使用panic
和recover
去当作java/phthon中的try/catch.这并不优雅。
在Go中,panic可以直接结束函数执行流并返回上一个堆栈直到协程被返回或者被recover捕获。要记住,recover函数只能作用在defer函数中,因为defer函数会继续执行即使函数panic了。 panic被用于标记一些意料之外的场景,比如程序错误。比如在net/http.WriteHeader
函数:
func checkWriteHeaderCode(code int) {
if code < 100 || code > 999 {
panic(fmt.Sprintf("invalid WriteHeader code %v", code))
}
}
再比如在database/sql
中:
func Register(name string, driver driver.Driver) {
.Lock()
driversMudefer driversMu.Unlock()
if driver == nil {
panic("sql: Register driver is nil")
}
if _, dup := drivers[name]; dup {
panic("sql: Register called twice for driver " + name)
}
[name] = driver
drivers}
另一种使用panic
的场景就是当我们应用需要一个依赖,但是初始化失败的时候。比如我们在业务中初始数据库失败。还有在regexp
包中的Compile
以及MustCompile
。
在大部分场景中,我们还是应该使用error
。
7.2 忽略什么时候需要包装error
从Go1.13起,%w
可以直接允许我们去包装error. 我们包装error主要有两种场景:
添加额外的上下文到error中
eg:
permission denied
变为user X access resourceY cause permission denied
把error标记为某种具体的error
eg:
permission denied
变为 Forbidden error.我们就可以根据error类型返回403.
此外 %w
的方式调用方还是可以拿到原始的error,如需禁止可以使用%v
.
7.3 错误的检查error类型
当我们使用%w
去包装error后,我们很容易会错误的去检查error类型
type testerror struct {
error
err }
func (t testerror) Error() string {
return fmt.Sprintf("test error: %v", t.err)
}
func wrap(e error) error {
return testerror{err: e}
}
func main() {
:= fmt.Errorf("test:%w", wrap(errors.New("aaa")))
err switch err.(type) {
case testerror:
.Println("true")
fmtdefault:
.Println("false")
fmt}
.Println(errors.As(err, &testerror{}))
fmt}
比如上面一个例子,返回结果是 false,true
.使用%w
之后,我们需要使用errors.As
去检查error类型。errors.As
会递归的去unwrap
。
7.4 错误的检查error值
项目中有很多已知的error,属于是预期之内的。比如:
sql.ErrNoRows
io.EOF
我们常常使用 ==
去检查error的值,但当error被%w
包装之后,这样判断就会有问题。这个时候我们就需要使用errors.Is
.他会递归unwrap,然后依次比对错误链上的值。
7.5 一个error处理多次
一个error处理多次在项目中很常见,比如下面的例子:
func GetRoute(srcLat, srcLng, dstLat, dstLng float32) (Route, error) {
:= validateCoordinates(srcLat, srcLng)
err if err != nil {
.Println("failed to validate source coordinates")
logreturn Route{}, err
}
= validateCoordinates(dstLat, dstLng)
err if err != nil {
.Println("failed to validate target coordinates")
logreturn Route{}, err
}
return getRoute(srcLat, srcLng, dstLat, dstLng)
}
func validateCoordinates(lat, lng float32) error {
if lat > 90.0 || lat < -90.0 {
.Printf("invalid latitude: %f", lat)
logreturn fmt.Errorf("invalid latitude: %f", lat)
}
if lng > 180.0 || lng < -180.0 {
.Printf("invalid longitude: %f", lng)
logreturn fmt.Errorf("invalid longitude: %f", lng)
}
return nil
}
我们会发现结果:
2021/06/01 20:35:12 invalid latitude: 200.000000
2021/06/01 20:35:12 failed to validate source coordinates
因为我们同时log&return 了error。如果再循环调用,就会很难debug。作为一个准则,一个error只处理一遍。所以当你ruturn了error,你就不需要再次log。
结合%w
,我们可以改为如下,既只处理一次,又不丢失上下文信息。
func GetRoute(srcLat, srcLng, dstLat, dstLng float32) (Route, error) {
:= validateCoordinates(srcLat, srcLng)
err if err != nil {
return Route{},
.Errorf("failed to validate source coordinates: %w",err)
fmt}
= validateCoordinates(dstLat, dstLng)
err if err != nil {
return Route{},
.Errorf("failed to validate target coordinates: %w",err)
fmt}
func validateCoordinates(lat, lng float32) error {
if lat > 90.0 || lat < -90.0 {
return fmt.Errorf("invalid latitude: %f", lat)
}
if lng > 180.0 || lng < -180.0 {
return fmt.Errorf("invalid longitude: %f", lng)
}
return nil
}
7.6 不需要处理的error
当我们不需要处理error的时候,经常会那么写:
func f() {
// ...
()
notify}
func notify() error {
// ...
}
这样其实会让读者产生误区,究竟是不需要处理error还是忘记处理了,所以我们可以改为:
= notify() _
这样会让读者确定,是有意不接收error的。此外,最好的方式还是应该使用一个低日志等级去log下。
7.7 错误处理defer中的errors
处理defer中的error时,需要避免覆盖主逻辑中的error,比较合适的情况就是log出来然后return nil。