最新動態(tài)

一次用Go語言編寫的腳本經(jīng)歷

2024-12-23

作者 | Eyal

譯者 | 金靈杰

本文介紹了我嘗試使用 Go 語言進(jìn)行腳本編程的經(jīng)歷。文中我將討論 Go 腳本的必要性,我們預(yù)期的表現(xiàn)以及可能的實現(xiàn)方式。在討論過程中,我將深入探討腳本、Shell 和 Shebang。最終,我們將會討論讓 Go 腳本工作的解決方案。
1 為什么 Go 語言適合編寫腳本?

通常認(rèn)為,Python 和 Bash 是熱門的腳本語言,而 C、C++ 和 Java 完全不適合用于腳本編程。有一些語言則處于兩者之間。

Go 語言適用于多種場景,包括編寫 Web 服務(wù)器、流程管理和系統(tǒng)編程。在后文中,我將論證,除了上述這些場景外,Go 語言還可以用于簡單的腳本編寫。

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

Go 語言簡潔易讀,并且相對簡潔。這使得編寫的腳本易于維護(hù)并且較短。

Go 語言提供了大量可用于各種用途的庫。假設(shè)這些庫是穩(wěn)定且經(jīng)過測試的,這可以使腳本簡潔且健壯。

如果我大部分代碼都是用 Go 編寫的,那么我更愿意使用 Go 作為我的腳本語言。當(dāng)代碼由多人協(xié)作維護(hù)時,使用一種大家都熟悉的語言會減少維護(hù)成本,即使是一些腳本。

2 Go 語言已經(jīng) 99% 支持腳本

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

這里,對于 go run 命令,我認(rèn)為需要特別關(guān)注一下。讓我們詳細(xì)說明一下。

Go 語言不同于 Bash 和 Python 的地方在于,后者通過解釋執(zhí)行,即它們的腳本在讀取時執(zhí)行。而對于 Go 語言,當(dāng)用戶輸入了 go run,Go 編譯這個 Go 程序,然后再執(zhí)行。因為 Go 編譯時間非常短,所以看起來像是解釋執(zhí)行。需要注意的是,很多人說“go run 只是一個玩具”,但是如果我們需要腳本,并且喜歡 Go 語言,那么這個玩具正是我們所需要的。

3 所以已經(jīng)支持得很好了,對吧?

我們可以編寫腳本,并通過 go run 命令來執(zhí)行。還有什么問題嗎?問題是我不想那么麻煩,希望通過類似 ./my-script.go 的方式來運行腳本,而不是 go run my-script.go。

這里我們討論一個簡單的腳本和 Shell 通過兩種方式進(jìn)行交互:它從命令行獲取輸入數(shù)據(jù),并設(shè)置退出狀態(tài)碼。這兩個是 Shell 腳本中較為復(fù)雜的交互方式。

這個腳本輸出“Hello”和從命令行獲取的第一個參數(shù),并設(shè)置退出狀態(tài)碼為 42:


        package main

        import (
            "fmt"
            "os"
        )

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

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


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

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

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


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

此時調(diào)試該腳本的流程變成了:


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

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


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

而對應(yīng)的工作流程是:


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

看上去很簡單,對吧?

4 Shebang

類 Unix 系統(tǒng)支持 Shebang。Shebang 用于告訴 Shell 使用什么解釋器來運行腳本。我們可以根據(jù)編寫腳本使用的語言來設(shè)置 Shebang 行。

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

Shell 開始運行 /usr/bin/env python example.py arg1 arg2。這實際上就是 Shebang 行加上腳本名再加上額外的參數(shù)。該命令執(zhí)行 /usr/bin/env,參數(shù)是 /usr/bin/env python example.py arg1 arg2。然后 env 命令調(diào)用 python 命令,執(zhí)行 python example.py arg1 arg2。最后 python 運行 example.py 腳本,參數(shù)是 example.py arg1 arg2。

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

第一次幼稚的嘗試

我們首先設(shè)置一個幼稚的 Shebang 來使用 go run 執(zhí)行這個腳本。加了 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
    

發(fā)生了什么?

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

第二次嘗試

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

輸出:


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

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

好消息是這個方案成功了,執(zhí)行腳本之后 go run 命令正常調(diào)用了。

壞消息:井號。在許多腳本語言中,Shebang 開頭的井號會被當(dāng)成注釋忽略。但是對 Go 語言編譯器來說,開頭的井號變成了“非法字符”。

解決方案

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

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


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

結(jié)果:


        $ ./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 (
    

解釋:

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

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


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

這是 go run 命令通過標(biāo)準(zhǔn)錯誤輸出的。go run 命令屏蔽了狀態(tài)碼,然后返回了狀態(tài)碼 1。關(guān)于這個行為的討論,可以參見 Github issue。

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

解決方案優(yōu)化

這個 StackOverflow 頁面建議在 Shebang 之后加上 ;exit "$?"。這會告訴 Bash 解釋器不要再繼續(xù)執(zhí)行。

完整的 Shebang:


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

結(jié)果:


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

基本上實現(xiàn)了:這里實現(xiàn)了讓 Bash 使用 go run 命令執(zhí)行腳本,然后立即退出,同時設(shè)置狀態(tài)碼為 go run 命令執(zhí)行后的狀態(tài)碼。

更進(jìn)一步,可以在 Shebang 行中添加一些命令,用于移除標(biāo)準(zhǔn)錯誤中的“退出狀態(tài)”內(nèi)容,甚至解析該文本并作為整個腳本的返回碼。

然而:

再增加 Bash 命令意味著冗長的 Shebang 行,這與最初期望的 #! /usr/bin/env go 相比過于復(fù)雜。

記住這只是一種 hack 的方式,而我并不喜歡 hack。畢竟我們只是想用標(biāo)準(zhǔn)的 Shebang 機(jī)制。為什么?因為這樣簡單、標(biāo)準(zhǔn)、優(yōu)雅。

這或多或少也是我想找一種更加方便的語言作為腳本語言(例如 Go)來替代 Bash 的原因。

5 幸運的是,我們有 gorun

gorun 就是我們想要的。我們只需在 Shebang 中寫 #! /usr/bin/env gorun,并賦予腳本可執(zhí)行權(quán)限。僅此而已,我們可以在 Shell 中執(zhí)行,獲得期望的結(jié)果!


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

太棒了!

警告:兼容性

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


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

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

使用 Shebang,并通過 ./example.go 方式運行腳本。

或者移除 Shebang,使用 go run ./example.go 運行腳本。

二者不可兼得!

另外一個問題,是當(dāng)腳本文件被放在 Go 工程中時,編譯器會發(fā)現(xiàn)這個 go 文件。雖然該文件并不是應(yīng)用程序所需要的,也會導(dǎo)致編譯失敗。一個解決方案是移除 .go 后綴,但是這樣就會無法使用類似 go fmt 等工具。

6 最后一些想法

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

解釋:

類型:如何運行腳本。

退出狀態(tài)碼:腳本執(zhí)行后,是否設(shè)置了腳本的退出狀態(tài)碼。

可執(zhí)行:腳本是否可以通過 chmod +x 設(shè)置可執(zhí)行權(quán)限。

可編譯:腳本是否可以通過 go build。

標(biāo)準(zhǔn):腳本是否需要標(biāo)準(zhǔn)庫之外的東西。

正如上表,目前沒有一種完美的解決方案??瓷先プ罘奖闱覇栴}最少的方式是使用 go run 命令。但是在我看來,這種方式太過“復(fù)雜”,而且無法“可執(zhí)行”,同時退出狀態(tài)碼也不正確。這將會導(dǎo)致難以區(qū)分腳本是否正確執(zhí)行。

因此,我認(rèn)為 Go 語言在這個領(lǐng)域仍然有許多工作要做。我不認(rèn)為讓語言支持忽略 Shebang 行會有什么問題。這將會解決執(zhí)行問題,但是類似這種變化可能不會被 Go 社區(qū)采納。

我的同事提醒我事實上 Shebang 行對于 JavaScript 同樣也是非法的。但是在 Node.js 中,他們增加了一個跳過 Shebang 函數(shù),讓 Node 腳本可以在 Shell 中直接運行。

如果 gorun 可以作為標(biāo)準(zhǔn)工具的一部分就更棒了,其他類似的還有 gofmt 和 godoc。

原文鏈接:

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