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

2020-12-21 InfoQ技術實驗室

本文介紹了我如何嘗試使用 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 mainimport ( "fmt" "os")func main() { fmt.Println("Hello", os.Args[1]) os.Exit(42)}這時,使用 go run 命令結果有些奇怪:

$ go run example.go worldHello worldexit status 42$ echo $?1這個問題我們稍後會討論。

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

$ go build$ ./example worldHello world$ echo $?42此時調試該腳本的流程變成了:

$ vim ./example.go$ go build$ ./example.go worldHi world$ vim ./example.go$ go build$ ./example.go worldBye world而我期望達到的是這樣來運行腳本:

$ chmod +x example.go$ ./example.go worldHello world$ echo $?42而對應的工作流程是:

$ vim ./example.go$ ./example.go worldHi world$ vim ./example.go$ ./example.go worldBye 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 runpackage mainimport ( "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.gopackage 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 worldHi worldexit 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 worldHello worldexit 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 worldHi worldexit 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 worldHello world$ echo $?42太棒了!

警告:兼容性

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

$ go run example.gopackage main:example.go:1:1: illegal character U+0023 '#'這兩種選擇不能兼得,我們只能二選一:

使用 Shebang,並通過./example.go 方式運行腳本。或者移除 Shebang,使用 go run ./example.go 運行腳本。二者不可兼得!

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

最後一些想法

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

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

解釋:

類型:如何運行腳本。退出狀態碼:腳本執行後,是否設置了腳本的退出狀態碼。可執行:腳本是否可以通過 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/

相關焦點

  • 編寫Linux Shell腳本的最佳實踐
    畢竟shell腳本這個東西不算是正經的程式語言,他更像是一個工具,用來雜糅不同的程序供我們調用。因此很多人在寫的時候也是想到哪裡寫到哪裡,基本上都像是一段超長的main函數,不忍直視。同時,由於歷史原因,shell有很多不同的版本,而且也有很多有相同功能的命令需要我們進行取捨,以至於代碼的規範很難統一。
  • 世界上第一個C語言編譯器是怎麼編寫的?它為什麼能夠用C語言編寫?
    不知道大家有沒有想過一個問題:C語言編譯器為什麼能夠用C語言編寫? 今天小編就帶大家一探究竟! 在C語言被用作系統程式語言之前,Tomphson已經使用B語言編寫過作業系統。可見在C語言實現以前,B語言已經可以投使用了。 因此第一個C語言編譯器的原型完全可能是用B語言或者混合B語言與PDP彙編語言編寫的。
  • 生而混亂,被設計者不喜,歷久彌新的腳本語言JavaScript
    也就是說,JavaScript語言編寫的腳本是通過嵌入在HTML中來實現自身的功能的。JavaScript最初由網景公司(Netscape)的 Brendan Eich 設計,將其命名為LiveScript,後來網景公司在與Sun合作之後將其改名為JavaScript。
  • Pokemon go自動掛機腳本系統下載 口袋妖怪go掛機攻略
    雖然最近一直有小道消息,說Pokemon go即將開放國服,但事實是,目前為止(2016年8月8日),國內依然只有極少一部分地區可以玩,不少玩家被逼的只能使用飛機版遊戲玩。
  • web開發我更喜歡使用GO語言
    go語言在2007年9月設計,然後於2009年11月正式向外宣布推出使用,而且是開放原始碼項目,首先在Linux系統與go語言可能是Google開發的程式語言,迅速受到開發的關注並願意使用它,在2016年被TIOBE 選為「TIOBE 年最佳程式語言」,可想而知go能被開發者認可一定有它獨特的優勢,而我更喜歡使用GO語言web開發。
  • GO語言:協程——Goroutine
    Go語言的協程——Goroutine 進程(Process),線程(Thread),協程(Coroutine,也叫輕量級線程) 進程進程是一個程序在一個數據集中的一次動態執行過程,可以簡單理解為「正在執行的程序」,它是CPU資源分配和調度的獨立單位。
  • [Go 語言教程] Go 語言簡介
    Go是靜態強類型語言,是區別於解析型語言的編譯型語言。解析型語言——原始碼是先翻譯為中間代碼,然後由解析器對代碼進行解釋執行。編譯型語言——原始碼編譯生成機器語言,然後由機器直接執行機器碼即可執行。百度,阿里巴巴,oppo,vivo等微服務的開發模式下Go語言是新寵4 Go 擅長領域服務開發,web的api開發,分布式服務集群的開發容器docker是go開源的產品,k8s等這些都是基於go語言的對高並發
  • 為什麼選擇go語言
    這裡,我並沒有噴python的意思,它真的是一門好語言,我能夠通過它快速的構建原型,驗證我的想法,而且還一直在使用。只是在項目中,我們的一些疏忽,導致代碼不可控了,到了不得不重構的地步了。Why GO?前面說了我的語言經歷,以及項目到了重構地步的原因,但是為什麼會是go呢?
  • 60分鐘快速了解Go語言
    Go使用UTF-8編碼。m := map[string]int{"three": 3, "four": 4}m["one"] = 1// 在Go語言中未使用的變量在編譯的時候會報錯,而不是warning。// 下劃線 _ 可以使你「使用」一個變量,但是丟棄它的值。
  • 微軟:Excel公式是世界上使用最廣泛的程式語言
    LAMBDA 允許使用 Excel 自身的公式語言自定義功能,而過去,Excel 中需要通過 JS 等語言編寫自定義函數。同時,LAMBDA 還可以實現一個函數對另一個函數的調用,通過單個函數調用可以部署的功能將不受限制。對於新功能的意義,微軟稱:LAMBDA 將徹底改變在 Excel 中構建公式的方式。
  • Total Control手機控腳本環境中集成的 RingoJS 框架是什麼?
    為了讓用戶更高效地編寫和執行JS腳本,我們在Total Control腳本環境中集成了 RingoJS 框架,用戶可在腳本中直接引入並使用RingoJS的庫。ECMA JavaScript規範將該語言描述為面向對象的程式語言,用於在主機環境中執行計算和處理計算對象。每個用JavaScript編寫的應用程式都需要一個主機環境,它提供特定於環境的對象和API來執行I / O。 Ringo為JavaScript提供了這樣一個環境,並附帶一組模塊以使應用程式開發更容易。
  • vb程式語言是做什麼用的_VB程式語言有哪些
    電腦每做的一次動作,一個步驟,都是按照已經用計算機語言編好的程序來執行的,程序是計算機要執行的指令的集合,而程序全部都是用我們所掌握的語言來編寫的。所以人們要控制計算機一定要通過計算機語言向計算機發出命令。 目前通用的程式語言有兩種形式:彙編語言和高級語言。
  • Go 語言之父:四十年軟體開發巨變與 Go 的過去和未來
    Rob Pike 是 Go 語言核心作者之一。本文是針對 Rob 的一次專訪,話題涉及 Rob 四十年的職業生涯、Go 語言過去十年的發展及其未來。  與現今的很多開發人員不一樣,你幾十年前就在貝爾實驗室開始了自己的職業生涯。從你角度來看,你認為軟體開發方式最大的變化是什麼?
  • JS教程:第一講 JavaScript語言概況
    一、什麼是JavaScriptJavaScript是一種基於對象(Object)和事件驅動(Event Driven)並具有安全性能的腳本語言。使用它的目的是與HTML超文本標記語言、Java 腳本語言(Java小程序)一起實現在一個Web頁面中連結多個對象,與Web客戶交互作用。從而可以開發客戶端的應用程式等。
  • go語言好不好?可以用來做什麼?如何學好golang?
    該語言是由編寫、閱讀、調試和維護大型軟體系統的人所設計,這也是為他們自己所設計的。」這就是說,設計Go時,有一系列特殊的問題要解決,最初擁有C、Pascal、Modula和Oberon等高級程式語言的最佳特性的堅實基礎。它還牢記了Python、C++、Java等語言的有用特性,這些就是Go要解決的問題。「很多Go的新人都會要求從所知的語言中獲取功能。
  • RPG DOTA2 自定義地圖製作指南——腳本編寫
    DOTA2創意工坊工具的程序使用的是LUA程式語言,如果你對於編寫其他語言比較熟悉的話,那麼編寫LUA也會是一件很容易的事。   提示:在遊戲運行的時候,你能夠使用script_reload命令來重新載入你的代碼。
  • 未來後端語言的趨勢——go語言免費學習網站大推薦!
    一.易百教程易百教程網的go語言教程是我首先要推薦的,為什麼呢?它相比於其它go語言教程網站的內容,除了一樣詳實的教程外,還增加了go編程代碼實例,最適合初次學習go語言的人邊看教程,變根據實例敲代碼。
  • 2011年6月程式語言排行榜,Lua進入前十
    TIOBE發布了2011年6月份的程式語言排行榜,其中Lua語言延續上個月的增長勢頭,歷史上第一次進入排行榜前十位,Java、C
  • 最理想的語言之一:GO為何如此與眾不同?
    該語言是由編寫、閱讀、調試和維護大型軟體系統的人所設計,這也是為他們自己所設計的。」這就是說,設計Go時,有一系列特殊的問題要解決,最初擁有C、Pascal、Modula和Oberon等高級程式語言的最佳特性的堅實基礎。它還牢記了Python、C++、Java等語言的有用特性,這些就是Go要解決的問題。
  • 我們是如何讓伺服器從30臺縮減到2臺的:從Ruby遷移到Go語言
    我們都有多年的開發Java的經歷,曾經寫過很多東西只需要很少的資源就能處理大量負載,遠比Ruby on Rails的處理能力強的多,我知道我們可以做出很多改進。於是,接下來的問題變成了應該使用哪種語言?選擇一種語言我對任何新建議都持開放的態度,最不濟,我還可以重回到Java。Java是一個在很多方面(比如性能上)很棒的語言(是嗎?)