在 Cloudflare 的人們都非常喜歡 Go 語言。我們在許多內部軟體項目以及更大的管道系統中使用它。但是,我們能否進入下一個層次並將其用作我們最喜歡的作業系統 Linux 的腳本語言呢?
為什麼考慮將 Go 作為腳本語言
簡短點的回答:為什麼不呢?Go 相對容易學習,不冗餘並且有一個強大的生態庫,這些庫可以重複使用避免我們從頭開始編寫所有代碼。它可能帶來的一些其他潛在優勢:
- 為你的 Go 項目提供一個基於 Go 的構建系統:go build 命令主要適用於小型自包含項目。更複雜的項目通常採用構建系統或腳本集。為什麼不用 Go 編寫這些腳本呢?
- 易於使用的非特權包管理:如果你想在腳本中使用第三方庫,你可以簡單的使用 go get 命令來獲取。而且由於拉取的代碼將安裝在你的 GOPATH 中,使用一些第三方庫並不需要系統管理員的權限(與其他一些腳本語言不同)。這在大型企業環境中尤其有用。
- 在早期項目階段進行快速的代碼原型設計:當您進行第一次代碼疊代時,通常需要進行大量的編輯,甚至進行編譯,而且您必須在 "編輯->構建->檢查" 循環中浪費大量的按鍵。相反,使用 Go,您可以跳過 build 部分,並立即執行源文件。
- 強類型的腳本語言:如果你在腳本中的某個地方有個小的輸入錯誤,大多數的腳本語言都會執行到有錯誤的地方然後停止。這可能會讓你的系統處於不一致的狀態(因為有些語句的執行會改變數據的狀態,從而污染了執行腳本之前的狀態)。使用強類型語言時,許多拼寫錯誤可以在編譯時被捕獲,因此有 bug 的腳本將不會首先運行。
Go 腳本的當前狀態
咋一看 Go 腳本貌似很容易實現 Unix 腳本的 shebang(#! ...) 支持。shebang 行)是腳本的第一行,以 #! 開頭,並指定腳本解釋器用於執行腳本(例如,#!/bin/bash 或 #!/usr/bin/env python),所以無論使用何種程式語言,系統都確切知道如何執行腳本。Go 已經使用 go run 命令支持 .go 文件的類似於解釋器的調用,所以只需要添加適當的 shebang 行(#!/usr/bin/env go run)到任何的 .go 文件中,設置好文件的可執行狀態,然後就可以愉快的玩耍了。
但是,直接使用 go run 還是有問題的。這篇牛 b 的文章 https://gist.github.com/posener/73ffd326d88483df6b1cb66e8ed1e0bd 詳細描述了圍繞 go run 的所有問題和潛在解決方法,但其要點是:
- go run 不能正確地將腳本錯誤代碼返回給作業系統,這對腳本很重要,因為錯誤代碼是多個腳本之間相互交互和作業系統環境最常見的方式之一。
- 你不能在有效的 .go 文件中創建一個 shebang 行,因為 Go 語言不知道如何處理以 # 開頭的行。而其他語言不存在這個問題,是由於 # 大多數情況下是一種注釋的方式,所以最後解釋器會忽略掉 shebang 行,但是 Go 注釋是以 // 開頭的並且在調用時運行會產生如下錯誤:
package main:
helloscript.go:1:1: illegal character U+0023 '#'
這篇文章描述了上述問題的幾種解決方法,包括使用一個自定義的包裝程序 gorun 作為解釋器,但是都沒有提供一個理想的解決方案。你可以:
- 必須使用非標準的 shebang 行,它以 // 開頭。這在技術上甚至不是 shebang 行,而是 bash shell 如何處理可執行文本文件的方式,所以這個解決方案是 bash 特有的。另外,由於 go run 的具體行為,這一行相當複雜並且不夠明顯(請參閱原始文章的示例)。
- 必須在 shebang 行中使用 gorun 自定義包裝程序,這很好,但是,最終得到的 .go 文件由於非法的 #字符而不能與標準 go build 命令編譯。
Linux 如何執行文件
OK,看起來 shebang 的方法並沒有為我們提供全面的解決方案。是否還有其他方式是我們可以使用的?讓我們仔細看看 Linux 內核如何執行二進位文件。 當你嘗試執行一個二進位/腳本(或任何有可執行位設置的文件)時,你的 shell 最後只會使用 Linux execve 系統調用,將它傳遞給二進位文件系統路徑,命令行參數和 當前定義的環境變量。 然後內核負責正確解析文件並用文件中的代碼創建一個新進程。 我們中的大多數人都知道 Linux (和許多其他類 Unix 作業系統)為其可執行文件使用 ELF 二進位格式。
然而,Linux 內核開發的核心原則之一是避免任何子系統的 「vendor/format lock-in」,這是內核的一部分。因此,Linux 實現了一個「可插拔」系統,它允許內核支持任何二進位格式 - 所有你需要做的就是編寫一個正確的模塊,它可以解析你選擇的格式。如果仔細研究內核原始碼,你會發現 Linux 支持更多的二進位格式。例如,最近的4.14 Linux 內核,我們可以看到它至少支持7種二進位格式(用於各種二進位格式的樹內模塊通常在其名稱中具有 binfmt_ 前綴)。值得注意的是 binfmt_script 模塊,它負責解析上面提到的 shebang 行並在目標系統上執行腳本(並不是每個人都知道 shebang 支持實際上是在內核本身而不是在 shell 或其他守護進程/進程中實現的)。
從用戶空間擴展受支持的二進位格式
但既然我們認為 shebang 不是 Go 腳本的最佳選擇,似乎我們需要別的東西。令人驚訝的是,Linux 內核已經有了一個「其他類型的」二進位支持模塊,它有一個貼切的名稱 binfmt_misc。該模塊允許管理員通過定義良好的 procfs 接口直接從用戶空間動態添加對各種可執行格式的支持,並且有詳細記錄。
讓我們按照文檔並嘗試為 .go 文件設置二進位格式描述。首先,該指南告訴您將特殊的 binfmt_misc 文件系統安裝到 /proc/sys/fs/binfmt_misc。如果您使用的是基於 systemd 的相對較新的 Linux 發行版,則很可能已經為您安裝了文件系統,因為默認情況下 system 會為此安裝特殊的 mount 和 automount 單元。 要仔細檢查,只需運行:
另一種方法是檢查 /proc/sys/fs/binfmt_misc 中是否有文件:正確安裝 binfmt_misc 文件系統將至少創建兩個名稱為 register 和 status 的特殊文件。
接下來,因為我們希望我們的 .go 腳本能夠正確地將退出代碼傳遞給作業系統,所以我們需要將定製的 gorun 包裝器作為我們的「解釋器」:
從技術角度上講,我們不需要將 gorun 移動到 /usr/local/bin 或任何其他系統路徑,而無論如何 binfmt_misc 都需要解釋器的完整路徑,但系統可以以任意權限運行此可執行文件,因此從安全視角來看限制文件訪問權限是一個好主意。
在這一點上,讓我們來建一個簡單的 go 腳本 helloscript.go 並驗證我們可以成功「解釋」它。腳本如下:
檢查參數傳遞和錯誤處理是否按預期工作:
現在我們需要告訴 binfmt_misc 模塊如何使用 gorun 執行 .go 文件。按照文檔中的描述我們需要配置如下字符串: :golang:E::go::/usr/local/bin/gorun:OC,意思是告訴系統:當遇到以 .go 為擴展名的可執行文件,請使用 /usr/local/bin/gorun 解釋器執行該文件。字符串末尾的 OC 標誌確保腳本將根據腳本本身設置的所有者信息和權限位執行,而不是在解釋器二進位文件上設置的那些位。這使 Go 腳本的執行行為與 Linux 中其他可執行文件和腳本的行為相同。
讓我們註冊我們新的 Go 腳本二進位格式:
如果系統成功註冊了,則應在 /proc/sys/fs/binfmt_misc 目錄下顯示新的 golang 文件。 最後,我們可以在本地執行我們的 .go 文件:
就這樣了!現在我們可以根據自己的喜好編輯 helloscript.go,並在下次執行文件時看到更改將立即可見。此外,和此前的 shebang 方式不同,我們可以隨時使用 go build 將文件編譯成真正的可執行文件。
via: https://blog.cloudflare.com/using-go-as-a-scripting-language-in-linux/
作者:Ignat Korchagin 譯者:shniu 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出