1 Go语言错误处理思想
Go语言的错误处理思想和设计原理包含以下特征:
- 一个可能发生错误的函数,需要返回值中返回一个错误接口(error)。如果函数调用是成功的,错误接口返回 nil,否则返回错误。
- 在函数调用后需要检查错误,如果发生错误,进行必要的错误处理。
<提示> Go语言没有类似Java、.Net中的异常处理机制,虽然可以使用defer、panic、recover模拟,但是官方并不主张这样做。Go语言的设计者认为其他语言的异常机制已被过度使用,上层逻辑需要为函数发生的异常付出太多的资源。同时,如果函数使用者觉得错误处理很麻烦而忽略错误,那么程序将在不可预知的时刻崩溃。
Go语言希望开发者将错误处理视为正常开发必须实现的环节,正确地处理每一个可能发生错误的函数。同时,Go语言使用返回值返回错误的机制,也能大幅降低编译器、运行时处理错误的复杂度,让开发者真正地掌握错误的处理。
2 error接口的定义格式
// src/builtin/builtin.go
type error interface {
Error() string
}
所有符合 Error() string 格式的方法,都能实现错误接口。Error()方法返回错误的具体描述信息,使用者可以通过这字符串知道发生了什么错误。
Go语言标准库提供了两个函数返回实现了 error 接口的具体类型实例,一般的错误可以使用这两个函数进行封装。遇到复杂的错误,用户可以自定义错误类型,只要实现 error 接口的 Error()方法即可。
2.1 errors包 -- errors.New() 方法
// src/errors/errors.go
// 创建错误对象
func New(text string) error {
return &errorString{text}
}
// 声明错误字符串结构体
type errorString struct {
s string
}
// 实现error接口的Error()方法,返回错误描述
func (e *errorString) Error() string {
return e.s
}
示例1:除数为0的错误。
import (
"errors" //需要导入errors包
"fmt"
)
//定义一个除数为0的错误对象
var errDivisionByZero = errors.New("division by zero")
func div(dividend int, divisor int) (int, error){
//判断除数为0的情况并返回
if divisor == 0 {
return 0, errDivisionByZero
}
//正常计算,返回空错误
return dividend / divisor, nil
}
func main(){
fmt.Println(div(24, 8)) //3 <nil>
fmt.Println(div(8, 0)) //0 division by zero
}
《代码说明》当div()函数返回除数为0的错误对象时,它会自动调用该结构体类型已经实现的Error()方法,从而输出错误字符串描述信息。
2.2 fmt包 -- fmt.Errorf() 方法
// src/fmt/errors.go
func Errorf(format string, a ...interface{}) error
《说明》该fmt.Errorf()函数返回一个格式化内容的错误对象。
示例2:修改上面的代码,改用fmt.Errorf()函数。
import (
"fmt"
)
func div(dividend int, divisor int) (int, error){
//判断除数为0的情况并返回
if divisor == 0 {
errDivisionByZero := fmt.Errorf("division by zero")
return 0, errDivisionByZero
}
//正常计算,返回空错误
return dividend / divisor, nil
}
func main(){
fmt.Println(div(24, 8)) //3 <nil>
fmt.Println(div(8, 0)) //0 division by zero
}
2.3 自定义错误类型
使用 errors.New() 定义的错误字符串的错误类型是无法提供丰富的错误信息的。如果需要携带多种错误信息返回,就需要借助自定义错误结构体类型并实现error接口来实现。
示例3:实现一个解析错误(ParseError)结构体类型,这个错误结构体包含两个内容:文件名和行号。解析错误结构体需要实现error接口的Error()方法,返回错误描述时,就需要将文件名和行号返回。
//声明一个自定义错误类型结构体
type ParseError struct{
Filename string
Line int
}
//实现error接口的Error()方法,返回错误描述
func (e *ParseError) Error() string {
return fmt.Sprintf("%s:%d", e.Filename, e.Line)
}
//创建解析错误函数,返回的是结构体对象指针
func newParseError(filename string, line int) error{
return &ParseError{filename, line}
}
func main(){
var e error
//创建一个错误实例,包含文件名和行号
e = newParseError("demo.go", 10)
//通过error接口查看错误信息
fmt.Println(e.Error())
//根据错误接口的具体类型,获取详细的错误信息
switch detail := e.(type){
case *ParseError:
fmt.Printf("Filename: %s, Line: %d\n", detail.Filename, detail.Line)
default:
fmt.Println("other error")
}
}
运行结果:
demo.go:10
Filename: demo.go, Line: 10
<提示> 自定义错误结构体类型都要实现error 接口的Error()方法,这样,所有的错误都可以获得字符串的描述。如果想进一步知道错误的详细信息,可以通过类型断言,将错误对象转换为具体的错误类型进行错误详细信息的获取。
3 错误处理的最佳实践
1、在多个返回值的函数中,error 通常作为函数最后一个返回值。
2、如果一个函数返回 error类型变量,则先用if 语句处理 error != nil 的异常情况,正常逻辑放到if 语句块的后面,保持代码平坦。
3、defer 语句应该放到 err 判断的后面,不然有可能产生 panic。示例代码如下:
// 正确写法
f, err := os.Open("defer.txt")
if err != nil {
return nil, errors.New("Open file failed")
}
defer f.Close()
// 错误写法
f, err := os.Open("defer.txt")
defer f.Close()
if err != nil {
return nil, errors.New("Open file failed")
}
// 如果f为空,就不能调用f.Close()函数了,会直接导致程序panic。
4、在错误逐级向上层传递的过程中,错误信息应该不断地丰富和完善,而不是简单地抛出下层调用的错误。这在错误日志分析时非常有用和友好。
参考
《Go语言从入门到进阶实战(视频教学版)》
《Go语言核心编程》
《Go语言学习笔记》