Go中应该何时使用 panic小结
作者:Go_error
如果你使用 Go 有一段时间了,可能听说过 Go 的谚语 "don't panic"。
这句话的核心意思是:“优雅地处理错误,或者将错误返回给调用方去处理,而不是直接传递给内置的 panic() 函数。”
尽管“不要 panic”是一个值得遵循的优秀准则,但有时候它被理解成“你绝对不能、绝不应该调用 panic()”。我并不这么认为,在某些少数情况下, panic() 是更合适的行为。
「为什么 panic 被视为“不好”?」
panic() 本身并不是不好。事实上它的行为(执行 defer、打印栈)还是挺有用的。
真正的问题在于:返回错误通常更好。
当你在函数中调用 panic() 时,它触发的是一个固定流程;而如果你返回错误,则由调用方决定如何处理这个错误。他们可以记录日志、提示用户、重试操作,甚至忽略它。灵活性更强。
返回错误还有其他优势:
- 可以在传播错误的过程中使用 fmt.Errorf() 或 errors.Wrap() 添加上下文,帮助排查问题。
- 写单元测试时更方便。虽然可以测试 panic 是否发生,但比测试返回值更复杂。
- 如果你开发的是一个库,panic 会终止整个应用,这很不友好。返回错误让调用方自己决定是否 panic 更合适。
- 最重要的是,返回错误是 Go 的惯例。Go 标准库也大多采用此方式,这样代码更符合预期、易于理解。
综上,返回错误或就地处理几乎总是更优选择。 但 panic() 依然有用,只是需要你非常谨慎地使用。
「什么时候适合 panic ?」
为了回答这个问题,我们可以将错误分为两类:
操作错误:
我们指的是在程序运行期间可能合理预期会发生的错误。一些例子如文件权限错误、长时间运行的操作超时或无效用户输入引起的错误。这些错误并不一定意味着你的程序本身有问题——事实上,它们通常是由程序无法控制的外部因素引起的。
操作错误是可以预期的。因为你知道它们在正常操作期间有可能发生,所以你应该努力将它们返回给调用者。不要使用 panic() 来管理它们。
程序员错误:
我们指的是在程序运行期间"永远"不应该发生的错误——这种错误源于开发人员的错误、代码库中的逻辑缺陷,或试图以不支持的方式使用语言特性或函数。理想情况下,你会在开发或测试期间发现程序员错误,而不是让它们在生产中暴露出来。
当你遇到程序员错误时,意味着你的程序处于意外状态。在这种情况下,调用 panic() 更普遍被认为是合适的行为。
除上述讨论之外,在以下情况下,使用 panic() 可能是一个好的适当的选择:
- 错误确实无法恢复(即没有合理的方法可以安全地继续操作并更优雅地处理错误);
- 返回错误会给代码库的其余部分增加不可接受的复杂性或额外的错误处理代码。
- 作为最后的"防护条款"以防止特定操作在不应发生时发生。如果 panic 被执行,表明程序存在错误或违反了某些内部业务逻辑。
- 当你不希望程序继续运行,并且除了调用 panic() 之外没有更好的处理错误的选项时。
你可以在触发 panic 的一些 Go 标准库操作中看到这种逻辑。例如:
- 整数或浮点数除以 0
- 访问切片或数组的越界索引
- 解引用 nil 指针
- 尝试使用 nil map
- 解锁未锁定的互斥锁
- 向已关闭的通道发送数据
- 当 sync.WaitGroup 计数器降到零以下
这些有什么共同点?
首先,它们都是程序员错误。如果发生这些情况,是由于代码库中的逻辑错误,或者你试图以不支持的方式使用语言特性或函数。这些情况不应该在正常的生产操作中发生。
而且如果它们返回错误,会给每个人的 Go 代码增加可以说是不可接受的额外错误处理。想象一下,如果你每次使用 / 运算符、访问切片中的值或解锁互斥锁时都必须检查错误返回值,这会增加很多开销。
实际示例
到目前为止,我希望清楚 panic() 应该谨慎被使用,并且只在真正有意义的时候使用。就我个人而言,我工作的大约一半代码库根本不调用 panic(),即使调用,也只是在少数地方。
以下是几个来自我最近工作代码库的实际示例:
示例一
这是一个来自 Web 应用程序的示例,其中有一些代码从 HTTP 请求上下文中检索用户值。
type contextKey string
const userContextKey = contextKey("user")
func contextGetUser(r *http.Request) user.User {
user, ok := r.Context().Value(userContextKey).(user.User)
if !ok {
panic("missing user value in request context")
}
return user
}
在这个特定的应用程序中,代码的结构使得 contextGetUser() 函数只在逻辑上期望请求上下文中存在用户值时被调用。在这个应用程序中,缺少值绝对是一个程序员错误,表明代码库存在问题。
当然,contextGetUser() 可以返回错误而不是 panic。但这个函数被调用很多次,感觉返回错误会为我们在正常操作中永远不应该看到的东西引入过多的错误处理。权衡之下,在这里使用 panic() 感觉是合适的。
示例二
这是来自同一应用程序的另一个示例:
func getEnvInt(key string, defaultValue int) int {
value, exists := os.LookupEnv(key)
if !exists {
return defaultValue
}
intValue, err := strconv.Atoi(value)
if err != nil {
panic(err)
}
return intValue
}
在这个应用程序中,getEnvInt() 是一个辅助函数,用于从环境变量读取值并将其转换为 int。如果转换失败,则 panic。
乍一看,这似乎不是使用 panic 的合适地方。尝试将特定环境变量转换为 int 时的错误似乎是我们程序控制之外的东西——一个操作错误。确实如此。
但在这种情况下,getEnvInt 函数在程序开始时用于从环境加载配置设置,如下所示:
httpPort := getEnvInt("HTTP_PORT", 3939)
在程序的这个早期阶段,日志记录器(它也依赖于环境设置)尚未初始化。由于程序无法在没有有效配置值的情况下运行,并且还没有适当的日志记录器来优雅地处理错误,因此没有其他好的选项来管理这个错误。诉诸 panic() 感觉是一个合理的选择。
这符合"你不希望程序继续运行,并且没有更好的选项来处理错误"的场景。
注意:我可以让 getEnvInt() 函数返回错误,并让调用者自己调用 panic()。但这会为基本相同的结果生成额外的错误处理,因此权衡之下,从 getEnvInt() 内部 panic 是有意义的。
示例三
这是一个我之前在防护条款中使用 panic 的示例。
var safeChars = regexp.MustCompile("^[a-z0-9_]+$")
type SortValues struct {
Column string
Ascending bool
}
func (sv *SortValues) OrderBySQL() string {
if !safeChars.MatchString(sv.Column) {
panic("unsafe sort column: " + sv.Column)
}
if sv.Ascending {
return fmt.Sprintf("ORDER BY %s ASC", sv.Column)
}
return fmt.Sprintf("ORDER BY %s DESC", sv.Column)
}
在这个特定的应用程序中,需要根据用户输入生成带有动态 ORDER BY 参数的 SQL 查询。
SortValues 类型保存用户提供的列名和排序方向,其 OrderBySQL() 方法返回一个像 ORDER BY title ASC 这样的字符串。
在调用 OrderBySQL() 方法时,上游函数应该已经根据允许列名的白名单验证了 SortValues.Column 值。但如果由于错误或疏忽导致该验证步骤被遗漏,应用程序将容易受到通过用户提供的列名的 SQL 注入攻击。
因此,作为最后的缓解措施,我们在 OrderBySQL() 中使用 panic 防护条款来确保 SortValues.Column 值只包含"安全"字符(a 到 z,0 到 9 和下划线)。
从 OrderBySQL() 返回错误似乎有些过头。但如果它确实发生了,触发 panic 比冒着危害数据库的风险感觉更好。
到此这篇关于Go中应该何时使用 panic小结的文章就介绍到这了,更多相关Go 使用 panic内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
