Golang学习实战笔记-基础 > golang代码性能优化
go字符串高效拼接

线上服务问题:  

  生产环境服务的pod内存会不断增加,后来我们在服务的加上定时采集pprof采集了一下程序运行的heap内存状态。发现 bytes.Bufffer String 方法占用了大量的堆内存。

  后来查看 bytes.Buffer 源代码发现了几个问题:

    (1)下面是(b *Buffer) String() string源码

func (b *Buffer) String() string {
	if b == nil {
		// Special case, useful in debugging.
		return "<nil>"
	}
	return string(b.buf[b.off:])
}

查看源码我们发现这里的Buffer.String() 方法返回的是一份副本相当于把字符串又拷贝一份。

我们对比一下strings.Builder的String() 方法,Builder 更加节约内存直接放了 buf的引用地址

func (b *Builder) String() string {
	return *(*string)(unsafe.Pointer(&b.buf))
}


    (2) 查看Buffer 的扩容机制

func (b *Buffer) grow(n int) int {
	m := b.Len()
	// If buffer is empty, reset to recover space.
	if m == 0 && b.off != 0 {
		b.Reset()
	}
	// Try to grow by means of a reslice.
	if i, ok := b.tryGrowByReslice(n); ok {
		return i
	}
	if b.buf == nil && n <= smallBufferSize {
		b.buf = make([]byte, n, smallBufferSize)
		return 0
	}
	c := cap(b.buf)
	if n <= c/2-m {
		copy(b.buf, b.buf[b.off:])
	} else if c > maxInt-c-n {
		panic(ErrTooLarge)
	} else {
		// Not enough space anywhere, we need to allocate.
		buf := makeSlice(2*c + n)
		copy(buf, b.buf[b.off:])
		b.buf = buf
	}
	// Restore b.off and len(b.buf).
	b.off = 0
	b.buf = b.buf[:m+n]
	return m
}

扩容的代码相对复杂一些,大概就是当内部的[]byte内存不够用了 1.如果前面字符串空间够先尝试将后面的挪到前面 ,2.如果不够用了在申请2*c +n的空间 。这里就是问题出现的地方。 设想当我们一开始向buffer 写入一个大的字符串,后面在写入一些小的字符串,这个时候buffer内部buf 的大小就是 大的字符串长度的2倍加上小字符串长度。无意中就多浪费了将近一倍的空间。

    

(3)Buffer 没有主动标记 内部的buf  为nil 导致。导致GC回收会更慢一些。

 下面我们对比一下 strings.Builder  和 bytes.Buffer Reset代码.

  strings.Builder.Reset

// 直接舍弃掉之前的  []byte 数组
func (b *Builder) Reset() {
	b.addr = nil
	b.buf = nil
}

bytes.Buffer.Reset

func (b *Buffer) String() string {
if b == nil {
// Special case, useful in debugging.
return "<nil>"
}
return string(b.buf[b.off:])
}


总结:

  1.  strings.Builder 更适合做字符串拼接,但是注意使用后最后reset 一下。

  2.  bytes.Buffer 适合做多次大内存读写操作,性能绝对没得说。

  3.  简单的一次字符串拼接不适合实用 bytes.buffer,尤其是第一次写入一个大的字符串后面在加上一个小字符串比较浪费空间。