Go语言补充
函数的一等公民特性
函数本身可以作为变量
匿名函数 闭包
函数可以满足接口
将函数作为参数传入有什么意义
在 Go 语言中,将函数作为参数传入另一个函数(高阶函数)可以让代码更加灵活、可复用、符合开闭原则。这样做的意义主要包括:
🌟 1. 代码更加通用
如果一个函数的某个逻辑部分可能变化,而其他部分保持不变,我们可以把变化的部分作为参数传递,从而使代码更加通用。
🎯 示例:排序逻辑作为参数
1 |
|
✅ 代码解读
SortNumbers
是一个通用的排序函数,它不关心具体排序规则,而是把比较逻辑作为参数传入。Ascending
和Descending
定义了不同的排序规则,可以自由切换。- 好处: 如果以后要支持新的排序方式,我们只需传入新的比较函数,而无需修改
SortNumbers
。
🌟 2. 代码更简洁(避免重复)
高阶函数可以减少重复代码,使代码更加精简。
🎯 示例:不同操作传递给 ApplyOperation
1 |
|
✅ 代码解读
ApplyOperation
是一个通用的数学运算函数,它可以接受任意运算函数(加、减、乘等)。- 这样,我们可以动态传入不同的运算逻辑,避免写多个
Add
,Subtract
等函数,提高代码复用性。
🌟 3. 延迟执行(回调函数)
有时候,我们希望一个函数在某个事件发生后才执行(比如 HTTP 请求返回、定时任务等),可以使用回调函数实现。
🎯 示例:执行完某个任务后回调
1 |
|
✅ 代码解读
DoSomething
先执行任务,然后调用回调callback()
。- 在
main
中,每次调用DoSomething
时,我们传入不同的回调函数,让任务结束后执行不同的逻辑。 - 好处: 这样可以动态决定任务完成后的行为,而不需要修改
DoSomething
的代码。
🌟 4. 用于 map/filter/reduce
(函数式编程风格)
Go 没有内置 map
, filter
, reduce
,但可以用函数作为参数实现这些功能。
🎯 示例:使用 map
处理切片
1 |
|
✅ 代码解读
Map
是一个通用的映射函数,它不关心具体变换逻辑,而是把转换函数f
作为参数传入。- 这样,我们可以用
Map
进行不同的转换(平方、翻倍等),而不需要写多个SquareAll
,DoubleAll
等函数。
🌟 5. 用于 http.HandlerFunc
(Web 开发)
在 Go 的 net/http
包中,http.HandlerFunc
本质上就是一个函数作为参数的应用。
🎯 示例:自定义 HTTP 处理函数
1 |
|
✅ 代码解读
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 |
|
✅ 代码解析
counter()
返回一个匿名函数,该匿名函数**可以访问并修改count
**。c1
和c2
是两个独立的闭包,每个都有自己的count
变量,互不影响。- 这样,闭包可以保存状态,即使
counter()
执行结束,count
变量仍然保留。
🔹 2. 为什么使用闭包?有什么好处?
📌 2.1 维持状态
闭包可以用来保持状态信息,即使外部函数已经返回,状态依然保留。这比在全局变量中存储数据更加安全。
1 |
|
sum
变量在acc
闭包中持续存在,并在每次调用时累加。- 优势:比全局变量更安全,不会被其他代码修改。
📌 2.2 作为回调函数
闭包可以作为回调函数,用于事件处理、异步编程等。
1 |
|
times2
生成一个乘 2 的闭包,times3
生成一个乘 3 的闭包,它们各自**“记住”了factor
*。- 这种方式可以动态生成不同的行为,比写多个函数更灵活。
📌 2.3 避免全局变量污染
全局变量容易导致命名冲突和数据污染,而闭包提供了一种封装数据的方式,使变量仅在特定作用域内有效。
1 |
|
gen1
和gen2
各自维护了counter
变量,避免了全局变量的冲突。
📌 2.4 用于装饰器模式
闭包可以用来增强已有函数的功能,类似于装饰器模式(Decorator Pattern)。
1 |
|
✅ 代码解析
withLogging()
接受一个函数f
并返回一个新的带日志功能的函数。- 这种方式可以动态增强函数的行为,而不改变原始函数。
🔹 3. 什么时候不适合用闭包?
虽然闭包很强大,但也有一些潜在的问题:
可能导致内存泄漏
- 如果闭包变量持有大量数据,但迟迟不被释放,会导致内存泄漏。
- 例如,长期运行的 Goroutine 里如果持有闭包变量,可能会占用大量内存。
变量共享问题
- 如果多个 Goroutine 访问同一个闭包变量,可能会出现并发安全问题,需要使用
sync.Mutex
或sync/atomic
保护。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package 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())
}解决方案:加锁保证线程安全。
- 如果多个 Goroutine 访问同一个闭包变量,可能会出现并发安全问题,需要使用
🚀 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 |
|
鸭子类型
当一只鸟看起来像鸭子,叫起来像鸭子,游起来也像鸭子,那就是鸭子,鸭子类型强调的事物的外部行为,而不是内部的结构
go语言中处处都是interface
接口到底是什么
接口是struct更高一层的抽象
具体逻辑是这样的
一个东西只要会汪汪叫,会走,就是狗(interface定义的行为)
一只具体的哈巴狗,是有腿和皮毛和名字的(struct定义的属性)
而一只现实里的狗,是有具体的属性的
var d Dog=&habaDog{color:black and white,name:waya}
是从行为→具体的承载行为的实体→一只具体的实体狗
go代码规范
- 代码规范
- 命名规范
- 包名要和目录一致
- 包名要有意义
- 不要和标准库名冲突
- 包名全部小写
- 文件名尽量小写,采用蛇形命名法user_name
- 变量名使用驼峰命名法,userName
- 结构体采用驼峰命名法
- 接口驼峰,结尾可以用er
- 常量命名全部大写,多个单词使用蛇形命名法,APP_VERSION
- 命名规范
- 注释规范
- import规范
- 分组放,首字母排序
单元测试
go test命令按照一定约定和组织的测试代码驱动程序,在包目录中所有以_test.go为后缀的源码文件都能被运行
我们写的源码文件不用担心内容过多,因为go build命令不会把_test.go打包
test文件有4类,我们只关注Test开头的代码功能测试,和Benchmark的性能测试,example的模糊测试
比如定义一个add函数
1 |
|
编写对应的单元测试
1 |
|
然后运行add_test.go文件,就能进行相应测试
跳过耗时的操作
1 |
|
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
18package 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
2go test
输出
1
2
3
4
5=== RUN TestSlowFunction
--- PASS: TestSlowFunction (3.00s)
PASS
ok example 3.005s- 这里会执行
TestSlowFunction
,并等待 3 秒。
2️⃣ 使用
short
选项(跳过慢测试)1
2go 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.002st.Skip("Skipping slow test in short mode.")
使得测试 直接跳过,避免运行 3 秒 的慢测试。
🔹 2.
testing.Short()
的作用- 跳过长时间运行的测试
- 适用于数据库查询、大型计算、网络请求等耗时操作的测试。
- 在本地开发或CI/CD(如 GitHub Actions)中,减少测试时间。
- 让开发者在调试时更快运行测试
- 当只想快速验证核心逻辑,可以使用
short
跳过部分测试。 - 在 CI/CD 全量测试 时,则不加
short
,确保所有测试都运行。
- 当只想快速验证核心逻辑,可以使用
🔹 3. 更复杂的示例
📌 跳过数据库相关的测试
在测试中,如果需要连接数据库,执行 SQL 查询,可能会很慢,可以使用
testing.Short()
跳过:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package 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
2go 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 |
|
性能测试
🔹 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. 运行基准测试
基准测试不会在 go test
默认情况下运行,必须加 -bench
参数:
1 |
|
✅ 输出示例
1 |
|
输出解析
BenchmarkAdd-8
:表示测试的函数是BenchmarkAdd
,8
代表 CPU 线程数(GOMAXPROCS)。1000000000
:表示测试执行的次数(b.N
)。0.321 ns/op
:表示每次调用Add(1,2)
的平均时间是0.321 纳秒
。
🎯 3. 基准测试的优化技巧
📌 示例 2:测试字符串拼接
在 Go 里,字符串拼接 +
和 strings.Builder
的性能差别很大,我们可以用基准测试来验证。
1 |
|
运行基准测试
1 |
|
可能的输出
1 |
|
✅ 结果分析
ConcatWithPlus
每次操作需要120000 ns
(120µs)。ConcatWithBuilder
只需要2000 ns
(2µs),明显更快。- 说明
strings.Builder
在频繁拼接字符串时,比+
高效得多。
🔹 4. 高级 Benchmark 选项
📌 示例 3:测试不同输入规模
有时候,我们需要测试不同规模的数据,可以使用 b.SetBytes(n)
:
1 |
|
运行
1 |
|
输出
1 |
|
✅ 这里多了 MB/s
,说明处理速度为 10.2MB 每秒,适用于测试I/O 操作。
📌 示例 4:测试并发性能
有些函数适用于并发,我们可以用 b.RunParallel()
进行测试:
1 |
|
运行
1 |
|
输出
1 |
|
✅ 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 |
|
然后可以使用 top
或 web
来查看 CPU 使用情况:
1 |
|
🎯 7. 结论
🚀 Go Benchmark 是性能优化的重要工具,可以帮助开发者找到代码的性能瓶颈! 🎯
💡 你可以使用它来:
- 比较不同算法的性能
- 测试字符串拼接、数据库查询等场景
- 分析并发性能
- 结合
pprof
进行深入优化
希望这篇指南能帮你掌握 Go 的基准测试!🚀🎯😃
context用于goroutine取消
withCancel提供一个主动取消的cancel函数
withTimeout也提供一个cancel函数,可是是到时间取消
withValue会在上下文里存储键值对
1 |
|
结构体空指针问题
指针类型一定要初始化
比如现在有一个type student struct
然后var s *student只声明没初始化就会报panic
泛型
1️⃣ 泛型函数
1 |
|
👉 T any
:
T
是类型参数any
是 Go 1.18 中引入的类型别名,等同于interface{}
,表示 T 可以是任意类型。
2️⃣ 泛型结构体
1 |
|
👉 这里 Pair
接受两个类型参数 T
和 U
。
3️⃣ 泛型方法
1 |
|
👉 这个例子就是一个泛型栈。
有时候我们希望泛型类型必须满足某些条件,比如:只能是可比较、可相加、实现某个接口等。
1️⃣ 使用 constraints
包
1 |
|
👉 constraints.Ordered
:限制 T 必须是可以排序的类型(比如 int、float、string)
2️⃣ 自定义约束接口
1 |
|
👉 ~int
表示可以是 int
或者自定义的底层类型是 int
的类型。
特性 | 说明 |
---|---|
类型参数 | 使用[] 来声明类型参数 |
any |
任意类型(等价于interface{} ) |
约束(constraints) | 限制泛型参数必须满足的条件 |
泛型函数、结构体、方法 | 都可以定义和使用泛型 |