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 上,这样文章的内容会更完整、更有逻辑、更少异常、对读者对自己都更负责。如果你发现了文章中出现问题,欢迎在评论区和我讨论,非常感谢!

相关文章
|
6天前
|
Go 开发者
Golang深入浅出之-Go语言 defer、panic、recover:异常处理机制
Go语言中的`defer`、`panic`和`recover`提供了一套独特的异常处理方式。`defer`用于延迟函数调用,在返回前执行,常用于资源释放。它遵循后进先出原则。`panic`触发运行时错误,中断函数执行,直到遇到`recover`或程序结束。`recover`在`defer`中捕获`panic`,恢复程序执行。注意避免滥用`defer`影响性能,不应对可处理错误随意使用`panic`,且`recover`不能跨goroutine捕获panic。理解并恰当使用这些机制能提高代码健壮性和稳定性。
29 2
|
6天前
|
监控 算法 Go
Golang深入浅出之-Go语言中的服务熔断、降级与限流策略
【5月更文挑战第4天】本文探讨了分布式系统中保障稳定性的重要策略:服务熔断、降级和限流。服务熔断通过快速失败和暂停故障服务调用来保护系统;服务降级在压力大时提供有限功能以保持整体可用性;限流控制访问频率,防止过载。文中列举了常见问题、解决方案,并提供了Go语言实现示例。合理应用这些策略能增强系统韧性和可用性。
56 0
|
6天前
|
分布式计算 Java Go
Golang深入浅出之-Go语言中的分布式计算框架Apache Beam
【5月更文挑战第6天】Apache Beam是一个统一的编程模型,适用于批处理和流处理,主要支持Java和Python,但也提供实验性的Go SDK。Go SDK的基本概念包括`PTransform`、`PCollection`和`Pipeline`。在使用中,需注意类型转换、窗口和触发器配置、资源管理和错误处理。尽管Go SDK文档有限,生态系统尚不成熟,且性能可能不高,但它仍为分布式计算提供了可移植的解决方案。通过理解和掌握Beam模型,开发者能编写高效的数据处理程序。
142 1
|
6天前
|
缓存 测试技术 持续交付
Golang深入浅出之-Go语言中的持续集成与持续部署(CI/CD)
【5月更文挑战第5天】本文介绍了Go语言项目中的CI/CD实践,包括持续集成与持续部署的基础知识,常见问题及解决策略。测试覆盖不足、版本不一致和构建时间过长是主要问题,可通过全面测试、统一依赖管理和利用缓存优化。文中还提供了使用GitHub Actions进行自动化测试和部署的示例,强调了持续优化CI/CD流程以适应项目需求的重要性。
57 1
|
6天前
|
Kubernetes Cloud Native Go
Golang深入浅出之-Go语言中的云原生开发:Kubernetes与Docker
【5月更文挑战第5天】本文探讨了Go语言在云原生开发中的应用,特别是在Kubernetes和Docker中的使用。Docker利用Go语言的性能和跨平台能力编写Dockerfile和构建镜像。Kubernetes,主要由Go语言编写,提供了方便的客户端库与集群交互。文章列举了Dockerfile编写、Kubernetes资源定义和服务发现的常见问题及解决方案,并给出了Go语言构建Docker镜像和与Kubernetes交互的代码示例。通过掌握这些技巧,开发者能更高效地进行云原生应用开发。
60 1
|
6天前
|
负载均衡 监控 Go
Golang深入浅出之-Go语言中的服务网格(Service Mesh)原理与应用
【5月更文挑战第5天】服务网格是处理服务间通信的基础设施层,常由数据平面(代理,如Envoy)和控制平面(管理配置)组成。本文讨论了服务发现、负载均衡和追踪等常见问题及其解决方案,并展示了使用Go语言实现Envoy sidecar配置的例子,强调Go语言在构建服务网格中的优势。服务网格能提升微服务的管理和可观测性,正确应对问题能构建更健壮的分布式系统。
31 1
|
6天前
|
消息中间件 Go API
Golang深入浅出之-Go语言中的微服务架构设计与实践
【5月更文挑战第4天】本文探讨了Go语言在微服务架构中的应用,强调了单一职责、标准化API、服务自治和容错设计等原则。同时,指出了过度拆分、服务通信复杂性、数据一致性和部署复杂性等常见问题,并提出了DDD拆分、使用成熟框架、事件驱动和配置管理与CI/CD的解决方案。文中还提供了使用Gin构建HTTP服务和gRPC进行服务间通信的示例。
32 0
|
6天前
|
Prometheus 监控 Cloud Native
Golang深入浅出之-Go语言中的分布式追踪与监控系统集成
【5月更文挑战第4天】本文探讨了Go语言中分布式追踪与监控的重要性,包括追踪的三个核心组件和监控系统集成。常见问题有追踪数据丢失、性能开销和监控指标不当。解决策略涉及使用OpenTracing或OpenTelemetry协议、采样策略以及聚焦关键指标。文中提供了OpenTelemetry和Prometheus的Go代码示例,强调全面可观测性对微服务架构的意义,并提示选择合适工具和策略以确保系统稳定高效。
146 5
|
6天前
|
负载均衡 算法 Go
Golang深入浅出之-Go语言中的服务注册与发现机制
【5月更文挑战第4天】本文探讨了Go语言中服务注册与发现的关键原理和实践,包括服务注册、心跳机制、一致性问题和负载均衡策略。示例代码演示了使用Consul进行服务注册和客户端发现服务的实现。在实际应用中,需要解决心跳失效、注册信息一致性和服务负载均衡等问题,以确保微服务架构的稳定性和效率。
22 3
|
6天前
|
前端开发 Go
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
【5月更文挑战第3天】Go语言通过goroutines和channels实现异步编程,虽无内置Future/Promise,但可借助其特性模拟。本文探讨了如何使用channel实现Future模式,提供了异步获取URL内容长度的示例,并警示了Channel泄漏、错误处理和并发控制等常见问题。为避免这些问题,建议显式关闭channel、使用context.Context、并发控制机制及有效传播错误。理解并应用这些技巧能提升Go语言异步编程的效率和健壮性。
30 5
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式