Go 语言编程 — 错误处理
目录
Golang 的错误处理哲学
首先需要注意的是,错误与异常有着本质的区别。
- 错误(Error):作为流程的一部分,被调用方显式返回,调用方主动处理。
- 异常(Exception):预料之外出现或者在流程中不应该出现的错误。
Golang 的特征是不使用异常进行错误处理,这一点有别于 Python、JavaScript 等变成语言。
Golang 的错误处理哲学就是:将错误视为编写函数的一等公民(返回值)。认为错误是正常流程的一部分,应该直接返回(return)。
例如:
func getUserFromDB() (*User, error) { ... }
func main() { user, err := getUserFromDB()
}
- 1
- 2
- 3
- 4
- 5
在阅读代码时,开发者可以清晰的看到存在的错误。相较于其他语言,例如:Python,可能就无法这么清晰的来展现一个错误了,因为 try/catch 在处理控制流方面是完全不透明的。
以标准的方式处理 Golang 中的错误,将获得以下好处:
- 没有隐藏的控制流。
- 没有意外未捕获的异常日志。
- 可以完全控制代码中的错误,可以选择处理,返回和执行任何其他操作。
Golang 为程序员提供了对错误处理的完全控制权,但也要你承担全部的责任。所以 Golang 的错误处理一直存在着争议。
为什么 Golang 不使用异常进行错误处理?
Golang 之禅提到了两个重要的谚语:
- 简单很重要。
- 为失败计划而不是成功。
对所有返回 (value, error)
的函数使用简单的 if err!= nil
语句有助于确保首先考虑程序失败的情况。而无需费心处理复杂的嵌套 try/catch 块,它们可以适当地处理所有可能出现的异常。
对于基于异常的代码,你可能会意识到这样的一个场景:你会依赖 try/catch 的全局捕获,而不会深入挖掘实际的具体错误类型。这就导致了,代码有实际的异常,但你并没有正确处理它。也就是说,基于异常的代码鼓励开发者不检查错误,而是认为某些异常(如果发生)将在运行时自动处理。
但是退后一步说,基于异常的语言有一个好处:即使程序在运行时发生了未被处理的异常,仍可以通过堆栈来跟踪它,而不会中止程序。
错误处理
Golang 的错误(Error)是一个接口,不是某种特定类型。通过内置的 error interface 提供了非常简单的错误处理机制。func f() (value, error)
的语法不仅易于理解,而且在任何 Golang 项目中都可确保语法的一致性。
error interface 的定义:
type error interface { Error() string
}
- 1
- 2
- 3
在代码中,可以通过实现 error 接口类型来生成错误信息。函数通常在最后的返回值中返回错误信息。使用 errors.New 可返回一个自定义的错误信息:
func Sqrt(f float64) (float64, error) { if f < 0 { return 0, errors.New("math: square root of negative number") } // 实现
}
result, err:= Sqrt(-1)
if err != nil { fmt.Println(err)
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
if err != nil
模式的优势在于,通过错误链能方便遍历程序层次结构,直到发生错误的地方。例如,由程序的 main 函数处理的常见 Go 错误可能如下所示:
[2020-07-05-9:00] ERROR: Could not create user: could not check if user already exists in DB: could not establish database connection: no internet
- 1
上面的错误链是清晰的,对于应用程序的哪一层函数出错了有足够的信息。这样的错误输出有别于难以理解的异常堆栈跟踪,程序员可以添加友好的可读上下文描述这些错误的原因,并且通过清晰错误链进行处理。
这种错误链自然成为了标准 Golang 程序结构的一部分,可能看起来像这样:
// In controllers/user.go
if err := db.CreateUser(user); err != nil { return fmt.Errorf("could not create user: %w", err)
}
// In database/user.go
func (db *Database) CreateUser(user *User) error { ok, err := db.DoesUserExist(user) if err != nil { return fmt.Errorf("could not check if user already exists in db: %w", err) } ...
}
func (db *Database) DoesUserExist(user *User) error { if err := db.Connected(); err != nil { return fmt.Errorf("could not establish db connection: %w", err) } ...
}
func (db *Database) Connected() error { if !hasInternetConnection() { return errors.New("no internet connection") } ...
}
- 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
- 26
- 27
并且,如果你还希望将堆栈跟踪附加到函数中,可以使用 github.com/pkg/errors 库,它将打印出堆栈跟踪以及开发者构建的错误链。
errors.Wrapf(err, "could not save user with email %s", email)
- 1
示例:
package main
import ( "fmt"
)
type divideError struct { dividee int divider int
}
func (de *divideError) Error() string { strFormat := ` Cannot proceed, the divider is zero. dividee: %d divider: 0
` return fmt.Sprintf(strFormat, de.dividee)
}
func divide(varDividee int, varDivider int) (result int, errorMsg string) { if varDivider == 0 { dData := divideError{ dividee: varDividee, divider: varDivider, } errorMsg = dData.Error() return 0, errorMsg } return varDividee / varDivider, ""
}
func main() { if result, errorMsg := divide(100, 10); errorMsg == "" { fmt.Println("100/10 = ", result) } if _, errorMsg := divide(100, 0); errorMsg != "" { fmt.Println("errorMsg is: ", errorMsg) }
}
- 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
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
错误处理策略
- 将错误返回给调用者。
resp, err := http.Get(url)
if err != nil{ return nill, err
}
- 1
- 2
- 3
- 4
- 5
- 构造新的信息返回给调用者。
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil { return nil, fmt.Errorf("parsing %s as HTML: %v", url,err)
}
- 1
- 2
- 3
- 4
- 5
- 6
- 如果错误发生后,程序无法继续运行,我们就可以采用第三种策略:输出错误信息并结束程序。需要注意的是,这种策略只应在 main() 函数中执行。对库函数而言,应仅向上传播错误,除非该错误意味着程序内部包含不一致性,即遇到了 bug,才能在库函数中结束程序。
if err := WaitForServer(url); err != nil {
fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
os.Exit(1)
}
- 1
- 2
- 3
- 4
- 有时,我们只需要输出错误信息就足够了,不需要中断程序的运行。我们可以通过 log 包提供函数,或者标准错误流输出错误信息。
if err := Ping(); err != nil { log.Printf("ping failed: %v; networking disabled",err)
}
// or
if err := Ping(); err != nil { fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err)
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 我们可以直接忽略掉错误。
dir, err := ioutil.TempDir("", "scratch")
if err != nil { return fmt.Errorf("failed to create temp dir: %v",err)
}
// ...use temp dir...
os.RemoveAll(dir)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
尽管 os.RemoveAll 会失败,但上面的例子并没有做错误处理。这是因为操作系统会定期的清理临时目录。正因如此,虽然程序没有处理错误,但程序的逻辑不会因此受到影响。
我们应该在每次函数调用后,都养成考虑错误处理的习惯,当你决定忽略某个错误时,你应该在清晰的记录下你的意图。
建议
- 当你的错误需要服务于开发人员时,建议添加堆栈跟踪。
- 对返回的错误进行处理,不要只是将它们忽略。
- 保持错误链的友好且明确。
文章来源: is-cloud.blog.csdn.net,作者:范桂飓,版权归原作者所有,如需转载,请联系作者。
原文链接:is-cloud.blog.csdn.net/article/details/107165165
- 点赞
- 收藏
- 关注作者
评论(0)