一次使用 Go 語言編寫腳本的經歷

一次使用 Go 語言編寫腳本的經歷

本文介紹了我如何嘗試使用 Go 語言進行腳本編程的經歷。文中我將討論 Go 腳本的必要性,我們預期的表現以及可能的實現方式。在討論過程中,我講深入探討腳本、Shell 和 Shebang。最終,我們將會討論讓 Go 腳本工作的解決方案。

為什麼 Go 語言適合編寫腳本?

通常認為,Python 和 Bash 是熱門的腳本語言,而 C、C++ 和 Java 完全不能被用作腳本編程,有一些語言卻夾在其中。

Go 語言試用場景很多,從編寫 Web 服務器到流程管理,甚至有些人用作系統編程語言。在後文中,我將論證,除了上述這些場景外,Go 語言還可以簡單地用於編寫腳本。

是什麼讓 Go 語言適合編寫腳本?

  • Go 語言簡潔易讀,並且不太冗長。這使得編寫的腳本易於維護且相對較短。
  • Go 語言有許多可用於各種用途的庫。假設這些庫是穩定且經過測試的,這可以讓腳本簡潔且健壯。
  • 如果我的大多數代碼使用 Go 語言編寫,那麼我更傾向於使用 Go 作為我的腳本語言。當代碼由許多人協作維護,那麼使用一種大家都能完全掌控的語言會降低維護成本,即使是一些腳本。

Go 語言已經 99% 支持腳本

事實上,我已經可以使用 Go 語言來編寫腳本。這需要使用 Go 的 run 子命令:如果腳本名稱是 my-script.go,我們可以簡單的通過 go run my-script.go 來運行。

這裡,對於 go run 命令,我認為需要特別關注一下。讓我們詳細說明下。

Go 語言區別於 Bash 和 Python 的地方是後者通過解釋執行,既它們的腳本在讀取的時候執行。而對於 Go 語言,當用戶輸入了 go run,Go 編譯這個 Go 程序,然後再執行。因為 Go 編譯時間非常短,所以看上去像是解釋執行。值得提醒的是,很多人都說“go run 只是一個玩具”,但是如果我們需要腳本,同時也喜歡 Go 語言,那麼這個玩具就是我們想要的。

所以已經支持的很好了,對吧?

我們可以編寫腳本,並通過 go run 命令來執行。還有什麼問題呢?問題是我很懶,希望通過類似./my-script.go 的方式來運行腳本,而不是 go run my-script.go。

這裡我們討論一個簡單的腳本和 Shell 通過兩種方式進行交互:它從命令行獲取輸入數據,並設置退出狀態碼。二者並非所有可能的交互方式(除此之外還可以有環境變量、信號、標準輸入、標準輸出、標準錯誤等),但是 Shell 腳本中較困難的兩個。

這個腳本輸出“Hello”和從命令行獲取的第一個參數,並設置退出狀態碼為 42:

package main

import (
"fmt"
"os"
)

func main() {
fmt.Println("Hello", os.Args[1])
os.Exit(42)
}

這時,使用 go run 命令結果有些奇怪:

$ go run example.go world
Hello world
exit status 42
$ echo $?
1

這個問題我們稍後會討論。

這時候可以使用 go build 命令。這是通過 go build 命令執行該腳本的方式:

$ go build
$ ./example world
Hello world
$ echo $?
42

此時調試該腳本的流程變成了:

$ vim ./example.go
$ go build
$ ./example.go world

Hi world
$ vim ./example.go
$ go build
$ ./example.go world
Bye world

而我期望達到的是這樣來運行腳本:

$ chmod +x example.go
$ ./example.go world
Hello world
$ echo $?
42

而對應的工作流程是:

$ vim ./example.go
$ ./example.go world
Hi world
$ vim ./example.go
$ ./example.go world
Bye world

看上去很簡答是吧?

Shebang

類 UNIX 系統支持Shebang。Shebang 用於告訴 Shell 使用什麼解釋器來運行腳本。我們可以根據編寫腳本使用的語言來設置 Shebang 行。

通常來說,我們會使用env命令最為腳本執行器,這樣就無需再使用解釋器的絕對路徑。例如:可以設置 Shebang 為 #! /usr/bin/env python 讓 Python 解釋器來運行該腳本。當名稱為 example.py 的腳本有上述的 Shebang 行,同時它具有可執行屬性(可以通過 chmod +x example.py 命令添加)時,可以在 Shell 中輸入./example.py arg1 arg2 來運行。此時 Shell 會讀取 Shebang 行,然後開始鏈式反應:

Shell 開始運行 /usr/bin/env python example.py arg1 arg2。這實際就是 Shebang 行加上腳本名再加上額外的參數。該命令執行 /usr/bin/env,參數是 /usr/bin/env python example.py arg1 arg2。然後 env 命令調用 python 命令,執行 python example.py arg1 arg2。最後 python 運行 example.py 腳本,參數是 example.py arg1 arg2。

讓我們開始嘗試給 Go 腳本添加 Shebang。

1、 第一次幼稚的嘗試

我們首先設置一個幼稚的 Shebang 來使用 go run 執行這個腳本。加了 Shebang 之後的腳本看上去是這樣的:

#! /usr/bin/env go run
package main

import (
"fmt"
"os"
)

func main() {
fmt.Println("Hello", os.Args[1])
os.Exit(42)
}

然後嘗試運行一下,輸出為:

$ ./example.go
/usr/bin/env: ‘go run’: No such file or directory

發生了什麼?

Shebang 機制將 go run 整體作為 env 命令的一個參數了,而實際不存在這個命令。輸入 which "go run" 也會有類似的錯誤。

2 、第二次嘗試

一個可行的方案是將 Shebang 設置為 #! /usr/local/go/bin/go run。在我們嘗試之前,就可以會發現一個問題:go 二進制文件在不同系統路徑不同,寫死絕對路徑會導致腳本無法兼容安裝在其他位置的 go。另外一個解決方案是使用 alias gorun="go run" 來創建一個別名,之後就能把 Shebang 修改成 #! /usr/bin/env gorun。使用這種方式,我們需要在運行這個腳本的系統中都設置這個別名。

輸出:

$ ./example.go
package main:
example.go:1:1: illegal character U+0023 '#'

解釋:

從這個輸出來看,我們有一個好消息,同時也有一個壞消息,你想先聽哪個?我先來說好消息:-)

  • 好消息是這個方案成功了,執行腳本之後 go run 命令正常調用了。
  • 壞消息:井號。在許多腳本語言中,Shebang 開頭的井號會被當成註釋忽略。但是對 Go 語言編譯器來說,開頭的井號變成了“非法字符”。

3、 解決方案

當腳本不包含 Shebang 行時,不同的 Shell 會回退到不同的解析器。Bash 會使用自己來運行腳本,而 zsh 會回退到使用 sh。這給我們提供了一種解決方案,這也是StackOverflow上提到的一種解決方案。

由於 // 是 Go 語言中定義的註釋,而我們可以使用 //usr/bin/env 來替代 /usr/bin/env(在路徑分割符中,// == /),因此第一行可以設置成:

//usr/bin/env go run "$0" "$@"

結果:

$ ./example.go world
Hi world
exit status 42
./test.go: line 2: package: command not found
./test.go: line 4: syntax error near unexpected token `newline'
./test.go: line 4: `import ('
$ echo $?
2

解釋:

我們距離成功又近了一步:終於有了正確的輸出。但是輸出中還包含一些錯誤,同時狀態碼也不對。讓我們來看下到底發生了什麼。正如之前所說的,Bash 沒有找到任何 Shebang,因此選擇使用 bash ./example.go world 的方式來運行腳本(直接使用該命令會有相同輸出,你也可以試下)。非常有意思,直接使用 Bash 來運行 Go 文件 :-) 下一步,Bash 讀取腳本的第一行,然後運行該命令:/usr/bin/env go run ./example.go world。之前腳本中的“0”代表第一個參數,因此實際值是我們運行的腳本文件名。“

0”代表第一個參數,因此實際值是我們運行的腳本文件名。“@”表示命令行中的所有參數。在這個例子中會被替換成“world”。到目前位置,使用./example.go world,腳本使用了正確的命令行參數,並輸出了正確的值。

輸出中還有詭異的一行:“exit status 42”。這是什麼?如果我們自己嘗試下命令就會了解:

$ go run ./example.go world
Hello world
exit status 42
$ echo $?
1

這是 go run 命令通過標準錯誤輸出的。go run 命令屏蔽了狀態碼,然後返回了狀態碼 1。關於這個行為的討論,可以參見Github issue。

好了,那麼其他幾行輸出呢?這是 Bash 試圖解析 Go 源碼,但實際失敗了。

4 、解決方案優化

這個 StackOverflow 頁面建議在 Shebang 之後加上 ;exit “$?”。這會告訴 Bash 解釋器不要再繼續執行。

完整的 Shebang:

//usr/bin/env go run "$0" "$@"; exit "$?"

結果:

$ ./test.go world
Hi world
exit status 42
$ echo $?
1

基本上實現了:這裡實現了讓 Bash 使用 go run 命令執行腳本,然後立即退出,同時設置狀態碼為 go run 命令執行後的狀態碼。

更進一步,可以在 Shebang 行中添加一些命令,用於移除標準錯誤中的“退出狀態”內容,甚至解析該文本並作為整個腳本的返回碼。

然而:

  • 再增加 Bash 命令意味著冗長的 Shebang 行,這與最初期望的 #! /usr/bin/env go 相比過於複雜。
  • 記住這只是一種 hack 的方式,而我並不喜歡 hack。畢竟我們只是想用標準的 Shebang 機制。為什麼?因為這樣簡單、標準、優雅。
  • 這或多或少也是我想找一種更加方便的語言作為腳本語言(例如 Go)來替代 Bash 的原因。

幸運的是,我們有gorun

gorun 就是我們想要的。我們只需在 Shebang 中寫 #! /usr/bin/env gorun,並賦予腳本可執行權限。僅此而已,我們可以在 Shell 中執行,獲得期望的結果!

$ ./example.go world
Hello world
$ echo $?
42

太棒了!

警告:兼容性

當文件包含 Shebang 之後,Go 將無法編譯(和我們之前看見的一樣)。

$ go run example.go
package main:
example.go:1:1: illegal character U+0023 '#'

這兩種選擇不能兼得,我們只能二選一:

  • 使用 Shebang,並通過./example.go 方式運行腳本。
  • 或者移除 Shebang,使用 go run ./example.go 運行腳本。

二者不可兼得!

另外一個問題,是當腳本文件被放在 Go 工程中時,編譯器會發現這個 go 文件。雖然該文件並不是應用程序所需要的,也會導致編譯失敗。一個解決方案是移除.go 後綴,但是這樣就會無法使用類似 go fmt 等工具。

最後一些想法

本文討論了使用 Go 語言來編寫腳本的重要性,同時介紹了幾種方式來實現腳本運行。這裡有一些總結。

類型退出狀態碼可執行可編譯標準go run✘✘✔✔gorun✔✔✘✘// 解決方案✘✔✔✔

解釋:

  • 類型:如何運行腳本。
  • 退出狀態碼:腳本執行後,是否設置了腳本的退出狀態碼。
  • 可執行:腳本是否可以通過 chmod +x 設置可執行權限。
  • 可編譯:腳本是否可以通過 go build。
  • 標準:腳本是否需要標準庫之外的東西。

正如上表,目前沒有一種完美的解決方案。看上去最方便且問題最少的方式是使用 go run 命令。但是在我看來,這種方式太過“複雜”,而且無法“可執行”,同時退出狀態碼也不正確。這將會導致難以區分腳本是否正確執行。

因此,我認為 Go 語言在這個領域仍然有許多工作要做。我不認為讓語言支持忽略 Shebang 行會有什麼問題。這將會解決執行問題,但是類似這種變化可能不會被 Go 社區採納。

我的同事提醒我事實上 Shebang 行對於 Javascript 同樣也是非法的。但是在 Node.js 中,他們增加了一個跳過 Shebang函數,讓 Node 腳本可以在 Shell 中直接運行。(譯者注:由於原文時間比較久遠,在c2b01881dcb3bf302f9d83157e719cc5240a9042版本之後 Node.js 已經對源碼進行了重構,在702331be906fe58e0ef66c7b31c7d2aeb3af3421版本之後,原文提及的 stripShebang 函數已經被移除。)

如果 gorun 可以作為標準工具的一部分就更棒了,其他類似的還有 gofmt 和 godoc。

原文鏈接:

https://posener.github.io/go-shebang-story/

"


分享到:


相關文章: