函数的定义和使用
Go语言的函数和其它语言相似,但在Go语言中,一切事物往往都被视作变量。函数也是变量的一种。一般的函数定义形式如下所示
func function_name( [parameter list] ) [return_types] {
body
}
-
func:函数声明的关键字
-
function_name:函数名
-
parameter list]:参数列表
-
return_types:函数的返回值列表,注意,在
Go语言中,函数的返回值不是唯一的 -
body:函数体,在函数内运行的代码块
-
以下的代码演示了一个求最大值函数的定义和使用
package main
import "fmt"
func main() {
a,b := 5,10
bigger,smaller := Max(a,b)
fmt.Println("bigger number:",bigger)
fmt.Println("smaller number:",smaller)
}
func Max( x int,y int ) (int,int) {
if x >= y {
return x,y
}else{
return y,x
}
}
- 运行结果如下所示
$ go run hello.go
bigger number: 10
smaller number: 5
- 我们也可以定义返回值变量,这些变量的作用域是整个函数,在return的时候会直接返回
package main
import "fmt"
func main() {
a,b := 5,10
bigger,smaller := Max(a,b)
fmt.Println("bigger number:",bigger)
fmt.Println("smaller number:",smaller)
}
func Max( x int,y int ) (bigger int,smaller int) {
if x >= y {
bigger = x
smaller = y
return
}else{
return y,x
}
}
- 代码的运行结果与原来一致
defer
Go语言使用defer作为延迟语句的标志,在函数即将返回时(函数内的语句执行完毕)执行与defer关键字相关的语句。一般defer都与释放资源有关(如释放进程锁)
package main
import "fmt"
func main() {
defer fmt.Println("第一个defer的语句")
defer fmt.Println("第二个defer的语句")
defer fmt.Println("第三个defer的语句")
fmt.Println("Hello Codey!")
}
- 代码的运行结果如下所示
$ go run hello.go
Hello Codey!
第三个defer的语句
第二个defer的语句
第一个defer的语句
-
可以看到,在执行代码时,
defer相关的语句被编译器略过,直接先执行除了defer之外的所有语句,最后再依次倒序执行defer相关的语句。这些语句在执行时像是分为了一个队列和一个栈,没有被defer标记的语句放入程序队列中,先扫到先执行,而被标记的语句则压入栈中,执行完程序队列的所有代码后,依次对defer栈的代码进行出栈操作,直到栈空整个程序执行完毕。 -
笔者认为在以上的过程中,
defer栈不仅保存了各个代码行,还保存了在该代码行执行前整个程序的状态,我们可以进行如下的实验
package main
import "fmt"
func main() {
a := 10
defer fmt.Println("defer时a的值为", a)
a = 100
fmt.Println("print时a的值为", a)
}
- 在这里,我们在
defer语句后又对变量的值进行了更改,如果defer真的是如之前所说,会在所有的语句执行完毕后执行,那么最后两个输出语句中a的值就会相同,果真如此吗?
$ go run hello.go
print时a的值为 100
defer时a的值为 10
- 这个结果出乎我们的意料,事实证明,我的猜想是正确的。
defer在被压入栈后,栈中的信息不只有其标记的语句,还有对应整个程序的状态。defer前a的值为10,那么在最后出栈时输出的结果它就是10,即使我们在其后改变了a的状态。
闭包
-
闭包本质上就是一个匿名函数。匿名函数就是一个内联的语句或者表达式,其优越性在于可以直接使用函数内的变量而无需额外声明。笔者由于有过
kotlin的学习经验,对闭包的感触很深。在kotlin的日常开发中经常会使用大量的lambda表达式来简化代码的书写,同时,lambda表达式的使用也使得代码看起来更简洁美观。lambda表达式实际上就是一个匿名函数。kotlin和go都支持函数式编程,这属于更高级的内容,我们将在其后的内容中讲到。 -
还记得我在前面说过在
Go语言中,函数也是变量的一种吗?在这部分内容,你将会对这一思想拥有更深刻的认识
package main
import "fmt"
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}
func main(){
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()
/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber())
fmt.Println(nextNumber())
fmt.Println(nextNumber())
/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println(nextNumber1())
fmt.Println(nextNumber1())
}
- 这里出现了两种闭包的使用形式,接下来我会逐一介绍。先看里层函数的
return部分
return func() int {
i+=1
return i
}
- 外层函数的返回值要求是 int类型的函数(非常重要!!!!),它等价于
ret := func() int {
i+=1
return i
}
return ret
- 这里的
ret接收的是func() int这个函数体,而不是返回的i的值。而最外层的匿名函数定义方式十分有意思,它和我们一般的函数定义形式很像
//闭包
func getSequence() func() int {
...
}
//一般的函数定义
func getSequence() int {
...
}
-
即使二者形式类似,他们在调用时的返回值却有着相当大的不同。第一个函数的返回值是一个以
int作为返回值的函数,第二个函数的返回值则是一个int值。在main函数中可以看到,我们用一个变量nextNumber接收了getSequence()的值,即一个匿名函数。我们如果需要调用这个匿名函数,我们还需要以nextNumber()的形式调用,在我看来更像是给这个匿名函数起了个名字。 -
但很奇怪的是,为什么输出的数字能够形成一个连续的序列,而我们给它换了个名字之后,
i的计数又重新开始了呢? -
仔细想想,唯一合理的解释是,函数里的
i在返回后它的生命周期并没有结束,而是以一种特殊的形式保存了下来。而不同函数里的i,它们不是同一个变量,或者说,它们的内存地址完全不同,自然两个i会输出不同的结果。 -
为了验证我们的猜想,我们可以看看它们的地址输出结果
func getSequence() func() int {
i:=0
fmt.Println("(outside) addr:",&i)//输出相应的地址
return func() int {
i+=1
fmt.Println("(inside) addr:",&i)//输出相应的地址
return i
}
}
- 运行结果
$ go run hello.go
(outside) addr: 0xc00000c0b8
(inside) addr: 0xc00000c0b8
1
(inside) addr: 0xc00000c0b8
2
(inside) addr: 0xc00000c0b8
3
(outside) addr: 0xc00000c0f0
(inside) addr: 0xc00000c0f0
1
(inside) addr: 0xc00000c0f0
2
-
结果验证了我们的猜想,我们用不同的变量接收最外层的匿名函数后,
i的地址果然不同,也就是说,它们在那一刻已经分道扬镳,是不同的函数了。但新的问题出现了:为什么在外层的函数只执行了一次地址的输出语句,内层函数却每次都执行呢? -
答案其实也很简单。我们每一次执行
getSequence(),outside都会被执行一遍,但它的返回值却是inside所在的匿名函数,当我们调用nextNumber()时,其实就是调用作为返回值的那个函数,也就是inside所在的匿名函数,外层的outside肯定不会执行,同时,因为变量保存了整个函数的状态,所以每次调用的输出都会延续上一个i的值,在上一个i值的基础上进行递增。 -
好了,现在两个问题都已经被解决了(没解决再自己试试吧少年)。还记得我们前面学过的关键字
defer吗?如果我们用它来标记一个匿名函数,将会爆发出怎样的火花呢?
package main
import "fmt"
func main(){
str := "Hello World!"
defer func() {
fmt.Println("defer str:",str)
}()
str = "Hello Sandy!"
fmt.Println("main str:",str)
}
- 运行结果
$ go run hello.go
main str: Hello Sandy!
defer str: Hello Sandy!
- 噢天哪,这似乎和我们认知的不太一样。我们之前说
defer栈保存了在其执行时整个程序的状态,也说匿名函数可以自由使用其外的变量,但为什么这里的defer没有保存Hello World,反而被其后的Hello Sandy更改了呢?实际上,在使用闭包外的变量时,闭包内的语句实际上是通过引用的方式获取了外部的值。什么是引用?学过C++/C的同学一定对这个词十分熟悉,简单点来说,就是通过不同的名称,访问相同地址上的值。不管是“儿子”/"女儿",还是你的本名,当你父母呼唤的时候,他们指的一定是同一个人——你,对吧?这里的引用就是类似的形式。当外部程序队列执行时,str指向的地址上的值已经被改变,defer栈在执行闭包内的代码时,由于闭包的变量是通过引用的方式取值,只能拿到对应地址上最后一次修改的值,所以我们最后的输出结果是Hello Sandy而不是Hello World。你也可以这么理解,在这里,defer栈只保存了它的内存地址,而没有选择保存变量的值。