Go语言错误处理最佳实践

错误处理实践

我们在go语言中设计error的处理体系时候, 一般都会去做下面两点

  1. 直接使用errors.New()生成error接口的值
  2. 扩展error接口, 并定义扩展error接口的实现类型

error接口是什么?

go语言的error是一个接口类型, 其源码如下:

go 复制代码
type error interface {
	Error() string
}

我们可以定义它的实现类型, 比如我们经常使用到的errors.New()方法, 返回值为一个error接口的实现类型*errorString的结构体字面量

go 复制代码
package errors

func New(text string) error {
	return &errorString{text}
}

type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}

//package main
xxxerr := errors.New("xxx")

所以我们可以直接调用errors.New()为我们生成一个error接口的值

扩展的error接口

我们为什么需要对error接口进行扩展呢?, 原因是error的实现类型范围太大了, 细粒度不够小, 所以我们需要实现更加精细的控制, 关于这种设计我们可以参考go语言标准库中的一些error处理代码, 比如下面的net.Error

go 复制代码
type Error interface {
	//嵌入了error接口, 实现net.Error也会实现error
    error

    //扩展
    Timeout() bool
	Temporary() bool
}

然后我们又可以定义一个类型来实现这个扩展错误接口类型, 比如下面这个OpError:

go 复制代码
type OpError struct {
	Op string
	Net string
	Source Addr
	Addr Addr
	Err error
}

//实现函数1
func (e *OpError) Error() string {
	return ""
}

//下面是实现函数2
func (e *OpError) Timeout() bool {
	//对应的处理逻辑
    return true
}

//下面是实现函数2
func (e *OpError) Temporary() bool {}

我们发现该结构体中存在一个名字叫做Err的类型为error的字段, 它代表了该错误的潜在错误, 有可能OpError类型的错误值还包含了AddrError这种错误

通过这种类型建立起树形的错误体系, 用统一字段建立可追溯的链式错误关联, 我们就可以建立起来一套优秀的错误处理机制

为了更好的表示, 我画了一张图

具体的错误

因为Go语言的error是一个接口, 所以这个它的值的实际类型是非常复杂的, 于是我们就需要去判断它的值的一个实际类型

  1. 如果错误值在某一个范围内, 我们可以使用类型断言表达式 或者类型断言+switch语句进行判断
  2. 对于已有相应变量且类型相同的一系列错误值, 一般直接使用判等操作 + switch语句
  3. 没有相应变量且类型未知的一系列错误值, 只能使用其错误信息的字符串表示形式来判断

下面我们分别来看上面的内容: 首先是第一点, 已知错误值的范围比如: {os.PathError|os.LinkError|os.SyscallError|exec.Error}, 是它们中的一个, 我们可以直接使用类型断言+switch, 然后返回潜在错误类型

go 复制代码
func underlyingError(err error) error {
  switch err := err.(type) {
  case *os.PathError:
    return err.Err
  case *os.LinkError:
    return err.Err
  case *os.SyscallError:
    return err.Err
  case *exec.Error:
    return err.Err
  }
  return err
}

当我已经知道某个错误是哪一个, 我们直接使用判等操作+switch,

go 复制代码
printError := func(i int, err error) {
    if err == nil {
        fmt.println("nil error")
        return
    }

    err = underlyingError(err)

    switch err {  
    case os.ErrClosed:    
        fmt.Printf("error(closed)[%d]: %s\n", i, err)  
    case os.ErrInvalid:    
        fmt.Printf("error(invalid)[%d]: %s\n", i, err)  
    case os.ErrPermission:    
        fmt.Printf("error(permission)[%d]: %s\n", i, err)  
    }
}

通过上面这种直接判等操作, 我们就可以锁定具体的错误值了

对于上面两种情况, 我们都会有比较明确的方法去解决, 但是我们对一个错误值可能代表的含义知道的很少, 那么就只能通过错误信息去判断了

相关推荐
519715 小时前
goup是一个纯Rust编写的优雅的Go多版本管理工具
go
豆浆Whisky20 小时前
Go微服务通信优化:从协议选择到性能调优全攻略|Go语言进阶(20)
后端·微服务·go
码一行2 天前
Eino AI 实战:解析 PDF 文件 & 实现 MCP Server
后端·go
赵大海3 天前
golang运维平台实战,服务树,日志监控,任务执行,分布式探测
go
喵个咪3 天前
go-kratos-admin 快速上手指南:从环境搭建到启动服务(Windows/macOS/Linux 通用)
vue.js·go
码一行3 天前
Eino AI 实战: Eino 的文档加载与解析
后端·go
码一行3 天前
Eino AI 实战:DuckDuckGo 搜索工具 V1 与 V2
后端·go
代码扳手3 天前
Golang 高效内网文件传输实战:零拷贝、断点续传与 Protobuf 指令解析(含完整源码)
后端·go
zhuyasen3 天前
Go Web 开发利器:如何让你的 Gin 服务拥有 Nginx 般的静态文件处理能力?
nginx·go·gin
爷_3 天前
Golang: sqlc 和 goose 最佳实践
后端·go·全栈