Go语言实践[回顾]教程23--详解Go语言函数的声明、变参、参数传递
- 函数的声明(定义)
- 函数的基本声明格式与调用
- 函数的变参(不定参)
- 值传递还是引用地址传递
函数是 Go 语言源代码的基本构造单位,其用花括号将具有一完整意义或功能的代码块包裹起来并定义名称,以便根据逻辑需要随时调用整体执行其内的代码块。编写函数的主要目的是分解复杂的代码逻辑,将一个需要很多行代码才能解决的问题封装成一个函数完成这个任务,然后遇到同一个任务可以多次调用该函数,这有助于代码重用及使代码结构清晰易懂。
函数定义包括一下几个部分:函数声明关键字 func、函数名、参数列表、返回值列表、函数体(花括号内的代码)。函数名同样遵循标识符的命名规则,首字母大写的其他包可以调用,首字母小写的只能在本包中使用。包裹函数体的花括号中的左括号必须在函数名所在行的行尾。
函数的声明(定义)
基本语法格式:func 函数名 ( 参数列表 ) ( 返回值列表 ) { 函数体 }
func:声明函数的关键字,必须在函数名的前面;
函数名:自定义的函数名称,不能以数字开头,要复合标识符规范;
参数列表:定义函数可接收的数据类型及个数位置,可以是 0到多个参数,没有参数就直接写空括号即可。有参数的函数调用时按照定义的顺序赋值,参数变量是函数体内的局部变量;
返回值列表:定义函数返回值的数据类型及个数位置,可以是 0到多个,没有或只有一个无名返回值时,可以不写返回值列表的括号。可以给返回值命名,若命名就也是函数体内的局部变量,且在 return 关键字后面不用写任何值和变量,return 会自动将命名返回值变量的值返回到调用处;
与其他语言比较,Go 语言的函数有以下不同特点:
● 不支持设置默认值参数,参数会被自动初始化为该类型的零值或空值;
● 支持多个返回值;
● 支持给返回值命名,返回值变量同参数一样,也是函数体内的局部变量,且会在调用 return 是自动返回;
● 不支持函数重载,同一包内不能有同名函数;
● 不支持命名函数的嵌套,但支持匿名函数嵌套。
函数的基本声明格式与调用
函数在有返回值的时候才会需要用 return 关键字结束函数的运行,否则不需要写 return 关键字,函数体内代码顺序执行完后自动结束。如果函数体内有条件分支想要提前结束函数运行,这是需要使用 return 关键字,无论有没有返回值。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 声明一个无参数、无返回值的函数
func noPramReturn() {
fmt.Println("无返回值无参数")
}
// 声明一个有多个参数、无返回值的函数
func pram2IntNoReturnA(a int, b int) {
fmt.Println("无返回值A有两个参数:", a, "和", b)
}
// 声明一个有多个参数、无返回值的函数
func pram2IntNoReturnB(a, b int) {
fmt.Println("无返回值B有两个参数:", a, "和", b)
}
// 声明一个有多个参数、有一个无名返回值的函数
func pram2IntReturnNoName1(a, b int) int {
fmt.Println("有一个无名返回值有两个参数:", a, "和", b)
return a + b // 计算结果就是返回值,在调用本函数的位置会收到这个 8
}
// 声明一个有一个参数、有一个命名返回值的函数
func pram1IntReturnName1(a int) (f int) {
f = a + 10
fmt.Println("有一个有名返回值:", f, ", 有一个参数:", a)
return // return 会自动把 f 返回到调用本函数的位置
}
// 声明一个有一个参数、有多个命名返回值的函数
func pram1IntReturnNoName2A(a int) (f1, f2 int) {
f1 = a + 10
f2 = a + 20
fmt.Println("有两个有名返回值:", f1, "和", f2, ", 有一个参数:", a)
return // return 会自动把 f 返回到调用本函数的位置
}
// 声明一个有一个参数、有多个命名返回值的函数
func pram1IntReturnNoName2B(a int) (f1 int, f2 string) {
f1 = a + 10
f2 = "我是返回的第二个参数"
fmt.Println("有两个有名返回值:", f1, "和", f2, ", 有参数:", a)
return // return 会自动把 f 返回到调用本函数的位置
}
// 主函数,程序入口
func main() {
noPramReturn()
pram2IntNoReturnA(1, 2)
pram2IntNoReturnB(1, 2)
a := pram2IntReturnNoName1(1, 2)
fmt.Println("pram2IntReturnNoName1的一个返回值:", a)
b := pram1IntReturnName1(1)
fmt.Println("pram1IntReturnName1的一个返回值:", b)
c, d := pram1IntReturnNoName2A(1)
fmt.Println("pram1IntReturnName1的两个返回值:", c, "和", d)
e, f := pram1IntReturnNoName2B(1)
fmt.Println("pram1IntReturnName1的两个返回值:", e, "和", f)
}
上述代码编译执行结果如下:
无返回值无参数
无返回值A有两个参数: 1 和 2
无返回值B有两个参数: 1 和 2
有一个无名返回值有两个参数: 1 和 2
pram2IntReturnNoName1的一个返回值: 3
有一个有名返回值: 11 , 有一个参数: 1
pram1IntReturnName1的一个返回值: 11
有两个有名返回值: 11 和 21 , 有一个参数: 1
pram1IntReturnName1的两个返回值: 11 和 21
有两个有名返回值: 11 和 我是返回的第二个参数 , 有参数: 1
pram1IntReturnName1的两个返回值: 11 和 我是返回的第二个参数
函数调用,就是要去执行函数体内的代码块,格式就是直接书写函数名及其后面跟的参数列表(无参数就是空括号)即可。如第54~57、59、61、63行,均是对函数的调用,或者说调用函数。函数调用时,先去执行调用函数的函数体内的代码,执行结束后,再回到调用处继续向下顺序执行。
第9~11行,是声明一个没有参数也没有返回值的函数。因没有返回值,所以也省略了返回值列表的括号。这个函数在调用时不需要接返回值,如第54行。因没有参数,所以函数名 noPramReturn 后面跟个空括号。
第14~16行,是声明一个有两个(多个)参数但无返回值的函数。每个参数变量都要声明类型,代表在调用这个函数时,该位置的参数应该赋值什么类型的数据。参数变量同时也是函数体内的局部变量,在函数体内按照局部变量同等使用,只是初始化的值是在调用函数的地方赋值的。如55行,就表示将 1 赋值给了参数变量 a,将 2 赋值给了参数变量 b。所以函数体内的打印输出语句可以输出 1 和 2 这两个值。
第19~21行,与第上面一个函数作用完全相同,只是声明参数的时候,因为两个参数都是 int 类型,所以可以简式声明。
第24~27行,是声明一个有两个(多个)参数且有一个无名返回值的函数。这里没有给返回值命名,值给了返回值的类型 int(参数右括号 “) ”与 “{” 之间的那个 int ),且只有一个返回值,这就不需要使用括号将返回值声明括起来。函数内的返回值在没有命名的情况下,需要将要返回的数据放在 return 关键字的后面才可以返回到调用的地方。第57行就是调用这个函数,并使用变量 a 接收了这个函数的返回值。第58行我们输出了这个接到的值,根据打印结果,接到的值就是函数内返回的。
return 关键字放在函数任何位置,只要执行到它,就会结束函数的执行,返回到调用处继续顺序执行。
第30~34行,声明一个有一个参数且有一个命名返回值的函数。因参数部分前面已经理解了,这里至描述返回值的不同。这个函数与上一个函数的差别就是使用了命名返回值,将返回值命名一个变量,我们称之为返回值变量。返回值变量与参数变量同样属于函数体的局部变量,但返回值变量是在函数体内执行到 return 语句时自动返回,不需要再跟在 return 的后面了。因为给返回值命名了,再加上返回值类型,那参数括号与左花括号之间就不只有一个标识符了,为了编译器识别方便,所以要求给返回值部分也加上括号。
第37~42行,声明一个有一个参数并有两个(多个)命名返回值的函数。Go 语言的函数支持多个返回值,使用逗号分隔。如果没有命名返回值变量而是多个返回值时,在 return 后面按照上面定义的顺序跟上数据即可。调用处需要按照定义的顺序使用多个变量接收,如第61行中的 c 接的就是函数定义的返回值 f1,d 接的就是函数定义的返回值 f2。如果哪个返回值不想接收使用,那就在对应的位置的变量写成匿名变量(_)即可,Go 语言就会在该位置接收后便抛弃,即不影响其他数据的返回顺序,也不要求后面必须使用这个变量。
第45~50行,与上一个函数定义的差别就是两个返回值类型不同,所以没有使用简式声明,改用了各自独立声明。在第63行接到两个返回值后,第64行输出的结果,我们看到了两个不同类型的数据都被接收到了。
函数的变参(不定参)
Go 语言的函数支持不确定数据的形参(形式参数,函数定义时的参数),声明语法是:参数名 …类型名。Go 语言函数的变参有以下特点:
● 仅允许定义一个变参,且必须是函数的最后一个参数;
● 所有变参类型相同;
● 变参变量在函数体内相当于切片,所以与切片有同样的操作特性;
● 调用函数时,切片可以传递给变参,但后面一定要加上“…”进行解包;
● 形参为变参与形参为切片的两个函数并不一样;
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 声明有一个定参和一个变参(b)函数
func abc(a string, b ...int) (n string, f int) { // 变参 b 必须是在最后一个参数
for _, v := range b { // 与遍历切片一样
f += v
}
n = "计算标志:" + a // 定参正常操作
return
}
// 主函数,程序入口
func main() {
c := []int{2, 4, 6}
a, b := abc("A1", 1, 3, 5)
fmt.Println(a, b)
a, b = abc("A2", c...)
fmt.Println(a, b)
}
上述代码编译执行结果如下:
计算标志:A1 9
计算标志:A2 12
示例中的参数 b 就是变参,表示数量有多少个不一定,由调用时赋值的数据决定,但是必须都是 int 类型的数据。在第20行的调用里,我们总计给了4个参数,第一个是一个字符串“A1”,后面三个依次是 1、3、5 三个数字。我们再看第9行的函数声明,只有两个参数,第一个是字符串变量 a,这个刚好接调用处赋值的“A1”,而 b 就是接余下所有的实参(调用处给参数赋的值)的,等于把余下的 1、3、5 实参全部接到一个切片 b 里。这样第10~12行的循环给切片取值就顺理成章了。循环内把所有数字结果累加到一起,返回给了第20行的 b,把“计算标志:”字符串与 a 接过来的“A1”拼接到一起返回给了第20行的 a。
第22行第二次调用这个函数,变参位置赋值的是切片 c,但是有解包标志“…”,也就是把 c 里的元素 2、4、6 分别解出来编程独立数字,那就与第20行调用原理一样了,所以也会正常运行。如果没有解包标志“…”,直接传递切边 c,就会报错,所以变参与切片类型的参还是不一样的,虽然函数体内操作是一样的。
值传递还是引用地址传递
在调用函数是,给通过变量参数赋值后,函数内如果修改对应的参数值,是否会影响外面赋值的实参本身呢?有人说 Go 语言的实参到形参(也就是参数赋值的过程)是值传递,永远都是值的拷贝,也有人说不尽然,有些情况也是引用了实参的地址。下面先看一下示例:
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
func abc(c []int) { // 参数是引用类型
c[0] = 99
fmt.Println("函数执行时内输出:", c)
}
func def(a int) { // 参数是基本数据类型
a = 99
fmt.Println("函数执行时内输出:", a)
}
// 主函数,程序入口
func main() {
cc := []int{2, 4, 6}
aa := 1
abc(cc)
fmt.Println("函数执行后外输出:", cc)
def(aa)
fmt.Println("函数执行后外输出:", aa)
}
上述代码编译执行结果如下:
函数执行时内输出: [99 4 6]
函数执行后外输出: [99 4 6]
函数执行时内输出: 99
函数执行后外输出: 1
示例中我们定义了两个函数,主要区别就是定义的参数类型不同,然后分别在调用时给参数赋值相应类型的变量,然后通过输出语句看看执行完函数,外面变量的值是否发生了变化。结果是第8行定义的函数改变了外面的变量值,第13行定义的函数没有修改外面的变量值。
经过几轮测试总结一下,对参数是基本数据类型的,是以值拷贝的形式传递的,也就是函数的参数变量与调用时赋值的外部变量没有任何关系,直接将它的值传递过来了;对参数是引用类型的(指针、切片、map、函数、通道等),传递过来的是引用地址,也就是地址指针的值拷贝,所以函数内修改参数变量会影响到外部变量。所以有人说 Go 语言的函数参数永远是值传递也对,只不过对不同的类型,这个值的意义不同了。对引用类型的参数,是引用地址的值,而不是变量实际保存的内容。知道区别就可以了,不去纠结到底属于那种方式的说法了。