Menu

Generics trong Golang: type parameter, constraint, ví dụ

Generics (Go 1.18 trở lên) cho phép một hàm hoặc một kiểu làm việc với nhiều kiểu dữ liệu mà vẫn an toàn về kiểu. Tìm hiểu type parameter, các constraint any, comparable và cmp.Ordered, constraint tùy chỉnh với ~, kiểu generic, và khi nào không nên dùng.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

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 đó.

ConstraintCho phépCho bạn dùng
anymọi kiểugán, truyền, lưu trữ
comparablecá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ứccá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 T là cách trả về "không có gì" trong code generic. Không có T{} hay nil nà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ải Stack. 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ên any(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.ID vẫ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ơn func 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ần comparable hoặc cmp.Ordered. Trình biên dịch từ chối == hoặc < trên một type parameter any.
  • Quên ~. Các kiểu do người dùng định nghĩa như type ID int không thỏa một constraint chỉ ghi int.
  • Trả về nil cho 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, maps và cmp trướ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].

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu