HTTP 服务

最小可用服务

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello, Go Web!")
    })

    if err := http.ListenAndServe(":8080", nil); err != nil {
        fmt.Println("服务退出:", err)
    }
}

运行后访问 http://localhost:8080 即可看到响应。

Go 1.22+ 增强路由

Go 1.22 为 ServeMux 引入了方法匹配与路径参数,覆盖了此前需要框架才能实现的能力:

package main

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

type Task struct {
    ID    int64  `json:"id"`
    Title string `json:"title"`
    Done  bool   `json:"done"`
}

var tasks = []Task{
    {ID: 1, Title: "学习 Go", Done: false},
    {ID: 2, Title: "部署服务", Done: true},
}

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

    // 精确匹配方法与路径
    mux.HandleFunc("GET /api/tasks", listTasks)
    mux.HandleFunc("POST /api/tasks", createTask)

    // 路径参数:{id} 会被解析并可通过 r.PathValue("id") 获取
    mux.HandleFunc("GET /api/tasks/{id}", getTask)

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

func listTasks(w http.ResponseWriter, r *http.Request) {
    writeJSON(w, http.StatusOK, tasks)
}

func createTask(w http.ResponseWriter, r *http.Request) {
    var t Task
    if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
        http.Error(w, "请求体不合法", http.StatusBadRequest)
        return
    }
    t.ID = int64(len(tasks) + 1)
    tasks = append(tasks, t)
    writeJSON(w, http.StatusCreated, t)
}

func getTask(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    for _, t := range tasks {
        if fmt.Sprintf("%d", t.ID) == id {
            writeJSON(w, http.StatusOK, t)
            return
        }
    }
    http.Error(w, "任务不存在", http.StatusNotFound)
}

func writeJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(status)
    _ = json.NewEncoder(w).Encode(v)
}
Tip

路由语法要点:

  • "GET /api/tasks" 限定 HTTP 方法,未匹配方法返回 405 Method Not Allowed
  • /{id} 匹配单段路径参数;/{path...} 匹配剩余全部路径(通配)。
  • / 结尾的注册路径(如 /static/)作为子树前缀匹配。

JSON 编解码细节见 JSON 处理章节。

中间件

中间件是包装 http.Handler 的函数,用于在业务逻辑前后插入横切逻辑(日志、恢复 panic、认证等):

package main

import (
    "log"
    "net/http"
    "time"
)

// 中间件:函数接收并返回 http.Handler
func logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r) // 调用链中的下一个处理器
        log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
    })
}

// 中间件:recover panic,避免单请求崩溃拖垮服务
func recoverPanic(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Println("panic:", err)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /ping", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("pong"))
    })

    // 链式包裹:请求依次经过 recover -> logging -> mux
    handler := recoverPanic(logging(mux))
    log.Fatal(http.ListenAndServe(":8080", handler))
}

优雅退出

生产服务需要处理 SIGTERM/SIGINT 信号,完成存量请求后再退出:

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("ok"))
    })

    srv := &http.Server{
        Addr:              ":8080",
        Handler:           mux,
        ReadHeaderTimeout: 5 * time.Second,  // 防止慢连接攻击
        ReadTimeout:       10 * time.Second,
        WriteTimeout:      10 * time.Second,
    }

    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatal("服务异常退出:", err)
        }
    }()

    // 阻塞等待终止信号
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    // 给存量请求 10 秒完成时间
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Println("强制退出:", err)
    }
}

超越标准库

当路由规则、参数校验、文档生成等需求增长时,可引入框架:

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/api/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        c.JSON(200, gin.H{"id": id})
    })
    r.Run(":8080")
}

小结

  • Go 1.22+ 的 ServeMux 支持方法匹配与路径参数,多数场景无需框架。
  • 中间件通过包装 http.Handler 实现横切逻辑,注意 recover 兜底。
  • 生产服务应配置超时并实现信号驱动的优雅退出。