iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 15
0

依據進度我們要進入實做的部分了,昨天的 docker-compose,剛好幫我們建立了一組 slave-master MySQL database,今天將利用昨日的 DB 加上 Gin & Gorm packages,完成最簡易的 HTTP service。

Gin

gin

配合本文請下載完整範例,並使用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)

Router

透過 gin.New() 實體化 Engine instance,並以 Method(path 路徑, handler 函式)的方式加入 routing path

  • RESTful
    • GET
    • POST
    • PUT
    • DELETE

httpServer/router.go

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)

Handler

Router 所使用的 handlers 需滿足 type HandlerFunc func(*Context)

r.GET("/ping", func(c *gin.Context) {
		c.String(http.StatusOK, "pong")
})

Param

  • 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

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()。這次提供的範例,僅提供我們基本所需的方法與技巧,需要更深入使用的朋友可以參考官方文件官方範例

Gorm

gorm

配合本文請使用以下 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/ 下。

Connection

僅需使用 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
}

Model

既然是 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 通常只需要建立一次後就不大會異動,所以我們只需辛苦一次就可以囉。

Insert

使用先前建立好的 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
}

Select

透過指標的方式,將 []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

使用涵式 .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)

上一篇
Day14 Dockerfile & Docker-Compose
下一篇
Day16 Transactions (MySQL)
系列文
Go Distributed & Go Consistently30

尚未有邦友留言

立即登入留言