บทที่ 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