【青训营】gin入门相关内容 | 青训营笔记

131 阅读8分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记

Gin框架学习

gin的安装

 go get -u github.com/gin-gonic/gin

从server.go入手

 package main
 ​
 import (
     "os"
     "github.com/Moonlight-Zhao/go-project-example/cotroller"
     "github.com/Moonlight-Zhao/go-project-example/repository"
     "gopkg.in/gin-gonic/gin.v1"
 )
 ​
 // 初始化数据索引、引擎配置;
 // 构建路由、启动服务
 func main() {
     if err := Init("./data/"); err != nil {
         os.Exit(-1)
     }
     // 创建路由
     r := gin.Default()
     // 绑定路由规则,其中gin.Context封装了request和response
     r.GET("/community/page/get/:id", func(c *gin.Context) {
         topicId := c.Param("id")
         data := cotroller.QueryPageInfo(topicId)
         c.JSON(200, data)
     })
     // 监听端口,默认在8080
     // err := r.Run(":6060")
     err := r.Run()
     if err != nil {
         return
     }
 }

路由路径的设置:

 // 设置路由
 r := gin.Default()
 // 第一个参数是:路径; 第二个参数是:具体操作 func(c *gin.Context)
 r.GET("/Get", getting)
 r.POST("/Post", posting)
 r.PUT("/Put", putting)
 r.DELETE("/Delete", deleting)
 // 默认启动的是 8080端口
 r.Run()

路由分组

 // 两个路由组,都可以访问,大括号是为了保证规范
 v1 := r.Group("/v1")
 {
     // 通过 localhost:8080/v1/hello访问,以此类推
     v1.GET("/hello", sayHello)
     v1.GET("/world", sayWorld)
 }
 v2 := r.Group("/v2")
 {
     v2.GET("/hello", sayHello)
     v2.GET("/world", sayWorld)
 }
 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) {...})
 }

大量路由实现

  • 当路由变多时候,需要建立routers,将不同的模块拆分到不同的go文件。
 // routers包下某个router对外开放的方法
 func LoadRouter(e *gin.Engine) {
     e.Group("v1")
     {
         v1.GET("/post", postHandler)
         v1.GET("/get", getHandler)
     }
 }
 ​
 // mian.go
 func main() {
     r := gin.Default()
     // 调用该方法实现注册
     routers.LoadRouter(r)
     ...
     r.Run()
 }
  • 但规模更大时,建立router包,内部划分模块,每个模块有个router.go文件负责该模块的路由注册。
 /*
 ├── routers
 │   │
 │   ├── say
 │   │   ├── sayWorld.go
 │   │   └── router.go
 │   │
 │   ├── hello
 │   │   ├── helloWorld.go
 │   │   └── router.go
 │   │
 │   └── setup_router.go
 │   
 └── main.go
 */
 ​
 // setup_router.go
 type Register func(*gin.Engine)
 ​
 func Init(routers ...Register) *gin.Engine {
     // 注册路由
     rs := append([]Register{}, routers...)
 ​
     r := gin.New()
     // 遍历调用方法
     for _, register := range rs {
         register(r)
     }
     return r
 }
 ​
 // main.go
 func main() {
     // 设置需要加载的路由配置
     r := routers.Init(
         say.Routers,
         hello.Routers, // 后面还可以有多个
     )
     r.Run()
 }

获取参数

路径参数

 // : 只能匹配1个,*可以匹配任意个数
 // 此规则能够匹配/user/xxx这种格式,但不能匹配/user/ 或 /user这种格式
 router.GET("/user/:name", func(c *gin.Context) {
     name := c.Param("name")
     c.String(http.StatusOK, "Hello %s", name)
 })
 ​
 // 此规则既能匹配 /user/xxx/ 格式也能匹配 /user/xxx/other1/other2 这种格式,注意只能在最后用
 // 注意如果最后没有匹配的(末尾没有/,如果末尾有/则依旧是匹配的),那么会优先使用无该参数的路由,比如上面那个(与代码顺序无关)
 router.GET("/user/:name/*action", func(c *gin.Context) {
     name := c.Param("name")
     action := c.Param("action")
     message := name + " is " + action
     c.String(http.StatusOK, message)
 })

Get方法

 r.GET("/user", func(c *gin.Context) {
     //指定默认值
     name := c.DefaultQuery("name", "normal")
     //获取具体值
     age := c.Query("age")
     c.String(http.StatusOK, fmt.Sprintf("hello %s, your age is %s", name, age))
 })

Post方法

 r.POST("/form", func(c *gin.Context) {
     // 设置默认值
     types := c.DefaultPostForm("type", "post")
     username := c.PostForm("username")
     password := c.PostForm("password")
     // 还可以使用Query实现 Get + Post的结合
     name := c.Query("name")
     c.JSON(200, gin.H{
         "username": username,
         "password": password,
         "types":    types,
         "name":     name,
     })
 })

文件获取

  • 单个文件获取
 // 给表单限制上传大小 (默认 32 MiB)
 r.MaxMultipartMemory = 8 << 20 // 8 MiB
 r.POST("/upload", func(c *gin.Context) {
     file, err := c.FormFile("file")
     if err != nil {
         c.String(500, "上传文件出错")
     }
 ​
     // 上传到指定路径
     c.SaveUploadedFile(file, "C:/desktop/"+file.Filename)
     c.String(http.StatusOK, "fileName:", file.Filename)
 })
  • 多个文件获取
 // 获取MultipartForm
 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 {
     // 逐个存
     fmt.Println(file.Filename)
 }
 c.String(200, fmt.Sprintf("upload ok %d files", len(files)))

接受处理

 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"`
 }

content-type绑定

 r.POST("/loginJSON", func(c *gin.Context) {
     // 声明接收的变量
     var login Login
 ​
     // 默认绑定form格式
     if err := c.Bind(&login); err != nil {
         // 根据请求头中content-type自动推断
         c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
         return
     }
 ​
     // 输出结果
     c.JSON(http.StatusOK, gin.H{
         "status":   "200",
         "user":     login.User,
         "password": login.Password,
     })
 })

json绑定

 r.POST("/loginJSON", func(c *gin.Context) {
     // 声明接收的变量
     var json Login
     
     // 将request的body中的数据,按照json格式解析到结构体
     if err := c.ShouldBindJSON(&json); err != nil {
         // 如果发送的不是json格式,那么输出:  "error": "invalid character '-' in numeric literal"
         c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
         return
     }
     
     // 输出结果
     c.JSON(http.StatusOK, gin.H{
         "status":   "200",
         "user":     json.User,
         "password": json.Password,
     })
 })

响应处理

 // 1.JSON
 r.GET("/someJSON", func(c *gin.Context) {
     c.JSON(200, gin.H{
         "message": "Json",
         "status":  200,
     })
 })
 // 2.XML
 r.GET("/someXML", func(c *gin.Context) {
     c.XML(200, gin.H{"message": "abc"})
 })
 // 3.YAML
 r.GET("/someYAML", func(c *gin.Context) {
     c.YAML(200, gin.H{"name": "zhangsan"})
 })
 // 4.protobuf
 r.GET("/someProtoBuf", func(c *gin.Context) {
     reps := []int64{1, 2}
     data := &protoexample.Test{
         Reps:  reps,
     }
     c.ProtoBuf(200, data)
 })

重定向

 // HTTP重定向
 r.GET("/index", func(c *gin.Context) {
     c.Redirect(http.StatusMovedPermanently, "https://www.baidu.com")
 })
 ​
 // 路由重定向
 r.GET("/test", func(c *gin.Context) {
     // 指定重定向的URL
     c.Request.URL.Path = "/test2"
     r.HandleContext(c)
 })
 r.GET("/test2", func(c *gin.Context) {
     c.JSON(http.StatusOK, gin.H{"hello": "world"})
 })

异步执行

 r.GET("/long_async", func(c *gin.Context) {
     // 需要搞一个副本
     copyContext := c.Copy()
     // 异步处理
     go func() {
         time.Sleep(3 * time.Second)
         log.Println("异步执行:" + copyContext.Request.URL.Path)
         // 注意不能在这执行重定向的任务,不然panic
     }()
 })

会话控制

cookie控制

 r.GET("/getCookie", func(c *gin.Context) {
     // 获取客户端是否携带cookie
     cookie, err := c.Cookie("key_cookie")
     if err != nil {
         cookie = "cookie"
         c.SetCookie("key_cookie", "value_cookie", // 参数1、2: key & value
                     60,          // 参数3: 生存时间(秒)
                     "/",         // 参数4: 所在目录
                     "localhost", // 参数5: 域名
                     false,       // 参数6: 安全相关 - 是否智能通过https访问
                     true,        // 参数7: 安全相关 - 是否允许别人通过js获取自己的cookie
                    )
     }
     fmt.Printf("cookie的值是: %s\n", cookie)
 })

session控制

 package main
 ​
 import (
     "fmt"
     "github.com/gin-contrib/sessions"
     "github.com/gin-contrib/sessions/cookie"
     "github.com/gin-gonic/gin"
 )
 ​
 func main() {
     r := gin.Default()
     
     // 注意该密钥不要泄露了
     store := cookie.NewStore([]byte("secret"))
     //路由上加入session中间件
     r.Use(sessions.Sessions("mySession", store))
 ​
     r.GET("/setSession", func(c *gin.Context) {
         // 设置session
         session := sessions.Default(c)
         session.Set("key", "value")
         session.Save()
     })
 ​
     r.GET("/getSession", func(c *gin.Context) {
         // 获取session
         session := sessions.Default(c)
         v := session.Get("key")
         fmt.Println(v)
     })
 ​
     r.Run(":8080")
 }

cookie和session的区别

Session是服务器用来跟踪用户的一种手段,每个Session都有一个唯一标识:Session ID。当服务器创建了一个Session时,给客户端发送的报文里包含了Set-Cookie字段,其中有一个sid键对值,就是Session ID。客户端收到后将Cookie保存在浏览器中,并且在之后每次发送的报文中都含有Session ID。HTTP就是Session和Cookie两种方式实现用户状态跟踪。其中Session用于服务端,Cookie用于客户端。

  • session保存在服务器,客户端不知道其信息;cookie保存在客户端服务器能够知道其中的信息
  • session中保存的是对象,cookie中保存的是字符串
  • session的运行以来session id,而session id是存在于cookie中。即如果浏览器禁用了cookie,同时session也会失效(可通过其他方式实现)。
  • session在用户会话结束后就会关闭,但是cookie保存在客户端,可以长期保存。
  • cookie不是很安全,他人可以分析存放在本地的cookie进行cookie欺骗,session相对更加安全。
  • 单个cookie保存的数据不能超过4k,很多浏览器限制一个站点最多保存20个cookie。

中间件

 func BasicAuth(accounts Accounts) HandlerFunc // 身份认证
 func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc
 func Bind(val interface{}) HandlerFunc //拦截请求参数并进行绑定
 func ErrorLogger() HandlerFunc       //错误日志处理
 func ErrorLoggerT(typ ErrorType) HandlerFunc //自定义类型的错误日志处理
 func Logger() HandlerFunc //日志记录
 func LoggerWithConfig(conf LoggerConfig) HandlerFunc
 func LoggerWithFormatter(f LogFormatter) HandlerFunc
 func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc
 func Recovery() HandlerFunc
 func RecoveryWithWriter(out io.Writer) HandlerFunc
 func WrapF(f http.HandlerFunc) HandlerFunc //将http.HandlerFunc包装成中间件
 func WrapH(h http.Handler) HandlerFunc //将http.Handler包装成中间件

自定义中间件

 // HandlerFunc 本质就是一个函数,入参为 *gin.Context
 // HandlerFunc defines the handler used by gin middleware as return value.
 type HandlerFunc func(*Context)
 ​
 // 日志类
 func MyLogMiddleWare() gin.HandlerFunc {
     return func(c *gin.Context) {
         fmt.Println("[MyLog] 用户ip:", c.ClientIP())
         fmt.Println("[MyLog] 用户request:", c.Request)
     }
 }
 ​
 func main() {
     r := gin.Default()
 ​
     // 配置中间件
     r.Use(MyLogMiddleWare())
 ​
     // 注册路由
     r.GET("/say", func(c *gin.Context) {
         c.String(200, "request: %s", c.Request)
     })
 ​
     r.Run(":8080")
 }

局部中间件

 // 如果我们自定义的中间件只需要在某个路由上使用,只需要在该路由路径上使用该方法即可.
 func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
     return group.handle(http.MethodGet, relativePath, handlers)
 }
 ​
 //局部中间件使用
 r.GET("/test", MyLogMiddleWare(), func(c *gin.Context) {
     // 页面接收
     c.JSON(200, gin.H{"success": "ok"})
 })
 ​
 // 根据分组来添加中间件
 v1 := r.Group("v1", MyLogMiddleWare())
 // 也可以这样书写
 // v1.Use(MyLogMiddleWare())
 v1.GET("/c1", func(c *gin.Context) {
     // 页面接收
     c.JSON(200, gin.H{"request": "ok"})
 })
 v1.GET("/c2", func(c *gin.Context) {
     // 页面接收
     c.JSON(200, gin.H{"request": "ok"})
 })

处理后续工作

 func CalcTimeMiddleWare() gin.HandlerFunc {
     return func(c *gin.Context) {
         start := time.Now()
         c.Next()
         // 统计时间
         since := time.Since(start)
         fmt.Println("程序用时:", since)
     }
 }
 ​
 func main() {
     r := gin.Default()
 ​
     // 注册路由
     r.GET("/time", CalcTimeMiddleWare(), func(c *gin.Context) {
         time.Sleep(2 * time.Second)
         c.String(200, "ok")
     })
 ​
     r.Run(":8080")
 }

Gin渲染

HTML渲染

 // Gin框架中使用LoadHTMLGlob()或者LoadHTMLFiles()方法进行HTML模板渲染。
 func main() {
     r := gin.Default()
     r.LoadHTMLGlob("templates/**/*")
     //r.LoadHTMLFiles("templates/posts/index.html", "templates/users/index.html")
     r.GET("/posts/index", func(c *gin.Context) {
         c.HTML(http.StatusOK, "posts/index.html", gin.H{
             "title": "posts/index",
         })
     })
 ​
     r.GET("users/index", func(c *gin.Context) {
         c.HTML(http.StatusOK, "users/index.html", gin.H{
             "title": "users/index",
         })
     })
 ​
     r.Run(":8080")
 }

静态文件处理

 func main() {
     r := gin.Default()
     r.Static("/static", "./static")
     r.LoadHTMLGlob("templates/**/*")
    // ...
     r.Run(":8080")
 }

JSON渲染

 func main() {
     r := gin.Default()
 ​
     // gin.H 是map[string]interface{}的缩写
     r.GET("/someJSON", func(c *gin.Context) {
         // 方式一:自己拼接JSON
         c.JSON(http.StatusOK, gin.H{"message": "Hello world!"})
     })
     r.GET("/moreJSON", func(c *gin.Context) {
         // 方法二:使用结构体
         var msg struct {
             Name    string `json:"user"`
             Message string
             Age     int
         }
         msg.Name = "小王子"
         msg.Message = "Hello world!"
         msg.Age = 18
         c.JSON(http.StatusOK, msg)
     })
     r.Run(":8080")
 }

XML渲染

 func main() {
     r := gin.Default()
     // gin.H 是map[string]interface{}的缩写
     r.GET("/someXML", func(c *gin.Context) {
         // 方式一:自己拼接JSON
         c.XML(http.StatusOK, gin.H{"message": "Hello world!"})
     })
     r.GET("/moreXML", func(c *gin.Context) {
         // 方法二:使用结构体
         type MessageRecord struct {
             Name    string
             Message string
             Age     int
         }
         var msg MessageRecord
         msg.Name = "小王子"
         msg.Message = "Hello world!"
         msg.Age = 18
         c.XML(http.StatusOK, msg)
     })
     r.Run(":8080")
 }

YAML渲染

 r.GET("/someYAML", func(c *gin.Context) {
     c.YAML(http.StatusOK, gin.H{"message": "ok", "status": http.StatusOK})
 })

protobuf渲染

 r.GET("/someProtoBuf", func(c *gin.Context) {
     reps := []int64{int64(1), int64(2)}
     label := "test"
     // protobuf 的具体定义写在 testdata/protoexample 文件中。
     data := &protoexample.Test{
         Label: &label,
         Reps:  reps,
     }
     // 请注意,数据在响应中变为二进制数据
     // 将输出被 protoexample.Test protobuf 序列化了的数据
     c.ProtoBuf(http.StatusOK, data)
 })

运行多个服务

 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)
     }
 }