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

2022-05-24 12:01:59

前言

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

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

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

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

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

原始碼分析

初覽kitex命令列工具

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

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

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的功能!

先回歸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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

豐富kitexx框架的功能

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

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

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

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

小結

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

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

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