一起读Go源码(第9期) bytes:buffer
友友们好啊,接着上次写完strings包的笔记,继续看bytes包。我发现如果把源码部分大小方法全介绍一遍不能抓住重点,不如介绍主流的、对用得多的函数结合场景进行解析。bytes包主要有buffer,bytes,reader这3个代码文件,其余都是单元测试文件。这次笔记就从buffer开始,Buffer是Go标准库中高效处理字节数据的核心工具,特别适合动态拼接、IO缓冲等场景,下面来看看。
字符串拼接
Buffer对字符串有几种方法,各有各的特点:
-
• WriteString直接写入字符串 -
• Write([]byte)写入字节切片 -
• WriteByte 写入单个字节 -
• WriteRune 写入unicode字符
具体示例如下,是不是越看越像strings包的一些方法
var buf bytes.Bufferbuf.WriteString("夏天夏天")buf.Write([]byte("悄悄过去"))buf.WriteByte('~')buf.WriteRune('小')data := buf.Bytes()log.Println(string(data))// 2026/01/09 20:19:15 夏天夏天悄悄过去~小
在strings包中也有WriteString,Write([]byte)等方法,符合Go标准库的设计理念——相同的行为,提供相同的API,以降低开发者的学习成本和心理负担;但实际上实现代码不一样,就拿WriteString源码看看:
// smallBufferSize is an initial allocation minimal capacity.const smallBufferSize = 64// A Buffer is a variable-sized buffer of bytes with [Buffer.Read] and [Buffer.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.}// WriteString appends the contents of s to the buffer, growing the buffer as// needed. The return value n is the length of s; err is always nil. If the// buffer becomes too large, WriteString will panic with [ErrTooLarge].func (b *Buffer) WriteString(s string) (n int, err error) { b.lastRead = opInvalid m, ok := b.tryGrowByReslice(len(s)) if !ok { m = b.grow(len(s)) } return copy(b.buf[m:], s), nil}
-
1. 首先看到一个 smaillBufferSize常量,意思是缓冲区初始分配的初始容量是64,64 字节是 Go 官方经过性能测试的黄金值,在绝大多数业务下可以避免小数据场景的频繁扩容。 -
2. Buffer是一个大小可变的缓冲区,有[Buffer.Read]和[Buffer.Write]的读写方法。0值Buffer是一个可以直接使用的空缓冲区,不需要初始化。Buffer结构体中有: -
• buf——缓冲区的有效内容区间; -
• off——读操作的指针,从&buf[off]开始读,从&buf[len(buf)]位置开始写。cap(buf)-len(buf)就是空闲容量; -
• lastRead——记录上一次执行的读操作类型,服务于Unread回退操作。 -
3. WriteString方法作用是将字符串s的内容追加的缓冲区末尾,缓冲区会按需自动扩容。返回值n是被写入的字节长度并与s相等,返回值err为nil;如果缓冲区扩容超过系统限制会触发panic,抛出[ErrTooLarge];具体我们看看 WriteString函数是怎么实现的: -
1. b.lastRead = opInvalid将上次读操作标记为无效, -
2. 尝试用 tryGrowByReslice方法扩容,如果不行就用grow方法扩容,这里的解决思路是如果缓冲区空闲大于写入的字节数用该方法写入,若空闲容量不足就返回再使用grow;我们看看tryGrowByReslice方法源码,// tryGrowByReslice is an 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}// 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 { // Add b.off to account for b.buf[:b.off] being sliced off the front. b.buf = growSlice(b.buf[b.off:], b.off+n) } // Restore b.off and len(b.buf). b.off = 0 b.buf = b.buf[:m+n] return m}该方法中先拿着计算缓冲区长度,计算空闲容量
cap(b.buf)-l能不能再写入n个字节,若满足条件则b.buf = b.buf[:l+n]将切片长度拉长到l+n,返回true告诉调用函数“空间挪好了”,调用函数可以直接进行拷贝,可以节省空间提高性能;若空间不满足则用grow扩容,提一下grow扩容的顺序是:当缓冲区为空单b.off != 0时,使用Reset回收已读空间,并继续使用tryGrowByReslice尝试重用当前容量,避免不必要的扩容。当所需空间不超过容量一半时滑动数据(将现有数据移动到缓冲区开头以回收已读取空间,避免重新分配内存),若容量不足才扩容。 -
3. 此时缓冲区已有足够容量,将字节拷贝并返回。 return copy(b.buf[m:], s), nil
IO接口
bytes.Buffer实现了io.Reader和io.Writer接口,可以直接跟文件、网络连接等IO对象交互以实现数据缓冲读写。就是一个buffer读取文件,打印内容以及创建文件并写入的示例。如下面os.Open打开文件,调用ReadRrom将文件数据读取到Buffer,再从Buffer读取内容转为字符串;或是从Buffer写入字符串,创建并调用WriteTo将字符串写入到文件。
buf := bytes.Buffer{}file, err := os.Open("file.txt")if err != nil { log.Println(err)}defer file.Close()res, err := buf.ReadFrom(file)if err != nil { log.Println(err)}log.Println("已读取字节:", res)log.Println(buf.String())var wbuf bytes.Bufferwbuf.WriteString("这是写入的内容")// 创建文件wfile, err := os.Create("output.txt")if err != nil { log.Println(err)}defer wfile.Close()wRes, err := wbuf.WriteTo(wfile)if err != nil { log.Println(err)}log.Println("已写入字节:", wRes)//2026/01/09 20:56:39 已读取字节: 28//2026/01/09 20:56:39 显示的是file.txt文件//2026/01/09 20:56:39 已写入字节: 21
读取文件
-
1. 从 os.Open读取文件,要记得defer file.Close关闭已打开的*os.File。 -
2. 从文件读取数据到 Buffer,看ReadFrom函数——该方法实现了Go标准库的io.ReadFrom接口,只要实现了该接口的对象都可以从io.Reader读取数据并写入,如文件、网络连接、字节流等;注释的意思是ReadFrom从r读取数据直到EOF(End of file,文件末尾),根据需要自动扩展缓冲区。返回的n是读取的字节数,读取过程中遇到除EOF外的错误都会一并返回,如果缓冲区过大会触发panic,错误为ErrTooLarge。// ReadFrom reads data from r until EOF and appends it to the buffer, growing// the buffer as needed. The return value n is the number of bytes read. Any// error except io.EOF encountered during the read is also returned. If the// buffer becomes too large, ReadFrom will panic with [ErrTooLarge].func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) { b.lastRead = opInvalid for { i := b.grow(MinRead) b.buf = b.buf[:i] m, e := r.Read(b.buf[i:cap(b.buf)]) if m < 0 { panic(errNegativeRead) } b.buf = b.buf[:i+m] n += int64(m) if e == io.EOF { return n, nil // e is EOF, so return nil explicitly } if e != nil { return n, e } }} -
1. 函数第一步重置读操作标记,然后进入for循环持续读数据,直到读完文件 -
2. 先扩容最小空间(MinRead = 512字节),从r中读取数据到buf的空闲空间。 -
3. b.buf = buf[:i+m]扩展buf的有效长度,写入多少字节就扩多长;并累加得到读取的字节数n,结束。 -
3. 此时已经得到读取字节,通过 .String()转换为字符串;注释内容是该方法返回缓冲区中尚未读取部分的内容,格式为字符串,如果buffer是一个nil指针则返回Nil;若想更高效拼接字符串请使用strings.Builder类型。// String returns the contents of the unread portion of the buffer// as a string. If the [Buffer] is a nil pointer, it returns "<nil>".//// 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:])} -
4. 那就能将buffer中的数据转换成string打印出来了~
写在最后
本人是新手小白,如果这篇笔记中有任何错误或不准确之处,真诚地希望各位读者能够给予批评和指正,如有更好的实现方法请给我留言,谢谢!欢迎大家在评论区留言!觉得写得还不错的话欢迎大家关注一波!下一篇我们来看看bytes的reader~
夜雨聆风
