Go语言中常见100问题-#52 Handling an error twice

2022年4月6日 225点热度 0人点赞 0条评论

开发者经常犯的一个错误是对error进行多次处理,这种情况不仅仅是在Go程序中存在。下面分析重复处理有什么问题以及如何有效地处理。

为了理清这个问题,以GetRoute进行说明。GetRoute函数输入为地理的经纬度坐标,返回路线信息。在该函数的内部会调用一个未导出的getRoute函数。getRoute会根据业务逻辑计算最佳路线。在调用getRoute之前,我们需要对坐标参数进行检查,检查逻辑在validateCoordinates函数中实现,对于出错信息以日志方式记录下来。具体实现代码如下:

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
}

上述代码存在什么问题?第一,validateCoordinates函数对无效的输入参数既通过log.Printf以日志形式记录下又通过return将error返回。第二,对于无效的输入,输出日志中存在重复的信息,如下。

invalid latitude: 200.000000
failed to validate source coordinates

同一个错误信息被记录两次,这为什么是一个问题呢?因为它将使得排查问题变得困难。例如,如果该函数被并发调用,这两条日志不一定是紧挨着出现,有可能是交叉出现,使得排查起来比较复杂。

根据经验,「同个error只能被处理一次,将error记录到日志中和return返回都各是一种处理」. 因此,选择其中之一处理即可,不要两种方式都使用。

下面是重构后,只对error进行一次处理实现。GetRoute内部不记录error日志,将错误处理返回给调用方通过记录日志方式处理。

func GetRoute(srcLat, srcLng, dstLat, dstLng float32) (Route, error) {
        err := validateCoordinates(srcLat, srcLng)
        if err != nil {
                return Route{}, err
        }

        err = validateCoordinates(dstLat, dstLng)
        if err != nil {
                return Route{}, err
        }

        return getRoute(srcLat, srcLng, dstLat, dstLng)
}

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
}

对于无效的坐标参数,上述代码输出为:

invalid latitude: 200.000000

上面重构后的代码是最佳写法吗?并不是。最初版本通过日志记录无效的经纬度情况,在重构后的版本中,是原位置参数错误还是目标位置参数错误,调用方不知道,因此需要将出错的上下文信息添加到error中。

下面通过Go1.13版本提供的wrap error方法再一次对上述代码进行重构。

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)
        }

        return getRoute(srcLat, srcLng, dstLat, dstLng)
}

在上面的第三版改进中,同时克服了之前两个版本中存在的上下文信息丢失和重复处理同个error两方面问题。

「error应该只能被处理一次」,如前面看到的,对error进行日志记录也算作对其进行了处理,要么日志记录error要么将error返回。这样做,可以简化代码并更好地了解错误情况,使用wrap error方法,可以很方便地将上下文信息添加到error中并保持了原error信息。

28370Go语言中常见100问题-#52 Handling an error twice

这个人很懒,什么都没留下

文章评论