目录

一次性学透 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.Gethttp.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 标准库:

  1. 入门:掌握客户端(Get/Post/NewRequest)和服务端(HandleFunc/ListenAndServe)的基础用法;
  2. 进阶:实现自定义路由、中间件,理解 context 的超时控制;
  3. 实战:解决跨域、表单解析、资源泄漏等实际问题;
  4. 优化:根据项目规模选择第三方库(如gorilla/mux、gin),但要先理解标准库原理。

其实 Go 的 HTTP 生态很简洁,标准库已经覆盖了 80% 的场景,剩下的 20% 再用第三方库补充(如 gin、echo、bingo等等…)。

如果有其他问题,欢迎在评论区交流!

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-http-guide/

备用原文链接: https://blog.fiveyoboy.com/articles/go-http-guide/