一篇文章搞定Gin框架


前言

安装:go get -u github.com/gin-gonic/gin
导入:import "github.com/gin-gonic/gin"
辅助包:import "net/http" (使用状态码如http.StatusOK时会用到)
注意:根据 官网 提示,需要Go语言编译器为1.13及以上版本才可以使用Gin框架

1. HTTP请求和参数解析

1.1 Engine的创建

使用gin框架的第一步就是创建engineengine为一个结构体,里面包含了路由组件、中间件、页面渲染接口、框架配置设置等相关内容。创建engine有两种方式:

  • engine := gin.Default()
  • engine := gin.New()

这两种方式的区别在于:gin.Default()会使用gin.New()来创建engine实例,但gin.Default()会默认使用Logger(负责打印日志) 和Recovery(恢复程序运行中出现的panic) 中间件,而gin.New()不使用任何中间件。

1.2 Handle处理HTTP请求

在engine中可以使用Handle方法直接对网络请求进行处理,Handle的函数签名为:
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes

参数含义
httpMethodhttp请求方法
relativePath要解析的接口
handlers处理对应请求的函数

简单的例子(代码1.1):

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
)

func main() {
	engine := gin.Default()
	// 请求形式:http://localhost:8080/hello?name=Alice
	engine.Handle("GET", "/hello", func(ctx *gin.Context) {
		path := ctx.FullPath()  // 获取请求的接口
		name := ctx.DefaultQuery("name", "游客")  // 获取name参数
		ctx.Writer.Write([]byte(fmt.Sprintf("亲爱的%v,您正在访问%v", name, path)))
	})
	engine.Run()
}

1.3 分类处理请求

可以根据不同的请求类型,使用指定的函数来进行请求处理。可以使用engine调用engine.GET()函数来处理GET请求,使用engine.POST()函数来处理POST请求,以此类推,Gin框架封装了处理各种请求的方法。改写后的代码1.1为:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
)

func main() {
	engine := gin.Default()
	// 请求形式:http://localhost:8080/hello?name=Alice
	engine.GET("/hello", func(ctx *gin.Context) {
		path := ctx.FullPath()  // 获取请求的接口
		name := ctx.DefaultQuery("name", "游客")  // 获取name参数
		ctx.Writer.Write([]byte(fmt.Sprintf("亲爱的%v,您正在访问%v", name, path)))
	})
	engine.Run()
}

1.4 重定向

重定向分为跳转路由重定向。跳转就是直接对目标地址页面进行渲染并返回,并不会使用路由函数对目标地址进行处理。路由重定向是经过目标地址对应的路由函数处理后将目标页面进行渲染并返回。

// 跳转使用Redirect函数
func userRegister(ctx *gin.Context) {
	// 用户注册功能
	ctx.Writer.WriteString("注册成功")
    // 跳转,第一个参数为状态码,第二个为目标地址
	ctx.Redirect(http.StatusMovedPermanently, "/api/user/login")
}

// 路由重定向
var engine = gin.New()
func userRegister(ctx *gin.Context) {
	// 用户注册功能
	ctx.Writer.WriteString("注册成功")
	ctx.Request.URL.Path = "/api/user/login"
	engine.HandleContext(ctx) // 路由重定向
}

2. 请求参数绑定与多数据格式处理

2.1 参数绑定

当请求中包含表单数据时,可以使用context.PostFormcontext.GetPostForm来获取。但如果表单数据较多时,PostFormGetPostForm一次只能获取一个表单数据,开发效率较慢,于是我们需要使用Gin框架提供的表单实体绑定功能,将表单数据与结构体绑定。
以用户注册为例,用户在注册时会提供用户名、密码、手机三种信息,通过POST请求,这三种信息会以表单的形式发送到服务器上,我们可以使用一个结构体来接收表单数据:

type UserRegister struct {
    UserName string   `form:"username" binding:"required"`
    Password string   `form:"password" binding:"required"`
    Phoen    string   `form:"phone" binding:"required"`
}

上述结构体中,我们通过结构体标签将结构体的字段名与表单中的字段名相对应,比如UserName对应表单中的username。通过binding设置属性是否为必须(绑定)。
举个例子(代码2.1):

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"log"
)

// 结构体的个字段名首字母必须大写(大写表示可导出)
type Student struct {
	Name string  `form:"name"`
	Age     int  `form:"age"`
}

func main() {
	engine := gin.Default()
	// http://localhost:8080/hello?name=Alice&&age=18
	engine.Handle("GET", "/hello", func(ctx *gin.Context) {
		var student Student
        // 将GET参数与结构体student绑定
		err := ctx.ShouldBindQuery(&student) 
		if err != nil {
			log.Fatal(err.Error())
		}
		ctx.Writer.Write([]byte(fmt.Sprintf("%v同学,您的年龄是%v", student.Name, student.Age)))
	})
	engine.Run()
}

Gin里面提供了各种绑定类型,除了上面用到的ShouldBindQuery函数,还有BindJSONShouldBindJSON等,不同的方法间有些许差异,比如BindJSON在解析错误时会在请求头中返回一个400的状态码、ShouldBindJSON则不会主动添加状态码,需要自己定义。

2.2 多数据格式返回

  • 返回字节类型的切片:context.Writer.Write([]byte(data))
  • 返回字符串类型:context.Writer.WriteString(data)
  • 返回json格式:
// 将map变为JSON格式返回
// JSON函数为:JSON(code int, obj interface{})
// 第一个参数为响应状态码
context.JSON(200, map[string]interface{}{
    "code": 200,
    "message": "Success",
    "data": "xxx",
})

// 将struct变为JSON格式返回
type Response struct {
	Code       int
    Message string
    Data    string
}
resp := Response{Code: 1, Message: "Success", Data: "111"}
context.JSON(200, &resp)

由于Gin框架自定义了一个名为H的类型type H map[string]interface{},我们可以使用H来返回JSON值:
context.JSON(200, gin.H{"code": 1, "message": "success", data: "111"})

  • 返回XML类型:context.XML(http._StatusOK_, gin.H{"code":200, "msg":"ok"})
  • 返回HTML格式:context.HTML(code int, name string, obj interface{})(name为html文件路径,obj直接赋值为nil)

如果想要在返回的HTML文件中加入一些静态资源(如图片),需要使用engine.Static(relativePath string, root string)(relativePath为HTTP请求的URL路径,root为图片所在目录路径)来加载静态资源。

注意:返回HTML文件时,需要先使用engine.LoadHTMLGlob(path string)来载入HTML文件所在的目录。如果需要载入template文件夹下所有HTML文件,则engine.LoadHTMLGlob("./template/*")
关于HTMLobj参数:如果要返回的HTML文件中使用了模板语法,那么可以将该参数设置为gin.H{"模板中变量名":值,}来为HTML模板变量赋值。

补充:关于HTML模板

上述已经提到,我们会使用engine.LoadHTMLGlob()来加载某一目录下的所有模板,如果该目录下存在多级目录,并且每个目录对应不同的模板组,那我们该如何处理呢?
如果要加载的模板路径为./template/user/vip/,那么可以这样配置路径:engine.LoadHTMLGlob("./template/**/**/*"),这里面的每个**都表示一层目录。为了防止出现多层目录下有相同文件名的模板文件(如/template/admin/index.html/template/default/index.html),应给每个模板文件取个名字,做法如下:

{{ define "default/index.html" }}
// 模板内容
{{ end }}

这个时候,如果我们想渲染/template/default/下面的index.html文件,而不是/template/admin/下面的,则可以这样写:

// 假设有 /template/default/index.html   /template/admin/index.html  两个模板
engin.LoadHTMLGlob("./template/**/*")
engine.GET("/news", func(ctx *gin.Context) {
    // 渲染 default下面的index.html模板
    ctx.HTML(200, "default/index.html", gin.H{})
})

在模板中定义变量:{{ $var := .data }}
在模板中使用比较数值大小:

eq等于
ne不等于
lt小于
le小于等于
gt大于
ge大于等于

条件判断:

{{ if gt .score 90 }} 
	<p>优秀</p>
{{ else if gt .score 60 }}
	<p>及格</p>
{{ else }}
	<p>不及格</p>
{{ end }}

遍历:

{{ range $key, $val := .data }}
	<li>{{ $val }} </li>
{{ else }}
	<li>切片中没有数据</li>
{{ end }}

模板函数:

  • 预定义模板函数

len:返回它的参数的整数类型长度

andand x y表示如果x为真则执行y,否则只执行x
oror x y表示如果x为为真则执行x,否则只执行y
not:返回单个参数布尔值的否定
index:返回第一个参数以后面参数为索引指向的值

  • 自定义模板函数
// 定义一个时间戳转时间的函数
func UnixToTime(timeStamp int) string {
	t := time.Unix(int64(timeStamp), 0)
	return t.Format("2006-01-02 15:04:05") // 此时间模板有讲究,参考format.go
}

func main() {
    engine := gin.Default()
    // 以下 template.FuncMap 是在 html 模块下的
    engine.SetFuncMap(template.FuncMap{"UnixToTime": UnixToTime}) // 注册模板函数(在加载模板之前)
    engine.LoadHTMLGlob("./template/*") // 加载模板(在绑定路由之前)
    engine.GET("/time", func(ctx *gin.Context) {
        ctx.HTML(http.StatusOK, "index.html", gin.H{"timeStamp": time.Now().Unix()})
    })
    
}

以上程序有两点注意:

  1. 使用Format函数将时间戳格式化为标准时间时,模板的编写是有相关标准的,具体请参考format.go文件,以下列举出常用格式:

![image.png](https://img-blog.csdnimg.cn/img_convert/bc8b4ea43f4ae3cff1e4b975a0e4c6fa.png#clientId=u3a8c3c8e-8ff7-4&from=paste&height=376&id=u5f969f2f&margin=[object Object]&name=image.png&originHeight=751&originWidth=705&originalType=binary&ratio=1&size=98251&status=done&style=none&taskId=uefb4cc9b-cd7d-4a29-99f2-3dc914fbcf2&width=352.5)

  1. 使用template.FuncMap时,templatehtml模块下的。

3. 路由组

在实际项目中,都会采取模块化开发。同一模块内的功能接口往往会有相同的接口前缀,如:

// 用户模块
注册:/api/user/register
登录:/api/user/login
用户信息:/api/user/info

以上模块可以这么写(代码3.1):

func main() {
	engine := gin.Default()
	// 用户模块
	// 注册:/api/user/register
	// 登录:/api/user/login
	// 用户信息:/api/user/info
	routerGroup := engine.Group("/api/user")
	routerGroup.POST("/register", userRegister)
	routerGroup.POST("/login", userLogin)
	routerGroup.GET("/info", userInfo)
	engine.Run("localhost:8080")
}
func userRegister(ctx *gin.Context) {
	// 用户注册功能
}
func userLogin(ctx *gin.Context) {
	// 用户登录功能
}
func userInfo(ctx *gin.Context) {
	// 查询用户信息功能
}

如果模块较多,也可以使用语句块包裹来突显不同:

func main() {
	router := gin.Default()
	//v1组路由
	v1:=router.Group("/v1")
	{
		v1.GET("/login", loginEndpoint)
		v1.GET("/submit", submitEndpoint)
		v1.GET("/read", readEndpoint)
	}

	//v2组路由
	v2:=router.Group("/v2")
	{
		v2.GET("/login", loginEndpoint)
		v2.GET("/submit", submitEndpoint)
		v2.GET("/read", readEndpoint)
	}
	router.Run()
}

4. 中间件

4.1 中间件的基本使用

在实际的业务开发过程中,会有很多通用的功能,比如日志记录、权限验证等,这些功能对系统中所有业务都适用,和具体的业务没有关联。为了使整个系统的架构更清晰、耦合度更低,我们可以将这些通用的功能抽离出来单独进行开发,封装成一个个小组件,这些组件被称为“中间件”。

  • Gin框架的Recovery中间件
func Recovery() HandlerFunc {
	return RecoveryWithWriter(DefaultErrorWriter)
}
  • Gin框架的Logger中间件
func Logger() HandlerFunc {
	return LoggerWithConfig(LoggerConfig{})
}
  • Gin使用中间件
func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery()) // 使用中间件
	return engine
}

中间件需要满足两个要求:

  • 是一个函数
  • 返回值为HandlerFunc类型

我们可以自定义一个中间件来实现对每次请求的类型和URL的打印(代码4.1):

// 中间件
func RequestInfo() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		now := time.Now()
		timeInfo := fmt.Sprintf("%v-%v-%v %v-%v-%v", now.Year(), int(now.Month()), now.Day(),
			now.Hour(), now.Minute(), now.Second())
		fmt.Printf("[%v] %v - %v\n", timeInfo, ctx.Request.Method, ctx.FullPath())
	}
}

func main() {
	engine := gin.New()
	engine.Use(RequestInfo()) // 使用中间件
	routerGroup := engine.Group("/api/user")
	routerGroup.POST("/register", userRegister)
	routerGroup.POST("/login", userLogin)
	routerGroup.GET("/info", userInfo)
	engine.Run("localhost:8080")
}

上述中间件的效果如下:
image.png
实际上,根据engine.Use函数的签名func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes可知,只需将HandlerFunc类型作为参数传入即可。代码4.1中间件的使用为engine.User(RequestInfo()),而不是engine.User(RequestInfo),是因为RequestInfo函数的返回值才是HandlerFunc类型。又因为Gin框架中HandlerFunc类型定义为type HandlerFunc func(*Context),可知HandlerFunc类型实际上就是参数为*Context的函数。所以,上述中间件还可以这样写(代码4.2):

func RequestInfo(ctx *gin.Context) {
	now := time.Now()
	timeInfo := fmt.Sprintf("%v-%v-%v %v-%v-%v", now.Year(), int(now.Month()), now.Day(),
                now.Hour(), now.Minute(), now.Second())
	fmt.Printf("[%v] %v - %v\n", timeInfo, ctx.Request.Method, ctx.FullPath())
} 

func main() {
    engine := gin.New()
    engin.Use(RequestInfo) // 使用中间件
    routerGroup.POST("/register", userRegister)
}

除了对整体使用中间件,还可以对特定请求使用中间件(代码4.3):

engine.GET("/login", 中间件1, 中间件2, ..., 处理函数)

以GET函数为例,其签名为func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes, 它的第二参数为不定长参数,并且参数类型为HandlerFunc,而中间件函数类型也为HandlerFunc,也就是说,对特定路由的处理函数其实也是一个中间件,只不过它作用于特定路由,不对整体生效。代码4.3中对路由的处理会按照从左到右
的顺序,先用中间件1处理,之后是中间件2,最后才是处理函数。
对路由组使用中间件(代码4.4):

routerGroup := engine.Group("/api/user", 中间件)
{
    routerGroup.GET("/login", Login)
    routerGroup.POST("/register", register)
}

接下来,我们来看看**Context.Next()**的使用。中间件总是作用于请求的处理函数之前,如果我们需要使用中间件将请求处理的结果打印出来,那么我们需要在中间件中就执行处理函数,该操作可以通过context.Next()函数来实现。中间件中,在context.Next()之前的部分会先执行,遇到context.Next()后程序会跳转执行处理函数,处理函数执行完成后会跳转回来去执行context.Next()函数之后的操作。如果想使用中间件打印出请求的执行状态码(代码4.5):

func RequestInfo(ctx *gin.Context) {
	now := time.Now()
	timeInfo := fmt.Sprintf("%v-%v-%v %v-%v-%v", now.Year(), int(now.Month()), now.Day(),
                now.Hour(), now.Minute(), now.Second())
	fmt.Printf("[%v] %v - %v", timeInfo, ctx.Request.Method, ctx.FullPath())
    ctx.Next() // 跳转到处理函数
    fmt.Printf("StatusCode:%v", ctx.Writer.Status()) // 获取响应状态码
} 

**Context.Abort()**的方法用于终止其它所有中间件的执行。如果在某一中间件里使用Abort函数,那么该中间执行完成后,它后面的所有中间件都不会执行。

4.2 中间件和控制器共享数据

在中间件中可以使用context.Set()函数来设置数据,然后在其它中间件中使用context.Get()获取数据,具体用法为(代码4.6):

func middleWareOne(ctx *gin.Context) {
    //Set函数的签名: func (c *Context) Set(key string, value interface{}) 
    ctx.Set("userName", "张三")
}
func HandleFunction(ctx *gin.Context) {
    // Get函数的签名:func (c *Context) Get(key string) (value interface{}, exists bool)
    userName, _ := ctx.Get("userName")
    if val, ok := userName.(string); ok {
        ctx.string(200, "用户列表: " + val)
    } else {
        ctx.string(200, "获取用户失败")
    }
}

如果在中间件中使用协程,则不能直接使用原本的context需要先使用Copy函数进行复制,然后使用副本,如(代码4.7):

func MiddleTwo(ctx *gin.Context) {
	fmt.Println("第二个中间件开始")
	ctxCopy := ctx.Copy() // 复制一个副本
	go func() {
		userName, _ := ctxCopy.Get("userName") // 使用副本
		if val, ok := userName.(string); ok {
			fmt.Println("用户列表:" + val)
		} else {
			fmt.Println("获取用户列表失败")
		}
	}()
	ctx.Next()
	fmt.Println("第二个中间件结束")
}

5. 文件上传

1. 文件上传
需要在上传文件的form表单上面加入enctype="multipart/form-data"

<form method="post" action="/api/user/upload" enctype="multipart/form-data">
	<input type="file" name="face" />
	<input type="submit" value="提交" />
</form>

上传文件:

func Upload(ctx *gin.Context) {
    file, err := ctx.FormFile("face") // 获取文件
    if err != nil {
        ctx.String("文件获取失败")
        return
    }
    dst := "./static/upload/" + file.Filename // 文件保存路径
    ctx.SaveUploadedFile(file, dst) // 将文件file保存到dst路径下
    ctx.String("文件上传成功")
}

2. 相同名字的多文件上传

engine.POST("/upload", func(ctx *gin.Context) {
    
    form, _ := ctx.MultipartForm()
    files := form.File["face[]"] // face为表单中文件上传框的name属性值
    
    for _, file := range files {
        c.SaveUploadedFile(file, dst)
    }
})

6. Cookie和Session

6.1 设置Cookie

设置Cookie使用SetCookie函数:
func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
name:cookie名, value:cookie值,maxAge:cookie的生命周期(单位是秒),path:cookie路径,domain:cookie路径的作用域,secure:为true表示cookie在http中无效,在https中生效,httpOnly:若为true则通过脚本将无法获取cookie信息,防止xss攻击产生。
如:ctx.SetCookie("userName", "张三", 1200, "/", "localhost", false, true)

6.2 获取Cookie

获取Cookie使用context.Cookie(name string)函数,name为Cookie名,如:context.Cookie("userName")

6.3 Session

Gin官方没有给我们提供Session相关文档,这个时候我们可以使用第三方的Session中间件来实现,第三方库地址为:
https://github.com/gin-contrib/sessions
gin-contrib/sessions中间件支持的存储引擎:

  • cookie
  • memstore
  • redis
  • memcached
  • mongodb

导入和基本用法:

import "github.com/gin-contrib/sessions"
import "github.com/gin-contrib/sessions/cookie"

func main() {
    engine := gin.Default()
    // 创建基于cookie的存储引擎,secret是用于加密的秘钥
    store := cookie.NewStore([]byte("secret"))
    // 配置session中间件,store是存储引擎,也可以换成别的
    engine.Use(sessions.Sessions("mysession", store))
    engine.GET("/user", func(ctx *gin.Context) {
        session := sessions.Default(ctx)
        session.Get("userName") // 获取cookie
        session.Set("userName", "111") // 设置cookie
        session.Save() // 设置完值后要保存
    })
}

注意:上述程序创建的Cookie名为mysession

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值