go 語言概述

1. Go語言從何而來?

關於Go語言的萌芽時期,我們可以追溯至上個世紀。不過,直至2009年,它才真正被披露,併成爲開源大家庭中的一員。在2012年,Go語言的創造者們發佈了它的1.0版本。大家可能有所耳聞,Go語言出自Google公司。但很多人可能並不清楚,它的創造者們更是名頭不小。他們包括Unix操作系統和B語言(C語言的前身)的創造者、UTF-8編碼的發明者Ken Thompson,Unix項目的參與者、UTF-8編碼的聯合創始人和Limbo編程語言(Go語言的前身)的創造者Rob Pike,以及著名的Javascript引擎V8的創造者Robert Griesemer。正因爲有了他們的引領,一批又一批的全球頂尖計算機軟件人才都相繼加入到了Go語言項目中。

Go語言是一門強類型的通用編程語言。它的基礎語法與C語言很類似,但同時也對其他的一些優秀編程語言有所借鑑。它的很多設計靈感都來源於Tony Hoare執筆的那篇著名的論文《Communicating Sequential Processes》。

2. Go語言意味着什麼?

Go語言意味着更自由、更高效和一站式的編程體驗,程序開發效率和運行效率之間的完美融合,以及天生的併發編程支持。

2.1自由的編程方式

我們下面重點說說Go語言對各種流行的編程範式的支持,以及它對併發編程的強悍支持。

使用Go語言就意味着你可以用自己喜歡的方式編程。因爲Go語言支持當今所有主流的編程範式。這包括面向對象編程、函數式編程,以及過程式編程。

面向對象編程

面向對象的設計和編程可以使我們更容易的對現實世界進行建模。這樣可以編寫出讓人類更易懂的代碼,並且還會大大增強代碼的可維護性和可擴展性。Go語言支持面向對象編程。因此,我們的Go語言程序也可以具備上述優勢。面向對象編程中的很多重要原則都可以很容易的在Go語言程序中體現出來,比如:“針對接口編程,而不是針對實現編程”、“對擴展開放,對修改關閉”和“多用組合,少用繼承”等等。另外,雖然Go語言中並沒有繼承的概念,但我們依然可以用類型嵌入的方式來模仿繼承並達到相同的效果。

函數式編程

函數式編程同樣可以讓我們更容易的跟進變化。它可以讓我們的程序實體的粒度更加小巧,從而使程序更加易變。此外,函數式編程還可以讓我們的程序更加健壯,因爲函數本身是沒有狀態的。要知道,對各種狀態的維護會使程序更加複雜和脆弱。Go語言對函數式編程提供支持的一個體現就是:它的函數是絕對的一等類型。這是函數式編程的一個重要特徵。更具體地說,我們可以把一個函數作爲傳給其他函數的參數,或成爲其他函數返回的結果。這是構建閉包的必要條件。同時,這也意味着,我們可以對程序的可變部分進行更加靈活的控制和管理。

過程式編程

至於過程式編程,我們自不必多說。程序員們應該再熟悉不過了。過程式編程最直接的體現了程序的本質,同時也是簡單程序的最常用的編寫手法。我們可以用腳本語言寫出純過程化的程序。我之前常常使用Python代碼來做這類事情。因爲它可以像Shell腳本那樣工作,並且總是能夠保持簡單。我現在的選擇當然是用Go語言。不論是語言語法還是程序運行方式,Go語言都非常的腳本化。簡約和易用是它的兩個顯著特點。

2.2便捷的併發編程

毋庸置疑,Go語言對併發編程的支持是天生的、自然的和高效的。Go語言爲此專門創造出了一個關鍵字“go”。使用這個關鍵字,我們就可以很容易的使一個函數被併發的執行。就像這樣:

go func() {
  fmt.Println("Concurrent execution!")
}()

上面的這段代碼使用關鍵字“go”併發的執行了一個匿名函數,雖然這個匿名函數只會在標準輸出上打印一句話而已。更確切地說,該匿名函數會在一個單獨的Goroutine中(或稱Go程)被執行。我們把“go”和跟在它後面的函數以及調用符號“(”和“)”合稱爲go語句,而把那個匿名函數稱爲go函數。

如果我們要在不同的Goroutine中運行的函數之間傳遞數據,那麼我們可以使用Channel(也可稱其爲通道)。這也是Go語言強烈推薦的做法。

我們可以用程序模仿自來水廠的淨水設施。這個淨水設施會引導水流先後流經三個淨水裝置,最後產出可飲用的自來水。爲了更好理解,我們用過濾數字來代表對水的過濾。

我們構建兩個數字過濾裝置、一個數字輸出裝置和三個數字通道。下面是三個數字通道的創建和初始化代碼:

numberChan1 := make(chan int64, 3) // 數字通道1。
numberChan2 := make(chan int64, 3) // 數字通道2。
numberChan3 := make(chan int64, 3) // 數字通道3。

Go語言的內建函數make可以用於創建和初始化一個通道。我們在這裏定義,通道的元素類型都是int64,而長度都是3。另外,特殊標記“:=”可被用來在函數中聲明並初始化變量。這種情況下,我們可以省略掉變量的類型的聲明。

這裏有一點需要特別說明,運行main函數的Goroutine(也被稱爲主Goroutine)會一條接一條地執行main函數中的語句,不論這些語句中是否存在以及有多少條go語句。換句話說,主Goroutine並不會等到其他被啓用的Goroutine運行結束之後再結束自身的運行。因此,如果我們不採取任何措施的話,很可能在欲執行的go函數得到執行機會之前主Goroutine就已經結束運行了。一旦如此,當前程序的執行也就會宣告結束。這也意味着,那些go函數中的語句根本就不會被執行。

所以,我們在這裏需要再聲明一個變量,並稍微設置一下。這個變量代表了一個同步工具。對它的合理運用可以避免上述情況的發生。對這個變量的聲明和初始化的代碼如下:

var waitGroup sync.WaitGroup // 用於等待一組操作執行完畢的同步工具。
waitGroup.Add(3)              // 該組操作的數量是3。

標識符sync.WaitGroup代表了一個類型。該類型的聲明存在於代碼包sync中,類型名爲WaitGroup。另外,上面的第二條語句預示着我們將要後面啓用三個Goroutine,或者說要併發的執行三個go函數。請記住,我們在這裏進行了一個“加3”的操作。

我們下面依次展現這三個go函數,並說明它們的功用。先來看第一個go函數,包含它的go語句如下:

go func() { // 數字過濾裝置1。
  for n := range numberChan1 { // 不斷的從數字通道1中接收數字,直到該通道關閉。
    if n%2 == 0 { // 僅當數字可以被2整除,纔將其發送到數字通道2.
      numberChan2 <- n
    } else {
      fmt.Printf("Filter %d. [filter 1]\n", n)
    }
  }     
  close(numberChan2) // 關閉數字通道2。
  waitGroup.Done()   // 表示一個操作完成。
}()

這段代碼代表了數字裝置1的功能。下面是對其中的go函數的解釋:

  • 函數的第一條語句爲for語句。它會不停的從數字通道numberChan1中接收元素值(在這裏是數字)並進行處理,直到numberChan1被關閉。
  • 只有可以被2整除的數纔可以被送往數字通道numberChan2,否則就會被過濾掉。爲了方便查看,我們每過濾一個數字都會打印出一句話。
  • 符號“<-”被稱爲接收操作符。在這裏,它會把標識符n代表的數字發送給數字通道numberChan2。
  • 由於numberChan1通道的關閉會使這裏的for語句結束執行。這意味着上游不會再有任何數字“流出”。所以,我們在這條for語句的後面順勢關閉通道numberChan2。這也是爲了告訴它的下游,沒有更多的數字需要被過濾了。
  • 對waitGroup的Done方法的調用表示了數字裝置1已經完成了所有工作。該方法會進行相應的“減1”操作。請記住它,我們後面會對此進行說明。

我們完成了數字過濾裝置1的編寫。數字過濾裝置2的功能與此如出一轍。只不過它會過濾掉不能被5整除的數字。大家應該可以仿照上面的代碼寫出數字過濾裝置2的實現代碼。注意,數字過濾裝置2會試圖從數字通道numberChan2中接收數字,並將未被過濾的數字發送給數字通道numberChan3。如此一來,數字過濾裝置1和2就經由數字通道2串聯起來了。請注意,不要忘記在數字過濾裝置2中的for語句後面添加對數字通道numberChan3的關閉操作,以及調用waitGroup變量的Done方法。這非常重要。

現在我們來看數字輸出裝置。它即由第三條go語句代表。以下是具體代碼:

go func() { // 數字輸出裝置。
  for n := range numberChan3 { // 不斷的從數字通道3中接收數字,直到該通道關閉。
    fmt.Println(n) // 打印數字。
  }
  waitGroup.Done() // 表示一個操作完成。
}()

這個go函數的功能就非常簡單了。它只是打印出“通過淨化”的數字。不過,waitGroup.Done()語句依然被包含在內。

好了,我們已經編寫出了所有的裝置,併合理運用了那三個數字通道。有一點值得說明,到相應的數字通道中沒有任何數字可取時,for語句的執行會被阻塞。也正因爲如此,我們可以把這三條go語句放置在前,並在之後激活這一過濾數字的流程。具體的激活方法是,向數字通道numberChan1發送數字。相關的代碼如下:

for i := 0; i < 100; i++ { // 先後向數字通道1傳送100個範圍在[0,100)的隨機數。
  numberChan1 <- rand.Int63n(100)
}
close(numberChan1) // 數字發送完畢,關閉數字通道1。

這段代碼的意圖很明顯。我們先向數字通道numberChan1發送100個隨機數,然後關閉numberChan1通道以表示所有需要過濾的數字都發送完畢。放心,對通道的關閉並不會影響到對已存於其中的數字的接收操作。

好了,我們至此實現了一個完整的數字過濾流程。爲了能夠讓這個流程能夠被完整的執行,我們還需要在最後加入這樣一條語句:

waitGroup.Wait() // 等待前面那組操作(共3個)的完成。

對waitGroup的Wait方法的調用會一直被阻塞,直到前面三個go函數中的三個waitGroup.Done()語句(即那三個“減1操作”)都被執行完畢。“加3”操作使變量waitGroup的狀態有所改變,並以此阻塞住了之後的waitGroup.Wait()語句的執行。而後續被併發執行的三個“減1”操作的執行又使變量waitGroup的狀態迴歸初始。這才能讓對waitGroup.Wait()語句的執行從阻塞中恢復並完成。這是防止主Goroutine過早的被運行結束的有效手段之一。

下面是一幅可以宏觀的展示數字過濾流程的圖示。

圖1-1 數字過濾流程

我們把上述代碼都放入到一個命令源碼文件的main函數中。並在文件的開始處添加一條代碼包導入語句:

import (
  "fmt"
  "math/rand"
  "sync"
)

大家可以試着使用“go run”命令運行這個命令源碼文件,並觀察輸出結果。

我們配合使用Goroutine和Channel讓數字過濾流程實現了全異步化,但卻沒有增加開發的難度。Channel可以讓我們以管道的方式在多個Goroutine之間交換數據。這也恰恰實踐和印證了這句話:

Do not communicate by sharing memory; instead, share memory by communicating.

在這之中起到關鍵作用的Channel有着非常靈活、多樣的使用方法。在後續的文章中,我會專門就此進行論述。

雖然我們可以使用其他語言的代碼實現這樣的異步化,但是它們不會像Go語言這樣爲此提供語言級別的原生支持。也正是由於這個原因,那些代碼會看起來複雜得多。它們不得不使用若干個類庫或輔助工具來滿足異步化的要求。另一方面,Go語言先進、高效的併發編程模型及其實現系統會使得程序對系統資源的消耗大大減少,並且會在很大程度上提高對這些資源的使用效率。這一優勢是很多其他語言望塵莫及的。同樣,我會在後面簡明扼要的說明Goroutine的運作機理。

3. Go語言的哲學

通過對前面內容的閱讀,大家應該能夠隱約的感覺到Go語言的關注點,以及它想爲軟件開發者們帶來的啓示和新思想。

作爲本篇文章的總結,我在下面列出幾點最重要的Go語言的哲學:

  1. Go語言集衆多編程範式之所長,並以自己獨到的方式將它們融合在一起。程序員們可以用他們喜歡的風格去設計程序。
  2. 相對於設計規則上的靈活,Go語言有着明確且近乎嚴格的編碼規範。我們可以通過“go fmt”命令來按照官方的規範格式化代碼。
  3. Go語言是強調軟件工程的編程語言。它自帶了非常豐富的標準命令,涵蓋了軟件生命週期(開發、測試、部署、維護等等)的各個環節。
  4. Go語言是雲計算時代的編程語言。它關注高併發程序,並旨在開發效率和運行效率上取得平衡。
  5. Go語言提倡交換數據,而不是共享數據。它的併發原語Goroutine和Channel是其中的兩大併發編程利器。同時,Go語言也提供了豐富的同步工具,以供程序員們根據場景選用。然而,後者就不屬於語言級別的支持了。
發佈了111 篇原創文章 · 獲贊 27 · 訪問量 55萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章