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}
}
有疑问加站长微信联系(非本文作者)