Go maps.DeleteFunc: ล้างสมาชิกที่ไม่ผ่านเงื่อนไขใน map เดิม
เวลา cache หรือ index ชั่วคราวเริ่มโต โค้ดที่เห็นบ่อยคือวน for k, v := range m แล้ว delete(m, k) เมื่อค่าไม่ผ่านเงื่อนไข วิธีนั้นถูกต้อง แต่ความตั้งใจว่า “เก็บเฉพาะสมาชิกที่ยังใช้ได้” กระจายอยู่หลายบรรทัดและมักถูกคัดลอกไปหลายที่ maps.DeleteFunc รวมรูปแบบนี้เป็นฟังก์ชันมาตรฐานครับ
ให้นึกถึงตู้ล็อกเกอร์ที่มีการ์ดหมดอายุปะปนอยู่ เจ้าหน้าที่เดินครบทุกช่องและหยิบเฉพาะใบที่ predicate ตอบว่าให้ลบ ไม่ได้สร้างตู้ใหม่ และไม่คืน map ก้อนใหม่ให้เรา
WHY: บอกนโยบายให้ชัดกว่าบอกขั้นตอน
DeleteFunc รับ map กับฟังก์ชัน func(K, V) bool ถ้า callback คืน true สมาชิกนั้นจะถูกลบจาก map เดิม เหมาะกับ cache cleanup, การตัดค่าศูนย์, หรือการลบ record ที่ไม่ผ่าน policy แบบ synchronous ขนาดเล็ก ความซับซ้อนเวลาเป็น O(n) เพราะต้องตรวจทุกสมาชิก และใช้หน่วยความจำเพิ่ม O(1) ไม่นับ closure ที่เราเตรียมเอง
จุดที่อ่านผิดบ่อยคือ true แปลว่า ลบ ไม่ใช่เก็บ ดังนั้นตั้งชื่อ callback เป็น expired, invalid หรือ shouldDelete จะช่วยลดการกลับตรรกะผิด
HOW: ลบ cache ที่หมดอายุด้วยเวลาที่ฉีดเข้ามา
package main
import (
"maps"
"testing"
"time"
)
type Entry struct {
Value string
ExpiresAt time.Time
}
func pruneExpired(cache map[string]Entry, now time.Time) {
maps.DeleteFunc(cache, func(_ string, entry Entry) bool {
return !entry.ExpiresAt.After(now)
})
}
func TestPruneExpired(t *testing.T) {
now := time.Date(2026, 10, 9, 3, 0, 0, 0, time.UTC)
cache := map[string]Entry{
"fresh": {Value: "A", ExpiresAt: now.Add(time.Minute)},
"edge": {Value: "B", ExpiresAt: now},
"old": {Value: "C", ExpiresAt: now.Add(-time.Minute)},
}
pruneExpired(cache, now)
if len(cache) != 1 || cache["fresh"].Value != "A" {
t.Fatalf("unexpected cache after prune: %#v", cache)
}
}
func TestNilMapIsSafe(t *testing.T) {
var cache map[string]Entry
pruneExpired(cache, time.Now())
}
การรับ now จากภายนอกแทนการเรียก time.Now() ใน callback ทำให้ทุกสมาชิกถูกเทียบกับจุดเวลาเดียวกัน และ test ไม่ต้องแข่งกับนาฬิกาจริง ตัวอย่างถือว่าหมดอายุเมื่อ ExpiresAt <= now จึงใช้ !After(now) ซึ่งครอบคลุมค่าที่เท่ากันด้วย
ขอบเขตที่ต้องรู้
การลบจาก nil map ปลอดภัย แต่การเขียนค่าใหม่ลง nil map จะ panic ส่วน callback ควรเป็น pure เท่าที่ทำได้ อย่าทำ network call, เขียนฐานข้อมูล หรือแก้ map เดียวกันเพิ่ม เพราะลำดับการเดิน map ไม่รับประกัน และ side effect จะทำให้พฤติกรรมขึ้นกับลำดับที่มองไม่เห็น
DeleteFunc แก้ map เดิม จึงไม่มี snapshot ให้ rollback ถ้าต้อง audit รายการที่ถูกลบ ให้เก็บ key ลง slice ใน callbackอย่างระมัดระวัง หรือแยกเป็นสองช่วง: คำนวณรายการก่อน แล้วค่อยลบ หาก map ถูกแชร์ข้าม goroutine ฟังก์ชันนี้ไม่ได้เพิ่ม synchronization ให้ ต้องล็อกหรือเปลี่ยนเจ้าของข้อมูลก่อน ไม่เช่นนั้นเสี่ยง data race หรือ concurrent map panic
ถ้าต้องการผลลัพธ์ใหม่โดยเก็บ map ต้นฉบับไว้ ใช้ maps.Clone แล้วค่อย DeleteFunc บนสำเนา แต่ต้นทุนความจำจะเป็น O(n) และค่าภายในยังเป็น shallow copy ถ้า value เป็น pointer หรือ slice ข้อมูลด้านในยังอ้างชุดเดิม
Recap
- predicate คืน
trueเมื่อ ต้องลบ - ส่งเวลาและ policy เข้ามาเพื่อให้ test ทำซ้ำได้
- อย่าใส่ I/O หรือแก้ map ซ้ำใน callback
- ล็อกหรือโอน ownership ก่อนแตะ map ที่หลาย goroutine ใช้
- clone ก่อนเมื่อผู้เรียกต้องเก็บต้นฉบับหรือมีทาง rollback
เริ่มจาก cleanup loop ที่มีเงื่อนไขเดียวและ test ครบขอบ ExpiresAt == now ก่อนครับ จะได้โค้ดสั้นลงโดยไม่ซ่อนนโยบายสำคัญ