Function(函数)
函数(function)把一段可复用的行为封装为具名调用点。Go 的函数是一等值:除了声明和调用,还可以赋给变量、作为参数传入、作为结果返回。掌握函数签名、显式错误返回和闭包捕获规则,是组织 Go 业务逻辑的基础。
声明、调用与函数签名
函数使用 func 声明。参数类型写在名称后面,返回类型写在参数列表之后;函数的参数和结果共同构成其函数签名:
package main
import "fmt"
func greet(name string) string {
return "Hello, " + name
}
func main() {
message := greet("Ada")
fmt.Println(message)
}
输出为:
Hello, Ada
当相邻参数类型相同时,可以只在最后一个参数后写类型:
func max(left, right int) int {
if left > right {
return left
}
return right
}
省略返回类型表示函数不返回结果。函数内的局部变量只在该次调用期间有效;Go 的参数传递始终是按值传递,传入指针、切片、映射或函数值时,复制的是这些值本身,但它们可能仍引用相同的底层状态。
多返回值与错误处理
Go 支持返回多个结果。惯用方式是将业务结果与 error 一起返回,调用方必须决定如何处理错误:
package main
import (
"errors"
"fmt"
)
func divide(dividend, divisor float64) (float64, error) {
if divisor == 0 {
return 0, errors.New("divisor must not be zero")
}
return dividend / divisor, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("divide:", err)
return
}
fmt.Println(result)
}
失败时第一个返回值通常应是该类型的零值,或由 API 文档明确说明其含义。不要忽略错误后继续使用可能无效的结果;如果调用方不能恢复,也应至少返回包装后的错误,让上层保留操作上下文:
value, err := divide(total, count)
if err != nil {
return 0, fmt.Errorf("calculate average: %w", err)
}
具名返回值可以在结果列表中声明名称:
func split(total, parts int) (each int, remainder int) {
return total / parts, total % parts
}
具名结果在函数开始时已被初始化为零值,且可使用无参数的 return 返回它们。短函数中具名结果能说明含义;较长函数中,裸 return 容易隐藏实际返回的值,通常直接写出表达式更清晰。
可变参数
参数类型前的 ... 表示可变参数。函数体内它是一个切片,可以零个或多个实参调用:
package main
import "fmt"
func sum(values ...int) int {
total := 0
for _, value := range values {
total += value
}
return total
}
func main() {
fmt.Println(sum()) // 0
fmt.Println(sum(1, 2, 3)) // 6
}
已有切片可以在调用处追加 ... 展开:
values := []int{4, 5, 6}
fmt.Println(sum(values...)) // 15
可变参数必须位于参数列表末尾。函数可能修改 values 中的元素;当调用方通过切片展开传参时,这种修改可能反映到同一个底层数组,因此应避免把可变参数当作天然隔离的副本。
函数类型与函数值
函数类型由参数和返回值决定,参数名称不属于类型的一部分。函数可以赋给变量、放入数据结构,或作为回调传入另一个函数:
package main
import "fmt"
type Transform func(int) int
func apply(value int, transform Transform) int {
return transform(value)
}
func main() {
double := func(value int) int {
return value * 2
}
fmt.Println(apply(21, double)) // 42
}
函数值的零值是 nil。调用 nil 函数值会触发 panic,因此可选回调需要先判断,或在构造时提供默认实现:
if transform != nil {
value = transform(value)
}
函数只能与 nil 比较,不能用 == 判断两个非 nil 函数是否相同。需要识别策略或处理器时,应保存明确的名称、枚举值或其他可比较标识。
闭包与捕获变量
函数字面量可以引用其外层函数的变量,这种函数值称为闭包(closure)。被捕获的变量在多次调用之间会保留状态:
package main
import "fmt"
func newCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
next := newCounter()
fmt.Println(next()) // 1
fmt.Println(next()) // 2
}
这里 next 不是每次都创建新的 count;它持有同一个被捕获变量。闭包适合封装局部状态或生成专用函数,但若被多个 goroutine 同时调用,count 的读写仍需要同步。闭包本身不会让可变状态变成并发安全的。
在循环中创建闭包时,应明确闭包需要捕获的值。Go 1.22 起,使用 := 声明的 for 循环迭代变量会为每次迭代创建新的变量,减少了常见的意外共享问题;为了让意图在所有读者面前保持直接,异步任务仍建议将当次值作为参数传给函数:
for _, name := range []string{"Ada", "Lin"} {
go func(currentName string) {
fmt.Println(currentName)
}(name)
}
生产代码还应使用 sync.WaitGroup、context 或通道管理 goroutine 的完成和取消;上例只用于说明参数绑定,不应直接作为生命周期管理方案。
递归
函数可以直接或间接调用自身。递归必须有明确的终止条件,否则会持续消耗调用栈并最终触发运行时错误:
package main
import "fmt"
func factorial(value int) int {
if value <= 1 {
return 1
}
return value * factorial(value-1)
}
func main() {
fmt.Println(factorial(5)) // 120
}
递归适合树遍历、分治等结构天然递归的问题。对于可用循环清晰表达且深度可能很大的线性任务,循环通常更容易控制内存和失败边界。factorial 也会因整数溢出失去数学上的正确性,真实业务应验证输入范围或选用合适的数据表示。
defer 与函数退出
defer 在当前函数即将返回时按后进先出顺序执行,适合注册文件、锁和连接的清理动作。defer 语句中的参数在注册时求值,闭包形式则会读取执行时的变量:
func trace() (result int) {
result = 1
defer func() { result++ }() // 修改具名返回值,最终返回 2。
defer fmt.Println("cleanup")
return result
}
片段需要导入 fmt。若不使用具名返回值,defer 闭包不能直接修改即将返回的结果变量;清理逻辑仍应处理自身的错误。
资源应在成功获取后立即注册 defer;长循环中不要在外层函数不断累积 defer,应把单轮工作提取为独立函数。defer 不会自动处理 goroutine 的退出,仍需配合 context、等待组或 channel 设计生命周期。
与 TypeScript 的关键差异
TypeScript 的函数类型写作 (value: number) => number,而 Go 写作 func(int) int。两者都能传递函数,但 Go 的多返回值和显式 error 是常规错误传递机制,不等同于 throw 或 Promise rejection。
TypeScript 闭包也能捕获外层变量,但 JavaScript 的异步模型与 Go 的 goroutine 调度不同。把闭包传给 goroutine 后,是否发生竞态取决于共享数据和同步关系,不能用事件循环的单线程经验推断安全性。
常见错误
- 忽略
error,然后继续使用失败调用返回的零值或不完整结果。 - 在参数较多或逻辑较长的函数中滥用具名返回值和裸
return,使返回来源难以判断。 - 假设可变参数总会复制数据,进而无意修改调用方切片的底层数组。
- 调用可能为
nil的函数值。 - 将带可变状态的闭包交给多个 goroutine 却没有同步,导致数据竞态。
- 为了避免递归而忽略问题结构,或在深度无上限的任务中不评估递归深度和溢出边界。
- 误以为
defer在循环每轮结束时执行,或忽略 defer 参数的注册时机。
小结
func声明函数;函数签名由参数和返回值决定。- 多返回值适合把结果与
error一并返回,调用方应显式处理错误。 - 可变参数在函数内是切片,切片展开传参时可能共享底层数组。
- 函数是一等值,可以作为变量、参数和返回值;函数值的零值为
nil。 - 闭包能保留外层变量,带来便利也可能引入共享可变状态。
- 递归必须有终止条件,并应评估输入深度和数值边界。
defer按注册逆序在函数返回时执行,资源应在成功获取后立即注册清理。