Gin框架
Gin
环境:https://goproxy.cn,driect
github.com/gin-gonic/gin
介绍
Gin 是一个用 Go (Golang) 编写的 Web 框架。 它具有类似 martini 的 API,性能要好得多,多亏了 httprouter,速度提高了 40 倍。 如果您需要性能和良好的生产力,您一定会喜欢 Gin。
在本节中,我们将介绍 Gin 是什么,它解决了哪些问题,以及它如何帮助你的项目。
或者, 如果你已经准备在项目中使用 Gin,请访问快速入门.
源码分析
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
实现
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
- 通过对象池来减少内存申请和GC回收的消耗
- 取出来要用的时候再初始化
- 后台
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func GetIndex(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "index.html", nil)
}
func PostIndex(ctx *gin.Context) {
username := ctx.PostForm("username")
password := ctx.PostForm("password")
fmt.Println(username, password)
data := map[string]interface{}{
"code": 2000,
"message": "成功",
}
ctx.JSON(http.StatusOK, data)
}
func main() {
router := gin.Default()
router.LoadHTMLGlob("template/*")
router.Static("/static", "static")
router.GET("/get_index", GetIndex)
router.POST("/post_index", PostIndex)
router.Run(":8080")
}
参数绑定
无论时get请求的参数还是post请求的请求体,在后台都需要通过对应的方法来获取对应参数的值,那么有没有一种方式能够让我们定义好请求数据的格式,然后自动进行获取,这里可以通过参数绑定的方式来进行处理。它能够基于请求自动提取JSON、form表单和QueryString类型的数据,并把值绑定到指定的结构体对象。
这里以get请求的查询参数为例:
- 请求格式
http://127.0.0.1:8080/index?username=%22llkk%22&password=%22123%22
- 后台处理
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
type User struct {
Username string `form:"username" json:"username"`
Password string `form:"password" json:"password"`
}
func Index(ctx *gin.Context) {
var user User
err := ctx.ShouldBind(&user)
fmt.Println(err)
fmt.Println(user.Username, user.Password) // "llkk" "123"
ctx.String(http.StatusOK, "success")
}
func main() {
router := gin.Default()
router.GET("/index", Index)
router.Run(":8080")
}
文件处理
上传单个文件
- multipart/form-data格式用于文件上传
- gin文件上传与原生的net/http方法类似,不同在于gin把原生的request封装到c.Request中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<form action="http://localhost:8080/upload" method="post" enctype="multipart/form-data">
上传文件:<input type="file" name="file" >
<input type="submit" value="提交">
</form>
</body>
</html>
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
//限制上传最大尺寸
r.MaxMultipartMemory = 8 << 20
r.POST("/upload", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.String(500, "上传图片出错")
}
// c.JSON(200, gin.H{"message": file.Header.Context})
c.SaveUploadedFile(file, file.Filename)
c.String(http.StatusOK, file.Filename)
})
r.Run()
}
上传多个文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<form action="http://localhost:8000/upload" method="post" enctype="multipart/form-data">
上传文件:<input type="file" name="files" multiple>
<input type="submit" value="提交">
</form>
</body>
</html>
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"fmt"
)
// gin的helloWorld
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// 限制表单上传大小 8MB,默认为32MB
r.MaxMultipartMemory = 8 << 20
r.POST("/upload", func(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("get err %s", err.Error()))
}
// 获取所有图片
files := form.File["files"]
// 遍历所有图片
for _, file := range files {
// 逐个存
if err := c.SaveUploadedFile(file, file.Filename); err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("upload err %s", err.Error()))
return
}
}
c.String(200, fmt.Sprintf("upload ok %d files", len(files)))
})
//默认端口号是8080
r.Run(":8000")
}
快速分析
GET
get请求的参数会放在地址栏里,用明文的形式提交给后台 .
例如:
http://127.0.0.1:8081/path/123
r.GET("/path/:id", func(c *gin.Context) {
c.JSON(200, gin.H{
"success": true,
})
})
获得URL里面的参数:
http://127.0.0.1:8081/path/123?user=abc&pwd=123456
r.GET("/path/:id", func(c *gin.Context) {
id := c.Param("id") //获取占位表达式后面的参数
//user和pwd放在地址栏后面,所以叫query传参
user := c.Query("user") //
pwd := c.Query("pwd")
c.JSON(200, gin.H{
"id": id,
"user": user,
"pwd": pwd,
})
})
设置一个参数的默认值:
r.GET("/path/:id", func(c *gin.Context) {
id := c.Param("id") //获取占位表达式后面的参数
//user和pwd放在地址栏后面,所以叫query传参
user := c.DefaultQuery("user", "kaka") //设置user的默认值为kaka
pwd := c.Query("pwd")
c.JSON(200, gin.H{
"id": id,
"user": user,
"pwd": pwd,
})
})
POST
post的请求参数放在form或者body里,form即表单,body是以当前最流行的json格式进行交互。
r.POST("/path", func(c *gin.Context) {
user := c.DefaultPostForm("user", "aaa")
pwd := c.PostForm("pwd")
c.JSON(200, gin.H{
"user": user,
"pwd": pwd,
})
})
Delete
delete请求一般为uri,同样也可以用body
r.DELETE("/path/:id", func(c *gin.Context) {
id := c.Param("id") //获取占位表达式后面的参数
c.JSON(200, gin.H{
"id": id,
})
})
delete请求实际工作中用的不多,传参和取参方法和get类似
PUT
参数在form、body或者uri里
r.PUT("/path", func(c *gin.Context) {
user := c.DefaultPostForm("user", "aaa")
pwd := c.PostForm("pwd")
c.JSON(200, gin.H{
"user": user,
"pwd": pwd,
})
})
put传参和获取参数的方法和post基本一样
返回数据
[]byte
...
engine := gin.Defaut()
engine.GET("/hello",func(context *gin.Context){
fullPath := "请求路径: "+context.FullPath()
fmt.Println(fullPath)
context.Writer.Writer([]byte(fullPath))
})
engine.Run()
...
调用了http.ResponseWriter中包含的方法
string
...
engine := gin.Defaut()
engine.GET("/hello",func(context *gin.Context){
fullPath := "请求路径: "+context.FullPath()
fmt.Println(fullPath)
context.Writer.WriterString([]byte(fullPath))
})
engine.Run()
...
JSON
map
...
engine := gin.Defaut()
engine.GET("/hello",func(context *gin.Context){
fullPath := "请求路径: "+context.FullPath()
fmt.Println(fullPath)
context.JSON(200,map[string]interface{}{
"code":1,
"message":"OK",
"data":FullPath
})
})
engine.Run()
...
结构体
type Resopnse type{
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
...
engine := gin.Defaut()
engine.GET("/hello",func(context *gin.Context){
fullPath := "请求路径: "+context.FullPath()
fmt.Println(fullPath)
context.JSON(200,&Resopnse{
Code:1,
Message:"OK",
Data:FullPath
})
})
engine.Run()
...
数据解析
Json 数据解析和绑定
ShouldBindJSON("&xx")
- 客户端传参,后端接收并解析到结构体
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
// 定义接收数据的结构体
type Login struct {
// binding:"required"修饰的字段,若接收为空值,则报错,是必须字段
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Pssword string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// JSON绑定
r.POST("loginJSON", func(c *gin.Context) {
// 声明接收的变量
var json Login
// 将request的body中的数据,自动按照json格式解析到结构体
if err := c.ShouldBindJSON(&json); err != nil {
// 返回错误信息
// gin.H封装了生成json数据的工具
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 判断用户名密码是否正确
if json.User != "root" || json.Pssword != "admin" {
c.JSON(http.StatusBadRequest, gin.H{"status": "304"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "200"})
})
r.Run(":8000")
}
表单数据解析和绑定
Bind("&xx")
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<form action="http://localhost:8000/loginForm" method="post" enctype="application/x-www-form-urlencoded">
用户名<input type="text" name="username"><br>
密码<input type="password" name="password">
<input type="submit" value="提交">
</form>
</body>
</html>
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// 定义接收数据的结构体
type Login struct {
// binding:"required"修饰的字段,若接收为空值,则报错,是必须字段
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Pssword string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// JSON绑定
r.POST("/loginForm", func(c *gin.Context) {
// 声明接收的变量
var form Login
// Bind()默认解析并绑定form格式
// 根据请求头中content-type自动推断
if err := c.Bind(&form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 判断用户名密码是否正确
if form.User != "root" || form.Pssword != "admin" {
c.JSON(http.StatusBadRequest, gin.H{"status": "304"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "200"})
})
r.Run(":8000")
}
URI数据解析和绑定
ShouldBindUri()
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// 定义接收数据的结构体
type Login struct {
// binding:"required"修饰的字段,若接收为空值,则报错,是必须字段
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Pssword string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// JSON绑定
r.GET("/:user/:password", func(c *gin.Context) {
// 声明接收的变量
var login Login
// Bind()默认解析并绑定form格式
// 根据请求头中content-type自动推断
if err := c.ShouldBindUri(&login); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 判断用户名密码是否正确
if login.User != "root" || login.Pssword != "admin" {
c.JSON(http.StatusBadRequest, gin.H{"status": "304"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "200"})
})
r.Run(":8000")
}
视图渲染
各种数据格式的响应
- json、结构体、XML、YAML类似于java的properties、ProtoBuf
JSON
// 1.json
r.GET("/someJSON", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "someJSON", "status": 200})
})
结构体
// 2. 结构体响应
r.GET("/someStruct", func(c *gin.Context) {
var msg struct {
Name string
Message string
Number int
}
msg.Name = "root"
msg.Message = "message"
msg.Number = 123
c.JSON(200, msg)
})
XML
// 3.XML
r.GET("/someXML", func(c *gin.Context) {
c.XML(200, gin.H{"message": "abc"})
})
YAML
// 4.YAML响应
r.GET("/someYAML", func(c *gin.Context) {
c.YAML(200, gin.H{"name": "zhangsan"})
})
protoduf
// 5.protobuf格式,谷歌开发的高效存储读取的工具
// 数组?切片?如果自己构建一个传输格式,应该是什么格式?
r.GET("/someProtoBuf", func(c *gin.Context) {
reps := []int64{int64(1), int64(2)}
// 定义数据
label := "label"
// 传protobuf格式数据
data := &protoexample.Test{
Label: &label,
Reps: reps,
}
c.ProtoBuf(200, data)
})
Gin框架使用HTML模板渲染
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.LoadHTMLGlob("./templates/*")
r.GET("/demo", func(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "test.html", gin.H{
"name": "admin",
"pwd": "123456",
})
})
r.Run()
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
账号是:{{.name}}<br>
密码是:{{.pwd}}
</body>
</html>
重定向
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/index", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.5lmh.com")
})
r.Run()
}
同步异步
- goroutine机制可以方便地实现异步处理
- 另外,在启动新的goroutine时,不应该使用原始上下文,必须使用它的只读副本
package main
import (
"log"
"time"
"github.com/gin-gonic/gin"
)
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// 1.异步
r.GET("/long_async", func(c *gin.Context) {
// 需要搞一个副本
copyContext := c.Copy()
// 异步处理
go func() {
time.Sleep(3 * time.Second)
log.Println("异步执行:" + copyContext.Request.URL.Path)
}()
})
// 2.同步
r.GET("/long_sync", func(c *gin.Context) {
time.Sleep(3 * time.Second)
log.Println("同步执行:" + c.Request.URL.Path)
})
r.Run(":8000")
}
日志的打印上:
[GIN] 2022/08/01 - 10:48:56 | 200 | 0s | ::1 | GET "/long_async"
2022/08/01 10:48:59 异步执行:/long_async
2022/08/01 10:49:17 同步执行:/long_sync
[GIN] 2022/08/01 - 10:49:17 | 200 | 3.0071153s | ::1 | GET "/long_sync"
中间件
全局中间件
- 所有请求都经过此中间件
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
// 定义中间
func MiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
fmt.Println("中间件开始执行了")
// 设置变量到Context的key中,可以通过Get()取
c.Set("request", "中间件")
status := c.Writer.Status()
fmt.Println("中间件执行完毕", status)
t2 := time.Since(t)
fmt.Println("time:", t2)
}
}
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// 注册中间件
r.Use(MiddleWare())
// {}为了代码规范
{
r.GET("/ce", func(c *gin.Context) {
// 取值
req, _ := c.Get("request")
fmt.Println("request:", req)
// 页面接收
c.JSON(200, gin.H{"request": req})
})
}
r.Run()
}
中间件开始执行了
中间件执行完毕 200
time: 516.1µs
request: 中间件
[GIN] 2022/10/02 - 14:56:20 | 200 | 516.1µs | ::1 | GET "/ce"
Next()方法
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
// 定义中间
func MiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
fmt.Println("中间件开始执行了")
// 设置变量到Context的key中,可以通过Get()取
c.Set("request", "中间件")
// 执行函数
c.Next()
// 中间件执行完后续的一些事情
status := c.Writer.Status()
fmt.Println("中间件执行完毕", status)
t2 := time.Since(t)
fmt.Println("time:", t2)
}
}
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// 注册中间件
r.Use(MiddleWare())
// {}为了代码规范
{
r.GET("/ce", func(c *gin.Context) {
// 取值
req, _ := c.Get("request")
fmt.Println("request:", req)
// 页面接收
c.JSON(200, gin.H{"request": req})
})
}
r.Run()
}
中间件开始执行了
request: 中间件
中间件执行完毕 200
time: 235.9µs
[GIN] 2022/10/02 - 14:57:46 | 200 | 754.8µs | ::1 | GET "/ce"
局部中间件
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
// 定义中间
func MiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
fmt.Println("中间件开始执行了")
// 设置变量到Context的key中,可以通过Get()取
c.Set("request", "中间件")
// 执行函数
c.Next()
// 中间件执行完后续的一些事情
status := c.Writer.Status()
fmt.Println("中间件执行完毕", status)
t2 := time.Since(t)
fmt.Println("time:", t2)
}
}
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
//局部中间键使用
r.GET("/ce", MiddleWare(), func(c *gin.Context) {
// 取值
req, _ := c.Get("request")
fmt.Println("request:", req)
// 页面接收
c.JSON(200, gin.H{"request": req})
})
r.Run()
}
在中间件中使用Goroutine
当在中间件或 handler 中启动新的 Goroutine 时,不能使用原始的上下文,必须使用只读副本。
package main
import (
"github.com/gin-gonic/gin"
"log"
"time"
)
func main() {
r := gin.Default()
r.GET("/long_async", func(c *gin.Context) {
// 创建在 goroutine 中使用的副本
tmp := c.Copy()
go func() {
// 用 time.Sleep() 模拟一个长任务。
time.Sleep(5 * time.Second)
// 请注意您使用的是复制的上下文 "tmp",这一点很重要
log.Println("Done! in path " + tmp.Request.URL.Path)
}()
})
r.GET("/long_sync", func(c *gin.Context) {
// 用 time.Sleep() 模拟一个长任务。
time.Sleep(5 * time.Second)
// 因为没有使用 goroutine,不需要拷贝上下文
log.Println("Done! in path " + c.Request.URL.Path)
})
r.Run()
}
会话控制
Cookie介绍
- HTTP是无状态协议,服务器不能记录浏览器的访问状态,也就是说服务器不能区分两次请求是否由同一个客户端发出
- Cookie就是解决HTTP协议无状态的方案之一,中文是小甜饼的意思
- Cookie实际上就是服务器保存在浏览器上的一段信息。浏览器有了Cookie之后,每次向服务器发送请求时都会同时将该信息发送给服务器,服务器收到请求后,就可以根据该信息处理请求
- Cookie由服务器创建,并发送给浏览器,最终由浏览器保存
Cookie的用途
- 测试服务端发送cookie给客户端,客户端请求时携带cookie
Cookie的使用
- 测试服务端发送cookie给客户端,客户端请求时携带cookie
package main
import (
"github.com/gin-gonic/gin"
"fmt"
)
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// 服务端要给客户端cookie
r.GET("cookie", func(c *gin.Context) {
// 获取客户端是否携带cookie
cookie, err := c.Cookie("key_cookie")
if err != nil {
cookie = "NotSet"
// 给客户端设置cookie
// maxAge int, 单位为秒
// path,cookie所在目录
// domain string,域名
// secure 是否智能通过https访问
// httpOnly bool 是否允许别人通过js获取自己的cookie
c.SetCookie("key_cookie", "value_cookie", 60, "/",
"localhost", false, true)
}
fmt.Printf("cookie的值是: %s\n", cookie)
})
r.Run(":8000")
}
Gin设置日志
在Gin框架中记录日志方法如下
package main
import (
"io"
"os"
"github.com/gin-gonic/gin"
)
func main() {
// 禁用控制台颜色,将日志写入文件时不需要控制台颜色。
gin.DisableConsoleColor()
// 记录到文件。
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)
// 如果需要同时将日志写入文件和控制台,请使用以下代码。
// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
r.Run()
}
以上代码执行结果如下
Cookie的联系
- 模拟实现权限验证中间件
- 有2个路由,login和home
- login用于设置cookie
- home是访问查看信息的请求
- 在请求home之前,先跑中间件代码,检验是否存在cookie
- 访问home,会显示错误,因为权限校验未通过
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func AuthMiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
// 获取客户端cookie并校验
if cookie, err := c.Cookie("abc"); err == nil {
if cookie == "123" {
c.Next()
return
}
}
// 返回错误
c.JSON(http.StatusUnauthorized, gin.H{"error": "err"})
// 若验证不通过,不再调用后续的函数处理
c.Abort()
return
}
}
func main() {
// 1.创建路由
r := gin.Default()
r.GET("/login", func(c *gin.Context) {
// 设置cookie
c.SetCookie("abc", "123", 60, "/",
"localhost", false, true)
// 返回信息
c.String(200, "Login success!")
})
r.GET("/home", AuthMiddleWare(), func(c *gin.Context) {
c.JSON(200, gin.H{"data": "home"})
})
r.Run(":8000")
}
Cookie的缺点
- 不安全,明文
- 增加带宽消耗
- 可以被禁用
- cookie有上限
Sessions
gorilla/sessions为自定义session后端提供cookie和文件系统session以及基础结构。
主要功能是:
- 简单的API:将其用作设置签名(以及可选的加密)cookie的简便方法。
- 内置的后端可将session存储在cookie或文件系统中。
- Flash消息:一直持续读取的session值。
- 切换session持久性(又称“记住我”)和设置其他属性的便捷方法。
- 旋转身份验证和加密密钥的机制。
- 每个请求有多个session,即使使用不同的后端也是如此。
- 自定义session后端的接口和基础结构:可以使用通用API检索并批量保存来自不同商店的session。
代码:
package main
import (
"fmt"
"net/http"
"github.com/gorilla/sessions"
)
// 初始化一个cookie存储对象
// something-very-secret应该是一个你自己的密匙,只要不被别人知道就行
var store = sessions.NewCookieStore([]byte("something-very-secret"))
func main() {
http.HandleFunc("/save", SaveSession)
http.HandleFunc("/get", GetSession)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("HTTP server failed,err:", err)
return
}
}
func SaveSession(w http.ResponseWriter, r *http.Request) {
// Get a session. We're ignoring the error resulted from decoding an
// existing session: Get() always returns a session, even if empty.
// 获取一个session对象,session-name是session的名字
session, err := store.Get(r, "session-name")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 在session中存储值
session.Values["foo"] = "bar"
session.Values[42] = 43
// 保存更改
session.Save(r, w)
}
func GetSession(w http.ResponseWriter, r *http.Request) {
session, err := store.Get(r, "session-name")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
foo := session.Values["foo"]
fmt.Println(foo)
}
删除session的值:
// 删除
// 将session的最大存储时间设置为小于零的数即为删除
session.Options.MaxAge = -1
session.Save(r, w)
参数验证
结构体验证
用gin框架的数据验证,可以不用解析数据,减少if else,会简洁许多。
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
//Person ..
type Person struct {
//不能为空并且大于10
Age int `form:"age" binding:"required,gt=10"`
Name string `form:"name" binding:"required"`
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
}
func main() {
r := gin.Default()
r.GET("/5lmh", func(c *gin.Context) {
var person Person
if err := c.ShouldBind(&person); err != nil {
c.String(500, fmt.Sprint(err))
return
}
c.String(200, fmt.Sprintf("%#v", person))
})
r.Run()
}
自定义验证
都在代码里自己看吧
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"gopkg.in/go-playground/validator.v9"
)
/*
对绑定解析到结构体上的参数,自定义验证功能
比如我们需要对URL的接受参数进行判断,判断用户名是否为root如果是root通过否则返回false
*/
type Login struct {
User string `uri:"user" validate:"checkName"`
Pssword string `uri:"password"`
}
// 自定义验证函数
func checkName(fl validator.FieldLevel) bool {
if fl.Field().String() != "root" {
return false
}
return true
}
func main() {
r := gin.Default()
validate := validator.New()
r.GET("/:user/:password", func(c *gin.Context) {
var login Login
//注册自定义函数,与struct tag关联起来
err := validate.RegisterValidation("checkName", checkName)
if err := c.ShouldBindUri(&login); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err = validate.Struct(login)
if err != nil {
for _, err := range err.(validator.ValidationErrors) {
fmt.Println(err)
}
return
}
fmt.Println("success")
})
r.Run()
}
示例2:
package main
import (
"net/http"
"reflect"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v8"
)
// Booking contains binded and validated data.
type Booking struct {
//定义一个预约的时间大于今天的时间
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
//gtfield=CheckIn退出的时间大于预约的时间
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}
func bookableDate(
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
) bool {
//field.Interface().(time.Time)获取参数值并且转换为时间格式
if date, ok := field.Interface().(time.Time); ok {
today := time.Now()
if today.Unix() > date.Unix() {
return false
}
}
return true
}
func main() {
route := gin.Default()
//注册验证
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
//绑定第一个参数是验证的函数第二个参数是自定义的验证函数
v.RegisterValidation("bookabledate", bookableDate)
}
route.GET("/5lmh", getBookable)
route.Run()
}
func getBookable(c *gin.Context) {
var b Booking
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}
// curl -X GET "http://localhost:8080/5lmh?check_in=2019-11-07&check_out=2019-11-20"
// curl -X GET "http://localhost:8080/5lmh?check_in=2019-09-07&check_out=2019-11-20"
// curl -X GET "http://localhost:8080/5lmh?check_in=2019-11-07&check_out=2019-11-01"
自定义验证v10
介绍
Validator 是基于 tag(标记)实现结构体和单个字段的值验证库,它包含以下功能:
- 使用验证 tag(标记)或自定义验证器进行跨字段和跨结构体验证。
- 关于 slice、数组和 map,允许验证多维字段的任何或所有级别。
- 能够深入 map 键和值进行验证。
- 通过在验证之前确定接口的基础类型来处理类型接口。
- 处理自定义字段类型(如 sql 驱动程序 Valuer)。
- 别名验证标记,它允许将多个验证映射到单个标记,以便更轻松地定义结构体上的验证。
- 提取自定义的字段名称,例如,可以指定在验证时提取 JSON 名称,并在生成的 FieldError 中使用该名称。
- 可自定义 i18n 错误消息。
- Web 框架 gin 的默认验证器。
安装:
使用 go get:
go get github.com/go-playground/validator/v10
然后将 Validator 包导入到代码中:
import "github.com/go-playground/validator/v10"
变量验证
Var 方法使用 tag(标记)验证方式验证单个变量。
func (*validator.Validate).Var(field interface{}, tag string) error
它接收一个 interface{} 空接口类型的 field 和一个 string 类型的 tag,返回传递的非法值得无效验证错误,否则将 nil 或 ValidationErrors 作为错误。如果错误不是 nil,则需要断言错误去访问错误数组,例如:
validationErrors := err.(validator.ValidationErrors)
如果是验证数组、slice 和 map,可能会包含多个错误。
示例代码:
func main() {
validate := validator.New()
// 验证变量
email := "admin#admin.com"
email := ""
err := validate.Var(email, "required,email")
if err != nil {
validationErrors := err.(validator.ValidationErrors)
fmt.Println(validationErrors)
// output: Key: '' Error:Field validation for '' failed on the 'email' tag
// output: Key: '' Error:Field validation for '' failed on the 'required' tag
return
}
}
结构体验证
结构体验证结构体公开的字段,并自动验证嵌套结构体,除非另有说明。
func (*validator.Validate).Struct(s interface{}) error
它接收一个 interface{} 空接口类型的 s,返回传递的非法值得无效验证错误,否则将 nil 或 ValidationErrors 作为错误。如果错误不是 nil,则需要断言错误去访问错误数组,例如:
validationErrors := err.(validator.ValidationErrors)
实际上,Struct 方法是调用的 StructCtx 方法,因为本文不是源码讲解,所以此处不展开赘述,如有兴趣,可以查看源码。
示例代码:
func main() {
validate = validator.New()
type User struct {
ID int64 `json:"id" validate:"gt=0"`
Name string `json:"name" validate:"required"`
Gender string `json:"gender" validate:"required,oneof=man woman"`
Age uint8 `json:"age" validate:"required,gte=0,lte=130"`
Email string `json:"email" validate:"required,email"`
}
user := &User{
ID: 1,
Name: "frank",
Gender: "boy",
Age: 180,
Email: "gopher@88.com",
}
err = validate.Struct(user)
if err != nil {
validationErrors := err.(validator.ValidationErrors)
// output: Key: 'User.Age' Error:Field validation for 'Age' failed on the 'lte' tag
// fmt.Println(validationErrors)
fmt.Println(validationErrors.Translate(trans))
return
}
}
细心的读者可能已经发现,错误输出信息并不友好,错误输出信息中的字段不仅没有使用备用名(首字母小写的字段名),也没有翻译为中文。通过改动代码,使错误输出信息变得友好。
注册一个函数,获取结构体字段的备用名称:
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return "j"
}
return name
})
错误信息翻译为中文:
zh := zh.New()
uni = ut.New(zh)
trans, _ := uni.GetTranslator("zh")
_ = zh_translations.RegisterDefaultTranslations(validate, trans)
标签
通过以上章节的内容,读者应该已经了解到 Validator 是一个基于 tag(标签),实现结构体和单个字段的值验证库。
本章节列举一些比较常用的标签:
标签 | 描述 |
---|---|
eq | 等于 |
gt | 大于 |
gte | 大于等于 |
lt | 小于 |
lte | 小于等于 |
ne | 不等于 |
max | 最大值 |
min | 最小值 |
oneof | 其中一个 |
required | 必需的 |
unique | 唯一的 |
isDefault | 默认值 |
len | 长度 |
邮箱格式 |
转自: Golang语言开发栈
多语言翻译验证
当业务系统对验证信息有特殊需求时,例如:返回信息需要自定义,手机端返回的信息需要是中文而pc端发挥返回的信息需要时英文,如何做到请求一个接口满足上述三种情况。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
"github.com/go-playground/locales/zh_Hant_TW"
ut "github.com/go-playground/universal-translator"
"gopkg.in/go-playground/validator.v9"
en_translations "gopkg.in/go-playground/validator.v9/translations/en"
zh_translations "gopkg.in/go-playground/validator.v9/translations/zh"
zh_tw_translations "gopkg.in/go-playground/validator.v9/translations/zh_tw"
)
var (
Uni *ut.UniversalTranslator
Validate *validator.Validate
)
type User struct {
Username string `form:"user_name" validate:"required"`
Tagline string `form:"tag_line" validate:"required,lt=10"`
Tagline2 string `form:"tag_line2" validate:"required,gt=1"`
}
func main() {
en := en.New()
zh := zh.New()
zh_tw := zh_Hant_TW.New()
Uni = ut.New(en, zh, zh_tw)
Validate = validator.New()
route := gin.Default()
route.GET("/5lmh", startPage)
route.POST("/5lmh", startPage)
route.Run(":8080")
}
func startPage(c *gin.Context) {
//这部分应放到中间件中
locale := c.DefaultQuery("locale", "zh")
trans, _ := Uni.GetTranslator(locale)
switch locale {
case "zh":
zh_translations.RegisterDefaultTranslations(Validate, trans)
break
case "en":
en_translations.RegisterDefaultTranslations(Validate, trans)
break
case "zh_tw":
zh_tw_translations.RegisterDefaultTranslations(Validate, trans)
break
default:
zh_translations.RegisterDefaultTranslations(Validate, trans)
break
}
//自定义错误内容
Validate.RegisterTranslation("required", trans, func(ut ut.Translator) error {
return ut.Add("required", "{0} must have a value!", true) // see universal-translator for details
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("required", fe.Field())
return t
})
//这块应该放到公共验证方法中
user := User{}
c.ShouldBind(&user)
fmt.Println(user)
err := Validate.Struct(user)
if err != nil {
errs := err.(validator.ValidationErrors)
sliceErrs := []string{}
for _, e := range errs {
sliceErrs = append(sliceErrs, e.Translate(trans))
}
c.String(200, fmt.Sprintf("%#v", sliceErrs))
}
c.String(200, fmt.Sprintf("%#v", "user"))
}
正确的链接:
http://localhost:8080/testing?user_name=枯藤&tag_line=9&tag_line2=33&locale=zh
http://localhost:8080/testing?user_name=枯藤&tag_line=9&tag_line2=3&locale=en 返回英文的验证信息
http://localhost:8080/testing?user_name=枯藤&tag_line=9&tag_line2=3&locale=zh 返回中文的验证信息
查看更多的功能可以查看官网 gopkg.in/go-playground/validator.v9
文件操作
Gin 并没有提供文件的创建,删除,读写这个操作的专门的接口,所以采用的是常用的ioutil这个包进行文件的读写操作,使用os这个包进行文件的创建和删除。
文件的创建,写入内容,读取内容,删除.(此实例使用的是txt文件):
-controller
+file.go
-router
+router.go
main.go
//文件的创建删除和读写
router.GET("/cont/filerw", controllers.Filerwhtml) //获取文件api操作信息
router.POST("/cont/addfile", controllers.FilerCreate) //创建文件
router.POST("/cont/writefile", controllers.FilerWrite) //写入文件
router.POST("/cont/readfile", controllers.FilerRead) //读取文件
router.POST("/cont/deletefile", controllers.FilerDelete) //删除文件
package controllers
import (
"fmt"
"github.com/gin-gonic/gin"
"io/ioutil"
"net/http"
"os"
)
// 定义接收数据的结构体
type FileData struct {
// binding:"required"修饰的字段,若接收为空值,则报错,是必须字段
FileName string `form:"filename" json:"filename" uri:"filename" xml:"filename" binding:"required"`
Content string `form:"content" json:"content" uri:"content" xml:"content"`
}
//文件操作接口信息
type Data struct {
Api string `json:"api"`
Params string `json:"params"`
Remark string `json:"remark"`
}
/**文件读写操作接口信息**/
func Filerwhtml(c *gin.Context) {
list := []Data{
Data{
"/cont/addfile",
"filename",
"创建文件",
},
Data{
"/cont/writefile",
"filename,content",
"写入文件",
},
Data{
"/cont/readfile",
"filename",
"读取文件",
},
Data{
"/cont/deletefile",
"filename",
"删除文件",
},
}
//返回结果
c.JSON(http.StatusOK, gin.H{"code": 0, "list": list})
return
}
创建文件
/**创建文件**/
func FilerCreate(c *gin.Context) {
// 声明接收的变量
var data FileData
// 将request的body中的数据,自动按照json格式解析到结构体
if err := c.ShouldBindJSON(&data); err != nil {
// 返回错误信息
// gin.H封装了生成json数据的工具
c.JSON(http.StatusMovedPermanently, gin.H{"code:": 1, "msg: ": err.Error()})
return
}
//创建文件
path := fmt.Sprintf("static/txtfile/%s.txt", data.FileName)
f, err := os.Create(path)
fmt.Print(path)
if err != nil {
c.JSON(http.StatusMovedPermanently, gin.H{"code:": 1, "msg: ": "创建文件失败"})
fmt.Print(err.Error())
return
}
defer f.Close()
//返回结果
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "创建成功", "filename": data.FileName})
return
}
写入文件
/**将内容写入文件**/
func FilerWrite(c *gin.Context) {
// 声明接收的变量
var data FileData
// 将request的body中的数据,自动按照json格式解析到结构体
if err := c.ShouldBindJSON(&data); err != nil {
// 返回错误信息
// gin.H封装了生成json数据的工具
c.JSON(http.StatusMovedPermanently, gin.H{"code:": 1, "msg: ": err.Error()})
return
}
//需要写入到文件的内容
content := data.Content
path := fmt.Sprintf("static/txtfile/%s.txt", data.FileName)
d1 := []byte(content)
err := ioutil.WriteFile(path, d1, 0644)
fmt.Print(path)
if err != nil {
c.JSON(http.StatusMovedPermanently, gin.H{"code:": 1, "msg: ": "写入内容失败"})
fmt.Print(err.Error())
return
}
//返回结果
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "写入内容成功", "filename": data.FileName, "content": content})
return
}
读取文件
/**读取文件内容**/
func FilerRead(c *gin.Context) {
// 声明接收的变量
var data FileData
// 将request的body中的数据,自动按照json格式解析到结构体
if err := c.ShouldBindJSON(&data); err != nil {
// 返回错误信息
// gin.H封装了生成json数据的工具
c.JSON(http.StatusMovedPermanently, gin.H{"code:": 1, "msg: ": err.Error()})
return
}
path := fmt.Sprintf("static/txtfile/%s.txt", data.FileName)
//文件读取任务是将文件内容读取到内存中。
info, err := ioutil.ReadFile(path)
fmt.Print(path)
if err != nil {
c.JSON(http.StatusMovedPermanently, gin.H{"code:": 1, "msg: ": "读取文件内容失败"})
fmt.Print(err.Error())
return
}
fmt.Println(info)
result := string(info)
//返回结果
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "读取内容成功", "filename": data.FileName, "content": result})
return
}
删除文件
/**删除文件**/
func FilerDelete(c *gin.Context) {
// 声明接收的变量
var data FileData
// 将request的body中的数据,自动按照json格式解析到结构体
if err := c.ShouldBindJSON(&data); err != nil {
// 返回错误信息
// gin.H封装了生成json数据的工具
c.JSON(http.StatusMovedPermanently, gin.H{"code:": 1, "msg: ": err.Error()})
return
}
path := fmt.Sprintf("static/txtfile/%s.txt", data.FileName) //源文件路径
//删除文件
cuowu := os.Remove(path)
fmt.Print(path)
if cuowu != nil {
//如果删除失败则输出 file remove Error!
fmt.Println("file remove Error!")
//输出错误详细信息
fmt.Printf("%s", cuowu)
c.JSON(http.StatusMovedPermanently, gin.H{"code:": 1, "msg: ": "删除文件失败"})
return
} else {
//如果删除成功则输出 file remove OK!
fmt.Print("file remove OK!")
}
//返回结果
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "删除文件成功", "filename": data.FileName})
return
}
文件上传下载
- controller
+file.go
-uploadFile
+.....
-router
+router.go
-main.go
package controller
import (
"fmt"
"log"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// AddUploads 上传文件
func AddUploads(c *gin.Context) {
username := c.PostForm("username")
// 单个文件
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}
log.Println(file.Filename)
// dst := fmt.Sprintf("D:/桌面/文件/updateFile/%s", username+"-"+file.Filename)
dst := fmt.Sprintf(".updateFile/%s", username+"-"+file.Filename)
// 上传文件到指定的目录
c.SaveUploadedFile(file, dst)
c.JSON(http.StatusOK, gin.H{
"username": username,
"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
})
}
// DownFile
func DownFile(c *gin.Context) {
fileName := "hjz-开题报告.docx"
filepath := "./updateFile/" + fileName
list := strings.Split(fileName, "-")
downFileName := list[1]
c.Writer.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downFileName))
c.Writer.Header().Add("Content-Type", "application/octet-stream")
fmt.Println(filepath)
c.File(filepath)
}
基本问题:
类型转换
string -- int64
func DeletedFileOne(c *gin.Context) {
fidStr := c.Param("fid")
fid, err := strconv.ParseInt(fidStr, 10, 64)
if err != nil {
ResponseError(c, CodeInvalidParam)
return
}
if err2 := logic.DeletedFileOne(fid); err2 != nil {
zap.L().Error("logic.DeletedFileOne () failed ", zap.Error(err))
ResponseError(c, CodeServerBusy)
return
}
ResponseSuccess(c, CodeSuccess)
}
int64--string
strconv.FormatInt(v.NucleicAcidID, 10)
得到当前时间戳
// GetNowTime 得到现在时间的年月日的时间戳
func GetNowTime() int64 {
tm := time.Now().Format("2006-01-02")
tt, _ := time.ParseInLocation("2006-01-02", tm, time.Local)
return tt.Unix()
}
时间戳---time
time.Unix(v.TodayTime, 0)
// 秒级时间戳转time
func UnixSecondToTime(second int64) time.Time {
return time.Unix(second, 0)
}
// 毫秒级时间戳转time
func UnixMilliToTime(milli int64) time.Time {
return time.Unix(milli/1000, (milli%1000)*(1000*1000))
}
// 纳秒级时间戳转time
func UnixNanoToTime(nano int64) time.Time {
return time.Unix(nano/(1000*1000*1000), nano%(1000*1000*1000))
}
Gin框架解析
路由解析
中间件解析
微信小程序
uni.login({
provider: "weixin",
success: function (res) {
uni.request({
method:'POST',
url: 'http://106.15.65.147:8081/api/v1/wx/openid', //仅为示例,并非真实接口地址。
data: {
"app_id": "wx8d36d8370b6e82f0",
"code": res.code,
"method": "get",
"secret": "092d4b45d6b6c8d2b99bf82c6e23657e",
"url": "https://api.weixin.qq.com/sns/jscode2session"
},
header: {
'content-type': 'application/json' //自定义请求头信息
},
success: (res) => {
var result = res.data
uni.setStorageSync('open_id',result.data.open_id)
uni.setStorageSync('session_key',result.data.session_key)
}
});
},
});
转发请求
post
// 发送post请求
func main() {
router := gin.Default()
router.GET("/test", func(c *gin.Context) {
var body = strings.NewReader("name=test&jab=teache")
response, err := http.Post("http://localhost:8888/base/captcha","application/json; charset=utf-8", body)
if err != nil || response.StatusCode != http.StatusOK {
c.Status(http.StatusServiceUnavailable)
return
}
reader := response.Body
contentLength := response.ContentLength
contentType := response.Header.Get("Content-Type")
extraHeaders := map[string]string{
//"Content-Disposition": `attachment; filename="gopher.png"`,
}
c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
})
router.Run(":8080")
}
golang发送get请求第三方数据
func main() {
router := gin.Default()
router.GET("/test", func(c *gin.Context) {
response, err := http.Get("https://baidu.com")
if err != nil || response.StatusCode != http.StatusOK {
c.Status(http.StatusServiceUnavailable)
return
}
reader := response.Body
contentLength := response.ContentLength
contentType := response.Header.Get("Content-Type")
extraHeaders := map[string]string{
//"Content-Disposition": `attachment; filename="gopher.png"`,
}
c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
})
router.Run(":8080")
}
跨域:
package router
import (
"net/http"
"github.com/gin-gonic/gin"
)
func CORSMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*") //允许所有IP访问
ctx.Writer.Header().Set("Access-Control-Max-Age", "86400")
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "*")
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE")
if ctx.Request.Method == http.MethodOptions {
ctx.AbortWithStatus(200)
} else {
ctx.Next()
}
}
}
实现404页面
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/user", func(c *gin.Context) {
//指定默认值
//http://localhost:8080/user 才会打印出来默认的值
name := c.DefaultQuery("name", "枯藤")
c.String(http.StatusOK, fmt.Sprintf("hello %s", name))2020-08-05 09:22:11 星期三
})
r.NoRoute(func(c *gin.Context) {
c.String(http.StatusNotFound, "404 not found2222")
})
r.Run()
}
JSON序列化和反序列化
package main
import (
"encoding/json"
"fmt"
"math"
)
type User struct {
UserID int64 `json:"id"`
UserName string `json:"name"`
}
// 第一层次
// 第二层次
// 第五层次
func main() {
//json序列化
user := User{
UserID: math.MaxInt64,
UserName: "hjz",
}
b, err := json.Marshal(user)
if err != nil {
print(err)
}
fmt.Println(string(b))
//使用Json的反序列化
s := `{"id":9223372036854775807,"name":"hjz"}`
var data User
err = json.Unmarshal([]byte(s), &data)
if err != nil {
fmt.Println(err)
}
fmt.Printf("userId:%d,userName:%s", user.UserID, user.UserName)
}
翻译Gin框架的日志
-controller
+ validator.go
- models
+ params.go
package controller
//翻译Gin框架的日志
import (
"fmt"
"reflect"
"strings"
"fileWeb/models"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
enTranslations "github.com/go-playground/validator/v10/translations/en"
zhTranslations "github.com/go-playground/validator/v10/translations/zh"
)
// 定义一个全局翻译器T
var trans ut.Translator
// InitTrans 初始化翻译器
func InitTrans(locale string) (err error) {
// 修改gin框架中的Validator引擎属性,实现自定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 注册一个获取json tag的自定义方法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
// 为SignUpParam注册自定义校验方法
v.RegisterStructValidation(SignUpParamStructLevelValidation, models.ParamSignUp{})
zhT := zh.New() // 中文翻译器
enT := en.New() // 英文翻译器
// 第一个参数是备用(fallback)的语言环境
// 后面的参数是应该支持的语言环境(支持多个)
// uni := ut.New(zhT, zhT) 也是可以的
uni := ut.New(enT, zhT, enT)
// locale 通常取决于 http 请求头的 'Accept-Language'
var ok bool
// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
}
// 注册翻译器
switch locale {
case "en":
err = enTranslations.RegisterDefaultTranslations(v, trans)
case "zh":
err = zhTranslations.RegisterDefaultTranslations(v, trans)
default:
err = enTranslations.RegisterDefaultTranslations(v, trans)
}
return
}
return
}
// RemoveTopStruct 去除提示信息中的结构体名称
func RemoveTopStruct(fields map[string]string) map[string]string {
res := map[string]string{}
for field, err := range fields {
res[field[strings.Index(field, ".")+1:]] = err
}
return res
}
// SignUpParamStructLevelValidation 自定义SignUpParam结构体校验函数
func SignUpParamStructLevelValidation(sl validator.StructLevel) {
su := sl.Current().Interface().(models.ParamSignUp)
if su.Password != su.RePassword {
// 输出错误提示信息,最后一个参数就是传递的param
sl.ReportError(su.RePassword, "re_password", "RePassword", "eqfield", "password")
}
}
// ParamSignUp 注册请求参数
type ParamSignUp struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
RePassword string `json:"re_password" binding:"required"`
Phone string `json:"phone" binding:"required"`
Name string `json:"name" binding:"required"`
}
雪花算法
-pkg
- snowflake
+ snowflake.go
package snowflake
//雪花算法
import (
"time"
"github.com/bwmarrin/snowflake"
)
var node *snowflake.Node
func Init(startTime string, machineID int64) (err error) {
var st time.Time
//指定时间因子-startTime
st, err = time.Parse("2006-01-02", startTime)
if err != nil {
return
}
snowflake.Epoch = st.UnixNano() / 1000000
node, err = snowflake.NewNode(machineID)
return
}
func GenID() int64 {
return node.Generate().Int64()
}
jwtToken
- pkg
-jwt
+ jwt.go
package jwt
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v4"
)
// token的过期时间
const TokenExpireDuration = time.Hour * 2
// token的sercet用于签名的字符串
var CustomSecret []byte = []byte("疫情小程序签名")
type CustomClaims struct {
jwt.RegisteredClaims // 内嵌标准的声明
UserID int64 `json:"user_id"`
Username string `json:"username"`
}
// GenToken 生成JWT
func GenToken(userID int64, username string) (string, error) {
// 创建一个我们自己的声明
claims := CustomClaims{
UserID: userID,
Username: username, // 自定义字段
}
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(TokenExpireDuration))
claims.Issuer = "my-project"
// 使用指定的签名方法创建签名对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 使用指定的secret签名并获得完整的编码后的字符串token
return token.SignedString(CustomSecret)
}
// ParseToken 解析JWT
func ParseToken(tokenString string) (*CustomClaims, error) {
// 解析token
// 如果是自定义Claim结构体则需要使用 ParseWithClaims 方法
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (i interface{}, err error) {
// 直接使用标准的Claim则可以直接使用Parse方法
//token, err := jwt.Parse(tokenString, func(token *jwt.Token) (i interface{}, err error) {
return CustomSecret, nil
})
if err != nil {
return nil, err
}
// 对token对象中的Claim进行类型断言
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { // 校验token
return claims, nil
}
return nil, errors.New("invalid token")
}
Code 返回的错误
- controller
+ Code.go
package controller
// ResCode 定义返回值类型
type ResCode int64
const (
CodeSuccess ResCode = 1000 + iota
CodeInvalidParam
CodeUserExist
CodeUserNotExist
CodeInvalidPassword
CodeServerBusy
CodeNeedLogin
CodeInvalidToken
)
var codeMsgMap = map[ResCode]string{
CodeSuccess: "success",
CodeInvalidParam: "请求参数错误",
CodeUserExist: "用户名已存在",
CodeUserNotExist: "用户名不存在",
CodeInvalidPassword: "用户名或密码错误",
CodeServerBusy: "服务器繁忙",
CodeNeedLogin: "需要登录",
CodeInvalidToken: "无效的token",
}
// GetMsg 得到对应的错误
func (r ResCode) GetMsg() string {
msg, ok := codeMsgMap[r]
if !ok {
msg = codeMsgMap[CodeServerBusy]
}
return msg
}
Response 返回响应方法
- controller
+ resopnse.go
package controller
import (
"net/http"
"github.com/gin-gonic/gin"
)
/*
{
"code":1001,//程序中的错误码
"msg":xx,提示信息
"data":{},//数据
}
*/
type Response struct {
Code ResCode `json:"code"`
Msg interface{} `json:"msg"`
Data interface{} `json:"data,omitempty"`
}
// ResponseError 返回错误类型
func ResponseError(c *gin.Context, code ResCode) {
resp := &Response{
Code: code,
Msg: code.GetMsg(),
Data: nil,
}
c.JSON(http.StatusOK, resp)
}
// ResponseSuccess 返回请求成功
func ResponseSuccess(c *gin.Context, data interface{}) {
resp := &Response{
Code: CodeSuccess,
Msg: CodeSuccess.GetMsg(),
Data: data,
}
c.JSON(http.StatusOK, resp)
}
// 自定义的返回错误
func ResponseErrorWithMsg(c *gin.Context, code ResCode, msg interface{}) {
resp := &Response{
Code: code,
Msg: msg,
Data: nil,
}
c.JSON(http.StatusOK, resp)
}
// ResponseSuccessLayUi 返回Layui 数据
func ResponseSuccessLayUi(c *gin.Context, code int, msg string, count int, data interface{}) {
c.JSON(http.StatusOK, gin.H{
"code": code,
"count": count,
"data": data,
"msg": msg,
})
}
Request 解析请求操作
- controller
+ request.go
package controller
import (
"errors"
"strconv"
"github.com/gin-gonic/gin"
)
const CtxUserIDkey = "userID"
var ErrorUserNotLogin = errors.New("用户未登录")
// GetCyrrentUserID 获取当前用户的ID
func GetCyrrentUserID(c *gin.Context) (userID int64, err error) {
uid, ok := c.Get(CtxUserIDkey)
if !ok {
err = ErrorUserNotLogin
return
}
userID, ok = uid.(int64)
if !ok {
err = ErrorUserNotLogin
return
}
return
}
// GetPageInfo 处理分页请求的参数
func GetPageInfo(c *gin.Context) (page, page_size int64, err error) {
//获得分页参数offer和limit
pageStr := c.Query("page")
page_sizeStr := c.Query("size")
page, err = strconv.ParseInt(pageStr, 10, 32)
if err != nil {
page = 1
}
page_size, err = strconv.ParseInt(page_sizeStr, 10, 32)
if err != nil {
page_size = 10
}
return
}
基本请求
1.参数处理
2.业务逻辑
3.返回数据
import (
"errors"
"strconv"
"webGin/dao/mysql"
"webGin/logic"
"webGin/models"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10" //注意这条正确
"go.uber.org/zap"
)
// SingUpHandlerInstructor 辅导员注册
func SingUpHandlerInstructor(c *gin.Context) {
p := new(models.ParamSignUp)
if err := c.ShouldBindJSON(p); err != nil {
zap.L().Error("SingUpHandlerInstructor with invalid param ", zap.Error(err))
//判断是否是校验错误
errs, ok := err.(validator.ValidationErrors)
if !ok {
ResponseError(c, CodeInvalidParam)
return
} else {
ResponseErrorWithMsg(c, CodeInvalidParam, RemoveTopStruct(errs.Translate(trans)))
return
}
}
//业务处理
if err := logic.SingUp(p); err != nil {
zap.L().Error("logic.SingUp() failed", zap.Error(err))
//依据错误的类型进行返回
if errors.Is(err, mysql.ErrorUserExist) {
ResponseError(c, CodeUserExist)
return
} else {
ResponseError(c, CodeServerBusy)
return
}
}
ResponseSuccess(c, CodeSuccess)
}