你们好,我是煎鱼,这是本项目的地址:github.com/eddycjy/go-… ,若是有什么问题欢迎随时交流和沟通。mysql
首先,在一个初始项目开始前,你们都要思考一下git
程序的文本配置写在代码中,好吗?github
API 的错误码硬编码在程序中,合适吗?golang
db句柄谁都去Open
,没有统一管理,好吗?sql
获取分页等公共参数,谁都本身写一套逻辑,好吗?数据库
显然在较正规的项目中,这些问题的答案都是不能够,为了解决这些问题,咱们挑选一款读写配置文件的库,目前比较火的有 viper,有兴趣你将来能够简单了解一下,没兴趣的话等之后接触到再说。json
可是本系列选用 go-ini/ini ,它的 中文文档。你们是必须须要要简单阅读它的文档,再接着完成后面的内容。segmentfault
在前一章节中,咱们初始化了一个 go-gin-example
项目,接下来咱们须要继续新增以下目录结构:安全
go-gin-example/
├── conf
├── middleware
├── models
├── pkg
├── routers
└── runtime
复制代码
打开 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-example
的conf
目录下新建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-example
的pkg
目录下新建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
复制代码
创建错误码的e
模块,在go-gin-example
的pkg
目录下新建e
目录(注意新增 replace 配置),新建code.go
和msg.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-example
的pkg
目录下新建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
}
复制代码
拉取gorm
的依赖包,以下:
go get -u github.com/jinzhu/gorm
复制代码
拉取mysql
驱动的依赖包,以下:
go get -u github.com/go-sql-driver/mysql
复制代码
完成后,在go-gin-example
的models
目录下新建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吧!
在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所涉及的知识点!
type Engine struct{...}
,里面包含RouterGroup
,至关于建立一个路由Handlers
,能够后期绑定各种的路由规则和函数、中间件等Handlers
中,也支持POST、PUT、DELETE、PATCH、OPTIONS、HEAD 等经常使用的Restful方法map[string]interface{}
Context
是gin
中的上下文,它容许咱们在中间件之间传递变量、管理流、验证JSON请求、响应JSON请求等,在gin
中包含大量Context
的方法,例如咱们经常使用的DefaultQuery
、Query
、DefaultPostForm
、PostForm
等等一、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
}
复制代码
:8000
ServeHTTP
,用于处理程序响应HTTP请求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-example
下routers
目录新建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…