Go 语言核心 36 讲
郝林
《Go 并发编程实战》作者,前轻松筹大数据负责人
79610 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 55 讲
Go 语言核心 36 讲
15
15
1.0x
00:00/00:00
登录|注册

39 | bytes包与字节串操作(下)

对比String方法的高效性
方法可能造成内容泄露的问题
扩容方法的原则
已读计数的重要性
使用字节切片作为内容容器
功能差异
问题的解决方式
Next方法
Bytes方法
定义内容泄露
零值状态的Buffer值
优化策略
扩容代码的操作
判断内容容器的剩余容量
手动扩容与自动扩容
思考题
bytes.Buffer与strings.Builder的比较
问题2:bytes.Buffer中可能造成内容泄露的方法
问题1:bytes.Buffer的扩容策略
总结
知识扩展
bytes包与字节串操作

该思维导图由 AI 生成,仅供参考

你好,我是郝林,今天我们继续分享 bytes 包与字节串操作的相关内容。
在上一篇文章中,我们分享了bytes.Buffer中已读计数的大致功用,并围绕着这个问题做了解析,下面我们来进行相关的知识扩展。

知识扩展

问题 1:bytes.Buffer的扩容策略是怎样的?

Buffer值既可以被手动扩容,也可以进行自动扩容。并且,这两种扩容方式的策略是基本一致的。所以,除非我们完全确定后续内容所需的字节数,否则让Buffer值自动去扩容就好了。
在扩容的时候,Buffer值中相应的代码(以下简称扩容代码)会先判断内容容器的剩余容量,是否可以满足调用方的要求,或者是否足够容纳新的内容。
如果可以,那么扩容代码会在当前的内容容器之上,进行长度扩充。
更具体地说,如果内容容器的容量与其长度的差,大于或等于另需的字节数,那么扩容代码就会通过切片操作对原有的内容容器的长度进行扩充,就像下面这样:
b.buf = b.buf[:length+need]
反之,如果内容容器的剩余容量不够了,那么扩容代码可能就会用新的内容容器去替代原有的内容容器,从而实现扩容。
不过,这里还有一步优化。
如果当前内容容器的容量的一半,仍然大于或等于其现有长度(即未读字节数)再加上另需的字节数的和即:
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文详细介绍了bytes.Buffer的扩容策略和可能导致内容泄露的方法。在扩容策略方面,文章指出Buffer值可以手动或自动扩容,扩容时会优先考虑减少内存分配和内容拷贝,只有在容量无法满足要求时才会创建新的内容容器。此外,文章还提到了Bytes方法和Next方法可能导致内容泄露,因为它们返回的结果值在一段时期内会与内容容器共用同一个底层数组,可能导致外界操纵相关联Buffer值的内容,造成严重的数据安全问题。总结来看,bytes.Buffer类型不仅可以拼接、截断字节序列,还可以顺序读取子序列,使用字节切片作为内容容器,并实时记录已读字节的计数。文章强调了已读计数在Buffer值中的关键作用,以及扩容方法的原则和内容泄露可能带来的严重后果。读者通过本文可以快速了解bytes.Buffer的特点和注意事项,对于使用和操作bytes.Buffer类型将有很大帮助。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Go 语言核心 36 讲》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(20)

  • 最新
  • 精选
  • moonfox
    文章中说:“如果当前内容容器的容量的一半,仍然大于或等于其现有长度再加上另需的字节数的和,cap(b.buf)/2 >= len(b.buf)+need”。 如果是这样的话,那说明还有很多的未使用的空间,起码有一半,那就不用扩容,直接追加就可以了。所以这里的代码是错的,源码(1.16.2)中是 n <= c/2-m ,即cap(b.buf) >= b.Len() + need,也就是未读字节长度 + 另需的字节数 小于等于容量的一半时,才会把未读字节copy到buf的头部,只有在未读字节较少的时候,才发生copy,如果有太多的未读字节,就不copy到头部了(费时)

    作者回复: 源码: n <= c/2-m 相当于: n <= cap(b.buf)/2 - b.Len() 相当于: n + b.Len() <= cap(b.buf)/2 得出: cap(b.buf)/2 >= len(b.buf) + need (为了在省略掉多余上下文的同时保持清晰,我在这里使用了更容易理解的代码 cap(b.buf) 和 len(b.buf) ,以及英文单词 need) 所以我这里没写错啊。你后面描述的不是跟我说的一个意思是么?? 不过,如果这里让你产生了误解,说明我的文字有所欠缺,我改进一下吧。

    2021-06-02
    3
    1
  • Lywane
    老师,Buffer的Write,WriteString方法返回一个int,一个err,int是参数长度,err永远是nil,感觉这个返回没啥用啊。这么设计是为了实现某些接口么?

    作者回复: 对,主要是实现接口,而且也保留返回错误值的权利和义务。

    2020-04-08
    1
  • 疯琴
    请问老师,用 bytes.Buffer 而不用字节切片是因为它有计数器和一些方法使得操作更方便,还有高效的扩容策略么。这么说对么?

    作者回复: 因为有缓冲区啊,比较适合分步构建字符串值。

    2020-01-07
    1
  • 窗外
    老师你好,为什么我本地的src/runtime包下的stringtoslicebyte方法里面tmpBuf的默认长度是32。 所以文中例子,输出的容量是32

    作者回复: 你可以把前因后果都摆上来。这篇文章里有 tmpBuf 吗?

    2019-08-11
    4
    1
  • killer
    2023年了都,大佬还在看评论吗?写了4年go了总感觉差点意思,最近把郝大佬的文章翻出来每篇仔细阅读、做笔记、看源码,另外也在团队给大家分享golang的学习之路特别充实; 对stringtoslicebyte做了debug,没想到cap为8是这么来的 func stringtoslicebyte(buf *tmpBuf, s string) []byte { var b []byte if buf != nil && len(s) <= len(buf) { *buf = tmpBuf{} b = buf[:len(s)] } else { b = rawbyteslice(len(s))// bytes.Buffer 初始化 buf 为空,需要调用 rawbyteslice 初始化 slice } copy(b, s) return b } func rawbyteslice(size int) (b []byte) { cap := roundupsize(uintptr(size))// roundupsize 计算容量,重点关注这里 p := mallocgc(cap, nil, false)// 分配内存 if cap != uintptr(size) { memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size)) } *(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)} return } // Returns size of the memory block that mallocgc will allocate if you ask for the size. func roundupsize(size uintptr) uintptr { if size < _MaxSmallSize { if size <= smallSizeMax-8 { // 以大佬例子为例:contents := "ab" buffer1 := bytes.NewBufferString(contents),size长度为2,肯定小于smallSizeMax-8 return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]]) // divRoundUp 函数算出来 为1, size_to_class8[1]=1;class_to_size[1]=8返回的uintptr为8,这里做了一个约定类似于查表 } else { return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]]) } } if size+_PageSize < size { return size } return alignUp(size, _PageSize) } // 这里算出来是1 func divRoundUp(n, a uintptr) uintptr { // a is generally a power of two. This will get inlined and // the compiler will optimize the division. return (n + a - 1) / a // (8+2-1)/ 8=1 }

    作者回复: 嗯嗯,虽然已经这么多年了,但是几乎每次开电脑的时候还是会打开这个页面。不错的,好好看源码,加油!

    2023-12-20归属地:四川
  • costaLong
    contents := "ab" buffer1 := bytes.NewBufferString(contents) fmt.Printf("The capacity of new buffer with content %q: %d\n", contents, buffer1.Cap()) // 内容容器的容量:8 单独执行这段代码输出的结果是:The capacity of new buffer with content "ab": 32 请问是原因呢

    作者回复: 没看明白,“单独执行这段代码”是什么意思?

    2022-02-17
  • rename
    如果当前内容容器的容量的一半,仍然大于或等于其现有长度再加上所需的字节数的和,即:cap(b.buf)/2 >= len(b.buf)+need 这边len(b.buf)用b.Len()似乎更准确?才是获取未读部分的实际长度

    作者回复: 当然是要看 buf 本身的长度了。这是 bytes.Buffer 内部的算法,你可以看一看源码。

    2019-07-14
    2
  • 嘎嘎
    源码里给了推荐的构建方法 // 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:]) }

    作者回复: 这个Buffer类型比strings.Builder类型出现要早。我觉得后者质量更高一些。你可以参看一下后者的String方法。

    2019-03-15
  • 失了智的沫雨
    如果只看strings.Builder 和bytes.Buffer的String方法的话,strings.Builder 更高效一些。 我们可以直接查看两个String方法的源代码,其中strings.Builder String方法中 *(*string)(unsafe.Pointer(&b.buf)) 是直接取得buf的地址然后转换成string返回。 而bytes.Buffer的String方法是 string(b.buf[b.off:]) 对buf 进行切片操作,我认为这比直接取址要花费更多的时间。 测试函数: func BenchmarkStrings(b *testing.B) { str := strings.Builder{}/bytes.Buffer{} str.WriteString("test") for i := 0; i < b.N; i++ { str.String() } } 结果为 BenchmarkStrings-8 2000000000 0.66 ns/op BenchmarkBuffer-8 300000000 5.64 ns/op 所以strings.Builder的String方法更高效
    2018-11-11
    3
    34
  • 🐻
    https://github.com/golang/go/blob/master/src/strings/builder_test.go#L319-L366 发现最后的问题,Go 的标准库中,已经给出了相关的测试代码了。
    2019-04-26
    7
收起评论
显示
设置
留言
20
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部