参考学习教程:
- 中文文档
- 【最新Go Web开发教程】基于gin框架和gorm的web开发实战 (七米出品)_哔哩哔哩_bilibili
Gin
是一个用Go语言编写的web框架。它是一个类似于martini
但拥有更好性能的API框架, 由于使用了httprouter
,速度提高了近40倍。如果你需要极好的性能,使用 Gin 吧
- Github地址
- 中文文档
- Gin框架介绍及使用 | 李文周的博客 (liwenzhou.com)
首先通过
go mod init 包名
命令创建一个go模块作为项目环境,然后进行后序操作
1、下载并安装 gin:
go get -u github.com/gin-gonic/gin
2、将 gin 引入到代码中:
import "github.com/gin-gonic/gin"
3、编写第一个gin实例:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
// 创建一个默认的路由引擎
r := gin.Default()
// 当客户端通过get方式请求/hello时,会执行后面的匿名函数
r.GET("/hello", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello Gin",
})
})
// 启动http服务,默认在8080端口
r.Run()
}
4、利用postman进行测试
5、使用Restful API示例
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
// 创建一个默认的路由引擎
r := gin.Default()
// 当客户端通过get方式请求/hello时,会执行后面的匿名函数
r.GET("/hello", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello Gin",
})
})
r.GET("/book", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "GET",
})
})
r.POST("/book", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "POST",
})
})
r.PUT("/book", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "PUT",
})
})
r.DELETE("/book", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "DELETE",
})
})
// 启动http服务,默认在8080端口
r.Run()
}
然后利用postman进行测试,采用不同的请求方式,请求同样的路径/book,会返回不同结果
ps:浏览器默认只支持get/post请求,如果想要发put/delete请求,需要通过ajax来进行。因此这里推荐使用postman测试
1️⃣ 模板定义
首先创建一个存放模板文件的templates
文件夹,然后在其内部按照业务分别定义一个post
文件夹和一个user
文件夹,分别编写对应的index.html
模板文件
post/index.html
文件的内容如下:
{
{define "post/index.html"}}
DOCTYPE html>
<html lang="en">
<head>
<title>post/indextitle>
head>
<body>
{
{.title}}
body>
html>
{
{end}}
user/index.html
文件的内容如下:
{
{define "user/index.html"}}
DOCTYPE html>
<html lang="en">
<head>
<title>user/indextitle>
head>
<body>
{
{.title}}
body>
html>
{
{end}}
2️⃣ 模板解析&渲染
创建main.go
编写http server代码并进行html模板解析渲染,可以使用LoadHTMLGlob()
或者LoadHTMLFiles()
两种方法进行HTML模板渲染
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
//创建默认的路由引擎
r := gin.Default()
//模板解析
// r.LoadHTMLFiles("./template/user/index.html", "./template/post/index.html")
r.LoadHTMLGlob("./template/**/*")
//以get方式发/post/index请求
r.GET("/post/index", func(c *gin.Context) {
//模板渲染
c.HTML(http.StatusOK, "post/index.html", gin.H{
"title": "this is post/index.html"})
})
//以get方式发/user/index请求
r.GET("user/index", func(c *gin.Context) {
//模板渲染
c.HTML(http.StatusOK, "user/index.html", gin.H{
"title": "this is user/index.html"})
})
//启动http服务在9090端口
r.Run(":9090")
}
3️⃣ postman测试
分别测试/user/index
和/post/index
查看结果
如果我们想传参BaretH的博客
,但是不希望自动转义成字符串,此时就可以自定义一个模板函数进行相应的处理,注意自定义函数的添加要在模板解析之前
package main
import (
"html/template"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
//创建默认的路由引擎
r := gin.Default()
//自定义函数safe
r.SetFuncMap(template.FuncMap{
"safe": func(str string) template.HTML {
return template.HTML(str)
},
})
//模板解析
r.LoadHTMLFiles("./index.html")
//以get方式发/post/index请求
r.GET("/index", func(c *gin.Context) {
//模板渲染
c.HTML(http.StatusOK, "index.html", "BaretH的博客")
})
//启动http服务在9090端口
r.Run(":9090")
}
这里定义了模板函数safe
,然后在模板文件中进行使用
DOCTYPE html>
<html lang="zh-CN">
<head>
<title>自定义函数title>
head>
<body>
<div>{
{ . | safe }}div>
body>
html>
当我们渲染的HTML文件中引用了静态文件时,我们只需要按照以下方式在渲染页面前调用gin.Static
方法即可
这里创建static
目录,然后新建index.css
body {
background-color: burlywood;
}
然后添加gin.Static
方法,该方法有两个参数,第一个为在html文件中引用的目录,第二个参数为实际的静态文件存放目录
package main
import (
"html/template"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
//创建默认的路由引擎
r := gin.Default()
//加载静态文件
r.Static("/xxx", "./statics")
//自定义函数safe
r.SetFuncMap(template.FuncMap{
"safe": func(str string) template.HTML {
return template.HTML(str)
},
})
//模板解析
r.LoadHTMLFiles("./index.html")
//以get方式发/post/index请求
r.GET("/index", func(c *gin.Context) {
//模板渲染
c.HTML(http.StatusOK, "index.html", "BaretH的博客")
})
//启动http服务在9090端口
r.Run(":9090")
}
然后在index.html
文件中引用css文件
DOCTYPE html>
<html lang="zh-CN">
<head>
<title>自定义函数title>
<link href="/xxx/index.css">link>
head>
<body>
<div>{
{ . | safe }}div>
body>
html>
目前主流的we开发方式有两种,一种是浏览器请求服务器,服务器返回完整的html页面内容;第二种就是通过前端框架如vue、react定义好一些模板文件,后端程序只需要返回给前端json格式数据,前端拿到json数据后自行渲染。那么在Gin框架中如何返回json格式数据呢?
在Go语言中,json格式数据可以以两种方式表示:map
和struct
,接下来我们模拟json数据,然后请求访问并返回
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
//模拟json数据
//方式一:map
user1 := gin.H{
"name": "zsr",
"age": 21,
"sex": "男",
}
//方式二:struct
type User struct {
Name string
Age int
Sex string
}
user2 := User{
"gcc",
21,
"女",
}
r.GET("/user1", func(c *gin.Context) {
c.JSON(http.StatusOK, user1)
})
r.GET("/user2", func(c *gin.Context) {
c.JSON(http.StatusOK, user2)
})
r.Run(":9090")
}
其中gin.H
本质上就是一个map
// H is a shortcut for map[string]interface{}
type H map[string]interface{
}
测试结果:
注意这里的c.JSON
本质上就是json数据的序列化,默认就是通过Go语言中json包使用反射机制读取数据的,而在Go语言中,首字母小写表示是不可导出的,因此结构体中字段首字母要大写,让json包读取到结构体中的字段,那如过就想要结构体中字段的首字母小写呢?我们可以通过结构体tag来解决,给结构体字段做一些自定制操作
例如下述Name字段后加的json:name
表示当使用json包操作Name字段时,名字为小写的name
type User struct {
Name string `json:"name"`
Age int
Sex string
}
querystring指的是URL中?
后面携带的参数,是键值对的形式,例如:localhost:8080?name=zsr&age=21
我们可以通过c.DefaultQuery
(可指定默认值获取不到就返回默认值)、c.Query
(获取不到返回空)、c.GetQuery
(有两个返回值,一个是获取的值,一个是bool类型的表示是否获取成功)三种方法来获取:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/hello", func(c *gin.Context) {
name := c.DefaultQuery("name", "用户名")
age := c.Query("age")
c.JSON(http.StatusOK, gin.H{
"name": name,
"age": age,
})
})
r.Run(":9090")
}
然后我们发起一个请求测试,可以看到成功返回请求参数
如果不传name和adreess参数,则返回指定的默认值
当前端请求的数据通过form表单提交时,如何获取数据呢?以下是一个小案例
首先是登录页login.html
,其中包含form表单有两个待提交参数username
和password
,提交时发起/home
请求
DOCTYPE html>
<html lang="en">
<head>
<title>logintitle>
head>
<body>
<form action="/home" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="text" name="password"><br>
<input type="submit" value="登录">
form>
body>
html>
然后是主页home.html
,接收显示参数username
和password
DOCTYPE html>
<html lang="en">
<head>
<title>logintitle>
head>
<body>
<h1>您的用户名为:{
{.username}}h1>
<h1>您的密码为:{
{.password}}h1>
body>
html>
最后是main.go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.LoadHTMLFiles("./login.html", "./home.html")
//处理/login的get请求
r.GET("/login", func(c *gin.Context) {
c.HTML(http.StatusOK, "login.html", nil)
})
//处理/home的post请求
r.POST("/home", func(c *gin.Context) {
//获取表单提交的数据
username := c.PostForm("username")
password := c.PostForm("password")
c.HTML(http.StatusOK, "home.html", gin.H{
"username": username,
"password": password,
})
})
r.Run(":9090")
}
其中获取参数时同样还有以下写法
//包含默认值
username := c.DefaultPostForm("username", "未知用户名")
password := c.DefaultPostForm("password", "未知密码")
//包含是否获取到结果的bool值
username, nameOk := c.GetPostForm("username")
if !nameOk {
username = "未知用户名"
}
password, pwdOk := c.GetPostForm("password")
if !pwdOk {
password = "未知密码"
}
访问测试:首先访问localhost:9090/login
,填写用户名和密码
点击登录,然后就会跳转到home.html
请求的参数通过URL路径传递,例如:/user/zsr/18
; 获取请求URL路径中的参数的方式如下
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
r.GET("/user/search/:username/:address", func(c *gin.Context) {
username := c.Param("username")
address := c.Param("address")
//输出json结果给调用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
r.Run(":8080")
}
测试访问:localhost:9090/user/zsr/18
注意:匹配的url不要冲突,比如现在新增以下请求处理,则会报错,因为路径冲突
r.GET("/user/:year/:month", func(c *gin.Context) {
year := c.Param("year")
month := c.Param("month")
c.JSON(200, gin.H{
"year": year,
"age": month,
})
})
上述我们获取前端请求的参数都是一个一个获取然后保存到变量中,当请求参数非常多时,就会十分不方便,为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type
识别请求数据类型并利用反射机制自动提取请求中QueryString
、form表单
、JSON
、XML
等参数到结构体中。 下面的示例代码演示了.ShouldBind()
强大的功能,它能够基于请求自动提取JSON
、form表单
和QueryString
类型的数据,并把值绑定到指定的结构体对象
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type UserInfo struct {
Username string
Password string
}
func main() {
r := gin.Default()
// 绑定JSON参数的示例
r.POST("/json", func(c *gin.Context) {
var user UserInfo
err := c.ShouldBind(&user) //注意:要通过地址传参,否则为值传递,无法修改数据!!
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error,
})
} else {
c.JSON(http.StatusOK, user)
}
})
// 绑定form表单参数示例
r.POST("/form", func(c *gin.Context) {
var user UserInfo
err := c.ShouldBind(&user) //注意:要通过地址传参,否则为值传递,无法修改数据!!
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error,
})
} else {
c.JSON(http.StatusOK, user)
}
})
// 绑定querystring参数示例
r.GET("/querystring", func(c *gin.Context) {
var user UserInfo
err := c.ShouldBind(&user) //注意:要通过地址传参,否则为值传递,无法修改数据!!
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error,
})
} else {
c.JSON(http.StatusOK, user)
}
})
r.Run(":9090")
}
type UserInfo struct {
Username string `form:"username" json:"username"`
Password string `form:"password" json:"password"`
}
文件上传也就是发http请求,下载也是如此
前端html
代码
DOCTYPE html>
<html lang="en">
<head>
<title>user/indextitle>
head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="f1">
<input type="submit" value="upload your file">
form>
body>
html>
package main
import (
"fmt"
"net/http"
"path"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.LoadHTMLFiles("./upload.html")
r.GET("/upload", func(c *gin.Context) {
c.HTML(http.StatusOK, "upload.html", nil)
})
r.POST("/upload", func(c *gin.Context) {
//获取上传的单个文件
file, err := c.FormFile("f1")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}
//定义文件保存路径
// dst := fmt.Sprintf("./%s", file.Filename)
dst := path.Join("./", file.Filename)
//将上传的文件保存在服务端本地
c.SaveUploadedFile(file, dst)
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
})
})
r.Run(":9090")
}
然后运行测试,选择文件上传
可以看到上传成功
然后在当前项目目录下可以看到上传的图片
前端页面修改为:
<!DOCTYPE html>
<html lang="en">
<head>
<title>user/index</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="f1" multiple>
<input type="submit" value="upload your file">
</form>
</body>
</html>
后端修改为:
package main
import (
"net/http"
"path"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
//处理multipart forms提交文件时默认的内存限制是32 MiB,可以通过下面的方式修改
//router.MaxMultipartMemory = 8 << 20 // 8 MiB
r.LoadHTMLFiles("./upload.html")
r.GET("/upload", func(c *gin.Context) {
c.HTML(http.StatusOK, "upload.html", nil)
})
r.POST("/upload", func(c *gin.Context) {
//获取上传的多个文件
form, _ := c.MultipartForm()
files := form.File["f1"]
for _, file := range files {
//定义文件保存路径
dst := path.Join("./", file.Filename)
//将上传的文件保存在服务端本地
c.SaveUploadedFile(file, dst)
}
c.JSON(http.StatusOK, gin.H{
"message": "uploaded ok!",
})
})
r.Run(":9090")
}
1️⃣ http重定向:使用Redirect
,内部、外部重定向均支持
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/")
})
2️⃣ 路由重定向:使用HandleContext
r.GET("/test", func(c *gin.Context) {
//跳转到到/test2的路由处理函数
c.Request.URL.Path = "/test2" //修改请求的url
r.HandleContext(c) //继续后续的处理
})
r.GET("/test2", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"hello": "world"})
})
1️⃣ 普通路由
基本的四种请求方式
//用于获取信息,类似于sql中的select
r.GET("/get", func(c *gin.Context) {
...})
//用于向服务端发送数据,类似于sql中的update,用来修改数据的内容,但是不会增加数据的种类等
r.POST("/post", func(c *gin.Context) {
...})
//向服务器端发送数据,但是该请求会改变数据的种类等资源,就像sql中的insert一样,会创建新的内容
r.PUT("/put", func(c *gin.Context) {
...})
//用来删除某一个资源的,类似于sql的delete操作
r.DELETE("/put", func(c *gin.Context) {
...})
可以匹配所有请求方式的Any
r.Any("/any", func(c *gin.Context) {
...})
为没有配置处理函数的路由添加处理程序(默认情况下它返回404代码)
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusNotFound, "views/404.html", nil)
})
2️⃣ 路由组
我们可以将拥有共同URL前缀的路由划分为一个路由组。为了美观,习惯性一对
{}
包裹同组的路由,用不用{}
包裹功能上没什么区别。路由组常用于在划分业务逻辑或划分API版本的情况
func main() {
r := gin.Default()
userGroup := r.Group("/user")
{
userGroup.GET("/index", func(c *gin.Context) {
...})
userGroup.GET("/login", func(c *gin.Context) {
...})
userGroup.POST("/login", func(c *gin.Context) {
...})
}
shopGroup := r.Group("/shop")
{
shopGroup.GET("/index", func(c *gin.Context) {
...})
shopGroup.GET("/cart", func(c *gin.Context) {
...})
shopGroup.POST("/checkout", func(c *gin.Context) {
...})
}
r.Run()
}
由组也是支持嵌套的,例如:
shopGroup := r.Group("/shop")
{
shopGroup.GET("/index", func(c *gin.Context) {
...})
shopGroup.GET("/cart", func(c *gin.Context) {
...})
shopGroup.POST("/checkout", func(c *gin.Context) {
...})
// 嵌套路由组
xx := shopGroup.Group("xx")
xx.GET("/oo", func(c *gin.Context) {
...})
}
Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件
middleware
,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等
Gin框架中中间件必须是HandlerFunc
类型,也就是func(*Context)
类型,这是一种函数类型,必须有*Context
参数
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
接下来自定义一个中间件Mymiddleware
,来统计请求处理的耗时时间
func Mymiddleware(c *gin.Context) {
fmt.Println("进入Mymiddleware")
start := time.Now() //计时
c.Next() //调用该请求剩余的处理程序
cost := time.Since(start)
fmt.Printf("路由处理耗时:%v\n", cost)
fmt.Println("退出Mymiddleware")
}
注意:
c.Next()
:表示调用该请求剩余的处理程序c.Abort()
:表示阻止调用剩余的处理程序1️⃣ 方式一:为单个路由注册
当我们通过c.Get
或其他方式编写路由处理程序时,其中的第二个参数就是HandlerFunc
类型的可变参数,因此自定义的中间件作为参数传入其中即可
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
接下来我们编写一个路由处理程序,将上述定义的Mymiddleware
中间件注册进去
func main() {
r := gin.Default()
r.GET("/index", Mymiddleware, func(c *gin.Context) {
fmt.Println("执行/index处理函数")
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
})
r.Run(":9090")
}
然后运行测试,访问localhost:9090/index,看后台打印信息,可以看到先通过了中间件再执行了路由处理函数
2️⃣ 为全局路由注册
如果我们有多个路由处理函数都想使用同一个中间件,每次传参有些麻烦,我们可以通过全局路由注册的方式,一处注册,多处生效
func main() {
r := gin.Default()
//全局注册中间件
r.Use(Mymiddleware)
r.GET("/index", func(c *gin.Context) {
fmt.Println("执行/index处理函数")
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
})
r.GET("/index2", func(c *gin.Context) {
fmt.Println("指定/index2处理函数")
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
})
r.Run(":9090")
}
运行测试,先后请求/index
和/index2
,根据打印信息可以看到中间件均生效
如果再定义一个中间件Mymiddleware2
,然后全局注册两个中间件
func Mymiddleware2(c *gin.Context) {
fmt.Println("进入Mymiddleware2")
c.Next() //调用该请求剩余的处理程序
fmt.Println("退出Mymiddleware2")
}
func main() {
r := gin.Default()
//全局注册中间件
r.Use(Mymiddleware,Mymiddleware2)
r.GET("/index", func(c *gin.Context) {
fmt.Println("执行/index处理函数")
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
})
}
再测试,访问/index
,打印结果如下
对应程序的执行顺序如下如所示:
3️⃣ 为路由组注册中间件
方式一:
xxGroup := r.Group("/shop", Mymiddleware)
{
xxGroup.GET("/index", func(c *gin.Context) {
c.JSON(http.StatusOK,gin.H{
"message":"xx",
})
})
}
方式二:
xxGroup := r.Group("/shop")
xxGroup.Use(Mymiddleware)
{
xxGroup.GET("/index", func(c *gin.Context) {
c.JSON(http.StatusOK,gin.H{
"message":"xx",
})
})
}
我们可以在中间件中存值,在路由处理函数中取到
在Mymiddlerware2
中间件中通过c.Set
存入name值
func Mymiddleware2(c *gin.Context) {
fmt.Println("进入Mymiddleware2")
c.Set("name", "zsr")
c.Next() //调用该请求剩余的处理程序
fmt.Println("退出Mymiddleware2")
}
在路由处理函数中可以通过c.Get
/c.MustGet
等函数取出name值
r.GET("/getValue", func(c *gin.Context) {
name, ok := c.Get("name")
if !ok {
name = "匿名用户"
} else {
c.JSON(http.StatusOK, gin.H{
"name": name,
})
}
})
1️⃣ gin默认中间件
gin.Default()
默认使用了Logger
和Recovery
中间件
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
其中:
Logger
中间件将日志写入gin.DefaultWriter
,即使配置了GIN_MODE=release
Recovery
中间件会recover任何panic
。如果有panic的话,会写入500响应码如果不想使用上面两个默认的中间件,可以使用gin.New()
新建一个没有任何默认中间件的路由
2️⃣ gin中间件中使用goroutine
当在中间件或handler
中启动新的goroutine
时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy()
)
3️⃣ 通用中间件模板
以用户认证为例,通常我们会把中间件写成闭包的形式,然后可以在其中添加一些具体的操作
func authMiddleware() gin.HandlerFunc{
//连接数据库或其他一些准备工作
return func (c *gin.Context) {
//存放具体的逻辑,例如进行是否登录的判断
if 是登录用户{
//如果是用户则进行登录的路由处理
c.Next()
}
else{
//如果不是用户则不处理
c.Abort()
}
}
}