33. 函数是一等公民(头等函数)

33. 函数是一等公民(头等函数)

现在简单概括一下本教程讨论的内容:

  • 什么是头等函数?
  • 匿名函数
  • 用户自定义的函数类型
  • 高阶函数
    • 把函数作为参数,传递给其它函数
    • 在其它函数中返回函数
  • 闭包
  • 头等函数的实际用途

什么是头等函数?

支持头等函数(First Class Function)的编程语言,可以把函数赋值给变量,也可以把函数作为其它函数的参数或者返回值。Go 语言支持头等函数的机制

本教程我们会讨论头等函数的语法和用例。

匿名函数

我们来编写一个简单的示例,把函数赋值给一个变量。

package main

import (  
    "fmt"
)

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

在上面的程序中,我们将一个函数赋值给了变量 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")
    }()
}

在上面的程序中,第 8 行定义了一个匿名函数,并在定义之后,我们使用 () 立即调用了该函数(第 10 行)。该程序会输出:

hello world first class function

就像其它函数一样,还可以向匿名函数传递参数。

package main

import (  
    "fmt"
)

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

在上面的程序中,我们向匿名函数传递了一个字符串参数(第 10 行)。运行该程序后会输出:

Welcome Gophers

用户自定义的函数类型

正如我们定义自己的结构体类型一样,我们可以定义自己的函数类型。

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)
}

在上面程序的第 10 行,我们定义了一个 add 类型的变量 a,并向它赋值了一个符合 add 类型签名的函数。我们在第 13 行调用了该函数,并将结果赋值给 s。该程序会输出:

Sum 11

高阶函数

高阶函数(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)
}

在上面的实例中,第 7 行我们定义了一个函数 simplesimple 接收一个函数参数(该函数接收两个 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))
}

在上面程序中,第 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)
    }()
}

在上面的程序中,匿名函数在第 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("!"))
}

在上面程序中,函数 appendStr 返回了一个闭包。这个闭包绑定了变量 t。我们来理解这是什么意思。

在第 17 行和第 18 行声明的变量 ab 都是闭包,它们绑定了各自的 t 值。

我们首先用参数 World 调用了 a。现在 at 值变为了 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)
}

main 函数中,我们首先创建了两个学生 s1s2,并将他们添加到了切片 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)
}

该程序会输出:

[25 30 35 40 45]
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容