Go 错误处理与单元测试
1、错误处理
1.1 如何定义错误
在 Go 语言中,无论是在类型检查还是编译过程中,都是将错误看做值来对待,和 string 或者 integer 这些类型值并不差别。声明一个 string 类型变量和声明一个 error 类型变量是没什么区别的。
你可以定义接口作为 error 的类型,有关 error 能够提供什么样信息都是由自己决定的,这是 error 在 golang 作为值的好处,不过这样做也自然有其坏处,有关 error 定义好坏就全由其定义开发人员所决定,也就是有关 error 融入过多人为的主观因素。
1.2 错误处理原则
错误处理的原则就是不能丢弃任何有返回 error 的调用,不要使用 _
丢弃,必须全部处理,接收到错误,要么返回 error ,或者使用 log 记录下来尽早 return。
一旦有错误发生,马上返回,尽量不要使用 panic,除非你知道你在做什么,错误描述如果是英文必须为小写,不需要标点结尾,采用独立的错误流进行处理。
// 错误写法 if err != nil { // 错误处理 } else { // 正常代码 } // 正确写法 if err != nil { // 错误处理 return // 或者继续 } // 正常代码
1.3 defer+recover 机制
defer 是 Go 语言中一种延迟调用机制,defer 后面的函数只有在当前函数执行完毕后才能执行,将延迟的语句按 defer 的逆序进行执行,也就是说先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行,即对 defer 延迟的语句做压栈操作(先进后出),通常用于释放资源。
recover 内建函数允许程序管理恐慌过程中的 Go 进程。在 defer 的函数中,执行 recover 调用会取回传至 panic 调用的错误值,恢复正常运行,停止恐慌过程。
实例:
package main import "fmt" func test() { // defer+recover捕获异常 defer func() { // 调用recover函数,捕获异常 // 如果没有捕获异常,返回值为零值(nil) err := recover() if err != nil { fmt.Printf("当前错误已经捕获: %v\n", err) } }() fmt.Println("函数执行第一阶段(第一小节)...") n1 := 10 n2 := 0 ret := n1 / n2 fmt.Println(ret) fmt.Println("函数执行第一阶段(第二小节)...") } func main() { fmt.Println("函数执行第一阶段...") test() fmt.Println("函数执行第二阶段...") } /* 执行结果: 函数执行第一阶段... 函数执行第一阶段(第一小节)... 当前错误已经捕获: runtime error: integer divide by zero 函数执行第二阶段... */
1.4 自定义错误
在 Go 语言中,有多种创建自定义错误信息的方法,每一种都有自己的优点和缺点。
1.4.1 基于字符串的错误
基于字符串的错误可以用 Go 语言中两个开箱即用方法来自定义错误,适用哪些仅返回描述错误信息的相对来说比较简单的错误。
语法:
// 方法一 err := errors.New("math: divided by zero") // 方法二 err := fmt.Errorf("math: divided by zero")
实例:
package main import ( "errors" "fmt" ) func test() (err error) { n1 := 10 n2 := 0 if n2 == 0 { // 抛出自定义错误 return errors.New("除数不能为0") } else { ret := n1 / n2 fmt.Println(ret) // 如果没有错误返回零值 return nil } } func main() { fmt.Println("函数执行第一阶段...") err := test() if err != nil { fmt.Printf("自定义错误: %v\n", err) } fmt.Println("函数执行第二阶段...") } /* 执行结果: 函数执行第一阶段... 自定义错误: 除数不能为0 函数执行第二阶段... */
1.4.2 自定义数据结构的错误
可以通过在你的结构上实现 Error 接口中定义的 Error()
函数来创建自定义的错误类型。
实例:
package main import ( "fmt" ) // 定义一个DivideError结构 type DivideError struct { dividee int divider int } // 实现error接口 func (de *DivideError) Error() string { strFormat := ` Cannot proceed, the divider is zero. dividee: %d divider: 0 ` return fmt.Sprintf(strFormat, de.dividee) } // 定义int类型除法运算的函数 func Divide(varDividee int, varDivider int) (result int, errorMsg string) { if varDivider == 0 { dData := DivideError{ dividee: varDividee, divider: varDivider, } errorMsg = dData.Error() return } else { return varDividee / varDivider, "" } } func main() { // 正常情况 result, errorMsg1 := Divide(100, 10) if errorMsg1 == "" { fmt.Println("100/10 = ", result) } // 当除数为零的时候会返回错误信息 _, errorMsg2 := Divide(100, 0) if errorMsg2 != "" { fmt.Println("errorMsg is: ", errorMsg2) } } /* 执行结果: 100/10 = 10 errorMsg is: Cannot proceed, the divider is zero. dividee: 100 divider: 0 */
1.5 panic
可以借助 builtin 包下的内建 panic 函数实现:程序出现错误后,后续代码没必要执行了,即程序中断、退出程序。
注意: 一旦有错误发生,马上返回,尽量不要使用 panic,除非你知道你在做什么。
实例:
package main import ( "errors" "fmt" ) func test() (err error) { n1 := 10 n2 := 0 if n2 == 0 { // 抛出自定义错误 return errors.New("除数不能为0") } else { ret := n1 / n2 fmt.Println(ret) // 如果没有错误返回零值 return nil } } func main() { fmt.Println("函数执行第一阶段...") err := test() if err != nil { fmt.Printf("自定义错误: %v\n", err) // panic panic(err) } fmt.Println("函数执行第二阶段...") } /* 执行结果: 函数执行第一阶段... 自定义错误: 除数不能为0 panic: 除数不能为0 goroutine 1 [running]: main.main() /Users/zhangyy/Documents/Gitlab/yannic/golang/zhangyydev.com/golang-basics/main/demo.go:28 +0x114 exit status 2 */
2、单元测试
单元测试文件名命名规范 example_test.go
测试用例函数名称必须以 Test 开头,例如:TestExample
。每个重要的函数都要首先编写测试用例,测试用例和正规代码一起提交方便进行回归测试。