Golang中API版本控制方法探讨
Golang中API版本控制方法探讨
API版本控制的传统方法是使用
/api/v1/endpoint、/api/v2/endpoint
我将此理解为这是一个API,只是具有不同的路由,对吗?
那么,另一种版本控制方法是使用多个API(Go可执行文件),通过子域名进行分离,这种方法如何?例如:
api1.domain/endpoint, api2.domain/endpoint
这种子域名方法在未来会引起任何问题吗?
func main() {
fmt.Println("hello world")
}
更多关于Golang中API版本控制方法探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
jarrodhroberson: 这会导致维护问题,是的。
怎么导致?请解释。
你好,
它运行得很好,其他方法也一样。
我的观点是……“选择对你来说有效的方法” 
jarrodhroberson:
这是对公共API进行版本控制的正确方式 使用Accept头部进行版本控制
这个头部可以用来管理子域名吗?
Accept: application/api2.example+json
hollowaykeanho: 我们很快就要离题太远,并且陷入太深的兔子洞了。
我主要的问题是是否使用子域名方法进行版本控制。我的结论是继续使用子域名,因为据我理解,这种方法似乎没有重大的缺点。
感谢大家的所有贡献。
是否有可能在升级版本后,信息会被黑客窃取,或者会出现一些错误报告。我过去十天一直在处理这个问题,它显示了一些错误代码EduHelpHub。我不确定是我的信息泄露了,还是这完全是一些错误报告的问题。
hollowaykeanho:
我猜你已经在测试中很自然地把它应用到你的防火墙过滤规则里了。
如果我正确理解了你的评论,我不需要防火墙过滤,因为我使用 Nginx 来代理 API 端口。只开放 443 和 80 端口。有更好的方法来做这件事吗?我理解得对吗?
我能看到的是,使用子域名方法的前提是,你能够方便地管理DNS以推出API版本更新。
是的。我有DNS的管理权限。我已经通过子域名方法,使用不同的端口创建了两个独立的模拟API。所以我认为这没有任何技术问题。
那么,我是否可以这样理解你的回答:只要我运用常识,这就是一种可行的方法?
Massimo.Costa:
没有一种解决方案是最好的;即使“Accept Header”被认为是最佳实践,它也会在路由配置中引发问题(例如,根据头部提供的版本使用不同的处理程序在某些平台/框架中配置起来更困难)
如果我使用“子域名方法”,某个API版本的处理程序是否只负责一个API版本?那么路由应该就不是问题了?这样理解对吗?
实际上并没有绝对的好方法或坏方法。这取决于你想要实现什么目标,而且围绕这个话题有很多优缺点。
然而,使用请求头会迫使你使用另一个中间件来检查版本,如果你有大量请求,这可能会降低速度(路由也会稍慢一些)。另一方面,使用子域名会使你依赖于DNS服务器,而DNS服务器有时可能较慢或宕机,如果你追求高可用性,这就不是很好。硬编码路由很快,但需要更改代码,等等。 你必须从你的特定用例的角度来审视。
我的两点看法。 没有一种解决方案是绝对最佳的;即使“Accept Header”被认为是业界最佳实践,它也会在路由配置中引发问题(例如,在某些平台/框架中,根据请求头中提供的版本使用不同的处理程序会更难配置)。
正如其他人所说,这就是为什么许多在线服务(GitHub、Twitter 等)在 URL 中指定版本。
我的建议是:
- 检查你是否有约束条件(需求)
- 检查你想使用的工具是否有局限性(或者,如果它不支持你必须使用的版本控制方案,就选择其他工具)
- 选择你更喜欢的方案
我认为这里没有“一刀切”的解决方案。只要您确定始终希望同步发布所有API的新版本,那么您使用 v1.mydomain.com 和 v2.mydomain.com 的策略就没有任何问题。我不太了解您的具体用例,但我能预见到可能只想发布某个API的新主版本,而将其他API留在旧版本的情况。只要不是这种情况,我认为您的方法似乎是合理的。
像Jarrod建议的那样使用请求头也是个好方法,但我有时更喜欢在URI中就能轻松看到正在使用的API版本,而不必去检查请求头。在我看来,这能让版本分离更加明显。
这是为公共API进行版本控制的正确方法
使用Accept标头进行版本控制
内容协商可以让你保持一组简洁的URL,但你仍然需要在某个地方处理提供不同版本内容的复杂性。这个负担往往会被转移到你的API控制器上,它们需要负责确定发送哪个版本的资源。最终结果往往是API变得更加复杂,因为客户端在请求资源之前必须知道要指定哪些标头。
例如:
Accept: application/vnd.example.v1+json
Accept: application/vnd.example+json;version=1.0
在现实世界中,API永远不会完全稳定。因此,如何管理这些变更非常重要。对于大多数API来说,一个文档完善且逐步弃用的API实践是可以接受的。
Sibert:
我理解得对吗?
不完全正确。多面向意味着您的1台源服务器同时为 v1.domain.com 和 v2.domain.com 提供服务。我这里提到的防火墙不仅仅是常规的端口限制。有些(硬件或软件)防火墙可以过滤自定义规则。
根据您加强安全性的方式,有些服务器在负载均衡器端(例如 Nginx 或 Apache)或实际的防火墙规则中实施了严格的接口限制。这样做的目的是通过让防火墙仅将 v1.domain.com、v2.domain.com 等列入白名单,来防止意外暴露(例如由初级开发人员或一些粗心的外包承包商导致的 testing.domain.com)。
在您的情况下,现在还为时过早,所以暂时只需记住这个步骤。
编辑:
- 一个著名的例子是 Cloudflare 的防火墙规则,如果您使用他们的 CDN+DNS 作为面向外部的服务,它允许自定义模式。
Sibert:
那么我理解你的回答是,只要我运用常识,这就是一个可行的方法?
是的,你已经准备好起飞了。记得处理好多面配置(我猜你在测试中已经自然而然地把它应用到防火墙过滤规则里了 😂)。
关键在于“文档 + 沟通”。其他的一切都只是随着时间的推移而持续改进。
仅供参考
@jarrodhroberson 关于请求头的建议,最好在你的 API 的 URI 路径设计保持一致且稳定之后再进行,这更多是为了加固的目的。一个好的指标可能是:在过去大概 10 个版本中,变更只涉及输入/输出的数据模式,而没有对路径进行任何修改。
实现过程也应该是无缝的,因为请求头问题而阻塞 API 调用,可能会严重困扰你的客户支持中心。此外,很多客户(包括我)很少会去深究响应头(你可以用 curl 命令进行客户测试)。
Sibert:
但我即将学习CORS。这会是通过某种密钥来管理多面性的方法吗?
如果我没记错的话,CORS 只对浏览器和 HTML+JavaScript 威胁模型有意义。我认为它在这里不适用(除非你的响应体是浏览器可访问的HTML?!)。
使用密钥来做防火墙的工作有点大材小用。请记住,你仍然需要用自己的认证令牌来处理用户的身份验证和授权。
你可以参考这个页面,根据你的需求来保护 API 服务器:
OWASP API 安全项目 | OWASP 基金会
OWASP API 安全项目位于 OWASP 基金会的主网站上。OWASP 是一个致力于提高软件安全性的非营利性基金会。
有点旧(2019年),但仍然可用。
编辑:另一个:
内容安全策略 | 文章 | web.dev
内容安全策略可以显著降低现代浏览器中跨站脚本攻击的风险和影响。
注意:我必须在这里打住你了,因为我们很快就要离题太远,并且会陷入太深的兔子洞 :grin:。继续用你的测试服务器探索,并在实践中学习会更好。干杯!
Sibert:
我理解这是同一个API具有不同的路由,对吗?
必须查看其文档。否则,我假设是1个API(/endpoint/),2种不同的路由(/api/v2 和 /api/v1),1个版本差异(v2)。
Sibert:
子域名方法在未来会引起任何麻烦吗?
应该不是问题,因为所有4个URI都是唯一的,并且您可以自由地推出以下更新:
- API路径改进(不向后兼容和向后兼容的更改)
- 输入数据模式更改
- 新的API可以拥有自己的缓存,而不会干扰现有API的缓存
- 输出数据模式更改
- 可以推出互不干扰的弃用和移除。
我能看到的是,使用子域名方法假设您可以轻松访问DNS管理以推出API版本更新。对于小型项目或者您就是DNS管理员,这不会是个问题。否则,您需要经过网络部门的一些繁琐流程。
/vX/ 路径方法允许您在相同的网络和DNS设置下工作,因此每次推出API更新时,可以少联系一个部门。
另一个问题是,假设您从一个源服务器提供服务,您需要将服务器配置为多面向以服务多个子域名。但是,如果每个版本都有自己的源服务器集群,那么使用子域名来分离 v1 和 v2 服务器是有意义的。
API版本控制有很多方法。最重要的部分是文档 + 弃用通知、内置弃用警告、与客户的沟通以及宽限期。
在Go中实现API版本控制,子域名方案是可行的生产级方案,但需要权衡架构复杂度。
子域名版本控制的实现示例
// main.go - 主路由分发
package main
import (
"net/http"
"strings"
)
func versionRouter(w http.ResponseWriter, r *http.Request) {
host := strings.Split(r.Host, ":")[0]
switch {
case strings.HasPrefix(host, "v1.api."):
v1Handler(w, r)
case strings.HasPrefix(host, "v2.api."):
v2Handler(w, r)
default:
http.NotFound(w, r)
}
}
func v1Handler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"api": "v1", "endpoint": "` + r.URL.Path + `"}`))
}
func v2Handler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"api": "v2", "endpoint": "` + r.URL.Path + `"}`))
}
func main() {
http.HandleFunc("/", versionRouter)
http.ListenAndServe(":8080", nil)
}
多可执行文件方案
// cmd/api-v1/main.go
package main
import (
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"version": "v1", "data": "users"}`))
})
http.ListenAndServe(":8081", mux)
}
// cmd/api-v2/main.go
package main
import (
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"version": "v2", "data": {"users": []}}`))
})
http.ListenAndServe(":8082", mux)
}
使用反向代理配置(Nginx示例)
server {
listen 80;
server_name v1.api.example.com;
location / {
proxy_pass http://localhost:8081;
}
}
server {
listen 80;
server_name v2.api.example.com;
location / {
proxy_pass http://localhost:8082;
}
}
潜在问题分析
- 证书管理复杂度:每个子域名需要SSL证书,通配符证书可缓解
- DNS配置:需要为每个版本配置DNS记录
- 客户端缓存:不同子域名导致客户端缓存隔离
- CORS配置:需要为每个子域名单独配置CORS
- 监控分散:日志和监控需要聚合多个服务
// 中间件示例:跨版本共享功能
type SharedMiddleware struct {
next http.Handler
}
func (m *SharedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 共享的认证、日志等逻辑
w.Header().Set("X-API-Version", getVersionFromHost(r.Host))
m.next.ServeHTTP(w, r)
}
func getVersionFromHost(host string) string {
parts := strings.Split(host, ".")
if len(parts) > 0 && strings.HasPrefix(parts[0], "v") {
return parts[0]
}
return "unknown"
}
子域名方案适合大型微服务架构,但会增加运维复杂度。路径方案(/api/v1/)在大多数场景下更简单直接。



