Generics
ทำไมต้องใช้ Generics?
หัวข้อที่มีชื่อว่า “ทำไมต้องใช้ Generics?”ก่อน Go 1.18 การเขียนฟังก์ชันที่ทำงานกับหลาย type ต้องการ code ซ้ำหรือใช้ interface{} + type assertion ที่ runtime ซึ่งสูญเสีย type safety Generics แก้ปัญหานี้: เขียนครั้งเดียว compiler สร้าง code เฉพาะ type และตรวจสอบการใช้งานที่ compile time
ตัวอย่างเช่น ฟังก์ชัน Max ที่ทำงานได้ทั้ง int และ float64 ก่อนหน้านี้ต้องเขียนสองฟังก์ชันที่เหมือนกัน หรือใช้ interface{} parameter ที่ยอมรับ type ผิดอย่างเงียบ ๆ ด้วย generics ฟังก์ชันเดียวครอบคลุมทั้งสอง — และ 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, การส่งไปยังฟังก์ชัน เป็นต้น)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 ได้เช่นกัน ทำให้ constraints ทำงานได้ตามธรรมชาติกับ domain-specific type aliases
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 ด้านล่าง cmp.Ordered ถูกแทนที่ด้วย local Number interface เพื่อหลีกเลี่ยงการ import package ภายนอก — yaegi (in-browser interpreter) รองรับ generics บางส่วนและทำงานได้น่าเชื่อถือที่สุดกับ inline constraints
Generic Types
หัวข้อที่มีชื่อว่า “Generic Types”struct ก็สามารถ parameterize ได้เช่นกันเหมือนกับฟังก์ชัน ประกาศ 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
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)}Loading Go runtime (first run only, ~8 MB)…
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| 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 — code ที่มี pointer shape ใช้ instance เดียวกัน ไม่ขยายแบบ C++ templates
- generic function เร็วกว่า interface เสมอ — generics avoid boxing แต่ overhead ของ instantiation per type อาจมีผลในบางกรณี
- generics เหมือน C++ templates — Go generics ตรวจสอบ constraint ตอน definition ไม่ใช่ instantiation — error messages ดีกว่ามาก
- ควรใช้ generics แทน interface เสมอ — interface เหมาะกว่าสำหรับ runtime polymorphism, generics เหมาะกว่าสำหรับ compile-time type-safe algorithms
💡 ตัวอย่างจากของจริง
golang.org/x/exp/slices: generic
slices.Sort[S ~[]E, E cmp.Ordered](s S)— type-safe sort ที่ทำงานกับทุก ordered type โดยไม่ต้อง castKubernetes: generic controller framework ใน Kubernetes 1.26+ ใช้ generics เพื่อลด code duplication ระหว่าง controller type ต่างๆ
database/sql: library wrapper หลายตัว (เช่น
sqlc) ใช้ generics สำหรับ type-safe query result scanning