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傳遞訊息。
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!
未完待續