这是我参与「第三届青训营 -后端场」笔记创作活动的的第5篇笔记。
6、gin框架
使用:
- 下载gin依赖
go get github.com/gin-gonic/gin - 导入gin
import "github.com/gin-gonic/gin"
6.1 gin渲染
- HTML渲染
- 自定义模板函数
- 静态文件处理
main.go文件
package main
import (
"github.com/gin-gonic/gin"
"html/template"
"net/http"
)
func sayHello(c *gin.Context){
c.JSON(200,gin.H{
"message": "hello,golang!",
})
}
func main() {
r := gin.Default()//返回默认的路由引擎
//静态文件加载
r.Static("/xxx","./statics")
//gin框架中给模板添加自定义函数
r.SetFuncMap(template.FuncMap{
"safe": func(str string) template.HTML{
return template.HTML(str)
},
})
// r.LoadHTMLFiles("templates/posts/index.tmpl")//模板解析
r.LoadHTMLGlob("templates/**/*")
r.GET("/posts/index",func (c *gin.Context) {
c.HTML(http.StatusOK,"posts/index.tmpl",gin.H{
"title": "www.posts.com",
})
})
r.GET("/users/index",func (c *gin.Context) {
c.HTML(http.StatusOK,"users/index.tmpl",gin.H{
"title": "<a href='https://www.qq.com'>qq首页</a>",
})
})
//加载网上下载的前端模板
r.GET("/car", func(c *gin.Context) {
c.HTML(http.StatusOK,"home.html",nil)
})
//启动服务
r.Run(":8080")
}
6.1.1 JSON渲染
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
//gin.H 是map[string]interface{}的缩写
r.GET("/test1", func(c *gin.Context) {
//data := map[string]interface{}{
// "username":"yg",
// "age":12,
//}
//c.JSON(http.StatusOK,data)
//gin.H 等效于 map[string]interface{}
c.JSON(http.StatusOK,gin.H{
"username": "yg",
"age": 12,
})
})
r.GET("/test2", func(c *gin.Context) {
// 方法二:使用结构体
//属性首字母必须大写 为了序列化
var msg struct {
Name string `json:"user"` //输出json格式的时候为此名
Message string
Age int
}
msg.Name = "yg"
msg.Message = "Hello golang!"
msg.Age = 18
c.JSON(http.StatusOK, msg) //json序列化
})
//启动服务
r.Run()//默认8080端口 修改端口的话传入参数例如":9999"
}
6.2 获取参数
6.2.1 获取querystring参数
GET请求 URL?后面的是querystring参数
querystring指的是URL中?后面携带的参数,例如:/user/get?username=yg&age=12。 获取请求的querystring参数的方法如下:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
//querystring
r.GET("/user/get", func(c *gin.Context) {
username := c.DefaultQuery("username","yg")
age := c.Query("age")
sex, ok := c.GetQuery("sex")//取不到第二个参数 返回false
c.JSON(http.StatusOK,gin.H{
"message": "success",
"username": username,
"age": age,
"sex": sex,
"sex_isok": ok,
})
})
//启动服务
r.Run()
}
6.2.2 获取form表单参数
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
//form表单数据
r.POST("/user/get", func(c *gin.Context) {
// DefaultPostForm取不到值时会返回指定的默认值
//username := c.DefaultPostForm("username", "yg")
username := c.PostForm("username")
age := c.PostForm("age")
//输出json结果给调用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"age": age,
})
})
//启动服务
r.Run()
}
6.2.3 获取json参数
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
//json参数
r.POST("/json", func(c *gin.Context) {
// 注意:下面为了举例子方便,暂时忽略了错误处理
b, _ := c.GetRawData() // 从c.Request.Body读取请求数据
// 定义map或结构体
var m map[string]interface{}
// 反序列化
_ = json.Unmarshal(b, &m)
c.JSON(http.StatusOK, m)
})
//启动服务
r.Run()
}
6.2.4 获取path参数
请求的参数通过URL路径传递,例如:/user/search/yg/18。 获取请求URL路径中的参数的方式如下。
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
//path参数
r.GET("/user/get/:username/:age", func(c *gin.Context) {
username := c.Param("username")
age := c.Param("age")
//输出json结果给调用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": age,
})
})
//启动服务
r.Run()
}
6.2.5 参数绑定
为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryString、form表单、JSON、XML等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSON、form表单和QueryString类型的数据,并把值绑定到指定的结构体对象。
ShouldBind会按照下面的顺序解析请求中的数据完成绑定:
- 如果是
GET请求,只使用Form绑定引擎(query)。 - 如果是
POST请求,首先检查content-type是否为JSON或XML,然后再使用Form(form-data)。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
type UserInfo struct {
Username string `form:"username" json:"username"`
Password string `form:"password" json:"password"`
}
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
//form表单
r.POST("test1", func(c *gin.Context) {
var u UserInfo //
//使用ShouldBind必须传入指针进去才能完成赋值
//底层通过反射
err := c.ShouldBind(&u)
if err != nil {
c.JSON(http.StatusBadRequest,gin.H{
"error": err.Error(),
})
}else{
fmt.Printf("%#v\n",u)
c.JSON(http.StatusOK,gin.H{
"status":"ok",
})
}
})
//绑定json
r.POST("test2", func(c *gin.Context) {
var u UserInfo
if err := c.ShouldBind(&u);err!=nil{
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}else{
fmt.Printf("UserInfo: %#v\n",u)
c.JSON(http.StatusOK,gin.H{
"status": "success",
"username": u.Username,
"password": u.Password,
})
}
})
//启动服务
r.Run()
}
6.3 上传文件
6.3.1 单文件上传
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"log"
"net/http"
"path"
)
func main() {
r := gin.Default()
r.LoadHTMLFiles("./index.html")
r.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK,"index.html",nil)
})
// 处理multipart forms提交文件时默认的内存限制是32 MiB
// 可以通过下面的方式修改
// r.MaxMultipartMemory = 8 << 20 // 8 MiB
r.POST("/upload", func(c *gin.Context) {
// 单个文件
//从请求中读取文件
file, err := c.FormFile("f1") //与请求参数中的name一致
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}
log.Println(file.Filename)
//将读取的文件保存在本地
//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()
}
6.3.2 多文件上传
func main() {
router := gin.Default()
// 处理multipart forms提交文件时默认的内存限制是32 MiB
// 可以通过下面的方式修改
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// Multipart form
form, _ := c.MultipartForm()
files := form.File["file"]
for index, file := range files {
log.Println(file.Filename)
dst := fmt.Sprintf("C:/tmp/%s_%d", file.Filename, index)
// 上传文件到指定的目录
c.SaveUploadedFile(file, dst)
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("%d files uploaded!", len(files)),
})
})
router.Run()
}
6.4 重定向
6.4.1 HTTP重定向
func main() {
r := gin.Default()
r.GET("/index", func(c *gin.Context) {
//c.JSON(http.StatusOK,gin.H{
// "status":"ok",
//})
c.Redirect(http.StatusMovedPermanently,"http://www.qq.com")
})
r.Run()
}
6.4.2 路由重定向
路由重定向,使用HandleContext:
func main() {
r := gin.Default()
r.GET("/a", func(c *gin.Context) {
// 指定重定向的URL
//跳转到 /b 对应的路由处理函数
c.Request.URL.Path = "/b" //把请求的URI修改
r.HandleContext(c) //继续后续的处理
})
r.GET("/b", func(c *gin.Context) {
c.JSON(http.StatusOK,gin.H{
"message": "已重定向至此处",
})
})
r.Run()
}
6.5 Gin路由
6.5.1 普通路由
r.GET("/index", func(c *gin.Context) {...})
r.GET("/login", func(c *gin.Context) {...})
r.POST("/login", func(c *gin.Context) {...})
此外,还有一个可以匹配所有请求方法的Any方法如下:
r.Any("/test", func(c *gin.Context) {
switch c.Request.Method {
case "GET":
c.JSON(http.StatusOK,gin.H{"methodName": "GET"})
case "POST":
c.JSON(http.StatusOK,gin.H{"methodName": "POST"})
case "PUT":
c.JSON(http.StatusOK,gin.H{"methodName": "PUT"})
case "DELETE":
c.JSON(http.StatusOK,gin.H{"methodName": "DELETE"})
default:
c.JSON(http.StatusNotFound,gin.H{"ERROR":"error method"})
}
})
为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html页面。
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusNotFound, "views/404.html", nil)
})
6.5.2 路由组
我们可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}包裹同组的路由,这只是为了看着清晰,你用不用{}包裹功能上没什么区别。
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) {...})
}
通常我们将路由分组用在划分业务逻辑或划分API版本时。
6.5.3 路由原理
Gin框架中的路由使用的是httprouter这个库。
其基本原理就是构造一个路由地址的前缀树。
6.6 Gin中间件
Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
6.6.1 定义中间件
Gin中的中间件必须是一个gin.HandlerFunc类型。例如我们像下面的代码一样定义一个统计请求耗时的中间件。
// StatCost 是一个统计耗时请求耗时的中间件
func StatCost() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Set("name", "yg") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
// 调用该请求的剩余处理程序
c.Next()
// 不调用该请求的剩余处理程序
// c.Abort()
// 计算耗时
cost := time.Since(start)
log.Println(cost)
}
}
6.6.2 注册中间件
在gin框架中,我们可以为每个路由添加任意数量的中间件。
为全局路由注册
func main() {
// 新建一个没有任何默认中间件的路由
r := gin.New()
// 注册一个全局中间件
r.Use(StatCost())
r.GET("/test", func(c *gin.Context) {
name := c.MustGet("name").(string) // 从上下文取值
log.Println(name)
c.JSON(http.StatusOK, gin.H{
"message": "Hello world!",
})
})
r.Run()
}
为某个路由单独注册
// 给/test2路由单独注册中间件(可注册多个)
r.GET("/test2", StatCost(), func(c *gin.Context) {
name := c.MustGet("name").(string) // 从上下文取值
log.Println(name)
c.JSON(http.StatusOK, gin.H{
"message": "Hello world!",
})
})
为路由组注册中间件
为路由组注册中间件有以下两种写法。
写法1:
shopGroup := r.Group("/shop", StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
写法2:
shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
6.6.3 中间件注意事项
gin默认中间件
gin.Default()默认使用了Logger和Recovery中间件,其中:
Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。
如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。
gin中间件中使用goroutine
当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
func testHandler1(c *gin.Context) {
fmt.Println("index1....")
if name,e := c.Get("name");e==true{
fmt.Println(name)
}
c.JSON(http.StatusOK,gin.H{
"status": 200,
"message": "index",
})
}
//定义一个中间件m1: 统计请求处理函数的耗时
func m1(c *gin.Context){
fmt.Println("m1 in....")
//计时
start := time.Now()
c.Next() //调用后续的处理函数
//c.Abort() //阻止调用后续的处理函数
cost := time.Since(start)
fmt.Printf("cost:%v\n",cost)
fmt.Println("m1 out....")
}
func m2(c *gin.Context){
fmt.Println("m2 in....")
c.Set("name","yg")
c.Next() //调用后续的处理函数
//c.Abort() //阻止调用后续的处理函数
//return //结束
fmt.Println("m2 out....")
}
func authMiddle(doCheck bool) gin.HandlerFunc{
//连接数据库
//其他准备工作
return func(c *gin.Context){
//是否登录的判断
if doCheck{
}else{
}
c.Next()
//if 是登录用户
//c.Next()
//else
//c.Abort()
}
}
func main() {
r := gin.Default()
r.Use(m1,m2,authMiddle(true)) //全局注册中间件函数m1
r.GET("/index1", testHandler1)
r.GET("/shop", func(c *gin.Context) {
c.JSON(http.StatusOK,gin.H{
"message": "shop",
})
})
r.GET("/user", func(c *gin.Context) {
c.JSON(http.StatusOK,gin.H{
"message": "user",
})
})
//404
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound,gin.H{
"message": "网页不存在,请联系管理员~~~",
})
})
r.Run()
}
6.6.4 运行多个服务
我们可以在多个端口启动服务,例如:
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
)
var (
g errgroup.Group
)
func router01() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 01",
},
)
})
return e
}
func router02() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 02",
},
)
})
return e
}
func main() {
server01 := &http.Server{
Addr: ":8080",
Handler: router01(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
server02 := &http.Server{
Addr: ":8081",
Handler: router02(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
// 借助errgroup.Group或者自行开启两个goroutine分别启动两个服务
g.Go(func() error {
return server01.ListenAndServe()
})
g.Go(func() error {
return server02.ListenAndServe()
})
if err := g.Wait(); err != nil {
log.Fatal(err)
}
}