Go 1.6中值得关注的几个变化

https://tonybai.com/2016/02/21/some-changes-in-go-1-6/
二月 21, 2016 2 条评论

北京时间2016年2月18日凌晨,在Go 1.5发布 半年后,Go 1.6正式Release 了。与Go 1.5的“惊天巨变”(主要指Go自举)相比,Go 1.6的Change 算是很小的了,当然这也与Go 1.6的dev cycle过于短暂有关。但Go社区对此次发布却甚是重视,其热烈程度甚至超出了Go 1.5。在Dave Cheney的倡导 下,Gophers们在全球各地举行了Go 1.6 Release Party。 Go Core Team也在Reddit上开了一个AMA – Ask Me Anything,RobPike、Russ Cox(Rsc)、Bradfitz等Go大神齐上阵,对广大Gophers们在24hour内的问题有问必答。

言归正传,我们来看看Go 1.6中哪些变化值得我们关注。不过在说变化之前,我们先提一嘴Go 1.6没变的,那就是Go语言的language specification,依旧保持Go 1兼容不变。预计在未来的几个stable release版本中,我们也不会看到Go language specification有任何改动。

一、cgo

cgo的变化在于:
1、定义了在Go code和C code间传递Pointer,即C code与Go garbage collector共存的rules和restriction;
2、在runtime加入了对违规传递的检查,检查的开关和力度由GODEBUG=cgocheck=1[0,2]控制。1是默认;0是关闭检 查;2是更全面彻底但代价更高的检查。

这个Proposal是由Ian Lance Taylor提出的。大致分为两种情况:

(一) Go调用C Code时

Go调用C Code时,Go传递给C Code的Go Pointer所指的Go Memory中不能包含任何指向Go Memory的Pointer。我们分为几种情况来探讨一下:

1、传递一个指向Struct的指针

  1. //cgo1_struct.go
  2. package main
  3. /*
  4. #include <stdio.h>
  5. struct Foo{
  6. int a;
  7. int *p;
  8. };
  9. void plusOne(struct Foo *f) {
  10. (f->a)++;
  11. *(f->p)++;
  12. }
  13. */
  14. import "C"
  15. import "unsafe"
  16. import "fmt"
  17. func main() {
  18. f := &C.struct_Foo{}
  19. f.a = 5
  20. f.p = (*C.int)((unsafe.Pointer)(new(int)))
  21. //f.p = &f.a
  22. C.plusOne(f)
  23. fmt.Println(int(f.a))
  24. }

从cgo1_struct.go代码中可以看到,Go code向C code传递了一个指向Go Memory(Go分配的)指针f,但f指向的Go Memory中有一个指针p指向了另外一处Go Memory: new(int),我们来run一下这段代码:

  1. $go run cgo1_struct.go
  2. # command-line-arguments
  3. ./cgo1_struct.go:12:2: warning: expression result unused [-Wunused-value]
  4. panic: runtime error: cgo argument has Go pointer to Go pointer
  5. goroutine 1 [running]:
  6. panic(0x4068400, 0xc82000a110)
  7. /Users/tony/.bin/go16/src/runtime/panic.go:464 +0x3e6
  8. main.main()
  9. /Users/tony/test/go/go16/cgo/cgo1_struct.go:24 +0xb9
  10. exit status 2

代码出现了Panic,并提示:“cgo argument has Go pointer to Go pointer”。我们的代码违背了Cgo Pointer传递规则,即便让f.p指向struct自身内存也是不行的,比如f.p = &f.a。

2、传递一个指向struct field的指针

按照rules中的说明,如果传递的是一个指向struct field的指针,那么”Go Memory”专指这个field所占用的内存,即便struct中有其他field指向其他Go memory也不打紧:

  1. //cgo1_structfield.go
  2. package main
  3. /*
  4. #include <stdio.h>
  5. struct Foo{
  6. int a;
  7. int *p;
  8. };
  9. void plusOne(int *i) {
  10. (*i)++;
  11. }
  12. */
  13. import "C"
  14. import (
  15. "fmt"
  16. "unsafe"
  17. )
  18. func main() {
  19. f := &C.struct_Foo{}
  20. f.a = 5
  21. f.p = (*C.int)((unsafe.Pointer)(new(int)))
  22. C.plusOne(&f.a)
  23. fmt.Println(int(f.a))
  24. }

上述程序的运行结果:

  1. $go run cgo1_structfield.go
  2. 6

3、传递一个指向slice or array中的element的指针

和传递struct field不同,传递一个指向slice or array中的element的指针时,需要考虑的Go Memory的范围不仅仅是这个element,而是整个Array或整个slice背后的underlying array所占用的内存区域,要保证这个区域内不包含指向任意Go Memory的指针。我们来看代码示例:

  1. //cgo1_sliceelem.go
  2. package main
  3. /*
  4. #include <stdio.h>
  5. void plusOne(int **i) {
  6. (**i)++;
  7. }
  8. */
  9. import "C"
  10. import (
  11. "fmt"
  12. "unsafe"
  13. )
  14. func main() {
  15. sl := make([]*int, 5)
  16. var a int = 5
  17. sl[1] = &a
  18. C.plusOne((**C.int)((unsafe.Pointer)(&sl[0])))
  19. fmt.Println(sl[0])
  20. }

从这个代码中,我们看到我们传递的是slice的第一个element的地址,即&sl[0]。我们并未给sl[0]赋值,但sl[1] 被赋值为另外一块go memory的address(&a),当我们将&sl[0]传递给plusOne时,执行结果如下:

  1. $go run cgo1_sliceelem.go
  2. panic: runtime error: cgo argument has Go pointer to Go pointer
  3. goroutine 1 [running]:
  4. panic(0x40dbac0, 0xc8200621d0)
  5. /Users/tony/.bin/go16/src/runtime/panic.go:464 +0x3e6
  6. main.main()
  7. /Users/tony/test/go/go16/cgo/cgo1_sliceelem.go:19 +0xe4
  8. exit status 2

由于违背规则,因此runtime panic了。

(二) C调用Go Code时

1、C调用的Go函数不能返回指向Go分配的内存的指针

我们看下面例子:

//cgo2_1.go

  1. package main
  2. // extern int* goAdd(int, int);
  3. //
  4. // static int cAdd(int a, int b) {
  5. // int *i = goAdd(a, b);
  6. // return *i;
  7. // }
  8. import "C"
  9. import "fmt"
  10. //export goAdd
  11. func goAdd(a, b C.int) *C.int {
  12. c := a + b
  13. return &c
  14. }
  15. func main() {
  16. var a, b int = 5, 6
  17. i := C.cAdd(C.int(a), C.int(b))
  18. fmt.Println(int(i))
  19. }

可以看到:goAdd这个Go函数返回了一个指向Go分配的内存(&c)的指针。运行上述代码,结果如下:

  1. $go run cgo2_1.go
  2. panic: runtime error: cgo result has Go pointer
  3. goroutine 1 [running]:
  4. panic(0x40dba40, 0xc82006e1c0)
  5. /Users/tony/.bin/go16/src/runtime/panic.go:464 +0x3e6
  6. main._cgoexpwrap_872b2f2e7532_goAdd.func1(0xc820049d98)
  7. command-line-arguments/_obj/_cgo_gotypes.go:64 +0x3a
  8. main._cgoexpwrap_872b2f2e7532_goAdd(0x600000005, 0xc82006e19c)
  9. command-line-arguments/_obj/_cgo_gotypes.go:66 +0x89
  10. main._Cfunc_cAdd(0x600000005, 0x0)
  11. command-line-arguments/_obj/_cgo_gotypes.go:45 +0x41
  12. main.main()
  13. /Users/tony/test/go/go16/cgo/cgo2_1.go:20 +0x35
  14. exit status 2

2、Go code不能在C分配的内存中存储指向Go分配的内存的指针

下面的例子模拟了这一情况:

  1. //cgo2_2.go
  2. package main
  3. // #include <stdlib.h>
  4. // extern void goFoo(int**);
  5. //
  6. // static void cFoo() {
  7. // int **p = malloc(sizeof(int*));
  8. // goFoo(p);
  9. // }
  10. import "C"
  11. //export goFoo
  12. func goFoo(p **C.int) {
  13. *p = new(C.int)
  14. }
  15. func main() {
  16. C.cFoo()
  17. }

不过针对此例,默认的GODEBUG=cgocheck=1偏是无法check出问题。我们将GODEBUG=cgocheck改为=2试试:

  1. $GODEBUG=cgocheck=2 go run cgo2_2.go
  2. write of Go pointer 0xc82000a0f8 to non-Go memory 0x4300000
  3. fatal error: Go pointer stored into non-Go memory
  4. runtime stack:
  5. runtime.throw(0x4089800, 0x24)
  6. /Users/tony/.bin/go16/src/runtime/panic.go:530 +0x90
  7. runtime.cgoCheckWriteBarrier.func1()
  8. /Users/tony/.bin/go16/src/runtime/cgocheck.go:44 +0xae
  9. runtime.systemstack(0x7fff5fbff8c0)
  10. /Users/tony/.bin/go16/src/runtime/asm_amd64.s:291 +0x79
  11. runtime.mstart()
  12. /Users/tony/.bin/go16/src/runtime/proc.go:1048
  13. ... ...
  14. goroutine 17 [syscall, locked to thread]:
  15. runtime.goexit()
  16. /Users/tony/.bin/go16/src/runtime/asm_amd64.s:1998 +0x1
  17. exit status 2

果真runtime panic: write of Go pointer 0xc82000a0f8 to non-Go memory 0×4300000

二、HTTP/2

HTTP/2原本是bradfitz维护的x项目,之前位于golang.org/x/net/http2包中,Go 1.6无缝合入Go标准库net/http包中。并且当你你使用https时,client和server端将自动默认使用HTTP/2协议。

HTTP/2与HTTP1.x协议不同在于其为二进制协议,而非文本协议,性能自是大幅提升。HTTP/2标准已经发布,想必未来若干年将大行其道。

HTTP/2较为复杂,这里不赘述,后续maybe会单独写一篇GO和http2的文章说明。

三、Templates

由于不开发web,templates我日常用的很少。这里粗浅说说templates增加的两个Feature

trim空白字符

Go templates的空白字符包括:空格、水平tab、回车和换行符。在日常编辑模板时,这些空白尤其难于处理,由于是对beatiful format和code readabliity有“强迫症”的同学,更是在这方面话费了不少时间。

Go 1.6提供了{{-和-}}来帮助大家去除action前后的空白字符。下面的例子很好的说明了这一点:

  1. //trimwhitespace.go
  2. package main
  3. import (
  4. "log"
  5. "os"
  6. "text/template"
  7. )
  8. var items = []string{"one", "two", "three"}
  9. func tmplbefore15() {
  10. var t = template.Must(template.New("tmpl").Parse(`
  11. <ul>
  12. {{range . }}
  13. <li>{{.}}</li>
  14. {{end }}
  15. </ul>
  16. `))
  17. err := t.Execute(os.Stdout, items)
  18. if err != nil {
  19. log.Println("executing template:", err)
  20. }
  21. }
  22. func tmplaftergo16() {
  23. var t = template.Must(template.New("tmpl").Parse(`
  24. <ul>
  25. {{range . -}}
  26. <li>{{.}}</li>
  27. {{end -}}
  28. </ul>
  29. `))
  30. err := t.Execute(os.Stdout, items)
  31. if err != nil {
  32. log.Println("executing template:", err)
  33. }
  34. }
  35. func main() {
  36. tmplbefore15()
  37. tmplaftergo16()
  38. }

这个例子的运行结果:

  1. $go run trimwhitespace.go
  2. <ul>
  3. <li>one</li>
  4. <li>two</li>
  5. <li>three</li>
  6. </ul>
  7. <ul>
  8. <li>one</li>
  9. <li>two</li>
  10. <li>three</li>
  11. </ul>

block action

block action提供了一种在运行时override已有模板形式的能力。

  1. package main
  2. import (
  3. "log"
  4. "os"
  5. "text/template"
  6. )
  7. var items = []string{"one", "two", "three"}
  8. var overrideItemList = `
  9. {{define "list" -}}
  10. <ul>
  11. {{range . -}}
  12. <li>{{.}}</li>
  13. {{end -}}
  14. </ul>
  15. {{end}}
  16. `
  17. var tmpl = `
  18. Items:
  19. {{block "list" . -}}
  20. <ul>
  21. {{range . }}
  22. <li>{{.}}</li>
  23. {{end }}
  24. </ul>
  25. {{end}}
  26. `
  27. var t *template.Template
  28. func init() {
  29. t = template.Must(template.New("tmpl").Parse(tmpl))
  30. }
  31. func tmplBeforeOverride() {
  32. err := t.Execute(os.Stdout, items)
  33. if err != nil {
  34. log.Println("executing template:", err)
  35. }
  36. }
  37. func tmplafterOverride() {
  38. t = template.Must(t.Parse(overrideItemList))
  39. err := t.Execute(os.Stdout, items)
  40. if err != nil {
  41. log.Println("executing template:", err)
  42. }
  43. }
  44. func main() {
  45. fmt.Println("before override:")
  46. tmplBeforeOverride()
  47. fmt.Println("after override:")
  48. tmplafterOverride()
  49. }

原模板tmpl中通过block action定义了一处名为list的内嵌模板锚点以及初始定义。后期运行时通过re-parse overrideItemList达到修改模板展示形式的目的。

上述代码输出结果:

  1. $go run blockaction.go
  2. before override:
  3. Items:
  4. <ul>
  5. <li>one</li>
  6. <li>two</li>
  7. <li>three</li>
  8. </ul>
  9. after override:
  10. Items:
  11. <ul>
  12. <li>one</li>
  13. <li>two</li>
  14. <li>three</li>
  15. </ul>

四、Runtime

降低大内存使用时的GC latency

Go 1.5.x用降低一些吞吐量的代价换取了10ms以下的GC latency。不过针对Go 1.5,官方给出的benchmark图中,内存heap size最多20G左右。一旦超过20G,latency将超过10ms,也许会线性增长。
在Go 1.6中,官方给出的benchmark图中当内存heap size在200G时,GC latency依旧可以稳定在10ms;在heap size在20G以下时,latency降到了6ms甚至更小。

panic info

Go 1.6之前版本,一旦程序以panic方式退出,runtime便会将所有goroutine的stack信息打印出来:

  1. $go version
  2. go version go1.5.2 darwin/amd64
  3. [ ~/test/go/go16/runtime]$go run panic.go
  4. panic: runtime error: invalid memory address or nil pointer dereference
  5. [signal 0xb code=0x1 addr=0x0 pc=0x20d5]
  6. goroutine 1 [running]:
  7. main.main()
  8. /Users/tony/test/go/go16/runtime/panic.go:19 +0x95
  9. goroutine 4 [select (no cases)]:
  10. main.main.func1(0x8200f40f0)
  11. /Users/tony/test/go/go16/runtime/panic.go:13 +0x26
  12. created by main.main
  13. /Users/tony/test/go/go16/runtime/panic.go:14 +0x72
  14. ... ...

而Go 1.6后,Go只会打印正在running的goroutine的stack信息,因此它才是最有可能造成panic的真正元凶:

  1. go 1.6
  2. $go run panic.go
  3. panic: runtime error: invalid memory address or nil pointer dereference
  4. [signal 0xb code=0x1 addr=0x0 pc=0x20d5]
  5. goroutine 1 [running]:
  6. panic(0x61e80, 0x8200ee0c0)
  7. /Users/tony/.bin/go16/src/runtime/panic.go:464 +0x3e6
  8. main.main()
  9. /Users/tony/test/go/go16/runtime/panic.go:19 +0x95
  10. exit status 2

map race detect

Go原生的map类型是goroutine-unsafe的,长久以来,这给很多Gophers带来了烦恼。这次Go 1.6中Runtime增加了对并发访问map的检测以降低gopher们使用map时的心智负担。

这里借用了Francesc Campoy在最近一期”The State of Go”中的示例程序:

  1. package main
  2. import "sync"
  3. func main() {
  4. const workers = 100
  5. var wg sync.WaitGroup
  6. wg.Add(workers)
  7. m := map[int]int{}
  8. for i := 1; i <= workers; i++ {
  9. go func(i int) {
  10. for j := 0; j < i; j++ {
  11. m[i]++
  12. }
  13. wg.Done()
  14. }(i)
  15. }
  16. wg.Wait()
  17. }

执行结果:

  1. $ go run map.go
  2. fatal error: concurrent map writes
  3. fatal error: concurrent map writes
  4. ... ...

这里在双核i5 mac air下亲测时,发现当workers=2,3,4均不能检测出race。当workers >= 5时可以检测到。

五、其他

手写parser替代yacc生成的parser

这个变化对Gopher们是透明的,但对于Go compiler本身却是十分重要的。

Robert Riesemer在Go 1.6代码Freezing前commit了手写Parser,以替代yacc生成的parser。在AMA上RobPike给出了更换Parser的些许理由:
1、Go compiler可以少维护一个yacc工具,这样更加cleaner;
2、手写Parser在性能上可以快那么一点点。

Go 1.6中GO15VENDOREXPERIMENT将默认开启

根据当初在Go 1.5中引入vendor时的计划,Go 1.6中GO15VENDOREXPERIMENT将默认开启。这显然会导致一些不兼容的情况出现:即如果你的代码在之前并未使用vendor机制,但目录组织中有vendor目录。Go Core team给出的解决方法就是删除vendor目录或改名。

遗留问题是否解决

Go 1.5发布后,曾经发现两个问题,直到Go 1.5.3版本发布也未曾解决,那么Go 1.6是否解决了呢?我们来验证一下。

internal问题

该问题的具体细节可参看我在go github上提交的issue 12217,我在自己的experiments中提交了问题的验证环境代码,这次我们使用Go 1.6看看internal问题是否还存在:

  1. $cd $GOPATH/src/github.com/bigwhite/experiments/go15-internal-issue-12217
  2. $cd otherpkg/
  3. $go build main.go
  4. package main
  5. imports github.com/bigwhite/experiments/go15-internal-issue-12217/mypkg/internal/foo: use of internal package not allowed

这回go compiler给出了error,而不是像之前版本那样顺利编译通过。看来这个问题是fix掉了。

GOPATH之外vendor机制是否起作用的问题

我们先建立实验环境:

  1. $tree
  2. .
  3. └── testvendor
  4. └── src
  5. └── proj1
  6. ├── main.go
  7. └── vendor
  8. └── github.com
  9. └── bigwhite
  10. └── foo
  11. └── foolib.go

进入proj1,build main.go

  1. go build main.go
  2. main.go:3:8: cannot find package "github.com/bigwhite/foo" in any of:
  3. /Users/tony/.bin/go16/src/github.com/bigwhite/foo (from $GOROOT)
  4. /Users/tony/Test/GoToolsProjects/src/github.com/bigwhite/foo (from $GOPATH)

go 1.6编译器没有关注同路径下的vendor目录,build失败。

我们设置GOPATH=xxx/testvendor后,再来build:

  1. $export GOPATH=~/Test/go/go16/others/testvendor
  2. $go run main.go
  3. Hello from temp vendor

这回编译运行ok。

由此看来,Go 1.6 vendor在GOPATH外依旧不生效。

六、小结

Go 1.6标准库细微变化还是有很多的,在Go 1.6 Release Notes中可细细品味。

Go 1.6的编译速度、编译出的程序的运行性能与Go 1.5.x也大致无二异。

另外本文实现环境如下:

  1. go version go1.6 darwin/amd64
  2. Darwin tonydeair-2.lan 13.1.0 Darwin Kernel Version 13.1.0: Thu Jan 16 19:40:37 PST 2014; root:xnu-2422.90.20~2/RELEASE_X86_64 x86_64

实验代码可在这里下载。

© 2016, bigwhite. 版权所有.

Related posts:

  1. Go 1.5中值得关注的几个变化
  2. 近期遇到的3个Golang代码问题
  3. 理解Golang包导入
  4. 一个有关Golang变量作用域的坑
  5. 理解Go 1.5 vendor
ft_authoradmin  ft_create_time2019-06-21 18:42
 ft_update_time2019-06-21 18:42