乐于分享
好东西不私藏

一起读Go源码(第9期) bytes:buffer

一起读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. 1. 首先看到一个smaillBufferSize常量,意思是缓冲区初始分配的初始容量是64,64 字节是 Go 官方经过性能测试的黄金值,在绝大多数业务下可以避免小数据场景的频繁扩容。
  2. 2. Buffer是一个大小可变的缓冲区,有[Buffer.Read]和[Buffer.Write]的读写方法。0值Buffer是一个可以直接使用的空缓冲区,不需要初始化。Buffer结构体中有:
    • • buf——缓冲区的有效内容区间;
    • • off——读操作的指针,从&buf[off]开始读,从&buf[len(buf)]位置开始写。cap(buf)-len(buf)就是空闲容量;
    • • lastRead——记录上一次执行的读操作类型,服务于Unread回退操作。
  3. 3. WriteString方法作用是将字符串s的内容追加的缓冲区末尾,缓冲区会按需自动扩容。返回值n是被写入的字节长度并与s相等,返回值err为nil;如果缓冲区扩容超过系统限制会触发panic,抛出[ErrTooLarge];具体我们看看WriteString函数是怎么实现的:
    1. 1.  b.lastRead = opInvalid将上次读操作标记为无效,
    2. 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. 3. 此时缓冲区已有足够容量,将字节拷贝并返回。return copy(b.buf[m:], s), nil

IO接口

bytes.Buffer实现了io.Readerio.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. 1. 从os.Open读取文件,要记得defer file.Close关闭已打开的*os.File
  2. 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. 1. 函数第一步重置读操作标记,然后进入for循环持续读数据,直到读完文件
    2. 2. 先扩容最小空间(MinRead = 512字节),从r中读取数据到buf的空闲空间。
    3. 3. b.buf = buf[:i+m]扩展buf的有效长度,写入多少字节就扩多长;并累加得到读取的字节数n,结束。
  3. 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. 4. 那就能将buffer中的数据转换成string打印出来了~

写在最后

本人是新手小白,如果这篇笔记中有任何错误或不准确之处,真诚地希望各位读者能够给予批评和指正,如有更好的实现方法请给我留言,谢谢!欢迎大家在评论区留言!觉得写得还不错的话欢迎大家关注一波!下一篇我们来看看bytes的reader~

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 一起读Go源码(第9期) bytes:buffer

评论 抢沙发

4 + 9 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮