1. 背景

上周四小伙伴发了Go社区一个帖子下hej8875的回复,如下:

package main
import "fmt"
func main() {
s := []byte("")
s1 := append(s, 'a')
s2 := append(s, 'b')
//fmt.Println(s1, "==========", s2)
fmt.Println(string(s1), "==========", string(s2))
}
// 出现个让我理解不了的现象, 注释时候输出是 b ========== b
// 取消注释输出是 [97] ========== [98] a ========== b

这个回复比原贴有意思,也很有迷惑性。作者测试了下,确实如此,于是和小伙伴们讨论深究下。开始以为应该挺简单的,理解后,发现涉及挺多知识点,值得跟大家分享下过程。

2. slice

2.1 内部结构

先抛去注释的这行代码//fmt.Println(s1, "==========", s2),后面在讲。 当输出 b ========== b时,已经不符合预期结果a和b了。我们知道slice内部并不会存储真实的值,而是对数组片段的引用,其内部结构是:

type slice struct {
data uintptr
len int
cap int
}

其中data是指向数组元素的指针,len是指slice要引用数组中的元素数量。cap是指要引用数组中(从data指向开始计算)剩余的元素数量,这个数量减去len,就是还能向这个slice(数组)添加多少元素,如果超出就会发生数据的复制。slice的示意图:

s := make([]byte, 5)// 下图

s = s[2:4]  //会重新生成新的slice,并赋值给s。与底层数组的引用也发生了改变

2.2 覆盖前值

回到问题上,由此可以推断出:s := []byte("") 这行代码中的s实际引用了一个 byte 的数组。

其capacity 是32,length是 0:

s := []byte("")
fmt.Println(cap(s), len(s))
//输出: 32 0

关键点在于下面代码s1 := append(s, 'a')中的append,并没有在原slice修改,当然也没办法修改,因为在Go中都是值传递的。当把s传入append函数内时,已经复制出一份s1,然后在s1上追加 a,s1长度是增加了1,但s长度仍然是0:

s := []byte("")
fmt.Println(cap(s), len(s))
s1 := append(s, 'a')
fmt.Println(cap(s1), len(s1))
// 输出
// 32 0
// 32 1

由于s,s1指向同一份数组,所以在s1上进行append a操作时(底层数组[0]=a),也是s所指向数组的操作,但s本身不会有任何变化。这也是Go中append的写法都是:

s = append(s,'a')

append函数会返回s1,需要重新赋值给s。 如果不赋值的话,s本身记录的数据就滞后了,再次对其append,就会从滞后的数据开始操作。虽然看起是append,实际上确是把上一次append的值给覆盖了。

所以问题的答案是:后append的b,把上次append的a给覆盖了,所以才会输出b b。

假设底层数组是arr,如注释:

s := []byte("")
s1 := append(s, 'a') // 等同于 arr[0] = 'a'
s2 := append(s, 'b') // 等同于 arr[0] = 'b'
fmt.Println(string(s1), "==========", string(s2)) // 只是把同一份数组打印出来了

3. string

3.1 重新分配

老湿,能不能再给力一点?可以,我们继续,先来看个题:

s := []byte{}
s1 := append(s, 'a')
s2 := append(s, 'b')
fmt.Println(string(s1), ",", string(s2))
fmt.Println(cap(s), len(s))

猜猜输出什么?

答案是:a , b 和 0 0,符合预期。

上面2.2章节例子中输出的是:32,0。看来问题关键在这里,两者差别在于一个是默认[]byte{},另外个是空字符串转的[]byte("")。其长度都是0,比较好理解,但为什么容量是32就不符合预期输出了?

因为 capacity 是数组还能添加多少的容量,在能满足的情况,不会重新分配。所以 capacity-length=32,是足够appenda,b的。我们用make来验证下:

// append 内会重新分配,输出a,b
s := make([]byte, 0, 0)
// append 内不会重新分配,输出b,b,因为容量为1,足够append
s := make([]byte, 0, 1)
s1 := append(s, 'a')
s2 := append(s, 'b')
fmt.Println(string(s1), ",", string(s2))

重新分配指的是:append 会检查slice大小,如果容量不够,会重新创建个更大的slice,并把原数组复制一份出来。在make([]byte,0,0)这样情况下,s容量肯定不够用,所以s1,s2使用的都是各自从s复制出来的数组,结果也自然符合预期a,b了。

测试重新分配后的容量变大,打印s1:

s := make([]byte, 0, 0)
s1 := append(s, 'a')
fmt.Println(cap(s1), len(s1))
// 输出 8,1。重新分配后扩大了

3.2 二者转换

那为什么空字符串转的slice的容量是32?而不是0或者8呢?

只好祭出杀手锏了,翻源码。Go官方提供的工具,可以查到编译后调用的汇编信息,不然在大片源码中搜索也很累。

-gcflags 是传递参数给Go编译器,-S -S是打印汇编调用信息和数据,-S只打印调用信息。

go run -gcflags '-S -S' main.go

下面是输出:

    0x0000 00000 ()    TEXT    "".main(SB), $264-0
0x003e 00062 () MOVQ AX, (SP)
0x0042 00066 () XORPS X0, X0
0x0045 00069 () MOVUPS X0, 8(SP)
0x004a 00074 () PCDATA $0, $0
0x004a 00074 () CALL runtime.stringtoslicebyte(SB)
0x004f 00079 () MOVQ 32(SP), AX
b , b

Go使用的是plan9汇编语法,虽然整体有些不好理解,但也能看出我们需要的关键点:

CALL    runtime.stringtoslicebyte(SB)

定位源码到src\runtime\string.go:

stringtoslicebyte函数中可以看出容量32的源头,见注释:

const tmpStringBufSize = 32
type tmpBuf [tmpStringBufSize]byte
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{} // tmpBuf的默认容量是32
b = buf[:len(s)] // 创建个容量为32,长度为0的新slice,赋值给b。
} else {
b = rawbyteslice(len(s))
}
copy(b, s) // s是空字符串,复制过去也是长度0
return b
}

那为什么不是走else中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
}

如果走else的话,容量就不是32了。假如走的话,也不影响得出的结论(覆盖),可以测试下:

	s := []byte(strings.Repeat("c", 33))
s1 := append(s, 'a')
s2 := append(s, 'b')
fmt.Println(string(s1), ",", string(s2))
// cccccccccccccccccccccccccccccccccb , cccccccccccccccccccccccccccccccccb

4. 逃逸分析

老湿,能不能再给力一点?什么时候该走else?老湿你说了大半天,坑还没填,为啥加上注释就符合预期输出a,b? 还有加上注释为啥连容量都变了?

s := []byte("")
fmt.Println(cap(s), len(s))
s1 := append(s, 'a')
s2 := append(s, 'b')
fmt.Println(s1, ",", s2)
fmt.Println(string(s1), ",", string(s2))
//输出
// 0 0
// [97] ========== [98]
// a , b

如果用逃逸分析来解释的话,就比较好理解了,先看看什么是逃逸分析。

4.1 提高性能

如果一个函数或子程序内有局部对象,返回时返回该对象的指针,那这个指针可能在任何其他地方会被引用,就可以说该指针就成功“逃逸”了 。 而逃逸分析(escape analysis)就是分析这类指针范围的方法,这样做的好处是提高性能:

  • 最大的好处应该是减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。
  • 因为逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好
  • 同步消除,如果定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。

Go在编译的时候进行逃逸分析,来决定一个对象放栈上还是放堆上,不逃逸的对象放栈上,可能逃逸的放堆上 。

4.2 逃到堆上

取消注释情况下:Go编译程序进行逃逸分析时,检测到fmt.Println有引用到s,所以在决定堆上分配s下的数组。在进行string转[]byte时,如果分配到栈上就会有个默认32的容量,分配堆上则没有。

用下面命令执行,可以得到逃逸信息,这个命令只编译程序不运行,上面用的go run -gcflags是传递参数到编译器并运行程序。

go tool compile -m main.go

取消注释fmt.Println(s1, ",", s2) 后 ([]byte)("")会逃逸到堆上:

main.go:23:13: s1 escapes to heap
main.go:20:13: ([]byte)("") escapes to heap // 逃逸到堆上
main.go:23:18: "," escapes to heap
main.go:23:18: s2 escapes to heap
main.go:24:20: string(s1) escapes to heap
main.go:24:20: string(s1) escapes to heap
main.go:24:26: "," escapes to heap
main.go:24:37: string(s2) escapes to heap
main.go:24:37: string(s2) escapes to heap
main.go:23:13: main ... argument does not escape
main.go:24:13: main ... argument does not escape

加上注释//fmt.Println(s1, ",", s2) 不会逃逸到堆上:

go tool compile -m main.go
main.go:24:20: string(s1) escapes to heap
main.go:24:20: string(s1) escapes to heap
main.go:24:26: "," escapes to heap
main.go:24:37: string(s2) escapes to heap
main.go:24:37: string(s2) escapes to heap
main.go:20:13: main ([]byte)("") does not escape //不逃逸
main.go:24:13: main ... argument does not escape

4.3 逃逸分配

接着继续定位调用stringtoslicebyte 的地方,在src\cmd\compile\internal\gc\walk.go 文件。 为了便于理解,下面代码进行了汇总:

const (
EscUnknown = iota
EscNone // 结果或参数不逃逸堆上.
)
case OSTRARRAYBYTE:
a := nodnil() //默认数组为空
if n.Esc == EscNone {
// 在栈上为slice创建临时数组
t := types.NewArray(types.Types[TUINT8], tmpstringbufsize)
a = nod(OADDR, temp(t), nil)
}
n = mkcall("stringtoslicebyte", n.Type, init, a, conv(n.Left, types.Types[TSTRING]))

不逃逸情况下会分配个32字节的数组 t。逃逸情况下不分配,数组设置为 nil,所以s的容量是0。接着从s上append a,b到s1,s2,其必然会发生复制,所以不会发生覆盖前值,也符合预期结果a,b 。再看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
}

4.4 大小分配

不逃逸情况下默认32。那逃逸情况下分配策略是?

s := []byte("a")
fmt.Println(cap(s))
s1 := append(s, 'a')
s2 := append(s, 'b')
fmt.Print(s1, s2)

如果是空字符串它的输出:0。”a“字符串时输出:8。

大小取决于src\runtime\size.go 中的roundupsize 函数和 class_to_size 变量。

这些增加大小的变化,是由 src\runtime\mksizeclasses.go 生成的。

5. 版本差异

老湿,能不能再给力一点? 老湿你讲的全是错误的,我跑的结果和你是反的。对,你没错,作者也没错,毕竟我们在用Go写程序,如果Go底层发生变化了,肯定结果不一样。作者在调研过程中,发现另外博客得到的stringtoslicebyte源码是:

func stringtoslicebyte(s String) (b Slice) {
b.array = runtime·mallocgc(s.len, 0, FlagNoScan|FlagNoZero);
b.len = s.len;
b.cap = s.len;
runtime·memmove(b.array, s.str, s.len);
}

上面版本的源码,得到的结果,也是符合预期的,因为不会默认分配32字节的数组。

继续翻旧版代码,到1.3.2版是这样:

func stringtoslicebyte(s String) (b Slice) {
uintptr cap;
cap = runtime·roundupsize(s.len);
b.array = runtime·mallocgc(cap, 0, FlagNoScan|FlagNoZero);
b.len = s.len;
b.cap = cap;
runtime·memmove(b.array, s.str, s.len);
if(cap != b.len)
runtime·memclr(b.array+b.len, cap-b.len);
}

1.6.4版:

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

更古老的:

struct __go_open_array
__go_string_to_byte_array (String str)
{
uintptr cap;
unsigned char *data;
struct __go_open_array ret; cap = runtime_roundupsize (str.len);
data = (unsigned char *) runtime_mallocgc (cap, 0, FlagNoScan | FlagNoZero);
__builtin_memcpy (data, str.str, str.len);
if (cap != (uintptr) str.len)
__builtin_memset (data + str.len, 0, cap - (uintptr) str.len);
ret.__values = (void *) data;
ret.__count = str.len;
ret.__capacity = str.len;
return ret;
}

作者在1.6.4版本上测试,得到的结果确实是反的,注释了反而得到预期结果 a, b。 本文中使用的是1.10.2

6. 结论

老湿,能不能再给力一点?

Go中string转[]byte的陷阱的更多相关文章

  1. java中string与byte[]的转换

    1.string 转 byte[] byte[] midbytes=isoString.getBytes("UTF8"); //为UTF8编码 byte[] isoret = sr ...

  2. Java中String和byte[]间的转换浅析

    Java语言中字符串类型和字节数组类型相互之间的转换经常发生,网上的分析及代码也比较多,本文将分析总结常规的byte[]和String间的转换以及十六进制String和byte[]间相互转换的原理及实 ...

  3. Java中String和byte[]间的 转换浅析

    Java语言中字符串类型和字节数组类型相互之间的转换经常发生,网上的分析及代码也比较多,本文将分析总结常规的byte[]和String间的转换以及十六进制String和byte[]间相互转换的原理及实 ...

  4. Java中String和byte[]间的 转换

    数据库的字段中使用了blob类型时,在entity中此字段可以对应为byte[] 类型,保存到数据库中时需要把传入的参数转为byte[]类型,读取的时候再通过将byte[]类型转换为String类型. ...

  5. Golang中string和[]byte的对比

    golang string和[]byte的对比 为啥string和[]byte类型转换需要一定的代价? 为啥内置函数copy会有一种特殊情况copy(dst []byte, src string) i ...

  6. C#中string和byte[]相互转换问题解决

    本来想讲string转换为byte数组,通过在VS上打 ‘str. “来找,结果半天没发现跳出来的函数中有想要的,哭瞎 /(ㄒoㄒ)/~~ 这回将两种情况都记下来了.... string ---> ...

  7. 转:Java中String与byte[]的转换

    原文地址:http://blog.csdn.net/llwan/article/details/7567906 String s = "fs123fdsa";//String变量 ...

  8. golang 中 string 转换 []byte 的一道笔试题

    背景 去面试的时候遇到一道和 string 相关的题目,记录一下用到的知识点.题目如下: s:="123" ps:=&s b:=[]byte(s) pb:=&b s ...

  9. C#中String与byte[]的相互转换

    从文件中读取字符串 string filePath = @"C:\Temp.xml"; string xmlString= File.ReadAllText(filePath); ...

随机推荐

  1. Android 文件操作心得体会

    android 的文件操作说白了就是Java的文件操作的处理.所以如果对Java的io文件操作比较熟悉的话,android的文件操作就是小菜一碟了.好了,话不多说,开始今天的正题吧. 先从一个小项目入 ...

  2. 转换xml格式的短信记录

    <?xml version="1.0" encoding="UTF-8"?> <SMSRecord>     <SMS>   ...

  3. Gradle笔记——依赖管理基础

    1. 什么是依赖管理 依赖管理可以分为两部分:一是依赖,即项目构建或运行时所需要的一些文件:二是发布,即构建完成后上传到某个地方. 1.1 依赖 大部分的项目都需要第三方库类或项目文件,这些文件就是项 ...

  4. Touch Handling in Cocos2D 3.x(五)

    实现新英雄的放置功能 首先我们需要一个变量来保持我们当前移动英雄的引用,因此我们将添加一个私有实例变量.修改MainScene.m中的代码. 用: @implementation MainScene ...

  5. Dynamics CRM 系统自定义部分的语言翻译

    Dynamics CRM 自带语言切换功能,在官网下载所需语言包安装后,在设置语言中就能看到你所添加的语言,勾选要启用的语言应用即可,再打开系统设置--语言就能看到可更改用户界面语言的显示了. 但官方 ...

  6. CTBS问题百科

    1.浏览CTBS网站时,Service Unavailable或应用程序池自动停止的现象 解决方法: 点击"开始"-"控制面板"-"管理工具" ...

  7. wing带你玩转自定义view系列(2) 简单模仿qq未读消息去除效果

    上一篇介绍了贝塞尔曲线的简单应用 仿360内存清理效果 这一篇带来一个  两条贝塞尔曲线的应用 : 仿qq未读消息去除效果. 转载请注明出处:http://blog.csdn.net/wingicho ...

  8. OpenCV特征点检测------Surf(特征点篇)

    Surf(Speed Up Robust Feature) Surf算法的原理                                                              ...

  9. mysql进阶(十九)SQL语句如何精准查找某一时间段的数据

    SQL语句如何精准查找某一时间段的数据 在项目开发过程中,自己需要查询出一定时间段内的交易.故需要在sql查询语句中加入日期时间要素,sql语句如何实现? SELECT * FROM lmapp.lm ...

  10. mysql进阶(五)数据表中带OR的多条件查询

    MySQL数据表中带OR的多条件查询 OR关键字可以联合多个条件进行查询.使用OR关键字时: 条件 1) 只要符合这几个查询条件的其中一个条件,这样的记录就会被查询出来. 2) 如果不符合这些查询条件 ...