Gin

一、Hello World

  • 创建一个默认的路由引擎
  • 配置路由,通过回调函数处理请求
  • 启动web服务
func main() {
	// 创建一个默认的路由引擎
	r := gin.Default()
	// 配置路由
	// 这个路由的作用就是用来接收请求,GET|POST|PUT|DELETE
	// 第一个参数为请求路径,第二个参数为这个请求的回调函数
	r.GET("/hello", test)
	r.GET("/news", test1)
	// 启动一个web服务
	r.Run()
}

func test(c *gin.Context) {
	// 给浏览器或者说给客户端返回对应的响应
	c.String(200, "hello world!")
}
func test1(c *gin.Context) {
	c.String(200, "news!")
}

这里的c.String就是说这个web服务返回给客户端、浏览器、前端的响应,这里是String,那么返回给前端的是字符串,但是在开发中,绝大多数情况采用前后端分离的开发方式,前端需要后端返回的大多数是JSON格式的数据,后端服务接收的前端发过来的参数也大多数是组装好的JSON格式的数据。

如果这里返回响应是JSON格式,那么就是c.JSON

在Java的springmvc中,一般都是返回对象,即加注解@ResponseBody + @Controller或者@RestController。

加了以上注解之后,代表响应的不是视图,而是数据,返回的是数据。

若控制器方法返回值类型是对象,那么这个对象会被转换为JSON格式的字符串,返回给前端,就和这里c.JSON一样了。

为什么返回的对象会被转换成JSON格式的字符串,这就是HttpMessageConverter的作用,这是springMVC的知识,大致就是因为返回的对象,这个对象如何处理?会依次调用HttpMessageConverter接口的实现类的canWrite()方法,找到能够处理对象的canWrite方法,并进而调用write方法。对象默认转换成JSON格式而不是XML格式。

springmvc也可以用一个对象来接收请求参数,这就是说明前后端交互数据是json格式是一样的。在gin这边也是一样的。

二、返回响应类型

web开发主要的两种模式

  1. 前后端不分离:客户端浏览器直接向服务器发送请求,服务器会把整个完整的HTML页面的内容返回给客户端
  2. 前后端分离:前端有自己的框架,向后端发送请求之后,后端服务程序只需要给前端返回JSON格式的响应,前端拿到响应之后自己去做渲染。

拼接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.Run(":8080")
}

结构体

func main() {
	r := gin.Default()
	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")
}

三、接收参数

接收querystring参数

querystring指的是URL中?后面携带的参数,例如:/user/search?username=小王子&address=沙河。 获取请求的querystring参数的方法如下:

func main() {
	//Default返回一个默认的路由引擎
	r := gin.Default()
	r.GET("/user/search", func(c *gin.Context) {
        // DefaultQuery取不到参数值时指定的默认值
		//username := c.DefaultQuery("username", "小王子")
		username := c.Query("username")
		address := c.Query("address")
		//输出json结果给调用方
		c.JSON(http.StatusOK, gin.H{
			"message":  "ok",
			"username": username,
			"address":  address,
		})
	})
	r.Run()
}

接收表单参数

func main() {
	//Default返回一个默认的路由引擎
	r := gin.Default()
	r.POST("/user/search", func(c *gin.Context) {
		// DefaultPostForm取不到值时指定的默认值
		//username := c.DefaultPostForm("username", "小王子")
		username := c.PostForm("username")
		address := c.PostForm("address")
		//输出json结果给调用方
		c.JSON(http.StatusOK, gin.H{
			"message":  "ok",
			"username": username,
			"address":  address,
		})
	})
	r.Run(":8080")
}

获取Path参数

请求的参数通过URL路径传递,获取请求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")
}

参数绑定

结构体定义如下:

type Userinfo struct {
	Username string `form:"username" json:"user"`
	Password string `form:"password" json:"password"`
}

这个结构体定义表明了结构体的字段和表单的什么字段对应,和json的什么字段对应。

这就相当于Java的springmvc中的用一个对象来接收请求参数。

在这里就是用结构体来接收请求参数。

不管是GET请求写在浏览器地址栏的参数还是表单参数,还是前端发过来的JSON字符串(RequestBody),都能绑定到结构体上,也就是都能用一个对象来接收请求参数!!

绑定JSON requestbody

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

四、路由及控制器

  1. 路由要分组,并且要将路由文件抽离。
  2. 除了要将路由文件抽离,控制器方法也要单独写,也就是说在gin框架中,最好是分为四层,即路由层,controller层,service层,dao层。
  3. controller层即控制器方法,这一层接收参数,和springmvc的controller层一样,只不过Java的SSM框架,在controller层,通过@RequestMapping,@GetMapping等注解来控制访问地址,一个路径对应于一个调用方法,没有将路由单独分层。在gin框架中,将路由和控制器方法进行了解耦,路由层专门用来处理路由,做路由分组等,而controller层接收参数,调用业务层(service)逻辑,返回响应。

五、中间件

简介

  1. Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数,这个钩子函数就叫做中间件。

    中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。

    image-20220811114543114

  2. Gin的中间件必须是一个gin.HandlerFunc类型。

    例如我们像下面的代码一样定义一个统计请求耗时的中间件。

    func StatCost() gin.HandlerFunc {
    	return func(c *gin.Context) {
    		start := time.Now()
    		c.Set("name", "xiaowangzi") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
    		// 调用该请求的剩余处理程序
            // c.Next()以后的程序会在路由匹配并处理之后再执行, 所以这里的意思是在c.Next()之前计算一个时间,然后进行业务逻辑处理,处理完成之后,再执行c.Next()之后的逻辑,即计算业务逻辑所花时间
    		c.Next()
    		// 不调用该请求的剩余处理程序
    		// c.Abort()
    		cost := time.Since(start)
    		log.Println(cost)
    	}
    }
    
    // M1 定义一个中间件m1, 可以看到和上面那种写法不一样,但是仍然是一个中间件,因为这个函数的类型就是gin.HandlerFunc
    func M1(c *gin.Context) {
    	fmt.Println("m1 in .....")
    }
    
  3. 配置路由的时候可以传递多个回调函数,

    最后一个func回调函数前面触发的方法都可以称为中间件。

    最后一个回调函数就是这个路由的处理程序。

  4. c.Next()可以调用请求的剩余处理程序

    中间件里加上c.Next()可以让我们在路由匹配完成后执行一些操作。

    c.Next()以后的程序会在匹配路由所对应的业务逻辑处理之后再执行。

    所以中间件也可以理解为匹配路由之前和匹配路由之后执行的一系列操作。

    中间件逻辑----匹配路由以及执行对应的匹配路由逻辑----中间件逻辑。

    这个是有点像springMVC中的拦截器:

    • 在请求处理之前,也就是控制器方法之前会执行拦截器
    • 在控制器方法执行之后,也会执行拦截器
    • 在请求处理完成后也会执行拦截器。
  5. c.Abort()

    执行中间件中,匹配路由之前的和匹配路由之后的逻辑,而不执行匹配路由本身的逻辑。

定义

  1. 中间件定义有两种写法,本质是函数。

    func middleW() gin.HandlerFunc {
    	return func(c *gin.Context) {
    		...
    	}
    }
    //注册一个全局中间件
    router.Use(middleW())
    
    func middleW(c *gin.Context) {
    	...
    }
    //注册一个全局中间件
    router.Use(middleW())
    

注册

  1. 在Gin框架中,我们可以注册全局中间件,也可以给单独路由或者路由组注册中间件,可以为路由添加任意数量的中间件。

  2. 当但存在多个中间件的时候,中间件的处理顺序如下:

    image-20220811141525168

    image-20221102153108540

    image-20221102153246434

  3. 注册全局中间件

    func main() {
    	r := gin.Default()
    	//注册一个全局中间件
    	r.Use(middleW())
    	r.GET("/test", func(c *gin.Context) {
    		fmt.Println("我在方法内部")
    		c.JSON(200, gin.H{
    			"msg": "成功了",
    		})
    	})
    	r.Run(":8080")
    }
    
  4. 单独注册某个路由中间件

    func main() {
    	r := gin.Default()
    	r.GET("/test", middleW(), func(c *gin.Context) {
    		fmt.Println("我在方法内部")
    		c.JSON(200, gin.H{
    			"msg": "成功了",
    		})
    	})
    	r.Run(":8080")
    }
    
  5. 给路由组注册中间件

    //定义一个路由组 并注册中间件
    v1 := r.Group("v1").Use(middleW())
    //或者
    v1 := r.Group("v1", middleW())
    
  6. 三个函数介绍

    • c.Next()

      表示跳过当前中间件剩余内容, 去执行下一个中间件。 当所有操作执行完之后,以出栈的执行顺序返回,执行中间件的剩余代码。

    • return

      终止执行当前中间件剩余内容,以出栈的顺序执行返回,但不执行return后的代码。

      image-20221102154809124

    • c.Abort()

      只执行当前中间件, 操作完成后,以出栈的顺序,依次返回上一级中间件

      即当前中间件会执行完,但是控制器函数不会执行,也就是如果遇到Abort(),那么就会只执行到当前函数为止,然后以出栈的执行顺序返回。把中间件函数也当作控制器函数,如下,把匹配路由的最后一个函数当作控制器函数,控制器函数之前的函数都是中间件,如果某个中间件函数中有c.Abort(),那么就会只执行到那个函数的位置,然后以出栈的顺序即后进先出返回,但是此中间件函数内部的逻辑会执行完!!控制器函数里才是真正调用service层执行业务逻辑的地方。

      Abort就是会把这个函数内部的逻辑执行完,然后以出栈的顺序返回,不再执行下一个中间件了。

      r.GET("/test", middlewOne(), middlewTwo(), middlewThree(), func(c *gin.Context) {
      	fmt.Println("我在方法内部")
      	c.JSON(200, gin.H{
      		"msg": "这里是test1",
      	})
      })
      

      image-20221102154102149

      //定义中间件1
      func middlewOne() gin.HandlerFunc {
      	return func(c *gin.Context) {
      		fmt.Println("我在方法前,我是1")
      		c.Next()
      		fmt.Println("我在方法后,我是1")
      	}
      }
      
      //定义中间件2
      func middlewTwo() gin.HandlerFunc {
      	return func(c *gin.Context) {
      		fmt.Println("我在方法前,我是2")
              // Abort就是会把这个函数内部的逻辑执行完,然后以出栈的顺序返回,要是连fmt.Println("我在方法后,我是2")都不想执行,就把Abort换成return
      		c.Abort()
      		fmt.Println("我在方法后,我是2")
      	}
      }
      
      //定义中间件3
      func middlewThree() gin.HandlerFunc {
      	return func(c *gin.Context) {
      		fmt.Println("我在方法前,我是3")
      		c.Next()
      		fmt.Println("我在方法后,我是3")
      	}
      }
      func main() {
      	r := gin.Default()
      	//使用多个中间件
      	r.GET("/test", middlewOne(), middlewTwo(), middlewThree(), func(c *gin.Context) {
      		fmt.Println("我在方法内部")
      		c.JSON(200, gin.H{
      			"msg": "这里是test1",
      		})
      	})
      	r.Run()
      }
      

      image-20220811142211372

  7. 在用写gin框架写web服务时,中间件要抽离出来单独写,路由层单独写,控制器方法单独写,业务层单独写。

  8. 中间件和控制器可以进行数据的传递,在中间件中可以设值,通过c.Set(),在之后的中间件和控制器中可以通过c.Get()获取值。

应用

  1. 登录认证

    func authMiddleware(c *gin.Context) {
       /*
       if 登录 {
          c.Next()
       } else {
          c.Abort()
       }
        */
    }
    

    虽然上面函数的类型(不是返回类型,这个函数的返回类型为void,就是无返回类型)是gin.HandlerFunc,也就是该函数可以直接当作一个中间件函数,但是还是经常把中间件函数写成如下形式。

    func authMiddleware(doCheck bool) gin.HandlerFunc {
    	// 是因为在这里可以做连接数据库或做一些其他准备工作
    	return func(c *gin.Context) {
    		if doCheck {
    			/*
    				if 登录 {
    					c.Next()
    				} else {
    					c.Abort()
    				}
    			*/
    		} else {
    			c.Next()
    		}
    	}
    }
    

注意

  1. gin.Default()默认使用了Logger和Recovery中间件,其中:

    • Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release

      Logger是把gin框架本身的日志输出到标准输出(我们本地开发调试时在终端输出的那些日志就是它的功劳)

    • Recovery中间件

      是在程序出现panic的时候恢复现场并写入500响应的。

    如果不想使用以上默认的两个中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。

  2. 中间件中使用goroutine

    当在中间件中启动新的goroutine时,不能使用原始的上下文c *gin.Context,必须使用其只读副本c.Copy()

六、Model

概述

  1. 如果应用非常简单的话,可以在Controller里面处理常见的业务逻辑(其实按照目前我的理解来看,Controller层也不是处理全部的业务逻辑,最主要的还是像servlet一样接收了参数之后,做一些简单的处理如参数校验,主要是去调用service层的业务)。但是如果我们有一个功能想在多个控制器复用的话,那么我们就可以把公共的功能单独抽取出来作为一个模块(类似Java的utils工具类??),Model是逐步抽象的过程,一般我们会在Model里面封装一些公共的方法让不同Controller使用,也可以在Model中实现和数据库打交道。
Last Updated:
Contributors: 陈杨