Golang之闭包

written by Alex Stocks on 2016/09/24

1 closure定义

关于closure的定义,可以参照golang官方示例(参考文档1)中的一句话:

Go supports anonymous functions, which can form closures. Anonymous functions are useful when you want to define a function inline without having to name it.

从上面这句话可以看出看出,closure首先是匿名函数,其次是在另一个函数里面实现。

很多语言都有closure,其实都是一种语法糖,它与其定义时所在的函数共享同一个函数栈,能够使用其所在函数的内存空间,其访问的内存空间的对象(可称之为closure context)会被runtime放在堆空间上,编译器编译closure后会被inline成所在函数的一部分语句块(golang中是Escape Analysis技术)以提高运行速度。

其实可以这么定义:closure = anonymous function + closure conetxt。关于closure的汇编层面解释,详见最下面列出的参考文档2。

下面列述最近遇到的几个比较典型的golang clousure code example。

2 closure与引用

golang中通过传递变量值能够起到引用效果的变量类型有slice & map & channel,其本质是这三种var type不是那种类似于int等可以让CPU直接访问的原子变量类型,而是一种C中的类似于struct的复合数据结构,其结构体中存储的值又指向的更大的一块内存地址,这个大内存区域才是真正的“值域”,结构体本身类似域大内存域的proxy。如果能够理解C++的shared_ptr的实现,就能够理解这种变量类型的本质。

因为closure与其所在的函数共享函数栈,所以也能实现类似于引用的效果。如下程序:

// output: 5
func main() {
    var v int = 3
    func() {
        v = 5
    }()
    println(v)
}

上面的例子中,main函数内部的closure修改了变量v的值,因为是函数内部调用,其结论可能不能为人信服,又有如下示例:

// output: 5
func test() (func(), func()) {
    var v int = 3
    return func() { v = 5 }, func() { println("v:", v) }
}

func main() {
    f1, f2 := test()
    f1()
    f2()
}

代码示例中f1和f2访问的变量v,其实v在使用时被runtime定义在了heap上。

参考文档1的代码示例也比较经典,一并补录如下:

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

func main() {
    nextInt := intSeq()

    println(nextInt()) // 1
    println(nextInt()) // 2
    println(nextInt()) // 3

    newInts := intSeq()
    println(newInts()) // 1
}

注意上面示例中最后一行的输出,当closure所在函数重新调用时,其closure是新的,其context引用的变量也是重新在heap定义过的。

3 closure与context

context是我见过的golang标准库(go1.7)中最优雅的库之一,对context的分析详见参考文档3,其cancel相关代码如下:

type CancelFunc func()

// WithCancel方法返回一个继承自parent的Context对象,同时返回的cancel方法可以用来关闭返回的Context当中的Done channel
// 其将新建立的节点挂载在最近的可以被cancel的父节点下(向下方向)
// 如果传入的parent是不可被cancel的节点,则直接只保留向上关系
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{
        Context: parent,
        done:    make(chan struct{}),
    }
}

从上可见cancel context也用到了closure,WithCancel返回了一个context对象和一个closure。cancel context的使用示例(参考文档4)如下:

// 模拟一个最小执行时间的阻塞函数
func inc(a int) int {
    res := a + 1                // 虽然我只做了一次简单的 +1 的运算,
    time.Sleep(1 * time.Second) // 但是由于我的机器指令集中没有这条指令,
    // 所以在我执行了 1000000000 条机器指令, 续了 1s 之后, 我才终于得到结果。B)
    return res
}

// 向外部提供的阻塞接口
// 计算 a + b, 注意 a, b 均不能为负
// 如果计算被中断, 则返回 -1
func Add(ctx context.Context, a, b int) int {
    res := 0
    for i := 0; i < a; i++ {
        res = inc(res)
        select {
        case <-ctx.Done():
            return -1
        default:
        }
    }
    for i := 0; i < b; i++ {
        res = inc(res)
        select {
        case <-ctx.Done():
            return -1
        default:
        }
    }

    return res
}

// output:
// Compute: 1+2, result: -1
// Compute: 1+2, result: -1
func main() {
    // 手动取消
    a := 1
    b := 2
    ctx, cancel := context.WithCancel(context.Background())
    go func() {
        time.Sleep(2 * time.Second)
        cancel() // 在调用处主动取消
    }()
    res := Add(ctx, 1, 2)
}

4 closure与error

golang中错误处理是一件令人头疼的事情:需要不断的写"if err != nil {}"这样的代码^_^。

golang官方的《Errors are values》(参考文档5)一文中给出了如下一段错误处理示例:

_, err = fd.Write(p0[a:b])
if err != nil {
    return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
    return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
    return err
}
// and so on

这段代码示例的机巧之处在于:三个错误处理针对同一个函数fd.Write,这便能通过closure上下其手了,官方给出的第一个改进就是:

var err error
write := func(buf []byte) {
    if err != nil {
        return
    }
    _, err = w.Write(buf)
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
// and so on
if err != nil {
    return err
}

上面write closure虽然没有减少代码量,但使得代码优雅了不少。后面官方又给出了第二个优化:

type errWriter struct {
    w   io.Writer
    err error
}

func (ew *errWriter) write(buf []byte) {
    if ew.err != nil {
        return
    }
    _, ew.err = ew.w.Write(buf)
}

ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
    return ew.err
}

这个代码示例把closure中的error放入了struct errWriter之中,使得代码更加精妙。

上面代码段中这个技巧被用到了bufio.Writer的实现上,所以调用(bufio.Writer)Write函数时候,不用不断检查其返回值error,其代码示例如下:

b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
// and so on
if b.Flush() != nil {
    return b.Flush()
}

本节的技巧只有在同一个函数接口以及同一个处理对象error这样的情况下才可使用。

6 总结

本文总结了closure的本质以及其一些使用场景,囿于个人golang知识范围低下,暂时只能写这么多了。

以后随着个人能力提升,我会逐渐补加此文。

此记。

参考文档

扒粪者-于雨氏

于雨氏,2016/09/24,初作此文于东沪。

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章