golang web框架 Gin 详解(四)|Go主题月

1,449 阅读10分钟

回顾

上一期上文中我们讲解了

  • 如何使用gin构建一个标准的web工程
  • 如何使用gin写出标准的api接口
  • ...

我们在上一期中编写了一个api接口、配置了数据库链接。学会了上一期内容的话,基本上编写增删查改的业务已经没有问题了。这一期我们主要讲一下在golang的一些编码思路,来帮我们补充一些框架上的功能,以及gin的一些拓展应用。

这一期我们的主要目标是

  • 完善web框架 (借助golang的协程和chan来完成一个限流中间件)
  • gin框架的进阶 (文件上传、静态文件、重定向...)

同样的我们接着上一期的工程(server1分支)继续

但是工程地址的分支从这一期开始切换为server1分支。希望大家注意

查看一下目录

go-gin-test git:(server1) ✗ tree -L 3
.
├── api
│   └── api.go
├── db
│   └── db.go
├── go.mod
├── go.sum
├── hello
├── mian.go
├── model
│   └── user.go
├── routerex
│   ├── mid
│   │   └── mid.go
│   └── router.go
├── server
│   └── server.go
└── service

完善web框架 -- 实现一个限流中间件

目标: 实现一个限流中间件

说明: 首先我们假设现在有这么一个需求,我们需要限制某一个接口同一时间最多只处理来自于10个不同的IP的请求并且同一个IP在同一时间只允许10个请求。也就是说我们限制为最多有100个请求。但是,如果在我们满负载的情况,我们不能丢弃或者直接返回后续的请求,而是应该让他们排队。

要点:

  • 数据结构的设计
  • 利用go的协程
  • 借助chan的特性

思路分析:

  1. 通过分析需求,感觉和队列有些相似,那么在数据结构上多半为数组的形态。实际上我们会控制两个数组,一个是IP的,另一个是IP的请求。那在我们的大脑中一个大概的数据结构就已经出现
  2. go的协程能够很好的帮助我们完成异步的处理,我们只需要控制同时开始的协程数量就好了
  3. 借助有大小的chan的特性,我们就能控制同一时间最大有几个协程运行,而不需要一个线程池来控制

代码:

mid目录下新建limit.go

package mid

import (
	"container/list"
	"fmt"
	"reflect"
	"sync"

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

//限流中间价
var LimitMid *MidLimit

//用来存放请求的token
var TokenMap sync.Map

//限流中间价的接口
type IMidLimit interface {
	Put(gCtx *gin.Context, Token string) bool
	Start()
}

//限流中间价的结构体
type MidLimit struct {
	//使用sync Map 防止并发问题
	sm sync.Map
	//同一时间访问的IP的chan,用来限制IP数量
	mc chan *Task
}

//初始化限流中间件
func InitLimitMid() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(fmt.Errorf("InitLimitMid  err:%+v", err))
			fmt.Println(fmt.Errorf("InitLimitMid  err:%+v", err))
		}
	}()

	l := &MidLimit{}
	l.sm = sync.Map{}
	//最大为10个IP
	l.mc = make(chan *Task, 10)
	LimitMid = l
	TokenMap = sync.Map{}
	//启动一个协程去监听这个chan
	go LimitMid.Start()
}

//IP 的chan的监听
func (c *MidLimit) Start() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(fmt.Errorf("MidLimit Start err:%+v", err))
			fmt.Println(fmt.Errorf("MidLimit Start err:%+v", err))
		}
	}()

	if c.mc == nil {
		fmt.Println("MidLimit err: chan is empty")
		return
	}
	//如果有任务,就直接运行
	for task := range c.mc {
		go task.Run()
	}

}

//放入请求
func (c *MidLimit) Put(gCtx *gin.Context, Token string) bool {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(fmt.Errorf("MidLimit Put err:%+v", err))
			fmt.Println(fmt.Errorf("MidLimit Put err:%+v", err))
		}
	}()

	if c.mc == nil {
		fmt.Println("MidLimit chan is empty")
		return false
	}
	//获取IP
	ip := gCtx.ClientIP()
	load, ok := c.sm.Load(ip)
	task := &Task{}
	if ok {
		if reflect.TypeOf(load).Kind() != reflect.Ptr {
			return false
		}
		task = load.(*Task)
	}
	//上锁,防止数据出问题
	task.lk.Lock()
	defer task.lk.Unlock()
	if task.ls == nil {
		task.ls = list.New()
		task.IP = ip
		task.tc = make(chan *Item, 500)
	}
	//放到队列的最末端
	task.ls.PushBack(&Item{Token: Token, GCtx: gCtx})
	//发送给chan
	LimitMid.mc <- task
	c.sm.Store(ip, task)
	TokenMap.Store(Token, Token)
	return true
}

//IP结构体,里面保存的是同一个IP的所有请求
type Task struct {
	IP string
	//锁,主要是保证安全
	lk sync.Mutex
	//请求的list
	ls *list.List
	//处理请求的chan
	tc chan *Item
}

func (c *Task) Run() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(fmt.Errorf("Task Run err:%+v", err))
			fmt.Println(fmt.Errorf("Task Run err:%+v", err))
		}
	}()

	go func() {
		for item := range c.tc {
			TokenMap.Delete(item.Token)
		}
	}()

	c.lk.Lock()
	defer c.lk.Unlock()
	//每次最多处理10个请求
	max := 10
	if c.ls.Len() > 0 && c.ls.Len() < 10 {
		max = c.ls.Len()
	}
	fmt.Printf("Before  ------ IP : %v count: %v", c.IP, c.ls.Len())
	fmt.Println()
	//TODO 这里可以修改逻辑,不一定要按我的思路
	for i := 0; i < max; i++ {
		front := c.ls.Front()
		if front == nil {
			continue
		}
		if reflect.TypeOf(front.Value).Kind() != reflect.Ptr {
			fmt.Println("Task Run reflect err not Ptr")
		}
		item := front.Value.(*Item)
		c.tc <- item
		//处理一个,就删除一个
		c.ls.Remove(front)
	}
	fmt.Printf("After  ------ IP : %v count: %v", c.IP, c.ls.Len())
	fmt.Println()
	//大于10个的部分,再把它放回IP的chan里面重新排队
	if c.ls.Len() > 0 {
		LimitMid.mc <- c
	}
}

//实际的请求
type Item struct {
	Token string
	GCtx  *gin.Context
}

获取一下uuid

go get github.com/satori/go.uuid

修改一下router.goserver.go

router.go

package routerex

import (
	"fmt"

	"example.com/m/v2/api"
	"example.com/m/v2/routerex/mid"
	"github.com/gin-gonic/gin"
	uuid "github.com/satori/go.uuid"
)

func MidLimit(c *gin.Context) {
	fmt.Println(fmt.Printf("---- %v", c.GetHeader("User-Agent")))
	s := uuid.NewV4().String()
	mid.LimitMid.Put(c, s)
	for {
		_, ok := mid.TokenMap.Load(s)
		if !ok {
			break
		}
	}
	c.Next()
}

//用作注册请求地址
func RegRouter(g *gin.Engine) {
	g1 := g.Group("/api")
	//中间件
	g1.Use(mid.MidCors)
	g1.Use(MidLimit)
	g1.POST("/register", api.Register)
	g1.GET("/limit", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"msg": "ok",
		})
	})
}

server.go

package server

import (
	"fmt"

	"example.com/m/v2/db"
	"example.com/m/v2/routerex"
	"example.com/m/v2/routerex/mid"
	"github.com/gin-gonic/gin"
)

func ServerStart(port string) {

	r := gin.Default()
	routerex.RegRouter(r)
	mid.InitLimitMid()
	err := db.InitDB()
	if err != nil {
		fmt.Println(fmt.Errorf("init db err: %v", err))
	}
	r.Run(":" + port) // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

随后就是编译和启动

这里我们使用一个go编写的测试工具进行并发测试

地址:https://github.com/link1st/go-stress-testing

下载二进制

运行 20个并发,一个并发请求10次 ./go-stress-testing-mac -c 20 -n 20 -u localhost:8080/api/limit

服务日志:

Before  ------ IP : ::1 count: 15
After  ------ IP : ::1 count: 5
Before  ------ IP : ::1 count: 5
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
[GIN] 2021/04/09 - 22:02:52 | 200 |   52.975949ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   33.017938ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   32.982102ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   49.785125ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   33.019011ms |             ::1 | GET      "/api/limit"
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
[GIN] 2021/04/09 - 22:02:52 | 200 |   33.045255ms |             ::1 | GET      "/api/limit"
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
---- Go-http-client/1.123 <nil>
---- Go-http-client/1.123 <nil>
[GIN] 2021/04/09 - 22:02:52 | 200 |   65.433351ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   50.847047ms |             ::1 | GET      "/api/limit"
Before  ------ IP : ::1 count: 1
[GIN] 2021/04/09 - 22:02:52 | 200 |     506.656µs |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |    9.930572ms |             ::1 | GET      "/api/limit"
---- Go-http-client/1.123 <nil>
Before  ------ IP : ::1 count: 3
After  ------ IP : ::1 count: 0
[GIN] 2021/04/09 - 22:02:52 | 200 |    1.372397ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |     602.793µs |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |    1.377011ms |             ::1 | GET      "/api/limit"
---- Go-http-client/1.123 <nil>
---- Go-http-client/1.123 <nil>
---- Go-http-client/1.123 <nil>
---- Go-http-client/1.123 <nil>
---- Go-http-client/1.123 <nil>
---- Go-http-client/1.123 <nil>
---- Go-http-client/1.123 <nil>
[GIN] 2021/04/09 - 22:02:52 | 200 |   72.990133ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |    53.24303ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   52.235207ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |    53.10638ms |             ::1 | GET      "/api/limit"
---- Go-http-client/1.123 <nil>
[GIN] 2021/04/09 - 22:02:52 | 200 |   50.019364ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   53.408403ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   51.147636ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   53.404998ms |             ::1 | GET      "/api/limit"
---- Go-http-client/1.123 <nil>
Before  ------ IP : ::1 count: 4
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
[GIN] 2021/04/09 - 22:02:52 | 200 |   12.975392ms |             ::1 | GET      "/api/limit"
---- Go-http-client/1.123 <nil>
---- Go-http-client/1.123 <nil>
Before  ------ IP : ::1 count: 2
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
[GIN] 2021/04/09 - 22:02:52 | 200 |   13.068885ms |             ::1 | GET      "/api/limit"
Before  ------ IP : ::1 count: 1
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
[GIN] 2021/04/09 - 22:02:52 | 200 |     13.5707ms |             ::1 | GET      "/api/limit"
---- Go-http-client/1.123 <nil>
---- Go-http-client/1.123 <nil>
---- Go-http-client/1.123 <nil>
---- Go-http-client/1.123 <nil>
---- Go-http-client/1.123 <nil>
[GIN] 2021/04/09 - 22:02:52 | 200 |   55.673286ms |             ::1 | GET      "/api/limit"
---- Go-http-client/1.123 <nil>
---- Go-http-client/1.123 <nil>
Before  ------ IP : ::1 count: 6
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
[GIN] 2021/04/09 - 22:02:52 | 200 |   53.126323ms |             ::1 | GET      "/api/limit"
---- Go-http-client/1.123 <nil>
---- Go-http-client/1.123 <nil>
[GIN] 2021/04/09 - 22:02:52 | 200 |    8.151735ms |             ::1 | GET      "/api/limit"
---- Go-http-client/1.123 <nil>
[GIN] 2021/04/09 - 22:02:52 | 200 |   53.223775ms |             ::1 | GET      "/api/limit"
---- Go-http-client/1.123 <nil>
Before  ------ IP : ::1 count: 8
After  ------ IP : ::1 count: 0
---- Go-http-client/1.123 <nil>
Before  ------ IP : ::1 count: 1
After  ------ IP : ::1 count: 0
---- Go-http-client/1.123 <nil>
Before  ------ IP : ::1 count: 2
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
[GIN] 2021/04/09 - 22:02:52 | 200 |     826.491µs |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |    1.040488ms |             ::1 | GET      "/api/limit"
---- Go-http-client/1.123 <nil>
Before  ------ IP : ::1 count: 2
After  ------ IP : ::1 count: 0
[GIN] 2021/04/09 - 22:02:52 | 200 |    9.693082ms |             ::1 | GET      "/api/limit"
---- Go-http-client/1.123 <nil>
[GIN] 2021/04/09 - 22:02:52 | 200 |   27.692747ms |             ::1 | GET      "/api/limit"
Before  ------ IP : ::1 count: 1
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
---- Go-http-client/1.123 <nil>
[GIN] 2021/04/09 - 22:02:52 | 200 |      838.92µs |             ::1 | GET      "/api/limit"
---- Go-http-client/1.123 <nil>
---- Go-http-client/1.123 <nil>
---- Go-http-client/1.123 <nil>
[GIN] 2021/04/09 - 22:02:52 | 200 |    41.30448ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   41.646191ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   33.808401ms |             ::1 | GET      "/api/limit"
---- Go-http-client/1.123 <nil>
[GIN] 2021/04/09 - 22:02:52 | 200 |    42.36363ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   38.868172ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |     465.389µs |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |     492.525µs |             ::1 | GET      "/api/limit"
Before  ------ IP : ::1 count: 2
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 1
After  ------ IP : ::1 count: 0
[GIN] 2021/04/09 - 22:02:52 | 200 |     278.108µs |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |     121.357µs |             ::1 | GET      "/api/limit"
Before  ------ IP : ::1 count: 1
After  ------ IP : ::1 count: 0
[GIN] 2021/04/09 - 22:02:52 | 200 |     341.215µs |             ::1 | GET      "/api/limit"
---- Go-http-client/1.123 <nil>
Before  ------ IP : ::1 count: 2
After  ------ IP : ::1 count: 0
[GIN] 2021/04/09 - 22:02:52 | 200 |     444.966µs |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |     956.363µs |             ::1 | GET      "/api/limit"
Before  ------ IP : ::1 count: 1
After  ------ IP : ::1 count: 0
[GIN] 2021/04/09 - 22:02:52 | 200 |     237.543µs |             ::1 | GET      "/api/limit"
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
Before  ------ IP : ::1 count: 0
After  ------ IP : ::1 count: 0
[GIN] 2021/04/09 - 22:02:52 | 200 |   22.753412ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   62.916527ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   63.053594ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   63.170451ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   63.097772ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   41.751858ms |             ::1 | GET      "/api/limit"
---- Go-http-client/1.123 <nil>
[GIN] 2021/04/09 - 22:02:52 | 200 |   63.128788ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   15.458979ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   60.814893ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   63.177745ms |             ::1 | GET      "/api/limit"
23 <nil>
Before  ------ IP : ::1 count: 1
After  ------ IP : ::1 count: 0
[GIN] 2021/04/09 - 22:02:52 | 200 |   63.054733ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |   63.346652ms |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |     367.146µs |             ::1 | GET      "/api/limit"
Before  ------ IP : ::1 count: 1
Before  ------ IP : ::1 count: 1
After  ------ IP : ::1 count: 0
[GIN] 2021/04/09 - 22:02:52 | 200 |     132.156µs |             ::1 | GET      "/api/limit"
[GIN] 2021/04/09 - 22:02:52 | 200 |     105.188µs |             ::1 | GET      "/api/limit"

这里的日志不太全,但是我们可以看出每次处理的请求不超过10个,而且如果数组大于10个,也只会处理10个。大家可以自行看下自己的控制台

gin框架的进阶 (文件上传、静态文件、重定向...)

  • 上传文件
func(c *gin.Context) {
		// 单文件
		file, _ := c.FormFile("file")
		log.Println(file.Filename)

		// 上传文件到指定的路径
		// c.SaveUploadedFile(file, dst)

		c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
	}
        
func(c *gin.Context) {
        // 多文件
        form, _ := c.MultipartForm()
        files := form.File["upload[]"]

        for _, file := range files {
                log.Println(file.Filename)

                // 上传文件到指定的路径
                // c.SaveUploadedFile(file, dst)
        }
        c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
}
  • 静态文件
	router := gin.Default()
	router.Static("/assets", "./assets")
	router.StaticFS("/more_static", http.Dir("my_file_system"))
	router.StaticFile("/favicon.ico", "./resources/favicon.ico")
  • 重定向
func(c *gin.Context) {
	c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
}

func(c *gin.Context) {
    c.Request.URL.Path = "/test2"
    r.HandleContext(c)
}

总结

  • 其实限流的中间件还有很多可以进步的地方,在这里地方主要是想让大家联系一下go语言的特性和拓展大家的编程思路。

  • gin框架的常用代码我也已经介绍完毕

  • 额。可能是因为我没有人关注的原因吧,关于gin框架也没有收到同学的反馈,我也不知道自己写得咋样,由于没有同学来问问题,那么gin框架的文章就到此为一段落

    想获取工程的同学可以关注超级英雄吉姆,在公众号里发送gin,获取工程。

gin相关的文章就到此结束,如果还有任何问题,可以关注我的公众号超级英雄吉姆,在公众号留言,我看到后第一时间回复。

后话

求求大家给个关注。要吃不上饭了~