现象
最近使用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 |