Go 从 0 到精通 · 第 06 课:函数基础

学习定位:这是整套 Go 教程的第 6 课,也是入门基础阶段的最后一课。
前置要求:已经完成第 5 课,掌握了 for 循环的所有常见用法、breakcontinue 以及 for range 遍历。
本课目标:掌握函数的定义、调用、参数传递、返回值机制,理解多返回值、命名返回值和可变参数,能将程序逻辑拆分成函数进行复用。


1. 本课你要解决的核心问题

前五课你写的所有代码都塞在 main() 一个函数里。随着逻辑变复杂,这种写法会越来越难维护。

函数就是让你把一段逻辑"包装"起来,给它一个名字,需要的时候调用它。

你需要搞明白以下问题:

  • 函数怎么定义、怎么调用
  • 参数是什么,怎么传
  • 返回值是什么,怎么接收
  • 为什么 Go 支持多返回值,有什么用
  • 什么是命名返回值
  • 什么是可变参数
  • 函数作为"一等公民"是什么意思
  • 函数相关的常见坑有哪些

学完这一课,你就完成了入门基础阶段的全部内容,具备了写基本 Go 程序的能力。


2. 函数的基本概念

2.1 什么是函数

函数可以理解为:

一段有名字、可以被反复调用的代码块。

它可以接收输入(参数),可以产出结果(返回值),也可以什么都不接收、什么都不返回。

你之前已经用过函数了:

  • fmt.Println 是函数
  • fmt.Printf 是函数
  • strconv.Atoi 是函数
  • main() 本身也是函数

现在你要学的是:怎么自己定义函数


2.2 为什么需要函数

假如你有一段"计算两个数之和"的逻辑,要在程序中用 5 次。

如果不用函数,你就要把同样的代码复制 5 次。一旦逻辑需要修改,你就要改 5 个地方。

函数解决的就是这个问题:

  • 复用:写一次,用多次
  • 组织:让代码更有结构
  • 可读性:给逻辑起一个有意义的名字
  • 可维护性:改一处,处处生效

3. 函数定义与调用

3.1 最简单的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func sayHello() {
fmt.Println("你好,Go!")
}

func main() {
sayHello()
sayHello()
sayHello()
}

输出:

1
2
3
你好,Go!
你好,Go!
你好,Go!

这里:

  • func sayHello() 定义了一个名为 sayHello 的函数
  • 它没有参数,也没有返回值
  • main() 里通过 sayHello() 调用它

3.2 函数定义的基本语法

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

各个部分的含义:

  • func:关键字,表示定义函数
  • 函数名:遵循 Go 的命名规则
  • 参数列表:函数接收的输入,可以为空
  • 返回值类型:函数返回的结果类型,可以没有
  • 函数体:具体的执行逻辑

3.3 函数名的命名规范

Go 的函数命名遵循以下惯例:

  • 使用驼峰命名法(camelCase
  • 首字母大写的函数是导出的(可以被其他包调用)
  • 首字母小写的函数是未导出的(仅当前包可用)
  • 名字应该简洁、有意义

例如:

  • calculateSum:好名字,清楚表达功能
  • cs:不好,看不出是做什么的
  • CalculateSum:首字母大写,表示可以被其他包调用

导出规则的细节在后面"包与模块管理"那一课会详细讲。


4. 带参数的函数

4.1 单个参数

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

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

func main() {
greet("小明")
greet("小红")
}

输出:

1
2
你好, 小明
你好, 小红

name string 表示这个函数接收一个 string 类型的参数,调用时需要传入一个字符串。


4.2 多个参数

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func add(a int, b int) {
fmt.Printf("%d + %d = %d\n", a, b, a+b)
}

func main() {
add(3, 5)
add(10, 20)
}

输出:

1
2
3 + 5 = 8
10 + 20 = 30

4.3 相同类型的参数可以简写

如果连续多个参数类型相同,可以只在最后写一次类型:

1
2
3
func add(a, b int) {
fmt.Println(a + b)
}

这等价于:

1
2
3
func add(a int, b int) {
fmt.Println(a + b)
}

这种简写在参数较多且类型相同时很实用。


5. 带返回值的函数

5.1 单个返回值

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

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

func main() {
result := add(3, 5)
fmt.Println("结果是:", result)
}

输出:

1
结果是: 8

注意:

  • 函数签名里 int 写在参数列表后面,表示返回值类型是 int
  • 函数体里用 return 返回结果
  • 调用方用变量接收返回值

5.2 return 的作用

return 做两件事:

  1. 把值返回给调用方
  2. 立刻结束函数执行

例如:

1
2
3
4
5
6
func check(age int) string {
if age >= 18 {
return "成年"
}
return "未成年"
}

age >= 18 时,第一个 return 执行,函数立刻结束,不会执行到第二个 return


6. 多返回值:Go 的标志性特性

6.1 基本写法

Go 的函数可以返回多个值。这是很多语言没有的特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

func divide(a, b float64) (float64, string) {
if b == 0 {
return 0, "除数不能为零"
}
return a / b, ""
}

func main() {
result, errMsg := divide(10, 3)
if errMsg != "" {
fmt.Println("错误:", errMsg)
} else {
fmt.Printf("结果:%.2f\n", result)
}

result2, errMsg2 := divide(10, 0)
if errMsg2 != "" {
fmt.Println("错误:", errMsg2)
} else {
fmt.Printf("结果:%.2f\n", result2)
}
}

输出:

1
2
结果:3.33
错误: 除数不能为零

多返回值的语法:

  • 返回值类型用括号包起来:(float64, string)
  • return 后面按顺序写多个值
  • 调用方用多个变量接收

6.2 为什么 Go 要支持多返回值

最核心的原因是:错误处理

Go 没有异常机制(没有 try...catch),它用返回值来传递错误。最典型的模式是:

1
2
3
4
result, err := someFunction()
if err != nil {
// 处理错误
}

这种"结果 + 错误"的双返回值模式,是 Go 代码中最常见的模式之一。你之前用过的 strconv.Atoi 就是这样:

1
n, err := strconv.Atoi("42")

第一个返回值是转换结果,第二个返回值是错误信息。


6.3 用 _ 忽略不需要的返回值

如果你只关心某个返回值,可以用 _ 忽略其他的:

1
2
result, _ := divide(10, 3)
fmt.Println(result)

_ 表示"我不需要这个值"。

但在实际开发中,不建议随意忽略错误。忽略错误是很多 bug 的根源。


7. 命名返回值

7.1 基本写法

Go 允许在函数签名中给返回值起名字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

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

func main() {
a, p := rectangleInfo(5, 3)
fmt.Printf("面积:%.1f,周长:%.1f\n", a, p)
}

输出:

1
面积:15.0,周长:16.0

注意:

  • 返回值写成 (area float64, perimeter float64)
  • 函数体里直接给 areaperimeter 赋值
  • return 后面不用写具体值,它会自动返回命名的返回值

7.2 命名返回值的相同类型简写

和参数一样,如果多个命名返回值类型相同,可以简写:

1
2
3
4
5
func swap(a, b int) (x, y int) {
x = b
y = a
return
}

7.3 什么时候用命名返回值

命名返回值适合的场景:

  • 返回值有 2 个以上,且含义需要明确区分
  • 函数逻辑比较复杂,命名能提高可读性
  • 想使用"裸 return"(不带值的 return

但也有争议的地方:

  • 函数比较短时,命名返回值可能显得多余
  • "裸 return"在长函数中可能降低可读性,因为你需要回头看函数签名才知道返回了什么

建议:

短函数直接 return 值 就好;长函数或返回值较多时考虑用命名返回值。


8. 可变参数

8.1 基本写法

有时候你希望一个函数能接收不确定数量的参数。Go 用 ... 来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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))
fmt.Println(sum(10, 20, 30, 40, 50))
fmt.Println(sum())
}

输出:

1
2
3
6
150
0

nums ...int 表示 nums 是一个可变参数,类型是 int。在函数内部,nums 实际上是一个 []int(整数切片)。


8.2 可变参数的规则

  • 可变参数必须是函数参数列表的最后一个
  • 一个函数最多只能有一个可变参数
  • 调用时可以传 0 个、1 个或多个值
1
2
3
4
5
6
7
// 正确:可变参数在最后
func log(level string, messages ...string) {
}

// 错误:可变参数不在最后
// func log(messages ...string, level string) {
// }

8.3 传递切片给可变参数

如果你已经有一个切片,想传给可变参数函数,需要用 ... 展开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

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

func main() {
numbers := []int{1, 2, 3, 4, 5}
fmt.Println(sum(numbers...))
}

输出:

1
15

numbers... 表示把切片展开成一个个单独的参数传入。切片的详细内容会在第 9 课讲。


9. Go 中的参数传递:值传递

9.1 Go 是值传递

Go 的函数参数传递方式是值传递

这意味着:调用函数时,传入的值会被复制一份给函数内部的参数。函数内部修改参数,不会影响外部的原始变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func tryChange(n int) {
n = 100
fmt.Println("函数内部 n =", n)
}

func main() {
x := 10
tryChange(x)
fmt.Println("函数外部 x =", x)
}

输出:

1
2
函数内部 n = 100
函数外部 x = 10

x 的值没有被改变,因为 tryChange 拿到的只是 x 的一份副本。


9.2 如果想在函数里修改外部变量怎么办

答案是用指针。这是下一课(第 7 课)的重点内容。

现在你只需要记住:

Go 的参数传递是值传递。函数内部修改参数不会影响外部变量。


10. 函数作为值:一等公民

10.1 什么意思

在 Go 中,函数是"一等公民",这意味着:

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

这一课先看最基本的用法。


10.2 把函数赋值给变量

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

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

func main() {
f := add
result := f(3, 5)
fmt.Println(result)
}

输出:

1
8

f := addadd 函数赋给了变量 f,之后通过 f(3, 5) 调用它。


10.3 匿名函数

Go 支持不给函数起名字,直接定义使用:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
greet := func(name string) {
fmt.Println("你好,", name)
}

greet("小明")
greet("小红")
}

输出:

1
2
你好, 小明
你好, 小红

匿名函数没有函数名,直接赋给变量使用。


10.4 立即执行的匿名函数

匿名函数也可以定义后立刻调用:

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
result := func(a, b int) int {
return a + b
}(3, 5)

fmt.Println(result)
}

输出:

1
8

这种写法在实际开发中偶尔会用到,例如初始化某些变量时。


11. 函数作为参数

函数可以作为参数传给另一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func apply(a, b int, op func(int, int) int) int {
return op(a, b)
}

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

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

func main() {
fmt.Println(apply(3, 5, add))
fmt.Println(apply(3, 5, multiply))
}

输出:

1
2
8
15

apply 函数的第三个参数 op 的类型是 func(int, int) int,表示"一个接收两个 int 参数、返回一个 int 的函数"。

调用时传入不同的函数,就能实现不同的行为。这是一种非常强大的抽象能力。

你现在理解基本原理就好,后面学到接口和并发时,函数作为值会更频繁出现。


12. 一段综合示例

把本课核心知识串到一个程序里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package main

import "fmt"

// 带参数和返回值的函数
func add(a, b int) int {
return a + b
}

// 多返回值函数
func divmod(a, b int) (int, int, string) {
if b == 0 {
return 0, 0, "除数不能为零"
}
return a / b, a % b, ""
}

// 命名返回值
func rectangleInfo(w, h float64) (area, perimeter float64) {
area = w * h
perimeter = 2 * (w + h)
return
}

// 可变参数
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}

// 演示值传递
func tryDouble(n int) int {
n = n * 2
return n
}

func main() {
// 基本调用
fmt.Println("3 + 5 =", add(3, 5))

// 多返回值
quotient, remainder, errMsg := divmod(17, 5)
if errMsg != "" {
fmt.Println("错误:", errMsg)
} else {
fmt.Printf("17 ÷ 5 = %d 余 %d\n", quotient, remainder)
}

// 命名返回值
area, peri := rectangleInfo(4, 3)
fmt.Printf("长方形:面积=%.0f,周长=%.0f\n", area, peri)

// 可变参数
fmt.Println("求和:", sum(1, 2, 3, 4, 5))
fmt.Println("空参求和:", sum())

// 值传递
x := 10
doubled := tryDouble(x)
fmt.Printf("原始值:%d,翻倍后:%d\n", x, doubled)

// 匿名函数
square := func(n int) int {
return n * n
}
fmt.Println("5 的平方:", square(5))
}

输出:

1
2
3
4
5
6
7
3 + 5 = 8
17 ÷ 5 = 3 余 2
长方形:面积=12,周长=14
求和: 15
空参求和: 0
原始值:10,翻倍后:20
5 的平方: 25

13. 常见坑总结

13.1 忘记接收返回值

1
2
3
4
5
6
7
func add(a, b int) int {
return a + b
}

func main() {
add(3, 5) // 返回值被丢弃了
}

Go 不会因为你丢弃了返回值而报错(除非返回值是错误且有 lint 规则),但这通常说明你的调用意图有问题。


13.2 多返回值只接收了部分

1
2
// divmod 返回 3 个值,但你只接收了 1 个
result := divmod(17, 5) // 编译错误

Go 要求你必须接收所有的返回值。如果不需要某个返回值,用 _ 忽略:

1
quotient, _, _ := divmod(17, 5)

13.3 以为函数内部能修改外部变量

Go 是值传递。函数内部修改参数不影响外部:

1
2
3
4
5
6
7
8
9
func change(s string) {
s = "新值"
}

func main() {
name := "旧值"
change(name)
fmt.Println(name) // 输出:旧值
}

要修改外部变量,需要用指针(下一课讲)。


13.4 可变参数不在最后

1
2
3
// 编译错误
func log(msgs ...string, level int) {
}

可变参数必须是参数列表的最后一个。


13.5 “裸 return” 在长函数中降低可读性

1
2
3
4
5
6
func process(data string) (result string, err string) {
// ... 很多行代码 ...
result = "ok"
// ... 又很多行代码 ...
return // 裸 return,读者要回头看函数签名才知道返回了什么
}

如果函数很长,建议还是显式写 return result, err,可读性更好。


13.6 匿名函数忘了调用

1
2
3
4
// 只是定义了匿名函数,没有调用
func(name string) {
fmt.Println("你好", name)
}

如果要立即执行,后面加参数:

1
2
3
func(name string) {
fmt.Println("你好", name)
}("小明")

如果要保存后调用,赋给变量:

1
2
3
4
f := func(name string) {
fmt.Println("你好", name)
}
f("小明")

14. 本课练习

一定要亲手写,不要只看。

练习 1:写一个求最大值的函数

要求:

  • 定义函数 max(a, b int) int
  • 返回两个整数中的较大值
  • main 中调用并打印结果

练习 2:写一个多返回值函数

要求:

  • 定义函数 minMax(a, b, c int) (int, int)
  • 返回三个整数中的最小值和最大值
  • main 中调用并打印结果

练习 3:体验值传递

要求:

  • 定义函数 doubleValue(n int),在函数内部把 n 乘以 2 并打印
  • main 中定义变量 x,调用 doubleValue(x) 后打印 x
  • 观察 x 是否被修改,解释原因

练习 4:可变参数求平均值

要求:

  • 定义函数 average(nums ...float64) float64
  • 计算并返回所有参数的平均值
  • 考虑参数为空的情况(返回 0
  • main 中测试多种调用

练习 5:写一个简单计算器函数

要求:

  • 定义函数 calculate(a, b float64, op string) (float64, string)
  • 根据 op 的值("+""-""*""/")执行对应运算
  • 除零时返回错误信息
  • 未知运算符返回错误信息
  • main 中测试各种情况

练习 6:用函数作为参数

要求:

  • 定义 apply(a, b int, op func(int, int) int) int
  • 定义 addsubtract 两个函数
  • 通过 apply 调用不同的运算函数

练习 7:用匿名函数改写练习 6

要求:

  • 不单独定义 addsubtract
  • 直接在调用 apply 时传入匿名函数

练习 8:命名返回值练习

要求:

  • 定义函数 circleInfo(radius float64) (area, circumference float64)
  • 计算圆的面积(π × r²)和周长(2 × π × r)
  • 使用 math.Pi 作为 π 的值
  • main 中调用并输出结果

15. 自测题

不看文档,试着回答:

15.1 概念题

  1. 函数定义的基本语法是什么?
  2. Go 的参数传递是值传递还是引用传递?
  3. 多返回值有什么用?最典型的使用模式是什么?
  4. 命名返回值是什么?“裸 return” 是什么意思?
  5. 可变参数的语法是什么?它在函数内部是什么类型?
  6. 可变参数必须在参数列表的什么位置?
  7. _ 在接收多返回值时的作用是什么?
  8. 函数名首字母大写和小写有什么区别?
  9. 匿名函数是什么?怎么立即执行?
  10. 为什么说 Go 的函数是"一等公民"?

如果你能流畅回答这些问题,说明这一课你已经真正理解了。


16. 本课总结

这一课你学到的是 Go 程序组织的基础能力——函数。

你现在应该已经理解:

  • 函数用 func 定义,可以有参数和返回值
  • 相同类型的连续参数可以简写
  • return 返回值并结束函数执行
  • Go 支持多返回值,最典型的模式是"结果 + 错误"
  • 命名返回值可以提高可读性,支持"裸 return"
  • 可变参数用 ... 表示,必须在参数列表最后
  • Go 是值传递,函数内修改参数不影响外部变量
  • 函数是一等公民,可以赋值给变量、作为参数传递
  • 匿名函数可以直接定义并使用

17. 阶段一完成回顾

到这里,你已经完成了阶段一:入门基础阶段的全部 6 课内容。

回顾一下你现在的能力:

课次 主题 你获得的能力
第 01 课 环境搭建 能创建并运行 Go 程序
第 02 课 变量常量类型 能声明变量、理解零值和类型系统
第 03 课 输入输出与类型转换 能写交互式程序、做类型转换
第 04 课 条件判断 能让程序根据条件做出决定
第 05 课 循环结构 能让程序重复执行逻辑
第 06 课 函数基础 能把逻辑封装成可复用的函数

你现在已经具备了写简单 Go 控制台程序的完整基础。接下来我们进入阶段二:核心语法阶段,开始学习指针、数组、切片、map、结构体和方法——这些是写出更复杂程序的核心工具。


18. 下一课预告

下一课我们进入阶段二,首先学习:指针入门

会重点讲:

  • 什么是指针,为什么需要它
  • 取地址 & 和解引用 * 的含义
  • 指针和函数参数的关系
  • 为什么有了指针就能在函数里修改外部变量
  • 指针的零值是什么
  • 新手常见的指针误区

学完下一课,你就能理解"为什么 fmt.Scan 需要加 &"这个问题了。