Một hàm generic
Type parameter được đặt trong ngoặc vuông trước các tham số thông thường. Mỗi type parameter có một constraint cho biết những kiểu nào được phép.
T và U được suy ra từ các đối số, nên bạn hiếm khi phải viết Map[int, string](...). Việc suy luận dựa trên đối số của hàm; Go không thể suy ra một kiểu chỉ xuất hiện trong kết quả. Với func Zero[T any]() T, bạn phải viết Zero[int]().
Trước Go 1.18, hàm này phải được viết lại cho từng kiểu, hoặc nhận []interface{} và mất đi sự an toàn về kiểu.
Constraint
Một constraint là một interface. Nó giới hạn những kiểu có thể dùng, và đổi lại, cho trình biên dịch biết những phép toán nào được phép trên giá trị thuộc kiểu đó.
| Constraint | Cho phép | Cho bạn dùng |
|---|---|---|
any | mọi kiểu | gán, truyền, lưu trữ |
comparable | các kiểu hỗ trợ == | ==, !=, khóa của map |
cmp.Ordered (Go 1.21) | số nguyên, số thực, chuỗi | <, <=, >, >=, cùng với == |
| một interface có phương thức | các kiểu có những phương thức đó | gọi các phương thức |
một type union như ~int | ~float64 | đúng những kiểu đó | các toán tử mà tất cả chúng đều hỗ trợ |
Nếu dùng any thay cho comparable, v == target sẽ không biên dịch được: invalid operation: v == target (incomparable types in type set). Chính constraint làm cho toán tử trở nên hợp lệ.
Hai hàm tiện ích này đã có sẵn: slices.Index và slices.Max. Các package chuẩn slices, maps và cmp đáp ứng hầu hết nhu cầu generic hằng ngày, nên hãy xem chúng trước khi tự viết.
Constraint tùy chỉnh với type union và ~
Một constraint có thể liệt kê các kiểu bằng |. Các toán tử được phép là những toán tử mà mọi kiểu trong danh sách đều hỗ trợ.
Dấu ngã rất quan trọng. ~int64 nghĩa là "bất kỳ kiểu nào có kiểu nền là int64", nên kiểu có tên Cents được chấp nhận. Nếu viết chỉ là int64, Sum([]Cents{...}) sẽ thất bại với Cents does not satisfy Number (possibly missing ~ for int64 in Number).
Interface chứa type union chỉ có thể dùng làm constraint, không bao giờ dùng làm kiểu biến thông thường. var n Number không biên dịch được.
Package golang.org/x/exp/constraints có sẵn các constraint Integer, Float và Signed. Nó nằm ngoài thư viện chuẩn, nên trình chạy ở đây không import được; cmp.Ordered là constraint chuẩn.
Kiểu generic
Struct, slice, map và các kiểu khác cũng có thể có type parameter. Phương thức trên một kiểu generic dùng type parameter của receiver.
Hai chi tiết trong ví dụ này xuất hiện liên tục:
- Giá trị zero của T.
var zero Tlà cách trả về "không có gì" trong code generic. Không cóT{}haynilnào dùng được cho mọi kiểu. - Khởi tạo kiểu (instantiation). Một kiểu generic phải được khởi tạo trước khi dùng:
Stack[string], không phảiStack. Bên trong các phương thức của chính nó, receiver được viết làStack[T].
Go 1.24 bổ sung type alias generic: type Set[T comparable] = map[T]struct{} giờ đã biên dịch được.
Những giới hạn bạn sẽ gặp
- Không có type parameter trên phương thức.
func (s *Stack[T]) Map[U any](...)không được phép. Hãy dùng một hàm ở cấp cao nhất. - Không có chuyên biệt hóa (specialization). Bạn không thể viết một bản cài đặt riêng cho
T = string. Dùng type switch trênany(v)bên trong hàm là cách lách, và thường là dấu hiệu cho thấy generics không phải công cụ phù hợp. - Không có constraint theo tên toán tử. Bạn không thể nói "bất kỳ kiểu nào có phương thức
+"; bạn phải liệt kê các kiểu trong một union. - Không truy cập được trường thông qua constraint. Kể cả khi mọi kiểu trong một union đều có trường
ID,v.IDvẫn không được phép. Hãy dùng một phương thức trong constraint thay thế.
Khi nào không nên dùng generics
Generics phù hợp với các kiểu container (stack, set, cache, cây) và các thuật toán trên slice và map làm cùng một việc với mọi kiểu phần tử. Chúng không phù hợp ở mọi nơi.
- Chỉ một kiểu cụ thể. Nếu hàm chỉ được gọi với
[]User, một phiên bản generic chỉ làm code khó đọc hơn mà không thêm lợi ích nào. - Hành vi khác nhau theo từng kiểu. Đó là việc của interface. Một hàm nhận
io.Writerđơn giản hơn một hàm nhận[T io.Writer], và hoạt động y hệt. - Thay thế tham số interface.
func Print[T fmt.Stringer](v T)không tốt hơnfunc Print(v fmt.Stringer).
Hướng dẫn của chính nhóm phát triển Go: hãy viết code cho một kiểu cụ thể trước, và chỉ dùng đến type parameter khi bạn nhận ra mình đang viết lại cùng một đoạn code lần thứ hai mà chỉ khác kiểu.
Những lỗi thường gặp
- Dùng
anyở nơi cầncomparablehoặccmp.Ordered. Trình biên dịch từ chối==hoặc<trên một type parameterany. - Quên
~. Các kiểu do người dùng định nghĩa nhưtype ID intkhông thỏa một constraint chỉ ghiint. - Trả về
nilcho một type parameter. Không được phép, trừ khi constraint giới hạn T vào các kiểu dạng con trỏ. Hãy trả về giá trị zero. - Viết một hàm tiện ích generic đã có sẵn. Hãy xem
slices,mapsvàcmptrước.
Câu hỏi thường gặp
Go có generics không?
Có, từ Go 1.18 (tháng 3 năm 2022). Hàm và kiểu có thể khai báo type parameter trong ngoặc vuông: func Map[T, U any](s []T, f func(T) U) []U. Mỗi type parameter có một constraint, là một interface mô tả những gì kiểu đó phải hỗ trợ.
any và comparable trong generics của Go khác nhau thế nào?
any cho phép mọi kiểu nhưng gần như không cho bạn làm gì với giá trị ngoài gán, truyền và lưu trữ. comparable chỉ cho phép những kiểu hỗ trợ == và !=, đúng thứ bạn cần cho khóa của map hoặc phép so sánh bằng. Với < và >, hãy dùng cmp.Ordered.
Dấu ngã (~) trong constraint của Go có nghĩa là gì?
~T nghĩa là "bất kỳ kiểu nào có kiểu nền là T". ~int khớp với int và cả type Celsius int. Không có dấu ngã, int trong một constraint chỉ khớp với chính int, nên các kiểu do người dùng định nghĩa sẽ bị từ chối.
Phương thức trong Go có thể có type parameter không?
Không. Một phương thức có thể dùng type parameter của kiểu receiver (func (s *Stack[T]) Push(v T)), nhưng không thể khai báo type parameter mới. Thay vào đó, hãy viết một hàm generic ở cấp cao nhất: func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U].