现象
最近使用bytes.Buffer的过程中发现了一个有意思的问题
| 1 | var b bytes.Buffer | 
在对bytes.Buffer进行Reset操作后,依然还可以影响到之前的数据。如果改为用String方法:
| 1 | var b bytes.Buffer | 
原理
首先看看String和Bytes方法的定义
| 1 | func (b *Buffer) Bytes() []byte { return b.buf[b.off:] } | 
| 1 | func (b *Buffer) String() string { | 
抛开处理nil情况的分支,其实只是进行了一次将[]byte转换为string的操作,仅仅多了这一次转换,就能造成结果的不同吗?
要弄懂原因,首先要看Reset方法到底reset了什么
| 1 | // Reset resets the buffer to be empty, | 
从代码和注释可以了解到,Reset方法只会将内部b.buf进行re-slice,reset后依旧是复用同一个底层数组
string 和 slice 在运行时的数据结构表示如下
| 1 | type StringHeader struct { | 
与slice相比,string结构中没有表示容量的Cap字段,这是由于golang中的string是不可变的,golang不支持直接修改string类型变量的内存空间,只能通过改变string类型变量指向的内存空间来达成“修改”内容的目的。即,runtime.stringStruct中的str指针针指向的内容是不可改变的,但指针本身可以改变
当我们通过string([]byte)将[]byte转换为string时, 背后将调用runtime.slicebytetostring 函数,该函数会根据传入缓冲区的大小决定是否要分配一个新的内存空间,runtime.stringStructOf 会将字符串指针转化为runtime.stringStruct结构体指针,再设置了字符串指针p和长度n之后调用memmove将原[]byte中的字节copy到新的内存中
| 1 | func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str string) { | 
所以Buffer.String方法实际上进行了一次Copy操作,因此之后对底层数组的修改并不会反映在其之上
strings.Builder
如果不想在[]byte转换为string时进行额外的内存分配,可以使用strings.Builder,strings.Builder和bytes.Buffer的底层实现基本类似,两者差别主要是String / Reset方法:
| 1 | func (b *Builder) String() string { | 
String方法使用指针操作,丢弃SliceHeader结构的Cap字段,将这一块内存布局解释为StringHeader,因此不会有额外内存分配
| 1 | func (b *Builder) Reset() { | 
Reset方法将b.buf置为空,因此reset之后新的写入操作并不是对同一片地址。这也是和bytes.Buffer的不同之处
和bytes.Buffer相比,string.Builer还有一个限制:为了保证共用buf导致的底层数组污染,strings.Builder 在Grow/ Write / WriteRune / WriteString 这几个方法中添加了禁止copy的限制
| 1 | func (b *Builder) copyCheck() { | 
| 1 | var b1 strings.Builder | 
但如果复制后的结构在写入前先进行了Reset操作,则调用Write相关方法也不会panic。因为此时buf已被设置为nil,相关的写入操作已无法对其他结构体中的buf造成影响
| 1 | var b1 strings.Builder |