go语言string之Buffer与Builder

操作字符串离不开字符串的拼接,但是Go中string是只读类型,大量字符串的拼接会造成性能问题。

  • 字符串拼接的方式与性能对比?
  • bytes.Buffer 与 strings.Builder?
  • Buffer 和 Builder底层原理实现?

字符串拼接的方式与性能对比

拼接字符串,无外乎四种方式,采用“+”,“fmt.Sprintf()”,"bytes.Buffer","strings.Builder"

import(
    "bytes"
    "fmt"
    "strings"
    "time"
)

func AppendWithAdd(n int) {
    var s string
    for i:=0; i < n; i++{
        s = s + "string"
    }
}

func AppendWithSprintf(n int) {
    var s string
    for i:=0; i < n; i++{
        s = fmt.Sprintf("%s%s", s, "string")
    }
}

func AppendWithBytesBuffer(n int) {
    var byt bytes.Buffer
    for i:=0; i < n; i++{
        byt.WriteString("string")
    }
    byt.String()
}

func AppendWithStringBuilder(n int) {
    var sbuilder strings.Builder
    for i:=0; i < n; i++{
        sbuilder.WriteString("string")
    }
    sbuilder.String()
}

 go test -test.bench=.* -count=5
goos: windows
goarch: amd64
pkg: /studyGo/first
Benchmark_AppendWithAdd-8                     42          27607255 ns/op
Benchmark_AppendWithAdd-8                     36          27935317 ns/op
Benchmark_AppendWithAdd-8                     42          27947221 ns/op
Benchmark_AppendWithAdd-8                     42          27915798 ns/op
Benchmark_AppendWithAdd-8                     37          27857249 ns/op
Benchmark_AppendWithSprintf-8                 30          35487310 ns/op
Benchmark_AppendWithSprintf-8                 32          35940897 ns/op
Benchmark_AppendWithSprintf-8                 32          36376653 ns/op
Benchmark_AppendWithSprintf-8                 32          35950091 ns/op
Benchmark_AppendWithSprintf-8                 32          36089072 ns/op
Benchmark_AppendWithBytesBuffer-8          17287             69116 ns/op
Benchmark_AppendWithBytesBuffer-8          17212             69301 ns/op
Benchmark_AppendWithBytesBuffer-8          17262             69235 ns/op
Benchmark_AppendWithBytesBuffer-8          14858            136065 ns/op
Benchmark_AppendWithBytesBuffer-8          17072            102340 ns/op
Benchmark_AppendWithStringBuilder-8        22573             57630 ns/op
Benchmark_AppendWithStringBuilder-8        21070             87849 ns/op
Benchmark_AppendWithStringBuilder-8        26326             53106 ns/op
Benchmark_AppendWithStringBuilder-8        20924             89193 ns/op
Benchmark_AppendWithStringBuilder-8        26348             52523 ns/op

上面我们创建10万字符串拼接的测试,可以发现"bytes.Buffer","strings.Builder"的性能最好,约是“+”的1000倍级别。

这是由于string是不可修改的,所以在使用“+”进行拼接字符串,每次都会产生申请空间,拼接,复制等操作,数据量大的情况下非常消耗资源和性能。而采用Buffer等方式,都是预先计算拼接字符串数组的总长度(如果可以知道长度),申请空间,底层是slice数组,可以以append的形式向后进行追加。最后在转换为字符串。这申请了不断申请空间的操作,也减少了空间的使用和拷贝的次数,自然性能也高不少。

bytes.Buffer 与 strings.Builder

bytes.buffer是一个缓冲byte类型的缓冲器存放着都是byte
是一个变长的 buffer,具有 Read 和Write 方法。 Buffer 的 零值 是一个 空的 buffer,但是可以使用,底层就是一个 []byte, 字节切片。

  • Buffer的使用
var b bytes.Buffer  //直接定义一个 Buffer 变量,而不用初始化
b.Writer([]byte("Hello ")) // 可以直接使用

b1 := new(bytes.Buffer)   //直接使用 new 初始化,可以直接使用
func NewBuffer(buf []byte) *Buffer
func NewBufferString(s string) *Buffer

向Buffer中写数据,可以看出Buffer中有个Grow函数用于对切片进行扩容。

// Write appends the contents of p to the buffer, growing the buffer as
// needed. The return value n is the length of p; err is always nil. If the
// buffer becomes too large, Write will panic with ErrTooLarge.
func (b *Buffer) Write(p []byte) (n int, err error) {
    b.lastRead = opInvalid
    m := b.grow(len(p))
    return copy(b.buf[m:], p), nil
}

从Buffer中读取数据

func (b *Buffer) Read(p []byte) (n int, err error) {}

 //声明一个空的slice,容量为8
    l := make([]byte, 8)
    //把bufs的内容读入到l内,因为l容量为8,所以只读了8个过来
    bufs.Read(l)
    fmt.Println("::bufs缓冲器内容::")
    fmt.Println(bufs.String())

···
//ReadString需要一个byte作为分隔符,读的时候从缓冲器里找第一个出现的分隔符
func (b *Buffer) ReadString(delim byte) (line string, err error) {}

//返回缓冲器头部的第一个byte,缓冲器头部第一个byte被拿掉
func (b *Buffer) ReadByte() (c byte, err error) {}
  • Builder的使用
var sbuilder strings.Builder
sbuilder.WriteString("string")

// 使用new也可以创建
var sb = new(strings.Builder)
sb.Write([]byte("hello"))
fmt.Printf("%s",sb.String())

strings.Builder的方法和bytes.Buffer的方法的命名几乎一致。

func (b *Builder) WriteByte(c byte) error {
    b.copyCheck()
    b.buf = append(b.buf, c)
    return nil
}

但实现并不一致,Builder的Write方法直接将字符拼接slice数组后。

其没有提供read方法,但提供了strings.Reader方式

Reader 结构:

type Reader struct {
    s        string
    i        int64 // current reading index
    prevRune int   // index of previous rune; or < 0
}

Buffer 和 Builder底层原理实现

Buffer:

// A Buffer is a variable-sized buffer of bytes with Read and Write methods.
// The zero value for Buffer is an empty buffer ready to use.
type Buffer struct {
    buf      []byte // contents are the bytes buf[off : len(buf)]
    off      int    // read at &buf[off], write at &buf[len(buf)]
    lastRead readOp // last read operation, so that Unread* can work correctly.
}

Builder:

// A Builder is used to efficiently build a string using Write methods.
// It minimizes memory copying. The zero value is ready to use.
// Do not copy a non-zero Builder.
type Builder struct {
    addr *Builder // of receiver, to detect copies by value
    buf  []byte
}

可以看出Buffer和Builder底层都是采用[]byte数组进行装载数据。


先来说说Buffer:

// Write appends the contents of p to the buffer, growing the buffer as
// needed. The return value n is the length of p; err is always nil. If the
// buffer becomes too large, Write will panic with ErrTooLarge.
func (b *Buffer) Write(p []byte) (n int, err error) {
    b.lastRead = opInvalid
    m, ok := b.tryGrowByReslice(len(p))
    if !ok {
        m = b.grow(len(p))
    }
    return copy(b.buf[m:], p), nil
}

创建好Buffer是一个empty的,off 用于指向读写的尾部。
在写的时候,先判断当前写入字符串长度是否大于Buffer的容量,如果大于就调用grow进行扩容,扩容申请的长度为当前写入字符串的长度。如果当前写入字符串长度小于最小字节长度64,直接创建64长度的[]byte数组。如果申请的长度小于二分之一总容量减去当前字符总长度,说明存在很大一部分被使用但已读,可以将未读的数据滑动到数组头。如果容量不足,扩展2*c + n 。

// To build strings more efficiently, see the strings.Builder type.
func (b *Buffer) String() string {
    if b == nil {
        // Special case, useful in debugging.
        return "<nil>"
    }
    return string(b.buf[b.off:])
}

其String()方法就是将字节数组强转为string


Builder是如何实现的。

// Write appends the contents of p to b's buffer.
// Write always returns len(p), nil.
func (b *Builder) Write(p []byte) (int, error) {
    b.copyCheck()
    b.buf = append(b.buf, p...)
    return len(p), nil
}

Builder采用append的方式向字节数组后添加字符串。

// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
//  slice = append(slice, elem1, elem2)
//  slice = append(slice, anotherSlice...)
// As a special case, it is legal to append a string to a byte slice, like this:
//  slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type
append源码
expand append(l1, l2...) to
//   init {
//     s := l1
//     n := len(s) + len(l2)
//     // Compare as uint so growslice can panic on overflow.
//     if uint(n) > uint(cap(s)) {
//       s = growslice(s, n)
//     }
//     s = s[:n]
//     memmove(&s[len(l1)], &l2[0], len(l2)*sizeof(T))
//   }


func main() {
    var sb = new(strings.Builder)
    sb.Write([]byte("12345678"))
    fmt.Printf("%s, %d\n",sb.String(),sb.Cap())
    sb.Write([]byte("9"))
    fmt.Printf("%s, %d\n",sb.String(),sb.Cap())
}

12345678, 8
123456789, 16

从上面可以看出,[]byte的内存大小也是以倍数进行申请的,初始大小为 0,第一次为大于当前申请的最大 2 的指数,不够进行翻倍.

append源码
if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }

可以看出如果旧容量小于1024进行翻倍,否则扩展四分之一。(2048 byte 后,申请策略的调整)。

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

其次String()方法与Buffer的string方法也有明显区别。Buffer的string是一种强转,我们知道在强转的时候是需要进行申请空间,并拷贝的。而Builder只是指针的转换。

这里我们解析一下*(*string)(unsafe.Pointer(&b.buf))这个语句的意思。

先来了解下unsafe.Pointer 的用法。

Pointer类型代表了任意一种类型的指针,类型Pointer有四种专属的操作:

任意类型的指针能够被转换成Pointer值
一个Pointer值能够被转换成任意类型的指针值
一个uintptr值能够被转换从Pointer值
一个Pointer值能够被转换成uintptr值

也就是说,unsafe.Pointer 可以转换为任意类型,那么意味着,通过unsafe.Pointer媒介,程序绕过类型系统,进行地址转换而不是拷贝。

即*A => Pointer => *B

func main() {
    var b = []byte{'H', 'E', 'L', 'L', 'O'}

    s := *(*string)(unsafe.Pointer(&b))

    fmt.Println("b =", b)
    fmt.Println("s =", s)

    b[1] = 'B'
    fmt.Println("s =", s)

    s = "WORLD"
    fmt.Println("b =", b)
    fmt.Println("s =", s)
    
    //b = [72 69 76 76 79]
    //s = HELLO
    //s = HBLLO
    //b = [72 66 76 76 79]
    //s = WORLD
}

就像上面例子一样,将字节数组转为unsafe.Pointer类型,再转为string类型,s和b中内容一样,修改b,s也变了,说明b和s是同一个地址。但是对s重新赋值后,意味着s的地址指向了“WORLD”,它们所使用的内存空间不同了,所以s改变后,b并不会改变。

所以他们的区别就在于 bytes.Buffer 是重新申请了一块空间,存放生成的string变量, 而strings.Builder直接将底层的[]byte转换成了string类型返回了回来,去掉了申请空间的操作。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容

  • 作者:李骁 10.1 字符串介绍 Go 语言中可以使用反引号或者双引号来定义字符串。反引号表示原生的字符串,即不进...
    ffhelicopter阅读 1,721评论 2 3
  • 目录 统一规范篇 命名篇 开发篇 优化篇 统一规范篇 本篇主要描述了公司内部同事都必须遵守的一些开发规矩,如统一开...
    零一间阅读 1,916评论 0 2
  • 最近研读了下go语言,所以想整理一番 string 在go中如何定义的?string 的底层原理与细节?strin...
    Tim在路上阅读 5,289评论 1 2
  • 内容 1 bytes.Buffer2 strings.Builder3 bufio包 前言bytes.Buffer...
    chase_lwf阅读 4,351评论 0 0
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,518评论 16 22