网站首页 文章专栏 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
版权声明:本文由星尘阁原创出品,转载请注明出处!