Go 中的函数是一等公民,这到底在说什么?

Go编程时光

共 4282字,需浏览 9分钟

 ·

2021-02-09 23:33

点击上方“Go编程时光”,选择“加为星标

第一时间关注Go技术干货!



在微信群里有人问了这么一个问题:

来自群友的问题

请问下各位大佬,这是什么语法,为什么不需要参数的?

对于有些人来说这根本不是问题,但有些人却想不明白。我提到,在 Go 语言中,函数是一等公民,但对方不清楚这到底在说什么。看来有必要解释下什么是一等公民。

再往下看之前,你能说出什么是一等公民吗?

关于一等公民[1](First-class citizen)看看维基百科的定义:

In programming language design, a first-class citizen (also type, object, entity, or value) in a given programming language is an entity which supports all the operations generally available to other entities. These operations typically include being passed as an argument, returned from a function, modified, and assigned to a variable.

大意是说,在编程语言中,所谓一等公民,是指支持所有操作的实体, 这些操作通常包括作为参数传递,从函数返回,修改并分配给变量等。

比如 int 类型,它支持作为参数传递,可以从函数返回,也可以赋值给变量,因此它是一等公民。

类似的,函数是一等公民,意味着可以把函数赋值给变量或存储在数据结构中,也可以把函数作为其它函数的参数或者返回值。关于函数是一等公民,在维基百科也有定义[2]

In computer science, a programming language is said to have first-class functions if it treats functions as first-class citizens. This means the language supports passing functions as arguments to other functions, returning them as the values from other functions, and assigning them to variables or storing them in data structures. Some programming language theorists require support for anonymous functions (function literals) as well.In languages with first-class functions, the names of functions do not have any special status; they are treated like ordinary variables with a function type. The term was coined by Christopher Strachey in the context of "functions as first-class citizens" in the mid-1960s.

函数作为一等公民的概念是 1960 年由英国计算机学家 Christopher Strachey[3] 提出来的。然而,并非所有语言都将函数作为一等公民,特别是早期,比如 C 语言中函数就不是一等公民,一些功能通过函数指针来实现的;再比如 C++、Java 等,都是后来的版本才加上的。

一般来说,函数式编程语言、动态语言和现代的编程语言,函数都会作为一等公民,比如:Scala、Julia 等函数式语言,JavaScript、Python 等动态语言,Go、Rust、Swift 等现代的编译型语言。

为了让大家对函数是一等公民有更深的理解,针对上文提到的一等公民的一等功能,我们看看 Go 语言是如何支持的。

匿名函数

函数一般是有名字的,但有时候没有名字的函数更简洁、好用。没有名字的函数叫匿名函数。

以下是 Go 语言匿名函数的一个例子:

package main

import (
 "fmt"
)

func main() {
 fn := func() {
  fmt.Println("This is anonymous function!")
 }
 fn()

 fmt.Printf("The type of fn: %T\n", fn)
}

// output:
// This is anonymous function!
// The type of fn: func()

在线运行:https://play.studygolang.com/p/IcInzZsAr0a。

在 Go 中,匿名函数最常使用的场景是开启一个 goroutine,经常会见到类似这样的代码:

go func() {
  // xxxx
}()

这里匿名函数定义后立即调用。此外,defer 语句中也常见。

定义函数类型

定义函数类型和其他类型类似,同时后半部分和匿名函数类似,只不过没有函数实现。比如 net/http 包中的 HandlerFunc 函数类型:

type HandlerFunc func(ResponseWriter, *Request)

怎么使用这个类型?能看懂这样的代码,表示你理解了:

var h http.HandlerFunc = func(w ResponseWriter, req *Request) {
  fmt.Fprintln(w, "Hello World!")
}

函数作为参数

意思是说,一个函数作为另一个函数的参数,也就是回调,在 JS 中很常见。在 Go 语言中也经常出现。文章开头的问题就是函数作为参数。根据 Gin 的 API 定义,router.GET 方法的签名如下:

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes

其中 HandlerFunc 是一个函数类型,它的定义如下:

type HandlerFunc func(*Context)

所以,router.GET("/users", Users) 中,Users 只是 GET 函数的参数,参数类型是 HandlerFunc,而 Users 的定义只要符合 HandlerFunc 即可:

func Users(ctx *gin.Context) {}

因为这里将函数 Users 作为参数,所以自然不需要给 Users 传递参数,Uers 的调用有 GET 内部负责,即所谓的回调。

函数作为返回值

函数作为返回值,在 Go 中,这样的函数一定是匿名函数。在进行 Web 开发时,中间件就会使用上函数作为返回值,还是以 Gin 为例,定义一个 Logger 中间件:

func Logger() gin.HandlerFunc {
 return func(c *gin.Context) {
  t := time.Now()

  // Set example variable
  c.Set("example""12345")

  // before request

  c.Next()

  // after request
  latency := time.Since(t)
  log.Print(latency)

  // access the status we are sending
  status := c.Writer.Status()
  log.Println(status)
 }
}

从上文知道,gin.HandlerFunc 是一个函数类型,因此需要返回一个该类型的实例,而匿名函数(函数字面值)只要和 gin.HandlerFunc 类型的底层类型一致,会进行隐式转换,所以可以直接返回  func(c *gin.Context) {} 这个匿名类型。

经常听到高阶函数,函数是一等公民,就支持高阶函数。一个函数只要接收一个或多个函数类型参数;亦或是返回一个函数,这样的函数就叫做高阶函数。

闭包

闭包(Closure)是匿名函数的一个特例。当一个匿名函数所访问的变量定义在函数体的外部时,就称这样的匿名函数为闭包。

一个简单的例子:

package main

import (  
    "fmt"
)

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

在上面的程序中,匿名函数在第 10 行访问了变量 a,而 a 存在于函数体的外部。因此这个匿名函数就是闭包。

总结

以上的知识点,可以说是学习现代编程语言必须会的。如果你还有哪个点不明白,欢迎留言交流。

最后说明一点,Go 是不支持命名函数内嵌的。即类似 JavaScript 中这样的语法,Go 不支持:

function outer({
  console.log("In outer function");
  
  function inner({
    console.log("In inner function");
  }
}

Go 只能通过匿名函数来实现。

参考资料

[1]

一等公民: https://en.wikipedia.org/wiki/First-class_citizen

[2]

维基百科也有定义: https://en.wikipedia.org/wiki/First-class_function

[3]

Christopher Strachey: https://en.wikipedia.org/wiki/Christopher_Strachey





喜欢明哥文章的同学
欢迎长按下图订阅!

⬇⬇⬇

浏览 125
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报