Go中函数特性简介
对Go中的函数特性做一个总结。懂则看,不懂则算。
- Go中有3种函数:普通函数、匿名函数(没有名称的函数)、方法(定义在struct上的函数)。
- Go编译时不在乎函数的定义位置,但建议init()定义在最前面(如果有的话),main函数定义在init()之后,然后再根据函数名的字母顺序或者根据调用顺序放置各函数的位置。
- 函数的参数、返回值以及它们的类型,结合起来成为函数的签名(signature)。
- 函数调用的时候,如果有参数传递给函数,则先拷贝参数的副本,再将副本传递给函数。
- 由于引用类型(slice、map、interface、channel)自身就是指针,所以这些类型的值拷贝给函数参数,函数内部的参数仍然指向它们的底层数据结构。
- 由于引用类型(slice、map、interface、channel)自身就是指针,所以这些类型的值拷贝给函数参数,函数内部的参数仍然指向它们的底层数据结构。
- 函数参数可以没有名称,例如
func myfunc(int,int)
。
- Go中的函数可以作为一种type类型,例如
type myfunc func(int,int) int
。- 实际上,在Go中,函数本身就是一种类型,它的signature就是所谓的type,例如
func(int,int) int
。所以,当函数ab()赋值给一个变量ref_ab
时ref_ab := ab
,不能再将其它函数类型的函数cd()赋值给变量ref_ab
。
- 实际上,在Go中,函数本身就是一种类型,它的signature就是所谓的type,例如
- Go中作用域是词法作用域,意味着函数的定义位置决定了它能看见的变量。
- Go中不允许函数重载(overload),也就是说不允许函数同名。
- Go中的函数不能嵌套函数,但可以嵌套匿名函数。
- Go实现了一级函数(first-class functions),Go中的函数是高阶函数(high-order functions)。这意味着:
- 函数是一个值,可以将函数赋值给变量,使得这个变量也成为函数
- 函数可以作为参数传递给另一个函数
- 函数的返回值可以是一个函数
- 这些特性使得函数变得无比的灵活,例如回调函数、闭包等等功能都依赖于这些特性。
- 函数是一个值,可以将函数赋值给变量,使得这个变量也成为函数
- Go中的函数不支持泛型(目前不支持),但如果需要泛型的情况,大多数时候都可以通过接口、type switch、reflection的方式来解决。但使用这些技术使得代码变得更复杂,性能更低。
参数和返回值
函数可以有0或多个参数,0或多个返回值,参数和返回值都需要指定数据类型,返回值通过return关键字来指定。
return可以有参数,也可以没有参数,这些返回值可以有名称,也可以没有名称。Go中的函数可以有多个返回值。
- (1).当返回值有多个时,这些返回值必须使用括号包围,逗号分隔
- (2).return关键字中指定了参数时,返回值可以不用名称。如果return省略参数,则返回值部分必须带名称
- (3).当返回值有名称时,必须使用括号包围,逗号分隔,即使只有一个返回值
- (4).但即使返回值命名了,return中也可以强制指定其它返回值的名称,也就是说return的优先级更高
- (5).命名的返回值是预先声明好的,在函数内部可以直接使用,无需再次声明。命名返回值的名称不能和函数参数名称相同,否则报错提示变量重复定义
- (6).return中可以有表达式,但不能出现赋值表达式,这和其它语言可能有所不同。例如
return a+b
是正确的,但return c=a+b
是错误的
例如:
// 单个返回值
func func_a() int{
return a
}
// 只要命名了返回值,必须括号包围
func func_b() (a int){
// 变量a int已存在,无需再次声明
a = 10
return
// 等价于:return a
}
// 多个返回值,且在return中指定返回的内容
func func_c() (int,int){
return a,b
}
// 多个返回值
func func_d() (a,b int){
return
// 等价于:return a,b
}
// return覆盖命名返回值
func func_e() (a,b int){
return x,y
}
Go中经常会使用其中一个返回值作为函数是否执行成功、是否有错误信息的判断条件。例如return value,exists
、return value,ok
、return value,err
等。
当函数的返回值过多时,例如有4个以上的返回值,应该将这些返回值收集到容器中,然后以返回容器的方式去返回。例如,同类型的返回值可以放进slice中,不同类型的返回值可以放进map中。
但函数有多个返回值时,如果其中某个或某几个返回值不想使用,可以通过下划线_
这个blank identifier来丢弃这些返回值。例如下面的func_a
函数两个返回值,调用该函数时,丢弃了第二个返回值b,只保留了第一个返回值a赋值给了变量a
。
func func_a() (a,b int){
return
}
func main() {
a,_ := func_a()
}
按值传参
Go中是通过传值的方式传参的,意味着传递给函数的是拷贝后的副本,所以函数内部访问、修改的也是这个副本。
例如:
a,b := 10,20
min(a,b)
func min(x,y int) int{}
上面调用min()时,是将a和b的值拷贝一份,然后将拷贝的副本赋值给变量x,y的,所以min()函数内部,访问、修改的一直是a、b的副本,和原始的数据对象a、b没有任何关系。
如果想要修改外部数据(即上面的a、b),需要传递指针。
例如,下面两个函数,func_value()
是传值函数,func_ptr()
是传指针函数,它们都修改同一个变量的值。
package main
import "fmt"
func main() {
a := 10
func_value(a)
fmt.Println(a) // 输出的值仍然是10
b := &a
func_ptr(b)
fmt.Println(*b) // 输出修改后的值:11
}
func func_value(x int) int{
x = x + 1
return x
}
func func_ptr(x *int) int{
*x = *x + 1
return *x
}
map、slice、interface、channel这些数据类型本身就是指针类型的,所以就算是拷贝传值也是拷贝的指针,拷贝后的参数仍然指向底层数据结构,所以修改它们可能会影响外部数据结构的值。
另外注意,赋值操作b = a+1
这种类型的赋值也是拷贝赋值。换句话说,现在底层已经有两个数据对象,一个是a,一个是b。但a = a+1
这种类型的赋值虽然本质上是拷贝赋值,但因为a的指针指向特性,使得结果上看是原地修改数据对象而非生成新数据对象。
变长参数"..."(variadic)
有时候参数过多,或者想要让函数处理任意多个的参数,可以在函数定义语句的参数部分使用ARGS...TYPE
的方式。这时会将...
代表的参数全部保存到一个名为ARGS的slice中,注意这些参数的数据类型都是TYPE。
...
在Go中称为variadic,在使用...
的时候(如传递、赋值),可以将它看作是一个slice,下面的几个例子可以说明它的用法。
例如:func myfunc(a,b int,args...int) int {}
。除了前两个参数a和b外,其它的参数全都保存到名