使用gin框架实现简单blog项目。
环境配置不讲了。
初始化项目目录
项目名称为ginBlog,生成目录结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
[xiong@AMDServer ginBlog]$ tree . ├── go.mod ├── go.sum ├── conf ├── middleware ├── models ├── pkg ├── routers └── runtime 6 directories, 2 files |
目录讲解:
- conf 用于存储配置文件
- middleware 应用中间件
- models 应用数据库模型
- pkg 第三方包
- routers 路由逻辑处理
- runtime 应用运行时数据
go.mod 文件是go mod init命令生成的,这个自己去学习为什么这么做。
初始化项目数据库
新建ginBlog数据库,编码默认utf8mb4_general_ci。
新建表如下:
1. 标签表
1 2 3 4 5 6 7 8 9 10 |
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 COMMENT='文章标签管理'; |
2. 文章表
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 COMMENT = '文章管理'; |
3. 认证表
1 2 3 4 5 6 |
CREATE TABLE `blog_auth` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(50) DEFAULT '' COMMENT '账号', `password` varchar(64) DEFAULT '' COMMENT '密码', PRIMARY KEY (`id`) ) ENGINE=InnoDB COMMENT = '账号管理'; |
4. 最后初始化一个用户
1 |
INSERT INTO `ginBlog`.`blog_auth` (`id`, `username`, `password`) VALUES (null, 'root', 'root'); |
到这里数据库就算初始化成功了。
准备项目配置包
这里我们使用 go-ini ,GitHub地址:https://github.com/go-ini/ini
使用go get命令获取依赖包:
1 2 3 |
[xiong@AMDServer ginBlog]$ go get -u github.com/go-ini/ini go: downloading github.com/go-ini/ini v1.60.2 go: github.com/go-ini/ini upgrade => v1.60.2 |
然后我们在conf目录下新建 app.ini 文件,写入内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#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 = 数据库密码 HOST = 192.168.1.101:3306 NAME = ginBlog TABLE_PREFIX = blog_ |
接下来我们在pkg目录下新建setting目录,新建 setting.go 文件。建立调用配置的setting模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
package setting import ( "log" "time" "github.com/go-ini/ini" ) var ( //Cfg 文件操作句柄 Cfg *ini.File //DEFAULT_SECTION runMode string //app pageSize int jwtSecret string //server httpPort int readTimeOut time.Duration writeTimeOut time.Duration ) func init() { var err error Cfg, err = ini.Load("conf/app.ini") if err != nil { log.Fatalf("Failed to parse 'conf/app.ini': %v", err) } loadBase() loadServer() loadApp() } func loadBase() { runMode = Cfg.Section(ini.DEFAULT_SECTION).Key("RUN_MODE").MustString("debug") } func loadServer() { section, err := Cfg.GetSection("server") if err != nil { log.Fatalf("Failed to get section 'server': %v", err) } httpPort = section.Key("HTTP_PORT").MustInt(8000) readTimeOut = time.Duration(section.Key("READ_TIMEOUT").MustInt(60)) * time.Second writeTimeOut = time.Duration(section.Key("WRITE_TIMEOUT").MustInt(60)) * time.Second } func loadApp() { section, err := Cfg.GetSection("app") if err != nil { log.Fatalf("Failed to get section 'app': %v", err) } jwtSecret = section.Key("JWT_SECRET").MustString("!@)*#)!@U#@*!@!)") pageSize = section.Key("PAGE_SIZE").MustInt(10) } //GetRunMode 获取 Default Section RunMode 字段 // ret: runMode string 运行模式 func GetRunMode() string { return runMode } //GetAPPPageSize 获取 app Section PageSize 字段 // ret: pageSize int 分页大小 func GetAPPPageSize() int { return pageSize } //GetAPPJwtSecret 获取 app Section jwtSecret 字段 // ret: jwtSecret string 身份校验 func GetAPPJwtSecret() string { return jwtSecret } //GetServerHTTPPort 获取 server Section HTTPPort 字段 // ret: httpPort int 服务端口号 func GetServerHTTPPort() int { return httpPort } //GetServerReadTimeOut 获取 server Section ReadTimeOut 字段 // ret: ReadTimeOut time.Duration 读取超时时长 func GetServerReadTimeOut() time.Duration { return readTimeOut } //GetServerWriteTimeOut 获取 server Section WriteTimeOut 字段 // ret: WriteTimeOut time.Duration 写入超时时长 func GetServerWriteTimeOut() time.Duration { return writeTimeOut } |
这样我们就算将配置文件的内容读取到了程序中。
因为go lint强制要求驼峰命名法,要求导出的信息有注释…
编写API错误码包
建立错误码err模块,在pkg目录下新建err目录。
新建code.go文件,它负责定义错误码,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
package err const ( //Success 成功 Success = 200 //InvalidParams 无效参数 InvalidParams = 400 //Error 未知错误 Error = 500 //ErrorTagExist 已存在的标签名称 ErrorTagExist = 10001 //ErrorTagNotExist 该标签不存在 ErrorTagNotExist = 10002 //ErrorArticleNotExist 该文章不存在 ErrorArticleNotExist = 10003 //ErrorAuthCheckTokenFailed Token鉴权失败 ErrorAuthCheckTokenFailed = 20001 //ErrorAuthCheckTokenTimeOut Token鉴权超时 ErrorAuthCheckTokenTimeOut = 20002 //ErrorAuthToken Token生成失败 ErrorAuthToken = 20003 //ErrorAuth Token错误 ErrorAuth = 20004 ) |
新建msg.go文件,它负责将错误码转为错误信息,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package err var msgFlags = map[int]string{ Success: "ok", InvalidParams: "无效参数", Error: "failed", ErrorTagExist: "已存在的标签名称", ErrorTagNotExist: "该标签不存在", ErrorArticleNotExist: "该文章不存在", ErrorAuthCheckTokenFailed: "Token鉴权失败", ErrorAuthCheckTokenTimeOut: "Token鉴权超时", ErrorAuthToken: "Token生成失败", ErrorAuth: "Token错误", } //GetMsg 获取错误信息 // code: 错误码 // ret: 返回错误信息 func GetMsg(code int) string { msg, ok := msgFlags[code] if ok { return msg } return msgFlags[Error] } |
编写工具包
先拉取com依赖包,命令如下:
go get -u github.com/unknwon/com
编写分页页码获取方法
在pkg目录下新建util目录,新建pagination.go文件,文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package util import ( "ginBlog/pkg/setting" "github.com/gin-gonic/gin" "github.com/unknwon/com" ) //GetPage 获取分页页码 func GetPage(c *gin.Context) int { result := 0 page, _ := com.StrTo(c.Query("page")).Int() if page > 0 { result = (page - 1) * setting.GetAPPPageSize() } return result } |
编写Models
gorm是一个go的ORM库。
项目地址:https://github.com/go-gorm/gorm 。
教程地址:https://gorm.io/zh_CN/。
GORM 官方支持的数据库类型有: MySQL, PostgreSQL, SQlite, SQL Server。
具体的我们不展开讲,拉取依赖包代码如下:
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
在ginBlog的models目录下新建models.go文件,它主要用于models的初始化使用,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
package models import ( "fmt" "log" "ginBlog/pkg/setting" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/schema" ) var db *gorm.DB func init() { var err error section, err := setting.Cfg.GetSection("database") if err != nil { log.Fatalf("[models init] Failed to get section 'database': %v", err) } //dbType := section.Key("TYPE").String() user := section.Key("USER").String() passwd := section.Key("PASSWORD").String() host := section.Key("HOST").String() dbName := section.Key("NAME").String() tablePrefix := section.Key("TABLE_PREFIX").String() dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, passwd, host, dbName) db, err = gorm.Open( mysql.New( mysql.Config{ DriverName: "mysql", //驱动名, "gorm.io/driver/mysql" DSN: dsn, }, ), &gorm.Config{ NamingStrategy: schema.NamingStrategy{ TablePrefix: tablePrefix, //表名前缀 SingularTable: true, //使用单数表名,启用该选项,此时,`User` 的表名应该是 `t_user` }, }, ) if err != nil { log.Println(err) } //连接池 sqlDB, err := db.DB() if err != nil { log.Fatalf("[models init] Failed to call db.DB(): %v", err) } sqlDB.SetMaxIdleConns(10) sqlDB.SetMaxOpenConns(100) } //CloseDB 关闭连接 func CloseDB() { sqlDB, err := db.DB() if err != nil { log.Fatalf("[models CloseDB] Failed to call db.DB(): %v", err) } sqlDB.Close() } |
在从gorm版本v2和之前的v1代码有些不一致,没有找到Close方法,Github上给出的解决方案如下:
https://github.com/go-gorm/gorm/issues/3145
一般来说是不需要Close的,但是如果需要,也可以调用。
编写项目启动、路由文件
在src下建立main.go作为启动文件,写入文件内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package main import ( "fmt" "net/http" "ginBlog/pkg/setting" "github.com/gin-gonic/gin" ) 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.GetServerHTTPPort()), Handler: router, ReadTimeout: setting.GetServerReadTimeOut(), WriteTimeout: setting.GetServerWriteTimeOut(), MaxHeaderBytes: 1 << 20, } s.ListenAndServe() } |
然后接下来我们尝试执行一下 go run main.go命令。
1 2 3 4 5 6 7 8 |
[xiong@AMDServer ginBlog]$ 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) |
然后我们可以访问 192.168.1.101:8000/test,检查返回是否为 {“message”:”test”}。
当我访问之后,就可以看到新增的一行:
1 |
[GIN] 2020/09/02 - 09:50:18 | 200 | 78.819µs | 192.168.1.102 | GET "/test" |
知识点
Gin
gin.Default()
返回gin的Engine结构体,我们看看函数具体实现:
1 2 3 4 5 6 7 |
// 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 } |
Engine里面包含一个RouterGroup,相当于创建一个路由Handlers,可以后期绑定各类路由规则、函数、中间件等。
router.GET
创建不同的HTTP方法绑定到Handlers中,也支持POST、PUT、DELETE、PATCH、OPTIONS、HEAD等常用的Restful方法。
gin.Context
Context是gin中的上下文,它允许我们再中间件之间传递变量、管理流、验证Json请求、响应Json请求等。
在gin中包含大量的Context方法,比如我们常用的DefaultQuery、Query、DefaultPostFrom、PostFrom等。
gin.H
它的实现是一个 map[string]interface{}
net/http
http.Server
一个结构体,保存一系列的参数变量。
- Addr:监听TCP地址,格式为:port
- Handler:http句柄,实质为ServerHTTP,用于处理程序响应HTTP请求。
- ReadTimeout:读取超时时间
- WriteTimeOut:写入超时时间
- MaxHeaderBytes:请求头的最大字节数
另外还有一些其他参数:
- TLSConfig: 安全传输层协议TLS的配置
- ReadHeaderTimeOut:读取请求头的超时时间
- IdleTimeOut:等待超时时间
- ConnState:指定可选回调函数,客户端连接发生变化时调用
- ErrorLog:指定可选日志记录器,用于接收程序意外行为和底层系统错误。默认控制台输出。
ListenAndServe()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func (srv *Server) ListenAndServe() error { if srv.shuttingDown() { return ErrServerClosed } addr := srv.Addr if addr == "" { addr = ":http" } ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(ln) } |
监听TCP网络地址,调用应用程序处理请求。Addr是我们设定的端口号。
修改路由规则
router.Get等路由规则不应该写在main包中。
在routers目录新建router.go文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package routers import ( "ginBlog/pkg/setting" "github.com/gin-gonic/gin" ) //InitRouter 初始化路由 func InitRouter() *gin.Engine { r := gin.New() r.Use(gin.Logger()) r.Use(gin.Recovery()) gin.SetMode(setting.GetRunMode()) r.GET("/test", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "test", }) }) return r } |
然后修改main.go文件内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package main import ( "fmt" "net/http" "ginBlog/pkg/setting" "ginBlog/routers" ) func main() { router := routers.InitRouter() s := &http.Server{ Addr: fmt.Sprintf(":%d", setting.GetServerHTTPPort()), Handler: router, ReadTimeout: setting.GetServerReadTimeOut(), WriteTimeout: setting.GetServerWriteTimeOut(), MaxHeaderBytes: 1 << 20, } s.ListenAndServe() } |
最后使用tree命令查看当前目录结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[xiong@AMDServer ginBlog]$ tree . ├── conf │ └── app.ini ├── go.mod ├── go.sum ├── main.go ├── middleware ├── models │ └── models.go ├── pkg │ ├── error │ │ ├── code.go │ │ └── msg.go │ ├── setting │ │ └── setting.go │ └── util │ └── pagination.go ├── routers │ └── router.go └── runtime 9 directories, 10 files |
然后访问 http://192.168.1.101:8000/test 检查返回正确。