=_= 学习煎鱼大佬的该项目
煎鱼大佬网站
原github地址
个人github项目地址,希望得到一点star
Go Modules 是go的依赖包管理工具,现在的go版本自动打开modules,目前的go get
命令也是需要进行初始化才能进行拉取
在准备的文件夹的终端中执行
$ go env -w GOPROXY=https://goproxy.cn,direct
$ go mod init [github.com/kingsill/gin-example]
go env -w GOPROXY=...
:设置 GOPROXY 代理,这里主要涉及到两个值,第一个是 https://goproxy.cn,它是由七牛云背书的一个强大稳定的 Go 模块代理,可以有效地解决你的外网问题;第二个是 direct,它是一个特殊的 fallback 选项,它的作用是用于指示 Go 在拉取模块时遇到错误会回源到模块版本的源地址去抓取(比如 GitHub 等)。
go mod init [MODULE_PATH]
:初始化 Go modules,它将会生成 go.mod 文件,需要注意的是 MODULE_PATH 填写的是模块引入路径,你可以根据自己的情况修改路径。
这里我们使用github域名作为项目名是证明这个包使用github进行存储。
此时 .mod文件内容。是当前的 模块路径 和 预期的 Go 语言版本 。
module github.com/kingsill/gin-example
go 1.21.4
在项目的根目录的命令行执行
$ go get -u github.com/gin-gonic/gin
此时我们的目录如下所示,多出.sum文件
go.sum
文件详细罗列了当前项目直接或间接依赖的所有模块版本,并写明了那些模块版本的 SHA-256 哈希值以备 Go 在今后的操作中保证项目所依赖的那些模块版本不会被篡改。
gin-example learn
├─ .idea
│ └─ workspace.xml
├─ go.mod
├─ go.sum
└─ README.md
此时go.mod文件也多了一些内容。
module github.com/kingsill/gin-example
go 1.21.4
require (
github.com/bytedance/sonic v1.10.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.16.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.6.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
go.mod 文件是启用了 Go modules 的项目所必须的最重要的文件,因为它描述了当前项目(也就是当前模块)的元信息,每一行都以一个动词开头,目前有以下 5 个动词:
你可能还会疑惑 indirect 是什么东西,indirect 的意思是传递依赖,也就是非直接依赖。
编写test.go文件并执行
package main
import "github.com/gin-gonic/gin"
func main() {
e := gin.Default()
e.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
e.Run()
}
访问 127.0.0.1:8080
,如下则正确安装
此时我们go.mod文件中使用的依赖 后缀也 都是indirect,这时候我们需要使用go mod tidy
进行依赖整理,这个命令非常常用
整理完之后go.mod文件如下所示:gin已经是直接依赖
module go-gin-example
go 1.21.4
require github.com/gin-gonic/gin v1.9.1
require (
github.com/bytedance/sonic v1.10.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.16.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.6.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
由于要避免以下问题:
配置文件本质上是包含成功操作程序所需信息的文件,这些信息以特定方式构成。是用户可配置的,通常存储在纯文本文件中
配置文件可以是各种格式,完全凭借程序员的发挥,不过出于方便,大部分会选择的配置文件格式集中在那几种.一般而言程序启动时,会加载该程序对应的配置文件内的信息
有这么几点作用:
1.数据库的连接工作
2.端口号的配置
3.打印日志等等
将目录结构更新到如下所示:
go-gin-example/
├── conf
├── middleware
├── models
├── pkg
├── routers
└── runtime
新建 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 get -u github.com/go-ini/ini
```
[ini中文文档](https://ini.unknwon.io/docs/intro/getting_started)
```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_
```
**相关内容记得填写自己的配置进去**
可以看到`ini配置`文件中,`默认分区`中定义RUN_MODE为debug,`app分区`中定义两项,`server分区`定义HTTP_PORT等,`database分区`定义了数据库相关信息
>个人理解:ini配置文件将我们可能用到的配置信息同意展示在配置文件里,有助于后续管理及更新
setting
模块go-gin-example的`pkg`目录下新建`setting目录`,新建 `setting.go` 文件,写入内容:
```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") //加载cong/app.ini文件,即我们自己创建的配置文件
if err != nil { //错误提示
log.Fatalf("Fail to parse 'conf/app.ini': %v", err)
}
LoadBase()
LoadServer()
LoadApp()
}
// LoadBase 加载运行模式
func LoadBase() {
//默认分区,使用"",key值为RUN_MODE,若配置中未指定,则默认为debug
RunMode = Cfg.Section("").Key("RUN_MODE").MustString("debug")
}
// LoadServer 加载服务器相关配置
func LoadServer() {
sec, err := Cfg.GetSection("server") //从配置文件检索server分区的内容,sec为获取的指定分区
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
}
// LoadApp 从配置文件中加载应用程序特定的设置
func LoadApp() {
sec, err := Cfg.GetSection("app") //从配置文件检索app分区的内容,sec为获取的指定分区
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
├─middleware
├─models
├─pkg
│ └─setting
| └──setting.go
├─routers
└─runtime
```
建立错误码的e
模块,在go-gin-example
的pkg
目录下新建e目录
,新建code.go
和msg.go
文件,写入内容:
这里即创建golang中的枚举方法,便于后期处理和维护,GO GORM 自定义数据类型-枚举这边文章具体提及其方法,可以参考
code.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
在msg.go
中 通过建立 int对应string的map表 将 code.go 中的字符常量 对应为 字符串,之后 通过GetMsg
函数 可以直接将 错误码 对应到 想要传递的错误信息
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
目录,并拉取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/kingsill/gin-example/pkg/setting"
)
// GetPage page 1 0;page 2 10;page 3 20
func GetPage(c *gin.Context) int {
result := 0 //默认查询结果为0,及第1页
page, _ := com.StrTo(c.Query("page")).Int() //查询url中包含的page信息并将其转化为int类型
if page > 0 { //如果获取的页数大于0
result = (page - 1) * setting.PageSize //返回(页数-1)×每页大小(在setting.go中已经配置过)
}
return result
}
关于其中gin的查询参数部分可以参考这部分gin 查询参数
//配置部分数据库连接相关内容
拉取gorm
的依赖包,拉取mysql驱动
的依赖包,如下:
$ go get -u github.com/jinzhu/gorm
$ go get -u github.com/go-sql-driver/mysql
有关gorm和mysql可以参看这两部分文章:
gorm mysql
完成后,在go-gin-example
的models
目录下新建models.go
,用于models的初始化使用
package models
import (
"fmt"
"log"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"github.com/kingsill/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"`
}
// 将一下定义为init函数
func init() {
var (
err error
dbType, dbName, user, password, host, tablePrefix string
)
//加载配置文件中database分区的数据
sec, err := setting.Cfg.GetSection("database") //cfj在setting模块中已经通过init函数进行初始化
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()
//使用gorm框架初始化数据库连接
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
}
//gorm默认使用复数映射,当前设置后即进行严格匹配
db.SingularTable(true)
//log记录打开
db.LogMode(true)
//进行连接池设置
db.DB().SetMaxIdleConns(10)
db.DB().SetMaxOpenConns(100)
}
// CloseDB 与数据库断开连接函数
func CloseDB() {
defer db.Close()
}
此时我依旧不太明白中间闭包的作用,还希望路过大神能够解惑
在go-gin-example下建立main.go作为启动文件(也就是main包),我们先写个Demo,帮助大家理解,写入文件内容:
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/kingsill/gin-example/pkg/setting"
)
// 这里的测试demo只使用到了pkg/setting包,即读取配置文件app. ini文件部分
func main() {
//创建一个默认路由器router
router := gin.Default()
//创建一个对应的路由handler处理get请求,这里定义了测试的ip即x.x.x/test
router.GET("/test", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "test",
})
})
//创建一个http服务器,将前面的router绑定为这里的处理器
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,或者在浏览器中输入ip地址,检查是否返回{“message”:“test”}。
笔者的路由、handler等定义可能比较混乱,也希望大家指出错误
那么,我们来延伸一下 Demo 所涉及的知识点!
标准库
Gin
&http.Server 和 ListenAndServe?
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
}
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中的参数,他们是相互影响的。
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 的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 {
//创建新的router而不是默认。
r := gin.New()
//定义router的配置
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
目标:完成博客的标签类接口定义和编写
本节正是编写标签的逻辑,我们想一想,一般接口为增删改查是基础的,那么我们定义一下接口吧!
开始编写路由文件逻辑,在routers下新建api目录,我们当前是第一个 API 大版本,因此在api下新建v1目录,再新建tag.go文件,写入内容:
package v1
import (
"github.com/gin-gonic/gin"
)
//获取多个文章标签
func GetTags(c *gin.Context) {
}
//新增文章标签
func AddTag(c *gin.Context) {
}
//修改文章标签
func EditTag(c *gin.Context) {
}
//删除文章标签
func DeleteTag(c *gin.Context) {
}
我们打开routers下的router.go文件,修改文件内容为:
package routers
import (
"github.com/gin-gonic/gin"
"github.com/kingsill/gin-example/pkg/setting"
"github.com/kingsill/gin-example/routers/api/v1"
)
func InitRouter() *gin.Engine {
//注册一个新的router
r := gin.New()
//使用logger
r.Use(gin.Logger())
r.Use(gin.Recovery())
//将运行模式放到setting中设置的模式上
gin.SetMode(setting.RunMode)
//路由分组,统一管理,统一增加 前缀
apiv1 := r.Group("/api/v1")
{
//获取标签列表
apiv1.GET("/tags", v1.GetTags)
//新建标签
apiv1.POST("/tags", v1.AddTag)
//更新指定标签
apiv1.PUT("/tags/:id", v1.EditTag)
//删除指定标签
apiv1.DELETE("/tags/:id", v1.DeleteTag)
}
//将本次注册的router返回,方便使用
return r
}
回到命令行,执行go run main.go
,检查路由规则是否注册成功。
$ go run main.go
[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 /api/v1/tags --> gin-blog/routers/api/v1.GetTags (3 handlers)
[GIN-debug] POST /api/v1/tags --> gin-blog/routers/api/v1.AddTag (3 handlers)
[GIN-debug] PUT /api/v1/tags/:id --> gin-blog/routers/api/v1.EditTag (3 handlers)
[GIN-debug] DELETE /api/v1/tags/:id --> gin-blog/routers/api/v1.DeleteTag (3 handlers)
首先我们要拉取validation
的依赖包,在后面的接口里会使用到表单验证
$ go get -u github.com/astaxie/beego/validation
创建models
目录下的tag.go
,写入文件内容:
package models
import (
"github.com/jinzhu/gorm"
"time"
)
// Tag 定义tag表的相关表头
type Tag struct {
Model
Name string `json:"name"`
CreatedBy string `json:"created_by"`
ModifiedBy string `json:"modified_by"`
State int `json:"state"`
}
//以下两个函数都为命名返回,函数内部有相关定义
// GetTags page-size,每页显示的tag数 pageNum,即为从第几条记录开始显示,从0开始计数
func GetTags(pageNum int, pageSize int, maps interface{}) (tags []Tag) {
// where查询使用map条件
//db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
db.Where(maps).Offset(pageNum).Limit(pageSize).Find(&tags) //db在models.go中已经定义,同一个包可以直接调用
return
}
// GetTagTotal 查询tag总数
func GetTagTotal(maps interface{}) (count int) {
db.Model(&Tag{}).Where(maps).Count(&count)
return
}
我们创建了一个Tag struct{},用于Gorm的使用。并给予了附属属性json,这样子在c.JSON的时候就会自动转换格式,非常的便利
可能会有的初学者看到return,而后面没有跟着变量,会不理解;其实你可以看到在函数末端,我们已经显示声明了返回值,这个变量在函数体内也可以直接使用,因为他在一开始就被声明了
有人会疑惑db是哪里来的;因为在同个models包下,因此db *gorm.DB是可以直接使用的
打开routers
目录下 v1
版本的tag.go
,第一我们先编写获取标签列表的接口
修改文件内容:
package v1
import (
"net/http"
"github.com/gin-gonic/gin"
//"github.com/astaxie/beego/validation"
"github.com/Unknwon/com"
"gin-blog/pkg/e"
"gin-blog/models"
"gin-blog/pkg/util"
"gin-blog/pkg/setting"
)
// GetTags 获取多个文章标签
func GetTags(c *gin.Context) {
//查询参数方法,及url中?name=xxx
name := c.Query("name")
maps := make(map[string]interface{})
data := make(map[string]interface{})
if name != "" {
maps["name"] = name
}
var state int = -1
if arg := c.Query("state"); arg != "" {
state = com.StrTo(arg).MustInt()
maps["state"] = state
}
code := e.SUCCESS //使用之前约定的错误码
data["lists"] = models.GetTags(util.GetPage(c), setting.PageSize, maps)
data["total"] = models.GetTagTotal(maps)
//gin.h是一种简便的返回json的方式
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": e.GetMsg(code),
"data": data,
})
}
//新增文章标签
func AddTag(c *gin.Context) {
}
//修改文章标签
func EditTag(c *gin.Context) {
}
//删除文章标签
func DeleteTag(c *gin.Context) {
}
在本机执行curl 127.0.0.1:8000/api/v1/tags
,正确的返回值为{"code":200,"data":{"lists":[],"total":0},"msg":"ok"}
,若存在问题请结合gin
结果进行拍错。
在获取标签列表接口中,我们可以根据name、state、page
来筛选查询条件,分页的步长可通过app.ini
进行配置,以lists、total
的组合返回达到分页效果。
接下来我们编写新增标签的接口
打开models
目录下的tag.go
,修改文件(增加 2 个方法):
...
// ExistTagByName 根据name查询tag是否存在
func ExistTagByName(name string) bool {
var tag Tag
//查询词条
db.Select("id").Where("name = ?", name).First(&tag)
if tag.ID > 0 {
return true
}
return false
}
// AddTag 创建新tag
func AddTag(name string, state int, createdBy string) bool {
//在对应数据表中创建词条
db.Create(&Tag{
Name: name,
State: state,
CreatedBy: createdBy,
})
return true
}
...
打开routers
目录下的tag.go
,修改文件(变动 AddTag
方法):
package v1
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/astaxie/beego/validation"
"github.com/Unknwon/com"
"gin-blog/pkg/e"
"gin-blog/models"
"gin-blog/pkg/util"
"gin-blog/pkg/setting"
)
...
// AddTag 新增文章标签
func AddTag(c *gin.Context) {
//参数查询 url中name
name := c.Query("name")
//参数查询 state 这里设置默认为0
state := com.StrTo(c.DefaultQuery("state", "0")).MustInt()
//参数查询
createdBy := c.Query("created_by")
valid := validation.Validation{}
valid.Required(name, "name").Message("名称不能为空")
valid.MaxSize(name, 100, "name").Message("名称最长为100字符")
valid.Required(createdBy, "created_by").Message("创建人不能为空")
valid.MaxSize(createdBy, 100, "created_by").Message("创建人最长为100字符")
valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
//运行到这里设置为 参数错误
code := e.INVALID_PARAMS
//将gin获得的数据与数据库作比较,进行验证
if !valid.HasErrors() {
if !models.ExistTagByName(name) {
code = e.SUCCESS
models.AddTag(name, state, createdBy)
} else {
code = e.ERROR_EXIST_TAG
}
}
//json相应
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": e.GetMsg(code),
"data": make(map[string]string),
})
}
...
用Postman
用 POST
访问http://127.0.0.1:8000/api/v1/tags?name=1&state=1&created_by=test
,查看code
是否返回200
及blog_tag
表中是否有值,有值则正确。
但是这个时候大家会发现,我明明新增了标签,但created_on
居然没有值,那做修改标签的时候modified_on
会不会也存在这个问题?
为了解决这个问题,我们需要打开models
目录下的tag.go
文件,修改文件内容(修改包引用和增加 2 个方法):
package models
import (
"time"
"github.com/jinzhu/gorm"
)
...
// BeforeCreate 建立hook钩子函数,在创建之前插入时间值
func (tag *Tag) BeforeCreate(scope *gorm.Scope) error {
//我们在定义的时候createon是int类型,因此我们这里使用unix方法将时间转变为时间戳
scope.SetColumn("CreatedOn", time.Now().Unix())
return nil
}
// BeforeUpdate 同为hook钩子函数,更新的时候加入修改时间值
func (tag *Tag) BeforeUpdate(scope *gorm.Scope) error {
scope.SetColumn("ModifiedOn", time.Now().Unix())
return nil
}
...
重启服务,再在用Postman
用 POST
访问http://127.0.0.1:8000/api/v1/tags?name=2&state=1&created_by=test
,发现created_on
已经有值了!
这里涉及到gorm的相关方式,更多的可以查看这篇gormhook相关的文章
接下来,我们一口气把剩余的两个接口(EditTag、DeleteTag
)完成吧
打开routers
目录下 v1
版本的tag.go
文件,修改内容:
...
// EditTag 修改文章标签
func EditTag(c *gin.Context) {
//.param 动态参数查询,并将其确定转换为int
id := com.StrTo(c.Param("id")).MustInt()
//参数查询,查询对应key
name := c.Query("name")
modifiedBy := c.Query("modified_by")
//设定验证信息
valid := validation.Validation{}
var state int = -1
if arg := c.Query("state"); arg != "" {
state = com.StrTo(arg).MustInt()
valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
}
valid.Required(id, "id").Message("ID不能为空")
valid.Required(modifiedBy, "modified_by").Message("修改人不能为空")
valid.MaxSize(modifiedBy, 100, "modified_by").Message("修改人最长为100字符")
valid.MaxSize(name, 100, "name").Message("名称最长为100字符")
code := e.INVALID_PARAMS
if !valid.HasErrors() {
code = e.SUCCESS
if models.ExistTagByID(id) {
data := make(map[string]interface{})
data["modified_by"] = modifiedBy
if name != "" {
data["name"] = name
}
if state != -1 {
data["state"] = state
}
models.EditTag(id, data)
} else {
code = e.ERROR_NOT_EXIST_TAG
}
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": e.GetMsg(code),
"data": make(map[string]string),
})
}
// DeleteTag 删除文章标签
func DeleteTag(c *gin.Context) {
//动态参数查询
id := com.StrTo(c.Param("id")).MustInt()
//验证信息
valid := validation.Validation{}
valid.Min(id, 1, "id").Message("ID必须大于0")
code := e.INVALID_PARAMS
if !valid.HasErrors() {
code = e.SUCCESS
if models.ExistTagByID(id) {
models.DeleteTag(id)
} else {
code = e.ERROR_NOT_EXIST_TAG
}
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": e.GetMsg(code),
"data": make(map[string]string),
})
}
打开models
下的tag.go
,修改文件内容:
...
// ExistTagByID 根据id查询表中tag是否存在
func ExistTagByID(id int) bool {
var tag Tag //实例化tag
db.Select("id").Where("id = ?", id).First(&tag)
if tag.ID > 0 {
return true
}
return false
}
// DeleteTag 根据id删除表中tag
func DeleteTag(id int) bool {
db.Where("id = ?", id).Delete(&Tag{})
return true
}
// EditTag 修改tag
func EditTag(id int, data interface{}) bool {
db.Model(&Tag{}).Where("id = ?", id).Updates(data)
return true
}
...
重启服务,用 Postman
PUT
访问 http://127.0.0.1:8000/api/v1/tags/1?name=edit1&state=0&modified_by=edit1
,查看 code
是否返回 200
DELETE
访问 http://127.0.0.1:8000/api/v1/tags/1
,查看 code
是否返回 200
Tag
的 API’s
完成,下一节我们将开始 Article
的 API’s
编写!本节编写文章的逻辑,我们定义一下接口吧!
在routers
的 v1
版本下,新建article.go
文件,写入内容:
package v1
import (
"github.com/gin-gonic/gin"
)
//获取单个文章
func GetArticle(c *gin.Context) {
}
//获取多个文章
func GetArticles(c *gin.Context) {
}
//新增文章
func AddArticle(c *gin.Context) {
}
//修改文章
func EditArticle(c *gin.Context) {
}
//删除文章
func DeleteArticle(c *gin.Context) {
}
我们打开routers
下的router.go
文件,修改文件内容为:
package routers
import (
"github.com/gin-gonic/gin"
"github.com/kingsill/gin-example/pkg/setting"
"github.com/kingsill/gin-example/routers/api/v1"
)
func InitRouter() *gin.Engine {
...
apiv1 := r.Group("/api/v1")
{
...
//获取文章列表
apiv1.GET("/articles", v1.GetArticles)
//获取指定文章
apiv1.GET("/articles/:id", v1.GetArticle)
//新建文章
apiv1.POST("/articles", v1.AddArticle)
//更新指定文章
apiv1.PUT("/articles/:id", v1.EditArticle)
//删除指定文章
apiv1.DELETE("/articles/:id", v1.DeleteArticle)
}
return r
}
当前目录结构:
go-gin-example/
├── conf
│ └── app.ini
├── main.go
├── middleware
├── models
│ ├── models.go
│ └── tag.go
├── pkg
│ ├── e
│ │ ├── code.go
│ │ └── msg.go
│ ├── setting
│ │ └── setting.go
│ └── util
│ └── pagination.go
├── routers
│ ├── api
│ │ └── v1
│ │ ├── article.go
│ │ └── tag.go
│ └── router.go
├── runtime
在基础的路由规则配置结束后,我们开始编写我们的接口吧!
创建models
目录下的article.go
,写入文件内容:
package models
import (
"github.com/jinzhu/gorm"
"time"
)
// Article 建立对应article表的struct结构体,方便进行信息读写
type Article struct {
Model
TagID int `json:"tag_id" gorm:"index"`
Tag Tag `json:"tag"`
Title string `json:"title"`
Desc string `json:"desc"`
Content string `json:"content"`
CreatedBy string `json:"created_by"`
ModifiedBy string `json:"modified_by"`
State int `json:"state"`
}
// BeforeCreate 与tag的逻辑相同 为了插入createOn时间戳
func (article *Article) BeforeCreate(scope *gorm.Scope) error {
scope.SetColumn("CreatedOn", time.Now().Unix())
return nil
}
// BeforeUpdate 与tag的逻辑相同 是为了更新数据是插入modifiedOn数据
func (article *Article) BeforeUpdate(scope *gorm.Scope) error {
scope.SetColumn("ModifiedOn", time.Now().Unix())
return nil
}
我们创建了一个Article struct {}
,与Tag
不同的是,Article
多了几项,如下:
gorm:index
,用于声明这个字段为索引,如果你使用了自动迁移功能则会有所影响,在不使用则无影响Tag
字段,实际是一个嵌套的struct
,它利用TagID
与Tag
模型相互关联,在执行查询的时候,能够达到Article、Tag
关联查询的功能;这里实际上为一对一的连接,具体可以查看该篇文章,但是如果是一对一连接的话(?),一篇文章就只能对应一个标签。这里暂且放过,看后续实现time.Now().Unix()
返回当前的时间戳...
// ExistArticleByID 根据id查询文章是否存在
func ExistArticleByID(id int) bool {
var article Article
db.Select("id").Where("id = ?", id).First(&article)
if article.ID > 0 {
return true
}
return false
}
// GetArticleTotal 获取文章总数,使用时通过map传递限制参数
func GetArticleTotal(maps interface{}) (count int) {
db.Model(&Article{}).Where(maps).Count(&count)
return
}
// GetArticles 显示文章列表,分页显示
func GetArticles(pageNum int, pageSize int, maps interface{}) (articles []Article) {
db.Preload("Tag").Where(maps).Offset(pageNum).Limit(pageSize).Find(&articles)
return
}
// GetArticle 通过id进行文章查询
func GetArticle(id int) (article Article) {
db.Where("id = ?", id).First(&article)
db.Model(&article).Related(&article.Tag)
return
}
// EditArticle 更新文章信息
func EditArticle(id int, data interface{}) bool {
db.Model(&Article{}).Where("id = ?", id).Updates(data)
return true
}
// AddArticle 添加文章
func AddArticle(data map[string]interface{}) bool {
db.Create(&Article{
TagID: data["tag_id"].(int),
Title: data["title"].(string),
Desc: data["desc"].(string),
Content: data["content"].(string),
CreatedBy: data["created_by"].(string),
State: data["state"].(int),
})
return true
}
// DeleteArticle 根据id删除文章
func DeleteArticle(id int) bool {
db.Where("id = ?", id).Delete(Article{})
return true
}
...
这里用到了gorm的很多相关知识,尤其是这里使用了关联的方法,可以从gorm专栏中学习有关知识,或者从官方的中文文档查询
func GetArticle(id int) (article Article) {
db.Where("id = ?", id).First(&article)
db.Model(&article).Related(&article.Tag)
return
}
能够达到关联,首先是gorm本身做了大量的约定俗成
Article
有一个结构体成员是TagID
,就是外键。gorm
会通过类名+ID
的方式去找到这两个类之间的关联关系Article
有一个结构体成员是Tag
,就是我们嵌套在Article
里的Tag
结构体,我们可以通过Related
进行关联查询Preload
是什么东西,为什么查询可以得出每一项的关联Tag
?func GetArticles(pageNum int, pageSize int, maps interface {}) (articles []Article) {
db.Preload("Tag").Where(maps).Offset(pageNum).Limit(pageSize).Find(&articles)
return
}
Preload
就是一个预加载器,它会执行两条 SQL,分别是SELECT * FROM blog_articles;和SELECT * FROM blog_tag WHERE id IN (1,2,3,4);,那么在查询出结构后,gorm
内部处理对应的映射逻辑,将其填充到Article
的Tag
中,会特别方便,并且避免了循环查询
v表示一个接口值,I表示接口类型。这个实际就是 Golang 中的类型断言,用于判断一个接口值的实际类型是否为某个类型,或一个非接口值的类型是否实现了某个接口类型
打开routers目录下 v1 版本的article.go文件,修改文件内容:
package v1
import (
"log"
"net/http"
"github.com/astaxie/beego/validation"
"github.com/gin-gonic/gin"
"github.com/unknwon/com"
"github.com/kingsill/gin-example/models"
"github.com/kingsill/gin-example/pkg/e"
"github.com/kingsill/gin-example/pkg/setting"
"github.com/kingsill/gin-example/pkg/util"
)
// GetArticle 获取单个文章
func GetArticle(c *gin.Context) {
id := com.StrTo(c.Param("id")).MustInt()
valid := validation.Validation{}
valid.Min(id, 1, "id").Message("ID必须大于0")
//定义初始的code
code := e.INVALID_PARAMS
var data interface{}
if !valid.HasErrors() {
if models.ExistArticleByID(id) {
data = models.GetArticle(id)
code = e.SUCCESS
} else {
code = e.ERROR_NOT_EXIST_ARTICLE
}
} else {
for _, err := range valid.Errors {
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
}
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": e.GetMsg(code),
"data": data,
})
}
// GetArticles 获取多个文章
func GetArticles(c *gin.Context) {
data := make(map[string]interface{})
maps := make(map[string]interface{})
valid := validation.Validation{}
var state int = -1
if arg := c.Query("state"); arg != "" {
state = com.StrTo(arg).MustInt()
maps["state"] = state
valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
}
var tagId int = -1
if arg := c.Query("tag_id"); arg != "" {
tagId = com.StrTo(arg).MustInt()
maps["tag_id"] = tagId
valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0")
}
code := e.INVALID_PARAMS
if !valid.HasErrors() {
code = e.SUCCESS
data["lists"] = models.GetArticles(util.GetPage(c), setting.PageSize, maps)
data["total"] = models.GetArticleTotal(maps)
} else {
for _, err := range valid.Errors {
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
}
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": e.GetMsg(code),
"data": data,
})
}
// AddArticle 新增文章
func AddArticle(c *gin.Context) {
tagId := com.StrTo(c.Query("tag_id")).MustInt()
title := c.Query("title")
desc := c.Query("desc")
content := c.Query("content")
createdBy := c.Query("created_by")
state := com.StrTo(c.DefaultQuery("state", "0")).MustInt()
valid := validation.Validation{}
valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0")
valid.Required(title, "title").Message("标题不能为空")
valid.Required(desc, "desc").Message("简述不能为空")
valid.Required(content, "content").Message("内容不能为空")
valid.Required(createdBy, "created_by").Message("创建人不能为空")
valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
code := e.INVALID_PARAMS
if !valid.HasErrors() {
if models.ExistTagByID(tagId) {
data := make(map[string]interface{})
data["tag_id"] = tagId
data["title"] = title
data["desc"] = desc
data["content"] = content
data["created_by"] = createdBy
data["state"] = state
models.AddArticle(data)
code = e.SUCCESS
} else {
code = e.ERROR_NOT_EXIST_TAG
}
} else {
for _, err := range valid.Errors {
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
}
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": e.GetMsg(code),
"data": make(map[string]interface{}),
})
}
// EditArticle 修改文章
func EditArticle(c *gin.Context) {
valid := validation.Validation{}
id := com.StrTo(c.Param("id")).MustInt()
tagId := com.StrTo(c.Query("tag_id")).MustInt()
title := c.Query("title")
desc := c.Query("desc")
content := c.Query("content")
modifiedBy := c.Query("modified_by")
var state int = -1
if arg := c.Query("state"); arg != "" {
state = com.StrTo(arg).MustInt()
valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
}
valid.Min(id, 1, "id").Message("ID必须大于0")
valid.MaxSize(title, 100, "title").Message("标题最长为100字符")
valid.MaxSize(desc, 255, "desc").Message("简述最长为255字符")
valid.MaxSize(content, 65535, "content").Message("内容最长为65535字符")
valid.Required(modifiedBy, "modified_by").Message("修改人不能为空")
valid.MaxSize(modifiedBy, 100, "modified_by").Message("修改人最长为100字符")
code := e.INVALID_PARAMS
if !valid.HasErrors() {
if models.ExistArticleByID(id) {
if models.ExistTagByID(tagId) {
data := make(map[string]interface{})
if tagId > 0 {
data["tag_id"] = tagId
}
if title != "" {
data["title"] = title
}
if desc != "" {
data["desc"] = desc
}
if content != "" {
data["content"] = content
}
data["modified_by"] = modifiedBy
models.EditArticle(id, data)
code = e.SUCCESS
} else {
code = e.ERROR_NOT_EXIST_TAG
}
} else {
code = e.ERROR_NOT_EXIST_ARTICLE
}
} else {
for _, err := range valid.Errors {
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
}
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": e.GetMsg(code),
"data": make(map[string]string),
})
}
// DeleteArticle 删除文章
func DeleteArticle(c *gin.Context) {
id := com.StrTo(c.Param("id")).MustInt()
valid := validation.Validation{}
valid.Min(id, 1, "id").Message("ID必须大于0")
code := e.INVALID_PARAMS
if !valid.HasErrors() {
if models.ExistArticleByID(id) {
models.DeleteArticle(id)
code = e.SUCCESS
} else {
code = e.ERROR_NOT_EXIST_ARTICLE
}
} else {
for _, err := range valid.Errors {
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
}
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": e.GetMsg(code),
"data": make(map[string]string),
})
}
当前目录结构
go-gin-example/
├── conf
│ └── app.ini
├── main.go
├── middleware
├── models
│ ├── article.go
│ ├── models.go
│ └── tag.go
├── pkg
│ ├── e
│ │ ├── code.go
│ │ └── msg.go
│ ├── setting
│ │ └── setting.go
│ └── util
│ └── pagination.go
├── routers
│ ├── api
│ │ └── v1
│ │ ├── article.go
│ │ └── tag.go
│ └── router.go
├── runtime
我们重启服务,执行go run main.go
,检查控制台输出结果
$ go run main.go
[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 /api/v1/tags --> gin-blog/routers/api/v1.GetTags (3 handlers)
[GIN-debug] POST /api/v1/tags --> gin-blog/routers/api/v1.AddTag (3 handlers)
[GIN-debug] PUT /api/v1/tags/:id --> gin-blog/routers/api/v1.EditTag (3 handlers)
[GIN-debug] DELETE /api/v1/tags/:id --> gin-blog/routers/api/v1.DeleteTag (3 handlers)
[GIN-debug] GET /api/v1/articles --> gin-blog/routers/api/v1.GetArticles (3 handlers)
[GIN-debug] GET /api/v1/articles/:id --> gin-blog/routers/api/v1.GetArticle (3 handlers)
[GIN-debug] POST /api/v1/articles --> gin-blog/routers/api/v1.AddArticle (3 handlers)
[GIN-debug] PUT /api/v1/articles/:id --> gin-blog/routers/api/v1.EditArticle (3 handlers)
[GIN-debug] DELETE /api/v1/articles/:id --> gin-blog/routers/api/v1.DeleteArticle (3 handlers)
使用Postman
检验接口是否正常,在这里大家可以选用合适的参数传递方式,此处为了方便展示我选用了GET/Param
传参的方式,而后期会改为 POST
。
新建文章:
POST:http://127.0.0.1:8000/api/v1/articles?tag_id=1&title=test1&desc=test-desc&content=test-content&created_by=test-created&state=1
获取文章列表:
GET:http://127.0.0.1:8000/api/v1/articles
查询指定文章:
GET:http://127.0.0.1:8000/api/v1/articles/1
修改文章:
PUT:http://127.0.0.1:8000/api/v1/articles/1?tag_id=1&title=test-edit1&desc=test-desc-edit&content=test-content-edit&modified_by=test-created-edit&state=0
删除文章:
DELETE:http://127.0.0.1:8000/api/v1/articles/1