Skip to content

函数:参数、返回值与多返回值

本章深入Go函数:命名返回值、多返回值+错误处理、匿名函数+闭包、defer机制。重点:Go函数是一等公民,支持多返回值和闭包;defer是独特的时间点执行模式。

前置知识:第3章 语法基础 学习目标:掌握Go函数特性、理解闭包陷阱、熟练使用defer


// 完整形式
func add(a int, b int) int {
return a + b
}
// 相同类型可以合并
func add(a, b int) int {
return a + b
}
// 无参数
func greet() {
fmt.Println("Hello!")
}
// 无返回值
func printSum(a, b int) {
fmt.Println(a + b)
}
def add(a, b):
return a + b
def greet():
print("Hello!")
def print_sum(a, b):
print(a + b)

Go允许为返回值命名,使其成为命名变量:

func divide(a, b int) (quotient int, remainder int) {
quotient = a / b // 命名变量赋值
remainder = a % b
return // 裸return返回这些值
}
// 调用
q, r := divide(10, 3)
fmt.Println(q, r) // 3 1
func split(sum int) (x, y int) {
// 此时x, y已经声明,类型为int,值为0
// 相当于:
// var x, y int
x = sum * 4 / 9 // 赋值
y = sum - x // 赋值
return // 返回x和y
}

**裸return(bare return)**只能用于短函数(如上所示),用于返回命名返回值。

// 多返回值
func getUser() (string, int) {
return "Alice", 30
}
// 命名+多返回值
func getUser() (name string, age int) {
name = "Alice"
age = 30
return // 自动返回name和age
}

本节目的:理解Go错误处理哲学、掌握多返回值和error类型

Go没有异常机制,错误通过返回值传递:

// Python: 用异常处理错误
def divide(a, b):
if b == 0:
raise ZeroDivisionError("除数不能为零")
return a / b
try:
result = divide(10, 0)
except ZeroDivisionError as e:
print(f"错误: {e}")
// Go: 错误作为返回值
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数不能为零")
}
return a / b, nil
}
result, err := divide(10, 0)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("结果:", result)
}

Go的标准错误接口:

type error interface {
Error() string
}

所有错误都实现了这个接口。errors.New()创建简单错误:

import "errors"
err := errors.New("错误信息")
fmt.Println(err.Error()) // 错误信息

模式一:立即检查

result, err := divide(10, 0)
if err != nil {
// 处理错误
return err // 或继续处理
}

模式二:传播错误

func processFile(filename string) error {
data, err := os.ReadFile(filename)
if err != nil {
return err // 错误原样返回
}
// 继续处理
return nil
}

模式三:包装错误(推荐)

import "fmt"
func processFile(filename string) error {
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("读取文件失败 %s: %w", filename, err)
}
return nil
}

fmt.Errorf的%w动词创建错误链,可用errors.Unwrap()解包。

如果确定不会出错,可以忽略error:

file, _ := os.Open("test.txt") // 忽略error
// 风险:忽略错误可能导致后续问题

函数可以返回多个错误:

func copyFile(src, dst string) (int64, error) {
srcFile, err := os.Open(src)
if err != nil {
return 0, fmt.Errorf("打开源文件失败: %w", err)
}
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return 0, fmt.Errorf("创建目标文件失败: %w", err)
}
defer dstFile.Close()
return io.Copy(dstFile, srcFile)
}
import "errors"
// 创建
errors.New("message")
// 判断
errors.Is(err, io.EOF) // 错误链中是否有特定错误
errors.As(err, new(*MyError)) // 错误链中是否有特定类型
// 错误链解包
err2 := fmt.Errorf("包装: %w", err1)
errors.Is(err2, err1) // true

本节目的:理解闭包概念、警惕循环变量捕获陷阱

// 声明并立即赋值给变量
add := func(a, b int) int {
return a + b
}
result := add(1, 2) // 3
// 立即执行
result := func(a, b int) int {
return a + b
}(1, 2)
fmt.Println(result) // 3

闭包是引用了外部变量的匿名函数:

func adder() func(int) int {
sum := 0 // 闭包捕获的变量
return func(x int) int {
sum += x
return sum
}
}
func main() {
add := adder() // add是一个闭包
fmt.Println(add(1)) // 1
fmt.Println(add(2)) // 3
fmt.Println(add(3)) // 6
// 每次调用adder()创建新的闭包
add2 := adder()
fmt.Println(add2(10)) // 10
}

常见错误:

func main() {
// 错误:所有闭包共享同一个变量
funcs := []func(){}
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Println(i) // 打印的是循环结束后的i值
})
}
for _, f := range funcs {
f() // 输出: 3 3 3(而非0 1 2)
}
}

正确做法:在循环内创建新变量

func main() {
funcs := []func(){}
for i := 0; i < 3; i++ {
i := i // 创建新变量
funcs = append(funcs, func() {
fmt.Println(i)
})
}
for _, f := range funcs {
f() // 输出: 0 1 2
}
}

另一种方法:作为参数传递

func main() {
funcs := []func(){}
for i := 0; i < 3; i++ {
v := i // 创建新变量
funcs = append(funcs, func() {
fmt.Println(v)
})
}
// ...
}
# Python闭包
def adder():
sum = 0
def add(x):
nonlocal sum
sum += x
return sum
return add
add = adder()
print(add(1)) # 1
print(add(2)) # 3
# Python不需担心循环闭包问题
funcs = []
for i in range(3):
funcs.append(lambda: print(i)) # 默认参数捕获当前值
for f in funcs:
f() # 输出: 0 1 2

本节目的:掌握defer的延迟执行特性、理解参数求值时机

defer语句延迟函数执行,直到周围函数返回:

func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
// defer确保文件关闭
defer file.Close()
// 读取文件...
return nil
}

LIFO(后进先出):

func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("main")
}
// 输出:
// main
// 3
// 2
// 1

1. 关闭资源

func process() {
// 打开连接
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
return
}
defer conn.Close()
// 使用连接...
}

2. 释放锁

var mu sync.Mutex
var counter int
func increment() {
mu.Lock()
defer mu.Unlock()
counter++
}

3. 记录函数执行时间

func slowOperation() {
defer trace("slowOperation")()
// ... 操作
}
func trace(name string) func() {
start := time.Now()
fmt.Printf("进入 %s\n", name)
return func() {
fmt.Printf("离开 %s (%v)\n", name, time.Since(start))
}
}

defer语句中的参数在defer时求值,而非函数返回时:

func main() {
i := 0
defer fmt.Println(i) // 打印0(此时i=0)
i = 10
return // 输出: 0
}

改变这个行为:使用命名返回值或闭包

func main() {
i := 0
defer func() {
fmt.Println(i) // 打印10(闭包捕获i的引用)
}()
i = 10
return
}

defer在panic时仍会执行:

func main() {
defer fmt.Println("defer执行")
fmt.Println("panic前")
panic("发生错误")
fmt.Println("panic后") // 不会执行
}
// 输出:
// panic前
// defer执行
// panic: 发生错误
func safeCall() {
defer func() {
if r := recover(); r != nil {
fmt.Println("捕获panic:", r)
}
}()
fmt.Println("可能panic的代码")
panic("oops")
}
func main() {
safeCall()
fmt.Println("继续执行")
}
// 输出:
// 可能panic的代码
// 捕获panic: oops
// 继续执行

  1. 错误处理链:每个错误都检查,不要忽略err != nil;用fmt.Errorf包装错误信息
  2. 避免defer in loop:循环内的defer会累积,函数返回时才执行,可能导致资源泄漏
  3. 闭包循环陷阱:循环内创建闭包时,循环变量是引用。用func(id int)传参捕获当前值
  4. defer参数立即求值:defer fmt.Println(x)会在defer时计算x,不是执行时
  5. defer vs return:defer在return之后、实际退出前执行;recover只能用于defer中

PythonGo
def f(a, b): return a+bfunc f(a, b int) int { return a + b }
多返回值需返回dict/tuple多返回值原生支持
try/except处理错误if err != nil 处理错误
lambdafunc(a, b int) int { return a + b }
nonlocal捕获变量闭包捕获外部变量
无defer延迟执行
finallydefer(更强大)

  1. 写一个divide函数,返回商和余数,使用命名返回值
  2. 写一个函数返回(int, error),练习错误处理
  3. 实现一个闭包:counter,每次调用递增1
  4. 写一个函数,用defer记录进入和退出时间
  5. 实现safeDivide,用recover捕获panic