[技術雜談] 從 Go 語言 (Golang) 來看程式設計的精簡哲學

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    前言

    許多程式語言以豐富的語法特性和表達力著稱,但是也有像 Go 語言 (golang) 反其道而行,抱著少就是多 (less is more) 的精簡哲學。本文從一些 Go 語言的設計來看如何實踐精簡 (simplicity) 哲學。

    使用內隱知識來簡化專案

    在程式碼專案中,Go 語言的專案是數一數二簡單的,因為 Go 專案不使用專案設定檔,直接使用專案資料夾的資訊作為專案的內隱設定;像是 Go 應用程式專案編譯時預設會直接以目錄名稱作為執行檔的名稱。

    引入專案時也是以目錄作為命名空間的一部分,如下例:

    import "github.com/gotk3/gotk3/gtk"
    

    這個專案託管在 github.com 上,使用者是 gotk3 ,專案根目錄是 gotk3 ,實際的函式庫位於 gtk 子目錄中。

    我們在建立有一定規模的程式時,本來就會將專案集中在同一個目錄下。Go 語言重複使用這項內隱規則,讓專案設定精簡化。

    重複使用關鍵字

    不同迴圈 (loop) 使用不同的迭代方式,可以用計數器 (counter)、條件句 (conditional)、迭代器 (iterator) 等,像 Ruby 用 for 表示計數器迴圈,while 表示條件句迴圈,each 表示迭代器迴圈,使用了三個不同的語法。

    註:each 在 Ruby 中是方法 (method)。

    但在 Go 語言中,重複使用 for 表達不同的概念。像是以下的迴圈使用計數器:

    for i := 0; i < 10; i++ {
        // Repeat something here.
    }
    

    以下的迴圈使用條件句:

    // `isDone` is a bool.
    
    for !isDone {
        // Repeat something here.
    }
    

    以下的迴圈使用迭代器:

    // `arr` is an array.
    
    for _, e := range arr {
    	// Repeat something here.
    }
    

    這算不算 Go 語言的優點呢?其實是有爭議性的。喜歡這項特性的程式人會覺得這樣的程式碼很精簡,但也有些程式人覺得用不同的語法比較能區分意圖。

    只給予必要的功能

    C 或 C++ 的前置處理器 (preprocessor) 其實本身是一個小型巨集語言,可以用來做很多事,像是引入函式庫的頭文字檔 (header)、宣告和呼叫巨集 (macro)、條件編譯等,甚至有程式人用前置處理器模擬泛型 (generics) (參考這裡) 和程式語法 (像這個模擬 try … catch … 的例子)。

    但前置處理器本質上是字串代換,既沒有型別安全,更難以除錯。Go 語言沒有放入巨集這類的特性,而是利用 build constraint 來滿足條件編譯的需求,這就是一個少即是多的實例。

    善用現有的線上資源

    當初 Perl 或 Python 在發展時,還沒有 GitHub 或 Bitbucket 這類程式碼專案托管網站,所以會架設網站以集中托管套件。在 Go 問世時,GitHub 等網站已經相當普遍了,所以 Go 語言沒有重造輪子,而直接利用這些現成的網站,以分散式的策略存放在現有的資源上,Go 工具再直接從這些網站直接存取 Go 套件。

    不過,這個模式並非完美無缺,像是對 Go 套件的開發過於理想化,忽略了對第三方套件進行版本控制的需求。近年來陸續出現數個管理 Go 套件的專案,像是 glidegovendordep 等;最近又出現 Go module 等用於管理套件的新特性。目前這個問題尚未完全解決。

    大神也會犯錯

    Go 語言是由 Ken ThompsonRob Pike 等大神級人物所設計,不過,Go 語言有時精簡過頭了。以下是一個排序 (sorting) 的例子 (摘自 Go by Example):

    package main
    
    import "sort"
    import "fmt"
    
    type byLength []string
    
    func (s byLength) Len() int {
        return len(s)
    }
    func (s byLength) Swap(i, j int) {
        s[i], s[j] = s[j], s[i]
    }
    func (s byLength) Less(i, j int) bool {
        return len(s[i]) < len(s[j])
    }
    
    func main() {
        fruits := []string{"peach", "banana", "kiwi"}
        sort.Sort(byLength(fruits))
        fmt.Println(fruits)
    }
    

    在這裡,我們需要實作 Len()Swap()Less() 三個函式,以滿足 sort.Sort 的介面;但對於有泛型 (generics) 的語言來說,這個問題可用更簡單的語法來完成。這個例子在概念上精簡,但在語法上卻冗餘。

    結語

    由 Go 語言的特性,我們可以看到 KISS (keep it simple, stupid) 原則的實踐;當然,沒有完美無缺的程式語言,Go 語言在某些地方的確有問題。精簡的軟體相對會易於使用和推廣,但在精簡的大原則下仍能保持充足的功能則需要在設計上權衡一番。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk