Files
OpenFlare/.agents/skills/go-defensive/references/PANIC-RECOVER.md
T
2026-08-04 11:40:32 +08:00

162 lines
4.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Panic 与 Recover 模式
> **来源**:Effective Go
## Panic 指南
`panic` 创建一个运行时错误来停止程序。仅在真正不可恢复的情况下使用。
### 何时 Panic
真正的库函数应**避免 panic**。如果问题可以被掩盖或绕过,让程序继续运行,而不是让整个程序崩溃。
```go
// 可接受:真正不可能的情况
func CubeRoot(x float64) float64 {
z := x/3
for i := 0; i < 1e6; i++ {
prevz := z
z -= (z*z*z-x) / (3*z*z)
if veryClose(z, prevz) {
return z
}
}
// 百万次迭代仍未收敛;出了问题。
panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}
```
### 初始化中的 Panic
例外:如果库在 `init()` 期间确实无法完成初始化,panic 可能是合理的:
```go
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
```
### 何时 Panic 是可接受的
除了初始化之外,panic 在以下窄泛场景中是可接受的:
1. **API 误用**——类似于核心语言对越界访问的 panic。`reflect` 包使用了这种方法。
2. **带有匹配 `recover` 的内部实现细节**在包边界处。Panic 简化了深层嵌套的控制流,而公共 API 仍然返回 error(下方的 Parse/parseInt 模式)。
3. **`panic("unreachable")`** 在 `log.Fatal` 之后,当编译器无法检测到不可达代码时。
#### Parse/parseInt 模式
在内部使用 panic 来回退复杂的递归,但始终在包边界处转换为 error:
```go
func parseInt(in string) int {
n, err := strconv.Atoi(in)
if err != nil {
panic(&syntaxError{"not a valid integer"})
}
return n
}
func Parse(in string) (_ *Node, err error) {
defer func() {
if p := recover(); p != nil {
sErr, ok := p.(*syntaxError)
if !ok {
panic(p) // 不是我们的——重新 panic
}
err = fmt.Errorf("syntax error: %v", sErr.msg)
}
}()
// ... 内部调用 parseInt
}
```
**关键**:类型检查 `p.(*syntaxError)` 确保只捕获*我们的* panic。意外的 panic(nil 指针等)正常传播。
---
## Recover 模式
`recover` 重新获得对正在 panic 的 goroutine 的控制。它只在延迟函数中有效。
### 基本恢复模式
```go
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
```
### 服务器 Goroutine 保护
在服务器中将 panic 隔离到各个 goroutine:
```go
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work) // 每个 worker 都受保护
}
}
```
如果 `do(work)` panic,结果会被记录,goroutine 干净退出而不影响其他 goroutine。
### 包内部的 Panic/Recover
在内部使用 panic 但在 API 边界处转换为 error:
```go
// Error 是一个解析错误类型
type Error string
func (e Error) Error() string { return string(e) }
// 内部:使用 Error 类型 panic
func (regexp *Regexp) error(err string) {
panic(Error(err))
}
// 外部 API:将 panic 转换为 error 返回
func Compile(str string) (regexp *Regexp, err error) {
regexp = new(Regexp)
defer func() {
if e := recover(); e != nil {
regexp = nil
err = e.(Error) // 如果不是我们的 Error 类型则重新 panic
}
}()
return regexp.doParse(str), nil
}
```
**要点:**
- 延迟函数可以修改命名返回值
- 类型断言 `e.(Error)` 对意外错误类型重新 panic
- 绝不向客户端暴露 panic——始终在 API 边界处转换
---
## 快速参考
| 模式 | 描述 |
|------|------|
| 基本恢复 | `defer func() { if err := recover(); err != nil { ... } }()` |
| 服务器保护 | 将每个 goroutine 处理器包装在 safelyDo 中 |
| 包内部 | 内部 panic,在 API 边界处 recover 并返回 error |
| 类型安全恢复 | 使用类型断言对意外错误重新 panic |
## 何时使用
- **Panic**:仅用于真正不可恢复的情况或初始化失败
- **Recover**:服务器处理器、包内部错误简化
- **绝不**:跨包边界暴露 panic——始终转换为 error