Go语言标准库中的net/http包同时提供了HTTP客户端和服务端的实现,这让网络请求的编写不必依赖第三方框架。客户端一侧通过http.Client和http.Request即可构造带超时、请求头、Cookie的完整请求;服务端一侧只要实现http.Handler接口,就能把路由、参数解析和响应写入统一起来。本文会从请求发起、响应解析、服务端参数处理和传输层调优四个方向,给出可以直接运行的代码示例。

一、用net/http包发起GET与POST请求
最简单的客户端请求来自http.Get函数,它内部使用默认的http.DefaultClient完成一次GET调用。下面的代码演示了如何读取一个接口返回的全部内容,并把响应体打印出来。示例中defer语句用于确保响应体在使用完毕后被关闭,这是释放连接资源的关键一步。
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
resp, err := http.Get("https://api.ipipp.com/users")
if err != nil {
fmt.Println("请求失败:", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
http.Get虽然写起来方便,但它依赖的DefaultClient没有设置超时时间。在生产环境中,如果目标服务长时间无响应,协程会一直阻塞,连接也无法及时释放。因此更推荐显式创建http.Client对象,并对超时进行控制。对于表单提交场景,可以使用http.PostForm,它会自动把Content-Type设置为application/x-www-form-urlencoded。
package main
import (
"fmt"
"io"
"net/http"
"net/url"
)
func main() {
data := url.Values{}
data.Set("name", "gopher")
data.Set("role", "developer")
resp, err := http.PostForm("https://api.ipipp.com/register", data)
if err != nil {
fmt.Println("请求失败:", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
当请求体是JSON格式,或者需要自定义Authorization、User-Agent等请求头时,http.NewRequest会更加灵活。通过http.NewRequest创建请求对象后,可以逐个设置Header,再把请求交给http.Client执行。注意NewRequest不会自动设置Content-Type,发送JSON时必须手工指定。
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
type User struct {
Name string `json:"name"`
Role string `json:"role"`
}
func main() {
user := User{Name: "gopher", Role: "developer"}
payload, _ := json.Marshal(user)
req, err := http.NewRequest(http.MethodPost, "https://api.ipipp.com/users", bytes.NewReader(payload))
if err != nil {
fmt.Println("创建请求失败:", err)
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer token123")
client := http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Println("请求失败:", err)
return
}
defer resp.Body.Close()
fmt.Println("状态码:", resp.StatusCode)
}
二、读取响应体并解析JSON数据
http.Response中的Body字段是一个io.ReadCloser接口,它既支持流式读取,也要求调用方在使用后关闭。读取小型响应体时,io.ReadAll是最直接的方式,它会一次性把全部字节读入内存。关闭响应体的操作必须放在错误判断之后,因为一旦请求失败,resp可能为nil。
func fetchUser(id string) {
resp, err := http.Get("https://api.ipipp.com/users/" + id)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("服务返回异常状态码: %d\n", resp.StatusCode)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("读取响应体失败:", err)
return
}
fmt.Println(string(body))
}
读取到字节切片之后,通常还需要把JSON字符串转换成结构体。encoding/json包的Unmarshal函数接收一个字节切片和一个目标结构体指针,它会按照字段名或json标签进行映射。如果响应体很大,可以用json.NewDecoder直接从响应体上流式解码,避免先把全部内容读进内存。
type ApiResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
ID int `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
} `json:"data"`
}
func parseUser(body []byte) {
var result ApiResponse
err := json.Unmarshal(body, &result)
if err != nil {
fmt.Println("JSON解析失败:", err)
return
}
fmt.Printf("用户ID: %d, 姓名: %s, 角色: %s\n", result.Data.ID, result.Data.Name, result.Data.Role)
}
流式解析的写法与Unmarshal类似,只是数据来源从字节切片换成了resp.Body。这种方式适合处理几兆甚至几十兆的响应数据,能明显降低内存占用。不过流式解码对JSON语法错误同样敏感,需要根据业务场景决定是否预先读取完整内容。
func parseFromBody(resp *http.Response) {
var result ApiResponse
decoder := json.NewDecoder(resp.Body)
if err := decoder.Decode(&result); err != nil {
fmt.Println("流式解析失败:", err)
return
}
fmt.Println(result.Msg)
}
三、构建HTTP服务端并解析请求参数
服务端开发同样围绕net/http展开。http.HandleFunc可以把一个路径与处理函数绑定,处理函数必须满足func(http.ResponseWriter, *http.Request)签名。下面的代码注册了/user和/health两个路由,其中/user只接受POST请求,并从请求体中解析JSON数据。
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type User struct {
Name string `json:"name"`
Role string `json:"role"`
}
func main() {
http.HandleFunc("/user", userHandler)
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println("服务启动失败:", err)
}
}
func userHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "只支持POST请求", http.StatusMethodNotAllowed)
return
}
defer r.Body.Close()
var user User
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
http.Error(w, "请求体不是合法JSON", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{
"status": "created",
"name": user.Name,
})
}
除了JSON请求体,服务端还经常需要读取URL查询参数和表单字段。r.URL.Query().Get可以直接获取问号后面的参数,而r.FormValue会同时检查URL查询参数和POST表单内容。若要获取文件上传等multipart数据,则需要调用r.ParseMultipartForm。
func queryHandler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
page := r.URL.Query().Get("page")
if page == "" {
page = "1"
}
fmt.Fprintf(w, "id=%s, page=%s\n", id, page)
}
func formHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "表单解析失败", http.StatusBadRequest)
return
}
name := r.FormValue("name")
role := r.FormValue("role")
fmt.Fprintf(w, "姓名=%s, 角色=%s\n", name, role)
}
服务端处理请求体时必须限制读取大小,否则客户端可能上传超大内容占用内存。http.MaxBytesReader可以包裹r.Body,设置一个最大字节数,超过限制后读取会返回错误,从而避免恶意请求拖垮服务。
func safeJSONHandler(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
defer r.Body.Close()
var data map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
http.Error(w, "请求体过大或JSON非法", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "收到字段数: %d\n", len(data))
}
四、自定义http.Client与传输层调优
直接使用http.Get或http.DefaultClient的最大隐患是没有超时。长时间运行的程序一旦遇到网络抖动,连接可能悬挂很久。显式创建http.Client并设置Timeout,可以约束从发起请求到读取完响应体的总时长,是编写健壮客户端的第一步。
package main
import (
"net/http"
"time"
)
func newClient() *http.Client {
return &http.Client{
Timeout: 30 * time.Second,
}
}
http.Client底层的连接复用、TLS握手、代理设置都由http.Transport负责。默认Transport会复用空闲连接,但某些高并发场景需要调整连接池上限和空闲超时。下面配置了每个主机的最大空闲连接数,以及TLS握手的最长等待时间,能够减少频繁建立连接带来的延迟。
func newTransport() *http.Transport {
return &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ForceAttemptHTTP2: true,
}
}
func newClientWithTransport() *http.Client {
return &http.Client{
Transport: newTransport(),
Timeout: 30 * time.Second,
}
}
默认情况下,遇到3xx重定向时http.Client会自动跟随,最多10次。如果希望手动处理重定向,或者限制重定向次数,可以通过CheckRedirect字段进行拦截。返回http.ErrUseLastResponse表示不再继续跳转,并把最后一次响应返回给调用方。
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 5 {
return http.ErrUseLastResponse
}
return nil
},
}
总的来说,Golang处理网络请求的核心优势在于标准库接口统一且足够灵活。客户端从简单的GET到带超时、重定向策略的复杂请求,服务端从路由注册到JSON参数解析,都能在同一套net/http体系内完成。开发时重点关注响应体关闭、超时控制和请求体大小限制,就能避免大多数网络编程中的隐蔽问题。

