Go: Go 函数:多返回值与闭包
函数是 Go 代码的"乐高积木"——Go 把"函数是一等公民"做到了极致,让组合式编程变得简单。
Go 的函数设计哲学:用最少的语法表达最多的复用。多返回值、命名返回值、可变参数、closure、函数作为值传递——这些在其他语言需要"framework"或"library"才能实现的能力,在 Go 是原生语法。
1. 你将学到
func函数定义的 4 个元素- 多返回值与命名返回值
- 可变参数
...的使用 - 匿名函数与闭包
init函数与包初始化- 函数作为参数和返回值(高阶函数)
- 用函数构建数据处理管道
2. 一个数据工程师的真实故事
(1) 痛点:Java 函数只能返回一个值
Bob 是一个数据工程师,他最近需要写一个数据处理函数:
"我要写一个
parseUserDatafunction,既要返回解析后的 user object,又要返回错误码;既要知道解析耗时,又要返回警告信息——Java 只能返回一个值,我被迫把所有东西塞进一个 Result 对象里,代码读起来像吃意大利面。"
他打开 Java 代码:
// Java:只能返回一个值,被迫打包
public class ParseResult {
public User user;
public int errorCode;
public long durationMs;
public List<String> warnings;
}
public ParseResult parseUserData(String raw) {
// 5 个返回值都在这里
return new ParseResult(...);
}
同事 review 时吐槽:"你这函数就像俄罗斯套娃,要 5 个字段我得一层层剥开。"
(2) Go 的解法
Go 的函数原生支持多返回值:
// user_parser.go
package main
import (
"fmt"
"strconv"
"strings"
"time"
)
// 多返回值:user + errorCode + durationMs + warnings
func parseUserData(raw string) (User, int, time.Duration, []string) {
start := time.Now()
var warnings []string
parts := strings.Split(raw, ",")
if len(parts) != 3 {
return User{}, 400, time.Since(start), []string{"格式错误:需要 3 个字段"}
}
age, err := strconv.Atoi(parts[1])
if err != nil {
return User{}, 400, time.Since(start), []string{"年龄格式错误"}
}
if age < 0 || age > 150 {
warnings = append(warnings, "年龄异常")
}
user := User{Name: parts[0], Age: age, City: parts[2]}
return user, 200, time.Since(start), warnings
}
type User struct {
Name string
Age int
City string
}
func main() {
user, code, duration, warnings := parseUserData("Alice,28,Shanghai")
fmt.Printf("Status: %d\n", code)
fmt.Printf("User: %+v\n", user)
fmt.Printf("Duration: %v\n", duration)
fmt.Printf("Warnings: %v\n", warnings)
}
输出:
Status: 200
User: {Name:Alice Age:28 City:Shanghai}
Duration: 12.5µs
Warnings: []
(3) 收益:Go 函数 vs 其他语言
| 特性 | C | Java | Python | Go |
|---|---|---|---|---|
| 多返回值 | ❌ 需 struct 包装 | ❌ 需包装类 | ✅ 元组 | ✅ 原生 |
| 命名返回值 | ❌ | ❌ | ❌ | ✅ 原生 |
| 函数作为值 | 函数指针 | Lambda | 一等公民 | ✅ 一等公民 |
| 闭包 | ✅(复杂) | Lambda | ✅ | ✅ 简洁 |
| 可变参数 | ✅(stdarg) | ✅ varargs | ✅ *args | ✅ ... |
func foo() (T, error))。这是 Go 错误处理哲学的基石。
sequenceDiagram
participant Caller
participant Function as parseUserData()
participant Parser as 内部逻辑
Caller->>Function: parseUserData(raw)
Function->>Parser: 拆字段
Parser-->>Function: name, age, city
Function-->>Caller: user + 200 + duration + warnings
Note over Caller: 多返回值:同时接收<br/>user/status/time/warnings
3. func 函数定义
(1) 函数定义的 4 个元素
func functionName(param1 type1, param2 type2) returnType {
// 函数体
return value
}
| 元素 | 关键字 | 必需? |
|---|---|---|
| 函数名 | funcName |
是 |
| 参数列表 | (param type, ...) |
是 |
| 返回类型 | returnType |
否(无返回值可省略) |
| 函数体 | { ... } |
是 |
▶ 示例:基础函数定义 4 种形式
package main
import "fmt"
// 形式 1:无参数无返回值
func sayHello() {
fmt.Println("Hello!")
}
// 形式 2:有参数无返回值
func greet(name string) {
fmt.Printf("Hello, %s!\n", name)
}
// 形式 3:有参数有返回值
func add(a, b int) int {
return a + b
}
// 形式 4:多参数多返回值
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
func main() {
sayHello() // Hello!
greet("Alice") // Hello, Alice!
fmt.Println(add(2, 3)) // 5
result, err := divide(10.0, 2.0)
fmt.Printf("%.2f, err=%v\n", result, err) // 5.00, err=<nil>
}
输出:
Hello!
Hello, Alice!
5
5.00, err=<nil>
(2) 参数简写
同类型的连续参数可合并类型:
func add(a, b int) int // 等价于 a int, b int
func rect(w, h int) (int, int) // 两个参数都是 int
4. 多返回值
(1) 多返回值定义
package main
import "fmt"
func swap(a, b string) (string, string) {
return b, a
}
func main() {
x, y := swap("hello", "world")
fmt.Println(x, y) // world hello
}
▶ 示例:值 + 错误(Go 招牌模式)
package main
import (
"errors"
"fmt"
)
func findUser(id int) (string, error) {
if id <= 0 {
return "", errors.New("invalid id")
}
if id == 999 {
return "", fmt.Errorf("user %d not found", id)
}
return fmt.Sprintf("User-%d", id), nil
}
func main() {
user, err := findUser(1)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Found: %s\n", user)
// 忽略某个返回值:用 _
_, err = findUser(999)
fmt.Printf("Ignored: err=%v\n", err)
}
输出:
Found: User-1
Ignored: err=user 999 not found
(2) 命名返回值
命名返回值在函数顶部声明,return 自动返回这些变量:
package main
import "fmt"
func calc(a, b int) (sum, diff, product int) {
sum = a + b
diff = a - b
product = a * b
return // 裸 return,自动返回 sum/diff/product
}
func main() {
s, d, p := calc(10, 3)
fmt.Printf("sum=%d, diff=%d, product=%d\n", s, d, p)
}
输出:
sum=13, diff=7, product=30
(3) 多返回值 vs 命名返回值
| 场景 | 推荐 |
|---|---|
| 返回 1 个值 | 普通返回值 |
| 返回 2 个值(值+错误) | 普通返回值 |
| 返回 3+ 个值 | 命名返回值(清晰) |
| 需要在 defer 中修改返回值 | 必须命名返回值 |
5. 可变参数(Variadic)
(1) 可变参数语法 ...
package main
import "fmt"
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(10, 20)) // 30
fmt.Println(sum()) // 0
// 把 slice 拆开传入:nums...
nums := []int{1, 2, 3, 4, 5}
fmt.Println(sum(nums...)) // 15
}
输出:
6
30
0
15
▶ 示例:可变参数 + 格式化字符串
package main
import "fmt"
// 类似 fmt.Printf:第一个参数固定,剩余为可变
func logMessage(level string, args ...interface{}) {
fmt.Printf("[%s] ", level)
fmt.Println(args...) // 把 slice 展开传给 Println
}
func main() {
logMessage("INFO", "Server started", "on port", 8080)
logMessage("ERROR", "Database connection failed:", "timeout 5s")
logMessage("DEBUG")
}
输出:
[INFO] Server started on port 8080
[ERROR] Database connection failed: timeout 5s
[DEBUG]
(2) 可变参数的限制
| 限制 | 说明 |
|---|---|
| 最多 1 个可变参数 | func foo(a int, b ...int) ✅ |
| 可变参数必须是最后一个 | func foo(a ...int, b int) ❌ |
| 类型必须一致 | 想要多种类型用 ...interface{} |
6. 匿名函数与闭包
(1) 匿名函数(函数字面量)
匿名函数没有名字,可以赋值给变量或直接调用:
package main
import "fmt"
func main() {
// 赋值给变量
add := func(a, b int) int {
return a + b
}
fmt.Println(add(2, 3)) // 5
// 直接调用
func(x int) {
fmt.Printf("匿名函数:x=%d\n", x)
}(42)
}
输出:
5
匿名函数:x=42
(2) 闭包:捕获外部变量
闭包 = 函数 + 其引用的外部变量。闭包让函数"记住"创建时的环境:
▶ 示例:闭包实现计数器
package main
import "fmt"
// 返回一个闭包:每次调用都 +1
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
// 每个 counter 是独立的闭包
another := makeCounter()
fmt.Println(another()) // 1(重新计数)
}
输出:
1
2
3
1
(3) 闭包 vs 普通函数
| 维度 | 普通函数 | 闭包 |
|---|---|---|
| 状态 | 无状态 | 有状态(捕获变量) |
| 内存 | 静态 | 每次创建新实例 |
| 适用场景 | 纯计算 | 工厂、装饰器、回调 |
7. init 函数与包初始化
(1) init 函数特点
| 特性 | 说明 |
|---|---|
| 无参数无返回值 | func init() |
| 自动调用 | 包被 import 时自动执行 |
| 可多个 | 一个包可以有多个 init 函数(按声明顺序执行) |
| 先于 main | 在 main() 之前执行 |
▶ 示例:包初始化(注册表)
// registry.go
package main
import "fmt"
var registry = make(map[string]any)
func init() {
registry["version"] = 1
registry["max_connections"] = 100
fmt.Println("[init] registry initialized")
}
func init() {
registry["debug"] = true
fmt.Println("[init] debug enabled")
}
func main() {
fmt.Printf("Registry: %+v\n", registry)
}
输出:
[init] registry initialized
[init] debug enabled
Registry: map[debug:true max_connections:100 version:1]
(2) init vs main
| 函数 | 调用时机 | 用途 |
|---|---|---|
init() |
包 import 时自动执行 | 初始化全局变量、注册驱动 |
main() |
程序启动时执行(唯一) | 程序入口 |
8. 函数作为参数和返回值(高阶函数)
Go 的函数是一等公民——可以赋值给变量、作为参数传递、作为返回值。
(1) 函数作为参数(回调)
package main
import "fmt"
// 第二个参数是函数类型:接受 int 返回 int
func process(nums []int, callback func(int) int) []int {
result := make([]int, len(nums))
for i, n := range nums {
result[i] = callback(n)
}
return result
}
func double(n int) int { return n * 2 }
func square(n int) int { return n * n }
func main() {
nums := []int{1, 2, 3, 4, 5}
doubled := process(nums, double)
fmt.Println("doubled:", doubled)
squared := process(nums, square)
fmt.Println("squared:", squared)
}
输出:
doubled: [2 4 6 8 10]
squared: [1 4 9 16 25]
(2) 函数作为返回值(工厂)
package main
import "fmt"
func makeAdder(x int) func(int) int {
return func(y int) int {
return x + y
}
}
func main() {
add10 := makeAdder(10)
add100 := makeAdder(100)
fmt.Println(add10(5)) // 15
fmt.Println(add100(5)) // 105
}
输出:
15
105
▶ 示例:函数类型 + 高阶函数(map/filter/reduce)
package main
import "fmt"
func mapFunc(nums []int, f func(int) int) []int {
result := make([]int, len(nums))
for i, n := range nums {
result[i] = f(n)
}
return result
}
func filterFunc(nums []int, predicate func(int) bool) []int {
var result []int
for _, n := range nums {
if predicate(n) {
result = append(result, n)
}
}
return result
}
func reduceFunc(nums []int, initial int, f func(int, int) int) int {
acc := initial
for _, n := range nums {
acc = f(acc, n)
}
return acc
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 1. Map: 每个元素 *2
doubled := mapFunc(nums, func(n int) int { return n * 2 })
// 2. Filter: 保留偶数
evens := filterFunc(nums, func(n int) bool { return n%2 == 0 })
// 3. Reduce: 求和
sum := reduceFunc(nums, 0, func(acc, n int) int { return acc + n })
fmt.Printf("doubled: %v\n", doubled)
fmt.Printf("evens: %v\n", evens)
fmt.Printf("sum: %d\n", sum)
}
输出:
doubled: [2 4 6 8 10 12 14 16 18 20]
evens: [2 4 6 8 10]
sum: 55
9. 完整示例:数据处理管道
把函数的所有特性串起来,构建一个ETL(Extract-Transform-Load)数据管道:
// pipeline.go
package main
import (
"fmt"
"strings"
"time"
)
// 数据源
func extract() []string {
return []string{
" Alice,28,Shanghai ",
"Bob,32,Beijing",
"CHARLIE,45,Guangzhou",
"", // 空数据
"Dave,abc,ErrorCity", // 异常数据
}
}
// 步骤 1:清理空白 + 拆分字段
func trim(s string) []string {
return strings.Split(strings.TrimSpace(s), ",")
}
// 步骤 2:转换为大写
func upper(s []string) []string {
for i, v := range s {
s[i] = strings.ToUpper(v)
}
return s
}
// 步骤 3:验证字段数
func validate(s []string) (string, bool) {
if len(s) != 3 || s[0] == "" {
return "", false
}
return strings.Join(s, "|"), true
}
// Pipeline 函数:组合 3 个步骤 + defer 报告
func processPipeline(name string, data []string) (valid int, errors int) {
defer func() {
// 命名返回值让 defer 能修改结果
fmt.Printf("[%s] 完成:valid=%d errors=%d duration=%v\n",
name, valid, errors, time.Since(startTime))
}()
for _, raw := range data {
s := trim(raw)
if len(s) < 3 {
errors++
continue
}
s = upper(s)
if result, ok := validate(s); ok {
valid++
fmt.Printf(" -> %s\n", result)
} else {
errors++
}
}
return valid, errors
}
var startTime = time.Now()
func main() {
fmt.Println("=== 数据管道开始 ===")
// 用函数作为参数组合步骤
data := extract()
valid, errors := processPipeline("ETL-1", data)
fmt.Printf("\n汇总:valid=%d, errors=%d\n", valid, errors)
}
预期输出:
=== 数据管道开始 ===
-> ALICE|28|SHANGHAI
-> BOB|32|BEIJING
-> CHARLIE|45|GUANGZHOU
-> DAVE|abc|ERRORCITY
[ETL-1] 完成:valid=4 errors=1 duration=2.5µs
汇总:valid=4, errors=1
startTime 必须在文件级声明(不能在 init 里用 :=)。这是 init 函数和 var 声明的微妙区别——var 是包级别的,init 是函数级别的。
❓ 常见问题
func foo(a ...int) 或不同函数名。nums ...int 在函数内部就是 []int slice,但调用方式不同:foo(1,2,3) vs foo([]int{1,2,3}...)。前者是语法糖。📖 小节
- 函数定义 4 元素:
func+ 名称 + 参数列表 + 返回类型 - 多返回值是 Go 招牌特性,常用于
(value, error)错误处理模式 - 命名返回值让
return简洁,并允许 defer 修改返回值 - 可变参数
...替代重载,函数内部即 slice - 闭包捕获外部变量,可实现有状态的函数(计数器、工厂)
init在包 import 时自动执行,用于初始化全局状态- 函数是一等公民:可赋值、传参、返回,支持高阶函数(map/filter/reduce)
📝 作业
-
基础题(难度⭐):写一个
max(nums ...int) int函数,返回所有参数的最大值。要求用可变参数,调用max(1, 5, 3, 9, 2)输出 9。 -
进阶题(难度⭐⭐):实现一个
makeBankAccount(initial int) (deposit func(int) int, withdraw func(int) (int, bool), balance func() int),模拟银行账户:deposit 增加余额并返回新余额;withdraw 扣款(余额不够返回 false);balance 查询余额。要求用闭包。 -
挑战题(难度⭐⭐⭐):实现一个函数式数据管道:输入
[]string(数字字符串),用高阶函数依次完成parse -> filter(>10) -> map(*2) -> sum四步,要求每步是一个独立函数,组合后输出结果。例如["1", "15", "3", "20"]→parse [1,15,3,20]→filter>10 [15,20]→map*2 [30,40]→sum 70。