刘沙河 刘沙河
首页
  • Go语言基础

    • 数据类型
    • 反射
    • Go指针
  • Go语言进阶

    • go泛型
    • go条件编译
    • cgo教程
    • Go协程调度原理及GPM模型
    • Go内存管理
    • Go垃圾回收机制
    • Go语言内存对齐
  • Go语言实现原理

    • channel 实现原理
    • slice 实现原理
    • map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁CAS 实现原理
    • singlefight 实现原理
  • gin框架

    • gin中间件原理
    • gin路由原理
  • gorm

    • GORM介绍和使用
    • GORM_CURD操作指南
  • go测试

    • benchmark基准测试
    • pprof 性能分析
  • python进阶

    • Numpy&Pandas
    • celery分布式任务队列
  • Django

    • Django 常见命令
    • middleware中间件
    • Django缓存系统
    • Django信号系统
    • Django REST Framework
  • Flask

    • Flask基础知识总结
    • Flask-SQLAlchemy
  • 爬虫

    • aiohttp
    • scrapy框架
  • Mysql

    • Mysql存储引擎和索引
    • MySQL主从复制
    • Mysql读写分离
    • 数据库分库分表
    • Mysql锁
    • Mysql事务和MVCC原理
    • 分库分表带来的读扩散问题
  • Redis

    • redis基础和数据类型
    • redis主从架构
    • redis哨兵架构
    • redis集群模式
    • 如何保证缓存和数据库双写一致
    • redis底层数据结构
    • redis分布式锁
  • Elasticsearch

    • es基本概念
    • es基础语法
    • es倒排索引
  • etcd

    • Go操作etcd
    • Raft原理
    • etcd分布式锁
  • kafka

    • 消息队列MQ总结
    • kafka 概述及原理
    • kafka 消费问题记录
    • 零拷贝技术
    • kafka分区规范
  • RabbitMQ

    • rabbitMQ基础
    • Go操作rabbitmq
  • RocketMQ

    • 可靠消息队列 rocketMQ
  • Http&Https

    • http&https
    • TCP和UDP
    • Ping 原理
  • RPC

    • RPC初识
    • grpc初识和实现
  • gRPC

    • grpc 初识
    • grpc 上下文 metadata
    • grpc 健康检查
    • grpc keepalive
    • grpc 命名解析
    • grpc 中间件&拦截器
    • grpc 负载均衡
    • grpc 身份认证
    • grpc 超时重试
    • grpc 链路追踪
    • grpc-gw将gRPC转RESTfu api
    • grpc-gw自定义选项
  • protobuf

    • protobuf 进阶
    • protobuf 编码原理
  • Docker

    • Docker基础
    • Docker常用命令
    • Dockerfile
    • Docker-Compose
    • Docker多阶段构建
    • Docker Config 教程
    • Docker Swarm 教程
    • Docker Stack 教程
    • Docker Buildx 教程
  • k8s

    • k8s 基础概念
    • k8s 集群架构
    • k8s 工作负载
    • Pod 网络
    • Service 网络
    • 外部接入网络
    • 一张图搞懂k8s各种pod
    • k8s 存储抽象
    • mac快速启动k8s
    • 自制申威架构k8s-reloader
  • go-kit

    • go-kit初识
    • go-kit启动http服务
    • go-kit集成gin启动服务
    • go-kit集成grpc和protobuf
    • go-kit中间件
    • go-kit服务注册发现与负载均衡
    • go-kit限流和熔断
    • go-kit链路追踪
    • go-kit集成Prometheus
  • 设计模式

    • 初识设计模式
    • 创建型模式
    • 结构型模式
    • 行为模式
  • 数据结构

    • 时间轮
    • 堆、双向链表、环形队列
    • 队列:优先队列
    • 队列:延迟队列
  • 算法

    • 递归算法
    • 枚举算法
    • 动态规划
    • 回溯算法
    • 分治算法
    • 贪心算法
    • LRU和LFU
    • 一致性哈希

花开半夏,半夏花开
首页
  • Go语言基础

    • 数据类型
    • 反射
    • Go指针
  • Go语言进阶

    • go泛型
    • go条件编译
    • cgo教程
    • Go协程调度原理及GPM模型
    • Go内存管理
    • Go垃圾回收机制
    • Go语言内存对齐
  • Go语言实现原理

    • channel 实现原理
    • slice 实现原理
    • map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁CAS 实现原理
    • singlefight 实现原理
  • gin框架

    • gin中间件原理
    • gin路由原理
  • gorm

    • GORM介绍和使用
    • GORM_CURD操作指南
  • go测试

    • benchmark基准测试
    • pprof 性能分析
  • python进阶

    • Numpy&Pandas
    • celery分布式任务队列
  • Django

    • Django 常见命令
    • middleware中间件
    • Django缓存系统
    • Django信号系统
    • Django REST Framework
  • Flask

    • Flask基础知识总结
    • Flask-SQLAlchemy
  • 爬虫

    • aiohttp
    • scrapy框架
  • Mysql

    • Mysql存储引擎和索引
    • MySQL主从复制
    • Mysql读写分离
    • 数据库分库分表
    • Mysql锁
    • Mysql事务和MVCC原理
    • 分库分表带来的读扩散问题
  • Redis

    • redis基础和数据类型
    • redis主从架构
    • redis哨兵架构
    • redis集群模式
    • 如何保证缓存和数据库双写一致
    • redis底层数据结构
    • redis分布式锁
  • Elasticsearch

    • es基本概念
    • es基础语法
    • es倒排索引
  • etcd

    • Go操作etcd
    • Raft原理
    • etcd分布式锁
  • kafka

    • 消息队列MQ总结
    • kafka 概述及原理
    • kafka 消费问题记录
    • 零拷贝技术
    • kafka分区规范
  • RabbitMQ

    • rabbitMQ基础
    • Go操作rabbitmq
  • RocketMQ

    • 可靠消息队列 rocketMQ
  • Http&Https

    • http&https
    • TCP和UDP
    • Ping 原理
  • RPC

    • RPC初识
    • grpc初识和实现
  • gRPC

    • grpc 初识
    • grpc 上下文 metadata
    • grpc 健康检查
    • grpc keepalive
    • grpc 命名解析
    • grpc 中间件&拦截器
    • grpc 负载均衡
    • grpc 身份认证
    • grpc 超时重试
    • grpc 链路追踪
    • grpc-gw将gRPC转RESTfu api
    • grpc-gw自定义选项
  • protobuf

    • protobuf 进阶
    • protobuf 编码原理
  • Docker

    • Docker基础
    • Docker常用命令
    • Dockerfile
    • Docker-Compose
    • Docker多阶段构建
    • Docker Config 教程
    • Docker Swarm 教程
    • Docker Stack 教程
    • Docker Buildx 教程
  • k8s

    • k8s 基础概念
    • k8s 集群架构
    • k8s 工作负载
    • Pod 网络
    • Service 网络
    • 外部接入网络
    • 一张图搞懂k8s各种pod
    • k8s 存储抽象
    • mac快速启动k8s
    • 自制申威架构k8s-reloader
  • go-kit

    • go-kit初识
    • go-kit启动http服务
    • go-kit集成gin启动服务
    • go-kit集成grpc和protobuf
    • go-kit中间件
    • go-kit服务注册发现与负载均衡
    • go-kit限流和熔断
    • go-kit链路追踪
    • go-kit集成Prometheus
  • 设计模式

    • 初识设计模式
    • 创建型模式
    • 结构型模式
    • 行为模式
  • 数据结构

    • 时间轮
    • 堆、双向链表、环形队列
    • 队列:优先队列
    • 队列:延迟队列
  • 算法

    • 递归算法
    • 枚举算法
    • 动态规划
    • 回溯算法
    • 分治算法
    • 贪心算法
    • LRU和LFU
    • 一致性哈希
  • go语言基础

  • go语言进阶

  • go语言实现原理

  • gin框架

    • gin框架
      • gin进阶
      • gin中使用jwt
      • go+redis
      • gin-swagger生成API文档
      • gin中间件原理
      • gin路由原理
    • gorm

    • go测试

    • Go语言
    • gin框架
    bigox
    2022-06-26
    目录

    gin框架

    # gin框架

    • gin是一个路由框架, 是对httpRoute的简单封装;
    • 主打轻量, 易学, 高性能

    # 创建gin项目

    使用go mod管理项目包

    • 开启go module

      go env -w GO111MODULE=on     // Windows 
       
      export GO111MODULE=on        // macOS 或 Linux
      
      1
      2
      3
    • 设置goproxy

      go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/       // Windows 
       
      export GOPROXY=https://mirrors.aliyun.com/goproxy/          // macOS 或 Linux
      
      1
      2
      3
    • 下载gin框架

      go get -u github.com/gin-gonic/gin //会下载到gopath的pkg文件夹下
      
      1
    • 创建项目文件夹,并初始化mod文件

      mkdir proDir
      cd proDir
      go mod init proDir
      touch main.go
      
      1
      2
      3
      4
    • 加载gin

      go run main.go
      
      1

    # 启动HTTP服务

    • main.go

      package main
      
      import (
      	"github.com/gin-gonic/gin"
      )
      
      func main() {
        //gin.DefaultWriter = os.Stdin  // 不再控制台输出日志
      	router:= gin.Default()
      	router.GET("/ping", func(context *gin.Context) {
      		context.JSON(200,gin.H{
      			"msg":"pong",
      		})
      	})
      	_ = router.Run("0.0.0.0:8080")
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16

    # 静态文件

    • gin对静态文件的支撑有很多种,这里只介绍两种

      • 针对文件夹 StaticFS(relativePath, fs)
        • 传入url的相对路径和系统文件夹路径(通过http.Dir获取)
      • 针对文件 StatiFS(relativePath, filepath)
        • 传入url的相对路径和系统文件的路径(直接传入相对路径的字符串)
      package main
      
      import (
      	"github.com/gin-gonic/gin"
      	"net/http"
      )
      
      func main() {
      	router:= gin.Default()
      	// 静态资源加载(以css,js,img为例)
      	router.StaticFS("/static",http.Dir("static"))
      	router.StaticFile("/favicon.ico","./static/favicon.ico")
      	_ = router.Run("0.0.0.0:8080")
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14

    # Gin路由

    # 1. 普通路由

    r.GET("/index", func(c *gin.Context) {...})
    r.GET("/login", func(c *gin.Context) {...})
    r.POST("/login", func(c *gin.Context) {...})
    
    1
    2
    3

    此外,还有一个可以匹配所有请求方法的Any方法如下:

    r.Any("/test", func(c *gin.Context) {...})
    
    1

    为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html页面。

    r.NoRoute(func(c *gin.Context) {
    		c.HTML(http.StatusNotFound, "views/404.html", nil)
    	})
    
    1
    2
    3

    # 2. 路由组

    • 可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}包裹同组的路由,这只是为了看着清晰,你用不用{}包裹功能上没什么区别。
    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()
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    • 路由组也是支持嵌套的,例如:
    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) {...})
    	}
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    通常我们将路由分组用在划分业务逻辑或划分API版本时。

    # 3. 路由原理

    Gin框架中的路由使用的是httprouter (opens new window)这个库。

    其基本原理就是构造一个路由地址的前缀树。

    # Gin渲染

    # 1. HTML渲染

    • 首先定义一个存放模板文件的templates文件夹,然后在其内部按照业务分别定义一个posts文件夹和一个users文件夹。 posts/index.html文件的内容如下:
    {{define "posts/index.html"}}
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>posts/index</title>
    </head>
    <body>
        {{.title}}
    </body>
    </html>
    {{end}}
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    • users/index.html文件的内容如下:
    {{define "users/index.html"}}
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>users/index</title>
    </head>
    <body>
        {{.title}}
    </body>
    </html>
    {{end}}
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    Gin框架中使用LoadHTMLGlob()或者LoadHTMLFiles()方法进行HTML模板渲染。

    func main() {
    	r := gin.Default()
    	r.LoadHTMLGlob("templates/**/*")
    	//r.LoadHTMLFiles("templates/posts/index.html", "templates/users/index.html")
    	r.GET("/posts/index", func(c *gin.Context) {
    		c.HTML(http.StatusOK, "posts/index.html", gin.H{
    			"title": "posts/index",
    		})
    	})
    
    	r.GET("users/index", func(c *gin.Context) {
    		c.HTML(http.StatusOK, "users/index.html", gin.H{
    			"title": "users/index",
    		})
    	})
    
    	r.Run(":8080")
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    # 2. 自定义分隔符

    • 可以使用自定义分隔
    	r := gin.Default()
    	r.Delims("{[{", "}]}")
    	r.LoadHTMLGlob("/path/to/templates")
    
    1
    2
    3

    # 3. 自定义模板函数

    • 自定义函数一定要在loadHtml之前!!

    • 定义一个不转义相应内容的safe模板函数如下:

    func main() {
    	router := gin.Default()
    	router.SetFuncMap(template.FuncMap{
    		"safe": func(str string) template.HTML{
    			return template.HTML(str)
    		},
    	})
    	router.LoadHTMLFiles("./index.tmpl")
    
    	router.GET("/index", func(c *gin.Context) {
    		c.HTML(http.StatusOK, "index.tmpl", "<a href='https://bigox.wiki'>刘沙河的博客</a>")
    	})
    
    	router.Run(":8080")
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    • 在index.tmpl中使用定义好的safe模板函数:
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <title>修改模板引擎的标识符</title>
    </head>
    <body>
    <div>{{ . | safe }}</div>
    </body>
    </html>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    • 为了让index.tmpl文件有语法显示,我们还需要配置一下

    image-20200913213156919

    然后我们加入 *.tmpl,保存即可

    # 4. 静态文件处理

    • 当我们渲染的HTML文件中引用了静态文件时,我们只需要按照以下方式在渲染页面前调用gin.Static方法即可。
    func main() {
    	r := gin.Default()
    	r.Static("/static", "./static")
    	r.LoadHTMLGlob("templates/**/*")
       // ...
    	r.Run(":8080")
    }
    
    1
    2
    3
    4
    5
    6
    7

    # 5. 使用模板继承

    • Gin框架默认都是使用单模板,如果需要使用block template功能,可以通过"github.com/gin-contrib/multitemplate"库实现,具体示例如下:

    • 首先,假设我们项目目录下的templates文件夹下有以下模板文件,其中home.tmpl和index.tmpl继承了base.tmpl:

    templates
    ├── includes
    │   ├── home.tmpl
    │   └── index.tmpl
    ├── layouts
    │   └── base.tmpl
    └── scripts.tmpl
    
    1
    2
    3
    4
    5
    6
    7
    • 然后我们定义一个loadTemplates函数如下:
    func loadTemplates(templatesDir string) multitemplate.Renderer {
    	r := multitemplate.NewRenderer()
    	layouts, err := filepath.Glob(templatesDir + "/layouts/*.tmpl")
    	if err != nil {
    		panic(err.Error())
    	}
    	includes, err := filepath.Glob(templatesDir + "/includes/*.tmpl")
    	if err != nil {
    		panic(err.Error())
    	}
    	// 为layouts/和includes/目录生成 templates map
    	for _, include := range includes {
    		layoutCopy := make([]string, len(layouts))
    		copy(layoutCopy, layouts)
    		files := append(layoutCopy, include)
    		r.AddFromFiles(filepath.Base(include), files...)
    	}
    	return r
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    • 我们在main函数中
    func indexFunc(c *gin.Context){
    	c.HTML(http.StatusOK, "index.tmpl", nil)
    }
    
    func homeFunc(c *gin.Context){
    	c.HTML(http.StatusOK, "home.tmpl", nil)
    }
    
    func main(){
    	r := gin.Default()
    	r.HTMLRender = loadTemplates("./templates")
    	r.GET("/index", indexFunc)
    	r.GET("/home", homeFunc)
    	r.Run()
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    # 6. 补充文件路径处理

    • 关于模板文件和静态文件的路径,我们需要根据公司/项目的要求进行设置。可以使用下面的函数获取当前执行程序的路径。
    func getCurrentPath() string {
    	if ex, err := os.Executable(); err == nil {
    		return filepath.Dir(ex)
    	}
    	return "./"
    }
    
    1
    2
    3
    4
    5
    6

    # 7. 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.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")
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    # 8. XML渲染

    注意需要使用具名的结构体类型。

    func main() {
    	r := gin.Default()
    	// gin.H 是map[string]interface{}的缩写
    	r.GET("/someXML", func(c *gin.Context) {
    		// 方式一:自己拼接JSON
    		c.XML(http.StatusOK, gin.H{"message": "Hello world!"})
    	})
    	r.GET("/moreXML", func(c *gin.Context) {
    		// 方法二:使用结构体
    		type MessageRecord struct {
    			Name    string
    			Message string
    			Age     int
    		}
    		var msg MessageRecord
    		msg.Name = "小王子"
    		msg.Message = "Hello world!"
    		msg.Age = 18
    		c.XML(http.StatusOK, msg)
    	})
    	r.Run(":8080")
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    # 9. YMAL渲染

    r.GET("/someYAML", func(c *gin.Context) {
    	c.YAML(http.StatusOK, gin.H{"message": "ok", "status": http.StatusOK})
    })
    
    1
    2
    3

    # 10. protobuf渲染

    r.GET("/someProtoBuf", func(c *gin.Context) {
    	reps := []int64{int64(1), int64(2)}
    	label := "test"
    	// protobuf 的具体定义写在 testdata/protoexample 文件中。
    	data := &protoexample.Test{
    		Label: &label,
    		Reps:  reps,
    	}
    	// 请注意,数据在响应中变为二进制数据
    	// 将输出被 protoexample.Test protobuf 序列化了的数据
    	c.ProtoBuf(http.StatusOK, data)
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    # 获取参数

    # 1. 获取querystring参数

    • querystring指的是URL中?后面携带的参数,例如:/user/search?username=小王子&address=沙河。 获取请求的querystring参数的方法如下:
    func main() {
    	//Default返回一个默认的路由引擎
    	r := gin.Default()
    	r.GET("/user/search", func(c *gin.Context) {
        // 可以添加默认值
    		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()
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    • 我们输入对应的URL,就能获取到对应的参数了
    http://localhost:9090/web?username=小王子&address=沙河
    
    1

    # 2. 获取form参数

    • 请求的数据通过form表单来提交,例如向/user/search发送一个POST请求,获取请求数据的方式如下:
    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")
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    # 3. 获取path参数

    # *必选参数

    使用 :param 的语法完成必选参数,例如 /user/:ID,即可匹配 /user/42 这个 URI,但不能匹配 /user 或 /user/ 这个 URI,例如:

    # :可选参数

    使用 *param 的语法完成可选参数的定义,例如 /user/*ID 可以匹配 /user/42 和 /user或 /user 的 URI,例如:

    router.GET("/user/:ID", func(c *gin.Context) {
       ID := c.Param("ID")
    })
    
    1
    2
    3

    若没有匹配到,则 ID 为空字符串。

    # 获取参数

    使用 gin.Context 对象的 c.Param("param") 来获取参数值,参见上面的示例。

    • 请求的参数通过URL路径传递,例如:/user/search/小王子/沙河。 获取请求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")
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    # 4. 参数绑定

    • 为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的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")
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55

    ShouldBind会按照下面的顺序解析请求中的数据完成绑定:

    1. 如果是 GET 请求,只使用 Form 绑定引擎(query)。
    2. 如果是 POST 请求,首先检查 content-type 是否为 JSON 或 XML,然后再使用 Form(form-data)。

    # 文件上传

    # 1. 单个文件上传

    • 文件上传前端页面代码:
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <title>上传文件示例</title>
    </head>
    <body>
    <form action="/upload" method="post" enctype="multipart/form-data">
        <input type="file" name="f1">
        <input type="submit" value="上传">
    </form>
    </body>
    </html>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    • 后端gin框架部分代码:
    func main() {
    	router := gin.Default()
    	// 处理multipart forms提交文件时默认的内存限制是32 MiB
    	// 可以通过下面的方式修改
    	// router.MaxMultipartMemory = 8 << 20  // 8 MiB
    	router.POST("/upload", func(c *gin.Context) {
    		// 单个文件
    		file, err := c.FormFile("f1")
    		if err != nil {
    			c.JSON(http.StatusInternalServerError, gin.H{
    				"message": err.Error(),
    			})
    			return
    		}
    
    		log.Println(file.Filename)
    		dst := fmt.Sprintf("C:/tmp/%s", file.Filename)
    		// 上传文件到指定的目录
    		c.SaveUploadedFile(file, dst)
    		c.JSON(http.StatusOK, gin.H{
    			"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
    		})
    	})
    	router.Run()
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    # 2. 多个文件上传

    func main() {
    	router := gin.Default()
    	// 处理multipart forms提交文件时默认的内存限制是32 MiB
    	// 可以通过下面的方式修改
    	// router.MaxMultipartMemory = 8 << 20  // 8 MiB
    	router.POST("/upload", func(c *gin.Context) {
    		// Multipart form
    		form, _ := c.MultipartForm()
    		files := form.File["file"]
    
    		for index, file := range files {
    			log.Println(file.Filename)
    			dst := fmt.Sprintf("C:/tmp/%s_%d", file.Filename, index)
    			// 上传文件到指定的目录
    			c.SaveUploadedFile(file, dst)
    		}
    		c.JSON(http.StatusOK, gin.H{
    			"message": fmt.Sprintf("%d files uploaded!", len(files)),
    		})
    	})
    	router.Run()
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    # 重定向

    # HTTP重定向

    • HTTP 重定向很容易。 内部、外部重定向均支持。
    r.GET("/test", func(c *gin.Context) {
    	c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/")
    })
    
    1
    2
    3

    # 路由重定向

    • 路由重定向,使用HandleContext:
    r.GET("/test", func(c *gin.Context) {
        // 指定重定向的URL
        c.Request.URL.Path = "/test2"
        r.HandleContext(c)
    })
    r.GET("/test2", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"hello": "world"})
    })
    
    1
    2
    3
    4
    5
    6
    7
    8

    # Gin中间件

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

    # 1. 定义中间件

    • Gin中的中间件必须是一个gin.HandlerFunc类型。例如我们像下面的代码一样定义一个统计请求耗时的中间件。

      c.Next() //调用该请求的剩余处理程序

      c.Abort() // 不调用该请求的剩余处理程序

    // StatCost 是一个统计耗时请求耗时的中间件
    func StatCost() gin.HandlerFunc {
    	return func(c *gin.Context) {
    		start := time.Now()
    		c.Set("name", "小王子") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
    		// 调用该请求的剩余处理程序
    		c.Next()
    		// 不调用该请求的剩余处理程序
    		// c.Abort()
    		// 计算耗时
    		cost := time.Since(start)
    		log.Println(cost)
    	}
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    # 2. 注册中间件

    • 在gin框架中,我们可以为每个路由添加任意数量的中间件。
    image-20210327185822087

    image-20210327185949523

    • 中间的这个通过 Abort() 可以阻止执行

    image-20200917211259531

    # 2.1 为全局路由注册

    func main() {
    	// 新建一个没有任何默认中间件的路由
    	r := gin.New()
    	// 注册一个全局中间件
    	r.Use(StatCost())
    	
    	r.GET("/test", func(c *gin.Context) {
    		name := c.MustGet("name").(string) // 从上下文取值
    		log.Println(name)
    		c.JSON(http.StatusOK, gin.H{
    			"message": "Hello world!",
    		})
    	})
    	r.Run()
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    # 2.2 为某个路由单独注册

    // 给/test2路由单独注册中间件(可注册多个)
    	r.GET("/test2", StatCost(), func(c *gin.Context) {
    		name := c.MustGet("name").(string) // 从上下文取值
    		log.Println(name)
    		c.JSON(http.StatusOK, gin.H{
    			"message": "Hello world!",
    		})
    	})
    
    1
    2
    3
    4
    5
    6
    7
    8

    # 2.3 为路由组注册中间件

    • 为路由组注册中间件有以下两种写法。
    1. 写法1:
    shopGroup := r.Group("/shop", StatCost())
    {
        shopGroup.GET("/index", func(c *gin.Context) {...})
        ...
    }
    
    1
    2
    3
    4
    5
    1. 写法2:
    shopGroup := r.Group("/shop")
    shopGroup.Use(StatCost())
    {
        shopGroup.GET("/index", func(c *gin.Context) {...})
        ...
    }
    
    1
    2
    3
    4
    5
    6

    # 3. 中间件注意事项

    # gin默认中间件

    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的优雅退出

    package main
    
    import (
        "context"
        "log"
        "net/http"
        "os"
        "os/signal"
        "syscall"
        "time"
    
        "github.com/gin-gonic/gin"
    )
    
    func main() {
        router := gin.Default()
        router.GET("/", func(c *gin.Context) {
            time.Sleep(5 * time.Second)
            c.String(http.StatusOK, "Welcome Gin Server")
        })
    
        srv := &http.Server{
            Addr:    ":8080",
            Handler: router,
        }
    
        go func() {
            // service connections
            if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
                log.Fatalf("listen: %s\n", err)
            }
        }()
    
        // Wait for interrupt signal to gracefully shutdown the server with
        // a timeout of 5 seconds.
        quit := make(chan os.Signal)
        // kill (no param) default send syscanll.SIGTERM
        // kill -2 is syscall.SIGINT
        // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
        signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
        <-quit
        log.Println("Shutdown Server ...")
    
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()
        if err := srv.Shutdown(ctx); err != nil {
            log.Fatal("Server Shutdown:", err)
        }
        // catching ctx.Done(). timeout of 5 seconds.
        select {
        case <-ctx.Done():
            log.Println("timeout of 5 seconds.")
        }
        log.Println("Server exiting")
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55

    # 运行多个服务

    我们可以在多个端口启动服务,例如:

    package main
    
    import (
    	"log"
    	"net/http"
    	"time"
    
    	"github.com/gin-gonic/gin"
    	"golang.org/x/sync/errgroup"
    )
    
    var (
    	g errgroup.Group
    )
    
    func router01() http.Handler {
    	e := gin.New()
    	e.Use(gin.Recovery())
    	e.GET("/", func(c *gin.Context) {
    		c.JSON(
    			http.StatusOK,
    			gin.H{
    				"code":  http.StatusOK,
    				"error": "Welcome server 01",
    			},
    		)
    	})
    
    	return e
    }
    
    func router02() http.Handler {
    	e := gin.New()
    	e.Use(gin.Recovery())
    	e.GET("/", func(c *gin.Context) {
    		c.JSON(
    			http.StatusOK,
    			gin.H{
    				"code":  http.StatusOK,
    				"error": "Welcome server 02",
    			},
    		)
    	})
    
    	return e
    }
    
    func main() {
    	server01 := &http.Server{
    		Addr:         ":8080",
    		Handler:      router01(),
    		ReadTimeout:  5 * time.Second,
    		WriteTimeout: 10 * time.Second,
    	}
    
    	server02 := &http.Server{
    		Addr:         ":8081",
    		Handler:      router02(),
    		ReadTimeout:  5 * time.Second,
    		WriteTimeout: 10 * time.Second,
    	}
       // 借助errgroup.Group或者自行开启两个goroutine分别启动两个服务
    	g.Go(func() error {
    		return server01.ListenAndServe()
    	})
    
    	g.Go(func() error {
    		return server02.ListenAndServe()
    	})
    
    	if err := g.Wait(); err != nil {
    		log.Fatal(err)
    	}
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74

    # 中文文档

    • https://gin-gonic.com/zh-cn/docs/
    #Go#
    上次更新: 2023/04/16, 18:35:33
    timerate令牌桶限流 实现原理
    gin进阶

    ← timerate令牌桶限流 实现原理 gin进阶→

    最近更新
    01
    go与http代理
    05-24
    02
    自制申威架构k8s-reloader
    12-06
    03
    Docker Buildx 教程
    12-01
    更多文章>
    Theme by Vdoing | Copyright © 2020-2024 小刘扎扎 | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式