前陣子我分享過最近觀注的程式語言 - Rust,Rust 主打「在記憶體安全的前題下保有跟 C/C++ 一樣的超高效能」,歷經過 CrowdStrike C++ 程式的記憶體管理 Bug 引爆 7/19 全球大當機的震憾教育,大家現在應該更能理解 Rust 語言的價值跟使命了。

今天再來談另一個也很值得認識的程式語言 - Go!

為什麼要花時間認識 Go?我有個好理由,Go 在今年 2 月擠進 TIOBE 程式排行前十名並持續上升,目前排名第七(另外,才兩個月 Fortran 爬上第九),僅次於 JavaScript。前十名裡的其他程式語言我都認識,只剩 Go,之前雖然 Debug 過 Go 程式,但當時只求快掉解掉問題,對 Go 是圓的還是方的都沒概念,我感覺此刻必須補完這塊知識,人生才不會有缺憾。(謎:其實不知道也不會死吧?)

我對 Go 語言的印象很好,源於過去使用 HugoGitea 的愉快經驗,單一執行檔、執行速度飛快,在我心中是門值得學習的程式語言。

我在 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 的定位。

thumbnail
圖片來源:How Golang Was Birthed a Transposition- Evolution of Go with time

若用好開發編譯快執行快三個維度分析程式語言的特性:C++ 是執行效能的王者無庸置疑,但其難學易砸鍋(想想上週 CrowdStrike 捅的簍子)及編譯耗時也舉世聞名;Python/JavaScript 為直譯式語言,易學好寫免編譯,但執行效率不佳;Java/C# 等程式語言能兼顧編譯及執行效率,但程式複雜度較高有一定學習門檻。Go 語言的問市,便是嘗試在這三者間取得平衡。

Go 的設計哲學

Go 是一個力求「簡單」的語言,從兩個小地方可見一斑:

  1. Go 全部就只有 25 個保留字(Reserved Word/Keyword) (Java 大約 50 個、C# 超過 100 個)
  2. Go 的迴圈寫法只有 for 一種,沒有 foreach、while、do until... 等變化型

基於 Keep It Simple and Stupid 的設計哲學,Go 寫不出花俏或超簡潔的語法,但開發者不需記憶太多語法招式,時時猶豫該用雙手劍還是長槍,從到到尾一把斧頭打通關,確實能節省腦力,專注於程式邏輯。

而 Go 具備以下特色:

  1. 強型別及靜態型別
  2. 仿效 C 語法風格
  3. 使用 GC 記憶體回收機制,降低開發難度,減少記憶體安全問題
  4. 完全編譯 (不像 Java/C# 依賴 JVM/Runtime),
  5. 強調快速編譯,Go 花了很多心力提升編譯效率
  6. 參照程式庫採靜態連結,輸出結果只有單一執行檔,部署簡單
  7. 跨平台,甚至可跨架構編譯,例如:在 Linux 編譯 Windows 執行檔或在 Windows 編譯 macOS 執行檔 參考
  8. 考量實務應用的大量平行處理需求,提供 Goroutine 功能簡化開發

有點值得一提的,Go 的程式寫法更像 C,而非 C++。

C++ 在 C 之上加入物件概念,之後衍生的 Java、C#、JavaScript 都沿襲類似的物件導向概念;而 Go 則選擇返樸歸真,延續 C 語言的風格,以函式為主,但因應新時代的高速運算、多核心、網路應用進行改良,內建對 HTTP/JSON/gRPC 等現代規格的支援,並強化平行運算,雖然也能透過 Struct、Composition、名稱大小寫控制變數是否可供其他 package 呼叫... 實現類似物件導向設計,但與 Java/C# 以類別、繼承為核心大不相同。

Go 擅長的應用情境

Go 在一些開發應用情境特別能展現威力:

  1. 雲端及網路服務
  2. 命令列介面程式(CLI)
  3. 雲端基礎架構 (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 編碼... 幾乎日常處理會用到的功能都有。但在進階開發時,有一些好用的程式庫或框架可資利用:

  1. Go Kit - 開發微服務的好用框架
  2. Gin Web Framework - 網站框架,提供路由機制,支援多種回應資料格式,方便 RESTful API 開發
  3. Gorilla Web Toolkit - 開發網站的好用工具包
  4. Cobra - CLI 解析參數、命令的框架 (註:類似 .NET 的 System.CommandLine)

結語

完成簡易導覽,寫了兩隻程式試手感,本次 Go 語言體驗圓滿完成。


Comments

# by yoyo

go的陣列是指array or slice?

# by Jeffrey

to yoyo,[]string 應為切片(Slice)才對,Go 陣列的長度固定不能動態調整。謝謝指正。

Post a comment