
























我们先来看一个简单的例子。假设我们要创建一个timeIn函数,这个函数接收一个时区名称,然后返回该时区的当前时间。
在Go语言中,当timeIn函数遇到错误时,标准的做法是将错误返回给调用者,让调用者自己决定如何处理。代码如下:
package main
import (
"fmt"
"os"
"time"
)
func timeIn(zone string) (time.Time, error) {
loc, err := time.LoadLocation(zone)
if err != nil {
return time.Time{}, err // 返回time.LoadLocation()产生的任何错误
}
return time.Now().In(loc), nil
}
func main() {
tz := "Asia/Shang"
t, err := timeIn(tz)
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
fmt.Println("Current time in", tz, "is", t)
}$ go run main.go
Error: unknown time zone Asia/Shang
exit status 1当然,你也可以选择另一种方式:在timeIn函数内部使用panic来处理错误,而不是将错误返回给调用者。代码可以这样写:
package main
import (
"fmt"
"time"
)
func timeIn(zone string) time.Time {
loc, err := time.LoadLocation(zone)
if err != nil {
panic(err) // 以错误为参数调用panic()
}
return time.Now().In(loc)
}
func main() {
tz := "Asia/Shang"
t := timeIn(tz)
fmt.Println("Current time in", tz, "is", t)
}$ go run main.go
panic: unknown time zone Asia/Shang
goroutine 1 [running]:
main.timeIn({0x4c2c7e?, 0x7d40fe626108?})
/tmp/main.go:11 +0xc5
main.main()
/tmp/main.go:20 +0x2b
exit status 2当在Go代码中使用panic时,会发生以下四个步骤:
不过,有一种方法可以阻止程序终止:在defer函数中使用recover函数来捕获和处理panic。这样只会执行到第2步,第3和第4步不会发生。
panic本身并不是一个坏东西。实际上,它提供了一些有用的功能,比如执行所有的defer函数、打印详细的堆栈信息等。
但在大多数情况下,返回错误是一种更好的选择。原因在于:
当使用panic时,程序会按照固定的流程执行(停止函数、执行defer、打印错误、终止程序),但你无法控制这个过程。
而当函数返回错误时,调用者有完全的自由决定如何处理这个错误。例如,调用者可以:
这种灵活性让程序能够更优雅地处理各种错误情况。返回错误还有其他好处:
要回答这个问题,我们需要区分两种不同类型的错误:“预期错误”和“代码缺陷”。
预期错误是指那些在正常运行中可能会发生的错误。比如:
这些错误并不意味着你的程序有问题,而是由外部因素引起的。因为这些错误是可以预见的,所以应该通过返回错误的方式来处理它们,而不是使用panic。
代码缺陷是指那些“本不应该发生”的错误。这些错误通常由以下原因导致:
这类错误应该在开发或测试阶段就被发现和解决,而不应该出现在生产环境中。
当遇到代码缺陷时,意味着程序已经处于一种意外的、不可预测的状态。在这种情况下,使用panic可能是一种合适的选择。
尽管我们前面提到了返回错误的各种好处,但在某些特定情况下,使用panic可能是更好的选择。
使用panic可能合适的两种主要情况:
Go标准库中有许多使用panic的例子,这些例子很好地展示了何时使用panic是合适的:
仔细观察这些例子,我们可以发现它们有两个重要的共同点:
所以,在这些情况下使用panic是合理的选择,因为它们要么是无法恢复的错误,要么是使用错误返回会导致代码过于复杂的情况。
当然,判断什么程度的错误处理复杂性是“难以接受的”,这个标准因人而异,也依赖于具体项目的特点。这并没有一个绝对的标准答案。
除了上面提到的情况外,还有两种常见的场景也适合使用panic:
到目前为止,我们已经讨论了使用panic的理论原则。现在,让我们通过一些实际的代码案例来看看这些原则如何应用。
需要强调的是,panic应该被谨慎使用,只有在真正适合的情况下才使用它。在我的实际工作经验中,大约有一半的项目完全不使用panic,即使使用,也只在少数几个关键位置。
下面是我在实际项目中遇到的几个使用panic的案例,这些案例可以帮助我们更好地理解何时使用panic是合适的。
这个例子来自一个Web应用,其中我们需要从请求的上下文中获取用户信息:
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函数返回一个错误,而不是使用panic。这样调用者可以处理这个错误,比如记录日志并返回一个500错误给用户。
但是,考虑到这个函数在程序中被频繁调用,如果每次调用都要处理这个在正常情况下不应该发生的错误,会导致代码中充充斥大量的错误处理逻辑。因此,在这种情况下使用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函数的作用是从环境变量中读取值并将其转换为整数。如果环境变量不存在,它会返回默认值;但如果环境变量存在但不能转换为整数,它会触发panic。
一开始看,这似乎不符合使用panic的原则。毕竟,环境变量的值无法转换为整数是一种完全可能发生的情况,应该属于“预期错误”。
但这个函数的使用场景很特殊:它只在程序启动时用来加载基本配置,如:
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)
}这段代码的背景是:我们需要根据用户的输入来生成SQL查询语句,其中包含动态的ORDER BY部分。
这里有一个安全问题:SQL不支持在ORDER BY子句中使用参数占位符(如?或:param),所以我们必须直接将列名插入到SQL字符串中。这就带来了SQL注入的风险。
正常情况下,在调用OrderBySQL方法之前,应该已经有其他代码验证了Column字段的值是否在允许的列名白名单中。但如果由于程序中的bug或开发者的疑忌,这个验证步骤被遗漏了,就可能导致SQL注入攻击。
因此,我们在OrderBySQL方法中添加了一个额外的安全检查,确保Column字段只包含安全的字符(小写字母、数字和下划线)。如果检测到不安全的字符,就触发panic。
这种情况下使用panic而不是返回错误的原因是:
通过以上的讨论和实例,我们可以得出以下结论:
在Go语言编程中,大多数情况下应该选择返回错误,而不是使用panic。这符合Go的设计哲学和最佳实践。
但在以下几种特定情况下,使用panic可能是合适的选择:
最重要的是,要记住panic会导致程序终止运行(除非被recover捕获),因此应该谨慎使用,只在真正适合的情况下才使用它。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。