资讯详情

Gin 快速入门知识点总结(奇淼)

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 参数:QueryDefaultQuery

// 匹配的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 参数:PostFormDefaultPostForm

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 提供了 MustBindShouldBind 两类绑定方法,使用 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

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台