開源庫「go home」聚焦Go語言技術堆疊與面試題,以協助Gopher登上更大的舞臺,歡迎
go home~
背景介紹
大家都知道行程是作業系統資源分配的基本單位,有獨立的記憶體空間,執行緒可以共享同一個行程的記憶體空間,所以執行緒相對輕量,背景關系切換開銷也小,雖然執行緒已經比較輕量了,但還是占近1M的記憶體,而今天介紹的有“輕量級執行緒”之稱的Goroutine,可以小至幾十K甚至幾K,切換的開銷更小,
除此之外,在傳統Socket編程時,需要維護一個執行緒池來為每個Socket收發包分配執行緒,而且需要將CPU與執行緒數建立對應關系,確保每個任務都能被及時分配給CPU,而Go程式可以智能地將goroutine中的任務分配到CPU,
如何使用
我們現在假設一個場景,你是一家公司老總,每天要花兩小時處理郵件,六小時開會,那么,程式可以這樣撰寫,
func main() {
time.Sleep(time.Hour * 2) //處理郵件
time.Sleep(time.Hour * 6) //開會
fmt.Println("作業完成了")
}
運行一下

果然,要8小時才能完成作業,那么怎么簡化作業呢?沒錯,請一個助理小姐姐幫忙,就讓她來處理郵件,這樣你就只需6小時開會就行了,

開始寫代碼吧,先來定義一個助理函式,
func assistant() {
time.Sleep(time.Hour * 2)
}
然后在主函式用一條神器的命令go呼叫它,這樣助理的耗時久不再占用你的時間了,
func main() {
go assistant()
time.Sleep(time.Hour * 6) //開會
fmt.Println("作業完成了")
}
運行一下

真的只花了六個小時就完成作業了,各位看官們,看到沒,這就是協程,只需要go命令加上函式名,就這么簡單,

但是我知道勤奮的你是不會滿足于現狀的,
匿名函式
另外,既然goroutine支持普通函式,當然也就支持匿名函式,
go func() {
time.Sleep(time.Hour * 2)
}()
協程間如何通訊
雖然我們可以輕松地創建一堆協程,但是不能通信的協程是沒有靈魂的,假如助理正在幫你處理郵件時,你突然想請她喝奶茶,那是不是要通知她?

那怎么通知呢?這就引出了大名鼎鼎的channel,漢譯“通道”,顧名思義它的作用就是在協程之間建立通道,一端可以將資料源源不斷地傳送到通道的另一端,

而宣告方式也非常簡單,只需要make一下,拿下方代碼為例,它代表初始化一個通道型別變數,并且通道里只能存放string型別的資料,
ch := make(chan string)
初始化完成后,要想與協程函式建立連接,得先把chan變數傳給協程函式,
go assistant(ch)
當然,協程函式要能接收chan才行,我們縱深到函式內部,看看都干了些什么,
func assistant(ch chan string) {
go func() {
for {
fmt.Println("看了一封郵件")
time.Sleep(time.Second * 1)
}
}()
msg := <-ch
if msg == "喝奶茶去唄" {
ch <- "好啊"
}
}
函式內部又起了一個協程專門處理郵件,同時另外一邊等待老板通知,細心的你應該看出如何取通道資料了,沒錯,只需要在通道變數前加上<-符號就可以將值取出,同樣的,符號加在后面就是往通道塞資料,
ch <- "pingyeaa"
<- ch

如果通道沒有資料,消費端就會一直阻塞,直到有資料為止,當然編譯器是很聰明的,在編譯的時候如果發現沒有地方往通道里塞資料,它就會panic,提示死鎖,
fatal error: all goroutines are asleep - deadlock!
繼續來看代碼,大致意思就是老板如果發“喝奶茶去唄”,就回傳“好啊”,因為通道里一開始是沒資料的,所以該協程會一直阻塞,直到主函式往通道中寫入了訊息,
現在來看下主函式的實作邏輯,宣告通道和傳入通道變數就不再贅述了,我們只需要等待5秒鐘之后往通道里寫入喝奶茶訊息即可,因為剛才assistant協程接收到訊息后會往ch寫入“好啊”訊息,所以主函式在發完請求之后應該再讀取從助理那邊傳遞來的訊息,
ch := make(chan string)
go assistant(ch)
time.Sleep(time.Second * 5)
ch <- "喝奶茶去唄"
resp := <-ch
fmt.Println(resp)
同樣,主函式的<-ch也會一直阻塞,直到助理回復訊息,另外有兩點需要注意,第一,如果main函式趕在goroutine之前執行完畢,那么goroutine也會銷毀;第二,main也是goroutine,
最后,關閉通道,其實通道關閉不是必須的,它與檔案不同,如果沒有goroutine使用到channel,就會自動銷毀,而close的作用是用來通知通道的另一端不再發送訊息了,另一端可以通過<-ch的第二個引數來獲取通道關閉情況,
close(ch)
data, ok := <-ch
通道的多路復用select
剛才的示例中的<-ch只能讀取通道的一條訊息,如果通道里不止一條訊息,該怎么讀取呢?

應該很多同學跟我一樣想到的是遍歷,沒錯,遍歷確實可以拿到通道資料,
for {
fmt.Println(<-ch)
}
也可以這么遍歷,
for d := range ch {
fmt.Println(d)
}
但是,如果需要同時接收多個通道資料該怎么辦?回圈中接收兩個通道變數?
for {
data, ok := <-ch1
data, ok := <-ch2
}
這種方式雖然可以取出資料,但是性能較差,官方給我們提供的select關鍵詞就是專門用來解決多通道資料讀取問題的,語法與switch非常相似,select會將多個通道傳來的資料分發到不同的處理邏輯中,
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for {
select {
case d := <-ch1:
fmt.Println("ch1", d)
case d := <-ch2:
fmt.Println("ch2", d)
}
}
}()
ch1 <- 1
ch1 <- 2
ch2 <- 2
ch1 <- 3
}
模擬超時
除此之外,有些情況下我們不希望通道阻塞太久,假設5秒鐘還取不出通道的資料,就超時退出,那我們可以使用time.After方法來實作,time.After會回傳一個通道型別,它的作用是傳入一個目標時間(比如5s),我們在5秒后就可以通過通道獲取預設定的超時通知,這樣就達到了定時器的目的,
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for {
select {
case d := <-ch1:
fmt.Println("ch1", d)
case d := <-ch2:
fmt.Println("ch2", d)
case <-time.After(time.Second * 5):
fmt.Println("接收超時")
}
}
}()
time.Sleep(time.Second * 6)
}
通道關閉延伸閱讀
已關閉的通道再發送資料會觸發panic
ch := make(chan int)
close(ch)
ch <- 1
panic: send on closed channel
通道設定長度
可以通過make方法設定通道長度,作為緩沖區,通道滿時生產者端會阻塞,通道取空后消費端會阻塞,
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 2
ch <- 2
fmt.Println(len(ch))
已關閉的通道依然可以讀取資料
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 2
close(ch)
for d := range ch {
fmt.Println(d)
}
感謝大家的觀看,如果覺得文章對你有所幫助,歡迎關注公眾號「平也」,聚焦Go語言與技術原理,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/24385.html
標籤:Go
上一篇:golang--深入簡出,帶你用golang的反射擼一個公用后臺查詢方法
下一篇:[go]map基本使用和底層原理
