go 函數使用,閉包

函數類型也是一等的數據類型。這是什麼意思呢?

這意味着函數不但能夠用於封裝代碼、分割功能、解耦邏輯,還能夠化身爲普通的值,在其餘函數間傳遞、賦予變量、作類型判斷和轉換等等,就像切片和字典的值那樣。
而更深層次的含義就是:函數值能夠由此成爲可以被隨意傳播的獨立邏輯組件(或者說功能模塊)。
對於函數類型來講,它是一種對一組輸入、輸出進行模板化的重要工具,它比接口類型更加輕巧、靈活,它的值也藉此變成了可被熱替換的邏輯組件。編程

我先聲明瞭一個函數類型,名叫Printer,注意這裏的寫法,在類型聲明的名稱右邊的是func關鍵字,咱們由此就可知道這是一個函數類型的聲明。設計模式

nc右邊的就是這個函數類型的參數列表和結果列表。其中,參數列表必須由圓括號包裹,而只要結果列表中只有一個結果聲明,而且沒有爲它命名,咱們就能夠省略掉外圍的圓括號。數組

書寫函數簽名的方式與函數聲明的是一致的。只是緊挨在參數列表左邊的不是函數名稱,而是關鍵字func。這裏函數名稱和func互換了一下位置而已閉包

函數的簽名其實就是函數的參數列表和結果列表的統稱,它定義了可用來鑑別不一樣函數的那些特徵,同時也定義了咱們與函數交互的方式。編程語言

注意,各個參數和結果的名稱不能算做函數簽名的一部分,甚至對於結果聲明來講,沒有名稱均可以。只要兩個函數的參數列表和結果列表中的元素順序及其類型是一致的,咱們就能夠說它們是同樣的函數,或者說是實現了同一個函數類型的函數。嚴格來講,函數的名稱也不能算做函數簽名的一部分,它只是咱們在調用函數時,須要給定的標識符而已。ide

聲明的函數printToStd的簽名與Printer的是一致的,所以前者是後者的一個實現,即便它們的名稱以及有的結果名稱是不一樣的。函數式編程

經過main函數中的代碼,咱們就能夠證明這二者的關係了,我順利地把printToStd函數賦給了Printer類型的變量p,而且成功地調用了它。函數

總之,「函數是一等的公民」是函數式編程(functional programming)的重要特徵。Go 語言在語言層面支持了函數式編程工具

package main

import "fmt"

//先聲明瞭一個函數類型,名叫Printer,函數簽名:函數的參數列表和結果列表的統稱
type Printer func(contents string) (n int, err error)

//定義了一個函數,printToStd的簽名與Printer的是一致的,所以printToStd是Printer的一個實現,即便它們的名稱以及有的結果名稱是不一樣的
func printToStd(contents string) (bytesNum int, err error) {
    return fmt.Println(contents)
}

func main() {
    var p Printer //初始化一個Printer 類型的p
    p = printToStd //順利地把printToStd函數賦給了Printer類型的變量p,而且成功地調用了它
    p("something")
}
go run demo26.go 
something

怎樣編寫高階函數?

什麼是高階函數?只要知足了其中任意一個特色,咱們就能夠說這個函數是一個高階函數設計

  1. 接受其餘的函數做爲參數傳入;
  2. 把其餘的函數做爲結果返回。

我想經過編寫calculate函數來實現兩個整數間的加減乘除運算,可是但願兩個整數和具體的操做都由該函數的調用方給出,那麼,這樣一個函數應該怎樣編寫呢。

咱們編寫calculate函數的簽名部分。這個函數除了須要兩個int類型的參數以外,還應該有一個operate類型的參數。該函數的結果應該有兩個,一個是int類型的,表明真正的操做結果,另外一個應該是error類型的,由於若是那個operate類型的參數值爲nil,那麼就應該直接返回一個錯誤

函數類型屬於引用類型,它的值能夠爲nil,而這種類型的零值偏偏就是nil。

calculate函數實現起來就很簡單了。咱們須要先用衛述語句檢查一下參數,若是operate類型的參數op爲nil,那麼就直接返回0和一個表明了具體錯誤的error類型值。

衛述語句是指被用來檢查關鍵的先決條件的合法性,並在檢查未經過的狀況下當即終止當前代碼塊執行的語句。在 Go 語言中,if 語句常被做爲衛述語句。若是檢查無誤,那麼就調用op並把那兩個操做數傳給它,最後返回op返回的結果和表明沒有錯誤發生的nil。

calculate函數的其中一個參數是operate類型的,並且後者就是一個函數類型。在調用calculate函數的時候,咱們須要傳入一個operate類型的函數值。這個函數值應該怎麼寫?

只要它的簽名與operate類型的簽名一致,而且實現得當就能夠了。咱們能夠像上一個例子那樣先聲明好一個函數,再把它賦給一個變量,也能夠直接編寫一個實現了operate類型的匿名函數。

calculate函數就是一個高階函數。可是咱們說高階函數的特色有兩個,而該函數只展現了其中一個特色,即:接受其餘的函數做爲參數傳入。

那另外一個特色,把其餘的函數做爲結果返回。這又是怎麼玩的呢?你能夠看看我在 demo27.go 文件中聲明的函數類型calculateFunc和函數genCalculator。其中,genCalculator函數的惟一結果的類型就是calculateFunc

package main

import (
    "errors"
    "fmt"
)

type operate func(x, y int) int //咱們來聲明一個名叫operate的函數類型,它有兩個參數和一個結果,都是int類型的。

// 方案1。calculate函數就是一個高階函數。該函數只展現了其中一個特色,即:接受其餘的函數做爲參數傳入。
func calculate(x int, y int, op operate) (int, error) {
    if op == nil { //衛述語句檢查op的合法性
        return 0, errors.New("invalid operation")
    }
    return op(x, y), nil
}

// 方案2。calculateFunc也是高階函數,把其餘的函數op做爲結果返回
type calculateFunc func(x int, y int) (int, error)

func genCalculator(op operate) calculateFunc {
    return func(x int, y int) (int, error) {
        if op == nil {
            return 0, errors.New("invalid operation")
        }
        return op(x, y), nil
    }
}

func main() {
    // 方案1。
    x, y := 12, 23
    op := func(x, y int) int {
        return x + y
    }
    result, err := calculate(x, y, op) //把函數op做爲一個普通的值賦給一個變量。
    fmt.Printf("The result: %d (error: %v)\n",
        result, err)
    result, err = calculate(x, y, nil)
    fmt.Printf("The result: %d (error: %v)\n",
        result, err)

    // 方案2。
    x, y = 56, 78
    add := genCalculator(op)
    result, err = add(x, y)
    fmt.Printf("The result: %d (error: %v)\n",
        result, err)
}
go run demo27.go 
The result: 35 (error: <nil>)
The result: 0 (error: invalid operation)
The result: 134 (error: <nil>)

如何實現閉包?

閉包又是什麼?你能夠想象一下,在一個函數中存在對外來標識符的引用。所謂的外來標識符,既不表明當前函數的任何參數或結果,也不是函數內部聲明的,它是直接從外邊拿過來的。

還有個專門的術語稱呼它,叫自由變量,可見它表明的確定是個變量。實際上,若是它是個常量,那也就造成不了閉包了,由於常量是不可變的程序實體,而閉包體現的倒是由「不肯定」變爲「肯定」的一個過程。

咱們說的這個函數(如下簡稱閉包函數)就是由於引用了自由變量,而呈現出了一種「不肯定」的狀態,也叫「開放」狀態。

也就是說,它的內部邏輯並非完整的,有一部分邏輯須要這個自由變量參與完成,然後者到底表明了什麼在閉包函數被定義的時候倒是未知的。

即便對於像 Go 語言這種靜態類型的編程語言而言,咱們在定義閉包函數的時候最多也只能知道自由變量的類型

在咱們剛剛提到的genCalculator函數內部,實際上就實現了一個閉包,而genCalculator函數也是一個高階函數。

genCalculator函數只作了一件事,那就是定義一個匿名的、calculateFunc類型的函數並把它做爲結果值返回。

而這個匿名的函數就是一個閉包函數。它裏面使用的變量op既不表明它的任何參數或結果也不是它本身聲明的,而是定義它的genCalculator函數的參數,因此是一個自由變量。

這個自由變量究竟表明了什麼,這一點並非在定義這個閉包函數的時候肯定的,而是在genCalculator函數被調用的時候肯定的。只有給定了該函數的參數op,咱們才能知道它返回給咱們的閉包函數能夠用於什麼運算。

看到if op == nil {那一行了嗎?Go 語言編譯器讀到這裏時會試圖去尋找op所表明的東西,它會發現op表明的是genCalculator函數的參數,而後,它會把這二者聯繫起來。這時能夠說,自由變量op被「捕獲」了。

當程序運行到這裏的時候,op就是那個參數值了。如此一來,這個閉包函數的狀態就由「不肯定」變爲了「肯定」,或者說轉到了「閉合」狀態,至此也就真正地造成了一個閉包。

看出來了嗎?咱們在用高階函數實現閉包。這也是高階函數的一大功用。

go 函數使用,閉包
(高階函數與閉包)

那麼,實現閉包的意義又在哪裏呢?表面上看,咱們只是延遲實現了一部分程序邏輯或功能而已,但實際上,咱們是在動態地生成那部分程序邏輯。

咱們能夠藉此在程序運行的過程當中,根據須要生成功能不一樣的函數,繼而影響後續的程序行爲。這與 GoF 設計模式中的「模板方法」模式有着殊途同歸之妙,不是嗎?

傳入函數的那些參數值後來怎麼樣了?

這個命令源碼文件(也就是 demo28.go示例一)在運行以後會輸出什麼?
答案是:原數組不會改變。爲何呢?緣由是,全部傳給函數的參數值都會被複制,函數在其內部使用的並非參數值的原值,而是它的副本。因爲數組是值類型,因此每一次複製都會拷貝它,以及它的全部元素值。我在modify函數中修改的只是原數組的副本而已,並不會對原數組形成任何影響。

對於引用類型,好比:切片、字典、通道,像上面那樣複製它們的值,只會拷貝它們自己而已,並不會拷貝它們引用的底層數據。也就是說,這時只是淺表複製,而不是深層複製。

以切片值爲例,如此複製的時候,只是拷貝了它指向底層數組中某一個元素的指針,以及它的長度值和容量值,而它的底層數組並不會被拷貝。

另外還要注意,就算咱們傳入函數的是一個值類型的參數值,但若是這個參數值中的某個元素是引用類型的,那麼咱們仍然要當心。

變量complexArray1是[3][]string類型的,也就是說,雖然它是一個數組,可是其中的每一個元素又都是一個切片。這樣一個值被傳入函數的話,函數中對該參數值的修改會影響到complexArray1自己嗎?我想,這能夠留做今天的思考題。

package main

import "fmt"

func main() {
    // 示例1。底層數組不會被修改,全部傳給函數的參數值都會被複制,函數在其內部使用的並非參數值的原值,而是它的副本
    array1 := [3]string{"a", "b", "c"}
    fmt.Printf("The array: %v\n", array1)
    array2 := modifyArray(array1)
    fmt.Printf("The modified array: %v\n", array2)
    fmt.Printf("The original array: %v\n", array1)
    fmt.Println()

    // 示例2。切片會被修改掉,可是切片底層的數組不變
    slice1 := []string{"x", "y", "z"}
    fmt.Printf("The slice: %v\n", slice1)
    slice2 := modifySlice(slice1)
    fmt.Printf("The modified slice: %v\n", slice2)
    fmt.Printf("The original slice: %v\n", slice1)
    fmt.Println()

    // 示例3。 /切片被修改,底層數組不變
    complexArray1 := [3][]string{
        []string{"d", "e", "f"},
        []string{"g", "h", "i"},
        []string{"j", "k", "l"},
    }
    fmt.Printf("The complex array: %v\n", complexArray1)
    complexArray2 := modifyComplexArray(complexArray1) //切片被修改,底層數組不變
    fmt.Printf("The modified complex array: %v\n", complexArray2)
    fmt.Printf("The original complex array: %v\n", complexArray1)
}

// 示例1。
func modifyArray(a [3]string) [3]string {
    a[1] = "x"
    return a
}

// 示例2。
func modifySlice(a []string) []string {
    a[1] = "i"
    return a
}

// 示例3。
func modifyComplexArray(a [3][]string) [3][]string {
    a[1][1] = "s"
    a[2] = []string{"o", "p", "q"}
    return a
}
go run demo28.go 
The array: [a b c]
The modified array: [a x c]
The original array: [a b c]

The slice: [x y z]
The modified slice: [x i z]
The original slice: [x i z]

The complex array: [[d e f] [g h i] [j k l]]
The modified complex array: [[d e f] [g s i] [o p q]]   //切片被修改,底層數組不變
The original complex array: [[d e f] [g s i] [j k l]]

問題:
一、complexArray1被傳入函數的話,這個函數中對該參數值的修改會影響到它的原值嗎?
若是修改了引用類型的值會受影響,1.數組的操做不影響原值 2.切片的操做會影響原值。
若是是進行一層修改,即數組的某個完整元素進行修改(指針變化),那麼原有數組不變;若是進行二層修改,即數組中某個元素切片內的某個元素再進行修改(指針未改變),那麼原有數據也會跟着改變,傳參能夠理解是淺copy,參數自己的指針是不一樣,可是元素指針相同,對元素指針所指向目的的操做會影響傳參過程當中的原始數據;

二、函數真正拿到的參數值其實只是它們的副本,那麼函數返回給調用方的結果值也會被複制嗎?好比你傳出去一個數組,它還會是函數中的那個數組嗎?通常來講應該是複製的,傳參和返回應該是一個對稱的過程,自己對這一片內存數據的操做只發生在函數內部,脫離函數就應該脫離這塊內存區域

相關文章
相關標籤/搜索