Lập trình tổng quát (Generic programming) là một phong cách hay một mô hình lập trình cho phép lập trình viên viết mã trong các ngôn ngữ có kiểu dữ liệu mạnh (strongly-typed) bằng cách sử dụng các kiểu dữ liệu sẽ được chỉ định sau. Các kiểu này được cung cấp dưới dạng tham số khi khởi tạo.
Generics giúp bạn viết mã áp dụng được cho nhiều kiểu dữ liệu khác nhau mà không phải lặp lại cùng một logic. Điều này cải thiện khả năng tái sử dụng mã, tăng tính linh hoạt và đảm bảo an toàn kiểu (type safety).
Trong Go, generics được triển khai thông qua tham số kiểu (type parameters). Một tham số kiểu là một dạng tham số đặc biệt, dùng làm đại diện cho bất kỳ kiểu dữ liệu nào. Chúng được sử dụng trong định nghĩa hàm, phương thức, và kiểu dữ liệu; và được thay thế bằng kiểu cụ thể khi hàm được gọi.
Trước khi có Generics
Xét ví dụ sau: viết một hàm nhận vào hai tham số kiểu int
và trả về giá trị nhỏ hơn trong hai số đó. Rất đơn giản:
func Min(a, b int) int { if a < b { return a } return b
}
Hàm trên hoạt động tốt, nhưng bị giới hạn: tham số chỉ được phép là kiểu int. Nếu yêu cầu mở rộng để hỗ trợ so sánh hai giá trị kiểu float64
, ta sẽ phải viết thêm:
func Min(a, b int) int { if a < b { return a } return b
} func MinFloat64(a, b float64) float64 { if a < b { return a } return b
}
Bạn có thể thấy, mỗi lần có yêu cầu mới, ta lại phải lặp lại cùng một logic. Generics chính là giải pháp cho vấn đề này.
import "golang.org/x/exp/constraints" func Min[T constraints.Ordered](x, y T) T { if x < y { return x } return y
}
Cú pháp cơ bản của Generics
// Định nghĩa hàm
func F[T any](p T){...} // Định nghĩa kiểu
type M[T any] []T // Ràng buộc kiểu cụ thể, như any, comparable
func F[T Constraint](p T){..} // Ký hiệu “~” đại diện cho kiểu cơ sở (underlying type)
type E interface { ~string
} // Chỉ định nhiều kiểu
type UnionElem interface { int | int8 | int32 | int64
}
Ký hiệu ~
trong Generics
Trong Go, ký hiệu ~
được dùng để biểu diễn ràng buộc kiểu cơ sở.
Ví dụ, ~int
có nghĩa là chấp nhận bất kỳ kiểu nào có kiểu cơ sở là int
, bao gồm cả kiểu tự định nghĩa:
type MyInt int type Ints[T int | int32] []T func main() { a := Ints[int]{1, 2} // Hợp lệ b := Ints[MyInt]{1, 2} // Lỗi biên dịch println(a) println(b)
}
MyInt
không thỏa mãn int | int32
. Cần sửa lại bằng cách thêm ~
:
type Ints[T ~int | ~int32] []T
Ràng buộc kiểu (Type Constraints)
any
: Chấp nhận mọi kiểu dữ liệucomparable
: Hỗ trợ toán tử==
và!=
ordered
: Hỗ trợ toán tử so sánh như>
,<
Tham khảo thêm tại: https://pkg.go.dev/golang.org/x/exp/constraints
Khi nào nên dùng Generics?
- Làm việc với container có sẵn trong ngôn ngữ: Khi viết các hàm thao tác với kiểu container như slice, map, channel mà không phụ thuộc vào kiểu phần tử, dùng generics sẽ hữu ích (ví dụ: hàm lấy danh sách các key của bất kỳ kiểu map nào).
- Cấu trúc dữ liệu dùng chung: Với các cấu trúc dữ liệu như linked list, binary tree... dùng generics giúp tạo ra các cấu trúc tổng quát hơn, hiệu quả hơn và được kiểm tra kiểu tại thời điểm biên dịch.
- Triển khai phương thức dùng chung: Khi các kiểu khác nhau cần triển khai phương thức giống nhau, generics giúp tái sử dụng logic.
- Ưu tiên function thay vì method: Khi cần viết hàm so sánh hay thao tác chung, nên viết dưới dạng function hơn là yêu cầu interface có method.
Ví dụ: Sắp xếp slice sử dụng hàm so sánh:
type SliceFn[T any] struct { s []T less func(T, T) bool
} func (s SliceFn[T]) Len() int { return len(s.s)
}
func (s SliceFn[T]) Swap(i, j int) { s.s[i], s.s[j] = s.s[j], s.s[i]
}
func (s SliceFn[T]) Less(i, j int) bool { return s.less(s.s[i], s.s[j])
} func SortFn[T any](s []T, less func(T, T) bool) { sort.Sort(SliceFn[T]{s, less})
}
Khi nào không nên dùng Generics?
- Không thay thế interface: Nếu chỉ cần gọi các method có sẵn, hãy dùng interface thay vì generics.
- Không dùng nếu mỗi kiểu có logic khác nhau: Nếu mỗi kiểu cần triển khai method khác nhau, hãy dùng interface và viết riêng, không nên dùng type parameter.
- Dùng reflection khi cần linh hoạt hơn: Khi phải thao tác với kiểu không có method và xử lý khác biệt rõ ràng, dùng reflection là phù hợp (ví dụ: gói
encoding/json
).
Nguyên tắc đơn giản
Nếu bạn đang viết đi viết lại cùng một đoạn code chỉ khác kiểu dữ liệu, thì hãy cân nhắc dùng generics.
Nói cách khác, chỉ nên dùng generics khi bạn thấy mình bắt đầu sao chép cùng một logic cho các kiểu khác nhau.