Golang 语言怎么处理错误?

简介: Golang 语言怎么处理错误?

介绍

golang 程序大多数是通过 if err != nil 处理错误,在 golang 社区中,有一部分 golang 程序员对此举是持反对观点,他们认为在 golang 代码中存在大量的错误处理代码 if err != nil,使整体代码变得非常不优雅,应该在 golang 中引入其他处理错误的机制,例如 try-catche 或其它此类处理错误的机制。其实,他们忽略了 golang 中一个特别重要的概念,即 errors are values,并且 golang 作者 Rob Pike 也对此问题做出过回应,在 golang 代码中出现重复的错误处理代码 if err != nil,可能是 golang 用户的使用方式有问题。

本文我们主要聊聊在 golang 中,怎么处理错误?

golang 定义错误的两种方式

使用 golang 标准库 errors 的 New() 函数,可以定义一个错误类型的变量。

func New(text string) error

New() 函数接收一个 string 类型的文本,返回一个 error 类型的变量。即使给定的文本不同,每次对 New() 函数的调用也会返回不同的错误值。

关于每次调用 New() 函数,都可以返回不同的错误值,golang 是怎么做到的呢?我们通过阅读 golang 的源码,找一下我们的问题答案。

源码 /usr/local/go/src/errors/errors.go

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
 return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
 s string
}
func (e *errorString) Error() string {
 return e.s
}

源码中,我们发现 New() 函数体中的代码是返回一个指针类型 &errorString{text},所以我们的疑问自然有了答案。

那么,golang 中定义错误的另外一种方式是什么?在 golang 标准库 fmt 中,通过调用 Errorf() 函数也可以返回一个 error 类型的错误。

源码 /usr/local/go/src/fmt/errors.go

func Errorf(format string, a ...interface{}) error {
 p := newPrinter()
 p.wrapErrs = true
 p.doPrintf(format, a)
 s := string(p.buf)
 var err error
 if p.wrappedErr == nil {
  err = errors.New(s)
 } else {
  err = &wrapError{s, p.wrappedErr}
 }
 p.free()
 return err
}

03

错误处理方式之“不透明错误处理”

正如我们在文章开篇所述,在 golang 程序中,我们见的最多的错误处理方式就是 if err != nil,此种错误处理方式,错误处理方不关心错误提供方的错误值。因此,我们将此种错误处理方式称为“不透明错误处理”。

示例代码:

err := errors.New("this is a error example")
if err != nil {
 fmt.Println(err)
  return
}

04

golang 1.13 新增 As() 函数

在 golang 1.13 中,新增 As() 函数,当 error 类型的变量是一个包装错误(wrap error)时,它可以顺着错误链(error chain)上所有被包装的错误(wrapped error)的类型做比较,直到找到一个匹配的错误类型,并返回 true,如果找不到,则返回 false。

通常,我们会使用 As() 函数判断一个 error 类型的变量是否为特定的自定义错误类型。

示例代码:

// 自定义的错误类型
type DefineError struct {
 msg string
}
func (d *DefineError) Error() string {
 return d.msg
}
func main() {
  // wrap error
 err1 := &DefineError{"this is a define error type"}
 err2 := fmt.Errorf("wrap err2: %w\n", err1)
 err3 := fmt.Errorf("wrap err3: %w\n", err2)
 var err4 *DefineError
 if errors.As(err3, &err4) {
  // errors.As() 顺着错误链,从 err3 一直找到被包装最底层的错误值 err1,并且将 err3 与其自定义类型 `var err4 *DefineError` 匹配成功。
  fmt.Println("err1 is a variable of the DefineError type")
  fmt.Println(err4 == err1)
  return
 }
 fmt.Println("err1 is not a variable of the DefineError type")
}

05

golang 1.13 新增 Is() 函数

在 Part03 中,我们讲述了“不透明错误处理”的错误处理方式,错误处理方不关心错误提供方的错误值。但是,在错误处理方需要关心错误提供方的错误值时,错误处理方要对错误提供方的错误值进行判定,这就造成了代码的耦合,错误提供方的错误值每次修改,错误处理方都需要跟着做出相应修改。

针对这种情况,golang 一般会采用“哨兵错误处理”的错误处理方式,即定义可导出的错误变量,错误处理方和错误提供方都只操作错误变量,这样做的好处是只需维护错误变量,但是还没有彻底解决问题,如果 error 类型的错误变量是一个包装错误(wrap error),“哨兵错误处理”的错误处理方式也不方便处理该错误。

好在 golang 1.13 新增 Is() 函数,它可以顺着错误链(error chain)上所有被包装的错误(wrapped error)的类型做比较,直到找到一个匹配的错误。

示例代码:

// 哨兵错误处理
var (
 ErrInvalidUser     = errors.New("invalid user")
 ErrNotFoundUser    = errors.New("not found user")
)
func main () {
  err1 := fmt.Errorf("wrap err1: %w\n", ErrInvalidUser)
 err2 := fmt.Errorf("wrap err2: %w\n", err1)
  // golang 1.13 新增 Is() 函数
 if errors.Is(err2, ErrInvalidUser) {
  fmt.Println(ErrInvalidUser)
  return
 }
 fmt.Println("success")
}

06

总结

本文我们开篇先是讲述了 golang 社区中,存在对待 golang 错误处理方式的反对态度的用户,这么一个客观事实。接着,我们介绍了 golang 中的两种定义错误的方式和底层源码实现,和 golang 1.13 中新增的关于错误处理的函数。如果你现在使用的是 golang 1.13 及以上版本,请使用 As()Is()

通过阅读源码 /usr/local/go/src/errors/wrap.go,我们可以发现 As()Is() 是通过在错误链中不断调用 Unwrap() 函数,最终找到匹配的错误值。其中,Unwrap() 函数也是在 golang 1.13 中新增的函数。

Unwrap() 函数的源码:

// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
func Unwrap(err error) error {
 u, ok := err.(interface {
  Unwrap() error
 })
 if !ok {
  return nil
 }
 return u.Unwrap()
}

推荐阅读:

Go 语言学习之错误处理

参考资料:

https://golang.org/pkg/errors/

延伸阅读:

https://blog.golang.org/error-handling-and-go

https://blog.golang.org/errors-are-values

https://blog.golang.org/go1.13-errors

https://golang.org/doc/tutorial/handle-errors

https://medium.com/rungo/error-handling-in-go-f0125de052f0

https://www.digitalocean.com/community/tutorials/handling-errors-in-go


目录
相关文章
|
2月前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
114 4
Golang语言之管道channel快速入门篇
|
2月前
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
67 4
Golang语言文件操作快速入门篇
|
2月前
|
Go
Golang语言之gRPC程序设计示例
这篇文章是关于Golang语言使用gRPC进行程序设计的详细教程,涵盖了RPC协议的介绍、gRPC环境的搭建、Protocol Buffers的使用、gRPC服务的编写和通信示例。
104 3
Golang语言之gRPC程序设计示例
|
2月前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
86 4
|
2月前
|
Go
Golang语言错误处理机制
这篇文章是关于Golang语言错误处理机制的教程,介绍了使用defer结合recover捕获错误、基于errors.New自定义错误以及使用panic抛出自定义错误的方法。
48 3
|
2月前
|
Go 调度
Golang语言goroutine协程篇
这篇文章是关于Go语言goroutine协程的详细教程,涵盖了并发编程的常见术语、goroutine的创建和调度、使用sync.WaitGroup控制协程退出以及如何通过GOMAXPROCS设置程序并发时占用的CPU逻辑核心数。
52 4
Golang语言goroutine协程篇
|
2月前
|
Prometheus Cloud Native Go
Golang语言之Prometheus的日志模块使用案例
这篇文章是关于如何在Golang语言项目中使用Prometheus的日志模块的案例,包括源代码编写、编译和测试步骤。
55 3
Golang语言之Prometheus的日志模块使用案例
|
2月前
|
Go
Golang语言之函数(func)进阶篇
这篇文章是关于Golang语言中函数高级用法的教程,涵盖了初始化函数、匿名函数、闭包函数、高阶函数、defer关键字以及系统函数的使用和案例。
64 3
Golang语言之函数(func)进阶篇
|
2月前
|
Go
Golang语言之函数(func)基础篇
这篇文章深入讲解了Golang语言中函数的定义和使用,包括函数的引入原因、使用细节、定义语法,并通过多个案例展示了如何定义不返回任何参数、返回一个或多个参数、返回值命名、可变参数的函数,同时探讨了函数默认值传递、指针传递、函数作为变量和参数、自定义数据类型以及返回值为切片类型的函数。
62 2
Golang语言之函数(func)基础篇
|
1月前
|
前端开发 中间件 Go
实践Golang语言N层应用架构
【10月更文挑战第2天】本文介绍了如何在Go语言中使用Gin框架实现N层体系结构,借鉴了J2EE平台的多层分布式应用程序模型。文章首先概述了N层体系结构的基本概念,接着详细列出了Go语言中对应的构件名称,包括前端框架(如Vue.js、React)、Gin的处理函数和中间件、依赖注入和配置管理、会话管理和ORM库(如gorm或ent)。最后,提供了具体的代码示例,展示了如何实现HTTP请求处理、会话管理和数据库操作。
31 0