Golang func使用的一些笔记

Panic 异常

Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。这些运行时错误会引起painc异常。

一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine(可以先理解成线程,在第8章会详细介绍)中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。对于每个goroutine,日志信息中都会有与之相对的,发生panic时的函数调用堆栈跟踪信息。通常,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据。因此,在我们填写问题报告时,一般会将panic异常和日志信息一并记录。

不是所有的panic异常都来自运行时,直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。当某些不应该发生的场景发生时,我们就应该调用panic。比如,当程序到达了某条逻辑上不可能到达的路径。

Recover 捕获异常

在 Go 语言中,错误(error)被认为是一种可以预期的结果;而异常(panic)则是一种非预期的结果,发生异常可能表示程序中存在 BUG 或发生了其它不可控的问题。Go 语言推荐使用 recover 函数将内部异常转为错误处理,这使得用户可以真正的关心业务相关的错误处理。

如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。

常用示例

recover

1
2
3
4
5
defer func() {
    if err := recover(); err != nil {
        fmt.Printf("recover panic: %+v", err)
    }
}()

panic

1
panic("panic error")

panic 没有 recover

code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import "fmt"

// main.go
func main() {
    fmt.Println(echo(10, 0))
    fmt.Println("end")
}

func echo(a, b int) int {
    v := a / b
    fmt.Println("echo a/b: ", v)
    return v
}

output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
GOROOT=D:\gosdk\go1.21.1 #gosetup
panic: runtime error: integer divide by zero    
                                                
goroutine 1 [running]:                          
main.echo(0x0?, 0xc00003bf30?)                  
        E:/work/awesomeProject/main.go:12 +0x98 
main.main()                                     
        E:/work/awesomeProject/main.go:7 +0x1e  

Process finished with the exit code 2

除法计算里0当除数触发panic,并且没有recover所以程序终止并退出了。

panic 有 recover

外部recover

code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

// main.go
func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("recover panic: %+v", err)
        }
    }()
    fmt.Println(echo(10, 0))
    fmt.Println("end")
}

func echo(a, b int) int {
    v := a / b
    fmt.Println("echo a/b: ", v)
    return v
}

output

1
2
3
GOROOT=D:\gosdk\go1.21.1 #gosetup
recover panic: runtime error: integer divide by zero 
Process finished with the exit code 0

echo函数外部有recover触发panic后会影响程序后续运行 上方的 fmt.Println("end")没有执行。

内部recover

code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

// main.go
func main() {
    fmt.Println(echo(10, 0))
    fmt.Println("end")
}

func echo(a, b int) int {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("recover panic: %+v", err)
        }
    }()
    v := a / b
    fmt.Println("echo a/b: ", v)
    return v
}

output

1
2
3
4
5
GOROOT=D:\gosdk\go1.21.1 #gosetup
recover panic: runtime error: integer divide by zero0 
end                                                   

Process finished with the exit code 0

echo函数内部有recover触发panic后会不影响程序后续运行。

外部recover(协程)

code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
	"fmt"
	"time"
)

// main.go
func main() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("recover panic: %+v", err)
		}
	}()
	go echo(10, 0)
	fmt.Println(echo(10, 5))
	fmt.Println("end")
	time.Sleep(1 * time.Minute)
}

func echo(a, b int) int {
	v := a / b
	fmt.Println("echo a/b: ", v)
	return v
}

output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
GOROOT=D:\gosdk\go1.21.1 #gosetup
echo a/b:  2
2                                                
panic: runtime error: integer divide by zero     
                                                 
goroutine 6 [running]:                           
main.echo(0x0?, 0x0?)                            
        E:/Mycode/awesomeProject/main.go:22 +0x98
created by main.main in goroutine 1              
        E:/Mycode/awesomeProject/main.go:15 +0x3b

Process finished with the exit code 2

main函数有recover触发并且使用go调用echo panic后recover捕获不到错误,main程序会异常退出。

内部recover(协程)

code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
	"fmt"
	"time"
)

// main.go
func main() {
	go echo(10, 0)
	fmt.Println(echo(10, 5))
	fmt.Println("end")
	time.Sleep(1 * time.Minute)
}

func echo(a, b int) int {
	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("recover panic: %+v", err)
		}
	}()
	v := a / b
	fmt.Println("echo a/b: ", v)
	return v
}

output

1
2
3
4
5
GOROOT=D:\gosdk\go1.21.1 #gosetup
echo a/b:  2
2
end
recover panic: runtime error: integer divide by zero

echo函数有recover触发并且使用go调用echo panic后recover捕获错误,main程序会照常执行不会异常退出。

总结

golang 中的 defer 的使用可以总结为,如果 panic 和 recover 发生在同一个协程,那么 recover 是可以捕获的,如果 panic 和 recover 发生在不同的协程,那么 recover 是不可以捕获的。

引用文章与材料