👨🍳 Nhiều Đầu Bếp Song Song - Goroutines
Chào mừng đến với đặc sản của Bếp Á! Hôm nay chúng ta học cách vận hành một bếp với nhiều đầu bếp làm việc song song - chính là Goroutines!
🎯 Món Ăn Hôm Nay
Tưởng tượng một nhà hàng phở đông khách vào giờ cao điểm:
- Bàn 1: Gọi phở bò
- Bàn 2: Gọi bún chả
- Bàn 3: Gọi gỏi cuốn
- Bàn 4: Gọi phở gà
- Bàn 5: Gọi nem rán
Nếu chỉ có 1 đầu bếp, khách sẽ chờ rất lâu! Nhưng với Goroutines (nhiều đầu bếp), mọi món được nấu song song!
🥘 Goroutine Là Gì?
Goroutine = Một đầu bếp độc lập trong bếp
Đặc điểm:
- 🏃 Nhanh: Tạo goroutine nhanh như gọi một đầu bếp vào bếp
- 💪 Nhẹ: Mỗi goroutine chỉ tốn ~2KB memory (so với thread ~1MB)
- 🔄 Song song: Hàng ngàn goroutines có thể chạy cùng lúc
- 🎯 Tự động: Go runtime tự động điều phối
🍜 Ẩn Dụ Nhà Hàng:
- 1 goroutine = 1 đầu bếp
- Go runtime = Bếp trưởng điều phối
- Main goroutine = Đầu bếp chính
👨🍳 Câu Chuyện Trong Bếp
Sáng nay nhà hàng nhận được 5 đơn hàng cùng lúc:
Cách 1: Một đầu bếp (Không dùng Goroutine)
func main() {
    nauPho()      // 10 phút
    nauBunCha()   // 8 phút
    gopGoiCuon()  // 5 phút
    // Tổng: 23 phút - Khách chờ lâu quá!
}
Cách 2: Nhiều đầu bếp (Dùng Goroutine)
func main() {
    go nauPho()      // Đầu bếp A nấu phở
    go nauBunCha()   // Đầu bếp B nấu bún chả
    go gopGoiCuon()  // Đầu bếp C gọp gỏi cuốn
    time.Sleep(12 * time.Second) // Chờ món lâu nhất
    // Tổng: 12 phút - Nhanh hơn gần gấp đôi!
}
🎉 Kết quả: Từ khóa
go= Gọi thêm một đầu bếp vào bếp!
📝 Công Thức Nấu (Code Examples)
Ví Dụ 1: Gọi Một Đầu Bếp
package main
import (
    "fmt"
    "time"
)
func main() {
    fmt.Println("🏪 Nhà hàng mở cửa!")
    // Gọi đầu bếp phụ nấu phở
    go nauPho()
    // Đầu bếp chính làm việc khác
    fmt.Println("👨🍳 Đầu bếp chính chuẩn bị gia vị...")
    time.Sleep(3 * time.Second) // Đợi món phở xong
    fmt.Println("✅ Ca làm việc kết thúc!")
}
func nauPho() {
    fmt.Println("🍜 Bắt đầu nấu phở...")
    time.Sleep(2 * time.Second)
    fmt.Println("🍜 Phở đã sẵn sàng!")
}
Ví Dụ 2: Nhiều Đầu Bếp Nấu Nhiều Món
func main() {
    fmt.Println("🏪 Giờ cao điểm - 5 bàn gọi món!")
    // Gọi 5 đầu bếp, mỗi người nấu một món
    for soBan := 1; soBan <= 5; soBan++ {
        go nauMonChoBan(soBan)
    }
    time.Sleep(5 * time.Second)
    fmt.Println("✅ Tất cả bàn đã được phục vụ!")
}
func nauMonChoBan(soBan int) {
    fmt.Printf("👨🍳 Đầu bếp %d: Bắt đầu nấu cho bàn %d\n", soBan, soBan)
    time.Sleep(2 * time.Second)
    fmt.Printf("✅ Bàn %d: Món ăn sẵn sàng!\n", soBan)
}
Ví Dụ 3: Goroutine Với Hàm Ẩn Danh
func main() {
    tenMon := "Phở Bò Đặc Biệt"
    // Tạo đầu bếp tạm thời (anonymous function)
    go func(mon string) {
        fmt.Printf("👨🍳 Đang nấu %s...\n", mon)
        time.Sleep(2 * time.Second)
        fmt.Printf("✅ %s đã xong!\n", mon)
    }(tenMon)
    time.Sleep(3 * time.Second)
}
🔍 Giải Thích:
go nauPho()= Gọi đầu bếp phụ nấu phở
go func() {...}()= Thuê đầu bếp tạm thời nấu món cụ thể
time.Sleep()= Chờ đầu bếp hoàn thành (tạm thời, chưa tối ưu)
🔄 WaitGroup - Điểm Danh Đầu Bếp
Vấn đề với time.Sleep():
- Không biết đầu bếp nào xong, đầu bếp nào chưa
- Chờ ước lượng → lãng phí thời gian hoặc thiếu thời gian
Giải pháp: WaitGroup = Bảng điểm danh
Ví Dụ 1: Điểm Danh Cơ Bản
package main
import (
    "fmt"
    "sync"
    "time"
)
func main() {
    var wg sync.WaitGroup // Bảng điểm danh
    soMonCanNau := 5
    fmt.Println("📋 Điểm danh: Cần " + fmt.Sprint(soMonCanNau) + " đầu bếp")
    for i := 1; i <= soMonCanNau; i++ {
        wg.Add(1) // Ghi tên đầu bếp vào bảng
        go func(id int) {
            defer wg.Done() // Đánh dấu hoàn thành khi xong
            fmt.Printf("👨🍳 Đầu bếp %d: Bắt đầu nấu\n", id)
            time.Sleep(2 * time.Second)
            fmt.Printf("✅ Đầu bếp %d: Hoàn thành!\n", id)
        }(i)
    }
    wg.Wait() // Chờ TẤT CẢ đầu bếp hoàn thành
    fmt.Println("🎉 Tất cả đầu bếp đã xong việc!")
}
🔍 Giải Thích:
wg.Add(1): Thêm tên đầu bếp vào bảng điểm danh
defer wg.Done(): Đánh dấu "đã xong" khi món nấu xong
wg.Wait(): Chờ đến khi TẤT CẢ đều đánh dấu xong
Ví Dụ 2: Nấu Menu Phức Tạp
func nấuMenu(danhSachMon []string) {
    var wg sync.WaitGroup
    for _, tenMon := range danhSachMon {
        wg.Add(1)
        go func(mon string) {
            defer wg.Done()
            nauMon(mon)
        }(tenMon)
    }
    wg.Wait()
    fmt.Println("🍽️ Tất cả món đã sẵn sàng phục vụ!")
}
func nauMon(ten string) {
    fmt.Printf("👨🍳 Đang nấu %s...\n", ten)
    time.Sleep(2 * time.Second)
    fmt.Printf("✅ %s đã xong!\n", ten)
}
🔒 Mutex - Khóa Kho Nguyên Liệu
Vấn đề: Nhiều đầu bếp lấy nguyên liệu từ cùng một kho → Xung đột!
Ví dụ: Đếm số món đã nấu
// ❌ SAI: Nhiều đầu bếp cùng ghi vào sổ → Số liệu sai!
var soMonDaNau int
func nauMon() {
    soMonDaNau++ // ⚠️ Nguy hiểm! Race condition!
}
Giải pháp: Mutex = Khóa kho, chỉ 1 người vào một lúc
Ví Dụ: Đếm Số Món An Toàn
type QuayPPhucVu struct {
    mu          sync.Mutex
    soMonDaNau  int
}
func (q *QuayPhucVu) NauXong() {
    q.mu.Lock()         // Khóa cửa kho
    defer q.mu.Unlock() // Mở khóa khi xong
    q.soMonDaNau++
    fmt.Printf("✅ Đã nấu %d món\n", q.soMonDaNau)
}
func main() {
    quay := &QuayPhucVu{}
    var wg sync.WaitGroup
    // 10 đầu bếp nấu song song
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            quay.NauXong() // An toàn với Mutex
        }()
    }
    wg.Wait()
    fmt.Printf("🎉 Tổng cộng: %d món\n", quay.soMonDaNau)
}
🔍 Giải Thích:
mu.Lock(): Khóa cửa kho - chỉ 1 đầu bếp vào
defer mu.Unlock(): Đảm bảo mở khóa khi ra- Ngăn nhiều đầu bếp ghi cùng lúc → Dữ liệu chính xác
⚠️ Những Lỗi Đầu Bếp Thường Gặp
Lỗi 1: Quên Chờ Đầu Bếp
// ❌ SAI: main() kết thúc trước khi goroutine hoàn thành
func main() {
    go nauPho()
    // Chương trình kết thúc ngay → Đầu bếp chưa kịp nấu!
}
// ✅ ĐÚNG: Dùng WaitGroup
func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer wg.Done()
        nauPho()
    }()
    wg.Wait() // Chờ đầu bếp xong việc
}
Lỗi 2: Race Condition (Xung Đột Dữ Liệu)
// ❌ SAI: Nhiều goroutine ghi cùng lúc
var soMon int
go func() { soMon++ }()
go func() { soMon++ }()
// ✅ ĐÚNG: Dùng Mutex
var mu sync.Mutex
go func() {
    mu.Lock()
    soMon++
    mu.Unlock()
}()
Lỗi 3: Tạo Quá Nhiều Goroutines
// ❌ SAI: Tạo 1 triệu đầu bếp!
for i := 0; i < 1000000; i++ {
    go nauMon()
}
// ✅ ĐÚNG: Giới hạn số lượng
const maxDauBep = 10
sem := make(chan struct{}, maxDauBep)
for i := 0; i < 1000000; i++ {
    sem <- struct{}{} // Chờ có chỗ trống
    go func() {
        nauMon()
        <-sem // Giải phóng chỗ
    }()
}
💡 Bí Quyết Của Đầu Bếp
- Luôn dùng WaitGroup hoặc Channels để đồng bộ
- Dùng Mutex khi nhiều goroutines truy cập dữ liệu chung
- Giới hạn số lượng goroutines - đừng tạo quá nhiều!
- defer Unlock() để đảm bảo luôn mở khóa Mutex
🎓 Bạn Đã Học Được
- ✅ Goroutine = Đầu bếp song song, làm việc độc lập
- ✅ Từ khóa gođể tạo goroutine
- ✅ WaitGroup để điểm danh và chờ đầu bếp
- ✅ Mutex để bảo vệ dữ liệu chung (kho nguyên liệu)
- ✅ Tránh race conditions và lỗi thường gặp
🍜 Món Tiếp Theo
Goroutines giúp nhiều đầu bếp làm việc song song, nhưng họ cần giao tiếp!
👉 Băng Chuyền Món Ăn - Channels - Học cách đầu bếp truyền món cho nhau!
💡 Lời Khuyên Cuối: Goroutines là đặc sản của Go - sức mạnh thật sự của Bếp Á! Hãy thực hành nhiều để thành thạo cách điều phối "đội bếp" của bạn! 👨🍳👨🍳👨🍳