网站首页 文章专栏 go语言让人又爱又恨的error
go的error其实很简单,就是一个普通的接口,一个普通的值。
我们看下源码:
package errors // 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 }
我们一般使用 errors.New()来返回一个error对象。实际error就是一个字符串,使用errorString进行保存,值得注意的是,new方法中返回的是内部errorString对象的指针。也就是如果用两个一摸一样的错误字符串,那么比较的时候也是不相同的,保证每个错误都是独立唯一的。
java中进入checked exception,方法的所有者必须申明,调用者必须处理。使用try catch对异常进行处理,导致java里面异常变得很常见,从普通的异常,到严重的异常都有,很多人经常就会使用一个Exception对象直接捕获,然后忽略,或者仅仅只是打个log。
catche( Exception e) { // ignore }
而go中由于支持多参数返回,它没进入exception,可以在函数签名带上实现error interface的对象,由调用者来进行判断怎么处理异常。类似这样:
func handle()(int,error){ return 1,nil } func handleError()(int,error){ return 1,errors.New("出现错误") }
那么我们在调用这个函数的时候,就会获得一个error对象,通过判断 err != nil 就可以由调用者判断是否出现了异常。如果一个函数返回了(value, error),那么就不能对error忽略,不能对value做任何假设,必须先判定error的情况,唯一可以忽略的是,如果根本不关心value的时候。
go中还存在panic的机制,panic和异常不同,异常仅仅表示出问题了,返回给调用者进行处理,而panic意味着fatal error,不能假设调用者来解决panic,也就是挂了,代码跑不下去了。某种意义上panic才代表了真正的‘异常’。panic我们可以使用recovery来进行兜底,牺牲单请求,让我们程序可以继续跑下去,一般也不做逻辑处理,或者其他处理,顶多打个log之类的。
比如我们要写一个函数,判断一个数字是正数还是负数。
func main() { Check(1) Check(-1) } func Check(num int) { if Positive(num) { fmt.Println("为正数") } else { fmt.Println("为负数") } } func Positive(num int) bool { return num > -1 }
如上很简单的一个函数,bool返回一个值是否是正数,但是问题来了,当入参为0时,我们没有处理,因为0既不是正数也不是负数。那么我们可以用多参数返回改写下:
func main() { Check(1) Check(-1) Check(0) } func Check(num int) { positive, ok := Positive(num) if !ok { fmt.Println("既不是正数,也不是负数") return } if positive { fmt.Println("为正数") } else { fmt.Println("为负数") } } func Positive(num int) (res bool,ok bool) { if num == 0 { return false, false } return num > -1, true }
通过多参数返回,先对特殊值进行处理,也就是参数校验,就可以了,但是好像也不是太好,我们可以用异常再处理下。
func Check(num int) { positive, err := Positive(num) if err != nil { fmt.Println("既不是正数,也不是负数") return } if positive { fmt.Println("为正数") } else { fmt.Println("为负数") } } func Positive(num int) (res bool,err error) { if num == 0 { return false, errors.New("undefined") } return num > -1, nil }
虽然看着没啥太大区别,但是语义上更直观了,出现不是期待的值时,就算是异常。当然还有种做法,就返回一个值,如果为nil就代表参数错误,如果不为nil再获取值,通过值来获取结果,实际上这种写法是非常不建议的,因为这个值就有了二义性,不再仅仅表示一个含义,在业务上会导致很多麻烦,比如我们在dao层,到底返回一个nil表示找不到对象,还是返回空数组表示呢?这个也是个仁者见仁智者见智的问题,我的理解是返回nil表示找不到对象,因为用空数组表示找不到对象的话,那么如果返回对象就应该为空数组,就会语义就不对了。
还有种写法,使用panic + recovery来处理:
func Check(num int) { defer func() { if recover() != nil { fmt.Println("既不是正数,也不是负数") return } }() if Positive(num) { fmt.Println("为正数") } else { fmt.Println("为负数") } } func Positive(num int) bool { if num == 0 { panic("undefined") } return num > -1 }
这种写法也是非常不推荐的,因为panic + recovery虽然看起来能模拟java里try catch的写法,但是go中panic就不是用来表示异常的,panic仅仅表示系统崩了,跑不下去了。对于真正意外的情况,那些表示不可恢复的程序错误,例如索引越界、不可恢复的环境问题(缺少配置文件)、栈溢出,我们才使用 panic。对于其他的错误情况,我们应该是期望使用 error 来进行判定。
通过上面我们能看出go中error的基本使用方法,很多人吐槽的点也是代码中会大量充斥这 if err != nil 这种写法,让人觉得不爽,那么它和java相比有什么好处呢
我们在java里可能会见到这样的代码:
public User AddNewUser(String userName) { User user = new User(userName); SaveUser(user); AddToGroup(user); return user; }
java中的异常可能会从任意一行抛出,它是不确定的,当我们在执行 saveUser函数时,如果发生了异常,就需要全局异常捕获再处理,而go则会立马在这个函数返回的时候去处理,相当于对saveUser这一行进行try catch单独处理,看起来好像没啥区别,但是体现go的思想是不一样的,go更鼓励对异常立即处理,尽可能让代码语义原子性,强行去细粒度的处理异常,而不是像java那样可以用一个大的try去包裹大量代码然后忽略,或者抛出去不管。
而且java的try catch也使得java代码从try一下跳到catch里面,有的人就用这个来做控制流程的业务了,这一点我觉得也是不太好的,我记得刚学java的时候就记得一点,永远不要用异常来去控制业务流程。而go细粒度的立即处理是没有隐藏控制流的。
总结下go的异常特点:
- 简单,就是一个interface,里面有个字符串保存异常信息
- 考虑失败,而不是成功(plan for failure, not success)
- 没有隐藏的控制流
- 完全交给你来控制 error
- Error are values
版权声明:本文由星尘阁原创出品,转载请注明出处!