来自公众号:新世界杂货铺

神奇的景象

切片, 切片, 又是切片!

前一篇文章讲的是切片, 明天遇到的神奇问题还是和切片无关, 具体怎么个神奇法, 咱们来看看上面几个景象

景象一

a := "abc"bs := []byte(a)fmt.Println(bs, len(bs), cap(bs))// 输入: [97 98 99] 3 8

景象二

a := "abc"bs := []byte(a)fmt.Println(len(bs), cap(bs))// 输入: 3 32

景象三

bs := []byte("abc")fmt.Println(len(bs), cap(bs))// 输入: 3 3

景象四

a := ""bs := []byte(a)fmt.Println(bs, len(bs), cap(bs))// 输入: [] 0 0

景象五

a := ""bs := []byte(a)fmt.Println(len(bs), cap(bs))// 输入: 0 32

剖析

到这儿我曾经满脑子问号了

字符串变量转切片

一个小小的字符串转切片, 外部到底产生了什么, 居然如此的神奇。 这种时候只好祭出前一篇文章的套路了, 看看汇编代码(心愿之后有机会可能对go的汇编语法进行简略的介绍)有没有什么关键词可能帮忙咱们

以下为景象一转换的汇编代码要害局部

"".main STEXT size=495 args=0x0 locals=0xd8    0x0000 00000 (test.go:5)    TEXT    "".main(SB), ABIInternal, $216-0    0x0000 00000 (test.go:5)    MOVQ    (TLS), CX    0x0009 00009 (test.go:5)    LEAQ    -88(SP), AX    0x000e 00014 (test.go:5)    CMPQ    AX, 16(CX)    0x0012 00018 (test.go:5)    JLS    485    0x0018 00024 (test.go:5)    SUBQ    $216, SP    0x001f 00031 (test.go:5)    MOVQ    BP, 208(SP)    0x0027 00039 (test.go:5)    LEAQ    208(SP), BP    0x002f 00047 (test.go:5)    FUNCDATA    $0, gclocals·7be4bbacbfdb05fb3044e36c22b41e8b(SB)    0x002f 00047 (test.go:5)    FUNCDATA    $1, gclocals·648d0b72bb9d7f59fbfdbee57a078eee(SB)    0x002f 00047 (test.go:5)    FUNCDATA    $2, gclocals·2dfddcc7190380b1ae77e69d81f0a101(SB)    0x002f 00047 (test.go:5)    FUNCDATA    $3, "".main.stkobj(SB)    0x002f 00047 (test.go:6)    PCDATA    $0, $1    0x002f 00047 (test.go:6)    PCDATA    $1, $0    0x002f 00047 (test.go:6)    LEAQ    go.string."abc"(SB), AX    0x0036 00054 (test.go:6)    MOVQ    AX, "".a+96(SP)    0x003b 00059 (test.go:6)    MOVQ    $3, "".a+104(SP)    0x0044 00068 (test.go:7)    MOVQ    $0, (SP)    0x004c 00076 (test.go:7)    PCDATA    $0, $0    0x004c 00076 (test.go:7)    MOVQ    AX, 8(SP)    0x0051 00081 (test.go:7)    MOVQ    $3, 16(SP)    0x005a 00090 (test.go:7)    CALL    runtime.stringtoslicebyte(SB)    0x005f 00095 (test.go:7)    MOVQ    40(SP), AX    0x0064 00100 (test.go:7)    MOVQ    32(SP), CX    0x0069 00105 (test.go:7)    PCDATA    $0, $2    0x0069 00105 (test.go:7)    MOVQ    24(SP), DX    0x006e 00110 (test.go:7)    PCDATA    $0, $0    0x006e 00110 (test.go:7)    PCDATA    $1, $1    0x006e 00110 (test.go:7)    MOVQ    DX, "".bs+112(SP)    0x0073 00115 (test.go:7)    MOVQ    CX, "".bs+120(SP)    0x0078 00120 (test.go:7)    MOVQ    AX, "".bs+128(SP)

以下为景象二转换的汇编代码要害局部

"".main STEXT size=393 args=0x0 locals=0xe0    0x0000 00000 (test.go:5)    TEXT    "".main(SB), ABIInternal, $224-0    0x0000 00000 (test.go:5)    MOVQ    (TLS), CX    0x0009 00009 (test.go:5)    LEAQ    -96(SP), AX    0x000e 00014 (test.go:5)    CMPQ    AX, 16(CX)    0x0012 00018 (test.go:5)    JLS    383    0x0018 00024 (test.go:5)    SUBQ    $224, SP    0x001f 00031 (test.go:5)    MOVQ    BP, 216(SP)    0x0027 00039 (test.go:5)    LEAQ    216(SP), BP    0x002f 00047 (test.go:5)    FUNCDATA    $0, gclocals·0ce64bbc7cfa5ef04d41c861de81a3d7(SB)    0x002f 00047 (test.go:5)    FUNCDATA    $1, gclocals·00590b99cfcd6d71bbbc6e05cb4f8bf8(SB)    0x002f 00047 (test.go:5)    FUNCDATA    $2, gclocals·8dcadbff7c52509cfe2d26e4d7d24689(SB)    0x002f 00047 (test.go:5)    FUNCDATA    $3, "".main.stkobj(SB)    0x002f 00047 (test.go:6)    PCDATA    $0, $1    0x002f 00047 (test.go:6)    PCDATA    $1, $0    0x002f 00047 (test.go:6)    LEAQ    go.string."abc"(SB), AX    0x0036 00054 (test.go:6)    MOVQ    AX, "".a+120(SP)    0x003b 00059 (test.go:6)    MOVQ    $3, "".a+128(SP)    0x0047 00071 (test.go:7)    PCDATA    $0, $2    0x0047 00071 (test.go:7)    LEAQ    ""..autotmp_5+64(SP), CX    0x004c 00076 (test.go:7)    PCDATA    $0, $1    0x004c 00076 (test.go:7)    MOVQ    CX, (SP)    0x0050 00080 (test.go:7)    PCDATA    $0, $0    0x0050 00080 (test.go:7)    MOVQ    AX, 8(SP)    0x0055 00085 (test.go:7)    MOVQ    $3, 16(SP)    0x005e 00094 (test.go:7)    CALL    runtime.stringtoslicebyte(SB)    0x0063 00099 (test.go:7)    MOVQ    40(SP), AX    0x0068 00104 (test.go:7)    MOVQ    32(SP), CX    0x006d 00109 (test.go:7)    PCDATA    $0, $3    0x006d 00109 (test.go:7)    MOVQ    24(SP), DX    0x0072 00114 (test.go:7)    PCDATA    $0, $0    0x0072 00114 (test.go:7)    PCDATA    $1, $1    0x0072 00114 (test.go:7)    MOVQ    DX, "".bs+136(SP)    0x007a 00122 (test.go:7)    MOVQ    CX, "".bs+144(SP)    0x0082 00130 (test.go:7)    MOVQ    AX, "".bs+152(SP)

在看汇编代码之前, 咱们首先来看一看runtime.stringtoslicebyte的函数签名

func stringtoslicebyte(buf *tmpBuf, s string) []byte

到这里只靠关键词曾经无奈看出更多的信息了,还是须要略微理解一下汇编的语法,笔者在这里列出一点简略的剖析, 之后咱们还是能够通过取巧的办法发现更多的货色

// 景象一给runtime.stringtoslicebyte的传参0x002f 00047 (test.go:6)    LEAQ    go.string."abc"(SB), AX // 将字符串"abc"放入寄存器AX0x0036 00054 (test.go:6)    MOVQ    AX, "".a+96(SP) // 将AX中的内容存入变量a中0x003b 00059 (test.go:6)    MOVQ    $3, "".a+104(SP) // 将字符串长度3存入变量a中0x0044 00068 (test.go:7)    MOVQ    $0, (SP) // 将0 传递个runtime.stringtoslicebyte(SB)的第一个参数(笔者猜想对应go中的nil)0x004c 00076 (test.go:7)    PCDATA    $0, $0 // 据说和gc无关, 具体还不分明, 个别状况能够疏忽0x004c 00076 (test.go:7)    MOVQ    AX, 8(SP) // 将AX中的内容传递给runtime.stringtoslicebyte(SB)的第二个参数0x0051 00081 (test.go:7)    MOVQ    $3, 16(SP) // 将字符串长度传递给runtime.stringtoslicebyte(SB)的第二个参数0x005a 00090 (test.go:7)    CALL    runtime.stringtoslicebyte(SB) // 调用函数, 此行前面的几行代码是将返回值赋值给变量bs// 景象二给runtime.stringtoslicebyte的传参0x002f 00047 (test.go:6)    LEAQ    go.string."abc"(SB), AX // 将字符串"abc"放入寄存器AX0x0036 00054 (test.go:6)    MOVQ    AX, "".a+120(SP) // 将AX中的内容存入变量a中0x003b 00059 (test.go:6)    MOVQ    $3, "".a+128(SP) // 将字符串长度3存入变量a中0x0047 00071 (test.go:7)    PCDATA    $0, $20x0047 00071 (test.go:7)    LEAQ    ""..autotmp_5+64(SP), CX // 将外部变量autotmp_5放入寄存器CX0x004c 00076 (test.go:7)    PCDATA    $0, $10x004c 00076 (test.go:7)    MOVQ    CX, (SP) // 将CX中的内容传递给runtime.stringtoslicebyte(SB)的第一个参数0x0050 00080 (test.go:7)    PCDATA    $0, $00x0050 00080 (test.go:7)    MOVQ    AX, 8(SP) // 将AX中的内容传递给runtime.stringtoslicebyte(SB)的第二个参数0x0055 00085 (test.go:7)    MOVQ    $3, 16(SP) // 将字符串长度传递给runtime.stringtoslicebyte(SB)的第二个参数0x005e 00094 (test.go:7)    CALL    runtime.stringtoslicebyte(SB)

通过下面汇编代码的剖析能够晓得,景象一和景象二的区别就是传递给runtime.stringtoslicebyte的第一个参数不同。 通过对runtime包中stringtoslicebyte函数剖析,第一个参数是否有值和字符串长度会影响代码执行的分支,从而生成不同的切片, 因而容量不一样也是常理之中, 上面咱们看源码

func stringtoslicebyte(buf *tmpBuf, s string) []byte {    var b []byte    if buf != nil && len(s) <= len(buf) {        *buf = tmpBuf{}        b = buf[:len(s)]    } else {        b = rawbyteslice(len(s))    }    copy(b, s)    return b}

然而, stringtoslicebyte的第一个参数什么状况下才会有值,什么状况下为nil, 咱们依然不分明。那怎么办呢, 只好祭出全局搜寻大法:

# 在go源码根目录执行上面的命令grep stringtoslicebyte -r . | grep -v "//"

最终在go的编译器源码cmd/compile/internal/gc/walk.go发现了如下代码块

咱们查看mkcall 函数签名能够晓得, 从第四个参数开始的所有变量都会作为参数传递给第一个参数对应的函数, 最初生成一个*Node的变量。其中Node构造体解释如下:

// A Node is a single node in the syntax tree.// Actually the syntax tree is a syntax DAG, because there is only one// node with Op=ONAME for a given instance of a variable x.// The same is true for Op=OTYPE and Op=OLITERAL. See Node.mayBeShared.

综合上述信息咱们得出的论断是,编译器会对stringtoslicebyte的函数调用生成一个AST(形象语法树)对应的节点。因而咱们也晓得传递给stringtoslicebyte函数的第一个变量也就对应于上图中的变量a.

其中a的初始值为nodnil()的返回值,即默认为nil. 然而n.Esc == EscNone时,a会变成一个数组。咱们看一下EscNone的解释.

// 此代码位于cmd/compile/internal/gc/esc.go中const (    // ...    EscNone           // Does not escape to heap, result, or parameters.    ...)

由上可知, EscNone用来判断变量是否逃逸,到这儿了咱们就很好办了,接下来咱们对景象一和景象二的代码进行逃逸剖析.

# 执行变量逃逸剖析命令: go run -gcflags '-m -l' test.go# 景象一逃逸剖析如下:./test.go:7:14: ([]byte)(a) escapes to heap./test.go:8:13: main ... argument does not escape./test.go:8:13: bs escapes to heap./test.go:8:21: len(bs) escapes to heap./test.go:8:30: cap(bs) escapes to heap[97 98 99] 3 8# 景象二逃逸剖析如下:./test.go:7:14: main ([]byte)(a) does not escape./test.go:8:13: main ... argument does not escape./test.go:8:17: len(bs) escapes to heap./test.go:8:26: cap(bs) escapes to heap3 32

依据下面的信息咱们晓得在景象一中,bs变量产生了逃逸,景象二中变量未产生逃逸,也就是说stringtoslicebyte函数的第一个参数在变量未产生逃逸时其值不为nil,变量产生逃逸时其值为nil。到这里咱们曾经搞明确stringtoslicebyte的第一个参数了, 那咱们持续剖析stringtoslicebyte的外部逻辑

咱们在runtime/string.go中看到stringtoslicebyte第一个参数的类型定义如下:

const tmpStringBufSize = 32type tmpBuf [tmpStringBufSize]byte

综上: 景象二中bs变量未产生变量逃逸, stringtoslicebyte第一个参数不为空且是一个长度为32的byte数组, 因而在景象二中生成了一个容量为32的切片

依据对stringtoslicebyte的源码剖析, 咱们晓得景象一调用了rawbyteslice函数

func rawbyteslice(size int) (b []byte) {    cap := roundupsize(uintptr(size))    p := mallocgc(cap, nil, false)    if cap != uintptr(size) {        memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size))    }    *(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)}    return}

由下面的代码晓得, 切片的容量通过runtime/msize.go中的roundupsize函数计算得出, 其中_MaxSmallSize和class_to_size均定义在runtime/sizeclasses.go

func roundupsize(size uintptr) uintptr {    if size < _MaxSmallSize {        if size <= smallSizeMax-8 {            return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])        } else {            return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]])        }    }    if size+_PageSize < size {        return size    }    return round(size, _PageSize)}

因为字符串abc的长度小于_MaxSmallSize(32768),故切片的长度只能取数组class_to_size中的值, 即0, 8, 16, 32, 48, 64, 80, 96, 112, 128....s

至此, 景象一中切片容量为什么为8也水落石出了。置信到这里很多人曾经明确景象四和景象五是怎么回事儿了, 其逻辑别离与景象一和景象二是统一的, 有趣味的, 能够在本人的电脑下面试一试。

字符串间接转切片

那你说了这么多, 景象三还是不能解释啊。请各位看官莫急, 接下来咱们持续剖析。

置信各位仔细的小伙伴应该早就发现了咱们在下面的cmd/compile/internal/gc/walk.go源码图中折叠了局部代码, 当初咱们就将这块神秘的代码赤裸裸的展现进去

咱们剖析这块代码发现,go编译器在将字符串转字节切片生成AST时,总共分为三步。

  1. 先判断该变量是否是常量字符串,如果是常量字符串,则间接通过types.NewArray创立一个和字符串等长的数组
  2. 常量字符串生成的切片变量也要进行逃逸剖析,并判断其大小是否大于函数栈容许调配给变量的最大长度, 从而判断节点是调配在栈上还是在堆上
  3. 最初,如果字符串长度是大于0, 将字符串内容复制到字节切片中, 而后返回。因而景象三中的切片容量是3也就齐全分明了

论断

字符串转字节切片步骤如下

  1. 判断是否是常量, 如果是常量则转换为等容量等长的字节切片
  2. 如果是变量, 先判断生成的切片是否产生变量逃逸

    • 如果逃逸或者字符串长度>32, 则依据字符串长度能够计算出不同的容量
    • 如果未逃逸且字符串长度<=32, 则字符切片容量为32

扩大

常见逃逸状况

  1. 函数返回部分指针
  2. 栈空间有余逃逸
  3. 动静类型逃逸, 很多函数参数为interface类型,比方fmt.Println(a ...interface{}),编译期间很难确定其参数的具体类型, 也会产生逃逸
  4. 闭包援用对象逃逸
注: 写本文时, 笔者所用go版本为: go1.13.4

参考

https://golang.org/src/cmd/compile/README.md

https://my.oschina.net/renhc/blog/2222104

生命不息, 摸索不止, 后续将继续更新有对于go的技术摸索

原创不易, 低微求关注珍藏二连.