一次性学透 Go HTTP:从基础到实战+常见问题解析
Go 标准库的 net/http 已经足够强大,掌握它再搭配实战技巧,就能应对大部分开发场景。
这篇文章就把我整理的知识点和实战经验分享给大家,希望对大家有所帮助
一、3 行代码搭起 HTTP 服务
Go 创建HTTP服务的核心是http.HandleFunc注册路由和http.ListenAndServe启动服务。
下面这个例子会创建一个接收 GET 请求的接口,返回 JSON 数据:
package main
import (
"encoding/json"
"net/http"
)
// 处理请求的处理器函数
func helloHandler(w http.ResponseWriter, r *http.Request) {
// 只允许GET方法,其他方法返回405
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte("只支持GET请求"))
return
}
// 设置响应头为JSON格式
w.Header().Set("Content-Type", "application/json;charset=utf-8")
// 构造响应数据
resp := map[string]string{
"msg": "Hello Go HTTP!",
"method": r.Method,
}
// 序列化JSON并返回
json.NewEncoder(w).Encode(resp)
}
func main() {
// 注册路由:路径"/hello"对应helloHandler处理器
http.HandleFunc("/hello", helloHandler)
// 启动服务,监听8080端口,第二个参数为nil表示使用默认的ServeMux
err := http.ListenAndServe(":8080", nil)
if err != nil {
panic(err)
}
}运行代码后,用浏览器访问http://localhost:8080/hello,就能看到JSON响应。
这里要注意两个点:
- 一是处理器函数的参数
http.ResponseWriter用于构建响应,*http.Request存储请求信息; - 二是默认的 ServeMux(路由多路复用器)会处理路由匹配。
二、发送各类 HTTP 请求
客户端请求常用的有 GET、POST 两种,标准库的http.Get、http.Post方法能满足基础需求,复杂场景可以用http.NewRequest自定义请求头、超时等参数。
(一)GET 请求
示例
package main
import (
"fmt"
"io/ioutil"
"net/http"
"time"
)
func main() {
// 方法1:直接用http.Get发送简单GET请求
resp, err := http.Get("http://localhost:8080/hello")
if err != nil {
fmt.Printf("请求失败:%v\n", err)
return
}
// 必须关闭响应体,避免资源泄漏
defer resp.Body.Close()
// 读取响应内容
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("读取响应失败:%v\n", err)
return
}
fmt.Printf("状态码:%d\n", resp.StatusCode)
fmt.Printf("响应内容:%s\n", body)
// 方法2:用http.NewRequest自定义请求(适合复杂场景)
req, err := http.NewRequest(http.MethodGet, "http://localhost:8080/hello", nil)
if err != nil {
fmt.Printf("创建请求失败:%v\n", err)
return
}
// 设置请求头
req.Header.Set("User-Agent", "Go-HTTP-Client/1.1")
// 创建客户端并设置超时时间(重要!避免无限等待)
client := &http.Client{
Timeout: 5 * time.Second,
}
resp2, err := client.Do(req)
if err != nil {
fmt.Printf("自定义请求失败:%v\n", err)
return
}
defer resp2.Body.Close()
body2, _ := ioutil.ReadAll(resp2.Body)
fmt.Printf("自定义请求响应:%s\n", body2)
}(二)POST 请求
示例(发送JSON数据)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
func main() {
// 构造POST请求的JSON数据
postData := map[string]string{
"username": "gopher",
"password": "123456",
}
jsonData, err := json.Marshal(postData)
if err != nil {
fmt.Printf("JSON序列化失败:%v\n", err)
return
}
// 创建POST请求,第三个参数为请求体(用bytes.NewBuffer包装JSON数据)
req, err := http.NewRequest(http.MethodPost, "http://localhost:8080/login", bytes.NewBuffer(jsonData))
if err != nil {
fmt.Printf("创建请求失败:%v\n", err)
return
}
// 设置请求头,告诉服务端是JSON格式
req.Header.Set("Content-Type", "application/json;charset=utf-8")
client := &http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("POST请求失败:%v\n", err)
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Printf("POST响应:%s\n", body)
}其他类型的请求可以从 http 包找到对应的 api,用法基本和 GET、POST 类似
三、进阶:高级功能
掌握基础后,我们需要解决实际开发中的复杂场景,比如自定义路由、中间件、超时控制等。
这些能力不用依赖第三方库,标准库就能实现。
(一)自定义路由
自定义路由:告别默认ServeMux的局限。
默认的 ServeMux 不支持路由参数(比如/user/:id),实际开发中我们可以自己实现一个简单的路由,用 map 存储路径与处理器的映射:
package main
import (
"encoding/json"
"net/http"
"strings"
)
// 自定义路由结构体
type MyMux struct {
routes map[string]http.HandlerFunc
}
// 初始化路由
func NewMyMux() *MyMux {
return &MyMux{
routes: make(map[string]http.HandlerFunc),
}
}
// 注册GET路由的方法
func (m *MyMux) Get(path string, handler http.HandlerFunc) {
m.routes[strings.ToUpper(http.MethodGet)+"_"+path] = handler
}
// 注册POST路由的方法
func (m *MyMux) Post(path string, handler http.HandlerFunc) {
m.routes[strings.ToUpper(http.MethodPost)+"_"+path] = handler
}
// 实现ServeHTTP方法,使MyMux满足http.Handler接口
func (m *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 构造键:方法+路径
key := strings.ToUpper(r.Method) + "_" + r.URL.Path
// 查找对应的处理器
handler, ok := m.routes[key]
if !ok {
// 未找到路由返回404
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"msg": "路由不存在"})
return
}
// 执行处理器
handler(w, r)
}
// 处理器函数1:获取用户信息(支持路由参数可进一步解析r.URL.Path)
func getUserHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json;charset=utf-8")
// 简单解析路径中的id参数(实际可用strings.Split或正则)
id := strings.TrimPrefix(r.URL.Path, "/user/")
json.NewEncoder(w).Encode(map[string]string{
"msg": "获取用户成功",
"id": id,
})
}
// 处理器函数2:用户登录
func loginHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json;charset=utf-8")
json.NewEncoder(w).Encode(map[string]string{"msg": "登录成功"})
}
func main() {
mux := NewMyMux()
// 注册路由
mux.Get("/user/123", getUserHandler)
mux.Post("/login", loginHandler)
// 启动服务,传入自定义路由
http.ListenAndServe(":8080", mux)
}这个自定义路由支持按请求方法区分路由,后续还能扩展路由参数解析、通配符等功能。
如果项目复杂,也可以用成熟的第三方路由库如 gorilla/mux,但理解底层原理很重要
(二)中间件
中间件:统一处理日志、鉴权等逻辑。
中间件能在请求到达处理器前、响应返回客户端后执行统一逻辑,比如日志记录、登录鉴权、跨域处理等。
它的核心是嵌套 http.HandlerFunc:
package main
import (
"encoding/json"
"log"
"net/http"
"time"
)
// 日志中间件:记录请求方法、路径、耗时
func LogMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 请求前:记录开始时间和请求信息
start := time.Now()
log.Printf("开始处理请求:%s %s", r.Method, r.URL.Path)
// 执行下一个处理器(核心:调用传入的next)
next(w, r)
// 请求后:记录耗时
duration := time.Since(start)
log.Printf("请求处理完成,耗时:%v", duration)
}
}
// 鉴权中间件:验证Token
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 从请求头获取Token
token := r.Header.Get("Authorization")
if token == "" || token != "valid_token" {
// 鉴权失败返回401
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"msg": "未授权,Token无效"})
return
}
// 鉴权通过,执行下一个处理器
next(w, r)
}
}
// 业务处理器:需要鉴权的接口
func profileHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json;charset=utf-8")
json.NewEncoder(w).Encode(map[string]string{"msg": "获取个人信息成功"})
}
func main() {
// 中间件嵌套:先日志,再鉴权,最后业务逻辑
http.HandleFunc("/profile", LogMiddleware(AuthMiddleware(profileHandler)))
log.Fatal(http.ListenAndServe(":8080", nil))
}中间件可以多层嵌套,执行顺序是“外层到内层”,响应时则“内层到外层”。
实际开发中,日志、跨域这类全局逻辑可以注册到路由层面,对所有接口生效。
常见问题
Q1. 响应体未关闭导致资源泄漏?
问题:客户端发送请求后,如果不关闭 resp.Body,会导致文件描述符泄漏,长期运行会耗尽资源。
解决:用 defer 关键字强制关闭,即使发生错误也要执行。
正确示例:
resp, err := http.Get("http://example.com")
if err != nil {
// 错误处理
return
}
// 必须加defer关闭
defer resp.Body.Close()
// 读取响应内容
body, _ := ioutil.ReadAll(resp.Body)Q2. 服务端处理耗时过长导致客户端超时?
问题:默认情况下客户端没有超时设置,服务端处理耗时过长会导致客户端无限等待。
解决:客户端创建 http.Client 时设置 Timeout(包含连接、请求、响应的总耗时);服务端用 context 控制处理器超时。
客户端超时示例:
client := &http.Client{
// 总超时5秒
Timeout: 5 * time.Second,
}
resp, err := client.Get("http://localhost:8080/slow")
if err != nil {
fmt.Printf("请求超时:%v\n", err) // 超过5秒会返回超时错误
return
}服务端超时示例:
func slowHandler(w http.ResponseWriter, r *http.Request) {
// 创建3秒超时的context
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
// 模拟耗时操作(比如数据库查询)
ch := make(chan string)
go func() {
time.Sleep(4 * time.Second) // 耗时4秒,超过超时时间
ch <- "操作完成"
}()
select {
case <-ctx.Done():
// 超时返回504
w.WriteHeader(http.StatusGatewayTimeout)
w.Write([]byte("处理超时"))
return
case res := <-ch:
w.Write([]byte(res))
return
}
}Q3. 跨域请求被拦截?
问题:前端页面与 Go 服务端不在同一域名时,会触发浏览器的跨域验证,导致请求失败。
解决:实现跨域中间件,设置允许的 Origin、Method、Header 等响应头。
跨域中间件示例:
// CORS中间件
func CORSMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 允许的源(生产环境建议指定具体域名,不要用*)
w.Header().Set("Access-Control-Allow-Origin", "*")
// 允许的请求方法
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
// 允许的请求头
w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Authorization")
// 预检请求的缓存时间
w.Header().Set("Access-Control-Max-Age", "86400")
// 处理OPTIONS预检请求
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
next(w, r)
}
}
// 使用方式
http.HandleFunc("/api/data", CORSMiddleware(dataHandler))Q4. 如何获取 POST 请求的表单数据?
问题:用 r.Form.Get() 获取不到 POST 表单数据,或者提示“missing content-type”。
解决:先调用 r.ParseForm() 解析表单,且确保前端发送的 Content-Type为application/x-www-form-urlencoded。
示例:
func formHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// 解析表单数据(必须调用,否则r.Form为空)
err := r.ParseForm()
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("表单解析失败"))
return
}
// 获取表单字段
username := r.Form.Get("username")
password := r.Form.Get("password")
w.WriteHeader(http.StatusOK)
w.Write([]byte(fmt.Sprintf("用户名:%s,密码:%s", username, password)))
}总结
Go 的 HTTP 开发核心是掌握 net/http 标准库:
- 入门:掌握客户端(Get/Post/NewRequest)和服务端(HandleFunc/ListenAndServe)的基础用法;
- 进阶:实现自定义路由、中间件,理解 context 的超时控制;
- 实战:解决跨域、表单解析、资源泄漏等实际问题;
- 优化:根据项目规模选择第三方库(如gorilla/mux、gin),但要先理解标准库原理。
其实 Go 的 HTTP 生态很简洁,标准库已经覆盖了 80% 的场景,剩下的 20% 再用第三方库补充(如 gin、echo、bingo等等…)。
如果有其他问题,欢迎在评论区交流!
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!