一個小時學會用 Go 編寫命令列工具
阿新 • • 發佈:2020-12-09
![](https://i.loli.net/2020/12/08/DMPGzwab7TyWeo9.jpg)
# 前言
最近因為專案需要寫了一段時間的 `Go` ,相對於 `Java` 來說語法簡單同時又有著一些 `Python` 之類的語法糖,讓人大呼”真香“。
![](https://i.loli.net/2020/12/08/tEPj46risD7ahUH.jpg)
但現階段相對來說還是 `Python` 寫的多一些,偶爾還得回爐寫點 `Java` ;自然對 `Go` 也談不上多熟悉。
於是便利用週末時間自己做個小專案來加深一些使用經驗。於是我便想到了之前利用 `Java` 寫的一個部落格[小工具](https://github.com/crossoverJie/blog.toolbox)。
那段時間正值微博圖床大量圖片禁止外鏈,導致許多個人部落格中的圖片都不能檢視。這個工具可以將文章中的圖片備份到本地,還能將圖片直接替換到其他圖床。
![](https://i.loli.net/2019/05/08/5cd1cc7612c25.gif)
我個人現在是一直在使用,通常是在碼字的時候利用 `iPic` 之類的工具將圖片上傳到微博圖床(主要是方便+免費)。寫完之後再通過這個工具一鍵切換到 `[SM.MS](http://sm.MS)` 這類付費圖床,同時也會將圖片備份到本地磁碟。
改為用 `Go` 重寫為 `cli` 工具後使用效果如下:
![3-min.gif](https://i.loli.net/2020/12/08/enU3RDfikIKq9ao.gif)
# 需要掌握哪些技能
之所以選擇這個工具用 `Go` 來重寫;一個是功能比較簡單,但也正好可以利用到 `Go` 的一些特點,比如網路 IO、協程同步之類。
同時修改為命令列工具後是不是感覺更極客了呢。
再開始之前還是先為不熟悉 `Go` 的 `Javaer` 介紹下大概會用到哪些知識點:
- 使用和管理第三方依賴包(`go mod`)
- 協程的運用。
- 多平臺打包。
下面開始具體操作,我覺得即便是沒怎麼接觸過 `Go` 的朋友看完之後也能快速上手實現一個小工具。
## 使用和管理第三方依賴
- 還沒有安裝 Go 的朋友請參考官網自行安裝。
首先介紹一下 Go 的依賴管理,在版本 `1.11` 之後官方就自帶了依賴管理模組,所以在當下最新版 `1.15` 中已經強烈推薦使用。
它的目的和作用與 `Java` 中的 `maven`,`Python` 中的 `pip` 類似,但使用起來比 `maven` 簡單許多。
![](https://i.loli.net/2020/12/08/kxClhNWr13UZHzT.jpg)
根據它的使用參考,需要首先在專案目錄下執行 `go mod init` 用於初始化一個 `go.mod` 檔案,當然如果你使用的是 `GoLang` 這樣的 `IDE`,在新建專案時會自動幫我們建立好目錄結構,當然也包含 `go.mod` 這個檔案。
在這個檔案中我們引入我們需要的第三方包:
```go
module btb
go 1.15
require (
github.com/cheggaaa/pb/v3 v3.0.5
github.com/fatih/color v1.10.0
github.com/urfave/cli/v2 v2.3.0
)
```
我這裡使用了三個包,分別是:
- `pb`: progress bar,用於在控制檯輸出進度條。
- `color`: 用於在控制檯輸出不同顏色的文字。
- `cli`: 命令列工具開發包。
---
```go
import (
"btb/constants"
"btb/service"
"github.com/urfave/cli/v2"
"log"
"os"
)
func main() {
var model string
downloadPath := constants.DownloadPath
markdownPath := constants.MarkdownPath
app := &cli.App{
Flags: []cli.Flag{
&cli.StringFlag{
Name: "model",
Usage: "operating mode; r:replace, b:backup",
DefaultText: "b",
Aliases: []string{"m"},
Required: true,
Destination: &model,
},
&cli.StringFlag{
Name: "download-path",
Usage: "The path where the image is stored",
Aliases: []string{"dp"},
Destination: &downloadPath,
Required: true,
Value: constants.DownloadPath,
},
&cli.StringFlag{
Name: "markdown-path",
Usage: "The path where the markdown file is stored",
Aliases: []string{"mp"},
Destination: &markdownPath,
Required: true,
Value: constants.MarkdownPath,
},
},
Action: func(c *cli.Context) error {
service.DownLoadPic(markdownPath, downloadPath)
return nil
},
Name: "btb",
Usage: "Help you backup and replace your blog's images",
}
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
}
}
```
程式碼非常簡單,無非就是使用了 `cli` 所提供的 api 建立了幾個命令,將使用者輸入的 `-dp`、`-mp` 引數對映到 `downloadPath`、`markdownPath` 變數中。
之後便利用這兩個資料掃描所有的圖片,以及將圖片下載到對應的目錄中。
更多使用指南可以直接參考[官方文件](https://github.com/urfave/cli/blob/master/docs/v2/manual.md)。
可以看到部分語法與 `Java` 完全不同,比如:
- 申明變數時型別是放在後邊,先定義變數名稱;方法引數類似。
- 型別推導,可以不指定變數型別(新版本的 `Java` 也支援)
- 方法支援同時返回多個值,這點非常好用。
- 公共、私用函式利用首字母大小寫來區分。
- 還有其他的就不一一列舉了。
---
## 協程
緊接著命令執行處呼叫了 `service.DownLoadPic(markdownPath, downloadPath)` 處理業務邏輯。
這裡包含的檔案掃描、圖片下載之類的程式碼就不分析了;官方 `SDK` 寫的很清楚,也比較簡單。
重點看看 `Go` 裡的 `goroutime` 也就是協程。
我這裡使用的場景是每掃描到一個檔案就利用一個協程去解析和下載圖片,從而可以提高整體的執行效率。
```go
func DownLoadPic(markdownPath, downloadPath string) {
wg := sync.WaitGroup{}
allFile, err := util.GetAllFile(markdownPath)
wg.Add(len(*allFile))
if err != nil {
log.Fatal("read file error")
}
for _, filePath := range *allFile {
go func(filePath string) {
allLine, err := util.ReadFileLine(filePath)
if err != nil {
log.Fatal(err)
}
availableImgs := util.MatchAvailableImg(allLine)
bar := pb.ProgressBarTemplate(constants.PbTmpl).Start(len(*availableImgs))
bar.Set("fileName", filePath).
SetWidth(120)
for _, url := range *availableImgs {
if err != nil {
log.Fatal(err)
}
err := util.DownloadFile(url, *genFullFileName(downloadPath, filePath, &url))
if err != nil {
log.Fatal(err)
}
bar.Increment()
}
bar.Finish()
wg.Done()
}(filePath)
}
wg.Wait()
color.Green("Successful handling of [%v] files.\n", len(*allFile))
if err != nil {
log.Fatal(err)
}
}
```
就程式碼使用層面看起來是不是要比 `Java` 簡潔許多,我們不用像 `Java` 那樣需要維護一個 `executorService`,也不需要考慮這個執行緒池的大小,一切都交給 `Go` 自己去排程。
使用時只需要在呼叫函式之前加上 `go` 關鍵字,只不過這裡是一個匿名函式。
而且由於 `goroutime` 非常輕量,與 `Java` 中的 `thread` 相比佔用非常少的記憶體,所以我們也不需要精準的控制建立數量。
---
不過這裡也用到了一個和 `Java` 非常類似的東西:`WaitGroup`。
它的用法與作用都與 `Java` 中的 `CountDownLatch` 非常相似;主要用於等待所有的 `goroutime` 執行完畢,在這裡自然是等待所有的圖片都下載完畢然後退出程式。
使用起來主要分為三步:
- 建立和初始化 `goruntime` 的數量:`wg.Add(len(number)`
- 每當一個 `goruntime` 執行完畢呼叫 `wg.Done()` 讓計數減一。
- 最終呼叫 `wg.Wait()` 等待`WaitGroup` 的數量減為0。
對於協程 Go 推薦使用 `chanel` 來互相通訊,這點今後有機會再討論。
## 打包
核心邏輯也就這麼多,下面來講講打包與執行;這點和 `Java` 的區別就比較大了。
眾所周知,`Java` 有一句名言:`write once run anywhere`
這是因為有了 `JVM` 虛擬機器,所以我們不管程式碼最終運行於哪個平臺都只需要打出一個包;但 `Go` 沒有虛擬機器它是怎麼做到在個各平臺執行呢。
簡單來說 `Go` 可以針對不同平臺打包出不同的二進位制檔案,這個檔案包含了所有執行所需要的依賴,甚至都不需要在目標平臺安裝 `Go` 環境。
- 雖說 Java 最終只需要打一個包,但也得在各個平臺安裝相容的 `Java` 執行環境。
我在這裡編寫了一個 `Makefile` 用於執行打包:`make release`
```makefile
# Binary name
BINARY=btb
GOBUILD=go build -ldflags "-s -w" -o ${BINARY}
GOCLEAN=go clean
RMTARGZ=rm -rf *.gz
VERSION=0.0.1
release:
# Clean
$(GOCLEAN)
$(RMTARGZ)
# Build for mac
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD)
tar czvf ${BINARY}-mac64-${VERSION}.tar.gz ./${BINARY}
# Build for arm
$(GOCLEAN)
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOBUILD)
tar czvf ${BINARY}-arm64-${VERSION}.tar.gz ./${BINARY}
# Build for linux
$(GOCLEAN)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD)
tar czvf ${BINARY}-linux64-${VERSION}.tar.gz ./${BINARY}
# Build for win
$(GOCLEAN)
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD).exe
tar czvf ${BINARY}-win64-${VERSION}.tar.gz ./${BINARY}.exe
$(GOCLEAN)
```
可以看到我們只需要在 `go build` 之前指定系統變數即可打出不同平臺的包,比如我們為 `Linux` 系統的 `arm64` 架構打包檔案:
`CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build main.go -o btb`
便可以直接在目標平臺執行 `./btb` 執行程式。
# 總結
本文所有程式碼都已上傳 `Github`: [https://github.com/crossoverJie/btb](https://github.com/crossoverJie/btb)
感興趣的也可以直接執行安裝指令碼體驗。
```bash
curl -fsSL https://raw.githubusercontent.com/crossoverJie/btb/master/install.sh | bash
```
- 目前這個版本只實現了圖片下載備份,後續會完善圖床替換及其他功能。
---
這段時間接觸 `Go` 之後給我的感觸頗深,對於年紀 25 歲的 `Java` 來說,`Go` 確實是後生可畏,更氣人的是還趕上了雲原生這個浪潮,就更惹不起了。
一些以前看來不那麼重要的小毛病也被重點放大,比如啟動慢、佔用記憶體多、語法囉嗦等;不過我依然對這位賞飯吃的祖師爺保持期待,從新版本的 `Java` 可以看出也在積極改變,更不用說它還有無人撼動的龐大生態。
更多 `Java` 後續內容可以參考周志明老師的文章:[雲原生時代,Java危矣?](https://mp.weixin.qq.com/s/fVz2A-AmgfhF0sTk