标签 汇编 下的文章

Goroutine调度实例简要分析

前两天一位网友在微博私信我这样一个问题:

抱歉打扰您咨询您一个关于Go的问题:对于goroutine的概念我是明了的,但很疑惑goroutine的调度问题, 根据《Go语言编程》一书:“当一个任务正在执行时,外部没有办法终止它。要进行任务切换,只能通过由该任务自身调用yield()来主动出让CPU使用权。” 那么,假设我的goroutine是一个死循环的话,是否其它goroutine就没有执行的机会呢?我测试的结果是这些goroutine会轮流执行。那么除了syscall时会主动出让cpu时间外,我的死循环goroutine 之间是怎么做到切换的呢?

我在第一时间做了回复。不过由于并不了解具体的细节,我在答复中做了一个假定,即假定这位网友的死循环带中没有调用任何可以交出执行权的代码。事后,这位网友在他的回复后道出了死循环goroutine切换的真实原因:他在死循环中调用了fmt.Println

事后总觉得应该针对这个问题写点什么? 于是就构思了这样一篇文章,旨在循着这位网友的思路通过一些例子来step by step演示如何分析go schedule。如果您对Goroutine的调度完全不了解,那么请先读一读这篇前导文 《也谈goroutine调度器》

一、为何在deadloop的参与下,多个goroutine依旧会轮流执行

我们先来看case1,我们顺着那位网友的思路来构造第一个例子,并回答:“为何在deadloop的参与下,多个goroutine依旧会轮流执行?”这个问题。下面是case1的源码:

//github.com/bigwhite/experiments/go-sched-examples/case1.go
package main

import (
    "fmt"
    "time"
)

func deadloop() {
    for {
    }
}

func main() {
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}

在case1.go中,我们启动了两个goroutine,一个是main goroutine,一个是deadloop goroutine。deadloop goroutine顾名思义,其逻辑是一个死循环;而main goroutine为了展示方便,也用了一个“死循环”,并每隔一秒钟打印一条信息。在我的macbook air上运行这个例子(我的机器是两核四线程的,runtime的NumCPU函数返回4):

$go run case1.go
I got scheduled!
I got scheduled!
I got scheduled!
... ...

从运行结果输出的日志来看,尽管有deadloop goroutine的存在,main goroutine仍然得到了调度。其根本原因在于机器是多核多线程的(硬件线程哦,不是操作系统线程)。Go从1.5版本之后将默认的P的数量改为 = CPU core的数量(实际上还乘以了每个core上硬线程数量),这样case1在启动时创建了不止一个P,我们用一幅图来解释一下:

img{512x368}

我们假设deadloop Goroutine被调度与P1上,P1在M1(对应一个os kernel thread)上运行;而main goroutine被调度到P2上,P2在M2上运行,M2对应另外一个os kernel thread,而os kernel threads在操作系统调度层面被调度到物理的CPU core上运行,而我们有多个core,即便deadloop占满一个core,我们还可以在另外一个cpu core上运行P2上的main goroutine,这也是main goroutine得到调度的原因。

Tips: 在mac os上查看你的硬件cpu core数量和硬件线程总数量:

$sysctl -n machdep.cpu.core_count
2
$sysctl -n machdep.cpu.thread_count
4

二、如何让deadloop goroutine以外的goroutine无法得到调度?

如果我们非要deadloop goroutine以外的goroutine无法得到调度,我们该如何做呢?一种思路:让Go runtime不要启动那么多P,让所有用户级的goroutines在一个P上被调度。

三种办法:

  • 在main函数的最开头处调用runtime.GOMAXPROCS(1);
  • 设置环境变量export GOMAXPROCS=1后再运行程序
  • 找一个单核单线程的机器^0^(现在这样的机器太难找了,只能使用云服务器实现)

我们以第一种方法为例:

//github.com/bigwhite/experiments/go-sched-examples/case2.go
package main

import (
    "fmt"
    "runtime"
    "time"
)

func deadloop() {
    for {
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}

运行这个程序后,你会发现main goroutine的”I got scheduled”字样再也无法输出了。这里的调度原理可以用下面图示说明:

img{512x368}

deadloop goroutine在P1上被调度,由于deadloop内部逻辑没有给调度器任何抢占的机会,比如:进入runtime.morestack_noctxt。于是即便是sysmon这样的监控goroutine,也仅仅是能给deadloop goroutine的抢占标志位设为true而已。由于deadloop内部没有任何进入调度器代码的机会,Goroutine重新调度始终无法发生。main goroutine只能躺在P1的local queue中徘徊着。

三、反转:如何在GOMAXPROCS=1的情况下,让main goroutine得到调度呢?

我们做个反转:如何在GOMAXPROCS=1的情况下,让main goroutine得到调度呢?听说在Go中 “有函数调用,就有了进入调度器代码的机会”,我们来试验一下是否属实。我们在deadloop goroutine的for-loop逻辑中加上一个函数调用:

// github.com/bigwhite/experiments/go-sched-examples/case3.go
package main

import (
    "fmt"
    "runtime"
    "time"
)

func add(a, b int) int {
    return a + b
}

func deadloop() {
    for {
        add(3, 5)
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}

我们在deadloop goroutine的for loop中加入了一个add函数调用。我们来运行一下这个程序,看是否能达成我们的目的:

$ go run case3.go

“I got scheduled!”字样依旧没有出现在我们眼前!也就是说main goroutine没有得到调度!为什么呢?其实所谓的“有函数调用,就有了进入调度器代码的机会”,实际上是go compiler在函数的入口处插入了一个runtime的函数调用:runtime.morestack_noctxt。这个函数会检查是否扩容连续栈,并进入抢占调度的逻辑中。一旦所在goroutine被置为可被抢占的,那么抢占调度代码就会剥夺该Goroutine的执行权,将其让给其他goroutine。但是上面代码为什么没有实现这一点呢?我们需要在汇编层次看看go compiler生成的代码是什么样子的。

查看Go程序的汇编代码有许多种方法:

  • 使用objdump工具:objdump -S go-binary
  • 使用gdb disassemble
  • 构建go程序同时生成汇编代码文件:go build -gcflags ‘-S’ xx.go > xx.s 2>&1
  • 将Go代码编译成汇编代码:go tool compile -S xx.go > xx.s
  • 使用go tool工具反编译Go程序:go tool objdump -S go-binary > xx.s

我们这里使用最后一种方法:利用go tool objdump反编译(并结合其他输出的汇编形式):

$go build -o case3 case3.go
$go tool objdump -S case3 > case3.s

打开case3.s,搜索main.add,我们居然找不到这个函数的汇编代码,而main.deadloop的定义如下:

TEXT main.deadloop(SB) github.com/bigwhite/experiments/go-sched-examples/case3.go
        for {
  0x1093a10             ebfe                    JMP main.deadloop(SB)

  0x1093a12             cc                      INT $0x3
  0x1093a13             cc                      INT $0x3
  0x1093a14             cc                      INT $0x3
  0x1093a15             cc                      INT $0x3
   ... ...
  0x1093a1f             cc                      INT $0x3

我们看到deadloop中对add的调用也消失了。这显然是go compiler执行生成代码优化的结果,因为add的调用对deadloop的行为结果没有任何影响。我们关闭优化再来试试:

$go build -gcflags '-N -l' -o case3-unoptimized case3.go
$go tool objdump -S case3-unoptimized > case3-unoptimized.s

打开 case3-unoptimized.s查找main.add,这回我们找到了它:

TEXT main.add(SB) github.com/bigwhite/experiments/go-sched-examples/case3.go
func add(a, b int) int {
  0x1093a10             48c744241800000000      MOVQ $0x0, 0x18(SP)
        return a + b
  0x1093a19             488b442408              MOVQ 0x8(SP), AX
  0x1093a1e             4803442410              ADDQ 0x10(SP), AX
  0x1093a23             4889442418              MOVQ AX, 0x18(SP)
  0x1093a28             c3                      RET

  0x1093a29             cc                      INT $0x3
... ...
  0x1093a2f             cc                      INT $0x3

deadloop中也有了对add的显式调用:

TEXT main.deadloop(SB) github.com/bigwhite/experiments/go-sched-examples/case3.go
  ... ...
  0x1093a51             48c7042403000000        MOVQ $0x3, 0(SP)
  0x1093a59             48c744240805000000      MOVQ $0x5, 0x8(SP)
  0x1093a62             e8a9ffffff              CALL main.add(SB)
        for {
  0x1093a67             eb00                    JMP 0x1093a69
  0x1093a69             ebe4                    JMP 0x1093a4f
... ...

不过我们这个程序中的main goroutine依旧得不到调度,因为在main.add代码中,我们没有发现morestack函数的踪迹,也就是说即便调用了add函数,deadloop也没有机会进入到runtime的调度逻辑中去。

不过,为什么Go compiler没有在main.add函数中插入morestack的调用呢?那是因为add函数位于调用树的leaf(叶子)位置,compiler可以确保其不再有新栈帧生成,不会导致栈分裂或超出现有栈边界,于是就不再插入morestack。而位于morestack中的调度器的抢占式检查也就无法得以执行。下面是go build -gcflags ‘-S’方式输出的case3.go的汇编输出:

"".add STEXT nosplit size=19 args=0x18 locals=0x0
     TEXT    "".add(SB), NOSPLIT, $0-24
     FUNCDATA        $0, gclocals·54241e171da8af6ae173d69da0236748(SB)
     FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
     MOVQ    "".b+16(SP), AX
     MOVQ    "".a+8(SP), CX
     ADDQ    CX, AX
     MOVQ    AX, "".~r2+24(SP)
    RET

我们看到nosplit字样,这就说明add使用的栈是固定大小,不会再split,且size为24字节。

关于在for loop中的leaf function是否应该插入morestack目前还有一定争议,将来也许会对这样的情况做特殊处理。

既然明白了原理,我们就在deadloop和add之间加入一个dummy函数,见下面case4.go代码:

//github.com/bigwhite/experiments/go-sched-examples/case4.go
package main

import (
    "fmt"
    "runtime"
    "time"
)

//go:noinline
func add(a, b int) int {
    return a + b
}

func dummy() {
    add(3, 5)
}

func deadloop() {
    for {
        dummy()
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}

执行该代码:

$go run case4.go
I got scheduled!
I got scheduled!
I got scheduled!

Wow! main goroutine果然得到了调度。我们再来看看go compiler为程序生成的汇编代码:

$go build -gcflags '-N -l' -o case4 case4.go
$go tool objdump -S case4 > case4.s

TEXT main.add(SB) github.com/bigwhite/experiments/go-sched-examples/case4.go
func add(a, b int) int {
  0x1093a10             48c744241800000000      MOVQ $0x0, 0x18(SP)
        return a + b
  0x1093a19             488b442408              MOVQ 0x8(SP), AX
  0x1093a1e             4803442410              ADDQ 0x10(SP), AX
  0x1093a23             4889442418              MOVQ AX, 0x18(SP)
  0x1093a28             c3                      RET

  0x1093a29             cc                      INT $0x3
  0x1093a2a             cc                      INT $0x3
... ...

TEXT main.dummy(SB) github.com/bigwhite/experiments/go-sched-examples/case4.s
func dummy() {
  0x1093a30             65488b0c25a0080000      MOVQ GS:0x8a0, CX
  0x1093a39             483b6110                CMPQ 0x10(CX), SP
  0x1093a3d             762e                    JBE 0x1093a6d
  0x1093a3f             4883ec20                SUBQ $0x20, SP
  0x1093a43             48896c2418              MOVQ BP, 0x18(SP)
  0x1093a48             488d6c2418              LEAQ 0x18(SP), BP
        add(3, 5)
  0x1093a4d             48c7042403000000        MOVQ $0x3, 0(SP)
  0x1093a55             48c744240805000000      MOVQ $0x5, 0x8(SP)
  0x1093a5e             e8adffffff              CALL main.add(SB)
}
  0x1093a63             488b6c2418              MOVQ 0x18(SP), BP
  0x1093a68             4883c420                ADDQ $0x20, SP
  0x1093a6c             c3                      RET

  0x1093a6d             e86eacfbff              CALL runtime.morestack_noctxt(SB)
  0x1093a72             ebbc                    JMP main.dummy(SB)

  0x1093a74             cc                      INT $0x3
  0x1093a75             cc                      INT $0x3
  0x1093a76             cc                      INT $0x3
.... ....

我们看到main.add函数依旧是leaf,没有morestack插入;但在新增的dummy函数中我们看到了CALL runtime.morestack_noctxt(SB)的身影。

四、为何runtime.morestack_noctxt(SB)放到了RET后面?

在传统印象中,morestack是放在函数入口处的。但实际编译出来的汇编代码中(见上面函数dummy的汇编),runtime.morestack_noctxt(SB)却放在了RET的后面。解释这个问题,我们最好来看一下另外一种形式的汇编输出(go build -gcflags ‘-S’方式输出的格式):

"".dummy STEXT size=68 args=0x0 locals=0x20
        0x0000 00000 TEXT    "".dummy(SB), $32-0
        0x0000 00000 MOVQ    (TLS), CX
        0x0009 00009 CMPQ    SP, 16(CX)
        0x000d 00013 JLS     61
        0x000f 00015 SUBQ    $32, SP
        0x0013 00019 MOVQ    BP, 24(SP)
        0x0018 00024 LEAQ    24(SP), BP
        ... ...
        0x001d 00029 MOVQ    $3, (SP)
        0x0025 00037 MOVQ    $5, 8(SP)
        0x002e 00046 PCDATA  $0, $0
        0x002e 00046 CALL    "".add(SB)
        0x0033 00051 MOVQ    24(SP), BP
        0x0038 00056 ADDQ    $32, SP
        0x003c 00060 RET
        0x003d 00061 NOP
        0x003d 00061 PCDATA  $0, $-1
        0x003d 00061 CALL    runtime.morestack_noctxt(SB)
        0x0042 00066 JMP     0

我们看到在函数入口处,compiler插入三行汇编:

        0x0000 00000 MOVQ    (TLS), CX  // 将TLS的值(GS:0x8a0)放入CX寄存器
        0x0009 00009 CMPQ    SP, 16(CX)  //比较SP与CX+16的值
        0x000d 00013 JLS     61 // 如果SP > CX + 16,则jump到61这个位置

这种形式输出的是标准Plan9的汇编语法,资料很少(比如JLS跳转指令的含义),注释也是大致猜测的。如果跳转,则进入到 runtime.morestack_noctxt,从 runtime.morestack_noctxt返回后,再次jmp到开头执行。

为什么要这么做呢?按照go team的说法,是为了更好的利用现代CPU的“static branch prediction”,提升执行性能。

五、参考资料

文中的代码可以点击这里下载。


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

微信赞赏:
img{512x368}

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

北京时间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的指针

//cgo1_struct.go
package main

/*
#include <stdio.h>
struct Foo{
    int a;
    int *p;
};

void plusOne(struct Foo *f) {
    (f->a)++;
    *(f->p)++;
}
*/
import "C"
import "unsafe"
import "fmt"

func main() {
    f := &C.struct_Foo{}
    f.a = 5
    f.p = (*C.int)((unsafe.Pointer)(new(int)))
    //f.p = &f.a

    C.plusOne(f)
    fmt.Println(int(f.a))
}

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

$go run cgo1_struct.go
# command-line-arguments
./cgo1_struct.go:12:2: warning: expression result unused [-Wunused-value]
panic: runtime error: cgo argument has Go pointer to Go pointer

goroutine 1 [running]:
panic(0x4068400, 0xc82000a110)
    /Users/tony/.bin/go16/src/runtime/panic.go:464 +0x3e6
main.main()
    /Users/tony/test/go/go16/cgo/cgo1_struct.go:24 +0xb9
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也不打紧:

//cgo1_structfield.go
package main

/*
#include <stdio.h>
struct Foo{
    int a;
    int *p;
};

void plusOne(int *i) {
    (*i)++;
}
*/
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    f := &C.struct_Foo{}
    f.a = 5
    f.p = (*C.int)((unsafe.Pointer)(new(int)))

    C.plusOne(&f.a)
    fmt.Println(int(f.a))
}

上述程序的运行结果:

$go run cgo1_structfield.go
6

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

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

//cgo1_sliceelem.go
package main

/*
#include <stdio.h>
void plusOne(int **i) {
    (**i)++;
}
*/
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    sl := make([]*int, 5)
    var a int = 5
    sl[1] = &a
    C.plusOne((**C.int)((unsafe.Pointer)(&sl[0])))
    fmt.Println(sl[0])
}

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

$go run cgo1_sliceelem.go
panic: runtime error: cgo argument has Go pointer to Go pointer

goroutine 1 [running]:
panic(0x40dbac0, 0xc8200621d0)
    /Users/tony/.bin/go16/src/runtime/panic.go:464 +0x3e6
main.main()
    /Users/tony/test/go/go16/cgo/cgo1_sliceelem.go:19 +0xe4
exit status 2

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

(二) C调用Go Code时

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

我们看下面例子:

//cgo2_1.go

package main

// extern int* goAdd(int, int);
//
// static int cAdd(int a, int b) {
//     int *i = goAdd(a, b);
//     return *i;
// }
import "C"
import "fmt"

//export goAdd
func goAdd(a, b C.int) *C.int {
    c := a + b
    return &c
}

func main() {
    var a, b int = 5, 6
    i := C.cAdd(C.int(a), C.int(b))
    fmt.Println(int(i))
}

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

$go run cgo2_1.go
panic: runtime error: cgo result has Go pointer

goroutine 1 [running]:
panic(0x40dba40, 0xc82006e1c0)
    /Users/tony/.bin/go16/src/runtime/panic.go:464 +0x3e6
main._cgoexpwrap_872b2f2e7532_goAdd.func1(0xc820049d98)
    command-line-arguments/_obj/_cgo_gotypes.go:64 +0x3a
main._cgoexpwrap_872b2f2e7532_goAdd(0x600000005, 0xc82006e19c)
    command-line-arguments/_obj/_cgo_gotypes.go:66 +0x89
main._Cfunc_cAdd(0x600000005, 0x0)
    command-line-arguments/_obj/_cgo_gotypes.go:45 +0x41
main.main()
    /Users/tony/test/go/go16/cgo/cgo2_1.go:20 +0x35
exit status 2

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

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

//cgo2_2.go
package main

// #include <stdlib.h>
// extern void goFoo(int**);
//
// static void cFoo() {
//     int **p = malloc(sizeof(int*));
//     goFoo(p);
// }
import "C"

//export goFoo
func goFoo(p **C.int) {
    *p = new(C.int)
}

func main() {
    C.cFoo()
}

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

$GODEBUG=cgocheck=2 go run cgo2_2.go
write of Go pointer 0xc82000a0f8 to non-Go memory 0x4300000
fatal error: Go pointer stored into non-Go memory

runtime stack:
runtime.throw(0x4089800, 0x24)
    /Users/tony/.bin/go16/src/runtime/panic.go:530 +0x90
runtime.cgoCheckWriteBarrier.func1()
    /Users/tony/.bin/go16/src/runtime/cgocheck.go:44 +0xae
runtime.systemstack(0x7fff5fbff8c0)
    /Users/tony/.bin/go16/src/runtime/asm_amd64.s:291 +0x79
runtime.mstart()
    /Users/tony/.bin/go16/src/runtime/proc.go:1048
... ...
goroutine 17 [syscall, locked to thread]:
runtime.goexit()
    /Users/tony/.bin/go16/src/runtime/asm_amd64.s:1998 +0x1
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前后的空白字符。下面的例子很好的说明了这一点:

//trimwhitespace.go
package main

import (
    "log"
    "os"
    "text/template"
)

var items = []string{"one", "two", "three"}

func tmplbefore15() {
    var t = template.Must(template.New("tmpl").Parse(`
    <ul>
    {{range . }}
        <li>{{.}}</li>
    {{end }}
    </ul>
    `))

    err := t.Execute(os.Stdout, items)
    if err != nil {
        log.Println("executing template:", err)
    }
}

func tmplaftergo16() {
    var t = template.Must(template.New("tmpl").Parse(`
    <ul>
    {{range . -}}
        <li>{{.}}</li>
    {{end -}}
    </ul>
    `))

    err := t.Execute(os.Stdout, items)
    if err != nil {
        log.Println("executing template:", err)
    }
}

func main() {
    tmplbefore15()
    tmplaftergo16()
}

这个例子的运行结果:

$go run trimwhitespace.go

    <ul>

        <li>one</li>

        <li>two</li>

        <li>three</li>

    </ul>

    <ul>
    <li>one</li>
    <li>two</li>
    <li>three</li>
    </ul>

block action

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

package main

import (
    "log"
    "os"
    "text/template"
)

var items = []string{"one", "two", "three"}

var overrideItemList = `
{{define "list" -}}
    <ul>
    {{range . -}}
        <li>{{.}}</li>
    {{end -}}
    </ul>
{{end}}
`

var tmpl = `
    Items:
    {{block "list" . -}}
    <ul>
    {{range . }}
        <li>{{.}}</li>
    {{end }}
    </ul>
    {{end}}
`

var t *template.Template

func init() {
    t = template.Must(template.New("tmpl").Parse(tmpl))
}

func tmplBeforeOverride() {
    err := t.Execute(os.Stdout, items)
    if err != nil {
        log.Println("executing template:", err)
    }
}

func tmplafterOverride() {
    t = template.Must(t.Parse(overrideItemList))
    err := t.Execute(os.Stdout, items)
    if err != nil {
        log.Println("executing template:", err)
    }
}

func main() {
    fmt.Println("before override:")
    tmplBeforeOverride()
    fmt.Println("after override:")
    tmplafterOverride()
}

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

上述代码输出结果:

$go run blockaction.go
before override:

    Items:
    <ul>

        <li>one</li>

        <li>two</li>

        <li>three</li>

    </ul>

after override:

    Items:
    <ul>
    <li>one</li>
    <li>two</li>
    <li>three</li>
    </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信息打印出来:

$go version
go version go1.5.2 darwin/amd64
[ ~/test/go/go16/runtime]$go run panic.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x0 pc=0x20d5]

goroutine 1 [running]:
main.main()
    /Users/tony/test/go/go16/runtime/panic.go:19 +0x95

goroutine 4 [select (no cases)]:
main.main.func1(0x8200f40f0)
    /Users/tony/test/go/go16/runtime/panic.go:13 +0x26
created by main.main
    /Users/tony/test/go/go16/runtime/panic.go:14 +0x72
... ...

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

go 1.6:
$go run panic.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x0 pc=0x20d5]

goroutine 1 [running]:
panic(0x61e80, 0x8200ee0c0)
    /Users/tony/.bin/go16/src/runtime/panic.go:464 +0x3e6
main.main()
    /Users/tony/test/go/go16/runtime/panic.go:19 +0x95
exit status 2

map race detect

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

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

package main

import "sync"

func main() {
    const workers = 100

    var wg sync.WaitGroup
    wg.Add(workers)
    m := map[int]int{}
    for i := 1; i <= workers; i++ {
        go func(i int) {
            for j := 0; j < i; j++ {
                m[i]++
            }
            wg.Done()
        }(i)
    }
    wg.Wait()
}

执行结果:

$ go run map.go
fatal error: concurrent map writes
fatal error: concurrent map writes
... ...

这里在双核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问题是否还存在:

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

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

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

我们先建立实验环境:

$tree
.
└── testvendor
    └── src
        └── proj1
            ├── main.go
            └── vendor
                └── github.com
                    └── bigwhite
                        └── foo
                            └── foolib.go

进入proj1,build main.go

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

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

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

$export GOPATH=~/Test/go/go16/others/testvendor
$go run main.go
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也大致无二异。

另外本文实现环境如下:

go version go1.6 darwin/amd64
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

实验代码可在这里下载。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats