Go 函数:把重复的事交给它
如果你仔细观察日常生活,你会发现很多事情都是重复的。每天早上你要泡咖啡,每周五你要写周报,每个月你要交房租。重复的事情做多了,你就会想:能不能把这些步骤打包,以后只需要一个指令就能搞定?
这就是函数(function)的核心思想。函数就是一段可以重复使用的代码块,你给它起个名字,定义好它需要接收什么输入、产生什么输出,以后随时可以调用它。
Go 语言的函数设计非常优雅。它既不像 C 语言那样简陋,也不像某些面向对象语言那样搞出一大堆复杂的概念。Go 的函数简单、强大,还有一些独特的特性,让我们一起来看看吧。
基本的函数定义
Go 语言使用 func 关键字来定义函数。基本的语法结构是:
func 函数名(参数列表) 返回类型 {
// 函数体
return 返回值
}
来看一个简单的例子:
package main
import "fmt"
// 定义一个函数,计算两个数的和
func add(a int, b int) int {
return a + b
}
func main() {
result := add(3, 5)
fmt.Println("3 + 5 =", result) // 3 + 5 = 8
}
这里有几个要点:
add是函数名(a int, b int)是参数列表,声明了两个int类型的参数- 参数列表后面的
int是返回类型,表示这个函数返回一个整数 return a + b返回计算结果
参数类型简写
如果相邻的参数类型相同,可以只在最后一个参数后面声明类型:
func add(a, b int) int {
return a + b
}
这和 func add(a int, b int) int 是完全等价的,只是写法更简洁。
多返回值
这是 Go 语言最让人称道的特性之一。Go 的函数可以返回多个值,这在实际开发中非常实用。
最常见的用法是同时返回结果和错误:
package main
import (
"errors"
"fmt"
)
// divide 执行除法运算,返回商和错误
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为 0")
}
return a / b, nil
}
func main() {
// 正常情况
result, err := divide(10, 3)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Printf("10 ÷ 3 = %.2f\n", result)
}
// 输出:10 ÷ 3 = 3.33
// 异常情况
result, err = divide(10, 0)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Printf("10 ÷ 0 = %.2f\n", result)
}
// 输出:错误: 除数不能为 0
}
注意第 9 行的 return a / b, nil——nil 表示没有错误。这是 Go 语言中非常常见的模式:函数返回一个有用的值和一个错误信息。调用者先检查错误,再使用值。
你会在 Go 的标准库和第三方库中到处看到这种模式。可以说,这是 Go 语言错误处理哲学的基石。
忽略返回值
如果你只关心其中一个返回值,可以用 _(空白标识符)忽略不需要的:
result, _ := divide(10, 3)
fmt.Println(result) // 3.3333333333333335
⚠️ 小贴士:虽然可以用 _ 忽略错误,但在生产代码中不建议这么做。忽略错误就像蒙着眼睛开车——你不知道什么时候会撞上什么东西。
命名返回值
Go 允许你给返回值起名字。命名后的返回值会自动声明为函数内部的变量,你可以在函数体中修改它们:
func swap(a, b int) (first, second int) {
first = b
second = a
return // 裸返回(naked return),自动返回命名返回值
}
func main() {
x, y := swap(1, 2)
fmt.Println(x, y) // 2 1
}
注意第 4 行的 return 后面什么都没写——这叫做"裸返回"(naked return)。Go 会自动返回命名返回值的当前值。
⚠️ 踩坑提示:裸返回只适合非常短的函数。在长函数中,裸返回会让代码变得难以理解,因为你不知道返回的是什么值。Go 官方建议在长函数中使用显式返回。
可变参数
有时候你不确定函数会接收多少个参数。Go 支持可变参数(variadic parameters),让你可以传入任意数量的参数:
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
func main() {
fmt.Println(sum(1, 2)) // 3
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(1, 2, 3, 4, 5)) // 15
}
nums ...int 表示 nums 是一个可变参数,它实际上是一个 []int 切片。
如果你已经有了一个切片,可以用 ... 把它展开后传给可变参数函数:
numbers := []int{1, 2, 3, 4, 5}
total := sum(numbers...) // 注意这里的 ...
fmt.Println(total) // 15
一个实际的例子——用 fmt.Sprintf 格式化字符串:
func logMessage(level string, args ...interface{}) string {
prefix := "[" + level + "] "
return prefix + fmt.Sprint(args...)
}
msg := logMessage("INFO", "服务器启动,端口:", 8080)
fmt.Println(msg) // [INFO] 服务器启动,端口:8080
函数作为值和参数
在 Go 语言中,函数是一等公民(first-class citizen)。这意味着函数可以像普通值一样被传递、赋值和返回。
package main
import "fmt"
// 定义一个函数类型
type operation func(int, int) int
func add(a, b int) int { return a + b }
func multiply(a, b int) int { return a * b }
// calculate 接收一个函数作为参数
func calculate(op operation, a, b int) int {
return op(a, b)
}
func main() {
fmt.Println(calculate(add, 3, 5)) // 8
fmt.Println(calculate(multiply, 3, 5)) // 15
}
在这个例子中,calculate 函数接收另一个函数作为参数。这种接受函数作为参数或返回函数的函数,叫做高阶函数(higher-order function)。
匿名函数
你可以定义没有名字的函数(匿名函数),直接赋值给变量或者立即执行:
// 赋值给变量
greet := func(name string) string {
return "你好," + name + "!"
}
fmt.Println(greet("张三")) // 你好,张三!
// 立即执行(IIFE)
result := func(a, b int) int {
return a * b
}(3, 5)
fmt.Println(result) // 15
匿名函数在需要简短逻辑但又不想单独定义一个命名函数的时候特别有用。
闭包
闭包(closure)是匿名函数的一个重要特性。闭包可以"记住"并访问它定义时所在作用域的变量,即使那个作用域已经结束了。
这听起来有点抽象,看个例子就明白了:
package main
import "fmt"
func makeCounter() func() int {
count := 0 // 这个变量会被闭包"记住"
return func() int {
count++
return count
}
}
func main() {
counter := makeCounter()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
fmt.Println(counter()) // 3
// 再创建一个新的计数器
counter2 := makeCounter()
fmt.Println(counter2()) // 1(独立的计数)
}
在这个例子中,makeCounter 返回了一个匿名函数。这个匿名函数引用了外部的 count 变量。正常情况下,makeCounter 执行完毕后,count 变量应该被销毁。但因为闭包的存在,count 被"保留"了下来。
每次调用 counter() 时,它都能记住上一次 count 的值,所以会依次返回 1、2、3。
闭包的实际应用
闭包在实际开发中有很多用途。一个常见的场景是创建"带状态的处理器":
package main
import "fmt"
// 创建一个限流器,每 n 次调用后执行一次回调
func makeThrottle(n int, callback func()) func() {
count := 0
return func() {
count++
if count%n == 0 {
callback()
}
}
}
func main() {
throttle := makeThrottle(3, func() {
fmt.Println("已处理 3 个请求!")
})
for i := 1; i <= 10; i++ {
fmt.Printf("处理请求 %d\n", i)
throttle()
}
}
// 输出:
// 处理请求 1
// 处理请求 2
// 处理请求 3
// 已处理 3 个请求!
// 处理请求 4
// ...
defer:延迟执行
defer 是 Go 语言中一个非常独特且实用的关键字。被 defer 修饰的语句不会立即执行,而是推迟到当前函数返回之前才执行。
package main
import "fmt"
func main() {
fmt.Println("第一行")
defer fmt.Println("第二行(defer)")
fmt.Println("第三行")
}
// 输出:
// 第一行
// 第三行
// 第二行(defer)
注意执行顺序:defer 语句被"推迟"到了函数最后才执行。
defer 的执行顺序
如果有多个 defer 语句,它们会按照后进先出(LIFO)的顺序执行:
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
}
// 输出:
// 3
// 2
// 1
就像叠盘子一样——最后放上去的盘子最先被拿走。
defer 的常见用法
1. 关闭文件
这是 defer 最经典的用途:
func readFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close() // 确保函数退出时文件被关闭
data, err := ioutil.ReadAll(f)
return data, err
}
不管函数是正常返回还是因为错误返回,f.Close() 都会被执行。你不需要在每个 return 前面都写一遍关闭文件的代码。
2. 释放锁
var mu sync.Mutex
func safeIncrement(counter *int) {
mu.Lock()
defer mu.Unlock() // 确保锁被释放
*counter++
}
3. 记录函数执行时间
func timeTrack(start time.Time, name string) {
elapsed := time.Since(start)
fmt.Printf("%s 执行耗时: %v\n", name, elapsed)
}
func slowFunction() {
defer timeTrack(time.Now(), "slowFunction")
time.Sleep(2 * time.Second)
// 做一些耗时的操作...
}
defer 的参数求值时机
一个重要的细节:defer 语句中的参数在 defer 声明时就被求值了,而不是在 defer 执行时。
func main() {
i := 1
defer fmt.Println("defer 中的 i =", i)
i = 2
fmt.Println("main 中的 i =", i)
}
// 输出:
// main 中的 i = 2
// defer 中的 i = 1
i 的值在 defer 声明时就已经被确定为 1,后面修改 i 不会影响 defer 中已经捕获的值。
但如果你通过闭包或者指针来访问变量,defer 执行时会读取变量的最新值:
func main() {
i := 1
defer func() {
fmt.Println("defer 闭包中的 i =", i)
}()
i = 2
fmt.Println("main 中的 i =", i)
}
// 输出:
// main 中的 i = 2
// defer 闭包中的 i = 2
init 函数
Go 有一个特殊的函数叫做 init()。它在包被导入时自动执行,不需要你手动调用。每个包可以有多个 init 函数,它们会按照文件名的字母顺序执行。
package main
import "fmt"
func init() {
fmt.Println("init 函数执行了")
}
func main() {
fmt.Println("main 函数执行了")
}
// 输出:
// init 函数执行了
// main 函数执行了
init 函数通常用于包的初始化工作,比如设置默认配置、注册驱动等。
实战:写一个简单的计算器
让我们用函数来写一个完整的命令行计算器:
package main
import (
"fmt"
)
// add 加法
func add(a, b float64) float64 {
return a + b
}
// subtract 减法
func subtract(a, b float64) float64 {
return a - b
}
// multiply 乘法
func multiply(a, b float64) float64 {
return a * b
}
// divide 除法,返回结果和错误
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为 0")
}
return a / b, nil
}
// calculate 根据运算符执行对应的运算
func calculate(op string, a, b float64) (float64, error) {
// 使用函数映射来实现运算符分发
operations := map[string]func(float64, float64) float64{
"+": add,
"-": subtract,
"*": multiply,
}
if fn, ok := operations[op]; ok {
return fn(a, b), nil
}
// 除法需要特殊处理(因为它可能返回错误)
if op == "/" {
return divide(a, b)
}
return 0, fmt.Errorf("不支持的运算符: %s", op)
}
func main() {
examples := []struct {
op string
a, b float64
}{
{"+", 10, 3},
{"-", 10, 3},
{"*", 10, 3},
{"/", 10, 3},
{"/", 10, 0},
{"^", 10, 3},
}
for _, ex := range examples {
result, err := calculate(ex.op, ex.a, ex.b)
if err != nil {
fmt.Printf("%.0f %s %.0f = 错误: %v\n", ex.a, ex.op, ex.b, err)
} else {
fmt.Printf("%.0f %s %.0f = %.4f\n", ex.a, ex.op, ex.b, result)
}
}
}
// 输出:
// 10 + 3 = 13.0000
// 10 - 3 = 7.0000
// 10 * 3 = 30.0000
// 10 / 3 = 3.3333
// 10 / 0 = 错误: 除数不能为 0
// 10 ^ 3 = 错误: 不支持的运算符: ^
小结
今天我们全面学习了 Go 语言的函数系统:
- 基本定义:
func关键字、参数列表、返回类型 - 多返回值:Go 函数可以返回多个值,常用来同时返回结果和错误
- 命名返回值:可以给返回值命名,支持裸返回
- 可变参数:用
...声明可以接收任意数量参数的函数 - 函数作为值:函数是一等公民,可以作为参数传递和赋值
- 匿名函数:没有名字的函数,可以立即执行或赋值给变量
- 闭包:匿名函数可以捕获外部作用域的变量
- defer:延迟执行,常用于资源清理(关闭文件、释放锁等)
- init 函数:包初始化时自动执行
Go 的函数设计追求实用。多返回值解决了错误处理的难题,defer 简化了资源管理,闭包提供了灵活的代码组织方式。这些特性组合在一起,让你能写出既简洁又可靠的代码。
练习时间
- 最大公约数:写一个函数,用辗转相除法计算两个数的最大公约数
- 多返回值:写一个函数,接收一个整数切片,返回最大值、最小值和平均值
- 闭包计数器:用闭包实现一个"斐波那契生成器",每次调用返回下一个斐波那契数
- defer 实验:写一个函数,里面放 3 个 defer 语句,观察它们的执行顺序
- 函数组合:写一个
compose函数,它接收两个函数f和g,返回一个新函数h,使得h(x) = f(g(x))
下一篇预告
下一篇文章,我们将学习 Go 语言中数组和切片。切片是 Go 语言中最常用的数据结构之一,但也是最容易让人困惑的。我们会深入探讨:
- 数组和切片的区别
- 切片的底层原理
make和append的工作方式- 切片的常见陷阱和最佳实践
我们下篇见!👋
参考资料:
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。