返回首页

Golang入门:1.5 函数与方法

函数是 Go 语言里最基础、也最常用的组织代码方式。前面我们已经接触过 main 函数,也在条件判断和循环中写过不少逻辑,但如果所有代码都堆在 main 里,程序很快就会变得臃肿、难读、难维护。

这一章我们系统学习 Go 中的函数与方法,包括:

  • 函数定义
  • 多返回值与命名返回值
  • 可变参数函数
  • 匿名函数与闭包
  • 函数作为一等公民(高阶函数)
  • 方法(Method)与接收者(值接收者 vs 指针接收者)

如果你已经有一些其他语言基础,那么可以把这一章理解为:Go 是怎样用尽量简洁的方式,让“行为”组织得更清楚、更安全、更适合工程实践的。

为什么函数和方法很重要

函数解决的是“把一段逻辑封装起来,重复使用”的问题;方法解决的是“让某个类型不仅有数据,还有与数据相关的行为”的问题。

简单来说:

  • 函数 更像独立工具
  • 方法 更像某个类型自带的能力

比如:

  • “计算两个数的和”更适合写成函数
  • “让用户打印自己的信息”更适合写成 User 类型的方法

它们共同决定了代码是否清晰、模块是否合理、后续是否容易扩展。

一、函数定义、多返回值、命名返回值

1. 函数的基本定义

Go 使用 func 关键字定义函数,基本结构如下:

func 函数名(参数列表) 返回值类型 {
    // 函数体
}

如果有多个参数,类型相同的相邻参数可以合并写。

下面是一个完整可运行的例子:

package main

import "fmt"

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

func greet(name string) string {
	return "你好," + name
}

func main() {
	sum := add(10, 20)
	message := greet("Go 开发者")

	fmt.Println("10 + 20 =", sum)
	fmt.Println(message)
}

这个例子里:

  • add 接收两个 int 参数,返回一个 int
  • greet 接收一个字符串参数,返回一个字符串
  • main 中通过调用函数,把不同逻辑拆开管理

函数最大的好处就是把“做什么”抽象成一个名字。以后再次需要这段逻辑时,不必重复写代码,只需要调用函数即可。

2. 多返回值:Go 很有代表性的特性

Go 的函数可以返回多个值,这是它非常实用、也非常有辨识度的语言特性。

在很多语言里,一个函数只能返回一个结果。如果想同时返回“处理结果”和“错误信息”,通常要借助对象、数组、元组等结构。而 Go 允许直接返回多个值,因此它特别适合处理“结果 + 状态”这类场景。

下面的例子演示如何返回商和余数:

package main

import "fmt"

func divide(dividend int, divisor int) (int, int) {
	quotient := dividend / divisor
	remainder := dividend % divisor
	return quotient, remainder
}

func main() {
	q, r := divide(17, 5)
	fmt.Println("商:", q)
	fmt.Println("余数:", r)
}

这里的返回值类型写成 (int, int),表示函数会返回两个整数。

调用时可以这样接收:

  • q 接收商
  • r 接收余数

这种写法在 Go 中极其常见,尤其是与错误处理结合时。

3. 多返回值与错误处理

Go 不鼓励把普通错误处理交给异常机制,而是倾向于通过返回值把错误显式传出来。最常见的函数签名形式是:

func xxx(...) (结果类型, error)

下面是一个更贴近真实开发的完整例子:

package main

import (
	"errors"
	"fmt"
)

func safeDivide(dividend int, divisor int) (int, error) {
	if divisor == 0 {
		return 0, errors.New("除数不能为 0")
	}

	return dividend / divisor, nil
}

func main() {
	result, err := safeDivide(20, 4)
	if err != nil {
		fmt.Println("计算失败:", err)
		return
	}
	fmt.Println("计算成功,结果为:", result)

	result, err = safeDivide(20, 0)
	if err != nil {
		fmt.Println("计算失败:", err)
		return
	}
	fmt.Println("计算成功,结果为:", result)
}

这个例子里:

  • 当除数不为 0 时,返回正常结果和 nil
  • 当除数为 0 时,返回默认值 0 和一个错误对象
  • 调用方通过 if err != nil 显式处理错误

你会发现,Go 的多返回值并不是“语法炫技”,而是和整个错误处理风格紧密绑定的。

4. 命名返回值

Go 允许在函数签名中,直接给返回值命名。这样函数内部就像拥有了几个默认局部变量,可以在函数体里直接使用,最后只写一个 return

下面是一个示例:

package main

import "fmt"

func rectangleStats(width float64, height float64) (area float64, perimeter float64) {
	area = width * height
	perimeter = 2 * (width + height)
	return
}

func main() {
	area, perimeter := rectangleStats(5.5, 3.0)
	fmt.Println("面积:", area)
	fmt.Println("周长:", perimeter)
}

这里:

  • areaperimeter 是命名返回值
  • 在函数体中可以直接给它们赋值
  • 最后 return 不需要再显式写变量名

这种写法在一些逻辑清晰、返回值含义明确的函数中很方便,但也不要滥用。因为一旦函数过长,读者可能不容易立刻知道最后到底返回了哪些值。

5. 什么时候使用命名返回值

可以用下面这张表快速判断:

场景 是否推荐 说明
返回值语义很明确,例如 width/height 计算得到 area/perimeter 推荐 可读性较好
函数较短,返回逻辑简单 推荐 代码会比较自然
函数较长,分支很多 谨慎使用 容易让返回路径不够直观
只是为了少写几个变量名 不推荐 可读性比“省几行代码”更重要

对初学者来说,一个实用原则是:优先写清楚,再考虑写得巧。

二、可变参数函数

有时候,一个函数接收的参数数量并不固定。比如:

  • 统计任意多个数字的总和
  • 打印任意数量的日志标签
  • 拼接多个字符串

Go 用 ...类型 表示可变参数。

1. 基本用法

下面是一个完整示例,用来计算任意多个整数的总和:

package main

import "fmt"

func sum(numbers ...int) int {
	total := 0
	for _, number := range numbers {
		total += number
	}
	return total
}

func main() {
	fmt.Println("sum() =", sum())
	fmt.Println("sum(1, 2, 3) =", sum(1, 2, 3))
	fmt.Println("sum(10, 20, 30, 40) =", sum(10, 20, 30, 40))
}

这里的 numbers ...int 表示:

  • 可以传 0 个 int
  • 也可以传 1 个、多个 int
  • 在函数内部,numbers 的表现形式其实像一个 []int 切片

所以你可以直接配合 range 来遍历。

2. 可变参数与切片展开

如果你手上已经有一个切片,想把它作为可变参数传进去,需要在切片后面加 ...

package main

import "fmt"

func sum(numbers ...int) int {
	total := 0
	for _, number := range numbers {
		total += number
	}
	return total
}

func main() {
	scores := []int{88, 92, 95, 90}

	total := sum(scores...)
	fmt.Println("总分:", total)
}

注意这里的 scores... 不能少。否则编译器会认为你传入的是一个切片,而不是展开后的多个参数。

3. 可变参数的使用建议

建议 说明
参数数量天然不固定时使用 如日志、求和、拼接等场景
可变参数通常放在最后一个参数位置 Go 语法也要求它必须是最后一个
参数过多、语义复杂时不要滥用 这种情况更适合结构体或普通参数
如果已有切片,记得用 slice... 展开 这是初学者常见易错点

三、匿名函数与闭包

Go 支持匿名函数,也就是没有名字的函数。这类函数通常用于:

  • 临时封装一小段逻辑
  • 作为参数传给其他函数
  • 在函数内部构造“带状态”的行为

1. 匿名函数的基本写法

匿名函数可以定义后立即调用,也可以赋值给变量。

先看一个立即执行的例子:

package main

import "fmt"

func main() {
	func(name string) {
		fmt.Println("你好,", name)
	}("Gopher")
}

上面这段代码的结构可以拆成两部分理解:

  1. func(name string) { ... } 定义了一个匿名函数
  2. 后面的 ("Gopher") 表示立刻调用它

这种形式常用于:

  • 初始化逻辑
  • 一次性封装小段代码
  • 配合 goroutine 启动并发任务(后面章节会学到)

再看一个赋值给变量的例子:

package main

import "fmt"

func main() {
	multiply := func(a int, b int) int {
		return a * b
	}

	fmt.Println("3 * 4 =", multiply(3, 4))
}

这说明函数本身也可以像普通值一样被保存和使用。

2. 什么是闭包

闭包可以理解为:函数“记住”了它定义时所处环境中的变量。

也就是说,一个函数不仅能执行代码,还能携带一些外部状态。

下面是一个经典示例:

package main

import "fmt"

func counter() func() int {
	count := 0

	return func() int {
		count++
		return count
	}
}

func main() {
	next := counter()

	fmt.Println(next())
	fmt.Println(next())
	fmt.Println(next())

	another := counter()
	fmt.Println(another())
}

运行这个程序时,你会看到:

  • next() 第一次返回 1
  • 第二次返回 2
  • 第三次返回 3
  • another() 会从 1 重新开始

原因就在于:

  • counter() 每调用一次,都会创建一个新的 count
  • 返回的匿名函数捕获了这个 count
  • 后续每次调用这个匿名函数,都会继续使用同一个 count

这就是闭包的核心价值:把数据和行为临时绑定在一起。

3. 闭包适合什么场景

闭包常见于:

  • 生成带状态的计数器
  • 包装日志、缓存、权限检查等逻辑
  • 给函数“预先绑定一部分参数”

不过对初学者来说,闭包虽然强大,但也要注意不要让状态来源太隐蔽。否则代码会变得不容易跟踪。

四、函数作为一等公民(高阶函数)

在 Go 中,函数是一等公民,意思是:

  • 函数可以赋值给变量
  • 函数可以作为参数传入另一个函数
  • 函数可以作为返回值返回

当一个函数“接收函数”或“返回函数”时,通常就可以称为高阶函数

1. 函数作为参数传递

下面我们写一个通用计算器,它接收一个“运算规则函数”:

package main

import "fmt"

func calculate(a int, b int, operation func(int, int) int) int {
	return operation(a, b)
}

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

func subtract(a int, b int) int {
	return a - b
}

func main() {
	fmt.Println("加法结果:", calculate(10, 5, add))
	fmt.Println("减法结果:", calculate(10, 5, subtract))

	multiply := func(a int, b int) int {
		return a * b
	}
	fmt.Println("乘法结果:", calculate(10, 5, multiply))
}

这个例子很值得体会:

  • calculate 并不关心“具体做什么运算”
  • 它只关心:传进来的函数符合 func(int, int) int 这个签名
  • 至于加法、减法、乘法,都可以动态切换

这会让代码更灵活,也更容易复用。

2. 函数作为返回值

除了把函数传进去,也可以把函数返回出来。

package main

import "fmt"

func makePrefixPrinter(prefix string) func(string) {
	return func(message string) {
		fmt.Println(prefix + message)
	}
}

func main() {
	infoPrinter := makePrefixPrinter("[INFO] ")
	errorPrinter := makePrefixPrinter("[ERROR] ")

	infoPrinter("服务启动成功")
	errorPrinter("数据库连接失败")
}

这个例子同时也体现了闭包:

  • makePrefixPrinter 返回一个函数
  • 返回的函数记住了 prefix
  • 所以我们可以得到不同风格的打印器

3. 高阶函数的价值

能力 说明
把变化点抽出来 让通用逻辑和具体策略分离
提高复用性 一个框架函数可搭配多个业务函数
让代码更灵活 运行时可以决定使用哪种行为
便于构建工具函数 如过滤、转换、包装、重试等

不过也要注意,过度函数式写法会让初学者不容易跟上。对于 Go 而言,实用和清晰永远比炫技巧更重要。

五、方法(Method)与接收者

学完函数之后,一个自然的问题是:既然函数已经能封装逻辑了,为什么还要方法?

答案是:方法让“行为”可以直接挂到某个类型上。

这意味着一个类型不只是保存数据,还能表达自己的操作方式。

1. 方法的基本定义

Go 中的方法本质上也是函数,只不过它在函数名前面多了一个接收者

基本形式如下:

func (接收者 变量名 类型) 方法名(参数列表) 返回值类型 {
    // 方法体
}

下面是一个完整示例:

package main

import "fmt"

type User struct {
	Name string
	Age  int
}

func (u User) Introduce() string {
	return fmt.Sprintf("大家好,我叫 %s,今年 %d 岁。", u.Name, u.Age)
}

func main() {
	user := User{Name: "小明", Age: 25}
	fmt.Println(user.Introduce())
}

这里的关键点是:

  • User 是一个结构体类型
  • Introduce 是绑定在 User 上的方法
  • u User 就是接收者
  • 调用方式是 user.Introduce()

如果写成普通函数,也不是不行,比如:

func introduceUser(u User) string

但方法的优势在于:表达更自然,代码组织更贴近“谁拥有这个行为”。

2. 方法和函数的区别

对比项 函数 方法
是否绑定类型
调用方式 fn(x) obj.method()
适用场景 通用工具逻辑 与某个类型强相关的行为
可读性特点 强调“执行某个动作” 强调“某个对象具备某种能力”

一个简单判断标准是:

  • 如果逻辑和某个类型密切相关,优先考虑方法
  • 如果逻辑更通用,与具体类型关系不强,优先考虑函数

六、值接收者 vs 指针接收者

这是 Go 初学者学习方法时最重要的知识点之一。很多人会写方法,但不知道为什么有时写 (u User),有时写 (*User)

先记住一句话:

  • 值接收者:方法拿到的是对象的一个副本
  • 指针接收者:方法拿到的是对象本身的地址,可以直接修改原对象

1. 值接收者示例

package main

import "fmt"

type Counter struct {
	Value int
}

func (c Counter) Increase() {
	c.Value++
	fmt.Println("方法内部的值:", c.Value)
}

func main() {
	counter := Counter{Value: 10}
	counter.Increase()
	fmt.Println("main 中的值:", counter.Value)
}

这个程序运行后你会发现:

  • 方法内部打印的是 11
  • main 中的值仍然是 10

因为值接收者拿到的是副本,方法里改的是副本,不是原始对象。

2. 指针接收者示例

package main

import "fmt"

type Counter struct {
	Value int
}

func (c *Counter) Increase() {
	c.Value++
	fmt.Println("方法内部的值:", c.Value)
}

func main() {
	counter := Counter{Value: 10}
	counter.Increase()
	fmt.Println("main 中的值:", counter.Value)
}

这一次输出会变成:

  • 方法内部的值:11
  • main 中的值:11

因为指针接收者操作的是原对象本身,所以修改会保留下来。

3. 值接收者和指针接收者怎么选

这一部分要重点理解,不只是“语法上能写出来”,更要知道工程上该怎么判断。

对比维度 值接收者 指针接收者
是否修改原对象
传递成本 会复制一份值 传递地址,通常更省拷贝成本
适合小型不可变数据 也可以,但不一定必要
适合大型结构体 不太推荐 更推荐
是否表达“这个操作会改变对象状态” 不明显 很明显

4. 什么时候优先使用值接收者

通常在下面这些场景中,值接收者比较合适:

  1. 方法不需要修改接收者状态
  2. 结构体比较小,复制成本低
  3. 你希望这个方法更像“读取”而不是“修改”
  4. 类型本身语义上接近不可变值

看一个典型例子:

package main

import "fmt"

type Point struct {
	X int
	Y int
}

func (p Point) DistanceSquared() int {
	return p.X*p.X + p.Y*p.Y
}

func main() {
	point := Point{X: 3, Y: 4}
	fmt.Println("距离平方:", point.DistanceSquared())
}

这里的 DistanceSquared 只是读取 Point 的数据,并不修改它,所以用值接收者很自然。

5. 什么时候优先使用指针接收者

以下场景通常更推荐指针接收者:

  1. 方法需要修改对象状态
  2. 结构体较大,复制代价高
  3. 结构体内部包含切片、映射、互斥锁等需要谨慎处理的字段
  4. 你希望整个类型的方法风格保持一致

下面是一个更贴近业务的例子:

package main

import "fmt"

type BankAccount struct {
	Owner   string
	Balance float64
}

func (b *BankAccount) Deposit(amount float64) {
	if amount <= 0 {
		fmt.Println("存款金额必须大于 0")
		return
	}
	b.Balance += amount
}

func (b *BankAccount) Withdraw(amount float64) bool {
	if amount <= 0 {
		fmt.Println("取款金额必须大于 0")
		return false
	}
	if amount > b.Balance {
		fmt.Println("余额不足")
		return false
	}
	b.Balance -= amount
	return true
}

func (b BankAccount) Display() {
	fmt.Printf("账户:%s,余额:%.2f\n", b.Owner, b.Balance)
}

func main() {
	account := BankAccount{Owner: "小明", Balance: 1000}
	account.Display()

	account.Deposit(500)
	account.Display()

	ok := account.Withdraw(300)
	fmt.Println("取款是否成功:", ok)
	account.Display()
}

这个例子里:

  • DepositWithdraw 会修改余额,所以使用指针接收者
  • Display 只是展示信息,不修改状态,所以可以使用值接收者

不过在真实项目中,如果一个类型的大部分方法都需要指针接收者,那么通常建议统一使用指针接收者风格,避免 API 风格混杂,减少理解成本。

6. 一个非常实用的选择原则

如果你还在犹豫接收者怎么选,可以记住下面这个经验法则:

问题 建议
这个方法会不会修改对象状态? 会,就优先指针接收者
这个结构体大不大,复制值划算吗? 不划算,就优先指针接收者
这个方法只是读取数据、做展示、做计算吗? 可以考虑值接收者
同一个类型里其他方法大多已经是指针接收者了吗? 那就尽量统一用指针接收者

对于初学者来说,最稳妥的理解方式是:

  • 读数据、不改状态:可以优先考虑值接收者
  • 改状态、避免拷贝、保持一致:优先考虑指针接收者

七、函数与方法的整体对比

学到这里,可以把本章知识点串起来看:

主题 核心作用 典型使用场景
普通函数 封装独立逻辑 工具函数、公共计算逻辑
多返回值 一次返回多个结果 结果 + 错误、商和余数
命名返回值 提升返回值语义表达 短小且含义明确的函数
可变参数函数 处理数量不固定的输入 求和、日志、拼接
匿名函数 临时定义一段逻辑 回调、立即执行、局部封装
闭包 让函数携带状态 计数器、包装器、配置绑定
高阶函数 让函数接收或返回函数 策略切换、通用框架封装
方法 给类型绑定行为 面向数据建模的业务逻辑
值接收者 基于副本工作 只读操作、小型结构体
指针接收者 基于原对象工作 修改状态、大型结构体

八、本章小结

这一章的重点,不只是记住几个语法形式,而是建立下面这些认知:

  1. 函数是组织逻辑的基础单位,让代码从“堆在一起”变成“按职责拆分”
  2. 多返回值是 Go 很重要的设计特征,尤其适合显式错误处理
  3. 可变参数、匿名函数、闭包、高阶函数 让 Go 的函数能力既简洁又灵活
  4. 方法让类型拥有行为,让代码更贴近真实业务模型
  5. 值接收者和指针接收者的选择,本质上是在权衡:是否修改状态、是否需要避免拷贝、是否保持风格一致

如果你刚学到这里,建议你在写练习代码时多问自己两个问题:

  • 这段逻辑更适合写成函数,还是某个类型的方法?
  • 这个方法到底是“读取对象”,还是“修改对象”?

把这两个问题想清楚,后面学习结构体、接口、并发和工程项目时,会顺畅很多。


📝 版权声明:本文为原创技术博客,转载请注明出处。

如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!

上一篇

Golang入门:1.4 复合数据类型

下一篇

Golang入门:1.6 结构体与面向对象