Menu

HTTP client trong Golang: GET, POST JSON, timeout và header

Cách gửi HTTP request trong Go bằng net/http: http.Get, đọc và đóng body, kiểm tra mã trạng thái, timeout của http.Client, request kèm context và header, tham số query, và POST JSON.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Một GET request

Mỗi ví dụ trên trang này tự khởi động máy chủ riêng bằng httptest.NewServer và gọi nó qua một kết nối TCP thật trên localhost. Đổi srv.URL thành một địa chỉ thật như https://api.example.com thì code phía client vẫn giữ nguyên.

Ba quy tắc thấy được ở đây:

  1. Kiểm tra err trước tiên. Nó báo những lần không nhận được response nào cả: lỗi DNS, kết nối bị từ chối, timeout, sự cố TLS.
  2. defer resp.Body.Close() ngay sau khi kiểm tra lỗi. Body giữ kết nối mở. Một body không được đóng sẽ làm rò rỉ kết nối.
  3. Đọc hoặc giải mã body. io.ReadAll cho văn bản, json.NewDecoder(resp.Body).Decode(&v) cho JSON.

Mã trạng thái không phải là lỗi

http.Get trả về err == nil với mọi response mà máy chủ gửi, kể cả 404 và 500. Bạn phải tự kiểm tra mã trạng thái:

io.LimitReader giới hạn lượng dữ liệu bạn đọc từ body lỗi, để một máy chủ hoạt động bất thường không thể bắt bạn nạp hàng megabyte vào một thông báo lỗi. Nhiều API trả về 201, 202 hoặc 204 khi thành công; hãy kiểm tra resp.StatusCode >= 200 && resp.StatusCode < 300 khi mã 2xx nào cũng chấp nhận được.

Timeout: đừng bao giờ dùng client mặc định cho lưu lượng thật

http.Get, http.Post và http.DefaultClient không có timeout tổng (transport mặc định chỉ giới hạn bước quay số kết nối và bắt tay TLS). Một máy chủ chấp nhận kết nối rồi không bao giờ trả lời sẽ chặn goroutine của bạn mãi mãi. Hãy tạo client riêng:

Client.Timeout bao trùm toàn bộ quá trình trao đổi: kết nối, chuyển hướng và đọc body. Lỗi nó trả về là một *url.Error báo Timeout() == true, mà errors.As tìm ra được qua interface net.Error. Hãy tạo một client và dùng lại ở mọi nơi; http.Client an toàn khi dùng đồng thời. Pool kết nối nằm trong Transport của nó, không nằm trong Client: một http.Client{} mới cho mỗi request vẫn dùng chung http.DefaultTransport và pool của nó, nhưng một http.Transport mới cho mỗi request thì lần nào cũng bắt đầu với pool trống.

Request kèm context và header

http.NewRequestWithContext tạo một request mà bạn có thể tùy chỉnh trước khi gửi bằng client.Do. Context cho thêm hạn chót hoặc việc hủy riêng cho từng request, bên trên timeout của client, và gắn các lời gọi đi ra với vòng đời của một request đi vào trong máy chủ.

Nối dữ liệu người dùng nhập vào URL ("?q=" + q) sẽ hỏng khi gặp &, # và khoảng trắng. url.Values cùng phương thức Encode của nó lo việc escape, và sắp xếp các khóa. Với các đoạn của đường dẫn, dùng url.PathEscape.

POST JSON

client.Post(url, contentType, body) là lối tắt cho một request có header Content-Type đó. Với mọi trường hợp khác (header xác thực, PUT, PATCH, DELETE), dùng http.NewRequestWithContext với phương thức tương ứng và client.Do. Để gửi form, client.PostForm(url, url.Values{...}) mã hóa các giá trị và đặt application/x-www-form-urlencoded.

Dùng lại kết nối

Client chỉ dùng lại một kết nối TCP khi bạn đã đọc body trước đó đến hết và đóng nó. Nếu bạn dừng đọc sớm (ví dụ sau một mã trạng thái lỗi), hãy đọc nốt phần còn lại trước khi đóng khi bạn gửi nhiều request đến cùng một host:

defer func() {
	io.Copy(io.Discard, resp.Body)
	resp.Body.Close()
}()

Với những body có thể rất lớn, hãy đọc nốt qua io.LimitReader, hoặc chấp nhận mất kết nối đó.

Chuyển hướng

Client tự động đi theo tối đa 10 lần chuyển hướng, và resp.Request.URL cho bạn biết cuối cùng mình đến đâu. Để dừng ở lần chuyển hướng đầu tiên, chẳng hạn để đọc header Location, đặt CheckRedirect:

client := &http.Client{
	Timeout: 10 * time.Second,
	CheckRedirect: func(req *http.Request, via []*http.Request) error {
		return http.ErrUseLastResponse // return the 3xx response itself
	},
}

Lỗi thường gặp

  • Dùng http.Get hoặc http.DefaultClient trên production. Không có timeout. Hãy tạo một http.Client có Timeout.
  • Quên resp.Body.Close(). Kết nối bị rò rỉ cho đến khi request bắt đầu thất bại.
  • Đóng body trước khi kiểm tra err. Khi err khác nil, resp là nil và resp.Body.Close() gây panic.
  • Coi err == nil là thành công. Hãy kiểm tra resp.StatusCode.
  • Tạo http.Transport mới cho mỗi request. Mỗi transport có pool kết nối riêng, nên request nào cũng mở một kết nối mới (và một lần bắt tay TLS mới). Hãy dùng chung một transport, thường là qua một http.Client dùng chung.
  • Tự tay ghép query string. Hãy dùng url.Values.

Câu hỏi thường gặp

Làm sao gửi một GET request trong Go?

resp, err := http.Get(url), kiểm tra err, rồi defer resp.Body.Close() và đọc body bằng io.ReadAll(resp.Body) hoặc giải mã nó bằng json.NewDecoder(resp.Body).Decode(&v). Nhớ kiểm tra cả resp.StatusCode: một mã 404 hay 500 không phải là lỗi do http.Get trả về.

Làm sao đặt timeout cho HTTP request trong Go?

Tạo một client có timeout, client := &http.Client{Timeout: 10 * time.Second}, và dùng nó thay cho http.Get hoặc http.DefaultClient, vốn không có timeout tổng: một máy chủ chấp nhận kết nối rồi không bao giờ trả lời sẽ chặn lời gọi mãi mãi. Để giới hạn riêng cho từng request, tạo request bằng http.NewRequestWithContext với một context từ context.WithTimeout.

Làm sao gửi POST request với body JSON trong Go?

Marshal giá trị rồi post nó: body, _ := json.Marshal(v) và http.Post(url, "application/json", bytes.NewReader(body)). Để thêm header như Authorization, tạo request bằng http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)), đặt req.Header.Set("Content-Type", "application/json"), rồi gửi bằng client.Do(req).

Vì sao phải đóng resp.Body trong Go?

Body là một luồng dữ liệu trên một kết nối đang mở. Đóng nó sẽ trả kết nối về pool để dùng lại; quên đóng sẽ làm rò rỉ kết nối và file descriptor cho đến khi chương trình chạm giới hạn. Hãy đóng nó kể cả khi bạn không đọc, và đọc đến hết nếu muốn kết nối được dùng lại.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu