3、异常处理和单元测试

错误处理

Go语言的错误处理思想及设计包含以下特征:

Go语言没有类似Java或.NET中的异常处理机制,虽然可以使用 defer、panic、recover 模拟,但官方并不主张这样做,Go语言的设计者认为其他语言的异常机制已被过度使用,上层逻辑需要为函数发生的异常付出太多的资源,同时,如果函数使用者觉得错误处理很麻烦而忽略错误,那么程序将在不可预知的时刻崩溃。

Go语言希望开发者将错误处理视为正常开发必须实现的环节,正确地处理每一个可能发生错误的函数,同时,Go语言使用返回值返回错误的机制,也能大幅降低编译器、运行时处理错误的复杂度,让开发者真正地掌握错误的处理。

error接口的定义格式

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
}

宕机(panic)

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)

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 的组合有如下特性

painc和defer

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开头,并且以(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纳秒