80
若有任何问题或建议,欢迎及时交流和碰撞。我的公众号是 【脑子进煎鱼了】,GitHub 地址:https://github.com/eddycjy

Gin搭建Blog API's (一)

项目地址:https://github.com/EDDYCJY/go...

思考

首先,在一个初始项目开始前,大家都要思考一下

  1. 各种的程序配置写在代码中,好吗
  2. API的错误码硬编在程序中,合适吗
  3. db句柄谁都去Open,好吗
  4. 获取分页等公共参数,不统一管理起来,好吗

显然在较正规的项目中,这些问题的答案都是不可以

为了解决这些问题,我们挑选一款读写配置文件的库,本系列中选用go-ini/ini ,它的中文文档。大家需要先简单阅读它的文档,再接着完成后面的内容。

我们还会编写一个简单的API错误码包,并且完成一个Demo示例和讲解知识点,便于后面的学习。

介绍和初始化项目

初始工作区

首先,我们需要增加一个工作区(GOPATH)路径用于我们的Blog项目。

将你新的工作区加入到/etc/profile中的GOPATH环境变量中, 并在新工作区中,建立binpkgsrc三个目录。

src目录下创建gin-blog目录,初始的目录结构:

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

初始化项目目录

gin-blog/
├── conf
├── middleware
├── models
├── pkg
├── routers
└── runtime
  • conf:用于存储配置文件
  • middleware:应用中间件
  • models:应用数据库模型
  • pkg:第三方包
  • routers 路由逻辑处理
  • runtime 应用运行时数据

初始项目数据库

新建blog数据库,编码为utf8_general_ci

blog数据库下,新建以下表

1、 标签表

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、 文章表

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、 认证表

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-ini/ini的依赖包

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

我们需要编写基础的应用配置文件,在gin-blogconf目录下新建app.ini文件,写入内容:

#debug or release
RUN_MODE = debug
[app]
PAGE_SIZE = 10
JWT_SECRET = 23347040412ドル
[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模块,在gin-blogpkg目录下新建setting目录,新建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)
}

当前的目录结构:

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

编写API错误码包

建立错误码的e模块,在gin-blogpkg目录下新建e目录,新建code.gomsg.go文件,写入内容:

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

编写工具包

gin-blogpkg目录下新建util目录,

拉取com的依赖包

go get -u github.com/Unknwon/com

编写分页页码的获取方法

util目录下新建pagination.go,写入内容:

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
}

编写models init

拉取gorm的依赖包

go get -u github.com/jinzhu/gorm

拉取mysql驱动的依赖包

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

完成后,在gin-blogmodels目录下新建models.go,用于models的初始化使用

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

编写项目启动、路由文件

最基础的准备工作完成啦,让我们开始编写Demo吧!

编写Demo

gin-blog下建立main.go作为启动文件(也就是main包),

我们先写个Demo,帮助大家理解,写入文件内容:

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

执行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所涉及的知识点!

1、 标准库:

  • fmt:实现了类似C语言printf和scanf的格式化I/O。格式化动作('verb')源自C语言但更简单
  • net/http:提供了HTTP客户端和服务端的实现

2、 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.Context:Contextgin中的上下文,它允许我们在中间件之间传递变量、管理流、验证JSON请求、响应JSON请求等,在gin中包含大量Context的方法,例如我们常用的DefaultQueryQueryDefaultPostFormPostForm等等

3、 &http.ServerListenAndServe?

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中的参数,他们是相互影响的。

4、 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信息在这里输出。

5、 为什么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.,是运行模式原因,并不难理解,已在配置文件的管控下 :-),运维人员随时就可以修改它的配置。

6、 Demo的router.GET等路由规则可以不写在main包中吗?

我们发现router.GET等路由规则,在Demo中被编写在了main包中,感觉很奇怪,我们去抽离这部分逻辑!

gin-blogrouters目录新建router.go文件,写入内容:

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
}

修改main.go的文件内容:

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

当前目录结构:

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

重启服务,执行curl 127.0.0.1:8000/test查看是否正确返回。

下一节,我们将以我们的Demo为起点进行修改,开始编码!

参考

本系列示例代码

本系列目录


煎鱼
8.4k 声望12.8k 粉丝

引用和评论

0 条评论
评论支持部分 Markdown 语法:**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用 @ 来通知其他用户。

AltStyle によって変換されたページ (->オリジナル) /