Máy chủ HTTP nằm trong thư viện chuẩn Go, và nó đủ tốt cho sản xuất — đó là lý do nhiều dịch vụ Go không dùng framework nào.

Mười dòng

mux := http.NewServeMux()
mux.HandleFunc("GET /don/{id}", func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "don id=%s", r.PathValue("id"))
})
http.ListenAndServe(":8080", mux)

Từ Go 1.22, ServeMux hiểu method và tham số đường dẫn:

  GET    /don/DH-9  -> 200 don id=DH-9 method=GET
  POST   /don       -> 200 tao don: {"x":1}
  GET    /khac      -> 404 khong tim thay
  DELETE /don/1     -> 404 khong tim thay

DELETE /don/1 trả 404 vì chỉ có GET /don/{id} được đăng ký — router phân biệt method.

Đây là thay đổi lớn: trước 1.22, ServeMux chỉ khớp tiền tố đường dẫn, và mọi người phải dùng chi, gorilla/mux, hay gin. Giờ thư viện chuẩn đủ cho phần lớn API.

Cú pháp mẫu:

"GET /don/{id}"          tham số đường dẫn
"GET /tep/{path...}"     bắt phần còn lại
"GET /don/{$}"           khớp CHÍNH XÁC /don/, không khớp /don/abc
"example.com/api/"       khớp theo host

Dấu {$} giải quyết một vấn đề cũ: "/don/" trước đây khớp mọi thứ bắt đầu bằng /don/.

Mỗi request một goroutine

  50 request song song -> đỉnh đồng thời 50

net/http tạo một goroutine cho mỗi request, và bài 31 đã đo vì sao điều đó khả thi: 2,47 µs và 576 byte.

Hệ quả cho cách bạn viết handler:

Handler chạy đồng thời — mọi trạng thái dùng chung phải được bảo vệ. Bài 34 và 36 áp dụng nguyên vẹn.

Không cần lo về pool luồng. Không có ExecutorService, không có cấu hình số worker.

Nhưng vẫn cần giới hạn tài nguyên. Mười nghìn request đồng thời là mười nghìn goroutine cùng xin kết nối CSDL. Giới hạn bằng pool kết nối (bài 49) hoặc semaphore.

Panic trong handler chỉ giết request đónet/httprecover sẵn. Nhưng nó chỉ in ra stderr, nên bạn vẫn nên có middleware riêng như bài 25 đã nói.

Bốn timeout bắt buộc

  http.ListenAndServe(addr, h) -> MỌI timeout đều bằng 0

Đây là điểm quan trọng nhất của bài. http.ListenAndServe dùng Server mặc định, và không có timeout nào.

Nghĩa là một client mở kết nối rồi không gửi gì sẽ giữ một goroutine của bạn mãi mãi. Vài nghìn kết nối như vậy là dịch vụ chết — và đó là một kiểu tấn công từ chối dịch vụ rất rẻ.

srv := &http.Server{
	Addr:              ":8080",
	Handler:           mux,
	ReadHeaderTimeout: 2 * time.Second,    // chống Slowloris
	ReadTimeout:       5 * time.Second,    // đọc xong cả request
	WriteTimeout:      10 * time.Second,   // ghi xong cả response
	IdleTimeout:       60 * time.Second,   // keep-alive rảnh
}
log.Fatal(srv.ListenAndServe())

ReadHeaderTimeout là cái quan trọng nhất và hay bị quên — nó chặn kiểu tấn công gửi header rất chậm.

Với endpoint stream hoặc tải tệp lớn, WriteTimeout phải đủ dài hoặc đặt 0 và dùng context để kiểm soát.

Handler và HandlerFunc

type Handler interface {
	ServeHTTP(w http.ResponseWriter, r *http.Request)
}

Lại là một interface một method. http.HandlerFunc là adapter biến hàm thành Handler:

type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }

Mẫu này đáng học vì nó xuất hiện khắp Go: định nghĩa kiểu hàm, gắn method cho nó, và giờ hàm thoả mãn interface.

Handler cần trạng thái thì dùng struct:

type API struct { kho *Kho }
func (a *API) LayDon(w http.ResponseWriter, r *http.Request) { ... }

mux.HandleFunc("GET /don/{id}", a.LayDon)

Ghi response cho đúng

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(don)

Thứ tự bắt buộc: đặt header → WriteHeader → ghi thân. Đặt header sau WriteHeaderkhông có tác dụng, và Go chỉ log một dòng cảnh báo nhỏ.

Gọi WriteHeader hai lần cũng chỉ có lần đầu tính, kèm cảnh báo superfluous response.WriteHeader call.

Không gọi WriteHeader thì lần ghi đầu tiên tự động gửi 200.

Một cái bẫy thật: nếu bạn đã ghi một phần thân rồi mới gặp lỗi, không đổi được mã trạng thái nữa. Nên với response lớn, hãy dựng xong trong bộ nhớ hoặc kiểm hết lỗi trước khi ghi byte đầu tiên.

Giới hạn thân request

r.Body = http.MaxBytesReader(w, r.Body, 1<<20)

Như bài 45 đã nói. Không có nó, một request 10GB làm hết bộ nhớ máy chủ.

Thử ba mươi giây

http.ListenAndServe(":8080", mux)

rồi từ terminal khác:

printf 'GET / HTTP/1.1\r\nHost: x\r\n' | nc localhost 8080

Không gửi dòng trống kết thúc header. Kết nối treo vĩnh viễn, và một goroutine của bạn kẹt theo.

Đặt ReadHeaderTimeout: 2 * time.Second và thử lại — nó đóng sau hai giây. Ba mươi giây đó là lý do bốn dòng timeout đáng viết.

Ngày mai: gọi API bằng net/http client.