《100 Go Mistakes and How to Avoid Them》之错误篇

2023-07-12 ⏳2.1分钟(0.8千字)

7.1 使用panic

在Go语言中,errors被用在函数或者方法上,常作为最后一个参数返回一种error类型的错误,很多开发者会喜欢使用panicrecover去当作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) {
    driversMu.Lock()
    defer driversMu.Unlock()
    if driver == nil {
        panic("sql: Register driver is nil")
    }
    if _, dup := drivers[name]; dup {
        panic("sql: Register called twice for driver " + name)
    }
    drivers[name] = driver
}

另一种使用panic的场景就是当我们应用需要一个依赖,但是初始化失败的时候。比如我们在业务中初始数据库失败。还有在regexp包中的Compile以及MustCompile

在大部分场景中,我们还是应该使用error

7.2 忽略什么时候需要包装error

从Go1.13起,%w可以直接允许我们去包装error. 我们包装error主要有两种场景:

此外 %w的方式调用方还是可以拿到原始的error,如需禁止可以使用%v.

7.3 错误的检查error类型

当我们使用%w去包装error后,我们很容易会错误的去检查error类型

type testerror struct {
        err error
}

func (t testerror) Error() string {
        return fmt.Sprintf("test error: %v", t.err)
}

func wrap(e error) error {
        return testerror{err: e}
}

func main() {
        err := fmt.Errorf("test:%w", wrap(errors.New("aaa")))
        switch err.(type) {
        case testerror:
                fmt.Println("true")
        default:
                fmt.Println("false")
        }
        fmt.Println(errors.As(err, &testerror{}))
}

比如上面一个例子,返回结果是 false,true.使用%w之后,我们需要使用errors.As去检查error类型。errors.As会递归的去unwrap

7.4 错误的检查error值

项目中有很多已知的error,属于是预期之内的。比如:

我们常常使用 ==去检查error的值,但当error被%w包装之后,这样判断就会有问题。这个时候我们就需要使用errors.Is.他会递归unwrap,然后依次比对错误链上的值。

7.5 一个error处理多次

一个error处理多次在项目中很常见,比如下面的例子:

func GetRoute(srcLat, srcLng, dstLat, dstLng float32) (Route, error) { 
    err := validateCoordinates(srcLat, srcLng)
    if err != nil {
        log.Println("failed to validate source coordinates")
        return Route{}, err
    }
    err = validateCoordinates(dstLat, dstLng)
    if err != nil {
        log.Println("failed to validate target coordinates")
        return Route{}, err
    }
    return getRoute(srcLat, srcLng, dstLat, dstLng)
}

func validateCoordinates(lat, lng float32) error {
    if lat > 90.0 || lat < -90.0 {
        log.Printf("invalid latitude: %f", lat)
        return fmt.Errorf("invalid latitude: %f", lat)
    }
    if lng > 180.0 || lng < -180.0 {
        log.Printf("invalid longitude: %f", lng)
        return 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) { 
    err := validateCoordinates(srcLat, srcLng)
    if err != nil {
        return Route{},
            fmt.Errorf("failed to validate source coordinates: %w",err)
    }

    err = validateCoordinates(dstLat, dstLng)
    if err != nil {
        return Route{},
            fmt.Errorf("failed to validate target coordinates: %w",err)
    }
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。