Go:10 個與衆不同的特性

大家好,我是程序員幽鬼。

Go 作爲一門相對較新的語言,能夠脫穎而出,肯定是多方面的原因。本文聊聊它不同於其他語言的 10 個特性。


Go 的創建者 Robert Griesemer[1] 、Rob Pike[2] 和 Ken Thompson[3] 在 Google 工作,在那裏,大規模擴展的挑戰激發了他們將 Go 設計爲具有大型代碼庫的項目的快速高效的編程解決方案,由多個開發人員管理,具有嚴格的性能要求,並跨越多個網絡和處理核心。

Go 的創始人在創建新語言時也抓住了這個機會,從其他編程語言的優勢,劣勢和疏忽中學習。結果是一種乾淨,清晰和實用的語言,具有相對較小的命令和特性集。

本文將介紹 Go 的 10 個特性,這些特性(根據我個人的觀察)將其與其他語言區分開來。

  1. Go 始終在構建中包含 runtime

Go 運行時提供內存分配、垃圾回收、併發支持和網絡等服務。它被編譯進每個 Go 二進制文件。這與許多其他語言不同,其中許多語言使用虛擬機,需要與程序一起安裝才能正常工作。

將運行時直接包含在二進制文件中使得分發和運行 Go 程序變得非常容易,並避免了運行時與程序之間的不兼容問題。Python,Ruby 和 JavaScript 等語言的虛擬機也沒有針對垃圾回收和內存分配進行優化,這解釋了 Go 相對於其他類似語言的優越速度。例如,Go 儘可能多地存儲在堆棧 [4] 上,其中數據按順序排列,以便比堆 [5] 更快地訪問。稍後將對此進行詳細介紹。

關於 Go 的靜態二進制文件的最後一件事是,由於不需要運行外部依賴項,因此它們的啓動速度非常快。如果你使用像 Google App Engine[6] 這樣的服務,這將非常有用,這是一種在 Google Cloud 上運行的平臺即服務,可以將你的應用程序擴展到零實例以節省雲成本。當有新的請求出現時,App Engine 可以在眨眼間啓動你的 Go 程序實例。在 Python 或 Node 中相同的體驗通常會導致 3-5 秒的等待(或更長時間),因爲所需的虛擬環境也與新實例一起旋轉。

  1. Go 沒有集中託管的程序依賴服務

爲了訪問已發佈的 Go 程序,開發人員不依賴於集中託管的服務,例如用於 Java 的 Maven Central[7] 或用於 JavaScript 的 NPM[8]。相反,項目通過其源代碼存儲庫(通常是 GitHub)共享。go get/install 命令行允許以這種方式下載存儲庫。

爲什麼我喜歡這個功能?我一直認爲集中託管的依賴服務(如 Maven Central、PIP 和 NPM)有着令人生畏的黑匣子,可能會抽象出下載和安裝依賴項(以及依賴項的依賴項)的麻煩,但當依賴項錯誤發生時,不可避免地會引發可怕的心跳加速(我經歷過太多了,無法計數)。

很多時候,我發現令人沮喪的是,我從來沒有完全理解它們內部是如何工作的。通過取消中央服務,安裝,版本控制和管理 Go 項目的依賴項的過程非常清晰,從而更加清晰。(當然,也有人喜歡集中託管)

此外,將模塊提供給其他人就像將其放入版本控制系統中一樣簡單,這是分發程序的一種非常簡單的方法。

  1. Go 是按值調用

在 Go 中,當你提供基本類型(數字、布爾值或字符串)或結構(類對象的大致等效項)作爲函數的參數時,Go 始終會創建變量值的副本

在許多其他語言如 Java,Python 和 JavaScript 中,基本類型是通過值傳遞 [9] 的,但是對象(類實例)是通過引用傳遞的,這意味着接收函數實際上接收到指向原始對象的指針而不是其副本

這意味着在接收函數中對對象所做的任何更改都將反映在原始對象中

在 Go 中,結構和基本類型默認按值傳遞,可以選擇通過使用_星號_運算符傳遞指針 [10]:

// pass by value
func MakeNewFoo(f Foo) (Foo, error) {
   f.Field1 = "New val"
   f.Field2 = f.Field2 + 1
   return f, nil
}

上述函數接收 Foo 的副本,並返回一個新的 Foo 對象

// pass by reference
func MutateFoo(f *Foo) error {
   f.Field1 = "New val"
   f.Field2 = 2
   return nil
}

上面的函數接收指向 Foo 的指針並改變原始對象

這種按值調用與按引用調用的明顯區別使你的意圖顯而易見,並減少了調用函數無意中改變傳入對象的可能性(這是許多初學者開發人員難以掌握的)。

正如麻省理工學院總結 [11] 的那樣:"可變性使得理解你的程序在做什麼變得更加困難,而執行合約也更難"

更重要的是,按值調用可顯著減少垃圾回收器的工作,這意味着更快、更節省內存的應用程序。這篇文章 [12] 得出的結論是,指針追蹤(從堆中檢索指針值)比從連續堆棧中檢索值慢 10 到 20 倍。要記住的一個很好的經驗法則是:從內存中讀取的最快方法是按順序讀取它,這意味着將隨機存儲在 RAM 中的指針數量減少到最低限度

  1. defer 關鍵字

在 NodeJS 中,在我開始使用 knex.js[13] 之前,我會在代碼中手動管理數據庫連接,方法是創建一個數據庫池,然後在每個函數的池中打開一個新連接,一旦所需的數據庫 CRUD 功能完成,就會在函數結束時釋放連接。

這有點像維護的噩夢,因爲如果我在每個函數結束時不釋放連接,未釋放的數據庫連接的數量將慢慢增長,直到池中沒有更多的可用連接,然後中斷應用程序。

現實情況是,程序通常必須發佈,清理和執行資源,文件,連接等,因此 Go 引入了 defer 關鍵字作爲管理這一點的有效方法。

任何前面帶有 defer 的語句都會延遲其調用,直到周圍的函數退出。這意味着你可以將清理 / 拆卸代碼放在函數的頂部(很明顯),知道一旦函數完成,它就會完成它的工作。

func main() {
    if len(os.Args) < 2 {
        log.Fatal("no file specified")
    }
    f, err := os.Open(os.Args[1])
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
    data := make([]byte, 2048)
    for {
        count, err := f.Read(data)
        os.Stdout.Write(data[:count])
        if err != nil {
            if err != io.EOF {
                log.Fatal(err)
            }
            break
        }
    }
}

在上面的示例中,文件關閉方法被延遲。我喜歡這種模式,在函數的頂部聲明你的內務管理意圖,然後忘記它,知道一旦函數退出,它就會完成它的工作。

  1. Go 吸納了函數式編程的最佳特性

函數式編程是一種高效且富有創造性的範式,值得慶幸的是,Go 採納了函數式編程的最佳特性。在 Go 中:

— 函數是值,這意味着它們可以作爲值添加到 map 中,作爲參數傳遞到其他函數中,設置爲變量,並從函數返回(稱爲 "高階函數",在 Go 中經常用於使用裝飾器模式創建中間件)。

— 匿名函數可以創建並自動調用。

— 在其他函數中聲明的函數允許閉包(其中在函數內部聲明的函數能夠訪問和修改在外部函數中聲明的變量)。在慣用的 Go 中,閉包被廣泛使用,限制了函數的作用域,並設置了函數在其邏輯中使用的狀態。

func StartTimer (name string) func(){
    t := time.Now()
    log.Println(name, "started")
    return func() {
        d := time.Now().Sub(t)
        log.Println(name, "took", d)
    }
}
func RunTimer() {
    stop := StartTimer("My timer")
    defer stop()
    time.Sleep(1 * time.Second)
}

以上是閉包的一個例子。'StartTimer' 函數返回一個新函數,該函數通過閉包可以訪問在其啓動作用域中設置的't' 值。然後,此函數可以將當前時間與 "t" 的值進行比較,從而創建一個有用的計時器。感謝 Mat Ryer[14] 的這個例子。

  1. Go 有隱式接口實現

任何讀過 SOLID[15] 編碼和設計模式 [16] 文獻的人都可能聽說過 "偏愛組合而不是繼承" 的口頭禪。簡而言之,這表明你應該將業務邏輯分解爲不同的接口,而不是依賴於父類中屬性和邏輯的分層繼承。

另一個流行的方法是 "面向接口編程,而不是實現":API 應該只發布其預期行爲的契約(其方法簽名),但不能詳細介紹如何實現該行爲。

這兩者都指出了接口在現代編程中的至關重要性。

因此,毫不奇怪,Go 支持接口。事實上,接口是 Go 中唯一的抽象類型。

然而,與其他語言不同,Go 中的接口不是顯式實現的,而是隱式實現的。具體類型不聲明它實現接口。相反,如果該具體類型的方法集包含基礎接口的所有方法集,則 Go 認爲該對象實現了該接口

這種隱式接口實現(正式名稱爲結構化類型 structural typing)允許 Go 強制實施類型安全和解耦,從而保留了動態語言中表現出的大部分靈活性。

相比之下,顯式接口將客戶端和實現綁定在一起,例如,在 Java 中替換依賴項比在 Go 中困難得多。

// this is an interface declaration (called Logic)
type Logic interface {
    Process(data string) string
}

type LogicProvider struct {}
// this is a method called 'Process' on the LogicProvider struct
func (lp LogicProvider) Process(data string) string {
    // business logic
}
// this is the client struct with the Logic interface as a property
type Client struct {
    L Logic
}
func(c Client) Program() {
    // get data from somewhere
    c.L.Process(data)
}
func main() {
    c := Client {
        L: LogicProvider{},
    }
    c.Program()
}

LogicProvider 中沒有任何聲明表明它實現了 Logic 接口。這意味着客戶端將來可以輕鬆替換其邏輯提供程序,只要該邏輯提供程序包含基礎接口 (Logic) 的所有方法集。

  1. 錯誤處理

Go 中的錯誤處理方式與其他語言大不相同。簡而言之,Go 通過返回 error 類型的值作爲函數的最後一個返回值來處理錯誤

當函數按預期執行時,將爲 error 參數返回 nil,否則返回錯誤值。然後,調用函數檢查錯誤返回值,並處理錯誤,或引發自己的錯誤。

// the function returns an int and an error
func calculateRemainder(numerator int, denominator int) (int, error) {
   // Error returned
   if denominator == 0 {
      return 9, errors.New("denominator is 0")
   }
   // No error returned
   return numerator / denominator, nil
}

Go 以這種方式運行是有原因的:它迫使編碼人員考慮異常並正確處理它們。傳統的 try-catch 異常還會在代碼中添加至少一個新的代碼路徑,並以難以遵循的方式縮進代碼。Go 更喜歡將 "快樂路徑" 視爲非縮進代碼,在 "快樂路徑" 完成之前識別並返回任何錯誤。

  1. 併發

併發可以說是 Go 最著名的功能,併發允許在機器或服務器上的可用內核數量上並行運行任務。當單獨的進程不相互依賴(不需要按順序運行)並且時間性能至關重要時,併發性最有意義。I/O 要求通常就是這種情況,其中讀取或寫入磁盤或網絡比除最複雜的內存中進程之外的所有進程慢幾個數量級。

函數調用之前的 'go' 關鍵字將開啓併發 goroutine 運行該函數。

func process(val int) int {
   // do something with val
}
// for each value in 'in', run the process function concurrently,
// and read the result of process to 'out'
func runConcurrently(in <-chan int, out chan<- int){
   go func() {
       for val := range in {
            result := process(val)
            out <- result
       }
   }
}

Go 中的併發性是一項深入且相當高級的功能,但在有意義的情況下,它提供了一種有效的方法來確保程序的最佳性能。

  1. Go 標準庫

Go 具有 "電池包含" 的理念,現代編程語言的許多需求都融入了標準庫中,這使得程序員的生活變得更加簡單。

如前所述,Go 是一種相對年輕的語言,這意味着標準庫中滿足了現代應用程序的許多問題 / 需求。

首先,Go 爲網絡(特別是 HTTP/2)和文件管理提供了世界一流的支持。它還提供本地 JSON 編碼和解碼。因此,設置服務器來處理 HTTP 請求和返回響應(JSON 或其他)非常簡單,這解釋了 Go 在開發基於 REST 的 HTTP Web 服務方面的受歡迎程度。

正如 Mat Ryer[17] 還指出的那樣,標準庫是開源的,是學習 Go 最佳實踐的絕佳方式。

  1. 調試:Go Playground

使用任何語言進行調試都是一項關鍵需求。大多數語言都依賴於第三方在線工具或聰明的 IDE 來提供調試工具,使開發人員能夠快速檢查其代碼。Go 提供了 Go Playground — https://go.dev/play 一個免費的在線工具,你可以在其中試用和共享小程序。這是一個非常有用的工具,使調試成爲一項簡單的練習。

沒記錯的話,Go 應該開啓了 playground 的先河,之後發佈的語言也提供類似的功能,比如 Rust 和 Swift。

總結

除了以上介紹的 10 個特性,你認爲還有其他特性是 Go 獨特的地方嗎?

參考資料

[1]

Robert Griesemer: https://en.wikipedia.org/wiki/Robert_Griesemer

[2]

Rob Pike: https://en.wikipedia.org/wiki/Rob_Pike

[3]

Ken Thompson: https://en.wikipedia.org/wiki/Ken_Thompson

[4]

堆棧: https://en.wikipedia.org/wiki/Stack-based_memory_allocation

[5]

堆: https://www.educba.com/what-is-heap-memory/

[6]

Google App Engine: https://cloud.google.com/appengine

[7]

Maven Central: https://search.maven.org/

[8]

NPM: https://www.npmjs.com/

[9]

是通過值傳遞: https://itnext.io/the-power-of-functional-programming-in-javascript-cc9797a42b60

[10]

指針: https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-stacks-and-pointers.html

[11]

總結: http://web.mit.edu/6.031/www/fa20/classes/08-immutability/

[12]

這篇文章: https://www.forrestthewoods.com/blog/memory-bandwidth-napkin-math/

[13]

knex.js: https://knexjs.org/

[14]

Mat Ryer: https://twitter.com/matryer

[15]

SOLID: https://en.wikipedia.org/wiki/SOLID

[16]

設計模式: https://en.wikipedia.org/wiki/Software_design_pattern

[17]

Mat Ryer: https://twitter.com/matryer


本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/nzm7VaP47Cj2v0nz4GqBfQ