Golang 中的 RESTful API 最佳實踐
【導讀】本文結合 Go Web 再次梳理了 RESTful API 的相關最佳實踐。
RESRful API 已經流行很多年了,我也一直在使用它。最佳實踐也看過不少,但當一個項目完成,再次回顧 / 梳理項目時,會發現很多 API 和規範還是多少有些出入。在這篇文章中,我們結合 Go Web 再次梳理一下 RESTful API 的相關最佳實踐。
示例完整代碼在這裏 https://github.com/razeencheng/demo-go/blob/master/restful-api/main.go
1. 使用 JSON
不管是接收還是返回數據都推薦使用 JSON。
通常返回數據的格式有 JSON 和 XML,但 XML 過於冗長,可讀性差,而且各種語言的解析上也不如 JSON,使用 JSON 的好處,顯而易見。
而接收數據,我們這裏也推薦使用 JSON,對於後端開發而言,入參直接與模型綁定,省去冗長的參數解析能簡化不少代碼,而且 JSON 能更簡單的傳遞一些更復雜的結構等。
正如示例代碼中的這一段,我們以gin
框架爲例。
// HandleLogin doc
func HandleLogin(c *gin.Context) {
param := &LoginParams{}
if err := c.BindJSON(param); err != nil {
c.JSON(http.StatusBadRequest, &Resp{Error: "parameters error"})
return
}
// 做一些校驗
// ...
session := sessions.Default(c)
session.Set(sessionsKey, param.UserID)
session.Save()
c.JSON(http.StatusOK, &Resp{Data: "login succeed"})
}
通過c.BindJSON
, 輕鬆的將入參於模型LoginParams
綁定;通過c.JSON
輕鬆的將數據 JSON 序列化返回。
但所有接口都必須用 JSON 麼?那也未必。比如文件上傳,這時我們使用FormData
比把文件 base64 之類的放到 JSON 裏面更高效。
2. 路徑中不包含動詞
我們的 HTTP 請求方法中已經有GET
,POST
等這些動作了,完全沒有必要再路徑中加上動詞。
我們常用 HTTP 請求方法包括GET
,POST
,PUT
和DELETE
, 這也對應了我們經常需要做的數據庫操作。GET
查找 / 獲取資源,POST
新增資源,PUT
修改資源,DELETE
刪除資源。
如下,這些路徑中沒有任何動詞,簡潔明瞭。
// 獲取文章列表
v1.GET("/articles", HandleGetArticles)
// 發佈文章
v1.POST("/articles", HandlePostArticles)
// 修改文章
v1.PUT("/articles", HandleUpdateArticles)
// 刪除文章
v1.DELETE("/articles/:id", HandleDeleteArticles)
3. 路徑中對應資源用複數
就像我們上面這段代碼,articles
對於的是我們的文章資源,背後就是一張數據庫表articles
, 所以操作這個資源的應該都用複數形式。
4. 次要資源可分層展示
一個博客系統中,最主要的應該是文章了,而評論應該是其子資源,我們可以評論嵌套在它的父資源後面,如:
// 獲取評論列表
v1.GET("/articles/:articles_id/comments", HandleGetComments)
// 添加評論
v1.POST("/articles/:articles_id/comments", HandleAddComments)
// 修改評論
v1.PUT("/articles/:articles_id/comments/:id", HandleUpdateComments)
// 刪除評論
v1.DELETE("/articles/:articles_id/comments/:id", HandleDeleteComments)
那麼,我們需要獲取所有文章的評論怎麼辦?可以這麼寫:
v1.GET("/articles/-/comments", HandleGetComments)
但這也不是決對的,資源雖然有層級關係,但這種層級關係不宜太深,個人感覺兩層最多了,如果超過,可以直接拿出來放在一級。
5. 分頁、排序、過濾
獲取列表時,會使用到分頁、排序過濾。一般:
?page=1&page_size=10 # 指定頁面page與分頁大小page_size
?sort=-create_at,+author # 按照創建時間create_at降序,作者author升序排序
?title=helloworld # 按字段title搜索
6. 統一數據格式
不管是路徑的格式,還是參數的格式,還是返回值的格式建議統一形式。
一般常用的格式有蛇形
,大駝峯
和小駝峯
,個人比較喜歡蛇形
。Anyway, 不管哪種,只要統一即可。
除了參數的命名統一外,返回的數據格式,最好統一,方便前端對接。
如下,我們定義Resp
爲通用返回數據結構,Data
中存放反會的數據,如果出錯,將錯誤信息放在Error
中。
// Resp doc
type Resp struct {
Data interface{} `json:"data"`
Error string `json:"error"`
}
// 登陸成功返回
c.JSON(http.StatusOK, &Resp{Data: "login succeed"})
// 查詢列表
c.JSON(http.StatusOK, &Resp{Data: map[string]interface{}{
"result": tempStorage,
"total": len(tempStorage),
}})
// 參數錯誤
c.JSON(http.StatusBadRequest, &Resp{Error: "parameters error"})
7. 善用 HTTP 狀態碼
HTTP 狀態碼有很多,我們沒有必要也不可能全部用上,常用如下:
-
200 StatusOK - 只有成功請求都返回 200。
-
400 StatusBadRequest - 當出現參數不對,用戶參數校驗不通過時,給出該狀態,並返回 Error
-
401 StatusUnauthorized - 沒有登陸 / 經過認證
-
403 Forbidden - 服務端拒絕授權 (如密碼錯誤),不允許訪問
-
404 Not Found - 路徑不存在
-
500 Internal Server Error - 所請求的服務器遇到意外的情況並阻止其執行請求
-
502 Bad Gateway - 網關或代理從上游接收到了無效的響應
-
503 Service Unavailable - 服務器尚未處於可以接受請求的狀態
其中502
,503
,我們寫程序時並不會明確去拋出。所以我們平常用 6 個狀態碼已經能很好的展示服務端狀態了。
同時,我們將狀態與返回值對應起來,200
狀態下,返回Data
數據;其他狀態返回Error
。
8.API 版本化
正如 Demo 中所示,我們將路由分組到了/api/v1
路徑下面,版本化 API。如果後續的服務端升級,但可能仍有很大部分客戶端請求未升級,依然請求老版本的 API,那麼我們只需要增加/api/v2
,然後在該路徑下爲已升級的客戶端提供服務。這樣,我們就做到了 API 的版本控制,可以平滑的從一個版本切換到另外一個版本。
v1 := r.Group("/api/v1")
{
v1.POST("/login", HandleLogin)
v1.GET("/articles", HandleGetArticles)
v1.GET("/articles/:id/comments", HandleGetComments)
// ....
9. 統一 /
開頭
所以路由中,路徑都以/
開頭,雖然框架會爲我們做這件事,但還是建議統一加上。
10. 增加 / 更新操作 返回資源
對於POST
,PUT
操作,建議操作後,返回更新後的資源。
11. 使用 HTTPS
對於暴露出去的接口/OpenAPI
,一定使用 HTTPS。一般時候,我們可以直接在服務前面架設一個 WebServer,在 WebServer 內部署證書即可。當然,如果是直接由後端暴露出的接口,有必要直接在後端開啓 HTTPS!
12. 規範的 API 文檔
對於我們這種前後端分離的架構,API 文檔是很重要。在 Go 中,我們很容易的能用 swag 結合代碼註釋自動生成 API 文檔。
總結
API 寫的好不好,重要的還是看是否遵循 WEB 標準和保持一致性,最終目的也是讓這些 API 更清晰,易懂,安全,希望這些建議對你有所幫助。
轉自:
razeencheng.com/posts/golang-and-restful-api/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/jUBIPf5kh-giNdeyjP1sKg