Kitex源碼閱讀——腳手架程式碼是如何通過命令行生成的(一)

前言

Kitex是字節跳動內部的Golang微服務RPC框架,先已開源。

Kitex文檔://www.cloudwego.io/zh/docs/kitex/getting-started/

Kitex體驗://juejin.cn/post/7098966260502921230

在Kitex體驗的文章中,我們使用Kitex從零構建了自己的服務,只要定義好IDL(介面描述語言),按照Kitex提供的命令行規則,就可以生成支援ThriftProtobuf的客戶端和服務端相關的腳手架程式碼,使得我們可以直接著手編寫服務端的響應實現和客戶端的請求發起邏輯。

那麼Kitex究竟是怎麼生成腳手架程式碼的?這系列文章將圍繞此展開源碼閱讀,並最終解答這個疑問。

源碼分析

初覽kitex命令行工具

在最初安裝或者更新Kitex的時候,用到了下面這條命令下載了Kitex可執行文件(用於腳手架生成):

go install github.com/cloudwego/kitex/tool/cmd/kitex

image-20220522202248260

kitex是一個可執行文件,因為go install做了兩件事(編譯+安裝),它將github.com/cloudwego/kitex/tool/cmd/kitex目錄下的main.go及其依賴庫編譯成了一個可執行文件,再將其下載到本地的$GOPATH/bin路徑下。

換句話說,你完全可以通過下面這命令將整個kitex依賴庫全部下載下來:

go get github.com/cloudwego/kitex@latest

然後進入github.com/cloudwego/kitex/tool/cmd/kitex 目錄去手動執行go build命令,根據目錄名(包名)將其編譯成一個可執行文件kitex,再將其移動到$GOPATH/bin目錄下,就能復現上面go install的工作。

go build -o ~/go/bin/kitexx # 使用-o參數可以將編譯的可執行文件指定位置和名稱

比如我構建了一個功能強大的kitexx工具!(可以在終端中調用,只是它還沒有接受命令行參數的能力,別擔心!隨著源碼的分析我們將會擴展kitexx的功能!

image-20220523140039096

image-20220522210247128

image-20220523135508309

先回歸Kitex,go install之後,我們在命令行中輸入下面的命令就可以實現項目腳手架程式碼的生成:

kitex -module example -service example echo.thrift

kitex就指代$GOPATH/bin下的可執行文件kitex,後面的-module xxx..都是指定的命令行參數。

下面讓我們看一下kitex負責腳手架程式碼生成的可執行文件編譯前的程式碼

# 使用tree命令查看$GOPATH/pkg/mod/github.com/cloudwego/[email protected]/tool/cmd/kitex的目錄結構,也就是這兩個文件中編寫了接受命令行參數、創建服務腳手架的核心程式碼
.
└── kitex
    ├── args.go
    └── main.go

分析main.go的初始化函數

下面這是main.goinit函數,看到初始化過程就是args調用了addExtraFlag方法,並且傳入了一個extraFlag

image-20220523141555269

那麼我們來看一下extraFlag的結構,通過首行注釋得知,這個結構是用於添加與程式碼生成無關的flag的(每一個flag可以理解成kitex工具命令行需要解析的參數,後面會講)。

結構體有兩個成員函數,第二個用於檢查需要添加的flag的合法性,第一個用於添加flags到FlagSetFlagSet出自於Go標準命令行解析庫flag

image-20220523141422644

看到這你大致能猜測解析命令行的工作最終還是落到了Go標準庫頭上,只是kitex在此基礎上訂製了自己需要的功能。

這篇文章介紹了命令行解析庫flag的使用://segmentfault.com/a/1190000021143456

那麼讓我們看一下FlagSet結構的源碼(這裡沒放出來),注釋描述FlagSet是一個用於存放flags的集合,並介紹了Usage的作用和觸發條件。

這時候我們已經深入源碼第三層了,先不急著深入,容易迷失方向。先回到最初init函數中,我們已經知道apply方法用於添加flagFlagSet中,那麼是如何添加的呢?

image-20220523144634677

我們來看一下FlagSetBoolVar方法源碼,newBoolValue的作用是將value的值賦給p然後返回(可以點進去看源碼),BoolVar方法的作用由注釋得知,為了定義一個bool類型的flag(由name、value、usage定義)

image-20220523145131221

然後調用了f.Var方法,猜測是用於將這個定義的bool類型的flag添加入FlagSet集合,看一下源碼。首先檢測要添加的flag的name不能以-或者=開頭,然後判斷map中是否存在相同名稱的flag,如果有則panic,然後按步添加flag到f.formal中(map[string]*Flag

image-20220523150557620

現在大致明白,init函數的作用就是調用了args.addExtraFlag方法,添加了一個額外的不是用於程式碼生成的flag,至於check部分就是當遇到指定錯誤的時候需要終止程式。

os.Exit()指定狀態碼,0表示成功,1表示內部錯誤,2表示無效參數

image-20220523154901265

image-20220523155212805

到目前為止,init函數部分基本已經分析了一遍,你可能好奇,既然在初始化階段已經為FlagSet添加了一個和version相關flag其實並沒有完成添加,這裡先賣個關子!下面解釋),那麼FlagSet本身是在哪裡初始化的?

這裡我們留意到init函數上方的全局變數args,並且留意到args.addExtraFlag也是調用自args,那麼勢必要看一下arguments的源碼,看看能否找到FlagSet的初始化工作。

image-20220523155513290

我們的目標是找到一個類似NewFlagSet的函數,那麼就進入args.go使用command+f吧!果然找到了,而且只有一處!

image-20220523161149808

那麼再看這個buildFlags函數在哪被調用的,沒辦法,看來還得接著查一下parseArgs函數的調用情況。

image-20220523161243216

終於你在main.go的主函數里找到了,但是問題來了,main.go文件的init()初始化函數你分析了之後是給FlagSet添加flag的,而且應該是先於主函數體執行的,那此時FlagSet還沒初始化啊?這不是驚天大BUG? 事實上這就是上面我賣的那個關子

image-20220523161421082

我們再看一下args.addExtraFlag方法和args的結構體,事實上,初始化的時候只是將extraFlag創建了出來,加入了一個切片,真正為FlagSet添加flag必然是等到flag.NewFlagSet方法初始化FlagSet之後。

image-20220523161658075

image-20220523161842064

上面這個烏龍其實在閱讀源碼的時候很可能遇到,因為我們在沒有全面的視角的情況下,往往很多問題的出現只是缺少對源碼的熟悉,只有反覆推敲,才能逐漸梳理清楚。

豐富kitexx框架的功能

事實上,main.go文件的init函數初始化只添加了一個flag,說明了這個flag的name、value還有usage,但是並沒有涉及到自動化構建腳手架的工作,當然這部分我相信通過繼續閱讀main函數的其餘部分可以得到解答。但是考慮到篇幅原因,我打算將其放在下一篇文章中。

先來豐富一下我們的kitexx框架,為其添加解析命令行的功能。(現階段只是簡單使用flag標準庫的一些API,後續再作更多的解釋)。

flag庫也可以看這篇文章://segmentfault.com/a/1190000021143456

image-20220523170836765

將上述程式碼手動編譯到$GOPATH/bin目錄下,並且嘗試通過命令行運行kitexx,輸入事先添加好的兩個flag(bool類型的flag後面可以不加參數),實現用我們輸入的參數值替換b和s的默認值並列印。

image-20220523171152435

image-20220523171422670

小結

通過這篇文章,我們初步分析了kitex框架的腳手架程式碼生成工具的源程式碼的init函數。並且體驗了一下實現自己的命令行解析框架kitexx。

在後續的文章中將繼續分析main函數的剩餘部分,並繼續擴展kitexx框架的功能。

關注公眾號【程式設計師白澤】,將同步文章更新。

Tags: