Go 语言自从诞生起,它的错误处理机制始终被喷出翔🙂。
没错,Go 语言在诞生初期的确简陋得不行,但在多个版本迭代以及各位前辈的摸索下还是找到了 Go 语言「错误处理」的最佳实际。
上面咱们深刻理解下 Go 的 error 包,并探讨如何让咱们的 Go 我的项目领有清新的错误处理。
Go 的 errors 包
Go 中的 error 是一个简略的内置接口类型。只有实现了这个接口,就能够将其视为一种 error。
type error interface {Error() string
}
与此同时,Go 的 errors 包实现了这个接口:调用 errors.New()
就会返回 error 接口
的实现类 errorString
,通过源码咱们看到errorString
的底层就是一字符串,可真是 ” 省事 ” 啊🙃。
package errors
func New(text string) error {return &errorString{text}
}
type errorString struct {s string}
func (e *errorString) Error() string {return e.s}
errors.New()函数
返回的是errorString
的指针类型,这样做的目标是为了避免字符串产生碰撞。咱们能够做个小测试:
error1
和error2
的 text 都是"error"
,然而二者并不相等。
func TestErrString(t *testing.T) {var error1 = errors.New("error")
var error2 = errors.New("error")
if error1 != error2 {log.Println("error1 != error2")
}
}
--------------------- 代码运行后果 --------------------------
=== RUN TestXXXX
2022/03/25 22:05:40 error1 != error2
这种创立 error 的形式很常见,在 Go 源码以及三方包源码中大量呈现。
var EOF = errors.New("EOF")
var ErrUnexpectedEOF = errors.New("unexpected EOF")
var ErrNoProgress = errors.New("multiple Read calls return no data or error")
然而很惋惜的是,Go 的 error 设计并不能满足所有场景。
Go error 的设计缺点
error 具备二义性
产生 error != nil
时不再意味着肯定产生了谬误,比方 io.Reader 返回 io.EOF 来告知调用者数据曾经读取结束,而这并不算是一个谬误。
在两个包之间创立了依赖
比方咱们应用了 io.EOF 来检查数据是否读取结束,那么代码里肯定会导入 io 包。
错误信息太薄弱
只有一个字符串表白谬误,过于薄弱。
改良 Go error
当初咱们晓得 error 底层其实就是一字符串,它很简洁,但反过来也意味着 ” 简陋 ”,无奈携带更多错误信息。
自定义谬误类型
所以程序员们决定本人封装一个 error 构造体,比方 Go 源码中的 os.PathError。
type PathError struct {
Op string
Path string
Err error
}
封装 error 堆栈信息
将 error 封装后的确能表白更多的错误信息,然而它还有一个致命问题:没有堆栈信息。
比方这种日志,鬼晓得代码哪一行报了错,Debug 时几乎要命。
SERVICE ERROR 2022-03-25T16:32:10.687+0800!!!
Error 1406: Data too long for column 'content' at row 1
咱们能够应用 github.com/pkg/error 包
解决这个问题,这个包提供了 errors.withStack() 办法
将堆栈信息封装进 error:
func WithStack(err error) error {
if err == nil {return nil}
return &withStack{
err,
callers(),}
}
type withStack struct {
error
*stack
}
避免 error 被笼罩
下层 error 想附带更多日志信息时,往往会应用 fmt.Errorf()
,fmt.Errorf()
会创立一个新的 error 笼罩掉本来的 error 类型,咱们写一个 demo 测试一下:
var errNoRows = errors.New("no rows")
// 模拟 sql 库返回一个 errNoRows
func sqlExec() error {return errNoRows}
func serviceNoErrWrap() error {err := sqlExec()
if err != nil {// fmt.Errorf() 吞掉了本来的 errNoRows 类型谬误。return fmt.Errorf("sqlExec failed.Err:%v", err)
}
return nil
}
func TestErrWrap(t *testing.T) {
// 应用 fmt.Errorf 创立了一个新的 err,失落了底层 err
err := serviceNoErrWrap()
if err != errNoRows {log.Println("===== errType don't equal errNoRows =====")
}
}
------------------------------- 代码运行后果 ----------------------------------
=== RUN TestErrWrap
2022/03/26 17:19:43 ===== errType don't equal errNoRows =====
同样,应用 github.com/pkg/error 包
的errors.Wrap()函数
能够帮忙咱们为 error 增加自定义的文本信息。
func Wrap(err error, message string) error {
if err == nil {return nil}
err = &withMessage{
cause: err,
msg: message,
}
return &withStack{
err,
callers(),}
}
github.com/pkg/error 包
内容很多,这里不开展聊了,前面独自讲。
到此为止,咱们深刻意识了 Go 的 error,当初咱们谈谈如何在大型项目中做好错误处理。
error 解决最佳实际
优先解决 error
当一个函数返回 error 时,应该优先解决 error,疏忽其余返回值。
只解决 error 一次
在 Go 中,每个 err 只应该被解决一次。如果一个函数返回了 err,那么咱们有两个抉择:
- 抉择一:立刻解决 err(包含记日志等行为),而后 return nil(把谬误吞掉)。
这个行为能够被认为是对 error 做降级解决,所以肯定要小心处理函数返回值。
- 抉择二:间接 return err,把 err 抛给调用者。
如果咱们违反了这个准则会导致什么结果?请看反例:
// 试想如果 writeAll 函数出错,会打印两遍日志
// 如果整个我的项目都这么做,最初会惊奇的发现咱们在处处打日志,我的项目中存在大量没有价值的垃圾日志
// unable to write:io.EOF
// could not write config:io.EOF
type config struct {}
func writeAll(w io.Writer, buf []byte) error {_, err := w.Write(buf)
if err != nil {log.Println("unable to write:", err)
return err
}
return nil
}
func writeConfig(w io.Writer, conf *config) error {buf, err := json.Marshal(conf)
if err != nil {log.Printf("could not marshal config:%v", err)
}
if err := writeAll(w, buf); err != nil {log.Println("count not write config: %v", err)
return err
}
return nil
}
不要重复包装 error
咱们应该包装 error,但仅包装一次,否则会造成日志反复打印。
下层业务代码倡议 Wrap error
,然而底层 根底 Kit 库
则不倡议这样做。比方 Go 的 sql 库
会返回 sql.ErrNoRows
这种预约义谬误,而后咱们的业务代码将其包装后 return。
不通明的错误处理
在大型项目中,举荐应用 不通明的错误处理(Opaque errors)
:不关怀谬误类型,只关怀 error 是否为 nil。
这种形式代码耦合小,不须要判断特定谬误类型,也就不须要导入相干包的依赖。
当然了,在这种状况下,只有咱们调用函数,就肯定跟着一组 if err != nil{}
,这也是大家常常吐槽 Go 我的项目if err != nil{}
满天飞的起因😂。
目前咱们只探讨在调用 Go 内置库和第三方库时产生的 error 的最佳解决实际,业务层面的错误处理是一个独自的话题,当前独自写一篇聊。
优化错误处理流程
Go 因为代码中有数的 if err != nil
被诟病,当初我教大家一个优化技巧:
咱们先看看 bufio.scan() 是如何简化 error 解决的:
// CountLines() 实现了 "读取内容的行数" 性能
func CountLines(r io.Reader) (int, error) {
var (br = bufio.NewReader(r)
lines int
err error
)
for {_, err := br.ReadString('\n')
lines++
if err != nil {break}
}
if err != io.EOF {return 0, nilsadwawa}
return lines, nil
}
// 利用 bufio.scan() 简化 error 的解决:func CountLinesGracefulErr(r io.Reader) (int, error) {sc := bufio.NewScanner(r)
lines := 0
for sc.Scan() {lines++}
return lines, sc.Err()}
源码解读:bufio.NewScanner()
返回一个 Scanner
对象,构造体外部蕴含了 error 类型,调用 Err()
办法即可返回封装好的 error。
type Scanner struct {
r io.Reader // The reader provided by the client.
split SplitFunc // The function to split the tokens.
maxTokenSize int // Maximum size of a token; modified by tests.
token []byte // Last token returned by split.
buf []byte // Buffer used as argument to split.
start int // First non-processed byte in buf.
end int // End of data in buf.
err error // Sticky error.
empties int // Count of successive empty tokens.
scanCalled bool // Scan has been called; buffer is in use.
done bool // Scan has finished.
}
func (s *Scanner) Err() error {
if s.err == io.EOF {return nil}
return s.err
}
利用下面学到的思路,咱们能够本人实现一个 errWriter
对象,简化对 error 的解决:
type Header struct {Key, Value string}
type Status struct {
Code int
Reason string
}
// WriteResponse()函数实现了 "构建 HttpResponse" 性能
func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {_, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
if err != nil {return err}
for _, h := range headers {_, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
if err != nil {return err}
}
if _, err := fmt.Fprintf(w, "\r\n"); err != nil {return err}
_, err = io.Copy(w, body)
return err
}
// 优化错误处理
type errWriter struct {
io.Writer
err error
}
func (e *errWriter) Write(buf []byte) (n int, err error) {
if e.err != nil {return 0, e.err}
n, e.err = e.Writer.Write(buf)
return n, nil
}
func WriteResponseGracefulErr(w io.Writer, st Status, headers []Header, body io.Reader) error {ew := &errWriter{w, nil}
fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
for _, h := range headers {fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
}
fmt.Fprintf(w, "\r\n")
io.Copy(ew, body)
return ew.err
}
Go.1.13 版本 error 的新个性
Go 1.13 版本借鉴了github.com/pkg/error 包
,大幅加强了 Golang 语言判断 error 类型的能力,这些函数平时还是用失去的,咱们深刻学习下:
errors.UnWrap()
// 与 errors.Wrap()行为相同
// 获取 err 链中的底层 err
func Unwrap(err error) error {
u, ok := err.(interface {Unwrap() error
})
if !ok {return nil}
return u.Unwrap()}
errors.Is()
在 1.13 版本之前,咱们能够用 err == targetErr
判断 err 类型
errors.Is()
是其增强版:error 链上的 任一 err == targetErr
,即return true
,咱们写个 demo 跑一下:
var errNoRows = errors.New("no rows")
// 模拟 sql 库返回一个 errNoRows
func sqlExec() error {return errNoRows}
func service() error {err := sqlExec()
if err != nil {return errors.WithStack(err) // 包装 errNoRows
}
return nil
}
func TestErrIs(t *testing.T) {err := service()
// errors.Is 递归调用 errors.UnWrap,命中 err 链上的任意 err 即返回 true
if errors.Is(err, errNoRows) {log.Println("===== errors.Is() succeeded =====")
}
//err 经 errors.WithStack 包装,不能通过 == 判断 err 类型
if err == errNoRows {log.Println("err == errNoRows")
}
}
------------------------------- 代码运行后果 ----------------------------------
=== RUN TestErrIs
2022/03/25 18:35:00 ===== errors.Is() succeeded =====
例子解读:因为应用 errors.WithStack
包装了 sqlError
,sqlError
位于 error 链的底层,下层的 error 曾经不再是 sqlError
类型,所以应用 ==
无奈判断出底层的sqlError
源码解读:
- 外部调用了
err = Unwrap(err)
办法来获取 error 链中每一个 error。 - 兼容自定义 error 类型。
func Is(err, target error) bool {
if target == nil {return err == target}
isComparable := reflectlite.TypeOf(target).Comparable()
for {
if isComparable && err == target {return true}
// 自定义的 error 能够实现 `Is 接口 ` 自定义 error 类型判断逻辑
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {return true}
if err = Unwrap(err); err == nil {return false}
}
}
上面咱们尝试应用 erros.Is()
辨认自定义 error 类型:
type errNoRows struct {Desc string}
func (e errNoRows) Unwrap() error { return e}
func (e errNoRows) Error() string { return e.Desc}
func (e errNoRows) Is(err error) bool {return reflect.TypeOf(err).Name() == reflect.TypeOf(e).Name()}
// 模拟 sql 库返回一个 errNoRows
func sqlExec() error {return &errNoRows{"Kaolengmian NB"}
}
func service() error {err := sqlExec()
if err != nil {return errors.WithStack(err)
}
return nil
}
func serviceNoErrWrap() error {err := sqlExec()
if err != nil {return fmt.Errorf("sqlExec failed.Err:%v", err)
}
return nil
}
func TestErrIs(t *testing.T) {err := service()
if errors.Is(err, errNoRows{}) {log.Println("===== errors.Is() succeeded =====")
}
}
------------------------------- 代码运行后果 ----------------------------------
=== RUN TestErrIs
2022/03/25 18:35:00 ===== errors.Is() succeeded =====
errors.As()
在 1.13 版本之前,咱们能够用 if _,ok := err.(targetErr)
判断 err 类型,当初 errors.As()
是其增强版:error 链上的 任一 err 与 targetErr 类型雷同
,即return true
,咱们写个 demo 跑一下:
// errors.WithStack 包装了 sqlError
// sqlError 位于 error 链的底层,下层的 error 曾经不再是 sqlError 类型
// 应用类型断言无奈判断出底层的 sqlError,而应用 errors.As() 函数能够判断出底层的 sqlError
type sqlError struct {error}
func (e *sqlError) IsNoRows() bool {t, ok := e.error.(ErrNoRows)
return ok && t.IsNoRows()}
type ErrNoRows interface {IsNoRows() bool
}
// 返回一个 sqlError
func sqlExec() error {return sqlError{}
}
// errors.WithStack 包装 sqlError
func service() error {err := sqlExec()
if err != nil {return errors.WithStack(err)
}
return nil
}
func TestErrAs(t *testing.T) {err := service()
// 递归应用 errors.UnWrap,只有 Err 链上有一种 Err 满足类型断言,即返回 true
sr := &sqlError{}
if errors.As(err, sr) {log.Println("===== errors.As() succeeded =====")
}
// 经 errors.WithStack 包装后,不能通过类型断言将以后 Err 转换成底层 Err
if _, ok := err.(sqlError); ok {log.Println("===== type assert succeeded =====")
}
}
---------------------------------- 代码运行后果 --------------------------------------------
=== RUN TestErrAs
2022/03/25 18:09:02 ===== errors.As() succeeded =====
总结
这篇文章咱们意识了 Go 的 error,钻研了 error 包
、github.com/pkg/error 包
的源码,也聊了聊针对 Go 我的项目错误处理的优化与最佳实际,文中有大量 Demo 代码,倡议 copy 代码跑上两遍,有助于了解我薄弱的文字,有助于疾速把握 Go 的 error 解决。
参考:
- 《Effective GO》
- Go 程序设计语言》
- https://dave.cheney.net/practical-go/presentations/qcon-china.html#_error_handling
文章归档:Go 源码解读
转载申明:本文容许转载,原文地址:Go error 源码解读、错误处理的优化与最佳实际