Gin Practice Serialization II Building Blog API’s (1)

  api, golang, mysql

Gin builds Blog API’s (1)

Project address:https://github.com/EDDYCJY/go …

Thinking

First of all, before an initial project starts, everyone should think about it

  1. All kinds of program configurations are written in code, okay
  2. Is it appropriate that the API error code is hard-coded into the program
  3. Db handle to everyoneOpen, ok
  4. Get public parameters such as paging and do not manage them uniformly, okay

Obviously, in more formal projects, the answers to these questions arecannot

In order to solve these problems, we choose a library for reading and writing configuration files, which is selected in this series.go-ini/ini, itsChinese document. Everyone needs to read its documents briefly first, and then finish the following contents.

We will also write a simple API error code package, andComplete a Demo example and explain the knowledge points.For later study.

Introduce and initialize the project

Initial workspace

First, we need to add a workspace GOPATH for ourBlogProject.

Add your new workspace to/etc/profilehit the targetGOPATHEnvironment variables, and in the new workspace, establishbinpkgsrcThree catalogues.

InsrcCreate under directorygin-blogDirectory, initial directory structure:

$GOPATH
├── bin
├── pkg
└── src
    └── gin-blog

Initialize project directory

gin-blog/
├── conf
├── middleware
├── models
├── pkg
├── routers
└── runtime
  • Conf: used to store configuration files
  • Middleware: Application Middleware
  • Models: applying database models
  • Pkg: Third Party Package
  • Routers routing logic processing
  • The runtime applies runtime data

Initial project database

NewblogDatabase, encoded asutf8_general_ci

InblogUnder the database, create the following table

1. Label List

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='文章标签管理';

2. Table of Articles

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='文章管理';

3. Certification Form

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');

Write a project configuration package

Pullgo-ini/iniDependency package for

go get -u github.com/go-ini/ini

We need to write basic application configuration files ingin-blogTheconfNew under directoryapp.iniFile, write content:

#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_

The that establishes the call configurationsettingModule, atgin-blogThepkgNew under directorysettingDirectory, Newsetting.goFile, write content:

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)
    }

    RunMode = Cfg.Section("").Key("RUN_MODE").MustString("debug")

    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)
}

Current directory structure:

gin-blog/
├── conf
│   └── app.ini
├── middleware
├── models
├── pkg
│   └── setting
│       └── setting.go
├── routers
├── runtime

Writing API Error Code Package

The that created the error codeeModule, atgin-blogThepkgNew under directoryeDirectory, Newcode.goAndmsg.goFile, write content:

1、 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
)

2、 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]
}

Develop a toolkit

Ingin-blogThepkgNew under directoryutilDirectory,

PullcomDependency package for

go get -u github.com/Unknwon/com

How to Get Pagination Page Number

InutilNew under directorypagination.go, write content:

package util

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

    "gin-blog/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
}

Writing modelinit

PullgormDependency package for

go get -u github.com/jinzhu/gorm

PullmysqlDriven dependency package

go get -u github.com/go-sql-driver/mysql

After completion, atgin-blogThemodelsNew under directorymodels.goFormodelsUse for initialization of

package models

import (
    "log"
    "fmt"

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

    "gin-blog/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.DB().SetMaxIdleConns(10)
    db.DB().SetMaxOpenConns(100)
}

func CloseDB() {
    defer db.Close()
}

Write project startup and routing files

The most basic preparations have been completed. Let’s start writing Demo.

Writing Demo

Ingin-blogUnder the establishment ofmain.goAs a startup file (i.e.mainBags),

Let’s write a firstDemo, to help you understand, write file content:

package main

import (
    "fmt"
    "net/http"

    "github.com/gin-gonic/gin"

    "gin-blog/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()
}

carry outgo run main.goTo see if the command line is displayed

[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)

Run locallycurl 127.0.0.1:8000/test, check whether to return{"message":"test"}.

Knowledge point

So, let’s extend the knowledge points involved in Demo!

1. Standard library:

  • fmt: Implemented formatted I/O similar to C languages printf and scanf. The formatting action (‘verb’) originates from the C language but is simpler
  • net/http: Provides the implementation of HTTP client and server

2、 Gin:

  • gin.Default(): that returns Gintype Engine struct{...}, which containsRouterGroupWhich is equivalent to creating a routeHandlers, you can later bind all kinds of routing rules and functions, middleware, etc.
  • router.GET(…){…}: Create different HTTP methods to bind toHandlersIn, also support POST, PUT, DELETE, PATCH, OPTIONS, HEAD and other commonly used Restful methods.
  • gin.H{…}: It’s just onemap[string]interface{}
  • gin.ContextContextYesginIt allows us to pass variables, manage flows, validate JSON requests, respond to JSON requests, and so on between middlewareginIt contains a large number ofContextThe method of, for example, we commonly usedDefaultQueryQueryDefaultPostFormPostFormWait

3、&http.ServerAndListenAndServe?

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 address monitored in the format:8000
  • Handler:http handle, essentiallyServeHTTPWhich is used by the handler to respond to HTTP requests
  • TLSConfig: configuration of security transport layer protocol (TLS)
  • ReadTimeout: maximum time allowed to read
  • ReadHeaderTimeout: Maximum time allowed to read the request header
  • WriteTimeout: maximum time allowed to write
  • IdleTimeout: Maximum Time to Wait
  • MaxHeaderBytes: Maximum number of bytes in the request header
  • ConnState: specifies an optional callback function to call when the client connection changes.
  • ErrorLog: Specify an optional logger to receive unexpected behavior of programs and underlying system errors; If is not set or isnilThe default is to use the standard logger of the log package (i.e. output at the console)

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)})
}

Start listening to services, listen to TCP network addresses, Addr, and call applications to process requests on connections.

We see in the source codeAddrIs to call us in&http.ServerThe parameter set in the, so we need to use when setting&, we have to change the value of the parameter because weListenAndServeAnd other methods need to be used&http.ServerThe parameters in the, they are mutual influence.

4、http.ListenAndServeAndSerial oneTher.Run()Is there any difference?

Let’s seer.RunImplementation of:

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
}

By analyzing the source code, we know thatThere is no difference in essence., at the same time also learned to startginWhen listening to debug information output here.

5. Why is there one in DemoWARNING?

First of all, we can look at itDefault()Implementation of

// 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
}

As you can see, by default, engine instances for logging and recovery middleware have been attached. And called at the beginningdebugPrintWARNINGDefault()And its implementation is to output the log of the line

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

And the other oneRunning in "debug" mode. Switch to "release" mode in production., which is the reason for the operation mode, it is not difficult to understand that under the control of the configuration file:-), the operation and maintenance personnel can modify its configuration at any time.

6. Demo’srouter.GETSuch routing rules may not be written inmainIs it in the bag?

We found thatrouter.GETSuch routing rules are written in DemomainIn the bag, I feel very strange, let’s pull away from this part of logic!

Ingin-blogunderroutersNew directoryrouter.goFile, write content:

package routers

import (
    "github.com/gin-gonic/gin"
    
    "gin-blog/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
}

Modifymain.goThe file content of:

package main

import (
    "fmt"
    "net/http"

    "gin-blog/routers"
    "gin-blog/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()
}

Current directory structure:

gin-blog/
├── 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

Restart service, executecurl 127.0.0.1:8000/testSee if it returns correctly.

In the next section, we will use our Demo as the starting point to modify and start coding!

References

This series of sample codes

This series of catalogues