go語言中的go func(){}() 表示以併發的方式調用匿名函數funchtml
前言 node
本文主要給你們介紹了Go語言中函數new與make的使用和區別,關於Go語言中new和make是內建的兩個函數,主要用來建立分配類型內存。在咱們定義生成變量的時候,可能會以爲有點迷惑,其實他們的規則很簡單,下面咱們就經過一些示例說明他們的區別和使用,話很少說了,來一塊兒看看詳細的介紹吧。 python
變量的聲明 git
var i int 程序員
var s string github
變量的聲明咱們能夠經過var關鍵字,而後就能夠在程序中使用。當咱們不指定變量的默認值時,這些變量的默認值是他們的零值,好比int類型的零值是0,string類型的零值是"",引用類型的零值是nil。 golang
對於例子中的兩種類型的聲明,咱們能夠直接使用,對其進行賦值輸出。可是若是咱們換成引用類型呢? 數據庫
package main express
import ( segmentfault
"fmt"
)
func main() {
var i *int
*i=10
fmt.Println(*i)
}
這個例子會打印出什麼?0仍是10?。以上全錯,運行的時候會painc,緣由以下:
panic: runtime error: invalid memory address or nil pointer dereference
從這個提示中能夠看出,對於引用類型的變量,咱們不光要聲明它,還要爲它分配內容空間,不然咱們的值放在哪裏去呢?這就是上面錯誤提示的緣由。
對於值類型的聲明不須要,是由於已經默認幫咱們分配好了。
要分配內存,就引出來今天的new和make。
new
對於上面的問題咱們如何解決呢?既然咱們知道了沒有爲其分配內存,那麼咱們使用new分配一個吧。
func main() {
var i *int
i=new(int)
*i=10
fmt.Println(*i)
}
如今再運行程序,完美PASS,打印10。如今讓咱們看下new這個內置的函數。
// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type
它只接受一個參數,這個參數是一個類型,分配好內存後,返回一個指向該類型內存地址的指針。同時請注意它同時把分配的內存置爲零,也就是類型的零值。
咱們的例子中,若是沒有*i=10,那麼打印的就是0。這裏體現不出來new函數這種內存置爲零的好處,咱們再看一個例子。
func main() {
u:=new(user)
u.lock.Lock()
u.name = "張三"
u.lock.Unlock()
fmt.Println(u)
}
type user struct {
lock sync.Mutex
name string
age int
}
示例中的user類型中的lock字段我不用初始化,直接能夠拿來用,不會有無效內存引用異常,由於它已經被零值了。
這就是new,它返回的永遠是類型的指針,指向分配類型的內存地址。
make
make也是用於內存分配的,可是和new不一樣,它只用於chan、map以及切片的內存建立,並且它返回的類型就是這三個類型自己,而不是他們的指針類型,由於這三種類型就是引用類型,因此就沒有必要返回他們的指針了。
注意,由於這三種類型是引用類型,因此必須得初始化,可是不是置爲零值,這個和new是不同的。
func make(t Type, size ...IntegerType) Type
從函數聲明中能夠看到,返回的仍是該類型。
兩者異同
因此從這裏能夠看的很明白了,兩者都是內存的分配(堆上),可是make只用於slice、map以及channel的初始化(非零值);而new用於類型的內存分配,而且內存置爲零。因此在咱們編寫程序的時候,就能夠根據本身的須要很好的選擇了。
make返回的仍是這三個引用類型自己;而new返回的是指向類型的指針。
其實new不經常使用
因此有new這個內置函數,能夠給咱們分配一塊內存讓咱們使用,可是現實的編碼中,它是不經常使用的。咱們一般都是採用短語句聲明以及結構體的字面量達到咱們的目的,好比:
i:=0
u:=user{}
這樣更簡潔方便,並且不會涉及到指針這種比麻煩的操做。
make函數是無可替代的,咱們在使用slice、map以及channel的時候,仍是要使用make進行初始化,而後才才能夠對他們進行操做。
總結
以上就是這篇文章的所有內容了,但願本文的內容對你們的學習或者工做具備必定的參考學習價值,若是有疑問你們能夠留言交流,謝謝你們對腳本之家的支持。
select是Go中的一個控制結構,相似於switch語句,用於處理異步IO操做。select會監聽case語句中channel的讀寫操做,當case中channel讀寫操做爲非阻塞狀態(即能讀寫)時,將會觸發相應的動做。
select中的case語句必須是一個channel操做
select中的default子句老是可運行的。
Go語言中有個概念叫作goroutine, 這相似咱們熟知的線程,可是更輕。
如下的程序,咱們串行地去執行兩次loop函數:
func loop() {
for i := 0; i < 10; 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裏跑,咱們可使用關鍵字go來定義並啓動一個goroutine:
func main() {
go loop() // 啓動一個goroutine
loop()
}
此次的輸出變成了:
0 1 2 3 4 5 6 7 8 9
但是爲何只輸出了一趟呢?明明咱們主線跑了一趟,也開了一個goroutine來跑一趟啊。
原來,在goroutine還沒來得及跑loop的時候,主函數已經退出了。
main函數退出地太快了,咱們要想辦法阻止它過早地退出,一個辦法是讓main等待一下:
func main() {
go loop()
loop()
time.Sleep(time.Second) // 停頓一秒
}
此次確實輸出了兩趟,目的達到了。
但是採用等待的辦法並很差,若是goroutine在結束的時候,告訴下主線說"Hey, 我要跑完了!"就行了,即所謂阻塞主線的辦法,回憶下咱們Python裏面等待全部線程執行完畢的寫法:
for thread in threads:
thread.join()
是的,咱們也須要一個相似join的東西來阻塞住主線。那就是信道
信道是什麼?簡單說,是goroutine之間互相通信的東西。相似咱們Unix上的管道(能夠在進程間傳遞消息),用來goroutine之間發消息和接收消息。其實,就是在作goroutine之間的內存共享。
使用make來創建一個信道:
var channel chan int = make(chan int)
// 或
channel := make(chan int)
那如何向信道存消息和取消息呢?一個例子:
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告訴主線我執行完畢了」的問題來, 使用一個信道來告訴主線便可:
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線都沒有機會執行、、、
其實,無緩衝的信道永遠不會存儲數據,只負責數據的流通,爲何這麼講呢?
因此,你能夠測試下,不管如何,咱們測試到的無緩衝信道的大小都是0 (len(channel))
若是信道正有數據在流動,咱們還要加入數據,或者信道乾澀,咱們一直向無數據流入的空信道取數據呢?就會引發死鎖
一個死鎖的例子:
func main() {
ch := make(chan int)
<- ch // 阻塞main goroutine, 信道c被鎖
}
執行這個程序你會看到Go報這樣的錯誤:
fatal error: all goroutines are asleep - deadlock!
何謂死鎖? 操做系統有講過的,全部的線程或進程都在等待資源的釋放。如上的程序中, 只有一個goroutine, 因此當你向裏面加數據或者存數據的話,都會鎖死信道,而且阻塞當前 goroutine, 也就是全部的goroutine(其實就main線一個)都在等待信道的開放(沒人拿走數據信道是不會開放的),也就是死鎖咯。
我發現死鎖是一個頗有意思的話題,這裏有幾個死鎖的例子:
func main() {
ch := make(chan int)
ch <- 1 // 1流入信道,堵塞當前線, 沒人取走數據信道不會打開
fmt.Println("This line code wont run") //在此行執行以前Go就會報死鎖
}
var ch1 chan int = make(chan int)
var ch2 chan int = make(chan int)
func say(s string) {
fmt.Println(s)
ch1 <- <- ch2 // ch1 等待 ch2流出的數據
}
func main() {
go say("hello")
<- ch1 // 堵塞主線
}
其中主線等ch1中的數據流出,ch1等ch2的數據流出,可是ch2等待數據流入,兩個goroutine都在等,也就是死鎖。
c, quit := make(chan int), make(chan int)
go func() {
c <- 1 // c通道的數據沒有被其餘goroutine讀取走,堵塞當前goroutine
quit <- 0 // quit始終沒有辦法寫入數據
}()
<- quit // quit 等待數據的寫
仔細分析的話,是因爲:主線等待quit信道的數據流出,quit等待數據寫入,而func被c通道堵塞,全部goroutine都在等,因此死鎖。
簡單來看的話,一共兩個線,func線中流入c通道的數據並無在main線中流出,確定死鎖。
可是,是否果然 全部不成對向信道存取數據的狀況都是死鎖?
以下是個反例:
func main() {
c := make(chan int)
go func() {
c <- 1
}()
}
程序正常退出了,很簡單,並非咱們那個總結不起做用了,仍是由於一個讓人很囧的緣由,main又沒等待其它goroutine,本身先跑完了,因此沒有數據流入c信道,一共執行了一個goroutine, 而且沒有發生阻塞,因此沒有死鎖錯誤。
那麼死鎖的解決辦法呢?
最簡單的,把沒取走的數據取走,沒放入的數據放入,由於無緩衝信道不能承載數據,那麼就趕忙拿走!
具體來說,就死鎖例子3中的狀況,能夠這麼避免死鎖:
c, quit := make(chan int), make(chan int)
go func() {
c <- 1
quit <- 0
}()
<- c // 取走c的數據!
<-quit
另外一個解決辦法是緩衝信道, 即設置c有一個數據的緩衝大小:
c := make(chan int, 1)
這樣的話,c能夠緩存一個數據。也就是說,放入一個數據,c並不會掛起當前線, 再放一個纔會掛起當前線直到第一個數據被其餘goroutine取走, 也就是隻阻塞在容量必定的時候,不達容量不阻塞。
這十分相似咱們python中的隊列Queue不是嗎?
咱們已經知道,無緩衝信道從不存儲數據,流入的數據必需要流出才能夠。
觀察如下的程序:
var ch chan int = make(chan int)
func foo(id int) { //id: 這個routine的標號
ch <- id
}
func main() {
// 開啓5個routine
for i := 0; i < 5; i++ {
go foo(i)
}
// 取出信道中的數據
for i := 0; i < 5; i++ {
fmt.Print(<- ch)
}
}
咱們開了5個goroutine,而後又依次取數據。其實整個的執行過程細分的話,5個線的數據依次流過信道ch, main打印之, 而宏觀上咱們看到的即 無緩衝信道的數據是先到先出,可是無緩衝信道並不存儲數據,只負責數據的流通
終於到了這個話題了, 其實緩存信道用英文來說更爲達意: buffered channel.
緩衝這個詞意思是,緩衝信道不只能夠流通數據,還能夠緩存數據。它是有容量的,存入一個數據的話 , 能夠先放在信道里,沒必要阻塞當前線而等待該數據取走。
當緩衝信道達到滿的狀態的時候,就會表現出阻塞了,由於這時不再能承載更多的數據了,「大家必須把數據拿走,才能夠流入數據」。
在聲明一個信道的時候,咱們給make以第二個參數來指明它的容量(默認爲0,即無緩衝):
var ch chan int = make(chan int, 2) // 寫入2個元素都不會阻塞當前goroutine, 存儲個數達到2的時候會阻塞
以下的例子,緩衝信道ch能夠無緩衝的流入3個元素:
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
}
若是你再試圖流入一個數據的話,信道ch會阻塞main線, 報死鎖。
也就是說,緩衝信道會在滿容量的時候加鎖。
其實,緩衝信道是先進先出的,咱們能夠把緩衝信道看做爲一個線程安全的隊列:
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 3
}
你也許發現,上面的代碼一個一個地去讀取信道簡直太費事了,Go語言容許咱們使用range來讀取信道:
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
for v := range ch {
fmt.Println(v)
}
}
若是你執行了上面的代碼,會報死鎖錯誤的,緣由是range不等到信道關閉是不會結束讀取的。也就是若是緩衝信道乾涸了,那麼range就會阻塞當前goroutine, 因此死鎖咯。
那麼,咱們試着避免這種狀況,比較容易想到的是讀到信道爲空的時候就結束讀取:
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
for v := range ch {
fmt.Println(v)
if len(ch) <= 0 { // 若是現有數據量爲0,跳出循環
break
}
}
以上的方法是能夠正常輸出的,可是注意檢查信道大小的方法不能在信道存取都在發生的時候用於取出全部數據,這個例子是由於咱們只在ch中存了數據,如今一個一個往外取,信道大小是遞減的。
另外一個方式是顯式地關閉信道:
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
// 顯式地關閉信道
close(ch)
for v := range ch {
fmt.Println(v)
}
被關閉的信道會禁止數據流入, 是隻讀的。咱們仍然能夠從關閉的信道中取出數據,可是不能再寫入數據了。
那好,咱們回到最初的一個問題,使用信道堵塞主線,等待開出去的全部goroutine跑完。
這是一個模型,開出不少小goroutine, 它們各自跑各自的,最後跑完了向主線報告。
咱們討論以下2個版本的方案:
對於方案1, 示例的代碼大概會是這個樣子:
var quit chan int // 只開一個信道
func foo(id int) {
fmt.Println(id)
quit <- 0 // ok, finished
}
func main() {
count := 1000
quit = make(chan int) // 無緩衝
for i := 0; i < count; i++ {
go foo(i)
}
for i := 0; i < count; i++ {
<- quit
}
}
對於方案2, 把信道換成緩衝1000的:
quit = make(chan int, count) // 容量1000
其實區別僅僅在於一個是緩衝的,一個是非緩衝的。
對於這個場景而言,二者都能完成任務, 都是能夠的。
不知道你有沒有注意到一個現象,仍是這段代碼,若是我跑在兩個goroutines裏面的話:
var quit chan int = make(chan int)
func loop() {
for i := 0; i < 10; i++ {
fmt.Printf("%d ", i)
}
quit <- 0
}
func main() {
// 開兩個goroutine跑函數loop, loop函數負責打印10個數
go loop()
go loop()
for i := 0; i < 2; i++ {
<- quit
}
}
咱們觀察下輸出:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
這是否是有什麼問題??
之前咱們用線程去作相似任務的時候,系統的線程會搶佔式地輸出,表現出來的是亂序地輸出。而goroutine爲何是這樣輸出的呢?
咱們找個例子測試下:
package main
import "fmt"
import "time"
var quit chan int
func foo(id int) {
fmt.Println(id)
time.Sleep(time.Second) // 停頓一秒
quit <- 0 // 發消息:我執行完啦!
}
func main() {
count := 1000
quit = make(chan int, count) // 緩衝1000個數據
for i := 0; i < count; i++ { //開1000個goroutine
go foo(i)
}
for i :=0 ; i < count; i++ { // 等待全部完成消息發送完畢。
<- quit
}
}
讓咱們跑一下這個程序(之因此先編譯再運行,是爲了讓程序跑的儘可能快,測試結果更好):
go build test.go
time ./test
./test 0.01s user 0.01s system 1% cpu 1.016 total
咱們看到,總計用時接近一秒。貌似並行了!
咱們須要首先考慮下什麼是併發, 什麼是並行
從概念上講,併發和並行是不一樣的, 簡單來講看這個圖片(原圖來自這裏)
更多的資料: 併發不是並行, 固然Google上有更多關於並行和併發的區別。
那麼回到一開始的疑問上,從上面的兩個例子執行後的表現來看,多個goroutine跑loop函數會挨個goroutine去進行,而sleep則是一塊兒執行的。
這是爲何?
默認地, Go全部的goroutines只能在一個線程裏跑 。
也就是說,以上兩個代碼都不是並行的,可是都是是併發的。
若是當前goroutine不發生阻塞,它是不會讓出CPU給其餘goroutine的, 因此例子一中的輸出會是一個一個goroutine進行的,而sleep函數則阻塞掉了當前goroutine, 當前goroutine主動讓其餘goroutine執行, 因此造成了邏輯上的並行, 也就是併發。
爲了達到真正的並行,咱們須要告訴go咱們容許同時最多使用多個核。
回到起初的例子,咱們設置最大開2個原生線程, 咱們須要用到runtime包(runtime包是goroutine的調度器):
import (
"fmt"
"runtime"
)
var quit chan int = make(chan int)
func loop() {
for i := 0; i < 100; i++ { //爲了觀察,跑多些
fmt.Printf("%d ", i)
}
quit <- 0
}
func main() {
runtime.GOMAXPROCS(2) // 最多使用2個核
go loop()
go loop()
for i := 0; i < 2; i++ {
<- quit
}
}
這下會看到兩個goroutine會搶佔式地輸出數據了。
咱們還能夠這樣顯式地讓出CPU時間:
func loop() {
for i := 0; i < 10; i++ {
runtime.Gosched() // 顯式地讓出CPU時間給其餘goroutine
fmt.Printf("%d ", i)
}
quit <- 0
}
func main() {
go loop()
go loop()
for i := 0; i < 2; i++ {
<- quit
}
}
觀察下結果會看到這樣有規律的輸出:
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
其實,這種主動讓出CPU時間的方式仍然是在單核裏跑。但手工地切換goroutine致使了看上去的"並行"。
其實做爲一個Python程序員,goroutine讓我更多地想到的是gevent的協程,而不是原生線程。
關於runtime包對goroutine的調度,在stackoverflow上有一個不錯的答案:http://stackoverflow.com/questions/13107958/what-exactly-does-runtime-gosched-do
我在Segmentfault看到了這個問題: http://segmentfault.com/q/1010000000207474
題目說,以下的程序,按照理解應該打印下5次 "world"呀,但是爲何什麼也沒有打印
package main
import (
"fmt"
)
func say(s string) {
for i := 0; i < 5; i++ {
fmt.Println(s)
}
}
func main() {
go say("world") //開一個新的Goroutines執行
for {
}
}
樓下的答案已經很棒了,這裏Go仍然在使用單核,for死循環佔據了單核CPU全部的資源,而main線和say兩個goroutine都在一個線程裏面,因此say沒有機會執行。解決方案仍是兩個:
runtime調度器是個很神奇的東西,可是我真是希望它不存在,我但願顯式調度能更爲天然些,多核處理默認開啓。
關於runtime包幾個函數:
咱們從例子中能夠看到,默認的, 全部goroutine會在一個原生線程裏跑,也就是隻使用了一個CPU核。
在同一個原生線程裏,若是當前goroutine不發生阻塞,它是不會讓出CPU時間給其餘同線程的goroutines的,這是Go運行時對goroutine的調度,咱們也可使用runtime包來手工調度。
本文開頭的兩個例子都是限制在單核CPU裏執行的,全部的goroutines跑在一個線程裏面,分析以下:
那麼關於咱們開啓多核的時候呢?Go語言對goroutine的調度行爲又是怎麼樣的?
咱們能夠在Golang官方網站的這裏 找到一句話:
When a coroutine blocks, such as by calling a blocking system call, the run-time automatically moves other coroutines on the same operating system thread to a different, runnable thread so they won't be blocked.
也就是說:
當一個goroutine發生阻塞,Go會自動地把與該goroutine處於同一系統線程的其餘goroutines轉移到另外一個系統線程上去,以使這些goroutines不阻塞
仍然須要作一個實驗,來測試下多核支持下goroutines的對原生線程的分配, 也驗證下咱們所獲得的結論"goroutine不阻塞不放開CPU"。
實驗代碼以下:
package main
import (
"fmt"
"runtime"
)
var quit chan int = make(chan int)
func loop(id int) { // id: 該goroutine的標號
for i := 0; i < 10; i++ { //打印10次該goroutine的標號
fmt.Printf("%d ", id)
}
quit <- 0
}
func main() {
runtime.GOMAXPROCS(2) // 最多同時使用2個核
for i := 0; i < 3; i++ { //開三個goroutine
go loop(i)
}
for i := 0; i < 3; i++ {
<- quit
}
}
多跑幾回會看到相似這些輸出(不一樣機器環境不同):
0 0 0 0 0 1 1 0 0 1 0 0 1 0 1 2 1 2 1 2 1 2 1 2 1 2 2 2 2 2
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2
0 0 0 0 0 0 0 1 1 1 1 1 0 1 0 1 0 1 2 1 2 1 2 2 2 2 2 2 2 2
0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 2 0 2 0 2 2 2 2 2 2 2 2
0 0 0 0 0 0 0 1 0 0 1 0 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 2 2
執行它咱們會發現如下現象:
那麼,咱們還會觀察到一個現象,不管是搶佔地輸出仍是順序的輸出,都會有那麼兩個數字表現出這樣的現象:
緣由是, 3個goroutine分配到至多2個線程上,就會至少兩個goroutine分配到同一個線程裏,單線程裏的goroutine 不阻塞不放開CPU, 也就發生了順序輸出。
如下設計模式和應用場景來自Google IO上的關於Goroutine的PPT:https://talks.golang.org/2012/concurrency.slide
本文的示例代碼在: https://github.com/hit9/Go-patterns-with-channel
在Python中咱們可使用yield關鍵字來讓一個函數成爲生成器,在Go中咱們可使用信道來製造生成器(一種lazy load相似的東西)。
固然咱們的信道並非簡單的作阻塞主線的功能來使用的哦。
下面是一個製做自增整數生成器的例子,直到主線向信道索要數據,咱們才添加數據到信道
func xrange() chan int{ // xrange用來生成自增的整數
var ch chan int = make(chan int)
go func() { // 開出一個goroutine
for i := 0; ; i++ {
ch <- i // 直到信道索要數據,才把i添加進信道
}
}()
return ch
}
func main() {
generator := xrange()
for i:=0; i < 1000; i++ { // 咱們生成1000個自增的整數!
fmt.Println(<-generator)
}
}
這不由叫我想起了python中可愛的xrange, 因此給了生成器這個名字!
好比咱們加載一個網站的時候,例如咱們登入新浪微博,咱們的消息數據應該來自一個獨立的服務,這個服務只負責返回某個用戶的新的消息提醒。
以下是一個使用示例:
func get_notification(user string) chan string{
/*
* 此處能夠查詢數據庫獲取新消息等等..
*/
notifications := make(chan string)
go func() { // 懸掛一個信道出去
notifications <- fmt.Sprintf("Hi %s, welcome to weibo.com!", user)
}()
return notifications
}
func main() {
jack := get_notification("jack") // 獲取jack的消息
joe := get_notification("joe") // 獲取joe的消息
// 獲取消息的返回
fmt.Println(<-jack)
fmt.Println(<-joe)
}
上面的例子都使用一個信道做爲返回值,能夠把信道的數據合併到一個信道的。不過這樣的話,咱們須要按順序輸出咱們的返回值(先進先出)。
以下,咱們假設要計算很複雜的一個運算 100-x , 分爲三路計算,最後統一在一個信道中取出結果:
func do_stuff(x int) int { // 一個比較耗時的事情,好比計算
time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond) //模擬計算
return 100 - x // 假如100-x是一個很費時的計算
}
func branch(x int) chan int{ // 每一個分支開出一個goroutine作計算並把計算結果流入各自信道
ch := make(chan int)
go func() {
ch <- do_stuff(x)
}()
return ch
}
func fanIn(chs... chan int) chan int {
ch := make(chan int)
for _, c := range chs {
// 注意此處明確傳值
go func(c chan int) {ch <- <- c}(c) // 複合
}
return ch
}
func main() {
result := fanIn(branch(1), branch(2), branch(3))
for i := 0; i < 3; i++ {
fmt.Println(<-result)
}
}
go有一個語句叫作select,用於監測各個信道的數據流動。
以下的程序是select的一個使用例子,咱們監視三個信道的數據流出並收集數據到一個信道中。
func foo(i int) chan int {
c := make(chan int)
go func () { c <- i }()
return c
}
func main() {
c1, c2, c3 := foo(1), foo(2), foo(3)
c := make(chan int)
go func() { // 開一個goroutine監視各個信道數據輸出並收集數據到信道c
for {
select { // 監視c1, c2, c3的流出,並所有流入信道c
case v1 := <- c1: c <- v1
case v2 := <- c2: c <- v2
case v3 := <- c3: c <- v3
}
}
}()
// 阻塞主線,取出信道c的數據
for i := 0; i < 3; i++ {
fmt.Println(<-c) // 從打印來看咱們的數據輸出並非嚴格的1,2,3順序
}
}
有了select, 咱們在多路複合中的示例代碼中的函數fanIn還能夠這麼來寫(這樣就不用開好幾個goroutine來取數據了):
func fanIn(branches ... chan int) chan int {
c := make(chan int)
go func() {
for i := 0 ; i < len(branches); i++ { //select會嘗試着依次取出各個信道的數據
select {
case v1 := <- branches[i]: c <- v1
}
}
}()
return c
}
使用select的時候,有時須要超時處理, 其中的timeout信道至關有趣:
timeout := time.After(1 * time.Second) // timeout 是一個計時信道, 若是達到時間了,就會發一個信號出來
for is_timeout := false; !is_timeout; {
select { // 監視信道c1, c2, c3, timeout信道的數據流出
case v1 := <- c1: fmt.Printf("received %d from c1", v1)
case v2 := <- c2: fmt.Printf("received %d from c2", v2)
case v3 := <- c3: fmt.Printf("received %d from c3", v3)
case <- timeout: is_timeout = true // 超時
}
}
在Go併發與並行筆記一咱們已經講過信道的一個很重要也很日常的應用,就是使用無緩衝信道來阻塞主線,等待goroutine結束。
這樣咱們沒必要再使用timeout。
那麼對上面的timeout來結束主線的方案做個更新:
func main() {
c, quit := make(chan int), make(chan int)
go func() {
c <- 2 // 添加數據
quit <- 1 // 發送完成信號
} ()
for is_quit := false; !is_quit; {
select { // 監視信道c的數據流出
case v := <-c: fmt.Printf("received %d from c", v)
case <-quit: is_quit = true // quit信道有輸出,關閉for循環
}
}
}
簡單地來講,數據從一端流入,從另外一端流出,看上去好像一個鏈表,不知道爲何要取這麼個尷尬的名字。。
菊花鏈的英文名字叫作: Daisy-chain, 它的一個應用就是作過濾器,好比咱們來篩下100之內的素數(你須要先知道什麼是篩法)
程序有詳細的註釋,再也不說明了。
/*
* 利用信道菊花鏈篩法求某一個整數範圍的素數
* 篩法求素數的基本思想是:把從1開始的、某一範圍內的正整數從小到大順序排列,
* 1不是素數,首先把它篩掉。剩下的數中選擇最小的數是素數,而後去掉它的倍數。
* 依次類推,直到篩子爲空時結束
*/
package main
import "fmt"
func xrange() chan int{ // 從2開始自增的整數生成器
var ch chan int = make(chan int)
go func() { // 開出一個goroutine
for i := 2; ; i++ {
ch <- i // 直到信道索要數據,才把i添加進信道
}
}()
return ch
}
func filter(in chan int, number int) chan int {
// 輸入一個整數隊列,篩出是number倍數的, 不是number的倍數的放入輸出隊列
// in: 輸入隊列
out := make(chan int)
go func() {
for {
i := <- in // 從輸入中取一個
if i % number != 0 {
out <- i // 放入輸出信道
}
}
}()
return out
}
func main() {
const max = 100 // 找出100之內的全部素數
nums := xrange() // 初始化一個整數生成器
number := <-nums // 從生成器中抓一個整數(2), 做爲初始化整數
for number <= max { // number做爲篩子,當篩子超過max的時候結束篩選
fmt.Println(number) // 打印素數, 篩子即一個素數
nums = filter(nums, number) //篩掉number的倍數
number = <- nums // 更新篩子
}
}
信道能夠作生成器使用,做爲一個特殊的例子,它還能夠用做隨機數生成器。以下是一個隨機01生成器:
func rand01() chan int {
ch := make(chan int)
go func () {
for {
select { //select會嘗試執行各個case, 若是均可以執行,那麼隨機選一個執行
case ch <- 0:
case ch <- 1:
}
}
}()
return ch
}
func main() {
generator := rand01() //初始化一個01隨機生成器
//測試,打印10個隨機01
for i := 0; i < 10; i++ {
fmt.Println(<-generator)
}
}
咱們剛纔其實已經接觸了信道做爲定時器, time包裏的After會製做一個定時器。
看看咱們的定時器吧!
/*
* 利用信道作定時器
*/
package main
import (
"fmt"
"time"
)
func timer(duration time.Duration) chan bool {
ch := make(chan bool)
go func() {
time.Sleep(duration)
ch <- true // 到時間啦!
}()
return ch
}
func main() {
timeout := timer(time.Second) // 定時1s
for {
select {
case <- timeout:
fmt.Println("already 1s!") // 到時間
return //結束程序
}
}
}
Google的應用場景例子。
本篇主要總結了使用信道, goroutine的一些設計模式。
2017年07月05日 16:12:12
閱讀數:6093
Channel是Go中的一個核心類型,你能夠把它當作一個管道,經過它併發核心單元就能夠發送或者接收數據進行通信(communication)。
它的操做符是箭頭 <- 。
ch <- v // 發送值v到Channel ch中
v := <-ch // 從Channel ch中接收數據,並將數據賦值給v
(箭頭的指向就是數據的流向)
就像 map 和 slice 數據類型同樣, channel必須先建立再使用:
ch := make(chan int)
Channel類型的定義格式以下:
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .
它包括三種類型的定義。可選的<-表明channel的方向。若是沒有指定方向,那麼Channel就是雙向的,既能夠接收數據,也能夠發送數據。
chan T // 能夠接收和發送類型爲 T 的數據
chan<- float64 // 只能夠用來發送 float64 類型的數據
<-chan int // 只能夠用來接收 int 類型的數據
<-老是優先和最左邊的類型結合。(The <- operator associates with the leftmost chan possible)
chan<- chan int // 等價 chan<- (chan int)
chan<- <-chan int // 等價 chan<- (<-chan int)
<-chan <-chan int // 等價 <-chan (<-chan int)
chan (<-chan int)
使用make初始化Channel,而且能夠設置容量:
make(chan int, 100)
容量(capacity)表明Channel容納的最多的元素的數量,表明Channel的緩存的大小。
若是沒有設置容量,或者容量設置爲0, 說明Channel沒有緩存,只有sender和receiver都準備好了後它們的通信(communication)纔會發生(Blocking)。若是設置了緩存,就有可能不發生阻塞,只有buffer滿了後 send纔會阻塞,而只有緩存空了後receive纔會阻塞。一個nil channel不會通訊。
能夠經過內建的close方法能夠關閉Channel。
你能夠在多個goroutine從/往一個channel 中 receive/send 數據, 沒必要考慮額外的同步措施。
Channel能夠做爲一個先入先出(FIFO)的隊列,接收的數據和發送的數據的順序是一致的。
channel的 receive支持 multi-valued assignment,如
v, ok := <-ch
它能夠用來檢查Channel是否已經被關閉了。
SendStmt = Channel "<-" Expression .
Channel = Expression .
在通信(communication)開始前channel和expression必選先求值出來(evaluated),好比下面的(3+4)先計算出7而後再發送給channel。
c := make(chan int)
defer close(c)
go func() { c <- 3 + 4 }()
i := <-c
fmt.Println(i)
send被執行前(proceed)通信(communication)一直被阻塞着。如前所言,無緩存的channel只有在receiver準備好後send才被執行。若是有緩存,而且緩存未滿,則send會被執行。
往一個已經被close的channel中繼續發送數據會致使run-time panic。
往nil channel中發送數據會一致被阻塞着。
從一個nil channel中接收數據會一直被block。
從一個被close的channel中接收數據不會被阻塞,而是當即返回,接收完已發送的數據後會返回元素類型的零值(zero value)。
如前所述,你可使用一個額外的返回參數來檢查channel是否關閉。
x, ok := <-ch
x, ok = <-ch
var x, ok = <-ch
若是OK 是false,代表接收的x是產生的零值,這個channel被關閉了或者爲空。
缺省狀況下,發送和接收會一直阻塞着,直到另外一方準備好。這種方式能夠用來在gororutine中進行同步,而沒必要使用顯示的鎖或者條件變量。
如官方的例子中x, y := <-c, <-c這句會一直等待計算結果發送到channel中。
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x+y)
}
make的第二個參數指定緩存的大小:ch := make(chan int, 100)。
經過緩存的使用,能夠儘可能避免阻塞,提供應用的性能。
for …… range語句能夠處理Channel。
func main() {
go func() {
time.Sleep(1 * time.Hour)
}()
c := make(chan int)
go func() {
for i := 0; i < 10; i = i + 1 {
c <- i
}
close(c)
}()
for i := range c {
fmt.Println(i)
}
fmt.Println("Finished")
}
range c產生的迭代值爲Channel中發送的值,它會一直迭代直到channel被關閉。上面的例子中若是把close(c)註釋掉,程序會一直阻塞在for …… range那一行。
select語句選擇一組可能的send操做和receive操做去處理。它相似switch,可是隻是用來處理通信(communication)操做。
它的case能夠是send語句,也能夠是receive語句,亦或者default。
receive語句能夠將值賦值給一個或者兩個變量。它必須是一個receive操做。
最多容許有一個default case,它能夠放在case列表的任何位置,儘管咱們大部分會將它放在最後。
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
若是有同時多個case去處理,好比同時有多個channel能夠接收數據,那麼Go會僞隨機的選擇一個case處理(pseudo-random)。若是沒有case須要處理,則會選擇default去處理,若是default case存在的狀況下。若是沒有default case,則select語句會阻塞,直到某個case須要處理。
須要注意的是,nil channel上的操做會一直被阻塞,若是沒有default case,只有nil channel的select會一直被阻塞。
select語句和switch語句同樣,它不是循環,它只會選擇一個case來處理,若是想一直處理channel,你能夠在外面加一個無限的for循環:
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
select有很重要的一個應用就是超時處理。由於上面咱們提到,若是沒有case須要處理,select語句就會一直阻塞着。這時候咱們可能就須要一個超時操做,用來處理超時的狀況。
下面這個例子咱們會在2秒後往channel c1中發送一個數據,可是select設置爲1秒超時,所以咱們會打印出timeout 1,而不是result 1。
import "time"
import "fmt"
func main() {
c1 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
c1 <- "result 1"
}()
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout 1")
}
}
其實它利用的是time.After方法,它返回一個類型爲<-chan Time的單向的channel,在指定的時間發送一個當前時間給返回的channel中。
咱們看一下關於時間的兩個Channel。
timer是一個定時器,表明將來的一個單一事件,你能夠告訴timer你要等待多長時間,它提供一個Channel,在未來的那個時間那個Channel提供了一個時間值。下面的例子中第二行會阻塞2秒鐘左右的時間,直到時間到了纔會繼續執行。
timer1 := time.NewTimer(time.Second * 2)
<-timer1.C
fmt.Println("Timer 1 expired")
固然若是你只是想單純的等待的話,可使用time.Sleep來實現。
你還可使用timer.Stop來中止計時器。
timer2 := time.NewTimer(time.Second)
go func() {
<-timer2.C
fmt.Println("Timer 2 expired")
}()
stop2 := timer2.Stop()
if stop2 {
fmt.Println("Timer 2 stopped")
}
ticker是一個定時觸發的計時器,它會以一個間隔(interval)往Channel發送一個事件(當前時間),而Channel的接收者能夠以固定的時間間隔從Channel中讀取事件。下面的例子中ticker每500毫秒觸發一次,你能夠觀察輸出的時間。
ticker := time.NewTicker(time.Millisecond * 500)
go func() {
for t := range ticker.C {
fmt.Println("Tick at", t)
}
}()
相似timer, ticker也能夠經過Stop方法來中止。一旦它中止,接收者再也不會從channel中接收數據了。
內建的close方法能夠用來關閉channel。
總結一下channel關閉後sender的receiver操做。
若是channel c已經被關閉,繼續往它發送數據會致使panic: send on closed channel:
import "time"
func main() {
go func() {
time.Sleep(time.Hour)
}()
c := make(chan int, 10)
c <- 1
c <- 2
close(c)
c <- 3
}
可是從這個關閉的channel中不但能夠讀取出已發送的數據,還能夠不斷的讀取零值:
c := make(chan int, 10)
c <- 1
c <- 2
close(c)
fmt.Println(<-c) //1
fmt.Println(<-c) //2
fmt.Println(<-c) //0
fmt.Println(<-c) //0
可是若是經過range讀取,channel關閉後for循環會跳出:
c := make(chan int, 10)
c <- 1
c <- 2
close(c)
for i := range c {
fmt.Println(i)
}
經過i, ok := <-c能夠查看Channel的狀態,判斷值是零值仍是正常讀取的值。
c := make(chan int, 10)
close(c)
i, ok := <-c
fmt.Printf("%d, %t", i, ok) //0, false
channel能夠用在goroutine之間的同步。
下面的例子中main goroutine經過done channel等待worker完成任務。 worker作完任務後只需往channel發送一個數據就能夠通知main goroutine任務完成。
import (
"fmt"
"time"
)
func worker(done chan bool) {
time.Sleep(time.Second)
// 通知任務已完成
done <- true
}
func main() {
done := make(chan bool, 1)
go worker(done)
// 等待任務完成
<-done
}
本身畢業工做事後,因爲時間有限,一度中斷了寫學習筆記。
最近心血來潮,逛CSDN發現博客已經改版了,添加了對Markdown語法的支持。加之最近在學習google的go語言,所以想借寫博客的機會學習Markdown語言,同時梳理所學的go語言知識。
本文主要講解go標準庫sync中的WaitGroup的用法。
WaitGroup用於goroutine的同步,當須要阻塞當前執行線程,等待一組goroutine執行完畢以後再繼續執行當前線程時,就須要用到WaitGroup。
type WaitGroup struct {
state1 [12]byte
sema uint32
}
WaitGroup的定義比較簡單,由一個12字節額 state1 字段和一個32位的 sema 組成。
1.Add(delta int)
Add的原型聲明以下:
func (wg *WaitGroup) Add(delta int)
Add函數接受一個int型的參數delta,用於設置WaitGroup實例(wg)的計數器,當計數器變爲0時,全部由於在該wg上調用Wait而阻塞的goroutine的都會被喚醒。
若調用Add致使計數器變爲負數,會引發panic。
2.Done()
Done的原型聲明以下:
func (wg *WaitGroup) Done()
Done函數的做用很簡單,就是將wg的計數器減一。該函數等同於wg.Add(-1),從go的源碼中能夠看到Done()就是用wg.Add(-1)實現的。
// Done decrements the WaitGroup counter.
func (wg *WaitGroup) Done() {
wg.Add(-1)
}
3.Wait()
Wait的原型聲明以下:
func (wg *WaitGroup) Wait()
Wait會阻塞調用該函數的goroutine,直到wg的計數器變爲1。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int){
fmt.Println("Hello world",i)
wg.Done()
}(i)
}
wg.Wait()
}
該程序啓動了十個goroutine,並在main函數中使用wg.Wait等待這些goroutine執行結束。該程序的輸出以下:
Hello world 9
Hello world 2
Hello world 3
Hello world 4
Hello world 5
Hello world 6
Hello world 7
Hello world 0
Hello world 8
Hello world 1
若去掉示例代碼中wg的使用,則不會輸出任何信息,由於main函數在goroutine執行以前就結束了。
Go語言中WaitGroup的用途是它可以一直等到全部的goroutine執行完成,而且阻塞主線程的執行,直到全部的goroutine執行完成。以前一直使用也沒有問題,但最近經過同事的一段代碼引發了關於WaitGroup的注意,下面這篇文章就介紹了WaitGroup使用時須要注意的坑及填坑。
前言
WaitGroup在go語言中,用於線程同步,單從字面意思理解,wait等待的意思,group組、團隊的意思,WaitGroup就是指等待一組,等待一個系列執行完成後纔會繼續向下執行。Golang 中的 WaitGroup 一直是同步 goroutine 的推薦實踐。本身用了兩年多也沒遇到過什麼問題。
直到最近的一天同事扔過來一段奇怪的代碼:
第一個坑
複製代碼 代碼以下:
package main
import (
"log"
"sync"
)
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
go func(wg sync.WaitGroup, i int) {
wg.Add(1)
log.Printf("i:%d", i)
wg.Done()
}(wg, i)
}
wg.Wait()
log.Println("exit")
}
撇了一眼,以爲沒什麼問題。
然而,它的運行結果是這樣:
複製代碼 代碼以下:
2016/11/27 15:12:36 exit
[Finished in 0.7s]
或這樣:
複製代碼 代碼以下:
2016/11/27 15:21:51 i:2
2016/11/27 15:21:51 exit
[Finished in 0.8s]
或這樣:
複製代碼 代碼以下:
2016/11/27 15:22:51 i:3
2016/11/27 15:22:51 i:2
2016/11/27 15:22:51 exit
[Finished in 0.8s]
一度讓我覺得手上的 mac 也沒睡醒……
這個問題若是理解了 WaitGroup 的設計目的就很是容易 fix 啦。由於 WaitGroup 同步的是 goroutine, 而上面的代碼卻在 goroutine 中進行 Add(1) 操做。所以,可能在這些 goroutine 還沒來得及 Add(1) 已經執行 Wait 操做了。
因而代碼改爲了這樣:
第二個坑
複製代碼 代碼以下:
package main
import (
"log"
"sync"
)
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(wg sync.WaitGroup, i int) {
log.Printf("i:%d", i)
wg.Done()
}(wg, i)
}
wg.Wait()
log.Println("exit")
}
然而,mac 又睡了過去,並且是睡死了過去:
複製代碼 代碼以下:
2016/11/27 15:25:16 i:1
2016/11/27 15:25:16 i:2
2016/11/27 15:25:16 i:4
2016/11/27 15:25:16 i:0
2016/11/27 15:25:16 i:3
fatal error: all goroutines are asleep - deadlock!
wg 給拷貝傳遞到了 goroutine 中,致使只有 Add 操做,其實 Done操做是在 wg 的副本執行的。所以 Wait 就死鎖了。
因而代碼改爲了這樣:
填坑
複製代碼 代碼以下:
package main
import (
"log"
"sync"
)
func main() {
wg := &sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(wg *sync.WaitGroup, i int) {
log.Printf("i:%d", i)
wg.Done()
}(wg, i)
}
wg.Wait()
log.Println("exit")
}
總結
好了,到這裏終於解決了,以上就是關於Go語言WaitGroup使用時須要注意的一些坑,但願本文中提到的這些問題對你們學習或者使用Go語言的時候能有所幫助,若是有疑問你們能夠留言交流。
// 1. 聲明
var m map[string]int
// 2. 初始化,聲明以後必須初始化才能使用
// 向未初始化的map賦值引發 panic: assign to entry in nil map.
m = make(map[string]int)
m = map[string]int{}
// 1&2. 聲明並初始化
m := make(map[string]int)
m := map[string]int{}
// 3. 增刪改查
m["route"] = 66
delete(m, "route") // 若是key不存在什麼都不作
i := m["route"] // 三種查詢方式,若是key不存在返回value類型的零值
i, ok := m["route"]
_, ok := m["route"]
// 4. 迭代(順序不肯定)
for k, v := range m {
use(k, v)
}
// 5. 有序迭代
import "sort"
var keys []string
for k, _ := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
use(k, m[k]
}
// 聲明&初始化
var counter = struct {
sync.RWMutex // gard m
m map[string]int
}{m:make(map[string]int)}
// 讀鎖
counter.RLock()
counter.m["route"]
counter.RUnlock()
// 寫鎖
counter.Lock()
counter.m["route"]++
counter.Unlock()
visited := map[*Node]bool
if visited[node] { // bool類型0值爲false,因此不須要檢查ok
return
}
likes := make(map[string][]*Person)
for _, p range people {
for _, l range p.Likes {
// 向一個nil的slice增長值,會自動allocate一個slice
likes[l] = append(likes[l], p)
}
}
// map[k1]map[k2]v
hits := make(map[string]map[string]int)
func add(m map[string]map[string]int, path, country string) {
mm, ok := m[path]
if !ok {
mm = make(map[string]int) // 須要檢查、建立子map
m[path] = mm
}
mm[country]++
}
add(hits, "/", "cn")
n := hits["/"]["cn"]
// map[struct{k1, k2}]v
type Key struct {
Path, Country string
}
hits := make(map[Key]int)
hits[Key{"/", "cn"}]++
n := hits[Key{"/", "cn"}]
}
golang爲每一個類型定義了類型描述器_type,並實現了hashable類型的_type.alg.hash和_type.alg.equal。
type typeAlg struct {
// function for hashing objects of this type
// (ptr to object, seed) -> hash
hash func(unsafe.Pointer, uintptr) uintptr
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
}
bmap - A bucket for a go map. bmap只有一個域tophash [bucketCnt]uint8,它保存了key的hash值的高8位uint8(hash >> (sys.PtrSize*8 - 8));一個bucket包括一個bmap(tophash數組),緊跟的bucketCnt個keys和bucketCnt個values,以及一個overfolw指針。
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap {
...
// initialize Hmap
if h == nil {
h = (*hmap)(newobject(t.hmap))
}
h.count = 0
h.B = B
h.flags = 0
h.hash0 = fastrand()
h.buckets = buckets
h.oldbuckets = nil
h.nevacuate = 0
h.noverflow = 0
return h
}
golang的maptype保存了key的類型描述器,以供訪問map時調用key.alg.hash, key.alg.equal。
type maptype struct {
key *_type
elem *_type
...
}
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
...
// 併發訪問檢查
if h.flags&hashWriting != 0 {
throw("concurrent map read and map write")
}
// 計算key的hash值
alg := t.key.alg
hash := alg.hash(key, uintptr(h.hash0)) // alg.hash
// 計算key所在的bucket的index
m := uintptr(1)<<h.B - 1
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
// 計算tophash
top := uint8(hash >> (sys.PtrSize*8 - 8))
...
for {
for i := uintptr(0); i < bucketCnt; i++ {
// 檢查top值
if b.tophash[i] != top {
continue
}
// 取key的地址
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if alg.equal(key, k) { // alg.equal
// 取value得地址
v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
}
}
...
if b == nil {
// 返回零值
return unsafe.Pointer(&zeroVal[0])
}
}
}
這部分留待之後有機會再續。這裏暫時附上Keith Randall的slide做爲參考。
結構體定義
上面咱們說過Go的指針和C的不一樣,結構體也是同樣的。Go是一門刪繁就簡的語言,一切使人困惑的特性都必須去掉。
簡單來說,Go提供的結構體就是把使用各類數據類型定義的不一樣變量組合起來的高級數據類型。閒話很少說,看例子:
type Rect struct {
width float64
length float64
}
上面咱們定義了一個矩形結構體,首先是關鍵是type表示要定義一個新的數據類型了,而後是新的數據類型名稱Rect,最後是struct關鍵字,表示這個高級數據類型是結構體類型。在上面的例子中,由於width和length的數據類型相同,還能夠寫成以下格式:
type Rect struct {
width, length float64
}
好了,來用結構體乾點啥吧,計算一下矩形面積。
package main
import (
"fmt"
)
type Rect struct {
width, length float64
}
func main() {
var rect Rect
rect.width = 100
rect.length = 200
fmt.Println(rect.width * rect.length)
}
從上面的例子看到,其實結構體類型和基礎數據類型使用方式差很少,惟一的區別就是結構體類型能夠經過.來訪問內部的成員。包括給內部成員賦值和讀取內部成員值。
在上面的例子中,咱們是用var關鍵字先定義了一個Rect變量,而後對它的成員賦值。咱們也可使用初始化的方式來給Rect變量的內部成員賦值。
package main
import (
"fmt"
)
type Rect struct {
width, length float64
}
func main() {
var rect = Rect{width: 100, length: 200}
fmt.Println(rect.width * rect.length)
}
固然若是你知道結構體成員定義的順序,也能夠不使用key:value的方式賦值,直接按照結構體成員定義的順序給它們賦值。
package main
import (
"fmt"
)
type Rect struct {
width, length float64
}
func main() {
var rect = Rect{100, 200}
fmt.Println("Width:", rect.width, "* Length:",
rect.length, "= Area:", rect.width*rect.length)
}
輸出結果爲
Width: 100 * Length: 200 = Area: 20000
結構體參數傳遞方式
咱們說過,Go函數的參數傳遞方式是值傳遞,這句話對結構體也是適用的。
package main
import (
"fmt"
)
type Rect struct {
width, length float64
}
func double_area(rect Rect) float64 {
rect.width *= 2
rect.length *= 2
return rect.width * rect.length
}
func main() {
var rect = Rect{100, 200}
fmt.Println(double_area(rect))
fmt.Println("Width:", rect.width, "Length:", rect.length)
}
上面的例子輸出爲:
80000
Width: 100 Length: 200
也就說雖然在double_area函數裏面咱們將結構體的寬度和長度都加倍,但仍然沒有影響main函數裏面的rect變量的寬度和長度。
結構體組合函數
上面咱們在main函數中計算了矩形的面積,可是咱們以爲矩形的面積若是可以做爲矩形結構體的"內部函數"提供會更好。這樣咱們就能夠直接說這個矩形面積是多少,而不用另外去取寬度和長度去計算。如今咱們看看結構體"內部函數"定義方法:
package main
import (
"fmt"
)
type Rect struct {
width, length float64
}
func (rect Rect) area() float64 {
return rect.width * rect.length
}
func main() {
var rect = Rect{100, 200}
fmt.Println("Width:", rect.width, "Length:", rect.length,
"Area:", rect.area())
}
咦?這個是什麼"內部方法",根本沒有定義在Rect數據類型的內部啊?
確實如此,咱們看到,雖然main函數中的rect變量能夠直接調用函數area()來獲取矩形面積,可是area()函數確實沒有定義在Rect結構體內部,這點和C語言的有很大不一樣。Go使用組合函數的方式來爲結構體定義結構體方法。咱們仔細看一下上面的area()函數定義。
首先是關鍵字func表示這是一個函數,第二個參數是結構體類型和實例變量,第三個是函數名稱,第四個是函數返回值。這裏咱們能夠看出area()函數和普通函數定義的區別就在於area()函數多了一個結構體類型限定。這樣一來Go就知道了這是一個爲結構體定義的方法。
這裏須要注意一點就是定義在結構體上面的函數(function)通常叫作方法(method)。
結構體和指針
咱們在指針一節講到過,指針的主要做用就是在函數內部改變傳遞進來變量的值。對於上面的計算矩形面積的例子,咱們能夠修改一下代碼以下:
package main
import (
"fmt"
)
type Rect struct {
width, length float64
}
func (rect *Rect) area() float64 {
return rect.width * rect.length
}
func main() {
var rect = new(Rect)
rect.width = 100
rect.length = 200
fmt.Println("Width:", rect.width, "Length:", rect.length,
"Area:", rect.area())
}
上面的例子中,使用了new函數來建立一個結構體指針rect,也就是說rect的類型是*Rect,結構體遇到指針的時候,你不須要使用*去訪問結構體的成員,直接使用.引用就能夠了。因此上面的例子中咱們直接使用rect.width=100 和rect.length=200來設置結構體成員值。由於這個時候rect是結構體指針,因此咱們定義area()函數的時候結構體限定類型爲*Rect。
其實在計算面積的這個例子中,咱們不須要改變矩形的寬或者長度,因此定義area函數的時候結構體限定類型仍然爲Rect也是能夠的。以下:
package main
import (
"fmt"
)
type Rect struct {
width, length float64
}
func (rect Rect) area() float64 {
return rect.width * rect.length
}
func main() {
var rect = new(Rect)
rect.width = 100
rect.length = 200
fmt.Println("Width:", rect.width, "Length:", rect.length,
"Area:", rect.area())
}
這裏Go足夠聰明,因此rect.area()也是能夠的。
至於使不使用結構體指針和使不使用指針的出發點是同樣的,那就是你是否試圖在函數內部改變傳遞進來的參數的值。再舉個例子以下:
package main
import (
"fmt"
)
type Rect struct {
width, length float64
}
func (rect *Rect) double_area() float64 {
rect.width *= 2
rect.length *= 2
return rect.width * rect.length
}
func main() {
var rect = new(Rect)
rect.width = 100
rect.length = 200
fmt.Println(*rect)
fmt.Println("Double Width:", rect.width, "Double Length:", rect.length,
"Double Area:", rect.double_area())
fmt.Println(*rect)
}
這個例子的輸出是:
{100 200}
Double Width: 200 Double Length: 400 Double Area: 80000
{200 400}
結構體內嵌類型
咱們能夠在一個結構體內部定義另一個結構體類型的成員。例如iPhone也是Phone,咱們看下例子:
package main
import (
"fmt"
)
type Phone struct {
price int
color string
}
type IPhone struct {
phone Phone
model string
}
func main() {
var p IPhone
p.phone.price = 5000
p.phone.color = "Black"
p.model = "iPhone 5"
fmt.Println("I have a iPhone:")
fmt.Println("Price:", p.phone.price)
fmt.Println("Color:", p.phone.color)
fmt.Println("Model:", p.model)
}
輸出結果爲
I have a iPhone:
Price: 5000
Color: Black
Model: iPhone 5
在上面的例子中,咱們在結構體IPhone裏面定義了一個Phone變量phone,而後咱們能夠像正常的訪問結構體成員同樣訪問phone的成員數據。可是咱們原來的意思是"iPhone也是(is-a)Phone",而這裏的結構體IPhone裏面定義了一個phone變量,給人的感受就是"iPhone有一個(has-a)Phone",挺奇怪的。固然Go也知道這種方式很奇怪,因此支持以下作法:
package main
import (
"fmt"
)
type Phone struct {
price int
color string
}
type IPhone struct {
Phone
model string
}
func main() {
var p IPhone
p.price = 5000
p.color = "Black"
p.model = "iPhone 5"
fmt.Println("I have a iPhone:")
fmt.Println("Price:", p.price)
fmt.Println("Color:", p.color)
fmt.Println("Model:", p.model)
}
輸出結果爲
I have a iPhone:
Price: 5000
Color: Black
Model: iPhone 5
在這個例子中,咱們定義IPhone結構體的時候,再也不定義Phone變量,直接把結構體Phone類型定義在那裏。而後IPhone就能夠像訪問直接定義在本身結構體裏面的成員同樣訪問Phone的成員。
上面的例子中,咱們演示告終構體的內嵌類型以及內嵌類型的成員訪問,除此以外,假設結構體A內部定義了一個內嵌結構體B,那麼A同時也能夠調用全部定義在B上面的函數。
package main
import (
"fmt"
)
type Phone struct {
price int
color string
}
func (phone Phone) ringing() {
fmt.Println("Phone is ringing...")
}
type IPhone struct {
Phone
model string
}
func main() {
var p IPhone
p.price = 5000
p.color = "Black"
p.model = "iPhone 5"
fmt.Println("I have a iPhone:")
fmt.Println("Price:", p.price)
fmt.Println("Color:", p.color)
fmt.Println("Model:", p.model)
p.ringing()
}
輸出結果爲:
I have a iPhone:
Price: 5000
Color: Black
Model: iPhone 5
Phone is ringing...
接口
咱們先看一個例子,關於Nokia手機和iPhone手機都可以打電話的例子。
package main
import (
"fmt"
)
type NokiaPhone struct {
}
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
type IPhone struct {
}
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
func main() {
var nokia NokiaPhone
nokia.call()
var iPhone IPhone
iPhone.call()
}
咱們定義了NokiaPhone和IPhone,它們都有各自的方法call(),表示本身都可以打電話。可是咱們想想,是手機都應該可以打電話,因此這個不算是NokiaPhone或是IPhone的獨特特色。不然iPhone不可能賣這麼貴了。
再仔細看一下接口的定義,首先是關鍵字type,而後是接口名稱,最後是關鍵字interface表示這個類型是接口類型。在接口類型裏面,咱們定義了一組方法。
Go語言提供了一種接口功能,它把全部的具備共性的方法定義在一塊兒,任何其餘類型只要實現了這些方法就是實現了這個接口,不必定非要顯式地聲明要去實現哪些接口啦。好比上面的手機的call()方法,就徹底能夠定義在接口Phone裏面,而NokiaPhone和IPhone只要實現了這個接口就是一個Phone。
package main
import (
"fmt"
)
type Phone interface {
call()
}
type NokiaPhone struct {
}
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
type IPhone struct {
}
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
func main() {
var phone Phone
phone = new(NokiaPhone)
phone.call()
phone = new(IPhone)
phone.call()
}
在上面的例子中,咱們定義了一個接口Phone,接口裏面有一個方法call(),僅此而已。而後咱們在main函數裏面定義了一個Phone類型變量,並分別爲之賦值爲NokiaPhone和IPhone。而後調用call()方法,輸出結果以下:
I am Nokia, I can call you!
I am iPhone, I can call you!
之前咱們說過,Go語言式靜態類型語言,變量的類型在運行過程當中不能改變。可是在上面的例子中,phone變量好像先定義爲Phone類型,而後是NokiaPhone類型,最後成爲了IPhone類型,真的是這樣嗎?
原來,在Go語言裏面,一個類型A只要實現了接口X所定義的所有方法,那麼A類型的變量也是X類型的變量。在上面的例子中,NokiaPhone和IPhone都實現了Phone接口的call()方法,因此它們都是Phone,這樣一來是否是感受正常了一些。
咱們爲Phone添加一個方法sales(),再來熟悉一下接口用法。
package main
import (
"fmt"
)
type Phone interface {
call()
sales() int
}
type NokiaPhone struct {
price int
}
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
func (nokiaPhone NokiaPhone) sales() int {
return nokiaPhone.price
}
type IPhone struct {
price int
}
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
func (iPhone IPhone) sales() int {
return iPhone.price
}
func main() {
var phones = [5]Phone{
NokiaPhone{price: 350},
IPhone{price: 5000},
IPhone{price: 3400},
NokiaPhone{price: 450},
IPhone{price: 5000},
}
var totalSales = 0
for _, phone := range phones {
totalSales += phone.sales()
}
fmt.Println(totalSales)
}
輸出結果:
14200
上面的例子中,咱們定義了一個手機數組,而後計算手機的總售價。能夠看到,因爲NokiaPhone和IPhone都實現了sales()方法,因此它們都是Phone類型,可是計算售價的時候,Go會知道調用哪一個對象實現的方法。
接口類型還能夠做爲結構體的數據成員。
假設有個敗家子,iPhone沒有出的時候,買了好幾款Nokia,iPhone出來後,又買了好多部iPhone,老爸要來看看這小子一共花了多少錢。
package main
import (
"fmt"
)
type Phone interface {
sales() int
}
type NokiaPhone struct {
price int
}
func (nokiaPhone NokiaPhone) sales() int {
return nokiaPhone.price
}
type IPhone struct {
price int
}
func (iPhone IPhone) sales() int {
return iPhone.price
}
type Person struct {
phones []Phone
name string
age int
}
func (person Person) total_cost() int {
var sum = 0
for _, phone := range person.phones {
sum += phone.sales()
}
return sum
}
func main() {
var bought_phones = [5]Phone{
NokiaPhone{price: 350},
IPhone{price: 5000},
IPhone{price: 3400},
NokiaPhone{price: 450},
IPhone{price: 5000},
}
var person = Person{name: "Jemy", age: 25, phones: bought_phones[:]}
fmt.Println(person.name)
fmt.Println(person.age)
fmt.Println(person.total_cost())
}
這個例子純爲演示接口做爲結構體數據成員,若有雷同,純屬巧合。這裏面咱們定義了一個Person結構體,結構體內部定義了一個手機類型切片。另外咱們定義了Person的total_cost()方法用來計算手機花費總額。輸出結果以下:
Jemy
25
14200
小結
Go的結構體和接口的實現方法可謂刪繁就簡,去除了不少別的語言使人困惑的地方,並且學習難度也不大,很容易上手。不過因爲思想比較獨到,也有可能會有人以爲功能太簡單而無用,這個就各有見解了,不過在逐漸的使用過程當中,咱們會慢慢領悟到這種設計所帶來的好處,以及所避免的問題。
參數傳遞是指在程序的傳遞過程當中,實際參數就會將參數值傳遞給相應的形式參數,而後在函數中實現對數據處理和返回的過程。比較常見的參數傳遞有:值傳遞,按地址傳遞參數或者按數組傳遞參數。
一、常規傳遞
使用普通變量做爲函數參數的時候,在傳遞參數時只是對變量值得拷貝,即將實參的值複製給變參,當函數對變參進行處理時,並不會影響原來實參的值。
例如:
package main
import (
"fmt"
)
func swap(a int, b int) {
var temp int
temp = a
a = b
b = temp
}
func main() {
x := 5
y := 10
swap(x, y)
fmt.Print(x, y)
}
輸出結果:5 10
傳遞給swap的是x,y的值得拷貝,函數對拷貝的值作了交換,但卻沒有改變x,y的值。
二、指針傳遞
函數的變量不只可使用普通變量,還可使用指針變量,使用指針變量做爲函數的參數時,在進行參數傳遞時將是一個地址看唄,即將實參的內存地址複製給變參,這時對變參的修改也將會影響到實參的值。
咱們仍是用上面的的例子,稍做修改以下:
package main
import (
"fmt"
)
func swap(a *int, b *int) {
var temp int
temp = *a
*a = *b
*b = temp
}
func main() {
x := 5
y := 10
swap(&x, &y)
fmt.Print(x, y)
}
輸出結果:10 5
三、數組元素做爲函數參數
使用數組元素做爲函數參數時,其使用方法和普通變量相同,便是一個"值拷貝"。
例:
package main
import (
"fmt"
)
func function(a int) {
a += 100
}
func main() {
var s = [5]int{1, 2, 3, 4, 5}
function(s[2])
fmt.Print(s[2])
}
輸出結果:3
能夠看到將數組元素s[2]的值做爲函數的實參,無論對形參作什麼操做,實參都沒有改變。
四、數組名做爲函數參數
和其餘語言不一樣的是,go語言在將數組名做爲函數參數的時候,參數傳遞便是對數組的複製。在形參中對數組元素的修改都不會影響到數組元素原來的值。這個和上面的相似,就不貼代碼了,有興趣的自行編寫代碼測試下吧。
五、slice做爲函數參數
在使用slice做爲函數參數時,進行參數傳遞將是一個地址拷貝,即將底層數組的內存地址複製給參數slice。這時,對slice元素的操做就是對底層數組元素的操做。例如:
package main
import (
"fmt"
)
func function(s1 []int) {
s1[0] += 100
}
func main() {
var a = [5]int{1, 2, 3, 4, 5}
var s []int = a[:]
function(s)
fmt.Println(s[0])
}
運行結果:101
六、函數做爲參數
在go語言中,函數也做爲一種數據類型,因此函數也能夠做爲函數的參數來使用。例如:
package main
import (
"fmt"
)
func function(a, b int, sum func(int, int) int) {
fmt.Println(sum(a, b))
}
func sum(a, b int) int {
return a + b
}
func main() {
var a, b int = 5, 6
f := sum
function(a, b, f)
}
運行結果:11
函數sum做爲函數function的形參,而變量f是一個函數類型,做爲function()調用時的實參。
iota是golang語言的常量計數器,只能在常量的表達式中使用。
iota在const關鍵字出現時將被重置爲0(const內部的第一行以前),const中每新增一行常量聲明將使iota計數一次(iota可理解爲const語句塊中的行索引)。
使用iota能簡化定義,在定義枚舉時頗有用。
舉例以下:
一、iota只能在常量的表達式中使用。
fmt.Println(iota)
編譯錯誤: undefined: iota
二、每次 const 出現時,都會讓 iota 初始化爲0.
const a = iota // a=0
const (
b = iota //b=0
c //c=1
)
三、自定義類型
自增加常量常常包含一個自定義枚舉類型,容許你依靠編譯器完成自增設置。
type Stereotype int
const (
TypicalNoob Stereotype = iota // 0
TypicalHipster // 1
TypicalUnixWizard // 2
TypicalStartupFounder // 3
)
四、可跳過的值
設想你在處理消費者的音頻輸出。音頻可能不管什麼都沒有任何輸出,或者它多是單聲道,立體聲,或是環繞立體聲的。
這可能有些潛在的邏輯定義沒有任何輸出爲 0,單聲道爲 1,立體聲爲 2,值是由通道的數量提供。
因此你給 Dolby 5.1 環繞立體聲什麼值。
一方面,它有6個通道輸出,可是另外一方面,僅僅 5 個通道是全帶寬通道(所以 5.1 稱號 - 其中 .1 表示的是低頻效果通道)。
無論怎樣,咱們不想簡單的增長到 3。
咱們可使用下劃線跳過不想要的值。
type AudioOutput int
const (
OutMute AudioOutput = iota // 0
OutMono // 1
OutStereo // 2
_
_
OutSurround // 5
)
五、位掩碼錶達式
type Allergen int
const (
IgEggs Allergen = 1 << iota // 1 << 0 which is 00000001
IgChocolate // 1 << 1 which is 00000010
IgNuts // 1 << 2 which is 00000100
IgStrawberries // 1 << 3 which is 00001000
IgShellfish // 1 << 4 which is 00010000
)
這個工做是由於當你在一個 const 組中僅僅有一個標示符在一行的時候,它將使用增加的 iota 取得前面的表達式而且再運用它,。在 Go 語言的 spec 中, 這就是所謂的隱性重複最後一個非空的表達式列表。
若是你對雞蛋,巧克力和海鮮過敏,把這些 bits 翻轉到 "on" 的位置(從左到右映射 bits)。而後你將獲得一個 bit 值 00010011,它對應十進制的 19。
fmt.Println(IgEggs | IgChocolate | IgShellfish)
// output:
// 19
六、定義數量級
type ByteSize float64
const (
_ = iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota) // 1 << (10*1)
MB // 1 << (10*2)
GB // 1 << (10*3)
TB // 1 << (10*4)
PB // 1 << (10*5)
EB // 1 << (10*6)
ZB // 1 << (10*7)
YB // 1 << (10*8)
)
七、定義在一行的狀況
const (
Apple, Banana = iota + 1, iota + 2
Cherimoya, Durian
Elderberry, Fig
)
iota 在下一行增加,而不是當即取得它的引用。
// Apple: 1
// Banana: 2
// Cherimoya: 2
// Durian: 3
// Elderberry: 3
// Fig: 4
八、中間插隊
const (
i = iota
j = 3.14
k = iota
l
)
那麼打印出來的結果是 i=0,j=3.14,k=2,l=3