Generics
ทำไมต้องใช้ Generics?
หัวข้อที่มีชื่อว่า “ทำไมต้องใช้ Generics?”ก่อน Go 1.18 การเขียน function ที่ทำงานกับหลาย type ต้องการ code ซ้ำหรือใช้ interface{} + type assertion ที่ runtime ซึ่งสูญเสีย type safety Generics แก้ปัญหานี้: เขียนครั้งเดียว compiler สร้าง code เฉพาะ type และตรวจสอบการใช้งานที่ compile time
ตัวอย่างเช่น function Max ที่ทำงานได้ทั้ง int และ float64 ก่อนหน้านี้ต้องเขียนสอง function ที่เหมือนกัน หรือใช้ interface{} parameter ที่ยอมรับ type ผิดอย่างเงียบ ๆ ด้วย generics function เดียวครอบคลุมทั้งสอง — และ compiler ปฏิเสธทุก call ที่ใช้ type ที่ไม่รองรับ
Type Parameters
หัวข้อที่มีชื่อว่า “Type Parameters”syntax ของ generic function คือ func FuncName[T Constraint](args) returnType โดย [T Constraint] เรียกว่า type parameter list T เป็น placeholder ที่ compiler แทนด้วย concrete type ที่แต่ละ call site
func Max[T Number](a, b T) T { if a > b { return a } return b}ที่ call site Go มักจะ อนุมาน T จาก argument types ได้ เขียน Max(3, 7) แล้ว compiler อนุมานว่า T เป็น int — ไม่ต้องเขียน Max[int](3, 7) อย่างชัดเจน ต้องระบุ type argument อย่างชัดเจนเฉพาะเมื่อการอนุมานคลุมเครือหรือเมื่อ call โดยไม่มี argument
Constraints
หัวข้อที่มีชื่อว่า “Constraints”constraint คือ interface ที่จำกัดว่า type ใดสามารถแทน type parameter ได้ constraints จะปรากฏหลังชื่อ type parameter ใน list [T Constraint]
any— constraint ที่กว้างที่สุด; เทียบเท่ากับinterface{}อนุญาต type ใดก็ได้ แต่จำกัด operations ที่ทำได้กับค่าของ typeT(เฉพาะ operations ที่ valid กับทุก type: การ assign, การส่งไปยัง function เป็นต้น)comparable— type ใดก็ตามที่รองรับ==และ!=ใช้เมื่อ generic function ต้องเปรียบเทียบค่าเพื่อความเท่าเทียม
เขียน custom constraints โดยใช้ interface union syntax ได้:
type Number interface { ~int | ~float64}prefix ~ หมายถึง “type ใดก็ตามที่มี underlying type เป็น int หรือ float64” นี่คือ tilde operator: หากไม่มี ~ เฉพาะ named type นั้นเท่านั้นที่ตรงกัน; เมื่อมี ~ custom types เช่น type Celsius float64 ก็ satisfy ~float64 ได้เช่นกัน — นี่คือ defined (named) type ทำให้ constraints ทำงานได้ตามธรรมชาติกับ type แบบนี้ ส่วน true alias อย่าง type C = float64 เป็น type เดียวกับ float64 อยู่แล้ว จึงไม่ต้องใช้ ~ เลย
Constraint interfaces ไม่ใช่ type ธรรมดา
หัวข้อที่มีชื่อว่า “Constraint interfaces ไม่ใช่ type ธรรมดา”constraint interface ที่มี type term คั่นด้วย | (หรือมี ~) ใช้ได้แค่ข้างใน list [T Constraint] เท่านั้น — ห้ามใช้เป็น type ของตัวแปรธรรมดา, field, หรือ parameter เลย แม้จะเขียนด้วย syntax interface { ... } ก็ตาม:
type Number interface { ~int | ~float64}
var x Number // BUG: constraint interfaces aren't usable this wayตรวจสอบแล้วบน 1.26.5 นี่คือ compile error:
cannot use type Number outside a type constraint: interface contains type constraintsinterface ที่ไม่มี type term เลย (มีแค่ method อย่าง error หรือ io.Reader) ใช้เป็น type ธรรมดาได้เสมอ — ข้อจำกัดนี้ใช้เฉพาะกับ interface ที่มี type term คั่นด้วย | เพราะมันมีไว้เพื่ออธิบาย constraint ตอน compile time เท่านั้น ไม่ใช่ interface value ตอน runtime
comparable มีข้อยกเว้นตอน runtime
หัวข้อที่มีชื่อว่า “comparable มีข้อยกเว้นตอน runtime”comparable ดูเหมือนจะรับประกันว่า == ไม่มีวันล้มเหลว แต่ไม่ใช่ทั้งหมด ตั้งแต่ Go 1.20 any (และ interface type ใด ๆ) satisfy comparable ได้ เพราะ static type ของ interface value รองรับ == ทาง syntax เสมอ ส่วนการเปรียบเทียบจะสำเร็จจริงหรือไม่ขึ้นอยู่กับ dynamic value ที่เก็บอยู่ข้างในตอน runtime:
func Equal[T comparable](a, b T) bool { return a == b}
var a, b any = []int{1, 2}, []int{1, 2}Equal(a, b) // compiles fine -- any satisfies comparableตรวจสอบแล้วบน 1.26.5 การเรียกนี้ panic เพราะ slice ไม่ comparable แม้ interface ที่ถือมันจะ comparable ก็ตาม:
panic: runtime error: comparing uncomparable type []inttype ที่ comparable ตามปกติ (int, string, struct ที่มี field เป็น comparable ทั้งหมด) ไม่มีวันเจอปัญหานี้ — ความเสี่ยงจะปรากฏเฉพาะเมื่อ T ถูก instantiate ด้วย any หรือ interface type อื่น และ caller ควบคุมไม่ได้ว่าข้างในจะมีอะไร
Built-in constraints จาก cmp
หัวข้อที่มีชื่อว่า “Built-in constraints จาก cmp”Go 1.21 นำเสนอ package cmp ซึ่ง export cmp.Ordered — constraint แบบ idiomatic สำหรับ type ที่รองรับ <, <=, >, >=, == ครอบคลุม integer, float, และ string ทุกชนิด การใช้ cmp.Ordered สะอาดกว่าการเขียน union เต็มรูปแบบ ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ... | ~float64 | ~string ด้วยตัวเอง
import "cmp"
func Min[T cmp.Ordered](a, b T) T { if a < b { return a } return b}playground snippet ด้านล่างใช้ local Number interface แทน cmp.Ordered เพื่อให้เห็น constraint อยู่ในไฟล์เดียวกัน
ก่อนที่ cmp.Ordered จะเข้ามาอยู่ใน standard library (Go 1.21) package ของ community อย่าง golang.org/x/exp/constraints ทำหน้าที่นี้อยู่ก่อน — constraints.Ordered มีรูปแบบเดียวกันและมีมาก่อน stdlib version โค้ดใหม่ควรใช้ cmp.Ordered; constraints.Ordered ยังพบได้ในโค้ดที่เขียนสำหรับ Go 1.18–1.20 หรือ package ที่ยังไม่ migrate และทั้งสองใช้แทนกันได้เมื่อคุณควบคุมทั้งสองฝั่ง
Generic Types
หัวข้อที่มีชื่อว่า “Generic Types”struct ก็สามารถ parameterize ได้เช่นกันเหมือนกับ function ประกาศ type parameter หลังชื่อ type:
type Stack[T any] struct { items []T}
func (s *Stack[T]) Push(v T) { s.items = append(s.items, v)}
func (s *Stack[T]) Pop() (T, bool) { if len(s.items) == 0 { var zero T return zero, false } last := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return last, true}methods บน generic type ใช้ type parameter ซ้ำใน receiver — (s *Stack[T]) — แต่ไม่เพิ่ม constraint ใหม่ constraint ถูกกำหนดไว้แล้วครั้งเดียวตอนประกาศ type
Pop ด้านบนใช้ var zero T เพื่อสร้าง zero value ของ T ไม่ว่ามันจะเป็นอะไร — 0 สำหรับตัวเลข, "" สำหรับ string, nil สำหรับ slice และ pointer *new(T) ทำสิ่งเดียวกัน: new(T) allocate T ที่เป็น zero แล้วคืน *T และการ dereference มันก็ได้ค่านั้นมา ทั้งสองแบบเทียบเท่ากันและพบได้ในโค้ดจริง var zero T เป็น idiom ที่พบบ่อยกว่าเพราะอ่านตรง ๆ ว่า “zero value ของ T” โดยไม่ต้องผ่านขั้นตอน allocate-แล้ว-dereference เพิ่ม
Instantiation ต้องชัดเจน; inference ไม่ครอบคลุมทุกที่
หัวข้อที่มีชื่อว่า “Instantiation ต้องชัดเจน; inference ไม่ครอบคลุมทุกที่”type แบบ generic ไม่เคย infer type argument ให้เลย — ทุกการใช้งานต้อง instantiate อย่างชัดเจน รวมถึงการประกาศตัวแปรเปล่า ๆ ด้วย:
type Stack[T any] struct{ items []T }
var s Stack // BUG: missing a type argumentตรวจสอบแล้วบน 1.26.5:
cannot use generic type Stack[T any] without instantiationทางแก้คือ var s Stack[int] (หรือ element type ที่ต้องการ) นี่ต่างจาก function แบบ generic ซึ่งมักไม่ต้องระบุ type argument อย่างชัดเจนเพราะ Go อนุมาน T จาก arguments ที่ call site ได้ — แต่การอนุมานนั้นดูแค่ arguments เท่านั้น ไม่เคยดูจาก return type ที่คาดหวัง:
func Zero[T any]() T { var zero T return zero}
var x int = Zero() // BUG: nothing to infer T fromตรวจสอบแล้วบน 1.26.5:
in call to Zero, cannot infer T (declared at ./main.go:5:11)แม้ผลลัพธ์จะถูก assign ให้ int Go ก็ไม่ทำ return-type inference ให้ — ต้องเขียน Zero[int]()
Generic type alias (Go 1.24)
หัวข้อที่มีชื่อว่า “Generic type alias (Go 1.24)”ตั้งแต่ Go 1.24 type alias รับ type parameter ได้ เหมาะกับการย่อ instantiation ยาว ๆ โดยไม่ต้องสร้าง named type ใหม่:
type Set[T comparable] = map[T]struct{}
var seen Set[string] = map[string]struct{}{} // same type, just a shorter nameSelf-referential constraint (Go 1.26)
หัวข้อที่มีชื่อว่า “Self-referential constraint (Go 1.26)”generic type อ้างถึงตัวเองใน type parameter list ของตัวเองได้ ใช้แสดงความหมายว่า “type ที่รวมกับค่าของ type เดียวกันได้” — รูปแบบ Adder แบบคลาสสิก:
package main
import "fmt"
type Adder[A Adder[A]] interface { Add(A) A}
func Sum[A Adder[A]](xs ...A) A { var acc A for _, x := range xs { acc = acc.Add(x) } return acc}
type Money int
func (m Money) Add(o Money) Money { return m + o }
func main() { fmt.Println(Sum(Money(5), Money(10), Money(20)))} Generic method (Go 1.27)
หัวข้อที่มีชื่อว่า “Generic method (Go 1.27)”ก่อน Go 1.27 มีแค่ function และ type เท่านั้นที่ประกาศ type parameter ได้ method ใช้ได้เฉพาะ parameter ของ receiver Go 1.27 ยกข้อจำกัดนี้: method ประกาศ type parameter ของตัวเองได้ standard library ใช้มันใน math/rand/v2 ที่ (*Rand).N ตอนนี้ทำงานเหมือน rand.N ระดับบนสุด:
// math/rand/v2, Go 1.27func (r *Rand) N[Int intType](n Int) Int
r := rand.New(rand.NewPCG(1, 2))d := r.N(10 * time.Second) // Int inferred as time.Durationi := r.N[int64](100) // or explicittype ของคุณเองก็ทำได้เหมือนกัน:
package main
import ( "fmt" "math/rand/v2" "time")
type Registry struct{ items map[string]any }
// Get declares its own type parameter T — allowed since Go 1.27.func (r *Registry) Get[T any](key string) (T, bool) { v, ok := r.items[key].(T) return v, ok}
func main() { reg := &Registry{items: map[string]any{"port": 8080, "host": "localhost"}}
port, ok := reg.Get[int]("port") fmt.Println(port, ok)
host, ok := reg.Get[string]("host") fmt.Println(host, ok)
_, ok = reg.Get[int]("host") // wrong type: zero value, false fmt.Println(ok)
r := rand.New(rand.NewPCG(1, 2)) d := r.N(10 * time.Second) // Int inferred as time.Duration i := r.N[int64](100) // or explicit fmt.Println(d < 10*time.Second, i < 100)} สองกฎที่ต้องจำ: method ใน interface ประกาศ type parameter ไม่ได้ และ generic method จึงใช้ satisfy interface ไม่ได้ generic method ต้องใช้ Go 1.27+ (go 1.27 ใน go.mod)
การอนุมาน function type แบบทั่วไปขึ้น (Go 1.27)
หัวข้อที่มีชื่อว่า “การอนุมาน function type แบบทั่วไปขึ้น (Go 1.27)”เดิมการอนุมาน type argument ของ generic function จำกัดอยู่แค่ตอน call เท่านั้น ตอนนี้มีอีกสองบริบทที่อนุมานได้เช่นกัน แต่มาจากคนละ release:
- assign ให้ตัวแปรที่เป็น function type ตรงกัน — ทำงานได้ตั้งแต่ Go 1.21 แล้ว
- convert แบบชัดเจนเป็น function type ที่ตรงกัน — นี่คือส่วนที่เพิ่มใน Go 1.27
func Max[T cmp.Ordered](a, b T) T { /* ... */ }
var maxInt func(int, int) int = Max // T inferred as int (Go 1.21+)sortBy := (func(string, string) string)(Max) // T inferred as string (Go 1.27)รูปแบบ assignment compile ผ่านบน toolchain Go 1.26.5 ของคอร์สนี้ (เมื่อ go.mod ระบุ go 1.21 ขึ้นไป) แต่รูปแบบ conversion ไม่ผ่าน: บน 1.26.5 compiler ฟ้อง cannot use generic function Max without instantiation ยืนยันแล้วว่าทำงานได้จริงบน Go 1.27 ผ่าน go.dev playground — โค้ดด้านบนรันได้เฉพาะที่นั่น
package main
import "fmt"
type Number interface { ~int | ~float64}
func Max[T Number](a, b T) T { if a > b { return a } return b}
func Map[T, U any](slice []T, f func(T) U) []U { result := make([]U, len(slice)) for i, v := range slice { result[i] = f(v) } return result}
func Filter[T any](slice []T, pred func(T) bool) []T { var result []T for _, v := range slice { if pred(v) { result = append(result, v) } } return result}
func main() { fmt.Println(Max(3, 7)) fmt.Println(Max[float64](3.14, 2.71))
nums := []int{1, 2, 3, 4, 5} doubled := Map(nums, func(n int) int { return n * 2 }) fmt.Println(doubled)
words := []string{"go", "generics", "fun", "code"} long := Filter(words, func(s string) bool { return len(s) > 3 }) fmt.Println(long)} เมื่อไหร่ไม่ควรใช้ generics
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้ generics”generics แก้ปัญหาเฉพาะเจาะจง: algorithm เดียวกัน เขียนครั้งเดียว ใช้กับหลาย concrete type โดยตรวจสอบตอน compile time มันเป็นเครื่องมือที่ผิดเมื่อ:
- มี concrete type เดียวที่ต้องการใช้จริง ๆ
Stackที่ใช้แค่กับintใน package เดียวไม่จำเป็นต้องมี type parameter — ค่อยเพิ่มทีหลังถ้ามี use case ที่สองเกิดขึ้นจริง - สิ่งที่ต้องการจริง ๆ คือ runtime polymorphism ถ้า caller ต้องเลือกพฤติกรรมต่างกันตอน runtime (
Reader,Handler, plugin) นั่นคือ interface ไม่ใช่ type parameter — generics ถูก resolve ตอน compile time จึงไม่สามารถแสดงออกว่า “เลือก implementation ไหนตอน runtime” แบบที่ interface value ทำได้ - body ของ function แทบไม่ได้ใช้
Tเลย function ที่ส่งผ่านTโดยไม่แตะต้องมันเลย (ไม่มี operation เฉพาะT) บางครั้งอ่านง่ายกว่าถ้าใช้anyกับ type assertion แค่จุดเดียวที่จำเป็น แทนที่จะร้อย type parameter ผ่านทั้ง call chain เพื่อ use case แคบ ๆ อันเดียว - constraint ต้องใหญ่โตหรือขัดแย้งกันเองมาก แค่เพื่อทำให้ function เดียวเป็น generic — ปกติแล้วเป็นสัญญาณว่ามี algorithm ที่ต่างกันจริง ๆ สองตัวถูกยัดเข้ารูปแบบเดียว
flowchart LR
subgraph Distinct["different GC shapes -- separate compiled code"]
IntCall["Max[int](3, 7)"] --> IntCode["compiled Max for int"]
F64Call["Max[float64](3, 7)"] --> F64Code["compiled Max for float64"]
end
subgraph Shared["pointer-shaped types -- one shared stencil"]
PtrACall["Max[*A](...)"] --> SharedCode["compiled Max for pointer shape"]
PtrBCall["Max[*B](...)"] --> SharedCode
SharedCode --> Dict["runtime dictionary picks *A vs *B behavior"]
end อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เมื่อโค้ดที่ AI generate ใช้ type parameter ให้มองหาสิ่งเหล่านี้ก่อนเชื่อมัน:
- constraint interface (ที่มี type term คั่นด้วย
|หรือ~) ถูกใช้เป็น type ของตัวแปร, field, หรือ parameter แทนที่จะใช้แค่ข้างใน[T Constraint]หรือเปล่า? นั่นคือ compile error ไม่ใช่ runtime surprise - generic type ถูกใช้โดยไม่มี type argument (
var s Stackแทนที่จะเป็นvar s Stack[int]) หรือเปล่า? type ไม่เคย infer ให้เลย — มีแค่ function call เท่านั้นที่ infer ได้ และ infer จาก argument เท่านั้น - โค้ดสมมติว่า
==จากEqual[T comparable]หรือคล้ายกัน panic ไม่ได้เลยหรือเปล่า? ถ้าTเป็นanyหรือ interface type อื่นได้ ค่าที่ไม่ comparable ข้างในจะ panic ตอน runtime - มี generic function ที่ concrete type เดียว หรือ interface ธรรมดา จะสื่อความหมายเดียวกันได้โดยใช้กลไกน้อยกว่าหรือเปล่า?
var zero T(หรือ*new(T)) ถูกใช้สร้างกรณี “ไม่มีผลลัพธ์” แต่ caller กลับปฏิบัติกับ zero value เหมือนมันมีความหมายต่างจาก “ไม่มี” หรือเปล่า — เช่น0หมายถึงทั้ง “ไม่มีค่า” และ “ค่าคือศูนย์” พร้อมกัน?
🐛 หา bug ในโค้ดนี้
func Equal[T comparable](a, b T) bool { return a == b}
func main() { var a, b any = []int{1, 2}, []int{1, 2} fmt.Println(Equal(a, b))}เฉลย
any satisfy comparable ได้ตั้งแต่ Go 1.20 — การตรวจ constraint ผ่านตอน compile time แต่ == ระหว่างค่า any สองตัวเทียบ concrete value ที่เก็บอยู่ข้างในตอน runtime และ []int ไม่ comparable ตรวจสอบแล้วบน 1.26.5: โค้ดนี้ panic ด้วย panic: runtime error: comparing uncomparable type []int comparable รับประกันแค่ว่าโค้ด compile ผ่าน ไม่ได้รับประกันว่าทุก instantiation รันได้ปลอดภัย — ปัญหานี้จะเกิดเฉพาะเมื่อ T ลงเอยเป็น any หรือ interface type อื่นที่ถือ slice, map, หรือ func
package main
import "fmt"
// Zero returns the zero value of T -- var zero T is the idiomatic form.func Zero[T any]() T { var zero T return zero}
// ZeroAlt is equivalent using *new(T): new(T) allocates a T and zeroes it,// then *new(T) dereferences to get the value. var zero T is more common.func ZeroAlt[T any]() T { return *new(T)}
// Equal works for any comparable type -- including plain values, where == never panics.func Equal[T comparable](a, b T) bool { return a == b}
func main() { fmt.Println(Zero[int](), Zero[string](), Zero[[]int]() == nil) fmt.Println(ZeroAlt[int](), ZeroAlt[string]() == "")
fmt.Println(Equal(3, 3)) fmt.Println(Equal("go", "rust"))
// any satisfies comparable since Go 1.20 -- fine as long as the // dynamic value inside it is itself comparable. var a, b any = 3, 3 fmt.Println(Equal(a, b))} ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| type parameters | code reuse โดยไม่ต้องใช้ interface{} | compile time เพิ่มขึ้น, syntax ซับซ้อน |
| type constraints | compile-time type safety | ต้อง define constraint interface ก่อนใช้ |
| comparable constraint | ใช้ == กับ generic type ได้ | ไม่รองรับ deep equality (slice, map) |
| ~T (tilde/approximation) | constraint ครอบคลุม named types เช่น type MyID int | เข้าใจยากสำหรับผู้เริ่มต้น |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- generics ทำให้ binary ใหญ่ขึ้นมาก — Go ใช้ GC-shape stenciling: type ที่มี pointer/interface shape เดียวกันแชร์ instantiation เดียวกันและใช้ dictionary ส่ง type info ตอน runtime ไม่ขยายแบบ C++ templates
- generic function เร็วกว่า interface เสมอ — เพราะ pointer/interface shape แชร์ instantiation ผ่าน dictionary กรณีนี้ generics อาจช้ากว่า interface ด้วยซ้ำ; ส่วน value type (
int, struct) ได้ code เฉพาะของตัวเองต่อ type จึงเร็วและ avoid boxing ได้จริง - generics เหมือน C++ templates — Go generics ตรวจสอบ constraint ตอน definition ไม่ใช่ instantiation — error messages ดีกว่ามาก
- ควรใช้ generics แทน interface เสมอ — interface เหมาะกว่าสำหรับ runtime polymorphism, generics เหมาะกว่าสำหรับ compile-time type-safe algorithms
💡 ตัวอย่างจากของจริง
slices: generic
slices.Sort[S ~[]E, E cmp.Ordered](s S)— type-safe sort ที่ทำงานกับทุก ordered type โดยไม่ต้อง cast