Gin框架
- Gin简介
- 第一个Gin示例Helloworld
- RESTful API
- Gin返回数据的几种格式
- Gin 获取参数
- HTTP重定向
- Gin路由&路由组
- Gin框架当中的中间件
Gin简介
Gin 是一个用 Go (Golang) 编写的 web 框架。它是一个类似于 martini 但拥有更好性能的 API 框架,由于 httprouter,速度提高了近 40 倍。Gin在GitHub上已经有47k的star,它和Golang的语法一样简洁明了,使得初学者得以迅速入门。只需要在终端上输入以下命令就可以将使用gin框架了
go get -u github.com/gin-gonic/gin
第一个Gin示例Helloworld
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()//默认引擎
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello World")
})
router.Run(":8000")//启动服务
}
下面解释一下这个上面这份代码的意思
- router:=gin.Default():这是默认的服务器。使用gin的Default方法创建一个路由Handler
- 然后通过Http方法绑定路由规则和路由函数。不同于net/http库的路由函数,gin进行了封装,把request和response都封装到了gin.Context的上下文环境中。
- 最后启动路由的Run方法监听端口。还可以用http.ListenAndServe(“:8080”, router),或者自定义Http服务器配置。
启动方式有如下两种:
// 启动方式一
router.Run(":8000")
// 启动方式二
http.ListenAndServe(":8000", router)
注意这个 :8080的意思其实是这个127.0.0.1:8080这一点大家需要注意了。当然我们也可以将这个**“:8080"改为这个"0.0.0.0:8080”**.
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func Index(context *gin.Context) {
context.String(http.StatusOK, "Hello ksy!")
}
func main() {
// 创建一个默认的路由
router := gin.Default()
// 绑定路由规则和路由函数,访问/index的路由,将由对应的函数去处理
router.GET("/index", Index)
// 启动监听,gin会把web服务运行在本机的0.0.0.0:8080端口上
router.Run("0.0.0.0:8080")
// 用原生http服务的方式, router.Run本质就是http.ListenAndServe的进一步封装
http.ListenAndServe(":8080", router)
}
在这里博主在说一下这个Index这个函数的参数是固定写死的就是确定的他的类型必须是这个gin.Context。上面的这个http.StatusOk代表的是这个状态码在这里代表的是这个200*
下面我们将上面这个程序跑起来,然后打开浏览器访问/index这个路径我看一下这个效果
RESTful API
REST与技术无关,代表的是一种软件架构风格,REST是Representational State Transfer的简称,中文翻译为“表征状态转移”或“表现层状态转化”。
简单来说,REST的含义就是客户端与Web服务器之间进行交互的时候,使用HTTP协议中的4个请求方法代表不同的动作。
- GET用来获取资源
- POST用来新建资源
- PUT用来更新资源
- DELETE用来删除资源。
Gin框架支持开发RESTful API的开发。
func main() {
r := gin.Default()
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",
})
})
}
其中这个c.JSON代表的是返回的是一个json格式的数据,而这个gin.H其实是一个这个Map。我们可以查看其定义。
// H is a shortcut for map[string]interface{}
type H map[string]any
当然我们也可以不使用这个gin框架给我提供的使用我们自己定义的。
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
r.GET("/json", func(c *gin.Context) {
//方法1使用map,方法二使用结构体
data := map[string]interface{}{
"name": "小王子",
"message": "ksy",
"age": 18,
}
var msg struct { //注意这个结构体里面的字段必须大写,因为golang当中包的访问性
Name string `json:"name"`
Age int `json:"age"`
Message string `json:"message"`
}
msg.Message = "ni hao ksy"
msg.Age = 20
msg.Name = "ksy"
//获取使用gin.H
c.JSON(http.StatusOK, data)
c.JSON(http.StatusOK, msg)
})
r.Run(":9090")
}
在这里我们需要注意的是这个结构体在进行序列化的时候这个首字母需要大写,赋值会序列化失败这是因为这个golang包的可见性决定的。
那会不会有这样一个场景了,就是某个路径Get,Post等方法都可以访问这个路径时我们又该如何来写了,或者是这个当用户返回这个服务器上的路径不存在时我们不希望返回这个404NotFound,我们希望返回我们这个我们自定义的东西时又该如何写了。gin框架已经提供给了我们这个处理的函数了下面我们一起来看看吧。
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
//可以接受任何请求
r.Any("usr", func(c *gin.Context) {
switch c.Request.Method {//判断到底是那种请求
case http.MethodGet:
c.JSON(http.StatusOK, gin.H{"method": "get"})
case http.MethodPost:
c.JSON(http.StatusOK, gin.H{"method": "post"})
}
})
//定义没有时执行的函数也就是用户访问的路径不存在时会调用这个函数
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"msg": "ksy.com",
})
})
r.Run(":8081")
}
我们可以使用这个Any方法进而在其回调方法当中获取其请求的方试通过一个简单的switch,case进行不同的处理将数据返回。还有就是这个NoRoute方法就是当用户这个输入的路径不存在时会执行这个回调。下面我们来演示一下这个效果。
当这个请求路径不存在时。
Gin返回数据的几种格式
在gin框架当中我们可以指定这个返回数据的格式。在前面的例子当中我们这个String,json类型的数据我们都返回了下面我们一起看看这个剩下的格式吧
1.返回字符串类型的数据
router.GET("/index", func(c *gin.Context) {
c.String(http.StatusOK, "hello world")
})
2.返回json类型数据
c.JSON(http.StatusOK, gin.H{"method": "get"})
3.返回xml数据格式
router.GET("/xml", func(c *gin.Context) {
c.XML(http.StatusOK, gin.H{"user": "hanru", "message": "hey", "status": http.StatusOK})
})
4.返回yaml数据格式
router.GET("/yaml", func(c *gin.Context) {
c.YAML(http.StatusOK, gin.H{"user": "hanru", "message": "hey", "status": http.StatusOK})
})
5.返回html数据格式
先要使用 **LoadHTMLGlob()或者LoadHTMLFiles()**方法来加载模板文件。
router.LoadHTMLGlob("gin框架/templates/*")
//router.LoadHTMLFiles("templates/index.html", "templates/index2.html")
//定义路由
router.GET("/html", func(c *gin.Context) {
//根据完整文件名渲染模板,并传递参数
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "hello world",
})
})
在模板中使用这个title,需要使用{{ .title }}
不同文件夹下模板名字可以相同,此时需要 LoadHTMLGlob() 加载两层模板路径
router.LoadHTMLGlob("templates/**/*")
router.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.html", gin.H{
"title": "Posts",
})
c.HTML(http.StatusOK, "users/index.html", gin.H{
"title": "Users",
})
})
6.返回文件响应
// 在golang总,没有相对文件的路径,它只有相对项目的路径
// 网页请求这个静态目录的前缀, 第二个参数是一个目录,注意,前缀不要重复
router.StaticFS("/static", http.Dir("static/static"))
// 配置单个文件, 网页请求的路由,文件的路径
router.StaticFile("/titian.png", "static/titian.png")
Gin 获取参数
1.查询参数 Query也就是这个获取querystring参数,querystring指的是URL中?后面携带的参数,例如:/user/search?username=匡思源&address=沙河。 获取请求的querystring参数的方法如下:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
//queryString 通常用在Get方法
func main() {
r := gin.Default()
r.GET("/get", func(c *gin.Context) {
//Get请求URL?后面是querystring参数 key=val形势多个用&连接
//获取json那边发请求携带的queryString
//Name := c.Query("name")
//Name:=c.DefaultQuery("name","ddd")//如果能查到用查到的否则用设置的默认值
Name, _ := c.GetQuery("name") //返回值bool取不到返回false
c.JSON(http.StatusOK, gin.H{
"name": Name,
})
})
r.Run(":8089")
}
在这里获取这个这个请求参数querystring,这个gin提供了多种函数:
- GetQuery:如果参数不存在第二个返回值为false
- DefaultQuery:如果参数不存在返回默认设置的值,存在用传进来的值
- Query:获取参数不存在为""
下面我们将这个程序运行起来看一看这个效果如何
剩下的各位铁子可以自行下去演示这个效果,在这里博主就不一一演示这个效果了。
2.获取form参数
当前端请求的数据通过form表单提交时,例如向/user/search发送一个POST请求,获取请求数据的方式如下:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
/*
获取form表单的提交参数
*/
func main() {
r := gin.Default()
r.LoadHTMLFiles("./login.html") //加载某版文件
r.GET("/login", func(c *gin.Context) {
c.HTML(http.StatusOK, "login.html", nil)
})
r.POST("/login", func(c *gin.Context) {
//获取form表单提交的数据
// username:=c.PostForm("username")
// password:=c.PostForm("password")
password := c.DefaultPostForm("password", "****")
username := c.DefaultPostForm("username", "somebody")
//c.GetPostForm("username")两个返回值有一个为这个是否存在
c.JSON(http.StatusOK, gin.H{
"username": username,
"password": password,
})
})
r.Run("127.0.0.1:9091")
}
和这个QueryString是一样的这个gin提供了多种获取函数:
- PostForm:获取用户传过来的表单数据
- DefaultPostForm:如果用户没传使用设置的默认值
- GetPostForm:第二个返回值可以判断用户是否传递了这个参数
下面我们可以使用这个PostMan来进行这个测试
3.获取json数据
当前端请求的数据通过JSON提交时,例如向/json发送一个POST请求,则获取请求参数的方式如下:
package main
import (
"encoding/json"
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
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(":8081")
}
在这里我们同样的使用这个Postman来进行测试
4.获取获取path参数
请求的参数通过URL路径传递,例如:/user/search/小王子/沙河。 获取请求URL路径中的参数的方式如下。
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
//获取请求path(URI)参数返回的都是字符串类型
func main() {
r := gin.Default()
r.GET("/:name/:age", func(c *gin.Context) {
//获取路径参数
name := c.Param("name")
age := c.Param("age")
c.JSON(http.StatusOK, gin.H{
"name": name,
"age": age,
})
})
r.GET("/blog/:year/:month", func(c *gin.Context) {
year := c.Param("year")
month := c.Param("month")
c.JSON(http.StatusOK, gin.H{
"year": year,
"month": month,
})
})
r.Run(":9092")
}
这个在这里就不演示了,非常的简单各位铁子可以自行演示即可。
5.参数绑定(非常重要)
为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryString、form表单、JSON、XML等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSON、form表单和QueryString类型的数据,并把值绑定到指定的结构体对象。
// Binding from JSON
type Login struct {
User string `form:"user" json:"user" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
func main() {
router := gin.Default()
// 绑定JSON的示例 ({"user": "q1mi", "password": "123456"})
router.POST("/loginJSON", func(c *gin.Context) {
var login Login
if err := c.ShouldBind(&login); err == nil {
fmt.Printf("login info:%#v\n", login)
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// 绑定form表单示例 (user=q1mi&password=123456)
router.POST("/loginForm", func(c *gin.Context) {
var login Login
// ShouldBind()会根据请求的Content-Type自行选择绑定器
if err := c.ShouldBind(&login); err == nil {
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// 绑定QueryString示例 (/loginQuery?user=q1mi&password=123456)
router.GET("/loginForm", func(c *gin.Context) {
var login Login
// ShouldBind()会根据请求的Content-Type自行选择绑定器
if err := c.ShouldBind(&login); err == nil {
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}
ShouldBind会按照下面的顺序解析请求中的数据完成绑定:
如果是 GET 请求,只使用 Form 绑定引擎(query)。
如果是 POST 请求,首先检查 content-type 是否为 JSON 或 XML,然后再使用 Form(form-data)功能非常的强大各位铁子需要这个重点看一下这个.
HTTP重定向
HTTP 重定向很容易。 内部、外部重定向均支持。下面我们一起来看看这个如何进行重定向
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
//重定向未登录进行重定向
func main() {
r := gin.Default()
r.GET("/a", func(c *gin.Context) {
//重定向
//c.Redirect(http.StatusMovedPermanently, "https://www.baidu.com") //永久重定向
c.Request.URL.Path = "/b"
r.HandleContext(c) //继续处理
})
//路由重定向
r.GET("/b", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "b",
})
})
r.Run(":9093")
}
注意上面注释那个是永久重定向当用户第二次请求时不会再这个返回它了,而是直接访问重定向的网址。而下面那个时使用这个路由重定向,使用HandleContext。
Gin路由&路由组
我们之前的例子使用的是这个普通路由,就是类似于这个下面这样
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) {...})
之前也已经写过了,还有一个为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html页面。
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusNotFound, "views/404.html", nil)
})
下面我们看看这个路由组吧,有时候这个路由都有这个公共的前缀,但是又有很多个函数此时我们就可以将其抽离出来。下面我们看看这个路由组的写法吧
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(":8900")
}
当然这个路由组也支持这个嵌套
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版本时。
Gin框架当中的中间件
Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
Gin中的中间件必须是一个gin.HandlerFunc类型。 这点非常的重要这个是固定不变的
下面我们就写一个小小的Demo来看看这个中间件的写法
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
//定义一个中间件
func httpfunc(c *gin.Context) {
start := time.Now()
c.Next() //调用后续的处理函数
//c.Abort()//阻止调用后面的函数
cost := time.Since(start)
fmt.Printf("一共花费了%dms", int(cost))
}
func main() {
r := gin.Default()
r.Use(httpfunc) //全局注册中间件函数httpfunc,所有的函数都会只会这个全局的中间件函数
r.GET("/index", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"msg": "index",
})
})
r.Run(":9098")
}
首先这个客户段的请求进来会执行这个httpFunc函数,这个是安装这个顺序决定的,c.Next()代表的是这个继续执行,而如果我们使用这个c.Abort()那么下面这个函数将不会执行也就是说他的意思就是停止的意思。
Demo2
我们有时候可能会想要记录下某些情况下返回给客户端的响应数据,这个时候就可以编写一个中间件来搞定。
type bodyLogWriter struct {
gin.ResponseWriter // 嵌入gin框架ResponseWriter
body *bytes.Buffer // 我们记录用的response
}
// Write 写入响应体数据
func (w bodyLogWriter) Write(b []byte) (int, error) {
w.body.Write(b) // 我们记录一份
return w.ResponseWriter.Write(b) // 真正写入响应
}
// ginBodyLogMiddleware 一个记录返回给客户端响应体的中间件
// https://stackoverflow.com/questions/38501325/how-to-log-response-body-in-gin
func ginBodyLogMiddleware(c *gin.Context) {
blw := &bodyLogWriter{body: bytes.NewBuffer([]byte{}), ResponseWriter: c.Writer}
c.Writer = blw // 使用我们自定义的类型替换默认的
c.Next() // 执行业务逻辑
fmt.Println("Response body: " + blw.body.String()) // 事后按需记录返回的响应
}
当然这个我们还可以为这个路由组定义这个中间件
shopGroup := r.Group("/shop", StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
当然还有另外一种写法
shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
Demo3 中间件传递数据
使用Set设置一个key-value,在后续中间件中使用Get接收数据。对应代码如下
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
type User struct {
Name string
Age int
}
func m10(c *gin.Context) {
fmt.Println("m1 ...in")
c.Set("name", User{"ranran", 21})
c.Next()
fmt.Println("m1 ...out")
}
func main() {
router := gin.Default()
router.Use(m10)
router.GET("/", func(c *gin.Context) {
fmt.Println("index ...in")
name, _ := c.Get("name")
user := name.(User)
fmt.Println(user.Name, user.Age)
c.JSON(200, gin.H{"msg": "index"})
})
router.Run(":8080")
}
value的类型是any类型,所有我们可以用它传任意类型,在接收的时候做好断言即可
中间件的使用还是这个非常好的,当前端发送请求过来之后我们可以定义中间件先检查这个参数的合法性如果不合法我们可以直接Abort停止,如果合法我们可以执行这个配置文件的初始化最后再这个到具体的业务逻辑。
最后说一下这个中间件再使用过程当中的几个注意事项:
1.gin.Default()默认使用了Logger和Recovery中间件,其中:Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。
如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。
2.gin中间件中使用goroutine
当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。因为传递指针会修改上一层的内容导致意想不到的事情发生