Skip to content

《Go语言实战》入门实战系列 09:测试与性能——单元测试、基准测试与示例代码

约 2785 字大约 9 分钟

《Go语言实战》入门实战系列云原生

2026-04-01

开篇引导

在软件开发中,测试不仅是保障代码质量的最后一道防线,更是设计优良接口、提升代码可维护性的有力工具。Go语言从诞生之初就将测试作为语言的一部分,内置了testing包和go test命令,让开发者能够轻松地编写单元测试、基准测试和示例代码。无论你是在开发一个库还是一个完整的应用,掌握Go的测试技巧都是成为合格Go开发者的必经之路。

本篇将带你系统学习Go的测试体系:从基础的单元测试、表组测试,到使用httptest模拟网络调用,再到编写示例代码和基准测试。你将学会如何为你的代码建立完整的测试集,并通过基准测试找到性能瓶颈。学完本篇,你将能够自信地写出健壮、高效且文档完善的Go程序。

【本篇核心收获】

  • 掌握单元测试的编写规范:函数命名、文件命名、测试输出风格
  • 学会使用表组测试(table-driven tests)高效覆盖多组输入输出
  • 利用httptest包模拟HTTP请求和响应,实现无外部依赖的测试
  • 通过示例代码(Example函数)为包生成文档,并验证代码正确性
  • 编写基准测试(Benchmark函数)来评估代码性能,理解b.N-benchmem的含义
  • 掌握go test的常用选项:-v-run-bench-benchtime-benchmem

1. 单元测试

单元测试用于验证代码在特定场景下是否按预期工作。Go的测试工具go test会查找并执行所有以_test.go结尾的文件中的测试函数。

1.1 基础单元测试

一个基础单元测试函数必须以Test开头,接收一个*testing.T参数,无返回值。

代码清单1:基础单元测试示例

package listing01

import (
    "net/http"
    "testing"
)

const checkMark = "\u2713"
const ballotX = "\u2717"

func TestDownload(t *testing.T) {
    url := "http://www.goinggo.net/feeds/posts/default?alt=rss"
    statusCode := 200

    t.Log("Given the need to test downloading content.")
    {
        t.Logf("\tWhen checking \"%s\" for status code \"%d\"",
            url, statusCode)
        {
            resp, err := http.Get(url)
            if err != nil {
                t.Fatal("\t\tShould be able to make the Get call.",
                    ballotX, err)
            }
            t.Log("\t\tShould be able to make the Get call.",
                checkMark)

            defer resp.Body.Close()

            if resp.StatusCode == statusCode {
                t.Logf("\t\tShould receive a \"%d\" status. %v",
                    statusCode, checkMark)
            } else {
                t.Errorf("\t\tShould receive a \"%d\" status. %v %v",
                    statusCode, ballotX, resp.StatusCode)
            }
        }
    }
}

图1:基础单元测试的输出

关键点

  • 测试文件必须以_test.go结尾。
  • 测试函数名以Test开头。
  • t.Logt.Logf用于输出信息(仅在-v选项下显示)。
  • t.Fatalt.Fatalf用于报告致命错误并终止当前测试。
  • t.Errort.Errorf用于报告错误但继续执行。

图2:基础单元测试的输出细节

1.2 表组测试

当需要测试多组输入输出时,可以使用表组测试(table-driven tests)。表组测试将测试数据定义在一个切片中,然后循环执行。

代码清单2:表组测试示例

func TestDownload(t *testing.T) {
    var urls = []struct {
        url        string
        statusCode int
    }{
        {
            "http://www.goinggo.net/feeds/posts/default?alt=rss",
            http.StatusOK,
        },
        {
            "http://rss.cnn.com/rss/cnn_topstbadurl.rss",
            http.StatusNotFound,
        },
    }

    t.Log("Given the need to test downloading different content.")
    {
        for _, u := range urls {
            t.Logf("\tWhen checking \"%s\" for status code \"%d\"",
                u.url, u.statusCode)
            {
                resp, err := http.Get(u.url)
                if err != nil {
                    t.Fatal("\t\tShould be able to Get the url.",
                        ballotX, err)
                }
                t.Log("\t\tShould be able to Get the url",
                    checkMark)

                defer resp.Body.Close()

                if resp.StatusCode == u.statusCode {
                    t.Logf("\t\tShould have a \"%d\" status. %v",
                        u.statusCode, checkMark)
                } else {
                    t.Errorf("\t\tShould have a \"%d\" status. %v %v",
                        u.statusCode, ballotX, resp.StatusCode)
                }
            }
        }
    }
}

图3:表组测试的输出

表组测试的优点:

  • 增加新测试用例只需在表中添加数据,无需重复代码。
  • 清晰展示测试的输入和预期输出。

1.3 使用httptest模拟网络调用

依赖外部网络服务的测试不可靠且难以复现。net/http/httptest包允许我们模拟HTTP服务器,使测试脱离外部网络。

代码清单3:使用httptest模拟服务器

package listing12

import (
    "encoding/xml"
    "fmt"
    "net/http"
    "net/http/httptest"
    "testing"
)

const checkMark = "\u2713"
const ballotX = "\u2717"

var feed = `<?xml version="1.0" encoding="UTF-8"?>
<rss>
<channel>
    <title>Going Go Programming</title>
    <description>Golang : https://github.com/goinggo</description>
    <link>http://www.goinggo.net</link>
    <item>
        <pubDate>Sun, 15 Mar 2015 15:04:00 +0000</pubDate>
        <title>Object Oriented Programming Mechanics</title>
        <description>Go is an object oriented language.</description>
        <link>http://www.goinggo.net/2015/03/object-oriented</link>
    </item>
</channel>
</rss>`

func mockServer() *httptest.Server {
    f := func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(200)
        w.Header().Set("Content-Type", "application/xml")
        fmt.Fprintln(w, feed)
    }
    return httptest.NewServer(http.HandlerFunc(f))
}

func TestDownload(t *testing.T) {
    statusCode := http.StatusOK

    server := mockServer()
    defer server.Close()

    t.Log("Given the need to test downloading content.")
    {
        t.Logf("\tWhen checking \"%s\" for status code \"%d\"",
            server.URL, statusCode)
        {
            resp, err := http.Get(server.URL)
            if err != nil {
                t.Fatal("\t\tShould be able to make the Get call.",
                    ballotX, err)
            }
            t.Log("\t\tShould be able to make the Get call.",
                checkMark)

            defer resp.Body.Close()

            if resp.StatusCode != statusCode {
                t.Fatal("\t\tShould receive a \"%d\" status. %v %v",
                    statusCode, ballotX, resp.StatusCode)
            }
            t.Logf("\t\tShould receive a \"%d\" status. %v",
                statusCode, checkMark)
        }
    }
}

图4:没有互联网连接时测试仍能通过

模拟服务器的核心:

  • httptest.NewServer 创建一个本地HTTP服务器,其URL字段指向该服务器。
  • 传入的http.HandlerFunc会在每次请求时被调用,我们可以自定义响应。
  • 通过defer server.Close()确保服务器在测试结束后关闭。

1.4 测试服务端点

对于Web应用,我们经常需要直接测试处理函数(handler)而无需启动整个服务。httptest包也提供了httptest.NewRecorder来捕获响应。

代码清单4:测试服务端点的示例(handlers包)

package handlers

import (
    "encoding/json"
    "net/http"
)

func Routes() {
    http.HandleFunc("/sendjson", SendJSON)
}

func SendJSON(rw http.ResponseWriter, r *http.Request) {
    u := struct {
        Name  string
        Email string
    }{
        Name:  "Bill",
        Email: "bill@ardanstudios.com",
    }

    rw.Header().Set("Content-Type", "application/json")
    rw.WriteHeader(200)
    json.NewEncoder(rw).Encode(&u)
}

测试代码

package handlers_test

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "beth.com/goinaction/code/chapter9/listing17/handlers"
)

const checkMark = "\u2713"
const ballotX = "\u2717"

func init() {
    handlers.Routes()
}

func TestSendJSON(t *testing.T) {
    t.Log("Given the need to test the SendJSON endpoint.")
    {
        req, err := http.NewRequest("GET", "/sendjson", nil)
        if err != nil {
            t.Fatal("\tShould be able to create a request.", ballotX, err)
        }
        t.Log("\tShould be able to create a request.", checkMark)

        rw := httptest.NewRecorder()
        http.DefaultServeMux.ServeHTTP(rw, req)

        if rw.Code != 200 {
            t.Fatal("\tShould receive \"200\"", ballotX, rw.Code)
        }
        t.Log("\tShould receive \"200\"", checkMark)

        u := struct {
            Name  string
            Email string
        }{}

        if err := json.NewDecoder(rw.Body).Decode(&u); err != nil {
            t.Fatal("\tShould decode the response.", ballotX)
        }
        t.Log("\tShould decode the response.", checkMark)

        if u.Name == "Bill" {
            t.Log("\tShould have a Name.", checkMark)
        } else {
            t.Error("\tShould have a Name.", ballotX, u.Name)
        }

        if u.Email == "bill@ardanstudios.com" {
            t.Log("\tShould have an Email.", checkMark)
        } else {
            t.Error("\tShould have an Email.", ballotX, u.Email)
        }
    }
}

测试端点的关键步骤:

  1. 使用http.NewRequest创建请求。
  2. 使用httptest.NewRecorder创建响应记录器。
  3. 通过http.DefaultServeMux.ServeHTTP将请求交给默认的多路复用器处理(前提是已经注册了路由)。
  4. 检查响应记录器中的状态码、头信息和Body。

模块小结:单元测试是Go开发的基础,通过go test可以轻松运行。表组测试让测试更简洁,httptest则让我们能模拟外部依赖,保证测试的可靠性和可重复性。

2. 示例代码

Go的godoc工具支持从代码中的示例函数生成文档。示例函数不仅展示了如何使用API,还可以作为测试被go test执行。

示例函数的规则

  • 函数名以Example开头,后跟要展示的公开函数或类型名。
  • 示例函数可以包含一个结尾的// Output:注释,用来声明期望的标准输出。
  • 如果提供了Output:go test会捕获标准输出并与注释内容比较,不一致则测试失败。

代码清单5:示例代码(handlers_example_test.go)

package handlers_test

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "net/http/httptest"
)

func ExampleSendJSON() {
    r, _ := http.NewRequest("GET", "/sendjson", nil)
    rw := httptest.NewRecorder()
    http.DefaultServeMux.ServeHTTP(rw, r)

    var u struct {
        Name  string
        Email string
    }

    if err := json.NewDecoder(rw.Body).Decode(&u); err != nil {
        log.Println("ERROR:", err)
    }

    fmt.Println(u)
    // Output:
    // {Bill bill@ardanstudios.com}
}

图5:包json的示例代码列表

图6:Go文档里显示的Decoder示例视图

图7:handlers包的godoc视图

图8:在godoc里显示完整的示例代码

运行示例

go test -run ExampleSendJSON

图9:运行示例代码

如果输出与期望不匹配,go test会显示差异(图10)。

图10:示例运行失败

模块小结:示例代码是Go文档的重要组成部分,既能帮助用户理解API,又能作为测试验证代码的正确性。

3. 基准测试

基准测试用于测量代码的性能。基准测试函数以Benchmark开头,接收*testing.B参数,并在循环中执行待测代码,循环次数由b.N控制。

代码清单6:三种整数转字符串的基准测试

package listing28_test

import (
    "fmt"
    "strconv"
    "testing"
)

func BenchmarkSprintf(b *testing.B) {
    number := 10
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        fmt.Sprintf("%d", number)
    }
}

func BenchmarkFormat(b *testing.B) {
    number := int64(10)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        strconv.FormatInt(number, 10)
    }
}

func BenchmarkItoa(b *testing.B) {
    number := 10
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        strconv.Itoa(number)
    }
}

运行基准测试

go test -v -run="none" -bench="BenchmarkSprintf"

图11:运行单个基准测试

  • 输出中的5000000表示执行次数。
  • 258 ns/op表示每次操作耗时258纳秒。

使用-benchtime选项增加运行时间

go test -bench="BenchmarkSprintf" -benchtime=3s

图12:使用-benchtime选项运行基准测试

同时运行三个基准测试

go test -bench="."

图13:运行所有三个基准测试

查看内存分配情况

go test -bench="." -benchmem

图14:使用-benchmem选项运行基准测试

  • B/op:每次操作分配的字节数。
  • allocs/op:每次操作分配内存的次数。

通过基准测试,我们发现strconv.FormatInt最快,strconv.Itoa次之,fmt.Sprintf最慢且内存分配最多。这提示我们在性能敏感的场景下应优先使用strconv包。

模块小结:基准测试是性能优化的利器。通过-bench-benchtime-benchmem选项,我们可以精确评估代码的耗时和内存占用,从而做出正确的优化决策。

4. 本篇核心知识点速记

  • 单元测试
    • 文件名:*_test.go
    • 函数名:TestXxx(t *testing.T)
    • 输出:t.Logt.Errort.Fatal
  • 表组测试:定义测试数据表,循环执行,便于扩展。
  • httptest
    • httptest.NewServer:模拟HTTP服务器,返回*httptest.Server
    • httptest.NewRecorder:记录响应,用于测试handler。
  • 示例代码
    • 函数名:ExampleXxx
    • 可选// Output:注释,用于测试和文档。
  • 基准测试
    • 函数名:BenchmarkXxx(b *testing.B)
    • 循环使用b.N,通过b.ResetTimer()排除初始化时间。
    • 选项:-bench-benchtime-benchmem

文末小结

本章我们完整学习了Go语言的测试体系。单元测试保证了代码的正确性;表组测试让测试更加简洁高效;httptest让我们可以模拟网络依赖,实现可靠的测试;示例代码既提供了文档,又充当了测试;基准测试则帮助我们度量性能,找到最佳实现。通过go test的强大工具链,我们可以在开发过程中持续验证代码质量,并生成高质量的文档。

测试是编写可靠软件不可或缺的一环。在后续的实战项目中,请将测试作为开发流程的一部分,让测试驱动你的代码设计。