Tạo một kết nối — tới database, một service qua TLS, một broker — là thao tác tốn kém: bắt tay TCP, thương lượng TLS, xác thực, tất cả mất vài mili giây. Nếu mỗi request mở một kết nối mới rồi đóng, chi phí đó nhân lên khủng khiếp. Connection pool giải bài này: giữ sẵn một số kết nối và tái dùng chúng. Trong Go, mẫu này đặc biệt đẹp vì buffered channel vừa làm kho chứa vừa lo đồng bộ mà không cần mutex. Bài này dựng một pool từ đầu chạy được, đo thật lợi ích, và chỉ ra các cạm bẫy.
Pool là một buffered channel
Ý tưởng cốt lõi: một buffered channel chứa các kết nối đang rảnh. Lấy kết nối là nhận từ channel; trả về là gửi vào channel. Channel tự lo phần đồng bộ giữa các goroutine:
type Pool struct {
conns chan *Conn // kênh giữ các kết nối rảnh
maxOpen int
}
func NewPool(max int) *Pool {
return &Pool{conns: make(chan *Conn, max), maxOpen: max}
}
Sức chứa của channel (max) chính là giới hạn số kết nối — không thể có nhiều hơn max kết nối trong pool. Đây cũng là cách bảo vệ server phía sau khỏi bị quá tải kết nối.
Get và Put: lấy ra, trả về, chờ có hạn
Get lấy một kết nối rảnh từ channel; nếu cạn, nó chờ nhưng có timeout để không kẹt vô hạn:
func (p *Pool) Get(timeout time.Duration) (*Conn, error) {
select {
case c := <-p.conns: // có kết nối rảnh -> tái dùng ngay
return c, nil
case <-time.After(timeout): // cạn -> chờ, hết giờ thì báo lỗi
return nil, ErrHetHan
}
}
Put trả kết nối về pool; nếu pool đầy (hiếm, khi trả nhiều hơn lấy), nhánh default bỏ kết nối thừa:
func (p *Pool) Put(c *Conn) {
select {
case p.conns <- c: // trả về kênh cho lần sau dùng
default: // pool đầy -> đóng kết nối
}
}

Hình 1: Pool là một buffered channel chứa kết nối rảnh (channel vừa là kho vừa đồng bộ, không cần mutex). Get nhận từ channel hoặc select timeout khi cạn; Put gửi về channel, nhánh default bỏ nếu đầy — sức chứa channel là trần kết nối.
Đo thật: tái dùng và tốc độ
Chạy thật (Go 1.23) với kết nối giả lập tốn 5ms để tạo:
Nạp sẵn 3 kết nối, soLanTao = 3
Sau 10 lần Get/Put, soLanTao = 3 (không tạo thêm)
Get khi pool cạn: hết hạn chờ kết nối
10 lần dùng chỉ cần 3 kết nối — pool xoay vòng chúng, không tạo thêm cái nào (soLanTao giữ nguyên 3). Khi pool cạn và không ai trả về, Get chờ rồi trả lỗi timeout đúng như thiết kế. Benchmark tái dùng qua pool so với tạo mới mỗi lần:

Hình 2: Pool tái dùng 3 kết nối cho 10 lần dùng (soLanTao giữ 3), timeout đúng khi cạn. Benchmark: có pool (chỉ Get/Put) 72,96 ns so với tạo mới mỗi lần (bắt tay 5ms) 574.319 ns — pool nhanh hơn hàng nghìn lần.
- có pool (tái dùng): 72,96 ns/op.
- không pool (tạo mới, bắt tay 5ms): 574.319 ns/op.
Pool nhanh hơn hàng nghìn lần vì Get/Put chỉ là thao tác channel (~73 ns), còn tạo mới trả cái giá bắt tay 5ms mỗi lần. Khoảng cách này tỉ lệ thuận với chi phí tạo kết nối thật — kết nối càng đắt (TLS, xa về địa lý), pool càng đáng giá.
Đánh đổi cần cân nhắc
Kết nối cũ có thể "chết" — cần kiểm tra sức khỏe. Một kết nối nằm trong pool lâu có thể bị server phía sau đóng, firewall ngắt, hay timeout. Nếu Get trả về một kết nối đã chết, request thất bại. Pool sản xuất cần: kiểm tra sức khỏe khi lấy ra (ping nhẹ), hoặc đặt max lifetime (đóng kết nối quá tuổi), hoặc thử lại với kết nối khác khi gặp lỗi. Pool đơn giản trong bài chưa có phần này.
Phải nhớ Put — quên là rò rỉ. Nếu code lấy kết nối bằng Get nhưng quên Put (do lỗi, panic, hay đường thoát sớm), kết nối đó biến mất khỏi pool vĩnh viễn — pool cạn dần rồi treo ở timeout. Luôn defer pool.Put(conn) ngay sau Get, và cân nhắc cơ chế bọc (một Release() an toàn) để khó quên. Đây là lỗi phổ biến nhất với pool tự viết.
Đừng viết lại pool nếu Go đã có. database/sql có connection pool đầy đủ bên trong (chỉnh bằng SetMaxOpenConns, SetMaxIdleConns, SetConnMaxLifetime), và http.Transport pool kết nối HTTP tự động. Với các trường hợp đó, dùng cái có sẵn — chúng đã xử lý health check, lifetime, và đồng thời kỹ càng. Viết pool tự chỉ khi bạn quản lý một loại tài nguyên tùy biến (kết nối tới hệ thống riêng, object đắt tiền) mà thư viện chuẩn không lo.
Ba ý mang về
- Connection pool dựng gọn bằng buffered channel trong Go: channel vừa là kho chứa kết nối rảnh vừa lo đồng bộ giữa goroutine (không cần mutex), và sức chứa channel chính là giới hạn số kết nối —
Getnhận từ channel,Putgửi lại,selectcho timeout khi cạn. - Pool tiết kiệm cực lớn khi kết nối tốn kém tạo: đo thật tái dùng 3 kết nối cho 10 lần dùng (không tạo thêm), và benchmark pool 72,96 ns so với tạo mới mỗi lần 574.319 ns (bắt tay 5ms) — khoảng cách tỉ lệ với chi phí tạo kết nối thật.
- Cạm bẫy: kết nối chết và quên Put: pool sản xuất cần health-check hoặc max lifetime cho kết nối cũ, và luôn
defer Putđể tránh rò rỉ — nhưng với DB và HTTP, dùng pool có sẵn củadatabase/sql/http.Transportthay vì viết lại.
Phần sau ta xét một mẫu bảo vệ hệ thống khỏi lỗi lan truyền khi service phía sau hỏng: Phần sau dựng một circuit breaker trong Go — tự ngắt lời gọi tới service đang lỗi để tránh dồn tải, và tự thử lại khi nó hồi phục.