> For the complete documentation index, see [llms.txt](https://sunwenfei.gitbook.io/sunwenfei/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sunwenfei.gitbook.io/sunwenfei/golang/golang-ji-chu-jiao-cheng/yi-deng-gong-min-han-shu.md).

# 一等公民函数

#### 函数是一等公民

Golang中函数可以：

* 赋值给某个变量
* 作为入参传递给另外一个函数
* 作为其他函数的返回值

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

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

#### 匿名函数

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

```go
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()
```

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

```go
package main

import "fmt"

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

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

```
hello world first class function
```

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

```go
package main

import "fmt"

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

该程序执行结果如下：

```
Welcome Gophers
```

#### 自定义函数类型

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

```go
type add func(a int, b int) int
```

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

```go
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
```

#### 高阶函数

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

* **存在一个或多个函数作为入参**
* **将一个新的函数作为返回值**

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

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

```go
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`。

**返回一个新函数**

```go
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`返回了一个新函数`f`，`f`入参为两个`int`，返回值类型也是`int`。我们在`main`函数中调用`simple()`，赋值给`s`。因此`s`的值即为函数`f`。程序输出为`67`。

#### 闭包

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

```go
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`的值。因此第一次调用`a`时`t`由`Hello`更新成了`Hello World`，第二次调用`a`时，`t`的值已经时`Hello World`了。

该程序输出如下：

```
Hello World
Hello World Gopher
```

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

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

```go
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`，第一次调用`appendStr`后`g`更新为`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`。

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

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

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

下面是完成程序：

```go
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`函数过滤出了`grade`为`B`的student。输出为：`[{Samuel Johnson B USA}]`。

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

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

是不是很方便好用？

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

```go
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]`。

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


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://sunwenfei.gitbook.io/sunwenfei/golang/golang-ji-chu-jiao-cheng/yi-deng-gong-min-han-shu.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
