Skip to content

4.1 学习 Go 函数:理解 Go 里的函数

函数是把一段可复用逻辑封装起来的代码单元。

本章说明:如何定义函数参数与返回值匿名函数与闭包defer 延迟调用,以及 panic / recover 的异常处理

一、函数是什么

  • 功能或逻辑拆分代码,有利于可读性维护
  • Go 里常见两类写法:具名函数匿名函数(无函数名)。
  • Go 是编译型语言函数定义顺序与能否被调用无关(不要求像部分脚本语言那样「先定义后调用」)。

二、函数的定义与声明

使用关键字 func,依次写:函数名参数列表返回值列表函数体(花括号内)。

text
func 函数名(形式参数列表)(返回值列表) {
    函数体
}
  • 形式参数:在函数内是局部变量,由调用方传入实参。
  • 返回值列表:可写返回值的类型;若只有一个返回值且未命名,外层括号可省略;无返回值时可不写返回值列表。

示例:

go
package main

import "fmt"

func sum(a int, b int) int {
	return a + b
}

func main() {
	fmt.Println(sum(1, 2))
}

相邻同类型参数可合并写法:func sum(a, b int) int

三、参数

1. 普通参数

调用时个数、类型需与定义一致(无 Python 式的默认参数、**kwargs 等语法)。少传、多传或类型不对,编译器会报错。

go
package main

import "fmt"

// 多个不同类型参数
func greet(name string, age int) {
	fmt.Printf("%s 今年 %d\n", name, age)
}

// 相邻同类型可合并:a、b 都是 int
func rectArea(width, height int) int {
	return width * height
}

func main() {
	greet("小明", 18)
	fmt.Println("面积:", rectArea(3, 4))
}

2. 可变参数:...T

使用 ...类型 表示「零个或多个该类型实参」,在函数体内对应 []T 切片可变参数必须是参数列表中的最后一个

go
package main

import "fmt"

func sum(args ...int) int {
	var s int
	for _, v := range args {
		s += v
	}
	return s
}

func main() {
	fmt.Println(sum(1, 2, 3))
}

3. 多类型可变参数:...interface{}

需要同时接收多种类型时,可用 ...interface{}(空接口,参见 相关章节),再按类型分支处理。标准库 fmt.Printf 即类似思路。

go
package main

import "fmt"

func MyPrintf(args ...interface{}) {
	for _, arg := range args {
		switch arg.(type) {
		case int:
			fmt.Println(arg, "is int")
		case string:
			fmt.Println(arg, "is string")
		default:
			fmt.Println(arg, "other")
		}
	}
}

func main() {
	MyPrintf(1, "hello", 3.14)
}

4. 展开切片:... 传参

调用可变参数函数时,可用 slice... 把切片拆开逐个传入,而不是把整个切片当作一个参数。

go
package main

import "fmt"

func sum(args ...int) int {
	var r int
	for _, v := range args {
		r += v
	}
	return r
}

func SumAll(parts ...int) int {
	return sum(parts...) // 等价于把 parts 中每个元素传给 sum
}

func main() {
	fmt.Println(SumAll(1, 2, 3))
}

四、返回值

1. 无返回值

不写返回值类型时,可用 return 提前结束,return 后不能带值

go
package main

import "fmt"

func logOnly(msg string) {
	fmt.Println(msg)
	return // 可省略
}

func main() {
	logOnly("hi")
}

2. 多个返回值

Go 支持多返回值,常用于 result, err 模式。

go
package main

import "fmt"

func double(a int) (int, int) {
	b := a * 2
	return a, b
}

func main() {
	x, y := double(2)
	fmt.Println(x, y)
}

3. 用 _ 忽略某个返回值

多返回值函数有时只需要其中一部分,另一部分可用空白标识符 _ 显式丢弃(与 for _, v := range 里忽略下标同理)。

go
package main

import "fmt"

func pair() (int, int) {
	return 1, 2
}

func main() {
	x, _ := pair() // 只要第一个返回值,第二个用 _ 忽略
	fmt.Println(x) // 1

	// 不能写成单变量接收多返回值,例如:
	// s := pair() // 编译错误:multiple-value pair() in single-value context
}

两个返回值都不需要时,可写 _, _ := pair()

4. 命名返回值

在返回值列表中写出变量名,相当于在函数开头已声明;函数体内可直接赋值,return 会返回这些命名变量(易读但也易让人忽略赋值,需留心)。

go
package main

import "fmt"

func doubleNamed(a int) (b int) {
	b = a * 2
	return // 等同于 return b
}

func main() {
	fmt.Println(doubleNamed(2))
}

五、匿名函数

没有函数名的函数,只有参数、返回值(可省略)与函数体:

go
func(参数列表)(返回参数列表) {
    函数体
}

典型用法:

  1. 定义后立即执行(末尾一对括号传入实参,常称 IIFE):
go
package main

import "fmt"

func main() {
	func(data int) {
		fmt.Println("hello", data)
	}(100)
}
  1. 赋给变量再调用:匿名函数本身是,可存进变量,类型为 func(...),之后像普通函数一样多次调用。
go
package main

import "fmt"

func main() {
	mul := func(a, b int) int {
		return a * b
	}
	fmt.Println(mul(2, 3)) // 6
	fmt.Println(mul(4, 5)) // 20
}
  1. 作为值传递(回调、传给 go func() 等):
go
package main

import "fmt"

func visit(list []int, f func(int)) {
	for _, v := range list {
		f(v)
	}
}

func main() {
	visit([]int{1, 2, 3, 4}, func(v int) {
		fmt.Println(v)
	})
}

只在一处使用的短逻辑,用匿名函数可减少「再起一个包级函数名」的负担;需要多次复用同一段逻辑时,可先赋给局部变量(如上第 2 种)。

六、闭包

闭包:函数引用了外部作用域的变量,这些变量被「捕获」在函数闭包中,多次调用可共享、修改同一状态。

go
package main

import "fmt"

func adder() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x
		return sum
	}
}

func main() {
	pos := adder()
	fmt.Println(pos(1)) // 1
	fmt.Println(pos(2)) // 3
	fmt.Println(pos(3)) // 6
}

注意: 若在循环里创建闭包并延迟异步使用(例如 go func()),勿直接捕获循环变量指针,应传参复制当前值,避免「全是最后一次循环的值」(详见并发章节常见写法)。

七、延迟调用 defer

defer函数调用推迟到当前函数返回之前执行(return 之后、真正返回给调用方之前,按栈处理)。

  • 多个 defer:按 LIFO(后进先出) 执行。
  • defer 后面的表达式会立即求值,只有被 defer 的函数调用在返回前执行。
go
package main

import "fmt"

func main() {
	defer fmt.Println("world")
	fmt.Println("hello")
}
// 输出:先 hello,再 world

常见用途: 关闭文件、解锁互斥锁、Close() 资源等,与「成对操作」写在一起,减少遗漏。

go
package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("example.txt")
    if err!= nil {
        fmt.Printf("无法打开文件: %v\n", err)
        return
    }
    defer file.Close()

    // 这里开始处理文件,例如读取文件内容
    var content []byte
    content, err = os.ReadFile("example.txt")
    if err!= nil {
        fmt.Printf("无法读取文件: %v\n", err)
        return
    }
    fmt.Println(string(content))
}

defer 的完整机制(执行顺序、LIFO、参数何时求值、资源清理、发生 panic 时是否仍会执行等)见 1.14 流程控制:defer 延迟语句

八、panicrecover

Go 没有 try/catch。运行时不可恢复错误可用 panic(v) 中断当前 goroutine 的正常流程,并沿调用栈向上展开;若未被恢复,程序崩溃。

recover() 只能在 defer 的函数里调用:可拦截当前 goroutine 上的 panic,拿到 panic 传入的值,使程序从 panic 点恢复继续执行(通常用于必须兜底的库或 HTTP 中间件等)。

go
package main

import "fmt"

func safe() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("recovered:", r)
		}
	}()
	panic("something wrong")
}

func main() {
	safe()
	fmt.Println("main continues")
}

实践建议:

  • 业务错误优先用 error 返回值,不要滥用 panic
  • recover 只应处理确实需要兜底的场景;滥用会掩盖 bug。

recover 为何必须在 defer 里调用deferpanic 的配合及示例,见 《1.16 异常机制:panic 和 recover》

九、方法与函数

方法绑定到类型(多为结构体)上的特殊函数,定义时带有接收者 func (r Receiver) Name(...)。普通函数无接收者。

方法与函数的对比及示例见 2.1 面向对象:结构体

小结

主题要点
定义func + 名 + 参数 + 返回值 + 体;定义顺序不限
参数可变参数 ...T、展开 slice...、多类型可用 ...interface{}
返回值多返回值、_ 忽略某项、命名返回值与裸 return
匿名函数立即执行(IIFE)、赋给变量再调用、作参数/回调
闭包捕获外部变量;注意循环变量与异步
defer返回前执行、LIFO、常用于释放资源
panic/recover非通用异常机制;优先 errorrecover 仅在 defer 中有效