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
语句的行为其实是可预测的,我们可以
记住这三条规则
:
-
在计算
defer
语句时,将立即计算被
defer
修饰的函数参数。
-
被
defer
修饰的函数,在外层函数返回后按后进先出的顺序(
LIFO
)执行。
-
现在,你再翻回去重新看看上面的几个示例程序,是不是都能理解了呢?
释放资源
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
1
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)
行为有所改变: