大家好,我是极客老墨。
函数返回多个值?刚接触 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 的三个特性
- 后进先出(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
- 参数立即求值:defer 语句的参数会立即求值,但函数调用延迟执行
1func deferEval() {
2 i := 0
3 defer fmt.Println(i) // 立即求值,i = 0
4 i++
5 return
6}
7// 输出: 0(不是 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.Is 和 errors.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 函数和方法的几个亮点:
- 多返回值:
result, err模式让错误处理更清晰,不用搞异常捕获 - 闭包:可以捕获外部变量,实现工厂函数、状态保持等场景
- defer:延迟执行,确保资源释放,遵循 LIFO 顺序
- 方法绑定:通过接收者给类型添加方法,指针接收者可以修改状态
- 方法集规则:
*T的方法集包含T的所有方法,接口实现要注意
实战建议:
- 优先使用指针接收者,避免大对象拷贝
- 同一类型的方法接收者类型要一致
- defer 确保资源释放,但注意性能开销
- 错误处理用
%w包装,保留错误链 - 闭包捕获变量要小心,特别是在循环中
Go 的函数设计简洁但强大,多返回值、闭包、defer 组合起来,让代码既优雅又实用。
你在用 Go 时,最喜欢哪个函数特性?defer 还是闭包?欢迎评论区讨论!
极客老墨,继续折腾!
练习题
- 编写一个函数
div(a, b int) (result int, err error),实现除法运算。如果除数为 0,返回错误信息(提示:使用errors.New和命名返回值) - 为
User结构体添加一个SetAge(age int)方法,确保能修改用户的年龄 - 编写一个
filter函数,接收一个整数切片和一个过滤函数,返回满足条件的元素:1func filter(nums []int, fn func(int) bool) []int - 使用闭包实现一个计数器生成器,每次调用返回递增的数字
- 编写一个函数,使用
defer来测量函数的执行时间 - 实现一个自定义错误类型
RangeError,包含最小值、最大值和实际值信息
快来评论区秀出你的代码,大家一起讨论!
极客老墨,继续折腾!
如果有任何问题,欢迎在评论区留言或关注公众号「极客老墨」交流。
完整示例代码在 go-tutorial-code/05-func。