Let's Go! 認識 Go 語言
2 |
前陣子我分享過最近觀注的程式語言 - Rust,Rust 主打「在記憶體安全的前題下保有跟 C/C++ 一樣的超高效能」,歷經過 CrowdStrike C++ 程式的記憶體管理 Bug 引爆 7/19 全球大當機的震憾教育,大家現在應該更能理解 Rust 語言的價值跟使命了。
今天再來談另一個也很值得認識的程式語言 - Go!
為什麼要花時間認識 Go?我有個好理由,Go 在今年 2 月擠進 TIOBE 程式排行前十名並持續上升,目前排名第七(另外,才兩個月 Fortran 爬上第九),僅次於 JavaScript。前十名裡的其他程式語言我都認識,只剩 Go,之前雖然 Debug 過 Go 程式,但當時只求快掉解掉問題,對 Go 是圓的還是方的都沒概念,我感覺此刻必須補完這塊知識,人生才不會有缺憾。(謎:其實不知道也不會死吧?)
我對 Go 語言的印象很好,源於過去使用 Hugo、Gitea 的愉快經驗,單一執行檔、執行速度飛快,在我心中是門值得學習的程式語言。
我在 PluralSight 找到一堂不錯的入門課程:Go: The Big Picture by Mike Van Sickle (大推) (同一作者在 YouTube 上有部 6.5 小時的免費教學影片:Learn Go Programming - Golang Tutorial for Beginners,前 17 分鐘是簡介),以其為基礎,再參照一些額外資料,建立了對 Go 的基本認識。
以下是我的消化整理後的心得。
為什麼要多發明一種程式語言
Go 語言是 Google 在 2009 年推出的程式語言,三位設計者大有來頭:Robert Griesemer 是 V8 JavaScript 開發者;Rob Pike 待過貝爾實驗室,是 UNIX 小組成員,並跟 Ken Thompson 一起發明了 UTF-8;Ken Thompson 發明了 B 語言(C 語言前身),後來並跟 Dennis Ritchie (C 語言發明者) 一起拿到圖靈獎。
至於為什麼沒事還要再發明一個新語言?以下這張圖說明 Go 的定位。
圖片來源:How Golang Was Birthed a Transposition- Evolution of Go with time
若用好開發、編譯快、執行快三個維度分析程式語言的特性:C++ 是執行效能的王者無庸置疑,但其難學易砸鍋(想想上週 CrowdStrike 捅的簍子)及編譯耗時也舉世聞名;Python/JavaScript 為直譯式語言,易學好寫免編譯,但執行效率不佳;Java/C# 等程式語言能兼顧編譯及執行效率,但程式複雜度較高有一定學習門檻。Go 語言的問市,便是嘗試在這三者間取得平衡。
Go 的設計哲學
Go 是一個力求「簡單」的語言,從兩個小地方可見一斑:
- Go 全部就只有 25 個保留字(Reserved Word/Keyword) (Java 大約 50 個、C# 超過 100 個)
- Go 的迴圈寫法只有 for 一種,沒有 foreach、while、do until... 等變化型
基於 Keep It Simple and Stupid 的設計哲學,Go 寫不出花俏或超簡潔的語法,但開發者不需記憶太多語法招式,時時猶豫該用雙手劍還是長槍,從到到尾一把斧頭打通關,確實能節省腦力,專注於程式邏輯。
而 Go 具備以下特色:
- 強型別及靜態型別
- 仿效 C 語法風格
- 使用 GC 記憶體回收機制,降低開發難度,減少記憶體安全問題
- 完全編譯 (不像 Java/C# 依賴 JVM/Runtime),
- 強調快速編譯,Go 花了很多心力提升編譯效率
- 參照程式庫採靜態連結,輸出結果只有單一執行檔,部署簡單
- 跨平台,甚至可跨架構編譯,例如:在 Linux 編譯 Windows 執行檔或在 Windows 編譯 macOS 執行檔 參考
- 考量實務應用的大量平行處理需求,提供 Goroutine 功能簡化開發
有點值得一提的,Go 的程式寫法更像 C,而非 C++。
C++ 在 C 之上加入物件概念,之後衍生的 Java、C#、JavaScript 都沿襲類似的物件導向概念;而 Go 則選擇返樸歸真,延續 C 語言的風格,以函式為主,但因應新時代的高速運算、多核心、網路應用進行改良,內建對 HTTP/JSON/gRPC 等現代規格的支援,並強化平行運算,雖然也能透過 Struct、Composition、名稱大小寫控制變數是否可供其他 package 呼叫... 實現類似物件導向設計,但與 Java/C# 以類別、繼承為核心大不相同。
Go 擅長的應用情境
Go 在一些開發應用情境特別能展現威力:
- 雲端及網路服務
- 命令列介面程式(CLI)
- 雲端基礎架構 (Docker、Kubernetes、Terraform 都是用 Go 寫的,剩下就不用多解釋了)
來幾個範例瞧瞧
來寫點簡單範例體驗一下。
有 Github Copilot 加持,只寫 Hello World 好像說不過去,我決定挑戰再複雜一點點的小練習。
第一個範例是個產生 Guid 的 WebAPI,並可透過 ?count=n 指定產生個數,結果以 JSON 格式回傳。
使用以下指令新建模組,並參照 google 的 uuid 程式庫以產生 RFC 9562 規範 UUID。
go mod init webapi
go get github.com/google/uuid
新增一個 main.go 檔,透過 http.HandleFunc() 設定接收 Http.Request 時的回應邏輯,http.ListenAndServe("localhost:3000", nil)
則指定傾聽 3000 Port 執行 HTTP 服務(註:":3000" 可繫結所有 IP (127.0.0.1 及實體網卡 IP),但會觸發開啟防火牆提示)。newGuidHandler() 函式透過 http.Request 讀取 ?count=n
參數決定 Guid 個數;strconv.Atoi() 將字串轉數字,make([]string, times)
建立字串切片(Slice,與陣列不同,長度可變),用 for 迴圈產生 times 個 GUID;字串切片轉 JSON 則可透過 json.Marshal()
完成;最後將結果寫回 http.ResponseWriter,大功告成。
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"time"
"github.com/google/uuid"
)
func main() {
http.HandleFunc("/", newGuidHandler)
log.Fatal(http.ListenAndServe("localhost:3000", nil)) // ":3000" 可 Bind 所有 IP,但會觸發 Windows 防火牆確認
}
func newGuidHandler(w http.ResponseWriter, r *http.Request) {
count := r.URL.Query().Get("count") // 取得 URL 中的 count 參數
if count == "" { // 若 count 參數不存在,則預設為 1
count = "1"
}
times, err := strconv.Atoi(count) // 字串轉數字
if err != nil {
http.Error(w, "Invalid count value", http.StatusBadRequest)
return
}
uuids := make([]string, times) // 建立一個長度為 times 的 []string
for i := 0; i < times; i++ {
uuids[i] = generateGUID()
}
jsonResponse, err := json.Marshal(uuids) // 將 uuids []string 轉為 JSON 格式
if err != nil {
http.Error(w, "Failed to generate JSON response", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
time.Sleep(time.Second) // 延遲 1 秒回應
w.Write(jsonResponse)
}
func generateGUID() string {
return uuid.New().String()
}
使用 go run .
執行程式,用 PowerShell 簡單測試,我寫好第一個 Go WebAPI 練習。
接著來寫個 CLI 客戶端。
透過 os.Args 可取得執行 CLI 時傳入的參數,第一個參數為 URL,第二個參數執行次數為選擇性,預設為 10 次。http.Get()
可取得結果,使用 io.ReadAll(response.Body)
讀取內容,以 json.Unmarshal(body, &data)
還原回 string[],最終統計成功及失敗次數。
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Please provide a URL")
os.Exit(1)
}
url := os.Args[1]
times := 10
if len(os.Args) > 2 {
count, err := strconv.Atoi(os.Args[2])
if err != nil {
fmt.Println("Invalid count value")
os.Exit(1)
}
times = count
}
start := time.Now()
succCount := 0
errCount := 0
for i := 0; i < times; i++ {
_, err := fetchData(url) // _ 忽略回傳結果,否則會有變數未使用的警告
if err != nil {
errCount++
} else {
succCount++
}
}
elapsed := time.Since(start)
fmt.Println("Execution time:", elapsed)
fmt.Println("Error count:", errCount)
succRate := float64(succCount) / float64(times) * 100
fmt.Println("Success count:", succCount)
fmt.Printf("Success rate: %.2f%%\n", succRate)
}
func fetchData(url string) ([]string, error) {
response, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("error making request: %v", err)
}
defer response.Body.Close() // 類似 using,指定在 fetchData 函式結束時執行
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("error reading response: %v", err)
}
var data []string
err = json.Unmarshal(body, &data)
if err != nil {
return nil, fmt.Errorf("error parsing JSON: %v", err)
}
return data, nil
}
測試結果如下,由於每次呼叫時 WebAPI 端會故意等待一秒回應,故完成 20 次呼叫耗時 20 秒多一點。
可以輕鬆實現平行處理是 Go 標榜特色之一,Go 有個 Goroutine 功能,透過 go 關鍵字、sync.WaitGroup 可將循序作業轉為平行處理。我們修改一下 client 程式,將 for 迴圈內容改為平行執行。
// ...略...
start := time.Now()
succCount := 0
errCount := 0
var wg sync.WaitGroup
var mutex sync.Mutex
for i := 0; i < times; i++ {
wg.Add(1) // 執行計數器加 1
go func() {
defer wg.Done() // 確保 goroutine 結束時呼叫 Done 方法,計數器減 1
_, err := fetchData(url)
mutex.Lock() // 使用 lock 機制避免多個 goroutine 同時修改變數
if err != nil {
errCount++
} else {
succCount++
}
mutex.Unlock()
}()
}
wg.Wait()
elapsed := time.Since(start)
fmt.Println("Execution time:", elapsed)
// ...略...
改用 goroutine 平行執行 http.Get() 後,20 次呼叫只需一秒完成。
常用程式庫與框架
Go 的標準程式庫(Standard Library) std 功能相當完整,舉凡壓縮、加解密、HTTP、繪圖、Regular Expression、JSON、XML、CSV、Unicode 編碼... 幾乎日常處理會用到的功能都有。但在進階開發時,有一些好用的程式庫或框架可資利用:
- Go Kit - 開發微服務的好用框架
- Gin Web Framework - 網站框架,提供路由機制,支援多種回應資料格式,方便 RESTful API 開發
- Gorilla Web Toolkit - 開發網站的好用工具包
- Cobra - CLI 解析參數、命令的框架 (註:類似 .NET 的 System.CommandLine)
結語
完成簡易導覽,寫了兩隻程式試手感,本次 Go 語言體驗圓滿完成。
Comments
# by yoyo
go的陣列是指array or slice?
# by Jeffrey
to yoyo,[]string 應為切片(Slice)才對,Go 陣列的長度固定不能動態調整。謝謝指正。