使错误信息简洁
合理的错误信息可以通过逐层包装让我们远离冗余信息。
很多人有在下面的事情上打印日志的习惯,加上参数,当前方法的名字,调用方法的名字,这是不必要的。
func Fn(id string) error { err := Fn1() if err != nil { return fmt.Errorf("Call Fn1 failed with id: %s, %w", id, err } ... return nil }
但是,清晰明了的错误日志只包含当前操作错误的信息、内部参数和动作,以及调用者不知道但调用者不知道的信息,例如当前的方法和参数。这是 Kubernetes 中 endpoints.go 的错误日志,一个非常好的例子,只打印内部 Pod 相关参数和 Unable to get Pod
的失败动作:
func (e *Controller) addPod(obj interface{}) { pod := obj.(*v1.Pod) services, err := e.serviceSelectorCache.GetPodServiceMemberships(e.serviceLister, pod) if err != nil { utilruntime.HandleError(fmt.Errorf("Unable to get pod %s/%s's service memberships: %v", pod.Namespace, pod.Name, err)) return } for key := range services { e.queue.AddAfter(key, e.endpointUpdatesBatchPeriod) } }
处理 error 的黄金五法则
以下介绍作者认为的黄金五法则。
errors.Is
优于==
== 比较容易出错,只能比较当前的错误类型而不能解包。因此,errors.Is
或 errors.As
是更好的选择。
package main import ( "errors" "fmt" ) type e1 struct{} func (e e1) Error() string { return "e1 happended" } func main() { err1 := e1{} err2 := e2() if err1 == err2 { fmt.Println("Equality Operator: Both errors are equal") } else { fmt.Println("Equality Operator: Both errors are not equal") } if errors.Is(err2, err1) { fmt.Println("Is function: Both errors are equal") } } func e2() error { return fmt.Errorf("e2: %w", e1{}) } // Output Equality Operator: Both errors are not equal Is function: Both errors are equal
- 打印错误日志,但不打印正常日志
buf, err := json.Marshal(conf) if err != nil { log.Printf(“could not marshal config: %v”, err) }
新手常犯的错误是使用 log.Printf
打印所有日志,包括错误日志,导致我们无法通过日志级别正确处理日志,调试难度大。我们可以从应用 log.Fatalf
的 dependencycheck.go 中学习正确的方法。
if len(args) != 1 { log.Fatalf(“usage: dependencycheck <json-dep-file> (e.g. ‘go list -mod=vendor -test -deps -json ./vendor/…’)”) } if *restrict == “” { log.Fatalf(“Must specify restricted regex pattern”) } depsPattern, err := regexp.Compile(*restrict) if err != nil { log.Fatalf(“Error compiling restricted dependencies regex: %v”, err) }
- 永远不要通过错误处理逻辑
这是错误过程的说明。
bytes, err := d.reader.Read() if err != nil && err != io.EOF { return err } row := db.QueryRow(“select name from user where id= ?”, 1) err := row.Scan(&name) if err != nil && err != sql.ErrNoRows{ return err }
可以看到,io.EOF
和 sql.ErrNoRows
这两个 error 都被忽略了,后者是一个典型的用 error 来表示业务逻辑(数据不存在)的例子。 我反对这样的设计但支持大小的优化, err:= row.Scan(&name) if size == 0 {log.Println(“no data”) }
,通过添加返回参数而不是直接抛出错误来提供帮助。
- bottom 方法返回错误,upper 方法处理错误
func Write(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err != nil { log.Println(“unable to write:”, err) return err } return nil }
与上面类似的代码有一个明显的问题。如果打印日志后返回错误,则很可能存在重复日志,因为调用者也可能打印日志。
那么如何避免呢?让每个方法只执行一个功能。而这里的一个常见选择是底层方法只返回错误,上层方法处理错误。
- 包装错误消息并添加有利于故障排除的上下文。
在 Go 中没有原生的 stacktrace 可以依赖,我们只能通过自己实现或第三方库来获取那些异常的堆栈信息。 比如 Kubernetes 实现了一个比较复杂的 klog 包来支持日志打印、堆栈信息和上下文。 如果您开发 Kubernetes 相关的应用程序,例如 Operator,您可以参考 Kubernetes 中的结构化日志记录。 此外,那些第三方错误封装库,如 pkg/errors 非常有名。
结语
Go 设计哲学的初衷是简化,但有时会使事情复杂化。 然而,你永远不能认为 Go 错误处理是没有用的,即使它不是那么用户友好。 至少,逐个错误返回是一个很好的设计,在最高层的调用处统一处理错误。 此外,我们仍然可以期待即将发布的版本中的这些改进将带来更简单的应用程序。
感谢阅读!