Buffer作为高效缓存支持读、写与撤销读取操作,可用于字节流缓存与解析等。这里简要分析一下其源码实现。
本文源码版本为go1.16.3 linux/amd64
Buffer可以在长度不足时增加自身长度,其核心增长函数是grow(n int) int
私有函数,源码在go/src/bytes/buffer.go
第117行:
// grow grows the buffer to guarantee space for n more bytes.
// It returns the index where bytes should be written.
// If the buffer can't grow it will panic with ErrTooLarge.
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 {
// We can slide things down instead of allocating a new
// slice. We only need m+n <= c to slide, but
// we instead let capacity get twice as large so we
// don't spend all our time copying.
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
}
其中,tryGrowByReslice
的作用主要就是在b.buf
的容量满足需求而长度len
不满足需求的情况下,初始化出n字节新空间,使长度达标,其源码如下:
// tryGrowByReslice is a inlineable version of grow for the fast-case where the
// internal buffer only needs to be resliced.
// It returns the index where bytes should be written and whether it succeeded.
func (b *Buffer) tryGrowByReslice(n int) (int, bool) {
if l := len(b.buf); n <= cap(b.buf)-l {
b.buf = b.buf[:l+n]
return l, true
}
return 0, false
}
而makeSlice
就是在make([]byte, n)
之前简单加了个defer
,就不放源码了。
可以看出,grow(n)
的作用是确保缓存可以腾出至少n字节的空闲空间。函数首先调用tryGrowByReslice
来判断b.buf
的剩余空间是否已经满足要求,并初始化n位字节空间;在不满足的情况下,再尝试通过重排已有元素来获取足够的空间;若重排不能解决问题,就重新分配一块更大的内存。
由于grow
函数的这种机制,Buffer在某些特性上类似于自增长型循环队列,其“rear指针”相当于len(b.buf)
,“front指针”相当于b.off
。写入操作(相当于入队)时,len(b.buf)
增长;发生读取操作(相当于出队)时,b.off
前移;与循环队列不同的是,当Buffer即将假溢出时,不是“rear指针”移到头部重新循环,而是从b.off
开始把所有未使用元素平移(slide down)至头部。虽然全体平移操作的时间复杂度O(m)比指针循环O(1)要高,但因为copy
函数是底层内建函数,其实际占用时间并不多;并且为了尽一步减少频繁平移造成的开销,“假溢出”的判断标准并不是当前cap
,而是cap
的两倍。这里也可以看出golang的设计逻辑,即在现代计算机硬件已经足够发达的前提下,可以通过适当牺牲资源开销来极大方便软件开发,这与C/C++的设计思想刚好相反。
Buffer支持Read
、ReadByte
与ReadRune
三大主要读取操作,其中Read
不可撤销,ReadByte
与ReadRune
可撤销一次。
Buffer在读取n个字节后,会把b.off
前移n字节。所谓的撤销,本质是把b.off
再移回读取操作之前的位置。具体实现上,UnreadByte
是b.off--
,UnreadRune
是前移b.lastRead
字节。
这里简单举一个撤销读取的应用:在流式数据传输时需要找同步头,可以读取一个字节并对比,如果是同步头就继续读取;如果不是同步头,就撤销读取,使数据维持原状并传递到下一个需要数据的环节。
Buffer.Truncate(n int)
可以截断缓存,只保留需要的部分。此函数会同步改变b.buf
的len
与cap
属性。源码如下:// Truncate discards all but the first n unread bytes from the buffer
// but continues to use the same allocated storage.
// It panics if n is negative or greater than the length of the buffer.
func (b *Buffer) Truncate(n int) {
if n == 0 {
b.Reset()
return
}
b.lastRead = opInvalid
if n < 0 || n > b.Len() {
panic("bytes.Buffer: truncation out of range")
}
b.buf = b.buf[:b.off+n]
}
Buffer.Reset()
会完全重置b.buf
与b.off
,其源码很简单:// Reset resets the buffer to be empty,
// but it retains the underlying storage for use by future writes.
// Reset is the same as Truncate(0).
func (b *Buffer) Reset() {
b.buf = b.buf[:0]
b.off = 0
b.lastRead = opInvalid
}