大家好,我是极客老墨。

函数返回多个值?刚接触 Go 时我还不习惯,写惯了 Java 的单返回值。后来发现这设计真香,错误处理直接 result, err := doSomething(),不用再搞什么异常捕获。

更绝的是 Go 的闭包、defer、方法绑定,这些特性组合起来,让代码既简洁又强大。

这篇就聊聊 Go 函数和方法的几个核心特性。

函数声明:类型在后面

Go 的函数声明用 func 关键字,参数类型写在变量名后面:

1func add(a int, b int) int {
2    return a + b
3}
4
5// 参数类型相同可以合并
6func add(a, b int) int {
7    return a + b
8}

这是 Go 的特色,跟 C/Java 反着来。习惯就好。

多返回值:Go 的杀手锏

Go 支持函数返回多个值,这在错误处理中特别好用:

 1func divide(a, b int) (int, error) {
 2    if b == 0 {
 3        return 0, errors.New("division by zero")
 4    }
 5    return a / b, nil
 6}
 7
 8// 调用时接收两个返回值
 9result, err := divide(10, 0)
10if err != nil {
11    fmt.Println("Error:", err)
12    return
13}
14fmt.Println("Result:", result)

不需要某个返回值?用下划线 _ 忽略:

1result, _ := divide(10, 2)  // 忽略错误(不推荐)

命名返回值:可以更简洁

给返回值命名后,可以直接在函数体内使用,还能用裸 return

1func divide(a, b int) (result int, err error) {
2    if b == 0 {
3        err = errors.New("division by zero")
4        return  // 等价于 return result, err
5    }
6    result = a / b
7    return  // 自动返回 result 和 err
8}

⚠️ 小坑:裸 return 在复杂函数中可能降低可读性,简单函数用用就好。

可变参数:接收任意数量参数

... 语法可以接收任意数量的参数,函数内部是个切片:

 1func sum(nums ...int) int {
 2    total := 0
 3    for _, num := range nums {
 4        total += num
 5    }
 6    return total
 7}
 8
 9// 调用
10sum(1, 2, 3)           // 6
11sum(1, 2, 3, 4, 5)     // 15
12
13// 传递切片,用 ... 展开
14numbers := []int{10, 20, 30}
15sum(numbers...)        // 60

⚠️ 注意:可变参数必须是函数的最后一个参数。

匿名函数和闭包:函数也是值

Go 支持匿名函数,可以在函数内部定义函数,还能捕获外部变量(闭包):

 1// 1. 匿名函数直接调用
 2func(msg string) {
 3    fmt.Println(msg)
 4}("Hello!")
 5
 6// 2. 赋值给变量
 7greet := func(name string) string {
 8    return "Hello, " + name
 9}
10fmt.Println(greet("Hank"))
11
12// 3. 闭包:捕获外部变量
13counter := 0
14increment := func() int {
15    counter++  // 捕获并修改外部变量
16    return counter
17}
18fmt.Println(increment())  // 1
19fmt.Println(increment())  // 2

闭包的经典应用

返回一个闭包函数,每个闭包都有自己的状态:

 1func makeAdder(x int) func(int) int {
 2    return func(y int) int {
 3        return x + y
 4    }
 5}
 6
 7add5 := makeAdder(5)
 8add10 := makeAdder(10)
 9
10fmt.Println(add5(3))   // 8
11fmt.Println(add10(3))  // 13

💡 技巧:闭包常用于工厂函数、延迟计算、状态保持等场景。

函数作为值:一等公民

在 Go 中,函数可以作为变量、参数和返回值:

 1// 定义函数类型
 2type Operation func(int, int) int
 3
 4func calculate(a, b int, op Operation) int {
 5    return op(a, b)
 6}
 7
 8// 使用
 9add := func(x, y int) int { return x + y }
10multiply := func(x, y int) int { return x * y }
11
12fmt.Println(calculate(10, 5, add))       // 15
13fmt.Println(calculate(10, 5, multiply))  // 50

这种特性让 Go 可以实现策略模式、回调函数等设计模式。

defer:延迟执行的魔法

defer 用于延迟函数的执行,直到包含它的函数返回。常用于资源清理:

 1func readFile(filename string) error {
 2    file, err := os.Open(filename)
 3    if err != nil {
 4        return err
 5    }
 6    defer file.Close()  // 确保函数返回前关闭文件
 7
 8    // 读取文件内容...
 9    return nil
10}

defer 的三个特性

  1. 后进先出(LIFO):多个 defer 按栈的顺序执行
1func deferOrder() {
2    defer fmt.Println("1")
3    defer fmt.Println("2")
4    defer fmt.Println("3")
5    fmt.Println("4")
6}
7// 输出: 4 3 2 1
  1. 参数立即求值:defer 语句的参数会立即求值,但函数调用延迟执行
1func deferEval() {
2    i := 0
3    defer fmt.Println(i)  // 立即求值,i = 0
4    i++
5    return
6}
7// 输出: 0(不是 1)
  1. 可以修改命名返回值:defer 中可以修改函数的返回值
1func returnValue() (result int) {
2    defer func() {
3        result++  // 修改命名返回值
4    }()
5    return 5  // result = 5, 然后执行 defer, result 变成 6
6}
7// 返回 6

💡 技巧:defer 常用于关闭文件、释放锁、记录日志、测量执行时间等场景。

方法:给类型绑定函数

方法就是带接收者的函数,可以给任何自定义类型绑定方法:

 1type User struct {
 2    Name string
 3    Age  int
 4}
 5
 6// 值接收者:不会修改原对象
 7func (u User) SayHello() {
 8    fmt.Printf("Hello, I'm %s\n", u.Name)
 9}
10
11// 指针接收者:可以修改原对象
12func (u *User) Grow() {
13    u.Age++
14}
15
16// 使用
17user := User{Name: "Hank", Age: 18}
18user.SayHello()  // Hello, I'm Hank
19user.Grow()
20fmt.Println(user.Age)  // 19

值接收者 vs 指针接收者

  • 值接收者 (u User):调用时复制数据,不会修改原对象
  • 指针接收者 (u *User):传递引用,可以修改原对象,且避免大对象拷贝

老墨建议:优先使用指针接收者,除非:

  • 类型很小(如 time.Time
  • 类型是不可变的(如只读配置)
  • 需要值语义(如 sync.Mutex 不能复制)

方法值和方法表达式

方法可以像函数一样被赋值和传递:

 1type Calculator struct {
 2    value int
 3}
 4
 5func (c *Calculator) Add(n int) {
 6    c.value += n
 7}
 8
 9// 方法值:绑定到特定实例
10calc := &Calculator{value: 10}
11addMethod := calc.Add
12addMethod(5)
13fmt.Println(calc.value)  // 15
14
15// 方法表达式:需要显式传递接收者
16addExpr := (*Calculator).Add
17addExpr(calc, 10)
18fmt.Println(calc.value)  // 25

方法集规则:接口实现的关键

这是 Go 中容易混淆的地方,理解方法集对于接口实现很重要:

  • 值类型 T 的方法集:只包含值接收者的方法
  • 指针类型 *T 的方法集:包含值接收者和指针接收者的所有方法
 1type Counter struct {
 2    count int
 3}
 4
 5func (c Counter) GetCount() int {
 6    return c.count
 7}
 8
 9func (c *Counter) Increment() {
10    c.count++
11}
12
13// 值类型
14c1 := Counter{count: 0}
15c1.GetCount()   // ✅ 可以调用
16c1.Increment()  // ✅ Go 自动转换为 (&c1).Increment()
17
18// 指针类型
19c2 := &Counter{count: 0}
20c2.GetCount()   // ✅ 可以调用
21c2.Increment()  // ✅ 可以调用

⚠️ 小坑:接口实现时的差异

 1type Incrementer interface {
 2    Increment()
 3}
 4
 5func doIncrement(i Incrementer) {
 6    i.Increment()
 7}
 8
 9c1 := Counter{count: 0}
10// doIncrement(c1)  // ❌ 编译错误:Counter 没有实现 Incrementer
11
12c2 := &Counter{count: 0}
13doIncrement(c2)     // ✅ *Counter 实现了 Incrementer

记住:如果方法有指针接收者,只有指针类型才实现了接口。

错误处理:Go 的显式风格

Go 使用显式的错误返回而不是异常机制:

 1func readConfig(filename string) (*Config, error) {
 2    data, err := os.ReadFile(filename)
 3    if err != nil {
 4        return nil, fmt.Errorf("failed to read config: %w", err)
 5    }
 6
 7    var config Config
 8    if err := json.Unmarshal(data, &config); err != nil {
 9        return nil, fmt.Errorf("failed to parse config: %w", err)
10    }
11
12    return &config, nil
13}

错误包装:保留错误链

使用 %w 可以包装错误,保留错误链,方便使用 errors.Iserrors.As 判断:

 1var ErrNotFound = errors.New("not found")
 2
 3func findUser(id int) (*User, error) {
 4    // 模拟查找失败
 5    return nil, fmt.Errorf("user %d: %w", id, ErrNotFound)
 6}
 7
 8// 判断是否是特定错误
 9user, err := findUser(123)
10if err != nil {
11    if errors.Is(err, ErrNotFound) {
12        fmt.Println("User not found, creating new one...")
13    } else {
14        fmt.Println("Unexpected error:", err)
15    }
16}

自定义错误类型

 1type ValidationError struct {
 2    Field string
 3    Value interface{}
 4    Msg   string
 5}
 6
 7func (e *ValidationError) Error() string {
 8    return fmt.Sprintf("validation failed for %s: %s (value: %v)",
 9        e.Field, e.Msg, e.Value)
10}
11
12func validateAge(age int) error {
13    if age < 0 || age > 150 {
14        return &ValidationError{
15            Field: "age",
16            Value: age,
17            Msg:   "must be between 0 and 150",
18        }
19    }
20    return nil
21}

完整示例

把前面讲的知识点串起来,看个完整的例子:

 1package main
 2
 3import (
 4    "errors"
 5    "fmt"
 6    "time"
 7)
 8
 9// 用户类型
10type User struct {
11    Name string
12    Age  int
13}
14
15// 值接收者方法
16func (u User) String() string {
17    return fmt.Sprintf("%s (%d years old)", u.Name, u.Age)
18}
19
20// 指针接收者方法
21func (u *User) Grow() {
22    u.Age++
23}
24
25// 多返回值 + 命名返回值
26func divide(a, b int) (result int, err error) {
27    if b == 0 {
28        err = errors.New("division by zero")
29        return
30    }
31    result = a / b
32    return
33}
34
35// 可变参数
36func sum(nums ...int) int {
37    total := 0
38    for _, num := range nums {
39        total += num
40    }
41    return total
42}
43
44// 返回闭包
45func makeMultiplier(factor int) func(int) int {
46    return func(x int) int {
47        return x * factor
48    }
49}
50
51// 使用 defer 测量执行时间
52func measureTime(name string) func() {
53    start := time.Now()
54    return func() {
55        fmt.Printf("%s took %v\n", name, time.Since(start))
56    }
57}
58
59func slowOperation() {
60    defer measureTime("slowOperation")()
61    time.Sleep(100 * time.Millisecond)
62    fmt.Println("Operation completed")
63}
64
65func main() {
66    // 1. 方法调用
67    user := User{Name: "Hank", Age: 18}
68    fmt.Println(user)
69    user.Grow()
70    fmt.Println(user)
71
72    // 2. 多返回值
73    result, err := divide(10, 2)
74    if err != nil {
75        fmt.Println("Error:", err)
76    } else {
77        fmt.Println("10 / 2 =", result)
78    }
79
80    // 3. 可变参数
81    fmt.Println("Sum:", sum(1, 2, 3, 4, 5))
82
83    // 4. 闭包
84    double := makeMultiplier(2)
85    triple := makeMultiplier(3)
86    fmt.Println("double(5) =", double(5))
87    fmt.Println("triple(5) =", triple(5))
88
89    // 5. defer
90    slowOperation()
91}

这个例子展示了:

  • 方法的值接收者和指针接收者
  • 多返回值和命名返回值
  • 可变参数
  • 闭包的使用
  • defer 的实际应用

老墨总结

Go 函数和方法的几个亮点:

  1. 多返回值result, err 模式让错误处理更清晰,不用搞异常捕获
  2. 闭包:可以捕获外部变量,实现工厂函数、状态保持等场景
  3. defer:延迟执行,确保资源释放,遵循 LIFO 顺序
  4. 方法绑定:通过接收者给类型添加方法,指针接收者可以修改状态
  5. 方法集规则*T 的方法集包含 T 的所有方法,接口实现要注意

实战建议

  • 优先使用指针接收者,避免大对象拷贝
  • 同一类型的方法接收者类型要一致
  • defer 确保资源释放,但注意性能开销
  • 错误处理用 %w 包装,保留错误链
  • 闭包捕获变量要小心,特别是在循环中

Go 的函数设计简洁但强大,多返回值、闭包、defer 组合起来,让代码既优雅又实用。


你在用 Go 时,最喜欢哪个函数特性?defer 还是闭包?欢迎评论区讨论!

极客老墨,继续折腾!

练习题

  1. 编写一个函数 div(a, b int) (result int, err error),实现除法运算。如果除数为 0,返回错误信息(提示:使用 errors.New 和命名返回值)
  2. User 结构体添加一个 SetAge(age int) 方法,确保能修改用户的年龄
  3. 编写一个 filter 函数,接收一个整数切片和一个过滤函数,返回满足条件的元素:
    1func filter(nums []int, fn func(int) bool) []int
    
  4. 使用闭包实现一个计数器生成器,每次调用返回递增的数字
  5. 编写一个函数,使用 defer 来测量函数的执行时间
  6. 实现一个自定义错误类型 RangeError,包含最小值、最大值和实际值信息

快来评论区秀出你的代码,大家一起讨论!


极客老墨,继续折腾!

如果有任何问题,欢迎在评论区留言或关注公众号「极客老墨」交流。

完整示例代码在 go-tutorial-code/05-func


相关阅读