关于http:Gin实战演练

3次阅读

共计 14348 个字符,预计需要花费 36 分钟才能阅读完成。

Gin 实战

1 gin 的简略应用

package main

import "github.com/gin-gonic/gin"

func main() {
   // Default 办法的次要作用是实例化一个带有日志、故障复原中间件的引擎。r := gin.Default() // 实例化一个 gin 对象
   // 定义申请
   // 定义一个 GET 申请的路由,参数一是路由地址,也就是在浏览器拜访的相对路径,//                     参数二是一个匿名函数,函数外部用于业务逻辑解决。r.GET("/login", func(c *gin.Context) {
      c.JSON(200, gin.H{ //JSON 内容能够通过 gin 提供的 H 办法来构建,十分不便。"msg": "login", // 调用 JSON 办法返回数据。JSON 的操作非常简单,参数一是状态码,参数二是 JSON 的内容。})
   })
   // Run 办法最终会调用内置 http 库的 ListenAndServe 办法来监听端口,如果不传参数默认监听 80 端口,// 也能够通过参数来变更地址和端口。r.Run(":12005")
}

2 RESTful API

RESTful 是⽹络应⽤程序的⼀种设计⻛格和开发⽅式,每⼀个 URI 代表⼀种资源,客户端通过 POST、DELETE、PUT、GET 四种申请⽅式来对资源做增删改查的操作。

同样的,Gin 框架给咱们提供的除这 4 种动词外,还有 PATCH、OPTION、HEAD 等,具体内容能够查看 rentergroup.go ⽂件的 IRoutes 接⼝

// IRoutes defines all router handle interface.
type IRoutes interface {Use(...HandlerFunc) IRoutes

   Handle(string, string, ...HandlerFunc) IRoutes
   Any(string, ...HandlerFunc) IRoutes
   GET(string, ...HandlerFunc) IRoutes
   POST(string, ...HandlerFunc) IRoutes
   DELETE(string, ...HandlerFunc) IRoutes
   PATCH(string, ...HandlerFunc) IRoutes
   PUT(string, ...HandlerFunc) IRoutes
   OPTIONS(string, ...HandlerFunc) IRoutes
   HEAD(string, ...HandlerFunc) IRoutes

   StaticFile(string, string) IRoutes
   Static(string, string) IRoutes
   StaticFS(string, http.FileSystem) IRoutes
}

例如接口:

func main() {router := gin.Default()
   // 申请动词的第一个参数是申请门路,第二个参数是用于逻辑解决的函数
   router.POST("/article", func(c *gin.Context) {c.String(200, "article post")
   })
   router.DELETE("/article", func(c *gin.Context) {c.String(200, "article delete")
   })
    
    router.GET("/article/:id/:action", func(c *gin.Context) {id := c.Param("id")
        action := c.Param("action")
        fmt.Printf("2 /article/:id->%s, action:%s\n", id, action)
        c.String(200, id+" "+action)
    })

    router.Run(":8080")
}
  • 通过 web 拜访 url
  • 应用 curl 命令来拜访 url

    / 测试方法
    // curl -X PUT http://localhost:8080/article
    // curl -X POST http://localhost:8080/article
    // curl -X GET http://localhost:8080/article
    // curl -X DELETE http://localhost:8080/article

路由参数

: 路由

这种匹配模式是准确匹配的,只能匹配⼀个

拜访:http://localhost:8080/users/123

输入:123

func main() {r := gin.Default()
   r.GET("/users/:id", func(c *gin.Context) {id := c.Param("id")
      c.String(200, "The user id is  %s", id)
   })
   r.Run(":8080")
}

* 路由

还有⼀种不常⽤的就是 * 号类型的参数,示意匹配所有,后果是⼀个 / 结尾的门路字符串

拜访:http://localhost:8080/users/123

输入:/123

func main() {r := gin.Default()
   r.GET("/users/*id", func(c *gin.Context) {id := c.Param("id")
      c.String(200, "The user id is  %s", id)
   })
   r.Run(":8080")
}

特地阐明⼀点

拜访 http://localhost:8080/users 时候,会被重定向到 http://localhost:8080/users/,根本原因在于 /users 没有匹配的路由,然而有匹配 /users/ 的路由,所以就会被重定向 到 /users/,如下:

func main() {r := gin.Default()
   r.GET("/users/*id", func(c *gin.Context) {id := c.Param("id")
      c.String(200, "The user id is  %s", id)
   })
}

禁止重定向

r.RedirectTrailingSlash = false

加上如上设置之后,拜访 http://localhost:8080/users,是拜访不胜利的,因为没有服务器去解决这个 url

3 Gin 获取查问参数

例如:

http://127.0.0.1:8080/users?k1=v1&k2=v2

以 ? 为终点,后⾯的 k=v&k1=v1&k2=v2 这样的字符串就是查问参数

上述案例中有 2 个参数键值对,通过 & 来连贯:

k1=v1
k2=v2

能够应用 gin 框架中的如下接口来获取理论的参数值

// 3-2-url-param.go url 参数获取
package main

import (
   "fmt"

   "github.com/gin-gonic/gin"
)

func main() {r := gin.Default()
   r.GET("/", func(c *gin.Context) {c.DefaultQuery("id", "0")
      value, ok := c.GetQuery("id") // 适宜用来判断是否存在该参数

      if ok {fmt.Println("id:", value)
      } else {fmt.Println("id: nil")
      }

      c.String(200, c.DefaultQuery("wechat", "default baidu_org"))
   })
   r.Run(":8080")
}

理论 GetQuery 具体实现:

func (c *Context) GetQuery(key string) (string, bool) {if values, ok := c.GetQueryArray(key); ok {return values[0], ok
   }
   return "", false
}

DefaultQuery 的具体实现也是调用 GetQuery:

func (c *Context) DefaultQuery(key, defaultValue string) string {if value, ok := c.GetQuery(key); ok {return value}
   return defaultValue
}

GetQuery 和 Query 的区别

GetQuery 中传入 key 值,会返回 value,ok 若 ok 为 true,则 value 有值

Query 是间接返回字符串

能够⽤ GetQuery 来代替 Query ⽅法。GetQuery ⽅法的底层实现其实是 c.Request.URL.Query().Get(key),通过 url.URL.Query() 来获取所有的参数键值对

认真看 GetQuery 的具体应用形式

// 实质上是调⽤的 GetQueryArray,取的数组中第⼀个值
func (c *Context) GetQuery(key string) (string, bool) {if values, ok := c.GetQueryArray(key); ok {return values[0], ok
    }
    return "", false
}

func (c *Context) GetQueryArray(key string) ([]string, bool) {c.getQueryCache()  // 失去缓存,这一点很要害,缓存所有的键值对
    if values, ok := c.queryCache[key]; ok && len(values) > 0 {return values, true}
    return []string{}, false
}

func (c *Context) getQueryCache() {
   if c.queryCache == nil {c.queryCache = c.Request.URL.Query()
   }
}

其中 c.Request.URL.Query() 这个⽅法就是把 ?k=v&k1=v1&k2=v2 这类查询键值对转换为

map[string][]string,所以还是很耗性能的,这⾥ Gin 采⽤了缓存的做法提⾼了性能挺好,这也是 Gin 成为性能最快的 Golang Web 框架的起因之⼀。

4 接管数组和 Map

QueryArray

例如理论业务中,URL ⼤概是这样的 ?a=b&a=c&a=d , key 值都⼀ 样,然而对应的 value 不⼀样。

这类 URL 查问参数,就是⼀个数组,那么在 Gin 中咱们如何获取它们呢?

// 在浏览器里拜访 http://localhost:8080/?media=blog&media=wechat 会看到如下信息:// ["blog","wechat"]
func main() {r := gin.Default()
   r.GET("/", func(c *gin.Context) {fmt.Println("media:", c.QueryArray("media"))
      c.JSON(200, c.QueryArray("media"))
   })
   r.Run(":8080")
}

QueryArray ⽅法也有对应的 GetQueryArray ⽅法,区别在于返回对应的 key 是否存在

QueryMap

把满⾜⼀定格局的 URL 查问参数,转换为⼀个 map

例如:拜访:http://localhost:8080/?ids[0]=a&ids[1]=b&ids[2]=c

输入:{“0″:”a”,”1″:”b”,”2″:”c”}

func main() {r := gin.Default()

   r.GET("/", func(c *gin.Context) {fmt.Println("map:", c.QueryMap("ids"))
      c.JSON(200, c.QueryMap("ids"))
   })
   r.Run(":8080")
}

其中 QueryMap 的原理和具体源码实现:

// QueryMap returns a map for a given query key.
func (c *Context) QueryMap(key string) map[string]string {dicts, _ := c.GetQueryMap(key)
   return dicts
}

// GetQueryMap returns a map for a given query key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {c.getQueryCache()
   return c.get(c.queryCache, key)
}

// get is an internal method and returns a map which satisfy conditions.
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {dicts := make(map[string]string)
    exist := false
    for k, v := range m {if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
                exist = true
                dicts[k[i+1:][:j]] = v[0]
            }
        }
    }
    return dicts, exist
}

5 Form 表单

待补充

6 上传⽂件

上传单个文件 FormFile

test 目录下的 html 文件源码:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title> 登录 </title>
</head>
<body>
    <form action="http://127.0.0.1:8080/upload" method="post" enctype="multipart/form-data">
        头像:
        <input type="file" name="file">
        <br>
        <input type="submit" value="提交">
    </form>
</body>
</html>
func main() {// 1 创立路由, 默认应用了两个中间件 Logger(),Recovery()
   r := gin.Default()
   // 给表单限度上传大小 (默认 32 MiB)
   r.MaxMultipartMemory = 8 << 20 // 8 MiB
   r.Static("/", "./test")
   // 2 绑定路由规定,
   // gin.Context, 封装了 request 和 respose
   r.POST("/upload", func(c *gin.Context) {file, _ := c.FormFile("file")
      log.Println("file:", file.Filename)
      c.SaveUploadedFile(file, "./"+"test/"+file.Filename) // 上传文件到指定的门路
      c.String(200, fmt.Sprintf("%s upload file!", file.Filename))
   })
   // 3 监听端口,默认 8080
   r.Run(":8080")
}

上传多个文件,就是在上传单个文件的根底上 循环遍历文件列表而已

public 下的 html 文件为


<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Multiple file upload</title>
</head>
<body>
<h1>Upload multiple files with fields</h1>

<form action="/upload" method="post" enctype="multipart/form-data">
    Name: <input type="text" name="name"><br>
    Email: <input type="email" name="email"><br>
    Files: <input type="file" name="files" multiple><br><br>
    <input type="submit" value="Submit">
</form>
</body>
</html>
func main() {router := gin.Default()
   // Set a lower memory limit for multipart forms (default is 32 MiB)
   router.MaxMultipartMemory = 8 << 20 // 8 MiB
   router.Static("/", "./public")
   router.POST("/upload", func(c *gin.Context) {name := c.PostForm("name")
      email := c.PostForm("email")

      // Multipart form
      form, err := c.MultipartForm()
      if err != nil {c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error()))
         return
      }
      files := form.File["files"]

      for _, file := range files {log.Println("file:", file.Filename)
         filename := filepath.Base(file.Filename)
         if err := c.SaveUploadedFile(file, filename); err != nil {c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
            return
         }
      }

      c.String(http.StatusOK, fmt.Sprintf("Uploaded successfully %d files with fields name=%s and email=%s.", len(files), name, email))
   })
   router.Run(":8080")
}

7 分组路由

⽐如基于模块化,把同样模块的放在⼀起,⽐如 基于版本,把雷同版本的 API 放⼀起,便于使⽤。在有的框架中,分组路由也被称之为命名空间

url 分组,能够是分版本 等等

func main() {r := gin.Default()
    // 路由组注册中间件办法 1:xx1Group := r.Group("/xx1", func(c *gin.Context) {fmt.Println("/xx1 中间件") })
    {xx1Group.GET("/index", func(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"msg": "xx1Group"})
        })
        xx1Group.GET("/index2", func(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"msg": "2222xx1Group"})
        })
    }
    // 路由组注册中间件办法 2:xx2Group := r.Group("/xx2")
    xx2Group.Use(authMiddleware(true))
    {xx2Group.GET("/index", func(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"msg": "xx2Group"})
        })
    }
    r.Run(":8080")
}

路由中间件

通过 Group ⽅法的定义,咱们能够看到,它是能够接管两个参数的:

func (group RouterGroup) Group(relativePath string, handlers …HandlerFunc) RouterGroup

第⼀个就是咱们注册的分组路由(命名空间);第⼆个是⼀个 …HandlerFunc,能够把它了解为这个 分组路由的中间件,所以这个分组路由下的⼦路由在执⾏的时候,都会调⽤它

如上述代码,拜访 xx1/index2 或者 xx1/index 都会打印出 /xx1 中间件

分组路由嵌套

和上述分组的做法是统一

原理解析

以 get 为例

留神第⼀个参数 relativePath,这是⼀个相对路径,也就是咱们传给 Gin 的是⼀个相对路径,那么是 绝对谁的呢?

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {return group.handle(http.MethodGet, relativePath, handlers)
}

通过这句 absolutePath := group.calculateAbsolutePath(relativePath) 代码,咱们能够 看出是绝对以后的这个 group (⽅法接收者)的。当初 calculateAbsolutePath ⽅法的源代码咱们临时不看,回过头来看 Group 这个⽣成分组路由的 ⽅法。

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {absolutePath := group.calculateAbsolutePath(relativePath)
   handlers = group.combineHandlers(handlers)
   group.engine.addRoute(httpMethod, absolutePath, handlers)
   return group.returnObj()}
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
   return &RouterGroup{Handlers: group.combineHandlers(handlers),
      basePath: group.calculateAbsolutePath(relativePath),
      engine:   group.engine,
   }
}

这⾥要留神的是, 咱们通过 gin.Default() ⽣成的 gin.Engine 其实蕴含⼀个 RouterGroup (嵌套组 合), 所以它能够⽤ RouterGroup 的⽅法。Group ⽅法⼜⽣成了⼀个 *RouterGroup,这⾥最重要的就是 basePath , 它的值是 group.calculateAbsolutePath(relativePath),和咱们刚刚暂停的剖析的⽅法⼀样,既然这 样,就来看看这个⽅法吧。

func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {return joinPaths(group.basePath, relativePath)
}

GIn 中间件

Gin 框架容许开发者在解决申请的过程中,加⼊⽤户⾃⼰的钩⼦(Hook)函数。这个钩⼦函数就叫中间件,中间件适宜解决⼀些公共的业务逻辑,⽐如登录认证、权限校验、数据分⻚、记录⽇志、耗时统计等

在 Gin 中,咱们能够通过 Gin 提供的默认函数,来构建⼀个⾃带默认中间件的 *Engine。

 r := gin.Default()

Default 函数会默认绑定两个曾经筹备好的中间件,它们就是 Logger 和 Recovery,帮忙咱们打印⽇志 输入和 painc 解决。

func Default() *Engine {debugPrintWARNINGDefault()
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
}

从中咱们能够看到,Gin 的中间件是通过 Use ⽅法设置的,它接管⼀个可变参数,所以咱们同时能够设置 多个中间件。

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {engine.RouterGroup.Use(middleware...)
   engine.rebuild404Handlers()
   engine.rebuild405Handlers()
   return engine
}

其实就是 Gin 定义的⼀个 HandlerFunc , ⽽它在我 们 Gin 中常常使⽤

r.GET("/", func(c *gin.Context) {fmt.Println("HandlerFunc") 
    c.JSON(200, "HandlerFunc")
    })

后⾯的 func(c *gin.Context) 这部分其实就是⼀个 HandlerFunc

中间件实现 HTTP Basic Authorization

HTTP Basic Authorization 是 HTTP 常⽤的认证⽅案,它通过 Authorization 申请音讯头含有服务器⽤于 验证⽤户代理身份的凭证,格局为:

Authorization: Basic <credentials>

如果认证不胜利,服务器返回 401 Unauthorized 状态码以及 WWW-Authenticate 音讯头,让客户端输⼊

⽤户名和明码进⼀步认证。

在 Gin 中,为咱们提供了 gin.BasicAuth 帮咱们⽣成根本认证的中间件,⽅便咱们的开发。

根本认证的中间件能够用在分组路由中,在特定的 url 下进行认证

func main() {r := gin.Default()
   r.Use(gin.BasicAuth(gin.Accounts{"admin": "123456",}))
   

   r.GET("/", func(c *gin.Context) {body, _ := ioutil.ReadAll(c.Request.Body)
      fmt.Println("---body--- \r\n" + string(body))
      fmt.Println("---header--- \r\n")
      for k, v := range c.Request.Header {fmt.Println(k, v)
      }
      fmt.Println("进入主页")
      c.JSON(200, "首页")
   })

   r.Run(":8080")
}

中间件注意事项

gin.Default()

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

gin 框架中间件 c.Next()了解

func main() {router := gin.New()

   mid1 := func(c *gin.Context) {fmt.Println("mid1 start")
      c.Next()
      fmt.Println("mid1 end")
   }
   mid2 := func(c *gin.Context) {fmt.Println("mid2 start")
      c.Next()
      fmt.Println("mid2 end")
   }
   mid3 := func(c *gin.Context) {fmt.Println("mid3 start")
      c.Next()
      fmt.Println("mid3 end")
   }
   router.Use(mid1, mid2)
   router.Use(mid3)
   router.GET("/index", func(c *gin.Context) {fmt.Println("process get request")
      c.JSON(http.StatusOK, "hello")
      fmt.Println("JSON after") //
      // c.Next() // 这里加是没有用})

   router.Run(":8080")
}
  • 失常写 next 是如下打印,相似于递归,洋葱模型

    mid1 start
    mid2 start
    mid3 start
    process get request
    JSON after
    mid3 end
    mid2 end
    mid1 end
  • 如果正文掉 3 个中间件中的 c.Next(),则执⾏状况如下,顺序调用每一个中间件

    mid1 start
    mid1 end
    mid2 start
    mid2 end
    mid3 start
    mid3 end
    process get request
    JSON after
  • 只在 m1 中写入 c.Next()

    mid1 start
    mid2 start
    mid2 end
    mid3 start
    mid3 end
    process get request
    JSON after
    mid1 end

总结:

最初的 get 路由处理函数能够了解为最初的中间件,在不是调⽤ c.Abort()的状况下,所有的中间件 都会被执⾏到。当某个中间件调⽤了 c.Next(), 则整个过程会产⽣嵌套关系 。如果某个中间件 调⽤了 c.Abort(),则此中间件完结后会间接返回,后⾯的中间件均不会调⽤

8 json、struct、xml、yaml、protobuf 渲染

各种数据格式的响应

func main() {r := gin.Default()
    //1. json 响应
    r.GET("/someJSON", func(c *gin.Context) {c.JSON(200, gin.H{"message": "someJSON", "status": 200})
    })
    //2. 构造体响应
    r.GET("/someStruct", func(c *gin.Context) {
        var msg struct {
            Name    string
            Message string
            Number  int
        }
        msg.Name = "root"
        msg.Message = "message"
        msg.Number = 123
        c.JSON(200, msg)
    })

    //3. XML
    r.GET("/someXML", func(c *gin.Context) {c.XML(200, gin.H{"message": "abc"})
    })

    //4. YAML 响应
    r.GET("/someYAML", func(c *gin.Context) {c.YAML(200, gin.H{"name": "you"})
    })

    //5.Protobuf 格局,谷歌开发的高效存储读取的工具
    r.GET("/someProtoBuf", func(c *gin.Context) {reps := []int64{int64(1), int64(2)}
        // 定义数据
        label := "label"
        // 传 protobuf 格局数据
        data := &protoexample.Test{
            Label: &label,
            Reps:  reps,
        }
        c.ProtoBuf(200, data)
    })

    r.Run(":8080")
}

9 HTML 模板渲染

  • gin ⽀持加载 HTML 模板,而后依据模板参数进⾏配置并返回响应的数据,实质上就是字符串替换
  • LoadHTMLGlob()⽅法能够加载模板⽂件

失常渲染 html 模板

func main() {r := gin.Default()
   r.LoadHTMLGlob("view/*")
   r.GET("/index", func(c *gin.Context) {c.HTML(http.StatusOK, "index.html", gin.H{"title": "我是 gin", "name": "you"})
   })
   r.GET("/", func(c *gin.Context) {c.HTML(http.StatusOK, "index.html", gin.H{"title": "我是 gin", "name": "you"})
   })
   r.Run(":8080")
}

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{.title}}</title>
</head>
<body bgcolor="#E6E600">
<h1>{{.title}}</h1>
name : {{.name}}
</body>
</html>

将 html 文件头尾拆散

func main() {r := gin.Default()
   r.LoadHTMLGlob("view2/**/*")
   r.GET("/index", func(c *gin.Context) {c.HTML(http.StatusOK, "user/index.html", gin.H{"title": "我是 gin", "name": "you2"})
   })
   r.Run()}

index.html

{{define "user/index.html"}}
    {{template "public/header" .}}
    name: {{.name}}
    {{template "public/footer" .}}
{{end}}

header.html

{{define "public/header"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{.title}}</title>
</head>
<body>
{{end}}

footer.html

{{define "public/footer"}}
      </body>
      </html>
  {{end}}

url 重定向

拜访 http://127.0.0.1:8080/ 会 主动重定向到 http://127.0.0.1:8080/index

func main() {r := gin.Default()
   r.LoadHTMLGlob("view/*")
   r.GET("/index", func(c *gin.Context) {c.HTML(http.StatusOK, "index.html", gin.H{"title": "我是 gin", "name": "you"})
   })
   r.GET("/", func(c *gin.Context) {c.Redirect(http.StatusMovedPermanently, "/index")  // 重定向
   })
   r.Run(":8080")
}

动态⽂件⽬录

须要引⼊动态⽂件能够定义⼀个动态⽂件⽬录

r.Static("/assets", "./assets")

10 异步协程

  • goroutine 机制能够⽅便地实现异步解决
  • 另外,在启动新的 goroutine 时,不应该使⽤原始高低⽂,必须使⽤它的只读正本。
func main() {r := gin.Default()
   //1. 异步
   r.GET("/long_async", func(c *gin.Context) {
      // 须要搞一个正本
      copyContext := c.Copy()
      // 异步解决
      go func() {time.Sleep(3 * time.Second)
         log.Println("异步执行:" + copyContext.Request.URL.Path)
         // copyContext.JSON(200, gin.H{"message": "someJSON", "status": 200})
      }()})

   //2. 同步
   r.GET("/long_sync", func(c *gin.Context) {time.Sleep(3 * time.Second)
      log.Println("同步执行:" + c.Request.URL.Path)
   })
   r.Run()}

作者:小魔童哪吒

正文完
 0