Gin闪亮登场

简介

Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确。
Gin特点主要有: 速度快、性能优;支持中间件操作,方便编码处理;非常简单的实现路由等。

安装gin框架库

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

基架搭建

package main

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

func main() {
	// 创建一个 默认的 路由引擎
	r := gin.Default() 

  // 路由  和  控制器
	r.GET("/hello", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"hello": "world",
		})
	})

	// 服务监听
	err := r.Run("localhost:8080") // 默认 监听并在 0.0.0.0:8080 上启动服务
  
	if err != nil {
		panic(err)
	}
}

路由引擎

路由引擎
engine1 = gin.Default() // 创建默认的 路由引擎
engine2 = gin.New() // 创建一个新的 路由引擎
LoggerRecovery
1. 项目配置

这是一种 通用的 项目配置文件 的手段。

1. 配置文件: config/app.json

{
  "app_name": "cloudrestaurant",
  "app_mode": "debug",
  "app_host": "localhost",
  "app_port": "8090",
  "database": {
    "dbsize": "mysql",
    "username": "root",
    "password": "123456",
    "host": "localhost",
    "port": "3679",
    "dbname": "gin",
    "charset": "utf8mb4"
  },
  "redis_config": {
    "addr": "127.0.0.1",
    "port": "6379",
    "password": "",
    "db": 0
  }
}

2. 解析文件: app/app.go

package app

import (
	"bufio"
	"encoding/json"
	"os"
)

type Config struct {
	AppName     string      `json:"app_name"`
	AppMode     string      `json:"app_mode"`
	AppHost     string      `json:"app_host"`
	AppPort     string      `json:"app_port"`
	Database    Database    `json:"database"`
	RedisConfig RedisConfig `json:"redis_config"`
}

type Database struct {
	DBsize   string `json:"dbsize"`
	Username string `json:"username"`
	Password string `json:"password"`
	Host     string `json:"host"`
	Port     string `json:"port"`
	DBname   string `json:"dbname"`
	Charset  string `json:"charset"`
}

type RedisConfig struct {
	Addr     string `json:"addr"`
	Port     string `json:"port"`
	Password string `json:"password"`
	Db       int    `json:"db"`
}

var AppConfig *Config = nil

func ParseConfig(path string) {
	file, err := os.Open(path)
	if err != nil {
		panic(err)
	}
	defer file.Close()

	reader := bufio.NewReader(file)
	decoder := json.NewDecoder(reader)
	err = decoder.Decode(&AppConfig)

	if err != nil {
		panic(err)
	}
}

func init() {
	ParseConfig("./config/app.json")
}
强调:
相对路径main包的角度

3. 激活使用

package main

import (
	// 在main文件引入,即 激活配置
	"项目名称/app"
	"fmt"
)

func main() {
	// 使用配置中的数据
	fmt.Println(app.AppConfig.AppName)
	fmt.Println(app.AppConfig.Database.DBname)
}
2. 控制器

外观描述

上下文

上下文(gin.Context) 上 的 api

  • Next()

将处理权 移交到 下一棒 中间件(或控制器) 手中

  • Abort()

终断该请求的处理

  • Set(“name”, “tom”)、Get(“name”)

在上下文中 写入数据,可以在 下一棒 中间件(或控制器) 手中 通过 Get()获取到对应数据

  • 重定向

Redirect(302, “https://www.baidu.com”)

  • 请求报文: Request
  • c.Request.Proto => 请求协议
  • c.Request.Host => 请求协议
  • c.Request.URL => 请求路径
  • c.Request.Method => 请求方法
  • c.Request.Header[“Content-Type”] => 请求头相关

控制器 的实现

package controller

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

// 将 控制器绑在 结构体上 是 模块化 思想的体现
// 很好的 避免了 不同路由 的 控制器 重名的问题。
type Testing struct {
}

func (that *Testing) HandGetTest(context *gin.Context) {
	...
}

func (that *Testing) HandPostTest(context *gin.Context) {
	...
}
3. 路由

单路由

路由引擎
package router

import (
	"testGin/controller"

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

// 路由表
func ActiveRouterList(e *gin.Engine) {
	e.Handle("GET", "/test", new(controller.Testing).HandGetTest)

  // "语法糖"
	e.GET("/test", new(controller.Testing).HandGetTest)
	e.POST("/test", new(controller.Testing).HandPostTest)

}
  • 在 main函数 中 激活?路由表
package main

import (
	"testGin/router"

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

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

	// 激活?路由表 
	router.ActiveRouterList(app)

	app.Run()
}

路由组

使用 路由引擎 的 Group 方法

// 激活 路由表
func ActiveRouterList(e *gin.Engine) {

	group := e.Group("/aaa")
	{ // 加这个 块级括号 ,是为了 彰显 路由组 层次感
		group.GET("/b1", new(controller.Testing).HandGetTest)
		group.GET("/b2", new(controller.Testing).HandGetTest)

		group2 := group.Group("b3")
		{
			group2.GET("/c1", new(controller.Testing).HandGetTest)
			group2.GET("/c2", new(controller.Testing).HandGetTest)
		}
	}

	// 路由组访问地址: /aaa/b1   和  /aaa/b3/c1 
}

其他路由

404路由

e.NoRoute(控制器)

广角路由

// 可以匹配 任何请求方法
e.Any("test", 控制器)

静态路由

上面的路由都是动态路由,下面介绍一下 静态路由。
静态路由,为静态文件 指定 的访问路径。

static文件夹images文件夹aaa.png
// 路由表
func ActiveRouterList(e *gin.Engine) {

	e.Static("/img", "./static/images/")
	// 图片的访问路径: /img/aaa.png
}
4. 中间件

gin框架允许开发者在 处理请求 的过程中,加入用户自己的 钩子函数。这个 钩子函数 就是 中间件。
中间件适合处理一些 公共的 业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
Gin 的中间件 是 洋葱模型的 运行流程。

定义一个中间件

// 设计一个 统计 请求耗时 的中间件(默认路由引擎 中已经实现,这里重点定义中间件)
package midlleware

import (
	"fmt"
	"time"

	"github.com/gin-gonic/gin"
)
// 中间件 的 本质上 就是 控制器 生产函数
// 所以 返回值 是一个 控制器
func TimeCost() (gin.HandlerFunc) {
	return func(c *gin.Context) {
		start := time.Now()
		c.Next() // 将控制权 主动交给 下一棒
    cost := time.Since(start)
		fmt.Println(cost)
	}
}

注册 使用 中间件

单路由 注册

// 在路由表中,找到 对应的路由 加入即可
e.GET("/test", midlleware.TimeCost(), new(controller.Testing).HandGetTest)
// 注意,要以调用的方式 放入 中间件

全局 注册

// 在路由表中
e.Use(midlleware.TimeCost())

路由组 注册

// 方式一, 单路由注册 的方式
group := e.Group("aaa", midlleware.TimeCost())
{
	...
}

// 方式二, 全局 注册 的方式
group := e.Group("aaa")
group.Use(midlleware.TimeCost())
{
	...
}

实用中间件: 请求追踪 中间件

package main

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net/http"
	"time"

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

// 两种启动模式
func main() {
	addr := "0.0.0.0:8888"
	// app := gin.Default()
	app := gin.New()
	app.Use(RequestLog)
	app.GET("/ping", func(ctx *gin.Context) {
		ctx.JSON(200, gin.H{
			"hello": "world",
		})
	})
	app.POST("/ping", func(ctx *gin.Context) {
		ctx.JSON(200, gin.H{
			"hello": "world",
		})
	})
	fmt.Println("服务监听:" + addr)

	err := app.Run(addr)
	if err != nil {
		panic(err)
	}
}

/*
gin的 请求日志 de 中间件,
记录请求信息: 请求数据、响应数据、请求路径、请求方法、客户端ip、请求发生时间、处理时长、请求头
*/
func RequestLog(c *gin.Context) {
	// 记录请求开始时间
	t := time.Now()
	blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
	// 必须!
	c.Writer = blw

	// 获取请求信息
	requestBody := getRequestBody(c)

	c.Next()

	// 记录请求所用时间
	latency := time.Since(t)

	// 获取响应内容
	responseBody := blw.body.String()

	logContext := make(map[string]interface{})
	// 日志 输出 项目:
	logContext["request_uri"] = c.Request.RequestURI       // 请求路径
	logContext["request_method"] = c.Request.Method        // 请求方法
	logContext["refer_service_name"] = c.Request.Referer() //
	logContext["refer_request_host"] = c.ClientIP()        // 客户端ip
	logContext["request_body"] = requestBody               // 请求数据
	logContext["request_time"] = t.String()                // 请求发生时间
	logContext["response_body"] = responseBody             // 响应数据
	logContext["time_used"] = fmt.Sprintf("%v", latency)   // 处理时长
	logContext["header"] = c.Request.Header                // 请求头

	fmt.Println(logContext)
}

// bodyLogWriter 定义一个存储响应内容的结构体
type bodyLogWriter struct {
	gin.ResponseWriter
	body *bytes.Buffer
}

// Write 读取响应数据
func (w bodyLogWriter) Write(b []byte) (int, error) {
	w.body.Write(b)
	return w.ResponseWriter.Write(b)
}

// getRequestBody 获取请求参数
func getRequestBody(c *gin.Context) interface{} {
	switch c.Request.Method {
	case http.MethodGet:
		return c.Request.URL.Query()

	case http.MethodPost:
		fallthrough
	case http.MethodPut:
		fallthrough
	case http.MethodPatch:
		var bodyBytes []byte // 我们需要的body内容
		// 可以用buffer代替ioutil.ReadAll提高性能
		bodyBytes, err := ioutil.ReadAll(c.Request.Body)
		if err != nil {
			return nil
		}
		// 将数据还回去
		c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

		return string(bodyBytes)
	}

	return nil
}

5. 前后交互

接收数据

api直取

获取get 请求的 Param参数(url参数)

e.GET("/test/:name", func(c *gin.Context){
	name := c.Param("name")
})

获取get 的 请求参数

e.GET("/test", func(c *gin.Context){
	name := c.Query("name")
})
name := c. DefaulQuery("name", " 默认值 ")
表单的形式
engine.Handle("POST", "/login", func(c *gin.Context) {
	username := c.PostForm("username")

	password := c.PostForm("pwd")
})
  • 说明:
name := c. DefaultPostForm ("name ", "默认值")

对应绑定

api直取 的 方式 获取数据 有两方面的 缺点:

json

获取 GET 的 请求参数 (ShouldBindQuery)

/aaa?name=tom&age=10
通过 ShouldBindQuery()方法实现

package router

import (
	"fmt"

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

//form之后的名 和 提交 数据 的键名 对应不起来,
//则无法做到对应赋值
type man struct {
	Name string `form:"name"` 
	Age  int    `form:"age"`
}

// 路由表
func ActiveRouterList(e *gin.Engine) {
	e.GET("/aaa", func(c *gin.Context) {
		var peop man

		err := c.ShouldBindQuery(&peop)

		if err != nil {
			fmt.Println(err)
		}
		fmt.Printf("%T >>---> %+v\n", peop, peop)
		//router.man >>---> {Name:tom Age:10}
		c.JSON(200, gin.H{"hi": "hi"})
	})
}
json形式
ShouldBindJSON()
表单的形式
ShouldBind()

获取上传文件

单文件

e.POST("/aaa", func(c *gin.Context) {
	file, err := c.FormFile("avatar")

	if err != nil {
		fmt.Println(err)
	}

	// SaveUploadedFile 不会创建文件夹,
	// 所以 确保 filePath 上的文件夹都是已经存在的。
	// 这里最好 用一下uuid
	var filePath = "./uploadfile/" + file.Filename
	err = ctx.SaveUploadedFile(file, filePath)
}

多文件

e.POST("/aaa", func(c *gin.Context) {

	form, _ := c.MultipartForm()
	files := form.File["file"]

	for index, file := range files {

	  var filePath = "./uploadfile/" + file.Filename
		// 上传文件到指定的目录
		c.SaveUploadedFile(file, filePath)
	}
}

流式上传文件

一个 HTML 表单中的 enctype 有三种类型

  • application/x-www-urlencoded
  • multipart/form-data
  • text-plain

默认情况下是 application/x-www-urlencoded,当表单使用 POST 请求时,数据会被以 x-www-urlencoded 方式编码到 Body 中来传送.
multipart/x-www-form-urlencoded:被发送到服务端的http消息的body在本质上是一个巨大的查询字符串:name/value对被&符号分开,name和value被=符号分开,例如:
MyVariableOne=ValueOne&MyVariableTwo=ValueTwo
非字母和数字的字符会被%HH来代替,一个百分比符号和两个16位进制的数字代表着那个字符的ASCII码
这意味着每一个在value中的非字母和数字的字节,都将被3个字节的数据代替。如果是一个大的二进制的文件,那么3倍的传输数据将会变得十分低效率。
这时multipart/form-data就该出现了。

multipart/form-data
  • main.go
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"os"
	"strconv"
	"strings"

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

/// 解析多个文件上传中,每个具体的文件的信息
type FileHeader struct {
	ContentDisposition string
	Name               string
	FileName           string ///< 文件名
	ContentType        string
	ContentLength      int64
}

/// 解析描述文件信息的头部
/// @return FileHeader 文件名等信息的结构体