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]{}) · ถ้าเจอ errorwithout 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 บาท — แปลว่าอะไร?
อาจแปลว่า "บัญชีนี้เงินหมดจริง ๆ" ก็ได้ หรือ "รายการไม่สำเร็จ เลยยังไม่รู้ยอด" ก็ได้
ตัวเลขอย่างเดียวแยกสองอย่างนี้ไม่ออก
ตู้จึงพิมพ์สถานะของรายการมาให้ด้วยเสมอ
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 เดียวกับที่รับเข้ามา | generics | interface คืนได้แค่ interface |
| เก็บของต่าง type ปนกันใน slice เดียว | interface | []Shape ใส่ทั้ง Circle และ Square ได้ · []T ใส่ได้ type เดียว |
ต้องใช้ + < == กับค่าที่รับมา | generics | interface ระบุ 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
ทำในเครื่อง (จะได้ต่อยอดในบทเรียน 24 ตอนเติม mutex) เขียนเทสด้วย — คุณทำเป็นแล้วตั้งแต่ Day 5
- สร้าง
type Cache[K comparable, V any] structที่ข้างในมีdata map[K]V
ทำไมKต้องเป็นcomparable? ตอบในใจก่อนเขียน - เขียน constructor
NewCache[K comparable, V any]() *Cache[K, V]ที่makemap ให้เรียบร้อย
ลืม make แล้วจะเจออะไร — ลองดูก็ได้ - เขียน method
Set(key K, value V)และGet(key K) (V, bool)
ตอน Get ไม่เจอ ต้องคืนvar zero V - เขียน
Len() intและDelete(key K) - เขียนเทส table-driven (บทเรียน 16) ที่ทดสอบ
Cache[string, int]และCache[int, User]ในไฟล์เทสเดียวกัน - ท้าทาย 1: ทำให้เกิด
cannot use generic type Cache[K comparable, V any] without instantiationโดยตั้งใจ แล้วแก้ให้ถูก - ท้าทาย 2 — คาไว้ก่อน:
Cacheของคุณ จะมีปัญหาเมื่อมีคนหลายคนเรียกSetพร้อมกัน (Go เรียกการทำหลายอย่างพร้อมกันนี้ว่า goroutine ซึ่งบทเรียน 21 จะสอน)
ยังไม่ต้องตอบตอนนี้ — ให้เขียนคอมเมนต์คาไว้เหนือ struct ว่า// TODO: ยังไม่ปลอดภัยเมื่อถูกเรียกจากหลาย goroutine พร้อมกันแล้วกลับมาเติมคำตอบเมื่ออ่านบทเรียน 24 จบ - ท้าทาย 3: ลองเพิ่ม
func (c *Cache[K, V]) Keys() []K— แล้วเขียนเทสที่เทียบผลลัพธ์กับ[]string{"a","b"}ตรง ๆ
เทสจะแดงเป็นบางครั้ง — ทำไม? (ใบ้: บทเรียน 19 หัวข้อ 4)
💬 ท้าทาย 3 คือข้อที่ผมอยากเห็นผลที่สุด —
เทสที่แดงเป็นบางครั้ง (flaky test) คือฝันร้ายของทุกทีม
ถ้าจับสาเหตุได้เองแปลว่าบทเรียน 19 เข้าเป้าแล้ว
บทถัดไปเปลี่ยนโลก: จากโปรแกรมที่ทำทีละอย่าง เป็นโปรแกรมที่ทำหลายอย่างพร้อมกัน
อ้างอิง
- When To Use Generics — Ian Lance Taylor · แหล่งหลักของครึ่งหลังบทนี้ เขียนโดยคนออกแบบ generics เอง
- An Introduction To Generics — Go Blog
- Go Spec — Instantiations
- Go Code Review Comments — Interfaces