Một máy chủ trong vài dòng
Handler là một hàm nhận request và ghi response. ServeMux định tuyến các request đến handler.
Trình soạn thảo không thể nhận kết nối từ trình duyệt của bạn, nên các ví dụ trên trang này khởi động máy chủ bằng httptest.NewServer và gọi nó từ chính chương trình đó. Trong một chương trình thật, phần cuối được thay bằng một dòng chặn lại và phục vụ mãi mãi:
log.Fatal(http.ListenAndServe(":8080", mux))
Khi đó curl localhost:8080/hello/gopher in ra Hello, gopher!. ListenAndServe chỉ trả về khi có lỗi (ví dụ cổng đã bị chiếm), vì vậy nó được bọc trong log.Fatal.
Mỗi request chạy trong goroutine riêng. Bất cứ thứ gì các handler dùng chung, như một map hay một bộ đếm, đều cần một mutex.
Handler
Bất cứ thứ gì có phương thức ServeHTTP(http.ResponseWriter, *http.Request) đều là một http.Handler. http.HandlerFunc chuyển một hàm thường thành interface đó, và mux.HandleFunc làm việc chuyển đổi này giúp bạn. Handler dạng struct tiện khi handler cần các phụ thuộc:
type API struct {
db *sql.DB
}
func (a *API) listItems(w http.ResponseWriter, r *http.Request) { /* uses a.db */ }
mux.HandleFunc("GET /items", api.listItems)
*http.Request cho bạn:
| Trường hoặc phương thức | Chứa gì |
|---|---|
r.Method | GET, POST, ... |
r.URL.Path | đường dẫn, /items/42 |
r.PathValue("id") | một wildcard trong pattern của route (Go 1.22) |
r.URL.Query().Get("q") | một tham số trong query string |
r.Header.Get("Authorization") | một header của request |
r.Body | body của request, một io.ReadCloser (máy chủ tự đóng nó) |
r.FormValue("name") | một trường form hoặc tham số query |
r.Context() | một context bị hủy khi client ngắt kết nối |
Pattern định tuyến (Go 1.22)
Từ Go 1.22, pattern của ServeMux có dạng [METHOD ][HOST]/[PATH], và đường dẫn có thể chứa wildcard.
| Pattern | Khớp với |
|---|---|
"/items/" | /items/ và mọi thứ bên dưới (dấu gạch chéo ở cuối = tiền tố) |
"/items" | chỉ /items |
"GET /items/{id}" | GET (và HEAD) trên /items/42; r.PathValue("id") == "42" |
"POST /items" | chỉ POST trên /items |
"/files/{path...}" | /files/a/b/c; path là "a/b/c" |
"/{$}" | chỉ /, không phải mọi đường dẫn |
"/" | mọi đường dẫn không khớp với pattern nào khác |
Khi hai pattern cùng khớp, pattern cụ thể hơn sẽ thắng, nên /items/new thắng /items/{id}. Nếu không pattern nào cụ thể hơn, ví dụ /items/{id} và /{kind}/new (cả hai đều khớp /items/new), việc đăng ký pattern thứ hai sẽ panic với một thông báo nêu tên cả hai pattern. Nếu đường dẫn khớp nhưng phương thức thì không, mux trả lời 405 Method Not Allowed kèm header Allow, mà bạn không cần viết dòng code nào.
Pattern "/" là pattern bắt tất cả. Điều đó làm nhiều người bất ngờ khi họ đăng ký trang chủ bằng "/" rồi thấy nó trả lời 200 cho mọi URL không xác định. Hãy dùng "GET /{$}" cho trang chủ.
Các pattern này cần go 1.22 trở lên trong go.mod. Với dòng phiên bản cũ hơn, mux quay về hành vi cũ: nó đọc "GET /items" như một tên host theo sau là một đường dẫn, nên route không bao giờ khớp và mọi request đến /items đều nhận 404.
Một JSON API nhỏ
Request cuối cùng dùng DELETE, phương thức không route nào chấp nhận, và mux tự trả lời 405 với Allow: GET, HEAD: đó là các phương thức đã đăng ký cho đường dẫn đó (một route GET cũng chấp nhận HEAD).
Mã trạng thái và thứ tự ghi
Một response có ba phần, và chúng phải được ghi theo thứ tự: header, trạng thái, body.
w.Header().Set(...)thay đổi header. Nó chỉ có tác dụng cho đến khi trạng thái được gửi đi.w.WriteHeader(code)gửi dòng trạng thái và các header.w.Write(...)(hoặcfmt.Fprint(w, ...), hoặc một encoder) gửi body. Nếu chưa gọiWriteHeader, lầnWriteđầu tiên sẽ gửi200 OKtrước.
Hệ quả:
- Đặt header sau khi body đã bắt đầu thì không có tác dụng gì.
- Gọi
WriteHeaderhai lần sẽ ghi loghttp: superfluous response.WriteHeader callvà giữ trạng thái đầu tiên. - Sau một response lỗi, hãy
return.http.Errorkhông dừng handler của bạn, và code phía sau vẫn tiếp tục ghi vào cùng response đó.
Hãy dùng các hằng số có tên (http.StatusOK, http.StatusCreated, http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError) thay vì số trần. http.StatusText(404) trả về "Not Found".
Middleware
Middleware là một hàm nhận một handler và trả về một handler, làm gì đó trước hoặc sau khi gọi handler tiếp theo:
Dòng log: luôn xuất hiện trước client got: với cùng một request. Goroutine của handler in nó ra trước khi trả về, và máy chủ không hoàn tất response cho đến khi handler trả về.
statusRecorder nhúng ResponseWriter thật và chỉ ghi đè WriteHeader, đây là cách chuẩn để quan sát mã trạng thái từ middleware.
Cấu hình cho production
http.ListenAndServe dùng một máy chủ không có timeout, nên một client chậm có thể giữ kết nối mở vô thời hạn. Hãy cấu hình rõ ràng một http.Server cho bất cứ thứ gì đưa ra internet, và tắt nó một cách an toàn:
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done() // wait for Ctrl+C or a SIGTERM from the orchestrator
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil { // finish in-flight requests
log.Println("shutdown:", err)
}
Shutdown ngừng nhận kết nối mới và chờ các request đang chạy hoàn tất, tối đa đến hạn chót của context. ListenAndServe trả về http.ErrServerClosed ngay khi Shutdown bắt đầu, vì vậy lỗi đó không bị coi là thất bại. Với những handler chạy lâu, hãy truyền r.Context() xuống để chúng dừng khi client rời đi.
Phục vụ tệp tĩnh chỉ cần một dòng: mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public")))).
Lỗi thường gặp
- Không return sau
http.Error. Handler tiếp tục chạy và ghi thêm vào response. - Đặt header sau khi đã ghi body. Chúng bị bỏ đi mà không có cảnh báo nào.
- Đăng ký trang chủ trên
"/". Nó trở thành pattern bắt mọi đường dẫn không xác định. Hãy dùng"/{$}". - Chia sẻ trạng thái giữa các handler mà không có khóa. Các request chạy đồng thời.
- Dùng máy chủ mặc định trên production. Nó không có timeout; hãy đặt timeout trên một
http.Server. - Pattern định tuyến bị bỏ qua. Nếu
"GET /x/{id}"không bao giờ khớp, hãy kiểm trago.modcó ghigo 1.22trở lên không.
Câu hỏi thường gặp
Làm sao tạo một web server đơn giản trong Go?
Đăng ký một handler rồi bắt đầu lắng nghe: http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello") }) rồi log.Fatal(http.ListenAndServe(":8080", nil)). Máy chủ của thư viện chuẩn đủ sức chạy production; nó xử lý HTTP/1.1, HTTP/2 qua TLS, keep-alive và chạy một goroutine cho mỗi kết nối.
Làm sao lấy tham số đường dẫn trong net/http của Go?
Từ Go 1.22, pattern của ServeMux có thể chứa wildcard: đăng ký mux.HandleFunc("GET /items/{id}", h) rồi đọc giá trị bên trong handler bằng r.PathValue("id"). Một {path...} ở cuối khớp với phần còn lại của đường dẫn. Trước 1.22, bạn phải tự tách r.URL.Path hoặc dùng một router như chi.
Có cần framework như Gin để viết REST API bằng Go không?
Không. Từ Go 1.22, net/http định tuyến được theo phương thức và tham số đường dẫn, vốn là lý do chính khiến mọi người dùng router. Với encoding/json cho body và vài hàm middleware nhỏ để ghi log và xác thực, thư viện chuẩn đủ cho phần lớn API. Framework thêm những tiện ích như bind request và kiểm tra dữ liệu.
Làm sao đặt mã trạng thái trong một HTTP handler của Go?
Gọi w.WriteHeader(http.StatusCreated) trước khi ghi body. Nếu bạn ghi body trước, Go tự động gửi 200 OK và lời gọi WriteHeader sau đó bị bỏ qua kèm thông báo log superfluous response.WriteHeader call. Đặt header bằng w.Header().Set cũng phải trước WriteHeader; với lỗi, http.Error(w, msg, code) làm hết những việc đó.