最近,我開發了一個非常簡單的小工具,總的程式碼量 200 行不到。今天,簡單介紹下它。這是個什麼工具呢?它是一個用於視覺化展示 Go Module 依賴關係的工具。
為何開發為什麼會想到開發這個工具?主要有兩點原因:
一是最近經常看到大家在社群討論 Go Module。於是,我也花了一些時間研究了下。期間,遇到了一個需求,如何清晰地識別模組中依賴項之間的關係。一番了解後,發現了 go mod graph。
效果如下:
$ go mod graphgithub.com/poloxue/testmod golang.org/x/[email protected]/poloxue/testmod rsc.io/quote/[email protected]/poloxue/testmod rsc.io/[email protected]/x/[email protected] golang.org/x/[email protected]/quote/[email protected] rsc.io/[email protected]/[email protected] golang.org/x/[email protected]/[email protected] golang.org/x/[email protected]
每一行的格式是 模組 依賴模組,基本能滿足要求,但總覺得還是不那麼直觀。
二是我之前手裡有一個專案,包管理一直用的是 dep。於是,我也了解了下它,把官方文件仔細讀了一遍。其中的某個章節[1]介紹了依賴項視覺化展示的方法。
文件中給出的包關係圖:
但 ...,隨著之來的問題是,go mod 沒這個能力啊。怎麼辦?
如何實現先看看是不是已經有人做了這件事了。網上搜了下,沒找到。那是不是能自己實現?應該可以借鑑下 dep 的思路吧?
如下是 dep 依賴實現視覺化的方式:
# linux$ sudo apt-get install graphviz$ dep status -dot | dot -T png | display# macOS$ brew install graphviz$ dep status -dot | dot -T png | open -f -a /Applications/Preview.app# Windows> choco install graphviz.portable> dep status -dot | dot -T png -o status.png; start status.png
這裡展示了三大系統下的使用方式,它們都安裝了一個軟體包,graphviz。從名字上看,這應該是一個用來實現視覺化的軟體,即用來畫圖的。事實也是這樣,可以看看它的官網[2]。
再看下它的使用,發現都是通過管道命令組合的方式,而且前面的部分基本相同,都是 dep status -dot | dot -T png。後面的部分在不同的系統就不同了,Linux 是 display,MacOS 是 open -f -a /Applications/Preview.app,Window 是 start status.png。
稍微分析下就會明白,前面是生成圖片,後面是顯示圖片。因為不同系統的圖片展示命令不同,所以後面的部分也就不同了。
現在關心的重點在前面,即 dep status -dot | dot -T png 幹了啥,它究竟是如何實現繪圖的?大致猜測,dot -T png 是由 dep status -dot 提供的資料生成圖片。繼續看下 dep status -dot 的執行效果吧。
$ dep status -dotdigraph {\tnode [shape=box];\t2609291568 [label="github.com/poloxue/hellodep"];\t953278068 [label="rsc.io/quote\\nv3.1.0"];\t3852693168 [label="rsc.io/sampler\\nv1.0.0"];\t2609291568 -> 953278068;\t953278068 -> 3852693168;}
咋一看,輸出的是一段看起來不知道是啥的程式碼,這應該是 graphviz 用於繪製圖表的語言。那是不是還有學習下?當然不用啊,這裡用的很簡單,直接套用就行了。
試著分析一下吧,前面兩行可以不用關心,這應該是 graphviz 特定的寫法,表示要畫的是什麼圖。我們主要關心如何將資料以正確形式提供出來。
2609291568 [label="github.com/poloxue/hellodep"];953278068 [label="rsc.io/quote\\nv3.1.0"];3852693168 [label="rsc.io/sampler\\nv1.0.0"];2609291568 -> 953278068;953278068 -> 3852693168;
一看就知道,這裡有兩種結構,分別是為依賴項關聯 ID ,和通過 ID 和 -> 表示依賴間的關係。
按上面的猜想測試下,畫出一個簡單的圖, 用於表示 a 模組依賴 b 模組。
$ echo 'digraph {node [shape=box];1 [label="a"];2 [label="b"];1 -> 2;}' | dot -T png | open -f -a /Applications/Preview.app
效果如下:
看到這裡,是不是發現問題已經變得非常簡單了,只要將 go mod graph 的輸出轉化為類似的結構就能實現可視化了。
開發流程介紹接下來,開發這個小程式吧,我將它命名為 modv,即 module visible 的意思。專案原始碼位於 polxue/modv[3]
接收管道的輸入先從管道接收資料,go mod graph 通過管道傳遞資料給 modv。下面是 main 入口函式的程式碼。
func main() {\tinfo, err := os.Stdin.Stat()\tif err != nil {\t\tfmt.Println("os.Stdin.Stat:", err)\t\tos.Exit(1)\t}\tif info.Mode()&os.ModeNamedPipe == 0 {\t\tfmt.Println("The command is intended to work with pipes.")\t\tfmt.Println("Usage: go mod graph | modv | dot -T png -o")\t\tos.Exit(1)\t}\t...}抽象實現結構
先定義一個結構體,並大致定義整個流程。
type ModGraph struct {\tReader io.Reader // 讀取資料流}func NewModGraph(r io.Reader) *ModGraph { return &ModGraph{Reader: r}}// 執行資料的處理轉化func (m *ModGraph) Parse() error {}// 結果渲染與輸出func (m *ModGraph) Render(w io.Writer) error {}
再看下 go mod graph 的輸出吧,如下:
github.com/poloxue/testmod golang.org/x/[email protected]/poloxue/testmod rsc.io/quote/[email protected]...
每一行的結構是 模組 依賴項。現在的目標是要它解析成下面這樣的結構:
digraph { node [shape=box]; 1 github.com/poloxue/testmod; 2 golang.org/x/[email protected]; 3 rsc.io/quote/[email protected]; 1 -> 2; 1 -> 3;}
前面說過,這裡包含了兩種不同的結構,分別是模組與 ID 關聯關係,以及模組 ID 表示模組間的依賴關聯。為 ModGraph 結構體增加兩個成員表示它們。
type ModGraph struct {\tr io.Reader // 資料流讀取例項,這裡即 os.Stdin\t// 每一項名稱與 ID 的對映\tMods map[string]int\t// ID 和依賴 ID 關係對映,一個 ID 可能依賴多個項\tDependencies map[int][]int}
要注意的是,增加了兩個 map 成員後,記住要在 NewModGraph 中初始化下它們。
mod graph 輸出解析如何進行解析?
介紹到這裡,目標已經很明白了。就是要將輸入資料解析到 Mods 和 Dependencies 兩個成員中,實現程式碼都是 Parse 方法中。
為了方便進行資料讀取,首先,我們利用 bufio 基於 reader 建立一個新的 bufReader,
func (m *ModGraph) Parse() error {\tbufReader := bufio.NewReader(m.Reader)\t...
為便於按行解析資料,我們通過 bufReader 的 ReadBytes() 方法迴圈一行一行地讀取 os.Stdin 中的資料。然後,對每一行資料按空格切分,獲取到依賴關係的兩項。程式碼如下:
for {\trelationBytes, err := bufReader.ReadBytes('\\n')\tif err != nil {\t\tif err == io.EOF {\t\t\treturn nil\t\t}\t\treturn err\t} relation := bytes.Split(relationBytes, []byte(" ")) // module and dependency mod, depMod := strings.TrimSpace(string(relation[0])), strings.TrimSpace(string(relation[1])) ...}
接下來,就是將解析出來的依賴關係組織到 Mods 和 Dependencies 兩個成員中。模組是 ID 生成規則採用的是最簡單的實現方式,從 1 自增。實現程式碼如下:
modId, ok := m.Mods[mod]if !ok {\tmodId = serialID\tm.Mods[mod] = modId\tserialID += 1}depModId, ok := m.Mods[depMod]if !ok {\tdepModId = serialID\tm.Mods[depMod] = depModId\tserialID += 1}if _, ok := m.Dependencies[modId]; ok {\tm.Dependencies[modId] = append(m.Dependencies[modId], depModId)} else {\tm.Dependencies[modId] = []int{depModId}}
解析的工作到這裡就結束了。
渲染解析結果這個小工具還剩下最後一步工作要做,即將解析出來的資料渲染出來,以滿足 graphviz 工具的作圖要求。實現程式碼是 Render部分:
首先,定義一個模板,以生成滿足要求的輸出格式。
var graphTemplate = `digraph {node [shape=box];{{ range $mod, $modId := .mods -}}{{ $modId }} [label="{{ $mod }}"];{{ end -}}{{- range $modId, $depModIds := .dependencies -}}{{- range $_, $depModId := $depModIds -}}{{ $modId }} -> {{ $depModId }};{{ end -}}{{- end -}}}`
這一塊沒啥好介紹的,主要是要熟悉 Go 中的 text/template 模板的語法規範。為了展示友好,這裡通過 - 實現換行的去除,整體而言不影響閱讀。
接下來,看 Render 方法的實現,把前面解析出來的 Mods 和 Dependencies 放入模板進行渲染。
func (m *ModuleGraph) Render(w io.Writer) error {\ttempl, err := template.New("graph").Parse(graphTemplate)\tif err != nil {\t\treturn fmt.Errorf("templ.Parse: %v", err)\t}\tif err := templ.Execute(w, map[string]interface{}{\t\t"mods": m.Mods,\t\t"dependencies": m.Dependencies,\t}); err != nil {\t\treturn fmt.Errorf("templ.Execute: %v", err)\t}\treturn nil}
現在,全部工作都完成了。最後,將這個流程整合到 main 函式。接下來就是使用了。
使用體驗開始體驗下吧。補充一句,這個工具,我現在只測試了 Mac 下的使用,如有問題,歡迎提出來。
首先,要先安裝一下 graphviz,安裝的方式在本文開頭已經介紹了,選擇你的系統安裝方式。
接著是安裝 modv,命令如下:
$ go get github.com/poloxue/modv
安裝完成!簡單測試下它的使用。
以 MacOS 為例。先下載測試庫,github.com/poloxue/testmod。 進入 testmod 目錄執行命令:
$ go mod graph | modv | dot -T png | open -f -a /Applications/Preview.app
如果執行成功,將看到如下的效果:
完美地展示了各個模組之間的依賴關係。
一些思考本文是篇實踐性的文章,從一個簡單想法到成功呈現出一個可以使用的工具。雖然,開發起來並不難,從開發到完成,僅僅花了一兩個小時。但我的感覺,這確實是個有實際價值的工具。
還有一些想法沒有實現和驗證,比如一旦專案較大,是否可以方便的展示某個指定節點的依賴樹,而非整個專案。還有,在其他專案向 Go Module 遷移的時候,這個小工具是否能產生一些價值。
參考資料
[1]
某個章節: https://golang.github.io/dep/docs/daily-dep.html
[2]
官網: http://www.graphviz.org/documentation/
[3]
polxue/modv: https://github.com/poloxue/modv