Vào ngày 04 tháng 9 năm 2021, Trần Hạo đã chia sẻ bài viết với 15 bình luận và hơn 78.655 lượt đọc.
- Go Programming Patterns: Slices, Interfaces, Thời gian và Hiệu suất
- Go Programming Patterns: Xử lý lỗi
- Go Programming Patterns: Functional Options
- Go Programming Patterns: Delegation và Inversion of Control
- Go Programming Patterns: Map-Reduce
- Go Programming Patterns: Go Generation
- Go Programming Patterns: Decorators
- Go Programming Patterns: Pipelines
- Go Programming Patterns: k8s Visitor Pattern
- Go Programming Patterns: Generic Programming
Giới thiệu sơ lược
Chúng ta hãy bắt đầu bằng một ví dụ đơn giản:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package main
import "fmt"
func print[T any](arr []T) {
for _, v := range arr {
fmt.Print(v)
fmt.Print(" ")
}
fmt.Println("")
}
func main() {
strs := []string{"Xin chào", "Thế giới", "Generics"}
decs := []float64{3.14, 1.14, 1.618, 2.718}
nums := []int{2, 4, 6, 8}
print(strs)
print(decs)
print(nums)
}
|
Trong ví dụ trên, hàm print()
được thiết kế để in giá trị của mảng mà không cần phải viết riêng cho từng kiểu dữ liệu như int
, float
hay string
. Với hỗ trợ của generics, chúng ta chỉ cần khai báo [T any]
để định nghĩa kiểu generic, và sử dụng T
để tuyên bố biến.
Để chạy đoạn chương trình này, bạn cần thêm tham số -gcflags=-G=3
khi biên dịch (tham số này sẽ trở thành mặc định từ phiên bản 1.18):
1
|
$ go run -gcflags=-G=3 ./main.go
|
Tiếp theo, chúng ta có thể áp dụng generic để viết các thuật toán chuẩn, chẳng hạn như hàm tìm kiếm:
1
2
3
4
5
6
7
8
|
func find[T comparable](arr []T, elem T) int {
for i, v := range arr {
if v == elem {
return i
}
}
return -1
}
|
Ở đây, chúng ta sử dụng [T comparable]
thay vì [T any]
. Kiểu comparable
yêu cầu rằng loại dữ liệu phải hỗ trợ phép so sánh (==
). Hàm find()
này có thể hoạt động tốt với các kiểu dữ liệu như int
, float64
hoặc string
.
Mặc dù Go đã cung cấp hỗ trợ cơ bản cho generics, vẫn còn một số vấn đề:
- Định dạng
%v
trong fmt.Printf()
chưa đủ linh hoạt để tùy chỉnh đầu ra giống như cách C++ sử dụng iostream
.
- Go không hỗ trợ overloading operator, điều này khiến việc sử dụng các toán tử generic như
==
trở nên khó khăn.
- Thuật toán
find()
hiện tại chỉ hoạt động với mảng. Để mở rộng cho các cấu trúc dữ liệu khác như hash-table, tree, graph hoặc linked list, cần phải viết lại code tương ứng.
Dẫu vậy, tính năng này đã mang lại nhiều tiềm năng. Hãy cùng xem chúng ta có thể làm gì với nó.
Cấu trúc dữ liệu
Stack (Ngăn xếp)
Một trong những lợi ích lớn nhất của generics chính là khả năng tạo ra các cấu trúc dữ liệu độc lập về kiểu dữ liệu. Chúng ta có thể dùng slices để triển khai stack như sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
type stack[T any] []T
func (s *stack[T]) push(elem T) {
*s = [thiên hạ bet j77](/uo/21708.html) append(*s, elem)
}
func (s *stack[T]) pop() {
if len(*s) > 0 {
*s = (*s)[:len(*s)-1]
}
}
func (s *stack[T]) top() [shbet love](/uo/22422.html) *T {
if len(*s) > 0 {
return &(*s)[len(*s)-1]
}
return nil
}
func (s *stack[T]) len() int { [game nổ hũ đăng ký tặng code](/uo/22398.html)
return len(*s)
}
func (s *stack[T]) print() {
for _, elem := range *s {
fmt.Print(elem)
fmt.Print(" ")
}
fmt.Println("")
}
|
Ví dụ về cách sử dụng stack:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func main() {
ss := stack[string]{}
ss.push("Xin chào")
ss.push("Hao")
ss.push("Chen")
ss.print()
fmt.Printf("Đỉnh stack là - %v\n", *(ss.top()))
ss.pop()
ss.pop()
ss.print()
ns := stack[int]{}
ns.push(10)
ns.push(20)
ns.print()
ns.pop()
ns.print()
*ns.top() += 1
ns.print()
ns.pop()
fmt.Printf("Đỉnh stack là - %v\n", ns.top())
}
|
Doubly Linked List (Danh sách liên kết hai chiều)
Chúng ta tiếp tục với một ví dụ về danh sách liên kết hai chiều. Các phương thức chính bao gồm:
add()
– Thêm nút từ đầu.
push()
– Thêm nút từ cuối.
del()
– Xóa nút (yêu cầu so sánh, do đó sử dụng comparable
).
print()
– In giá trị các phần tử từ đầu đến cuối.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
type node[T comparable] struct {
data T
prev *node[T]
next *node[T]
}
type list[T comparable] struct {
head, tail *node[T]
len int
}
func (l *list[T]) isEmpty() bool {
return l.head == nil && l.tail == nil
}
func (l *list[T]) add(data T) {
n := &node[T]{
data: data,
prev: nil,
next: l.head,
}
if l.isEmpty() {
l.head = n
l.tail = n
} else {
l.head.prev = n
l.head = n
}
}
func (l *list[T]) push(data T) {
n := &node[T]{
data: data,
prev: l.tail,
next: nil,
}
if l.isEmpty() {
l.head = n
l.tail = n
} else {
l.tail.next = n
l.tail = n
}
}
func (l *list[T]) del(data T) {
for p := l.head; p != nil; p = p.next {
if data == p.data {
if p == l.head {
l.head = p.next
}
if p == l.tail {
l.tail = p.prev
}
if p.prev != nil {
p.prev.next = p.next
}
if p.next != nil {
p.next.prev = p.prev
}
return
}
}
}
func (l *list[T]) print() {
if l.isEmpty() {
fmt.Println("Danh sách liên kết trống.")
return
}
for p := l.head; p != nil; p = p.next {
fmt.Printf("[%v] -> ", p.data)
}
fmt.Println("nil")
}
|
Ví dụ sử dụng:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func main() {
var l = list[int]{}
l.add(1)
l.add(2)
l.push(3)
l.push(4)
l.add(5)
l.print() //[5] -> [2] -> [1] -> [3] -> [4] -> nil
l.del(5)
l.del(1)
l.del(4)
l.print() //[2] -> [3] -> nil
}
|
Lập trình hàm kiểu generic
Generic Map
1
2
3
4
5
6
7
|
func gMap[T1 any, T2 any](arr []T1, f func(T1) T2) []T2 {
result := make([]T2, len(arr))
for i, elem := range arr {
result[i] = f(elem)
}
return result
}
|
Ví dụ sử dụng:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
nums := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
squares := gMap(nums, func(elem int) int {
return elem * elem
})
print(squares) //0 1 4 9 16 25 36 49 64 81
strs := []string{"Hao", "Chen", "MegaEase"}
upstrs := gMap(strs, func(s string) string {
return strings.ToUpper(s)
})
print(upstrs) //HAO CHEN MEGAEASE
dict := []string{"Không", "Một", "Hai", "Ba", "Bốn", "Năm", "Sáu", "Bảy", "Tám", "Chín"}
strs = gMap(nums, func(elem int) string {
return dict[elem]
})
print(strs) //Không Một Hai Ba Bốn Năm Sáu Bảy Tám Chín
|
Generic Reduce
1
2
3
4
5
6
7
|
func gReduce[T1 any, T2 any](arr []T1, init T2, f func(T2, T1) T2) T2 {
result := init
for _, elem := range arr {
result = f(result, elem)
}
return result
}
|
Ví dụ sử dụng:
1
2
3
4
5
|
nums := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
sum := gReduce(nums, 0, func(result, elem int) int {
return result + elem
})
fmt.Printf("Tổng = %d \n", sum)
|
Generic Filter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func gFilter[T any](arr []T, in bool, f func(T) bool) []T {
result := []T{}
for _, elem := range arr {
choose := f(elem)
if (in && choose) || (!in && !choose) {
result = append(result, elem)
}
}
return result
}
func gFilterIn[T any](arr []T, f func(T) bool) []T {
return gFilter(arr, true, f)
}
func gFilterOut[T any](arr []T, f func(T) bool) []T {
return gFilter(arr, false, f)
}
|
Ví dụ sử dụng:
1
2
3
4
5
|
nums := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
odds := gFilterIn(nums, func(elem int) bool {
return elem%2 == 1
})
print(odds)
|
Ví dụ thực tế
Giả sử chúng ta có một danh sách nhân viên:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
type Employee struct {
Name string
Age int
Vacation int
Salary float32
}
var employees = []Employee{
{"Hao", 44, 0, 8000.5},
{"Bob", 34, 10, 5000.5},
{"Alice", 23, 5, 9000.0},
{"Jack", 26, 0, 4000.0},
{"Tom", 48, 9, 7500.75},
{"Marry", 29, 0, 6000.0},
{"Mike", 32, 8, 4000.3},
}
|
Chúng ta có thể sử dụng hàm reduce để tính tổng lương:
1
2
3
4
|
total_pay := gReduce(employees, 0.0, func(result float32, e Employee) float32 {
return result + e.Salary
})
fmt.Printf("Tổng lương: %0.2f\n", total_pay) // Tổng lương: 43502.05
|
Các ví dụ khác:
- Đếm số nhân viên trên 40 tuổi:
1
2
3
4
|
old := gCountIf(employees, func(e Employee) bool {
return e.Age > 40
})
fmt.Printf("Nhân viên trên 40 tuổi: %d\n", old) // Nhân viên trên 40 tuổi: 2
|
- Đếm số nhân viên có lương trên 6000:
1
2
3
4
|
high_pay := gCountIf(employees, func(e Employee) bool {
return e.Salary >= 6000
})
fmt.Printf("Nhân viên lương cao (>6k): %d\n", high_pay) // Nhân viên lương cao (>6k): 4
|
- Tính tổng lương của nhân viên dưới 30 tuổi:
1
2
3
4
5
6
7
|
younger_pay := gSum(employees, func(e Employee) float32 {
if e.Age < 30 {
return e.Salary
}
return 0
})
fmt.Printf("Tổng lương người trẻ: %0.2f\n", younger_pay) // Tổng lương người trẻ: 19000.00
|
- Tính tổng số ngày nghỉ của tất cả nhân viên:
1
2
3
4
|
total_vacation := gSum(employees, func(e Employee) int {
return e.Vacation
})
fmt.Printf("Tổng ngày nghỉ: %d ngày\n", total_vacation) // Tổng ngày nghỉ: 32 ngày
|
- Lọc ra danh sách nhân viên không có ngày nghỉ:
1
2
3
4
|
no_vacation := gFilterIn(employees, func(e Employee) bool {
return e.Vacation == 0
})
print(no_vacation) // {Hao 44 0 8000.5} {Jack 26 0 4000} {Marry 29 0 6000}
|
Bạn đã hiểu được ý nghĩa của lập trình generic chưa?