<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go能實(shí)現(xiàn)AOP嗎?

          共 4338字,需瀏覽 9分鐘

           ·

          2022-04-26 13:43

          今天分享的話題是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



          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù)?ebook?獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬(wàn) Gopher 交流學(xué)習(xí)。

          瀏覽 40
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  大香蕉夜夜撸 | 91精品国产91久久久久 | 人人干人人操人人摸 | 日产精品久久久一区二区 | 日逼福利视频 |