以下文章来源于新世界杂货铺 ,作者许文

新世界杂货铺

作为一名Gopher, 我愿称之为Go的干(杂)货铺子!

神奇的现象

切片, 切片, 又是切片!

今天遇到的神奇问题和切片有关, 具体怎么个神奇法, 我们来看看下面几个现象

现象一

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=0xd80x0000 00000 (test.go:5)TEXT"".main(SB), ABIInternal, $216-00x0000 00000 (test.go:5)MOVQ(TLS), CX0x0009 00009 (test.go:5)LEAQ-88(SP), AX0x000e 00014 (test.go:5)CMPQAX, 16(CX)0x0012 00018 (test.go:5)JLS4850x0018 00024 (test.go:5)SUBQ$216, SP0x001f 00031 (test.go:5)MOVQBP, 208(SP)0x0027 00039 (test.go:5)LEAQ208(SP), BP0x002f 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, $10x002f 00047 (test.go:6)PCDATA$1, $00x002f 00047 (test.go:6)LEAQgo.string."abc"(SB), AX0x0036 00054 (test.go:6)MOVQAX, "".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, $00x004c 00076 (test.go:7)MOVQAX, 8(SP)0x0051 00081 (test.go:7)MOVQ$3, 16(SP)0x005a 00090 (test.go:7)CALLruntime.stringtoslicebyte(SB)0x005f 00095 (test.go:7)MOVQ40(SP), AX0x0064 00100 (test.go:7)MOVQ32(SP), CX0x0069 00105 (test.go:7)PCDATA$0, $20x0069 00105 (test.go:7)MOVQ24(SP), DX0x006e 00110 (test.go:7)PCDATA$0, $00x006e 00110 (test.go:7)PCDATA$1, $10x006e 00110 (test.go:7)MOVQDX, "".bs+112(SP)0x0073 00115 (test.go:7)MOVQCX, "".bs+120(SP)0x0078 00120 (test.go:7)MOVQAX, "".bs+128(SP)

以下为现象二转换的汇编代码关键部分

"".main STEXT size=393 args=0x0 locals=0xe00x0000 00000 (test.go:5)TEXT"".main(SB), ABIInternal, $224-00x0000 00000 (test.go:5)MOVQ(TLS), CX0x0009 00009 (test.go:5)LEAQ-96(SP), AX0x000e 00014 (test.go:5)CMPQAX, 16(CX)0x0012 00018 (test.go:5)JLS3830x0018 00024 (test.go:5)SUBQ$224, SP0x001f 00031 (test.go:5)MOVQBP, 216(SP)0x0027 00039 (test.go:5)LEAQ216(SP), BP0x002f 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, $10x002f 00047 (test.go:6)PCDATA$1, $00x002f 00047 (test.go:6)LEAQgo.string."abc"(SB), AX0x0036 00054 (test.go:6)MOVQAX, "".a+120(SP)0x003b 00059 (test.go:6)MOVQ$3, "".a+128(SP)0x0047 00071 (test.go:7)PCDATA$0, $20x0047 00071 (test.go:7)LEAQ""..autotmp_5+64(SP), CX0x004c 00076 (test.go:7)PCDATA$0, $10x004c 00076 (test.go:7)MOVQCX, (SP)0x0050 00080 (test.go:7)PCDATA$0, $00x0050 00080 (test.go:7)MOVQAX, 8(SP)0x0055 00085 (test.go:7)MOVQ$3, 16(SP)0x005e 00094 (test.go:7)CALLruntime.stringtoslicebyte(SB)0x0063 00099 (test.go:7)MOVQ40(SP), AX0x0068 00104 (test.go:7)MOVQ32(SP), CX0x006d 00109 (test.go:7)PCDATA$0, $30x006d 00109 (test.go:7)MOVQ24(SP), DX0x0072 00114 (test.go:7)PCDATA$0, $00x0072 00114 (test.go:7)PCDATA$1, $10x0072 00114 (test.go:7)MOVQDX, "".bs+136(SP)0x007a 00122 (test.go:7)MOVQCX, "".bs+144(SP)0x0082 00130 (test.go:7)MOVQAX, "".bs+152(SP)

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

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

到这里只靠关键词已经无法看出更多的信息了,还是需要稍微了解一下汇编的语法,笔者在这里列出一点简单的分析, 之后我们还是可以通过取巧的方法发现更多的东西

// 现象一给runtime.stringtoslicebyte的传参0x002f 00047 (test.go:6)LEAQgo.string."abc"(SB), AX // 将字符串"abc"放入寄存器AX0x0036 00054 (test.go:6)MOVQAX, "".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)MOVQAX, 8(SP) // 将AX中的内容传递给runtime.stringtoslicebyte(SB)的第二个参数0x0051 00081 (test.go:7)MOVQ$3, 16(SP) // 将字符串长度传递给runtime.stringtoslicebyte(SB)的第二个参数0x005a 00090 (test.go:7)CALLruntime.stringtoslicebyte(SB) // 调用函数, 此行后面的几行代码是将返回值赋值给变量bs// 现象二给runtime.stringtoslicebyte的传参0x002f 00047 (test.go:6)LEAQgo.string."abc"(SB), AX // 将字符串"abc"放入寄存器AX0x0036 00054 (test.go:6)MOVQAX, "".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)MOVQCX, (SP) // 将CX中的内容传递给runtime.stringtoslicebyte(SB)的第一个参数0x0050 00080 (test.go:7)PCDATA$0, $00x0050 00080 (test.go:7)MOVQAX, 8(SP) // 将AX中的内容传递给runtime.stringtoslicebyte(SB)的第二个参数0x0055 00085 (test.go:7)MOVQ$3, 16(SP) // 将字符串长度传递给runtime.stringtoslicebyte(SB)的第二个参数0x005e 00094 (test.go:7)CALLruntime.stringtoslicebyte(SB)

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

func stringtoslicebyte(buf *tmpBuf, s string) []byte {var b []byteif 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. 如果是变量, 先判断生成的切片是否发生变量逃逸
  3. 如果逃逸或者字符串长度>32, 则根据字符串长度可以计算出不同的容量
  4. 如果未逃逸且字符串长度<=32, 则字符切片容量为32

扩展

常见逃逸情况

  1. 函数返回局部指针
  2. 栈空间不足逃逸
  3. 动态类型逃逸, 很多函数参数为interface类型,比如fmt.Println(a ...interface{}),编译期间很难确定其参数的具体类型, 也会发生逃逸
  4. 闭包引用对象逃逸

注: 写本文时, 笔者所用go版本为: go1.13.4

生命不息, 探索不止, 后续将持续更新有关于go的技术探索

pandas 字符串切片后保存_我擦~字符串转字节切片后,切片的容量竟然千奇百怪...相关推荐

  1. pandas 字符串切片后保存_pandas:快速处理字符串方法

    前言 当我们遇到一个超级大的DataFrame,里面有一列类型为字符串,要将每一行的字符串都用同一方式进行处理,一般会想到遍历整合DataFrame,但是如果直接这样做的话将会耗费很长时间,有时几个小 ...

  2. 空气培养皿采样后保存_六级撞击式微生物采样器是什么?用途有哪些?

    原标题:什么是六级撞击式微生物采样器呢? 微生物的定义:就是指悬浮在空气中的活微生物粒子,六级撞击式微生物采样器是采集空气中的微生物的仪器.聚创环保目前生产的六级撞击式微生物采样器是我们的热销产品,我 ...

  3. 字符串删除重复字符_高效的字符串清理-删除内部重复空间

    字符串删除重复字符 介绍 (Introduction) 我经常回答一些问题,其中的字符串需要"清除"多个空格字符. 最常见的解决方法是删除前导或尾随空格. 对于这个问题,有非常方便 ...

  4. string字符串数字自增_常见的字符串操作

    一部分字符串操作的总结,比较基础.目录: 使用str.split() 切分数据 将 datetime 类型转化为字符串类型 字符串的合并操作 使用 str.strip() 去除字符串前面和后面所有的字 ...

  5. c字符串分割成数组_数组与字符串

    定义数组时,应该注意以下几点: (1) 数组使用的是方括号[ ],不要误写成小括号( ). (2) 常量表达式的值必须是一个正整数值,不能含有变量,但是可以是符号常数或常量表达式. (3) 数组定义后 ...

  6. python数字字符串和数字相加_数字和字符串

    数字和字符串 本节我们将了解最基础的数据类型,数字和字符串.打开你的Python REPL并写出以下语句. >>> a = 2 >>> type(a) >&g ...

  7. linuxsed替换字符串后保存_字符串方法——replace()

    1.字符串方法-- replace() str.replace(old, new[,max]) 参数说明(Parameters) old:被替换的字符串 new:新字符串,替换原来的old字符串 ma ...

  8. 空气培养皿采样后保存_环境监测基础知识——环境空气监测技术之布点采样

    环境空气布点与采样(HJ-194) 1. 点位布设 1.1. 一般原则 1.1.1. 采样点位应根据监测任务的目的. 要求布设, 必要时进行现场踏勘后确定 1.1.2. 所选点位应具有较好的代表性, ...

  9. 字符串在Java中_字符和字符串在Java中的旅程

    以下是个人对java中字符和字符串的见解,如有疏漏之处,还请不吝赐教. 下面通过一个简单的程序来说明字符和字符串在Java中的旅程. 以字符 ' 中 '为例, 它的GBK编码是2个字节:0xd6d0, ...

最新文章

  1. php tp 微信支付,PHP实现的微信APP支付功能示例【基于TP5框架】
  2. Html5表单元素-搜索框和上传文件框
  3. 【STM32】【STM32CubeMX】STM32CubeMX的使用之五:定时器时基配置及其中断
  4. jira 查找issue_JIRA使用教程:高级搜索—字段参考4/4
  5. Git服务器Gitosis安装设置
  6. WebLogic的下载与安装(图文教程)
  7. 使用Golang搭建web服务
  8. Windows 键盘快捷键概述
  9. 【计算机基础】解决Win10电脑主机前面的耳机插口没声音的问题
  10. 2018版USBASP烧录器改通用版教程
  11. 业界动态-新一代万亿级消息系统Pulsar的应用实践-03
  12. 前端微信签名验证工具_微信支付服务商签名验证无误,但是统一下单后服务器返回【签名错误】...
  13. 数电技术基础大恶补09:脉冲产生及其整形
  14. 【LeetCode】222. Count Complete Tree Nodes 解题报告(Python)
  15. 进程、线程及python的多线程编程
  16. macd的原理是什么
  17. python 异步io框架_Python并发编程之学习异步IO框架:asyncio 中篇(十)
  18. Cisco 安全设备管理工具 (SDM)
  19. 谷歌浏览器又曝漏洞,黑客可劫持目标计算机
  20. 黑魂3修改魂后进服务器,黑暗之魂3怎么改存档位置 黑魂3修改存档位置教程_3DM单机...

热门文章

  1. Docker镜像导致centos-root根分区容量爆满
  2. 【Spring Boot】3.Spring Boot的配置
  3. iOS 动画之Spring动画、Block动画、GIF图
  4. 又见The request sent by the client was syntactically incorrect ()
  5. Coding List
  6. 装个discuz论坛
  7. 25.使用getScript()方法异步加载并执行js文件
  8. OpenGL中的投影使用
  9. [Swift A] - Using Swift with Cocoa and Objective-C--Mix and Match
  10. 玩转VIM编辑器-自动补全