处理错误
- Go 语言允许函数和方法同时返回多个值
- 按照惯例,函数在返回错误时,最后边的返回值应用来表示错误。
- 调用函数后,应立即检查是否发生错误。
- 如果没有错误发生,那么返回的错误值为 nil。
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
files, err := ioutil.ReadDir(".")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
for _, file := range files {
fmt.Println(file.Name())
}
}
- 当错误发生时,函数返回的其它值通常就不再可信
优雅的错误处理
- 减少错误处理代码的一种策略是:将程序中不会出错的部分和包含潜在错误隐患的部分隔离开来。
- 对于不得不返回错误的代码,应尽力简化相应的错误处理代码。
Errors are values.
Don’t just check errors, handle them gracefully.
Don’t panic.
Make the zero value useful.
The bigger the interface, the weaker the abstraction.
interface{} says nothing.
Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite.
Documentation is for users.
A little copying is better than a little dependency.
Clear is better than clever.
Concurrency is not parallelism.
Don’t communicate by sharing memory, share memory by communicating.
Channels orchestrate; mutexes serialize.
文件写入
- 写入文件的时候可能出错:
- 路径不正确
- 权限不够
- 磁盘空间不足
…
- 文件写入完毕后,必须被关闭,确保文件被刷到磁盘上,避免资源的泄露。
package main
import (
"fmt"
"os"
)
func proverbs(name string) error {
f, err := os.Create(name)
if err != nil {
return err
}
_, err = fmt.Fprintln(f, "Errors are values.")
if err != nil {
f.Close()
return err
}
_, err = fmt.Fprintln(f, "Don’t just check errors, handle them gracefully.")
f.Close()
return err
}
func main() {
err := proverbs("proverbs.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
内置类型 error
- 内置类型 error 用来表示错误。
defer 关键字
- 使用 defer 关键字,Go 可以确保所有 deferred 的动作可以在函数返回前执行。
package main
import (
"fmt"
"os"
)
func proverbs(name string) error {
f, err := os.Create(name)
if err != nil {
return err
}
defer f.Close()
_, err = fmt.Fprintln(f, "Errors are values.")
if err != nil {
return err
}
_, err = fmt.Fprintln(f, "Don’t just check errors, handle them gracefully.")
return err
}
func main() {
err := proverbs("proverbs.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
- 可以 defer 任意的函数和方法。
- defer 并不是专门做错误处理的。
- defer 可以消除必须时刻惦记执行资源释放的负担
有创意的错误处理
package main
import (
"fmt"
"io"
"os"
)
type safeWriter struct {
w io.Writer
err error
}
func (sw *safeWriter) writeln(s string) {
if sw.err != nil {
return
}
_, sw.err = fmt.Fprintln(sw.w, s)
}
func proverbs(name string) error {
f, err := os.Create(name)
if err != nil {
return err
}
defer f.Close()
sw := safeWriter{w: f}
sw.writeln("Errors are values.")
sw.writeln("Don’t just check errors, handle them gracefully.")
sw.writeln("Don't panic.")
sw.writeln("Make the zero value useful.")
sw.writeln("The bigger the interface, the weaker the abstraction.")
sw.writeln("interface{} says nothing.")
sw.writeln("Gofmt's style is no one's favorite, yet gofmt is everyone's favorite.")
sw.writeln("Documentation is for users.")
sw.writeln("A little copying is better than a little dependency.")
sw.writeln("Clear is better than clever.")
sw.writeln("Concurrency is not parallelism.")
sw.writeln("Don’t communicate by sharing memory, share memory by communicating.")
sw.writeln("Channels orchestrate; mutexes serialize.")
return sw.err
}
func main() {
err := proverbs("proverbs.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
New error
- errors 包里有一个构造用 New函数,它接收 string 作为参数用来表示错误信息。该函数返回 error 类型。
package main
import (
"errors"
"fmt"
"os"
)
const rows, columns = 9, 9
// Grid is a Sudoku grid
type Grid [rows][columns]int8
// Set a digit on a Sudoku grid
func (g *Grid) Set(row, column int, digit int8) error {
if !inBounds(row, column) {
return errors.New("out of bounds")
}
g[row][column] = digit
return nil
}
func inBounds(row, column int) bool {
if row < 0 || row >= rows {
return false
}
if column < 0 || column >= columns {
return false
}
return true
}
func main() {
var g Grid
err := g.Set(10, 0, 5)
if err != nil {
fmt.Printf("An error occurred: %v.\n", err)
os.Exit(1)
}
}
按需返回错误
- 按照惯例,包含错误信息的变量名应以 Err 开头。
package main
import (
"errors"
"fmt"
"os"
)
const rows, columns = 9, 9
// Grid is a Sudoku grid
type Grid [rows][columns]int8
// Errors that could occur.
var (
ErrBounds = errors.New("out of bounds")
ErrDigit = errors.New("invalid digit")
)
// Set a digit on a Sudoku grid
func (g *Grid) Set(row, column int, digit int8) error {
if !inBounds(row, column) {
return ErrBounds
}
if !validDigit(digit) {
return ErrDigit
}
g[row][column] = digit
return nil
}
func inBounds(row, column int) bool {
if row < 0 || row >= rows {
return false
}
if column < 0 || column >= columns {
return false
}
return true
}
func validDigit(digit int8) bool {
return digit >= 1 && digit <= 9
}
func main() {
var g Grid
err := g.Set(0, 0, 15)
if err != nil {
switch err {
case ErrBounds, ErrDigit:
fmt.Println("Les erreurs de paramètres hors limites.")
default:
fmt.Println(err)
}
os.Exit(1)
}
}
- errors.New 这个构造函数是使用指针实现的,所以上例中的 switch 语句比较的是内存地址,而不是错误包含的文字信息。
自定义错误类型
- error 类型是一个内置的接口:任何类型只要实现了返回 string 的 Error() 方法就满足了该接口。
- 可以创建新的错误类型。
package main
import (
"errors"
"fmt"
"os"
"strings"
)
const rows, columns = 9, 9
// Grid is a Sudoku grid
type Grid [rows][columns]int8
// Errors that could occur.
var (
ErrBounds = errors.New("out of bounds")
ErrDigit = errors.New("invalid digit")
)
// SudokuError is a slice of errors.
type SudokuError []error
// Error returns one or more errors separated by commas.
func (se SudokuError) Error() string {
var s []string
for _, err := range se {
s = append(s, err.Error())
}
return strings.Join(s, ", ")
}
// Set a digit on a Sudoku grid
func (g *Grid) Set(row, column int, digit int8) error {
var errs SudokuError
if !inBounds(row, column) {
errs = append(errs, ErrBounds)
}
if !validDigit(digit) {
errs = append(errs, ErrDigit)
}
if len(errs) > 0 {
return errs
}
g[row][column] = digit
return nil
}
func inBounds(row, column int) bool {
if row < 0 || row >= rows {
return false
}
if column < 0 || column >= columns {
return false
}
return true
}
func validDigit(digit int8) bool {
return digit >= 1 && digit <= 9
}
func main() {
var g Grid
err := g.Set(10, 0, 15)
if err != nil {
if errs, ok := err.(SudokuError); ok {
fmt.Printf("%d error(s) occurred:\n", len(errs))
for _, e := range errs {
fmt.Printf("- %v\n", e)
}
}
os.Exit(1)
}
}
- 按照惯例,自定义错误类型的名字应以 Error 结尾。
- 有时候名字就是 Error,例如 url.Error
类型断言
- 上例中,我们可以使用类型断言来访问每一种错误。
- 使用类型断言,你可以把接口类型转化成底层的具体类型。
- 例如:err.(SudokuError)
- 如果类型满足多个接口,那么类型断言可使它从一个接口类型转化为另一个接口类型。
不要恐慌(don’t panic)
- Go 没有异常,它有个类似机制 panic。
- 当 panic 发生,那么程序就会崩溃。
其它语言的异常 vs Go 的错误值
- 其它语言的异常在行为和实现上与 Go 语言的错误值有很大的不同:
- 如果函数抛出异常,并且附近没人捕获它,那么它就会“冒泡”到函数的调用者那里,如果还没有人进行捕获,那么就继续“冒泡”到更上层的调用者… 直到达到栈(Stack)的顶部(例如 main 函数)。
- 异常这种错误处理方式可被看作是可选的:
- 不处理异常,就不需要加入其它代码。
- 想要处理异常,就需要加入相当数量的专用代码。
- Go 语言中的错误值更简单灵活:
- 忽略错误是有意识的决定,从代码上看也是显而易见的。
如何 panic
- Go 里有一个和其他语言异常类似的机制:panic。
- 实际上,panic 很少出现。
- 创建 panic:
- panic(“I forgot my towel”)
- panic 的参数可以是任意类型
- panic(“I forgot my towel”)
错误值、panic、os.Exit ?
- 通常,更推荐使用错误值,其次才是 panic。
- panic 比 os.Exit 更好:panic 后会执行所有 defer 的动作,而 os.Exit 则不会。
- 有时候 Go 程序会 panic 而不是返回错误值(比如执行除零计算)
保持冷静并继续
- 为了防止 panic 导致程序崩溃,Go 提供了 recover 函数。
- defer 的动作会在函数返回前执行,即使发生了 panic。
- 但如果 defer 的函数调用了 recover,panic 就会停止,程序将继续运行。
package main
import "fmt"
func main() {
defer func() {
if e := recover(); e != nil {
fmt.Println(e)
}
}()
panic("I forgot my towel")
}
作业题
- 编写一个程序:
- 在 Go 标准库里,有个函数可以解析网址(golang.org/pkg/net/url/#Parse):
- 使用一个非法网址传递到 url.Parse 函数,把发生的错误显示出来。
- 使用 %#v 和 Printf 打印错误,看看都显示什么。
- 然后执行 *url.Error 类型断言,来访问和打印底层结构体的字段和内容。
package main
import (
"fmt"
"net/url"
"os"
)
func main() {
u, err := url.Parse("https://a b.com/")
if err != nil {
fmt.Println(err)
fmt.Printf("%#v\n", err)
if e, ok := err.(*url.Error); ok {
fmt.Println("Op:", e.Op)
fmt.Println("URL:", e.URL)
fmt.Println("Err:", e.Err)
}
os.Exit(1)
}
fmt.Println(u)
}