- Gin搭建Blog API’s (一)
- 思考
- 介绍和初始化项目
- 初始工作区
- 初始化项目目录
- 初始项目数据库
- 编写项目配置包
- 编写API错误码包
- 编写工具包
- 编写分页页码的获取方法
- 编写models init
- 编写项目启动、路由文件
- 编写Demo
- 知识点
- 参考
- 本系列示例代码
- 本系列示例代码
Gin搭建Blog API’s (一)
项目地址:https://github.com/EDDYCJY/go-gin-example
思考
首先,在一个初始项目开始前,大家都要思考一下
- 各种的程序配置写在代码中,好吗
- API的错误码硬编在程序中,合适吗
- db句柄谁都去
Open,好吗 - 获取分页等公共参数,不统一管理起来,好吗
显然在较正规的项目中,这些问题的答案都是不可以
为了解决这些问题,我们挑选一款读写配置文件的库,本系列中选用go-ini/ini ,它的中文文档。大家需要先简单阅读它的文档,再接着完成后面的内容。
我们还会编写一个简单的API错误码包,并且完成一个Demo示例和讲解知识点,便于后面的学习。
介绍和初始化项目
初始工作区
首先,我们需要增加一个工作区(GOPATH)路径用于我们的Blog项目。
将你新的工作区加入到/etc/profile中的GOPATH环境变量中, 并在新工作区中,建立bin、pkg、src三个目录。
在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 '修改人',`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 '修改人',`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-blog的conf目录下新建app.ini文件,写入内容:
#debug or releaseRUN_MODE = debug[app]PAGE_SIZE = 10JWT_SECRET = 23347$040412[server]HTTP_PORT = 8000READ_TIMEOUT = 60WRITE_TIMEOUT = 60[database]TYPE = mysqlUSER = 数据库账号PASSWORD = 数据库密码#127.0.0.1:3306HOST = 数据库IP:数据库端口号NAME = blogTABLE_PREFIX = blog_
建立调用配置的setting模块,在gin-blog的pkg目录下新建setting目录,新建setting.go文件,写入内容:
package settingimport ("log""time""github.com/go-ini/ini")var (Cfg *ini.FileRunMode stringHTTPPort intReadTimeout time.DurationWriteTimeout time.DurationPageSize intJwtSecret string)func init() {var err errorCfg, 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.SecondWriteTimeout = 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-blog的pkg目录下新建e目录,新建code.go和msg.go文件,写入内容:
1、 code.go:
package econst (SUCCESS = 200ERROR = 500INVALID_PARAMS = 400ERROR_EXIST_TAG = 10001ERROR_NOT_EXIST_TAG = 10002ERROR_NOT_EXIST_ARTICLE = 10003ERROR_AUTH_CHECK_TOKEN_FAIL = 20001ERROR_AUTH_CHECK_TOKEN_TIMEOUT = 20002ERROR_AUTH_TOKEN = 20003ERROR_AUTH = 20004)
2、 msg.go:
package evar 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-blog的pkg目录下新建util目录,
拉取com的依赖包
go get -u github.com/Unknwon/com
编写分页页码的获取方法
在util目录下新建pagination.go,写入内容:
package utilimport ("github.com/gin-gonic/gin""github.com/Unknwon/com""gin-blog/pkg/setting")func GetPage(c *gin.Context) int {result := 0page, _ := 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-blog的models目录下新建models.go,用于models的初始化使用
package modelsimport ("log""fmt""github.com/jinzhu/gorm"_ "github.com/jinzhu/gorm/dialects/mysql""gin-blog/pkg/setting")var db *gorm.DBtype Model struct {ID int `gorm:"primary_key" json:"id"`CreatedOn int `json:"created_on"`ModifiedOn int `json:"modified_on"`}func init() {var (err errordbType, 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 mainimport ("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:
Context是gin中的上下文,它允许我们在中间件之间传递变量、管理流、验证JSON请求、响应JSON请求等,在gin中包含大量Context的方法,例如我们常用的DefaultQuery、Query、DefaultPostForm、PostForm等等
3、 &http.Server和ListenAndServe?
http.Server:
type Server struct {Addr stringHandler HandlerTLSConfig *tls.ConfigReadTimeout time.DurationReadHeaderTimeout time.DurationWriteTimeout time.DurationIdleTimeout time.DurationMaxHeaderBytes intConnState 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.Addrif 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-blog下routers目录新建router.go文件,写入内容:
package routersimport ("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 mainimport ("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为起点进行修改,开始编码!
参考
本系列示例代码
- go-gin-example
