Go 闭包变量作用域

13

我正在阅读《CreateSpace An Introduction to Programming in Go 2012》,在第86页找到了这段邪恶的代码

func makeEvenGenerator() func() uint {
    i := uint(0)

    return func() (ret uint) {
        ret = i
        i += 2
        return
    }
}

// here's how it's called
nextEven := makeEvenGenerator()
fmt.Println(nextEven())
fmt.Println(nextEven())
fmt.Println(nextEven())

1) 为什么i没有重置? 2) nextEven()返回uint还是Println太聪明了,可以与任何东西一起使用?


2
  1. 是的,它返回一个 uint。请参阅命名结果参数的文档。
- Michael
我之所以问是因为makeEvenGenerator()返回一个函数指针(如果可以这样称呼的话)。 - waaadim
1
相关代码的演示。 - J. Holmes
1
nextEven 是一个类型为 func() uint 的变量,因此 nextEven() 返回一个 uint。 - nvcnvn
1
@waaadim 在 Go 语言中,函数是值,因此 makeEvenGenerator 返回一个函数值,而不是指针。因此正确的术语应该是“函数”或“函数值”,但绝对不是函数指针。 - nemo
3个回答

15
为了更清晰明了,我将为这两个函数分别指定名称:
func makeEvenGenerator() func() uint { // call this "the factory"
    i := uint(0)

    return func() (ret uint) { // call this "the closure"
        ret = i
        i += 2
        return
    }
}

工厂函数返回闭包——在Go语言中,函数是一等公民,也就是说它们可以作为右值表达式使用,例如:

f := func() { fmt.Println("f was called"); }

f() // prints "f was called"
在你的代码中,闭包包含了工厂函数的上下文环境,这被称为“词法作用域”。这就是为什么变量i在闭包内部可用,而不是作为副本,而是作为对i本身的引用。
闭包使用了一个名为“ret”的命名返回值。这意味着,在闭包内部你将隐式地声明ret,在return点时,无论ret有什么值,都会被返回。
以下是一行代码:
ret = i

将当前的i值赋值给ret。它不会改变i的值。但是,这行代码:

will assign the current value of i to ret. It will not change i. However, this line:


i += 2

将会改变闭包下一次被调用时i的值。


这里有一个我写的小闭包示例,虽然不是非常有用,但在我看来很好地说明了闭包的作用域、目的和使用方法:

package main

import "fmt"

func makeIterator(s []string) func() func() string {
    i := 0
    return func() func() string {
        if i == len(s) {
            return nil
        }
        j := i
        i++
        return func() string {
            return s[j]
        }
    }
}

func main() {

    i := makeIterator([]string{"hello", "world", "this", "is", "dog"})

    for c := i(); c != nil; c = i() {
        fmt.Println(c())
    }

}

1
看起来像是一个只有一个方法的奇怪类。factory 这个东西只被调用一次,就像构造函数一样。然后只有 closure 被调用。那么是否会出现需要使用带参数的闭包的情况呢?还是这已经是它的“最终状态”了? - waaadim
1
如果您觉得这很有趣,我建议您深入了解函数式编程。函数作为一等公民的概念就来自于此。 - thwd

8

1) 为什么 i 没有重置?

在 Go 中,闭包通过引用捕获变量。这意味着内部函数持有对外部作用域中 i 变量的引用,每次调用它都会访问同一个变量。

2) nextEven() 返回 uint 吗?还是 Println 很聪明,可以处理所有类型?

fmt.Println()(以及fmt.Print()fmt.Fprint()等等)可以处理大多数类型。它使用 “默认格式” 打印其参数。这与使用 fmt.Printf()%v 动词打印的内容相同。


-1

闭包中的变量既不受代码段的限制,也不受上下文的影响。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接