处理错误
- Go 语言允许函数和方法同时返回多个值
- 按照惯例,函数在返回错误时,最后边的返回值应用来表示错误。
- 调用函数后,应立即检查是否发生错误。
- 如果没有错误发生,那么返回的错误值为 nil。
package mainimport ("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 mainimport ("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 mainimport ("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 mainimport ("fmt""io""os")type safeWriter struct {w io.Writererr 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 mainimport ("errors""fmt""os")const rows, columns = 9, 9// Grid is a Sudoku gridtype Grid [rows][columns]int8// Set a digit on a Sudoku gridfunc (g *Grid) Set(row, column int, digit int8) error {if !inBounds(row, column) {return errors.New("out of bounds")}g[row][column] = digitreturn 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 Griderr := g.Set(10, 0, 5)if err != nil {fmt.Printf("An error occurred: %v.\n", err)os.Exit(1)}}
按需返回错误
- 按照惯例,包含错误信息的变量名应以 Err 开头。
package mainimport ("errors""fmt""os")const rows, columns = 9, 9// Grid is a Sudoku gridtype 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 gridfunc (g *Grid) Set(row, column int, digit int8) error {if !inBounds(row, column) {return ErrBounds}if !validDigit(digit) {return ErrDigit}g[row][column] = digitreturn 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 Griderr := 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 mainimport ("errors""fmt""os""strings")const rows, columns = 9, 9// Grid is a Sudoku gridtype 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 []stringfor _, err := range se {s = append(s, err.Error())}return strings.Join(s, ", ")}// Set a digit on a Sudoku gridfunc (g *Grid) Set(row, column int, digit int8) error {var errs SudokuErrorif !inBounds(row, column) {errs = append(errs, ErrBounds)}if !validDigit(digit) {errs = append(errs, ErrDigit)}if len(errs) > 0 {return errs}g[row][column] = digitreturn 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 Griderr := 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 mainimport "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 mainimport ("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)}
