從DevOps到日常腳本:聊聊Go語言的多面性
2024年初,TIOBE編程語言排行榜上,Go再次進入了前十,并在之后又成功沖高至第七名。
Go語言的排名上升,至少在Reddit Go論壇[1]上帖子數(shù)量和在線人數(shù)上得到了體現(xiàn),盡管目前與Rust[2]熱度仍有差距,但可見Go的關注度在提升:
2024年國慶節(jié)假期某天下午的實時在線數(shù)對比
隨著Go語言人氣的上升,論壇中的問題也變得愈發(fā)多樣化。許多Gopher常常問及為何Go是DevOps語言[3]和Go適合用作腳本語言嗎[4]等問題,這些都反映了Go語言的多面性。
從最初的系統(tǒng)編程語言,到如今在DevOps領域的廣泛應用,再到一些場合被探索用作腳本語言,Go展現(xiàn)出了令人驚嘆的靈活性和適應性。在本篇文章中,我們將聚焦于Go語言在DevOps領域的應用以及它作為腳本替代語言的潛力,聊聊其強大多面性如何滿足這些特定場景的需求。
1. Go在DevOps中的優(yōu)勢
隨著DevOps的發(fā)展,平臺工程(Platform Engineering)[5]這一新興概念逐漸興起。在自動化任務、微服務部署和系統(tǒng)管理中,編程語言的作用變得愈發(fā)重要。Go語言憑借其高性能、并發(fā)處理能力以及能夠編譯成單一二進制文件的特點,越來越受到DevOps領域開發(fā)人員的青睞,成為開發(fā)DevOps工具鏈的重要組成部分。
首先,Go的跨平臺編譯能力使得DevOps團隊可以在一個平臺上編譯,然后在多個不同的操作系統(tǒng)和架構上運行,結合編譯出的單一可執(zhí)行文件的能力,大大簡化了部署流程,這也是很多Go開發(fā)者認為Go適合DevOps的第一優(yōu)勢:
$GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64 main.go
$GOOS=linux GOARCH=arm64 go build -o myapp-linux-arm64 main.go
$GOOS=darwin GOARCH=amd64 go build -o myapp-darwin-amd64 main.go
$GOOS=windows GOARCH=amd64 go build -o myapp-windows-amd64.exe main.go
其次,Go的標準庫仿佛“瑞士軍刀”,開箱即用,為DevOps場景提供了所需的豐富的網絡、加密和系統(tǒng)操作功能庫,大幅降低對外部的依賴,即便不使用第三方包生態(tài)系統(tǒng),也可以滿足大部分的DevOps功能需求。
此外,Go的goroutines和channels為處理高并發(fā)任務提供了極大便利,這在DevOps中也尤為重要。例如,以下代碼展示了如何使用goroutines并發(fā)檢查多個服務的健康狀態(tài):
func checkServices(services []string) {
var wg sync.WaitGroup
for _, service := range services {
wg.Add(1)
go func(s string) {
defer wg.Done()
if err := checkHealth(s); err != nil {
log.Printf("Service %s is unhealthy: %v", s, err)
} else {
log.Printf("Service %s is healthy", s)
}
}(service)
}
wg.Wait()
}
并且,許多知名的DevOps基礎設施、中間件和工具都是用Go編寫的,如Docker、Kubernetes、Prometheus等,集成起來非常絲滑。這些工具的成功進一步證明了Go在DevOps領域的適用性。
2. Go作為腳本語言的潛力
在傳統(tǒng)的DevOps任務中,Python和Shell腳本長期以來都是主力軍,它們(尤其是Python)以其簡潔的語法和豐富的生態(tài)系統(tǒng)贏得了DevOps社區(qū)的廣泛青睞。然而,傳統(tǒng)主力Python和Shell腳本雖然靈活易用,但在處理大規(guī)模數(shù)據(jù)或需要高性能的場景時往往力不從心。此外,它們的動態(tài)類型系統(tǒng)可能導致運行時錯誤,增加了調試難度。
隨著Go的普及,它的“超高性價比”逐漸被開發(fā)運維人員所接受:既有著接近于腳本語言的較低的學習曲線與較高的生產力(也得益于Go超快的編譯速度),又有著靜態(tài)語言的高性能,還有單一文件在部署方面的便利性。
下面是一個簡單的文件處理腳本,用于向大家展示Go的簡單易學:
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
file, err := os.Open("input.txt")
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "ERROR") {
fmt.Println(line)
}
}
}
這個示例雖然要比同等功能的Python或shell代碼行數(shù)要多,但由于Go的簡單和直觀,多數(shù)人都很容易看懂這段代碼。
此外,Go的靜態(tài)強類型系統(tǒng)可以在編譯時捕獲更多錯誤,避免在運行時的調試,提高了腳本在運行時的可靠性。
開發(fā)運維人員眼中的腳本語言,如Shell腳本和Python腳本,通常是直接基于源代碼進行解釋和運行的。實際上,Go語言同樣可以實現(xiàn)這一點,而其關鍵工具就是go run命令。這個命令允許開發(fā)者快速執(zhí)行Go代碼,從而使Go源碼看起來更像是“腳本”,下面我們就來看看go run。
3. go run:橋接編譯型語言與腳本語言的利器
我們知道go run命令實際上是編譯和運行的組合,它首先編譯源代碼,然后立即執(zhí)行生成的二進制文件。這個過程對用戶來說是透明的,使得Go程序可以像腳本一樣方便地運行。這一命令也大大簡化了Go程序的開發(fā)流程,使Go更接近傳統(tǒng)的腳本語言工作流。可以說,通過go run,Go語言向腳本語言的使用體驗更靠近了一步。
此外,go run與go build在編譯階段的行為并不完全相同:
- go run在運行結束后,不保留編譯后的二進制文件;而go build生成可執(zhí)行文件并保留。
- go run編譯時默認不包含調試信息,以減少構建時間;而go build則保留完整的調試信息。
- go run可以使用-exec標志指定運行環(huán)境,比如:
$go run -exec="ls" main.go
/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build1742641170/b001/exe/main
我們看到,如果設置了-exec標志,那么go run -exec="prog" main.go args編譯后的命令執(zhí)行就變?yōu)榱?prog a.out args"。go run還支持跨平臺模擬執(zhí)行,當GOOS或GOARCH與系統(tǒng)默認值不同時,如果在PATH路徑下存在名為"go_GOOS_$GOARCH_exec"的程序,那么go run就會執(zhí)行:
$go_$GOOS_$GOARCH_exec a.out args
比如:go_js_wasm_exec a.out args
- go run通常用于運行main包,在go module開啟的情況下,go run使用的是main module的上下文。go build可以編譯多個包,對于非main包時只檢查構建而不生成輸出
- go run還支持運行一個指定版本號的包
當指定了版本后綴(如@v1.0.0或@latest)時,go run會進入module-aware mode(模塊感知模式),并忽略當前目錄或上級目錄中的go.mod文件。這意味著,即使你當前的項目中存在依賴管理文件go.mod,go run也不會影響或修改當前項目的依賴關系,下面這個示例展示了這一點:
$go run golang.org/x/example/hello@latest
go: downloading golang.org/x/example v0.0.0-20240925201653-1a5e218e5455
go: downloading golang.org/x/example/hello v0.0.0-20240925201653-1a5e218e5455
Hello, world!
這個功能特別適合在不影響主模塊依賴的情況下,臨時運行某個工具或程序。例如,如果你只是想測試某個工具的特定版本,或者快速運行一個遠程程序包,而不希望它干擾你正在開發(fā)的項目中的依賴項,這種方式就很實用。
不過有一點要注意的是:go run的退出狀態(tài)并不等于編譯后二進制文件的退出狀態(tài),看下面這個示例:
// main.go成功退出
$go run main.go
Hello from myapp!
$echo $?
0
// main.go中調用os.Exit(2)退出
$go run main.go
Hello from myapp!
exit status 2
$echo $?
1
go run使用退出狀態(tài)1來表示其運行程序的異常退出狀態(tài),但這個值和真實的exit的狀態(tài)值不相等。
到這里我們看到,go run xxx.go可以像bash xxx.sh或python xxx.py那樣,以“解釋”方式運行一個Go源碼文件。這使得Go語言在某種程度上具備了腳本語言的特性。然而,在腳本語言中,例如Bash或Python等,用戶可以通過將源碼文件設置為可執(zhí)行,并在文件的首行添加適當?shù)慕忉屍髦噶睿瑥亩苯舆\行腳本,而無需顯式調用解釋器。這種靈活性使得腳本的執(zhí)行變得更加簡便。那么Go是否也可以做到這一點呢?我們繼續(xù)往下看。
4. Go腳本化的實現(xiàn)方式
下面是通過一些技巧或第三方工具實現(xiàn)Go腳本化的方法。對于喜歡使用腳本的人來說,最熟悉的莫過于shebang(即解釋器指令)。在許多腳本語言中,通過在文件的第一行添加指定的解釋器路徑,可以直接運行腳本,而無需顯式調用解釋器。例如,在Bash或Python腳本中,通常會看到這樣的行:
#!/usr/bin/env python3
那么Go語言支持shebang嗎? 是否可以實現(xiàn)實現(xiàn)類似的效果呢?我們下面來看看。
4.1 使用“shebang(#!)”運行Go腳本
很遺憾,Go不能直接支持shebang,我們看一下這個示例main.go:
#!/usr/bin/env go run
package main
import (
"fmt"
"os"
)
func main() {
s := "world"
if len(os.Args) > 1 {
s = os.Args[1]
}
fmt.Printf("Hello, %v!\n", s)
}
這一示例的第一行就是一個shebang解釋器指令,我們chmod u+x main.go,然后執(zhí)行該Go“腳本”:
$./main.go
main.go:1:1: illegal character U+0023 '#'
這個執(zhí)行過程中,Shell可以正常識別shebang,然后調用go run去運行main.go,問題就在于go編譯器視shebang這一行為非法語法!
常規(guī)的shebang寫法行不通,我們就使用一些trick,下面是改進后的示例:
//usr/bin/env go run $0 $@; exit
package main
import (
"fmt"
"os"
)
func main() {
s := "world"
if len(os.Args) > 1 {
s = os.Args[1]
}
fmt.Printf("Hello, %v!\n", s)
}
這段代碼則可以chmod +x 后直接運行:
$./main.go
Hello, world!
$./main.go gopher
Hello, gopher!
這是因為它巧妙地結合了shell腳本和Go代碼的特性。我們來看一下第一行:
//usr/bin/env go run $0 $@; exit
這一行看起來像是Go的注釋,但實際上是一個shell命令。當文件被執(zhí)行時,shell會解釋這一行,/usr/bin/env用于尋找go命令的路徑,go run @ 告訴go命令運行當前腳本文件(以及所有傳遞給腳本的參數(shù)@),當go run編譯這個腳本時,又會將第一行當做注釋行而忽略,這就是關鍵所在。最后的exit確保shell在Go程序執(zhí)行完畢后退出。如果沒有exit,shell會執(zhí)行后續(xù)Go代碼,那顯然會導致報錯!
除了上述trick外,我們還可以將Go源碼文件注冊為可執(zhí)行格式(僅在linux上進行了測試),下面就是具體操作步驟。
4.2 在Linux系統(tǒng)中注冊Go為可執(zhí)行格式
就像在Windows上雙擊某個文件后,系統(tǒng)打開特定程序處理對應的文件一樣,我們也可以將Go源文件(xxx.go)注冊為可執(zhí)行格式,并指定用于處理該文件的程序。實現(xiàn)這一功能,我們需要借助binfmt_misc。binfmt_misc是Linux內核的一個功能,允許用戶注冊新的可執(zhí)行文件格式。這使得Linux系統(tǒng)能夠識別并執(zhí)行不同類型的可執(zhí)行文件,比如腳本、二進制文件等。
我們用下面命令將Go源文件注冊到binfmt_misc中:
echo ':golang:E::go::/usr/local/bin/gorun:OC' | sudo tee /proc/sys/fs/binfmt_misc/register
簡單解釋一下上述命令:
- :golang::這是注冊的格式的名稱,可以自定義。
- E:::表示執(zhí)行文件的魔數(shù)(magic number),在這里為空,表示任何文件類型。
- go:::指定用于執(zhí)行的解釋器,這里是go命令。
- /usr/local/bin/gorun:指定用于執(zhí)行的程序路徑,這里是一個自定義的gorun腳本
- :OC:表示這個格式是可執(zhí)行的(O)并且支持在運行時創(chuàng)建(C)。
當你執(zhí)行一個Go源文件時,Linux內核會檢查文件的類型。如果文件的格式與注冊的格式匹配,內核會調用指定的解釋器(在這個例子中是gorun)來執(zhí)行該文件。
gorun腳本是我們自己編寫的,源碼如下:
#!/bin/bash
# 檢查是否提供了源文件
if [ -z "$1" ]; then
echo "用法: gorun <go源文件> [參數(shù)...]"
exit 1
fi
# 檢查文件是否存在
if [ ! -f "$1" ]; then
echo "錯誤: 文件 $1 不存在"
exit 1
fi
# 將第一個參數(shù)作為源文件,剩余的參數(shù)作為執(zhí)行參數(shù)
GO_FILE="$1"
shift # 移除第一個參數(shù),剩余的參數(shù)將會被傳遞
# 使用go run命令執(zhí)行Go源文件,傳遞其余參數(shù)
go run "$GO_FILE" "$@"
將gorun腳本放置帶/usr/local/bin下,并chmod +x使其具有可執(zhí)行權限。
接下來,我們就可以直接執(zhí)行不帶有"shebang"的正常go源碼了:
// main.go
package main
import (
"fmt"
"os"
)
func main() {
s := "world"
if len(os.Args) > 1 {
s = os.Args[1]
}
fmt.Printf("Hello, %v!\n", s)
}
直接執(zhí)行上述源文件:
$ ./main.go
Hello, world!
$ ./main.go gopher
Hello, gopher!
4.3 第三方工具支持
Go社區(qū)也有一些將支持將Go源文件視為腳本的解釋器工具,比如:traefik/yaegi[6]等。
$go install github.com/traefik/yaegi/cmd/yaegi@latest
go: downloading github.com/traefik/yaegi v0.16.1
$yaegi main.go
Hello, main.go!
yaegi還可以像python那樣,提供Read-Eval-Print-Loop功能,我們可以與yaegi配合進行交互式“Go腳本”編碼:
$ yaegi
> 1+2
: 3
> import "fmt"
: 0xc0003900d0
> fmt.Println("hello, golang")
hello, golang
: 14
>
類似的提供REPL功能的第三方Go解釋器還包括:cosmos72/gomacro[7]、x-motemen/gore[8]等,這里就不深入介紹了,感興趣的童鞋可以自行研究。
5. 小結
在本文中,我們探討了Go語言在DevOps和日常腳本編寫中的多面性。首先,Go語言因其高性能、并發(fā)處理能力及跨平臺編譯特性,成為DevOps領域的重要工具,助力于自動化任務和微服務部署。其次,隨著Go語言的普及,其作為腳本語言的潛力逐漸被開發(fā)運維人員認識,Go展現(xiàn)出了優(yōu)于傳統(tǒng)腳本語言的高效性和可靠性。
我們還介紹了Go腳本的實現(xiàn)方式,包括使用go run命令,它使得Go程序的執(zhí)行更像傳統(tǒng)腳本語言,同時也探討了一些技巧和工具,幫助開發(fā)者將Go源碼文件作為可執(zhí)行腳本直接運行。通過這些探索,我們可以看到Go語言在現(xiàn)代開發(fā)中的靈活應用及其日益增長的吸引力。
隨著AI能力的飛速發(fā)展,使用Go編寫一個日常腳本就是分分鐘的事情,但Go的特性讓這樣的腳本具備了傳統(tǒng)腳本語言所不具備的并發(fā)性、可靠性和性能優(yōu)勢。我們有理由相信,Go在DevOps和腳本編程領域的應用將會越來越廣泛,為開發(fā)者帶來更多的可能性和便利。
6. 參考資料
- Using Go as a scripting language in Linux[9] - https://blog.cloudflare.com/using-go-as-a-scripting-language-in-linux/
- Go as a Scripting Language[10] - https://www.infoq.com/news/2020/04/go-scripting-language/
- Go compared to Python for small scale system administration scripts and tools[11] - https://utcc.utoronto.ca/~cks/space/blog/sysadmin/SysadminGoVsPython
參考資料
[1] Reddit Go論壇: https://www.reddit.com/r/golang/
[2] Rust: https://tonybai.com/tag/rust
[3] 為何Go是DevOps語言: https://www.reddit.com/r/golang/comments/1fqwbv0/why_is_golang_the_language_of_devops/
[4] Go適合用作腳本語言嗎: https://www.reddit.com/r/golang/comments/1ftpk2m/do_you_use_go_for_scripts/
[5] 平臺工程(Platform Engineering): https://en.wikipedia.org/wiki/Platform_engineering
[6] traefik/yaegi: https://github.com/traefik/yaegi
[7] cosmos72/gomacro: https://github.com/cosmos72/gomacro
[8] x-motemen/gore: https://github.com/x-motemen/gore
[9] Using Go as a scripting language in Linux: https://blog.cloudflare.com/using-go-as-a-scripting-language-in-linux/
[10] Go as a Scripting Language: https://www.infoq.com/news/2020/04/go-scripting-language/
[11] Go compared to Python for small scale system administration scripts and tools: https://utcc.utoronto.ca/~cks/space/blog/sysadmin/SysadminGoVsPython