简介
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 文件名等信息的结构体