Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Go语言闭包 #4

Open
ma6174 opened this issue Jul 20, 2014 · 0 comments

Comments

@ma6174
Copy link
Owner

commented Jul 20, 2014

A Tour of Go看到这样一段代码:

package main

import "fmt"

func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    pos, neg := adder(), adder()
    for i := 0; i < 10; i++ {
        fmt.Println(
            pos(i),
            neg(-2*i),
        )
    }
}

上述代码执行结果:

0 0
1 -2
3 -6
6 -12
10 -20
15 -30
21 -42
28 -56
36 -72
45 -90

官方是这么解释的:

Go 函数可以是闭包的。闭包是一个函数值,它来自函数体的外部的变量引用。函数可以对这个引用值进行访问和赋值;换句话说这个函数被“绑定”在这个变量上。例如,函数 adder 返回一个闭包。每个闭包都被绑定到其各自的 sum 变量上。

记得之前在面试某团的时候面试官一直问我闭包的定义,当时只知道这个东西怎么定义,如何使用,概念没答上来,后来特意搜了一下,wikipedia是这么说的:

在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的 函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。其中所引用的变量称作上值(upvalue)。

闭包可以用来在一个函数与一组“私有”变量之间建立关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。不过,变量的生存期是可以很长,在一次函数调用期间所建立所生成的值在下次函数调用时仍然存在。正因为这一特点,闭包可以用来完成信息隐藏,并进而应用于需要状态表达的某些编程范型中。

有这样一个需求:我要测试一个服务的平均响应时间、最大响应时间,每100次输出一次结果。
一般我们会这样写:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func testService() {
    time.Sleep(time.Duration(rand.Intn(10)+1) * time.Millisecond)
}

func main() {
    rand.Seed(time.Now().UnixNano())
    timeTotal := 0.0
    timeMax := 0.0
    timeCount := 0
    printCount := 100
    for i := 0; i < 1000; i++ {
        timeStart := time.Now()
        testService()
        timeCost := time.Since(timeStart).Seconds()
        timeCount += 1
        timeTotal += timeCost
        if timeCost > timeMax {
            timeMax = timeCost
        }
        if timeCount%printCount == 0 {
            fmt.Printf("%d runs ave time: %.2fms, max time %.2fms\n", printCount, timeTotal/float64(timeCount)*1000, timeMax*1000)
            timeMax = 0
            timeCount = 0
            timeTotal = 0
        }
    }
}

执行结果示例

$ go run count_time.go
100 runs ave time: 5.26ms, max time 10.18ms
100 runs ave time: 6.03ms, max time 10.19ms
100 runs ave time: 5.75ms, max time 10.19ms
100 runs ave time: 6.06ms, max time 10.31ms
100 runs ave time: 5.62ms, max time 10.19ms
100 runs ave time: 5.52ms, max time 10.20ms
100 runs ave time: 5.82ms, max time 10.19ms
100 runs ave time: 5.61ms, max time 10.19ms
100 runs ave time: 5.41ms, max time 10.19ms
100 runs ave time: 6.24ms, max time 10.19ms

上面的代码在功能方面是没有问题的,但是服务和统计代码混合到一块,看起来比较乱。程序也不方便复用,如果有其他类似的统计又要再写一次,比较麻烦。

看看用闭包如何实现类似的功能:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func testService() {
    time.Sleep(time.Duration(rand.Intn(10)+1) * time.Millisecond)
}

func NewTimeCount(printCount int) func(timeCost float64) {
    rand.Seed(time.Now().UnixNano())
    timeTotal := 0.0
    timeMax := 0.0
    timeCount := 0
    return func(timeCost float64) {
        timeCount += 1
        timeTotal += timeCost
        if timeMax < timeCost {
            timeMax = timeCost
        }
        if timeCount%printCount == 0 {
            fmt.Printf("%d runs ave time: %.2fms, max time %.2fms\n", printCount, timeTotal/float64(timeCount)*1000, timeMax*1000)
            timeMax = 0
            timeCount = 0
            timeTotal = 0
        }
    }
}

func main() {
    timeCount := NewTimeCount(100)
    for i := 0; i < 1000; i++ {
        timeStart := time.Now()
        testService()
        timeCost := time.Since(timeStart).Seconds()
        timeCount(timeCost)
    }
}

这里我们定义了一个NewTimeCount函数,这个函数的参数是每隔printCount输出一次统计结果。函数内部包含了timeTotaltimeMaxtimeCount三个变量,在函数内部又定义了一个匿名函数来做我们的统计工作,我们需要每次将timeCost参数传递给这个匿名函数,执行匿名函数的时候,计数器自动加一,时间总数,最大时间自动自动更新,到我们设定的timeCount之后自动输出并清零。timeTotaltimeMaxtimeCount这三个变量在NewTimeCount之外是不可见的,可以认为是匿名函数的全局变量。

再看一下main函数,比之前清晰了好多,这里我们是每100次输出一次结果,我们可以通过NewTimeCount来生成多个计数器来做统计,代码可重用行和可读性都比之前的代码要好很多。

这里还有一个问题是要计算TimeCost,对首次使用的人来说可能不不清楚如何计算。如果程序再简化一些,将计算TimeCost也自动化的话就方便多了。可以这样做:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func testService() {
    time.Sleep(time.Duration(rand.Intn(10)+1) * time.Millisecond)
}

func NewTimeCount(printCount int) (clockStart, clockStop func()) {
    rand.Seed(time.Now().UnixNano())
    var (
        timeTotal float64
        timeMax   float64
        timeCount int
        timeStart time.Time
    )
    clockStart = func() {
        timeStart = time.Now()
    }
    clockStop = func() {
        timeCost := time.Since(timeStart).Seconds()
        timeCount += 1
        timeTotal += timeCost
        if timeMax < timeCost {
            timeMax = timeCost
        }
        if timeCount%printCount == 0 {
            fmt.Printf("%d runs ave time: %.2fms, max time %.2fms\n", printCount, timeTotal/float64(timeCount)*1000, timeMax*1000)
            timeMax = 0
            timeCount = 0
            timeTotal = 0
        }
    }
    return
}

func main() {
    clockStart, clockStop := NewTimeCount(100)
    for i := 0; i < 1000; i++ {
        clockStart()
        testService()
        clockStop()
    }
}

这次我们修改了NewTimeCount函数,让其返回两个函数,一个函数用来指定程序的起始时间,另一个来指定终止时间并统计和输入。main函数得到更进一步简化。

如果将testService作为参数传入的话代码可以更进一步简化:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func testService() {
    time.Sleep(time.Duration(rand.Intn(10)+1) * time.Millisecond)
}

func NewTimeCount(printCount int) func(f func()) {
    rand.Seed(time.Now().UnixNano())
    timeTotal := 0.0
    timeMax := 0.0
    timeCount := 0
    return func(f func()) {
        timeStart := time.Now()
        f()
        timeCost := time.Since(timeStart).Seconds()
        timeCount += 1
        timeTotal += timeCost
        if timeMax < timeCost {
            timeMax = timeCost
        }
        if timeCount%printCount == 0 {
            fmt.Printf("%d runs ave time: %.2fms, max time %.2fms\n", printCount, timeTotal/float64(timeCount)*1000, timeMax*1000)
            timeMax = 0
            timeCount = 0
            timeTotal = 0
        }
    }
}

func main() {
    timeCount := NewTimeCount(100)
    for i := 0; i < 1000; i++ {
        timeCount(testService)
    }
}

总的来说,闭包这个功能还是比较实用的。

@ma6174 ma6174 added the Go label Jul 20, 2014

@ma6174 ma6174 changed the title go语言闭包 Go语言闭包 Aug 2, 2014

@ma6174 ma6174 self-assigned this Aug 3, 2014

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
1 participant
You can’t perform that action at this time.