专栏名称: GoCN
最具规模和生命力的 Go 开发者社区
目录
相关文章推荐
51好读  ›  专栏  ›  GoCN

Go 错误处理指北:Defer、Panic、Recover 三剑客

GoCN  · 公众号  ·  · 2024-10-18 12:32

正文

Go 语言中的错误处理不仅仅只有 if err != nil defer panic recover 这三个相对来说不不如 if err != nil 有名气的控制流语句,也与错误处理息息相关。本文就来讲解下这三者在 Go 语言中的应用。

Defer

defer 是一个 Go 中的关键字,通常用于简化执行各种清理操作的函数。 defer 后跟一个函数(或方法)调用,该函数(或方法)的执行会被推迟到外层函数返回的那一刻,即函数(或方法)要么遇到了 return ,要么遇到了 panic

语法

defer 功能使用语法如下:

defer Expression

其中 Expression 必须是函数或方法的调用。

defer 使用示例如下:

func f() {
 defer fmt.Println("deferred in f")
 fmt.Println("calling f")
}

func main() {
 f()
}

执行示例代码,得到输出如下:

$ go run main.go            
calling f
deferred in f

根据输出可以发现,被 defer 修饰的 fmt.Println("deferred in f") 调用并没有立即执行,而是先执行了 fmt.Println("calling f") ,然后才会执行 defer 修饰的函数调用语句。

执行顺序

一个函数中可以写多个 defer 语句:

func f() {
 defer fmt.Println("deferred in f 1")
 defer fmt.Println("deferred in f 2")
 defer fmt.Println("deferred in f 3")
 fmt.Println("calling f")
}

执行示例代码,得到输出如下:

$ go run main.go
calling f
deferred in f 3
deferred in f 2
deferred in f 1

defer 修饰的函数调用,在外层函数返回后按后进先出顺序执行,即 Last In First Out( LIFO )。

不仅如此, defer 可以写在任意位置,并且还可以嵌套,即在被 defer 修饰的函数中再次使用 defer

示例如下:

func




    
 f() {
 fmt.Println("1")

 defer func() {
  fmt.Println("2")
  defer fmt.Println("3")
  fmt.Println("4")
 }()

 fmt.Println("5")

 defer fmt.Println("6")

 fmt.Println("7")
}

执行示例代码,得到输出如下:

$ go run main.go
1
5
7
6
2
4
3

这个输出结果符合你的预期吗?

先看外层函数 f 的代码逻辑,有两个 defer 语句,无论位置在哪, defer 都会使函数调用延迟执行,所以先输出了 1 5 7

然后根据 LIFO 原则,先执行第 2 个 defer 语句所修饰的函数调用,所以输出 6

接着执行第 1 个 defer 语句所修饰的函数调用,其内部同样会按顺序执行没有被 defer 语句修饰的代码,所以先输出 2 4 ,然后执行 defer 语句所修饰的函数调用,输出 3

读写函数返回值

有时候,我们可以使用 defer 语句来读取或修改函数的返回值。

有如下示例,试图在 defer 中修改函数的返回值:

func f() int {
 r := 2
 defer func() {
  fmt.Println("r:", r)
  r *= 3
 }()
 return r
}

func main() {
 fmt.Println(f())
}

执行示例代码,得到输出如下:

$ go run main.go
r: 2
2

看来没有成功。

函数使用具名返回值再来看看:

func f() (r int) {
 r = 2
 defer func() {
  fmt.Println("r:", r)
  r *= 3
 }()
 return r
}

执行示例代码,得到输出如下:

$ go run main.go
r: 2
6

这次成功了。

如果改成这样呢:

func f() (r int) {
 defer func() {
  fmt.Println("r:", r)
  r *= 3
 }()
 return 2
}

现在,返回值直接写成了 2 ,而非变量 r

执行示例代码,得到输出如下:






    
$ go run main.go
r: 2
6

这次返回值依然修改成功了。

前面几个示例,其实都算使用了闭包。因为被 defer 修饰的函数内部都引用了外部变量 r

我们再看一个不使用闭包的示例:

func f() (r int) {
 defer func(r int) {
  fmt.Println("r:", r)
  r *= 3
 }(r)
 return 2
}

执行示例代码,得到输出如下:

$ go run main.go
r: 0
2

这次返回值没有修改成功,并且被 defer 修饰的函数内部读到的 r 值为 0 ,并不是前面示例中的 2

也就是说,实际上虽然被 defer 修饰的函数调用会延迟执行,但是我们 传递给函数的参数,会被立即求值

我们接着看下面这个示例:

func f() (r int) {
 x := 2
 defer func() {
  fmt.Println("r:", r)
  fmt.Println("x:", x)
  x *= 3
 }()
 return x
}

执行示例代码,得到输出如下:

$ go run main.go
r: 2
x: 2
2

当代码执行到 return x 时, r 值也会被赋值为 2 ,这没什么好解释的。

然后在 defer 所修饰的函数内部,我们只修改了 x 变量,这对返回结果 r 没有影响。

把函数返回值类型改成指针试试呢:

func f() (r *int) {
 x := 2
 defer func() {
  fmt.Println("r:", *r)
  fmt.Println("x:", x)
  x *= 3
 }()
 return &x
}

func main() {
 fmt.Println(*f())
}

执行示例代码,得到输出如下:

$ go run main.go
r: 2
x: 2
6

这次返回值又成功被修改了。

看到这里,你是不是对 defer 语句的效果有点懵,没关系,我们再来梳理下 defer 执行时机。

defer 语句的行为其实是可预测的,我们可以 记住这三条规则

  1. 在计算 defer 语句时,将立即计算被 defer 修饰的函数参数。
  2. defer 修饰的函数,在外层函数返回后按后进先出的顺序( LIFO )执行。
  3. 延迟函数可以读取或赋值给外层函数的具名返回值。

现在,你再翻回去重新看看上面的几个示例程序,是不是都能理解了呢?

释放资源

defer 还常被用来释放资源,比如关闭文件对象。

这里有个示例程序,可以将一个文件内容复制到另外一个文件中:

func CopyFile(dstName, srcName string) (written int64, err error) {
 src, err := os.Open(srcName)
 if err != nil {
  return
 }

 dst, err := os.Create(dstName)
 if err != nil {
  return
 }

 written, err = io.Copy(dst, src)
 dst.Close()
 src.Close()
 return
}

不过这个程序存在 bug ,如果 os.Create 执行失败,函数返回后 src 并没有被关闭。

而这种场景刚好适用 defer ,示例如下:

func CopyFile(dstName, srcName string) (written int64, err error) {
 src, err := os.Open(srcName)
 if err != nil {
  return
 }
 defer src.Close()

 dst, err := os.Create(dstName)
 if err != nil {
  return
 }
 defer dst.Close()

 return io.Copy(dst, src)
}

此时如果 os.Create 执行失败,函数返回后 defer src.Close() 将会被执行,文件资源得以释放。

切记,不要在 if err != nil 之前调用 defer 释放资源,这很可能会触发 panic

src, err := os.Open(srcName)
defer src.Close()
if err != nil {
 return
}

因为,如果调用 os.Open 报错, src 值将为 nil ,而 nil.Close() 会触发 panic ,导致程序意外终止而退出。

此外,在处理释放资源的情况,你可能写出如下代码:

type fakeFile struct {
 name string
}

func (f *fakeFile) Close() error {
 fmt.Println("close:", f)
 return nil
}

// 错误写法:f 变量的值最终是 f2,所以 f2 会被关闭两次,f1 没关闭
func processFile() {
 f := fakeFile{name: "f1"}
 defer f.Close()

 f = fakeFile{name: "f2"}
 defer f.Close()

 fmt.Println("calling processFile")
 return
}

func main() {
 processFile()
}

执行示例代码,得到输出如下:

$ go run main.go
calling processFile
close: &{f2}
close: &{f2}

可以发现,在函数 processFile 中,因为 f 被重复赋值,导致 f 变量的值最终是 f2 ,所以 f2 会被关闭两次, f1 并没有被关闭。

还记得我们前面讲过的规则吗: 在计算 defer 语句时,将立即计算被 defer 修饰的函数参数

所以,我们可以在 defer 处让变量 f 先被计算出来:

func processFile1() {
 f := fakeFile{name: "f1"}
 defer func(f fakeFile) {
  f.Close()
 }(f)

 f = fakeFile{name: "f2"}
 defer func(f fakeFile) {
  f.Close()
 }(f)

 fmt.Println("calling processFile1")
 return
}

这样就解决了问题。

当然,更简单的方式是我们压根就不要使用同一个变量来表示不同的文件对象:

func processFile2() {
 f1 := fakeFile{name: "f1"}
 defer f1.Close()

 f2 := fakeFile{name: "f2"}
 defer f2.Close()

 fmt.Println("calling processFile2")
 return
}

不过,有时候在在 for 循环中,就是会出现 f 被重复赋值的情况,在 for 循环中使用 defer 语句,我们可能还会踩到类似的坑,所以你一定要小心。

WithClose

文章读到这里,想必你也看出来了, defer 功能正是对标了 Python 中的 try...finally 或者 with 语句的效果。

Python 的 with 语法非常优雅,如何使用 defer 实现近似效果呢?

你可以在我的另一篇文章 《在 Go 中如何实现类似 Python 中的 with 上下文管理器》 中找到答案。

篇幅所限,我就不在这里再废话连篇的讲一遍了。

如果你想用下面这种单独的代码块作用域来实现:

func f() {
 {
  // defer 函数一定是在函数退出时才会执行,而不是代码块退出时执行
  defer fmt.Println("defer done")
  fmt.Println("code block")
 }

 fmt.Println("calling f")
}

很遗憾的告诉你,这并不能达到想要的效果,你可以思考后再点击我的另一篇文章来对比下你我二人的实现是否相同。

结构体方法是否使用指针接收者

当结构体方法使用指针作为接收者时,也要小心。

示例如下:

type User struct {
 name string
}

func (u User) Name() {
 fmt.Println("Name:", u.name)
}

func (u *User) PointName() {
 fmt.Println("PointName:", u.name)
}

func printUser() {
 u := User{name: "user1"}

 defer u.Name()
 defer u.PointName()

 u.name = "user2"
}

func main() {
 printUser()
}

执行示例代码,得到输出如下:

$ go run main.go
PointName: user2
Name: user1

User.Name 方法接收者为结构体,在 defer 中被调用,最终输出结果为初始 name user1

User.PointName 方法接收者为指针,在 defer 中被调用,最终输出结果为修改后的 name user2

可见, defer 处不仅会计算函数参数,其实它会对其后面的表达式求值,并计算出最终将要执行的函数或方法。

也就是说,代码执行到 defer u.Name() 时,变量 u 的值就已经计算出来了,相当于“复制”了一个新的变量,后面再通过 u.name = "user2" 修改其属性,二者已经不是同一个变量了。

而代码执行到 defer u.PointName() 时,其实这里的 u 是指针类型,即使“复制”了一个新的变量,其内部保存的指针依然相等,所以可以被修改。

如果将代码修改成如下这样,执行结果又会怎样呢?

func printUser() {
 u := User{name: "user1"}

 defer func() {
  u.Name()
  u.PointName()
 }()

 u.name = "user2"
}

这个就交给你自己去实验了。

当 defer 遇到 os.Exit

defer 遇到 os.Exit 时会怎样呢?

func f() {
 defer fmt.Println("deferred in f")
 fmt.Println("calling f")
 os.Exit(0)
}

func main() {
 f()
}

执行示例代码,得到输出如下:

$ go run main.go
calling f

可见,当遇到 os.Exit 时,程序直接退出, defer 并不会被执行,这一点平时开发过程中要格外注意。

一个过时的面试题

前几年,有一个考察 defer 的面试题经常在网上出现:

func f() {
 for i := 0; i 3; i++ {
  defer func() {
   fmt.Println(i)
  }()
 }
}

问执行 f 以后,输出什么?

既然会成为面试题,执行结果就肯定有猫腻。

如果你使用 Go 1.22 以前的版本执行示例代码,将得到如下结果:

$ go run main.go
3
3
3

而如果你使用 Go 1.22 及以后的版本执行示例代码,将得到如下结果:

$ go run main.go
2
1
0

这是由于,在 Go 1.22 以前,由 for 循环声明的变量只会被 创建一次,并在每次迭代时更新 。在 Go 1.22 中,循环的每次迭代都会 创建新的变量,以避免意外的共享错误

这在 Go 1.22 Release Notes 中有说明。

在旧版本的 Go 中要修复这个问题,只需要这样写即可:

func f() {
 for i := 0; i 3; i++ {
  defer fmt.Println(i)
 }
}

直接把 defer 放在外面,不要构成闭包。

又或者为 defer 函数增加参数:

func f() {
 for i := 0; i 3; i++ {
  defer func(i int) {
   fmt.Println(i)
  }(i)
 }
}

总之,解决方案就是不要出现闭包。

不要出现 defer nil 的情况

前文说过, defer 后面支持函数或方法的调用。

但是,如果计算 defer 后的表达式出现 nil 的情况,则会触发 panic

func deferNil() {
 var f func()
 defer f()
 fmt.Println("calling deferNil")
}

func main() {
 deferNil()
}

执行示例代码,得到输出如下:

calling deferNil
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x10264f88c]

goroutine 1 [running]:
main.deferNil()
        /go/blog-go-example/error/defer-panic-recover/defer/main.go:363 +0x6c
main.main()
        /go/blog-go-example/error/defer-panic-recover/defer/main.go:384 +0x1c
exit status 2

因为 nil 不可被调用。

至于到底什么是 panic ,咱们往下看。

Panic

在 Go 中, error 表示一个错误,错误通常会返给调用方,交由调用方来决定如何处理。而 panic 则表示一个无法挽回的异常, panic 会直接终止当前执行的控制流。

panic 是一个内置函数,它会停止程序的正常控制流并输出 panic 相关信息。

有两种方式可以触发 panic ,一种是非法操作导致运行时错误,比如访问数组索引越界,此时会触发运行时 panic 。另一种是主动调用 panic 函数。

当在函数 F 中调用了 panic 后,程序执行流程如下:

函数 F 调用 panic 时, F 的执行会被停止,接下来会执行 F 中调用 panic 之前的所有 defer 函数,然后 F 返回给调用者。

接着,对于 F 的调用方 G 的行为也类似于对 panic 的调用。

该过程继续向上返回,直到当前 goroutine 中的所有函数都返回,此时程序崩溃。

最后,你将在执行 Go 程序的控制台看到程序执行异常的堆栈信息。

使用

panic 使用示例如下:

func f() {
 defer fmt.Println("defer 1")
 fmt.Println(1)
 panic("woah")
 defer fmt.Println("defer 2")
 fmt.Println(2)
}

func main() {
 f()
}

执行示例代码,得到输出如下:

$ go run main.go
1
defer 1
panic: woah

goroutine 1 [running]:
main.f()
        /go/blog-go-example/error/defer-panic-recover/panic/main.go:10 +0xa0
main.main()
        /go/blog-go-example/error/defer-panic-recover/panic/main.go:29 +0x1c
exit status 2

可以发现, panic 会输出异常堆栈信息。

并且 1 defer 1 都被输出了,而 2 defer 2 没有输出,说明 panic 调用之后的代码不会执行,但它不影响 panic 之前 defer 函数的执行。

此外,如果你足够细心,还可以发现 panic 后程序的退出码为 2

子 Goroutine 中 panic

如果在子 goroutine 中发生 panic ,也会导致主 goroutine 立即退出:

func g() {
 fmt.Println("calling g")
 // 子 goroutine 中发生 panic,主 goroutine 也会退出
 go f(0)
 fmt.Println("called g")
}

func f(i int) {
 fmt.Println("panicking!")
 panic(fmt.Sprintf("i=%v", i))
 fmt.Println("printing in f", i) // 不会被执行
}

func main() {
 g()
 time.Sleep(10 * time.Second)
}

执行示例代码,程序并不会等待 10s 后才退出,而是立即 panic 并退出,得到输出如下:

$ go run main.go
calling g
called g
panicking!
panic: i=0

goroutine 3 [running]:
main.f(0x0)
        /go/blog-go-example/error/defer-panic-recover/panic/main.go:25 +0xa0
created by main.g in goroutine 1
        /go/blog-go-example/error/defer-panic-recover/panic/main.go:19 +0x5c
exit status 2

panic 和 os.Exit

虽然 panic os.Exit 都能使程序终止并退出,但它们有着显著的区别,尤其在触发时的行为和对程序流程的影响上。

panic 用于在程序中出现异常情况时引发一个运行时错误,通常会导致程序崩溃(除非被 recover 恢复)。当触发 panic 时, defer 语句仍然会执行。 panic 还会打印详细的堆栈信息,显示引发错误的调用链。 panic 退出状态码固定为 2

os.Exit 会立即终止程序,并返回指定的状态码给操作系统。当执行 os.Exit 时, defer 语句不会执行。 os.Exit 直接通知操作系统退出程序,它不会返回给调用者,也不会引发运行时堆栈追踪,所以也就不会打印堆栈信息。 os.Exit 可以设置程序退出状态码。

因为 panic 比较暴力,所以一般只建议在 main 函数中使用,比如应用的数据库初始化失败后直接 panic ,因为程序无法连接数据库,程序继续执行意义不大。而普通函数中推荐尽量返回 error 而不是直接 panic

不过 panic 也不是没有挽救的余地, recover 就是来恢复 panic 的。

Recover

recover 也是一个函数,用来从 panic 所导致的程序崩溃中恢复执行。

使用

recover 使用示例如下:

func f() {
 defer func() {
  recover()
 }()
 
 defer fmt.Println("defer 1")
 fmt.Println(1)
 panic("woah")
 defer fmt.Println("defer 2")
 fmt.Println(2)
}

func main() {
 f()
}

执行示例代码,得到输出如下:

$ go run main.go
1
defer 1

recover() 的调用捕获了 panic 触发的异常,并且程序正常退出。

recover 函数只在 defer 语句的上下文中才有效,直接调用的话,只会返回 nil

如下两种方式都是错误的用法:

recover




    
()
defer recover()

可见, recover 必须与 defer 一同使用,来从 panic 中恢复程序。不过 panic 之后的代码依旧不会执行, recover() 调用后只会执行 defer 语句中的剩余代码。

下面这个例子将会捕获到 panic ,并且输出 panic 信息:

func f() {
 defer func() {
  if r := recover(); r != nil {
   fmt.Println("recover:", r)
  }
 }()
 panic("woah")
}

执行示例代码,得到输出如下:

$ go run main.go
recover: woah

可以发现, recover 函数的返回值,正是 panic 函数的参数。

不要在 defer 中出现 panic

为了避免不必要的麻烦, defer 函数中最好不要有能够引起 panic 的代码。

正常来说, defer 用来释放资源,不会出现大量代码。如果 defer 函数中逻辑过多,则需要斟酌下有没有更优解。

如下示例将输出什么?

func f() {
 defer func() {
  if r := recover(); r != nil {
   fmt.Println("recover:", r)
  }
 }()

 defer func() {
  panic("woah 1")
 }()
 panic("woah 2")
}

执行示例代码,得到输出如下:

$ go run main.go
recover: woah 1

看来, defer 中的 panic("woah 1") 覆盖了程序正常控制流中的 panic("woah 2")

如果我们将代码顺序稍作修改:

func f() {
 defer func() {
  panic("woah 1")
 }()

 defer func() {
  if r := recover(); r != nil {
   fmt.Println("recover:", r)
  }
 }()

 panic("woah 2")
}

执行示例代码,得到输出如下:

$ go run main.go
recover: woah 2
panic: woah 1

goroutine 1 [running]:
main.f.func1()
        /go/blog-go-example/error/defer-panic-recover/recover/main.go:68 +0x2c
main.f()
        /go/blog-go-example/error/defer-panic-recover/recover/main.go:77 +0x68
main.main()
        /go/blog-go-example/error/defer-panic-recover/recover/main.go:142 +0x1c
exit status 2

看来,调用 recover defer 应该放在函数的入口处,成为第一个 defer

recover 只能捕获当前 Goroutine 中的 panic

需要额外注意的一点是, recover 只会捕获当前 goroutine 所触发的 panic

示例如下:

func f() {
 defer func() {
  if r := recover(); r != nil {
   fmt.Println("recover:", r)
  }
 }()

 go func() {
  panic("woah")
 }()
 time.Sleep(1 * time.Second)
}

执行示例代码,得到输出如下:

$ go run main.go
panic: woah

goroutine 18 [running]:
main.f.func2()
        /go/blog-go-example/error/defer-panic-recover/recover/main.go:91 +0x2c
created by main.f in goroutine 1
        /go/blog-go-example/error/defer-panic-recover/recover/main.go:90 +0x40
exit status 2

goroutine 中触发的 panic 并没有被 recover 捕获。

所以,如果你认为代码中需要捕获 panic 时,就需要在每个 goroutine 中都执行 recover

将 panic 转换成 error 返回

有时候,我们可能需要将 panic 转换成 error 并返回,防止当前函数调用他人提供的不可控代码时出现意外的 panic

func g(i int) (number int, err error) {
 defer func() {
  if r := recover(); r != nil {
   var ok bool
   err, ok = r.(error)
   if !ok {
    err = fmt.Errorf("f returns err: %v", r)
   }
  }
 }()

 number, err = f(i)
 return number, err
}

func f(i int) (int, error) {
 if i == 0 {
  panic("i=0")
 }
 return i * i, nil
}

func main() {
 fmt.Println(g(1))
 fmt.Println(g(0))
}

执行示例代码,得到输出如下:

$ go run main.go

0 f returns err: i=0

net/http 使用 recover 优雅处理 panic

我们在开发 HTTP Server 程序时,即使某个请求遇到了 panic 也不应该使整个程序退出。所以,就需要使用 recover 来处理 panic

来看一个使用 net/http 创建的 HTTP Server 程序示例:

package main

import (
 "fmt"
 "log"
 "net/http"
 "os"
)

func handler(w http.ResponseWriter, r *http.Request) {
 if r.URL.Path == "/panic" {
  panic("url is error")
 }
 // 打印请求的路径
 fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
}

func main() {
 // 创建一个日志实例,写到标准输出
 logger := log.New(os.Stdout, "http: ", log.LstdFlags)

 // 自定义 HTTP Server
 server := &http.Server{
  Addr:     ":8080",
  ErrorLog: logger, // 设置日志记录器
 }

 // 注册处理函数
 http.HandleFunc("/" , handler)

 // 启动服务器
 fmt.Println("Starting server on :8080")
 if err := server.ListenAndServe(); err != nil {
  logger.Println("Server failed to start:", err)
 }
}

启动示例,程序会阻塞在这里等待请求进来:

$ go run main.go
Starting server on :8080

使用 curl 命令分别对 HTTP Server 发送三次请求:

$ curl localhost:8080
Hello, you've requested: /
$ curl localhost:8080/panic
curl: (52) Empty reply from server
$ curl localhost:8080/hello
Hello, you'
ve requested: /hello

可以发现,在请求 /panic 路由时,HTTP Server 触发了 panic 并返回了空内容,然后第三个请求依然能够得到正确的响应。

可见 HTTP Server 并没有退出。

现在回去看一下执行 HTTP Server 的控制台日志:

Starting server on :8080
http: 2024/10/13 23:08:28 http: panic serving [::1]:50547: url is error
goroutine 34 [running]:
net/http.(*conn).serve.func1()
        /go/pkg/mod/golang.org/[email protected]/src/net/http/server.go:1947 +0xb0
panic({0x10114c000?, 0x1011a4ba8?})
        /go/pkg/mod/golang.org/[email protected]/src/runtime/panic.go:785 +0x124
main.handler({0x1011a8178?, 0x140001440e0?}, 0x1400010bb28?)
        /workspace/projects/go/blog-go-example/error/defer-panic-recover/recover/http/main.go:12 +0x130
net/http.HandlerFunc.ServeHTTP(0x101348320?, {0x1011a8178?, 0x140001440e0?}, 0x1010999e4?)
        /go/pkg/mod/golang.org/[email protected]/src/net/http/server.go:2220 +0x38
net/http.(*ServeMux).ServeHTTP(0x0?, {0x1011a8178, 0x140001440e0}, 0x14000154140)
        /go/pkg/mod/golang.org/[email protected]/src/net/http/server.go:2747 +0x1b4
net/http.serverHandler.ServeHTTP({0x1400011ade0?}, {0x1011a8178?, 0x140001440e0?}, 0x6?)
        /go/pkg/mod/golang.org/[email protected]/src/net/http/server.go:3210 +0xbc
net/http.(*conn).serve(0x140000a4120, {0x1011a8678, 0x1400011acf0})
        /go/pkg/mod/golang.org/[email protected]/src/net/http/server.go:2092 +0x4fc
created by net/http.(*Server).Serve in goroutine 1
        /go/pkg/mod/golang.org/[email protected]/src/net/http/server.go:3360 +0x3dc

panic 信息 url is error 被输出了,并且打印了堆栈信息。

不过这 HTTP Server 依然在运行,并能提供服务。

这其实就是在 net/http 中使用了 recover 来处理 panic

我们可以看下 http.Server.Serve 的源码:

func (srv *Server) Serve(l net.Listener) error {
 ...

 ctx := context.WithValue(baseCtx, ServerContextKey, srv)
 for {
  rw, err := l.Accept()
  if err != nil {
   ...
   return err
  }
  connCtx := ctx
  if cc := srv.ConnContext; cc != nil {
   connCtx = cc(connCtx, rw)
   if connCtx == nil {
    panic("ConnContext returned nil")
   }
  }
  tempDelay = 0
  c := srv.newConn(rw)
  c.setState(c.rwc, StateNew, runHooks) // before Serve can return
  go c.serve(connCtx)
 }
}

可以发现,在 for 循环中,每接收到一个请求都会交给 go c.serve(connCtx) 开启一个新的 goroutine 来处理。

那么在 serve 方法中就一定会有 recover 语句:

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
 if ra := c.rwc.RemoteAddr(); ra != nil {
  c.remoteAddr = ra.String()
 }
 ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
 var inFlightResponse *response
 defer func() {
  if err := recover(); err != nil && err != ErrAbortHandler {
   const size = 64 <10
   buf := make([]byte, size)
   buf = buf[:runtime.Stack(buf, false)]
   c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
  }
  if inFlightResponse != nil {
   inFlightResponse.cancelCtx()
   inFlightResponse.disableWriteContinue()
  }
  if !c.hijacked() {
   if inFlightResponse != nil {
    inFlightResponse.conn.r.abortPendingRead()
    inFlightResponse.reqBody.Close()
   }
   c.close()
   c.setState(c.rwc, StateClosed, runHooks)
  }
 }()

 ...
}

果然,在 serve 方法源码中发现了 defer + recover 的组合。

并且这行代码:

c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)

可以在执行 HTTP Server 的控制台日志中得到印证:

http: 2024/10/13 23:08:28 http: panic serving [::1]:50547: url is error

panic(nil)

panic 函数签名如下:

func panic(v any)

既然 panic 参数是 any 类型,那么 nil 当然也可以作为参数。

可以写出 panic(nil) 程序示例代码如下:

func f() {
 defer func() {
  if err := recover(); err != nil {
   fmt.Println(err)
  }
 }()
 panic(nil)
}

执行示例代码,得到输出如下:

$ go run main.go
panic called with nil argument

这没什么问题。

但是在 Go 1.21 版本以前,执行上述代码,将得到如下结果:

$ go run main.go

你没看错,我也没写错误,这里什么都没输出。

在旧版本的 Go 中, panic(nil) 并不能被 recover 捕获, recover() 调用结果将返回 nil

你可以在 issues/25448 中找到关于此问题的讨论。

幸运的是,在 Go 1.21 发布时,这个问题得以解决。

不过,这就破坏了 Go 官方承诺的 Go1 兼容性保障。因此,Go 团队又提供了 GODEBUG=panicnil=1 标识来恢复旧版本中的 panic 行为。

使用方式如下:

$ GODEBUG=panicnil=1 go run main.go

其实,根据 panic 声明中的注释我们也能够观察到 Go 1.21 后 panic(nil) 行为有所改变:







请到「今天看啥」查看全文


推荐文章
考研研学姐  ·  Mark!应届毕业生档案处理全攻略
8 年前
创业投资最前线  ·  韩国向中国发公函:请让乐天恢复营业
7 年前
考研英语时事阅读  ·  【卫报】有减产预警的坚果作物丨第53期
7 年前