Gin是一个轻量级的Go语言Web框架,它具有高性能和简洁的设计。由于其快速的路由匹配和处理性能,Gin成为Go语言中最受欢迎的Web框架之一。
GitHub:github.com/gin-gonic/gin
部分函数、结构体的详细说明请通过IDE查看。
1 环境搭建与程序热加载
下载并安装Gin:
go get -u github.com/gin-gonic/gin
将gin
引入,若使用诸如http.StatusOK
等常量,则还需引入net/http
包:
import (
"github.com/gin-gonic/gin"
"net/http"
)
新建main.go
配置路由,详情如下所示(若想改变默认启动端口,只需传入参数r.Run(":9000")
):
package main
import "github.com/gin-gonic/gin"
func main() {
// 创建一个默认路由引擎
r := gin.Default()
// 配置路由
r.GET("/", func(c *gin.Context) {
// c.JSON:返回JSON格式的数据
c.JSON(200, gin.H{
"message": "Hello, Hyplus!",
})
})
// 启动HTTP服务,默认为0.0.0.0:8080
r.Run()
}
热加载是指当对代码进行修改时,程序能够自动重新加载并执行,使得开发更加便利,可以快速进行代码测试,省去了每次手动重新编译。可以借助第三方包进行实现:
- fresh:github.com/gravityblast/fresh(推荐)
go get github.com/pilu/fresh
# 开启fresh
fresh
go run github.com/pilu/fresh # 若fresh命令不存在,则运行该条
2 路由与模板基础
路由(Routing)由一个URL(路径)和一个特定的HTTP方法(GET
、POST
等)组成,涉及到应用如何响应客户端对某个网站节点的访问。
2.1 RESTful API
RESTful API是目前较为成熟的一套互联网应用程序API设计理论,设计路由时建议参考RESTful API指南。在RESTful架构中,每个网址代表一种资源,不同的请求方式表示执行不同的操作:
GET
(SELECT):从服务器取出资源(一项或多项),获取Get
传值可使用c.Query()
方法。POST
(CREATE):在服务器新建一个资源PUT
(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)DELETE
(DELETE):从服务器删除资源
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
r.GET("/news", func(c *gin.Context) {
aid := c.Query("aid")
c.String(http.StatusOK, "aid=%s", aid)
})
r.POST("/add", func(c *gin.Context) {
c.String(http.StatusOK, "POST请求-主要用于增加数据")
})
r.PUT("/edit", func(c *gin.Context) {
c.String(http.StatusOK, "PUT请求-主要用于编辑数据")
})
r.DELETE("/delete", func(c *gin.Context) {
c.String(http.StatusOK, "DELETE请求-主要用于删除数据")
})
r.Run()
}
动态路由(域名/user/20
):
r.GET("/user/:uid", func(c *gin.Context) {
uid := c.Param("uid")
c.String(http.StatusOK, "userID=%s", uid)
})
2.2 响应数据函数
通过c.String()
、c.JSON()
、c.JSONP()
、c.XML()
、c.HTML()
函数返回数据:
c.String()
返回一个字符串,需传入状态码、格式字符串和参数(类似于Printf()
):
r.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello, %v!", "首页")
})
c.JSON()
返回一个JSON数据,需传入状态码、gin.H
(实为map[string]interface{}
)或自定义结构体:
type Article struct {
Title string `json:"title"`
Desc string `json:"desc"`
Content string `json:"content"`
}
func main() {
r := gin.Default()
// 使用gin.H
r.GET("/json1", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"msg": "Hyplus GO",
})
})
// 使用自定义结构体
r.GET("/json2", func(c *gin.Context) {
a := &Article{
Title: "Gin框架基础教程",
Desc: "Hyplus GO",
Content: "test",
}
c.JSON(http.StatusOK, a)
})
r.Run()
}
c.JSONP()
用于响应JSONP数据格式(主要用于跨域请求):
type Article struct {
Title string `json:"title"`
Desc string `json:"desc"`
Content string `json:"content"`
}
func main() {
r := gin.Default()
// 响应JSONP请求,可传入回调函数(callback)
// 【例】请求:http://localhost:8080/jsonp?callback=xxxx
// 返回数据:xxxx({"title":"Gin框架基础教程","desc":"Hyplus GO","content":"Gin框架基础教程"});
r.GET("/jsonp", func(c *gin.Context) {
a := &Article{
Title: "Gin框架基础教程",
Desc: "Hyplus GO",
Content: "Gin框架基础教程",
}
c.JSONP(http.StatusOK, a)
})
r.Run()
}
c.XML()
返回XML数据,类似于c.JSON()
:
type Article struct {
Title string `json:"title"`
Desc string `json:"desc"`
Content string `json:"content"`
}
func main() {
r := gin.Default()
// 使用gin.H
r.GET("/xml1", func(c *gin.Context) {
c.XML(http.StatusOK, gin.H{
"success": true,
"msg": "Hello XML",
})
})
// 使用自定义结构体
r.GET("/xml2", func(c *gin.Context) {
a := &Article{
Title: "Gin框架基础教程",
Desc: "Hello XML",
Content: "test",
}
c.XML(http.StatusOK, a)
})
r.Run()
}
2.3 渲染模板
c.HTML()
用于渲染模板,需传入状态码、模板地址、gin.H
。
2.3.1 加载模板
使用LoadHTMLGlob()
方法加载模板(或LoadHTMLFiles()
直接指定文件),传入参数形如templates/*
,表示加载该目录下所有模板:
type Article struct {
Title string `json:"title"`
Desc string `json:"desc"`
Content string `json:"content"`
}
func main() {
r := gin.Default()
// 配置模板文件
r.LoadHTMLGlob("templates/*")
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "首页",
})
})
r.GET("/news", func(c *gin.Context) {
news := &Article{
Title: "Gin框架基础教程",
Desc: "Hello Templates",
Content: "test",
}
c.HTML(http.StatusOK, "news.html", gin.H{
"title": "I'm data from server",
"news": news,
})
})
r.GET("/goods", func(c *gin.Context) {
c.HTML(http.StatusOK, "goods.html", gin.H{
"title": "This is Goods Page, data from server!",
"price": 20,
})
})
r.Run()
}
在前台可通过形如{{.xxx}}
的插值表达式来访问后台数据,例如/news
对应的页面如下所示(对结构体成员的访问同样使用.
):
<!-- templates/news.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Document</title>
</head>
<body>
<h2>{{.title}}</h2>
<p>
{{.news.Title}}
</p>
<p>
{{.news.Desc}}
</p>
<p>
{{.news.Content}}
</p>
</body>
</html>
若模板位于多级目录,则LoadHTMLGlob()
方法所传参数应形如templates/**/*
(若目录层数更深则为templates/**/**/*
)。
// 配置模板文件
r.LoadHTMLGlob("templates/**/*")
// 后台首页
r.GET("/admin", func(c *gin.Context) {
c.HTML(http.StatusOK, "admin/index.html", gin.H{
"title": "后台首页",
})
})
同时,在定义模板时需通过define
定义名称,define
、end
成对出现,如下为templates/admin/index.html
的定义:
{{ define "admin/index.html" }}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Document</title>
</head>
<body>
<h2>{{.title}}</h2>
</body>
</html>
{{ end }}
2.3.2 模板基本语法
模板语法都包含在{{
和}}
之间,其中{{.}}
中的点表示当前对象。
当传入一个结构体对象时,可用.
访问结构体的对应字段;模板中应使用define
、end
定义。(见前述案例)
注释使用/*
、*/
,执行时会忽略;可以多行;不能嵌套,并且必须紧贴分界符始止:
{{/* comment */}}
可以在模板中声明变量,用于保存传入模板的数据或其他语句生成的结果。变量名前需加$
,与Go语言一样使用:=
初始化值,如下例所示:
{{$obj := .title}} <!-- 定义变量 -->
<h4>{{$obj}}</h4>
有时在使用模板语法时会不可避免地引入空格或换行符,导致模板最终渲染结果不符预期,此时可用{{-
/-}}
分别去除模板内容左/右侧的所有空白符号(-
紧挨花括号,并且与模板值之间使用空格分隔):
{{- .Name -}}
布尔函数(比较函数)会将任何类型的零值视为假,其余视为真:eq
、ne
、lt
、le
、gt
、ge
(例如gt arg1 arg2
表示arg1 > arg2
)。常结合条件判断(if
...else if
...else
...end
)使用,如下例所示:
{{if pipeline}} T1 {{end}}
{{if pipeline}} T1 {{else}} T0 {{end}}
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
{{if gt .score 60}}
及格
{{else}}
不及格
{{end}}
{{if gt .score 90}}
优秀
{{else if gt .score 60}}
及格
{{else}}
不及格
{{end}}
使用range
关键字进行遍历,有以下两种写法,其中pipeline
的值必须是数组、切片、字典或通道:
{{range $key, $value := .obj}}
{{$value}}
{{else}}
{{???}}
{{end}}
如下例所示:
r.GET("/admin", func(c *gin.Context) {
c.HTML(http.StatusOK, "admin/index.html", gin.H{
"title": "后台首页",
"langs": []string{"Go", "Java", "C", "Python"},
"newsList": []interface{}{
&Article{Title: "Go语言基础教程", Desc: "Hello Go!", Content: "content1"},
&Article{Title: "Java语言基础教程", Desc: "Hello Java!", Content: "content2"},
&Article{Title: "C语言基础教程", Desc: "Hello C!", Content: "content2"},
},
"emptySlice": []int{},
})
})
{{ define "admin/index.html" }}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Document</title>
</head>
<body>
<h2>{{.title}}</h2>
<!--循环遍历数据-->
<ul>
{{range $key, $value := .langs}}
<li>{{$key}} - {{$value}}</li>
{{else}}
Nope.
{{end}}
</ul>
<br>
<ul>
{{range $key, $value := .newsList}}
<li>{{$key}} - {{$value.Title}} - {{$value.Content}}</li>
{{else}}
今日无新闻
{{end}}
</ul>
<br>
<ul>
{{range $key, $value := .emptySlice}}
<li>{{$key}} - {{$value}}</li>
{{else}}
<li>数组中无数据!</li>
{{end}}
</ul>
</body>
</html>
{{ end }}
with
用于解构结构体来输出数据:
user := UserInfo{
Name: "Teresa",
Gender: "F",
Age: 17,
}
router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "default/index.html", gin.H{
"user": user,
})
})
{{with .user}}
<h4>姓名:{{.Name}}</h4>
<h4>性别:{{.Gender}}</h4>
<h4>年龄:{{.Age}}</h4>
{{end}}
<!-- 相当于
<h4>{{.user.Name}}</h4>
<h4>{{.user.Gender}}</h4>
<h4>{{.user.Age}}</h4>
-->
2.3.3 预定义函数与自定义函数
执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。
预定义的全局函数如下:
and
:返回其第一个empty参数或最后一个参数,and x y
等价于if x then y else x
;所有参数均可执行。or
:返回第一个非empty
参数或最后一个参数,or x y
等价于if x then x else y
;所有参数均可执行。not
:返回其单个参数的布尔值的否定。len
:返回其参数的整数类型长度。index
:执行结果为第一个参数以剩下的参数为索引/键指向的值,例如index x 1 2 3
返回x[1][2][3]
的值;每个被索引的主体必须是数组、切片或者字典。print
:即fmt.Sprint
printf
:即fmt.Sprintf
println
:即fmt.SprintIn
html
:返回与其参数的文本表示形式等效的转义HTML;该函数在html/template
中不可用。urlquery
:以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值;该函数在html/template
中不可用。js
:返回与其参数的文本表示形式等效的转义JavaScript。call
:执行结果为调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数;例如call .X.Y 1 2
等价于Go语言中的dot.X.Y(1, 2)
,其中Y
为函数类型的字段或字典的值,或其他类似情况call
的第一个参数的执行结果必须是函数类型的值(与print
等预定义函数明显不同)- 该函数类型值必须有1到2个返回值,若有2个则后一个必须为
error
接口类型 - 若有2个返回值的方法返回的
error
非nil
,模板执行会中断并返回给调用模板执行者该错误
{{len .title}}
{{index .hobby 2}}
一般通过router.SetFuncMap()
方法自定义模板函数,参数需传入template.FuncMap
,如下所示:
router.SetFuncMap(template.FuncMap{
"formatDate": formatAsDate,
})
【例】注册全局模板函数并加载模板(注意顺序,注册模板函数应在加载模板之前)
package main
import (
"fmt"
"html/template"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func formatAsDate(t time.Time) string {
year, month, day := t.Date()
return fmt.Sprintf("%d/%02d/%02d", year, month, day)
}
func main() {
router := gin.Default()
// 注册全局模板函数
// 注意顺序,注册模板函数应在加载模板之前
router.SetFuncMap(template.FuncMap{
"formatDate": formatAsDate,
})
// 加载模板
router.LoadHTMLGlob("templates/**/*")
router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "default/index.html", map[string]interface{}{
"title": "前台首页",
"now": time.Now(),
})
})
router.Run(":8080")
}
2.3.4 嵌套template
新建templates/deafult/page_header.html
:
{{ define "default/page_header.html" }}
<h1>这是一个头部</h1>
{{end}}
在其他模板中外部引入,语法如下,注意以下两点:
- 引入名称为
page_header.html
中定义的名称 - 引入时注意最后的点(
.
)!
{{template "default/page_header.html" .}}
如下例所示:
{{ define "default/index.html" }}
<!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>
{{template "default/page_header.html" .}}
</body>
</html>
{{end}}
2.4 静态文件服务
当渲染的HTML文件中引用了静态文件时,需要配置静态web服务。方式形如r.Static("/static", "./static")
,其中前面的/static
表示路由,后面的./static
表示路径:
func main() {
r := gin.Default()
r.Static("/static", "./static")
r.LoadHTMLGlob("templates/**/*")
// ...
r.Run(":8080")
}
<!-- 模板文件 ... -->
<link rel="stylesheet" href="/static/css/base.css" />
3 路由详解
本节详细介绍路由传值、返回值。
3.1 Get、Post请求响应
RESTful API基础详见2.1节。
3.1.1 Get请求传值、动态路由传值
可以使用Query()
方法获取指定参数的值,还可使用DefaultQuery()
方法在第2参数设置默认值。以GET /user?uid=20&page=1
为例:
router.GET("/user", func(c *gin.Context) {
uid := c.Query("uid")
page := c.DefaultQuery("page", "0")
c.String(http.StatusOK, "uid=%v page=%v", uid, page)
})
可以动态路由传值,只需使用Param()
方法。以hyperplasma.top/user/20
为例:
r.GET("/user/:uid", func(c *gin.Context) {
uid := c.Param("uid")
c.String(200, "userID=%s", uid)
})
3.1.2 Post请求传值、获取表单数据
定义add_user.html
页面:
{{ define "default/add_user.html" }}
<!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>
<form action="/doAddUser" method="post">
用户名:<input type="text" name="username" />
密码: <input type="password" name="password" />
<input type="submit" value="提交">
</form>
</body>
</html>
{{end}}
通过PostForm()
方法接收表单(Form)数据,还可使用DefaultPostForm()
方法在第2参数设置默认值。对于上述模板中的表单,可如下设置请求响应:
router.GET("/addUser", func(c *gin.Context) {
c.HTML(http.StatusOK, "default/add_user.html", gin.H{})
})
router.POST("/doAddUser", func(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
age := c.DefaultPostForm("age", "20")
c.JSON(http.StatusOK, gin.H{
"username": username,
"password": password,
"age": age,
})
})
3.1.3 请求数据绑定到结构体
为了能够更方便的获取请求相关参数,提高开发效率,可以基于请求的Content-Type
识别请求数据类型并利用反射机制自动提取请求中QueryString
、form表单、JSON、XML等参数到结构体中。
ShouldBind()
方法非常强大能够基于请求自动提取JSON、form表单和QueryString
类型的数据,并将值绑定到指定的结构体对象,用法如下所示——
事先定义结构体类型:
// 注意首字母大写
type Userinfo struct {
Username string `form:"username" json:"user"`
Password string `form:"password" json:"password"`
}
Get传值绑定到结构体——/?username=akira37&password=123456
:
router.GET("/", func(c *gin.Context) {
var userinfo Userinfo
if err := c.ShouldBind(&userinfo); err == nil {
c.JSON(http.StatusOK, userinfo) // 返回数据:{"user":"akira37","password":"123456"}
} else {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error()
})
}
})
Post传值绑定到结构体(表单定义于请求体):
router.POST("/doLogin", func(c *gin.Context) {
var userinfo Userinfo
if err := c.ShouldBind(&userinfo); err == nil {
c.JSON(http.StatusOK, userinfo) // 返回数据:{"user":"akira37","password":"123456"}
} else {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error()
})
}
})
3.1.4 Post获取XML数据
在开发API时经常会用到JSON或XML来作为数据交互的格式,可以用c.GetRawData()
获取数据。
【例】接收如下所示的XML数据:
<?xml version="1.0" encoding="UTF-8"?>
<article>
<content type="string">我是Akira37</content>
<title type="string">Hyperplasma!!!</title>
</article>
定义如下结构体:
type Article struct {
Title string `xml:"title"`
Content string `xml:"content"`
}
路由如下:
router.POST("/xml", func(c *gin.Context) {
b, _ := c.GetRawData() // 从c.Request.Body读取请求数据
article := &Article{}
if err := xml.Unmarshal(b, &article); err == nil {
c.JSON(http.StatusOK, article)
} else {
c.JSON(http.StatusBadRequest, err.Error())
}
})
3.2 简单的路由组
使用router.Group()
方法定义一个路由组
func main() {
router := gin.Default()
v1 := router.Group("/v1")
{
v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEndpoint)
}
v2 := router.Group("/v2")
{
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint)
}
router.Run(":8080")
}
3.3 路由文件、分组
设该项目为gin_demo
,新建routes
文件夹,在其中新建adminRoutes.go
、apiRoutes.go
、defaultRoutes.go
:
/* adminRoutes.go */
package routes
import (
"net/http"
"github.com/gin-gonic/gin"
)
func AdminRoutesInit(router *gin.Engine) {
adminRouter := router.Group("/admin")
{
adminRouter.GET("/user", func(c *gin.Context) {
c.String(http.StatusOK, "用户")
})
adminRouter.GET("/news", func(c *gin.Context) {
c.String(http.StatusOK, "news")
})
}
}
/* apiRoutes.go */
package routes
import (
"net/http"
"github.com/gin-gonic/gin"
)
func ApiRoutesInit(router *gin.Engine) {
apiRoute := router.Group("/api")
{
apiRoute.GET("/user", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"username": "Akira",
"age": 108,
})
})
apiRoute.GET("/news", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"title": "这是新闻",
})
})
}
}
/* defaultRoutes.go */
package routes
import (
"net/http"
"github.com/gin-gonic/gin"
)
func DefaultRoutesInit(router *gin.Engine) {
defaultRoute := router.Group("/")
{
defaultRoute.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "首页")
})
}
}
配置main.go
:
package main
import (
"gin_demo/routes"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
routes.AdminRoutesInit(r)
routes.ApiRoutesInit(r)
routes.DefaultRoutesInit(r)
r.Run(":8080")
}
4 自定义控制器
4.1 控制器分组
当项目较大时需要对控制器进行分组:
新建controller/admin
目录,在其中新建NewsController.go
、UserController.go
等文件:
/* NewsController.go */
package admin
import (
"net/http"
"github.com/gin-gonic/gin"
)
type NewsController struct {
}
func (c NewsController) Index(ctx *gin.Context) {
ctx.String(http.StatusOK, "新闻首页")
}
/* UserController.go */
package admin
import (
"net/http"
"github.com/gin-gonic/gin"
)
type UserController struct {
}
func (c UserController) Index(ctx *gin.Context) {
ctx.String(http.StatusOK, "这是用户首页")
}
func (c UserController) Add(ctx *gin.Context) {
ctx.String(http.StatusOK, "增加用户")
}
在前述routes
文件夹中各路由文件中进行配置即可。此处以adminRoutes.go
为例,其他路由的配置方法类似:
/* adminRoutes.go */
package routes
import (
"gin_demo/controller/admin"
"net/http"
"github.com/gin-gonic/gin"
)
func AdminRoutesInit(router *gin.Engine) {
adminRouter := router.Group("/admin")
{
adminRouter.GET("/user", admin.UserController{}.Index)
adminRouter.GET("/user/add", admin.UserController{}.Add)
adminRouter.GET("/news", admin.NewsController{}.Add)
}
}
4.2 控制器的继承
新建controller/admin/BaseController.go
:
/* BaseController.go */
package admin
import (
"net/http"
"github.com/gin-gonic/gin"
)
type BaseController struct {
}
func (c BaseController) Success(ctx *gin.Context) {
ctx.String(http.StatusOK, "成功")
}
func (c BaseController) Error(ctx *gin.Context) {
ctx.String(http.StatusOK, "失败")
}
新建NewsController
,让其继承BaseController
,继承后即可调用控制器中的公共方法:
package admin
import (
"github.com/gin-gonic/gin"
)
type NewsController struct {
BaseController
}
func (c NewsController) Index(ctx *gin.Context) {
c.Success(ctx)
}
5 中间件
Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数,即中间件。中间件适合处理公共的业务逻辑,例如登录认证、权限校验、数据分页、记录日志、耗时统计等。
通俗地讲,中间件就是匹配路由前和匹配路由完成后执行的一系列操作。
5.1 路由中间件
Gin中的中间件必须为gin.HandlerFunc
类型。配置路由时可以传递多个回调函数,最后一个回调函数前触发的方法都可称为中间件,如下例所示:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func initMiddleware(ctx *gin.Context) {
fmt.Println("这是中间件")
}
func main() {
r := gin.Default()
r.GET("/", initMiddleware, func(ctx *gin.Context) {
ctx.String(200, "首页——中间件演示")
})
r.GET("/news", initMiddleware, func(ctx *gin.Context) {
ctx.String(200, "新闻页面——中间件演示")
})
r.Run(":8080")
}
中间件中加入ctx.Next()
可以在路由匹配完成后执行一些操作。例如统计一个请求的执行时间:
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
func initMiddleware(ctx *gin.Context) {
fmt.Println("Step1: 中间件执行中")
start := time.Now().UnixNano()
// 调用该请求的剩余处理程序
ctx.Next()
fmt.Println("Step3: 程序执行完成,计算时间…" )
// 计算耗时
end := time.Now().UnixNano()
fmt.Println(end - start)
}
func main() {
r := gin.Default()
r.GET("/", initMiddleware, func(ctx *gin.Context) {
fmt.Println("Step2: 执行首页返回数据")
ctx.String(200, "首页——中间件演示")
})
r.GET("/news", initMiddleware, func(ctx *gin.Context) {
ctx.String(200, "新闻页面——中间件演示")
})
r.Run(":8080")
}
一个路由配置多个中间件的执行顺序:
func initMiddlewareOne(ctx *gin.Context) {
fmt.Println("initMiddlewareOne - 1 - 中间件执行中")
ctx.Next() // 调用该请求的剩余处理程序
fmt.Println("initMiddlewareOne - 2 - 中间件执行中")
}
func initMiddlewareTwo(ctx *gin.Context) {
fmt.Println("initMiddlewareTwo - 1 - 中间件执行中")
ctx.Next() // 调用该请求的剩余处理程序
fmt.Println("initMiddlewareTwo - 2 - 中间件执行中")
}
func main() {
r := gin.Default()
r.GET("/", initMiddlewareOne, initMiddlewareTwo, func(ctx *gin.Context) {
fmt.Println("执行路由中的语句…")
ctx.String(200, "首页——中间件演示")
})
r.Run(":8080")
}
initMiddlewareOne - 1 - 中间件执行中
initMiddlewareTwo - 1 - 中间件执行中
执行路由中的语句…
initMiddlewareTwo - 2 - 中间件执行中
initMiddlewareOne - 2 - 中间件执行中
ctx.Abort()
方法用于终止调用该请求的剩余处理程序:
func initMiddlewareOne(ctx *gin.Context) {
fmt.Println("initMiddlewareOne - 1 - 中间件执行中")
ctx.Next() // 调用该请求的剩余处理程序
fmt.Println("initMiddlewareOne - 2 - 中间件执行中")
}
func initMiddlewareTwo(ctx *gin.Context) {
fmt.Println("initMiddlewareTwo - 1 - 中间件执行中")
ctx.Abort() // 终止调用该请求的剩余处理程序
fmt.Println("initMiddlewareTwo - 2 - 中间件执行中")
}
func main() {
r := gin.Default()
r.GET("/", initMiddlewareOne, initMiddlewareTwo, func(ctx *gin.Context) {
fmt.Println("执行路由中的语句…")
ctx.String(200, "首页——中间件演示")
})
r.Run(":8080")
}
initMiddlewareOne - 1 - 中间件执行中
initMiddlewareTwo - 1 - 中间件执行中
initMiddlewareTwo - 2 - 中间件执行中
initMiddlewareOne - 2 - 中间件执行中
5.2 全局中间件
通过r.Use()
方法配置全局中间件
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func initMiddleware(ctx *gin.Context) {
fmt.Println("全局中间件通过r.Use()方法配置")
ctx.Next()
}
func main() {
r := gin.Default()
r.Use(initMiddleware)
r.GET("/", initMiddleware, func(ctx *gin.Context) {
ctx.String(200, "首页——中间件演示")
})
r.GET("/news", initMiddleware, func(ctx *gin.Context) {
ctx.String(200, "新闻页面——中间件演示")
})
r.Run(":8080")
}
5.3 在路由分组中配置中间件
为路由组注册中间件有以下两种写法(以下以中间件StatCost()
为例):
- 作为
r.Group()
路由组方法的可变参数:
shopGroup := r.Group("/shop", StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {
// ...
})
// ...
}
- 使用路由组对象的
Use()
方法(与前述全局中间件类似)
shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {
// ...
})
// ...
}
分组路由AdminRoutes.go
中配置中间件:
package routes
import (
"fmt"
"gin_demo/controller/admin"
"net/http"
"github.com/gin-gonic/gin"
)
func initMiddleware(ctx *gin.Context) {
fmt.Println("路由分组中间件")
ctx.Next()
}
func AdminRoutesInit(router *gin.Engine) {
adminRouter := router.Group("/admin", initMiddleware)
{
adminRouter.GET("/user", admin.UserController{}.Index)
adminRouter.GET("/user/add", admin.UserController{}.Add)
adminRouter.GET("/news", func(c *gin.Context) {
c.String(http.StatusOK, "news")
})
}
}
5.4 中间件和对应控制器之间共享数据
分别通过ctx.Set()
和ctx.Get()
方法设置、获取值。
中间件设置值:
func InitAdminMiddleware(ctx *gin.Context) {
fmt.Println("路由分组中间件")
ctx.Set("username", "张三") // 通过ctx.Set()在请求上下文中设置值,后续的处理函数能够获取该值
ctx.Next()
}
控制器获取值:
func (c UserController) Index(ctx *gin.Context) {
username, _ := ctx.Get("username")
fmt.Println(username)
ctx.String(http.StatusOK, "这是用户首页")
}
5.5 注意事项
gin.Default()
默认使用了Logger
和Recovery
中间件,其中:
Logger
中间件将日志写入gin.DefaultWriter
(即使配置了GIN_MODE=release
)Recovery
中间件会recover
任何panic
:若存在panic
,会写入500
响应码
若不想使用上述两个默认中间件,可以使用gin.New()
新建一个没有任何默认中间件的路由。
当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context
), 必须使用其只读副本(c.Copy()
):
r.GET("/", func(c *gin.Context) {
cCp := c.Copy()
go func() {
time.Sleep(5 * time.Second)
fmt.Println("Done in path " + cCp.Request.URL.Path) // 使用创建的副本
}()
c.String(http.StatusOK, "首页")
})
6 自定义Model
对于简单应用,可以直接在Controller中完成常见的业务逻辑。但若有个功能可在多个控制器或多个模板中复用,则可将公共的功能单独抽取出来作为一个模块(Model)。
Model是逐步抽象的过程,一般会在Model中封装一些公共方法让各Controller使用;亦可在Model中实现数据库相关操作。
6.1 封装公共函数
新建models
目录,在其中新建tools.go
:
package models
import (
"crypto/md5"
"fmt"
"time"
"github.com/astaxie/beego"
)
// 时间戳间戳转换成日期
func UnixToDate(timestamp int) string {
t := time.Unix(int64(timestamp), 0)
return t.Format("2006-01-02 15:04:05")
}
//日期转换成时间戳
func DateToUnix(str string) int64 {
template := "2006-01-02 15:04:05"
t, err := time.ParseInLocation(template, str, time.Local)
if err != nil {
return 0
}
return t.Unix()
}
func GetUnix() int64 {
return time.Now().Unix()
}
func GetDate() string {
template := "2006-01-02 15:04:05"
return time.Now().Format(template)
}
func GetDay() string {
template := "20060102"
return time.Now().Format(template)
}
func Md5(str string) string {
data := []byte(str)
return fmt.Sprintf("%x\n", md5.Sum(data))
}
6.2 调用Model、注册全局模板函数
控制器中调用Model中的函数:
package controllers
import (
"gin_demo/models"
)
func main() {
day := models.GetDay()
}
如2.3.3所述,在main.go
中通过router.SetFuncMap()
方法自定义模板函数,(注意顺序,注册模板函数应在加载模板之前):
r := gin.Default()
r.SetFuncMap(template.FuncMap{
"unixToDate": models.UnixToDate,
})
在控制器中应用:
func (c UserController) Add(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "admin/user/add.html", gin.H{
"now": models.GetUnix(),
})
}
对应的模板:
<h2>{{.now | unixToDate}}</h2>
6.3 补充内容:Md5加密
可在go语言包官网(pkg.go.dev)获取md5
包。
方式1:
data := []byte("123456")
h := md5.Sum(data)
md5str := fmt.Sprintf("%x", h)
fmt.Println(md5str)
方式2:
h := md5.New()
io.WriteString(h, "123456")
fmt.Printf("%x\n", h.Sum(nil))
7 文件上传
注:上传文件的表单应设置enctype="multipart/form-data"
7.1 单文件上传
简单示例:
func main() {
router := gin.Default()
// 为multipart forms设置较低的内存限制(默认为32 MiB)
router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// 单文件
file, _ := c.FormFile("file")
log.Println(file.Filename)
dst := "./" + file.Filename
// 上传文件至指定的完整文件路径
c.SaveUploadedFile(file, "./files/" + file.Filename)
c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
})
router.Run(":8080")
}
文件上传基本步骤:
- 定义模板:上传文件的表单应设置
enctype="multipart/form-data"
{{ define "admin/user/add.html" }}
<!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>
<form action="/admin/user/doAdd" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="username" placeholder="用户名"><br><br>
头像:<input type="file" name="face"><br><br>
<input type="submit" value="提交">
</form>
</body>
</html>
{{ end }}
- 定义业务逻辑:使用
ctx.FormFile()
方法从获取表单中的文件,使用path.Join()
函数指定文件路径(类似于Python中的os.path.join()
),使用ctx.SaveUploadedFile(file, dst)
方法将文件以指定路径保存
func (c UserController) DoAdd(ctx *gin.Context) {
username := ctx.PostForm("username")
file, err := ctx.FormFile("face")
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}
// 上传文件到指定的目录
dst := path.Join("./static/upload", file.Filename)
fmt.Println(dst)
ctx.SaveUploadedFile(file, dst)
ctx.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
"username": username,
})
}
7.2 多文件上传
上传不同名称的多个文件(相当于多次单文件上传):
- 定义模板:同前述
{{ define "admin/user/add.html" }}
<!-- ... -->
<form action="/admin/user/doAdd" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="username" placeholder="用户名"><br><br>
头像1:<input type="file" name="face1"><br><br>
头像2:<input type="file" name="face2"><br><br>
<input type="submit" value="提交">
</form>
<!-- ... -->
{{ end }}
- 定义业务逻辑:相当于多次单文件上传
func (c UserController) DoAdd(ctx *gin.Context) {
username := ctx.PostForm("username")
face1, err1 := ctx.FormFile("face1")
face2, err2 := ctx.FormFile("face2")
if err1 != nil && err2 != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}
// 上传文件到指定的目录
if err1 == nil {
dst1 := path.Join("./static/upload", face1.Filename)
ctx.SaveUploadedFile(face1, dst1)
}
if err2 == nil {
dst2 := path.Join("./static/upload", face2.Filename)
ctx.SaveUploadedFile(face2, dst2)
}
ctx.JSON(http.StatusOK, gin.H{
"message": "文件上传成功",
"username": username,
})
}
上传相同名称的多个文件:
- 定义模板:同前述
{{ define "admin/user/add.html" }}
<!-- ... -->
<form action="/admin/user/doAdd" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="username" placeholder="用户名"><br><br>
头像1:<input type="file" name="face[]"><br><br>
头像2:<input type="file" name="face[]"><br><br>
<input type="submit" value="提交">
</form>
<!-- ... -->
{{ end }}
- 定义业务逻辑:先使用
ctx.MultipartForm()
方法获取Multipart form,再直接对form.File
使用name
索引得到全部该名称的文件,最后使用for range
遍历、上传每个文件
func (c UserController) DoAdd(ctx *gin.Context) {
username := ctx.PostForm("username")
// Multipart form
form, _ := ctx.MultipartForm()
files := form.File["face[]"]
for _, file := range files {
// 上传文件至指定目录
dst := path.Join("./static/upload", file.Filename)
ctx.SaveUploadedFile(file, dst)
}
ctx.JSON(http.StatusOK, gin.H{
"message": "文件上传成功",
"username": username,
})
}
7.3 按日期存储上传的文件
- 定义模板:同前述
{{ define "admin/user/add.html" }}
<!-- ... -->
<form action="/admin/user/doAdd" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="username" placeholder="用户名"><br><br>
头像:<input type="file" name="face"><br><br>
<input type="submit" value="提交">
</form>
<!-- ... -->
{{ end }}
- 定义业务逻辑
func (c UserController) DoAdd(ctx *gin.Context) {
username := ctx.PostForm("username")
// 1. 获取上传的文件
file, err1 := ctx.FormFile("face")
if err1 == nil {
//2. 获取后缀名(.jpg .png .gif .jpeg),判断类型是否正确
extName := path.Ext(file.Filename)
allowExtMap := map[string]bool{
".jpg": true,
".png": true,
".gif": true,
".jpeg": true,
}
if _, ok := allowExtMap[extName]; !ok {
ctx.String(http.StatusInternalServerError, "文件类型不合法")
return
}
//3. 创建图片保存目录
day := models.GetDay() // 调用此前在Model中定义的工具函数,生成日期
dir := "./static/upload/" + day
if err := os.MkdirAll(dir, 0666); err != nil {
log.Error(err)
}
//4. 生成文件名称
fileUnixName := strconv.FormatInt(models.GetUnix(), 10) // 时间戳作为文件名
saveDir := path.Join(dir, fileUnixName + extName)
ctx.SaveUploadedFile(file, saveDir)
}
ctx.JSON(http.StatusOK, gin.H{
"message": "文件上传成功",
"username": username,
})
}
8 Cookie
HTTP是无状态协议,若要实现多个页面之间共享数据,可以使用Cookie或Session。
Cookie存储于访问者计算机的浏览器中,使得可以用同一浏览器访问同一域名时共享数据。
Cookie可实现的功能:
- 保持用户登录状态
- 保存用户历史记录
- 智能推荐
- 购物车
8.1 设置和获取Cookie
设置cookie使用c.SetCookie()
方法,函数签名如下所示,其中各参数意义如下:
name
:cookie的键。value
:cookie的值。maxAge
:过期时间,若不设置则传入nil
。path
:cookie的路径。domain
:作用域,本地调试为localhost
,正式上线配置成域名secure
:为true
时,cookie在HTTP中无效,在HTTPS中 才有效httpOnly
:微软对COOKIE做的扩展,若在COOKIE中设置了httpOnly
属性, 则其他程序(JS脚本、applet等)将无法读取COOKIE信息,防止XSS攻击。
SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
获取cookie使用c.Cookie()
方法,传入cookie的name
即可,使用方式:
cookie, err := c.Cookie("name")
综合使用例:
package main
import (
"gin_demo/models"
"html/template"
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
r.SetFuncMap(template.FuncMap{
"unixToDate": models.UnixToDate,
})
r.GET("/", func(c *gin.Context) {
c.SetCookie("usrename", "Akira37" , 3600, "/", "localhost", false, true)
c.String(http.StatusOK, "首页")
})
r.GET("/user", func(c *gin.Context) {
username, _ := c.Cookie("usrename")
c.String(http.StatusOK, "用户-" + username)
})
r.Run(":8080")
}
8.2 多个二级域名共享Cookie
若想让多个域名(例如kina.hyperplasma.top
与io.hyperplasma.top
)共享cookie,只需将c.SetCookie()
方法的domain
作用域参数配置为“.
+根域名”即可,例如:
c.SetCookie("usrename", "Akira37" , 3600, "/", ".hyperplasma.top", false, true)
9 Session
Session是另一种记录客户状态的机制,保存在服务器上。当客户端浏览器第一次访问服务器并发送请求时,服务器端会创建一个session对象,生成一个key-value键值对,然后将value保存到服务器,将key(cookie)返回到浏览器客户端。浏览器下次访问时会携带key(cookie),找到对应的 value(session)。
Gin官方未提供Session相关的文档,可以使用第三方的Session中间件来实现,例如gin-contrib/sessions
(github.com/gin-contrib/sessions),使用以下命令安装:
go get github.com/gin-contrib/sessions
gin-contrib/sessions
中间件支持的存储引擎:
- cookie
- memstore
- redis
- memcached
- mongodb
9.1 基于Cookie存储Session
额外引入github.com/gin-contrib/sessions/cookie
,通过cookie.NewStore()
函数创建基于Cookie的存储引擎(需传入用于加密的[]byte
);通过sessions.Sessions()
生成Session中间件(需传入Session名称与存储引擎)并配置。
控制器中使用sessions.Default()
函数初始化Session对象,可调用其Options()
方法设置过期时间等属性(需传入sessions.Options
结构体),调用其Set(name, value)
方法设置键-值,最后调用其Save()
方法保存;接收方则通过其Get(name)
方法读取Session值。
如下例所示:
package main
import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 创建基于Cookie的存储引擎,secret11111参数为用于加密的密钥
store := cookie.NewStore([]byte("secret11111"))
// 设置Session中间件,参数mysession为Session的名称(此处亦为Cookie的名称)
r.Use(sessions.Sessions("mysession", store))
r.GET("/", func(c *gin.Context) {
// 初始化Session对象
session := sessions.Default(c)
// 设置过期时间
session.Options(sessions.Options{
MaxAge: 3600 * 6, // 6hrs
})
//设置Session
session.Set("username", "Akira37")
session.Save()
c.JSON(200, gin.H{
"msg": session.Get("username")
})
})
r.GET("/user", func(c *gin.Context) {
// 初始化Session对象
session := sessions.Default(c)
// 通过session.Get()读取Session值
username := session.Get("username")
c.JSON(200, gin.H{"username": username})
})
r.Run(":8000")
}
9.2 基于Redis存储Session
若想将Session数据保存到Redis中,只需将Session的存储引擎改为Redis即可。
额外引入github.com/gin-contrib/sessions/redis
,通过redis.NewStore()
函数创建基于Redis的存储引擎,参数说明如下:
size
:Redis最大空闲连接数network
:通信协议,tcp
或udp
address
:Redis地址,格式为host:port
password
:Redis密码keyPairs
:Session加密密钥,类型为[]bytes
(同前述Cookie存储引擎)
其他函数使用方式与Cookie存储引擎基本一致,综合使用例如下:
package main
import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/redis"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 初始化基于Redis的存储引擎
store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret1122"))
r.Use(sessions.Sessions("mysession", store))
r.GET("/", func(c *gin.Context) {
session := sessions.Default(c)
session.Set("username", "Teresa")
session.Save()
c.JSON(200, gin.H{
"username": session.Get("username")
})
})
r.GET("/user", func(c *gin.Context) {
session := sessions.Default(c)
username := session.Get("username")
c.JSON(200, gin.H{
"username": username})
})
r.Run(":8000")
}
《Go语言Web开发(1):Gin框架》有1条评论