Go Gin 系列二:搭建Blog API's (一)

你們好,我是煎魚,這是本項目的地址:github.com/eddycjy/go-… ,若是有什麼問題歡迎隨時交流和溝通。mysql

思考

首先,在一個初始項目開始前,你們都要思考一下git

  • 程序的文本配置寫在代碼中,好嗎?github

  • API 的錯誤碼硬編碼在程序中,合適嗎?golang

  • db句柄誰都去Open,沒有統一管理,好嗎?sql

  • 獲取分頁等公共參數,誰都本身寫一套邏輯,好嗎?數據庫

顯然在較正規的項目中,這些問題的答案都是不能夠,爲了解決這些問題,咱們挑選一款讀寫配置文件的庫,目前比較火的有 viper,有興趣你將來能夠簡單瞭解一下,沒興趣的話等之後接觸到再說。json

可是本系列選用 go-ini/ini ,它的 中文文檔。你們是必須須要要簡單閱讀它的文檔,再接着完成後面的內容。segmentfault

本文目標

  • 編寫一個簡單的API錯誤碼包。
  • 完成一個 Demo 示例。
  • 講解 Demo 所涉及的知識點。

介紹和初始化項目

初始化項目目錄

在前一章節中,咱們初始化了一個 go-gin-example 項目,接下來咱們須要繼續新增以下目錄結構:安全

go-gin-example/
├── conf
├── middleware
├── models
├── pkg
├── routers
└── runtime
複製代碼
  • conf:用於存儲配置文件
  • middleware:應用中間件
  • models:應用數據庫模型
  • pkg:第三方包
  • routers 路由邏輯處理
  • runtime:應用運行時數據

添加 Go Modules Replace

打開 go.mod 文件,新增 replace 配置項,以下:bash

module github.com/EDDYCJY/go-gin-example

go 1.13

require (...)

replace (
		github.com/EDDYCJY/go-gin-example/pkg/setting => ~/go-application/go-gin-example/pkg/setting
		github.com/EDDYCJY/go-gin-example/conf    	  => ~/go-application/go-gin-example/pkg/conf
		github.com/EDDYCJY/go-gin-example/middleware  => ~/go-application/go-gin-example/middleware
		github.com/EDDYCJY/go-gin-example/models 	  => ~/go-application/go-gin-example/models
		github.com/EDDYCJY/go-gin-example/routers 	  => ~/go-application/go-gin-example/routers
)
複製代碼

可能你會不理解爲何要特地跑來加 replace 配置項,首先你要看到咱們使用的是完整的外部模塊引用路徑(github.com/EDDYCJY/go-gin-example/xxx),而這個模塊還沒推送到遠程,是沒有辦法下載下來的,所以須要用 replace 將其指定讀取本地的模塊路徑,這樣子就能夠解決本地模塊讀取的問題。

注:後續每新增一個本地應用目錄,你都須要主動去 go.mod 文件裏新增一條 replace(我不會提醒你),若是你漏了,那麼編譯時會出現報錯,找不到那個模塊。

初始項目數據庫

新建 blog 數據庫,編碼爲utf8_general_ci,在 blog 數據庫下,新建如下表

一、 標籤表

CREATE TABLE `blog_tag` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT '' COMMENT '標籤名稱',
  `created_on` int(10) unsigned DEFAULT '0' COMMENT '建立時間',
  `created_by` varchar(100) DEFAULT '' COMMENT '建立人',
  `modified_on` int(10) unsigned DEFAULT '0' COMMENT '修改時間',
  `modified_by` varchar(100) DEFAULT '' COMMENT '修改人',
  `deleted_on` int(10) unsigned DEFAULT '0',
  `state` tinyint(3) unsigned DEFAULT '1' COMMENT '狀態 0爲禁用、1爲啓用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章標籤管理';
複製代碼

二、 文章表

CREATE TABLE `blog_article` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `tag_id` int(10) unsigned DEFAULT '0' COMMENT '標籤ID',
  `title` varchar(100) DEFAULT '' COMMENT '文章標題',
  `desc` varchar(255) DEFAULT '' COMMENT '簡述',
  `content` text,
  `created_on` int(11) DEFAULT NULL,
  `created_by` varchar(100) DEFAULT '' COMMENT '建立人',
  `modified_on` int(10) unsigned DEFAULT '0' COMMENT '修改時間',
  `modified_by` varchar(255) DEFAULT '' COMMENT '修改人',
  `deleted_on` int(10) unsigned DEFAULT '0',
  `state` tinyint(3) unsigned DEFAULT '1' COMMENT '狀態 0爲禁用1爲啓用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章管理';
複製代碼

三、 認證表

CREATE TABLE `blog_auth` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(50) DEFAULT '' COMMENT '帳號',
  `password` varchar(50) DEFAULT '' COMMENT '密碼',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `blog`.`blog_auth` (`id`, `username`, `password`) VALUES (null, 'test', 'test123456');

複製代碼

編寫項目配置包

go-gin-example 應用目錄下,拉取 go-ini/ini 的依賴包,以下:

$ go get -u github.com/go-ini/ini
go: finding github.com/go-ini/ini v1.48.0
go: downloading github.com/go-ini/ini v1.48.0
go: extracting github.com/go-ini/ini v1.48.0
複製代碼

接下來咱們須要編寫基礎的應用配置文件,在 go-gin-exampleconf目錄下新建app.ini文件,寫入內容:

#debug or release
RUN_MODE = debug

[app]
PAGE_SIZE = 10
JWT_SECRET = 23347$040412

[server]
HTTP_PORT = 8000
READ_TIMEOUT = 60
WRITE_TIMEOUT = 60

[database]
TYPE = mysql
USER = 數據庫帳號
PASSWORD = 數據庫密碼
#127.0.0.1:3306
HOST = 數據庫IP:數據庫端口號
NAME = blog
TABLE_PREFIX = blog_
複製代碼

創建調用配置的setting模塊,在go-gin-examplepkg目錄下新建setting目錄(注意新增 replace 配置),新建 setting.go 文件,寫入內容:

package setting

import (
	"log"
	"time"

	"github.com/go-ini/ini"
)

var (
	Cfg *ini.File

	RunMode string
	
	HTTPPort int
	ReadTimeout time.Duration
	WriteTimeout time.Duration

	PageSize int
	JwtSecret string
)

func init() {
	var err error
	Cfg, err = ini.Load("conf/app.ini")
	if err != nil {
		log.Fatalf("Fail to parse 'conf/app.ini': %v", err)
	}

	LoadBase()
	LoadServer()
	LoadApp()
}

func LoadBase() {
	RunMode = Cfg.Section("").Key("RUN_MODE").MustString("debug")
}

func LoadServer() {
	sec, err := Cfg.GetSection("server")
	if err != nil {
		log.Fatalf("Fail to get section 'server': %v", err)
	}

	HTTPPort = sec.Key("HTTP_PORT").MustInt(8000)
	ReadTimeout = time.Duration(sec.Key("READ_TIMEOUT").MustInt(60)) * time.Second
	WriteTimeout =  time.Duration(sec.Key("WRITE_TIMEOUT").MustInt(60)) * time.Second	
}

func LoadApp() {
	sec, err := Cfg.GetSection("app")
	if err != nil {
		log.Fatalf("Fail to get section 'app': %v", err)
	}

	JwtSecret = sec.Key("JWT_SECRET").MustString("!@)*#)!@U#@*!@!)")
	PageSize = sec.Key("PAGE_SIZE").MustInt(10)
}
複製代碼

當前的目錄結構:

go-gin-example
├── conf
│   └── app.ini
├── go.mod
├── go.sum
├── middleware
├── models
├── pkg
│   └── setting.go
├── routers
└── runtime
複製代碼

編寫API錯誤碼包

創建錯誤碼的e模塊,在go-gin-examplepkg目錄下新建e目錄(注意新增 replace 配置),新建code.gomsg.go文件,寫入內容:

一、 code.go:

package e

const (
	SUCCESS = 200
	ERROR = 500
	INVALID_PARAMS = 400

	ERROR_EXIST_TAG = 10001
	ERROR_NOT_EXIST_TAG = 10002
	ERROR_NOT_EXIST_ARTICLE = 10003

	ERROR_AUTH_CHECK_TOKEN_FAIL = 20001
	ERROR_AUTH_CHECK_TOKEN_TIMEOUT = 20002
	ERROR_AUTH_TOKEN = 20003
	ERROR_AUTH = 20004
)
複製代碼

二、 msg.go:

package e

var MsgFlags = map[int]string {
	SUCCESS : "ok",
	ERROR : "fail",
	INVALID_PARAMS : "請求參數錯誤",
	ERROR_EXIST_TAG : "已存在該標籤名稱",
	ERROR_NOT_EXIST_TAG : "該標籤不存在",
	ERROR_NOT_EXIST_ARTICLE : "該文章不存在",
	ERROR_AUTH_CHECK_TOKEN_FAIL : "Token鑑權失敗",
	ERROR_AUTH_CHECK_TOKEN_TIMEOUT : "Token已超時",
	ERROR_AUTH_TOKEN : "Token生成失敗",
	ERROR_AUTH : "Token錯誤",
}

func GetMsg(code int) string {
	msg, ok := MsgFlags[code]
	if ok {
		return msg
	}

	return MsgFlags[ERROR]
}
複製代碼

編寫工具包

go-gin-examplepkg目錄下新建util目錄(注意新增 replace 配置),並拉取com的依賴包,以下:

go get -u github.com/unknwon/com
複製代碼

編寫分頁頁碼的獲取方法

util目錄下新建pagination.go,寫入內容:

package util

import (
	"github.com/gin-gonic/gin"
	"github.com/unknwon/com"

	"github.com/EDDYCJY/go-gin-example/pkg/setting"
)

func GetPage(c *gin.Context) int {
	result := 0
	page, _ := com.StrTo(c.Query("page")).Int()
    if page > 0 {
        result = (page - 1) * setting.PageSize
    }

    return result
}
複製代碼

編寫models init

拉取gorm的依賴包,以下:

go get -u github.com/jinzhu/gorm
複製代碼

拉取mysql驅動的依賴包,以下:

go get -u github.com/go-sql-driver/mysql
複製代碼

完成後,在go-gin-examplemodels目錄下新建models.go,用於models的初始化使用

package models

import (
	"log"
	"fmt"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"

	"github.com/EDDYCJY/go-gin-example/pkg/setting"
)

var db *gorm.DB

type Model struct {
	ID int `gorm:"primary_key" json:"id"`
	CreatedOn int `json:"created_on"`
	ModifiedOn int `json:"modified_on"`
}

func init() {
	var (
		err error
		dbType, dbName, user, password, host, tablePrefix string
	)

	sec, err := setting.Cfg.GetSection("database")
	if err != nil {
		log.Fatal(2, "Fail to get section 'database': %v", err)
	}

	dbType = sec.Key("TYPE").String()
	dbName = sec.Key("NAME").String()
	user = sec.Key("USER").String()
	password = sec.Key("PASSWORD").String()
	host = sec.Key("HOST").String()
	tablePrefix = sec.Key("TABLE_PREFIX").String()

	db, err = gorm.Open(dbType, fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", 
		user, 
		password, 
		host, 
		dbName))

	if err != nil {
		log.Println(err)
	}

	gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string  {
	    return tablePrefix + defaultTableName;
	}

	db.SingularTable(true)
	db.LogMode(true)
	db.DB().SetMaxIdleConns(10)
	db.DB().SetMaxOpenConns(100)
}

func CloseDB() {
	defer db.Close()
}
複製代碼

編寫項目啓動、路由文件

最基礎的準備工做完成啦,讓咱們開始編寫Demo吧!

編寫Demo

go-gin-example下創建main.go做爲啓動文件(也就是main包),咱們先寫個Demo,幫助你們理解,寫入文件內容:

package main

import (
    "fmt"
	  "net/http"

    "github.com/gin-gonic/gin"

	  "github.com/EDDYCJY/go-gin-example/pkg/setting"
)

func main() {
	router := gin.Default()
    router.GET("/test", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "test",
		})
	})

	s := &http.Server{
		Addr:           fmt.Sprintf(":%d", setting.HTTPPort),
		Handler:        router,
		ReadTimeout:    setting.ReadTimeout,
		WriteTimeout:   setting.WriteTimeout,
		MaxHeaderBytes: 1 << 20,
	}

	s.ListenAndServe()
}
複製代碼

執行go run main.go,查看命令行是否顯示

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[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    /test                     --> main.main.func1 (3 handlers)
複製代碼

在本機執行curl 127.0.0.1:8000/test,檢查是否返回{"message":"test"}

知識點

那麼,咱們來延伸一下Demo所涉及的知識點!

標準庫
  • fmt:實現了相似C語言printf和scanf的格式化I/O。格式化動做('verb')源自C語言但更簡單
  • net/http:提供了HTTP客戶端和服務端的實現
Gin
  • gin.Default():返回Gin的type Engine struct{...},裏面包含RouterGroup,至關於建立一個路由Handlers,能夠後期綁定各種的路由規則和函數、中間件等
  • router.GET(...){...}:建立不一樣的HTTP方法綁定到Handlers中,也支持POST、PUT、DELETE、PATCH、OPTIONS、HEAD 等經常使用的Restful方法
  • gin.H{...}:就是一個map[string]interface{}
  • gin.ContextContextgin中的上下文,它容許咱們在中間件之間傳遞變量、管理流、驗證JSON請求、響應JSON請求等,在gin中包含大量Context的方法,例如咱們經常使用的DefaultQueryQueryDefaultPostFormPostForm等等
&http.Server 和 ListenAndServe?

一、http.Server:

type Server struct {
    Addr    string
    Handler Handler
    TLSConfig *tls.Config
    ReadTimeout time.Duration
    ReadHeaderTimeout time.Duration
    WriteTimeout time.Duration
    IdleTimeout time.Duration
    MaxHeaderBytes int
    ConnState func(net.Conn, ConnState)
    ErrorLog *log.Logger
}
複製代碼
  • Addr:監聽的TCP地址,格式爲:8000
  • Handler:http句柄,實質爲ServeHTTP,用於處理程序響應HTTP請求
  • TLSConfig:安全傳輸層協議(TLS)的配置
  • ReadTimeout:容許讀取的最大時間
  • ReadHeaderTimeout:容許讀取請求頭的最大時間
  • WriteTimeout:容許寫入的最大時間
  • IdleTimeout:等待的最大時間
  • MaxHeaderBytes:請求頭的最大字節數
  • ConnState:指定一個可選的回調函數,當客戶端鏈接發生變化時調用
  • ErrorLog:指定一個可選的日誌記錄器,用於接收程序的意外行爲和底層系統錯誤;若是未設置或爲nil則默認以日誌包的標準日誌記錄器完成(也就是在控制檯輸出)

二、 ListenAndServe:

func (srv *Server) ListenAndServe() error {
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
複製代碼

開始監聽服務,監聽TCP網絡地址,Addr和調用應用程序處理鏈接上的請求。

咱們在源碼中看到Addr是調用咱們在&http.Server中設置的參數,所以咱們在設置時要用&,咱們要改變參數的值,由於咱們ListenAndServe和其餘一些方法須要用到&http.Server中的參數,他們是相互影響的。

三、 http.ListenAndServe連載一r.Run()有區別嗎?

咱們看看r.Run的實現:

func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()

    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine)
    return
}
複製代碼

經過分析源碼,得知本質上沒有區別,同時也得知了啓動gin時的監聽debug信息在這裏輸出。

四、 爲何Demo裏會有WARNING

首先咱們能夠看下Default()的實現

// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}
複製代碼

你們能夠看到默認狀況下,已經附加了日誌、恢復中間件的引擎實例。而且在開頭調用了debugPrintWARNINGDefault(),而它的實現就是輸出該行日誌

func debugPrintWARNINGDefault() {
	debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
`)
}
複製代碼

而另一個Running in "debug" mode. Switch to "release" mode in production.,是運行模式緣由,並不難理解,已在配置文件的管控下 :-),運維人員隨時就能夠修改它的配置。

五、 Demo的router.GET等路由規則能夠不寫在main包中嗎?

咱們發現router.GET等路由規則,在Demo中被編寫在了main包中,感受很奇怪,咱們去抽離這部分邏輯!

go-gin-examplerouters目錄新建router.go文件,寫入內容:

package routers

import (
    "github.com/gin-gonic/gin"
    
    "github.com/EDDYCJY/go-gin-example/pkg/setting"
)

func InitRouter() *gin.Engine {
    r := gin.New()

    r.Use(gin.Logger())

    r.Use(gin.Recovery())

    gin.SetMode(setting.RunMode)

    r.GET("/test", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "test",
        })
    })

    return r
}
複製代碼

修改main.go的文件內容:

package main

import (
	"fmt"
	"net/http"

	"github.com/EDDYCJY/go-gin-example/routers"
	"github.com/EDDYCJY/go-gin-example/pkg/setting"
)

func main() {
	router := routers.InitRouter()

	s := &http.Server{
		Addr:           fmt.Sprintf(":%d", setting.HTTPPort),
		Handler:        router,
		ReadTimeout:    setting.ReadTimeout,
		WriteTimeout:   setting.WriteTimeout,
		MaxHeaderBytes: 1 << 20,
	}

	s.ListenAndServe()
}
複製代碼

當前目錄結構:

go-gin-example/
├── conf
│   └── app.ini
├── main.go
├── middleware
├── models
│   └── models.go
├── pkg
│   ├── e
│   │   ├── code.go
│   │   └── msg.go
│   ├── setting
│   │   └── setting.go
│   └── util
│       └── pagination.go
├── routers
│   └── router.go
├── runtime
複製代碼

重啓服務,執行 curl 127.0.0.1:8000/test查看是否正確返回。

下一節,咱們將以咱們的 Demo 爲起點進行修改,開始編碼!

參考

本系列示例代碼

若是有任何疑問或錯誤,歡迎在 issues 進行提問或給予修正意見,若是喜歡或對你有所幫助,歡迎 Star,對做者是一種鼓勵和推動。

個人博客

跟煎魚學 Go:github.com/eddycjy/blo…

個人公衆號

image
相關文章
相關標籤/搜索