分享

Go 语言系列29:错误处理

 菜籽爱编程 2022-04-27

在 Go 中, 错误 使用内建的 error 类型表示。error 类型是一个接口类型,它的定义如下:

type error interface {
    Error() string
}

error 有了一个签名为 Error() string 的方法。所有实现该接口的类型都可以当作一个错误类型。Error() 方法给出了错误的描述。fmt.Println 在打印错误时,会在内部调用 Error() string 方法来得到该错误的描述。

下面的例子演示了程序尝试打开一个不存在的文件导致的报错:

package main

import (
 "fmt"
 "os"
)

func main() {
 // 尝试打开文件
 file, err := os.Open("/a.txt")
 // 如果打开文件时发生错误 返回一个不等于 nil 的错误
 if err != nil {
  fmt.Println(err)
  return
 }
 // 如果打开文件成功 返回一个文件句柄 和 一个值为 nil 的错误
 fmt.Println(file.Name(), "opened successfully")
}

我们这里没有存在一个文件 a.txt ,所以尝试打开文件将会返回一个不等于 nil 的错误,在程序中我们简单的将其进行输出。所以运行程序输出如下:

open /a.txt: The system cannot find the file specified.


自定义错误

使用 errors 包中的 New 函数可以创建自定义错误。下面是 errors 包中 New 函数的实现代码:

package errors

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
 return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
 s string
}

func (e *errorString) Error() string {
 return e.s
}

errorString 是一个结构体类型,只有一个字符串字段 s 。它使用了 errorString 指针接受者,来实现 error 接口的 Error() string 方法。New 函数有一个字符串参数,通过这个参数创建了 errorString 类型的变量,并返回了它的地址。于是它就创建并返回了一个新的错误。

下面是一个简单的自定义错误例子,该例子创建了一个计算矩形面积的函数,当矩形的长和宽两者有一个为负数时,就会返回一个错误:

package main

import (
 "errors"
 "fmt"
)

func rectangleArea(a, b int) (int, error) {
 if a < 0 || b < 0 {
  return 0, errors.New("area calculation failed, Length or width is less than zero")
 }
 return a * b, nil
}
func main() {
 a := 100
 b := -10
 area, err := rectangleArea(a, b)
 if err != nil {
  fmt.Println(err)
  return
 }
 fmt.Println("Area =", area)
}

运行上面的程序会报出自定义的错误:

area calculation failed, Length or width is less than zero


给错误添加更多信息

上面的程序能报出我们自定义的错误,但是没有具体说明是哪个数据出了问题,所以下面就来改进一下这个程序,我们使用 fmt 包中的 Errorf 函数,规定错误格式,并返回一个符合该错误的字符串。

package main

import (
 "fmt"
)

func rectangleArea(a, b int) (int, error) {
 if a < 0 || b < 0 {
  return 0, fmt.Errorf("area calculation failed, Length %d or width %d is less than zero", a, b)
 }
 return a * b, nil
}
func main() {
 a := 100
 b := -10
 area, err := rectangleArea(a, b)
 if err != nil {
  fmt.Println(err)
  return
 }
 fmt.Println("Area =", area)
}

运行上面的程序,我们可以看到输出的错误中打印了长度和宽度的具体值:

area calculation failed, Length 100 or width -10 is less than zero

当然,给错误添加更多信息还可以 使用结构体类型和字段 实现。下面还是通过改进上面的程序来讲解这种方法的实现:

首先创建一个表示错误的结构体类型,一般错误类型名称都是以 Error 结尾,上面的错误是由于面积计算中长度或宽度错误导致的,所以这里把结构体命名为 areaError

type areaError struct {
 // 错误信息
 err string
 // 错误有关的长度
 length int
 // 错误有关的宽度
 width int
}

接下来当然是实现 error 接口啦:

// 使用指针接收者 *areaError 实现了 error 接口的 Error() string 方法
func (e *areaError) Error() string {
 // 打印长度和宽度以及错误的描述
 return fmt.Sprintf("length %d, width %d : %s", e.length, e.width, e.err)
}

下面是整个程序的代码:

package main

import (
 "fmt"
)

type areaError struct {
 // 错误信息
 err string
 // 错误有关的长度
 length int
 // 错误有关的宽度
 width int
}

// 使用指针接收者 *areaError 实现了 error 接口的 Error() string 方法
func (e *areaError) Error() string {
 // 打印长度和宽度以及错误的描述
 return fmt.Sprintf("length %d, width %d : %s", e.length, e.width, e.err)
}

func rectangleArea(a, b int) (int, error) {
 if a < 0 || b < 0 {
  return 0, &areaError{"length or width is negative", a, b}
 }
 return a * b, nil
}
func main() {
 a := 100
 b := -10
 area, err := rectangleArea(a, b)
 // 检查了错误是否为 nil
 if err != nil {
  // 断言 *areaError 类型
  if err, ok := err.(*areaError); ok {
   // 如果错误是 *areaError 类型
   // 用 err.length 和 err.width 来获取错误的长度和宽度 打印出自定义错误的消息
   fmt.Printf("length %d or width %d is less than zero", err.length, err.width)
   return
  }
  fmt.Println(err)
  return
 }
 fmt.Println("Area =", area)
}

运行该程序输出如下:

length 100 or width -10 is less than zero

当然,我们还可以使用 结构体类型的方法 来给错误添加更多信息。下面我们继续完善上面的程序,让程序更加精确的定位是长度引发的错误还是宽度引发的错误。

首先,我们还是跟上面一样创建一个表示错误的结构体:

type areaError struct {
 // 错误信息
 err string
 // 长度引发的错误
 length int
 // 宽度引发的错误
 width int
}

下面还是一样实现 error 接口,这里给错误类型添加了两个方法,使它提供更多的错误信息:

// 使用指针接收者 *areaError 实现了 error 接口的 Error() string 方法
func (e *areaError) Error() string {
 return e.err
}

// 长度为负数返回 true
func (e *areaError) lengthNegative() bool {
 return e.length < 0
}

// 宽度为负数返回 true
func (e *areaError) widthNegative() bool {
 return e.width < 0
}

接下来就是修改计算面积的函数了:

func rectangleArea(length, width int) (int, error) {
 err := ""
 if length < 0 {
  err += "length is less than zero"
 }
 if width < 0 {
  if err == "" {
   err = "width is less than zero"
  } else {
   err += " and width is less than zero"
  }
 }
 if err != "" {
  return 0, &areaError{err, length, width}
 }
 return length * width, nil
}

最后我们编写主函数,整个程序如下:

package main

import (
 "fmt"
)

type areaError struct {
 // 错误信息
 err string
 // 长度引发的错误
 length int
 // 宽度引发的错误
 width int
}

// 使用指针接收者 *areaError 实现了 error 接口的 Error() string 方法
func (e *areaError) Error() string {
 return e.err
}

// 长度为负数返回 true
func (e *areaError) lengthNegative() bool {
 return e.length < 0
}

// 宽度为负数返回 true
func (e *areaError) widthNegative() bool {
 return e.width < 0
}

func rectangleArea(length, width int) (int, error) {
 err := ""
 if length < 0 {
  err += "length is less than zero"
 }
 if width < 0 {
  if err == "" {
   err = "width is less than zero"
  } else {
   err += " and width is less than zero"
  }
 }
 if err != "" {
  return 0, &areaError{err, length, width}
 }
 return length * width, nil
}

func main() {
 length := 100
 width := -10
 area, err := rectangleArea(length, width)
 // 检查了错误是否为 nil
 if err != nil {
  // 断言 *areaError 类型
  if err, ok := err.(*areaError); ok {
   // 如果错误是 *areaError 类型
   // 如果长度为负数 打印错误长度具体值
   if err.lengthNegative() {
    fmt.Printf("error: length %d is less than zero\n", err.length)
   }
   // 如果宽度为负数 打印错误宽度具体值
   if err.widthNegative() {
    fmt.Printf("error: width %d is less than zero\n", err.width)
   }
   return
  }
  fmt.Println(err)
  return
 }
 fmt.Println("Area =", area)
}

还是使用之前的例子中的参数,但我们这次报错结果更加具体,运行该程序输出如下:

error: width -10 is less than zero

参考文献:

[1] Alan A. A. Donovan; Brian W. Kernighan, Go 程序设计语言, Translated by 李道兵, 高博, 庞向才, 金鑫鑫 and 林齐斌, 机械工业出版社, 2017.

👇周一至周五更新,期待你的关注👇

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多