golang没有try...catch panic表示不服

2,219 阅读5分钟

golang的诟病

Golang错误处理方式一直是很多人诟病的地方,有些人吐槽说一半的代码都是

if err != nil { 
    打印 && 错误处理 
}

太不优雅,像狗皮膏药一样太难看了。真的是这样么?golang真的没有异常捕获,只能不停的判断错误么?相信google的工程师应该早就考虑到这个问题了吧?首先让我们来定义什么是错误,什么是异常

panic.jpeg

定义错误和异常

错误指的是可能出现问题的地方出现了问题:

  1. 打开一个文件时失败
  2. 参数校验参数签名等失败
  3. 支付一个关闭的订单,等业务逻辑的错误

而异常指的是不应该出现问题的地方出现了问题:

  1. 访问了空指针
  2. 数组越界
  3. 被0除等

定义完毕错误是业务过程的一部分,而异常不是

golang是如何处理错误和异常的

Go语言不支持传统的 try…catch…finally 这种异常。 google设计者们认为,将异常与控制结构混在一起会很容易使得代码变得混乱。 因为开发者很容易滥用异常,甚至一个小小的错误都抛出一个异常。 在Go语言中,使用多值返回来返回错误。不要用异常代替错误,更不要用来控制流程。在极个别的情况下,也就是说,遇到真正的异常的情况下(比如除数为 0了)。才使用Go中引入的Exception处理:defer, panic, recover。

重点:golang支持多返回,不是给你滥用的,golang的error是用来给你处理业务错误的,defer, panic, recover才是真正的异常处理

避免if err != nil 的狗皮膏药

golang的错误判断确实很让人烦,例如:

if err != nil { 
    打印 && 错误处理 
}

不过我们可以通过以下几个小技巧来避免代码里面全是“狗皮膏药”

1. 能不返回错误就不返回错误

很多人随便写个方法就要返回error,其实完全没必要啊!!!!

func InitLogger(logpath string) error {
    hook := lumberjack.Logger{
        Filename: logpath, // 日志文件路径
        MaxSize: 10, // 每个日志文件保存的最大尺寸 单位:M
        MaxBackups: 30, // 日志文件最多保存多少个备份
        MaxAge: 7, // 文件最多保存多少天
        Compress: true, // 是否压缩
    }
    Log = logrus.New()
    Log.SetReportCaller(true)
    Log.SetFormatter(new(LogFormatter))
    Log.SetOutput(&hook)
    return nil
}

你初始化一个日志,都没啥错误,就不用返回error了

func InitLogger(logpath string){
    ...
}

不就很干脆么?

2. 能恢复的错误尽量重试避免错误

func GetPageInfo(url string) error {
    //TODO: 调用接口查询状态
    return nil
}

像上面这种查询状态的方法,由于网络异常,业务异常等通过重试的恢复的就尽量避免返回错误,在方法内部重试。

如果错误的发生是偶然性的,或由不可预知的问题导致。一个明智的选择是重新尝试失败的操作,有时第二次或第三次尝试时会成功。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。

3. 用状态码代替错误

对于错误类型名确定的,我们可以直接返回状态码来代替错误

const (
_  = iota
ERR_EOF 
ERR_CLOSED_PIPE 
ERR_NO_PROGRESS
ERR_SHORT_BUFFER
ERR_SHORT_WRITE
ERR_UNEXPECTED_EOF 
)

func CheckNetState() int {
    i := ERR_EOF
    ...
    switch net: {
        ...
    }
    retuen i
}

4. 忽略可选择忽略错误

有些返回的错误,其实是可以忽略,例如:json数据的序列化和反序列化

type data struct{
    Name string `json:"name"`  // 字段解释,可指json 字符串的名字
    Age int `json: age`
    Like string `json: like`
} 
json_str, _ := json.Marshal(data{"yxl", 25, "freedom"})

5. 多层嵌套时使用defer, panic, recover统一在外层处理

这里就列举一下gin的统一异常拦截器

func PanicHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                utils.Log.Error("PanicHandler:", err)
                c.AbortWithError(500, errors.New("Oops!服务器出错了!"))
            }
        }()
        c.Next()
    }
}

6. 开发测试阶段速错,尽量排除错误

在学习rabbitmq的时候接触到了Erlang,于是就了速错的理念,简单来讲就是“让程序挂掉”,只有挂了你才会第一时间知道错误。在早期开发以及任何发布阶段之前,最简单的同时也可能是最好的方法是调用panic函数来中断程序的执行以强制发生错误,使得该错误不会被忽略,因而能够被尽快修复。

踩坑踩多了,路就平了,所以不用害怕掉坑里,程序猿的一生就是与bug斗争的一生!

当然上了生产记得recover掉错误,否则小心夺命连环call~

最后一个问题:父协程无法捕获子协程的panic

func main() {
  // 希望捕获所有所有 panic
  defer func() {
    r := recover()
    fmt.Println(r)
  }()
  // 启动新协程
  go func() {
    panic(123)
  }()
  // 等待一下,不然协程可能来不及执行
  time.Sleep(1 * time.Second)
  fmt.Println("这条消息打印不出来")
}

自己执行一下就会发现,最后的fmt.Println没有执行,也就是说开始的recover()没有捕获协程中的panic,整个进程都退出了。

一般的Go框架都是针对每一个请求启一个新协程,并统一捕获所有的panic。

如果程序员在写业务代码的时候开了新协程而且忘记了在协程中捕获panic的话,服务的进程就会因为某个未捕获的panic而退出。 这不是坑爹嘛!

协程池

手把手教你写一个golang协程池

还记得前面的文章么?

标准协程不支持的话,就写个框架来支持

func (pool *GorunPool) call(task *GorunTask) {
    pool.Ticket++
    go func() {
        go func() {
            if task.Ctx != nil {
                for {
                    select {
                    case <-task.Ctx.Done():
                        task.Status = "exit!"
                        pool.ResChan <- task
                        return
                    default:
                        time.Sleep(10 * time.Millisecond)
                    }
                }
            }
        }()
        defer func() {
            if err := recover(); err != nil {
                task.Status = "paniced!"
                task.Err = errors.New(fmt.Sprintf("panic %+v", err))
                pool.ResChan <- task
            }
        }()
        task.Status = "running"
        task.Err = task.Run()
        task.Status = "exected!"
        pool.ResChan <- task
    }()
}

代码验证一下:

func TestPanic(t *testing.T) {
    pool := NewGorunPool(5)
    job := func() error {
        panic("job paniced!")
        t.Error("do thread!")
        return nil
    }
    callback := func(task *GorunTask) {
        t.Error(task.Name, "do callback!", task.Err, task.Status)
    }
    ctx1, cancel1 := context.WithCancel(context.TODO())
    task1 := NewGorunTaskWithCtx(ctx1, job, callback)
    task1.Name = "T1"
    pool.Execute(task1)
    defer cancel1()
    time.Sleep(1 * time.Second)
}

执行结果如下:

Jietu20220302-200924.jpg

协程的回调函数被正常执行了,框架帮我们捕获了异常,是不是超方便,要验证没有捕获的情况,取消以下代码即可

        defer func() {
            if err := recover(); err != nil {
                task.Status = "paniced!"
                task.Err = errors.New(fmt.Sprintf("panic %+v", err))
                pool.ResChan <- task
            }
        }()

执行结果如下:

Jietu20220302-201415.jpg 直接panic,速错是不是又粗暴又直接

(o゜▽゜)o☆[Panic!]