一個小時學會用 Go 編寫命令行工具

前言

最近由於項目須要寫了一段時間的 Go ,相對於 Java 來講語法簡單同時又有着一些 Python 之類的語法糖,讓人大呼」真香「。linux

但現階段相對來講仍是 Python 寫的多一些,偶爾還得回爐寫點 Java ;天然對 Go 也談不上多熟悉。git

因而便利用週末時間本身作個小項目來加深一些使用經驗。因而我便想到了以前利用 Java 寫的一個博客小工具github

那段時間正值微博圖牀大量圖片禁止外鏈,致使許多我的博客中的圖片都不能查看。這個工具能夠將文章中的圖片備份到本地,還能將圖片直接替換到其餘圖牀。windows

我我的如今是一直在使用,一般是在碼字的時候利用 iPic 之類的工具將圖片上傳到微博圖牀(主要是方便+免費)。寫完以後再經過這個工具一鍵切換到 [SM.MS](http://sm.MS) 這類付費圖牀,同時也會將圖片備份到本地磁盤。api

改成用 Go 重寫爲 cli 工具後使用效果以下:bash

3-min.gif

須要掌握哪些技能

之因此選擇這個工具用 Go 來重寫;一個是功能比較簡單,但也正好能夠利用到 Go 的一些特色,好比網絡 IO、協程同步之類。markdown

同時修改成命令行工具後是否是感受更極客了呢。網絡

再開始以前仍是先爲不熟悉 GoJavaer 介紹下大概會用到哪些知識點:架構

  • 使用和管理第三方依賴包(go mod)
  • 協程的運用。
  • 多平臺打包。

下面開始具體操做,我以爲即使是沒怎麼接觸過 Go 的朋友看完以後也能快速上手實現一個小工具。app

使用和管理第三方依賴

  • 尚未安裝 Go 的朋友請參考官網自行安裝。

首先介紹一下 Go 的依賴管理,在版本 1.11 以後官方就自帶了依賴管理模塊,因此在當下最新版 1.15 中已經強烈推薦使用。

它的目的和做用與 Java 中的 mavenPython 中的 pip 相似,但使用起來比 maven 簡單許多。

根據它的使用參考,須要首先在項目目錄下執行 go mod init 用於初始化一個 go.mod 文件,固然若是你使用的是 GoLang 這樣的 IDE,在新建項目時會自動幫咱們建立好目錄結構,固然也包含 go.mod 這個文件。

在這個文件中咱們引入咱們須要的第三方包:

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: 命令行工具開發包。

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 參數映射到 downloadPathmarkdownPath 變量中。

以後便利用這兩個數據掃描全部的圖片,以及將圖片下載到對應的目錄中。

更多使用指南能夠直接參考官方文檔

能夠看到部分語法與 Java 徹底不一樣,好比:

  • 申明變量時類型是放在後邊,先定義變量名稱;方法參數相似。
  • 類型推導,能夠不指定變量類型(新版本的 Java 也支持)
  • 方法支持同時返回多個值,這點很是好用。
  • 公共、私用函數利用首字母大小寫來區分。
  • 還有其餘的就不一一列舉了。

協程

緊接着命令執行處調用了 service.DownLoadPic(markdownPath, downloadPath) 處理業務邏輯。

這裏包含的文件掃描、圖片下載之類的代碼就不分析了;官方 SDK 寫的很清楚,也比較簡單。

重點看看 Go 裏的 goroutime 也就是協程。

我這裏使用的場景是每掃描到一個文件就利用一個協程去解析和下載圖片,從而能夠提升總體的運行效率。

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

# 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: github.com/crossoverJi…

感興趣的也能夠直接運行安裝腳本體驗。

curl -fsSL https://raw.githubusercontent.com/crossoverJie/btb/master/install.sh | bash
複製代碼
  • 目前這個版本只實現了圖片下載備份,後續會完善圖牀替換及其餘功能。

這段時間接觸 Go 以後給個人感觸頗深,對於年紀 25 歲的 Java 來講,Go 確實是後生可畏,更氣人的是還遇上了雲原生這個浪潮,就更惹不起了。

一些之前看來不那麼重要的小毛病也被重點放大,好比啓動慢、佔用內存多、語法囉嗦等;不過我依然對這位賞飯吃的祖師爺保持期待,重新版本的 Java 能夠看出也在積極改變,更不用說它還有無人撼動的龐大生態。

更多 Java 後續內容能夠參考周志明老師的文章:雲原生時代,Java危矣?

相關文章
相關標籤/搜索