ข้ามไปยังเนื้อหา

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 ที่ไม่รองรับ

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

constraint คือ interface ที่จำกัดว่า type ใดสามารถแทน type parameter ได้ constraints จะปรากฏหลังชื่อ type parameter ใน list [T Constraint]

  • any — constraint ที่กว้างที่สุด; เทียบเท่ากับ interface{} อนุญาต type ใดก็ได้ แต่จำกัด operations ที่ทำได้กับค่าของ type T (เฉพาะ 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

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

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)
}
สิ่งที่ได้ประโยชน์ต้นทุน
type parameterscode reuse โดยไม่ต้องใช้ interface{}compile time เพิ่มขึ้น, syntax ซับซ้อน
type constraintscompile-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 โดยไม่ต้อง cast

Kubernetes: generic controller framework ใน Kubernetes 1.26+ ใช้ generics เพื่อลด code duplication ระหว่าง controller type ต่างๆ

database/sql: library wrapper หลายตัว (เช่น sqlc) ใช้ generics สำหรับ type-safe query result scanning

ใน func Max[T Number](a, b T) T ส่วน [T Number] หมายความว่าอะไร?
prefix ~ ใน constraint เช่น ~int หมายความว่าอะไร?
constraint ใดที่อนุญาต type ที่รองรับ == และ !=?
ที่ call site Max(3, 7) จำเป็นต้องเขียน Max[int](3, 7) หรือไม่?