KHOÁ HỌC Phần 1 — Nền tảng Timeout & xử lý lỗi — context, select

Timeout & xử lý lỗi — context, select

🧠 Suy nghĩ

"Nếu DB-Server treo mãi thì sao? Không lẽ chờ vô hạn? Phải có hạn chót."

Trong ví dụ, DB-Server cố tình mất 3 giây, nhưng timeout là 2 giây → DB sẽ bị huỷ và gửi cảnh báo.

📐 Logic

context = "tín hiệu huỷ" — truyền xuyên suốt các goroutine để nói "hết giờ rồi, dừng lại!".

select = "chờ nhiều sự kiện cùng lúc" — cái nào xảy ra trước thì làm cái đó:

  • Sự kiện A: Server phản hồi kịp
  • Sự kiện B: Hết thời gian (context bị huỷ)

💻 Code

Trong main():

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // Giải phóng tài nguyên context khi main thoát

Sửa hàm checkServerHealth:

func checkServerHealth(ctx context.Context, wg *sync.WaitGroup, s *Server,
                      results chan<- string, notifier Notifier) {
    defer wg.Done()

    // Giả lập độ trễ khác nhau
    var processingTime time.Duration
    if s.Name == "DB-Server" {
        processingTime = 3 * time.Second // cố tình chậm
    } else {
        processingTime = 1 * time.Second
    }

    select {
    case <-time.After(processingTime):
        // Trường hợp 1: kịp thời gian
        s.SetStatus(true)
        results <- fmt.Sprintf("✅ %s (%s) ĐANG HOẠT ĐỘNG.", s.Name, s.IP)

    case <-ctx.Done():
        // Trường hợp 2: hết giờ
        s.SetStatus(false)

        // Bọc lỗi: giữ nguyên lỗi gốc + thêm thông tin
        err := fmt.Errorf("không thể kết nối %s: %w", s.Name, ctx.Err())

        results <- fmt.Sprintf("❌ LỖI: %s (%s) bị treo!", s.Name, s.IP)
        notifier.SendAlert(err.Error()) // Đa hình: gọi qua interface
    }
}

🔍 Giải thích sâu

context.WithTimeout(...) trả về 2 thứ

  • ctx — cái để truyền đi cho các goroutine
  • cancel — hàm để huỷ chủ động (bắt buộc defer cancel() để tránh rò rỉ)

<-time.After(3s) là gì?

time.After(3s) trả về một channel, sau 3 giây sẽ có tín hiệu. <- = "đọc từ channel này". Nghĩa là "chờ 3 giây".

<-ctx.Done() là gì?

ctx.Done() cũng là channel. Khi timeout hết → channel đóng → <-ctx.Done() chạy ngay.

select { case A: ... case B: ... }

Chờ cả A và B cùng lúc. Cái nào xảy ra trước thì chạy case đó. Cái còn lại bị bỏ qua.

Ví dụ với DB-Server:

  • Case A: chờ 3 giây
  • Case B: chờ 2 giây (timeout)
  • → Case B xảy ra trước → chạy nhánh lỗi ✅

fmt.Errorf("... %w", err) — Wrap error

%w = "gói lỗi gốc bên trong". Sau này có thể dùng errors.Is() để kiểm tra lỗi gốc mà không mất thông tin.

notifier.SendAlert(...) — Đa hình (polymorphism)

Ở đây notifier có kiểu interface Notifier. Go tự động gọi đúng phương thức của TelegramBot. Nếu mai truyền SlackBot vào → tự gọi Slack. Đây là sức mạnh của interface.

← Chạy song song — goroutine, WaitGroup, channel