开发者经常犯的一个错误是对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信息。