Go 编程实战:函数 Function——参数、返回值与代码复用

0 阅读10分钟

上一篇我们学习了指针 Pointer,理解了变量地址、& 取地址、* 解引用以及函数参数的值传递。实际上前面的代码中,我们已经大量使用了函数,例如:

func main() {
	fmt.Println("Hello Go")
}

这里的 main() 就是一个函数。随着程序越来越复杂,如果所有代码都写在 main() 中,代码会变得非常难维护。因此需要把不同功能拆分成一个个函数。函数是 Go 程序最基础的代码组织方式之一,也是后面学习匿名函数、闭包、方法和并发编程的重要基础。

一、什么是函数

函数可以理解为一段完成特定功能、可以重复调用的代码。
例如需要计算两个数字之和,如果直接写:

a := 10
b := 20
result := a + b

如果很多地方都需要计算,就会出现大量重复代码。可以封装:

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

以后直接:

result := add(10, 20)

函数最大的价值就是:

封装功能
减少重复代码
提高代码复用
降低程序复杂度
方便测试和维护

二、函数基本语法

Go 定义函数使用 func

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

例如:

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

这里:

func       定义函数
add        函数名称
a、b       参数
int        参数和返回值类型
return     返回结果

调用:

result := add(10, 20)
fmt.Println(result)

输出:

30

三、没有参数的函数

函数可以没有参数:

func hello() {
	fmt.Println("Hello Go")
}

调用:

func main() {
	hello()
}

注意调用函数必须加:

()

因此:

hello()

表示执行函数。

四、带参数的函数

例如:

func hello(name string) {
	fmt.Println("Hello", name)
}

调用:

hello("Tom")
hello("Jack")

输出:

Hello Tom
Hello Jack

多个参数:

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

调用:

add(10, 20)

参数类型不同也可以:

func user(name string, age int) {
	fmt.Println(name, age)
}

调用:

user("Tom", 20)

五、相同类型参数简写

如果多个连续参数类型相同:

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

可以简写:

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

例如:

func sum(a, b, c int) int {
	return a + b + c
}

这种写法在 Go 项目中非常常见。

六、函数返回值

函数可以通过 return 返回数据:

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

调用:

result := add(10, 20)
fmt.Println(result)

还可以直接:

fmt.Println(add(10, 20))

如果函数没有返回值:

func hello() {
	fmt.Println("Hello")
}

就不需要声明返回类型。

七、return 会结束函数

return 不仅用于返回数据,也会立即结束当前函数。
例如:

func checkAge(age int) {
	if age < 18 {
		fmt.Println("未成年")
		return
	}
	fmt.Println("成年人")
}

当:

checkAge(16)

执行到:

return

函数就结束了,后面的:

fmt.Println("成年人")

不会继续执行。
这种写法可以减少多层 if 嵌套,在实际项目中非常常见。

八、多个返回值

Go 函数一个非常重要的特点是支持多个返回值。
例如:

func calculate(a, b int) (int, int) {
	return a + b, a - b
}

调用:

sum, sub := calculate(20, 10)
fmt.Println(sum)
fmt.Println(sub)

输出:

30
10

多个返回值在 Go 中非常常见,特别是:

result, err := someFunction()

一个返回正常结果,一个返回错误信息。
这也是后面学习 error 错误处理时最常见的代码形式。

九、忽略某个返回值

如果函数返回多个值,但只需要其中一个,可以使用:

_

例如:

func calculate(a, b int) (int, int) {
	return a + b, a - b
}

只需要加法结果:

sum, _ := calculate(20, 10)
fmt.Println(sum)

_ 叫空白标识符,表示这个值不需要使用。

十、命名返回值

Go 还支持给返回值命名:

func calculate(a, b int) (sum int, sub int) {
	sum = a + b
	sub = a - b
	return
}

这里:

sum int
sub int

就是命名返回值。
由于已经赋值:

sum = a + b
sub = a - b

最后可以直接:

return

不过对于比较短的函数:

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

通常更加直观。命名返回值应该根据代码可读性合理使用。

十一、函数参数是值传递

上一篇指针已经讲过,Go 函数参数默认都是值传递。
例如:

func change(num int) {
	num = 100
}
func main() {
	num := 10
	change(num)
	fmt.Println(num)
}

输出:

10

调用:

change(num)

相当于把 num 的值复制给函数参数。
函数内部修改的是副本,因此不会影响外部变量。

十二、使用指针参数

如果需要修改外部变量:

func change(num *int) {
	*num = 100
}

调用:

num := 10
change(&num)
fmt.Println(num)

输出:

100

这里:

&num

传入地址,

*num

通过地址修改原变量。
因此是否使用指针参数,需要根据函数是否需要修改调用方数据来决定。

十三、Slice 作为函数参数

实际开发中经常:

func printUsers(users []string) {
	for _, user := range users {
		fmt.Println(user)
	}
}

调用:

users := []string{"Tom", "Jack", "Lucy"}
printUsers(users)

Slice 参数可以修改底层数组中的元素:

func change(nums []int) {
	nums[0] = 100
}

例如:

nums := []int{10, 20, 30}
change(nums)
fmt.Println(nums)

输出:

[100 20 30]

如果函数需要追加元素,更推荐返回新的 Slice:

func add(nums []int, value int) []int {
	return append(nums, value)
}

调用:

nums = add(nums, 40)

十四、Map 作为函数参数

Map 也经常作为参数:

func update(scores map[string]int) {
	scores["Tom"] = 100
}

调用:

scores := map[string]int{
	"Tom": 90,
}
update(scores)
fmt.Println(scores["Tom"])

输出:

100

因此普通情况下不需要:

*map[string]int

直接传递 Map 就可以修改其中的数据。

十五、可变参数

如果不知道函数需要接收多少个参数,可以使用可变参数。
例如:

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

调用:

fmt.Println(sum(1, 2))
fmt.Println(sum(1, 2, 3))
fmt.Println(sum(1, 2, 3, 4, 5))

都可以正常执行。
这里:

nums ...int

表示可以接收任意数量的 int 参数。
函数内部可以把 nums 当作:

[]int

使用。

十六、Slice 展开为可变参数

假设:

nums := []int{10, 20, 30}

已经有一个 Slice,希望传给:

func sum(nums ...int) int

可以:

result := sum(nums...)

注意后面的:

...

表示把 Slice 中的元素展开作为参数传入。
这与前面:

append(a, b...)

中的用法类似。

十七、函数也有类型

在 Go 中,函数本身也有类型。
例如:

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

它的函数类型可以表示为:

func(int, int) int

因此可以定义函数变量:

var f func(int, int) int

然后:

f = add

调用:

result := f(10, 20)
fmt.Println(result)

输出:

30

也就是说,函数可以像普通数据一样赋值给变量。

十八、函数作为参数

既然函数可以保存到变量中,也可以作为另一个函数的参数。
例如:

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

调用:

result := calculate(10, 20, add)
fmt.Println(result)

输出:

30

这里:

operation func(int, int) int

表示参数 operation 本身就是一个函数。
这种设计在排序、回调、HTTP 处理等场景中非常常见。

十九、函数作为返回值

函数甚至可以返回另一个函数:

func getOperation() func(int, int) int {
	return add
}

使用:

operation := getOperation()
fmt.Println(operation(10, 20))

这说明函数在 Go 中也是一种可以传递的数据。
这一点非常重要,因为下一篇学习匿名函数和闭包时会大量使用。

二十、递归函数

函数可以调用自己,这种方式叫递归。
例如计算阶乘:

func factorial(n int) int {
	if n <= 1 {
		return 1
	}
	return n * factorial(n-1)
}

调用:

fmt.Println(factorial(5))

计算过程:

5 * 4 * 3 * 2 * 1

结果:

120

递归一定需要明确的结束条件:

if n <= 1 {
	return 1
}

否则函数会不断调用自己,最终可能导致栈空间耗尽。

二十一、defer 延迟执行

Go 函数中还有一个非常重要的关键字:

defer

例如:

func test() {
	defer fmt.Println("结束")
	fmt.Println("开始")
}

输出:

开始
结束

defer 后面的调用会延迟到当前函数即将返回时执行。
它非常适合释放资源,例如:

file, err := os.Open("test.txt")
if err != nil {
	return
}
defer file.Close()

这样可以把“获取资源”和“释放资源”的代码放在比较接近的位置。

二十二、多个 defer 的执行顺序

如果:

func test() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	defer fmt.Println(3)
}

输出:

3
2
1

多个 defer 通常按照后注册先执行的顺序运行,可以简单理解为栈:

defer -> 后执行
后 defer -> 先执行

这也是 Go 面试中比较常见的基础问题。

二十三、函数命名

Go 函数命名通常采用驼峰方式:

getUser()
createOrder()
calculatePrice()

如果函数名称首字母小写:

func getUser() {}

只能在当前 package 中访问。
如果首字母大写:

func GetUser() {}

则该标识符可以被其他 package 访问,但前提是所在 package 本身已经被正确导入。
因此 Go 中经常看到:

http.Get()
json.Marshal()
fmt.Println()

这些对外公开的函数名称都是大写字母开头。

二十四、函数应该只做一件事

不要把大量完全不同的业务全部塞进一个函数:

func process() {
	// 查询数据库
	// 创建订单
	// 支付
	// 发邮件
	// 写日志
	// 统计数据
}

随着项目变大,这种函数会越来越难维护。
可以根据职责拆分:

func createOrder() {}
func payOrder() {}
func sendEmail() {}
func writeLog() {}

好的函数通常应该:

功能明确
名称清晰
参数合理
返回值明确
尽量减少副作用
避免过度复杂

这会显著提高项目可维护性。

二十五、函数实战:计算订单总价

例如:

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)
}

如果后续需要计算不同订单,只需要继续调用:

calculateTotal(prices)

不需要重复编写统计逻辑,这就是函数带来的代码复用。

二十六、常见错误

第一个错误是参数类型不匹配:

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

字符串不能直接作为 int 参数。
第二个错误是返回值数量不匹配:

func test() (int, int) {
	return 1, 2
}
result := test()

应该:

a, b := test()

或者忽略其中一个:

a, _ := test()

第三个错误是认为普通参数可以直接修改外部变量:

func change(num int) {
	num = 100
}

这里修改的是副本。
第四个错误是递归没有结束条件,会不断调用自身。
第五个错误是函数过长、承担太多职责,最终导致代码难以理解和维护。

二十七、总结

定义函数:

func hello() {
	fmt.Println("Hello")
}

带参数:

func hello(name string) {}

返回值:

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

多个返回值:

func calculate(a, b int) (int, int) {
	return a + b, a - b
}

可变参数:

func sum(nums ...int) {}

函数变量:

var f func(int, int) int

函数作为参数:

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

延迟执行:

defer file.Close()

学习函数需要重点掌握:

1.函数用于封装和复用代码
2.函数可以有参数和返回值
3.Go 支持多个返回值
4.Go 的函数参数始终是值传递
5.可以使用指针修改外部数据
6.支持可变参数
7.函数可以赋值给变量
8.函数可以作为参数和返回值
9.defer 会在函数返回前执行
10.函数应该保持职责清晰

我们已经知道函数可以保存到变量中:

f := add

但这里的 add 仍然是一个提前定义并拥有名字的函数。如果某个函数只使用一次,或者希望直接把一段逻辑作为参数传递,再专门定义一个名字就显得有些繁琐。Go 因此支持一种非常灵活的函数写法——匿名函数 Anonymous Function
下一篇:匿名函数 Anonymous Function——没有名字的函数与灵活回调