Golang 網路編程:HTTP 客戶端(http.Get、http.Post)
簡介
在現代的服務導向架構中,HTTP 是最常見的通訊協定之一。無論是呼叫第三方 API、與微服務互相協作,或是簡單的網頁爬蟲,都離不開 HTTP 客戶端的操作。Go 語言內建的 net/http 套件提供了直觀且效能優異的 API,使得開發者能以最少的程式碼完成 GET、POST、PUT、DELETE 等各種請求。
本篇文章聚焦於 http.Get 與 http.Post 兩個最常使用的函式,從概念說明、實作範例、常見陷阱到最佳實踐,完整呈現從「第一次發送請求」到「在生產環境中安全、可維護」的全流程。即使你是剛接觸 Go 的新手,也能在閱讀完後自行撰寫可靠的 HTTP 客戶端程式。
核心概念
1. http.Get 的基本用法
http.Get 是一個簡化版的 GET 請求,只需要傳入目標 URL,便會回傳 *http.Response 與 error。它會自動建立一個預設的 http.Client,使用預設的 Transport(即底層的 TCP 連線管理)。
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
// 發送 GET 請求
resp, err := http.Get("https://api.github.com/repos/golang/go")
if err != nil {
// 錯誤處理:網路錯誤、URL 格式錯誤等
panic(err)
}
defer resp.Body.Close() // 確保資源釋放
// 讀取回應內容
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Printf("Status: %s\n", resp.Status)
fmt.Printf("Body: %s\n", body[:200]) // 只印前 200 byte
}
重點:
defer resp.Body.Close()必須在取得resp後立即呼叫,否則會造成連線資源泄漏。resp.Status包含 HTTP 狀態碼與文字說明(例如200 OK)。
2. http.Post 的基本用法
http.Post 用於 POST 請求,除了 URL 之外,還需要提供 Content-Type 與 body(io.Reader)。常見的 Content-Type 包括 application/json、application/x-www-form-urlencoded、multipart/form-data 等。
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
// 準備 JSON payload
payload := map[string]string{
"name": "Gopher",
"email": "gopher@example.com",
}
data, _ := json.Marshal(payload)
// 發送 POST 請求
resp, err := http.Post(
"https://httpbin.org/post",
"application/json",
bytes.NewBuffer(data),
)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println("Status:", resp.Status)
}
技巧:使用
bytes.NewBuffer包裝[]byte,即可符合io.Reader介面,讓http.Post能直接傳遞 JSON、XML 或其他二進位資料。
3. 自訂 http.Client 與 Timeout
http.Get 與 http.Post 內部會使用 http.DefaultClient。在生產環境中,我們常需要設定 Timeout、TLS 設定、或 Proxy。此時就要自行建立 http.Client。
package main
import (
"net"
"net/http"
"time"
)
func main() {
// 建立自訂的 Transport,設定連線逾時與 Keep-Alive
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
}
// 建立 Client,設定整體請求逾時
client := &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
}
// 使用 client 執行 GET
resp, err := client.Get("https://httpbin.org/delay/2") // 2 秒延遲的測試 API
if err != nil {
panic(err)
}
defer resp.Body.Close()
// ...
}
為什麼要自訂?
- Timeout:防止因遠端服務卡住而導致程式永久阻塞。
- Transport:可重複使用,減少 TCP 連線建立成本,提升效能。
- Proxy / TLS:企業環境常需要透過代理或自行驗證憑證。
4. POST 表單資料 (application/x-www-form-urlencoded)
許多舊式 API 仍使用表單編碼的方式傳遞參數。http.PostForm 提供了便利的封裝。
package main
import (
"fmt"
"net/http"
"net/url"
)
func main() {
form := url.Values{}
form.Add("username", "admin")
form.Add("password", "secret")
resp, err := http.PostForm("https://httpbin.org/post", form)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println("Status:", resp.Status)
}
注意:
PostForm內部會自動設定Content-Type: application/x-www-form-urlencoded,且會把url.Values轉成key=value&...的字串。
5. 上傳檔案 (multipart/form-data)
當需要上傳檔案時,必須使用 multipart.Writer 手動組合請求體。
package main
import (
"bytes"
"io"
"mime/multipart"
"net/http"
"os"
)
func main() {
var b bytes.Buffer
w := multipart.NewWriter(&b)
// 加入文字欄位
_ = w.WriteField("description", "測試檔案上傳")
// 加入檔案欄位
fw, err := w.CreateFormFile("file", "example.txt")
if err != nil {
panic(err)
}
file, err := os.Open("example.txt")
if err != nil {
panic(err)
}
defer file.Close()
_, err = io.Copy(fw, file)
if err != nil {
panic(err)
}
w.Close() // 必須關閉,寫入結尾的 boundary
// 建立請求
req, err := http.NewRequest("POST", "https://httpbin.org/post", &b)
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", w.FormDataContentType())
// 使用預設 client 送出
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
// ...
}
關鍵點:
multipart.NewWriter會自動產生唯一的 boundary,必須透過FormDataContentType()把它寫入Content-Type。w.Close()必須在組完所有欄位後呼叫,否則請求體不完整。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記關閉 resp.Body |
會造成連線泄漏,最終導致 FD 用盡。 | 使用 defer resp.Body.Close(),或在 io.ReadAll 前先關閉。 |
| 未設定 Timeout | 網路卡住時程式會永遠等待。 | 為 http.Client 設定 Timeout,或在 Transport 中設定 DialContext、TLSHandshakeTimeout。 |
直接使用 http.Get/http.Post 於大量請求 |
每次呼叫都會重建 Transport,效能低下。 |
建立共享的 http.Client(建議作為全域或注入式),重複使用。 |
| JSON 編碼失敗未處理 | json.Marshal 失敗會回傳 error,若忽略會導致空 body。 |
必須檢查 err,或使用 log.Fatalf 立即失敗。 |
表單或檔案上傳忘記設定 Content-Type |
伺服器無法正確解析請求。 | 使用 multipart.Writer 的 FormDataContentType() 或 http.PostForm。 |
| 過度信任 TLS 證書 | 企業內部可能使用自簽證書,若不信任會失敗。 | 在 Transport.TLSClientConfig 中設定 InsecureSkipVerify(僅測試)或自訂根證書。 |
進階最佳實踐
使用 Context 控制取消
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) resp, err := client.Do(req)- 可以在請求過程中主動取消,避免因外部條件(如使用者關閉頁面)而浪費資源。
重試機制
- 對於暫時性錯誤(如 502、504)可使用指數退避(exponential backoff)重試。
- 建議使用第三方套件(如
github.com/cenkalti/backoff)或自行實作。
日誌與度量
- 在每次請求前後記錄 URL、狀態碼、耗時,方便排錯與性能分析。
- 結合 Prometheus、OpenTelemetry 可自動收集指標。
避免全局變數
- 雖然
http.DefaultClient方便,但在大型專案中應該將*http.Client注入至需要的結構體或函式,以便於測試(mock)與設定差異化的 Timeout。
- 雖然
實際應用場景
| 場景 | 為何使用 http.Get / http.Post |
範例程式碼簡述 |
|---|---|---|
| 呼叫第三方 REST API(如天氣、付款) | 多數為 GET/POST,需設定 Header(API Key)與 JSON Body。 | 使用自訂 http.Client、NewRequest 加上 Authorization Header。 |
| 微服務間的同步呼叫 | 服務 A 需要即時取得服務 B 的資料,使用短暫 Timeout。 | client.Get(serviceBURL),Timeout 設為 2 秒。 |
| 批次上傳檔案 | 大量檔案一次性上傳,需要 multipart/form-data。 |
參考上傳檔案範例,配合 io.Pipe 以流式方式傳輸。 |
| 簡易健康檢查 | 定時 GET 某個 endpoint,確認服務是否存活。 | http.Get(healthURL),檢查回傳 200 即為 OK。 |
| Webhook 接收端 | 第三方系統會 POST JSON 到自家服務,需解析 Body。 | 使用 json.NewDecoder(r.Body).Decode(&payload)。 |
總結
http.Get與http.Post為 Go 語言提供的 即時、簡潔 的 HTTP 客戶端介面,適合一次性、簡單的請求。- 在需要 自訂 Timeout、重用連線、設定 Header、或上傳檔案 時,應該建立自己的
http.Client與http.Request,並配合 Context 以支援取消與超時。 - 常見的錯誤包括忘記關閉
Body、未設定 Timeout、以及錯誤的 Content-Type。透過 最佳實踐(共享 Client、重試、日誌、度量)可以讓你的 HTTP 客戶端在生產環境中更加穩定與可觀測。
掌握了這些核心概念與實作技巧後,你就能在 Go 專案中自信地與任何支援 HTTP 的服務互動,無論是呼叫外部 API、實作微服務間的同步呼叫,或是開發自動化測試腳本,都能以簡潔且高效的程式碼完成。祝你在 Golang 的網路編程之路上玩得開心、寫得順利! 🚀