上一篇我们学习了指针 Pointer,理解了变量地址、& 取地址、* 解引用以及函数参数的值传递。实际上前面的代码中,我们已经大量使用了函数,例如:
csharp
func main() {
fmt.Println("Hello Go")
}
这里的 main() 就是一个函数。随着程序越来越复杂,如果所有代码都写在 main() 中,代码会变得非常难维护。因此需要把不同功能拆分成一个个函数。函数是 Go 程序最基础的代码组织方式之一,也是后面学习匿名函数、闭包、方法和并发编程的重要基础。
一、什么是函数
函数可以理解为一段完成特定功能、可以重复调用的代码。
例如需要计算两个数字之和,如果直接写:
go
a := 10
b := 20
result := a + b
如果很多地方都需要计算,就会出现大量重复代码。可以封装:
go
func add(a int, b int) int {
return a + b
}
以后直接:
sql
result := add(10, 20)
函数最大的价值就是:
封装功能
减少重复代码
提高代码复用
降低程序复杂度
方便测试和维护
二、函数基本语法
Go 定义函数使用 func:
go
func 函数名(参数列表) 返回值 {
函数体
}
例如:
go
func add(a int, b int) int {
return a + b
}
这里:
csharp
func 定义函数
add 函数名称
a、b 参数
int 参数和返回值类型
return 返回结果
调用:
sql
result := add(10, 20)
fmt.Println(result)
输出:
30
三、没有参数的函数
函数可以没有参数:
csharp
func hello() {
fmt.Println("Hello Go")
}
调用:
scss
func main() {
hello()
}
注意调用函数必须加:
scss
()
因此:
scss
hello()
表示执行函数。
四、带参数的函数
例如:
go
func hello(name string) {
fmt.Println("Hello", name)
}
调用:
scss
hello("Tom")
hello("Jack")
输出:
Hello Tom
Hello Jack
多个参数:
css
func add(a int, b int) {
fmt.Println(a + b)
}
调用:
scss
add(10, 20)
参数类型不同也可以:
go
func user(name string, age int) {
fmt.Println(name, age)
}
调用:
scss
user("Tom", 20)
五、相同类型参数简写
如果多个连续参数类型相同:
go
func add(a int, b int) int {
return a + b
}
可以简写:
go
func add(a, b int) int {
return a + b
}
例如:
go
func sum(a, b, c int) int {
return a + b + c
}
这种写法在 Go 项目中非常常见。
六、函数返回值
函数可以通过 return 返回数据:
go
func add(a, b int) int {
return a + b
}
调用:
sql
result := add(10, 20)
fmt.Println(result)
还可以直接:
csharp
fmt.Println(add(10, 20))
如果函数没有返回值:
csharp
func hello() {
fmt.Println("Hello")
}
就不需要声明返回类型。
七、return 会结束函数
return 不仅用于返回数据,也会立即结束当前函数。
例如:
go
func checkAge(age int) {
if age < 18 {
fmt.Println("未成年")
return
}
fmt.Println("成年人")
}
当:
scss
checkAge(16)
执行到:
kotlin
return
函数就结束了,后面的:
arduino
fmt.Println("成年人")
不会继续执行。
这种写法可以减少多层 if 嵌套,在实际项目中非常常见。
八、多个返回值
Go 函数一个非常重要的特点是支持多个返回值。
例如:
css
func calculate(a, b int) (int, int) {
return a + b, a - b
}
调用:
css
sum, sub := calculate(20, 10)
fmt.Println(sum)
fmt.Println(sub)
输出:
30
10
多个返回值在 Go 中非常常见,特别是:
css
result, err := someFunction()
一个返回正常结果,一个返回错误信息。
这也是后面学习 error 错误处理时最常见的代码形式。
九、忽略某个返回值
如果函数返回多个值,但只需要其中一个,可以使用:
_
例如:
css
func calculate(a, b int) (int, int) {
return a + b, a - b
}
只需要加法结果:
bash
sum, _ := calculate(20, 10)
fmt.Println(sum)
_ 叫空白标识符,表示这个值不需要使用。
十、命名返回值
Go 还支持给返回值命名:
perl
func calculate(a, b int) (sum int, sub int) {
sum = a + b
sub = a - b
return
}
这里:
python
sum int
sub int
就是命名返回值。
由于已经赋值:
css
sum = a + b
sub = a - b
最后可以直接:
kotlin
return
不过对于比较短的函数:
go
func add(a, b int) int {
return a + b
}
通常更加直观。命名返回值应该根据代码可读性合理使用。
十一、函数参数是值传递
上一篇指针已经讲过,Go 函数参数默认都是值传递。
例如:
go
func change(num int) {
num = 100
}
func main() {
num := 10
change(num)
fmt.Println(num)
}
输出:
10
调用:
scss
change(num)
相当于把 num 的值复制给函数参数。
函数内部修改的是副本,因此不会影响外部变量。
十二、使用指针参数
如果需要修改外部变量:
go
func change(num *int) {
*num = 100
}
调用:
dart
num := 10
change(&num)
fmt.Println(num)
输出:
100
这里:
yaml
&num
传入地址,
yaml
*num
通过地址修改原变量。
因此是否使用指针参数,需要根据函数是否需要修改调用方数据来决定。
十三、Slice 作为函数参数
实际开发中经常:
go
func printUsers(users []string) {
for _, user := range users {
fmt.Println(user)
}
}
调用:
bash
users := []string{"Tom", "Jack", "Lucy"}
printUsers(users)
Slice 参数可以修改底层数组中的元素:
go
func change(nums []int) {
nums[0] = 100
}
例如:
scss
nums := []int{10, 20, 30}
change(nums)
fmt.Println(nums)
输出:
csharp
[100 20 30]
如果函数需要追加元素,更推荐返回新的 Slice:
go
func add(nums []int, value int) []int {
return append(nums, value)
}
调用:
ini
nums = add(nums, 40)
十四、Map 作为函数参数
Map 也经常作为参数:
go
func update(scores map[string]int) {
scores["Tom"] = 100
}
调用:
go
scores := map[string]int{
"Tom": 90,
}
update(scores)
fmt.Println(scores["Tom"])
输出:
100
因此普通情况下不需要:
c
*map[string]int
直接传递 Map 就可以修改其中的数据。
十五、可变参数
如果不知道函数需要接收多少个参数,可以使用可变参数。
例如:
go
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
调用:
bash
fmt.Println(sum(1, 2))
fmt.Println(sum(1, 2, 3))
fmt.Println(sum(1, 2, 3, 4, 5))
都可以正常执行。
这里:
arduino
nums ...int
表示可以接收任意数量的 int 参数。
函数内部可以把 nums 当作:
arduino
[]int
使用。
十六、Slice 展开为可变参数
假设:
go
nums := []int{10, 20, 30}
已经有一个 Slice,希望传给:
go
func sum(nums ...int) int
可以:
sql
result := sum(nums...)
注意后面的:
erlang
...
表示把 Slice 中的元素展开作为参数传入。
这与前面:
css
append(a, b...)
中的用法类似。
十七、函数也有类型
在 Go 中,函数本身也有类型。
例如:
go
func add(a, b int) int {
return a + b
}
它的函数类型可以表示为:
go
func(int, int) int
因此可以定义函数变量:
go
var f func(int, int) int
然后:
ini
f = add
调用:
css
result := f(10, 20)
fmt.Println(result)
输出:
30
也就是说,函数可以像普通数据一样赋值给变量。
十八、函数作为参数
既然函数可以保存到变量中,也可以作为另一个函数的参数。
例如:
go
func add(a, b int) int {
return a + b
}
func calculate(a, b int, operation func(int, int) int) int {
return operation(a, b)
}
调用:
sql
result := calculate(10, 20, add)
fmt.Println(result)
输出:
30
这里:
go
operation func(int, int) int
表示参数 operation 本身就是一个函数。
这种设计在排序、回调、HTTP 处理等场景中非常常见。
十九、函数作为返回值
函数甚至可以返回另一个函数:
go
func getOperation() func(int, int) int {
return add
}
使用:
css
operation := getOperation()
fmt.Println(operation(10, 20))
这说明函数在 Go 中也是一种可以传递的数据。
这一点非常重要,因为下一篇学习匿名函数和闭包时会大量使用。
二十、递归函数
函数可以调用自己,这种方式叫递归。
例如计算阶乘:
go
func factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}
调用:
less
fmt.Println(factorial(5))
计算过程:
5 * 4 * 3 * 2 * 1
结果:
120
递归一定需要明确的结束条件:
kotlin
if n <= 1 {
return 1
}
否则函数会不断调用自己,最终可能导致栈空间耗尽。
二十一、defer 延迟执行
Go 函数中还有一个非常重要的关键字:
go
defer
例如:
go
func test() {
defer fmt.Println("结束")
fmt.Println("开始")
}
输出:
开始
结束
defer 后面的调用会延迟到当前函数即将返回时执行。
它非常适合释放资源,例如:
go
file, err := os.Open("test.txt")
if err != nil {
return
}
defer file.Close()
这样可以把"获取资源"和"释放资源"的代码放在比较接近的位置。
二十二、多个 defer 的执行顺序
如果:
go
func test() {
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
}
输出:
3
2
1
多个 defer 通常按照后注册先执行的顺序运行,可以简单理解为栈:
go
先 defer -> 后执行
后 defer -> 先执行
这也是 Go 面试中比较常见的基础问题。
二十三、函数命名
Go 函数命名通常采用驼峰方式:
scss
getUser()
createOrder()
calculatePrice()
如果函数名称首字母小写:
csharp
func getUser() {}
只能在当前 package 中访问。
如果首字母大写:
csharp
func GetUser() {}
则该标识符可以被其他 package 访问,但前提是所在 package 本身已经被正确导入。
因此 Go 中经常看到:
scss
http.Get()
json.Marshal()
fmt.Println()
这些对外公开的函数名称都是大写字母开头。
二十四、函数应该只做一件事
不要把大量完全不同的业务全部塞进一个函数:
csharp
func process() {
// 查询数据库
// 创建订单
// 支付
// 发邮件
// 写日志
// 统计数据
}
随着项目变大,这种函数会越来越难维护。
可以根据职责拆分:
csharp
func createOrder() {}
func payOrder() {}
func sendEmail() {}
func writeLog() {}
好的函数通常应该:
功能明确
名称清晰
参数合理
返回值明确
尽量减少副作用
避免过度复杂
这会显著提高项目可维护性。
二十五、函数实战:计算订单总价
例如:
go
package main
import "fmt"
func calculateTotal(prices []float64) float64 {
var total float64
for _, price := range prices {
total += price
}
return total
}
func main() {
prices := []float64{99.9, 199, 59.9, 299}
total := calculateTotal(prices)
fmt.Printf("订单总价:%.2f\n", total)
}
如果后续需要计算不同订单,只需要继续调用:
scss
calculateTotal(prices)
不需要重复编写统计逻辑,这就是函数带来的代码复用。
二十六、常见错误
第一个错误是参数类型不匹配:
csharp
func add(a, b int) int {
return a + b
}
add("10", "20")
字符串不能直接作为 int 参数。
第二个错误是返回值数量不匹配:
go
func test() (int, int) {
return 1, 2
}
result := test()
应该:
css
a, b := test()
或者忽略其中一个:
css
a, _ := test()
第三个错误是认为普通参数可以直接修改外部变量:
go
func change(num int) {
num = 100
}
这里修改的是副本。
第四个错误是递归没有结束条件,会不断调用自身。
第五个错误是函数过长、承担太多职责,最终导致代码难以理解和维护。
二十七、总结
定义函数:
csharp
func hello() {
fmt.Println("Hello")
}
带参数:
go
func hello(name string) {}
返回值:
go
func add(a, b int) int {
return a + b
}
多个返回值:
css
func calculate(a, b int) (int, int) {
return a + b, a - b
}
可变参数:
go
func sum(nums ...int) {}
函数变量:
go
var f func(int, int) int
函数作为参数:
go
func calculate(
a, b int,
fn func(int, int) int,
) int {
return fn(a, b)
}
延迟执行:
go
defer file.Close()
学习函数需要重点掌握:
go
1.函数用于封装和复用代码
2.函数可以有参数和返回值
3.Go 支持多个返回值
4.Go 的函数参数始终是值传递
5.可以使用指针修改外部数据
6.支持可变参数
7.函数可以赋值给变量
8.函数可以作为参数和返回值
9.defer 会在函数返回前执行
10.函数应该保持职责清晰
我们已经知道函数可以保存到变量中:
csharp
f := add
但这里的 add 仍然是一个提前定义并拥有名字的函数。如果某个函数只使用一次,或者希望直接把一段逻辑作为参数传递,再专门定义一个名字就显得有些繁琐。Go 因此支持一种非常灵活的函数写法------匿名函数 Anonymous Function 。
下一篇:匿名函数 Anonymous Function------没有名字的函数与灵活回调