导航菜单

HTTP 编程基础

Go 语言的标准库 net/http 提供了功能强大且易于使用的 HTTP 服务器与客户端实现,无需任何第三方框架即可构建完整的 Web 应用。本章将系统讲解 Go HTTP 编程的核心概念与实战技巧。

http.Handler 与 http.HandlerFunc

http.Handler 接口

http.Handler 是 Go HTTP 处理的核心接口。任何实现了 ServeHTTP(ResponseWriter, *Request) 方法的类型都可以作为 HTTP 请求的处理器。

package main

import (
    "fmt"
    "net/http"
)

// 自定义类型实现 http.Handler 接口
type MyHandler struct {
    greeting string
}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%s, World!", h.greeting)
}

func main() {
    handler1 := &MyHandler{greeting: "Hello"}
    handler2 := &MyHandler{greeting: "Hi"}

    http.Handle("/hello", handler1)
    http.Handle("/hi", handler2)

    http.ListenAndServe(":8080", nil)
}
http.HandlerFunc

http.HandlerFunc 是一个适配器类型,它将普通的函数转换为 http.Handler 接口的实现。这样你无需定义新类型,直接使用函数即可处理请求。

package main

import (
    "fmt"
    "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, World!")
}

func main() {
    // http.HandlerFunc 自动满足 http.Handler 接口
    http.HandleFunc("/hello", helloHandler)

    http.ListenAndServe(":8080", nil)
}

http.ServeMux 路由器

Go 1.22 对 http.ServeMux 进行了重大升级,支持路径参数和方法匹配:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    mux := http.NewServeMux()

    // Go 1.22+ 支持路径参数
    mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
        id := r.PathValue("id")
        fmt.Fprintf(w, "获取用户: %s", id)
    })

    // 支持方法匹配
    mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, "创建用户")
    })

    // 通配符匹配
    mux.HandleFunc("/static/{path...}", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "静态文件路径: %s", r.PathValue("path"))
    })

    http.ListenAndServe(":8080", mux)
}

http.Request 详解

http.Request

http.Request 表示服务器收到的 HTTP 请求,包含了请求方法、URL、Header、Body、表单数据、Cookie 等所有请求信息。

func handler(w http.ResponseWriter, r *http.Request) {
    // 请求方法
    method := r.Method // "GET", "POST", "PUT", "DELETE" 等

    // URL 信息
    path := r.URL.Path           // 请求路径,如 "/users/123"
    query := r.URL.Query()       // 查询参数 map
    name := query.Get("name")    // 获取单个查询参数

    // 请求头
    contentType := r.Header.Get("Content-Type")
    userAgent := r.Header.Get("User-Agent")

    // 解析表单数据(Content-Type: application/x-www-form-urlencoded)
    r.ParseForm()
    username := r.FormValue("username")

    // 解析多部分表单(文件上传)
    r.ParseMultipartForm(10 << 20) // 最大内存 10MB
    file, handler, err := r.FormFile("avatar")
    if err == nil {
        defer file.Close()
        // handler.Filename: 文件名
        // handler.Size: 文件大小
        // handler.Header: 文件头信息
    }

    // 请求上下文
    ctx := r.Context()
    requestID := ctx.Value("requestID")
}

http.ResponseWriter 详解

http.ResponseWriter

http.ResponseWriter 用于构造 HTTP 响应,支持设置状态码、响应头和响应体。

func handler(w http.ResponseWriter, r *http.Request) {
    // 设置响应头(必须在 WriteHeader 之前调用)
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.Header().Set("X-Custom-Header", "my-value")

    // 设置状态码(默认为 200,调用后不能再修改头)
    w.WriteHeader(http.StatusCreated) // 201

    // 写入响应体
    w.Write([]byte(`{"message": "created"}`))
}

处理查询参数与表单数据

查询参数(Query Parameters)

// GET /search?q=golang&page=1&limit=10
func searchHandler(w http.ResponseWriter, r *http.Request) {
    q := r.URL.Query()

    keyword := q.Get("q")         // "golang"
    page := q.Get("page")         // "1"(字符串类型,需手动转换)
    limit := q.Get("limit")       // "10"

    // 获取多值参数
    // GET /tags?tag=go&tag=web
    tags := q["tag"] // []string{"go", "web"}

    // 带默认值的获取方式
    if page == "" {
        page = "1"
    }
}

表单数据(Form Data)

func formHandler(w http.ResponseWriter, r *http.Request) {
    // 必须先调用 ParseForm 或 ParseMultipartForm
    err := r.ParseForm()
    if err != nil {
        http.Error(w, "解析表单失败", http.StatusBadRequest)
        return
    }

    // 获取表单值
    username := r.FormValue("username")
    password := r.FormValue("password")

    // PostForm 只包含 POST 请求体中的数据,不含 URL query
    email := r.PostFormValue("email")

    fmt.Fprintf(w, "用户名: %s", username)
}

JSON 请求体

import "encoding/json"

type CreateUserRequest struct {
    Username string `json:"username"`
    Email    string `json:"email"`
    Age      int    `json:"age"`
}

func createUserHandler(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest

    // 解析 JSON 请求体
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "无效的请求数据", http.StatusBadRequest)
        return
    }

    // 使用请求参数...
    fmt.Fprintf(w, "创建用户: %s (%d岁)", req.Username, req.Age)
}

文件上传处理

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "path/filepath"
    "time"
)

func uploadHandler(w http.ResponseWriter, r *http.Request) {
    // 限制请求体大小(10MB)
    r.Body = http.MaxBytesReader(w, r.Body, 10<<20)

    if err := r.ParseMultipartForm(10 << 20); err != nil {
        http.Error(w, "文件太大", http.StatusRequestEntityTooLarge)
        return
    }

    file, header, err := r.FormFile("file")
    if err != nil {
        http.Error(w, "获取文件失败", http.StatusBadRequest)
        return
    }
    defer file.Close()

    // 生成安全的文件名
    ext := filepath.Ext(header.Filename)
    filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)

    // 保存到指定目录
    dst, err := os.Create(filepath.Join("./uploads", filename))
    if err != nil {
        http.Error(w, "保存文件失败", http.StatusInternalServerError)
        return
    }
    defer dst.Close()

    if _, err := io.Copy(dst, file); err != nil {
        http.Error(w, "写入文件失败", http.StatusInternalServerError)
        return
    }

    fmt.Fprintf(w, "文件上传成功: %s (%d bytes)", header.Filename, header.Size)
}

对应的 HTML 上传表单:

<form method="POST" action="/upload" enctype="multipart/form-data">
    <input type="file" name="file" />
    <button type="submit">上传</button>
</form>
func setCookieHandler(w http.ResponseWriter, r *http.Request) {
    // 设置 Cookie
    cookie := &http.Cookie{
        Name:     "username",
        Value:    "zhangsan",
        Path:     "/",
        MaxAge:   3600,             // 1小时(秒)
        HttpOnly: true,             // 禁止 JavaScript 访问
        Secure:   true,             // 仅 HTTPS 传输
        SameSite: http.SameSiteLaxMode,
    }
    http.SetCookie(w, cookie)

    fmt.Fprint(w, "Cookie 已设置")
}

func getCookieHandler(w http.ResponseWriter, r *http.Request) {
    // 读取 Cookie
    cookie, err := r.Cookie("username")
    if err != nil {
        http.Error(w, "Cookie 不存在", http.StatusNotFound)
        return
    }
    fmt.Fprintf(w, "用户名: %s", cookie.Value)
}

func deleteCookieHandler(w http.ResponseWriter, r *http.Request) {
    // 删除 Cookie(将 MaxAge 设为 -1)
    cookie := &http.Cookie{
        Name:   "username",
        Value:  "",
        MaxAge: -1,
    }
    http.SetCookie(w, cookie)
    fmt.Fprint(w, "Cookie 已删除")
}

简易 Session 实现

package main

import (
    "crypto/rand"
    "encoding/hex"
    "fmt"
    "net/http"
    "sync"
    "time"
)

type Session struct {
    Data     map[string]interface{}
    ExpireAt time.Time
}

type SessionManager struct {
    mu       sync.RWMutex
    sessions map[string]*Session
}

func NewSessionManager() *SessionManager {
    sm := &SessionManager{
        sessions: make(map[string]*Session),
    }
    go sm.cleanup() // 后台清理过期 session
    return sm
}

func (sm *SessionManager) generateID() string {
    b := make([]byte, 16)
    rand.Read(b)
    return hex.EncodeToString(b)
}

func (sm *SessionManager) Get(r *http.Request) (*Session, error) {
    cookie, err := r.Cookie("session_id")
    if err != nil {
        return nil, err
    }

    sm.mu.RLock()
    defer sm.mu.RUnlock()
    session, ok := sm.sessions[cookie.Value]
    if !ok || time.Now().After(session.ExpireAt) {
        return nil, fmt.Errorf("session not found or expired")
    }
    return session, nil
}

func (sm *SessionManager) Create(w http.ResponseWriter) string {
    sm.mu.Lock()
    defer sm.mu.Unlock()

    id := sm.generateID()
    sm.sessions[id] = &Session{
        Data:     make(map[string]interface{}),
        ExpireAt: time.Now().Add(30 * time.Minute),
    }

    http.SetCookie(w, &http.Cookie{
        Name:  "session_id",
        Value: id,
        Path:  "/",
    })
    return id
}

func (sm *SessionManager) cleanup() {
    ticker := time.NewTicker(1 * time.Minute)
    for range ticker.C {
        sm.mu.Lock()
        for id, session := range sm.sessions {
            if time.Now().After(session.ExpireAt) {
                delete(sm.sessions, id)
            }
        }
        sm.mu.Unlock()
    }
}

静态文件服务

http.FileServer

http.FileServer 返回一个使用根目录文件系统提供文件服务的 http.Handler。配合 http.StripPrefix 可以将 URL 前缀映射到文件系统目录。

package main

import (
    "log"
    "net/http"
)

func main() {
    mux := http.NewServeMux()

    // 静态文件服务
    // 访问 /static/css/style.css → 读取 ./public/css/style.css
    fileServer := http.FileServer(http.Dir("./public"))
    mux.Handle("/static/", http.StripPrefix("/static/", fileServer))

    // 单页应用(SPA)配置:所有未匹配路由返回 index.html
    spaHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        http.ServeFile(w, r, "./public/index.html")
    })
    mux.Handle("/", spaHandler)

    log.Println("服务器启动于 :8080")
    http.ListenAndServe(":8080", mux)
}

练习题

练习 1:实现一个请求信息回显 API

编写一个 HTTP 处理器,接收任何 HTTP 请求,将请求的方法、URL、Header、请求体等信息以 JSON 格式返回。

参考答案
package main

import (
    "encoding/json"
    "io"
    "net/http"
)

type EchoResponse struct {
    Method  string            `json:"method"`
    URL     string            `json:"url"`
    Header  map[string]string `json:"header"`
    Body    string            `json:"body"`
}

func echoHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)

    // 提取所有请求头
    header := make(map[string]string)
    for k, v := range r.Header {
        if len(v) > 0 {
            header[k] = v[0]
        }
    }

    resp := EchoResponse{
        Method: r.Method,
        URL:    r.URL.String(),
        Header: header,
        Body:   string(body),
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}

func main() {
    http.HandleFunc("/echo", echoHandler)
    http.ListenAndServe(":8080", nil)
}

练习 2:实现图片上传 API

编写一个图片上传处理器,支持上传 JPG/PNG/GIF 格式的图片,限制文件大小为 5MB,保存到 ./uploads 目录,并返回图片的访问 URL。

参考答案
package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "path/filepath"
    "strings"
    "time"
)

var allowedExts = map[string]bool{
    ".jpg": true, ".jpeg": true, ".png": true, ".gif": true,
}

func uploadImageHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "仅支持 POST 请求", http.StatusMethodNotAllowed)
        return
    }

    // 限制 5MB
    r.Body = http.MaxBytesReader(w, r.Body, 5<<20)

    file, header, err := r.FormFile("image")
    if err != nil {
        http.Error(w, "获取文件失败", http.StatusBadRequest)
        return
    }
    defer file.Close()

    // 验证文件扩展名
    ext := strings.ToLower(filepath.Ext(header.Filename))
    if !allowedExts[ext] {
        http.Error(w, "仅支持 JPG/PNG/GIF 格式", http.StatusBadRequest)
        return
    }

    // 创建上传目录
    if err := os.MkdirAll("./uploads", 0755); err != nil {
        http.Error(w, "创建目录失败", http.StatusInternalServerError)
        return
    }

    // 生成唯一文件名
    filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
    dstPath := filepath.Join("./uploads", filename)

    dst, err := os.Create(dstPath)
    if err != nil {
        http.Error(w, "保存文件失败", http.StatusInternalServerError)
        return
    }
    defer dst.Close()

    if _, err := io.Copy(dst, file); err != nil {
        http.Error(w, "写入文件失败", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"url": "/uploads/%s", "filename": "%s"}`, filename, header.Filename)
}

func main() {
    http.HandleFunc("/upload/image", uploadImageHandler)
    http.Handle("/uploads/", http.StripPrefix("/uploads/",
        http.FileServer(http.Dir("./uploads"))))
    http.ListenAndServe(":8080", nil)
}

实现 /login/profile 两个接口。/login 接收用户名密码(表单提交),验证成功后设置 Cookie;/profile 通过 Cookie 识别用户,返回用户信息。

参考答案
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

type UserInfo struct {
    Username string `json:"username"`
    Role     string `json:"role"`
}

// 模拟数据库
var users = map[string]string{
    "admin":     "admin123",
    "zhangsan":  "123456",
}

var userInfos = map[string]UserInfo{
    "admin":    {Username: "admin", Role: "administrator"},
    "zhangsan": {Username: "zhangsan", Role: "member"},
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "仅支持 POST", http.StatusMethodNotAllowed)
        return
    }

    r.ParseForm()
    username := r.FormValue("username")
    password := r.FormValue("password")

    if pwd, ok := users[username]; !ok || pwd != password {
        http.Error(w, `{"error":"用户名或密码错误"}`, http.StatusUnauthorized)
        return
    }

    http.SetCookie(w, &http.Cookie{
        Name:     "session_user",
        Value:    username,
        Path:     "/",
        MaxAge:   86400,
        HttpOnly: true,
    })

    w.Header().Set("Content-Type", "application/json")
    fmt.Fprint(w, `{"message":"登录成功"}`)
}

func profileHandler(w http.ResponseWriter, r *http.Request) {
    cookie, err := r.Cookie("session_user")
    if err != nil {
        http.Error(w, `{"error":"未登录"}`, http.StatusUnauthorized)
        return
    }

    info, ok := userInfos[cookie.Value]
    if !ok {
        http.Error(w, `{"error":"用户不存在"}`, http.StatusNotFound)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(info)
}

func main() {
    http.HandleFunc("/login", loginHandler)
    http.HandleFunc("/profile", profileHandler)
    http.ListenAndServe(":8080", nil)
}

搜索