用 panic-recover 做错误处理?会影响 Go 程序性能的

大家好,我是煎鱼。

Go 这一门编程语音的 if err != nil 错误处理机制一直饱受争议。既然争议那么多,想必有很多人想出各种奇思妙计,试图解决这个问题。

其中一个办法是使用 panic-recover 来替代 if err != nil 来做错误处理机制。

对于 Go 应用来讲,这种方式也是有一定的损耗。今天结合在网上看到的 The cost of Go’s panic and recover 和大家一起来看看。

Effective Java 例子

原作者 @Julien Cretel 是在阅读经典作品《Effective Java》中学习时,看到 Java 异常进行控制流的示例:

try {
  int i = 0;
  while (true)
    range[i++].climb();
} catch (ArrayIndexOutOfBoundsException e) {
}

该程序的变量 i 最终会递增到数组的长度,此时如果试图访问索引 i 处的数组,就会引发 ArrayIndexOutOfBoundsException 异常,该异常会被捕获并立即忽略。

使用 panic/recover 做控制流

很多 Java、PHP 等其他很多编程语言转过来的同学,第一次接触 Go 时,会试图像上述例子去寻找能做一定的 try-catch 错误处理机制的例子。

而在 Go 这一门编程语言中,能做到的类似模式的只有 panic-recover 的机制,因此可能产生了一些滥用的情况。

下述是模拟 Java try-catch 示例粗略翻译成 Go panic-recover 示例的演示,以此验证性能基准的测试。

1、主程序:

package main

type Mountain struct {
 climbed bool
}

func (m *Mountain) Climb() {
 m.climbed = true
}

func main() {
 mountains := make([]Mountain, 8)
 ClimbAllPanicRecover(mountains)
}

func ClimbAllPanicRecover(mountains []Mountain) {
deferfunc() {
recover()
 }()
for i := 0; ; i++ {
  mountains[i].Climb()
 }
}

func ClimbAll(mountains []Mountain) {
for i := range mountains {
  mountains[i].Climb()
 }
}

2、基准测试:

package main

import (
"fmt"
"testing"
)

var cases [][]Mountain

func init() {
for _, size := range []int{0, 1, 1e1, 1e2, 1e3, 1e4, 1e5} {
  s := make([]Mountain, size)
  cases = append(cases, s)
 }
}

func BenchmarkClimbAll(b *testing.B) {
 benchmark(b, "idiomatic", ClimbAll)
 benchmark(b, "panic-recover", ClimbAllPanicRecover)
}

func benchmark(b *testing.B, impl string, climbAll func([]Mountain)) {
for _, ns := range cases {
  f := func(b *testing.B) {
   b.ReportAllocs()
   for b.Loop() {
    climbAll(ns)
   }
  }
  desc := fmt.Sprintf("impl=%s/size=%d", impl, len(ns))
  b.Run(desc, f)
 }
}

测试结果

从测试结果可以得知,即使是在处理小规模输入切片时,ClimbAllPanicRecover 的性能也明显劣于 ClimbAll 的实现

如下测试报告:

➜  demo1 benchstat -col '/impl@(idiomatic panic-recover)' results.txt
goos: darwin
goarch: arm64
pkg: example.com/greet
cpu: Apple M3 Pro
                        │  idiomatic   │             panic-recover              │
                        │    sec/op    │    sec/op     vs base                  │
ClimbAll/size=0-11        1.046n ± 32%   94.320n ± 0%  +8921.52% (p=0.000 n=10)
ClimbAll/size=1-11        1.612n ± 20%   94.400n ± 0%  +5754.26% (p=0.000 n=10)
ClimbAll/size=10-11       4.202n ±  6%   97.565n ± 0%  +2221.87% (p=0.000 n=10)
ClimbAll/size=100-11      26.69n ±  0%   120.20n ± 0%   +350.36% (p=0.000 n=10)
ClimbAll/size=1000-11     255.0n ±  0%    354.8n ± 2%    +39.14% (p=0.000 n=10)
ClimbAll/size=10000-11    2.479µ ±  0%    2.595µ ± 0%     +4.68% (p=0.000 n=10)
ClimbAll/size=100000-11   24.72µ ±  0%    24.87µ ± 0%     +0.61% (p=0.001 n=10)
geomean                   60.46n          422.2n        +598.25%

                        │  idiomatic  │        panic-recover         │
                        │    B/op     │    B/op     vs base          │
ClimbAll/size=0-11        0.00 ± 0%     24.00 ± 0%  ? (p=0.000 n=10)
ClimbAll/size=1-11        0.00 ± 0%     24.00 ± 0%  ? (p=0.000 n=10)
ClimbAll/size=10-11       0.00 ± 0%     24.00 ± 0%  ? (p=0.000 n=10)
ClimbAll/size=100-11      0.00 ± 0%     24.00 ± 0%  ? (p=0.000 n=10)
ClimbAll/size=1000-11     0.00 ± 0%     24.00 ± 0%  ? (p=0.000 n=10)
ClimbAll/size=10000-11    0.00 ± 0%     24.00 ± 0%  ? (p=0.000 n=10)
ClimbAll/size=100000-11   0.00 ± 0%     24.00 ± 0%  ? (p=0.000 n=10)
geomean                             ¹   24.00       ?
¹ summaries must be >0 to compute geomean

                        │  idiomatic   │        panic-recover         │
                        │  allocs/op   │ allocs/op   vs base          │
ClimbAll/size=0-11        0.000 ± 0%     1.000 ± 0%  ? (p=0.000 n=10)
ClimbAll/size=1-11        0.000 ± 0%     1.000 ± 0%  ? (p=0.000 n=10)
ClimbAll/size=10-11       0.000 ± 0%     1.000 ± 0%  ? (p=0.000 n=10)
ClimbAll/size=100-11      0.000 ± 0%     1.000 ± 0%  ? (p=0.000 n=10)
ClimbAll/size=1000-11     0.000 ± 0%     1.000 ± 0%  ? (p=0.000 n=10)
ClimbAll/size=10000-11    0.000 ± 0%     1.000 ± 0%  ? (p=0.000 n=10)
ClimbAll/size=100000-11   0.000 ± 0%     1.000 ± 0%  ? (p=0.000 n=10)
geomean                              ¹   1.000       ?
¹ summaries must be >0 to compute geomean

具体差异体现在:

  • 执行效率差距:在小数据集场景下,panic/recover 的操作成本占据主导地位,导致 ClimbAllPanicRecover 运行迟缓。在 64 位系统中,每次调用会产生 24 字节的堆内存分配(推测源于运行时触发的 runtime.boundsError 边界错误)。

  • 内存管理差异:标准实现 ClimbAll 完全避免了内存分配,因此不会对垃圾回收机制造成额外压力。

  • 性能趋势变化:随着输入切片长度的增加,两种实现的性能差异逐渐缩小。这是因为 panic-recover 的固定开销被更大的计算量所稀释,不再成为主要性能瓶颈。

为什么 Go 不支持 try-catch

Go 官方早在《Error Handling — Problem Overview[1]》提案早已明确提过,Go 官方在设计上会有意识地选择使用显式错误结果和显式错误检查

结合《language: Go 2: error handling meta issue[2]》可得知,要拒绝 try-catch 关键字的主要原因是:

  • 会涉及到额外的流程控制,因为使用 try 的复杂表达式,会导致函数意外返回。

  • 在表达式层面上没有流程控制结构,只有 panic 关键字,它不只是从一个函数返回。

说白了,就是设计理念不合,加之实现上也不大合理。在以往的多轮讨论中早已被 Go 团队拒绝了。

反之 Go 团队倒是一遍遍在回答这个问题,已经不大耐烦了,直接都整理了 issues 版的 FAQ 了。

总结

对于 Go 这一门编程语言来讲,if err != nil 是其提供的最基本的错误处理机制。

虽然很多开发同学略感不适,但官方依然是建议在其基础上做设计模式的设计和调整。这是较为推荐的。

在很多妙计中,也有像本文使用 panic-recover 的方式。但通过实际测试来讲,是会明确影响性能的。

官方也明确过不推荐该类错误处理机制的方式。

见仁见智了。

参考资料

[1] 

Error Handling — Problem Overview: https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md

[2] 

language: Go 2: error handling meta issue: https://github.com/golang/go/issues/40432

关注和加煎鱼微信,

一手消息和知识,拉你进技术交流群👇

你好,我是煎鱼,出版过 Go 畅销书《Go 语言编程之旅》,再到获得 GOP(Go 领域最有观点专家)荣誉,点击蓝字查看我的出书之路

日常分享高质量文章,输出 Go 面试、工作经验、架构设计,加微信拉读者交流群,和大家交流!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值