基于头部的版本控制在Golang中的实现

huangapple go评论89阅读模式
英文:

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'

huangapple
  • 本文由 发表于 2023年3月28日 06:46:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/75860989.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定