Go从入门到精通——函数(function)—— 声明函数
函数(function)—— 声明函数+函数参数传递效果示例
函数是组织好的,可重复使用的,用来实现单一或相关功能的代码段,其可以提高应用的模块性和代码的重复利用率。
Go 语言支持普通函数、函数匿名和闭包,从设计上对函数进行了优化和改进,让函数使用起来更加方便。
Go 语言的函数属于 “一等公民” (first-class),也就是说:
-
- 函数本身可以作为值进行传递。
- 支持匿名函数和闭包(closure)。
- 函数可以满足接口。
1.1、声明函数
普通函数需要先声明才能调用。一个函数的声明包括参数和函数名等,编译器通过声明才能了解函数应该怎样在调用代码和函数体之间传递参数和返回参数。
Go 语言的函数声明以 func 标识,后面紧接着函数名、参数列表、返回参数列表及函数体,具体形式如下:
func 函数名(参数列表)(返回参数列表){
函数体
}
- 函数名:由字母、数字、下划线组成。其中,函数名的第一个字母不能为数字。在同一个包内,函数名称不能重名。
- 参数列表:一个参数由参数变量和参数类型组成,其中,参数列表中的变量作为函数的局部变量而存在。
- 返回参数列表:可以是返回值类型列表,也可以是类似参数列表中变量名和类型名的组合。函数在声明有返回值时,必须在函数体中使用 return 语句提供返回值列表。
- 函数体:能够被重复调用的代码片段。
1.2、参数类型的简写
在参数列表中,如有多个参数变量,则以逗号分隔;如果相邻变量是同类型,则可以将类型省略。例如:
func add(a, b int) int {
return a + b
}
1.3、函数的返回值
Go 语言支持多返回值,多返回值能方便地获得函数执行后的多个返回参数,Go 语言经常使用多返回值中的最后一个返回参数返回函数执行中可能发生的错误。
conn, err := connectToNetwork()
1.3.1、同一种类型返回值
如果返回值是同一种类型,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型。
使用 return 语句返回时,值列表的顺序需要与函数声明的返回值类型一致。
func typedTwoValues()(int, int) {
return 1, 2
}
a, b := typedTwoValue()
fmt.Println(a,b)
纯类型的返回值对于代码可读性不是很友好,特别是在同类型的返回值出现时,无法区分每个返回参数的意义(所以建议使用下面的带有变量名的返回值)。
1.3.2、带有变量名的返回值
Go 语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型。
命名的返回值变量的默认值为类型的默认值,即数值为 0,字符串为空字符串,布尔为 false、指针为 nil 等。
func namedRetValues()(a, b int){
a = 1
b = 2
return
}
当函数使用命名返回值时,可以在 return 中不填写返回值列表,如果填写也是可行的。下面代码和上面代码的效果一样:
func namedRetValues()(a, b int) {
a = 1
return a, 2
}
1.4、调用函数
函数在定义后,可以通过调用的方式,让当前代码跳转到被调用的函数中进行执行。
调用前的函数局部变量都会被保存起来不会丢失:被调用的函数结束后,恢复到被调用函数的下一行继续执行代码,之前的局部变量也能继续访问。
函数内的局部变量只能在函数体中使用,函数调用结束后,这些局部变量都会被释放并且失效。
Go 语言的函数调用格式如下:
返回值变量列表 = 函数名(参数列表)
- 函数名:需要调用的函数名
- 参数列表:参数变量以逗号分隔,尾部无须以分号结尾。
- 返回值变量列表:多个返回值使用逗号分隔。
1.5、函数示例
1.5.1、将 "秒" 解析为时间单位
在本例中,使用一个数值表示时间中的 "秒" 值,然后使用 resolveTime() 函数将传入的秒数转换为天、小时和分钟等时间单位。
package main
import (
"fmt"
)
const (
//定义每分钟的秒数
SecondsPerMinute = 60
//定义每小时的秒数
SecondsPerHour = SecondsPerMinute * 60
//定义每天的秒数
SecondsPerDay = SecondsPerHour * 24
)
// 将传入的 "秒" 解析为 3 种时间单位
func resolveTime(seconds int) (day int, hour int, minute int) {
day = seconds / SecondsPerDay
hour = seconds / SecondsPerHour
minute = seconds / SecondsPerMinute
return
}
func main() {
//将返回值作为打印参数
fmt.Println(resolveTime(1000))
//只获取消息和分钟
_, hour, minute := resolveTime(18000)
fmt.Println(hour, minute)
//只获取天
day, _, _ := resolveTime(80000)
fmt.Println(day)
}
1.5.2、函数中的参数传递效果测试
Go 语言中传入和返回参数在调用和返回时都使用值传递,这里需要注意的是指针、切片 和 map 等引用类型对象的内容在参数传递中不会发生复制,而是将指针进行复制,类似于创建一次引用。
下面举个例子来详细了解 Go 语言的参数值传递:
//用于测试值传递效果的结构体 Data,并填充所有成员,这些成员类型包括切片、结构体成员及指针。 type Data struct { //测试切片在参数传递中的效果, //complax 为整型切片类型,切片是一种动态类型,内部以指针存在。 complax []int //实例分配的 InnerData。instance 成员以 InnerData 类型作为 Data 成员。 instance InnerData //将 ptr 声明为 InnerData 的指针类型。 ptr *InnerData } //InnerData 结构体 type InnerData struct { a int }
1.5.2.1、值传递的测试函数
passByValue() 函数用于值传递的测试,该函数的参数和返回值都是 Data 类型。在调用中, Data 的内存会被复制后传入函数,当函数返回时,又会将返回值复制一次,赋给函数返回值的接收变量。代码如下:
//值传递测试函数,函数的参数和返回值都是 Data 类型。 func passByValue(inFunc Data) Data { //passByValue() 函数 in 传入后的数据成员情况。 fmt.Printf("in 传入函数后的数据成员情况: \t%+v, 打印 &inFunc 指针地址是多少: %p\n", inFunc, &inFunc) return inFunc }
代码说明如下:
- 第 5 行,使用格式化的 "%+v" 动词输出 inFunc 变量的详细结构体,以便观察 Data 结构在传递前后的内部数值的变化情况。
- 第 8 行,打印传入参数 inFunc 的指针地址。在计算机中,拥有相同地址且类型相同的变量,表示的是同一块内存区域。
- 第 10 行,将传入的变量作为返回值返回,返回的过程将要发生值复制。
1.5.2.2 测试流程(完整代码)
测试流程会准备一个 Data 格式的数据结构并填充所有成员,这些成员类型包括切片、结构体成员及指针。通过调用测试函数,传入 Data 结构数据,并获得返回值,对比输入和输出后的 Data 结构数值变化,特别是指针变化情况以及输入和输出整块数据是否被复制,代码如下:
package main import ( "fmt" ) //用于测试值传递效果的结构体 Data,并填充所有成员,这些成员类型包括切片、结构体成员及指针。 type Data struct { //测试切片在参数传递中的效果, //complax 为整型切片类型,切片是一种动态类型,内部以指针存在。 complax []int //实例分配的 InnerData。instance 成员以 InnerData 类型作为 Data 成员。 instance InnerData //将 ptr 声明为 InnerData 的指针类型。 ptr *InnerData } //InnerData 结构体 type InnerData struct { a int } //值传递测试函数,函数的参数和返回值都是 Data 类型。 func passByValue(inFunc Data) Data { //passByValue() 函数 in 传入后的数据成员情况。 fmt.Printf("in 传入函数后的数据成员情况: \t%+v, 打印 &inFunc 指针地址是多少: %p\n", inFunc, &inFunc) return inFunc } func main() { in := Data{ complax: []int{1, 2, 3}, instance: InnerData{ 5, }, ptr: &InnerData{1}, } //打印输入值 in 变量的成员情况,打印输入的指针地址。 fmt.Printf("打印输入值 in 变量的成员情况: \t%+v, 打印 &in 指针地址是多少: %p\n", in, &in) //传入 in 结构,调用 passByValue() 测试函数获得 out 返回, //此时,passByValue() 函数会打印 in 传入后的数据成员请。 out := passByValue(in) //打印返回值 out 变量的成员情况,打印输出结构的地址。 fmt.Printf("打印返回值 out 变量的成员情况: \t%+v, 打印 &out 指针地址是多少: %p\n", out, &out) }
程序执行后:
Starting: D:\go-testfiles\bin\dlv.exe dap --check-go-version=false --listen=127.0.0.1:52187 from d:\go-testfiles DAP server listening at: 127.0.0.1:52187 Type 'dlv help' for list of commands. 打印输入值 in 变量的成员情况: {complax:[1 2 3] instance:{a:5} ptr:0xc000014088}, 打印 &in 指针地址是多少: 0xc000078480 in 传入函数后的数据成员情况: {complax:[1 2 3] instance:{a:5} ptr:0xc000014088}, 打印 &inFunc 指针地址是多少: 0xc000078510 打印返回值 out 变量的成员情况: {complax:[1 2 3] instance:{a:5} ptr:0xc000014088}, 打印 &out 指针地址是多少: 0xc0000784e0 Process 5864 has exited with status 0 Detaching dlv dap (9300) exited with code: 0
从结果中发现:
- 所有的 Data 结构的指针地址发生了变化,意味着所有的结构都是一块新的内存,无论是 Data 结构传入函数内部,还是通过函数返回值传回 Data 都会发生复制行为。
- 所有的 Data 结构中的成员值都没有发生变化,原样传递,意味着所有参数都是值传递。
- Data 结构的 ptr 成员在传递过程中保持一致,表示指针在函数参数值传递中传递的只是指针值(&ptr),不会复制指针指向的部分(*ptr)。
提示:
对于Go语言,严格意义上来讲,只有一种传递,也就是按值传递(by value)。当一个变量当作参数传递的时候,会创建一个变量的副本,然后传递给函数或者方法,你可以看到这个副本的地址和变量的地址是不一样的。
当变量当做指针被传递的时候,一个新的指针被创建,它指向变量指向的同样的内存地址,所以你可以将这个指针看成原始变量指针的副本。
当这样理解的时候,我们就可以理解成Go总是创建一个副本按值转递,只不过这个副本有时候是变量的副本,有时候是变量指针的副本。
经常会见到: p . *p , &p 三个符号:
- p 是一个指针变量的名字,表示此指针变量指向的内存地址,如果使用 %p 来输出的话,它将是一个 16进制 数。
- *p 可以表示一个变量是指针类型,也可以表示此指针指向的内存地址中存放的内容,一般是一个和指针类型一致的变量或者常量。
&是取地址运算符,&p就是取指针p的地址。它到底和p有什么区别?
- 指针 p 同时也是个变量,既然是变量,编译器肯定要为其分配内存地址,就像程序中定义了一个 int 型的变量 i,编译器要为其分配一块内存空间一样。
- 而 &p 就表示编译器为变量 p 分配的内存地址,而因为 p 是一个指针变量,这种特殊的身份注定了它要指向另外一个内存地址,程序员按照程序的需要让它指向一个内存地址,这个它指向的内存地址就用 p 表示。而且,p 指向的地址中的内容就用 *p 表示。