บทที่ 9 · Part 3 — Language and Data Semantics
Modern Collections
ใช้ slices, maps, iterators และ samber/lo โดยไม่ซ่อน cost หรือสร้าง pipeline ที่อ่านยาก
ทีมที่เพิ่งใช้ generics มักแกว่งระหว่าง 2 สุดขั้ว: เขียน loop ซ้ำทุกแห่งเพราะ “Go ต้องเรียบง่าย” หรือสร้าง functional pipeline ทุกบรรทัดเพราะ type-safe helpers ทำได้แล้ว คำตอบที่ดูแลได้คือเลือก abstraction ตาม intent, data size และ debugging cost โดยไม่ซ่อน allocation
จบบทนี้คุณจะ
- ป้องกัน slice aliasing และ retained backing array
- ใช้
slices,maps,iterและ loop ธรรมดาในจุดที่เหมาะ - รู้ว่าเมื่อไร
samber/loเป็น established default ที่คุ้ม dependency
Slice คือ View บน Array
slice มี pointer, length และ capacity การ append อาจเขียนลง backing array เดิมหรือ allocate ใหม่
ขึ้นกับ capacity ดังนั้น code ที่ดูเหมือนคืนค่าใหม่อาจแก้ข้อมูลของ caller:
func AppendPending(statuses []Status) []Status {
out := slices.Clone(statuses)
return append(out, StatusPending)
}
อย่า clone อัตโนมัติทุกครั้ง ให้ clone เมื่อ contract ต้อง independent และ document เมื่อ function
รับ ownership เพื่อ reuse memory การตัด subslice ขนาดเล็กจาก buffer ใหญ่อาจทำให้ buffer ทั้งก้อน
ยังไม่ถูก GC; ถ้าจะ retain ระยะยาวให้ strings.Clone, bytes.Clone หรือ copy เฉพาะส่วนที่ต้องใช้
ถ้ารู้จำนวนโดยประมาณ ใช้ make([]T, 0, n) เพื่อลด allocation แต่ไม่ preallocate จาก input
ที่ผู้ใช้ควบคุมโดยไม่มี limit เพราะค่า n ขนาดใหญ่กลายเป็น memory exhaustion ได้
Standard Library ก่อน
Go รุ่นปัจจุบันมี slices และ maps สำหรับงานทั่วไป:
slices.Sort(batchIDs)
batchIDs = slices.Compact(batchIDs)
statusByID := maps.Clone(cachedStatuses)
maps.DeleteFunc(statusByID, func(_ string, status Status) bool {
return status == StatusExpired
})
ใช้ built-in clear เมื่อต้องล้าง map/slice ตาม semantics ของภาษา ใช้ min/max แทน helper
ของทีมเมื่อ type รองรับ และจำว่า map iteration order ไม่ใช่ contract หาก response/test ต้อง stable
ให้รวบรวม keys แล้ว sort อย่างชัดเจน
loop ธรรมดายังเป็น default ที่ดีเมื่อมีหลายเงื่อนไข, early continue, error หรือ state ระหว่างทาง:
selected := make([]Batch, 0, len(batches))
for _, batch := range batches {
if batch.Status != StatusPending {
continue
}
if err := authorize(batch); err != nil {
return nil, err
}
selected = append(selected, batch)
}
Iterator เมื่อข้อมูลควรไหลแบบ Lazy
Go 1.23 เพิ่ม range-over-function และ package iter ทำให้ API ส่ง sequence แบบ lazy ได้ เหมาะกับ
tree traversal, pagination หรือ pipeline ที่ไม่ควรโหลดทุก item เข้าหน่วยความจำ แต่ iterator ไม่ได้
ทำ I/O lifecycle หายไป API ต้องอธิบายว่า error ส่งอย่างไร, resource ปิดเมื่อ consumer หยุดกลางทาง
อย่างไร และ sequence ใช้ซ้ำหรือ concurrent ได้หรือไม่
func Pending(batches []Batch) iter.Seq[Batch] {
return func(yield func(Batch) bool) {
for _, batch := range batches {
if batch.Status == StatusPending && !yield(batch) {
return
}
}
}
}
ถ้า sequence มีเพียง 10 รายการและ caller ต้องการ slice อยู่แล้ว iterator อาจเพิ่ม cognitive cost โดยไม่ลด memory หรือ latency
เมื่อไรให้ใช้ samber/lo
คอร์สนี้แนะนำ github.com/samber/lo เป็น library เจ้าประจำ
สำหรับ operations ที่ standard library ยังไม่มีและปรากฏซ้ำอย่างมีรูปแบบ เช่น FilterMap,
GroupBy, Chunk, UniqBy หรือการทำงานกับ map entries มันแทน helper generic ที่แต่ละทีมเขียน
เองและมี test ไม่ครบ
pendingIDs := lo.FilterMap(batches, func(batch Batch, _ int) (string, bool) {
return batch.ID, batch.Status == StatusPending
})
อย่างไรก็ตามอย่า chain หลาย operation จน allocation และ failure path มองไม่เห็น หาก pipeline ต้อง scan slice 3 รอบ สร้าง intermediate 2 ชุด และมี error กลางทาง loop เดียวมักชัดและเร็วกว่า ก่อนเพิ่ม dependency ให้ pin version, ตรวจ license, release activity, vulnerability และผลต่อ binary/module graph ตามกระบวนการใน Tool-Driven Review
Production Toolbox
Default: slices/maps สำหรับ primitive มาตรฐาน, loop สำหรับ logic ที่มี state/error และ
samber/lo สำหรับ collection vocabulary ที่ stable และใช้ซ้ำ มันแทน generic helpers ของทีม
ไม่ได้แทนความจำเป็นในการเข้าใจ allocation, ordering และ ownership
Checklist เลือก Collection API
- contract เรื่อง nil/empty, order และ ownership ชัด
- preallocation มี bound และอิงข้อมูลจริง
- ไม่ retain backing array ใหญ่เพราะ subslice เล็ก
- ใช้ stdlib operation เมื่อมีชื่อและ semantics ตรง
- ใช้
loเมื่อ vocabulary ช่วยอ่านและลด helper ซ้ำ ไม่ใช่เพื่อหลีกเลี่ยงfor - iterator ระบุ error, cleanup, early stop และ concurrency contract
- ถ้าอ้าง performance มี benchmark จาก shape ของข้อมูลจริง
อ่านเพิ่ม: slices, maps,
Range Over Function Types และ
Go 1.23 release notes