Gin 路由注冊與請求參數(shù)獲取
一、Web應(yīng)用開發(fā)的兩種模式
1.前后端不分離模式
- 也叫前后端混合開發(fā)模式, 需要后端寫模板語言(dtl), 返回的是HTML頁面
- 瀏覽器 : 請求動態(tài)頁面
- 后端 : 返回HTML
優(yōu)點:可以直接渲染頁面, 方便處理請求數(shù)據(jù)
缺點:耦合度非常高, 不方便擴展
2.前后端分離模式
- 前端 : 只寫前端
- 后端 : 只專注于寫后端接口, 返回 json, xml格式數(shù)據(jù)
- 流程 :
瀏覽器到靜態(tài)文件服務(wù)器請求靜態(tài)頁面, 靜態(tài)服務(wù)器返回靜態(tài)頁面
JS 請求達(dá)到后端, 后端再返回 JSON 或 XML格式的數(shù)據(jù)
- 優(yōu)點
- 不需要管前端怎么實現(xiàn), 后端開發(fā)者需要做的就是寫接口
- 只需要知道, 你前端傳過來什么, 然后需要后端這邊傳回去什么就行了
- 主要的就是操作邏輯, 解耦合性高
- 缺點
- 程序員不知道前端的具體流程, 然后對表的設(shè)計, 對業(yè)務(wù)或許就理解的沒有那么透徹
- 還存在前后端聯(lián)調(diào)各種問題, 前端和后端的溝通等
二、RESTful介紹
RESTful(Representational State Transfer)代表的是一種基于HTTP協(xié)議設(shè)計的軟件架構(gòu)風(fēng)格,它通常用于構(gòu)建Web服務(wù),是Representational State Transfer的簡稱,中文翻譯為“表征狀態(tài)轉(zhuǎn)移”或“表現(xiàn)層狀態(tài)轉(zhuǎn)化”。RESTful架構(gòu)的設(shè)計理念是將資源表示為URI(統(tǒng)一資源標(biāo)識符),通過HTTP協(xié)議的GET、POST、PUT、DELETE等方法對資源進(jìn)行操作。以下是RESTful架構(gòu)的一些關(guān)鍵特點:
- 資源(Resource):在RESTful架構(gòu)中,所有的數(shù)據(jù)或服務(wù)都被抽象為資源,每個資源都有一個唯一的標(biāo)識符(URI)。
- 表現(xiàn)層(Representation):資源的表現(xiàn)層是指資源在不同的表示形式之間進(jìn)行切換,通常使用JSON或XML格式。客戶端和服務(wù)器之間通過資源的表現(xiàn)層進(jìn)行通信。
- 狀態(tài)轉(zhuǎn)移(State Transfer):RESTful架構(gòu)通過HTTP方法(GET、POST、PUT、DELETE等)實現(xiàn)狀態(tài)的轉(zhuǎn)移,對資源進(jìn)行增刪改查的操作。
- 無狀態(tài)(Stateless):RESTful服務(wù)是無狀態(tài)的,每個請求都包含足夠的信息,使服務(wù)器能夠理解和處理請求,而無需依賴之前的請求。
三、API接口
3.1 RESTful API設(shè)計指南
參考資料 阮一峰 理解RESTful架構(gòu)
3.2 API與用戶的通信協(xié)議
總是使用HTTPs協(xié)議。
3.3 RestFul API接口設(shè)計規(guī)范
3.3.1 api接口
- 規(guī)定了前后臺信息交互規(guī)則的url鏈接,也就是前后臺信息交互的媒介
3.3.2 接口文檔:
- 可以手動寫(公司有平臺,錄到平臺里)
- 自動生成(coreapi,swagger)
3.4 restful規(guī)范(10條,規(guī)定了這么做,公司可以不采用)
數(shù)據(jù)的安全保障,通常使用https進(jìn)行傳輸
-
域名中會含有API標(biāo)識
https://api.example.com 盡量將API部署在專用域名
https://127.0.0.0:8080/api/ API很簡單
-
請求地址中帶版本信息,或者在請求頭中
-
任何東西都是資源,均使用名詞表示 (盡量不要用動詞)
https://api.example.com/v1/books/
https://api.example.com/v1/get_all_books(不符合規(guī)范)
-
請求方式區(qū)分不同操作
get獲?。簭姆?wù)器取出資源(一項或多項)
post新增數(shù)據(jù):在服務(wù)器新建一個資源
put/patch:patch是局部更新,put是全部(基本上更新都用put)
delete:從服務(wù)器中刪除
-
在請求路徑中帶過濾,通過在url上傳參的形式傳遞搜索條件
https://api.example.com/v1/?name='金'&order=asc
https://api.example.com/v1/name?sortby=name&order=asc
https://api.example.com/v1/zoos?limit=10:指定返回記錄的數(shù)量
https://api.example.com/v1/zoos?offset=10:指定返回記錄的開始位置
https://api.example.com/v1/zoos?page=2&per_page=100:指定第幾頁,以及每頁的記錄數(shù)
https://api.example.com/v1/zoos?sortby=name&order=asc:指定返回結(jié)果按照哪個屬性排序,以及排序順序
-
返回數(shù)據(jù)中帶狀態(tài)碼
http請求的狀態(tài)碼
-
返回的json格式中到狀態(tài)碼(標(biāo)志當(dāng)次請求成功或失敗)
200 OK - [GET]:服務(wù)器成功返回用戶請求的數(shù)據(jù),該操作是冪等的(Idempotent)。 201 CREATED - [POST/PUT/PATCH]:用戶新建或修改數(shù)據(jù)成功。 202 Accepted - [*]:表示一個請求已經(jīng)進(jìn)入后臺排隊(異步任務(wù)) 204 NO CONTENT - [DELETE]:用戶刪除數(shù)據(jù)成功。 400 INVALID REQUEST - [POST/PUT/PATCH]:用戶發(fā)出的請求有錯誤,服務(wù)器沒有進(jìn)行新建或修改數(shù)據(jù)的操作,該操作是冪等的。 401 Unauthorized - [*]:表示用戶沒有權(quán)限(令牌、用戶名、密碼錯誤)。 403 Forbidden - [*] 表示用戶得到授權(quán)(與401錯誤相對),但是訪問是被禁止的。 404 NOT FOUND - [*]:用戶發(fā)出的請求針對的是不存在的記錄,服務(wù)器沒有進(jìn)行操作,該操作是冪等的。 406 Not Acceptable - [GET]:用戶請求的格式不可得(比如用戶請求JSON格式,但是只有XML格式)。 410 Gone -[GET]:用戶請求的資源被永久刪除,且不會再得到的。 422 Unprocesable entity - [POST/PUT/PATCH] 當(dāng)創(chuàng)建一個對象時,發(fā)生一個驗證錯誤。 500 INTERNAL SERVER ERROR - [*]:服務(wù)器發(fā)生錯誤,用戶將無法判斷發(fā)出的請求是否成功。
-
返回數(shù)據(jù)中帶錯誤信息
-
錯誤處理,應(yīng)返回錯誤信息,error當(dāng)做key
{ error: "Invalid API key" }
-
-
對不同操作,返回數(shù)據(jù)符合如下規(guī)范(這只是規(guī)范)
GET /books:返回資源對象的列表(數(shù)組)[{},{},{}] GET /books/1:返回單個資源對象 {} POST /books:返回新生成的資源對象 {新增的書} PUT /books/1:返回完整的資源對象 {返回修改后的} PATCH /books/1: 返回完整的資源對象 {返回修改后的} DELETE /books/1: 返回一個空文檔 {status:100,msg:查詢成功,data:null} -
返回結(jié)果中帶連接
RESTful API最好做到Hypermedia,即返回結(jié)果中提供鏈接,連向其他API方法,使得用戶不查文檔,也知道下一步應(yīng)該做什么。
{"link": { "rel": "collection https://www.example.com/zoos", "href": "https://api.example.com/zoos", "title": "List of zoos", "type": "application/vnd.yourformat+json" }}
四、圖書管理系統(tǒng)設(shè)計
例如,我們現(xiàn)在要編寫一個管理書籍的系統(tǒng),我們可以查詢對一本書進(jìn)行查詢、創(chuàng)建、更新和刪除等操作,我們在編寫程序的時候就要設(shè)計客戶端瀏覽器與我們Web服務(wù)端交互的方式和路徑。按照經(jīng)驗我們通常會設(shè)計成如下模式:
| 請求方法 | URL | 含義 |
|---|---|---|
| GET | /book | 查詢書籍信息 |
| POST | /create_book | 創(chuàng)建書籍記錄 |
| POST | /update_book | 更新書籍信息 |
| POST | /delete_book | 刪除書籍信息 |
同樣的需求我們按照RESTful API設(shè)計如下:
| 請求方法 | URL | 含義 |
|---|---|---|
| GET | /book | 查詢書籍信息 |
| POST | /book | 創(chuàng)建書籍記錄 |
| PUT | /book | 更新書籍信息 |
| DELETE | /book | 刪除書籍信息 |
新建一個book.go文件,鍵入如下代碼:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
r.GET("/book", func(c *gin.Context) {
c.String(http.StatusOK, "查詢書籍信息")
})
r.POST("/book", func(c *gin.Context) {
c.String(http.StatusOK, "新增書籍信息")
})
r.PUT("/book", func(c *gin.Context) {
c.String(http.StatusOK, "修改書籍信息")
})
r.DELETE("/book", func(c *gin.Context) {
c.String(http.StatusOK, "刪除書籍信息")
})
r.Run(":8080")
}
接下來我們可以使用Postman來作為客戶端的來調(diào)用我們剛剛寫好的接口。
五、Gin 路由類型
Gin 支持很多類型的路由:
- 靜態(tài)路由:完全匹配的路由,也就是前面 我們注冊的 hello 的路由。
- 參數(shù)路由:在路徑中帶上了參數(shù)的路由。
- 通配符路由:任意匹配的路由。

通配符路由
通配符路由究竟匹配上了什么,也是通過 Param 方法獲得的。


通配符路由不能注冊這種 /users/*,/users/*/a。也就是說,* 不能單獨出現(xiàn)。
六、路由參數(shù)
6.1 獲取URL后面的參數(shù)
- URL參數(shù)可以通過
DefaultQuery()或Query()方法獲取 -
DefaultQuery()若參數(shù)不存在則返回默認(rèn)值,Query()若不存在,返回空串 - 指的是URL中
?后面攜帶的參數(shù),例如:/user/search?username=賈維斯&address=北京。
func main() {
//Default返回一個默認(rèn)的路由引擎
r := gin.Default()
r.GET("/user/search", func(c *gin.Context) {
username := c.DefaultQuery("username", "賈維斯")
//username := c.Query("username")
address := c.Query("address")
//輸出json結(jié)果給調(diào)用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
r.Run()
}

6.2 獲取path參數(shù)
請求的參數(shù)通過URL路徑傳遞,例如:/user/search/賈維斯/北京。在Gin框架中,提供了c.Param方法可以獲取路徑中的參數(shù)。 獲取請求URL路徑中的參數(shù)的方式如下。
func main() {
//Default返回一個默認(rèn)的路由引擎
r := gin.Default()
r.GET("/user/search/:username/:address", func(c *gin.Context) {
username := c.Param("username")
address := c.Param("address")
//輸出json結(jié)果給調(diào)用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
r.Run(":8080")
}
6.3 取JSON參數(shù)
當(dāng)前端請求的數(shù)據(jù)通過JSON提交時,例如向/json發(fā)送一個JSON格式的POST請求,則獲取請求參數(shù)的方式如下:
package main
import (
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
r.POST("/json", func(c *gin.Context) {
// 注意:下面為了舉例子方便,暫時忽略了錯誤處理
b, _ := c.GetRawData() // 從c.Request.Body讀取請求數(shù)據(jù)
fmt.Printf("raw data: %s\n", string(b))
// 定義map或結(jié)構(gòu)體
var m map[string]interface{}
// 反序列化
_ = json.Unmarshal(b, &m)
c.JSON(http.StatusOK, m)
})
r.Run(":8080")
}
七、路由組
在Gin框架中,路由組是一種用于組織和管理路由的機制。路由組可以幫助開發(fā)者更好地組織代碼,提高可讀性,并且能夠?qū)σ唤M路由應(yīng)用相同的中間件。以下是關(guān)于路由組的介紹:
7.1 普通路由
普通路由是指直接注冊在Gin引擎上的路由,這些路由沒有被分組,是獨立存在的。下面是一個普通路由的簡單例子:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
router.GET("/hello", func(c *gin.Context) {
c.String(http.StatusOK, "Hello, Gin!")
})
router.GET("/world", func(c *gin.Context) {
c.String(http.StatusOK, "World, Gin!")
})
router.Run(":8080")
}
上述例子中,/hello 和 /world 是兩個獨立的普通路由。
7.2 路由組
路由組通過Group方法創(chuàng)建,可以將一組相關(guān)的路由放到同一個路由組中。通過路由組,可以更好地組織代碼和應(yīng)用中間件。以下是一個簡單的路由組示例:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
// 創(chuàng)建一個路由組
apiGroup := router.Group("/api")
// 在路由組中注冊路由
apiGroup.GET("/users", func(c *gin.Context) {
c.String(http.StatusOK, "Get Users")
})
apiGroup.POST("/users", func(c *gin.Context) {
c.String(http.StatusOK, "Create User")
})
router.Run(":8080")
}
上述例子中,/api 是一個路由組,包含了兩個路由 /users(GET和POST)。這樣,相同業(yè)務(wù)功能的路由被組織在一起,提高了代碼的可讀性和可維護(hù)性。
八、重定向
8.1 HTTP重定向
HTTP 重定向很容易。 內(nèi)部、外部重定向均支持。
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/")
})
8.2 路由重定向
路由重定向,使用HandleContext:
r.GET("/test", func(c *gin.Context) {
// 指定重定向的URL
c.Request.URL.Path = "/test2"
r.HandleContext(c)
})
r.GET("/test2", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"hello": "world"})
})
九、請求參數(shù)綁定
在Gin框架中,請求參數(shù)綁定是一種常見的操作,它允許你從HTTP請求中提取參數(shù)并將其綁定到Go語言結(jié)構(gòu)體中。這樣可以更方便地處理請求數(shù)據(jù)。以下是關(guān)于請求參數(shù)綁定的一些建議和示例:
9.1 獲取查詢參數(shù)
你可以使用c.Query或c.DefaultQuery方法來獲取URL中的查詢參數(shù)。
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
type QueryParams struct {
Name string `form:"name"`
Age int `form:"age"`
}
func main() {
router := gin.Default()
router.GET("/user", func(c *gin.Context) {
var queryParams QueryParams
// 使用 c.ShouldBindQuery 綁定查詢參數(shù)到結(jié)構(gòu)體
if err := c.ShouldBindQuery(&queryParams); err == nil {
c.JSON(http.StatusOK, gin.H{
"name": queryParams.Name,
"age": queryParams.Age,
})
} else {
c.String(http.StatusBadRequest, "參數(shù)綁定失敗")
}
})
router.Run(":8080")
}
上述例子中,通過c.ShouldBindQuery將查詢參數(shù)綁定到QueryParams結(jié)構(gòu)體中,然后使用這個結(jié)構(gòu)體處理請求。
9.2 獲取表單數(shù)據(jù)
使用c.ShouldBind或c.ShouldBindJSON方法可以將POST請求的表單數(shù)據(jù)或JSON數(shù)據(jù)綁定到結(jié)構(gòu)體中。
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
type FormData struct {
Name string `form:"name"`
Age int `form:"age"`
}
func main() {
router := gin.Default()
router.POST("/user", func(c *gin.Context) {
var formData FormData
// 使用 c.ShouldBind 綁定表單數(shù)據(jù)到結(jié)構(gòu)體
if err := c.ShouldBind(&formData); err == nil {
c.JSON(http.StatusOK, gin.H{
"name": formData.Name,
"age": formData.Age,
})
} else {
c.String(http.StatusBadRequest, "參數(shù)綁定失敗")
}
})
router.Run(":8080")
}
在上述例子中,c.ShouldBind將表單數(shù)據(jù)綁定到FormData結(jié)構(gòu)體中。
十、小黃書起步:Web 接口之用戶模塊設(shè)計
10.1 用戶模塊分析
我們現(xiàn)在要設(shè)計一個用戶模塊,對于一個用戶模塊來說,最先要設(shè)計的接口就是:注冊和登錄。而后要考慮提供:編輯和查看用戶信息。同樣的需求我們按照RESTful API設(shè)計如下:
| 請求方法 | URL | 含義 |
|---|---|---|
| GET | /users/profile | 查詢用戶信息 |
| POST | /users/signup | 用戶登錄 |
| POST | /users/login | 用戶注冊 |
| POST | /users/edit | 編輯用戶信息 |
首先,我們創(chuàng)建一個webook目錄,并且初始化go mod
mkdir webook
go mod init webook
10.2 目錄結(jié)構(gòu)
項目目錄結(jié)構(gòu)如圖:

在 webook 頂級目錄下有:
- main 文件,用于啟動 webook。
- 一個 internal 包,里面放著的就是我們所有的業(yè)務(wù) 代碼。
- 一個 pkg 包,這是我們用于存放公共庫和包。
10.3 Handler 的用途
接著我們在user.go 中直接定義了一個 UserHandler,然后將所有 和用戶有關(guān)的路由都定義在了這個 Handler 上,同時,也定義了一個 RegisterRoutes 的方法,用來注冊路由。這里用定義在UserHandler 上的方法來作為對應(yīng)路由的處理邏輯。

10.4 用分組路由來簡化注冊
你可以注意到,就是我們所有的路由都有 /users 這個前綴,要是手一抖就有可能寫錯,這時候可以考慮使用 Gin 的分組路由功能,修改后如下:

10.5 接收請求數(shù)據(jù):接收請求結(jié)構(gòu)體
一般來說,我們都是定義一個結(jié)構(gòu)體來接受數(shù)據(jù)。這里我們使用了方法內(nèi)部類 SignUpRequest 來接收數(shù)據(jù)。

10.6 接收請求數(shù)據(jù):Bind 方法
Bind 方法是 Gin 里面最常用的用于接收請求的方法。
Bind 方法會根據(jù) HTTP 請求的 Content-Type 來決定怎么處理。
比如我們的請求是 JSON 格式,Content-Type 是 application/json,那么 Gin 就會使用 JSON 來反序列化。
如果 Bind 方法發(fā)現(xiàn)輸入有問題,它就會直接返回一個錯誤響應(yīng)到前端。

10.7 校驗請求:正則表達(dá)式
在我們這個注冊的業(yè)務(wù)里面,校驗分為如下:
- 郵箱需要符合一定的格式:也就是賬號這里,必須是一個合法的郵箱。
- 密碼和確認(rèn)密碼需要相等:這是為了確保用戶沒有輸錯。
- 密碼需要符合一定的規(guī)律:要求用戶輸入的密碼必須不少于八位,必須要包含數(shù)字、特殊字符。
綜上所述,我們用正則表達(dá)式來校驗請求,正則表達(dá)式是一種用于匹配和操作文本的強大工 具,它是由一系列字符和特殊字符組成的模式,用 于描述要匹配的文本模式。正則表達(dá)式可以在文本中查找、替換、提取和驗證 特定的模式。代碼如圖:

10.8 校驗請求:預(yù)編譯正則表達(dá)式
我們可以預(yù)編譯正則表達(dá)式來提高校驗速度。

10.9 校驗請求:Go 正則表達(dá)式不支持部分語法
前面我們用的是官方自帶的,但是 Go 自帶的正 則表達(dá)式不支持一些語法,比如說我這里想要用 的表達(dá)式:^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$
類似于 ?=.這種就不支持。所以我們換用另外一個開源的正則表達(dá)式匹配 庫:github.com/dlclark/regexp2。


10.10 校驗請求:全部校驗
整體的校驗如圖,注意我們區(qū)分了不同的錯誤,返回了不同的錯誤提示。

最后,完整代碼如下:
user.go 文件
package web
import (
"fmt"
regexp "github.com/dlclark/regexp2"
"github.com/gin-gonic/gin"
"net/http"
)
type UserHandler struct {
emailExp *regexp.Regexp
passwordExp *regexp.Regexp
}
func NewUserHandler() *UserHandler {
const (
emailRegexPattern = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"
passwordRegexPattern = `^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$`
)
emailExp := regexp.MustCompile(emailRegexPattern, regexp.None)
passwordExp := regexp.MustCompile(passwordRegexPattern, regexp.None)
return &UserHandler{
emailExp: emailExp,
passwordExp: passwordExp,
}
}
func (u *UserHandler) RegisterRoutes(server *gin.Engine) {
ug := server.Group("/user") //ug is user group
ug.GET("/profile", u.Profile) // 查詢用戶信息接口
ug.POST("/signup", u.SignUp) // 注冊接口
ug.POST("/login", u.Login) // 登錄接口
ug.POST("/logout", u.Logout) // 登出接口
ug.POST("/edit", u.Edit) // 修改用戶信息接口
}
func (u *UserHandler) RegisterRoutesV1(ug *gin.RouterGroup) {
ug.GET("/profile", u.Profile) // 查詢用戶信息接口
ug.POST("/signup", u.SignUp) // 注冊接口
ug.POST("/login", u.Login) // 登錄接口
ug.POST("/logout", u.Logout) // 登出接口
ug.POST("/edit", u.Edit) // 修改用戶信息接口
}
func (u *UserHandler) Profile(ctx *gin.Context) {
}
func (u *UserHandler) SignUp(ctx *gin.Context) {
type SignUpRequest struct {
Email string `json:"email"`
Password string `json:"password"`
ConfirmPassword string `json:"confirmPassword"`
}
var request SignUpRequest
// 如果 Bind 方法發(fā)現(xiàn)輸入有問題,它就會直接返回一 個錯誤響應(yīng)到前端。
if err := ctx.Bind(&request); err != nil {
return
}
ok, err := u.emailExp.MatchString(request.Email)
if err != nil {
ctx.String(http.StatusOK, "系統(tǒng)錯誤")
return
}
if !ok {
ctx.String(http.StatusOK, "郵箱格式錯誤")
return
}
ok, err = u.passwordExp.MatchString(request.Password)
if err != nil {
ctx.String(http.StatusOK, "系統(tǒng)錯誤")
return
}
if !ok {
ctx.String(http.StatusOK, "密碼必須包含至少一個數(shù)字、一個字母、一個特殊字符,并且長度至少為8位")
return
}
if request.Password != request.ConfirmPassword {
ctx.String(http.StatusOK, "兩次密碼不一致")
return
}
ctx.String(http.StatusOK, "注冊成功")
fmt.Printf("請求體為:%v", request)
}
func (u *UserHandler) Login(ctx *gin.Context) {
}
func (u *UserHandler) Logout(ctx *gin.Context) {
}
func (u *UserHandler) Edit(ctx *gin.Context) {
}
main.go 文件:
package main
import (
"github.com/gin-gonic/gin"
"strings"
"time"
"webook/internal/web"
)
func main() {
server := gin.Default()
u := web.NewUserHandler()
u.RegisterRoutes(server)
//ug := server.Group("/user/v1") //ug is user group
//c.RegisterRoutesV1(ug)
server.Run(":8080")
}
最后,我們通過postman 請求接口:http://127.0.0.1:8080/user/signup/


