Go 函数
函数是 Go 程序的基本组成单元。Go 的函数设计简洁而强大,支持多返回值、闭包、一等公民等特性。理解函数的工作原理和最佳实践,是编写高质量 Go 代码的基础。
函数基础
定义函数
func 函数名(参数列表) 返回值列表 {
// 函数体
return 返回值
}
函数由以下部分组成:
func关键字:声明函数- 函数名:首字母大写表示可导出
- 参数列表:可以有零个或多个参数
- 返回值列表:可以有零个或多个返回值
- 函数体:实际的代码逻辑
基本示例
// 无参数无返回值
func sayHello() {
fmt.Println("Hello!")
}
// 有参数无返回值
func greet(name string) {
fmt.Println("Hello,", name)
}
// 有参数有返回值
func add(a int, b int) int {
return a + b
}
// 相同类型参数可以合并
func addCompact(a, b int) int {
return a + b
}
// 调用
sayHello()
greet("张三")
result := add(1, 2)
fmt.Println(result) // 3
函数声明规则
// 规则一:相邻的同类型参数可以合并
func process(a, b int, c string) {}
// 规则二:不同类型必须分开声明
func mixed(a int, b string, c float64) {}
// 规则三:参数名和类型不能省略(除了合并的情况)
// func broken(int, string) {} // 编译错误
// 规则四:不接收参数的函数,参数列表为空
func noParams() {}
// 规则五:不返回值的函数,返回值列表可以省略
func noReturn() {
// 隐式返回
}
参数传递机制
理解 Go 的参数传递机制对于编写正确的程序至关重要。Go 中所有参数传递都是值传递,但这个"值"的含义需要仔细理解。
值传递的本质
当调用函数时,参数会被复制一份传递给函数。这意味着函数内部对参数的修改不会影响原始值:
func modifyValue(x int) {
x = 100 // 修改的是副本
fmt.Println("函数内:", x) // 100
}
func main() {
a := 10
modifyValue(a)
fmt.Println("函数外:", a) // 10,原值不变
}
指针传递
如果需要在函数内部修改原始值,需要传递指针:
func modifyPointer(x *int) {
*x = 100 // 通过指针修改原始值
}
func main() {
a := 10
modifyPointer(&a)
fmt.Println(a) // 100,原值被修改
}
指针传递仍然是值传递:传递的是指针的副本,但两个指针指向同一个地址。
切片和映射的传递
切片和映射本身是引用类型,但传递时仍然发生值复制——复制的是切片头或映射头的副本:
func modifySlice(s []int) {
s[0] = 100 // 可以修改底层数组
s = append(s, 4) // 修改的是切片头副本,不影响原切片
}
func main() {
s := []int{1, 2, 3}
modifySlice(s)
fmt.Println(s) // [100, 2, 3],第一个元素被修改,但长度不变
}
理解要点:
// 切片头结构(伪代码)
type slice struct {
ptr unsafe.Pointer // 底层数组指针
len int // 长度
cap int // 容量
}
传递切片时,复制的是这个 slice 结构。因此:
- 可以通过 ptr 修改底层数组的元素
- 但修改 len 或 cap 不会影响原切片
如果需要函数能够修改切片的长度,需要传递切片的指针:
func modifySlicePtr(s *[]int) {
*s = append(*s, 4)
}
func main() {
s := []int{1, 2, 3}
modifySlicePtr(&s)
fmt.Println(s) // [1, 2, 3, 4]
}
传递大结构体
对于大型结构体,传递指针可以避免复制开销:
type LargeStruct struct {
data [10000]int
}
// 不推荐:复制整个结构体
func processValue(v LargeStruct) {}
// 推荐:只传递指针(8字节)
func processPointer(v *LargeStruct) {}
// 推荐:对于只读操作,也可以传递指针
func readOnly(v *LargeStruct) {
// 只读取,不修改
}
最佳实践:
- 小结构体(如
Point{X, Y int}):值传递 - 大结构体:指针传递
- 需要修改原值:指针传递
- 只读操作:视情况选择(一致性原则)
返回值
单返回值
func add(a, b int) int {
return a + b
}
多返回值
Go 的多返回值是其特色之一,常用于返回结果和错误:
// 返回商和余数
func divide(a, b int) (int, int) {
quotient := a / b
remainder := a % b
return quotient, remainder
}
// 调用
q, r := divide(10, 3)
fmt.Println(q, r) // 3 1
// 忽略某个返回值
q, _ := divide(10, 3)
// 忽略所有返回值(不推荐)
divide(10, 3)
命名返回值
可以为返回值命名,命名后会在函数开始时自动初始化为零值:
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // 裸返回,自动返回 x 和 y
}
// 等价于
func splitExplicit(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return x, y
}
命名返回值的优缺点:
优点:
- 代码更清晰,明确每个返回值的含义
- 可以简化代码(裸返回)
- 文档生成时更友好
缺点:
- 裸返回可能降低可读性
- 容易造成变量遮蔽
建议:在简短函数中使用命名返回值,复杂函数显式返回更清晰。
返回错误
Go 的惯用方式是通过返回 error 来处理错误:
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数不能为零")
}
return a / b, nil
}
// 调用
result, err := divide(10, 0)
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Println("结果:", result)
错误处理的最佳实践:
// 好的实践:正常路径在 if 外面
result, err := someFunction()
if err != nil {
return err
}
// 继续处理 result
// 不推荐:正常路径嵌套在 else 中
if result, err := someFunction(); err == nil {
// 处理 result
} else {
return err
}
不定参数
不定参数允许函数接受零个或多个同类型参数。
基本语法
// 语法:...类型
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// 调用方式
fmt.Println(sum()) // 0
fmt.Println(sum(1)) // 1
fmt.Println(sum(1, 2)) // 3
fmt.Println(sum(1, 2, 3)) // 6
不定参数的本质
不定参数在函数内部是切片类型:
func inspect(nums ...int) {
fmt.Printf("类型: %T\n", nums) // []int
fmt.Printf("长度: %d\n", len(nums))
fmt.Printf("值: %v\n", nums)
}
切片展开
可以使用 ... 操作符将切片展开传递给不定参数函数:
nums := []int{1, 2, 3, 4, 5}
result := sum(nums...) // 展开切片
fmt.Println(result) // 15
不定参数的位置
不定参数必须是函数的最后一个参数:
// 正确:不定参数在最后
func format(prefix string, values ...int) string {
return fmt.Sprintf("%s: %v", prefix, values)
}
// 错误:不定参数不在最后
// func invalid(values ...int, suffix string) {}
不定参数与空接口
结合空接口可以实现接受任意类型的参数:
func printAll(values ...interface{}) {
for _, v := range values {
fmt.Printf("%v (%T)\n", v, v)
}
}
printAll(1, "hello", 3.14, true)
// 1 (int)
// hello (string)
// 3.14 (float64)
// true (bool)
函数类型
在 Go 中,函数是一等公民,这意味着函数可以像其他值一样被传递、赋值和操作。
函数类型声明
// 声明函数类型
type Operation func(int, int) int
// 使用函数类型
func calculate(a, b int, op Operation) int {
return op(a, b)
}
func add(a, b int) int { return a + b }
func sub(a, b int) int { return a - b }
func main() {
// 将函数赋值给变量
var op Operation = add
fmt.Println(op(10, 5)) // 15
// 作为参数传递
fmt.Println(calculate(10, 5, add)) // 15
fmt.Println(calculate(10, 5, sub)) // 5
}
函数作为参数
// 接受函数作为参数
func apply(fn func(int, int) int, a, b int) int {
return fn(a, b)
}
func main() {
// 传递匿名函数
result := apply(func(a, b int) int {
return a * b
}, 10, 20)
fmt.Println(result) // 200
// 传递命名函数
result = apply(add, 10, 20)
fmt.Println(result) // 30
}
函数作为返回值
// 返回函数
func getOperation(name string) func(int, int) int {
switch name {
case "add":
return func(a, b int) int { return a + b }
case "sub":
return func(a, b int) int { return a - b }
case "mul":
return func(a, b int) int { return a * b }
default:
return func(a, b int) int { return 0 }
}
}
func main() {
add := getOperation("add")
fmt.Println(add(10, 5)) // 15
mul := getOperation("mul")
fmt.Println(mul(10, 5)) // 50
}
函数类型的方法
函数类型可以定义方法,这在创建中间件等场景非常有用:
type Middleware func(http.Handler) http.Handler
// 链式调用中间件
func (m Middleware) Then(next http.Handler) http.Handler {
return m(next)
}
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("Request: %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
func main() {
var m Middleware = loggingMiddleware
handler := m.Then(http.DefaultServeMux)
http.ListenAndServe(":8080", handler)
}
闭包
闭包是指一个函数可以访问并操作其外部作用域的变量。闭包是 Go 中实现函数式编程的重要工具。
闭包的基本概念
func counter() func() int {
count := 0 // 外部变量
return func() int {
count++ // 访问并修改外部变量
return count
}
}
func main() {
c1 := counter()
c2 := counter()
// 每个闭包有自己独立的变量环境
fmt.Println(c1()) // 1
fmt.Println(c1()) // 2
fmt.Println(c1()) // 3
fmt.Println(c2()) // 1
fmt.Println(c2()) // 2
}
闭包捕获变量
闭包捕获的是变量的引用,而不是值:
func main() {
var funcs []func()
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Println(i) // 捕获的是变量 i 的引用
})
}
// 所有闭包都打印 3(循环结束后的值)
for _, f := range funcs {
f()
}
}
// 输出: 3, 3, 3
Go 1.22 前后的行为差异
Go 1.22 对 for 循环变量作用域进行了重大修改,解决了这个长期存在的问题:
Go 1.21 及之前:循环变量在整个循环中只创建一次,每次迭代更新同一个变量。
// Go 1.21 及之前:所有闭包捕获同一个变量
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // 捕获的是同一个变量 i
}()
}
// 可能输出: 3, 3, 3(或 2, 3, 3 等不确定结果)
Go 1.22 及之后:每次迭代都创建新的变量实例,闭包捕获的是各自迭代的值。
// Go 1.22+:每次迭代创建新变量,闭包捕获各自迭代值
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // 每个 goroutine 捕获不同的 i
}()
}
// 输出: 0, 1, 2(顺序可能不同)
官方文档说明:
根据 Go 官方博客《Fixing For Loops in Go 1.22》,这个变更的影响是:
- 对于
for range循环,效果相当于每次迭代开始时执行v := v - 对于三段式
for循环,效果相当于每次迭代开始时执行i := i
兼容性控制:
新语义只应用于 go.mod 中声明 go 1.22 或更高版本的模块。这确保了:
- 旧代码行为不变
- 开发者可以逐步迁移
- 向前兼容性得到保证
预览模式(Go 1.21):
在 Go 1.21 中可以预览这个变更:
GOEXPERIMENT=loopvar go test
Go 1.21 及之前的解决方案
如果需要兼容 Go 1.21 及更早版本,仍应使用以下方式:
// 方式一:创建局部变量(推荐)
func main() {
var funcs []func()
for i := 0; i < 3; i++ {
i := i // 创建新变量,捕获当前值
funcs = append(funcs, func() {
fmt.Println(i)
})
}
for _, f := range funcs {
f()
}
}
// 输出: 0, 1, 2
// 方式二:将值作为参数传递
func main() {
var funcs []func()
for i := 0; i < 3; i++ {
funcs = append(funcs, func(n int) {
fmt.Println(n)
})
}
for i, f := range funcs {
f(i)
}
}
测试中的常见陷阱
Go 1.22 的变更暴露了许多错误的测试代码:
// 错误的测试:Go 1.21 中会错误地通过
func TestAllEvenBuggy(t *testing.T) {
testCases := []int{1, 2, 4, 6}
for _, v := range testCases {
t.Run("sub", func(t *testing.T) {
t.Parallel() // 并行执行
if v&1 != 0 {
t.Fatal("odd v", v)
}
})
}
}
// Go 1.21 中:测试通过(错误!因为所有子测试都检查 v=6)
// Go 1.22+ 中:测试失败(正确!会检测到 1 是奇数)
最佳实践:在 Go 1.22+ 中,可以放心地在并行测试中使用循环变量。对于旧版本,需要在 t.Parallel() 之前捕获变量。
闭包的实际应用
工厂函数:
func makeSuffix(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
func main() {
addJpg := makeSuffix(".jpg")
addTxt := makeSuffix(".txt")
fmt.Println(addJpg("photo")) // photo.jpg
fmt.Println(addJpg("photo.jpg")) // photo.jpg
fmt.Println(addTxt("doc")) // doc.txt
}
状态保持:
func makeAccumulator(initial int) func(int) int {
sum := initial
return func(n int) int {
sum += n
return sum
}
}
func main() {
acc := makeAccumulator(10)
fmt.Println(acc(5)) // 15
fmt.Println(acc(10)) // 25
fmt.Println(acc(3)) // 28
}
延迟计算:
func lazyValue(fn func() int) func() int {
var value int
var computed bool
return func() int {
if !computed {
value = fn()
computed = true
}
return value
}
}
func main() {
expensive := lazyValue(func() int {
fmt.Println("计算中...")
time.Sleep(time.Second)
return 42
})
fmt.Println("准备获取值")
fmt.Println(expensive()) // 第一次调用会计算
fmt.Println(expensive()) // 后续调用返回缓存值
}
defer
defer 语句将函数调用推迟到外层函数返回之后执行。它是 Go 中管理资源的核心机制。
基本用法
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // 确保文件关闭
// 处理文件...
return nil
}
defer 的执行时机
defer 在函数返回前执行,但在返回值被计算之后:
func deferReturn() (result int) {
defer func() {
fmt.Println("defer 执行, result =", result)
}()
result = 42
fmt.Println("函数体执行完成")
return result
}
func main() {
fmt.Println("返回值:", deferReturn())
}
// 输出:
// 函数体执行完成
// defer 执行, result = 42
// 返回值: 42
defer 与返回值
这是一个容易出错的点:
func f1() int {
i := 0
defer func() {
i++ // 修改的是局部变量 i
}()
return i // 返回 0
}
func f2() (i int) {
defer func() {
i++ // 修改的是命名返回值
}()
return i // 返回 1
}
func f3() (i int) {
defer func() {
i++ // defer 可以修改返回值
}()
return 0 // 返回 1(defer 后执行)
}
多个 defer 的执行顺序
多个 defer 按 LIFO(后进先出)顺序执行:
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("函数体")
}
// 输出:
// 函数体
// 3
// 2
// 1
defer 参数求值
defer 的参数在声明时就已求值:
func main() {
i := 0
defer fmt.Println("defer:", i) // 此时 i = 0
i++
fmt.Println("main:", i)
}
// 输出:
// main: 1
// defer: 0
如果需要在执行时才求值:
func main() {
i := 0
defer func() {
fmt.Println("defer:", i) // 执行时读取 i
}()
i++
fmt.Println("main:", i)
}
// 输出:
// main: 1
// defer: 1
defer 常见用途
资源清理:
func processFile(path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
// 处理文件...
return nil
}
互斥锁释放:
var mu sync.Mutex
var counter int
func increment() {
mu.Lock()
defer mu.Unlock()
counter++
}
追踪函数执行时间:
func trackTime(start time.Time, name string) {
elapsed := time.Since(start)
log.Printf("%s 执行耗时: %v", name, elapsed)
}
func slowOperation() {
defer trackTime(time.Now(), "slowOperation")
time.Sleep(time.Second)
}
恢复 panic:
func safeOperation() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
// 可能 panic 的操作
riskyOperation()
return nil
}
defer 的性能考虑
defer 有少量性能开销,但在大多数情况下可以忽略。对于极高性能要求的代码,可以考虑手动管理:
// 高性能场景:避免 defer
func highPerformance() {
mu.Lock()
// 关键代码
mu.Unlock() // 手动释放
}
// 正常场景:使用 defer
func normalCase() {
mu.Lock()
defer mu.Unlock() // 推荐,更安全
// 代码
}
函数式选项模式
函数式选项模式是 Go 中配置复杂对象的惯用方法,广泛应用于标准库和第三方库中。
问题:多种配置方式
假设我们需要创建一个服务器,有很多配置选项:
type Server struct {
host string
port int
timeout time.Duration
maxConns int
tls bool
certFile string
keyFile string
}
// 方式一:多参数构造函数(不好)
func NewServer(host string, port int, timeout time.Duration,
maxConns int, tls bool, certFile, keyFile string) *Server {
// 参数太多,容易出错
}
// 方式二:配置结构体(部分解决)
type ServerConfig struct {
Host string
Port int
Timeout time.Duration
MaxConns int
TLS bool
CertFile string
KeyFile string
}
func NewServerWithConfig(cfg ServerConfig) *Server {
// 但默认值处理麻烦
}
解决方案:函数式选项模式
// 定义选项函数类型
type Option func(*Server)
// 定义选项函数
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout time.Duration) Option {
return func(s *Server) {
s.timeout = timeout
}
}
func WithTLS(certFile, keyFile string) Option {
return func(s *Server) {
s.tls = true
s.certFile = certFile
s.keyFile = keyFile
}
}
func WithMaxConnections(max int) Option {
return func(s *Server) {
s.maxConns = max
}
}
// 构造函数
func NewServer(host string, opts ...Option) *Server {
// 设置默认值
server := &Server{
host: host,
port: 8080, // 默认端口
timeout: 30 * time.Second, // 默认超时
maxConns: 100, // 默认最大连接数
}
// 应用选项
for _, opt := range opts {
opt(server)
}
return server
}
// 使用
func main() {
// 只使用默认值
s1 := NewServer("localhost")
// 自定义部分选项
s2 := NewServer("localhost",
WithPort(9000),
WithTimeout(60*time.Second),
)
// 启用 TLS
s3 := NewServer("localhost",
WithPort(443),
WithTLS("cert.pem", "key.pem"),
WithMaxConnections(1000),
)
}
模式的优点
- API 清晰:选项名有明确的语义
- 向后兼容:添加新选项不影响现有代码
- 默认值明确:在构造函数中统一设置
- 灵活组合:可以任意组合选项
- 可扩展:添加新选项非常简单
验证选项
func WithPort(port int) Option {
return func(s *Server) {
if port < 0 || port > 65535 {
panic("invalid port")
}
s.port = port
}
}
// 或者返回错误
type Option func(*Server) error
func WithPort(port int) Option {
return func(s *Server) error {
if port < 0 || port > 65535 {
return errors.New("invalid port")
}
s.port = port
return nil
}
}
func NewServer(host string, opts ...Option) (*Server, error) {
server := &Server{host: host, port: 8080}
for _, opt := range opts {
if err := opt(server); err != nil {
return nil, err
}
}
return server, nil
}
中间件模式
中间件是一种装饰器模式的应用,常用于 HTTP 服务中对请求进行预处理和后处理。
基本中间件
// 中间件类型
type Middleware func(http.Handler) http.Handler
// 日志中间件
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 调用下一个处理器
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
// 恢复中间件(捕获 panic)
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %v", err)
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}
// 使用
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello"))
})
// 包装中间件
handler := RecoveryMiddleware(LoggingMiddleware(mux))
http.ListenAndServe(":8080", handler)
}
中间件链
// 创建中间件链
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)
// 应用中间件链
handler := Chain(mux,
LoggingMiddleware,
RecoveryMiddleware,
AuthMiddleware,
)
http.ListenAndServe(":8080", handler)
}
认证中间件
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// 验证 token
userID, err := validateToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// 将用户信息存入上下文
ctx := context.WithValue(r.Context(), "userID", userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
CORS 中间件
func CORSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
高阶函数
高阶函数是指接受函数作为参数或返回函数的函数。Go 中有很多使用高阶函数的场景。
切片操作
// 过滤
func Filter[T any](slice []T, predicate func(T) bool) []T {
result := make([]T, 0)
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
// 映射
func Map[T, U any](slice []T, transform func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = transform(v)
}
return result
}
// 归约
func Reduce[T, U any](slice []T, initial U, reducer func(U, T) U) U {
result := initial
for _, v := range slice {
result = reducer(result, v)
}
return result
}
// 使用
func main() {
nums := []int{1, 2, 3, 4, 5}
// 过滤偶数
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
fmt.Println(evens) // [2, 4]
// 平方
squares := Map(nums, func(n int) int { return n * n })
fmt.Println(squares) // [1, 4, 9, 16, 25]
// 求和
sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
fmt.Println(sum) // 15
}
函数组合
// 组合两个函数
func Compose[A, B, C any](f func(B) C, g func(A) B) func(A) C {
return func(a A) C {
return f(g(a))
}
}
// 使用
func main() {
addOne := func(x int) int { return x + 1 }
double := func(x int) int { return x * 2 }
// 先加一,再翻倍
addThenDouble := Compose(double, addOne)
fmt.Println(addThenDouble(3)) // 8
// 先翻倍,再加一
doubleThenAdd := Compose(addOne, double)
fmt.Println(doubleThenAdd(3)) // 7
}
柯里化
// 柯里化:将多参数函数转换为单参数函数链
func Curry[A, B, C any](f func(A, B) C) func(A) func(B) C {
return func(a A) func(B) C {
return func(b B) C {
return f(a, b)
}
}
}
func main() {
add := func(a, b int) int { return a + b }
curriedAdd := Curry(add)
addFive := curriedAdd(5) // 固定第一个参数
fmt.Println(addFive(3)) // 8
fmt.Println(addFive(10)) // 15
}
偏函数应用
// 偏应用:固定部分参数
func Partial[A, B, C any](f func(A, B) C, a A) func(B) C {
return func(b B) C {
return f(a, b)
}
}
func main() {
multiply := func(a, b int) int { return a * b }
double := Partial(multiply, 2)
triple := Partial(multiply, 3)
fmt.Println(double(5)) // 10
fmt.Println(triple(5)) // 15
}
迭代器函数(Go 1.23+)
Go 1.23 引入了迭代器(Range Over Func)特性,允许 for range 直接遍历函数。这是一种强大的函数式编程模式,使得自定义集合的遍历变得简洁优雅。
迭代器函数的类型定义
Go 1.23 在标准库中定义了两种迭代器函数类型:
// 单值迭代器
type Seq[V any] func(yield func(V) bool)
// 键值对迭代器
type Seq2[K, V any] func(yield func(K, V) bool)
核心概念:迭代器函数接受一个 yield 回调函数作为参数。每次调用 yield 会产生一个迭代值,如果 yield 返回 false,表示调用者希望停止迭代。
基本迭代器示例
package main
import "fmt"
// 定义一个简单的整数序列迭代器
func Ints(n int) func(func(int) bool) {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
if !yield(i) {
return // 如果 yield 返回 false,停止迭代
}
}
}
}
func main() {
// 使用 for range 遍历迭代器
for v := range Ints(5) {
fmt.Println(v)
}
// 输出: 0, 1, 2, 3, 4
}
执行机制说明:
迭代器的执行是一种"推"模式:
for range调用迭代器函数- 迭代器函数执行,通过
yield"推送"值给循环体 - 循环体处理值,返回是否继续
- 如果继续,迭代器继续执行;否则迭代器提前返回
迭代器的实际应用
遍历自定义集合:
package main
import "fmt"
// 自定义集合类型
type IntSet struct {
values map[int]bool
}
func NewIntSet(vals ...int) *IntSet {
s := &IntSet{values: make(map[int]bool)}
for _, v := range vals {
s.values[v] = true
}
return s
}
// 定义迭代器方法
func (s *IntSet) All() func(func(int) bool) {
return func(yield func(int) bool) {
for v := range s.values {
if !yield(v) {
return
}
}
}
}
func main() {
set := NewIntSet(1, 2, 3, 4, 5)
// 使用 for range 遍历自定义集合
for v := range set.All() {
fmt.Println(v)
}
}
生成器模式:
package main
import (
"fmt"
"strings"
)
// 单词生成器
func Words(text string) func(func(string) bool) {
return func(yield func(string) bool) {
for _, word := range strings.Fields(text) {
if !yield(word) {
return
}
}
}
}
func main() {
text := "Hello, Go 1.23 迭代器示例"
for word := range Words(text) {
fmt.Println(word)
}
}
无限序列:
package main
import "fmt"
// 无限自然数序列
func Naturals() func(func(int) bool) {
return func(yield func(int) bool) {
for i := 0; ; i++ {
if !yield(i) {
return
}
}
}
}
func main() {
// 打印前 10 个自然数
count := 0
for n := range Naturals() {
fmt.Println(n)
count++
if count >= 10 {
break // 可以安全地提前退出
}
}
}
提前终止迭代
当 for range 循环提前退出(如 break、return),迭代器会被正确通知:
func main() {
for v := range Ints(100) {
fmt.Println(v)
if v == 3 {
break // 提前终止
}
}
// 迭代器函数会收到 yield 返回 false,执行清理逻辑
}
组合迭代器
迭代器可以方便地组合,实现链式处理:
package main
import "fmt"
// 过滤迭代器
func Filter[T any](seq func(func(T) bool), predicate func(T) bool) func(func(T) bool) {
return func(yield func(T) bool) {
for v := range seq {
if predicate(v) {
if !yield(v) {
return
}
}
}
}
}
// 映射迭代器
func Map[T, U any](seq func(func(T) bool), transform func(T) U) func(func(U) bool) {
return func(yield func(U) bool) {
for v := range seq {
if !yield(transform(v)) {
return
}
}
}
}
func main() {
// 链式操作:生成 -> 过滤 -> 映射
for v := range Map(
Filter(Ints(20), func(n int) bool { return n%2 == 0 }),
func(n int) int { return n * n },
) {
fmt.Println(v) // 0, 4, 16, 36, 64, ...
}
}
Pull 迭代器
除了"推"模式,Go 还提供了"拉"模式迭代器,用于需要手动控制迭代的场景:
package main
import (
"fmt"
"iter"
"slices"
)
func main() {
numbers := []int{1, 2, 3, 4, 5}
// 将推迭代器转换为拉迭代器
next, stop := iter.Pull(slices.Values(numbers))
defer stop() // 确保释放资源
for {
v, ok := next()
if !ok {
break
}
fmt.Println(v)
}
}
Pull 迭代器的适用场景:
- 需要在迭代过程中做复杂的状态管理
- 需要跨多个函数传递迭代状态
- 与其他语言的迭代器模式对接
- 实现复杂的解析逻辑
标准库中的迭代器支持
Go 1.23 在标准库中广泛添加了迭代器支持:
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
// slices 包的迭代器
nums := []int{1, 2, 3, 4, 5}
for v := range slices.Values(nums) {
fmt.Println(v)
}
for i, v := range slices.All(nums) {
fmt.Printf("索引 %d: %d\n", i, v)
}
// maps 包的迭代器
m := map[string]int{"a": 1, "b": 2}
for k := range maps.Keys(m) {
fmt.Println(k)
}
for v := range maps.Values(m) {
fmt.Println(v)
}
for k, v := range maps.All(m) {
fmt.Printf("%s: %d\n", k, v)
}
}
迭代器的最佳实践
- 命名约定:迭代器方法通常命名为
All()、Values()、Keys()等 - 资源管理:如果迭代器持有资源,确保在提前终止时正确释放
- 避免副作用:迭代器应该是纯函数,避免修改被迭代的集合
- 文档说明:明确说明迭代器是否保证顺序
// 好的实践:带资源管理的迭代器
func (f *File) Lines() func(func(string) bool) {
return func(yield func(string) bool) {
scanner := bufio.NewScanner(f)
defer f.Close() // 确保资源释放
for scanner.Scan() {
if !yield(scanner.Text()) {
return
}
}
}
}
迭代器与闭包的关系
迭代器本质上是闭包的高级应用——它捕获了外部状态(如集合、计数器),并通过 yield 函数与调用者交互。理解闭包是理解迭代器的基础。
init 和 main 函数
main 函数
main 函数是程序的入口点:
package main
import "fmt"
func main() {
fmt.Println("程序开始")
// 程序逻辑
fmt.Println("程序结束")
}
特点:
- 必须在
main包中 - 不接受参数,不返回值
- 程序从这里开始执行
main函数结束,程序结束
init 函数
init 函数在包初始化时自动执行:
package main
import "fmt"
var name string
func init() {
name = "张三"
fmt.Println("init 函数执行")
}
func main() {
fmt.Println("main 函数执行:", name)
}
// 输出:
// init 函数执行
// main 函数执行: 张三
特点:
- 每个包可以有多个
init函数 - 每个源文件可以有多个
init函数 - 不能被调用,自动执行
- 按照导入依赖顺序执行
执行顺序
导入包 -> 常量初始化 -> 变量初始化 -> init() -> main()
// 文件 a.go
package main
import "fmt"
var a = initA()
func initA() int {
fmt.Println("初始化变量 a")
return 1
}
func init() {
fmt.Println("init a.go")
}
// 文件 b.go
package main
import "fmt"
var b = initB()
func initB() int {
fmt.Println("初始化变量 b")
return 2
}
func init() {
fmt.Println("init b.go")
}
func main() {
fmt.Println("main")
}
// 可能的输出(顺序可能变化):
// 初始化变量 a
// 初始化变量 b
// init a.go
// init b.go
// main
init 的常见用途
// 注册数据库驱动
package mysql
import "database/sql"
func init() {
sql.Register("mysql", &MySQLDriver{})
}
// 配置初始化
package config
var Config *Configuration
func init() {
var err error
Config, err = LoadConfig("config.yaml")
if err != nil {
panic(err)
}
}
// 验证前置条件
package main
func init() {
if os.Getenv("API_KEY") == "" {
panic("API_KEY environment variable is required")
}
}
panic 和 recover
panic 和 recover 是 Go 的异常处理机制,但应该谨慎使用。
panic
panic 会立即停止当前函数的执行,并开始展开调用栈:
func mustPositive(n int) {
if n < 0 {
panic(fmt.Sprintf("负数不被允许: %d", n))
}
fmt.Println("数字:", n)
}
func main() {
mustPositive(10) // 正常
mustPositive(-5) // panic
fmt.Println("不会执行")
}
适用场景:
- 程序遇到无法恢复的错误
- 初始化失败
- 不可能发生的情况(防御性编程)
recover
recover 用于捕获 panic,只能在 defer 中使用:
func safeOperation() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
mightPanic()
return nil
}
func mightPanic() {
panic("something went wrong")
}
func main() {
err := safeOperation()
if err != nil {
fmt.Println("捕获到错误:", err)
}
fmt.Println("程序继续执行")
}
HTTP 服务器中的 recover
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %v\n%s", err, debug.Stack())
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}
何时使用 panic
应该使用 panic:
- 程序初始化失败
- 达到不可能的代码路径
- 关键资源不可用
不应该使用 panic:
- 文件不存在
- 网络连接失败
- 用户输入错误
- 配置错误
递归
递归是函数调用自身的编程技术。
基本递归
// 阶乘
func factorial(n int) int {
if n <= 1 {
return 1 // 终止条件
}
return n * factorial(n-1)
}
// 斐波那契数列
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
尾递归
Go 不自动优化尾递归,但可以手动转换为迭代:
// 尾递归形式(Go 不优化)
func factorialTail(n, acc int) int {
if n <= 1 {
return acc
}
return factorialTail(n-1, n*acc)
}
// 迭代形式(推荐)
func factorialIterative(n int) int {
result := 1
for i := 2; i <= n; i++ {
result *= i
}
return result
}
递归的实际应用
遍历树结构:
type TreeNode struct {
Value int
Children []*TreeNode
}
func (n *TreeNode) Traverse(fn func(*TreeNode)) {
fn(n) // 处理当前节点
for _, child := range n.Children {
child.Traverse(fn) // 递归处理子节点
}
}
深度优先搜索:
func DFS(graph map[int][]int, start int, visited map[int]bool) {
visited[start] = true
fmt.Println("访问:", start)
for _, neighbor := range graph[start] {
if !visited[neighbor] {
DFS(graph, neighbor, visited)
}
}
}
最佳实践
函数命名
// 好的命名:动词开头,描述清晰
func CalculateTotalPrice(items []Item) float64
func SendEmail(to, subject, body string) error
func GetUserByID(id int) (*User, error)
// 不好的命名
func Total() float64 // 不清楚是什么的总计
func Email() error // 发送还是获取?
func Process() error // 处理什么?
命名约定:
| 函数类型 | 命名模式 | 示例 |
|---|---|---|
| 获取值 | Get* 或直接用名词 | GetName(), User.Name() |
| 设置值 | Set* | SetName(name string) |
| 判断布尔 | Is*, Has*, Can* | IsValid(), HasPermission() |
| 转换 | To* | ToString(), ToJSON() |
| 创建 | New* | NewUser(), NewConfig() |
| 验证 | Validate* 或 Check* | ValidateEmail(), CheckPermission() |
函数长度
- 理想情况:函数应该足够短,一目了然
- 经验法则:一个函数不应该超过一屏(约 50 行)
- 如果函数太长,考虑拆分成多个小函数
单一职责
// 不好:函数做了太多事情
func ProcessOrder(order *Order) error {
// 验证订单
// 检查库存
// 计算价格
// 创建支付
// 发送邮件
// 更新数据库
}
// 好:每个函数只做一件事
func ProcessOrder(order *Order) error {
if err := ValidateOrder(order); err != nil {
return err
}
if err := CheckInventory(order); err != nil {
return err
}
if err := CalculatePrice(order); err != nil {
return err
}
// ...
}
错误处理
// 好:立即处理错误
result, err := someFunction()
if err != nil {
return fmt.Errorf("操作失败: %w", err)
}
// 不好:忽略错误
result, _ := someFunction()
// 不好:错误处理嵌套太深
if result, err := someFunction(); err == nil {
if result2, err := anotherFunction(result); err == nil {
// 继续嵌套...
} else {
return err
}
} else {
return err
}
参数设计
参数数量:
- 理想情况:0-3 个参数
- 如果需要更多参数,考虑使用结构体或选项模式
// 不好:参数太多
func CreateUser(name string, age int, email string, phone string,
address string, city string, country string) *User
// 好:使用配置结构体
type UserConfig struct {
Name string
Age int
Email string
Phone string
Address string
}
func CreateUser(cfg UserConfig) *User
// 好:使用函数式选项模式
func NewUser(name string, opts ...UserOption) *User
避免布尔参数:
// 不好:布尔参数含义不清
func Process(data []byte, async bool)
// 调用时看不懂含义
Process(data, true) // true 是什么意思?
// 好:使用明确的方式
func ProcessSync(data []byte)
func ProcessAsync(data []byte)
// 或者使用选项
func Process(data []byte, opts ...ProcessOption)
Process(data, WithAsync())
文档注释
// Add 返回两个整数的和。
// 参数 a 和 b 应该是有效的整数。
// 如果结果溢出,行为未定义。
func Add(a, b int) int {
return a + b
}
// 更完整的注释格式
// CreateUser 创建一个新的用户实例。
//
// 参数:
// - name: 用户名,不能为空
// - age: 年龄,必须为非负数
//
// 返回:
// - *User: 创建的用户实例
// - error: 如果参数无效,返回错误
//
// 示例:
//
// user, err := CreateUser("张三", 25)
// if err != nil {
// log.Fatal(err)
// }
func CreateUser(name string, age int) (*User, error) {
if name == "" {
return nil, errors.New("name cannot be empty")
}
if age < 0 {
return nil, errors.New("age cannot be negative")
}
return &User{Name: name, Age: age}, nil
}
避免副作用
// 不好:函数有副作用
var counter int
func GetID() int {
counter++ // 副作用:修改了全局状态
return counter
}
// 好:函数是纯函数,没有副作用
func NextID(current int) int {
return current + 1
}
// 或者使用结构体封装状态
type IDGenerator struct {
counter int
}
func (g *IDGenerator) Next() int {
g.counter++
return g.counter
}
上下文传递
对于长时间运行或需要取消的操作,应该接受 context.Context 作为第一个参数:
// 好:支持取消和超时
func FetchData(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
// ...
}
// 调用示例
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
data, err := FetchData(ctx, "https://example.com")
优雅的错误处理模式
// 使用 defer 简化错误处理
func ProcessFile(path string) (err error) {
file, err := os.Open(path)
if err != nil {
return err
}
defer func() {
if closeErr := file.Close(); closeErr != nil {
if err == nil {
err = closeErr // 只有在没有其他错误时才返回关闭错误
}
}
}()
// 处理文件...
return nil
}
函数作为参数的设计
当函数作为参数时,定义清晰的函数类型:
// 定义清晰的函数类型
type CompareFunc[T any] func(a, b T) int
type FilterFunc[T any] func(T) bool
type MapFunc[T, U any] func(T) U
// 使用
func Sort[T any](slice []T, compare CompareFunc[T]) {
// ...
}
func Filter[T any](slice []T, filter FilterFunc[T]) []T {
// ...
}
小结
本章详细介绍了 Go 函数的核心概念和高级用法:
- 函数基础:定义、参数、返回值
- 参数传递:理解值传递的本质
- 多返回值:Go 的特色功能
- 不定参数:灵活的参数处理
- 函数类型:函数作为一等公民
- 闭包:捕获外部变量,注意 Go 1.22 前后的行为差异
- defer:延迟执行和资源管理
- 函数式选项模式:配置复杂对象
- 中间件模式:装饰器的应用
- 高阶函数:函数组合和变换
- 迭代器函数(Go 1.23+):使用
for range遍历函数 - panic/recover:异常处理
- 递归:函数调用自身
练习
- 基础练习:实现一个支持链式调用的计算器函数
- 闭包练习:使用闭包实现一个令牌桶限流器
- 高阶函数练习:实现一个通用的重试函数,支持自定义重试次数和延迟
- 选项模式练习:使用函数式选项模式重构一个复杂的配置函数
- 中间件练习:编写一个中间件链,包含日志、认证、恢复功能
- 函数组合练习:实现一个高阶函数
Pipe,支持多个函数的管道式组合 - 迭代器练习(Go 1.23+):实现一个树的深度优先遍历迭代器
- 错误处理练习:实现一个数据库事务包装函数,正确处理各种错误场景