Go语言入门[四] 函数| 青训营

56 阅读4分钟

golang.jpeg

函数的定义和使用

  • 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在被压入栈后,栈中的信息不只有其标记的语句,还有对应整个程序的状态。defera的值为10,那么在最后出栈时输出的结果它就是10,即使我们在其后改变了a的状态。

闭包

  • 闭包本质上就是一个匿名函数。匿名函数就是一个内联的语句或者表达式,其优越性在于可以直接使用函数内的变量而无需额外声明。笔者由于有过kotlin的学习经验,对闭包的感触很深。在kotlin的日常开发中经常会使用大量的lambda表达式来简化代码的书写,同时,lambda表达式的使用也使得代码看起来更简洁美观。lambda表达式实际上就是一个匿名函数。kotlingo都支持函数式编程,这属于更高级的内容,我们将在其后的内容中讲到。

  • 还记得我在前面说过在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只保存了它的内存地址,而没有选择保存变量的值。