// +build ignore package main import "fmt" func main() { num := 3601 fmt.Println(toTime(num)) } func toTime(num int) string { return fmt.Sprintf("%d = %dh:%dm:%ds", num, (num / 3600), (num / 3600 / 60), (num % 60)) }其中 build 标签需要放到源代码文件的包声明语句头部,build 标签也可以使用逻辑运算符 &&、|| 和 ! 来组合多个条件,例如这表示只有在 Linux 的 amd64 架构上才能编译,但不是不能在 Darwin 的 386 和 arm64 架构上编译。
// +build linux,amd64 && !darwin && !386 && !arm64 package main import "syscall" func main() { syscall.Syscall(1, 0, 0, 0) // This is the "exit" syscall on Linux }最后使用 GOOS=linux GOARCH=amd64 go build 就可以生成对应的平台架构二进制文件。另外一个标签在范型特性没有出现之前使用的比较多,在源代码中包含 //go:generate 指令,我们可以在构建时执行一些自定义的命令或脚本,以自动生成一些代码或文件,例如在编译之前使用 go generate 就会生成了一个名字位 version.go 文件输出当前 Go 的 SDK 版本:
package main import "fmt" //go:generate sh -c "echo \"package main\n\nconst VERSION = \\\"$(go version | awk '{print $3}')\\\"\" > version.go" func main() { fmt.Println("Version:", VERSION) }还可以指定结构体自定义的名称,历史代码中结构体和函数都可以复用自定义名称,方便复用已经存在函数和代码,只是重新创建了一个别名:
type People struct { Name string `json:"name"` Age int `json:"age"` } // +person //go:generate some-code-generator --name=ToString --group=toStr func ToString() string { // dosomething }另外用的比较少的为 cgo 编程,使用 //go:cgo_import_dynamic 是 Go 中用于指定在 C 代码中导入动态链接库中的函数的名称、库名和符号的标签,例如:
package main import ( "unsafe" ) //go:noescape func Copy(dest, src []byte) int { n := len(src) if len(dest) < n { n = len(dest) } if n > 0 { copy(dest[:n], src[:n]) } return n } func main() { buf1 := make([]byte, 10) buf2 := make([]byte, 20) // 使用 Go 语言的 copy 函数复制切片 n := copy(buf1, buf2) println(n) // 使用 C 语言的 memcpy 函数复制内存 n = Copy(buf1, buf2) println(n) // 使用 unsafe 包将一个字符串转换为一个不可变的 []byte 切片 s := "hello" b := *(*[]byte)(unsafe.Pointer(&s)) println(string(b)) }在上面例子中,使用了 go:noescape 指令来确保 Copy 函数中的指针不会被意外地逃逸到堆上。因为 Copy 函数实际上是一个简单的内存复制函数,不需要对切片进行任何操作,所以使用 go:noescape 指令可以避免额外的内存分配和拷贝操作,提高性能和可靠性。
package main import "fmt" //go:noinline func myFunc(x int) int { return x + 1 } func main() { // 调用 myFunc 函数 10 次,并打印返回值 for i := 0; i < 10; i++ { fmt.Println(myFunc(i)) } }上面代码中我们在 fmt.Println 函数中调用了 myFunc 函数,并且循环了 10 次,智能编译器开启了函数内联优化,就会降低函数的调用的时候开销,具体是编译器实现的。如果我们不使用 //go:noinline 指令,并将 myFunc 函数内联到 main 函数中,那么编译器可能会优化掉大部分函数调用开销。但是由于我们禁用了内联优化,编译器将生成一个函数调用指令,每次调用 myFunc 函数时都会执行额外的逻辑,从而使得每个函数调用都有一些开销。
package main import ( "fmt" "os" "testing" ) func TestMain(m *testing.M) { // 执行一些初始化操作,例如设置测试环境 fmt.Println("TestMain setup") // 调用 m.Run() 运行所有测试 exitCode := m.Run() // 执行一些清理操作,例如关闭数据库连接 fmt.Println("TestMain teardown") // 退出测试,并使用 exitCode 作为退出状态码 os.Exit(exitCode) } func TestAddition(t *testing.T) { // 测试加法函数的行为 result := add(2, 3) if result != 5 { t.Errorf("Expected 5, but got %d", result) } } func add(a, b int) int { return a + b }而简单的单元测试可以使用 t *testing.T 进行测试,没有前置和后置条件,例如下面代码:
# go test -run=^TestAdditio$ func TestAddition(t *testing.T) { result := Add(2, 3) if result != 5 { t.Errorf("Addition failed. Expected 5 but got %d", result) } }上面的代码可以通过 go test 命令进行测试,如果有对过函数需要测试不必写多个函数名称,使用 -run 参数后面的字符串是一个正则表达式,^ 和 $ 表示匹配字符串的开头和结尾,上述命令将只运行名为 TestAddition 的测试用例。
package main import ( "math/rand" "testing" ) // 基准测试随机数生成器的性能 go test -bench=. func BenchmarkRand(b *testing.B) { for i := 0; i < b.N; i++ { rand.Int() } }单个测试用例不能有效的测试函数的功能,可以使用 t.Run 来测试多个条件情况下预期结果,来帮助完成代码健壮性测试:
func TestSum(t *testing.T) { type test struct { name string numbers []int expected int } tests := []test{ {name: "Test Sum Positive Numbers", numbers: []int{1, 2, 3, 4, 5}, expected: 15}, {name: "Test Sum Negative Numbers", numbers: []int{-1, -2, -3, -4, -5}, expected: -15}, {name: "Test Sum Mixed Numbers", numbers: []int{-1, 2, 3, -4, 5}, expected: 5}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { result := Sum(tc.numbers...) if result != tc.expected { t.Errorf("Sum(%v) = %d; want %d", tc.numbers, result, tc.expected) } }) } }上述代码中提供了多个参数多个测试条件看是否正常提供单元测试,称之为 Tables Test ,内置的 testing 包功能很多,可以查看观点的 API 文档 testing,另外的网络服务的话可以使用端点测试和调用测试,这些可以使用现有的 Postman 工具进行测试。
// Package mypack provides utility functions for performing various operations. package mypack // MaxRetryCount specifies the maximum number of times to retry a failed operation. var MaxRetryCount = 5 // IsZero returns true if the complex number is zero. func (c Complex) IsZero() bool { return c.real == 0 && c.imag == 0 }上面都是为一些常用代码注释格式,按照规范编写之后,使用内置的 go doc [package |symbol] 来查看对应的文档信息,例如 go doc fmt.Println 可以查看这个函数的作用。另外是为代码片段生产对应的文档信息,方便 pkg.go.dev 建立索引信息,要为代码编写 Example 的代码示例,从而让 go doc 来生产对应的 API 文档信息,例如编写一个简单 Add 函数:
package calculator import ( "fmt" "os" ) // Add returns the sum of two integers. func Add(a, b int) int { return a + b } // ExampleAdd demonstrates how to use Add. func ExampleAdd() { fmt.Println(Add(2, 3)) // Output: 5 } func main() { doc := `Package calculator provides basic math operations.` fmt.Fprintln(os.Stdout, doc) }
在 ExampleAdd 函数中编写了关于如何使用 Add 函数的示例,并使用 fmt.Println 打印出了 Add(2, 3) 的结果。在 // Output: 注释下面,我们写下了期望的输出值(即 5),这个注释是 Go 的一个特殊注释,它与示例函数一起使用,可以用于验证示例是否正确,就可以使用 go doc --package 命令来生成对应注释文档了。
package math func init() { Math := make(map[string]func(n, m int) int, 4) Math["add"] = Add Math["sub"] = Sub Math["multi"] = Multi Math["div"] = Div } func Add(n, m int) int { return n + m } func Sub(n, m int) int { return n - m } func Multi(n, m int) int { return n * m } func Div(n, m int) int { return n / m }对应的测试代码:
package math_test import "testing" func TestMath(t *testing.T) { // 自定义测试结构体 type MathCase struct { n, m, result int } // 自定义子测试map testGroup := map[string]MathCase{ "add": {1, 2, 3}, "sub": {3, 1, 2}, "multi": {3, 2, 6}, "div": {6, 2, 3}, } // 测试执行函数 for name, mathCase := range testGroup { t.Run(name, func(t *testing.T) { s := -1 switch name { case "add": s = Add(mathCase.n, mathCase.m) case "sub": s = Sub(mathCase.n, mathCase.m) case "multi": s = Multi(mathCase.n, mathCase.m) case "div": s = Div(mathCase.n, mathCase.m) default: t.Fatalf("No executable testing name :%s", name) } if mathCase.result != s { t.Fatalf(" add computer result error, want %d , got %d", mathCase.result, s) } }) } }整个测试覆盖率支持生成 html 报表的方式进行展示,需生成对应的 cover 文件,如何通过文件生成对应 html 通过浏览器浏览,通过go test -cover -coverprofile=cover.out 的 coverprofile 参数用来将覆盖率相关的记录信息输出到一个文件,在使用 go tool cover -html=cover.out ,结果如下图:
package main import ( "log" "net/http" _ "net/http/pprof" ) func main() { // 在主程序的某处开启 pprof 监听,端口号为 6060 log.Println("start pprof server...") go func() { err := http.ListenAndServe("localhost:6060", nil) if err != nil { log.Fatalln("pprof server start failed: ", err) } }() // 程序的其他逻辑代码 ... }在代码中添加需要分析的代码段,代码中的 runtime/pprof 包提供了自定义 profile 的方法:
package main import ( "fmt" "os" "runtime/pprof" ) func main() { // ... // 堆代码 duidaima.com // 创建一个 CPU profile 文件 f, err := os.Create("cpu.prof") if err != nil { panic(err) } defer f.Close() // 开始 CPU profiling if err := pprof.StartCPUProfile(f); err != nil { panic(err) } defer pprof.StopCPUProfile() // 需要分析的代码 for i := 0; i < 100000; i++ { fmt.Println(i) } // ... }运行应用程序,运行应用程序后,可以在浏览器中打开 http://localhost:6060/debug/pprof/ 来查看当前的 profiling 数据,这些数据包括 Goroutine、堆内存、堆内存分配、阻塞 goroutine 等等,例如下面有一段简短问题代码,可以使用这个工具进行分析:
package main import ( "flag" "fmt" "os" "runtime/pprof" "time" ) // 一段有问题的代码 func logicCode() { var c chan int for { select { case v := <-c: fmt.Printf("recv from chan, value:%v\n", v) default: } } } func main() { var isCPUPprof bool var isMemPprof bool flag.BoolVar(&isCPUPprof, "cpu", false, "turn cpu pprof on") flag.BoolVar(&isMemPprof, "mem", false, "turn mem pprof on") flag.Parse() if isCPUPprof { file, err := os.Create("./cpu.pprof") if err != nil { fmt.Printf("create cpu pprof failed, err:%v\n", err) return } pprof.StartCPUProfile(file) defer pprof.StopCPUProfile() } for i := 0; i < 8; i++ { go logicCode() } time.Sleep(20 * time.Second) if isMemPprof { file, err := os.Create("./mem.pprof") if err != nil { fmt.Printf("create mem pprof failed, err:%v\n", err) return } pprof.WriteHeapProfile(file) file.Close() } }可以看到上面的那个 select 代码块里面的 channel 没有初始化,会一直阻塞着这样一个简单的代码可以通过我们肉眼出来了,但是如果是复杂的我们就需要使用 go tool pprof cpu.pprof 工具来分析具体错误在哪里,分析结果如下:
$: go tool pprof cpu.pprof Type: cpu Time: May 16, 2020 at 12:59pm (CST) Duration: 20.14s, Total samples = 56.90s (282.48%) Entering interactive mode (type "help" for commands, "o" for options) (pprof) top3 Showing nodes accounting for 51.91s, 91.23% of 56.90s total Dropped 7 nodes (cum <= 0.28s) Showing top 3 nodes out of 4 flat flat% sum% cum cum% 23.04s 40.49% 40.49% 45.18s 79.40% runtime.selectnbrecv 17.26s 30.33% 70.83% 18.93s 33.27% runtime.chanrecv 11.61s 20.40% 91.23% 56.83s 99.88% main.logicCode (pprof) list logicCode Total: 56.90s ROUTINE ======================== main.logicCode in /Users/ding/Documents/GO_CODE_DEV/src/Lets_Go/lets_37_pprof/main.go 11.61s 56.83s (flat, cum) 99.88% of Total . . 16:// 一段有问题的代码 . . 17:func logicCode() { . . 18: var c chan int . . 19: for { . . 20: select { 11.61s 56.83s 21: case v := <-c: . . 22: fmt.Printf("recv from chan, value:%v\n", v) . . 23: default: . . 24: . . 25: } . . 26: } (pprof)通过 go 的 pprof 就已经分析出来了包为 main 中的 main.logicCode 函数存在一些问题,cpu 资源占用过多,如何对症下药就可以解决这些问题了。