Go能實(shí)現(xiàn)AOP嗎?
今天分享的話題是Go是否能實(shí)現(xiàn)AOP?
背景
寫Java的同學(xué)來(lái)寫Go就特別喜歡將兩者進(jìn)行對(duì)比,經(jīng)常看到技術(shù)群里討論,比如Go能不能實(shí)現(xiàn)Java那樣的AOP啊?Go寫個(gè)事務(wù)好麻煩啊,有沒(méi)有Spring那樣的@Transactional注解啊?
遇到這樣的問(wèn)題我通常會(huì)回復(fù):沒(méi)有、實(shí)現(xiàn)不了、再見(jiàn)。

直到看了《Go語(yǔ)言底層原理剖析》這本書(shū),我開(kāi)始了一輪認(rèn)真地探索。
Java是如何實(shí)現(xiàn)AOP的
AOP概念第一次是在若干年前學(xué)Java時(shí)看的一本書(shū)《Spring實(shí)戰(zhàn)》中看到的,它指的是一種面向切面編程的思想。注意它只是一種思想,具體怎么實(shí)現(xiàn),你看著辦。
AOP能在你代碼的前后織入代碼,這就能做很多有意思的事情了,比如統(tǒng)一的日志打印、監(jiān)控埋點(diǎn),事務(wù)的開(kāi)關(guān),緩存等等。
可以分享一個(gè)我當(dāng)年學(xué)習(xí)AOP時(shí)的筆記片段:

在Java中的實(shí)現(xiàn)方式可以是JDK動(dòng)態(tài)代理和字節(jié)碼增強(qiáng)技術(shù)。
JDK動(dòng)態(tài)代理是在運(yùn)行時(shí)動(dòng)態(tài)地生成了一個(gè)代理類,JVM通過(guò)加載這個(gè)代理類再實(shí)例化來(lái)實(shí)現(xiàn)AOP的能力。
字節(jié)碼增強(qiáng)技術(shù)可以多嘮叨兩句,當(dāng)年學(xué)Java時(shí)第一章就說(shuō)Java的特點(diǎn)是「一次編譯,到處運(yùn)行」。
但當(dāng)我們真正在工作中這個(gè)特性用處大嗎?好像并不大,生產(chǎn)中都使用了同一種服務(wù)器,只編譯了一次,也都只在這個(gè)系統(tǒng)運(yùn)行。做到一次編譯,到處運(yùn)行的技術(shù)底座是JVM,JVM可以加載字節(jié)碼并運(yùn)行,這個(gè)字節(jié)碼是平臺(tái)無(wú)關(guān)的一種二進(jìn)制中間碼。
似乎這個(gè)設(shè)定帶來(lái)了一些其他的好處。在JVM加載字節(jié)碼時(shí),字節(jié)碼有一次被修改的機(jī)會(huì),但這個(gè)字節(jié)碼的修改比較復(fù)雜,好在有現(xiàn)成的庫(kù)可用,如ASM、Javassist等。
至于像ASM這樣的庫(kù)是如何修改字節(jié)碼的,我還真就去問(wèn)了Alibaba Dragonwell的一位朋友,他回答ASM是基于Java字節(jié)碼規(guī)范所做的「硬改」,但做了一些抽象,總體來(lái)說(shuō)還是比較枯燥的。
由于這不是本文重點(diǎn),所以只是提一下,如果想更詳細(xì)地了解可自行網(wǎng)上搜索。
Go能否實(shí)現(xiàn)AOP?
之前用「扁鵲三連」的方式回復(fù)Go不能實(shí)現(xiàn)AOP的基礎(chǔ)其實(shí)就是我對(duì)Java實(shí)現(xiàn)AOP的思考,因?yàn)镚o沒(méi)有虛擬機(jī)一說(shuō),也沒(méi)有中間碼,直接源碼編譯為可執(zhí)行文件,可執(zhí)行文件基本沒(méi)法修改,所以做不了。
但真就如此嗎?我搜索了一番。
運(yùn)行時(shí)攔截
還真就在Github找到了一個(gè)能實(shí)現(xiàn)類似AOP功能的庫(kù)gohook(當(dāng)然也有類似的其他庫(kù)):
https://github.com/brahma-adshonor/gohook
看這個(gè)項(xiàng)目的介紹:

運(yùn)行時(shí)動(dòng)態(tài)地hook Go的方法,也就是可以在方法前插入一些邏輯。它是怎么做到的?

通過(guò)反射找到方法的地址(指針),然后插入一段代碼,執(zhí)行完后再執(zhí)行原方法。聽(tīng)起來(lái)很牛X,但它下面有個(gè)Notes:

使用有一些限制,更重要的是沒(méi)有完全測(cè)試,不建議生產(chǎn)使用。這種不可靠的方式也就不嘗試了。
AST修改源碼
這種方式就是我在看《Go語(yǔ)言底層原理剖析》第一章看到的,其實(shí)我之前的文章也有寫過(guò)關(guān)于AST的,《Cobar源碼分析之AST》。
AST即抽象語(yǔ)法樹(shù),可以認(rèn)為所有的高級(jí)編程語(yǔ)言源碼都可以抽象為一種語(yǔ)法樹(shù),即對(duì)代碼進(jìn)行結(jié)構(gòu)化的抽象,這種抽象可以讓我們更加簡(jiǎn)單地分析甚至操作源碼。
Go在編譯時(shí)大概分為詞法與語(yǔ)法分析、類型檢查、通用 SSA 生成和最后的機(jī)器代碼生成這幾個(gè)階段。
其中詞法與語(yǔ)法分析之后,生成一個(gè)AST樹(shù),在Go中我們能調(diào)用Go提供的API很輕易地生成AST:
fset?:=?token.NewFileSet()
//?這里file就是一個(gè)AST對(duì)象
file,?err?:=?parser.ParseFile(fset,?"aop.go",?nil,?parser.ParseComments)
比如這里我的aop.go文件是這樣的:
package?main
import?"fmt"
func?main()?{
?fmt.Println(execute("roshi"))
}
func?execute(name?string)?string?{
?return?name
}
想看生成的AST長(zhǎng)什么樣,可調(diào)用下面的方法:
ast.Print(fset,?file)
由于篇幅太長(zhǎng),我截個(gè)圖感受下即可:

當(dāng)然也有一些開(kāi)源的可視化工具,但我覺(jué)得大可不必,想看的話Debug看下file的結(jié)構(gòu)即可。
至于Go AST結(jié)構(gòu)的介紹,也不是本文的重點(diǎn),而且AST中的類型很多很多,敘述起來(lái)比較枯燥。
我們這里就實(shí)現(xiàn)一個(gè)簡(jiǎn)單的,在execute方法執(zhí)行之前添加一條打印before的語(yǔ)句,接上述代碼:
const?before?=?"fmt.Println(\"before\")"
...
exprInsert,?err?:=?parser.ParseExpr(before)
if?err?!=?nil?{
?panic(err)
}
decls?:=?make([]ast.Decl,?0,?len(file.Decls))
for?_,?decl?:=?range?file.Decls?{
?fd,?ok?:=?decl.(*ast.FuncDecl)
?if?ok?{
??if?fd.Name.Name?==?"execute"?{
???stats?:=?make([]ast.Stmt,?0,?len(fd.Body.List)+1)
???stats?=?append(stats,?&ast.ExprStmt{
????X:?exprInsert,
???})
???stats?=?append(stats,?fd.Body.List...)
???fd.Body.List?=?stats
???decls?=?append(decls,?fd)
???continue
??}?else?{
???decls?=?append(decls,?decl)
??}
?}?else?{
??decls?=?append(decls,?decl)
?}
}
file.Decls?=?decls
這里AST就被我們修改了,雖然我們是寫死了針對(duì)execute方法,但總歸是邁出了第一步。
再把AST轉(zhuǎn)換為源碼輸出,Go也提供了API:
var?cfg?printer.Config
var?buf?bytes.Buffer
cfg.Fprint(&buf,?fset,?file)
fmt.Printf(buf.String())
輸出效果如下:

看到這里,我猜你應(yīng)該有和我相同的想法,這玩意是不是可以用來(lái)格式化代碼?
沒(méi)錯(cuò),Go自帶的格式化代碼工具gofmt的原理就是如此。
當(dāng)我們寫完代碼時(shí),可以執(zhí)行g(shù)ofmt對(duì)代碼進(jìn)行格式化:
gofmt test.go
這相比于其他語(yǔ)言方便很多,終于有個(gè)官方的代碼格式了,甚至你可以在IDEA中安裝一個(gè)file watchers插件,監(jiān)聽(tīng)文件變更,當(dāng)文件有變化時(shí)自動(dòng)執(zhí)行 gofmt 來(lái)格式化代碼。

看到這里你可能覺(jué)得太簡(jiǎn)單了,我查了下資料,AST中還能拿到注釋,這就厲害了,我們可以把注釋當(dāng)注解來(lái)玩,比如我加了 // before: 的注釋,自動(dòng)把這個(gè)注釋后的代碼添加到方法之前去。
//?before:fmt.Println("before...")
func?executeComment(name?string)?string?{
?return?name
}
修改AST代碼如下,為了篇幅,省略了打印代碼:
cmap?:=?ast.NewCommentMap(fset,?file,?file.Comments)
for?_,?decl?:=?range?file.Decls?{
?fd,?ok?:=?decl.(*ast.FuncDecl)
?if?ok?{
??if?cs,?ok?:=?cmap[fd];?ok?{
???for?_,?cg?:=?range?cs?{
????for?_,?c?:=?range?cg.List?{
?????if?strings.HasPrefix(c.Text,?"http://?before:")?{
??????txt?:=?strings.TrimPrefix(c.Text,?"http://?before:")
??????ei,?err?:=?parser.ParseExpr(txt)
??????if?err?==?nil?{
???????stats?:=?make([]ast.Stmt,?0,?len(fd.Body.List)+1)
???????stats?=?append(stats,?&ast.ExprStmt{
????????X:?ei,
???????})
???????stats?=?append(stats,?fd.Body.List...)
???????fd.Body.List?=?stats
???????decls?=?append(decls,?fd)
???????continue
??????}
?????}
????}
???}
??}?else?{
???decls?=?append(decls,?decl)
??}
?}?else?{
??decls?=?append(decls,?decl)
?}
}
file.Decls?=?decls
跑一下看看:

雖然代碼很丑,但這不重要,又不是不能用~

但你發(fā)現(xiàn),這樣實(shí)現(xiàn)AOP有個(gè)缺點(diǎn),必須在編譯期對(duì)代碼進(jìn)行一次重新生成,理論上來(lái)說(shuō),所有高級(jí)編程語(yǔ)言都可以這么操作。
但這不是說(shuō)毫無(wú)用處,比如這篇文章《每個(gè) gopher 都需要了解的 Go AST》就給了我們一個(gè)實(shí)際的案例:

最后
寫到最后,我又在思考另一個(gè)問(wèn)題,為什么Go的使用者沒(méi)有AOP的需求呢?反倒是寫Java的同學(xué)會(huì)想到AOP。
我覺(jué)得可能還是Go太年輕了,Java之所以要用AOP,很大的原因是代碼已經(jīng)堆積如山,沒(méi)法修改,歷史包袱沉重,最小代價(jià)實(shí)現(xiàn)需求是首選,所以會(huì)選擇AOP這種技術(shù)。
反觀Go還年輕,大多數(shù)項(xiàng)目屬于造輪子期間,需要AOP的地方早就在代碼中提前埋伏好了。我相信隨著發(fā)展,一定也會(huì)出現(xiàn)一個(gè)生產(chǎn)可用Go AOP框架。
至于現(xiàn)在問(wèn)我,Go能否實(shí)現(xiàn)AOP,我還是回答:沒(méi)有、實(shí)現(xiàn)不了、再見(jiàn)。
對(duì)了,本文的完整測(cè)試代碼這里可以看到:
https://github.com/lkxiaolou/all-in-one/tree/master/go-in-one/samples/tree
推薦閱讀
