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