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

上一篇我们学习了指针 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------没有名字的函数与灵活回调

相关推荐
艾莉丝努力练剑2 小时前
【AI接入大模型SDK】ChatSDK示例验证
人工智能·学习·面试·大模型·sdk
To_OC6 小时前
跑了3个Docker容器后,我终于搞懂镜像和容器到底啥关系
后端·docker·容器
东风破_7 小时前
Docker 基础:为什么需要容器?
前端·后端·nginx
东风破_7 小时前
Docker 实战:使用 Nginx 作为容器入口代理 Node 服务
前端·后端·nginx
Lyyaoo.7 小时前
Spring Boot Validation 声明式参数校验
spring boot·后端·mysql
剩下了什么8 小时前
float32 与 float64 精度陷阱:如何在 Go 中避免错误使用
开发语言·后端·golang
Rain的Java大神之路8 小时前
介绍一下分布式事务
java·分布式·后端·spring·spring cloud·架构·springcloud
码匠许师傅11 小时前
【C++ 面试真题】24. 聊聊 C++ 的时间日期处理
java·c++·面试
IT_陈寒12 小时前
Vue的响应式什么时候会失灵?这个坑我踩了
前端·人工智能·后端