【在主畫面加入捷徑】
       
【選擇語系】
繁中 简中

[Windows] 求生手冊:使用 Go 語言 (Golang) 取代 Python 當成腳本語言 (Scripting Language)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
【贊助商連結】

    Go 語言是非典型的腳本語言

    傳統上,用於處理日常事務的腳本語言 (scripting language) 有兩種:

    • 命令列環境自帶的語言:像 DOS 批次檔 (batch file)、PowerShell、shell 命令稿 (shell script) 等
    • 高階通用型直譯語言:像 Perl、Python、Ruby 等

    命令列腳本顯而易見的缺點是平台相容性很差;像是 Windows 下使用批次檔或 PowerShell,類 Unix 系統使用 Bash 或 Zsh 等。另外,命令列腳本沒有函式庫的概念,需依賴命令列工具來完成任務。在類 Unix 系統上,命令列工具很豐富,使用命令列腳本相當方便;但在 Windows 上,命令列工具相對缺乏,命令列腳本能執行的任務就沒那麼多。

    PowerShell 原本是 Windows 下的腳本語言,現在也有類 Unix 系統的版本;但在類 Unix 系統上 PowerShell 仍非主流,目前筆者仍將其視為 Windows 限定的方案。

    使用 Python 或其他的直譯語言做為腳本語言是較佳的選擇,筆者在這篇文章有討論過,有興趣的讀者可以看一看。基本上,使用 Python 沒什麼大問題;我們在本文中先討論為什麼使用 Go 語言 (golang) 做為腳本語言,再回頭和 Python 比較。

    像腳本語言般簡單的編譯語言

    Go 是編譯語言,但 Go 官方團隊刻意讓 Go 語言簡單易用。包括 (但不限於) 以下特性:

    • 語法上像直譯語言般簡單
    • 快速編譯,讓開發迭代變快
    • 可以用 go run 指令即時 (on the fly) 執行 Go 程式碼
    • 跨平台的標準函式庫
    • 可產生靜態連結執行檔,易於發佈

    結合這些特性,我們可以把 Go 語言當成另類的腳本語言,Go 程式碼當成另類的命令稿,代替 Python (或其他直譯語言) 做為處理日常事務的腳本語言。

    Python 轉執行檔容易失敗

    基本上,Python 可說是近年來最合用的腳本語言 (之一),有跨平台、豐富的函式庫、大量的教學資料等優點。然而,相對於 Go 語言來說,Python 有以下的議題:

    • 動態型別,較不易檢查錯誤
    • 使用 Python 命令稿需重建整個運行環境
    • 承上,Python 命令稿轉執行檔的軟體不一定會成功
    • Python 命令稿無法保護程式碼

    重建 Python 運行環境不是什麼了不起的事,Python 社群的習慣是使用 requirements.txt 做為相依性的標註。但不一定每個 Python 社群套件都那麼好裝,這個議題在 Windows 上尤其顯著。

    著眼於發佈 Python 命令稿的議題,有些聰明的開發者開發出一些將 Python 命令稿轉執行檔的軟體。這些方案聽起來很好很強大,但轉換的過程不保證總是能成功。與其要在寫了 Python 命令稿後才在想能不能轉換成功,為什麼不用一個一開始就能編譯成執行檔的語言呢?

    如果這隻程式要給客戶,但我們想保護程式碼,這時候使用編譯語言對程式碼的保護性會好得多。

    當然,筆者並不覺得 Python 有什麼問題,這些特性都是相對的考量,而非絕對的標準。如果這些議題都不是議題,繼續使用 Python 仍是個好選擇。

    使用情境:轉換網站文章

    在本節中,我們以一個應用情境來說明寫 Go 「命令稿」的時機。

    我們有兩佪靜態網站產生器產生的網站,兩個網站各自以 Git 來管理;這兩個網站內容是相同的,只是分別使用正體中文 (zh-TW) 和簡體中文 (zh-CN) 來呈現。

    當我們要發布文章 (post) 時,我們會先在正體中文分站上撰寫文章並發布。接著,將文章 (Markdown 檔案) 轉到簡體中文分站,以我們預寫好的程式將文章轉為簡體中文,再重新發布到簡體中文分站上。這個動作蠻機械化的,而且算特殊需求,不太可能在網路上找到立即可用的程式碼,這時候就適合撰寫 Go 程式來簡化這個任務。

    這裡先假定我們已經寫好 Go 程式碼了,該檔案為 update.go 。為了簡化 Windows 終端機的指令,我們額外寫了一個 update.bat ,其內容如下:

    go run update.go %1
    

    基本上這個批次檔只是把第一個參數帶給 Go 程式。

    同理,為了在類 Unix 系統上使用這隻 Go 程式,我們額外寫了 update.sh ,其內容如下:

    #!/bin/sh
    
    go run update.go $1
    

    這個 shell 命令稿和上述批次檔做一樣的事,只是換不同的語言。

    在這個情境中,命令列腳本只是用來傳遞參數和簡化指令,實際執行動作的「命令稿」是 Go 程式。雖然命令列腳本只能用在特定平台,但 Go 程式碼是跨平台的,所以這些程式在不同平台都可使用。

    為什麼我們在這裡要用 go run 執行 Go 程式而不直接生成執行檔呢?因為我們這個專案會拿到不同的系統上執行,我們不希望留下多餘的執行檔。使用 go run 執行 Go 程式,程式會即時執行;而且 Go 語言即時執行的速度很快,使用起來和直譯語言差不了多少。

    程式碼展示:轉換文章的 Go 語言命令稿

    有些讀者可能不太會寫處理日常事務的腳本,其實這類腳本的撰寫原則相當簡單,只要將想執行的任務拆解成許多步驟,再將每個步驟寫成相對應的程式碼即可。我們承接上一節的情境,撰寫一個 update.go 「命令稿」。

    我們先來拆解這隻程式運行的步驟:

    • 讀入正體中文分站的文章
    • 將相同的內容寫入簡體中文分站
    • 呼叫外部程式將該篇文章轉為簡體中文
    • 移除備分檔 (backup file),此處備份檔以 .bak 結尾
    • 將轉換後的文章加入 Git 專案
    • 將更動後的 Git 專案推送 (push) 到遠端站台

    只要把這個過程寫成程式碼,日後就可以自動執行,簡化工作流程。可參考以下附有註解的範例程式碼:

    package main
    
    import (
    	"io/ioutil"
    	"log"
    	"os"
    	"os/exec"
    	"path/filepath"
    	"strings"
    )
    
    func main() {
    	twSite := "twSite"
    	cnSite := "cnSite"
    
    	// Get the page path.
    	args := os.Args[1:]
    	if len(args) < 1 {
    		log.Fatal("No valid page")
    	}
    	page := args[0]
    
    	// Update TW site repo.
    	twSitePull := exec.Command("git", "pull")
    	err := twSitePull.Run()
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// Get current working directory.
    	cwd, err := os.Getwd()
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// Get absolute page path for TW and CN site.
    	prefix := filepath.Join(cwd, "..")
    	twPagePath := filepath.Join(prefix, twSite, page)
    	cnPagePath := filepath.Join(prefix, cnSite, page)
    
    	// Copy page from TW site to CN site.
    	input, err := ioutil.ReadFile(twPagePath)
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	err = ioutil.WriteFile(cnPagePath, input, 0644)
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// Change working directory to CN site.
    	cnSitePath := filepath.Join(prefix, cnSite)
    	err = os.Chdir(cnSitePath)
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// Update CN site repo.
    	cnSitePull := exec.Command("git", "pull")
    	err = cnSitePull.Run()
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// Convert the characters of the page from TW to CN.
    	convert := exec.Command("perl", "-00", "-p", "-i.bak", "zhConvert.pl", cnPagePath)
    	err = convert.Run()
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// Remove backup file.
    	bakPagePath := filepath.Join(prefix, cnSite, strings.Join([]string{page, ".bak"}, ""))
    	err = os.Remove(bakPagePath)
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// Add the update page to CN site repo.
    	gitAdd := exec.Command("git", "add", ".")
    	err = gitAdd.Run()
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// Commit the change.
    	gitCommit := exec.Command("git", "commit", "-m", "Update a post")
    	err = gitCommit.Run()
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// Push to remote site.
    	gitPush := exec.Command("git", "push")
    	err = gitPush.Run()
    	if err != nil {
    		log.Fatal(err)
    	}
    }
    

    實際的程式碼會比較長,因為要處理一些細微的議題,像是路徑 (file path) 轉換及錯誤處理 (error handling) 等。這裡不詳述程式碼,有興趣的讀者可試著自行閱讀範例程式碼。

    由於我們的程式依賴外部軟體,包括 Perl 和 Git 等,即使將程式碼轉為執行檔也無法直接使用,還是得重新部署環境才能使用。當然,我們也可以使用 Python 或其他語言重寫這個例子,但這就算個人偏好,沒有絕對的對錯。

    結語

    雖然 Go 語言是編譯語言,但 Go 語言使用起來卻如同直譯語言般簡單,因此可用來當成另一個自動化日常事務的語言。對於相同的任務來說,Go 程式碼寫起來會比等效的直譯語言程式碼來得長,但 Go 語言有著靜態型別、運行快速、發佈容易等優點,故仍是一個值得考慮的腳本語言方案。

    【贊助商連結】
    【贊助商連結】
    【分類瀏覽】