前言
本文介紹使用 Golang (Go 語言) 當成腳本語言的方式,以實例來介紹。
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 仍是個好選擇。
使用 Golang 搭配 shell wrapper 當成系統腳本語言
Golang 本身是編譯語言,但以 go run
可即時執行該 Golang 程式,這種使用模式和傳統的腳本語言差異不大。但每次都打 go run
指令比較瑣碎,故會搭配殼程式腳本來簡化指令。
在本節中,我們以一個應用情境來說明寫 Golang 「命令稿」的方式。
我們有兩佪靜態網站產生器產生的網站,兩個網站各自以 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 命令稿和上述批次檔做一樣的事,只是換不同的語言。
在這個情境中,命令列腳本只是用來傳遞參數和簡化指令,實際執行動作的「命令稿」是 Golang 程式。雖然命令列腳本只能用在特定平台,但 Golang 程式碼是跨平台的,所以這些程式在不同平台都可使用。
為什麼我們在這裡要用 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 語言有著靜態型別、運行快速、發佈容易等優點,故仍是一個值得考慮的腳本語言方案。