sunwenfei
  • 关于我
  • Golang
    • Goglang基础教程【译】
      • 介绍
        • 安装
        • Hello World
      • 变量、基本类型以及常量
        • 变量
        • 基本类型
        • 常量
      • 函数和包
        • 函数
        • 包
      • 条件、循环流程控制语句
        • if else条件语句
        • switch语句
        • 循环语句
      • 数组、切片、变参函数
        • 数组(Array)
        • 切片(Slice)
        • 变参函数
      • 其他数据类型
        • 映射(Map)
        • 字符串
      • 指针、结构体和方法
        • 指针
        • 结构体
        • 方法
      • 面向对象编程
        • 结构体 vs 类
        • 组合 vs 继承
        • 接口
        • 多态
      • 并发
        • 并发介绍
        • 协程(goroutine)
        • 管道(channel)
        • 带缓存的管道(buffered channel)
        • 协程池
        • 管道选择器(select)
        • 互斥锁(Mutex)
      • Defer
      • 一等公民函数
      • 反射
      • 错误
        • 错误处理
        • 自定义错误类型
        • panic和recover
      • 文件读写
        • 读文件
        • 写文件
    • Golang面向对象编程
    • Golang函数式编程
    • Golang并发编程
    • Golang web服务编程
    • Golang数据结构与算法
  • Shell编程
    • Find命令
  • JavaScript
    • browser
    • Node.JS
    • Deno
  • TypeScript
  • HTTP
    • 【译】通过信鸽理解HTTPS交互原理
  • React
    • React16
      • Hooks
        • 使用React Hooks拉取数据
  • 移动端开发
    • 原生
    • Flutter
    • ReactNative
    • 小程序
  • 前端测试
Powered by GitBook
On this page

Was this helpful?

  1. Golang
  2. Goglang基础教程【译】

一等公民函数

函数是一等公民

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返回了一个新函数f,f入参为两个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的值。因此第一次调用a时t由Hello更新成了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,第一次调用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。

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函数过滤出了grade为B的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]。

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

PreviousDeferNext反射

Last updated 5 years ago

Was this helpful?