【golang】是否复用buffer?分级缓冲池实现&slice底层原理

是否复用buffer?

场景:压缩/解压缩场景,需要buffer来缓冲压缩或解压缩数据,eg:

go 复制代码
func main() {
   var buf = make([]byte, 0, 1024)
   out, err := gzipCompress([]byte("hello world"), buf)
   if err != nil {
      fmt.Println("err:", err)
      return
   }
   fmt.Println(out)
}

func gzipCompress(src []byte, buf []byte) (out []byte, err error) {
   buffer := bytes.NewBuffer(buf)
   writer := gzip.NewWriter(buffer)
   _, err = writer.Write(src)
   if err != nil {
      writer.Close()
      fmt.Println("err1:", err)
      return nil, err
   }

   err = writer.Close()
   if err != nil {
      fmt.Println("err2:", err)
      return nil, err
   }

   return buffer.Bytes(), nil

   // ====另一种返回数据的方式。(埋个坑,后续讲)====
   //out = make([]byte, len(buffer.Bytes()))
   //copy(out, buffer.Bytes())
   //return out, nil
}

每次调用gzipCompress时都要一个缓冲区,有2种选择:

  1. 每次都新建,但频繁新建对象开销会比较大
  2. 使用sync.Pool创建缓冲池,复用之前新建的buffer,减少频繁新建对象的开销。

那到底是复用buffer好,还是每次都新建buffer好呢?

先说结论:

复用切片是有坑的:复用buffer的话,压缩后返回数据最好copy一份新的,避免复用带来的坑:下次压缩再复用buffer时可能把上次的返回结果给篡改(后续「复用buffer的坑」会讲)。

但copy数据的话就要out = make([]byte, len(buffer))copy(out, buffer),和不复用buffer一样都要频繁创建buffer,反而不复用buffer时还不会copy数据,所以是不是不复用更好?其实也不一定,复用的话,copy时make([]byte, len(buffer))容量是提前定好的,不复用的话,除非每次新建的buffer容量一定足够、开销才会比复用低,否则就会扩容,而扩容的开销很大:至少会扩容一次,创建一次容量足够的数组,然后copy数据,也会包含复用buffer时「out = make([]byte, len(buffer))copy(out, buffer)的开销」。

复用buffer的坑

go 复制代码
func main() {
   test1()
}

func test1() {
   var b = make([]byte, 0, 1024)
   b = b[:0]

   out := compress([]byte("hello"), b)
   fmt.Println("out:", string(out))

   b = b[:0]
   out2 := compress([]byte("world"), b)
   fmt.Println("out:", string(out))
   fmt.Println("out2:", string(out2))
}

func compress(data []byte, b []byte) (out []byte) {
   buf := bytes.NewBuffer(b)
   buf.Write(data)
   buf.Write([]byte("compress_tag"))

   // ====返回数据的方案1,copy一份新数据===
   out = make([]byte, len(buf.Bytes()))
   copy(out, buf.Bytes())
   return out

   // ====返回数据的方案2===
   //return buf.Bytes()
}

compress方法采用方案1(copy一份新数据)返回数据的执行结果:

text 复制代码
out: hellocompress_tag
out: hellocompress_tag
out2: worldcompress_tag

compress方法采用方案2返回数据的执行结果:

text 复制代码
out: hellocompress_tag
out: worldcompress_tag
out2: worldcompress_tag

可以看到,方案2的话,第2次compress会影响第1次compress的返回结果。

slice底层原理

参考

分级缓冲池

固定缓冲池:

go 复制代码
var pool = sync.Pool{
   New: func() any {
      return make([]byte, 0, 1024)
   },
}

分级缓冲池,更精细化控制

go 复制代码
type MultiLevelBufPool struct {
   small  sync.Pool
   medium sync.Pool
   large  sync.Pool
}

func NewMultiLevelBufPool() *MultiLevelBufPool {
   return &MultiLevelBufPool{
      small: sync.Pool{
         New: func() any {
            return make([]byte, 0, 1024) // 1KB
         },
      },
      medium: sync.Pool{
         New: func() any {
            return make([]byte, 0, 4096) // 4KB
         },
      },
      large: sync.Pool{
         New: func() any {
            return make([]byte, 0, 16<<10) // 16KB
         },
      },
   }
}
func (m *MultiLevelBufPool) GetBuffer(expectedSize int) []byte {
   switch {
   case expectedSize <= 1024:
    return m.small.Get().([]byte)[:0]
   case expectedSize <= 4096:
    return m.medium.Get().([]byte)[:0]
   default:
    return m.large.Get().([]byte)[:0]
   }
}
func (m *MultiLevelBufPool) PutBuffer(buf []byte) {
   switch {
   case len(buf) <= 1024:
      m.small.Put(buf[:0])
   case len(buf) <= 4096:
      m.medium.Put(buf[:0])
   case len(buf) <= 16<<10:
      m.large.Put(buf[:0])      
   default:
      // 丢弃
      return
   }
}
相关推荐
_码力全开_17 小时前
P1005 [NOIP 2007 提高组] 矩阵取数游戏
java·c语言·c++·python·算法·矩阵·go
程序员爱钓鱼20 小时前
Python编程实战 · 基础入门篇 | Python程序的运行方式
后端·go
光头闪亮亮2 天前
gozxing库-对图片中多个二维码进行识别的完整示例教程
go
召摇2 天前
在浏览器中无缝运行Go工具:WebAssembly实战指南
后端·面试·go
王中阳Go3 天前
我发现不管是Java还是Golang,懂AI之后,是真吃香!
后端·go·ai编程
半枫荷3 天前
二、Go语法基础(基本语法)
go
struggle20254 天前
AxonHub 开源程序是一个现代 AI 网关系统,提供统一的 OpenAI、Anthropic 和 AI SDK 兼容 API
css·人工智能·typescript·go·shell·powershell
Mgx4 天前
高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全
go
考虑考虑4 天前
go格式化时间
后端·go
光头闪亮亮5 天前
ZBar 条码/二维码识别工具介绍及golang通过cmd调用ZBar从图片中批量识别二维码
go