一等公民函数

函数是一等公民

Golang中函数可以:

  • 赋值给某个变量

  • 作为入参传递给另外一个函数

  • 作为其他函数的返回值

在编程语言中,我们一般说这种函数是类似于变量的一等公民,因为在其他编程语言中,一般只有变量才符合这些特性。

当然,除了Golang,Javascript中函数也是一等公民。将函数做为一等公民非常有意义,这类编程语言一般可以做为函数式编程语言使用。

匿名函数

我们先来看一个将函数赋值给变量的例子。

package main

import "fmt"

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

这里我们将一个函数赋值给了变量a。注意这个函数没有声明函数名称,这类函数一般称为匿名函数

由于该匿名函数没有函数名称,因此我们只能通过变量a来调用。该程序执行结果如下:

hello world first class function
func()

当然我们也可以通过立即调用来调用匿名函数。

package main

import "fmt"

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

这里我们定义了一个匿名函数并进行了立即调用,直接结果如下:

hello world first class function

立即调用也可以传参数进去。

package main

import "fmt"

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

该程序执行结果如下:

Welcome Gophers

自定义函数类型

我们不仅可以定义结构体类型、接口类型,还可以自定义函数类型。

type add func(a int, b int) int

这行代码定义了一个新的函数类型add,该类型的函数要求接收两个int参数作为入参,函数返回值类型也为int。下面我们定义一个该类型函数的变量。

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

这里我们定义了一个add类型的变量a,我们给该变量赋的值是一个入参为两个int,返回值也为int的函数。注意我们赋值的函数签名必须符合add类型的定义。该程序执行结果如下:

Sum 11

高阶函数

符合下面两个条件之一的函数我们称为高阶函数:

  • 存在一个或多个函数作为入参

  • 将一个新的函数作为返回值

我们分别就这两种情况举个例子。

将函数作为入参传给其他函数

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

这里我们定义了函数simple,该函数的入参是函数类型。我们在main函数内部创建了一个符合simple入参要求的匿名函数,并传递给simple函数来调用。程序输出为67

返回一个新函数

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

这里定义的函数simple返回了一个新函数ff入参为两个int,返回值类型也是int。我们在main函数中调用simple(),赋值给s。因此s的值即为函数f。程序输出为67

闭包

将函数作为一等公民的编程语言一般都有闭包的概念,比如Javascript,Golang也不例外。闭包是指在函数内部可以访问到函数外部定义的变量。当然,如果函数嵌套级别较深,可能会有多层访问,也就形成了作用域链。 听起来有点晦涩,我们通过例子来理解下。

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()
    fmt.Println(a("World"))
    fmt.Println(a("Gopher"))
}

我们定义了一个函数appendStr,该函数的返回值是一个新函数,因此该函数是一个高阶函数。在appendStr函数内部,我们定义了变量t,因此在被返回的函数c内部我们是能够访问到t这个变量的,也就是说t在函数c作用域链上。

我们在main函数中调用appendStr()赋值给变量a,这时a的值即为c,是appendStr返回的函数。由于t变量在函数c的作用域链上,因此每次调用a(也就是调用c)时都能访问到变量t,也就是说可以多次调用a来更新变量t的值。因此第一次调用atHello更新成了Hello World,第二次调用a时,t的值已经时Hello World了。

该程序输出如下:

Hello World
Hello World Gopher

这里提到的作用域链非常重要,原文没有提到这个,我个人觉得重要就补充了下,是学习高阶函数和闭包不容易理解的地方,是难点也是核心。

下面我们升级下前面的程序,来深刻体会下作用域链。

package main

import "fmt"

var g = "Hi"

func appendStr() func(string) string {
    t := "Hello"
    g = g + " " + "Hi"
    c := func(b string) string {
        t = t + " " + b + g
        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外部定义了一个变量g,因此g位于函数appendStr的作用域链上。调用appendStr可以访问到其作用域链上的g,第一次调用appendStrg更新为Hi Hi,第二次调用后g更新为Hi Hi Hi。该程序输出为:

Hello WorldHi Hi Hi
Hello EveryoneHi Hi Hi
Hello WorldHi Hi Hi GopherHi Hi Hi
Hello EveryoneHi Hi Hi !Hi Hi Hi

真实适用场景

前面我们看了几个例子,下面我们看下作为一等公民的函数都有哪些真实适用场景。 我来实现实现一个函数filter来根据某些条件过滤一个切片,这在函数式编程中是非常常见的一种用法。

首先我们定义几个结构体类型student

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

下面实现函数filter,入参为一个切片列表,一个过滤条件判断函数。

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

下面是完成程序:

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) {
            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 {
        return s.grade == "B"
    })
    fmt.Println(f)
}

我们定义了两个student结构体变量,然后通过filter函数过滤出了gradeB的student。输出为:[{Samuel Johnson B USA}]

假设我们现在像过滤出来自India,即印度的同学,那么只需要把过滤判断函数改为如下即可:

f := filter(s, func(s student) bool {
    return s.country == "India"
})
fmt.Println(f)

是不是很方便好用?

下面我们再实现一个函数iMap,用来遍历处理一个int类型的切片,并返回处理后的切片。

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

这里我们将切片中的每个元素都乘了5,输出为:[25 30 35 40 45]

filtermap这些函数都是借助高阶函数这个特性来实现的,其实本质上是属于函数式编程的范畴,感兴趣的话可以接着去深入学习函数式编程,Haskell、Golang、Javascript、Python等编程语言都支持函数式编程。学习函数式编程对编程水平的提升绝对大有裨益。

Last updated