hello~大家好,我是小樓,今天分享的話題是Go是否能實現AOP?
背景
寫Java的同學來寫Go就特別喜歡將兩者進行對比,就經常看到技術群裡討論,比如Go能不能實現Java那樣的AOP啊?Go寫個事務好麻煩啊,有沒有Spring那樣的@Transactional註解啊?
遇到這樣的問題我通常會回覆:沒有、實現不了、再見。
直到看了《Go語言底層原理剖析》這本書,開始了一輪認真地探索。
Java是如何實現AOP的
AOP概念第一次是在若干年前學Java時看的一本書《Spring實戰》中看到的,它指的是一種面向切面程式設計的思想。注意它只是一種思想,具體怎麼實現,你看著辦。
AOP能在你程式碼的前後織入程式碼,這就能做很多有意思的事情了,比如統一的日誌列印、監控埋點,事務的開關,快取等等。
可以分享一個我當年學習AOP時的筆記片段:
在Java中的實現方式可以是JDK動態代理
和位元組碼增強技術
。
JDK動態代理是在執行時動態地生成了一個代理類,JVM通過載入這個代理類再例項化來實現AOP的能力。
位元組碼增強技術可以多嘮叨兩句,當年學Java時第一章就說Java的特點是「一次編譯,到處執行」。
但當我們真正在工作中這個特性用處大嗎?好像並不大,生產中都使用了同一種伺服器,只編譯了一次,也都只在這個系統執行。做到一次編譯,到處執行的技術底座是JVM,JVM可以載入位元組碼並執行,這個位元組碼是平臺無關的一種二進位制中間碼。
似乎這個設定帶來了一些其他的好處。在JVM載入位元組碼時,位元組碼有一次被修改的機會,但這個位元組碼的修改比較複雜,好在有現成的庫可用,如ASM、Javassist等。
至於像ASM這樣的庫是如何修改位元組碼的,我還真就去問了Alibaba Dragonwell的一位朋友,他回答ASM是基於Java位元組碼規範所做的「硬改」,但做了一些抽象,總體來說還是比較枯燥的。
由於這不是本文重點,所以只是提一下,如果想更詳細地瞭解可自行網上搜尋。
Go能否實現AOP?
之前用「扁鵲三連」的方式回覆Go不能實現AOP的基礎其實就是我對Java實現AOP的思考,因為Go沒有虛擬機器一說,也沒有中間碼,直接原始碼編譯為可執行檔案,可執行檔案基本沒法修改,所以做不了。
但真就如此嗎?我搜尋了一番。
執行時攔截
還真就在Github找到了一個能實現類似AOP功能的庫gohook
(當然也有類似的其他庫):
看這個專案的介紹:
執行時動態地hook Go的方法,也就是可以在方法前插入一些邏輯。它是怎麼做到的?
通過反射找到方法的地址(指標),然後插入一段程式碼,執行完後再執行原方法。聽起來很牛X,但它下面有個Notes:
使用有一些限制,更重要的是沒有完全測試,不建議生產使用。這種不可靠的方式也就不嘗試了。
AST修改原始碼
這種方式就是我在看《Go語言底層原理剖析》第一章看到的,其實我之前的文章也有寫過關於AST的,《Cobar原始碼分析之AST》。
AST即抽象語法樹,可以認為所有的高階程式語言都可以抽象為一種語法樹,即對程式碼進行結構化的抽象,這種抽象可以讓我們更加簡單地分析甚至操作原始碼。
Go在編譯時大概分為詞法與語法分析、型別檢查、通用 SSA 生成和最後的機器程式碼生成這幾個階段。
其中詞法與語法分析之後,生成一個AST樹,在Go中我們能呼叫Go提供的API很輕易地生成AST:
fset := token.NewFileSet()
// 這裡file就是一個AST物件
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長什麼樣,可呼叫下面的方法:
ast.Print(fset, file)
由於篇幅太長,我截個圖感受下即可:
當然也有一些開源的視覺化工具,但我覺得大可不必,想看的話Debug看下file
的結構。
至於Go AST結構的介紹,也不是本文的重點,而且AST中的型別很多很多,我建議如果你想看的話直接Debug來看,對照原始碼比較清晰。
我們這裡就實現一個簡單的,在execute方法執行之前新增一條列印before
的語句,接上述程式碼:
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就被我們修改了,雖然我們是寫死了針對execute方法,但總歸是邁出了第一步。
再把AST轉換為原始碼輸出,Go也提供了API:
var cfg printer.Config
var buf bytes.Buffer
cfg.Fprint(&buf, fset, file)
fmt.Printf(buf.String())
輸出效果如下:
看到這裡,我猜你應該有和我相同的想法,這玩意是不是可以用來格式化程式碼?
沒錯,Go自帶的格式化程式碼工具gofmt的原理就是如此。
當我們寫完程式碼時,可以執行gofmt對程式碼進行格式化:
gofmt test.go
這相比於其他語言方便很多,終於有個官方的程式碼格式了,甚至你可以在IDEA中安裝一個file watchers外掛,監聽檔案變更,當檔案有變化時自動執行 gofmt 來格式化程式碼。
看到這裡你可能覺得太簡單了,我查了下資料,AST中還能拿到註釋,這就厲害了,我們可以把註釋當註解來玩,比如我加了 // before:
的註釋,自動把這個註釋後的程式碼新增到方法之前去。
// 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, "// before:") {
txt := strings.TrimPrefix(c.Text, "// 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
跑一下看看:
雖然又是硬編碼,但這不重要,又不是不能用~
但你發現,這樣實現AOP有個缺點,必須在編譯期對程式碼進行一次重新生成,理論上來說,所有高階程式語言都可以這麼操作。
但這不是說毫無用處,比如這篇文章《每個 gopher 都需要了解的 Go AST》就給了我們一個實際的案例:
最後
寫到最後,我又在思考另一個問題,為什麼Go的使用者沒有AOP的需求呢?反倒是寫Java的同學會想到AOP。
我覺得可能還是Go太年輕了,Java之所以要用AOP,很大的原因是程式碼已經堆積如山,沒法修改,歷史包袱沉重,最小代價實現需求是首選,所以會選擇AOP這種技術。
反觀Go還年輕,大多數專案屬於造輪子期間,需要AOP的地方早就在程式碼中提前埋伏好了。我相信隨著發展,一定也會出現一個生產可用Go AOP框架。
至於現在問我,Go能否實現AOP,我還是回答:沒有、實現不了、再見。
對了,本文的完整測試程式碼這裡可以看到:
https://github.com/lkxiaolou/all-in-one/tree/master/go-in-one/samples/tree
感謝大家,如果有點收穫,點個在看
、贊
、關注
吧,我們下期再見。
搜尋關注微信公眾號"捉蟲大師",後端技術分享,架構設計、效能優化、原始碼閱讀、問題排查、踩坑實踐。