英文:
Header-based versioning on golang
问题
我想在Go语言中使用gin实现基于头部的版本控制。我打算在路由器上使用中间件函数来实现这个功能。
客户端将调用相同的API URL,版本信息将包含在自定义的HTTP头部中,如下所示:
调用版本1:
GET /users/12345678
Accept-version: v1
调用版本2:
GET /users/12345678
Accept-version: v2
因此,路由器可以识别头部信息并调用特定的版本。代码示例如下:
router := gin.Default()
v1 := router.Group("/v1")
v1.Use(VersionMiddleware())
v1.GET("/user/:id", func(c *gin.Context) {
c.String(http.StatusOK, "This is the v1 API")
})
v2 := router.Group("/v2")
v2.Use(VersionMiddleware())
v2.GET("/user/:id", func(c *gin.Context) {
c.String(http.StatusOK, "This is the v2 API")
})
func VersionMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
version := c.Request.Header.Get(configuration.GetConfigValue("VersionHeader"))
// 根据版本号构造新的URL路径
path := fmt.Sprintf("/%s%s", version, c.Request.URL.Path)
// 修改请求的URL路径,指向新的版本特定的端点
c.Request.URL.Path = path
c.Next()
}
}
以上是使用gin实现基于头部的版本控制的示例代码。
英文:
I would like to implement header-based versioning on go using gin. I'm thinking to do this on the router using a middleware function.
The client will call the same API URL, and the version will be on a custom HTTP header, like this:
To call version 1
GET /users/12345678
Accept-version: v1
To call version 2:
GET /users/12345678
Accept-version: v2
So, the router can identify the header and call the specific version. Something like this:
router := gin.Default()
v1 := router.Group("/v1")
v1.Use(VersionMiddleware())
v1.GET("/user/:id", func(c *gin.Context) {
c.String(http.StatusOK, "This is the v1 API")
})
v2 := router.Group("/v2")
v2.Use(VersionMiddleware())
v2.GET("/user/:id", func(c *gin.Context) {
c.String(http.StatusOK, "This is the v2 API")
})
func VersionMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
version := c.Request.Header.Get(configuration.GetConfigValue("VersionHeader"))
// Construct the new URL path based on the version number
path := fmt.Sprintf("/%s%s", version, c.Request.URL.Path)
// Modify the request URL path to point to the new version-specific endpoint
c.Request.URL.Path = path
c.Next()
}
}
答案1
得分: 1
请检查下面的代码片段。我使用了__ReverseProxy__来重定向到给定的版本。你需要仔细验证给定的版本,否则会导致递归调用。
注意:我使用了两个版本的/user
GET(/v1/user
和/v2/user
)。
示例代码
package main
import (
"net/http"
"net/http/httputil"
"regexp"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.Use(VersionMiddleware())
v1 := router.Group("/v1")
v1.GET("/user", func(c *gin.Context) {
c.String(http.StatusOK, "This is the v1 API")
})
v2 := router.Group("/v2")
v2.GET("/user", func(c *gin.Context) {
c.String(http.StatusOK, "This is the v2 API")
})
router.Run(":8082")
}
func VersionMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// You need to check c.Request.URL.Path whether
// already have a version or not, If it has a valid
// version, return.
regEx, _ := regexp.Compile("/v[0-9]+")
ver := regEx.MatchString(c.Request.URL.Path)
if ver {
return
}
version := c.Request.Header.Get("Accept-version")
// You need to validate given version by the user here.
// If version is not a valid version, return error
// mentioning that given version is invalid.
director := func(req *http.Request) {
r := c.Request
req.URL.Scheme = "http";
req.URL.Host = r.Host
req.URL.Path = "/" + version + r.URL.Path
}
proxy := &httputil.ReverseProxy{Director: director}
proxy.ServeHTTP(c.Writer, c.Request)
}
}
或者
你可以使用下面的gin包装器实现。
- 示例
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/udayangaac/stackoverflow/golang/75860989/ginwrapper"
)
func main() {
engine := gin.Default()
router := ginwrapper.NewRouter(engine)
defaultRouter := router.Default()
defaultRouter.Get("/profile",func(ctx *gin.Context) {
})
v1 := router.WithVersion("/v1")
v1.Get("/user",func(ctx *gin.Context) {
ctx.String(http.StatusOK, "This is the profile v1 API")
})
v2 := router.WithVersion("/v2")
v2.Get("/user",func(ctx *gin.Context) {
ctx.String(http.StatusOK, "This is the profile v2 API")
})
engine.Run(":8082")
}
- 包装器
package ginwrapper
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
type Router struct {
router *gin.Engine
versionGroups map[string]*gin.RouterGroup
}
type VersionedRouter struct {
version string
Router
}
func NewRouter(router *gin.Engine) *Router {
return &Router{
router: router,
versionGroups: make(map[string]*gin.RouterGroup),
}
}
func (a *Router) Default() VersionedRouter {
return VersionedRouter{Router: *a }
}
func (a *Router) WithVersion(version string) VersionedRouter {
if _,ok := a.versionGroups[version]; ok {
panic("cannot initialize same version multiple times")
}
a.versionGroups[version] = a.router.Group(version)
return VersionedRouter{Router: *a,version:version }
}
func (vr VersionedRouter) Get(relativePath string, handlers ...gin.HandlerFunc) {
vr.handle(http.MethodGet,relativePath,handlers...)
}
// 注意:你需要按照相同的方式处理其他HTTP方法。
// 例如,我们可以为Post HTTP方法编写一个方法,如下所示,
//
// func (vr VersionedRouter) Post(relativePath string, handlers ...gin.HandlerFunc) {
// vr.handle(http.MethodPost,relativePath,handlers...)
// }
func (vr VersionedRouter)handle(method,relativePath string, handlers ...gin.HandlerFunc) {
if !vr.isRouteExist(method,relativePath) {
vr.router.Handle(method,relativePath,func(ctx *gin.Context) {
version := ctx.Request.Header.Get("Accept-version")
if len(version) == 0 {
ctx.String(http.StatusBadRequest,"Accept-version header is empty")
}
ctx.Request.URL.Path = fmt.Sprintf("/%s%s", version, ctx.Request.URL.Path)
vr.router.HandleContext(ctx)
})
}
versionedRelativePath := vr.version + relativePath
if !vr.isRouteExist(method,versionedRelativePath) {
vr.router.Handle(method,versionedRelativePath,handlers... )
}
}
func (a VersionedRouter) isRouteExist(method,relativePath string) bool {
for _,route := range a.router.Routes() {
if route.Method == method && relativePath == route.Path {
return true
}
}
return false
}
示例请求
/v1/user
curl --location 'localhost:8082/user' \
--header 'Accept-version: v1'
/v2/user
curl --location 'localhost:8082/user' \
--header 'Accept-version: v2'
英文:
Please check the below code snippet. I used ReverseProxy to redirect to the given version. You need to validate given version carefully. Otherwise, it will cause a recursive call.
Note: I used two versions of /user
GET (/v1/user
and /v2/user
).
Sample Code
package main
import (
"net/http"
"net/http/httputil"
"regexp"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.Use(VersionMiddleware())
v1 := router.Group("/v1")
v1.GET("/user", func(c *gin.Context) {
c.String(http.StatusOK, "This is the v1 API")
})
v2 := router.Group("/v2")
v2.GET("/user", func(c *gin.Context) {
c.String(http.StatusOK, "This is the v2 API")
})
router.Run(":8082")
}
func VersionMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// You need to check c.Request.URL.Path whether
// already have a version or not, If it has a valid
// version, return.
regEx, _ := regexp.Compile("/v[0-9]+")
ver := regEx.MatchString(c.Request.URL.Path)
if ver {
return
}
version := c.Request.Header.Get("Accept-version")
// You need to validate given version by the user here.
// If version is not a valid version, return error
// mentioning that given version is invalid.
director := func(req *http.Request) {
r := c.Request
req.URL.Scheme = "http"
req.URL.Host = r.Host
req.URL.Path = "/"+ version + r.URL.Path
}
proxy := &httputil.ReverseProxy{Director: director}
proxy.ServeHTTP(c.Writer, c.Request)
}
}
OR
You can use below wrapper implementation for gin.
- Example
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/udayangaac/stackoverflow/golang/75860989/ginwrapper"
)
func main() {
engine := gin.Default()
router := ginwrapper.NewRouter(engine)
defaultRouter := router.Default()
defaultRouter.Get("/profile",func(ctx *gin.Context) {
})
v1 := router.WithVersion("/v1")
v1.Get("/user",func(ctx *gin.Context) {
ctx.String(http.StatusOK, "This is the profile v1 API")
})
v2 := router.WithVersion("/v2")
v2.Get("/user",func(ctx *gin.Context) {
ctx.String(http.StatusOK, "This is the profile v2 API")
})
engine.Run(":8082")
}
- Wrapper
package ginwrapper
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
type Router struct {
router *gin.Engine
versionGroups map[string]*gin.RouterGroup
}
type VersionedRouter struct {
version string
Router
}
func NewRouter(router *gin.Engine) *Router {
return &Router{
router: router,
versionGroups: make(map[string]*gin.RouterGroup),
}
}
func (a *Router) Default() VersionedRouter {
return VersionedRouter{Router: *a }
}
func (a *Router) WithVersion(version string) VersionedRouter {
if _,ok := a.versionGroups[version]; ok {
panic("cannot initialize same version multiple times")
}
a.versionGroups[version] = a.router.Group(version)
return VersionedRouter{Router: *a,version:version }
}
func (vr VersionedRouter) Get(relativePath string, handlers ...gin.HandlerFunc) {
vr.handle(http.MethodGet,relativePath,handlers...)
}
// Note: You need to follow the same for other HTTP Methods.
// As an example, we can write a method for Post HTTP Method as below,
//
// func (vr VersionedRouter) Post(relativePath string, handlers ...gin.HandlerFunc) {
// vr.handle(http.MethodPost,relativePath,handlers...)
// }
func (vr VersionedRouter)handle(method,relativePath string, handlers ...gin.HandlerFunc) {
if !vr.isRouteExist(method,relativePath) {
vr.router.Handle(method,relativePath,func(ctx *gin.Context) {
version := ctx.Request.Header.Get("Accept-version")
if len(version) == 0 {
ctx.String(http.StatusBadRequest,"Accept-version header is empty")
}
ctx.Request.URL.Path = fmt.Sprintf("/%s%s", version, ctx.Request.URL.Path)
vr.router.HandleContext(ctx)
})
}
versionedRelativePath := vr.version + relativePath
if !vr.isRouteExist(method,versionedRelativePath) {
vr.router.Handle(method,versionedRelativePath,handlers... )
}
}
func (a VersionedRouter) isRouteExist(method,relativePath string) bool {
for _,route := range a.router.Routes() {
if route.Method == method && relativePath == route.Path {
return true
}
}
return false
}
Sample Requests
/v1/user
curl --location 'localhost:8082/user' \
--header 'Accept-version: v1'
/v2/user
curl --location 'localhost:8082/user' \
--header 'Accept-version: v2'
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论