1. 程式人生 > >Go基礎系列:函式(2)——回撥函式和閉包

Go基礎系列:函式(2)——回撥函式和閉包

回撥函式和閉包

當函式具備以下兩種特性的時候,就可以稱之為高階函式(high order functions):

  1. 函式可以作為另一個函式的引數(典型用法是回撥函式)
  2. 函式可以返回另一個函式,即讓另一個函式作為這個函式的返回值(典型用法是閉包)

一般來說,附帶的還具備一個特性:函式可以作為一個值賦值給變數。

f := func(){...}
f()

由於Go中函式不能巢狀命名函式,所以函式返回函式的時候,只能返回匿名函式。

先簡單介紹下高階函式,然後介紹閉包。

高階函式示例

例如,將函式作為另一個函式的引數:

package main

import "fmt"

func added(msg string, a func(a, b int) int) {
    fmt.Println(msg, ":", a(33, 44))
}

func main() {
    // 函式內部不能巢狀命名函式
    // 所以main()中只能定義匿名函式
    f := func(a, b int) int {
        return a + b
    }
    added("a+b", f)
}

以下示例是函式返回另一個函式:

package main

import "fmt"

func added() func(a, b int) int {
    f := func(a, b int) int {
        return a + b
    }
    return f
}

func main() {
    m := added()
    fmt.Println(m(33, 44))
}

回撥函式(sort.SliceStable)

將函式B作為另一個函式A的引數,可以使得函式A的通用性更強,可以隨意定義函式B,只要滿足規則,函式A都可以去處理,這比較適合於回撥函式。

在Go的sort包中有一個很強大的Slice排序工具SliceStable(),它就是將排序函式作為引數的:

func SliceStable(slice interface{}, less func(i, j int) bool)

這個函式是什麼意思呢?給定一個名為slice的Slice結構,使用名為less的函式去對這個slice排序。這個less函式的結構為less func(i, j int) bool,其中i和j指定排序依據。Go中已經內建好了排序的演算法,我們無需自己去定義排序演算法,Go會自動從Slice中每次取兩個i和j索引對應的元素,然後去回撥排序函式less。所以我們只需要傳遞升序還是降序、根據什麼排序就可以。

例如:

package main

import (
    "fmt"
    "sort"
)

func main() {
    s1 := []int{112, 22, 52, 32, 12}
    // 定義排序函式
    less := func(i, j int) bool {
        // 降序排序
        return s1[i] > s1[j]
        // 升序排序:s1[i] < s1[j]
    }
    //
    sort.SliceStable(s1, less)
    fmt.Println(s1)
}

這裡的排序函式就是回撥函式。每取一次i和j對應的元素,就呼叫一次less函式。

可以將排序函式直接寫在SliceStable()的引數位置:

sort.SliceStable(s1, func(i, j int) bool {
    return s1[i] > s1[j]
})

還可以更強大更靈活。例如,按照字元大小順序來比較,而不是按照數值大小比較:

package main

import (
    "fmt"
    "sort"
    "strconv"
)

func main() {
    s1 := []int{112, 220, 52, 32, 42}
    sort.SliceStable(s1, func(i, j int) bool {
        // 將i和j對應的元素值轉換成字串
        bi := strconv.FormatInt(int64(s1[i]), 10)
        bj := strconv.FormatInt(int64(s1[j]), 10)
        // 按字元順序降序排序
        return bi > bj
    })
    fmt.Println(s1)
}

按照字串長度來比較:

package main

import (
    "fmt"
    "sort"
)

func main() {
    s1 := []string{"hello","malong","gaoxiao"}
    sort.SliceStable(s1, func(i, j int) bool {
        // 按位元組大小順序降序排序
        return len(s1[i]) > len(s1[j])
    })
    fmt.Println(s1)
}

更嚴格地說是按位元組數比較,因為len()操作字串時獲取的是位元組數而非字元數。如果要按照字元數比較,則使用如下程式碼:

package main

import (
    "fmt"
    "sort"
)

func main() {
    s1 := []string{"hello","世界","gaoxiao"}
    sort.SliceStable(s1, func(i, j int) bool {
        // 按位元組大小順序降序排序
        return len([]rune(s1[i])) > len([]rune(s1[j]))
    })
    fmt.Println(s1)
}

閉包

函式A返回函式B,最典型的用法就是閉包(closure)。

簡單地說,閉包就是"一個函式+一個作用域環境"組成的特殊函式。這個函式可以訪問不是它自己內部的變數,也就是這個變數在其它作用域內,且這個變數是未賦值的,而是等待我們去賦值的。

例如:

package main

import "fmt"

func f(x int) func(int) int{
    g := func(y int) int{
        return x+y
    }
    // 返回閉包
    return g
}

func main() {
    // 將函式的返回結果"閉包"賦值給變數a
    a := f(3)
    // 呼叫儲存在變數中的閉包函式
    res := a(5)
    fmt.Println(res)

    // 可以直接呼叫閉包
    // 因為閉包沒有賦值給變數,所以它稱為匿名閉包
    fmt.Println(f(3)(5))
}

上面的f()返回的g之所以稱為閉包函式,是因為它是一個函式,且引用了不在它自己範圍內的變數x,這個變數x是g所在作用域環境內的變數,因為x是未知、未賦值的自由變數。

如果x在傳遞給g之前是已經賦值的,那麼閉包函式就不應該稱為閉包,因為這樣的閉包已經失去意義了。

下面這個g也是閉包函式,但這個閉包函式是自定義的,而不是通過函式返回函式得到的。

package main

import "fmt"

func main() {
    // 自由變數x
    var x int
    // 閉包函式g
    g := func(i int) int {
        return x + i
    }
    x = 5
    // 呼叫閉包函式
    fmt.Println(g(5))
    x = 10
    // 呼叫閉包函式
    fmt.Println(g(3))
}

之所以這裡的g也是閉包函式,是因為g中訪問了不屬於自己的變數x,而這個變數在閉包函式定義時是未繫結值的,也就是自由變數。

閉包的作用很明顯,在第一個閉包例子中,f(3)退出後,它返回的閉包函式g()仍然記住了原本屬於f()中的x=3。這樣就可以讓很多閉包函式共享同一個自由變數x的值

例如,下面的a(3)a(5)a(8)都共享來自f()的x=3

a := f(3)
a(3)
a(5)
a(8)

再往外層函式看,f(3)可以將自由變數x繫結為x=3,自然也可以繫結為x=5x=8等等。

所以,什麼時候使用閉包?一般來說,可以將過程分成兩部分或更多部分都進行工廠化的時候,就適合閉包。第一個部分是可以給自由變數批量繫結不同的值,第二部分是多個閉包函式可以共享第一步繫結後的自由變數