Điều khiến generics của Go dễ chịu khi dùng là bạn hiếm khi phải viết Tong[int](...) — chỉ cần Tong(...) và compiler tự điền T. Cơ chế đứng sau sự tiện lợi đó gọi là type inference (suy luận kiểu), và nó tinh vi hơn nhiều so với "nhìn đối số đoán kiểu". Bài trước ta thấy constraint là một type set; bài này mổ xẻ compiler suy ra type argument thế nào, ba tầng suy luận, và — quan trọng không kém — chính xác khi nào nó chịu thua và buộc bạn khai rõ.
Tầng 1 và 2: suy từ đối số, và qua tham số hàm
Tầng cơ bản nhất: nếu một type parameter xuất hiện trong kiểu của một tham số, Go suy nó từ kiểu đối số bạn truyền vào.
func Tong[T Num](xs []T) T { /* ... */ }
func Map[T, U any](xs []T, f func(T) U) []U { /* ... */ }
Tong([]int{1, 2, 3}) // T=int, suy từ []int
Tong([]int{...}) — Go thấy tham số kiểu []T, đối số kiểu []int, nên T=int. Đơn giản. Tầng thứ hai thú vị hơn: type parameter có thể được suy qua chữ ký của một hàm truyền vào. Trong Map, U không xuất hiện ở bất kỳ đối số dữ liệu nào — nó chỉ nằm trong func(T) U. Nhưng vì bạn truyền một hàm cụ thể, Go đọc chữ ký hàm đó để suy cả T lẫn U:

Hình 1: Ba tầng suy luận. Tầng 1: T từ kiểu đối số. Tầng 2: cả T và U suy qua chữ ký func(T) U. Tầng 3: constraint type inference suy E từ S ~[]E. Và hai trường hợp compiler chịu thua.
Đo thật (Go 1.23):
Tong: 6 // Tong([]int) -> T=int
Tong f64: 4 // Tong([]float64) -> T=float64
Map double: [2 4 6] // T=int, U=int
Map len: [1 2 3] // T=string, U=int (qua func(T)U)
Map->string: [n1 n2 n3] // T=int, U=string
Map([]string{...}, func(s string) int {...}) cho T=string, U=int — Go suy T từ slice, rồi khớp chữ ký hàm để lấy U. Đây là lý do bạn viết được Map(data, transform) gọn gàng thay vì Map[string, int](data, transform).
Khi suy luận thất bại: compiler báo thẳng, không đoán
Suy luận không phải phép màu. Có hai trường hợp kinh điển nó không thể suy, và cách Go xử lý chúng nói lên triết lý của ngôn ngữ.
Case A — type parameter chỉ ở kiểu trả về. Nếu T không xuất hiện trong bất kỳ tham số nào, không có gì để suy từ đó:
func Zero[T any]() T { var z T; return z }
Zero() // KHÔNG biên dịch
Zero[int]() // OK — khai rõ type argument
Case B — đối số là nil hoặc hằng không kiểu. nil không mang kiểu cụ thể nên không giúp suy được gì:
func Lam[T any](x T) T { return x }
Lam(nil) // KHÔNG biên dịch
Đo thật, cả hai cho lỗi biên dịch rõ ràng:

Hình 2: Case A và B đều cho lỗi biên dịch cannot infer T (tại đúng dòng gọi). Compiler không đoán mò một kiểu mặc định, không đẩy lỗi sang lúc chạy — nó dừng ở go build và bảo bạn khai rõ.
Điểm đáng nói: ./failA.go:10:10: in call to Zero, cannot infer T. Go không chọn một kiểu mặc định (như interface{}) để cho qua. Nó dừng lại và bắt bạn viết rõ ý định. So với ngôn ngữ suy luận một kiểu "an toàn" rồi để lỗi lộ lúc chạy, cách này giữ đúng tinh thần Go: mơ hồ về kiểu là lỗi biên dịch, không phải quả bom hẹn giờ.
Tầng 3: constraint type inference
Đây là tầng tinh vi nhất và cũng là thứ khiến API generic của Go gọn gàng bất ngờ. Xét một hàm nhận slice mà bạn muốn trả về phần tử:
func Dau[S ~[]E, E any](s S) E { return s[0] }
Dau([]int{10, 20, 30}) // chỉ truyền slice, không truyền E
Dau([]string{"x", "y"})
Hàm có hai type parameter S và E, nhưng bạn chỉ truyền một đối số. Go suy theo dây chuyền hai bước: trước tiên từ đối số []int suy S=[]int (tầng 1). Sau đó nó dùng ràng buộc S ~[]E — biết S là []int và constraint nói S phải có dạng ~[]E, nó giải ra E=int. Bước thứ hai này chính là constraint type inference: suy một type parameter từ ràng buộc của một type parameter khác đã biết.
Đo thật:
Dau int: 10 // Dau([]int) -> S=[]int -> E=int
Dau str: x // Dau([]string) -> S=[]string -> E=string
Zero[int]=0 Zero[string]="" // Case A sửa bằng type arg rõ
Nhờ tầng này, các hàm trong thư viện chuẩn như slices.Index[S ~[]E, E comparable](s S, v E) gọi được đơn giản slices.Index(xs, 42) — bạn không bao giờ phải viết slices.Index[[]int, int](xs, 42). Constraint type inference làm phần việc bắc cầu giữa kiểu slice và kiểu phần tử.
Đánh đổi cần cân nhắc
Khai rõ type argument khi suy luận mơ hồ hoặc khó đọc. Đôi khi suy được nhưng người đọc code khó theo (nhiều type parameter lồng nhau). Viết rõ [int, string] là hoàn toàn hợp lệ và đôi khi rõ ý hơn — inference là tiện lợi, không phải luật bắt buộc phải dựa vào. Với Case A/B thì khai rõ là bắt buộc.
Suy luận không xuyên qua ép kiểu ngầm. Go không tự nới int thành int64 hay ngược lại trong lúc suy. Nếu bạn truyền []int32 vào hàm [T Num] mà Num không có ~int32, đó là lỗi kiểu — inference chỉ khớp kiểu, không chuyển đổi. Điều này nhất quán với việc Go không có ép kiểu số ngầm ở mọi nơi khác.
Hằng không kiểu là điểm dễ vấp. Lam(42) chạy được (42 mặc định về int), nhưng Lam(nil) thì không. Khi làm việc với hằng, con trỏ nil, hay iota, hãy để ý: nếu compiler không có kiểu cụ thể để bám vào, bạn phải khai rõ. Đây là nguồn của phần lớn lỗi cannot infer trong thực tế.
Ba ý mang về
- Go suy luận kiểu ở ba tầng: từ kiểu đối số (
Tong([]int)→T=int), qua chữ ký hàm truyền vào (Mapsuy cảTvàUtừfunc(T) U), và constraint type inference (Dau[S ~[]E]suyEtừSđã biết) — nhờ đó API generic gọi gọn không cần khai type argument. - Suy luận thất bại là lỗi BIÊN DỊCH, không đoán mò: khi
Tchỉ ở kiểu trả về (Zero()) hoặc đối số lànil, compiler báocannot infer Tngay tại dòng gọi — không chọn kiểu mặc định, không đẩy lỗi sang lúc chạy. - Khai rõ
[T]là lối thoát chính thức: bắt buộc cho Case A/B, và tùy chọn khi inference gây khó đọc — inference là tiện lợi chứ không phải ràng buộc, và nó chỉ khớp kiểu chứ không ép kiểu số ngầm.
Phần sau ta lấy thước đo đặt lên bàn cân: Phần sau đo chi tiết chi phí generics so với interface — GC shape stenciling, dictionary cho kiểu con trỏ, và khi nào mỗi cách thắng thật sự.