go strings.Builder

发布时间 2023-07-15 10:29:12作者: 王景迁

字符串拼接和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")
}

参考资料

strings.Builder 源码分析