Thực Hành Lập Trình Go (Phần 2) - nói dối e blog

Thực Hành Lập Trình Go (Phần 2)

Tiếp nối từ hôm qua, chúng ta sẽ cùng xây dựng một cấu trúc map đặc biệt hỗ trợ hai thao tác pushpop với giao diện như sau:

1
2
3
4
type myMap interface {
  push(key string, e interface{}) interface{} 
  pop(key string) interface{}
}

Yêu cầu chi tiết

  • Khi thực hiện push:
    • Nếu key đã tồn tại trong map → trả về giá trị cũ
    • Nếu key chưa tồn tại → thêm cặp key-value mới và trả về nil
  • Khi thực hiện pop:
    • Trả về giá trị tương ứng với key
    • Xóa cặp key-value khỏi map sau khi trả về

Phiên bản sử dụng Mutex (sync.Mutex)

Đây là cách tiếp cận truyền thống nhưng hiệu quả để đảm bảo an toàn luồng (thread-safe) trong môi trường đa luồng:

 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
package main

import (
	"fmt"
	"sync"
)

type myMap struct {
	m map[string]interface{}
	sync.Mutex
}

func (m *myMap) push(key string, e interface{}) interface{} {
	m.Lock()
	defer m.Unlock()

	if val, exists := m.m[key]; exists {
		return val
	}
	
	m.m[key] = e
	return nil
}

func (m *myMap) pop(key string) interface{} {
	m.Lock()
	defer m.Unlock()

	if val, exists := m.m[key]; exists {
		delete(m.m, key)
		return val
	}
	
	return nil
}

func newMap() *myMap {
	return &myMap{
		m: make(map[string]interface{}),
	}
}

func main() {
	m := newMap()
	fmt.Println("Thêm lần 1:", m.push("xin_chào", "thế_giới")) // nil
	fmt.Println("Thêm lần 2:", m.push("xin_chào", "thế_giới")) // thế_giới
	fmt.Println("Lấy ra lần 1:", m.pop("xin_chào"))            // thế_giới
	fmt.Println("Lấy ra lần 2:", m.pop("xin_chào"))            // nil
}

💡 Giải thích quan trọng:
Chúng ta phải sử dụng con trỏ (*myMap) trong các phương thức vì cấu trúc sync.Mutex không thể sao chép. Go không hỗ trợ các kỹ thuật như copy constructor hay overload toán tử gán như C++, do đó sử dụng con trỏ là giải pháp tối ưu nhờ vào cơ chế garbage collection tự động.

Phiên bản sử dụng Channel (Goroutine-based)

Một cách tiếp cận hiện đại hơn là tận dụng mô hình CSP (Communicating Sequential Processes) của Go thông qua channel:

 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
package main

import "fmt"

type myMap interface {
	push(key string, e interface{}) interface{} 
	pop(key string) interface{}
}

type mapRequest struct {
	key   string
	value interface{}
}

type mapService struct {
	pushReq chan *mapRequest
	pushRes chan interface{}
	popReq  chan string
	popRes  chan interface{}
}

func (s *mapService) push(key string, e interface{}) interface{} {
	s.pushReq <- &mapRequest{key: key, value: e}
	return <-s.pushRes
}

func (s *mapService) pop(key string) interface{} {
	s.popReq <- key
	return <-s.popRes
}

func newMap() myMap {
	service := &mapService{
		pushReq: make(chan *mapRequest),
		pushRes: make(chan interface{}),
		popReq:  make(chan string),
		popRes:  make(chan interface{}),
	}

	go func() {
		internalMap := make(map[string]interface{})
		for {
			select {
			case req := <-service.pushReq:
				if val, exists := internalMap[req.key]; exists {
					service.pushRes <- val
				} else {
					internalMap[req.key] = req.value
					service.pushRes <- nil
				}
			case key := <-service.popReq:
				if val, exists := internalMap[key]; exists {
					delete(internalMap, key)
					service.popRes <- val
				} else {
					service.popRes <- nil
				}
			}
		}
	}()

	return service
}

func main() {
	m := newMap()
	fmt.Println("Thêm lần 1:", m.push("xin_chào", "thế_giới")) // nil
	fmt.Println("Thêm lần 2:", m.push("xin_chào", "thế_giới")) // thế_giới
	fmt.Println("Lấy ra lần 1:", m.pop("xin_chào"))            // thế_giới
	fmt.Println("Lấy ra lần 2:", m.pop("xin_chào"))            // nil
}

So sánh hai cách tiếp cận

Tiêu chí Dùng Mutex Dùng Channel
Độ phức tạp Đơn giản, dễ hiểu Phức tạp hơn, cần tư duy bất đồng bộ
Hiệu năng Tốt cho đa số trường hợp Có thể tối ưu hơn với I/O密集型
Khả năng mở rộng Hạn chế khi logic phức tạp Dễ mở rộng với nhiều loại request
Phong cách lập trình Truyền thống, mệnh lệnh Hiện đại, hướng sự kiện

💡 Mẹo chuyên nghiệp:
Mô hình channel rất mạnh mẽ nhưng dễ dẫn đến deadlock nếu không xử lý cẩn thận. Khi xây dựng service dựa trên channel, hãy luôn đảm bảo có cơ chế timeout hoặc context cancellation để tránh treo chương trình.

Kết luận

Go cung cấp cho chúng ta hai lựa chọn thú vị:

  1. Cách tiếp cận truyền thống với sync.Mutex – phù hợp cho các ứng dụng đơn giản, dễ bảo trì
  2. Cách tiếp cận hiện đại với goroutine + channel – mở ra tiềm năng xây dựng các hệ thống phản ứng (reactive systems)

Tùy theo ngữ cảnh sử dụng, bạn có thể chọn giải pháp phù hợp. Trong thực tế, nhiều dự án kết hợp cả hai cách để tối ưu hiệu năng và khả năng bảo trì.

0%