处理错误

  • Go 语言允许函数和方法同时返回多个值
  • 按照惯例,函数在返回错误时,最后边的返回值应用来表示错误。
  • 调用函数后,应立即检查是否发生错误。
    • 如果没有错误发生,那么返回的错误值为 nil。
  1. package main
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "os"
  6. )
  7. func main() {
  8. files, err := ioutil.ReadDir(".")
  9. if err != nil {
  10. fmt.Println(err)
  11. os.Exit(1)
  12. }
  13. for _, file := range files {
  14. fmt.Println(file.Name())
  15. }
  16. }
  • 当错误发生时,函数返回的其它值通常就不再可信

优雅的错误处理

  • 减少错误处理代码的一种策略是:将程序中不会出错的部分和包含潜在错误隐患的部分隔离开来。
  • 对于不得不返回错误的代码,应尽力简化相应的错误处理代码。

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.

文件写入

  • 写入文件的时候可能出错:
    • 路径不正确
    • 权限不够
    • 磁盘空间不足
  • 文件写入完毕后,必须被关闭,确保文件被刷到磁盘上,避免资源的泄露。
  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. )
  6. func proverbs(name string) error {
  7. f, err := os.Create(name)
  8. if err != nil {
  9. return err
  10. }
  11. _, err = fmt.Fprintln(f, "Errors are values.")
  12. if err != nil {
  13. f.Close()
  14. return err
  15. }
  16. _, err = fmt.Fprintln(f, "Don’t just check errors, handle them gracefully.")
  17. f.Close()
  18. return err
  19. }
  20. func main() {
  21. err := proverbs("proverbs.txt")
  22. if err != nil {
  23. fmt.Println(err)
  24. os.Exit(1)
  25. }
  26. }

内置类型 error

  • 内置类型 error 用来表示错误。

defer 关键字

  • 使用 defer 关键字,Go 可以确保所有 deferred 的动作可以在函数返回前执行。
  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. )
  6. func proverbs(name string) error {
  7. f, err := os.Create(name)
  8. if err != nil {
  9. return err
  10. }
  11. defer f.Close()
  12. _, err = fmt.Fprintln(f, "Errors are values.")
  13. if err != nil {
  14. return err
  15. }
  16. _, err = fmt.Fprintln(f, "Don’t just check errors, handle them gracefully.")
  17. return err
  18. }
  19. func main() {
  20. err := proverbs("proverbs.txt")
  21. if err != nil {
  22. fmt.Println(err)
  23. os.Exit(1)
  24. }
  25. }
  • 可以 defer 任意的函数和方法。
  • defer 并不是专门做错误处理的。
  • defer 可以消除必须时刻惦记执行资源释放的负担

有创意的错误处理

  1. package main
  2. import (
  3. "fmt"
  4. "io"
  5. "os"
  6. )
  7. type safeWriter struct {
  8. w io.Writer
  9. err error
  10. }
  11. func (sw *safeWriter) writeln(s string) {
  12. if sw.err != nil {
  13. return
  14. }
  15. _, sw.err = fmt.Fprintln(sw.w, s)
  16. }
  17. func proverbs(name string) error {
  18. f, err := os.Create(name)
  19. if err != nil {
  20. return err
  21. }
  22. defer f.Close()
  23. sw := safeWriter{w: f}
  24. sw.writeln("Errors are values.")
  25. sw.writeln("Don’t just check errors, handle them gracefully.")
  26. sw.writeln("Don't panic.")
  27. sw.writeln("Make the zero value useful.")
  28. sw.writeln("The bigger the interface, the weaker the abstraction.")
  29. sw.writeln("interface{} says nothing.")
  30. sw.writeln("Gofmt's style is no one's favorite, yet gofmt is everyone's favorite.")
  31. sw.writeln("Documentation is for users.")
  32. sw.writeln("A little copying is better than a little dependency.")
  33. sw.writeln("Clear is better than clever.")
  34. sw.writeln("Concurrency is not parallelism.")
  35. sw.writeln("Don’t communicate by sharing memory, share memory by communicating.")
  36. sw.writeln("Channels orchestrate; mutexes serialize.")
  37. return sw.err
  38. }
  39. func main() {
  40. err := proverbs("proverbs.txt")
  41. if err != nil {
  42. fmt.Println(err)
  43. os.Exit(1)
  44. }
  45. }

New error

  • errors 包里有一个构造用 New函数,它接收 string 作为参数用来表示错误信息。该函数返回 error 类型。
  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. "os"
  6. )
  7. const rows, columns = 9, 9
  8. // Grid is a Sudoku grid
  9. type Grid [rows][columns]int8
  10. // Set a digit on a Sudoku grid
  11. func (g *Grid) Set(row, column int, digit int8) error {
  12. if !inBounds(row, column) {
  13. return errors.New("out of bounds")
  14. }
  15. g[row][column] = digit
  16. return nil
  17. }
  18. func inBounds(row, column int) bool {
  19. if row < 0 || row >= rows {
  20. return false
  21. }
  22. if column < 0 || column >= columns {
  23. return false
  24. }
  25. return true
  26. }
  27. func main() {
  28. var g Grid
  29. err := g.Set(10, 0, 5)
  30. if err != nil {
  31. fmt.Printf("An error occurred: %v.\n", err)
  32. os.Exit(1)
  33. }
  34. }

按需返回错误

  • 按照惯例,包含错误信息的变量名应以 Err 开头。
  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. "os"
  6. )
  7. const rows, columns = 9, 9
  8. // Grid is a Sudoku grid
  9. type Grid [rows][columns]int8
  10. // Errors that could occur.
  11. var (
  12. ErrBounds = errors.New("out of bounds")
  13. ErrDigit = errors.New("invalid digit")
  14. )
  15. // Set a digit on a Sudoku grid
  16. func (g *Grid) Set(row, column int, digit int8) error {
  17. if !inBounds(row, column) {
  18. return ErrBounds
  19. }
  20. if !validDigit(digit) {
  21. return ErrDigit
  22. }
  23. g[row][column] = digit
  24. return nil
  25. }
  26. func inBounds(row, column int) bool {
  27. if row < 0 || row >= rows {
  28. return false
  29. }
  30. if column < 0 || column >= columns {
  31. return false
  32. }
  33. return true
  34. }
  35. func validDigit(digit int8) bool {
  36. return digit >= 1 && digit <= 9
  37. }
  38. func main() {
  39. var g Grid
  40. err := g.Set(0, 0, 15)
  41. if err != nil {
  42. switch err {
  43. case ErrBounds, ErrDigit:
  44. fmt.Println("Les erreurs de paramètres hors limites.")
  45. default:
  46. fmt.Println(err)
  47. }
  48. os.Exit(1)
  49. }
  50. }
  • errors.New 这个构造函数是使用指针实现的,所以上例中的 switch 语句比较的是内存地址,而不是错误包含的文字信息。

自定义错误类型

  • error 类型是一个内置的接口:任何类型只要实现了返回 string 的 Error() 方法就满足了该接口。
  • 可以创建新的错误类型。
  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. "os"
  6. "strings"
  7. )
  8. const rows, columns = 9, 9
  9. // Grid is a Sudoku grid
  10. type Grid [rows][columns]int8
  11. // Errors that could occur.
  12. var (
  13. ErrBounds = errors.New("out of bounds")
  14. ErrDigit = errors.New("invalid digit")
  15. )
  16. // SudokuError is a slice of errors.
  17. type SudokuError []error
  18. // Error returns one or more errors separated by commas.
  19. func (se SudokuError) Error() string {
  20. var s []string
  21. for _, err := range se {
  22. s = append(s, err.Error())
  23. }
  24. return strings.Join(s, ", ")
  25. }
  26. // Set a digit on a Sudoku grid
  27. func (g *Grid) Set(row, column int, digit int8) error {
  28. var errs SudokuError
  29. if !inBounds(row, column) {
  30. errs = append(errs, ErrBounds)
  31. }
  32. if !validDigit(digit) {
  33. errs = append(errs, ErrDigit)
  34. }
  35. if len(errs) > 0 {
  36. return errs
  37. }
  38. g[row][column] = digit
  39. return nil
  40. }
  41. func inBounds(row, column int) bool {
  42. if row < 0 || row >= rows {
  43. return false
  44. }
  45. if column < 0 || column >= columns {
  46. return false
  47. }
  48. return true
  49. }
  50. func validDigit(digit int8) bool {
  51. return digit >= 1 && digit <= 9
  52. }
  53. func main() {
  54. var g Grid
  55. err := g.Set(10, 0, 15)
  56. if err != nil {
  57. if errs, ok := err.(SudokuError); ok {
  58. fmt.Printf("%d error(s) occurred:\n", len(errs))
  59. for _, e := range errs {
  60. fmt.Printf("- %v\n", e)
  61. }
  62. }
  63. os.Exit(1)
  64. }
  65. }
  • 按照惯例,自定义错误类型的名字应以 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、os.Exit ?

  • 通常,更推荐使用错误值,其次才是 panic。
  • panic 比 os.Exit 更好:panic 后会执行所有 defer 的动作,而 os.Exit 则不会。
  • 有时候 Go 程序会 panic 而不是返回错误值(比如执行除零计算)

保持冷静并继续

  • 为了防止 panic 导致程序崩溃,Go 提供了 recover 函数。
  • defer 的动作会在函数返回前执行,即使发生了 panic。
  • 但如果 defer 的函数调用了 recover,panic 就会停止,程序将继续运行。
  1. package main
  2. import "fmt"
  3. func main() {
  4. defer func() {
  5. if e := recover(); e != nil {
  6. fmt.Println(e)
  7. }
  8. }()
  9. panic("I forgot my towel")
  10. }

作业题

  • 编写一个程序:
    • 在 Go 标准库里,有个函数可以解析网址(golang.org/pkg/net/url/#Parse):
    • 使用一个非法网址传递到 url.Parse 函数,把发生的错误显示出来。
    • 使用 %#v 和 Printf 打印错误,看看都显示什么。
    • 然后执行 *url.Error 类型断言,来访问和打印底层结构体的字段和内容。
  1. package main
  2. import (
  3. "fmt"
  4. "net/url"
  5. "os"
  6. )
  7. func main() {
  8. u, err := url.Parse("https://a b.com/")
  9. if err != nil {
  10. fmt.Println(err)
  11. fmt.Printf("%#v\n", err)
  12. if e, ok := err.(*url.Error); ok {
  13. fmt.Println("Op:", e.Op)
  14. fmt.Println("URL:", e.URL)
  15. fmt.Println("Err:", e.Err)
  16. }
  17. os.Exit(1)
  18. }
  19. fmt.Println(u)
  20. }