返回

Go学习笔记03-Gin框架+GORM基础使用

准备工作

  1. 创建工程参考之前的Go 学习笔记 01-Go 项目的创建与运行文章
  2. 创建完成后,导入gin包:github.com/gin-gonic/gin
  3. 运行下面这段代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import "github.com/gin-gonic/gin"

func main() {
    // 使用默认中间件创建一个gin路由器
    // logger and recovery (crash-free) 中间件
    router := gin.Default()
    //接收Get请求
    router.GET("/test", func(c *gin.Context) {
        //返回响应信息
        c.JSON(200, gin.H{
            "message": "hello gin",
        })
    })
    // 默认启动的是8080端口,也可以自己定义启动端口
    router.Run()
}

在浏览器中访问127.0.0.1:8080/test,可以看到浏览器显示hello gin即准备工作完成

四种请求的使用

 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
func main() {
    // 使用默认中间件创建一个gin路由器
    // logger and recovery (crash-free) 中间件
    router := gin.Default()
    //接收Get请求
    // :id是占位符,可以用来取url中的参数
    // 匹配的url格式: /test/123?user=wyatt&pwd=123456
    router.GET("/test/:id", func(c *gin.Context) {
        //获取uri参数
        id := c.Param("id")
        //获取query参数
        user := c.DefaultQuery("user", "nil") //默认值设置为nil
        pwd := c.Query("pwd")
        //返回响应信息
        c.JSON(200, gin.H{
            "success": true,
            "id":      id,
            "user":    user,
            "pwd":     pwd,
        })
    })
    router.POST("/test", func(c *gin.Context) {
        //获取表单中数据
        user := c.DefaultPostForm("user", "nil")
        pwd := c.PostForm("pwd")
        //返回响应信息
        c.JSON(200, gin.H{
            "success": true,
            "user":    user,
            "pwd":     pwd,
        })
    })
    // 匹配的url格式: /test/123
    router.DELETE("/test/:id", func(c *gin.Context) {
        id := c.Param("id")
        //返回响应信息
        c.JSON(200, gin.H{
            "success": true,
            "id":      id,
        })
    })
    router.PUT("/test", func(c *gin.Context) {
        //获取表单中数据
        user := c.DefaultPostForm("user", "nil")
        pwd := c.PostForm("pwd")
        //返回响应信息
        c.JSON(200, gin.H{
            "success": true,
            "user":    user,
            "pwd":     pwd,
        })
    })

    // 默认启动的是8080端口,也可以自己定义启动端口
    router.Run()
}

模型绑定和验证

  • 先创建一个结构体,然后把客户端传来的参数通过绑定的形式直接映射到结构体实例上
  • 模型绑定和验证 | Gin Web Framework
  • 可以给字段指定特定规则的修饰符,如果一个字段用binding:"required"修饰,并且在绑定时该字段的值为空,那么将返回一个错误。
 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
type PostParams struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
    Sex  bool   `json:"sex"`
}

func main() {
    router := gin.Default()
    router.POST("/test", func(c *gin.Context) {
        var p PostParams
        err := c.ShouldBindJSON(&p)
        if err != nil {
            c.JSON(200, gin.H{
                "msg":  "失败",
                "data": gin.H{},
            })
        } else {
            c.JSON(200, gin.H{
                "msg":  "成功",
                "data": p,
            })
        }
    })
    router.Run(":8080")
}

请求响应结果:

文件上传和返回

  • 注意:前端上传文件时需要在请求头Headers设置Content-Typemultipart/from-dataBody选择from-data

单文件

上传文件到服务器,服务器将接收到的文件先保存到本地然后直接返回接收到的文件信息(不是文件本身):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func main() {
    router := gin.Default()
    router.POST("/test", func(c *gin.Context) {
        //接收到文件file
        file, _ := c.FormFile("file")
        //获取附加的表单信息
        name:=c.PostForm("uploader")
        //保存到本地
        c.SaveUploadedFile(file, "./"+file.Filename)
        //返回响应信息
        c.JSON(200, gin.H{
            "msg": file,
            "uploader": name,
        })
    })
    router.Run(":8080")
}

请求响应结果:

上传文件到服务器,服务器将接收到的文件先保存到本地再将保存到本地的文件返回:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func main() {
    router := gin.Default()
    router.POST("/test", func(c *gin.Context) {
        //接收到文件file
        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")
}

请求响应结果:

多文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func main() {
    router := gin.Default()
    router.POST("/test", func(c *gin.Context) {
        //多文件接收
        form, _ := c.MultipartForm()
        files := form.File["file"]
        //打印接收到的所有文件名
        for _, file := range files {
            log.Println(file.Filename)
        }
    })
    router.Run(":8080")
}

路由分组和中间件

  • 路由分组:同一组的路由前缀相同
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func main() {
    router := gin.Default()
    // 简单的路由组: v1
    v1 := router.Group("/v1")
    // url: /v1/test
    v1.GET("/test", func(context *gin.Context) {
        fmt.Println("我在分组方法内部")
        context.JSON(200, gin.H{
            "success": true,
        })
    })
    router.Run(":8080")
}
  • 路由分组+中间件:洋葱模型
 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
func middle1() gin.HandlerFunc {
    return func(context *gin.Context) {
        fmt.Println("我在方法前,我是中间件1")
        context.Next() //继续往下走
        fmt.Println("我在方法后,我是中间件1")
    }
}
func middle2() gin.HandlerFunc {
    return func(context *gin.Context) {
        fmt.Println("我在方法前,我是中间件2")
        context.Next() //继续往下走
        fmt.Println("我在方法后,我是中间件2")
    }
}
func main() {
    router := gin.Default()
    // 简单的路由组: v1
    v1 := router.Group("/v1").Use(middle1()).Use(middle2())
    v1.GET("/test", func(context *gin.Context) {
        fmt.Println("我在分组方法内部")
        context.JSON(200, gin.H{
            "success": true,
        })
    })
    router.Run(":8080")
}

访问/v1/test后,控制台打印:

注意:当在中间件或handler中启动新的goroutine时,不能使用原始的上下文c *gin.Context, 必须使用其只读副本c.Copy()

日志和日志格式

初识GORM

对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。ORM框架是连接数据库的桥梁,只要提供了持久化类与表的映射关系,ORM框架在运行时就能参照映射文件的信息,把对象持久化到数据库中。

连接数据库

  1. 导入驱动程序和gorm包
  2. 打开Navicatmysql,新建一个数据库
  3. 填入数据库名、用户名和密码等信息后连接数据库
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)
func main() {
    dsn := "root:123@tcp(127.0.0.1:3306)/goclass?charset=utf8mb4&parseTime=True&loc=Local"
    _, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic(err)
    }
}

自动化创建数据库表

  1. 创建结构体
  2. 使用AutoMigrate方法,若表不存在则自动创建
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type Person struct {
	Name       string
	Sex        bool
	Age        int
	gorm.Model //gorm的底层model,是一些规范:主键、新增时间、删除时间、更新时间
}

func main() {
    dsn := "root:123@tcp(127.0.0.1:3306)/goclass?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic(err)
    }
    db.AutoMigrate(&Person{})
}

基本的增删改查

1
2
3
4
5
6
db.Create(&Person{
    Model: gorm.Model{},
    Name:  "wyatt",
    Sex:   true,
    Age:   21,
})
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//查询符合条件的一条记录
var person1 Person
db.First(&person1, "name = ?", "wyatt")
fmt.Println(person1)
//查询符合条件的所有记录
var persons1 []Person
db.Where("sex=?", 1).Find(&persons1)
fmt.Println(persons1)
//查询符合多个条件的所有记录
var persons2 []Person
db.Where("sex = ? AND age < ?", 1, 22).Find(&persons2)
fmt.Println(persons2)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//修改一条数据的单个属性
db.Where("id=?", 1).First(&Person{}).Update("name", "wang")
//修改一条数据的多个属性
db.Where("id=?", 1).First(&Person{}).Updates(Person{
    Name: "77",
    Age:  20,
})
//批量修改多条数据的多个属性
db.Where("id in (?)", []int{1, 2}).Find(&[]Person{}).Updates(Person{
    Name: "77",
    Age:  20,
})
1
2
3
4
5
//软删除
db.Delete(&Person{}, "id=?", 1)
db.Where("id in (?)", []int{1, 2}).Delete(&Person{})
//硬删除
db.Where("id in (?)", []int{1, 2}).Unscoped().Delete(&Person{})

gorm结构体创建技巧和结合gin使用

gorm结构体创建技巧

tag设置

工作中常用的:

1
2
3
4
5
6
7
8
type User struct {
    //包含第一个主键 id
    gorm.Model
    //定义第二个主键 Name
    //列名重定义为user_name
    //指定数据类型为varchar 长度为100
    Name string `gorm:"primarykey;column:user_name;type:varchar(100)"`
}

自定义表名

1
2
3
4
5
//这里实际是实现了gorm中一个接口的方法
func (u User) TableName() string {
    //可以用if语句作分支控制,这里不演示
    return "my_users"
}

关联

 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
type Class struct {
    gorm.Model
    ClassName string
    Students   []Student //1.一对多
}
type Student struct {
    gorm.Model
    StudentName string
    ClassID     uint   // 1.一对多
    IDCard      IDcard // 2.一对一
    // 3.多对多
    //`student_teachers` 是连接表
    Teachers []Teacher `gorm:"many2many:student_teachers;"`
}
type IDcard struct {
    gorm.Model
    StudentID uint // 2.一对一
    Num       int
}
type Teacher struct {
    gorm.Model
    TeacherName string
    // 3.多对多
    Students []Student `gorm:"many2many:student_teachers;"`
}

func main() {
    dsn := "root:123@tcp(127.0.0.1:3306)/goclass?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic(err)
    }
    db.AutoMigrate(&Teacher{}, &Class{}, &Student{}, &IDcard{})
}

运行结果:

填充数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
//填充数据
i := IDcard{
    Num: 123456,
}
t := Teacher{
    TeacherName: "老师傅",
}
s := Student{
    StudentName: "wyatt",
    IDCard:      i,
    Teachers:    []Teacher{t},
}
c := Class{
    ClassName: "wyatt's class",
    Students:  []Student{s},
}
// classes -> students -> id_cards -> teachers -> student_teachers
// 创建班级就能通过表之间关系创建其他表数据
//不然通过teacher反向创建学生没班级gorm v2版本会报外键约束错误
_ = db.Create(&c).Error

结合gin使用

从前端获取数据后写入数据库中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
    dsn := "root:123@tcp(127.0.0.1:3306)/goclass?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic(err)
    }
    db.AutoMigrate(&Teacher{}, &Class{}, &Student{}, &IDcard{})
    router := gin.Default()
    router.POST("/create_class", func(context *gin.Context) {
        var class Class
        _ = context.BindJSON(&class)
        db.Create(&class)
    })
    router.Run(":8888")
}

前端构造的json数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
    "ClassName":"class1",
    "Students":[
        {
            "StudentName":"学生1",
            "IDCard":{
                "Num":123
            },
            "Teachers":[{
                "TeacherName":"melody"
            }]
        },
        {
            "StudentName":"学生2",
            "IDCard":{
                "Num":234
            },
            "Teachers":[{
                "TeacherName":"melody"
            }]
        }]
}

从前端发送GET请求查询数据:

  • 涉及一对多、多对多时,要使用preload()预加载
  • 预加载可以嵌套
 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
func main() {
    dsn := "root:123@tcp(127.0.0.1:3306)/goclass?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic(err)
    }
    db.AutoMigrate(&Teacher{}, &Class{}, &Student{}, &IDcard{})
    router := gin.Default()
    router.POST("/create_class", func(context *gin.Context) {
        var class Class
        _ = context.BindJSON(&class)
        db.Create(&class)
    })
    router.GET("/student/:ID", func(context *gin.Context) {
        id := context.Param("ID")
        var student Student
        _ = context.BindJSON(&student)
        db.Preload("Teachers").Preload("IDCard").First(&student, "id=?", id)
        //预加载可以嵌套
        //db.Preload("Teachers").Preload("Teachers.Students").Preload("IDCard").First(&student, "id=?", id)
        context.JSON(200, gin.H{
            "s": student,
        })
    })
    router.Run(":8888")
}

查询到的结果:

gorm更多内容:bilibili

JWT-GO

  • JWT全称:JSON WEB TOKEN
  • 是一种后台不做存储的前端身份验证工具
  • 分为三部分:Header Claims Signature
  • GitHub
  • 文档

创建JWT

 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
type MyClaims struct {
    Username string `json:"username"`
    jwt.RegisteredClaims
}

func main() {
    //密钥
    mySigningKey := []byte("thisisakey")

    // Create the claims
    claims := MyClaims{
        "wyatt",
        jwt.RegisteredClaims{
            //生效时间
            NotBefore: jwt.NewNumericDate(time.Now()),
            //过期时间
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
            //签发人
            Issuer: "wyatt",
            //IssuedAt:  jwt.NewNumericDate(time.Now()),
            //Subject:  "somebody",
            //ID:       "1",
            //Audience: []string{"somebody_else"},
        },
    }

    //包含结构的创建一个JWT
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    //加密token
    ss, err := token.SignedString(mySigningKey)
    if err != nil {
        fmt.Printf("%v", err)
    }
    fmt.Printf("%v", ss)
}

解析JWT

1
2
3
4
5
6
7
8
9
//解析JWT
token, err := jwt.ParseWithClaims(ss, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
    return mySigningKey, nil
})
if err != nil {
    fmt.Println(err)
}
//这里用到了断言
fmt.Println(token.Claims.(*MyClaims).Username)

Casbin模型

Casbin基础模型

PERM元模型

  • Policy策略、Effect影响、Request请求、Matchers匹配规则
  • 定义一个策略P和匹配规则M,通过请求R过来的参数,与策略P通过规则M进行匹配,获取一个影响E,拿到影响E的结果进到影响E的表达式,返回一个布尔值

Policy策略

1
p={sub,obj,act,eft}
  • sub(subject,访问实体)、obj(obj,访问资源)、act(action,访问方法)、eft(策略结果,一般为空,默认指定allow,还可以定义为deny)
  • 策略一般存储到数据库,因为数量很多
1
2
[policy_definition]
p=sub,obj,act

Request请求

1
r={sub,obj,act}

Matchers匹配规则

1
m = r.sub == p.sub && r.act == p.act && r.obj == p.obj
  • MatchersRequestPolicy的匹配规则
  • 会把rp按照上述描述进行匹配,从而返回匹配结果(eft),如果不定义,会返回allow,如果定义过了,会返回我们定义过的那个结果

Effect影响

1
e = some(where(p.eft == allow))
  • 决定是否可以放行
  • 这里的规则是定死的,不是自定义的

role_definition角色域

role_definition角色域:用来存储用户的角色

1
g  =  用户,角色
  • 表示以角色为基础
  • 用户是那个角色
1
g = 用户,角色,域
  • 表示以域为基础(多商户模式)
  • 用户是那个角色属于那个商户(域)

Casbin使用

本地文件模式

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
31
32
package main

import (
    "fmt"
    "github.com/casbin/casbin/v2"
)

func main() {
    //要先手动在当前目录下创建两个文件
    e, err := casbin.NewEnforcer("./src/model.conf", "./src/policy.csv")
    if err != nil {
        fmt.Println(err)
    }
    sub := "zhangsan" // 想要访问资源的用户。
    obj := "data1"    // 将被访问的资源。
    act := "read"     // 用户对资源执行的操作。

    ok, err := e.Enforce(sub, obj, act)

    if err != nil {
        // 处理err
        //fmt.Printf("%s", err)
    }

    if ok == true {
        // 允许alice读取data1
        fmt.Println("通过")
    } else {
        // 拒绝请求,抛出异常
        fmt.Println("未通过")
    }
}

model.conf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

policy.csv

1
p,zhangsan,data1,read

使用数据库存储policy

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
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
    "fmt"
    "github.com/casbin/casbin/v2"
    gormadapter "github.com/casbin/gorm-adapter/v3"
    _ "github.com/go-sql-driver/mysql"
)

func main() {
    // Your driver and data source.
    a, _ := gormadapter.NewAdapter("mysql", "root:123@tcp(127.0.0.1:3306)/casbin", true)
    e, _ := casbin.NewEnforcer("./src/model.conf", a)

    sub := "alice" // 想要访问资源的用户。
    obj := "data1" // 将被访问的资源。
    act := "read"  // 用户对资源执行的操作。

    //通过API向数据库添加policy
    added, err := e.AddPolicy("alice", "data1", "read")
    fmt.Println(added)
    if err != nil {
        fmt.Println(err)
    }
    ok, err := e.Enforce(sub, obj, act)

    if err != nil {
        // 处理err
        //fmt.Printf("%s", err)
    }

    if ok == true {
        // 允许alice读取data1
        fmt.Println("通过")
    } else {
        // 拒绝请求,抛出异常
        fmt.Println("未通过")
    }
}
//运行结果:
//true
//通过

model.conf同上

提前建一个数据库casbin,casbin会自动建一个表casbin_rule

对policy增删改查

查:GetFilteredPolicy

1
2
3
4
5
//查看v0是alice的policy
filteredPolicy := e.GetFilteredPolicy(0, "alice")
fmt.Println(filteredPolicy)
//运行结果:
//[[alice data1 read]]

增:

1
added, err := e.AddGroupingPolicy("alice", "data2_admin")

model.conf中添加role_definition,修改role_definition

1
2
3
4
[role_definition]
g = _,_
[matchers]
m = g(r.sub,p.sub) && r.obj == p.obj && r.act == p.act

删:

改:

自定义比较函数

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
31
32
33
34
35
36
37
38
39
40
func main() {
    // Your driver and data source.
    a, _ := gormadapter.NewAdapter("mysql", "root:123@tcp(127.0.0.1:3306)/casbin", true)
    e, _ := casbin.NewEnforcer("./src/model.conf", a)

    // 注册自定义匹配原则 KeyMatchFunc
    e.AddFunction("my_func", KeyMatchFunc)

    sub := "alice" // 想要访问资源的用户。
    obj := "data1" // 将被访问的资源。
    act := "read"  // 用户对资源执行的操作。

    ok, err := e.Enforce(sub, obj, act)

    if err != nil {
        // 处理err
        fmt.Printf("%s", err)
    }

    if ok == true {
        // 允许alice读取data1
        fmt.Println("通过")
    } else {
        // 拒绝请求,抛出异常
        fmt.Println("未通过")
    }
}

// KeyMatch 第一个匹配原则
func KeyMatch(key1 string, key2 string) bool {
    return key1 == key2
}

// KeyMatchFunc 包装一下 KeyMatch
func KeyMatchFunc(args ...interface{}) (interface{}, error) {
    name1 := args[0].(string)
    name2 := args[1].(string)

    return (bool)(KeyMatch(name1, name2)), nil
}

model.conf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _,_

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub,p.sub) && my_func(r.obj,p.obj) && r.act == p.act
Built with Hugo
Theme Stack designed by Jimmy