3. Gin RESTful API 開發(fā)
3.1 RESTful API簡介
3.1.1 RESTful API 定義
? ? REST(Representational State Transfer,表現(xiàn)層狀態(tài)轉換)是一種軟件架風格、設計風格,而不是一種標準。它提供了一組設計原則和約束條件,主要用于客戶端和服務端交互類的軟件。基于這個風格設計的軟件可以更簡潔、具有層次。
? ? REST 并沒有一個明確的標準,更像是一種設計風格,滿足這種設計風格的程序或接口稱之為RESTful,因此RESTful API就是滿足REST風格的接口。
? ? RESTful 風格的程序或API需要遵循一些約束條件和規(guī)則,其主要特點如下所示:
- 以資源為基礎
? ? 資源可以是圖片、音樂、視頻等,它是網(wǎng)絡上的一個實體。
- 統(tǒng)一接口
? ? 對資源的操作包括查詢、創(chuàng)建、修改和刪除,這些操作分別對應于HTTP協(xié)議中的GET、POST、PUT、DELETE方法。即使用RESTful風格的接口單從接口上只能定位其資源,但無法知道它具體進行了什么操作,需要具體了解其發(fā)生了什么操作,要從HTTP請求方法類型上進行判斷。
3.1.2 RESTful API 設計規(guī)范
? ? 遵循RESTful風格的設計規(guī)范主要如下所示:
- 使用斜杠(
/)表示層級關系且層級不要過深 - 不要在URI尾部使用斜杠(
/) - 使用連字符(
-)來提高URI可讀性,而不使用橫線(-) - 在URI中使用小寫英文字母
- 不要使用文件擴展名
- URL中不要使用動詞,使用請求方式表示動作
- 資源表示使用復數(shù)
- 使用查詢參數(shù)過濾URI集合
3.2 API設計與實現(xiàn)
3.2.1 前后端分離
? ? 前后端分離是指軟件設計中用戶界面(前端)和后端邏輯、數(shù)據(jù)存儲等的分離,前后端通信通過API進行通信。
- 前端可以看作是直接與用戶進行交互的部分,前端負責收集用戶的輸入和展現(xiàn)輸出結果
- 后端一般是指在后臺服務器上運行的程序,包含后端邏輯處理、數(shù)據(jù)存儲和一些其他依賴的組件等。
? ? 前后端分離所帶來的好處如下所示:
- 模塊化:前后端分離,可以獨立處理每個部分、從而實現(xiàn)模塊化,使得維護起來更容易
- 可擴展性:前后端分離允許更大的可擴展性,使得每個部分都可以根據(jù)程序的需要獨立擴展
- 可復用性:前后端分離可以使得每個部分在不同的程序或上下文中進行復用
- 安全性:前后端分離有助于提高安全性,更好的控制對敏感數(shù)據(jù)或功能的訪問
- 可屏蔽前后端技術棧所帶來的差異:前后端關注的技術領域不同,采用前后端分離后,可以使得各自關注自身領域內(nèi)的技術棧
3.2.2 設計RESTful API
? ? 在前后端分離的架構中,RESTful API是連接前后端的重要橋梁,一個清晰且規(guī)范的RESTful API可以有效幫助前后端開發(fā)人員進行查詢數(shù)據(jù)、執(zhí)行操作、相互通信等。主要包含以下幾個部分。
1.資源與URI設計
? ? 在RESTful API中,資源是要操作的對象,而URI是資源的唯一標識。一個良好的API通常使用名詞表示資源,并盡量保持簡潔清晰。
- 資源名稱:應使用復數(shù)名詞來表示資源集合,例如使用
/users表示所有用戶 - 資源標識符:一個具體的資源應使用唯一標識符來表示,例如
/users/{id}表示為特定ID的用戶
2.HTTP方法設計
? ? 不要在URI中使用動詞,而是要依賴于HTTP方法來表示資源的操作類型。
GET /users # 獲取所有用戶
POST /users # 創(chuàng)建一個新的用戶
PUT /users/{id} # 更新指定ID的用戶
DELETE /users/{id} # 刪除指定ID的用戶
3.請求與響應格式
? ? RESTful API 通常使用JSON來進行數(shù)據(jù)交換。前端可以通過API獲取數(shù)據(jù),后端則根據(jù)請求返回響應數(shù)據(jù)。
- 請求格式:前端發(fā)送的請求數(shù)據(jù)需要放在請求體中傳遞
- 響應格式:后端根據(jù)請求返回響應數(shù)據(jù),需要包含請求結果和相應的錯誤信息
4.狀態(tài)碼
? ? 狀態(tài)碼用于表示API請求的結果,遵循HTTP協(xié)議中的狀態(tài)碼。
5.分頁與過濾
? ? 當資源數(shù)據(jù)量比較大時,常常需要支持分頁和過濾功能。通過在API中加入分頁參數(shù)和過濾條件可以有效減小前端一次性加載大量數(shù)據(jù)的負擔。
GET /users?page_num=1&limit=20
GET /users?status=actived
6.版本管理
? ? 為保證新舊版本的API兼容性,通過在API的URI中加入相應的版本號以方便管理,示例如下所示:
GET /v1/users
GET /v2/users
7.安全性與身份驗證
? ? 在目前的Web環(huán)境中,API的安全性也日益重要,常用和身份驗證如下所示:
- API Key: 通過請求頭可查詢參數(shù)傳遞API密鑰進行身份驗證
- JWT(JSON Web Token):通過Bear Token機制在請求頭中傳遞JWT進行身份驗證
- OAuth: 用于第三方授權驗證
8.錯誤響應
? ? API 應當能夠返回明確的錯誤信息,以幫助客戶端處理異常情況。通常錯誤信息需要包含狀態(tài)碼、錯誤信息或錯誤詳細信息。
3.3 RESTful API案例
? ? 以下將演示通過接口從數(shù)據(jù)庫中查詢、創(chuàng)建、更新和刪除用戶。
3.3.1 路由設計
{
users := r.Group("/api/v2/users")
users.GET("/", controller.GetUsers)
users.GET("/:id", controller.GetUsersById)
users.POST("/", controller.CreateUsers)
users.PUT("/:id", controller.UpdateUsers)
users.DELETE("/:id", controller.DeleteUsersById)
}
3.3.2 數(shù)據(jù)庫設計
CREATE DATABASE IF NOT EXISTS restful_api_db;
USE restful_api_db;
CREATE TABLE IF NOT EXISTS users (
id INT NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL ,
age INT DEFAULT NULL,
location varchar(255) DEFAULT NULL,
passwd varchar(255) NOT NULL,
PRIMARY KEY (id)
)
ENGINE=InnoDB ,
CHARACTER SET utf8mb4,
COLLATE utf8mb4_0900_ai_ci;
3.3.3 模型代碼
package models
// 數(shù)據(jù)庫對應的結構體
type User struct {
Id int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
Location string `json:"location"`
Passwd string `json:"passwd"`
}
3.3.4 邏輯代碼
- 查詢用戶
package controller
func GetUsers(ctx *gin.Context) {
var queryUsers []models.User
var responseUsers []models.User
// 從數(shù)據(jù)庫中查詢數(shù)據(jù)
db.ConnectDB().Find(&queryUsers)
if len(queryUsers) <= 0 {
ctx.JSON(
http.StatusNotFound,
gin.H{
"status_code": http.StatusNotFound,
"message": "查詢結果為空",
})
return
}
for _, v := range queryUsers {
responseUsers = append(responseUsers, models.User{
Id: v.Id,
Name: v.Name,
Age: v.Age,
Location: v.Location,
})
}
ctx.JSON(
http.StatusOK,
gin.H{
"status_code": http.StatusOK,
"count": len(responseUsers),
"data": responseUsers,
})
}
- 按用戶ID查詢
func GetUsersById(ctx *gin.Context) {
var requestUsers models.User
id := ctx.Param("id")
// 從數(shù)據(jù)庫查詢指定ID的用戶
db.ConnectDB().Where("id = ?", id).Find(&requestUsers)
result := models.User{
Id: requestUsers.Id,
Name: requestUsers.Name,
Age: requestUsers.Age,
Location: requestUsers.Location,
}
ctx.JSON(
http.StatusOK,
gin.H{
"status_code": http.StatusOK,
"data": result,
})
}
- 創(chuàng)建用戶
func CreateUsers(ctx *gin.Context) {
name := ctx.PostForm("name")
age, err := strconv.Atoi(ctx.PostForm("age"))
if err != nil {
log.Panicf("解析年齡出錯:%v", err)
return
}
location := ctx.PostForm("location")
passwd := ctx.PostForm("passwd")
requestUsers := models.User{
Name: name,
Age: age,
Location: location,
Passwd: passwd,
}
// 保存數(shù)據(jù)到數(shù)據(jù)庫
db.ConnectDB().Create(&requestUsers)
ctx.JSON(
http.StatusOK,
gin.H{
"status_code": http.StatusOK,
"message": fmt.Sprintf("用戶%s創(chuàng)建成功", requestUsers.Name),
})
}
- 更新用戶
func UpdateUsers(ctx *gin.Context) {
var requestUsers models.User
id := ctx.Param("id")
age, err := strconv.Atoi(ctx.PostForm("age"))
if err != nil {
log.Printf("解析年齡出錯:%s", err)
} else {
db.ConnectDB().Model(&requestUsers).Where("id = ? ", id).Update("age", age)
}
location := ctx.PostForm("location")
passwd := ctx.PostForm("passwd")
if location != "" {
db.ConnectDB().Model(&requestUsers).Where("id = ? ", id).Update("location", location)
}
if passwd != "" {
db.ConnectDB().Model(&requestUsers).Where("id = ? ", id).Update("passwd", passwd)
}
ctx.JSON(
http.StatusOK,
gin.H{
"status_code": http.StatusOK,
"message": fmt.Sprintf("ID=%s的用戶更新成功", id),
})
}
- 刪除用戶
func DeleteUsersById(ctx *gin.Context) {
id := ctx.Param("id")
// 刪除指定ID的用戶
db.ConnectDB().Delete(&models.User{}, id)
ctx.JSON(
http.StatusOK,
gin.H{
"status_code": http.StatusOK,
"message": fmt.Sprintf("ID=%s的用戶刪除成功", id),
})
}
- 數(shù)據(jù)庫邏輯
func getDsn() string {
mysqlHost := os.Getenv("MYSQL_HOST")
mysqlPort := os.Getenv("MYSQL_PORT")
mysqlUsername := os.Getenv("MYSQL_USERNAME")
mysqlPasswd := os.Getenv("MYSQL_PASSWD")
dbName := "restful_api_db"
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)
return dsn
}
var db *gorm.DB
var err error
func ConnectDB() *gorm.DB {
dsn := getDsn()
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("連接數(shù)據(jù)庫錯誤:%+v", err)
}
return db
}
3.4 RESTful API 測試
? ? 在以上接口完成后,即可以使用API接口工具,例如Postman、Apifox、cURL等工具進行測試。