编程Go 开发人员

在 Go 中处理错误(errors)的特点是什么,如何正确创建和处理错误,以及如何实现自定义错误类型?

用 Hintsage AI 助手通过面试

回答

Go 从一开始就围绕显式返回错误而不是异常进行构建。这使得代码的可预测性得以提升,避免了像其他语言(例如 Java 或 C++)中的 try/catch 结构所固有的隐藏陷阱。在 Go 中,错误是实现了 Error() 方法的接口,这使得可以创建简单的以及带有上下文的复合/包装错误。

当错误处理不当时(例如通过 _ 忽略错误或不提供额外调试信息包裹错误)或者创建不符合 error 接口的“魔术”错误时,会出现问题。还很重要的是能够从公共函数返回错误并在每个调用中检查它们。

解决方案是使用标准方法:

  • 始终将错误作为函数的最后一个返回值。
  • 使用 errors.New,fmt.Errorf 创建错误。
  • 为自定义错误创建自己的类型,实现 Error()。
  • 对于复杂情况,使用错误包装(errors.Wrap),以免丢失上下文。

代码示例:

import ( "errors" "fmt" ) type NotFoundError struct { Resource string } func (e *NotFoundError) Error() string { return fmt.Sprintf("%s not found", e.Resource) } func GetUser(id int) (string, error) { if id != 1 { return "", &NotFoundError{"User"} } return "Steve", nil } func main() { user, err := GetUser(2) if err != nil { if nfe, ok := err.(*NotFoundError); ok { fmt.Println(nfe.Resource, "problem") } else { fmt.Println("error:", err) } } fmt.Println("user:", user) }

关键特点:

  • 错误总是显式返回,通常是函数的最后一个参数。
  • 对于复杂错误,可以定义自己的类型(struct),实现 interface error。
  • 为了包装错误和保存堆栈跟踪,可以使用 "errors" 包(Go 1.13+ 支持 errors.Is 和 errors.As 来处理错误链)。

反向问题。

在处理错误时可以通过 == 来比较,还是需要使用特殊方法?

最好始终使用 errors.Is() 进行错误包装比较,否则使用 == 可能在错误被包装时失效。

if errors.Is(err, os.ErrNotExist) { // 处理缺少文件的情况 }

实现一个自定义错误类型的结构体是必须的吗,还是使用 errors.New("...") 就足够了?

如果只需要错误的文本,使用 errors.New() 就足够了。如果需要保留上下文(例如资源名称),最好定义一个具有 Error() 方法的 struct。

在函数成功执行的情况下如何正确返回错误?

在成功的情况下始终返回 nil 错误。

return result, nil

常见错误和反模式

  • 忽略返回的错误(通过 _ 或跳过处理)
  • 仅根据 == 比较错误,尽管存在嵌套错误
  • 硬编码字符串而不是错误
  • 错误中缺乏额外上下文

生活中的例子

负面案例

开发人员编写一个返回没有说明的错误的函数(仅仅是 errors.New("fail"))。在分析日志时无法确定原因。

优点:

  • 实现速度快

缺点:

  • 错误信息不明
  • 调试困难

积极案例

开发人员定义自定义错误类型 NotFoundError,并返回详细描述。

优点:

  • 方便日志过滤
  • 错误的查找和处理容易

缺点:

  • 需要描述额外的结构