这是我参与「第五届青训营 」伴学笔记创作活动的第 7 天。
1. 错误处理
- 意料之中的错误:使用
error。如:文件不存在 - 意料之外的错误:使用
panic。如:数组越界
1.1. error
错误只是一种接口类型
// $GOPATH/src/builtin/builtin.go
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
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
}
var e error = errors.New("出错了")
fmt.Println(e) // 出错了
// PathError records an error and the operation and file path that caused it.
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
func main() {
_, err := os.OpenFile("a.txt", os.O_RDWR, 0666)
if err != nil {
fmt.Printf("%T\n", err)
// *fs.PathError
fmt.Println(err)
// open a.txt: The system cannot find the file specified.
}
}
1.2. panic 恐慌
进程运行时,发生错误了错误后,它就会恐慌(panic)然后崩溃退出。
- panic 会停止当前函数的执行;一直向上返回并执行每一层的 defer
- 若上述过程中没遇到 recover 则会返回到 main 函数,最终程序退出
- panic 只会执行当前协程的 defer,不会执行其他协程的 defer,包括启动当前协程的协程,甚至主协程的 defer 都不会被执行,最终整个程序崩溃
所以,为防止让所有协程都崩溃,一定要在当前携程的 defer 中执行 recover
// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}
func panic(v any)
除数为 0 造成的恐慌:
func main() {
a := 10
b := 0
fmt.Println(a / b)
// 进程崩溃, 并显示:
// panic: runtime error: integer divide by zero
fmt.Println("这里不会执行了")
}
自己制造恐慌:
func main() {
panic("制造恐慌")
// 进程崩溃, 并显示:
// panic: 制造恐慌
}
1.3. recover 恢复
可以在进程处于恐慌时,捕获到异常,使进程从恐慌中恢复(recover)过来。
没有 recover 则 fmt.Println("这里执行了") 不会被执行
func main() {
defer fmt.Println("defer执行了")
makePanic()
fmt.Println("这里执行了")
}
func makePanic() {
panic("制造恐慌")
}
// 这里执行了
// 然后程序奔溃
有 recover 程序可以恢复执行
func main() {
defer fmt.Println("defer执行了")
tryRecover()
fmt.Println("这里执行了")
}
func tryRecover() {
defer func() {
recover()
}()
panic("制造恐慌")
}
// 这里执行了
// defer执行了
2. 测试
Go 是以表格驱动测试为宗旨的。
- 分离测试数据和测试逻辑
- 明确的出错信息
- 可以部分失败:某个测试用例失败,不影响后续测试的进行
- Go 的语法使我们更容易实现表格驱动测试
Go testing
- 测试源文件命名为
xxx_test.go,xxx是被测试的源文件名 - 运行测试:
# 若所以测试都正确, 则不输出日志; 有错误才测试输出 go test # 都输出测试日志 go test -v # 运行单个测试源文件 go test 测试源文件 被测试源文件 # 运行单个测试函数 go test -test.run 测试函数名
2.1. 单元测试
- 单元测试的测试函数命名为
TestXxxYyy,XxxYyy是被测试的函数名 - 单元测试的测试函数的形参列表必须是
(t *testing.T)
// calculate.go
func Add(a, b int) int {
return a + b
}
func Sub(a, b int) int {
return a - b
}
// calculate_test.go
import (
"math"
"testing"
)
func TestAdd(t *testing.T) {
testCases := []struct{ a, b, expectedResult int }{
{0, 0, 0},
{1, 1, 2},
{1, 1, 0}, // 假装失败
{-1, 1, 0},
{math.MaxInt, 1, math.MinInt},
}
for _, testCase := range testCases {
actualResult := Add(testCase.a, testCase.b)
if actualResult != testCase.expectedResult {
t.Errorf("测试失败: Add(%d, %d) got %d; expected %d\n",
testCase.a, testCase.b, actualResult, testCase.expectedResult)
}
}
}
测试失败:
=== RUN TestAdd
calculate_test.go:20: 测试失败: Add(1, 1) got 2; expected 0
--- FAIL: TestAdd (0.00s)
FAIL
测试成功:
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
2.2. TB.Errorf 与 TB.Fatalf
TB.Errorf 和 TB.Fatalf 都能用来输出格式化的错误信息,并表示该条用例测试失败。
TB.Fatalf 被调用后会退出测试进程,就不再执行后续的用例了;而 TB.Errorf 会继续执行后续的测试用例。
// Errorf is equivalent to Logf followed by Fail.
func (c *common) Errorf(format string, args ...any) {
c.checkFuzzFn("Errorf")
c.log(fmt.Sprintf(format, args...))
c.Fail()
}
// Fatalf is equivalent to Logf followed by FailNow.
func (c *common) Fatalf(format string, args ...any) {
c.checkFuzzFn("Fatalf")
c.log(fmt.Sprintf(format, args...))
c.FailNow()
}
例子:
func TestAdd(t *testing.T) {
testCases := []struct{ a, b, expectedResult int }{
{0, 0, 0},
{1, 1, 2},
{1, 1, 0}, // 假装失败
{-1, 1, 0},
{math.MaxInt, 1, math.MinInt},
}
for _, testCase := range testCases {
actualResult := Add(testCase.a, testCase.b)
if actualResult != testCase.expectedResult {
t.Fatalf("测试失败: Add(%d, %d) got %d; expected %d\n",
testCase.a, testCase.b, actualResult, testCase.expectedResult)
}
t.Logf("测试成功: Add(%d, %d) got %d; expected %d\n",
testCase.a, testCase.b, actualResult, testCase.expectedResult)
}
}
// === RUN TestAdd
// calculate_test.go:25: 测试成功: Add(0, 0) got 0; expected 0
// calculate_test.go:25: 测试成功: Add(1, 1) got 2; expected 2
// calculate_test.go:20: 测试失败: Add(1, 1) got 2; expected 0
// --- FAIL: TestAdd (0.00s)
// FAIL
func TestAdd(t *testing.T) {
testCases := []struct{ a, b, expectedResult int }{
{0, 0, 0},
{1, 1, 2},
{1, 1, 0}, // 假装失败
{-1, 1, 0},
{math.MaxInt, 1, math.MinInt},
}
for _, testCase := range testCases {
actualResult := Add(testCase.a, testCase.b)
if actualResult != testCase.expectedResult {
t.Errorf("测试失败: Add(%d, %d) got %d; expected %d\n",
testCase.a, testCase.b, actualResult, testCase.expectedResult)
}
t.Logf("测试成功: Add(%d, %d) got %d; expected %d\n",
testCase.a, testCase.b, actualResult, testCase.expectedResult)
}
}
// === RUN TestAdd
// calculate_test.go:25: 测试成功: Add(0, 0) got 0; expected 0
// calculate_test.go:25: 测试成功: Add(1, 1) got 2; expected 2
// calculate_test.go:22: 测试失败: Add(1, 1) got 2; expected 0
// calculate_test.go:25: 测试成功: Add(1, 1) got 2; expected 0
// calculate_test.go:25: 测试成功: Add(-1, 1) got 0; expected 0
// calculate_test.go:25: 测试成功: Add(9223372036854775807, 1) got -92 23372036854775808; expected -9223372036854775808
// --- FAIL: TestAdd (0.00s)
// FAIL
2.3. 压力测试
- 压力测试的测试函数命名为
BenchmarkXxxYyy,XxxYyy是被测试的函数名 - 压力测试的测试函数的形参列表必须是
(b *testing.B)
func BenchmarkSub(b *testing.B) {
// 一般压力测试只需要选一个开销最大的测试用例即可
testCase := struct{ a, b, expectedResult int }{
math.MaxInt, 1, math.MinInt,
}
// 重置压力测试的计时器, 以不计算上次测试用例的准备时间
b.ResetTimer()
for i := 0; i < b.N; i++ {
actualResult := Add(testCase.a, testCase.b)
if actualResult != testCase.expectedResult {
b.Errorf("测试失败: Add(%d, %d) got %d; expected %d\n",
testCase.a, testCase.b, actualResult, testCase.expectedResult)
}
}
}
// goos: windows
// goarch: amd64
// pkg: awesomeProject/test
// cpu: Intel(R) Core(TM) ix-xxxx CPU @ 5.40GHz
// BenchmarkSub
// BenchmarkSub-8 1000000000 0.3135 ns/op
// PASS
2.4. 示例测试
- 示例测试的测试函数命名为
ExampleXxxYyy,XxxYyy是被测试的函数名
示例测试的测试函数命名为ExampleAaa_Bbb,Bbb是被测试的方法名,Aaa是方法对应的接收者类型 - 用注释写上预期的输出
// Output: - 示例测试通过后,代码和预期输出会出现在
go doc生成的文档中
func ExampleAdd() {
fmt.Println(Add(1, 2))
fmt.Println(Add(-1, -1))
fmt.Println(Add(0, 0))
// Output:
// 3
// 0
// 0
}
输出:
=== RUN ExampleAdd
--- FAIL: ExampleAdd (0.00s)
got:
3
-2
0
want:
3
0
0
FAIL
func ExampleAdd() {
fmt.Println(Add(1, 2))
fmt.Println(Add(-1, -1))
fmt.Println(Add(0, 0))
// Output:
// 3
// -2
// 0
}
输出:
=== RUN ExampleAdd
--- PASS: ExampleAdd (0.00s)
PASS