Generics trong Go là chủ đề được bàn tán sôi nổi nhiều năm qua. Sau tất cả thì những nỗ lực của cộng đồng đã được đền đáp. Go sẽ chính thức hỗ trợ Generics bắt đầu từ version 1.18.
Dẫu cho vào thời điểm mình viết bài này, version 1.18 vẫn chưa được công bố chính thức (beta). Nhưng cũng đáng để chúng ta bắt đầu tìm hiểu về nó.
Generics là gì?
Generics là một kỹ thuật lập trình thường xuất hiện trong OOP (lập trình hướng đối tượng). Kỹ thuật này giúp lập trình viên định nghĩa được những hàm (phương thức hay method/function) chấp nhận các tham số chung chung, không cần chỉ định rõ nó thuộc kiểu dữ liệu gì. Tới khi hàm này được sử dụng, người gọi sẽ quyết định việc này.
Một số ngôn ngữ khác nhau có thể thiết kế Generics khác nhau. Đơn cử như Java hay Dart thì chúng ứng dụng vào khai báo các class nữa. VD: List<T>
, Future<T>
,... Golang thì không có class nên sẽ không có những khai báo này.
Vì sao mãi tới bây giờ Go mới chấp nhận Generics?!
Riêng vấn đề này cũng có rất nhiều ý kiến ủng hộ và không ủng hộ. Vì trên cơ bản, Go là ngôn ngữ static type. Nghĩa là khi biên dịch (compile), Go sẽ cần biết kiểu dữ liệu của tất cả các biến và tham số trong hàm.
Nếu dùng generics sẽ phá vỡ nguyên tắc này. Nghĩa là phải tới khi chương trình thực thi (runtime) thì mới phát hiện lỗi. Việc này một phần làm giảm tính an toàn của ngôn ngữ Go. Hay thậm chí còn ảnh hưởng tới tốc độ thực thi (performance), một trong những tính chất làm nên thương hiệu Go.
Phía ủng hộ thì cũng có lý của họ vì không có Generics thì code Go quá cứng nhắc, không thể tái sử dụng lại. Đơn cử là viết hàm tìm số lớn nhất của một mảng int, bạn sẽ phải viết hàm cho []int
, hàm cho []int32
, hàm cho []int64
,... trong khi cái body (thân hàm) chả khác gì cả.
Generics trong Go sử dụng ra sao?!
Để nhanh gọn thì các bạn có thể truy cập vào https://go2goplay.golang.org/ để trải nghiệm luôn, đoạn code đơn giản mà đội ngũ Go đã cung cấp:
package main
import (
"fmt"
)
// This playground uses a development build of Go:
// devel go1.18-b1c7703f26 Wed Dec 15 14:48:19 2021 +0000
func Print[T any](s ...T) {
for _, v := range s {
fmt.Print(v)
}
}
func main() {
Print("Hello, ", "playground\n")
}
Hãy để ý ở dòng định nghĩ hàm Print có xuất hiện syntax mới là [T any]
sau đó là phần tiếp nhận tham số T
: (s ...T)
. Nghĩa là hàm Print nhận một dãy các giá trị T. T
này vào lúc khai báo vẫn chưa biết cụ thể là kiểu dữ liệu nào, nhưng vì là any
nên dữ liệu nào cũng được. Công việc còn lại chỉ là duyệt qua mảng T rồi print ra thôi.
Trong trường hợp nếu các bạn muốn chạy trên editor hoặc trên máy tính cá nhân thì hãy mở terminal và thực hiện các lệnh sau:
#download Go source
git clone https://go.googlesource.com/g
# checkout go2go branch
cd go && git checkout dev.go2go
# compile
cd src && ./make.bash
# build tools
GO111MODULE=on go get golang.org/x/tools/gopls@dev.go2go golang.org/x/tools@dev.go2go
Lưu ý file code Go có đuôi là .go2
nhé!!
Hiểu về Generics trong Go
Go khi có Generics vẫn giữ được tính chất đơn giản và tinh gọn trong code. Thật ra Generic cũng chỉ có 2 phần thôi:
- Tên biến dùng làm "giữ chỗ". Thật ra trong Go thì gọi là Type (kiểu biến), nhưng mình không muốn các bạn nhầm lẫn
type
trong kiểu khai báo biến nên gọi là "tên". VD: T, K, V,... - Ràng buộc (Constraint) cho biến rõ ràng hơn hoặc cụ thể hơn một chút. Nếu không cần ràng buộc gì, chúng ta có thể dùng
any
.
Sử dụng Generics cho function trong Go:
Đây chính là ví dụ cơ bản mà team Go cung cấp ở trên. Tuy nhiên để thú vị hơn thì chúng ta có thể thử sức làm lại hàm Map (rất phổ biến trong JS). Hàm Map nhận vào một mảng các giá trị bất kì sau đó sẽ biến đổi mỗi item trong đó thành giá trị khác.
package main
import (
"fmt"
)
func Map[K, V any](s []K, transform func(K) V) []V {
rs := make([]V, len(s))
for i, v := range s {
rs[i] = transform(v)
}
return rs
}
func main() {
arr := []int{1, 2, 3}
resultArr := Map(arr, func(v int) int { return v * 2 })
fmt.Println(resultArr)
arr2 := []string{"a", "b", "c"}
resultArr2 := Map(arr2, func(v string) string { return v + v })
fmt.Println(resultArr2)
}
// [2, 4, 6]
// ["aa", "bb", "cc"]
Tuyệt vời, vì có Generics nên chúng ta chỉ cần define một lần duy nhất cho hàm Map là xong. Trước đó các bạn phải define cho tất cả các kiểu dữ liệu mà bạn muốn.
Giải thích một chút về Map ở trên nhé:
K
vàV
sẽ là 2 kiểu dữ liệu generics cho hàm Map. Chúng ta chưa rõ nó là gì nhưng chúng ta muốn mọi kiểu dữ liệu đều có thể dùng được nên cả 2 đều làany
.- Ở phần định nghĩa tham số truyền vào chúng ta sẽ cần một mảng K:
[]K
và một hàmtransform
nhận kiểuK
và trả về kiểuV
. Hàm này là để người gọi muốn làm gì với mỗi itemK
thì làm, miễn returnV
là okie. - Cuối cùng hàm Map phải trả về một mảng mới:
[]V
- Ở hàm main, mình chạy thử với 2 trường hợp mảng int và mảng string. Các bạn có thể đổi lại tuỳ mục đích biến đổi item trong mảng ban đầu nhé.
Sử dụng Generics định nghĩa các cấu trúc dữ liệu trong Go:
Một số trường hợp nếu các bạn không muốn phải liên tục định nghĩa lại các cấu trúc dữ liệu trong Go. Các bạn có thể dùng Generics để hỗ trợ, ví dụ:
Sử dụng constraint Generics trong Go:
Đầu tiên mình sẽ thử viết một hàm tìm số nhỏ nhất trong mảng int với việc ứng dụng Generic xem sao nhé:
package main
func Min[T any](s []T) T {
r := s[0]
for _, v := range s[1:] {
if v < r {
r = v
}
}
return r
}
Có vẻ đơn giản, nhưng bị báo lỗi khi compile:
./prog.go:6:6: invalid operation: cannot compare v < r (operator < not defined on T)
Lỗi này cũng dễ hiểu vì là any
nên không phải value nào cũng thực hiện phép toán so sánh được.
Ở đây chúng ta có một vài giải pháp, trong đó truyền vào thêm một hàm compare để so sánh cũng là một cân nhắc:
package main
func Smallest[T any](s []T, compare func(T, T) int) T {
r := s[0]
for _, v := range s[1:] {
if compare(r, v) == 1 {
r = v
}
}
return r
}
Giải pháp mình muốn giới thiệu cho các bạn là thiết lập Constraint:
type SignedInteger interface {
int | int8 | int16 | int32 | int64
}
func Smallest[T SignedInteger](s []T) T {
r := s[0]
for _, v := range s[1:] {
if r > v {
r = v
}
}
return r
}
Bây giờ T bắt buộc phải thuộc một trong những kiểu int | int8 | int16 | int32 | int64
thì mới dùng phép toán so sánh được.
Hiệu năng của Generics trong Go
Để thực hiện được các "magic" trên, đội ngũ phát triển Go để phải thay đổi bộ biên dịch để xử lý lại giai đoạn phân tích ngôn ngữ, để từ đó mới biết được syntax đó có đúng kiểu Generics hay không. Các bạn có thể xem qua tại:
go tool go2go translate xx.go2
Về hiệu năng thì đội ngũ có đề cập rằng các generics function cũng sẽ biên dịch một lần duy nhất. Tuy nhiên khi chạy thử trên goplayground chúng ta có thể nhận ra tốc độ biên dịch giảm rõ rệ so với hàm thông thường (không có generics), thậm chí đôi khi bị timeout. Tuy nhiên việc này có thể là do goplayground có vấn đề chứ không hẳn đến từ Golang. Chúng ta sẽ phải đợi thêm các bài test chi tiết hơn cũng như cho tới khi Go 1.18 chính thức phát hành.
Lời kết
Cá nhân mình nghĩ rằng Generics xuất hiện trong ngôn ngữ Go thực sự là một cải tiến lớn cho bản thân ngôn ngữ này. Mặt khác, nó sẽ góp phần thay đổi rõ rệt cách mà các developer Go tổ chức source code để tận dụng được chức năng này tốt hơn.
Nếu các bạn là một tay đua đặt tốc độ thực thi và biên dịch lên hàng đầu thì có khả năng các bạn sẽ không quan tâm đến Generics. Không sao cả, vì trước khi có nó thì hệ thống vẫn chạy ổn. Bản thân mình hài lòng với việc hy sinh chút hiệu năng và thời gian biên dịch đổi lấy code đỡ nhọc hơn.
Dù ở thời điểm mình viết bài này Generics có thể chưa stable, syntax vẫn đang thay đổi. Chúng ta vẫn cần chờ đợi đến khi phiên bản chính thức để "chốt" syntax cũng như các vấn đề hiệu năng.
Hy vọng các bạn sẽ thích bài viết này. Nếu cần tham khảo và tìm hiểu nhiều hơn về Go, các bạn có thể xem lại link này nhé:
Hoặc tham gia KHOÁ HỌC GOLANG for SCALABLE BACKEND.
Việt Trần
Yêu thích tìm hiểu các công nghệ cốt lõi, kỹ thuật lập trình và thích chia sẻ chúng tới cộng đồng
follow me :
Bài viết liên quan
Design Pattern là gì? 23 Classic Design Pattern với Golang
Aug 21, 2024 • 3 min read
Concurrency pattern trong Go - Phần 2
Apr 11, 2024 • 7 min read
Concurrency pattern trong Golang - Phần 1
Mar 25, 2024 • 8 min read
So sánh Golang và NodeJS chi tiết
Oct 14, 2023 • 22 min read
Buffered Channel là gì? Thường xuyên hỏi trong phỏng vấn Golang Dev
Jun 14, 2023 • 6 min read
Golang Channel là gì? Các ứng dụng Channel trong Golang
Jun 14, 2023 • 12 min read