Go语言补充

函数的一等公民特性

函数本身可以作为变量

匿名函数 闭包

函数可以满足接口

将函数作为参数传入有什么意义

在 Go 语言中,将函数作为参数传入另一个函数(高阶函数)可以让代码更加灵活、可复用、符合开闭原则。这样做的意义主要包括:

🌟 1. 代码更加通用

如果一个函数的某个逻辑部分可能变化,而其他部分保持不变,我们可以把变化的部分作为参数传递,从而使代码更加通用。

🎯 示例:排序逻辑作为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"fmt"
"sort"
)

// 自定义比较函数
func Ascending(a, b int) bool {
return a < b
}

func Descending(a, b int) bool {
return a > b
}

// 使用函数作为参数,接收不同的排序逻辑
func SortNumbers(arr []int, compare func(int, int) bool) {
sort.Slice(arr, func(i, j int) bool {
return compare(arr[i], arr[j])
})
}

func main() {
numbers := []int{5, 3, 8, 1, 2}

// 升序排序
SortNumbers(numbers, Ascending)
fmt.Println("Ascending:", numbers)

// 降序排序
SortNumbers(numbers, Descending)
fmt.Println("Descending:", numbers)
}

✅ 代码解读

  • SortNumbers 是一个通用的排序函数,它不关心具体排序规则,而是把比较逻辑作为参数传入。
  • AscendingDescending 定义了不同的排序规则,可以自由切换。
  • 好处: 如果以后要支持新的排序方式,我们只需传入新的比较函数,而无需修改 SortNumbers

🌟 2. 代码更简洁(避免重复)

高阶函数可以减少重复代码,使代码更加精简。

🎯 示例:不同操作传递给 ApplyOperation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

// 定义一个函数,接收一个操作函数作为参数
func ApplyOperation(a, b int, operation func(int, int) int) int {
return operation(a, b)
}

func main() {
// 定义不同的操作
add := func(x, y int) int { return x + y }
subtract := func(x, y int) int { return x - y }
multiply := func(x, y int) int { return x * y }

// 直接传入不同的函数,避免重复代码
fmt.Println("Addition:", ApplyOperation(5, 3, add))
fmt.Println("Subtraction:", ApplyOperation(5, 3, subtract))
fmt.Println("Multiplication:", ApplyOperation(5, 3, multiply))
}

✅ 代码解读

  • ApplyOperation 是一个通用的数学运算函数,它可以接受任意运算函数(加、减、乘等)。
  • 这样,我们可以动态传入不同的运算逻辑,避免写多个 Add, Subtract 等函数,提高代码复用性

🌟 3. 延迟执行(回调函数)

有时候,我们希望一个函数在某个事件发生后才执行(比如 HTTP 请求返回、定时任务等),可以使用回调函数实现。

🎯 示例:执行完某个任务后回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

// 定义一个任务执行器,执行完任务后调用回调函数
func DoSomething(task string, callback func()) {
fmt.Println("Doing task:", task)
callback() // 任务完成后执行回调
}

func main() {
DoSomething("Downloading file", func() {
fmt.Println("Download complete!")
})

DoSomething("Processing data", func() {
fmt.Println("Processing complete!")
})
}

✅ 代码解读

  • DoSomething 先执行任务,然后调用回调 callback()
  • main 中,每次调用 DoSomething 时,我们传入不同的回调函数,让任务结束后执行不同的逻辑。
  • 好处: 这样可以动态决定任务完成后的行为,而不需要修改 DoSomething 的代码。

🌟 4. 用于 map/filter/reduce(函数式编程风格)

Go 没有内置 map, filter, reduce,但可以用函数作为参数实现这些功能。

🎯 示例:使用 map 处理切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

// `Map` 函数,接收一个转换函数 `f`
func Map(arr []int, f func(int) int) []int {
result := make([]int, len(arr))
for i, v := range arr {
result[i] = f(v)
}
return result
}

func main() {
numbers := []int{1, 2, 3, 4, 5}

// 传入不同的函数,实现不同的转换
squared := Map(numbers, func(n int) int { return n * n })
double := Map(numbers, func(n int) int { return n * 2 })

fmt.Println("Squared:", squared) // [1, 4, 9, 16, 25]
fmt.Println("Doubled:", double) // [2, 4, 6, 8, 10]
}

✅ 代码解读

  • Map 是一个通用的映射函数,它不关心具体变换逻辑,而是把转换函数 f 作为参数传入。
  • 这样,我们可以用 Map 进行不同的转换(平方、翻倍等),而不需要写多个 SquareAll, DoubleAll 等函数。

🌟 5. 用于 http.HandlerFunc(Web 开发)

在 Go 的 net/http 包中,http.HandlerFunc 本质上就是一个函数作为参数的应用。

🎯 示例:自定义 HTTP 处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"net/http"
)

// 定义一个通用的处理函数
func HelloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, world!")
}

func main() {
// `http.HandleFunc` 允许我们直接传入函数
http.HandleFunc("/", HelloHandler)

fmt.Println("Server is running at <http://localhost:8080>")
http.ListenAndServe(":8080", nil)
}

✅ 代码解读

  • http.HandleFunc 允许我们直接传入一个函数,而不是要求定义一个完整的 struct
  • 这让代码更加简洁,灵活地传递不同的处理逻辑

🚀 6. 总结:为什么要用函数作为参数?

目的 例子 好处
让代码更通用 排序函数 (SortNumbers) 代码适应不同逻辑,复用性高
减少重复代码 ApplyOperation(a, b, func) 避免写多个类似函数
延迟执行(回调) DoSomething(task, callback) 任务完成后执行不同逻辑
实现map/filter/reduce Map(arr, func) 模拟函数式编程
Web 服务器处理请求 http.HandleFunc("/", handler) 代码更简洁

👉 结论函数作为参数可以提高代码复用性,减少重复,让代码更灵活、更易维护! 🚀😃

函数闭包究竟是什么,有什么用

🔹 Go 语言的闭包(Closure)

在 Go 语言中,闭包(closure) 是指一个函数可以捕获并“记住”其外部作用域中的变量,即使外部作用域已经结束,变量仍然存在

闭包通常是在返回一个匿名函数时创建的,这个匿名函数可以访问外部作用域的变量,即使外部函数已经执行完毕。


🎯 1. 基本示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

func counter() {
func() int {
count := 0 // 外部变量
return func() int {
count++ // 修改外部变量
return count
}
}

func main() {
// 创建闭包
c1 := counter()
fmt.Println(c1()) // 输出 1
fmt.Println(c1()) // 输出 2
fmt.Println(c1()) // 输出 3

// 创建新的闭包,具有独立的 `count`
c2 := counter()
fmt.Println(c2()) // 输出 1
}

✅ 代码解析

  • counter() 返回一个匿名函数,该匿名函数**可以访问并修改 count**。
  • c1c2 是两个独立的闭包,每个都有自己的 count 变量,互不影响。
  • 这样,闭包可以保存状态,即使 counter() 执行结束,count 变量仍然保留。

🔹 2. 为什么使用闭包?有什么好处?

📌 2.1 维持状态

闭包可以用来保持状态信息,即使外部函数已经返回,状态依然保留。这比在全局变量中存储数据更加安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func accumulator(start int) func(int) int {
sum := start
return func(x int) int {
sum += x
return sum
}
}

func main() {
acc := accumulator(10)
fmt.Println(acc(5)) // 输出 15
fmt.Println(acc(10)) // 输出 25
}

  • sum 变量在 acc 闭包中持续存在,并在每次调用时累加。
  • 优势:比全局变量更安全,不会被其他代码修改。

📌 2.2 作为回调函数

闭包可以作为回调函数,用于事件处理、异步编程等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func makeMultiplier(factor int) func(int) int {
return func(n int) int {
return n * factor
}
}

func main() {
times2 := makeMultiplier(2)
times3 := makeMultiplier(3)

fmt.Println(times2(5)) // 输出 10
fmt.Println(times3(5)) // 输出 15
}

  • times2 生成一个乘 2 的闭包times3 生成一个乘 3 的闭包,它们各自**“记住”了 factor*。
  • 这种方式可以动态生成不同的行为,比写多个函数更灵活。

📌 2.3 避免全局变量污染

全局变量容易导致命名冲突和数据污染,而闭包提供了一种封装数据的方式,使变量仅在特定作用域内有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func generator() func() int {
counter := 0
return func() int {
counter++
return counter
}
}

func main() {
gen1 := generator()
gen2 := generator()

fmt.Println(gen1()) // 1
fmt.Println(gen1()) // 2
fmt.Println(gen2()) // 1 (独立的闭包)
}

  • gen1gen2 各自维护了 counter 变量,避免了全局变量的冲突。

📌 2.4 用于装饰器模式

闭包可以用来增强已有函数的功能,类似于装饰器模式(Decorator Pattern)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

// 计时器装饰器
func withLogging(f func(int) int) func(int) int {
return func(n int) int {
fmt.Println("Calling function with argument:", n)
result := f(n)
fmt.Println("Function returned:", result)
return result
}
}

func square(n int) int {
return n * n
}

func main() {
loggedSquare := withLogging(square)
fmt.Println(loggedSquare(5))
}

✅ 代码解析

  • withLogging()接受一个函数 f 并返回一个新的带日志功能的函数。
  • 这种方式可以动态增强函数的行为,而不改变原始函数。

🔹 3. 什么时候不适合用闭包?

虽然闭包很强大,但也有一些潜在的问题:

  1. 可能导致内存泄漏

    • 如果闭包变量持有大量数据,但迟迟不被释放,会导致内存泄漏。
    • 例如,长期运行的 Goroutine 里如果持有闭包变量,可能会占用大量内存。
  2. 变量共享问题

    • 如果多个 Goroutine 访问同一个闭包变量,可能会出现并发安全问题,需要使用 sync.Mutexsync/atomic 保护。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package main

    import (
    "fmt"
    "sync"
    )

    func counter() func() int {
    var mu sync.Mutex
    count := 0
    return func() int {
    mu.Lock()
    defer mu.Unlock()
    count++
    return count
    }
    }

    func main() {
    c := counter()
    fmt.Println(c())
    }

    解决方案:加锁保证线程安全。


🚀 4. 总结

特点 描述
定义 闭包是一个能够捕获外部变量的函数,即使外部作用域结束,变量仍然存在。
用途 1️⃣ 维持状态(如计数器) 2️⃣ 作为回调函数 3️⃣ 避免全局变量污染 4️⃣ 用于装饰器模式
好处 1️⃣ 代码更简洁 2️⃣ 变量封装更安全 3️⃣ 适用于动态逻辑
注意点 1️⃣ 可能导致内存泄漏2️⃣并发访问需加锁

👉 结论:闭包让 Go 语言更具灵活性,尤其适合 状态管理、函数式编程、回调函数、动态行为增强 等场景。但要注意内存管理并发问题


🚀 你可以将闭包理解为一个“记住”变量的函数,让代码更优雅、更灵活! 🎯😃

go的error设计理念

开发函数必须要有一个返回值去告诉调用者是否成功,go语言要求我们必须处理这个err

go设计者认为我们必须处理这个error,而不是单纯向上抛

recover和panic如何正确使用

panic会直接导致程序退出,在开发里要用的时候应该如下

比如服务器启动的时候,要把依赖服务完全准备好,如果其中任何一个没启动再主动用panic

一旦程序启动但是不小心使用panic会导致程序挂了

可是有时候会不小心触发panic,比如声明了个map但是没有初始化就使用,就会出panic,这时候就用recover捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println(r)
}
}()
var names map[string]string
names["go"] = "golang"

return
}

鸭子类型

当一只鸟看起来像鸭子,叫起来像鸭子,游起来也像鸭子,那就是鸭子,鸭子类型强调的事物的外部行为,而不是内部的结构

go语言中处处都是interface

接口到底是什么

接口是struct更高一层的抽象

具体逻辑是这样的

一个东西只要会汪汪叫,会走,就是狗(interface定义的行为)

一只具体的哈巴狗,是有腿和皮毛和名字的(struct定义的属性)

而一只现实里的狗,是有具体的属性的

var d Dog=&habaDog{color:black and white,name:waya}

是从行为→具体的承载行为的实体→一只具体的实体狗

go代码规范

  1. 代码规范
    1. 命名规范
      1. 包名要和目录一致
      2. 包名要有意义
      3. 不要和标准库名冲突
      4. 包名全部小写
    2. 文件名尽量小写,采用蛇形命名法user_name
    3. 变量名使用驼峰命名法,userName
    4. 结构体采用驼峰命名法
    5. 接口驼峰,结尾可以用er
    6. 常量命名全部大写,多个单词使用蛇形命名法,APP_VERSION
  2. 注释规范
  3. import规范
    1. 分组放,首字母排序

单元测试

go test命令按照一定约定和组织的测试代码驱动程序,在包目录中所有以_test.go为后缀的源码文件都能被运行

我们写的源码文件不用担心内容过多,因为go build命令不会把_test.go打包

test文件有4类,我们只关注Test开头的代码功能测试,和Benchmark的性能测试,example的模糊测试

比如定义一个add函数

1
2
3
4
5
6
package practice

func Add(a int, b int) int {
return a + b
}

编写对应的单元测试

1
2
3
4
5
6
7
8
9
10
11
package practice

import "testing"

func TestAdd(t *testing.T) {
re := Add(1, 3)
if re != 4 {
t.Errorf("add(1,3) = %d; want 4", re)
}
}

然后运行add_test.go文件,就能进行相应测试

跳过耗时的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package practice

import "testing"

func TestAdd(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
re := Add(1, 3)
if re != 4 {
t.Errorf("add(1,3) = %d; want 4", re)
}
}

go test -short,会在测试的时候跳过testing.Short之后的部分

  • ai解释

    🔹 testing.Short() 在 Go 测试中的作用

    testing.Short() 是 Go 的 testing 包中的一个辅助函数,它用于判断是否应该跳过长时间运行的测试

    通常,我们会在测试代码中检查 testing.Short() 的返回值,如果 testing.Short() 返回 true,就跳过某些运行时间较长的测试。这在CI/CD(持续集成)、本地开发调试时非常有用,可以避免执行不必要的耗时测试。


    🎯 1. testing.Short() 的基本用法

    在 Go 语言的测试函数中,我们可以用 t.Skip() 配合 testing.Short() 来跳过长时间运行的测试。

    📌 示例:使用 short 跳过慢测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package main

    import (
    "testing"
    "time"
    )

    func TestSlowFunction(t *testing.T) {
    // 检查是否启用了 "-short" 选项
    if testing.Short() {
    t.Skip("Skipping slow test in short mode.")
    }

    // 这里是一个很慢的测试
    time.Sleep(3 * time.Second)
    t.Log("Slow test finished.")
    }

    ✅ 运行方式

    1️⃣ 正常模式(执行所有测试,包括慢测试)

    1
    2
    go test

    输出

    1
    2
    3
    4
    5
    === RUN   TestSlowFunction
    --- PASS: TestSlowFunction (3.00s)
    PASS
    ok example 3.005s

    • 这里会执行 TestSlowFunction,并等待 3 秒

    2️⃣ 使用 short 选项(跳过慢测试)

    1
    2
    go test -short

    输出

    1
    2
    3
    4
    5
    6
    === RUN   TestSlowFunction
    slow_test.go:9: Skipping slow test in short mode.
    --- SKIP: TestSlowFunction (0.00s)
    PASS
    ok example 0.002s

    • t.Skip("Skipping slow test in short mode.") 使得测试 直接跳过,避免运行 3 秒 的慢测试。

    🔹 2. testing.Short() 的作用

    1. 跳过长时间运行的测试
      • 适用于数据库查询、大型计算、网络请求等耗时操作的测试。
      • 本地开发CI/CD(如 GitHub Actions)中,减少测试时间。
    2. 让开发者在调试时更快运行测试
      • 当只想快速验证核心逻辑,可以使用 short 跳过部分测试。
      • CI/CD 全量测试 时,则不加 short,确保所有测试都运行。

    🔹 3. 更复杂的示例

    📌 跳过数据库相关的测试

    在测试中,如果需要连接数据库,执行 SQL 查询,可能会很慢,可以使用 testing.Short() 跳过:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package db

    import (
    "testing"
    )

    func TestDatabaseQuery(t *testing.T) {
    if testing.Short() {
    t.Skip("Skipping database test in short mode.")
    }

    // 假设这里有数据库查询逻辑
    // db.Query("SELECT * FROM users")
    t.Log("Database test executed.")
    }

    1
    2
    go test -short

    输出

    1
    2
    3
    4
    5
    6
    === RUN   TestDatabaseQuery
    db_test.go:7: Skipping database test in short mode.
    --- SKIP: TestDatabaseQuery (0.00s)
    PASS
    ok db 0.002s

    ✅ 这样,在 本地快速测试 时,可以跳过数据库测试,而在 CI/CD 环境运行完整测试。

    🚀 4. 结论

    特性 描述
    作用 允许跳过长时间运行的测试,避免 CI/CD 和本地测试耗时过长
    用法 在测试函数中使用if testing.Short() { t.Skip("Skipping...") }
    适用场景 跳过数据库查询、大量计算、网络请求等慢测试
    运行方式 go test(运行所有测试) vsgo test -short(跳过慢测试)

    🚀 结论testing.Short() 让 Go 测试更灵活、可控,适用于快速调试CI/CD 自动化测试场景! 🎯😃

用表格管理单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package practice

import "testing"

func TestAdd(t *testing.T) {
var tests = []struct {
a int
b int
want int
}{
{1, 2, 3},
{12, 12, 24},
{-9, 8, -1},
{0, 0, 0},
}

for _, tt := range tests {
Add(tt.a, tt.b)
if Add(tt.a, tt.b) != tt.want {
t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, tt.a, tt.want)
}
}
}

性能测试

🔹 Go 的 Benchmark(基准测试)

Go 提供了基准测试(Benchmark Testing) 来测量函数的执行效率,主要用于性能分析。基准测试会重复运行某个函数,并计算其平均执行时间,帮助开发者找到性能瓶颈。


🎯 1. Go Benchmark 的基本用法

📌 基准测试函数的规则

  • 函数名必须以 Benchmark 开头,例如 BenchmarkXxx
  • 参数必须是 b *testing.B(类似 t *testing.T)。
  • 基准测试逻辑需要在 for 循环中执行 b.N 次,b.N 由 Go 测试框架自动调整,以确保测试时间足够长。

📌 示例 1:简单的基准测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"testing"
)

// 需要测试的函数
func Add(a, b int) int {
return a + b
}

// Benchmark 测试 Add 函数
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}


🔹 2. 运行基准测试

基准测试不会在 go test 默认情况下运行,必须加 -bench 参数:

1
2
go test -bench=.

✅ 输出示例

1
2
3
4
5
6
7
goos: darwin
goarch: amd64
pkg: example
BenchmarkAdd-8 1000000000 0.321 ns/op
PASS
ok example 0.522s

输出解析

  • BenchmarkAdd-8:表示测试的函数是 BenchmarkAdd8 代表 CPU 线程数(GOMAXPROCS)。
  • 1000000000:表示测试执行的次数(b.N)。
  • 0.321 ns/op:表示每次调用Add(1,2) 的平均时间是 0.321 纳秒

🎯 3. 基准测试的优化技巧

📌 示例 2:测试字符串拼接

在 Go 里,字符串拼接 +strings.Builder 的性能差别很大,我们可以用基准测试来验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
"strings"
"testing"
)

// 使用 `+` 拼接字符串
func ConcatWithPlus() string {
s := ""
for i := 0; i < 100; i++ {
s += "a"
}
return s
}

// 使用 strings.Builder 拼接字符串
func ConcatWithBuilder() string {
var sb strings.Builder
for i := 0; i < 100; i++ {
sb.WriteString("a")
}
return sb.String()
}

// 基准测试 `+`
func BenchmarkConcatWithPlus(b *testing.B) {
for i := 0; i < b.N; i++ {
ConcatWithPlus()
}
}

// 基准测试 strings.Builder
func BenchmarkConcatWithBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
ConcatWithBuilder()
}
}

运行基准测试

1
2
go test -bench=.

可能的输出

1
2
3
BenchmarkConcatWithPlus-8        10000        120000 ns/op
BenchmarkConcatWithBuilder-8 500000 2000 ns/op

结果分析

  • ConcatWithPlus 每次操作需要 120000 ns(120µs)。
  • ConcatWithBuilder 只需要 2000 ns(2µs),明显更快。
  • 说明 strings.Builder频繁拼接字符串时,比 +高效得多

🔹 4. 高级 Benchmark 选项

📌 示例 3:测试不同输入规模

有时候,我们需要测试不同规模的数据,可以使用 b.SetBytes(n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"bytes"
"testing"
)

func GenerateBytes(size int) []byte {
return bytes.Repeat([]byte("a"), size)
}

func BenchmarkGenerateBytes(b *testing.B) {
b.SetBytes(1024) // 1KB
for i := 0; i < b.N; i++ {
GenerateBytes(1024)
}
}

运行

1
2
go test -bench=.

输出

1
2
BenchmarkGenerateBytes-8     1000000   100 ns/op   10.2 MB/s

✅ 这里多了 MB/s,说明处理速度为 10.2MB 每秒,适用于测试I/O 操作


📌 示例 4:测试并发性能

有些函数适用于并发,我们可以用 b.RunParallel() 进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"sync"
"testing"
)

// 模拟计数器
type Counter struct {
mu sync.Mutex
n int
}

func (c *Counter) Inc() {
c.mu.Lock()
c.n++
c.mu.Unlock()
}

// 基准测试并发性能
func BenchmarkCounter(b *testing.B) {
c := &Counter{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
c.Inc()
}
})
}

运行

1
2
go test -bench=.

输出

1
2
BenchmarkCounter-8  50000000   30 ns/op

b.RunParallel()自动创建多个 Goroutine 并发执行,提高测试的真实度。


🚀 5. 总结

特性 描述
作用 评估函数性能,找出最优实现
运行 go test -bench=.
命名规则 BenchmarkXxx(b *testing.B),循环b.N
并发测试 b.RunParallel()
测试大数据 b.SetBytes(n)
性能分析 ns/op(每次操作时间),MB/s(吞吐量)

🎯 6. 进阶优化

🔹 如何结合 pprof 进行性能分析

1
2
3
go test -bench=. -cpuprofile=cpu.out
go tool pprof cpu.out

然后可以使用 topweb 来查看 CPU 使用情况:

1
2
3
(pprof) top
(pprof) web


🎯 7. 结论

🚀 Go Benchmark 是性能优化的重要工具,可以帮助开发者找到代码的性能瓶颈! 🎯

💡 你可以使用它来:

  1. 比较不同算法的性能
  2. 测试字符串拼接、数据库查询等场景
  3. 分析并发性能
  4. 结合 pprof 进行深入优化

希望这篇指南能帮你掌握 Go 的基准测试!🚀🎯😃

context用于goroutine取消

withCancel提供一个主动取消的cancel函数

withTimeout也提供一个cancel函数,可是是到时间取消

withValue会在上下文里存储键值对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"context"
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup

// context.Context本身是一种接口,任意传入一种实现进来即可
// 一般会直接在函数前面加上ctx context.Context
func cpuInfo(ctx context.Context) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Println("监控完毕")
return
default:
time.Sleep(2 * time.Second)
fmt.Println("cpu info")
}

}
}

func main() {
wg.Add(1)
//context包提供三种实现,withCancel,withTimeout,withValue
ctx1, cancel1 := context.WithCancel(context.Background())
//ctx2是子context
ctx2, _ := context.WithCancel(ctx1)
go cpuInfo(ctx2)
time.Sleep(6 * time.Second)
//调用父context的cancel也能取消
cancel1()
wg.Wait()
fmt.Println("done")
}

结构体空指针问题

指针类型一定要初始化

比如现在有一个type student struct

然后var s *student只声明没初始化就会报panic

泛型


1️⃣ 泛型函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

// 定义一个泛型函数,T 是类型参数
func PrintSlice[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}

func main() {
PrintSlice([]int{1, 2, 3})
PrintSlice([]string{"hello", "world"})
}

👉 T any

  • T 是类型参数
  • any 是 Go 1.18 中引入的类型别名,等同于 interface{},表示 T 可以是任意类型。

2️⃣ 泛型结构体

1
2
3
4
5
6
7
8
9
10
type Pair[T, U any] struct {
First T
Second U
}

func main() {
p := Pair[int, string]{First: 1, Second: "apple"}
fmt.Println(p)
}

👉 这里 Pair 接受两个类型参数 TU


3️⃣ 泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Stack[T any] struct {
elements []T
}

func (s *Stack[T]) Push(v T) {
s.elements = append(s.elements, v)
}

func (s *Stack[T]) Pop() T {
n := len(s.elements)
v := s.elements[n-1]
s.elements = s.elements[:n-1]
return v
}

👉 这个例子就是一个泛型栈


有时候我们希望泛型类型必须满足某些条件,比如:只能是可比较、可相加、实现某个接口等。

1️⃣ 使用 constraints

1
2
3
4
5
6
import "golang.org/x/exp/constraints"

func Add[T constraints.Ordered](a, b T) T {
return a + b
}

👉 constraints.Ordered:限制 T 必须是可以排序的类型(比如 int、float、string)

2️⃣ 自定义约束接口

1
2
3
4
5
6
7
8
type Adder interface {
~int | ~float64
}

func Add[T Adder](a, b T) T {
return a + b
}

👉 ~int 表示可以是 int 或者自定义的底层类型是 int 的类型。


特性 说明
类型参数 使用[]来声明类型参数
any 任意类型(等价于interface{}
约束(constraints) 限制泛型参数必须满足的条件
泛型函数、结构体、方法 都可以定义和使用泛型


Go语言补充
http://example.com/2023/12/20/Go语言补充/
作者
WoodQ
发布于
2023年12月20日
许可协议