1. 構建一個http server:
// api.test.com/topic/main.go:
type Topic struct {Id int // 如果寫成小寫的,不能訪問,因為是私有的.Title string
}
func main() {data := make(map[string]interface{})data["name"] = "david"data["age"] = 12// 默認路由router := gin.Default()router.GET("/", func(context *gin.Context) {context.Writer.Write([]byte("hello")) // (1)// context.JSON(http.StatusOK, data) // (2)// context.JSON(http.StatusOK, gin.H{"name": "daivd", "age": 12}) // (3)// context.JSON(http.StatusOK, Topic{100, "話題標題"}) // (4)})router.Run()
}
(1). 返回text/plain格式的結果:hello
HTTP/1.1 200 OK
Date: Fri, 01 Nov 2019 04:09:18 GMT
Content-Length: 5
Content-Type: text/plain; charset=utf-8
(2). 返回json的格式:{“age”:12, “name”:“david”}
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 04 Nov 2019 11:00:06 GMT
Content-Length: 25
(3). 返回如上json的格式:{“age”:12, “name”:“david”}
# 1. gin.H{}底層實現方案:
// H is a shortcut for map[string]interface{}
type H map[string]interface{}# 2. 模擬實現:
type M map[string]interface{}
①. make的形式:m := make(M)m["name"] = "david"# 在使用map前,需要先make,make的作用就是給map分配數據空間②. 直接賦值:map[string]interface{}{"name": "daivd"}③. M就相當于map[string]interface{}的簡寫:M{"name": "daivd"}
(4). 對象返回如上json的格式:{“Id”:100, “Title”:“話題標題”}
2. API的URL規則設計:
2.1 httprouter庫:
①. gin的路由使用的是httprouter庫.
②. 不支持正則、不支持固定路徑和參數路徑.
③. 保證了性能,但功能不強大.
2.2 API設計建議:
(1). 常規上的路由:
GET /topic/{topic_id}:獲取帖子明細
GET /topic/{user_name}:獲取用戶發布的帖子列表
GET /topic/top:獲取最熱帖子列表
(2). 建議改進路由規則:
①. 最好API前面加上版本:GET /v1/topic/:id②. 盡可能使用復數的語義化名詞:GET /v1/topics③. 使用GET參數規劃數據展現規則:GET /v1/users // 顯示全部用戶GET /v1/users?limit=10 // 顯示10條用戶GET /v1/topics?username=xx // 顯示xx的帖子
3.3 增加其它路由:
api.test.com/topic/main.go:
router := gin.Default()
router.GET("/v1/topics/:topic_id", func(context *gin.Context) {context.String(http.StatusOK, "獲取%s的帖子", context.Param("topic_id"))
})
router.GET("/v1/topics", func(context *gin.Context) {if context.Query("username") == "" {context.String(http.StatusOK, "獲取所有帖子")} else {context.String(http.StatusOK, "獲取%s所有帖子", context.Query("username"))}
})
router.Run()
注:
①. 以下幾條路由不允許:router.GET("/topic/:id")router.GET("/topic/topic")router.GET("/topic/topic/:username")②. 報錯:panic: 'topic' in new path '/topic/topic' conflicts with existing wildcard ':topic_id' in existing prefix '/:topic_id'③. context.Param可以獲取"/v1/topics/:topic_id"中以冒號的形式設置url參數topic_id.④. context.Query可以當get傳參的時候,url上顯示的參數.
3.4 路由分組:
(1). api.test.com/topic/main.go:
import "topic.test.com/src" // moduel/文件名
func main() {router := gin.Default()// 定義一個路由分組v1 := router.Group("/v1/topics")// {}不是強關聯,只是為了美觀,做代碼切割,不寫也可以,有作用域的區別{// 什么都不寫,表示與組的路由是一致的(GET /v1/topics)v1.GET("", src.GetList) // 將業務代碼分離出去v1.GET("/:topic_id", func(context *gin.Context) {context.String(http.StatusOK, "獲取%s的帖子", context.Param("topic_id"))})// 在這個下面所有定義的路由都需要鑒權v1.Use(src.MustLogin()){v1.POST("", src.AddTopic)}}router.Run()
}注:
①. 如果src.GetList(),不想寫src的話.需要這樣引入 => . "topic.test.com/src"②. 不能寫src.GetList(),否則是表示執行函數得到的結果.③. 如果想寫src.GetList(),則需要改造如下:func GetList2() gin.HandlerFunc {return func (c *gin.Context) {//}}④. v1.Use的參數為HandlerFunc,所以是執行函數返回結果.⑤. 為了美觀,可以在路由上加一個{}代碼塊(代碼塊有作用域),不是強關聯,只是為了美觀.
(2). api.test.com/topic/src/TopicDao.go:
src是所有業務代碼
func MustLogin() gin.HandlerFunc {return func (c *gin.Context) {if _, status := c.GetQuery("token");!status {c.String(http.StatusUnauthorized, "授權失敗")c.Abort() // 沒有的話,會繼續往下執行} else {c.Next()}}
}
func GetList(c *gin.Context) {// if c.Query("username") == "" {// c.String(http.StatusOK, "獲取所有帖子")// } else {// c.String(http.StatusOK, "獲取%s所有帖子", c.Query("username"))// }// get參數校驗query := TopicQuery{}err := c.BindQuery(&query) // 是BindQueryif err != nil {c.JSON(500, "參數錯誤") // 自動報一個400 Bad Request的錯誤,寫成500也是400} else {c.JSON(http.StatusOK, query)}
}
func AddTopic(c *gin.Context) {// c.JSON(http.StatusOK, CreateTopic(1, "PHP"))// post參數校驗query := Topic{}err := c.BindJSON(&query) // 是BindJSONif err != nil {c.JSON(500, "參數錯誤")} else {c.JSON(http.StatusOK, query)}
}注:
①. Dao層操作數據庫實體、redis實體.②. 路由測試:POST localhost:8080/v1/topics => 結果是:授權失敗POST localhost:8080/v1/topics?token=123 => 結果是:{ "id": 1, "title": "PHP" }如果沒有json映射字段則返回:{ "TopicId": 1, "TopicTitle": "PHP" }③. 參數綁定Model:a. GetList中,如果參數有username、page、pagesize等,用if來判斷的話,非常繁瑣.b. 可以將get參數綁定(get參數映射為一個模型).c. 很多流行框架支持參數綁定Model(傳過來的參數與model進行綁定).④. GET localhost:8080/v1/topics?username=david&page=1&pagesize=10=> 如果TopicQuery struct中沒有form字段會是{ "username": "", "page": 0, "pagesize": 0 }localhost:8080/v1/topics?username=david&page=1=> { "username": "david", "page": 1, "pagesize": 0 }localhost:8080/v1/topics?username=david&page=1&pagesize=10=> { "username": "david", "page": 1, "pagesize": 10 }localhost:8080/v1/topics?username=david&pagesize=10=> 參數錯誤localhost:8080/v1/topics?username=david&page=0=> 參數錯誤,page為0localhost:8080/v1/topics?username=david&page=""=> 參數錯誤,page為""
(3). api.test.com/topic/src/TopicModel.go:
package src
type Topic struct {TopicId int `json:"id"` // json反射的映射字段TopicTitle string `json:"title" binding:"min=4,max=20"` // 在4和20字之間,中英文都是一樣TopicShortTitle string `json:"stitle" binding:"requried,nefield=TopicTitle"` // 不能與TopicTitle一樣UserIP string `json:"ip" binding:"ipv4"`TopicScore int `json:"score" binding:"omitempty,gt=5"` // 可以不填,填了就大于5TopicUrl string `json:"url" binding:"omitempty,topicurl"`
}
func CreateTopic (id int, title string) Topic {return Topic{id, title}
}
// query參數綁定
type TopicQuery struct {UserName string `json:"username" form:"username"`Page int `json:"page" form:"page" binding:"required"`PageSize int `json:"pagesize" form:"pagesize"`
}注:
①. form決定了綁定query參數的key到底是什么.沒寫form,則不會綁定.
②. 內置驗證器是第三方庫github.com/go-playground/validator