​Go 經典入門系列 33:函數是一等公民(頭等函數)

2021-02-19 Go語言中文網

first class functions

歡迎來到 Golang 系列教程[1]的第 33 篇。

什麼是頭等函數?

支持頭等函數(First Class Function)的程式語言,可以把函數賦值給變量,也可以把函數作為其它函數的參數或者返回值。Go 語言支持頭等函數的機制

本教程我們會討論頭等函數的語法和用例。

匿名函數

我們來編寫一個簡單的示例,把函數[2]賦值給一個變量[3]。

package main

import (
    "fmt"
)

func main() {
    a := func() {
        fmt.Println("hello world first class function")
    }
    a()
    fmt.Printf("%T", a)
}

在 playground 上運行[4]

在上面的程序中,我們將一個函數賦值給了變量 a(第 8 行)。這是把函數賦值給變量的語法。你如果觀察得仔細的話,會發現賦值給 a 的函數沒有名稱。由於沒有名稱,這類函數稱為匿名函數(Anonymous Function)

調用該函數的唯一方法就是使用變量 a。我們在下一行調用了它。a() 調用了這個函數,列印出 hello world first class function。在第 12 行,我們列印出 a 的類型。這會輸出 func()。

運行該程序,會輸出:

hello world first class function
func()

要調用一個匿名函數,可以不用賦值給變量。通過下面的例子,我們看看這是怎麼做到的。

package main

import (
    "fmt"
)

func main() {
    func() {
        fmt.Println("hello world first class function")
    }()
}

在 playground 上運行[5]

在上面的程序中,第 8 行定義了一個匿名函數,並在定義之後,我們使用 () 立即調用了該函數(第 10 行)。該程序會輸出:

hello world first class function

就像其它函數一樣,還可以向匿名函數傳遞參數。

package main

import (
    "fmt"
)

func main() {
    func(n string) {
        fmt.Println("Welcome", n)
    }("Gophers")
}

在 playground 上運行[6]

在上面的程序中,我們向匿名函數傳遞了一個字符串參數(第 10 行)。運行該程序後會輸出:

Welcome Gophers

用戶自定義的函數類型

正如我們定義自己的結構體[7]類型一樣,我們可以定義自己的函數類型。

type add func(a int, b int) int

以上代碼片段創建了一個新的函數類型 add,它接收兩個整型參數,並返回一個整型。現在我們來定義 add 類型的變量。

我們來編寫一個程序,定義一個 add 類型的變量。

package main

import (
    "fmt"
)

type add func(a int, b int) int

func main() {
    var a add = func(a int, b int) int {
        return a + b
    }
    s := a(5, 6)
    fmt.Println("Sum", s)
}

在 playground 上運行[8]

在上面程序的第 10 行,我們定義了一個 add 類型的變量 a,並向它賦值了一個符合 add 類型籤名的函數。我們在第 13 行調用了該函數,並將結果賦值給 s。該程序會輸出:

Sum 11

高階函數

wiki[9] 把高階函數(Hiher-order Function)定義為:滿足下列條件之一的函數

針對上述兩種情況,我們看看一些簡單實例。

把函數作為參數,傳遞給其它函數
package main

import (
    "fmt"
)

func simple(a func(a, b int) int) {
    fmt.Println(a(60, 7))
}

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

在 playground 上運行[10]

在上面的實例中,第 7 行我們定義了一個函數 simple,simple 接收一個函數參數(該函數接收兩個 int 參數,返回一個 a 整型)。在 main 函數的第 12 行,我們創建了一個匿名函數 f,其籤名符合 simple 函數的參數。我們在下一行調用了 simple,並傳遞了參數 f。該程序列印輸出 67。

在其它函數中返回函數

現在我們重寫上面的代碼,在 simple 函數中返回一個函數。

package main

import (
    "fmt"
)

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

func main() {
    s := simple()
    fmt.Println(s(60, 7))
}

在 playground 上運行[11]

在上面程序中,第 7 行的 simple 函數返回了一個函數,並接受兩個 int 參數,返回一個 int。

在第 15 行,我們調用了 simple 函數。我們把 simple 的返回值賦值給了 s。現在 s 包含了 simple 函數返回的函數。我們調用了 s,並向它傳遞了兩個 int 參數(第 16 行)。該程序輸出 67。

閉包

閉包(Closure)是匿名函數的一個特例。當一個匿名函數所訪問的變量定義在函數體的外部時,就稱這樣的匿名函數為閉包。

看看一個示例就明白了。

package main

import (
    "fmt"
)

func main() {
    a := 5
    func() {
        fmt.Println("a =", a)
    }()
}

在 playground 上運行[12]

在上面的程序中,匿名函數在第 10 行訪問了變量 a,而 a 存在於函數體的外部。因此這個匿名函數就是閉包。

每一個閉包都會綁定一個它自己的外圍變量(Surrounding Variable)。我們通過一個簡單示例來體會這句話的含義。

package main

import (
    "fmt"
)

func appendStr() func(string) string {
    t := "Hello"
    c := func(b string) string {
        t = t + " " + b
        return t
    }
    return c
}

func main() {
    a := appendStr()
    b := appendStr()
    fmt.Println(a("World"))
    fmt.Println(b("Everyone"))

    fmt.Println(a("Gopher"))
    fmt.Println(b("!"))
}

在 playground 上運行[13]

在上面程序中,函數 appendStr 返回了一個閉包。這個閉包綁定了變量 t。我們來理解這是什麼意思。

在第 17 行和第 18 行聲明的變量 a 和 b 都是閉包,它們綁定了各自的 t 值。

我們首先用參數 World 調用了 a。現在 a 中 t 值變為了 Hello World。

在第 20 行,我們又用參數 Everyone 調用了 b。由於 b 綁定了自己的變量 t,因此 b 中的 t 還是等於初始值 Hello。於是該函數調用之後,b 中的 t 變為了 Hello Everyone。程序的其他部分很簡單,不再解釋。

該程序會輸出:

Hello World
Hello Everyone
Hello World Gopher
Hello Everyone !

頭等函數的實際用途

迄今為止,我們已經定義了什麼是頭等函數,也看了一些專門設計的示例,來學習它們如何工作。現在我們來編寫一些實際的程序,來展現頭等函數的實際用處。

我們會創建一個程序,基於一些條件,來過濾一個 students 切片。現在我們來逐步實現它。

首先定義一個 student 類型。

type student struct {
    firstName string
    lastName string
    grade string
    country string
}

下一步是編寫一個 filter 函數。該函數接收一個 students 切片和一個函數作為參數,這個函數會計算一個學生是否滿足篩選條件。寫出這個函數後,你很快就會明白,我們繼續吧。

func filter(s []student, f func(student) bool) []student {
    var r []student
    for _, v := range s {
        if f(v) == true {
            r = append(r, v)
        }
    }
    return r
}

在上面的函數中,filter 的第二個參數是一個函數。這個函數接收 student 參數,返回一個 bool 值。這個函數計算了某一學生是否滿足篩選條件。我們在第 3 行遍歷了 student 切片,將每個學生作為參數傳遞給了函數 f。如果該函數返回 true,就表示該學生通過了篩選條件,接著將該學生添加到了結果切片 r 中。你可能會很困惑這個函數的實際用途,等我們完成程序你就知道了。我添加了 main 函數,整個程序如下所示:

package main

import (
    "fmt"
)

type student struct {
    firstName string
    lastName  string
    grade     string
    country   string
}

func filter(s []student, f func(student) bool) []student {
    var r []student
    for _, v := range s {
        if f(v) == true {
            r = append(r, v)
        }
    }
    return r
}

func main() {
    s1 := student{
        firstName: "Naveen",
        lastName:  "Ramanathan",
        grade:     "A",
        country:   "India",
    }
    s2 := student{
        firstName: "Samuel",
        lastName:  "Johnson",
        grade:     "B",
        country:   "USA",
    }
    s := []student{s1, s2}
    f := filter(s, func(s student) bool {
        if s.grade == "B" {
            return true
        }
        return false
    })
    fmt.Println(f)
}

在 playground 上運行[14]

在 main 函數中,我們首先創建了兩個學生 s1 和 s2,並將他們添加到了切片 s。現在假設我們想要查詢所有成績為 B 的學生。為了實現這樣的功能,我們傳遞了一個檢查學生成績是否為 B 的函數,如果是,該函數會返回 true。我們把這個函數作為參數傳遞給了 filter 函數(第 38 行)。上述程序會輸出:

[{Samuel Johnson B USA}]

假設我們想要查找所有來自印度的學生。通過修改傳遞給 filter 的函數參數,就很容易地實現了。

實現它的代碼如下所示:

c := filter(s, func(s student) bool {
    if s.country == "India" {
        return true
    }
    return false
})
fmt.Println(c)

請將該函數添加到 main 函數,並檢查它的輸出。

我們最後再編寫一個程序,來結束這一節的討論。這個程序會對切片的每個元素執行相同的操作,並返回結果。例如,如果我們希望將切片中的所有整數乘以 5,並返回出結果,那麼通過頭等函數可以很輕鬆地實現。我們把這種對集合中的每個元素進行操作的函數稱為 map 函數。相關代碼如下所示,它們很容易看懂。

package main

import (
    "fmt"
)

func iMap(s []int, f func(int) int) []int {
    var r []int
    for _, v := range s {
        r = append(r, f(v))
    }
    return r
}
func main() {
    a := []int{5, 6, 7, 8, 9}
    r := iMap(a, func(n int) int {
        return n * 5
    })
    fmt.Println(r)
}

在 playground 上運行[15]

該程序會輸出:

[25 30 35 40 45]

現在簡單概括一下本教程討論的內容:

本教程到此結束。祝你愉快。

上一教程 - panic 和 recover

下一教程 - 反射[16]

via: https://golangbot.com/first-class-functions/

作者:Nick Coghlan[17]譯者:Noluye[18]校對:polaris1119[19]

本文由 GCTT[20] 原創編譯,Go 中文網[21] 榮譽推出

參考資料[1]

Golang 系列教程: https://studygolang.com/subject/2

[2]

函數: https://studygolang.com/articles/11892

[3]

變量: https://studygolang.com/articles/11756

[4]

在 playground 上運行: https://play.golang.org/p/Xm_ihamhlEv

[5]

在 playground 上運行: https://play.golang.org/p/c0AjB3g8UEn

[6]

在 playground 上運行: https://play.golang.org/p/9ttJ5Wi4fj4

[7]

結構體: https://studygolang.com/articles/12263

[8]

在 playground 上運行: https://play.golang.org/p/n3yPQ7hG7ip

[9]

wiki: https://en.wikipedia.org/wiki/Higher-order_function

[10]

在 playground 上運行: https://play.golang.org/p/C0MNwz2TSGU

[11]

在 playground 上運行: https://play.golang.org/p/82y2caejUy8

[12]

在 playground 上運行: https://play.golang.org/p/6QriMs-zbnf

[13]

在 playground 上運行: https://play.golang.org/p/134NiQGPOcS

[14]

在 playground 上運行: https://play.golang.org/p/YUL1CqSrvfc

[15]

在 playground 上運行: https://play.golang.org/p/cs37QwCQ_0H

[16]

反射: https://studygolang.com/articles/13178

[17]

Nick Coghlan: https://golangbot.com/about/

[18]

Noluye: https://github.com/Noluye

[19]

polaris1119: https://github.com/polaris1119

[20]

GCTT: https://github.com/studygolang/GCTT

[21]

Go 中文網: https://studygolang.com/

相關焦點

  • Go 中的函數是一等公民,這到底在說什麼?
    我提到,在 Go 語言中,函數是一等公民,但對方不清楚這到底在說什麼。看來有必要解釋下什麼是一等公民。再往下看之前,你能說出什麼是一等公民嗎?大意是說,在程式語言中,所謂一等公民,是指支持所有操作的實體, 這些操作通常包括作為參數傳遞,從函數返回,修改並分配給變量等。比如 int 類型,它支持作為參數傳遞,可以從函數返回,也可以賦值給變量,因此它是一等公民。
  • 導函數的兩大特性及「導函數大家庭」簡介(高等數學入門系列拓展閱讀)
    系列簡介:這個系列文章講解高等數學的基礎內容,注重學習方法的培養,對初學者不易理解的問題往往會不惜筆墨加以解釋。
  • Go 經典入門系列 32:panic 和 recover
    panic 和 recover歡迎來到 Golang 系列教程[1]的第 32 篇。什麼是 panic? 在 Go 語言中,程序中一般是使用錯誤[2]來處理異常情況。對於程序中出現的大部分異常情況,錯誤就已經夠用了。
  • 高等數學入門——反函數的求導法則及反三角函數的導數公式總結
    在內容上,以國內的經典教材」同濟版高等數學「為藍本,並對具體內容作了適當取捨與拓展。例如用ε-δ語言證明函數極限這類高等數學課程不要求掌握的內容,我們不作過多介紹。本系列文章適合作為大一新生初學高等數學時的課堂同步輔導,也可作為高等數學期末複習以及考研第一輪複習時的參考資料。文章中的例題大多為紮實基礎的常規性題目和幫助加深理解的概念辨析題,並適當選取了一些考研數學試題。
  • 高等數學入門——連續函數的基本性質
    系列簡介:這個系列文章講解高等數學的基礎內容,注重學習方法的培養,對初學者不易理解的問題往往會不惜筆墨加以解釋。
  • 高等數學入門——函數極限定義分類總結
    在內容上,以國內的經典教材」同濟版高等數學「為藍本,並對具體內容作了適當取捨與拓展。例如用ε-δ語言證明函數極限這類高等數學課程不要求掌握的內容,我們不作過多介紹。本系列文章適合作為大一新生初學高等數學時的課堂同步輔導,也可作為高等數學期末複習以及考研第一輪複習時的參考資料。文章中的例題大多為紮實基礎的常規性題目和幫助加深理解的概念辨析題,並適當選取了一些考研數學試題。
  • 每天都在調用函數,Go 中函數調用的原理你知道嗎?
    函數是 Go 語言中的一等公民,理解和掌握函數的調用過程是深入學習 Golang 時無法跳過的步驟,這裡會介紹 Go 語言中函數調用的過程和實現原理並與 C 語言中函數執行的過程進行對比,同時對參數傳遞的原理進行剖析,讓讀者能夠清楚地知道 Go 在函數的執行過程中究竟都做了哪些工作。
  • Go 經典入門系列 28:Defer
    歡迎來到 Golang 系列教程[1]的第 29 篇。什麼是 defer? defer 語句的用途是:含有 defer 語句的函數,會在該函數將要返回之前,調用另一個函數。這個定義可能看起來很複雜,我們通過一個示例就很容易明白了。
  • 高等數學入門——連續函數的整體性質選講
    系列簡介:這個系列文章講解高等數學的基礎內容,注重學習方法的培養,對初學者不易理解的問題往往會不惜筆墨加以解釋。
  • 深入理解go的函數參數傳遞
    首先我們要有一個理解:go的函數參數傳遞都是值傳遞,為什麼說是傳值呢?因為go的函數傳遞都是複製了一份傳遞到參數中。
  • 高等數學入門——原函數形式上的「多樣性」及其簡單應用
    系列簡介:這個系列文章講解高等數學的基礎內容,注重學習方法的培養,對初學者不易理解的問題往往會不惜筆墨加以解釋。
  • 高等數學入門——計算乘積函數高階導數的萊布尼茲公式
    系列簡介:這個系列文章講解高等數學的基礎內容,注重學習方法的培養,對初學者不易理解的問題往往會不惜筆墨加以解釋。
  • 高等數學入門——閉區間上連續函數的基本定理
    系列簡介:這個系列文章講解高等數學的基礎內容,注重學習方法的培養,對初學者不易理解的問題往往會不惜筆墨加以解釋。
  • Go 經典入門系列 24:Select
    歡迎來到 Golang 系列教程[1]的第 24 篇。什麼是 select? select 語句用於在多個發送/接收信道操作中進行選擇。select 語句會一直阻塞,直到發送/接收操作準備就緒。如果有多個信道操作準備完畢,select 會隨機地選取其中之一執行。
  • 高等數學入門——函數極限的定義(自變量趨於有限值情形)
    系列簡介:這個系列文章講解高等數學的基礎內容,注重學習方法的培養,對初學者不易理解的問題往往會不惜筆墨加以解釋。
  • Go 經典入門系列 26:結構體取代類​
    歡迎來到 Golang 系列教程[1]的第 26 篇。Go 支持面向對象嗎? Go 並不是完全面向對象的程式語言。Go 官網的 FAQ[2] 回答了 Go 是否是面向對象語言,摘錄如下。可以說是,也可以說不是。
  • 高等數學入門——關於連續函數的典型證明題與綜合題
    系列簡介:這個系列文章講解高等數學的基礎內容,注重學習方法的培養,對初學者不易理解的問題往往會不惜筆墨加以解釋。
  • 高等數學入門——常見初等函數的n階導數公式的推導與總結
    系列簡介:這個系列文章講解高等數學的基礎內容,注重學習方法的培養,對初學者不易理解的問題往往會不惜筆墨加以解釋。
  • [翻譯]淺談JavaScript中的高階函數
    高階函數的採用使得JavaScript適合用來做函數式編程。在JavaScript中,高階函數的使用隨處可見。如果你已經用JavaScript寫過一陣子的代碼,那麼你可能已經在不知情的情況下使用過它了。為了完全理解這個概念,你首先要了解什麼是函數式編程以及頭等函數的概念。
  • 高等數學入門——計算有理函數不定積分的部分分式法
    系列簡介:這個系列文章講解高等數學的基礎內容,注重學習方法的培養,對初學者不易理解的問題往往會不惜筆墨加以解釋。