Go 学习笔记:函数

998 阅读17分钟

简介

Go 语言中,函数是一等(first-class)类型,这意味着可以赋值给变量。

除特殊函数(如 main()init())之外,其它所有类型的函数都可以有参数与返回值。函数参数、返回值以及它们的类型被统称为函数签名。

Go 是编译型语言,函数的编写顺序无关紧要。但为了可读性,最好把一特殊函数(如 main())写在文件的前面,其他函数按照一定逻辑顺序编写(例如函数被调用的顺序)。

创建函数

语法:

func funcName(parameter-list) (result-list) {
    // body
}
  • 形参列表:描述了函数的参数名以及参数类型,这些参数是局部变量,初始化为调用者提供的值;
  • 返回值列表:描述了函数返回值的变量名以及类型,如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值。

示例:

func myFunc(part1 string, part2 string) (result string) {
    result = part1 + part2
    return result 
}

// 函数是一等类型
var myFunc1 = func(part1 string, part2 string) string {
    return part1 + part2
}  

任何一个有返回值(单个或多个)的函数都必须以 returnpanic 结尾。

参数&返回值

形参列表描述了函数的参数名以及参数类型,这些参数是局部变量,初始化为调用者提供的值

  • 形参一般都有名字,但也可以只有类型没有名字的形参的函数,如 func test(int, int) int {}
  • 函数的参数是值传递的,所以形参是函数调用者提供的实参的拷贝,见函数传参
  • 每次调用函数都必须严格按照声明顺序为所有参数提供值
  • Go 语言中没有默认参数,也不能通过声明时的形参名指定参数,即函数声明时的参数、返回值的变量名,对调用者来说没有意义

返回值列表描述了函数返回值的变量名以及类型,如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值。

  • 返回值列表的括号可以省略,如果函数没有返回值,或者返回一个没有名字的变量
  • 同参数一样,返回值也被声明成一个局部变量,并根据该返回值的类型,将其初始化
  • 如果函数有返回值,则必须以 return 语句结尾,除非函数明显无法运行到结尾处,例如函数在结尾时调用了 panic 异常或函数中存在无限循环
  • 即使函数使用了命名返回值,仍然可以无视它而返回明确的值

形参和有名返回值是函数最外层的局部变量,其中,形参被初始化为调用者提供的值:

func add(a, b int) (result int) {
    fmt.Println(a, b, result)  // 2 3 0
    result = a + b
    return result
}

a := add(2, 3)
fmt.Println(a)                 // 5

无名参数

// 没有名字的参数、返回值
func test(int, int) int {
    return 10
}
a := test(1, 1)
fmt.Println(a)    // 10

相同类型

如果一组形参或返回值有相同的类型,我们不必为每个形参都写出参数类型。下面 2 个声明是等价的:

func f(i, j, k int, s, t string)                 { /* ... */ }
func f(i int, j int, k int,  s string, t string) { /* ... */ }

可变参数

参数数量可变的函数,有时也被称为变长函数、可变参数函数。

使用 ...type 的方式定义参数,参数数量可以是 0 个或多个:

package main

import "fmt"

func sum(vals ...int) int {  // sum(vals...int) int
    total := 0
    for _, val := range vals {
        total += val
    }
    return total
}

func main() {
    fmt.Println(sum())              // 0
    fmt.Println(sum(1, 2, 3, 4))    // 10
} 

将切片类型传入可变函数中:

a := []int{1, 2, 3, 4}
total := sum(a...)
fmt.Println(total)

虽然在可变参数函数内部,...int 型参数的行为像是切片类型,但实际上,可变参数函数和以切片作为参数的函数是不同的:

func f(...int) {}
func g([]int) {}

多返回值

Go 语言中的函数可以返回多个返回值:

func exchange(a, b int) (x, y int) {
    x, y = b, a
    return
}

a := 1
b :=2
x, y := exchange(a, b)
fmt.Println(x, y)    // 2 1

通常第二个参数返回 error 类型,方便调用者处理(见后续笔记)。

命名返回值

如果返回值列表中定义了返回变量名字,那么函数需要返回时,可以直接使用 return 语句。(上面的例子中已经这么使用了)

func add(a, b int) (res int) {
    res = a + b
    return
}

fmt.Println(add(1, 2))    // 3


// 也可以无视结果声明,返回其他的值
func add(a, b int) (res int) {
    res = a + b
    return 100
}

fmt.Println(add(1, 2))    // 100


// 这样也可以
func add(a, b int) (res int) {
    // return res = a + b   // 这个是错误的写法
    return a + b
}

函数类型

声明函数类型,不需要函数体:

type MyFunc func(parameter-list) (result-list)

举例:

type MyFunc func(input1 string ,input2 string) string

func myFunc(part1 string, part2 string) (result string) {
    result = part1 + part2
    return result 
}

函数 myFunc 是函数类型 MyFunc 的一个实现,只要一个函数的参数声明列表和结果声明列表中的数据类型的顺序和名称与某一个函数类型完全一致,前者就是后者的一个实现。

内置函数

Go 语言中有一些内置函数,不需要导入就可直接使用。下面是简单的几个内置函数:

内置函数 说明
close 用于通道
new、make 用于分配内存。new(T) 分配类型 T 的零值并返回类型为 *T 的内存地址,也就是指向类型 T 的指针;make(T) 返回类型 T 的初始值,它只适用于 引用类型:切片、mapchannel
len、cap len 返回某个类型的长度或数量,cap 返回容量。见切片
copy、append 复制、添加切片
delete 从字典删除元素
panic、recover 错误处理
print、println 打印函数,但是建议使用 fmt
complex、real、imag 用于复数

递归函数

函数直接或间接调用函数本身,则称为递归函数。

举个经典的例子 - 计算斐波那契数列:

package main

import "fmt"

func main() {
    for i := 0; i <= 5; i++ {
        fmt.Println(fib(i))
    }
}

func fib(n int) (res int) {
    if n <= 1 {
        res = 1
    } else {
        res = fib(n-1) + fib(n - 2)
    }
    return
}

注意,在使用递归函数时会遇到一个重要的问题:栈溢出。大量的递归调用导致的程序栈内存分配耗尽。但可以使用 channelgoroutine 实现惰性求值,或者通过循环来实现。

举例:阶乘

// 递归
func fact(n uint) (res uint) {
    if n <= 1 {
        res = 1
    } else {
        res = n * fact(n - 1)
    }
    return
}


// 循环
func fact2(n uint) (res uint) {
    res = 1
    var j uint = 1
    for i := j; i <= n; i++ {
        res *= i
    }
    return
}

匿名函数(闭包)

函数值/类型

Go 中,函数是一等类型(first-class),也就是函数可像其他值一样拥有类型、赋值给其他变量、传递给函数、从函数返回。

func square(n int) int { 
    return n * n 
}

f := square
fmt.Println(f(3))   // "9"


// 函数类型的零值是nil。调用值为nil的函数值会引起panic错误
var f func(int) int     // 函数值 函数类型

fmt.Println(f == nil)   // true

// 但是函数值之间是不可比较的,不能用函数值作为 mapkey

闭包

函数值被称为匿名函数,也叫闭包。从形式上看,定义没有名字的函数就可以使用匿名函数。这样的函数不能够独立存在,但可以被赋值于某个变量,通过此变量来调用匿名函数,也可以直接对匿名函数进行调用:

package main

import "fmt"

func main() {
    // 赋值于变量再调用
    f := func(x, y int) int {return x + y}
    fmt.Println(f(1, 2))

    // 直接调用匿名函数
    func(x, y int) int { return x + y  }(3, 4)

    func() {
        sum := 0
        for i := 1; i <= 10; i++ {
            sum += i
        }
        fmt.Println(sum)
    }()
}

另外,举个返回匿名函数的例子:

package main

import "fmt"

func main() {
    f := squares()
    g := squares()

    fmt.Println(f())  // 1
    fmt.Println(f())  // 4
    fmt.Println(f())  // 9

    fmt.Println(g())  // 1
    fmt.Println(g())  // 4
    fmt.Println(g())  // 9
}

func squares() func() int {
    var x int
    return func() int {
        x++
        return x * x
    }
}
  • 函数 squares 返回另一个类型为 func() int 的函数,每次调用 squares 都会返回一个匿名函数
  • 每次调用匿名函数时,该函数都会先使 x 的值加 1,再返回 x 的平方
  • 第二次调用 squares 时,会生成第二个 x 变量,并返回一个新的匿名函数,新匿名函数操作的是第二个局部变量 x。

通过这个例子,我们看到变量的生命周期不由它的作用域决定:squares 返回后,变量 x 仍然隐式的存在于 f/g 中。

defer语句

defer 是在函数结束之前(或任意位置执行 return 语句之后)一刻才执行某个语句或函数,不论包含 defer 语句的函数是通过 return 正常结束,还是由于 panic 导致的异常结束。

defer 语句由关键字 defer 和一个调用表达式组成。注意,这里的调用表达式所表示的既不能是对 Go 语言内建函数的调用也不能是对 Go 语言标准库代码包 unsafe 中的那些函数的调用。

defer 语句仅能被放置在函数或方法中。

举个例子,读取文件内容的 readFile 函数:

func readFile(path string) ([]byte, error) {
    file, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer file.Close()
    return ioutil.ReadAll(file)
}

打开指定文件且未发现有错误发生之后,紧跟了一条 defer 语句:关闭打开的文件。注意,执行到这条 defer 语句时,其后面的表达式语句并不会立即被执行,而是在函数 readFile 的执行即将结束的那个时刻。也就是说,在 readFile 函数真正结束执行的前一刻,file.Close() 才会被执行。

更为关键的是,无论 readFile 函数正常地返回了结果还是由于在其执行期间有运行时恐慌发生而被剥夺了流程控制权,其中的 file.Close() 都会在该函数即将退出那一刻被执行。这就更进一步地保证了资源的及时释放。

多个defer语句

在一个函数中执行多条 defer 语句,它们的执行顺序与声明顺序相反:

func deferIt() {
    defer func() {
        fmt.Println(1)
    }()
    defer func() {
        fmt.Println(2)
    }()
    defer func() {
        fmt.Println(3)
    }()
    fmt.Println(4)
}
// 4
// 3
// 2
// 1

带参数的defer

当defer被声明时,其参数就会被实时解析

func deferIt() {
    f := func(i int) int {
        fmt.Printf("%d ",i)
        return i * 10
    }
    for i := 1; i < 5; i++ {
        defer fmt.Printf("%d ", f(i))
    }
}
// 1 2 3 4 40 30 20 10
// 先依次执行了 f(1)...f(4),再按照 defer 规则执行了 defer

defer匿名函数

关键字 defer 经常配合匿名函数使用,它可以用于改变函数的命名返回值。

func deferIt() {
    for i := 1; i < 5; i++ {
        defer func() {
            fmt.Print(i)
        }()
    }
} 

输出结果是 5555,而不是 4321,原因是 defer 语句携带的表达式语句中的那个匿名函数包含了对外部的变量的使用(确切地说,是该 defer 语句之外)。等到这个匿名函数要被执行(且会被执行4次)的时候,包含该 defer 语句的 for 语句已经执行完毕了,此时的变量 i 的值已经变为了 5。

正确的用法是,把要使用的外部变量作为参数传入到匿名函数中:

func deferIt() {
    for i := 1; i < 5; i++ {
        defer func(n int) {
            fmt.Print(n)
        }(i)
    }
}
// 4321

放在一起对比:

package main

import "fmt"

func main() {
	deferIt1()  // 0  声明defer时,参数实时解析
	deferIt2()  // 2  匿名函数执行时,i已经被改成 100 了
}

func deferIt1() {
	i := 0
	defer fmt.Println(i)
	i = 2
}

func deferIt2() {
	i := 0
	defer func() {
		fmt.Println(i)
	}()
	i = 2
}

被延迟执行的匿名函数甚至可以修改函数返回给调用者的返回值

func double(x int) (result int) {
    result = x + x
    defer func() {
        result += 1
    }()
    return
}

循环体中的defer

上面已经看到了,在循环体中的 defer 语句需要特别注意,因为只有在函数执行完毕后,这些被延迟的函数才会执行。

// 下面的代码会导致系统的文件描述符耗尽,因为在所有文件都被处理之前,没有文件会被关闭。
for _, filename := range filenames {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close() // NOTE: risky; could run out of file descriptors
    // ...process f…
}


// 改进方法一:将循环体中的defer语句移至另外一个函数,在每次循环时,调用这个函数。

for _, filename := range filenames {
    if err := doFile(filename); err != nil {
        return err
    }
}
func doFile(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close()
    // ...process f…
}

应用场景

关闭文件

// open a file  
defer file.Close()

解锁资源

mu.Lock()  
defer mu.Unlock()

打印报告

printHeader()  
defer printFooter()

关闭链接

// open a database connection  
defer disconnectFromDB()

代码追踪

在进入和离开某个函数打印相关的消息

package main

import "fmt"

func trace(s string)   { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

func a() {
    trace("a")
    defer untrace("a")
    fmt.Println("in a")
}

func b() {
    trace("b")
    defer untrace("b")
    fmt.Println("in b")
    a()
}

func main() {
    b()
}


// 简写

package main

import "fmt"

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

记录参数与返回值

package main

import (
    "io"
    "log"
)

func func1(s string) (n int, err error) {
    defer func() {
        log.Printf("func1(%q) = %d, %v", s, n, err)
    }()
    return 7, io.EOF
}

func main() {
    func1("Go")
}

例2,doulbe 函数过于简单,对于有许多 return 语句的函数而言这个技巧很有用:

func double(x int) (result int) {
    defer func() { fmt.Printf("double(%d) = %d\n", x,result) }()
    return x + x
}

函数传参!

先说结论:Go 中的函数传参只有传递值这一种方式

值传递

值传递是指在调用函数时将实际参数复制一份传递到函数中,也就是形参是实参的副本。这样在函数中如果对参数进行修改,将不会影响到实际参数。

举例,传递整数和数组:

func main() {
    i := 10
    arr := [2]int{1, 2}
    fmt.Printf("before  calling: i=%p, arr=%p\n", &i, &arr)
    foo(i, arr)
    fmt.Printf("after   calling: i=%p, arr=%p\n", &i, &arr)
}

func foo(i int, arr [2]int) {
    fmt.Printf("in foo function: i=%p, arr=%p\n", &i, &arr)
}

// before  calling: i=0xc420018100, arr=0xc420018110
// in foo function: i=0xc420018108, arr=0xc420018120
// after   calling: i=0xc420018100, arr=0xc420018110

实参 iarr 在传递给函数 foo 的形参 iarr 后,在 foo 的内部拷贝实参的值,但内存地址完全不同。这说明,Go 中对于整型和数组类型的参数是值传递的。即便在 foo 中修改 iarr 也不会影响 main 中的 iarr

指针传递

当传递指针时,举个例子:

func main() {
    i := 1
    fmt.Printf("before  calling: i=%v, &i=%p\n", i, &i)
    foo(&i)
    fmt.Printf("after   calling: i=%v, &i=%p\n", i, &i)
}

func foo(ptr *int) {
    fmt.Printf("in foo function: ptr=%v, &ptr=%p\n", ptr, &ptr)
    *ptr = 2
}
// before  calling: i=1, &i=0xc420018100
// in foo function: ptr=0xc420018100, &ptr=0xc42000c030
// after   calling: i=2, &i=0xc420018100

当传递 i 的指针时,实际上是把指针的内容传递给 foo&iptr 相同。形参 ptr 和实参 &i 的地址不同,但 ptr的值是变量 i 的内存地址,相当于通过指针修改了 i 的值。

可以看出,传递指针仍然是值传递

当传递占用内存较大的数据结构时,传递指针的效率会很高,指针只占几个字节而已。

“引用传递”

引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

Go 中并没有引用传递,那些常说的引用传递是指传递 SliceMapChannel 这几种引用类型。

举例:

func main() {
    arr := [3]int{1, 2, 3}
    s := arr[:]

    fmt.Printf("before: arr=%v, &arr=%p, &arr[0]=%p, s=%v, &s=%p\n", arr, &arr, &arr[0], s, &s)
    foo(s)
    fmt.Printf("after : arr=%v, &arr=%p, &arr[0]=%p, s=%v, &s=%p\n", arr, &arr, &arr[0], s, &s)
}

func foo(s []int) {
    fmt.Printf("in foo:                                    s[0]=%p, s=%v, &s=%p\n", &s[0], s, &s)
    s[0] = 5
}   
// before: arr=[1 2 3], &arr=0xc420012400, &arr[0]=0xc420012400, s=[1 2 3], &s=0xc42000a080
// in foo:                                    s[0]=0xc420012400, s=[1 2 3], &s=0xc42000a0c0
// after : arr=[5 2 3], &arr=0xc420012400, &arr[0]=0xc420012400, s=[5 2 3], &s=0xc42000a080

函数 foo 的形参 s 和实际的切片 s 的地址不同,传递的仍然只是 s 的值。但切片是引用类型,传递进函数的切片 s 仍然引用原底层数组(arr[0]s[0] 的地址相同)。所以在函数内部修改 s 可能会影响到底层数组 arr,进而改变其切片。

所以,传递引用类型时仍然是值传递。

结论

Go 语言中的函数传参使用的是值传递,接收方收到参数时会是调用方的参数的副本。理解这点之后,当传递内存较大的数据结构时,传递指针可以避免大量的数据拷贝,进而提升性能。

函数作参数-回调

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

func main() {
    callback(1, Add)
}

func Add(a, b int) {
    fmt.Println(a + b)
}

func callback(y int, f func(int, int)) {
    f(y, 1)
}

在闭包中,有函数作返回值的例子

注意事项

函数重载

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

Go 语言不支持这项特性的主要原因是函数重载需要进行多余的类型匹配影响性能,没有重载意味着只是一个简单的函数调度。

niladic 函数

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

作用域陷阱:捕获迭代变量

这里介绍 Go 作用域的一个陷阱,即使是经验丰富的程序员也会在这个问题上犯错误。

有这么一个情景:创建一些目录,再将这些目录删除。下面的代码需要引入 os 包,为了简单,忽略所有的异常处理:

var rmdirs []func()
for _, d := range tempDirs() {
    dir := d                  // NOTE: necessary!!!
    os.MkdirAll(dir, 0755)    // creates parent directories too
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dir)
    })
}

// ...do some work...
for _, rmdir := range rmdirs {
    rmdir()    // clean up
}

为了更直观一点,我们用来模拟上述行为:

package main

import "fmt"

func main() {
    printVars := []func(){}
    vars := []string{"a", "b", "c"}

    for _, v := range vars {
        printVars = append(printVars, func() {
                fmt.Println(v)
            })
    }

    for _, printVar := range printVars {
        printVar()
    }
}
// c
// c
// c

定义一个元素为函数的切片,分别来打印 vars 中的元素。我们期待的结果是 a b c,实际上的输出却是 c c c。这是为什么呢?

原因在于循环变量的作用域for 循环中,局部变量 v 在这里被声明。在该循环中生成的所有函数都循环变量的内存地址,而不是循环变量某一时刻的值。虽然后续的迭代不断更新 v 的值,但当 printVar 执行时,for 循环已完成,v 中存储的值等于最后一次迭代的值。所以,每次的输出都是 c

为了解决这个问题,引入一个与循环变量同名的局部变量,作为循环变量的副本。虽然这看起来很奇怪,但却很有用:

// ...
    for _, v := range vars {
        v := v
        printVars = append(printVars, func() {
                fmt.Println(v)
            })
    }
// ...

回过头来再看下 defer 语句中例子,也有类似的问题。

参考目录