Go Error 嵌套到底是怎么实现的?
原文链接: Go Error 嵌套到底是怎么实现的?
Go Error 的设计哲学是 「Errors Are Values」。
这句话应该怎么理解呢?翻译起来挺难的。不过从源码的角度来看,好像更容易理解其背后的含义。
Go Error 源码很简单,寥寥几行:
// src/builtin/builtin.go
type error interface {
	Error() string
}
error 是一个接口类型,只需要实现 Error() 方法即可。在 Error() 方法中,就可以返回自定义结构体的任意内容。
下面首先说说如何创建 error。
创建 Error
创建 error 有两种方式,分别是:
errors.New();fmt.Errorf()。
errors.New()
errors.New() 的使用延续了 Go 的一贯风格,New 一下就可以了。
举一个例子:
package main
import (
	"errors"
	"fmt"
)
func main() {
	err := errors.New("这是 errors.New() 创建的错误")
	fmt.Printf("err 错误类型:%T,错误为:%v\n", err, err)
}
/* 输出
err 错误类型:*errors.errorString,错误为:这是 errors.New() 创建的错误
*/
这段代码唯一让人困惑的地方可能就是错误类型了,但没关系。只要看一下源码,就瞬间迎刃而解。
源码如下:
// 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
}
可以看到,errorString 是一个结构体,实现了 Error() 方法,New 函数直接返回 errorString 指针。
这种用法很简单,但不实用。假如我还想返回程序的上下文信息,它就没辙了。
下面看第二种方式。
fmt.Errorf()
还是先看一个例子:
package main
import (
	"database/sql"
	"fmt"
)
func foo() error {
	return sql.ErrNoRows
}
func bar() error {
	return foo()
}
func main() {
	err := bar()
	if err == sql.ErrNoRows {
		fmt.Printf("data not found, %+v\n", err)
		return
	}
	if err != nil {
		fmt.Println("Unknown error")
	}
}
/* 输出
data not found, sql: no rows in result set
*/
这个例子输出了我们想要的结果,但是还不够。
一般情况下,我们会通过使用 fmt.Errorf() 函数,附加上我们想添加的文本信息,使返回内容更明确,处理起来更灵活。
所以,foo() 函数会改成下面这样:
func foo() error {
   return fmt.Errorf("foo err, %v", sql.ErrNoRows)
}
这时问题就出现了,经过 fmt.Errorf() 的封装,原始 error 类型发生了改变,这就导致 err == sql.ErrNoRows 不再成立,返回信息变成了 Unknown error。
如果想根据返回的 error 类型做不同处理,就无法实现了。
因此,Go 1.13 为我们提供了 wrapError 来处理这个问题。
Wrap Error
看一个例子:
package main
import (
	"fmt"
)
type myError struct{}
func (e myError) Error() string {
	return "Error happended"
}
func main() {
	e1 := myError{}
	e2 := fmt.Errorf("E2: %w", e1)
	e3 := fmt.Errorf("E3: %w", e2)
	fmt.Println(e2)
	fmt.Println(e3)
}
/* output
E2: Error happended
E3: E2: Error happended
*/
乍一看好像好没什么区别,但背后的实现原理却并不相同。
Go 扩展了 fmt.Errorf() 函数,增加了一个 %w 标识符来创建 wrapError。
// 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
}
当使用 w% 时,函数会返回 &wrapError{s, p.wrappedErr},wrapError 结构体定义如下:
// src/fmt/errors.go
type wrapError struct {
	msg string
	err error
}
func (e *wrapError) Error() string {
	return e.msg
}
func (e *wrapError) Unwrap() error {
	return e.err
}
实现了 Error() 方法,说明它是一个 error,而 Unwrap() 方法是为了获取被封装的 error。
// src/errors/wrap.go
func Unwrap(err error) error {
	u, ok := err.(interface {
		Unwrap() error
	})
	if !ok {
		return nil
	}
	return u.Unwrap()
}
它们之间的关系是这样的:

因此,我们可以使用 w% 将上文中的程序进行改造,使其内容输出更丰富。
如下:
package main
import (
	"database/sql"
	"errors"
	"fmt"
)
func bar() error {
	if err := foo(); err != nil {
		return fmt.Errorf("bar failed: %w", foo())
	}
	return nil
}
func foo() error {
	return fmt.Errorf("foo failed: %w", sql.ErrNoRows)
}
func main() {
	err := bar()
	if errors.Is(err, sql.ErrNoRows) {
		fmt.Printf("data not found,  %+v\n", err)
		return
	}
	if err != nil {
		fmt.Println("Unknown error")
	}
}
/* output
data not found,  bar failed: foo failed: sql: no rows in result set
*/
终于有了让人满意的输出结果,每个函数都增加了必要的上下文信息,而且也符合对错误类型的判断。
errors.Is() 函数用来判断 err 以及其封装的 error 链中是否包含目标类型。这也就解决了上文提出的无法判断错误类型的问题。
后记
其实,Go 目前对 Error 的处理方式也是充满争议的。不过,官方团队正在积极和社区交流,提出改进方法。相信在不久的将来,一定会找到更好的解决方案。
现阶段来说,大部分团队可能会选择 github.com/pkg/errors 包来进行错误处理。如果感兴趣的话,可以学学看。
好了,本文就到这里吧。关注我,带你通过问题读 Go 源码。
源码地址:
推荐阅读:
参考文章:
- https://chasecs.github.io/posts/the-philosophy-of-go-error-handling/
 - https://medium.com/@dche423/golang-error-handling-best-practice-cn-42982bd72672
 - https://www.flysnow.org/2019/09/06/go1.13-error-wrapping.html
 
Go Error 嵌套到底是怎么实现的?的更多相关文章
- 读 Go 源码,可以试试这个工具
		
原文链接: 读 Go 源码,可以试试这个工具 编程发展至今,从面向过程到面向对象,再到现在的面向框架.写代码变成了一件越来越容易的事情. 学习基础语法,看看框架文档,几天时间搞出一个小项目并不是一件很 ...
 - Go语言(golang)新发布的1.13中的Error Wrapping深度分析
		
Go 1.13发布的功能还有一个值得深入研究的,就是对Error的增强,也是今天我们要分析的 Error Wrapping. 背景 做Go语言开发的,肯定经常用error,但是我们也知道error非常 ...
 - [Effective JavaScript 笔记]第24条:使用变量保存arguments对象
		
迭代器(iterator)是一个可以顺序存取数据集合的对象.其一个典型的API是next方法.该方法获得序列中的下一个值. 迭代器示例 题目:希望编写一个便利的函数,它可以接收任意数量的参数,并为这些 ...
 - socket+mysql
		
测试windows 下 Socket+Mysql调用. 环境: OS:Win7; DBMS:Mysql; IDE:codeblockS. server: #include <WINSOCK2. ...
 - winSocket数据传输
		
服务器端: #include <WINSOCK2.H> #include <stdio.h> #pragma comment(lib,"ws2_32.lib" ...
 - Oracle学习系列1-7
		
Oracle学习系列1 两个服务必须启动: OracleOraDb10g*TNListener 和 OracleService*** 使用sqlplusw先进行环境的设置 set linesize 3 ...
 - Oracle学习系列7
		
Oracle学习系列7 ************************************************************************************ 关联表 ...
 - c++有默认参数的函数---4
		
原创博客:转载请标明出处:http://www.cnblogs.com/zxouxuewei/ 1.默认参数的目的 C++可以给函数定义默认参数值.通常,调用函数时,要为函数的每个参数给定对应的实参. ...
 - winsock2之最简单的win socket编程
		
原文:winsock2之最简单的win socket编程 server.cpp #include <WINSOCK2.H> #include <stdio.h> #pragma ...
 
随机推荐
- Python中冷门但非常好用的内置函数
			
Python中有许多内置函数,不像print.len那么广为人知,但它们的功能却异常强大,用好了可以大大提高代码效率,同时提升代码的简洁度,增强可阅读性 Counter collections在pyt ...
 - [BUUCTF]PWN——[V&N2020 公开赛]warmup
			
[V&N2020 公开赛]warmup 附件 步骤: 例行检查,64位程序,除了canary,其他保护都开 本地运行一下,看看大概的情况 64位ida载入,从main函数开始看程序 看到程序将 ...
 - [BUUCTF]PWN12——[BJDCTF 2nd]r2t3
			
[BUUCTF]PWN12--[BJDCTF 2nd]r2t3 题目网址:https://buuoj.cn/challenges#[BJDCTF%202nd]r2t3 步骤: 例行检查,32位,开启了 ...
 - 移动应用开发:Flutter
			
目录 前言 Flutter 介绍 移动应用开发:选择对比 原生开发 H5 技术 React Native Flutter 总结 参考 前言 "镜子镜子 告诉我,世界上最好的语言是什么&quo ...
 - CF1090M The Pleasant Walk 题解
			
Content 有一个长度为 \(n\) 的数组 \(a_1,a_2,a_3,...,a_n\),并已知有 \(k\) 个不相同的元素求最长连续的一段区间,使得这里面的元素互不相同. 数据范围:\(1 ...
 - 磁盘分区级文件系统丢失找回 testdisk
			
故障修复步骤: 1. 检查磁盘分区级文件系统确实不在: 2. 云主机内部下载testdisk工具修复 yum install testdisk -y 3. 执行命令testdisk /dev/vdc进 ...
 - 利用struts2上传文件时,如果文件名中含有-符号,那么会出错
			
利用struts2上传文件时,如果文件名中含有-符号,那么会出错 报错如下: HTTP Status 500 - C:\Program Files\Apache Software Foundation ...
 - Android NDK开发篇:如何使用JNI中的global reference和local reference
			
JNI提供了一些实例和数组类型(jobject.jclass.jstring.jarray等)作为不透明的引用供本地代码使用.本地代码永远不会直接操作引用指向的VM内部的数据内容.要进行这些操作,必须 ...
 - 【LeetCode】118. Pascal's Triangle 解题报告(Python)
			
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 Java解法 Python解法 日期 [LeetCo ...
 - 【LeetCode】106. Construct Binary Tree from Inorder and Postorder Traversal 解题报告
			
[LeetCode]106. Construct Binary Tree from Inorder and Postorder Traversal 解题报告(Python) 标签: LeetCode ...