golang异常处理详解

简介: golang异常处理详解

小熊今天有意外收获,忍不住给大家分享我愉快的心情!昨天中午下楼取外卖的时候被一个同事认出来了,他问我:“是不是【编程三分钟】的作者,文章写的不错”。


你知道吗!我当时就是一愣,然后差点感动到哭出来,虽然小熊的号比不上大牛的号,不能随便发一篇文章都有成千上万的阅读量;但是非常开心的是,我还有你们,默默的关注我,爱你们~!


今天想和大家聊聊 golang 的异常处理


异常处理思想


go 语言里是没有 try catch 的概念的,因为 try catch 会消耗更多资源,而且不管从 try 里面哪个地方跳出来,都是对代码正常结构的一种破坏。


所以 go 语言的设计思想中主张


如果一个函数可能出现异常,那么应该把异常作为返回值,没有异常就返回 nil

每次调用可能出现异常的函数时,都应该主动进行检查,并做出反应,这种 if 语句术语叫卫述语句


所以异常应该总是掌握在我们的手上,保证每次操作产生的影响达到最小,保证程序即使部分地方出现问题,也不会影响整个程序的运行,及时的处理异常,这样就可以减轻上层处理异常的压力。


同时也不要让未知的异常使你的程序崩溃。


异常的形式


我们应该让异常以这样的形式出现


func Demo() (int, error)


我们应该让异常以这样的形式处理(卫述语句)


_,err := errorDemo()
  if err!=nil{
    fmt.Println(err)
    return
  }


自定义异常


比如程序有一个功能为除法的函数,除数不能为 0 ,否则程序为出现异常,我们就要提前判断除数,如果为 0 返回一个异常。那他应该这么写。


func divisionInt(a, b int) (int, error) {
  if b == 0 {
    return -1, errors.New("除数不能为0")
  }
  return a / b, nil
}


这个函数应该被这么调用

a, b := 4, 0
res, err := divisionInt(a, b)
if err != nil {
  fmt.Println(err.Error())
  return
}
fmt.Println(a, "除以", b, "的结果是 ", res)

可以注意到上面的两个知识点


  • 创建一个异常 errors.New("字符串")
  • 打印异常信息 err.Error()


只要记得这些,你就掌握了自定义异常的基本方法。

但是 errors.New("字符串") 的形式我不建议使用,因为他不支持字符串格式化功能,所以我一般使用 fmt.Errorf 来做这样的事情。


err = fmt.Errorf("产生了一个 %v 异常", "喝太多")


详细的异常信息


上面的异常信息只是简单的返回了一个字符串而已,想在报错的时候保留现场,得到更多的异常内容怎么办呢?这就要看看 errors 的内部实现了。其实相当简单。


errors 实现了一个叫 error 的接口,这个接口里就一个 Error 方法且返回一个 string ,如下


type error interface {
  Error() string
}



只要结构体实现了这个方法就行,源码的实现方式如下

type errorString struct {
  s string
}
func (e *errorString) Error() string {
  return e.s
}
// 多一个函数当作构造函数
func New(text string) error {
  return &errorString{text}
}

所以我们只要扩充下自定义 error 的结构体字段就行了。

这个自定义异常可以在报错的时候存储一些信息,供外部程序使用

type FileError struct {
  Op   string
  Name string
  Path string
}
// 初始化函数
func NewFileError(op string, name string, path string) *FileError {
  return &FileError{Op: op, Name: name, Path: path}
}
// 实现接口
func (f *FileError) Error() string {
  return fmt.Sprintf("路径为 %v 的文件 %v,在 %v 操作时出错", f.Path, f.Name, f.Op)
}

调用


f := NewFileError("读", "README.md", "/home/how_to_code/README.md")
fmt.Println(f.Error())


输出


路径为 /home/how_to_code/README.md 的文件 README.md,在 读 操作时出错

defer


上面说的内容很简单,在工作里也是最常用的,下面说一些拓展知识。


Go 中有一种延迟调用语句叫 defer 语句,它在函数返回时才会被调用,如果有多个 defer 语句那么它会被逆序执行。


比如下面的例子是在一个函数内的三条语句,他是这么怎么执行的呢?


defer fmt.Println("see you next time!")
defer fmt.Println("close all connect")
fmt.Println("hei boy")

输出如下, 可以看到两个 defer 在程序的最后才执行,而且是逆序。


hei boy
close all connect
see you next time!


这一节叫异常处理详解,终归是围绕异常处理来讲述知识点, defer 延迟调用语句的用处是在程序执行结束,甚至是崩溃后,仍然会被调用的语句,通常会用来执行一些告别操作,比如关闭连接,释放资源(类似于 c++ 中的析构函数)等操作。


涉及到 defer 的操作

  • 并发时释放共享资源锁
  • 延迟释放文件句柄
  • 延迟关闭 tcp 连接
  • 延迟关闭数据库连接


这些操作也是非常容易被人忘记的操作,为了保证不会忘记,建议在函数的一开始就放置 defer 语句。


panic


刚刚有说到 defer 是崩溃后,仍然会被调用的语句,那程序在什么情况下会崩溃呢?


Go 的类型系统会在编译时捕获很多异常,但有些异常只能在运行时检查,如数组访问越界、空指针引用等。这些运行时异常会引起 painc 异常(程序直接崩溃退出)。然后在退出的时候调用当前 goroutine 的 defer 延迟调用语句。


有时候在程序运行缺乏必要的资源的时候应该手动触发宕机(比如配置文件解析出错、依赖某种独有库但该操作系统没有的时候)

defer fmt.Println("关闭文件句柄")
panic("人工创建的运行时异常")

报错如下


aHR0cHM6Ly9jb2RpbmczbWluLm9zcy1hY2NlbGVyYXRlLmFsaXl1bmNzLmNvbS8yMDIwLzA1LzI4L0JsNHl1eDIwNTEucG5n.png

panic recover

出现 panic 以后程序会终止运行,所以我们应该在测试阶段发现这些问题,然后进行规避,但是如果在程序中产生不可预料的异常(比如在线的web或者rpc服务一般框架层),即使出现问题(一般是遇到不可预料的异常数据)也不应该直接崩溃,应该打印异常日志,关闭资源,跳过异常数据部分,然后继续运行下去,不然线上容易出现大面积血崩。


然后再借助运维监控系统对日志的监控,发送告警给运维、开发人员,进行紧急修复。


语法如下:


func divisionIntRecover(a, b int) (ret int) {
  defer func() {
    if err := recover(); err != nil {
      // 打印异常,关闭资源,退出此函数
      fmt.Println(err)
      ret = -1
    }
  }()
  return a / b
}

调用

var res int
  datas := []struct {
    a int
    b int
  }{
    {2, 0},
    {2, 2},
  }
  for _, v := range datas {
    if res = divisionIntRecover(v.a, v.b); res == -1 {
      continue
    }
    fmt.Println(v.a, "/", v.b, "计算结果为:", res)
  }


输出结果


runtime error: integer divide by zero
2 / 2 计算结果为: 1



调用 panic 后,当前函数从调用点直接退出

recover 函数只有在 defer 代码块中才会有效果

recover 可以放在最外层函数,做统一异常处理。

这就是 go 异常处理,我所能想到和找到的全部内容了,希望你在工作中用的更顺手。


小熊虽然工作忙,文章没办法发的那么频繁,但是我有时间就写一点,反复校对,代码也反复测试最后放 github 上,这样文章的内容会更完整、更有逻辑、更少异常、对读者对自己都更负责。如果你发现了文章中出现问题,欢迎在评论区和我讨论,非常感谢!

相关文章
|
3月前
|
Go 开发者
Golang 中的异常处理机制详解
【8月更文挑战第31天】
52 0
|
6月前
|
Go 开发者
Golang深入浅出之-Go语言 defer、panic、recover:异常处理机制
Go语言中的`defer`、`panic`和`recover`提供了一套独特的异常处理方式。`defer`用于延迟函数调用,在返回前执行,常用于资源释放。它遵循后进先出原则。`panic`触发运行时错误,中断函数执行,直到遇到`recover`或程序结束。`recover`在`defer`中捕获`panic`,恢复程序执行。注意避免滥用`defer`影响性能,不应对可处理错误随意使用`panic`,且`recover`不能跨goroutine捕获panic。理解并恰当使用这些机制能提高代码健壮性和稳定性。
131 2
|
2月前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
111 4
Golang语言之管道channel快速入门篇
|
2月前
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
66 4
Golang语言文件操作快速入门篇
|
2月前
|
Go
Golang语言之gRPC程序设计示例
这篇文章是关于Golang语言使用gRPC进行程序设计的详细教程,涵盖了RPC协议的介绍、gRPC环境的搭建、Protocol Buffers的使用、gRPC服务的编写和通信示例。
101 3
Golang语言之gRPC程序设计示例
|
2月前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
86 4
|
2月前
|
Go
Golang语言错误处理机制
这篇文章是关于Golang语言错误处理机制的教程,介绍了使用defer结合recover捕获错误、基于errors.New自定义错误以及使用panic抛出自定义错误的方法。
46 3
|
2月前
|
Go 调度
Golang语言goroutine协程篇
这篇文章是关于Go语言goroutine协程的详细教程,涵盖了并发编程的常见术语、goroutine的创建和调度、使用sync.WaitGroup控制协程退出以及如何通过GOMAXPROCS设置程序并发时占用的CPU逻辑核心数。
50 4
Golang语言goroutine协程篇
|
2月前
|
Prometheus Cloud Native Go
Golang语言之Prometheus的日志模块使用案例
这篇文章是关于如何在Golang语言项目中使用Prometheus的日志模块的案例,包括源代码编写、编译和测试步骤。
53 3
Golang语言之Prometheus的日志模块使用案例
|
2月前
|
Go
Golang语言之函数(func)进阶篇
这篇文章是关于Golang语言中函数高级用法的教程,涵盖了初始化函数、匿名函数、闭包函数、高阶函数、defer关键字以及系统函数的使用和案例。
59 3
Golang语言之函数(func)进阶篇