[翻译]Go tip(2013-08-23)带来的变化

Dominik Honnef(之前搞错成 Russ Cox 了)在 What’s happening in Go tip (2013-08-23) 中介绍了一些关于 Go 语言的一些变化。这些变化包含了语法、性能、潜在风险和工具链。并且,这些新的东西可能会随着 Go 1.2 版本一同发布。为了方便中文读者,翻译在此。

————翻译分隔线————

Go tip(2013-08-23)带来的变化

上周我发布了关于 Go tip 的变化的系列文章的第一篇。得到了大量的肯定,因此这是第二篇。感谢你的支持,并且希望你能像喜欢第一篇文章一样喜欢本文。

有哪些变化

这次,将对以下内容进行探讨:

  • 关于切片的新语法
  • 性能改进
  • 快速的,常量时间的 P-256 椭圆曲线
  • godoc 到哪去了?

关于切片的新语法

相关 CL:CL 10743046CL 12931044

首先让我们来看看附加在 Go 1.2 中的、可能是最具争议的变化:允许设置 slice 的容量的新切片语法。不过在讨论为什么它存在争议之前,先来了解一下它究竟是什么。

所有人应当都已经熟悉了切片操作 mySlice[i:j],从 i 到 j 的范围创建一个新的 slice。mySlice 和新的 slice 将共享底层的数组,并且对其中一个 slice 操作会影响另一个。这是预期的众所周知的行为。

但一些人可能并未意识到,这两个 slice 也会共享容量。一个 slice 有长度和容量;当前可以看到的元素的数量和可以访问到的元素的数量。在使用 append()¹ 时,容量有着重要的作用:当对一个 slice 进行附加时,首先会检查底层数组是否留有足够的空间(容量)。如果有,则创建一个有着更大长度的新 slice 指向相同的底层数组,相同的内存空间。

¹: 如果容量允许,也可以手工重建 slice 并传递长度。也就是 append() 初步完成的事情。

考虑下面的代码片段

orig := make([]int, 10, 20)
subSlice := orig[5:10]
fmt.Printf("len(orig) = %d, cap(orig) = %d, len(subSlice) = %d, cap(subSlice) = %d\n",
len(orig), cap(orig), len(subSlice), cap(subSlice))
// 输出:len(orig) = 10, cap(orig) = 20, len(subSlice) = 5, cap(subSlice) = 15

orig = append(orig, 1)
fmt.Println(orig)
// 输出:[0 0 0 0 0 0 0 0 0 0 1]

subSlice = append(subSlice, 2)
fmt.Println(orig)
// 输出:[0 0 0 0 0 0 0 0 0 0 2]

这证明了 subSlice,即使切片为 orig 的一个窗口,仍然访问相同的内存,由于切片不能收缩,所以至少在前 5 个元素是这样。也同样证明在使用 append 会导致“诡异”的结果,导致原先的 slice 中的元素被覆盖。

那么,为什么这在真实的代码中是一个问题呢?设想当你基于 []byte 编写了一个自己的内存分配器——当有处理大量的可能降低 GC 性能的小内存分配时,这种方式很常见。当用户请求了 N 字节的内存,你返回了一个从 []byte 上的某处切片出来的 slice,长度为 N。然后用户做了一些傻事情:他对其进行 append 操作。跟之前我们看到的一样,这个 append 将会超过 slice 的长度把内存“泄漏”出去,同时也超出内存分配器预期的那样。你改写了其他的内存!

一个安全的选择是允许实现自定义的内存分配器,不过通过限制返回的 slice 的容量来防止内存被侵蚀,这样 append 就不会在不应该的地方修改内存。而这个安全的选择就是新的切片语法:mySlice[i:j:k] —— 前面的两个元素跟以前一样,从 i 到 j 进行切片。第三个元素表示容量,这里的容量为 k – i 的结果。

简单来说,k – i 作为容量也就意味着 k – 1 是可以被新的 slice 访问到的映射到底层数组绝对序号,跟序号 j – 1 是长度一样。让 k 等于 j,就创建了一个不能向后访问超过其长度的 slice。现在就可以编写一个返回 N 字节的内存,并且不允许向后访问的分配器了。

在介绍中,我说这个补充是存在争议的。你可能已经猜到了原因:如果你继续思考“为什么我需要这个”,你就会猜到。这个特性极少会用到。只有极少的用例会需要它,标准库中也没有多少用到的例子。但这并不意味着这个特性不应当存在,虽然它解决了一个有效的问题,不过也被质疑切片语法不应当被扩展。像 setcap() 这样的函数被建议用作避免向 Go 添加新的语法,不过最终 Go 团队决定采纳新的切片语法。如果你感兴趣,这里有前前后后相关的讨论。当然,这会包含在 Go 1.2 中。

还有设计文档,描述了原始的动机和思路。

性能改进和垃圾回收

相关 CL:CL 11573043CL 9129044CL 12894043CL 8179043CL 9492044CL 9432046CL 8819049CL 9459044CL 12603049CL 12708046CL 10079043CL 8670044CL 12680046CL 12694048CL 11874043CL 12072045CL 9915043CL 12662043CL 9462044

将会在 Go 1.2 中看到许多的性能改进。这些改进主要包括两类:更好的代码和编译器带来的直接的性能提升,和减少垃圾带来的内存分配器和垃圾回收器的工作的减少。

Brad Fitzpatrick 在探索高性能 HTTP 的时候,已经在 net/http 包和相关的常用包中进行了许多的工作。他的大多数修改都落在了“减少垃圾”分类中。由于大多数修改都十分简单,我仅列出 CL 编号以便你检出代码。全部这些在描述部分都包括了性能测试:CL 9129044(更快的 JSON 编码),CL 12894043(更快的 ZIP 压缩),CL 8179043CL 9492044CL 9432046(若干 HTTP 的改进)。

不管怎样,有些 CL 确实很有趣。一组 CL 是 CL 8819049CL 9459044CL 12603049 —— 前面两个为 bufio 添加了缓冲池和缓冲重用,而最后一个移除了前面所说的缓冲池和缓冲重用,将其用一个简单的 Reset 方法替代了。这样做的原因是,这些缓冲区可以被用户控制,并且可能绕过 API 来损坏其他用户的数据,产生如“use after free”这样的错误,而这类错误由于 Go 明确的避免提供手工的方式来释放内存,会导致难以调试。并且,它为不必要的复杂代码引起额外的性能开销埋下了伏笔。

新添加的 Reset 方法用来达到相似的性能改进,强制用户激活重用 buffer,而不是依赖于包完成这个。net/http 就是众多用户中的一个,使用 Reset 的实现在 CL 12708046。务必注意,性能测试是与池化的 bufio 进行对比,而不是 Go 1.1。

对于原始的讨论,参阅 Russ Cox 提交的 issue 6086

Brad 名下的最后一个修改是 CL 10079043,聚合了多个正在进行的 DNS 查询。或者换种说法:即使同一个 DNS 查询被执行了上百次,且都未结束的时候,其实只向服务器发送了一个真正的查询请求。

不过不仅 Brad 做了工作,其他人也贡献了大量的改进。

必须提到的一个是 CL 8670044,它为 Windows 实现了集成的网络池,使得在这个平台上的网络操作性能提升了 30% —— 在 Linux 上 Go 1.1 已经实现了的奢侈品。

另一个必须提及的应当是 CL 11573043,它极大的提升了 sync.Cond 的速度 —— 并且完全剔除了处理过程中的内存分配。

CL 12680046CL 12694048 为 encoding/binary 添加了快速路由,处理整数类型的 slice,使得这一常见类型的处理又快,成本又低廉。

DES 加密得到了五倍的速度提升(不过,悲哀的是,它仍然很慢),感谢 CL 11874043CL 12072045

bzip2 解压缩的速度快了 30%(CL 9915043),而 net 包中的 DNS 客户端产生了少于 350 字节的更少的垃圾(CL 12662043)。

最后,但是并不是不重要的,io.Copy 现在通过 ReaderFrom 的 WriterTo 来区分优先顺序,当类型实现了着两个函数时,这会带来极大的可以看到的性能改进(且在 ioutil.Discard 中几乎没有操作)。

快速的,常量时间的 P-256 椭圆曲线

相关 CL:CL 10551044

一个读者向我指出,添加了常量时间的 P-256 实现是一个非常有趣的变化,而这一实现比之前的也快了不少。

当然性能很好,不过真正的要点在“常量时间”:函数对于所有可能的输入都花费相同的时间,这避免了计时攻击,一种通过计时信息进行密码破解的旁路攻击方式。

godoc 到哪去了?

使用 Go tip 总是要求你对开发过程更加熟悉,并且随时准备着损坏、诡异的行为和突然的改变。

然而,也不是每个使用 Go tip 的人都有时间检查整个开发记录,特别是在被告知“尝试 Go tip 看看你的问题是不是能解决”的时候。例如,有一大票人最近在编译了 Go tip 以后,都很惊讶和迷惑,godoc 不再工作了,不光是没有二进制文件了,有的甚至还提示了一个错误:

readTemplate: open /Users/rsc/g/go/lib/godoc/codewalk.html: no such file or directory

在两种情况里,修复方式都是运行`go get code.google.com/p/go.tools/cmd/godoc` —— godoc 已经被移动到了 go.tools 子版本库中。

未来

呼~从 Go 1.1 开始就一直在赶开发进度,但是终点还很遥远。幸运的是当前的 Go tip 的开发已经减速了,不再产生许多有趣的变化。而哪些重要的事情发生变化时,例如 godoc 的变化,我们讲在文章里提及。

有心的读者可能已经留意到之前的文章承诺了共享链接的支持。不幸的是,为了给更加重要的改变留出空间,也为了让大家能更好的理解切片语法的变化,在本文中并未提及。

下周有什么计划?更多的变化!

Join the Conversation

9 Comments

  1. hi,咨询一下,golang如何使用linux内置的消息队列,PHP中可以直接$r = msg_get_queue(…),然后msg_send。
    在golang中是如何操作的呢?难道需要import “C”?如果不import C的话,又该如何操作?
    是否能够给个方案或者例程做参考?谢谢

  2. 在朋友的帮助下。实现了如下代码,但不知道为什么。。。少了8个字节, 我在注释里写清楚了。。

    package main

    import (
    “fmt”
    “syscall”
    “unsafe”
    )

    func main() {
    var r1, r2, a3 uintptr
    var err error
    r1, r2, err = syscall.Syscall(syscall.SYS_MSGGET, 111, 0666, a3) //r1就是key
    fmt.Println(r1, r2, err, a3)
    s := “this is a test ”
    //发过去的时候,头上怎么会少了8个字符?强塞8个空格 。。。
    b := append([]byte(” “), 0)
    b = append(b, []byte(s)…)
    // b := append([]byte(s), 0)
    ptr := unsafe.Pointer(&b[0])
    fmt.Println(uintptr(ptr))
    var a4, a5 uintptr
    r1, r2, err = syscall.Syscall6(syscall.SYS_MSGSND, r1, uintptr(ptr), uintptr(len(s)), uintptr(1), a4, a5)
    fmt.Println(r1, r2, err, a3, a4, a5)
    }

    //php 测试代码。
    /**
    print_r($argv);
    $q = msg_get_queue(111,0666);
    if($argv[1] ==”1″){
    msg_remove_queue($q);
    }
    $q = msg_get_queue(111,0666);
    print_r(msg_stat_queue($q));

    $a = 0;
    $b = 1;
    $m = ”;
    msg_receive($q, $a , $b , 10240 , $m , false );
    print_r($m);
    */

  3. 上述代码中:b := append([]byte(” “), 0),这里是我强制写的8个空格,但被编辑器替换成1个了。

  4. The author of the article is Dominik Honnef, not Russ Cox 🙂

Leave a comment

Your email address will not be published. Required fields are marked *