依據進度我們要進入實做的部分了,昨天的 docker-compose,剛好幫我們建立了一組 slave-master MySQL database,今天將利用昨日的 DB 加上 Gin & Gorm packages,完成最簡易的 HTTP service。
配合本文請下載完整範例,並使用Day14 compose.yml 建立 DB。
/ithome12 > ENV=local go run *.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /ping --> main.Router.func1 (5 handlers)
[GIN-debug] GET /echo --> main.Echo (5 handlers)
[GIN-debug] GET /book/:page --> main.BookPage (5 handlers)
[GIN-debug] POST /body/json --> main.JSONBody (5 handlers)
[GIN-debug] POST /body/form_data --> main.FormData (5 handlers)
透過 gin.New()
實體化 Engine instance,並以 Method(path 路徑, handler 函式)
的方式加入 routing path
r := gin.New()
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
r.GET("/echo", Echo)
r.GET("/book/:page", BookPage)
r.POST("/body/json", JSONBody)
r.POST("/body/form_data", FormData)
Router 所使用的 handlers 需滿足 type HandlerFunc func(*Context)
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
Query
func Echo(c *gin.Context) {
name := c.Query("name")
if name == "" {
c.String(http.StatusPaymentRequired, "name required")
return
}
age := c.Query("age")
c.String(http.StatusOK, fmt.Sprintf("Receive name: %s, age: %s", name, age))
}
Parameters in path
r.GET("/book/:page", BookPage)
//BookPage get param in url
func BookPage(c *gin.Context) {
page := c.Param("page")
c.String(http.StatusOK, fmt.Sprintf("Now we got page: %s", page))
}
Form Data
//FormData receive body with form-data
func FormData(c *gin.Context) {
category := c.PostForm("category")
page := c.PostForm("page")
pInt, err := strconv.Atoi(page)
if err != nil {
c.String(http.StatusPaymentRequired, err.Error())
return
}
resp := struct {
Category string `json:"category"`
Page int `json:"page"`
}{category, pInt}
c.JSON(http.StatusOK, resp)
}
Row JSON
//JSONBody receive body with row json
func JSONBody(c *gin.Context) {
type request struct {
Category string `json:"category"`
Page int `json:"page"`
}
var req request
if err := c.BindJSON(&req); err != nil {
c.String(http.StatusPaymentRequired, err.Error())
return
}
fmt.Println("category", req.Category, "page", req.Page)
c.JSON(http.StatusOK, req)
}
Gin 依據我們實作出的 handler,將 request 資訊放入 gin.Context
,並提供我們一些較為便利的函式處理接收參數,加速整體開發流程。另外 gin.Context
也同時提供了 response 方法,我們可以直接使用 c.JSON()
, c.String()
給予 HTTP response。
Middleware 是用來處理共通性事務,像是 access log 或 accept headers 之類的前期處理,讓我們可以免去在每一個 handler 都要處理一次的麻煩,有效地抽出 middleware 可以讓我們的 handler 簡潔許多。
r := gin.New()
r.Use(
gin.Recovery(), // Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
crosMiddleware(), // Allow Header
accessMiddleware(), // Some important access log
)
// Header Allow
func crosMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("origin")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, XMLHttpRequest, "+
"Accept-Encoding, X-CSRF-Token, Authorization, token, Accept, Referer, Origin, User-Agent")
if c.Request.Method == "OPTIONS" {
c.String(200, "ok")
return
}
c.Next()
}
}
注意 middleware 需滿足 gin.HandlerFunc
,而 middleware 向下繼續傳遞的方法是 c.Next()
。這次提供的範例,僅提供我們基本所需的方法與技巧,需要更深入使用的朋友可以參考官方文件 與 官方範例。
配合本文請使用以下 DB table,MySQL database 可使用 Day14 compose.yml 建立。
SET NAMES utf8;
SET time_zone = '+00:00';
SET foreign_key_checks = 0;
SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';
CREATE DATABASE `netfliiix` /*!40100 DEFAULT CHARACTER SET utf8 */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `netfliiix`;
CREATE TABLE `film` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`category` varchar(50) NOT NULL,
`length` int NOT NULL,
`created_time` timestamp NOT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
以下範例皆配合 gin http server 使用,同樣置於 httpServer/ 下。
僅需使用 gorm.Open()
帶入連線用的使用者、密碼、host 與 DB名稱,並在實體化連線池後,設定最大連線數、最大閒置數、最大連線時間等設定。下面範例為了方便創建 slave & master conn pool 我將實體化過程抽出成 OpenPool()
。
//OpnePool 啟用連線池
func (c *ConfigSet) OpnePool() (db *gorm.DB, err error) {
connInfo := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True", c.Username, c.Password, c.Host, c.DBname)
db, err = gorm.Open(mysql.Open(connInfo), &gorm.Config{})
if err != nil {
fmt.Println("OpneMySQLPool:", connInfo, err.Error())
return nil, err
}
return
}
既然是 ORM 當然要有 model object 供我們操作,因此我們依據方才建立的 film table,在專案內建立相同的 model struct。
package main
import "time"
//FilmModel 系統資料
type FilmModel struct {
ID int64 `gorm:"column:id; type: bigint(20); NOT NULL; AUTO_INCREMENT"`
Name string `gorm:"column:name; type: varchar(100); NOT NULL"`
Category string `gorm:"column:category; type: varchar(50); NOT NULL"`
Length int64 `gorm:"column:length; type: int(20); NOT NULL"`
CreatedTime *time.Time `gorm:"column:created_time; type: timestamp; NOT NULL; default:CURRENT_TIMESTAMP"`
}
//TableName users
func (FilmModel) TableName() string {
return "film"
}
注意gorm:"name; type: varchar(100); NOT NULL"
需對應正確欄位名稱與型別。這邊也順便抱怨一下,雖然目前 gorm tag 可以完成我們所有 model 需求,但部分 tag 的使用較不直覺,首次使用的時候可能會多試幾次,好在 model 通常只需要建立一次後就不大會異動,所以我們只需辛苦一次就可以囉。
使用先前建立好的 conn pool sqlMaster
,指定 table name .Table()
,建立新資料 .Create()
。我們可以注意到建立的時候是使用 FilmModel{}
,並且如果沒有填入值的話會使用預設值,這點是使用上需注意的(由於 golang struct 實體化出來的時候,會以該 field 的預設值初始化)。
film := FilmModel{
Name: req.Name,
Category: req.Category,
}
if err := sqlMaster.Table(FilmModel{}.TableName()).Create(&film).Error; err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
透過指標的方式,將 []FilmModel
的 address 傳入進行查詢結果的乘載。支援 prepare ? 的使用方式。Find()是將所有查詢結果返回,可使用 First() 返回首筆結果。
func GetFilmByCategory(c *gin.Context) {
name := c.Query("name")
category := c.Query("category")
var list []FilmModel
if err := sqlMaster.Table(FilmModel{}.TableName()).Where("category=? AND name=?", category, name).Find(&list).Error; err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
c.JSON(http.StatusOK, list)
}
使用涵式 .Update()
,如需更新多個值也可以使用 map[string]interface{}
,請參考官方文件。
if err := sqlMaster.Table(FilmModel{}.TableName()).Update("length", req.LengthMin).Where("id=?", req.ID).Error; err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
我們今天利用 Gin 獲得了 http server 的服務接口,Gorm 則提供我們資料存取的能力。有了這樣的基礎,再加上先前篇幅關於 consistency 的概念,熟悉後將可以繼續實作各式各樣的範例。當然僅靠本篇文章,是不可能完全涵蓋這兩個套件的所有功能,建議大家還是看一下官方文件,裡面有更詳盡的解說。
本篇範例中的所有 API,皆已組好 curl ,大家不訪從頭試試
/*
Echo print param that request carried
curl --location --request GET 'localhost/echo?name=weiweiwesley&age=87'
*/
func Echo(c *gin.Context)