在go语言中调试程序

本文涉及的产品
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
应用实时监控服务-应用监控,每月50GB免费额度
可观测可视化 Grafana 版,10个用户账号 1个月
简介: 【6月更文挑战第29天】Go语言内置`testing`包支持单元测试、基准测试和模糊测试。`go test`命令可执行测试,如`-run`选择特定测试,`-bench`运行基准测试,`-fuzz`进行模糊测试。

前言

我们都知道,越早发现问题越早纠正,那么程序的成本将更小。
每个语言都有针对自身代码的单元测试框架,这里简单做些对比介绍。
go简化了并发程序调试,goroutines是轻量级线程,动态调整栈大小。
测试函数以Test开头,
基准测试用Benchmark
模糊测试用Fuzz
TestMain用于自定义测试设置。
ReportMetric添加自定义基准性能度量。
RunParallel用于并行基准测试。

mandala曼德罗符号.png

1 go 的内建框架: 调试并发程序

go是 多线程的, 也就是基于消息传递的并发。
多线程和基于消息的并发编程是等价的。 多线程并发模型可以容易对应到多核处理器,主流操作系统也提供了系统级的多线程支持。

go 语言是基于消息并发模型的集大成者,它将基于CSP 模型的并发编程内置到语言中,通过一个go关键字 轻易启动 一个例程。 并且共享内存。

可以说 goroutine是 用户态的 ,与 系统线程不是等价的,也可以说它是一个轻量级的线程。

每个系统线程都有一个固定大小的栈,一般默认2MB,这个栈大小导致了两个问题:一个对于很多只需要很小的栈空间的线性是巨大浪费,二是对于少数需要巨大栈空间的线程又面临栈溢出的风险。

因此要么降低固定栈大小,提升空间利用率,要么增大栈大小允许更深的函数递归调用,但这无法兼有。

goroutine 以一个很小的栈启动 可能是 2KB 4KB,遇到深度递归导致栈空间不足时。
goroutine 根据需要动态伸缩栈大小。 最大可达 1GB

启动代价很小,就如调用函数一样简单,而例程之间的调度代价也很低,而伸缩空间很大,这极大促进了并发编程的流行和发展。

其调度使用半抢占式调度。调度器根据具体函数只保留必要寄存器,切换的代价比系统线程低得多。 运行时有一个 runtime.GOMAXPROCS 变量,用于控制当前允许正常非阻塞 例程的系统线程数目。

并发程序基准测试在其他语言中可能是个麻烦事情,在golang中,内建单测包提供了丰富的支持。

2 内置 测试框架 testing

基于xUnit套件层次结构,顺序结构 用于组织测试代码之间的结构。
那么在测试用例代码内部其一般逻辑是怎样的?

go测函数就是一个普通的go函数,仅对测试函数的函数名和函数原型有特定要求, 在测试函数TestXXX会其子测试函数 subtest, 如何执行测试逻辑并没有显式约束。

对失败与否的判断在于测试代码逻辑是否进入了包含 Error/Errorf, Fatal/Fatalf 等方法调用
一旦进入这些分支,即代表该测试失败。

不同的是 error/Errorf 并不会立刻终止当前 goroutine 的执行,还会继续执行 例程的后续测试。
而Fatal/Fatalf将立刻结束当前例程的执行。

这里介绍testing功能和代码结构。
内置的性能基准数据,它有内置的pprof 和 expvar 包支持导出导入go应用的数据,有接口或json的方式可用。

基准性能测试 形式如下

  func BenchmarkXxx(* testing.B)

被视为基准测试,并在提供其 -bench 标志时由“go test”命令执行。基准测试按顺序运行。

示例基准函数如下所示:

func BenchmarkRandInt(b *testing.B) {
    for i := 0;i < bN; i++ {
        rand.Int()
    }
}

3 基准结果 ReportMetric testing.B

ReportMetric 将“n unit”添加到报告的基准测试结果中。
如果度量是每次迭代,调用者应该除以 bN,并且按照惯例,输出的报告以“/op”结尾。

ReportMetric 覆盖任何先前报告的相同单位的值。如果 unit 是空字符串或 unit 包含任何空格,则 ReportMetric 会发生混乱。

如果单位是基准框架本身通常报告的单位(例如“allocs/op”),ReportMetric 将覆盖该指标。
将“ns/op”设置为 0 将抑制该内置指标。

如下这个 排序的例子提供了一个报告与特定算法相关的自定义基准性能度量,它之间调用包内的函数:

b.ReportMetric 它除以 b.N 得到平均值,并输出到执行报告的 /op 单元.

  • 基准测试例子

     package main
    
       import (
         "sort"
         "testing"
       )
    
       func main() {
          
         testing.Benchmark(func(b * testing.B) {
           var compares int64
           for i := 0; i < b.N; i++ {
             s := []int{5, 4, 3, 2, 1}
             sort.Slice(s, func(i, j int) bool {
               compares++
               return s[i] < s[j]
             })
           }
            
           b.ReportMetric(float64(compares)/float64(b.N), "compares/op")
         })
       }
    

    基准函数必须运行目标代码 bN 次。在基准执行期间,调整 bN 直到基准函数持续足够长的时间以可靠地计时

  • 并行基准测试 RunParallel testing.PB

    RunParallel 并行运行基准测试。它创建多个 goroutines 并在它们之间分配 bN 次迭代。
    goroutines 的数量默认为 GOMAXPROCS。

    要增加非 CPU 绑定基准测试的并行性,请在 RunParallel 之前调用 SetParallelism 。

    RunParallel 通常与 go test -cpu 标志一起使用。

    如下例子在单个对象提供 text/template.Template.Execute
    RunParallel 将长久 GOMAXPROCS 例程并分散任务到它们。 每个例程有自己的字节缓冲。

    for pb.Next() 循环体在全部例程中共被执行 b.N次

     package main
    
     import (
       "bytes"
       "testing"
       "text/template"
     )
    
     func main() {
        
       testing.Benchmark(func(b * testing.B) {
         templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
          
         b.RunParallel(func(pb * testing.PB) { 
           var buf bytes.Buffer
           for pb.Next() { 
             buf.Reset()
             templ.Execute(&buf, "World")
           }
         })
       })
     }
    

主体函数将在每个 goroutine 中运行。它应该设置任何 goroutine-local 状态,然后迭代直到 pb.Next 返回 false。它不应使用 StartTimer、StopTimer 或 ResetTimer 函数,因为它们具有全局作用。它也不应该调用 Run。

4 模糊测试 FuzzXxx testing.F 和 主要测试 testing.M

  • 模拟测试

    “go test”和测试包支持模糊测试,这是一种测试技术,其中使用随机生成的输入调用函数以查找单元测试未预料到的错误。

功能函数定义形式:

    func FuzzXxx(* testing.F)

被认为是模糊测试。

  • 主要测试 TestMain testing.M

    测试或基准测试程序有时需要在执行之前或之后进行额外的设置或拆卸。
    有时还需要控制哪些代码在主线程上运行。为了支持这些和其他情况,如果测试文件包含一个函数:

    func TestMain(m * testing.M)
    那么生成的测试将调用 TestMain(m) 而不是直接运行测试或基准测试。

    TestMain 在主 goroutine 中运行,并且可以围绕对 m.Run 的调用进行任何必要的设置和拆卸。

    m.Run 将返回一个可以传递给 os.Exit 的退出代码。如果 TestMain 返回,测试包装器会将 m.Run 的结果传递给 os.Exit 本身。

    调用 TestMain 时,flag.Parse 尚未运行。如果 TestMain 依赖于命令行标志,包括测试包的标志,它应该显式调用 flag.Parse。命令行标志始终由运行的时间测试或基准函数解析。

    TestMain 的一个简单实现是:

       func TestMain(m * testing.M) {
         // 如果 TestMain 使用标志,则在此处调用 flag.Parse()
         os.Exit(m.Run())
       }
    

    TestMain 是一个低级原语,对于临时测试需求来说不是必需的,普通测试函数就足够了。

5 组织 testing 包

go的testing包为了给自动化测试提供支持。
它旨在与“go test”命令一起使用,该命令自动执行表单的任何功能和基准测试。

其中 Xxx 不以小写字母开头。函数名称用于标识测试例程。

在这些功能中,使用 Error、Fail 或相关方法来指示失败。

要编写新的测试套件,请创建一个名称以 _ test.go 结尾的文件,其中包含此处所述的 TestXxx 函数。
将文件放在与被测文件相同的包中。

该文件将从常规包构建中排除,但会在运行“go test”命令时包含在内。
有关更多详细信息,请运行“go help test”和“go help testflag”。

一个简单的测试函数如下所示:

func TestAbs(t * testing.T) {
    hava := Abs(-1)
    if have != 1 {
        t.Errorf("Abs(-1) = %d; need 1", hava)
    }
}

由于go 的特征,它的测试一般封装为 模块 demo_test.go --> 函数 Context, 一些常用的特殊的测试关键字 如下”

测试函数

   TestXxxx()

测试函数的 套件设置和销毁

   Setup()
   TestCase()
   TearDown()

测试套件的 设置和销毁

   Setup()
   TestSuite()
   TearDown()
   

包级别的 设置和销毁

   pkgSetup()
   pkgTearDown()

支持回收测试套件设置 对于 包级别的测试固件的创建和销毁有了正式的原生支持 >go 1.14

   TestMain()    
   

6 使用方式:

  • 运行全部测试

      go test -run ''  
    
  • 运行匹配“Foo”的顶级测试,例如“TestFooBar”。

      go test -run Foo  
    
  • 对于匹配“Foo”的顶级测试,运行匹配“A=”的子测试。

      go test -run Foo/A= 
    
  • 对于所有顶级测试,运行匹配“A=1”的子测试。

      go test -run /A=1  
    
  • 模糊匹配“FuzzFoo”的目标

      go test -fuzz FuzzFoo   
    

** 例子:

设置测试套件

  func suiteSetup(suiteName string) func() {
      fmt.Printf("\tsetUp fixture for suite %v \n", suiteName)
      return func() {
        fmt.Printf("\ttearDown fixture for suite %v\n", suiteName)
      }
    }

设置测试用例

    func funcOneTestCase(t * testing.T) {
      fmt.Printf("\t\tExecute test:%v\n", t.Name())
    }

    func TestFuncOne(t * testing.T) {
      t.CleanUp(suiteSetUp(t.Name()))
      t.Run("testcase1", funcOneTestCase1)
    }

用例的函数

     func funcTwoTestCase(t * testing.T) {
      fmt.Printf("\t\tExecute test:%v\n", t.Name())
    }

执行清理和用例

    func TestFuncTwo(t * testing.T) {
      t.CleanUp(suiteSetUp(t.Name()))
      t.Run("testcase1", funcTwoTestCase1)
    }

包初始化

  func pkgSetUp(pkgName string) func() {
    fmt.Printf("package SetUp fixture for %v\n", pkgName)
    return func() {
      fmt.Printf("package TearDown fixture for %v\n", pkgName)
    }
  }

控制哪些代码在主例程上运行

  func TestMain(m * testing.M) {
    defer pkgSetUp("package demo_test")()
    m.Run()
  }

7 小结

  • go内置的 testing 包,其测试方式为:

    1  测试用例封装在普通go函数中,
    2 针对给定的输入数据,比较被测试函数,方法返回的实际结果值和预期值,
    若有差异,则通过testing 包提供的函数输出差异信息。
    
    3 失败断定为进入  Error/Errorf(退出当前用例), 
    Fatal/Fatalf(立刻退出测试主进程), 
    当函数进入 这些地方将被记录在testing的输出报告中。
    
    4 go 内置了不少特别的测试方式,如性能基准,输出接口,模糊测试,关键测试等等。
    

更多信息:

http://docs.python.org/library/unittest.html

目录
相关文章
|
4月前
|
Kubernetes Linux Go
使用 Uber automaxprocs 正确设置 Go 程序线程数
`automaxprocs` 包就是专门用来解决此问题的,并且用法非常简单,只需要使用匿名导入的方式 `import _ "go.uber.org/automaxprocs"` 一行代码即可搞定。
232 78
|
7月前
|
编译器 Go
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。
|
7月前
|
运维 监控 算法
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
|
1月前
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。
|
1月前
|
数据采集 JSON Go
Go语言实战案例:实现HTTP客户端请求并解析响应
本文是 Go 网络与并发实战系列的第 2 篇,详细介绍如何使用 Go 构建 HTTP 客户端,涵盖请求发送、响应解析、错误处理、Header 与 Body 提取等流程,并通过实战代码演示如何并发请求多个 URL,适合希望掌握 Go 网络编程基础的开发者。
|
2月前
|
JSON 前端开发 Go
Go语言实战:创建一个简单的 HTTP 服务器
本篇是《Go语言101实战》系列之一,讲解如何使用Go构建基础HTTP服务器。涵盖Go语言并发优势、HTTP服务搭建、路由处理、日志记录及测试方法,助你掌握高性能Web服务开发核心技能。
|
2月前
|
Go
如何在Go语言的HTTP请求中设置使用代理服务器
当使用特定的代理时,在某些情况下可能需要认证信息,认证信息可以在代理URL中提供,格式通常是:
211 0
|
3月前
|
JSON 编解码 API
Go语言网络编程:使用 net/http 构建 RESTful API
本章介绍如何使用 Go 语言的 `net/http` 标准库构建 RESTful API。内容涵盖 RESTful API 的基本概念及规范,包括 GET、POST、PUT 和 DELETE 方法的实现。通过定义用户数据结构和模拟数据库,逐步实现获取用户列表、创建用户、更新用户、删除用户的 HTTP 路由处理函数。同时提供辅助函数用于路径参数解析,并展示如何设置路由器启动服务。最后通过 curl 或 Postman 测试接口功能。章节总结了路由分发、JSON 编解码、方法区分、并发安全管理和路径参数解析等关键点,为更复杂需求推荐第三方框架如 Gin、Echo 和 Chi。
|
4月前
|
分布式计算 Go C++
初探Go语言RPC编程手法
总的来说,Go语言的RPC编程是一种强大的工具,让分布式计算变得简单如同本地计算。如果你还没有试过,不妨挑战一下这个新的编程领域,你可能会发现新的世界。
106 10
|
7月前
|
存储 Go
Go 语言入门指南:切片
Golang中的切片(Slice)是基于数组的动态序列,支持变长操作。它由指针、长度和容量三部分组成,底层引用一个连续的数组片段。切片提供灵活的增减元素功能,语法形式为`[]T`,其中T为元素类型。相比固定长度的数组,切片更常用,允许动态调整大小,并且多个切片可以共享同一底层数组。通过内置的`make`函数可创建指定长度和容量的切片。需要注意的是,切片不能直接比较,只能与`nil`比较,且空切片的长度为0。
173 3
Go 语言入门指南:切片

热门文章

最新文章