Go Phase 3: Concurrency, HTTP & Advanced Patterns

Chào mừng đến Phase 3 của Go Learning Roadmap — phase cuối cùng! Trong Phase 1 bạn đã học syntax của Go, và trong Phase 2 bạn đã nắm vững structs, interfaces, error handling, và packages. Giờ là lúc học thứ làm Go nổi tiếng: concurrency.
Concurrency model của Go là vũ khí lợi hại nhất — goroutines và channels cho phép bạn viết concurrent code mà vẫn đọc được. Ngoài concurrency, phase này còn cover context package, testing, HTTP, và build một REST API nhỏ.
Bạn Sẽ Học Được Gì
✅ Launch goroutines và hiểu Go scheduler hoạt động như thế nào
✅ Giao tiếp giữa các goroutines với channels (buffered + unbuffered)
✅ Multiplex channels bằng select
✅ Phối hợp goroutines với sync.WaitGroup, sync.Mutex, sync.Once
✅ Dùng context để cancel và timeout
✅ Viết tests với go test — table-driven, subtests, benchmarks
✅ Build HTTP servers và clients với net/http
✅ Encode/decode JSON
✅ Build một REST API đơn giản từ đầu
Part 1: Goroutines
Go Scheduler Hoạt Động Ra Sao
Goroutine là một lightweight thread do Go runtime quản lý — không phải OS thread. Bạn có thể chạy hàng nghìn goroutines trên chỉ một vài OS threads. Go scheduler (dạng M:N, work-stealing) ghép goroutines vào các CPU đang rảnh.
// Launch một goroutine bằng từ khóa `go`
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
fmt.Printf("Xin chào, %s!\n", name)
}
func main() {
go sayHello("Alice") // chạy đồng thời
go sayHello("Bob") // chạy đồng thời
go sayHello("Carol") // chạy đồng thời
// Không có cái này, main() có thể kết thúc trước khi goroutines chạy
time.Sleep(10 * time.Millisecond)
}Chỉ cần từ khóa go là xong. Nhưng time.Sleep là cách làm tạm bợ — trong code thực tế, bạn dùng channels hoặc sync.WaitGroup để phối hợp.
sync.WaitGroup — Chờ Tất Cả Goroutines
WaitGroup cho phép goroutine chính chờ một tập goroutines hoàn thành:
package main
import (
"fmt"
"sync"
)
func processItem(id int, wg *sync.WaitGroup) {
defer wg.Done() // báo hiệu xong khi hàm return
fmt.Printf("Đang xử lý item %d\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // đăng ký thêm một goroutine
go processItem(i, &wg) // launch goroutine
}
wg.Wait() // block cho đến khi tất cả gọi Done()
fmt.Println("Đã xử lý xong tất cả items")
}Quy tắc:
- Gọi
wg.Add(1)trước khi launch goroutine (không phải bên trong) - Dùng
defer wg.Done()làm dòng đầu tiên trong goroutine function - Truyền
*sync.WaitGroup(pointer), không bao giờ copy nó
Part 2: Channels
Channels là cách để các goroutines giao tiếp an toàn với nhau. Triết lý của Go: "Đừng chia sẻ memory để giao tiếp; hãy giao tiếp để chia sẻ memory."
Unbuffered Channels
Unbuffered channel sẽ block sender cho đến khi receiver sẵn sàng (và ngược lại):
package main
import "fmt"
func sum(nums []int, ch chan int) {
total := 0
for _, n := range nums {
total += n
}
ch <- total // gửi vào channel
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7, 8}
ch := make(chan int)
// Hai goroutines mỗi cái tính tổng nửa slice
go sum(nums[:4], ch)
go sum(nums[4:], ch)
// Nhận hai giá trị (block cho đến khi mỗi giá trị tới)
a, b := <-ch, <-ch
fmt.Println("Tổng:", a+b) // 36
}Buffered Channels
Buffered channel có capacity — sends chỉ block khi buffer đầy:
// Buffered channel với capacity 3
ch := make(chan string, 3)
ch <- "first" // không block
ch <- "second" // không block
ch <- "third" // không block
// ch <- "fourth" // sẽ block — buffer đầy rồi
fmt.Println(<-ch) // "first"
fmt.Println(<-ch) // "second"
fmt.Println(<-ch) // "third"Dùng buffered channels khi bạn biết số lượng items trước, hoặc khi muốn tách tốc độ producer khỏi consumer.
Range Over Channels
Bạn có thể range over một channel để nhận values cho đến khi nó bị đóng:
package main
import "fmt"
func producer(ch chan<- int) {
// chan<- nghĩa là send-only
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // báo hiệu: không có thêm values nữa
}
func main() {
ch := make(chan int)
go producer(ch)
for v := range ch { // nhận cho đến khi channel đóng
fmt.Println(v) // 0, 1, 2, 3, 4
}
}Quan trọng: Chỉ sender mới nên đóng channel. Đóng channel hai lần sẽ panic. Gửi vào channel đã đóng cũng panic.
Directional Channels
Channels có thể được type là send-only (chan<-) hoặc receive-only (<-chan):
// Tham số send-only — chỉ có thể gửi vào channel này
func producer(out chan<- string) {
out <- "hello"
out <- "world"
close(out)
}
// Tham số receive-only — chỉ có thể nhận từ channel này
func consumer(in <-chan string) {
for msg := range in {
fmt.Println(msg)
}
}
func main() {
ch := make(chan string, 2)
go producer(ch) // bidirectional ch tự động convert thành chan<-
consumer(ch) // bidirectional ch tự động convert thành <-chan
}Directional channels là một dạng documentation — compiler đảm bảo các functions tôn trọng vai trò của mình.
Part 3: Select Statement
select cho phép goroutine chờ nhiều channel operations cùng lúc — giống như switch nhưng dành cho channels:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "một"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "hai"
}()
// Chờ channel nào gửi trước thì nhận
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println("Nhận từ ch1:", msg)
case msg := <-ch2:
fmt.Println("Nhận từ ch2:", msg)
}
}
}Non-Blocking với Default
select {
case msg := <-ch:
fmt.Println("Nhận được:", msg)
default:
fmt.Println("Không có message — tiếp tục thôi")
}Timeout Pattern
select {
case result := <-ch:
fmt.Println("Kết quả:", result)
case <-time.After(2 * time.Second):
fmt.Println("Timeout — bỏ cuộc thôi")
}Done Channel Pattern (Tín Hiệu Dừng)
func worker(done <-chan struct{}) {
for {
select {
case <-done:
fmt.Println("Worker đang dừng")
return
default:
// làm việc...
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
done := make(chan struct{})
go worker(done)
time.Sleep(500 * time.Millisecond)
close(done) // báo hiệu tất cả workers dừng lại
}Part 4: sync Package
sync.Mutex — Mutual Exclusion
Khi các goroutines chia sẻ mutable state (không qua channels), dùng Mutex để tránh data race:
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
value int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
counter := &SafeCounter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println("Kết quả cuối:", counter.Value()) // luôn là 1000
}Chạy với go run -race để phát hiện data races trong quá trình development.
sync.RWMutex — Read-Write Lock
Khi có nhiều readers và ít writers, RWMutex cải thiện performance:
type Cache struct {
mu sync.RWMutex
store map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock() // nhiều reads đồng thời được phép
defer c.mu.RUnlock()
v, ok := c.store[key]
return v, ok
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // exclusive write lock
defer c.mu.Unlock()
c.store[key] = value
}sync.Once — Chạy Đúng Một Lần
sync.Once đảm bảo một function chỉ chạy đúng một lần, dù được gọi từ nhiều goroutines — hoàn hảo cho lazy initialization:
package main
import (
"fmt"
"sync"
)
type Database struct {
connection string
}
var (
db *Database
once sync.Once
)
func getDB() *Database {
once.Do(func() {
fmt.Println("Đang kết nối database...") // chỉ chạy đúng một lần
db = &Database{connection: "postgres://localhost/mydb"}
})
return db
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
d := getDB()
fmt.Println("Lấy được db:", d.connection)
}()
}
wg.Wait()
// "Đang kết nối database..." chỉ in một lần duy nhất
}Part 5: Context Package
Package context là cách Go truyền tín hiệu cancel, deadline, và request-scoped values qua call tree. Đây là thứ cần thiết cho HTTP handlers, database calls, và bất cứ thứ gì có timeout.
context.WithTimeout
package main
import (
"context"
"fmt"
"time"
)
func fetchData(ctx context.Context) (string, error) {
// Mô phỏng thao tác chậm
select {
case <-time.After(3 * time.Second): // mất 3 giây
return "data", nil
case <-ctx.Done(): // bị cancel hoặc timeout
return "", ctx.Err()
}
}
func main() {
// Cho operation 1 giây để hoàn thành
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() // luôn gọi cancel để giải phóng resources
result, err := fetchData(ctx)
if err != nil {
fmt.Println("Lỗi:", err) // context deadline exceeded
return
}
fmt.Println("Kết quả:", result)
}context.WithCancel
func main() {
ctx, cancel := context.WithCancel(context.Background())
// Launch workers biết lắng nghe context
for i := 0; i < 3; i++ {
go func(id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d đang dừng: %v\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d đang làm việc...\n", id)
time.Sleep(200 * time.Millisecond)
}
}
}(i)
}
time.Sleep(500 * time.Millisecond)
cancel() // cancel tất cả workers
time.Sleep(100 * time.Millisecond) // chờ goroutines kết thúc
}context.WithValue
Truyền request-scoped data (user ID, trace ID, auth token) mà không làm rối function signatures:
type contextKey string
const userIDKey contextKey = "userID"
func withUserID(ctx context.Context, userID string) context.Context {
return context.WithValue(ctx, userIDKey, userID)
}
func getUserID(ctx context.Context) (string, bool) {
userID, ok := ctx.Value(userIDKey).(string)
return userID, ok
}
// Dùng trong HTTP middleware
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("X-User-ID")
ctx := withUserID(r.Context(), userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}Quy tắc dùng context:
- Truyền
ctxlàm tham số đầu tiên cho mọi function thực hiện I/O - Không bao giờ lưu context trong struct — truyền tường minh
- Luôn
defer cancel()sauWithTimeouthoặcWithCancel - Dùng
context.Background()ở top level (main, tests) - Dùng
context.TODO()làm placeholder khi chưa chắc cần dùng context không
Part 6: Testing với go test
Go có testing package ngay trong standard library — không cần third-party.
Tests Cơ Bản
// calculator.go
package calculator
func Add(a, b int) int { return a + b }
func Subtract(a, b int) int { return a - b }
func Multiply(a, b int) int { return a * b }
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("chia cho 0")
}
return a / b, nil
}// calculator_test.go
package calculator
import (
"testing"
)
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; muốn 5", result)
}
}
func TestDivide(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Error("mong đợi lỗi khi chia cho 0, nhưng nhận được nil")
}
}Chạy với: go test ./...
Table-Driven Tests
Pattern test idiomatic nhất trong Go — test nhiều cases với một function:
func TestAdd_TableDriven(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"số dương", 2, 3, 5},
{"số âm", -2, -3, -5},
{"hỗn hợp", -2, 3, 1},
{"toàn số 0", 0, 0, 0},
{"số lớn", 1000000, 2000000, 3000000},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := Add(tc.a, tc.b)
if result != tc.expected {
t.Errorf("Add(%d, %d) = %d; muốn %d", tc.a, tc.b, result, tc.expected)
}
})
}
}Chạy một subtest cụ thể: go test -run TestAdd_TableDriven/số_âm
Benchmarks
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(100, 200)
}
}
func BenchmarkMultiply(b *testing.B) {
for i := 0; i < b.N; i++ {
Multiply(100, 200)
}
}Chạy với: go test -bench=. -benchmem
BenchmarkAdd-8 1000000000 0.23 ns/op 0 B/op 0 allocs/op
BenchmarkMultiply-8 1000000000 0.23 ns/op 0 B/op 0 allocs/opTesting với Interfaces (Mocking)
Go làm mocking dễ dàng thông qua interfaces — không cần reflection-based mocking framework:
// email.go
type EmailSender interface {
Send(to, subject, body string) error
}
type UserService struct {
email EmailSender
}
func (s *UserService) Register(name, emailAddr string) error {
// ... tạo user ...
return s.email.Send(emailAddr, "Chào mừng!", "Cảm ơn bạn đã đăng ký, "+name)
}// email_test.go
type MockEmailSender struct {
SentMessages []struct{ To, Subject, Body string }
ShouldFail bool
}
func (m *MockEmailSender) Send(to, subject, body string) error {
if m.ShouldFail {
return fmt.Errorf("dịch vụ email không khả dụng")
}
m.SentMessages = append(m.SentMessages, struct{ To, Subject, Body string }{to, subject, body})
return nil
}
func TestUserService_Register(t *testing.T) {
mock := &MockEmailSender{}
svc := &UserService{email: mock}
err := svc.Register("Alice", "[email protected]")
if err != nil {
t.Fatalf("lỗi không mong đợi: %v", err)
}
if len(mock.SentMessages) != 1 {
t.Errorf("mong đợi 1 email được gửi, nhận được %d", len(mock.SentMessages))
}
if mock.SentMessages[0].To != "[email protected]" {
t.Errorf("sai người nhận: %s", mock.SentMessages[0].To)
}
}Part 7: HTTP với net/http
Standard library net/http của Go đã sẵn sàng cho production — nhiều công ty chạy Go HTTP servers chỉ với thư viện này (không cần Express.js hay tương tự).
HTTP Server Đơn Giản
package main
import (
"fmt"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Xin chào, %s!", r.URL.Query().Get("name"))
}
func main() {
http.HandleFunc("/hello", helloHandler)
fmt.Println("Server khởi động trên :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}JSON Encoding và Decoding
import (
"encoding/json"
"net/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // bỏ qua nếu rỗng
}
// Encode struct thành JSON response
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// Decode JSON request body
func readJSON(r *http.Request, dst any) error {
return json.NewDecoder(r.Body).Decode(dst)
}Part 8: Build một REST API
Hãy tổng hợp tất cả lại — một REST API đơn giản quản lý users, chỉ dùng standard library:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
)
// Domain types
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// In-memory store (thread-safe)
type UserStore struct {
mu sync.RWMutex
users map[int]User
nextID int
}
func NewUserStore() *UserStore {
return &UserStore{users: make(map[int]User), nextID: 1}
}
func (s *UserStore) GetAll() []User {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]User, 0, len(s.users))
for _, u := range s.users {
result = append(result, u)
}
return result
}
func (s *UserStore) GetByID(id int) (User, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
u, ok := s.users[id]
return u, ok
}
func (s *UserStore) Create(name, email string) User {
s.mu.Lock()
defer s.mu.Unlock()
u := User{ID: s.nextID, Name: name, Email: email}
s.users[s.nextID] = u
s.nextID++
return u
}
func (s *UserStore) Delete(id int) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.users[id]; !ok {
return false
}
delete(s.users, id)
return true
}
// HTTP helpers
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{"error": message})
}
// Handler
type UserHandler struct {
store *UserStore
}
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Route: /users hoặc /users/{id}
path := strings.TrimPrefix(r.URL.Path, "/users")
path = strings.Trim(path, "/")
if path == "" {
// /users
switch r.Method {
case http.MethodGet:
writeJSON(w, http.StatusOK, h.store.GetAll())
case http.MethodPost:
var body struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "JSON không hợp lệ")
return
}
if body.Name == "" || body.Email == "" {
writeError(w, http.StatusBadRequest, "cần có name và email")
return
}
user := h.store.Create(body.Name, body.Email)
writeJSON(w, http.StatusCreated, user)
default:
writeError(w, http.StatusMethodNotAllowed, "method không được phép")
}
return
}
// /users/{id}
id, err := strconv.Atoi(path)
if err != nil {
writeError(w, http.StatusBadRequest, "user ID không hợp lệ")
return
}
switch r.Method {
case http.MethodGet:
user, ok := h.store.GetByID(id)
if !ok {
writeError(w, http.StatusNotFound, "không tìm thấy user")
return
}
writeJSON(w, http.StatusOK, user)
case http.MethodDelete:
if !h.store.Delete(id) {
writeError(w, http.StatusNotFound, "không tìm thấy user")
return
}
w.WriteHeader(http.StatusNoContent)
default:
writeError(w, http.StatusMethodNotAllowed, "method không được phép")
}
}
func main() {
store := NewUserStore()
handler := &UserHandler{store: store}
mux := http.NewServeMux()
mux.Handle("/users", handler)
mux.Handle("/users/", handler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
fmt.Println("🚀 Server chạy tại http://localhost:8080")
log.Fatal(server.ListenAndServe())
}Test thử:
# Tạo users
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "[email protected]"}'
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name": "Bob", "email": "[email protected]"}'
# Lấy tất cả
curl http://localhost:8080/users
# Lấy một user
curl http://localhost:8080/users/1
# Xóa
curl -X DELETE http://localhost:8080/users/1Part 9: Concurrency Patterns
Worker Pool
Pattern phổ biến: giới hạn concurrency với N workers xử lý một queue jobs:
package main
import (
"fmt"
"sync"
)
type Job struct {
ID int
Data string
}
type Result struct {
JobID int
Output string
}
func worker(id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
// Xử lý job
output := fmt.Sprintf("Worker %d xử lý job %d: %s", id, job.ID, job.Data)
results <- Result{JobID: job.ID, Output: output}
}
}
func main() {
const numWorkers = 3
const numJobs = 10
jobs := make(chan Job, numJobs)
results := make(chan Result, numJobs)
var wg sync.WaitGroup
// Khởi động worker pool
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// Gửi jobs
for j := 1; j <= numJobs; j++ {
jobs <- Job{ID: j, Data: fmt.Sprintf("data-%d", j)}
}
close(jobs) // hết jobs rồi
// Đóng results khi tất cả workers xong
go func() {
wg.Wait()
close(results)
}()
// Thu thập kết quả
for r := range results {
fmt.Println(r.Output)
}
}Fan-Out / Fan-In
// Fan-out: phân phối công việc cho nhiều goroutines
// Fan-in: gộp kết quả lại vào một channel
func fanOut(input <-chan int, numWorkers int) []<-chan int {
outputs := make([]<-chan int, numWorkers)
for i := 0; i < numWorkers; i++ {
out := make(chan int)
outputs[i] = out
go func(out chan<- int) {
for v := range input {
out <- v * v // bình phương mỗi số
}
close(out)
}(out)
}
return outputs
}
func fanIn(channels ...<-chan int) <-chan int {
merged := make(chan int)
var wg sync.WaitGroup
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c {
merged <- v
}
}(ch)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}Pipeline
Kết nối các stages lại, mỗi stage xử lý và chuyển tiếp values:
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func filter(in <-chan int, pred func(int) bool) <-chan int {
out := make(chan int)
go func() {
for n := range in {
if pred(n) {
out <- n
}
}
close(out)
}()
return out
}
func main() {
// Pipeline: generate → square → lọc số chẵn
nums := generate(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
squared := square(nums)
evens := filter(squared, func(n int) bool { return n%2 == 0 })
for n := range evens {
fmt.Println(n) // 4, 16, 36, 64, 100
}
}Những Cạm Bẫy Thường Gặp
1. Goroutine Leak — Quên Không Dừng Workers
// ❌ Goroutine chạy mãi mãi nếu không ai đọc từ ch
func leakyFunc() {
ch := make(chan int)
go func() {
for {
ch <- expensiveComputation() // block mãi nếu caller return
}
}()
// caller return mà không đọc ch — goroutine bị leak
}
// ✅ Dùng done/ctx channel làm tín hiệu dừng
func safeFunc(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for {
select {
case <-ctx.Done():
return
case ch <- expensiveComputation():
}
}
}()
return ch
}2. Đóng Channel Hai Lần (Panic)
// ❌ Đóng channel đã đóng sẽ panic
close(ch)
close(ch) // panic: close of closed channel
// ✅ Dùng sync.Once để đóng đúng một lần
var once sync.Once
safeClose := func() {
once.Do(func() { close(ch) })
}3. Capture Loop Variable Trong Goroutine
// ❌ Tất cả goroutines thấy cùng một biến (giá trị cuối cùng)
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // có thể in ra 3 3 3
}()
}
// ✅ Truyền làm argument — tạo một bản sao cho mỗi goroutine
for i := 0; i < 3; i++ {
go func(i int) {
fmt.Println(i) // in ra 0 1 2 (thứ tự bất kỳ)
}(i)
}4. Không Xử Lý Errors Từ Goroutines
// ❌ Errors bị mất im lìm
go func() {
if err := doWork(); err != nil {
// chẳng ai nghe cả!
}
}()
// ✅ Gửi errors qua error channel
errCh := make(chan error, 1)
go func() {
if err := doWork(); err != nil {
errCh <- err
}
close(errCh)
}()
if err := <-errCh; err != nil {
log.Fatal(err)
}5. Bỏ Qua Race Detector
Luôn chạy go test -race hoặc go run -race trong quá trình development — race detector tích hợp sẵn của Go phát hiện data races ngay lúc runtime.
Tổng Kết và Những Điều Cần Nhớ
Goroutines & Channels:
✅ go func() launch một goroutine — nhẹ, khởi tạo rẻ
✅ Channels là conduits có type — cách ưu tiên để goroutines chia sẻ dữ liệu
✅ Unbuffered channels đồng bộ; buffered channels tách biệt producer/consumer
✅ select chờ nhiều channels — cho phép timeout, cancellation, non-blocking ops
Sync Primitives:
✅ sync.WaitGroup — chờ một nhóm goroutines hoàn thành
✅ sync.Mutex / sync.RWMutex — bảo vệ shared state khỏi concurrent access
✅ sync.Once — chạy initialization đúng một lần, an toàn
Context:
✅ Truyền ctx context.Context làm tham số đầu tiên cho mọi function thực hiện I/O
✅ Dùng WithTimeout cho deadline, WithCancel để cancel thủ công
✅ Luôn defer cancel() — để leak context gây resource leak
Testing:
✅ Table-driven tests là idiomatic Go — quản lý nhiều cases gọn gàng
✅ Dùng interfaces để mock dependencies — không cần reflection framework
✅ go test -race phát hiện race conditions sớm
HTTP & REST:
✅ net/http đã sẵn sàng cho production — không cần framework với hầu hết use cases
✅ encoding/json với struct tags cho JSON APIs sạch đẹp
✅ Dùng http.ServeMux để routing, custom http.Handler cho code có cấu trúc
Bước Tiếp Theo
Bạn đã hoàn thành Go Learning Roadmap! Đây là hướng để đi sâu hơn:
- Deep Dive: Concurrency với Goroutines & Channels — patterns, pitfalls, và advanced concurrency
- Deep Dive: Error Handling Patterns — wrapping, sentinel errors, custom error types
- Deep Dive: Testing trong Go — testify, mocks, integration tests, benchmarks
- Deep Dive: Context, HTTP & Building APIs — middleware, routing libraries, full API design
Các bài liên quan:
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.