Go bytes.Buffer.AvailableBuffer: ต่อข้อมูลลงพื้นที่ว่างโดยลด allocation
เมื่อเราประกอบข้อความทีละช่อง การสร้าง string ชั่วคราวทุกค่าเหมือนหยิบถุงใหม่ให้ของทุกชิ้นครับ bytes.Buffer.AvailableBuffer เปิด slice ของพื้นที่ว่างให้ strconv.Append* เติมข้อมูลตรงนั้น แล้ว Write ยืนยันความยาวกลับเข้า buffer
WHY: ลดของชั่วคราวใน hot path
วิธีนี้เหมาะกับ serializer เล็ก ๆ ที่วัดแล้วว่า allocation สำคัญ ไม่ใช่ของที่ควรใช้แทน fmt ทุกจุด สัญญาสำคัญคือ slice ใช้ได้จนถึงการเขียนครั้งถัดไปเท่านั้น ห้ามเก็บไว้ใน struct หรือใช้หลัง Write, Grow, Reset หรือการเปลี่ยน buffer อื่น
package main
import (
"bytes"
"strconv"
"testing"
)
func appendRow(buf *bytes.Buffer, name string, score int64) {
tail := buf.AvailableBuffer()
tail = strconv.AppendQuote(tail, name)
tail = append(tail, ',')
tail = strconv.AppendInt(tail, score, 10)
tail = append(tail, '\n')
_, _ = buf.Write(tail)
}
func TestAvailableBuffer(t *testing.T) {
var buf bytes.Buffer
buf.Grow(64)
appendRow(&buf, "mint", 42)
appendRow(&buf, "ไทย", -3)
want := "\"mint\",42\n\"ไทย\",-3\n"
if buf.String() != want {
t.Fatalf("got %q want %q", buf.String(), want)
}
if buf.Available() <= 0 {
t.Fatal("expected reusable capacity")
}
}
Trade-off ที่ต้องรู้
AvailableBuffer ไม่เพิ่มความยาว buffer เอง เราต้อง Write slice ที่ต่อเสร็จกลับไป และต้องจัดการ error ของ writer จริงเสมอ ตัวอย่างใช้ bytes.Buffer ซึ่ง Write ไม่คืน error แต่ถ้าย้ายแนวคิดไป I/O อื่นต้องไม่ทิ้ง error
ถ้าโค้ดไม่ได้อยู่ใน hot path ให้เริ่มจาก fmt.Fprintf หรือ strings.Builder ที่อ่านง่ายกว่า แล้ว benchmark ด้วย input จริงก่อน optimize เหมือนร้านข้าวตามสั่งที่เตรียมถาดไว้ล่วงหน้าเฉพาะช่วงเที่ยง ไม่จำเป็นต้องกางถาดทั้งวัน
Recap
- ใช้ slice ก่อนการเปลี่ยน buffer ครั้งถัดไปเท่านั้น
- ต่อข้อมูลด้วย
strconv.Append*แล้วWriteกลับ -
Growเมื่อประมาณขนาดได้ แต่ไม่พึ่ง capacity เป็น contract - benchmark ก่อนแลก readability กับ allocation ที่ลดลง