Golang 学习之路六:函数

Golang学习:函数

一、函数定义

  不支持嵌套(nested)、重载(overload)和默认参数(default parameter)。

  • 无需声明类型
  • 支持不定长变参
  • 支持多返回值
  • 支持命名返回参数
  • 支持匿名函数和闭包
    使用关键字func定义函数,左大括号需要在行末,不能另起一行。
unc test(x, y int, s string) (int, string) { // 类型相同的相邻参数可合并。
n := x + y // 多返回值必须用括号。
return n, fmt.Sprintf(s, n)
}
  • 函数是第一类对象(first class),可以作为参数传递,建议将复杂签名定义为函数类型,以便于阅读。
func test(fn func() int) int {
    return fn()
}
type FormatFunc func(s string, x, y int) string // 定义函数类型。
func format(fn FormatFunc, s string, x, y int) string {
    return fn(s, x, y)
}
func main() {
    s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。
    s2 := format(func(s string, x, y int) string {
    return fmt.Sprintf(s, x, y)
    }, "%d, %d", 10, 20)

    println(s1, s2)
}

  有返回值的函数,必须有明确的终止语句,否则会引发编译错误。

二、变参

  变参本质上就是slice。只能有一个,且必须是最后一个。

func test(s string, n ...int) string {
    var x int
    for _, i := range n {
        x += i
    }
    return fmt.Sprintf(s, x)
}
func main() {
    println(test("sum: %d", 1, 2, 3))
}

  使用slice做变参时,必须展开。

func main(){
    s := []int{1,2,3}
    println(test("sum: %d",s...))
}

结果输出:

sum: 6

三、返回值

  不能用容器对象接受多返回值。只能用多个变量,或用_忽略。

func test() (int, int) {
    return 1, 2
}
func main() {
    // s := make([]int, 2)
    // s = test() // Error: multiple-value test() in single-value context
    x, _ := test()
    println(x)
}

  多返回值可以直接作为其他函数调用的实参。

func test() (int, int) {
    return 1, 2
}

func add(x, y int) int {
    return x + y
}

func sum(n ...int) int {
    var x int
    for _, i := range n {
        x += i
    }
    return x
}
func main() {
    println(add(test()))
    println(sum(test()))
}
  • 命名返回参数可看做与形参类似的局部变量,最后由return隐式返回。
func add(x, y int) (z int) {
    z = x + y
    return
}
  • 命名返回参数可以被同名局部变量遮蔽,此时需要显示返回。
func add(x, y int) (z int) {
    { // 不能在一个级别,引发 "z redeclared in this block" 错误。
        var z = x + y
        // return // Error: z is shadowed during return
        return z // 必须显式返回。
    }
}
  • 命名返回参数允许defer延迟调用通过闭包读取和修改。
func add(x, y int) (z int) {
    defer func() {
        z += 100
    }()
    z = x + y
    return
}
func main() {
    println(add(1, 2)) // 输出: 103
}
  • 显示return返回前,会先修改命名返回参数。
func add(x, y int) (z int) {
    defer func() {
        println(z) // 输出: 203
    }()
    z = x + y
    return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (ret)
}
func main() {
    println(add(1, 2)) // 输出: 203
}

再次说明其顺序为: (z = z + 200) -> (call defer) -> (ret)

四、匿名函数

  匿名函数可赋值给变量,作为结构字段,或者在channel里传送。

// --- function variable ---
fn := func() { println("Hello, World!") }
fn()
// --- function collection ---
fns := [](func(x int) int){
    func(x int) int { return x + 1 },
    func(x int) int { return x + 2 },
}
println(fns[0](100))
// --- function as field ---
d := struct {
    fn func() string
}{
    fn: func() string { return "Hello, World!" },
}
println(d.fn())
// --- channel of function ---
fc := make(chan func() string, 2)
fc <- func() string { return "Hello, World!" }
println((<-fc)()
  • 闭包赋值的是原对象指针,这就解释了延迟引用的现象。
package main

import "fmt"

func test() func() {
    x := 100
    fmt.Printf("x (%p) = %d\n", &x, x)
    return func() {
        fmt.Printf("x (%p) = %d\n", &x, x)
    }
}
func main() {
    f := test()
    f()
}

输出结果:

x (0x115720cc) = 100
x (0x115720cc) = 100

  在汇编层面,test实际返回的是FuncVal对象,其中包含了匿名函数地址、闭包对象指针。当调用匿名函数时,只需以某个寄存器传递该对象即可。

五、延迟调用

  • 关键字defer用于注册延迟调用。这些调用直到ret前才被执行,通常用于释放资源或错误处理。
func test() error {
    f, err := os.Create("test.txt")
    if err != nil { return err }
    defer f.Close() // 注册调用,而不是注册函数。必须提供参数,哪怕为空。
    f.WriteString("Hello, World!")
    return nil
}
  • 多个defer注册,按FILO次序执行。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
package main

func test(x int) {
    defer println("a")
    defer println("b")
    defer func() {
        println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
    }()
    defer println("c")
}
func main() {
    test(0)
}

输出结果:

c
b
a
panic: runtime error: integer divide by zero

注意: 在异常处并未停止,依然向外传递调用

  • 延迟调用参数在注册时求值或复制,可用指针或闭包“延迟”读取。
package main

func test() {
    x, y := 10, 20
    defer func(i int) {
        println("defer:", i, y) // y 闭包引用
    }(x) // x 被复制
    x += 10
    y += 100
    println("x =", x, "y =", y)
}
func main() {
    test()
}

输出结果:

x = 20 y = 120
defer: 10 120

注意:x 被复制,指的是在push的时候复制了x值:10

  • 滥用defer可能会导致性能问题,尤其是在一个“大循环”里。
var lock sync.Mutex

func test() {
    lock.Lock()
    lock.Unlock()
}
func testdefer() {
    lock.Lock()
    defer lock.Unlock()
}
func BenchmarkTest(b *testing.B) {
    for i := 0; i < b.N; i++ {
        test()
    }
}
func BenchmarkTestDefer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        testdefer()
    }
}

输出结果:

BenchmarkTest" 50000000 43 ns/op
BenchmarkTestDefer 20000000 128 ns/op

六、错误处理

  • 没有结构化异常,使用panic抛出错误,recover捕获错误。
func test(){
    defer func() {
        if err := recover(); err != nil {
        println(err.(string)) // 将 interface{} 转型为具体类型。
        }
    }()
    panic("panic error!")
}

  由于panic、recover参数类型为interface{},因此可抛出任何类型对象

func panic(v interface{})
func recover() interface{}
  • 延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。
package main

import "fmt"

func test() {
    defer func() {
        fmt.Println(recover())
    }()
    defer func() {
        panic("defer panic")
    }()
    panic("test panic")
}
func main() {
    test()
}

输出结果:

defer panic
  • 捕获函数recover只有在延迟调用才会终止错误,否则总会返回nil。任何未被捕获的错误都会沿调用堆栈向外传递。
package main

import "fmt"

func test() {
    defer recover()              // 无效!
    defer fmt.Println(recover()) // 无效!
    defer func() {
        func() {
            println("defer inner")
            recover() // ⽆效!
        }()
    }()
    panic("test panic")
}
func main() {
    test()
}

输出结果:

defer inner
<nil>
panic: test panic
  • 使用延迟匿名函数或下面这样都是有效的。
func except() {
    recover()
}
func test() {
    defer except()
    panic("test panic")
}
  • 如果需要保护代码片段,可将代码重构成匿名函数,如此可确保后续代码被执行。
func test(x, y int) {
    var z int
    func() {
        defer func() {
            if recover() != nil { z = 0 }
        }()
        z = x / y
        return
    }()
    println("x / y =", z)
}
  • 除了使用panic引发中断性错误外,还可以返回error类型错误来表示函数调用状态。
type error interface {
    Error() string
}
  • 标准库error.New和fmt.Errorf函数用于创建实现error接口的错误对象。通过判断错误对象的实例来确定具体的错误类型。
var ErrDivByZero = errors.New("division by zero")

func div(x, y int) (int, error) {
    if y == 0 {
        return 0, ErrDivByZero
    }
    return x / y, nil
}
func main() {
    switch z, err := div(10, 0); err {
    case nil:
        println(z)
    case ErrDivByZero:
        panic(err)
    }
}
  • 导致关键流程出现不可修复性错误的使用 panic,其他使用 error。

七、概念补充

1. Panic和Recover

  Go没有像Java那样的异常机制,它不能抛出异常,而是使用了panicrecover机制。一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有panic的东西。这是个强大的工具,请明智地使用它。那么,我们应该如何使用它呢?

Panic

是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函数F调用panic,函数F的执行被中断,但是F中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,F的行为就像调用了panic。这一过程继续向上,直到发生panicgoroutine中所有调用的函数返回,此时程序退出。恐慌可以直接调用panic产生。也可以由运行时错误产生,例如访问越界的数组。

Recover

是一个内建的函数,可以让进入令人恐慌的流程中的goroutine恢复过来。recover仅在延迟函数中有效。在正常的执行过程中,调用recover会返回nil,并且没有其它任何效果。如果当前的goroutine陷入恐慌,调用recover可以捕获到panic的输入值,并且恢复正常的执行。

2. main函数和init函数

  Go里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。虽然一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package中每个文件只写一个init函数。

Go程序会自动调用init()main(),所以你不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数。

程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。

3. 匿名函数

  只要一个函数的参数声明列表和结果声明列表中的数据类型的顺序和名称与某一个函数类型完全一致,前者就是后者的一个实现。请大家回顾上面的示例并深刻理解这句话。

 var splice func(string, string) string // 等价于 var splice MyFunc
  • 然后把函数myFunc赋给它:
splice = myFunc

  如此一来,我们就可以在这个变量之上实施调用动作了:

splice("1", "2")

  实际上,这是一个调用表达式。它由代表函数的标识符(这里是splice)以及代表调用动作的、由圆括号包裹的参数值列表组成。
  如果你觉得上面对splice变量声明和赋值有些啰嗦,那么可以这样来简化它:

var splice = func(part1 string, part2 string) string {
    return part1 + part2
}  

  在这个示例中,我们直接使用了一个匿名函数来初始化splice变量。顾名思义,匿名函数就是不带名称的函数值。匿名函数直接由函数类型字面量和由花括号包裹的语句列表组成。注意,这里的函数类型字面量中的参数名称是不能被忽略的。
  其实,我们还可以进一步简化——索性省去splice变量。既然我们可以在代表函数的变量上实施调用表达式,那么在匿名函数上肯定也是可行的。因为它们的本质是相同的。后者的示例如下:

var result = func(part1 string, part2 string) string {
    return part1 + part2
}("1", "2")

  可以看到,在这个匿名函数之后的即是代表调用动作的参数值列表。注意,这里的result变量的类型不是函数类型,而与后面的匿名函数的结果类型是相同的。

八、总结

  可以看到,Go在函数方面较C语言有很多有趣也很用的特性。例如多返回值,延迟函数等。灵活熟练使用这些特性,在开发过程中会带来很多便利。

九、参考资料

  1. Go 学习笔记(雨痕)
  2. Go Web 编程
  3. Go 语言第一课
  4. Go 官网教程 Tour

你可能感兴趣的:(Golang,学习之路)