分享
  1. 首页
  2. 文章

Golang中Error处理方案

bysir · · 15760 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

error 与 try catch 对比

有人说err写得繁琐,而我却觉得更准确,清晰。更明确的说明这个函数可能有错,提示程序猿需要显示处理。

当然在使用err之前,我也没觉得java那样的try catch有什么问题,不过现在我更喜欢err。

我们来对比一下两者的优劣,从写法和排错方法两个方面。

写法

在编写被调用函数方两者基本差别不大,一边需要写err 一边需要throw。

在调用方try的方式可以一次捕获多个thower,而err就需要繁琐的写多次。

当然try也有坏处,try会导致代码缩进一层。

排错

try看问题必须看错误堆栈,而我发现堆栈中80%的字母都是无用的。

而err的错误信息就一行,清晰明了。

那有人说了,就一行这么少信息,怎么定位是谁调用了它,怎么确定入参是什么才引起的错误呢?

为了解释一行错误信息也能定位问题,我先提出一个概念:

一个函数应该是完善的,确定的。

先看例子再来说概念吧,这样更好理解。

// 糟糕的代码
// 入参模糊, 当发生错误, 必须依赖上层参数才能找到错误.
func Insert(data interface{}) (err error) {
 err = mysql.Insert(data)
 if err != nil {
 return
 }
 return
}
// 正确的代码
// 确定性: 入参确定
func InsertUser(user *User) (err error) {
 // 完善性: 函数应该自己能够判断一个入参是否非法并立刻返回错误
 if user == nil {
 err = ...
 return
 }
 if user.UserName == "" {
 err = ...
 return
 }
 err = mysql.Insert(user)
 if err != nil {
 return
 }
 return
}

完善的: 自己管理自己的入参,函数应该自己能够判断一个入参是否非法并立刻返回错误,不要等到最后报一个模棱两可的错。

确定的: 一个函数做一件事情,并且入参和出参是确定的,这样无论多少个调用者来调用这个函数,都不会因为入参不同而需要上层调用者信息来排查问题。

可以看到,排错的简易程度和错误堆栈并没有必然关系,而和编写代码的人有必然关系。

error的问题

我认为err的缺陷也很明显,由于没有错误堆栈,如果只有一个string提示的话,当我们程序比较复杂,层级较多时,我们很难定位到是那一层出错了。

那么又矛盾了,那还是需要错误堆栈的呀?

并不是,我们再理一下思路,想一想我们到底需要什么。

在这之前,我们先要区分开两种错误:意料之中的和意料之外的。

意料之中的err

意料之中指的是前端请求的参数不按照规定来,这种错误我们不需要打印错误日志,甚至可以不用管。不过为了前端让前端知道是哪个参数有问题,后端还是需要返回一个错误信息。
这时候需要:

  • 能让前端清晰知道缘由的错误提示。如:"id不能为空"。

不过事实没这么简单,当一个api会做两件事情的时候,前端需要得到更详细的错误信息:

  • "插入订单表错误:id不能为空"
  • "插入商品表错误:id不能为空"

一般来说"id不能为空"是被调用函数报的,而"插入订单表错误"则是由调用方报的。这时候就涉及到err的拼接问题,先把这个问题记下来,待会解决。

意料之外的err

意料之外指的是服务器异常,如数据库连接不上,这个时候错误不需要告诉前端,而是应该收集起来发送给后端开发人员。
这时候就需要有:

  • 错误原因,这个当然是必须的。如数据库连接不上。
  • 错误位置,哪一个文件哪一行代码。这个也是必须的。

区分了两种错误事情就简单多了。

下面开始写代码

优化error

主要思想有几点:

  • 使用错误码区分上述"意料之中"与"意料之外"(下文会用"错误"和"异常"代替)
  • 使用runtime.Caller来获取当前行数
  • 使用warp提供更多信息来代替"堆栈"

那接下来就看简化版代码吧 (伪代码)

定义错误

type ErrorCode struct {
 code uint32
 msg string
 where string
}
func (e *ErrorCode) Error() string {
 return fmt.Sprintf("code = %d ; msg = %s", e.code, e.msg)
}
// 声明一个错误
func NewCoder(code uint32, msg string) *ErrorCode {
 where := caller(1, false)
 return &ErrorCode{code: code, msg: msg, where: where}
}
// 对一个错误追加信息
func Wrap(err error, extMsg ...string) *ErrorCode {
 msg := err.Error()
 if len(extMsg) != 0 {
 msg = strings.Join(extMsg, " : ") + " : " + msg
 }
 return &ErrorCode{msg: msg}
}
// 获取源代码行数
func caller(calldepth int, short bool) string {
 _, file, line, ok := runtime.Caller(calldepth + 1)
 if !ok {
 file = "???"
 line = 0
 } else if short {
 file = filepath.Base(file)
 }
 return fmt.Sprintf("%s:%d", file, line)
}

使用

// 子数据层
func InsertA(id int) (err error) {
 if id == 0 {
 // 非500, 则是"错误"
 err = error.NewCoder(400, "id不能为空")
 return
 }
 err := mysql.Insert(A{id:id})
 if err != nil {
 // 500则是"异常"
 err = error.NewCoder(500, err.String(), "插入数据库错误")
 return
 }
 return
}
func InsertB(id int) (err error) {
 if id == 0 {
 err = error.NewCoder(400, "id不能为空")
 return
 }
 return
}
// 数据层
func Main(aid int, bid int) (err error) {
 err = InsertA(aid)
 if err != nil {
 // 使用warp方法返回更详细的错
 err = error.Warp(err, "插入A错误")
 return
 }
 err = InsertB(aid)
 if err != nil{
 err = error.Warp(err, "插入B错误")
 return
 }
 return
}
// controll层
// 区分"错误"与"异常"并返回对应的响应
func Api(ctx *api.Content){
 var aid = 1
 var bid = 1
 err = Main(a, b)
 if err.Code() == 500 {
 // 将"异常"位置和信息都打印方便排除
 log.Error(err.Where(), err.String())
 ctx.response(500, "服务器错误")
 return
 }
 
 ctx.response(err.Code(), err.String())
}

最终的错误提示会是这样:

  • (错误) 插入A错误: id不能为空
  • (错误) 插入B错误: id不能为空
  • (异常) code.go:154 插入A错误: 插入数据库错误: sql: Scan error on column index 1: unsupported Scan, storing driver.Value type into type *string

emm 这样应该好排错吧

总结

当然这个方案并不完美,欢迎吐槽与讨论。

完整代码在这,还统一了gprc的错误处理:

package errors
import (
 "fmt"
 "strings"
 "google.golang.org/grpc/status"
 "google.golang.org/grpc/codes"
 "path/filepath"
 "runtime"
)
// 业务代码通用的错误
type ErrorCoder interface {
 Error() string
 Code() uint32
 Msg() string
 Where() string // 第一次生成这个错的地方, 第一次: 当newCoder和wrap一个非errorCoder的时候
}
// Grpc的错误
type GRPCStatuser interface {
 GRPCStatus() *status.Status
 Error() string
}
type ErrorCode struct {
 code uint32
 msg string
 where string
}
// 错误,附带code
func (e *ErrorCode) Error() string {
 return fmt.Sprintf("code = %d ; msg = %s", e.code, e.msg)
}
// 不带code的错误消息
func (e *ErrorCode) Msg() string {
 return e.msg
}
func (e *ErrorCode) Code() uint32 {
 return e.code
}
func (e *ErrorCode) Where() string {
 return e.where
}
func NewCoder(code uint32, msg string, extMsg ...string) *ErrorCode {
 if len(extMsg) != 0 {
 msg = strings.Join(extMsg, " : ") + " : " + msg
 }
 where := caller(1, false)
 return &ErrorCode{code: code, msg: msg, where: where}
}
func NewCoderWhere(code uint32, callDepth int, msg string, extMsg ...string) *ErrorCode {
 if len(extMsg) != 0 {
 msg = strings.Join(extMsg, " : ") + " : " + msg
 }
 where := caller(callDepth, false)
 return &ErrorCode{code: code, msg: msg, where: where}
}
func NewCodere(code uint32, err error, extMsg ...string) *ErrorCode {
 var msg string
 if err != nil {
 msg = err.Error()
 }
 if len(extMsg) != 0 {
 msg = strings.Join(extMsg, " : ") + " : " + msg
 }
 where := caller(1, false)
 return &ErrorCode{code: code, msg: msg, where: where}
}
// Wrap 为error添加一个说明, 当这个err不确定是否应该报500或者是由其他服务返回时使用
// 如果err是ErrorCoder或者GRPCStatuser, code将继承, 否则code为0
func Wrap(err error, extMsg ...string) *ErrorCode {
 var msg string
 var code uint32
 var where string
 switch v := err.(type) {
 case ErrorCoder:
 msg = v.Msg()
 code = v.Code()
 where = v.Where()
 case GRPCStatuser:
 s := v.GRPCStatus()
 if s.Code() == codes.Unknown {
 code = 0
 } else if s.Code() < 20 {
 // 只要是grpc自带的错误就说明是系统错误
 code = 500
 } else {
 code = uint32(s.Code())
 }
 msg = s.Message()
 where = caller(1, false)
 default:
 msg = v.Error()
 code = 0
 where = caller(1, false)
 }
 if len(extMsg) != 0 {
 msg = strings.Join(extMsg, " : ") + " : " + msg
 }
 return &ErrorCode{code: code, msg: msg, where: where}
}
func caller(calldepth int, short bool) string {
 _, file, line, ok := runtime.Caller(calldepth + 1)
 if !ok {
 file = "???"
 line = 0
 } else if short {
 file = filepath.Base(file)
 }
 return fmt.Sprintf("%s:%d", file, line)
}
func New(msg string) *ErrorCode {
 where := caller(1, false)
 return &ErrorCode{code: 0, msg: msg, where: where}
}

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:bysir

查看原文:Golang中Error处理方案

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
15760 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏