1. 程式人生 > >Go語言的那些事兒(第一篇)

Go語言的那些事兒(第一篇)

Golang的特點

說明:本文大量借鑑了文章,在此表示感謝。

  • 部署簡單

Golang編譯生成的是一個靜態可執行檔案,除了 glibc 外沒有其他外部依賴,完全不需要操心應用所需的各種包、庫的依賴關係,大大減輕了維護的負擔。

  • 併發性好

Goroutine和Channel機制使得編寫高併發的服務端軟體變得相當容易,很多情況下完全不需要考慮鎖機制以及由此帶來的問題。單個Go應用也能有效的利用多個CPU核,並行執行的效能好。

  • 效能優良

Golang佔用的CPU資源更少,通常比一般語言(Java和Python)更節省資源。【這裡存在爭議】

GO程式設計師的五個進化階段:

第一個階段(菜逼)

: 剛剛學習了這門語言。 已經通過一些教程或者培訓班瞭解基本的語法,可以寫短的程式碼片段。

第二個階段 (探索者): 可以寫一個完整的程式,但不懂一些更高階的語言特徵,比如“channels”。還沒有使用GO寫一個大專案。

第三個階段(大手): 你能熟練的使用Go, 能夠用GO去解決,生產環境中一個具體和完整的問題。已經形成了一套自己的慣用法和常用程式碼庫。在你的編碼方案中Go是一個非常好用的工具。

第四階段 (大神): 絕逼清楚Go語言的設計選擇和背後的動機。能理解的簡潔和可組合性哲學。

佈道師: 積極地與他人分享關於Go語言知識和你對Go語言的理解。在各種合適的場所發出自己的聲音, 參與郵件列表、建立QQ群、做專題報告。成為一個佈道者不見得是一個完全獨立的階段,這個角色可以在上述的任何一個階段中。

併發支援

  • goroutine

goroutine非常類似執行緒。通過 go f()的方式使用,開啟一個新的goroutine去完成相應任務。

  • chennel

goroutine開始執行,但是如何知道執行結束呢?這就需要使用channel傳遞訊息。

Goroutine

channel的緩衝區

有緩衝的channel

var a string
var c = make(chan int, 1)

func f() {
    a = "hello, world"
    c <- 0
}

func main() {
    go f()
    <-c
    print
(a) }

無緩衝的channel

var a string
var c = make(chan int)

func f() {
    a = "hello, world"
    <-c
}

func main() {
    go f()
    c <- 0
    print(a)
}

Go語言的併發

從語言層面支援併發是Go語言最大的特色。為什麼很多人形容Go是為雲端計算而生的語言呢?主要原因就是在分散式系統中,併發是必須考慮的一個問題,Go語言能夠快速高效地處理這個問題,受到廣大雲端計算開發者的青睞。我們嘗試從原理上理解下Go語言是如何處理高併發的。

我們觀察下面的程式:

func loop() {
    for i := 0; i < ; i++ {
        fmt.Printf("%d ", i)
    }
}

func main() {
    loop()
    loop()
}

我們得到的輸出結果:

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

如果我們把一個loop放到子執行緒(goroutine)中去跑,我們會得到如下結果:

0 1 2 3 4 5 6 7 8 9

奇怪的是,為什麼只輸出了一次呢?主執行緒一次,子routine一次,應該是兩次才是啊。原來,在goroutine還沒準備跑的時候,主函式已經退出了。main函式退出太快了,我們要想辦法阻止它過早地退出,一個辦法是讓main等待一下:

func main() {
    go loop()
    loop()
    time.Sleep(time.Second) // 停頓一秒
}

這次確實輸出了兩次,目的達到了。可是採用等待的辦法並不好,首先我們並不知道go routine要跑多久。其次這裡有硬編碼。這裡,在GO裡面有標準的方式之一是waitgroup方法。

我們看看使用WaitGroup的例子

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    wg.Add(1)

    go func() {
        defer wg.Done()
        for i := 0; i < 10; i++ {
            fmt.Printf("i=%v\n", i)
        }
    }()

    for i := 0; i < 10; i++ {
        fmt.Printf("i=%v\n", i)
    }

    wg.Wait()
}

waitgroup會等待goroutine退出。

除了使用waitgroup之外,我們還可以使用channel,我們看看channel怎麼使用:

package main

import (
    "fmt"
)

func main() {
    var messages chan string = make(chan string)
    go func(message string) {
        messages <- message // 存訊息
    }("Ping!")

    fmt.Println(<-messages) // 取訊息
}

這裡需要說明的是,預設通道的存訊息和取訊息都是阻塞的。也就是說, 無緩衝的通道在取訊息和存訊息的時候都會掛起當前的goroutine,除非另一端已經準備好。比如以下的main函式和foo函式:

var ch chan int = make(chan int)

func foo() {
    ch <- 0  // 向ch中加資料,如果沒有其他goroutine來取走這個資料,那麼掛起foo, 直到main函式把0這個資料拿走
}

func main() {
    go foo()
    <- ch // 從ch取資料,如果ch中還沒放資料,那就掛起main線,直到foo函式中放資料為止
}

那既然通道可以阻塞當前的goroutine, 那麼回到上一部分「goroutine」所遇到的問題「如何讓goroutine告訴主線我執行完畢了」 的問題來, 使用一個通道來告訴主線即可:

package main

import (
    "fmt"
)

var complete chan int = make(chan int)

func loop() {
    for i := 0; i < 10; i++ {
        fmt.Printf("%d ", i)
    }

    complete <- 0 // 執行完畢了,發個訊息
}

func main() {
    go loop()
    <-complete // 直到執行緒跑完, 取到訊息. main在此阻塞住
}

如果不用通道來阻塞主線的話,主線就會過早跑完,loop線都沒有機會執行。

其實,無緩衝的通道永遠不會儲存資料,只負責資料的流通,為什麼這麼講呢?
* 從無緩衝通道取資料,必須要有資料流進來才可以,否則當前線阻塞
* 資料流入無緩衝通道, 如果沒有其他goroutine來拿走這個資料,那麼當前線阻塞

所以,你可以測試下,無論如何,我們測試到的無緩衝通道的大小都是0 (len(channel))

如果通道正有資料在流動,我們還要加入資料,或者通道枯竭,我們一直向無資料流入的空通道取資料呢? 就會引起死鎖

死鎖

一個死鎖的例子:

func main() {
    ch := make(chan int)
    <- ch // 阻塞main goroutine, 通道c被鎖
}

執行這個程式你會看到Go報這樣的錯誤:

fatal error: all goroutines are asleep - deadlock!

未完待續