函数基础
函数基础
函数就像生活中的"自动贩卖机"——你投入参数,它按照既定规则处理,然后返回结果。在Go语言中,函数是一等公民,它可以赋值给变量、作为参数传递、作为返回值,甚至可以没有名字。掌握函数,是写出优雅Go代码的关键一步。
1. 核心概念
| 概念 | 说明 |
|---|---|
func 定义 |
使用 func 关键字声明函数,支持参数和返回值类型 |
| 多返回值 | Go函数可以返回多个值,常用于返回结果+错误 |
| 命名返回值 | 返回值可以命名,在函数体内直接使用,return 时自动返回 |
| 可变参数 | 使用 ...Type 接收任意数量的参数 |
| 匿名函数 | 没有名字的函数,常用于即时调用或赋值给变量 |
| 闭包 | 匿名函数捕获外部变量,形成闭包 |
init 函数 |
每个包可有多个 init 函数,程序启动时自动执行 |
defer |
延迟执行,函数返回前按栈顺序执行 |
2. 基本语法/用法
函数定义
GO
// 基本函数定义
func 函数名(参数列表) 返回值类型 {
// 函数体
return 值
}
// 无参数无返回值
func sayHello() {
fmt.Println("Hello!")
}
// 有参数有返回值
func add(a int, b int) int {
return a + b
}
// 参数类型相同时可简写
func add(a, b int) int {
return a + b
}
多返回值
GO
// 返回两个值:结果和错误
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为零")
}
return a / b, nil
}
// 调用时接收两个返回值
result, err := divide(10, 3)
命名返回值
GO
// 命名返回值:函数体内直接使用变量名
func rectangleInfo(length, width float64) (area, perimeter float64) {
area = length * width
perimeter = 2 * (length + width)
return // 自动返回命名的返回值
}
可变参数
GO
// 可变参数:使用 ...Type 接收任意数量参数
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// 调用
sum(1, 2, 3) // 6
sum(1, 2, 3, 4, 5) // 15
匿名函数与闭包
GO
// 匿名函数赋值给变量
greet := func(name string) string {
return "你好, " + name
}
// 立即执行函数
func() {
fmt.Println("立即执行")
}()
// 闭包:捕获外部变量
func counter() func() int {
count := 0
return func() int {
count++ // 捕获外部的 count 变量
return count
}
}
init 函数
GO
// init 函数:程序启动时自动执行,无需手动调用
// 每个包可以有多个 init 函数
func init() {
fmt.Println("init 函数执行")
}
defer
GO
// defer:延迟执行,函数返回前按 LIFO(后进先出)顺序执行
func readFile(filename string) {
f, _ := os.Open(filename)
defer f.Close() // 函数结束前关闭文件
// ... 读取文件
}
💡 提示:Go语言中,函数参数都是值传递。切片、map、channel、指针等传递的是引用的副本,但指向的数据是共享的。
💡 提示:
defer 语句的参数在声明时就会被求值,而不是在执行时求值。这是一个常见的陷阱。
💡 提示:
init 函数不能被其他函数调用,它会在 main 函数之前自动执行。如果有多个 init,按源文件字母顺序执行。
3. 基本语法/用法(示例)
示例 1:基础用法(难度⭐)
目标: 学习基本的函数定义、调用和多返回值。
GO
package main
import (
"errors"
"fmt"
)
// greet 接收一个名字,返回问候语
func greet(name string) string {
return "你好, " + name + "!"
}
// add 返回两个整数的和
func add(a, b int) int {
return a + b
}
// swap 交换两个字符串(演示多返回值)
func swap(a, b string) (string, string) {
return b, a
}
// divide 演示返回错误
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为零")
}
return a / b, nil
}
func main() {
// 调用单返回值函数
fmt.Println(greet("小明")) // 输出: 你好, 小明!
fmt.Println(add(3, 5)) // 输出: 8
// 调用多返回值函数
x, y := swap("hello", "world")
fmt.Println(x, y) // 输出: world hello
// 调用带错误处理的函数
result, err := divide(10, 3)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Printf("10 / 3 = %.2f\n", result) // 输出: 10 / 3 = 3.33
}
// 测试除零错误
_, err = divide(10, 0)
if err != nil {
fmt.Println("错误:", err) // 输出: 错误: 除数不能为零
}
}
运行结果:
TEXT
你好, 小明!
8
world hello
10 / 3 = 3.33
错误: 除数不能为零
要点:
- 使用
func关键字定义函数,参数类型在参数名之后 - 多返回值用逗号分隔,调用时用多个变量接收
- 错误处理是Go的核心模式:返回
(结果, error)
示例 2:进阶用法(难度⭐⭐)
目标: 学习命名返回值、可变参数、匿名函数和闭包。
GO
package main
import "fmt"
// ============ 命名返回值 ============
// rectangle 计算矩形的面积和周长(使用命名返回值)
func rectangle(length, width float64) (area, perimeter float64) {
area = length * width // 直接使用命名变量
perimeter = 2 * (length + width)
return // 自动返回命名的返回值,无需写 return area, perimeter
}
// ============ 可变参数 ============
// sum 计算任意个整数的和
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// average 计算平均值,演示可变参数与其他参数混用
func average(first float64, rest ...float64) float64 {
total := first
count := 1.0
for _, v := range rest {
total += v
count++
}
return total / count
}
// ============ 匿名函数 ============
// apply 对整数应用一个函数,返回结果
func apply(n int, f func(int) int) int {
return f(n)
}
func main() {
// 命名返回值
area, perimeter := rectangle(5, 3)
fmt.Printf("面积: %.1f, 周长: %.1f\n", area, perimeter)
// 输出: 面积: 15.0, 周长: 16.0
// 可变参数
fmt.Println("sum(1,2,3):", sum(1, 2, 3)) // 6
fmt.Println("sum(1,2,3,4,5):", sum(1, 2, 3, 4, 5)) // 15
// 可变参数:传递切片
nums := []int{10, 20, 30}
fmt.Println("sum(nums...):", sum(nums...)) // 60(使用 ... 展开切片)
// 可变参数与其他参数混用
fmt.Printf("average: %.2f\n", average(10, 20, 30)) // 20.00
// 匿名函数作为参数
double := func(n int) int { return n * 2 }
square := func(n int) int { return n * n }
fmt.Println("apply(5, double):", apply(5, double)) // 10
fmt.Println("apply(5, square):", apply(5, square)) // 25
// 立即执行匿名函数
msg := "你好"
func(m string) {
fmt.Println(m)
}(msg)
// ============ 闭包 ============
// counter 返回一个计数器闭包
counter := func() func() int {
count := 0
return func() int {
count++ // 捕获外部变量 count
return count
}
}()
fmt.Println("counter:", counter()) // 1
fmt.Println("counter:", counter()) // 2
fmt.Println("counter:", counter()) // 3
// 闭包实现累加器
adder := func() func(int) int {
sum := 0
return func(n int) int {
sum += n
return sum
}
}()
fmt.Println("adder(10):", adder(10)) // 10
fmt.Println("adder(20):", adder(20)) // 30
fmt.Println("adder(30):", adder(30)) // 60
}
运行结果:
TEXT
面积: 15.0, 周长: 16.0
sum(1,2,3): 6
sum(1,2,3,4,5): 15
sum(nums...): 60
average: 20.00
apply(5, double): 10
apply(5, square): 25
你好
counter: 1
counter: 2
counter: 3
adder(10): 10
adder(20): 30
adder(30): 60
要点:
- 命名返回值让代码更清晰,但不要过度使用导致可读性下降
- 可变参数
...Type必须是函数的最后一个参数 - 传递切片给可变参数时需要使用
slice...展开 - 闭包"记住"了外部变量的状态,每次调用共享同一份变量
示例 3:综合应用(难度⭐⭐⭐)
目标: 实际项目中的函数设计模式:函数选项模式、中间件链、defer与错误处理。
GO
package main
import (
"fmt"
"strings"
"time"
)
// ============ 函数选项模式 ============
// Server 服务器配置
type Server struct {
host string
port int
timeout time.Duration
maxConn int
}
// Option 配置选项函数类型
type Option func(*Server)
// WithHost 设置主机地址
func WithHost(host string) Option {
return func(s *Server) {
s.host = host
}
}
// WithPort 设置端口
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
// WithTimeout 设置超时时间
func WithTimeout(timeout time.Duration) Option {
return func(s *Server) {
s.timeout = timeout
}
}
// WithMaxConn 设置最大连接数
func WithMaxConn(max int) Option {
return func(s *Server) {
s.maxConn = max
}
}
// NewServer 使用选项模式创建服务器
func NewServer(opts ...Option) *Server {
// 默认配置
s := &Server{
host: "localhost",
port: 8080,
timeout: 30 * time.Second,
maxConn: 100,
}
// 应用所有选项
for _, opt := range opts {
opt(s)
}
return s
}
// String 实现 Stringer 接口
func (s *Server) String() string {
return fmt.Sprintf("Server{host:%s, port:%d, timeout:%v, maxConn:%d}",
s.host, s.port, s.timeout, s.maxConn)
}
// ============ 中间件链模式 ============
// Handler 处理函数类型
type Handler func(string) string
// Middleware 中间件类型
type Middleware func(Handler) Handler
// loggingMiddleware 日志中间件
func loggingMiddleware(next Handler) Handler {
return func(input string) string {
fmt.Printf("[日志] 输入: %s\n", input)
result := next(input)
fmt.Printf("[日志] 输出: %s\n", result)
return result
}
}
// upperMiddleware 转大写中间件
func upperMiddleware(next Handler) Handler {
return func(input string) string {
return next(strings.ToUpper(input))
}
}
// wrapMiddleware 包裹中间件
func wrapMiddleware(next Handler) Handler {
return func(input string) string {
return "【" + next(input) + "】"
}
}
// chain 将多个中间件组合成链
func chain(handler Handler, middlewares ...Middleware) Handler {
// 从后向前包裹,确保第一个中间件最先执行
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// ============ defer 实战:资源清理与错误恢复 ============
// simulateDB 模拟数据库操作
func simulateDB() (err error) {
fmt.Println("1. 获取数据库连接")
// defer 按 LIFO 顺序执行
defer fmt.Println("5. 释放数据库连接(最后执行)")
defer fmt.Println("4. 记录操作日志")
defer fmt.Println("3. 关闭游标")
fmt.Println("2. 执行查询")
// 模拟 panic 恢复
defer func() {
if r := recover(); r != nil {
fmt.Printf(" [恢复] 捕获 panic: %v\n", r)
err = fmt.Errorf("操作失败: %v", r)
}
}()
// 模拟一个 panic
panic("连接超时")
// 后面的代码不会执行
// fmt.Println("查询完成")
// return nil
}
func main() {
// ===== 函数选项模式 =====
fmt.Println("=== 函数选项模式 ===")
// 使用默认配置
s1 := NewServer()
fmt.Println(s1)
// 自定义配置
s2 := NewServer(
WithHost("0.0.0.0"),
WithPort(9090),
WithTimeout(60*time.Second),
WithMaxConn(500),
)
fmt.Println(s2)
// ===== 中间件链 =====
fmt.Println("\n=== 中间件链 ===")
// 基础处理函数
handler := func(s string) string {
return "处理: " + s
}
// 组合中间件
final := chain(handler, loggingMiddleware, upperMiddleware, wrapMiddleware)
result := final("hello world")
fmt.Println("最终结果:", result)
// ===== defer 与错误恢复 =====
fmt.Println("\n=== defer 执行顺序 ===")
err := simulateDB()
if err != nil {
fmt.Println("主函数捕获错误:", err)
}
}
运行结果:
TEXT
=== 函数选项模式 ===
Server{host:localhost, port:8080, timeout:30s, maxConn:100}
Server{host:0.0.0.0, port:9090, timeout:1m0s, maxConn:500}
=== 中间件链 ===
[日志] 输入: HELLO WORLD
[日志] 输出: 【处理: HELLO WORLD】
最终结果: 【处理: HELLO WORLD】
=== defer 执行顺序 ===
1. 获取数据库连接
2. 执行查询
[恢复] 捕获 panic: 连接超时
3. 关闭游标
4. 记录操作日志
5. 释放数据库连接(最后执行)
主函数捕获错误: 操作失败: 连接超时
要点:
- 函数选项模式:通过返回闭包来实现灵活的配置,是Go社区广泛使用的设计模式
- 中间件链:函数作为一等公民可以组合、传递,形成处理管道
- defer执行顺序:LIFO(后进先出),先声明的defer后执行
- panic/recover:
defer+recover可以捕获panic,防止程序崩溃
3. 常见应用场景
场景一:错误处理封装
在实际项目中,经常需要封装统一的错误处理逻辑:
GO
package main
import (
"fmt"
"strconv"
)
// Result 统一结果封装
type Result struct {
Data interface{}
Err error
}
// parseJSON 模拟JSON解析,返回结果和错误
func parseJSON(jsonStr string) (map[string]interface{}, error) {
// 简化演示:实际项目中使用 encoding/json
if jsonStr == "" {
return nil, fmt.Errorf("JSON字符串不能为空")
}
return map[string]interface{}{"key": "value"}, nil
}
// parseInt 安全地将字符串转为整数
func parseInt(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("解析整数失败 '%s': %w", s, err)
}
return n, nil
}
// withRetry 重试函数
func withRetry(attempts int, f func() error) error {
var err error
for i := 0; i < attempts; i++ {
err = f()
if err == nil {
return nil
}
fmt.Printf(" 第%d次尝试失败: %v\n", i+1, err)
}
return fmt.Errorf("重试%d次后失败: %w", attempts, err)
}
func main() {
// 错误处理
result, err := parseJSON(`{"name": "test"}`)
if err != nil {
fmt.Println("解析错误:", err)
return
}
fmt.Println("解析结果:", result)
// 安全类型转换
num, err := parseInt("123")
if err != nil {
fmt.Println(err)
return
}
fmt.Println("转换结果:", num)
// 重试机制
attempt := 0
err = withRetry(3, func() error {
attempt++
if attempt < 3 {
return fmt.Errorf("模拟失败")
}
return nil
})
if err != nil {
fmt.Println("最终失败:", err)
} else {
fmt.Println("最终成功!")
}
}
场景二:函数作为数据处理管道
GO
package main
import (
"fmt"
"strings"
)
// Transform 数据转换函数类型
type Transform func(string) string
// Pipeline 数据处理管道
type Pipeline struct {
steps []Transform
}
// NewPipeline 创建新的管道
func NewPipeline() *Pipeline {
return &Pipeline{}
}
// Add 添加处理步骤
func (p *Pipeline) Add(steps ...Transform) *Pipeline {
p.steps = append(p.steps, steps...)
return p
}
// Execute 执行管道
func (p *Pipeline) Execute(input string) string {
result := input
for _, step := range p.steps {
result = step(result)
}
return result
}
func main() {
// 定义转换函数
trim := func(s string) string { return strings.TrimSpace(s) }
lower := func(s string) string { return strings.ToLower(s) }
replace := func(s string) string { return strings.ReplaceAll(s, " ", "-") }
prefix := func(s string) string { return "processed-" + s }
// 构建处理管道
pipeline := NewPipeline().
Add(trim, lower, replace, prefix)
// 执行管道
result := pipeline.Execute(" Hello World Go ")
fmt.Println(result) // 输出: processed-hello-world-go
// 复用管道处理多个输入
inputs := []string{" Foo Bar ", " HELLO ", " Go Lang "}
for _, input := range inputs {
fmt.Printf("%-20s => %s\n", input, pipeline.Execute(input))
}
}
❓ 常见问题
Q1:函数参数是值传递还是引用传递?
A: Go语言中所有函数参数都是值传递。但切片、map、channel、指针等类型传递的是"引用的副本",修改它们指向的数据会影响原数据,但重新赋值不会影响原变量。
GO
package main
import "fmt"
// modifySlice 修改切片内容(会影响原切片)
func modifySlice(s []int) {
s[0] = 999 // 修改元素,影响原切片
fmt.Println("函数内:", s)
}
// reassignSlice 重新赋值切片(不影响原切片)
func reassignSlice(s []int) {
s = append(s, 4, 5, 6) // append 可能创建新底层数组
fmt.Println("函数内:", s)
}
func main() {
nums := []int{1, 2, 3}
modifySlice(nums)
fmt.Println("函数外:", nums) // [999 2 3],原切片被修改
reassignSlice(nums)
fmt.Println("函数外:", nums) // [999 2 3],原切片未变
}
Q2:defer 的参数什么时候求值?
A: defer 语句的参数在声明时就会被求值,而不是在执行时求值。
GO
package main
import "fmt"
func main() {
x := 10
// defer 的参数在声明时求值,此时 x = 10
defer fmt.Println("defer 中 x =", x)
x = 20
fmt.Println("修改后 x =", x)
// 如果需要在 defer 执行时求值,使用闭包
defer func() {
fmt.Println("闭包 defer 中 x =", x) // x = 20
}()
x = 30
}
输出:
TEXT
修改后 x = 20
闭包 defer 中 x = 30
defer 中 x = 10
Q3:一个包可以有多个 init 函数吗?执行顺序是什么?
A: 可以。一个包(甚至一个文件)可以有多个 init 函数。执行顺序:
- 首先执行被导入包的
init函数 - 同一包内,按源文件名字母顺序执行
- 同一文件内,按
init函数出现的顺序执行
GO
package main
import "fmt"
func init() {
fmt.Println("第一个 init")
}
func init() {
fmt.Println("第二个 init")
}
func main() {
fmt.Println("main 函数")
}
输出:
TEXT
第一个 init
第二个 init
main 函数
Q4:如何实现可选参数?
A: Go语言不直接支持可选参数,常用以下三种方式实现:
GO
package main
import "fmt"
// 方式1:使用结构体
type Config struct {
Host string
Port int
Debug bool
}
func connect(cfg Config) {
fmt.Printf("连接到 %s:%d (debug=%v)\n", cfg.Host, cfg.Port, cfg.Debug)
}
// 方式2:使用函数选项模式(推荐)
type Option func(*Config)
func WithHost(host string) Option {
return func(c *Config) { c.Host = host }
}
func WithPort(port int) Option {
return func(c *Config) { c.Port = port }
}
func WithDebug(debug bool) Option {
return func(c *Config) { c.Debug = debug }
}
func newConnect(opts ...Option) {
cfg := &Config{Host: "localhost", Port: 8080, Debug: false}
for _, opt := range opts {
opt(cfg)
}
fmt.Printf("连接到 %s:%d (debug=%v)\n", cfg.Host, cfg.Port, cfg.Debug)
}
func main() {
// 结构体方式
connect(Config{Host: "127.0.0.1", Port: 3306})
// 选项模式
newConnect()
newConnect(WithHost("127.0.0.1"), WithPort(3306), WithDebug(true))
}
📖 小节
- 函数定义:使用
func关键字,参数类型在参数名之后,支持多返回值 - 命名返回值:返回值可命名,函数体内直接使用,
return时自动返回 - 可变参数:
...Type接收任意数量参数,必须是最后一个参数 - 匿名函数:没有名字的函数,可赋值给变量或立即执行
- 闭包:匿名函数捕获外部变量,变量的生命周期被延长
- init 函数:程序启动时自动执行,不能手动调用,适合做初始化工作
- defer:延迟执行,按 LIFO 顺序,参数在声明时求值
- 函数是一等公民:可以赋值、传递、作为返回值,实现灵活的设计模式
📝 作业
练习 1(⭐):基础函数练习
要求: 编写以下函数并测试:
max(a, b int) int— 返回两个整数中较大的一个isEven(n int) bool— 判断一个整数是否为偶数swap(a, b *int)— 使用指针交换两个整数的值
GO
// 参考框架
package main
import "fmt"
func max(a, b int) int {
// 在此实现
return 0
}
func isEven(n int) bool {
// 在此实现
return false
}
func swap(a, b *int) {
// 在此实现(提示:使用指针)
}
func main() {
fmt.Println(max(3, 5)) // 应输出: 5
fmt.Println(isEven(4)) // 应输出: true
fmt.Println(isEven(7)) // 应输出: false
x, y := 10, 20
swap(&x, &y)
fmt.Println(x, y) // 应输出: 20 10
}
练习 2(⭐⭐):可变参数与闭包
要求:
- 编写
filter(nums []int, predicate func(int) bool) []int— 过滤切片中满足条件的元素 - 编写
makeMultiplier(factor int) func(int) int— 返回一个乘法闭包 - 使用
filter和闭包,从切片中筛选出所有偶数
GO
// 参考框架
package main
import "fmt"
func filter(nums []int, predicate func(int) bool) []int {
// 在此实现:遍历 nums,将满足 predicate 的元素加入结果切片
return nil
}
func makeMultiplier(factor int) func(int) int {
// 在此实现:返回一个闭包,将输入乘以 factor
return nil
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 筛选偶数
evens := filter(nums, func(n int) bool {
return n%2 == 0
})
fmt.Println("偶数:", evens) // [2 4 6 8 10]
// 筛选大于5的数
big := filter(nums, func(n int) bool {
return n > 5
})
fmt.Println("大于5:", big) // [6 7 8 9 10]
// 使用闭包创建乘法器
double := makeMultiplier(2)
triple := makeMultiplier(3)
fmt.Println("double(5):", double(5)) // 10
fmt.Println("triple(5):", triple(5)) // 15
}
练习 3(⭐⭐⭐):综合实战——计算器与命令模式
要求: 实现一个支持注册命令的计算器系统:
- 定义
Calculator结构体,包含一个命令映射map[string]func([]int) int - 实现
Register(name string, op func([]int) int)方法注册命令 - 实现
Execute(name string, args []int) (int, error)方法执行命令 - 注册以下命令:
add(求和)、mul(求积)、max(最大值) - 使用
init函数自动注册所有命令
GO
// 参考框架
package main
import (
"errors"
"fmt"
)
type Calculator struct {
// 在此定义字段
}
func NewCalculator() *Calculator {
// 在此实现
return nil
}
func (c *Calculator) Register(name string, op func([]int) int) {
// 在此实现
}
func (c *Calculator) Execute(name string, args []int) (int, error) {
// 在此实现:查找命令并执行,不存在则返回错误
return 0, nil
}
func (c *Calculator) ListCommands() []string {
// 在此实现:返回所有已注册的命令名
return nil
}
func main() {
calc := NewCalculator()
// 注册命令
calc.Register("add", func(args []int) int {
sum := 0
for _, v := range args {
sum += v
}
return sum
})
calc.Register("mul", func(args []int) int {
product := 1
for _, v := range args {
product *= v
}
return product
})
calc.Register("max", func(args []int) int {
if len(args) == 0 {
return 0
}
m := args[0]
for _, v := range args[1:] {
if v > m {
m = v
}
}
return m
})
// 测试
tests := []struct {
cmd string
args []int
}{
{"add", []int{1, 2, 3, 4, 5}},
{"mul", []int{2, 3, 4}},
{"max", []int{10, 3, 7, 9, 1}},
}
for _, t := range tests {
result, err := calc.Execute(t.cmd, t.args)
if err != nil {
fmt.Printf("错误: %v\n", err)
} else {
fmt.Printf("%s(%v) = %d\n", t.cmd, t.args, result)
}
}
// 列出所有命令
fmt.Println("可用命令:", calc.ListCommands())
// 测试不存在的命令
_, err := calc.Execute("sqrt", []int{16})
if err != nil {
fmt.Println("错误:", err)
}
}
预期输出:
TEXT
add([1 2 3 4 5]) = 15
mul([2 3 4]) = 24
max([10 3 7 9 1]) = 10
可用命令: [add mul max]
错误: 未知命令: sqrt



