likes
comments
collection
share

深入理解Go语言中的闭包

作者站长头像
站长
· 阅读数 73

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

在本文中,我们首先介绍 Go 语言中闭包的定义,并且将探索闭包的几种不同的现实用例,以便您更好地了解闭包何时使用。

一、什么是闭包

go官方有一句解释:

Function literals are closures: they may refer to variables defined in a surrounding function. Those variables are then shared between the surrounding function and the function literal, and they survive as long as they are accessible.

翻译过来就是:

函数字面量(匿名函数)是闭包:它们可以引用在周围函数中定义的变量。然后,这些变量在周围的函数和函数字面量之间共享,只要它们还可以访问,它们就会继续存在。

什么意思呢?闭包是由函数及其相关引用环境组合而成的实体。即:

闭包 = 函数 + 引用环境

1.一个栗子

package main

import "fmt"

func intSeq() func() int {

    i := 0
    return func() int {
        i++
        return i
    }
}

func main() {

    nextInt := intSeq()

    fmt.Println(nextInt())
    fmt.Println(nextInt())

    nextInt2 := intSeq()
    fmt.Println(nextInt2())
}

我们有一个整数序列intSeq函数,它生成一个整数序列。它就返回一个包含递增i变量的闭包

func intSeq() func() int 

intSeq是一个函数,它返回一个返回整数的函数 。

func intSeq() func() int {

    i := 0
    return func() int {
        i++
        return i
    }
}

函数中定义的变量i具有局部函数作用域,但是,在这种情况下,即使 intSeq函数执行完成后,闭包仍会绑定到变量i

nextInt := intSeq()

我们调用intSeq函数。它返回一个函数,该函数将增加一个计数器。返回的函数关闭了变量 i以形成闭包,并绑定到 nextInt变量上。

关闭是什么意思呢?可以这么理解:我们知道函数的局部变量是在栈上分配的,而闭包上的变量会逃逸到堆上(是由编译器的一种叫escape analyze的技术实现的)

fmt.Println(nextInt())
fmt.Println(nextInt())

连续调用两次nexInt,因为都是在同一环境下,所以变量i会累加

我们再次调用闭包(生成了一个新的闭包)。

nextInt2 := intSeq()
fmt.Println(nextInt2())

函数的下一次调用intSeq返回一个新的闭包。这个新的闭包有自己独特的状态,所以变量i重置了。

返回如下:

$ go run closure.go
1
2
1

二、应用场景

1. 隔离数据

假设您要创建一个函数,该函数可以访问即使在函数退出后仍然存在的数据。例如,您想计算函数被调用的次数,或者您想创建一个斐波那契数生成器,但您不希望其他人访问该数据(因此他们不会意外更改它),您可以使用闭包来实现这一点。

package main

import "fmt"

func main() {
  gen := makeFibGen()
  for i := 0; i < 10; i++ {
    fmt.Println(gen())
  }
}

func makeFibGen() func() int {
  f1 := 0
  f2 := 1
  return func() int {
    f2, f1 = (f1 + f2), f2
    return f1
  }
}

2.封装函数和创建中间件

Go 中的函数是一等公民,这意味着您不仅可以动态创建匿名函数,还可以将函数作为参数传递给函数。例如,在创建 Web 服务器时,通常会提供一个函数来处理对特定路由的 Web 请求。

package main

import (
  "fmt"
  "net/http"
)

func main() {
  http.HandleFunc("/hello", hello)
  http.ListenAndServe(":3000", nil)
}

func hello(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintln(w, "<h1>Hello!</h1>")
}

在这种情况下,该函数hello()被传递给该http.HandleFunc()函数,并在该路由匹配时被调用。此时,如果我们想要在hello执行前或者执行后添加一些逻辑该怎么办呢?比如我想统计一下调用hello函数花费的时间。 我们可以使用中间件来完成它!

package main

import (
  "fmt"
  "net/http"
  "time"
)

func main() {
  http.HandleFunc("/hello", timed(hello))
  http.ListenAndServe(":3000", nil)
}

func timed(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
  return func(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    f(w, r)
    end := time.Now()
    fmt.Println("The request took", end.Sub(start))
  }
}

func hello(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintln(w, "<h1>Hello!</h1>")
}

请注意,我们的timed()函数返回了一个可以作为HandleFunc参数的函数。返回的闭包记录当前时间,调用原始函数,最后记录结束时间并打印出请求的持续时间。同时timed()并不知道我们的hell0内部实际发生的事情,需要做的只是对我们的处理程序进行计时,并将它们包装起来timed(hello)并将闭包传递给http.HandleFunc()函数调用。

golang 官网文档也有一类似的例子

func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        m := validPath.FindStringSubmatch(r.URL.Path)
        if m == nil {
            http.NotFound(w, r)
            return
        }
        fn(w, r, m[2])
    }
}

3. 访问通常不可用的数据

http.HandleFunc 的定义如下

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}

第二个参数 handler是不能传递参数的,但是有时候我们想给它传递自定义的变量。比如下面的代码:

package main

import (
  "fmt"
  "net/http"
)

type Database struct {
  Url string
}

func NewDatabase(url string) Database {
  return Database{url}
}

func main() {
  db := NewDatabase("localhost:5432")

  http.HandleFunc("/hello", hello(db))
  http.ListenAndServe(":3000", nil)
}

func hello(db Database) func(http.ResponseWriter, *http.Request) {
  return func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, db.Url)
  }
}

我们的处理函数hello,就好像它们可以访问一个Database对象一样,同时仍然返回一个带有http.HandleFunc()预期签名的函数。这允许我们绕过http.HandleFunc()不允许我们传递自定义变量的事实。

4. sort 包

这个包提供的函数有很多地方用到了闭包,比如

people := []string{"Alice", "Bob", "Dave"}
sort.Slice(people, func(i, j int) bool {
    return len(people[i]) < len(people[j])
} )
fmt.Println(people)
// Output: [Bob Dave Alice]

参考

转载自:https://juejin.cn/post/7140664403996868615
评论
请登录