Gin 快速入门总结
-
- 各种请求方式获取参数
- 验证绑定参数和参数
- 上传和返回文件
-
- 读取文件
- 将文件返回到前端
- 中间件和路由分组
-
- 分组
- 中间件
- 日志和日志格式
- 使用 GORM 操作数据库
-
- GORM 创建结构体的技巧
- GORM 结合 Gin 示例
- jwt-go
-
- 创建一个 JWT
- 解析一个 JWT
- 例子:创建和分析 JWT
- Casbin
-
- Casbin 模型基础
- RBAC
- 实战模型
- 模拟
- Casbin 代码
-
- 本地文件模式
- 使用数据库存储 policy
- 数据库对 policy 增删改查
- 自定义比较函数
参考视频:go圈里最会写js的奇淼 - gin 教程
参考文档:
- Gin 框架中文文档
- Gin 官方文档
- Gin 中文文档代码示例
最基本的项目:
import "github.com/gin-gonic/gin" func main() {
r := gin.Default() r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong", }) }) r.Run(":8090") // 修改端口号, 默认8080 }
浏览器访问:http://localhost:1010/path
,以下响应:
{
"success":true, }
获取各种请求方法的参数
获取 URI 中的参数:Param
// 本规则可匹配 /user/john 这种格式不匹配 /user/ 或 /user 这种格式 router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name") c.String(http.StatusOK, "Hello %s", name)
})
// 此规则既能匹配 /user/john/ 格式也能匹配 /user/john/send 这种格式
// 如果没有其他路由器匹配 /user/john,它将重定向到 /user/john/
router.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action
c.String(http.StatusOK, message)
})
获取 Get 参数:Query
、DefaultQuery
// 匹配的url格式: /welcome?firstname=Jane&lastname=Doe
router.GET("/welcome", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest") // 设置默认值
lastname := c.Query("lastname")
c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})
获取 Post 参数:PostForm
、DefaultPostForm
router.POST("/form_post", func(c *gin.Context) {
message := c.PostForm("message")
nick := c.DefaultPostForm("nick", "anonymous") // 此方法可以设置默认值
c.JSON(200, gin.H{
"status": "posted",
"message": message,
"nick": nick,
})
})
绑定参数和参数验证
文档:模型绑定和验证
Gin 提供了 MustBind
和 ShouldBind
两类绑定方法,使用 ShouldBind
就可以了。
文件的上传和返回
文档:
- 上传文件 - 单文件
- 上传文件 - 多文件
这里主要学习一下单文件的流程,多文件是差不多的。
读取文件
利用 c.FormFile("file")
读取文件,其中 "file"
是前端放到 file 里的 name
利用原生的方法将文件写到本地:
import (
"io"
"os"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.POST("/upload", func(c *gin.Context) {
file, _ := c.FormFile("file")
name := c.PostForm("name") // 获取其他数据
// 将文件写到本地
in, _ := file.Open()
defer in.Close()
out, _ := os.Create("./" + file.Filename)
defer out.Close()
io.Copy(out, in)
c.JSON(200, gin.H{
"file": file,
"name": name,
})
})
router.Run(":8080")
}
上面代码中也可以使用 :
file, _ := c.FormFile("file")
c.SaveUploadedFile(file, "./"+file.Filename) // 保存文件到指定位置
给前端返回文件
func main() {
router := gin.Default()
router.POST("/upload", func(c *gin.Context) {
file, _ := c.FormFile("file")
// 保存文件到本地
c.SaveUploadedFile(file, "./"+file.Filename)
// 将文件返给前端
c.Writer.Header().Add("Content-Disposition",
fmt.Sprintf("attachment; filename=%s", file.Filename))
c.File("./" + file.Filename)
})
router.Run(":8080")
}
中间件和路由分组
路由组 | Gin Web Framework (gin-gonic.com)
分组
?
- 对 router 创建 Group 就是分组,同一分组会拥有同一前缀和同一中间件
?
router := gin.Default()
// 简单的路由组: v1
v1 := router.Group("/v1")
{
v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEndpoint)
}
// 简单的路由组: v2
v2 := router.Group("/v2")
{
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint)
}
?
- 让路由结构更加清晰,更加方便的管理路由
中间件
使用中间件 | Gin Web Framework (gin-gonic.com)
自定义中间件 | Gin Web Framework (gin-gonic.com)
?
- 在请求达到路由的方法的前和后进行的一系列操作(洋葱模型)
?
- 在路由组上进行
use
操作,后面传入中间件函数即可
?
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
// 设置 example 变量
c.Set("example", "12345")
// 请求前
c.Next()
// 请求后
latency := time.Since(t)
log.Print(latency)
// 获取发送的 status
status := c.Writer.Status()
}
}
日志和日志格式
?
- 记录参数信息
- 猜测用户行为
- 复现系统 bug 并修复
Gin 中自带日志写入的中间件,但是自定义比较麻烦,不推荐使用。
:
- go-logging
- logrus
- zap
:
- 自行根据时间在写入时进行切割日志
- 借助现成的日志包:go-file-rotatelogs、file-rotatelogs
使用 GORM 进行数据库操作
参考:
- 1.1. 快速开始 · GORM 中文文档 (studygolang.com)
- GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.
- 《GORM 中文文档》 | Go 技术论坛 (learnku.com)
?
- 一种数据库操作辅助工具
- 在 go 的结构体和数据库之间进行映射,让表的内容直观的体现在结构体上
- 使用结构体来完成增删改查操作
?
- 导入 gorm
- 导入 mysql 驱动器
- 使用 open 链接得到 数据库操作对象
func main() {
db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
defer db.Close()
}
:
- gorm 支持自动迁移模式,使用
AutoMigrate
方法来自动化创建数据库表
db.AutoMigrate(&User{
})
主要参考文档,比较详细。
GORM 结构体的创建技巧
:模型定义 | GORM
- 设置主键:
primary_key
- 自定义字段名字:
column:user_id
- 忽略:
"_"
- 指定数据类型:
type:varchar(100)
- 非空:
not null
- 创建索引:
index
- 设置外键:
ForeignKey
- 关联外键:
AssociationForeignKey
- 多对多:
many2many2:表名
使用示例:
type user struct {
gorm.Model
Name string `gorm:"primary_key;column:user_name;type:varchar(100);"`
}
:约定 | GORM
func (User) TableName() string {
return "qm_user"
}
- 甚至可以做一些 :
func (u User) TableName() string {
if u.Role == "admin" {
return "admin_users"
} else {
return "qm_users"
}
}
:
- :学生和身份证,Has One | 关联
type Student struct {
gorm.Model
ClassID uint
IDCard IDCard // 1个学生拥有1张身份证
}
type IDCard struct {
gorm.Model
StudentID uint // 1张身份证属于1个学生
Num int
}
- :学生和班级,Has Many | 关联
type Class struct {
gorm.Model
Students []Student // 1个班级有N个学生
}
type IDCard struct {
gorm.Model
StudentID uint // 1张身份证属于1个学生
Num int
}
- :用户和语言,Many To Many | 关联
// User 拥有并属于多种 language,`user_languages` 是连接表
type User struct {
gorm.Model
// 用户可以拥有多种语言
Languages []*Language `gorm:"many2many:user_languages;"`
}
type Language struct {
gorm.Model
Name string
//
Users []*User `gorm:"many2many:user_languages;"`
}
建表示例:
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type Class struct {
gorm.Model
ClassName string
Students []Student // 1个班级有多个学生
}
type Student struct {
gorm.Model
StudentName string
ClassID uint // 1个学生属于多个班级
IDCard IDCard // 1个学生拥有1张身份证
// 多对多, 通过中间表关联
Teachers []Teacher `gorm:"many2many:student_teachers;"`
}
type IDCard struct {
gorm.Model
Num int
StudentID uint // 1张身份证属于1个学生
}
type Teacher struct {
gorm.Model
TeacherName string
// 多对多, 通过中间表关联
Students []Student `gorm:"many2many:student_teachers;"`
}
func main() {
CreateTable()
}
func CreateTable() {
dsn := "root:lzy123456@tcp(127.0.0.1:3306)/ginclass?charset=utf8mb4&parseTime=True&loc=Local"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
})
db.AutoMigrate(&Teacher{
}, &Class{
}, &Student{
}, &IDCard{
})
i := IDCard{
Num: 123456,
}
t := Teacher{
TeacherName: "老师傅",
}
s := Student{
StudentName: "qm",
IDCard: i,
Teachers: []Teacher{
t},
}
c := Class{
ClassName: "奇妙的班级",
Students: []Student{
s},
}
db.Create(&c)
db.Create(&t)
}
GORM 结合 Gin 示例
预加载 | 关联 |《GORM 中文文档 v2》| Go 技术论坛 (learnku.com)
注意:使用 Preload
可以查询出关联数据
func main() {
// 连接数据库
dsn := "root:lzy123456@tcp(127.0.0.1:3306)/ginclass?charset=utf8mb4&parseTime=True&loc=Local"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
})
db.AutoMigrate(&Teacher{
}, &Class{
}, &Student{
}, &IDCard{
})
r := gin.Default()
// 新增学生
r.POST("/student", func(c *gin.Context) {
var student Student
_ = c.BindJSON(&student)
db.Create(&student)
})
// 根据id查询学生
r.GET("/student/:id", func(c *gin.Context) {
id := c.Param("id")
var student Student
_ = c.BindJSON(&student)
// db.First(&student, "id = ?", id) // 无法加载出关联的数据
db.Preload("Teachers").Preload("IDCard").
First(&student, "id = ?", id) // 可以查出关联数据
c.JSON(200, gin.H{
"student": student})
})
// 根据id查询班级
r.GET("/class/:id", func(c *gin.Context) {
id := c.Param("id")
var class Class
// db.First(&class, "id = ?", id) // 无法加载出关联的数据
db.Preload("Students").Preload("Students.Teachers").Preload("Students.IDCard").
First(&class, "id = ?", id) // 可以查出关联数据
c.JSON(200, gin.H{
"class": class})
})
r.Run(":8888")
}
jwt-go
?
- 全称 JSON WEB TOKEN
- 一种后台不做存储的前端身份验证的工具
- 分为三部分
也有的地方分成:
引入 jwt-go
第三方库:
import "github.com/dgrijalva/jwt-go"
文档地址:https://pkg.go.dev/github.com/dgrijalva/jwt-go@v3.2.0+incompatible#section-readme
创建一个 JWT
通常使用 NewWithClaims
,因为我们可以通过匿名结构体来实现 Claims
接口,从而携带自己的参数。
源码中的 StandardClaims
结构体:
// Structured version of Claims Section, as referenced at
// https://tools.ietf.org/html/rfc7519#section-4.1
// See examples for how to use this with your own claim types
type StandardClaims struct {
Audience string `json:"aud,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
Id string `json:"jti,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
Issuer string `json:"iss,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
}
其中最重要的三个参数:NoteBefore
生效时间、ExpiresAt
过期时间、Issuer
签发者
两种常用的 Claims 的实现方式:
- 自定义结构体嵌入接口
type MyClaims struct {
Username string `json:"username"`
jwt.StandardClaims
}
- 使用它提供的 map
type MapClaims map[string]interface{
}
创建一个 Token:(详见后面的示例)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c) // 创建
s, err := t.SignedString(mySigningKey) // 签发
解析一个 JWT
主要通过以下方法实现:
func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error)
其中 keyFunc
是个特殊的回调函数,固定接受 *Token
类型指针,返回一个 i 和 err,i 就是我们的密钥
token, err := jwt.ParseWithClaims(s, &MyClaims{
},
func(t *jwt.Token) (interface{
}, error) {
return mySigningKey, nil
})
获取的 token 是一个 jwtToken
类型的数据,我们需要其中的 Claims
对 Claims 进行断言,然后取用即可
fmt.Println(token.Claims.(*MyClaims).Issuer)
示例:创建和解析 JWT
创建和解析 JWT 的示例:
type MyClaims struct { Username string `json:"username"` jwt.StandardClaims } func main() { // 创建一个 JWT mySigningKey := []byte("woxiangbianqiang!") c := MyClaims{ Username: "qimiao", StandardClaims: jwt.StandardClaims{ NotBefore: time.Now().Unix() - 60, // 生效时间 1 分钟 ExpiresAt: time.Now().Unix() + 2*60*60, // 失效时间 2 小时 Issuer: "lzy", // 签发者 }, } t := jwt.NewWithClaims(jwt.SigningMethodHS256, c) // 创建 Token s, _ := t.SignedString(mySigningKey) // 签发 Token fmt.Println(s) // token 的字符串 // 解析 JWT token, err := jwt.ParseWithClaims(s, &MyClaims{ }, func(t *jwt.Token) (interface{ }, error) { return mySigningKey, nil 标签:
sub连接器78p