Go Gin 建立 API - 加入 Middleware, Database...
tags: Go
Gin
API
category: Back-End
description: Go Gin 建立 API - 加入 middleware, i18n, config, database, service, dao
created_at: 2021/07/09 14:00:00
寫法偏舊,未來有空會再寫一個新的系列,有興趣再往下閱讀
上一篇最後的檔案目錄
<gin-api>
<internal>
<requests>
user.go
<routers>
<api>
user.go
<pkg>
<app>
request.go
response.go
<errors>
error.go
errors.go
http.go
router.go
main.go
或是你可以直接去Github上載(?)
中介層(middleware) 與 i18n
在做 i18n 之前,先了解一下中介層,用一張圖簡單的說明
在有人發 Request 進來,可以先通過一層或多層的中介層做一些事,看你要塞資料還是驗證看要不要給人過或是導向別的頁面都可以。
所以打算註冊一個中介層,先抓到 Request 的語系,再來做之後的處理,所以要先建立檔案
<gin-api>
<internal>
<middleware>
translations.go <-- 多了這個
main.go
塞入下面程式碼
package middleware
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh_Hant_TW"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
enTranslation "github.com/go-playground/validator/v10/translations/en"
zhTranslation "github.com/go-playground/validator/v10/translations/zh_tw"
)
func Translations() gin.HandlerFunc {
return func(ctx *gin.Context) {
universalTranslator := ut.New(en.New(), zh_Hant_TW.New()) // 英文與繁體中文
locale := ctx.GetHeader("locale") // 從 header 抓語系
translator, _ := universalTranslator.GetTranslator(locale)
v, ok := binding.Validator.Engine().(*validator.Validate)
if ok {
switch locale {
case "en":
_ = enTranslation.RegisterDefaultTranslations(v, translator)
break
default:
_ = zhTranslation.RegisterDefaultTranslations(v, translator) // 預設中文
break
}
ctx.Set("translator", translator) // 存一個值供之後拿
}
ctx.Next() // 允許通過這個中介層
}
}
中介層寫完之後要去路由註冊,告訴他誰使用,而當下來說是全部都要,所以在最外層 Use
他
// ...
router := gin.New()
router.Use(middleware.Translations())
// ...
這時候註冊好了,但是要去改 request.go
func BindAndValidation(c *gin.Context, v interface{}) (bool, []string) {
var errors []string
if err := c.ShouldBind(v); err != nil {
translatorVal := c.Value("translator") // 抓到中介層抓到的語系
translator, _ := translatorVal.(ut.Translator)
validationErrors, ok := err.(validator.ValidationErrors)
if !ok {
return false, errors
}
for _, value := range validationErrors.Translate(translator) {
errors = append(errors, value)
}
return false, errors
}
return true, nil
}
然後這時去跑伺服器,忘記指令了嗎?
$ go run main.go
這時候噴一堆錯
go: github.com/LaiJunBin/gin-api/internal/middleware: package github.com/go-playground/locales/en imported from implicitly required module; to add missing requirements, run:
go get github.com/go-playground/locales/[email protected]
go: github.com/LaiJunBin/gin-api/internal/middleware: package github.com/go-playground/locales/zh_Hant_TW imported from implicitly required module; to add missing requirements, run:
go get github.com/go-playground/locales/[email protected]
go: github.com/LaiJunBin/gin-api/internal/middleware: package github.com/go-playground/universal-translator imported from implicitly required module; to add missing requirements, run:
go get github.com/go-playground/[email protected]
go: github.com/LaiJunBin/gin-api/pkg/app: package github.com/go-playground/universal-translator imported from implicitly required module; to add missing requirements, run:
go get github.com/go-playground/[email protected]
他其實是跟你說要去裝缺少的套件
$ go get github.com/go-playground/locales/[email protected]
$ go get github.com/go-playground/locales/[email protected]
$ go get github.com/go-playground/[email protected]
處理好i18n之後來比較一下,這是原本的
{
"details": [
"Key: 'RegisterUserRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag"
],
"message": "InvalidParams",
"success": false
}
這是處理之後的
{
"details": [
"Name為必填欄位"
],
"message": "InvalidParams",
"success": false
}
抽離設定檔
建立一些檔案,負責存跟處理設定
<gin-api>
<global>
setting.go <-- 多了這個
<pkg>
<setting> <-- 多了這些
section.go
setting.go
config.yaml <-- 多了這個
要讀取 yaml
的設定檔,可以安裝一個套件處理
$ go get github.com/spf13/viper
config.yaml
改成你的設定
Server:
RunMode: debug
HttpPort: 8080
ReadTimeout: 60
WriteTimeout: 60
Database:
DBType: mysql
UserName: root
Password:
Host: 127.0.0.1:3306
DBName: gin_api_db
Charset: utf8
ParseTime: True
然後先來設定基底 setting
結構
setting.go
package settings
import "github.com/spf13/viper"
type Setting struct {
viper *viper.Viper
}
func NewSetting() (*Setting, error) {
viper := viper.New()
viper.SetConfigName("config")
viper.AddConfigPath("configs/")
viper.SetConfigType("yaml")
err := viper.ReadInConfig()
if err != nil {
return nil, err
}
return &Setting{viper}, nil
}
section.go
定義設定檔中每個區塊的結構
package setting
import "time"
type ServerSetting struct {
RunMode string
HttpPort string
ReadTimeout time.Duration
WriteTimeout time.Duration
}
type DatabaseSetting struct {
DBType string
UserName string
Password string
Host string
DBName string
Charset string
ParseTime bool
}
func (setting *Setting) ReadSection(k string, v interface{}) error {
return setting.viper.UnmarshalKey(k, v)
}
再來在 global/setting.go 設定全域變數
package global
import "github.com/LaiJunBin/gin-api/pkg/setting"
var (
ServerSetting *setting.ServerSetting
DatabaseSetting *setting.DatabaseSetting
)
然後最後修改 main.go
套用設定檔
package main
import (
"fmt"
"github.com/LaiJunBin/gin-api/global"
"github.com/LaiJunBin/gin-api/internal/routers"
"github.com/LaiJunBin/gin-api/pkg/setting"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
func init() {
err := setupSetting()
if err != nil {
fmt.Printf("init.setupSetting error: %v", err)
}
}
func setupSetting() error {
setting, err := setting.NewSetting()
if err != nil {
return err
}
err = setting.ReadSection("Server", &global.ServerSetting)
if err != nil {
return err
}
err = setting.ReadSection("Database", &global.DatabaseSetting)
if err != nil {
return err
}
global.ServerSetting.ReadTimeout *= time.Second
global.ServerSetting.WriteTimeout *= time.Second
return nil
}
func main() {
gin.SetMode(global.ServerSetting.RunMode)
router := routers.NewRouter()
s := &http.Server{
Addr: ":" + global.ServerSetting.HttpPort,
Handler: router,
ReadTimeout: global.ServerSetting.ReadTimeout,
WriteTimeout: global.ServerSetting.WriteTimeout,
}
s.ListenAndServe()
}
init()
是一個特殊的函數,他會在 main()
函數之前執行,有點像前端框架生命週期的概念,他也是一個特殊的函數,你放多個也沒關係,他會從上往下執行。
加入資料庫
安裝 ORM 套件
$ go get gorm.io/gorm
還有安裝 mysql 的 driver
$ go get gorm.io/driver/mysql
一樣先建立幾個檔案
<gin-api>
<global>
db.go <-- global db
<internal>
<model>
model.go <-- init db
model.go
package model
import (
"fmt"
"github.com/LaiJunBin/gin-api/pkg/setting"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func NewDBEngine(databaseSetting *setting.DatabaseSetting) (*gorm.DB, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=%s&parseTime=%t&loc=Local",
databaseSetting.UserName,
databaseSetting.Password,
databaseSetting.Host,
databaseSetting.DBName,
databaseSetting.Charset,
databaseSetting.ParseTime,
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
return db, nil
}
db.go
package global
import "gorm.io/gorm"
var (
DBEngine *gorm.DB
)
再來回到 main.go
去初始化他
// ..
func init() {
err := setupSetting()
if err != nil {
fmt.Printf("init.setupSetting error: %v", err)
}
err = setupDBEngine()
if err != nil {
fmt.Printf("init.setupDBEngine error: %v", err)
}
}
func setupDBEngine() error {
var err error
global.DBEngine, err = model.NewDBEngine(global.DatabaseSetting)
return err
}
// ..
再來需要建立資料表
先建立 model 描述結構
檔案: internal/model/user.go
package model
import "gorm.io/gorm"
type User struct {
gorm.Model
Name string `gorm:"not null" json:"name,omitempty"`
Username string `gorm:"not null; unique" json:"username,omitempty"`
Password string `gorm:"not null" json:"-"`
}
再來建立一個檔案負責做 migration
檔案: scripts/migration.go
package main
import (
"fmt"
"github.com/LaiJunBin/gin-api/global"
"github.com/LaiJunBin/gin-api/internal/model"
"github.com/LaiJunBin/gin-api/pkg/setting"
)
func main(){
setting, _ := setting.NewSetting()
_ = setting.ReadSection("Database", &global.DatabaseSetting)
db, err := model.NewDBEngine(global.DatabaseSetting)
err = db.AutoMigrate(&model.User{})
if err != nil {
fmt.Println(err)
}
}
$ go run scripts\migration.go
成功執行後資料庫中應該多出了一張 users
表,再來就可以開始做CRUD了,先建立一些資料夾與檔案
<gin-api>
<internal>
<dao> <-- 資料訪問(存取)
dao.go
user.go
<service> <-- 就..service
service.go
user.go
<pkg>
<errors> <-- user相關的error
user.go
打算以這樣的流程設計: router -> api -> service -> dao -> model
先來看 dao
的部分
dao.go
package dao
import "gorm.io/gorm"
type Dao struct {
Engine *gorm.DB
}
func New(engine *gorm.DB) *Dao {
return &Dao{Engine: engine}
}
再來是 service.go
package service
import (
"context"
"github.com/LaiJunBin/gin-api/global"
"github.com/LaiJunBin/gin-api/internal/dao"
)
type Service struct {
Ctx context.Context
Dao *dao.Dao
}
func New(ctx context.Context) Service{
service := Service{Ctx: ctx}
service.Dao = dao.New(global.DBEngine)
return service
}
再來在model新增function model/user.go
func (u User) Create(db *gorm.DB) (User, error) {
err := db.Create(&u).Error
return u, err
}
再來處理 dao/user.go
package dao
import "github.com/LaiJunBin/gin-api/internal/model"
func (dao *Dao) CreateUser(name, username, password string) (model.User, error) {
user := model.User{
Name: name,
Username: username,
Password: password,
}
return user.Create(dao.Engine)
}
再來到 service/user.go
package service
import (
"github.com/LaiJunBin/gin-api/internal/model"
"github.com/LaiJunBin/gin-api/internal/requests"
)
func (s Service) CreateUser(params *requests.RegisterUserRequest) (model.User, error) {
return s.Dao.CreateUser(params.Name, params.Username, params.Password)
}
最後回到 api/user.go
完成註冊 function
func (u User) Register(c *gin.Context) {
params := requests.RegisterUserRequest{}
response := app.NewResponse(c)
valid, err := app.BindAndValidation(c, ¶ms)
if !valid {
errorResponse := errors.InvalidParams.WithDetails(err...)
response.MakeErrorResponse(errorResponse)
return
}
s := service.New(c.Request.Context())
user, userErr := s.CreateUser(¶ms)
if userErr != nil {
response.MakeErrorResponse(errors.CreateUserFail)
return
}
response.MakeResponse(user.ID)
}
這時候功能運作正常,但是有一個問題,密碼是以明文存進資料庫的,所以要特別處理一下,我們可以安裝 bcrypt
套件
$ go get golang.org/x/crypto/bcrypt
然後建立一個檔案
<gin-api>
<pkg>
<utils>
bcrypt.go
然後填入以下內容
package utils
import "golang.org/x/crypto/bcrypt"
// 加密
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
// 檢查
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
然後修改一下 service/user.go
func (s Service) CreateUser(params *requests.RegisterUserRequest) (model.User, error) {
hashedPassword, _ := utils.HashPassword(params.Password)
return s.Dao.CreateUser(params.Name, params.Username, hashedPassword)
}
稍微偷懶一下不做錯誤處理
另外RUD也順便偷懶了
下一篇來做登入、JWT Token與上傳檔案
這篇進度的Source code