You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Golang 1.22中net/http子路由末尾斜杠重定向问题

问题:Golang net/http子路由兼容末尾斜杠问题

我用Golang标准库net/http实现简易API的子路由,遇到一个问题:用户注册路由是POST /v1/users/,但发送POST /v1/users请求时会返回404错误,必须加末尾斜杠才能正常访问。我希望这个路由能同时兼容有无末尾斜杠的请求,求修复方案。另外根据官方文档,POST /v1/users应该被重定向到POST /v1/users/,这个理解对吗?


代码示例

main.go

package main

import (
    "log/slog"
    "net/http"
    "os"

    routes "go-url-shortener-api/api/v1"
    "go-url-shortener-api/internal/config"
    "go-url-shortener-api/pkg/logger"
)

func main() {
    config, err := config.LoadConfig()
    if err != nil {
        slog.Error("Error loading config", "error", err)
        os.Exit(1)
    }

    router := routes.RegisterV1Routes()
    server := http.Server{
        Addr:    ":" + config.Port,
        Handler: router,
    }

    logger.Logger.Info("Starting server", slog.String("port", config.Port))
    server.ListenAndServe()
}

api/v1.go

package routes

import "net/http"

func RegisterV1Routes() http.Handler {
    router := http.NewServeMux()

    router.Handle("/v1/", http.StripPrefix("/v1", RegisterRoutes()))

    return router
}

api/routes.go

package routes

import "net/http"

func RegisterRoutes() http.Handler {
    router := http.NewServeMux()

    router.Handle("/users/", http.StripPrefix("/users", RegisterUserRoutes()))

    return router
}

api/user_routes.go

package routes

import "net/http"

func RegisterUserRoutes() http.Handler {
    router := http.NewServeMux()

    router.HandleFunc("POST /", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("<p>POST /users</p>"))
    })
    router.HandleFunc("POST /login", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("<p>POST /users/login</p>"))
    })
    router.HandleFunc("POST /logout", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("<p>POST /users/logout</p>"))
    })

    return router
}

解答

关于重定向的理解

不对。Golang的http.ServeMux默认仅对GET请求的无末尾斜杠路径执行重定向(到带斜杠的版本),POST、PUT等其他方法的请求不会自动触发重定向,直接返回404,这是标准库的既定行为。

修复方案

方案1:同时注册带/不带斜杠的路由

在对应的ServeMux中添加无斜杠路径的映射,让其指向同一个处理器。修改api/routes.go:

func RegisterRoutes() http.Handler {
    router := http.NewServeMux()
    userHandler := http.StripPrefix("/users", RegisterUserRoutes())
    // 同时注册带斜杠和不带斜杠的路由
    router.Handle("/users/", userHandler)
    router.Handle("/users", userHandler)
    return router
}

此方法直接让两种路径都匹配到目标处理器,无需客户端跳转。

方案2:全局重定向中间件

如果需要批量处理所有类似场景,可以编写中间件,自动将无末尾斜杠的非GET请求重定向到带斜杠的路径(用307状态码保留原请求方法):

import "strings"

func TrailingSlashRedirect(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 排除根路径、已带斜杠的路径、以及带文件后缀的路径
        if r.URL.Path != "/" && !strings.HasSuffix(r.URL.Path, "/") && !strings.Contains(r.URL.Path, ".") {
            newPath := r.URL.Path + "/"
            // 307临时重定向,保持原请求方法
            http.Redirect(w, r, newPath, http.StatusTemporaryRedirect)
            return
        }
        next.ServeHTTP(w, r)
    })
}

然后在main.go中为服务器绑定中间件:

router := routes.RegisterV1Routes()
server := http.Server{
    Addr:    ":" + config.Port,
    Handler: TrailingSlashRedirect(router),
}

此方案全局生效,无需逐个修改路由注册。

方案3:直接在子路由中注册双路径

在user_routes.go中为注册逻辑添加无斜杠的路径匹配:

func RegisterUserRoutes() http.Handler {
    router := http.NewServeMux()
    registerHandler := func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("<p>POST /users</p>"))
    }
    // 同时处理带斜杠和不带斜杠的根路径
    router.HandleFunc("POST /", registerHandler)
    router.HandleFunc("POST", registerHandler)
    
    // 其他路由保持不变
    router.HandleFunc("POST /login", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("<p>POST /users/login</p>"))
    })
    router.HandleFunc("POST /logout", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("<p>POST /users/logout</p>"))
    })
    return router
}

适合路由层级较少的场景,多层嵌套时推荐前两种方案。


内容的提问来源于stack exchange,提问作者Rohit Patil

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 14:14:54