《Go语言四十二章经》第十六章 函数

    Go 里面有三种类型的函数:

    • 普通的带有名字的函数
    • 匿名函数或者lambda函数
    • 方法(Methods)

    除了main()、init()函数外,其它所有类型的函数都可以有参数与返回值。

    函数参数、返回值以及它们的类型被统称为函数签名。

    函数重载(function overloading)指的是可以编写多个同名函数,只要它们拥有不同的形参或者不同的返回值,在 Go 里面函数重载是不被允许的。

    如果需要申明一个在外部定义的函数,你只需要给出函数名与函数签名,不需要给出函数体:

    函数也可以以申明的方式被使用,作为一个函数类型,就像:

    在这里,不需要函数体 {}。

    函数是一等值(first-class value):它们可以赋值给变量,就像下面一样:

    1. add := binOp

    这个变量知道自己指向的函数的签名,所以给它赋一个具有不同签名的函数值是不可能的。

    函数值(functions value)之间可以相互比较:如果它们引用的是相同的函数或者都是 nil 的话,则认为它们是相同的函数。函数不能在其它函数里面声明(不能嵌套),不过我们可以通过使用匿名函数来破除这个限制。

    没有参数的函数通常被称为 无参数函数(niladic function),就像 main.main()

    16.2 函数调用

    • 按值传递(call by value)
    • 按引用传递(call by reference)

    如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加&符号,比如 &variable)传递给函数,这就是按引用传递,比如 Function(&arg1),此时传递给函数的是一个指针。如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。

    在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)

    命名返回值作为结果形参(result parameters)被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的return语句。需要注意的是,即使只有一个命名返回值,也需要使用 () 括起来

    如果函数的最后一个参数是采用 …type 的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数。

    这个函数接受一个类似某个类型的 slice 的参数,该参数可以通过 for 循环结构迭代。

    1. func min(s ...int) int {
    2. if len(s)==0 {
    3. return 0
    4. }
    5. min := s[0]
    6. for _, v := range s {
    7. if v < min {
    8. }
    9. }
    10. return min
    11. }

    Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作:

    16.4 递归与回调

    使用递归函数时经常会遇到的一个重要问题就是栈溢出:一般出现在大量的递归调用导致的程序栈内存分配耗尽。这个问题可以通过一个名为懒惰求值的技术解决,在 Go 语言中,我们可以使用通道(channel)和 goroutine。

    Go 语言中也可以使用相互调用的递归函数:多个函数之间相互调用形成闭环。因为 Go 语言编译器的特殊性,这些函数的声明顺序可以是任意的。

    函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。

    当我们不希望给函数起名字的时候,可以使用匿名函数,例如:

    1. func(x, y int) int { return x + y }

    这样的一个函数不能够独立存在(编译器会返回错误:non-declaration statement outside function body),但可以被赋值于某个变量,即保存函数的地址到变量中:fplus := func(x, y int) int { return x + y },然后通过变量名对函数进行调用:fplus(3, 4)。

    1. func(x, y int) int { return x + y } (3, 4)

    下面是一个计算从 1 到 1 百万整数的总和的匿名函数:

    1. func() {
    2. sum := 0
    3. sum += i
    4. }
    5. }()

    表示参数列表的第一对括号必须紧挨着关键字 func,因为匿名函数没有名称。花括号 {} 涵盖着函数体,最后的一对括号表示对该匿名函数的调用。

    16.6 闭包函数

    匿名函数同样被称之为闭包(函数式语言的术语):它们被允许调用定义在其它环境下的变量。闭包可使得某个函数捕捉到一些外部状态,例如:函数被创建时的状态。另一种表示方式为:一个闭包继承了函数所声明时的作用域。这种状态(作用域内的变量)都被共享到闭包的环境中,因此这些变量可以在闭包中被操作,直到被销毁。闭包经常被用作包装函数:它们会预先定义好 1 个或多个参数以用于包装。另一个不错的应用就是使用闭包来完成更加简洁的错误检查。

    仅仅从形式上将闭包简单理解为匿名函数是不够的,还需要理解闭包实质上的含义。

    实质上看,闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。由闭包的实质含义,我们可以推论:闭包获取捕获变量相当于引用传递,而非值传递;对于闭包函数捕获的常量和变量,无论闭包何时何处被调用,闭包都可以使用这些常量和变量,而不用关心它们表面上的作用域。

    应用闭包:将函数作为返回值,我们用一个例子来进行验证。

    1. 程序输出:
    2. x: 5, addr of x:0xc042054080
    3. x: 6, addr of x:0xc042054080
    4. y: 6, addr of y:0xc042054098
    5. x: 7, addr of x:0xc042054080
    6. y: 7, addr of y:0xc0420540d0
    7. x: 8, addr of x:0xc042054080
    8. y: 8, addr of y:0xc0420540e8
    9. ---------------------
    10. x: 5, addr of x:0xc042054100
    11. y: 6, addr of y:0xc042054110
    12. x: 7, addr of x:0xc042054100
    13. y: 7, addr of y:0xc042054128
    14. y: 8, addr of y:0xc042054140

    首先强调一点,x是闭包中被捕获的变量,y只是闭包内部的局部变量,而非被捕获的变量。因此,对于每一次引用,x的地址都是固定的,是同一个引用变量;y的地址则是变化的。另外,闭包被引用了两次,由此产生了两个闭包实例,即addNum := addNumber(5)和addNum1 :=addNumber(5)是两个不同实例,其中引用的两个x变量也来自两个不同的实例。

    当您在分析和调试复杂的程序时,无数个函数在不同的代码文件中相互调用,如果这时候能够准确地知道哪个文件中的具体哪个函数正在执行,对于调试是十分有帮助的。您可以使用 runtime 或 log 包中的特殊函数来实现这样的功能。包 runtime 中的函数 Caller() 提供了相应的信息,因此可以在需要的时候实现一个 where() 闭包函数来打印函数执行的位置:

    1. where := func() {
    2. _, file, line, _ := runtime.Caller(1)
    3. log.Printf("%s:%d", file, line)
    4. }
    5. where()
    6. // some code
    7. where()
    8. // some more code
    9. where()

    或使用一个更加简短版本的 where 函数:

    1. var where = log.Print
    2. func func1() {
    3. where()
    4. ... some code
    5. where()
    6. ... some code
    7. where()

    16.8 高阶函数

    在定义所需功能时我们可以利用函数可以作为(其它函数的)参数的事实来使用高阶函数

    定义一个通用的 Process() 函数,它接收一个作用于每一辆 car 的 f 函数作参数: