8 错误
Go 语言错误处理详解
8.1 接口 error 是什么
在 C 语言中,通常使用整数错误码(errno)来表示函数处理出错,用 -1 表示错误,0 表示正确。
在 Go 中,使用 error 类型来表示错误,它是一个接口类型:
type error interface {
Error() string
}
创建 error 的方式
1. errors.New()
// src/errors/errors.go
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
使用 errors.New() 创建的错误实际上是 errors 包里未导出的 errorString 类型,包含唯一字段 s,实现了 Error() string 方法。
2. fmt.Errorf()
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
// implementation
}
fmt.Errorf 先将字符串格式化,再调用 errors.New() 创建错误。
重要原则: 返回错误的函数要给出具体的"上下文"信息,如上例中要给出负数到底是什么值。
使用 error 的约定
- 将 error 放在函数返回值的最后一个
- 构造 error 时,传入的字符串首字母小写,结尾不带标点符号
err := errors.New("error example")
fmt.Printf("The returned error is %s.\n", err)
8.2 接口 error 有什么问题
优点
Go 从语言层面要求明确处理错误,而不是像 Java 使用 try-catch-finally。
缺点
代码中"error"满天飞,显得冗长拖沓,容易掩盖正常逻辑。
err := doStuff1()
if err != nil {
// handle error...
}
err = doStuff2()
if err != nil {
// handle error...
}
err = doStuff3()
if err != nil {
// handle error...
}
官方观点
Russ Cox(Go 作者之一):
- 返回值错误处理机制适用于大型软件,try-catch 更适合小程序
Go 官网 FAQ:
- try-catch 会让代码变得混乱
- 程序员倾向将常见错误也抛到异常里,使错误处理更冗长且易出错
- Go 的多返回值使返回错误异常简单
- 普通错误使用 error,真正异常使用 panic-recover
承认:Go 的错误处理机制对开发人员确实有一定的心智负担。
8.3 如何理解关于 error 的三句谚语
8.3.1 视错误为值
箴言:Errors are just values(错误就是值)
只要实现了 Error 接口的类型都可以认为是 Error。
处理 error 的三种方式
1. Sentinel errors(哨兵错误)
预先约定好的错误,处理流程必须在此停下。
func main() {
r := bytes.NewReader([]byte("0123456789"))
_, err := r.Read(make([]byte, 10))
if err == io.EOF {
log.Fatal("read failed:", err)
}
}
问题:
- 必须在定义 error 和使用 error 的包之间建立依赖关系
- 当需要返回带上下文信息的错误时,调用者不得不使用字符串匹配方式判断错误类型(代码坏味道)
func readfile(path string) error {
err := openfile(path)
if err != nil {
return fmt.Errorf("cannot open file %s: %v", path, err)
}
return nil
}
// 调用者需要字符串匹配,这是坏味道
if strings.Contains(error.Error(), "not found") {
// handle error
}
建议:尽量避免 Sentinel errors
2. Error Types
实现了 error 接口的类型,可以附带额外字段提供更多信息。
// src/os/error.go
type PathError struct {
Op string
Path string
Err error
}
外层调用者使用类型断言判断错误:
func underlyingError(err error) error {
switch err := err.(type) {
case *PathError:
return err.Err
case *LinkError:
return err.Err
case *SyscallError:
return err.Err
}
return err
}
问题:同样存在引入包依赖的问题
建议:不要把 Error types 作为导出类型
3. Opaque errors(黑盒错误)
只知道错误发生了,但看不到内部细节。
func fn() error {
x, err := bar.Foo()
if err != nil {
return err
}
// use x
return nil
}
策略:一旦出错,直接返回错误;否则继续正常流程。
高级用法:判断错误是否具有某种行为(接口),而不是具体类型
type temporary interface {
Temporary() bool
}
func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}
优点:
- 不需要 import 定义错误的包
- 不需要知道 error 的具体类型
- 面向接口编程
8.3.2 检查并优雅地处理错误
箴言:Don't just check errors, handle them gracefully
错误示例
func AuthenticateRequest(r *Request) error {
err := authenticate(r.User)
if err != nil {
return err
}
return nil
}
应优化为:
func AuthenticateRequest(r *Request) error {
return authenticate(r.User)
}
添加上下文信息的问题
func AuthenticateRequest(r *Request) error {
err := authenticate(r.User)
if err != nil {
return fmt.Errorf("authenticate failed: %v", err)
}
return nil
}
这种做法破坏了相等性检测,无法判断错误是否是预先定义好的错误。
解决方案:pkg/errors
Go 1.13 之前使用 github.com/pkg/errors,Go 1.13 后自带了 error 高级功能,但输出堆栈仍推荐前者。
// Wrap annotates cause with a message.
func Wrap(cause error, message string) error
// Cause unwraps an annotated error.
func Cause(err error) error
使用示例:
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, errors.Wrap(err, "open failed")
}
defer f.Close()
buf, err := ioutil.ReadAll(f)
if err != nil {
return nil, errors.Wrap(err, "read failed")
}
return buf, nil
}
func ReadConfig() ([]byte, error) {
home := os.Getenv("HOME")
config, err := ReadFile(filepath.Join(home, ".settings.xml"))
return config, errors.Wrap(err, "could not read config")
}
func main() {
_, err := ReadConfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
输出:
could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory
使用 %+v 输出堆栈:
fmt.Printf("%+v", err)
输出详细的错误堆栈信息。
Cause 函数示例:
type temporary interface {
Temporary() bool
}
func IsTemporary(err error) bool {
te, ok := errors.Cause(err).(temporary)
return ok && te.Temporary()
}
8.3.3 只处理错误一次
箴言:Only handle error once
什么是"处理"错误:
Handling an error means inspecting the error value, and making a decision.
即检查错误并做出一个决定。
错误示例 1:忽略错误
func Write(w io.Writer, buf []byte) {
w.Write(buf) // 忽略了返回的错误
}
错误示例 2:处理两次错误
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
}
问题:
- 日志文件中有重复的错误描述
- 最上层调用者拿到的错误缺少上下文信息
8.4 错误处理的改进
Go 1.13 错误包裹(wrapping)
Go 1.13 支持 error wrapping,通过提供 Unwrap 方法实现。
特性:
fmt.Errorf增加了%w格式符- error 包增加了三个函数:
errors.Unwrap、errors.Is、errors.As
fmt.Errorf 使用 %w
// 嵌套 error
err := fmt.Errorf("open failed: %w", originalErr)
errors.Unwrap
返回被嵌套的下一层 error,需要多次调用才能获取最里层的 error。
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}
errors.Is
判断 err 是否和 target 是同一类型,或者 err 嵌套的 error 有没有和 target 是同一类型的。
func Is(err, target error) bool {
if target == nil {
return err == target
}
isComparable := reflectlite.TypeOf(target).Comparable()
for {
if isComparable && err == target {
return true
}
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
if err = Unwrap(err); err == nil {
return false
}
}
}
errors.As
从 err 错误链里找到第一个和 target 相等的值,并设置 target 所指向的变量为 err。
func As(err error, target interface{}) bool {
if target == nil {
panic("errors: target cannot be nil")
}
val := reflectlite.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflectlite.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
targetType := typ.Elem()
for err != nil {
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
err = Unwrap(err)
}
return false
}
注意事项
如果 target 不是一个指向实现了 error 接口的类型或者其他接口类型的非空指针,函数会 panic。
总结
| 概念 | 说明 |
|---|---|
| error 接口 | 包含 Error() string 方法 |
| 创建错误 | errors.New()、fmt.Errorf() |
| Sentinel errors | 预定义错误,如 io.EOF,避免过度使用 |
| Error Types | 自定义错误类型,避免导出 |
| Opaque errors | 黑盒错误,通过行为(接口)判断 |
| 错误处理三原则 | 视错误为值、优雅处理、只处理一次 |
| Go 1.13 改进 | %w、Unwrap、Is、As |

浙公网安备 33010602011771号