Go语言的错误处理思想及设计包含以下特征:
Go语言没有类似Java或.NET中的异常处理机制,虽然可以使用 defer、panic、recover 模拟,但官方并不主张这样做,Go语言的设计者认为其他语言的异常机制已被过度使用,上层逻辑需要为函数发生的异常付出太多的资源,同时,如果函数使用者觉得错误处理很麻烦而忽略错误,那么程序将在不可预知的时刻崩溃。
Go语言希望开发者将错误处理视为正常开发必须实现的环节,正确地处理每一个可能发生错误的函数,同时,Go语言使用返回值返回错误的机制,也能大幅降低编译器、运行时处理错误的复杂度,让开发者真正地掌握错误的处理。
type error interface {
Error() string
}
实现Error() string
格式方法的结构体,即可自定义error类型,Error()
方法返回错误的具体描述。
Go语言提供了errors
包,对error
接口进行了简单实现,源码中具体实现如下
// 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
}
使用方法如下
package main
import (
"errors"
"fmt"
)
func main() {
e := errors.New("this is a error")
fmt.Println(e) // this is a error
}
package main
import "fmt"
// 自定义文件解析错误类型
type FileParseError struct {
// 文件名
FileName string
// 行号
Line int
}
// 实现error接口的Error方法,定义错误返回描述
func (f *FileParseError) Error() string {
return fmt.Sprintf("Error in line %d of file : %s\n", f.Line, f.FileName)
}
// 新建文件解析错误
func NewFileParseError(fileName string, line int) error {
return &FileParseError{fileName, line}
}
func main() {
// 模拟在文件test.txt的18行发生错误
err := NewFileParseError("test.txt", 18)
fmt.Println(err) // Error in line 18 of file : test.txt
}
Go语言的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等,这些运行时错误会引起宕机。
一般而言,当宕机发生时,程序会中断运行,并立即执行在该 goroutine(可以先理解成线程)中被延迟的函数(defer 机制),随后,程序崩溃并输出日志信息,日志信息包括 panic()
的参数以及函数调用的堆栈跟踪信息, panic()
的参数通常是某种错误信息。
虽然Go语言的 panic 机制类似于其他语言的异常,但 panic 的适用场景有一些不同,由于panic 会引起程序的崩溃,因此 panic 一般用于严重错误,如程序内部的逻辑不一致。任何崩溃都表明了我们的代码中可能存在漏洞,所以对于大部分漏洞,我们应该使用Go语言提供的错误机制,而不是 panic。
func panic(v interface{}) //panic() 的参数可以是任意类型的。
由于函数ff()
中触发了panic()
所以程序宕机中断,后面的代码也不会被执行了
package main
import "fmt"
func main() {
ff()
fmt.Println("CC")
}
func ff() {
fmt.Println("AA")
panic("Test Panic")
fmt.Println("BB")
}
/*
AA
panic: Test Panic
goroutine 1 [running]:
main.ff()
/Users/yanggang/Desktop/helloworld/main.go:12 +0x68
main.main()
/Users/yanggang/Desktop/helloworld/main.go:6 +0x1c
exit status 2
*/
recover()
是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover仅在延迟函数defer中直接调用才有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入panic,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。
注意:通常来说,不应该对进入 panic 宕机的程序做任何处理,但有时,需要我们可以从宕机中恢复,至少我们可以在程序崩溃前,做一些操作。举个例子,当 web 服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭,如果不做任何处理,会使得客户端一直处于等待状态,如果 web 服务器还在开发阶段,服务器甚至可以将异常信息反馈到客户端,帮助调试。
由于函数ff()
中有recover()
,所以当panic发生时,会执行包含了recover()
的defer语句,然后退出当前函数ff()
,程序继续执行
package main
import "fmt"
func main() {
ff()
fmt.Println("CC")
}
func ff() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("has panic : %v\n", err)
}
}()
fmt.Println("AA")
panic("Test Panic")
fmt.Println("BB")
}
/*
AA
has panic : Test Panic
CC
*/
panic 和 recover 的组合有如下特性
当 panic()
触发的宕机发生时,panic()
后面的代码将不会被运行
但是在panic()
函数前程序执行到的 defer 语句依然会在panic()
前执行
package main
import "fmt"
func main() {
fmt.Println("start")
defer fmt.Println("AA")
defer fmt.Println("BB")
panic("Test Panic")
defer fmt.Println("CC")
}
/*
start
BB
AA
panic: Test Panic
goroutine 1 [running]:
main.main()
/Users/yanggang/Desktop/helloworld/main.go:9 +0xe0
exit status 2
*/
Go语言自带了 testing 测试包,可以进行自动化的单元测试,输出结果验证,并且可以测试性能。
要开始一个单元测试,需要准备一个 go 源码文件,在命名文件时文件名必须以_test.go
结尾,单元测试源码文件可以由多个测试用例(可以理解为函数)组成,每个测试用例的名称需要以 Test 为前缀
func TestXxx( t *testing.T ){
//......
}
编写测试用例有以下几点需要注意:
_test.go
结尾;Test
(单元测试)、Benchmark
(性能测试)开头,后面可以跟任意字母组成的字符串,但第一个字母必须大写,例如 TestAbc(),一个测试用例文件中可以包含多个测试函数;(t *testing.T)
作为参数,性能测试以(t *testing.B)
做为参数;go test
命令来执行,源码中不需要 main() 函数作为入口,所有以_test.go
结尾的源码文件内以Test
开头的函数都会自动执行。测试函数名需要以Test
开头,并且以(t *testing.T)
作为参数
main.go
package main
func Subtract(a int, b int) int {
return a - b
}
demo_test.go
package main
import (
"fmt"
"testing"
)
func TestSubtract(t *testing.T) {
fmt.Println(Subtract(1, 2))
}
运行该测试用例,得到结果
=== RUN TestSubtract
-1
--- PASS: TestSubtract (0.00s)
PASS
测试函数名需要以Benchmark
开头,并且以(t *testing.B)
作为参数
main.go
package main
func Add(a int, b int) int {
return a + b
}
demo_test.go
package main
import (
"fmt"
"testing"
)
func BenchmarkCount(t *testing.B) {
total := 0
for i := 0; i < 10000; i++ {
total += i
}
fmt.Println("total:", total)
}
运行用例,得到测试结果
goos: windows
goarch: amd64
pkg: demo
cpu: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
BenchmarkCount
total: 499999500000
total: 499999500000
total: 499999500000
total: 499999500000
total: 499999500000
total: 499999500000
BenchmarkCount-8 1000000000 0.0005003 ns/op
PASS
表示程序执行了1000000000
次,共耗时0.0005003
纳秒