Go语言中的闭包:封装数据与功能的强大工具

TimLiuDream · · 1004 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

闭包是包括 Go 在内的编程语言的一项强大功能。通过闭包,您可以在函数中封装数据,并通过函数的返回值访问这些数据。在本文中,我们将介绍 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. > 翻译过来就是: > 函数字面量(匿名函数)是*闭包*:它们可以引用在周围函数中定义的变量。然后,这些变量在周围的函数和函数字面量之间共享,只要它们还可以访问,它们就会继续存在。 > 闭包是一种创建函数的方法,这些函数可以访问在其主体之外定义的变量。闭包是一个可以捕捉其周围环境状态的函数。这意味着函数可以访问不在其参数列表中或在其主体中定义的变量。**闭包函数可以在外部函数返回后访问这些变量**。 ## 在 Go 中创建闭包 在 Go 中,您可以使用匿名函数创建闭包。创建闭包时,函数会捕获其周围环境的状态,包括外部函数中定义的任何变量。闭包函数可以在外部函数返回后访问这些变量。 下面是一个在 Go 中创建闭包的示例: ```go func adder() func(int) int { // 外部函数 sum := 0 return func(x int) int { // 内部函数 fmt.Println("func sum: ", sum) sum += x return sum } } func main() { a := adder() fmt.Println(a(1)) fmt.Println(a(2)) fmt.Println(a(3)) } ``` 在本例中,我们定义了一个返回匿名函数的加法器函数。匿名函数捕捉加法器函数中定义的 `sum` 变量的状态。每次调用匿名函数时,它都会将参数加到求和变量中,并返回结果。 所以其输出结果为: ```go func sum: 0 1 func sum: 1 3 func sum: 3 6 ``` ## 在 Go 中使用闭包 在 Go 中,闭包可用于多种用途,包括用函数封装数据、创建生成器、迭代器和 memoization 函数。 下面是一个使用闭包将数据与函数封装在一起的示例: ```go func makeGreeter(greeting string) func(string) string { return func(name string) string { fmt.Printf("func greeting: %s, name: %s\n", greeting, name) return greeting + ", " + name } } func main() { englishGreeter := makeGreeter("Hello") spanishGreeter := makeGreeter("Hola") fmt.Println(englishGreeter("John")) fmt.Println(englishGreeter("Tim")) fmt.Println(spanishGreeter("Juan")) fmt.Println(spanishGreeter("Taylor")) } ``` 在本例中,我们定义了一个名为 `makeGreeter` 的函数,它返回一个匿名函数。该匿名函数接收一个字符串参数,并返回一个将问候语和名称连接起来的字符串。我们创建了两个问候语程序,一个用于英语,一个用于西班牙语,然后用不同的名称调用它们。 所以其输出为: ```go func greeting: Hello, name: John Hello, John func greeting: Hello, name: Tim Hello, Tim func greeting: Hola, name: Juan Hola, Juan func greeting: Hola, name: Taylor Hola, Taylor ``` ## 替换捕获的变量 Go 闭包的强大功能之一是能够更改捕获的变量。这使得代码中的行为更加灵活和动态。下面是一个例子: ```go func makeCounter() func() int { i := 0 return func() int { fmt.Println("func i: ", i) i++ return i } } func main() { counter := makeCounter() fmt.Println(counter()) fmt.Println(counter()) fmt.Println(counter()) } ``` 在本例中,`makeCounter` 函数返回一个闭包,每次调用都会使计数器递增。i 变量被闭包捕获,并可被修改以更新计数器。 所以其输出为: ```go func i: 0 1 func i: 1 2 func i: 2 3 ``` ## 逃逸变量 Go 闭包的另一个高级概念是变量逃逸分析。在 Go 中,变量通常在堆栈上分配,并在超出作用域时被去分配。然而,**当变量被闭包捕获时,它必须在堆上分配**,以确保在函数返回后可以访问它。这会导致性能开销,因此了解变量何时以及如何逃逸非常重要。 我们对比一下两个方法: ```go func makeAdder1(x1 int) func(int) int { return func(y1 int) int { return x1 + y1 } } func makeAdder2(x2 int) func(int) int { fmt.Println(x2) return func(y2 int) int { return x2 + y2 } } func main() { a := makeAdder1(5) fmt.Println(a(1)) b := makeAdder2(6) fmt.Println(b(1)) } ``` `makeAdder1` 和 `makeAdder2` 的区别在于函数内的 `x` 是否被使用。 而我们通过逃逸分析: ```go go build -gcflags "-m" main.go ``` 会得到以下输出: ```go ./main.go:5:6: can inline makeAdder1 ./main.go:6:9: can inline makeAdder1.func1 ./main.go:13:9: can inline makeAdder2.func1 ./main.go:12:13: inlining call to fmt.Println ./main.go:19:17: inlining call to makeAdder1 ./main.go:6:9: can inline main.makeAdder1.func1 ./main.go:20:15: inlining call to main.makeAdder1.func1 ./main.go:20:13: inlining call to fmt.Println ./main.go:23:13: inlining call to fmt.Println ./main.go:6:9: func literal escapes to heap ./main.go:12:13: ... argument does not escape ./main.go:12:14: x2 escapes to heap ./main.go:13:9: func literal escapes to heap ./main.go:19:17: func literal does not escape ./main.go:20:13: ... argument does not escape ./main.go:20:15: ~R0 escapes to heap ./main.go:23:13: ... argument does not escape ./main.go:23:15: b(1) escapes to heap ``` 从逃逸分析结果来看,`x` 变量被闭包捕获,必须在堆上分配。不过,如果 `x` 变量不被闭包之外的任何其他代码使用,编译器可以进行优化,将其分配到栈中。 ## 共享闭包 最后,Go 中的闭包可以在多个函数之间共享,从而实现更高的灵活性和模块化代码。下面是一个例子: ```go type Calculator struct { add func(int, int) int } func NewCalculator() *Calculator { c := &Calculator{} c.add = func(x, y int) int { fmt.Printf("func x: %d, y: %d\n", x, y) return x + y } return c } func (c *Calculator) Add(x, y int) int { return c.add(x, y) } func main() { calc := NewCalculator() fmt.Println(calc.Add(1, 2)) fmt.Println(calc.Add(2, 3)) } ``` 在本例中,`Calculator` 结构具有一个 `add` 函数,该函数在 `NewCalculator` 函数中通过闭包进行了初始化。`Calculator` 结构的 `Add` 方法只需调用 `add` 函数,这样就可以在多个上下文中重复使用。 所以其输出为: ```go func x: 1, y: 2 3 func x: 2, y: 3 5 ``` ## 结论 在 Go 编程中,闭包是一个强大的工具,可用于用函数封装数据,并创建生成器和迭代器等。它们提供了一种访问函数体外定义的变量的方法,即使在函数返回后也是如此。 > 关注公众号【爱发白日梦的后端】分享技术干货、读书笔记、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力!

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1004 次点击  ∙  2 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传