Go 错误处理与测试 | 青训营笔记

106 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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)过来。

没有 recoverfmt.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 是以表格驱动测试为宗旨的。

  1. 分离测试数据和测试逻辑
  2. 明确的出错信息
  3. 可以部分失败:某个测试用例失败,不影响后续测试的进行
  4. Go 的语法使我们更容易实现表格驱动测试

Go testing

  • 测试源文件命名为 xxx_test.goxxx 是被测试的源文件名
  • 运行测试:
    # 若所以测试都正确, 则不输出日志; 有错误才测试输出
    go test
    # 都输出测试日志
    go test -v
    
    # 运行单个测试源文件
    go test 测试源文件 被测试源文件
    # 运行单个测试函数
    go test -test.run 测试函数名
    

2.1. 单元测试

  • 单元测试的测试函数命名为 TestXxxYyyXxxYyy 是被测试的函数名
  • 单元测试的测试函数的形参列表必须是 (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.ErrorfTB.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. 压力测试

  • 压力测试的测试函数命名为 BenchmarkXxxYyyXxxYyy 是被测试的函数名
  • 压力测试的测试函数的形参列表必须是 (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. 示例测试

  • 示例测试的测试函数命名为 ExampleXxxYyyXxxYyy 是被测试的函数名
    示例测试的测试函数命名为 ExampleAaa_BbbBbb 是被测试的方法名,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