Go 如何實現 AOP 切面操作?
【導讀】本文介紹了在 go 語言中實現 AOP 操作的實踐。
-
golang 如何進行 AOP 操作
-
怎樣的 AOP 風格最好用
-
gin 的中間件是如何實現的
前言
我們將一個事件處理標記爲 handler, 那麼 AOP 指代的,就是圍繞這個 handler 的【執行前】【執行後】的切面操作,他可以形象地描述爲:
這種執行流程,容易開發成以下樣式:
beforeHandler()
handler()
AfterHandler()
一旦做成這樣子,那麼在同一個 handler 接入不同的切面操作時,便會需要無限侵入代碼,變成:
...
countExecTime()
startRateLimit()
handler
endRateLimit()
endExecTime()
...
這樣子的操作,不只是醜陋,而且隨着 aop 操作增多,維護難度會劇增。
所以到底要如何設計一套可以複用的機制,來優雅使用 AOP 操作。熟悉 golang Web 開發的人都知道,github.com/gin-gonic/gin 裏,可以通過c.Next
和 c.Abort()
的機制,來優雅設計 aop 切面 (中間件)。
那麼,我們能否依照同類 api,來達到類似的效果呢?
答案是,可以的。
合理的 AOP 設計,應該是這樣的
它的執行流爲:
限流 start --> 熔斷 start —> handler —> 熔斷 end --> 限流 end
設計
設計前,考慮到大部分羣體,都對 http gin 的中間件流,十分熟悉。所以我們也基於同樣的方式,來實現 aop 操作。實現以前呢,我們同步一下設計的要求:
-
需要支持,對任意方法執行切面操作。
-
需要和 gin 共享相似度極高的 api。
-
支持所有框架場景。
-
實現包體簡單。
-
支持對【全局】【單個路由】兩個維度支持中間件
中間件樣例:
func AlertTimeout(c *irr.Context) {
// c.Abort() 阻斷
// c.Next() 放行
}
使用時
wrapF := irr.WrapFunc(func(){
// 原本的handler邏輯
})
// 流程超時報警
wrapF.Use(AlertTimeout)
// 流程熔斷報警
wrapF.Use(AlertFuse)
wrapF.Handle()
實現
目錄層級
irr
| - context.go
| - core.go
context.go
package irr
import (
"math"
)
const ABORT = math.MaxInt32 - 10000
type Context struct {
offset int
handlers []func(*Context)
}
func newContext() *Context {
return &Context{
offset: -1,
handlers: make([]func(*Context), 0, 10),
}
}
func (ctx *Context) Next() {
ctx.offset ++
s := len(ctx.handlers)
for ; ctx.offset < s; ctx.offset++ {
if !ctx.isAbort() {
ctx.handlers[ctx.offset](ctx)
} else {
return
}
}
}
func (ctx *Context) Reset() {
//ctx.PerRequestContext = &sync.Map{}
ctx.offset = -1
ctx.handlers = ctx.handlers[:0]
}
// stop middleware chain
func (ctx *Context) Abort() {
ctx.offset = math.MaxInt32 - 10000
}
func (ctx *Context) isAbort() bool {
if ctx.offset >= ABORT {
return true
}
return false
}
func (ctx *Context) addHandler(f func(ctx *Context)) {
ctx.handlers = append(ctx.handlers, f)
}
core.go
package irr
type WrapF struct {
f func()
ctx *Context
}
func WrapFunc(f func()) *WrapF {
return &WrapF{
f: f,
ctx: newContext(),
}
}
func (wf *WrapF) Use(f func(c *Context)) {
wf.ctx.addHandler(f)
}
func (wf *WrapF) Handle() {
wf.ctx.handlers = append(wf.ctx.handlers, func(c *Context) {
wf.f()
})
if len(wf.ctx.handlers) > 0 {
wf.ctx.Next()
}
wf.ctx.Reset()
}
使用
1. 對不支持中間件的 ws/tcp 框架接入中間件
假設你的 tcp/websocket 框架長這樣
for {
conn := l.Accept()
go func(conn net.Conn){
for {
packet, _ = readAPackFrom(conn)
go handle(packet, conn)
}
}(conn)
}
接入 aop 後, 全局中間件:
for {
conn := l.Accept()
go func(conn net.Conn){
for {
packet, _ = readAPackFrom(conn)
go func(){
wrapF := irr.WrapFunc(func(){
handle(packet, conn)
})
wrapF.Use(AlertTimeout)
wrapF.Use(Sentinel)
wrapF.Handle()
}()
}
}(conn)
}
路由級:
func handlePacket(packet []byte, conn) {
wrapF := irr.WrapFunc(func(){
// 路由邏輯
handle(packet, conn)
})
// 熔斷
wrapF.Use(Fuse("app:get_user_info", 20,10))
wrapF.Use(LimitRate(50,1))
wrapF.Handle()
}
2. 接入 gin
- 複製一份
func (c *irr.Context)
,將 irr 改成 gin,即可直接投入 gin 使用。
結語
-
通過 wrap 任意函數
func(){}
達到了對任意方法切面封裝。 -
使用方法和 gin 完全一致。
-
對不支持中間件的框架,可以快速接入。
-
實現包體簡單,累計不到 40 行。
-
支持對【全局】【單個路由】兩個維度支持中間件
本次實現的包體很輕便,所以直接將實現源碼貼出來了。c.Next
c.Abort
Use(middleware)
的設計原理,和以下倉庫同源:
github.com/gin-gonic/gin
github.com/fwhezfwhez/tcpx
github.com/fwhezfwhez/wsx
最後,如果不想以包體嵌入的形式,來使用 irr,可以直接使用倉庫:
github.com/fwhezfwhez/irr
轉自:
blog.csdn.net/fwhezfwhez/article/details/115471158
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/VkWK5_qX7qT5AhPoPtmH7Q