字符串拼接和strings.Buffer缺点
Go里面的字符串是常量,对字符串的修改会重新申请内存地址。
虽然bytes.Buffer避免了字符串修改过程中的内存申请,但是最后从[]byte转成字符串时会重新内存申请。从Go 1.10开始,提供了性能更好的方法strings.Builder,与bytes.Buffer 一样,通过[]byte来保存数据。
实现原理
字段
type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte
}
写入数据
在[]byte后面追加内容。
func (b *Builder) Write(p []byte) (int, error) {
b.copyCheck()
b.buf = append(b.buf, p...)
return len(p), nil
}
预分配内存
func (b *Builder) grow(n int) {
buf := make([]byte, len(b.buf), 2*cap(b.buf)+n)
copy(buf, b.buf)
b.buf = buf
}
func (b *Builder) Grow(n int) {
b.copyCheck()
if n < 0 {
panic("strings.Builder.Grow: negative count")
}
if cap(b.buf)-len(b.buf) < n {
b.grow(n)
}
}
扩容的结果是2*cap(b.buf)+n,之前容量的两倍加n。
如果容量是10,长度是5,调用Grow(3)结果是没有任何操作。
如果容量是10,长度是5,调用Grow(7)结果是2*10+7=27。
返回string时避免字符串拷贝
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
先获取字节数组的首地址,再通过unsafe.Pointer作为桥梁来转化成string的指针类型,最后获取该指针类型的值。
不允许值拷贝
addr指针指向自身。
func (b *Builder) copyCheck() {
if b.addr == nil {
// This hack works around a failing of Go's escape analysis
// that was causing b to escape and be heap allocated.
// See issue 23382.
// TODO: once issue 7921 is fixed, this should be reverted to
// just "b.addr = b".
b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
} else if b.addr != b {
panic("strings: illegal use of non-zero Builder copied by value")
}
}
在Grow、Write、WriteByte、WriteString、WriteRune这五个函数里都有这个检查逻辑,避免发生strings.Builder的值拷贝。
package main
import "strings"
func main() {
a := strings.Builder{}
a.WriteString("abc")
b := a
b.WriteString("abc")
}