深入理解Go语言中的闭包
我报名参加金石计划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