, January 26, 2022

0 kết quả được tìm thấy

Generics trong Golang


  •   8 min reads
Generics trong Golang

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:

  1. 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,...
  2. 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é:

  1. KV 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.
  2. Ở 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àm transform nhận kiểu K và trả về kiểu V. Hàm này là để người gọi muốn làm gì với mỗi item K thì làm, miễn return V là okie.
  3. Cuối cùng hàm Map phải trả về một mảng mới: []V
  4. Ở 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ụ:

type Vector[T any] []T

type LinkedList[T any] struct {
	next *LinkedList[T]
	val  T
}

type Pair[T1, T2 any] struct {
	v1 T1
	v2 T2
}

type Tuple[T1, T2, T3 any] struct {
	v1 T1
	v2 T2
	v3 T3
}
f

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é:

Tìm hiểu ngôn ngữ lập trình Golang. Tại sao bạn nên học Golang vào bây giờ?
Golang là ngôn ngữ lập trình mã nguồn mở giúp xây dựng phần mềm dễ dành, tin cậy và hiệu quả. Hiện tại Golang đang dần trở nên phổ biến trên thế giới, rất nhiều nơi đã chuyển đổi hệ thống về Golang, trong đó có cả Việt Nam.

Hoặc tham gia KHOÁ HỌC GOLANG for SCALABLE BACKEND.

Bài viết liên quan

Những sai lầm thường thấy khi bạn sử dụng goroutines

Trong bài viết này, mình sẽ đề cập đến một số trường hợp và sự cố phổ biến mà bạn có thể gặp phải khi sử dụng goroutines và cách để giải quyết chúng....

Những sai lầm thường thấy khi bạn sử dụng goroutines
Tìm hiểu ngôn ngữ lập trình Golang. Tại sao bạn nên học Golang vào bây giờ?

Golang là ngôn ngữ lập trình mã nguồn mở giúp xây dựng phần mềm dễ dành, tin cậy và hiệu quả. Hiện tại Golang đang dần trở nên phổ biến trên thế giới, rất nhiều nơi đã chuyển đổi hệ thống về Golang, trong đó có cả Việt Nam....

Tìm hiểu ngôn ngữ lập trình Golang. Tại sao bạn nên học Golang vào bây giờ?
So sánh chi tiết Golang vs NodeJS

Golang và Nodejs từ lâu đã trở thành chủ đề sôi nổi. Nodejs đã chiếm ưu thế trong một khoảng thời gian dài. Golang liệu có đủ sức thuyết phục để chuyển đổi?...

So sánh chi tiết Golang vs NodeJS
You've successfully subscribed to 200Lab Blog
Great! Next, complete checkout for full access to 200Lab Blog
Xin chào mừng bạn đã quay trở lại
OK! Tài khoản của bạn đã kích hoạt thành công.
Success! Your billing info is updated.
Billing info update failed.
Your link has expired.