บทเรียน 20 · Day 6 · Generics

Generic Struct และเมื่อไหร่ที่ยังไม่ควรใช้

~14 นาที · ต้องมี: บทเรียน 19, บทเรียน 10 (interface)

ครึ่งแรกของบทนี้คือ "generic ใส่ใน struct ได้ด้วย" ซึ่งง่าย ครึ่งหลังยากกว่ามาก: เมื่อไหร่ที่ไม่ควรใช้ สไลด์ปิดหัวข้อนี้ด้วยประโยคเดียว — "อย่ารีบทำทุกอย่างเป็น Generic" — บทนี้จะบอกว่าเส้นแบ่งอยู่ตรงไหน และทำไม

1 · ปัญหา: struct เดียวกัน ต่างกันแค่ type ของ field

❌ ก่อนมี generics

type IntBox struct {
    Value int
}

type StringBox struct {
    Value string
}

type UserBox struct {
    Value User
}

// เพิ่ม type ใหม่ = เพิ่ม struct ใหม่ ตลอดไป

✅ Generic struct

type Box[T any] struct {
    Value T
}

numberBox := Box[int]{Value: 100}
nameBox   := Box[string]{Value: "Alice"}

fmt.Printf("%T\n", numberBox.Value)
// int
fmt.Printf("%T\n", nameBox.Value)
// string

⚠️ ตอนใช้งาน struct แบบ generic ต้องระบุ type ในวงเล็บเหลี่ยมเสมอ เขียน Box{} เฉย ๆ ไม่ได้ ต้องเขียน Box[int]{} — ถ้าลืมจะได้:

cannot use generic type Box[T any] without instantiation

(instantiation แปลว่า "การใส่ type จริงลงไปแทน T" ตราบใดที่ยังไม่ใส่ compiler ก็ยังไม่รู้ว่า field Value เป็น type อะไร จึงยังสร้างของจริงให้ไม่ได้)

🧰 เทียบให้เห็นภาพ — กล่องถนอมอาหารในตู้เย็น

ในตู้เย็นบ้านเรามีกล่องพลาสติกทรงเดียวกันหลายใบ ใบหนึ่งใส่แกง อีกใบใส่ข้าว ตัวกล่องเหมือนกันเป๊ะ ต่างกันแค่ของข้างใน
ถ้าโลกนี้ไม่มีกล่องแบบนี้ ก็ต้องไปหาซื้อ "กล่องใส่แกงโดยเฉพาะ" กับ "กล่องใส่ข้าวโดยเฉพาะ" แยกคนละใบ — ซึ่งก็คือ IntBox / StringBox / UserBox ในตัวอย่างข้างบนนั่นเอง

· Box[T any] = แบบของกล่อง ที่ยังไม่ได้บอกว่าใส่อะไร
· Box[int] = แบบกล่องที่ติดป้ายแล้วว่า "ใส่จำนวนเต็ม" — ยังเป็นแค่แบบ ยังไม่ใช่กล่องใบจริง
· Box[int]{} = กล่องใบจริงที่ทำออกมาตามแบบนั้น
นี่คือเหตุผลที่เขียน Box{} เฉย ๆ ไม่ได้ — สั่งว่า "ขอกล่อง" โดยไม่บอกว่าใส่อะไร Go ก็ยังทำกล่องให้ไม่ได้

ที่ไม่เหมือนกัน: กล่องจริงเราแกะป้ายออกแล้วเปลี่ยนไปใส่อย่างอื่นได้ หรือจะแอบยัดของผิดประเภทลงไปก็ยังทำได้ แต่กล่องของ Go ติดป้ายแล้ว เปลี่ยนป้ายไม่ได้ — ของข้างในเปลี่ยนเป็นเลขตัวอื่นได้เรื่อย ๆ (numberBox.Value = 200 ทำได้ปกติ) แต่จะเปลี่ยนจากใส่เลขไปใส่ข้อความไม่ได้ ต้องทำกล่องใบใหม่ · และถ้ายัดของผิดชนิดลงไปตั้งแต่แรก โปรแกรมจะไม่ยอมคอมไพล์ ไม่ใช่ปล่อยให้ไปเปิดเจอตอนหิว

🗣 ศัพท์ที่เพิ่งเรียนไป
generic type / generic struct
"แบบของกล่อง" ที่ยังไม่ระบุชนิดของข้างใน · เวลาถามให้พูดว่า "ตรงนี้ควรทำเป็น generic struct ไหม" จะสื่อกว่าถามว่า "ทำให้มันใส่อะไรก็ได้ยังไง"
type parameter / type argument
T ในวงเล็บเหลี่ยม = ช่องว่างที่รอให้กรอกชนิดข้อมูล เรียกว่า type parameter · ส่วน int ใน Box[int] คือ สิ่งที่กรอกลงช่อง เรียกว่า type argument · ประโยคที่ใช้ได้จริง: "struct ตัวนี้มี type parameter กี่ตัว" — ถามแบบนี้เวลาเจอของอย่าง Cache[K, V] ที่มีสองช่อง
instantiate / instantiation
การเติม type จริงลงไปแทน T จนได้ type ที่ระบุชนิดครบแล้ว เช่น Box[int] · ระวังว่าคนละความหมายกับ instantiate ใน Java/C# ที่แปลว่า "สร้าง object" — ของ Go ได้เป็น type ก่อน แล้วค่อยเอา type นั้นไปสร้างค่าอีกที (Box[int]{}) · ถ้าเจอ error without instantiation ให้พูดกับ senior หรือพิมพ์ถาม AI ว่า "ลืม instantiate generic type ตรงไหน" เขาจะรู้ทันทีว่าต้องไปดูวงเล็บเหลี่ยม ดีกว่าบอกว่า "มัน error แปลก ๆ"

2 · ตัวอย่างที่ใกล้งานจริงที่สุด: API response

ทุก endpoint คืนโครงเหมือนกัน ต่างแค่ก้อนข้อมูลข้างใน — นี่คือที่ที่ generic struct คุ้มค่าที่สุด:

type Result[T any] struct {
    Data    T    `json:"data"`
    Success bool `json:"success"`
}

userResult := Result[User]{
    Data:    User{Name: "Alice"},
    Success: true,
}

productResult := Result[Product]{
    Data:    Product{Name: "Mechanical Keyboard"},
    Success: true,
}

listResult := Result[[]User]{   // T เป็น slice ก็ได้
    Data:    users,
    Success: true,
}

⚠️ สไลด์หน้านี้คอมไพล์ไม่ผ่าน — สไลด์ประกาศ Result[T] ที่มี field Data กับ Success bool แต่ตอนใช้งานกลับใส่ Count: 29301 (ค้างมาจากสไลด์หน้าก่อนที่ struct ยังมี field Count)
ผมลองรันแล้วได้:

unknown field Count in struct literal of type Result[User]

ตัวอย่างข้างบนคือฉบับที่แก้แล้ว — ถ้าจะเก็บจำนวนด้วย ให้เพิ่ม Count int เข้าไปใน struct ก่อน

3 · Method บน generic struct

type Stack[T any] struct {
    items []T
}

// receiver ต้องเขียน [T] ต่อท้ายชื่อ type ด้วย
func (s *Stack[T]) Push(value T) {
    s.items = append(s.items, value)
}

func (s *Stack[T]) Pop() (T, bool) {
    var zero T                 // ← จุดสำคัญ
    if len(s.items) == 0 {
        return zero, false
    }
    lastIndex := len(s.items) - 1
    value := s.items[lastIndex]
    s.items = s.items[:lastIndex]
    return value, true
}

ทำไม receiver ต้องเป็น *Stack[T] ไม่ใช่ Stack[T]: (receiver คือตัวแปรที่เขียนไว้หน้าชื่อ method บอกว่า method นี้ติดอยู่กับ type ไหน) Push แก้ field items — ถ้าใช้ value receiver มันแก้สำเนา ของหายหมด เรื่องเดิมเป๊ะ ๆ จาก บทเรียน 07
generics ไม่ได้ยกเลิกกฎเดิมของภาษาสักข้อ

numbers := Stack[int]{}
numbers.Push(10)
numbers.Push(20)
numbers.Push(30)
fmt.Println(numbers.Pop())

empty := Stack[int]{}
v1, ok1 := empty.Pop()
fmt.Println(v1, ok1)

emptyStr := Stack[string]{}
v2, ok2 := emptyStr.Pop()
fmt.Printf("%q %v\n", v2, ok2)

var zero T ให้ค่าอะไรออกมา เมื่อ stack ว่าง?

30 true
0 false
"" false

var zero T ให้ zero value ของ type ที่ใส่เข้าไปตอนนั้น — Stack[int] ได้ 0 · Stack[string] ได้ "" · Stack[*User] จะได้ nil

นี่คือสำนวนที่ต้องจำ — ในฟังก์ชัน generic คุณเขียน return 0 หรือ return nil ตรง ๆ ไม่ได้ เพราะไม่รู้ว่า T จะเป็นอะไร var zero T คือทางเดียวที่ใช้ได้กับทุก type
และเพราะ zero value อาจเป็นค่าที่ถูกต้องจริง ๆ (เช่น 0) จึงต้องคืน bool คู่มาด้วยเสมอ — แบบเดียวกับ comma-ok ที่คุณเห็นมาตั้งแต่บทเรียน 11

🏧 เทียบให้เห็นภาพ — เลข 0 บนสลิป ATM

กดดูยอดเงินแล้วสลิปขึ้นว่า 0 บาท — แปลว่าอะไร?
อาจแปลว่า "บัญชีนี้เงินหมดจริง ๆ" ก็ได้ หรือ "รายการไม่สำเร็จ เลยยังไม่รู้ยอด" ก็ได้ ตัวเลขอย่างเดียวแยกสองอย่างนี้ไม่ออก ตู้จึงพิมพ์สถานะของรายการมาให้ด้วยเสมอ

Pop() เจอปัญหาเดียวกันเป๊ะ ๆ — stack ว่างแล้วคืน 0 ออกมา คนเรียกจะแยกไม่ออกว่านี่คือ "เลข 0 ที่ push เข้าไปเองจริง ๆ" หรือ "ของหมดแล้ว"
จึงต้องคืน bool พ่วงมาด้วย — มันคือสถานะบนสลิปนั่นเอง

ที่ไม่เหมือนกัน: สลิป ATM มีค่าว่างแบบเดียวคือเลข 0 แต่ var zero T เปลี่ยนหน้าตาไปตามชนิดที่ใส่เข้ามา — int ได้ 0 · string ได้ "" · bool ได้ false · pointer ได้ nil นี่คือเหตุผลที่เขียน return 0 ตรง ๆ ไม่ได้ เพราะยังไม่รู้ว่า T จะเป็นอะไร

🗣 ศัพท์ที่เพิ่งเรียนไป
zero value
"ค่าตั้งต้น" ที่ Go ใส่ให้เองเมื่อประกาศตัวแปรแล้วยังไม่ได้กำหนดค่า · แต่ละ type มีค่าไม่เหมือนกัน · ประโยคที่ใช้ได้จริง: "zero value ของ type นี้คืออะไร" — ถามแบบนี้จะได้คำตอบตรงกว่าถามว่า "มันว่างเป็นอะไร"
comma-ok
สำนวนของ Go ที่คืนค่าคู่กัน: ค่าที่ได้ กับ bool ที่บอกว่าค่านั้นใช้ได้จริงไหม (value, ok := ...) · ตัวที่สองต้องเป็น bool — ถ้าเป็น error จะไม่เรียกว่า comma-ok · ของแท้ที่ตัวภาษาให้มามี 3 ที่: หยิบจาก map (v, ok := m[k]), type assertion (v, ok := x.(T)) และรับจาก channel (v, ok := <-ch) · ส่วนฟังก์ชันที่เราเขียนเองก็เลียนแบบสำนวนนี้ได้ เช่น Pop() ในหัวข้อนี้ · บอก senior ว่า "ตรงนี้ทำเป็น comma-ok ดีไหม" เขาจะเข้าใจทันทีว่าคุณหมายถึงคืน bool พ่วงมาด้วย
receiver
ตัวแปรหน้าชื่อ method ที่บอกว่า method นี้ติดอยู่กับ type ไหน · ถ้าเป็น *Stack[T] เรียกว่า pointer receiver (แก้ของจริง) ถ้าเป็น Stack[T] เรียกว่า value receiver (แก้แค่สำเนา) · เวลาโค้ดแก้ค่าแล้วค่าไม่เปลี่ยน ให้ถามว่า "อันนี้เป็น value receiver อยู่หรือเปล่า"

⚠️ method เพิ่ม type parameter ของตัวเองไม่ได้
func (s *Stack[T]) MapTo[R any](f func(T) R) []R → method must have no type parameters
ถ้าต้องแปลง type ต้องเขียนเป็นฟังก์ชันธรรมดา: func MapStack[T, R any](s *Stack[T], f func(T) R) []R

4 · เมื่อไหร่ที่ ยังไม่ ควรใช้ generics

หัวข้อนี้คือสิ่งที่แยกคนเขียน Go เป็นจากคนเพิ่งอ่านเจอ generics เมื่อวาน สามกรณีที่สไลด์ยกมา ถูกทั้งหมด:

4.1 มี type เดียว ยังไม่มีความซ้ำ

✅ เขียนแบบนี้ไปก่อน

func CalculateInvoiceTotal(
    items []InvoiceItem,
) int64 {
    // business logic
}

❌ ยังไม่จำเป็นต้องรีบ

func CalculateTotal[T Billable](
    items []T,
) int64 {
    // business logic
}

ตอนนี้มี InvoiceItem อยู่ type เดียว การทำเป็น generic ไม่ได้ลดโค้ดสักบรรทัด แต่เพิ่มของให้คนอ่านต้องแกะ — Billable คืออะไร? T เป็นอะไรได้บ้าง?
วันไหนมี type ที่สองจริง ค่อยแปลง ตอนนั้นคุณจะรู้ด้วยว่า อะไรคือส่วนที่เหมือนกันจริง ๆ

4.2 ฟังก์ชันทำแค่เรียก method — ใช้ interface

✅ interface พอแล้ว

func Save(w io.Writer, data []byte) error {
    _, err := w.Write(data)
    return err
}

❌ generic ที่ไม่ได้อะไรเพิ่ม

func Save[W io.Writer](w W, data []byte) error {
    _, err := w.Write(data)
    return err
}
💡 เส้นแบ่งที่ใช้ตัดสินใจได้จริง

ฟังก์ชันสนใจแค่ "มันทำอะไรได้" → ใช้ interface
ฟังก์ชันต้องรู้ "มันเป็น type อะไร" เพื่อคืนค่า type เดียวกัน หรือเก็บลง container → ใช้ generics

Save แค่เรียก .Write() — ไม่สนว่าเป็นไฟล์หรือ buffer → interface
First[T any](values []T) T ต้องคืนของ type เดิม → interface ทำไม่ได้ ต้อง generics

(container = โครงที่เอาไว้เก็บของหลายชิ้น เช่น slice, map หรือ Stack ในหัวข้อ 3)

👕 เทียบให้เห็นภาพ — ร้านซักรีดหน้าปากซอย

ตอนรับผ้า พนักงานไม่สนว่าเป็นเสื้อ กางเกง หรือผ้าปูที่นอน ขอแค่ "ซักได้" ก็รับหมด — สนใจแค่ว่ามันทำอะไรได้
นี่คือ interface · เหมือน Save(w io.Writer, ...) ที่ขอแค่ "เขียนลงไปได้" ไม่สนว่าปลายทางเป็นไฟล์หรือหน้าจอ

แต่ตอนคืนของ ถ้าร้านยื่น "ผ้าชิ้นหนึ่ง" มาให้โดยไม่บอกว่าเป็นอะไร คุณต้องมานั่งแกะดูเองก่อนถึงจะใส่ได้
ร้านที่ดีจำได้ว่า "รับเสื้อมา ก็คืนเสื้อกลับ" — อันนี้คือ generics: รับ T เข้าไป คืน T ตัวเดิมออกมา ไม่ต้องแกะซ้ำ

ที่ไม่เหมือนกัน: ร้านซักรีดจำผิดได้ — ยื่นเสื้อคนอื่นมาให้ ก็ยังเกิดขึ้นได้ตอนไปรับของ · ฝั่ง Go ต่างตรงที่ generics ตัดปัญหานี้ทิ้งตั้งแต่ตอนคอมไพล์ — ใส่ T เป็นอะไร ก็ได้ T ตัวนั้นกลับมาแน่นอน หยิบผิดไม่ได้
ส่วนทาง interface ยังต้องแกะด้วย type assertion อยู่ดี และถ้าแกะผิดชนิดจะพังตอนรัน แบบที่ บทเรียน 11 เตือนไว้ — นี่แหละคือเหตุผลที่ generics ชนะ interface ในเคสที่ต้องคืน type เดิม

🗣 ศัพท์ที่เพิ่งเรียนไป
behavior (พฤติกรรม)
"ของชิ้นนี้ทำอะไรได้บ้าง" — คือสิ่งที่ interface สนใจ · ประโยคที่ใช้ได้จริงตอนออกแบบ: "ฟังก์ชันนี้สนใจแค่พฤติกรรม หรือต้องรู้ตัว type จริง ๆ" ถามตัวเองประโยคเดียวนี้ก็ตัดสินใจได้แล้วว่าจะใช้ interface หรือ generics
type assertion
การ "แกะ" ค่าที่อยู่ในรูป interface ให้กลับมาเป็น type จริง · ถ้าโค้ดเต็มไปด้วยการแกะแบบนี้ ให้ถาม senior ว่า "ตรงนี้ใช้ generics แทน type assertion ได้ไหม"
constraint
เงื่อนไขที่กำกับว่า T เป็นอะไรได้บ้าง เช่น any, comparable หรือ interface ที่เราเขียนขึ้นเอง · พูดว่า "constraint ของ T คืออะไร" เวลาอ่านโค้ด generic ของคนอื่นแล้วงง
premature abstraction
"รีบทำให้ยืดหยุ่นทั้งที่ยังไม่มีใครต้องการ" — คือสิ่งที่หัวข้อ 4.1 เตือน · เวลารีวิวโค้ดกัน ประโยคที่ใช้ได้คือ "อันนี้ premature abstraction หรือเปล่า ตอนนี้มี type เดียวเอง" ฟังเป็นข้อเสนอมากกว่าการบอกว่า "เขียนซับซ้อนเกินไป"

4.3 generic แล้ว caller อ่านยากกว่าเดิม

func Execute[
    T any,
    R comparable,
    S ~[]T,
    M ~map[R]S,
](
    // ...
) {
    // Business Logic
}

ตอนเขียนรู้สึกฉลาดมาก ตอนคนอื่นมาอ่าน (หรือตัวคุณเองอีกสามเดือน) ต้องนั่งไล่ว่า M คือ map ของอะไรไปหาอะไร แค่เครื่องหมาย ~ ก็ต้องไปเปิดหาก่อนแล้วว่าแปลว่าอะไร (~[]T = "type อะไรก็ได้ที่ underlying type — เนื้อในจริง ๆ ตามบทเรียน 09 — เป็น []T" รวมถึง type ที่ใครสักคนตั้งชื่อขึ้นมาเองจาก slice) ถ้าคนเรียกต้องนั่งแกะ type parameter ก่อนจะเรียกได้ — แสดงว่าเกินพอดีแล้ว

4.4 Map/Filter ไม่ได้ชนะ for loop เสมอ

Map + Filter แบบ generic

activeNames := Map(
    Filter(users, func(u User) bool {
        return u.Active
    }),
    func(u User) string {
        return u.Name
    },
)

for loop ธรรมดา

activeNames := make([]string, 0)

for _, user := range users {
    if user.Active {
        activeNames = append(
            activeNames, user.Name,
        )
    }
}

ถ้ามาจาก JavaScript หรือ Python ฝั่งซ้ายจะดูคุ้นและ "สะอาด" กว่า แต่ในวัฒนธรรม Go ฝั่งขวาถือว่าอ่านง่ายกว่า — อ่านจากบนลงล่างได้ตรง ๆ ไม่ต้องกลับหัวอ่านจากในออกนอก และ debug ง่ายกว่ามาก (ใส่ fmt.Println ตรงไหนก็ได้)
ไม่ใช่ว่าฝั่งซ้ายผิด — แต่อย่าเปลี่ยนเพราะคิดว่าอีกฝั่งดูโบราณ

5 · Interface หรือ Generics — ตารางตัดสิน

สถานการณ์เลือกเพราะ
รับอะไรก็ได้ที่ Write() ได้interfaceสนใจแค่พฤติกรรม
เก็บของหลาย type ในโครงเดียว (Stack, Cache)genericsต้องจำ type ไว้ตอนดึงออก
คืนค่า type เดียวกับที่รับเข้ามาgenericsinterface คืนได้แค่ interface
เก็บของต่าง type ปนกันใน slice เดียวinterface[]Shape ใส่ทั้ง Circle และ Square ได้ · []T ใส่ได้ type เดียว
ต้องใช้ + < == กับค่าที่รับมาgenericsinterface ระบุ method ได้ แต่ระบุ operator ไม่ได้
ยังมี type เดียวไม่ต้องทั้งคู่เขียนตรง ๆ ไปก่อน

ข้อที่คนมองข้ามคือแถวที่ 4 — Stack[Shape] ก็ยังใส่ของต่างชนิดได้อยู่ ถ้า Shape เป็น interface สองอย่างนี้ใช้ร่วมกันได้ ไม่ได้แข่งกัน
constraint ในบทเรียน 19 ก็คือ interface นั่นเอง — func FindByID[T Identifiable](values []T, id string) (T, bool) ใช้ทั้งสองอย่างพร้อมกัน: interface กำหนดว่า T ต้องมี ID() · generics ทำให้คืนค่าเป็น type เดิมได้

ทดสอบความเข้าใจ

1. ในฟังก์ชัน generic จะคืน "ค่าว่าง" ของ T ต้องเขียนยังไง?

nil ใช้ไม่ได้เพราะ T อาจเป็น int · T{} ใช้ไม่ได้เพราะ T อาจไม่ใช่ struct
var zero T ได้ zero value ที่ถูกต้องของ type นั้น ๆ เสมอ — 0 · "" · nil · false แล้วแต่ว่าใส่อะไรเข้าไป

2. func Save(w io.Writer, data []byte) error ควรเปลี่ยนเป็น [W io.Writer] ไหม?

ฟังก์ชันสนใจแค่ว่า "เขียนได้ไหม" ไม่ได้ต้องคืน type เดิมหรือเก็บลง container generic version ไม่ได้อะไรเพิ่มเลย แต่เพิ่มของให้อ่าน
เส้นแบ่ง: สนใจพฤติกรรม → interface · ต้องรู้ตัว type → generics
(io.Writer ใช้เป็น constraint ได้จริง — ตัวเลือกสุดท้ายผิดข้อเท็จจริง)

3. อยากได้ slice ที่เก็บทั้ง Circle และ Square ปนกัน ควรใช้อะไร?

[]T คือ slice ของ type เดียว — []Circle หรือ []Square อย่างใดอย่างหนึ่ง ไม่ใช่ปนกัน
ของปนกันต้องใช้ interface: []Shape เก็บอะไรก็ได้ที่ implement Shape
([]any ทำได้ทางเทคนิค แต่ทิ้ง type ทิ้งความปลอดภัยไปหมด)

4. ทำไม func (s *Stack[T]) MapTo[R any](...) ถึงคอมไพล์ไม่ผ่าน?

method must have no type parameters — method ใช้ได้เฉพาะ T ที่ประกาศไว้ที่ตัว struct เท่านั้น
ถ้าต้องแปลงไป type ใหม่ ให้เขียนเป็นฟังก์ชันธรรมดาแทน: func MapStack[T, R any](s *Stack[T], f func(T) R) []R

🛠️ ภารกิจ — Cache generic ที่ใช้ได้จริง

ทำในเครื่อง (จะได้ต่อยอดในบทเรียน 24 ตอนเติม mutex) เขียนเทสด้วย — คุณทำเป็นแล้วตั้งแต่ Day 5

  1. สร้าง type Cache[K comparable, V any] struct ที่ข้างในมี data map[K]V
    ทำไม K ต้องเป็น comparable? ตอบในใจก่อนเขียน
  2. เขียน constructor NewCache[K comparable, V any]() *Cache[K, V] ที่ make map ให้เรียบร้อย
    ลืม make แล้วจะเจออะไร — ลองดูก็ได้
  3. เขียน method Set(key K, value V) และ Get(key K) (V, bool)
    ตอน Get ไม่เจอ ต้องคืน var zero V
  4. เขียน Len() int และ Delete(key K)
  5. เขียนเทส table-driven (บทเรียน 16) ที่ทดสอบ Cache[string, int] และ Cache[int, User] ในไฟล์เทสเดียวกัน
  6. ท้าทาย 1: ทำให้เกิด cannot use generic type Cache[K comparable, V any] without instantiation โดยตั้งใจ แล้วแก้ให้ถูก
  7. ท้าทาย 2 — คาไว้ก่อน: Cache ของคุณ จะมีปัญหาเมื่อมีคนหลายคนเรียก Set พร้อมกัน (Go เรียกการทำหลายอย่างพร้อมกันนี้ว่า goroutine ซึ่งบทเรียน 21 จะสอน)
    ยังไม่ต้องตอบตอนนี้ — ให้เขียนคอมเมนต์คาไว้เหนือ struct ว่า // TODO: ยังไม่ปลอดภัยเมื่อถูกเรียกจากหลาย goroutine พร้อมกัน แล้วกลับมาเติมคำตอบเมื่ออ่านบทเรียน 24 จบ
  8. ท้าทาย 3: ลองเพิ่ม func (c *Cache[K, V]) Keys() []K — แล้วเขียนเทสที่เทียบผลลัพธ์กับ []string{"a","b"} ตรง ๆ
    เทสจะแดงเป็นบางครั้ง — ทำไม? (ใบ้: บทเรียน 19 หัวข้อ 4)

💬 ท้าทาย 3 คือข้อที่ผมอยากเห็นผลที่สุด — เทสที่แดงเป็นบางครั้ง (flaky test) คือฝันร้ายของทุกทีม ถ้าจับสาเหตุได้เองแปลว่าบทเรียน 19 เข้าเป้าแล้ว
บทถัดไปเปลี่ยนโลก: จากโปรแกรมที่ทำทีละอย่าง เป็นโปรแกรมที่ทำหลายอย่างพร้อมกัน

อ้างอิง