上一篇(一日一學_Go從錯誤中學習基礎一)講了部分Golang容易出錯地方,為了讓讀者清晰學習,我決定分開。
new()與make()使用
數組、結構體和所有的值類型都可以使用new,切片、映射和通道,使用make。
為了能更深刻的理解,不混淆使用new和make下面開展內存的分析,防止跳坑
type Person struct {
name string
age int
}
func main() {
p1 := Person{"wuxiao", 10}
p2 := &PersonP{"wuxiao", 15} // == new(Person)
}
可以看出p1在內存中是以連續的內存塊存在
十六進制值表示指針地址0x ..
,并引用實際數據。
在一些博客中,經常提到如下例子:
p1 := Person{"wuxiao", 10}
func setName(p Person) {
p.name = "xiaobai"
}
上面code進行了值拷貝
p2 := &PersonP{"wuxiao", 15}
func setName(p *Person) {
p.name = "dabai"
}
從上面分析看出,第一種情況在函數setName() 中修改p不會修改原始數據,但是在第二種情況下肯定會修改,因為存儲在p中的地址引用了原始數據塊。
總結: 將一個值類型作為一個參數傳遞給函數或者作為一個方法的接收者,似乎是對內存的濫用,因為值類型一直是傳遞拷貝。但是另一方面,值類型的內存是在棧上分配,內存分配快速且開銷不大。因此當使用指針代替值類型作為參數傳遞時,需要根據自己需求來使用。
接下來我以切片理解make。
我們創建一個具有6個元素的底層數組的切片,使用make([] int,len,cap)語法來指定容量。如下:
arr = make([]int, 5, 6)
arr[3] = 44
arr[4] = 333
我們創建另一個子切片并更改一些元素,會發生什么?
arr := make([]int, 5, 6)
arr[3] = 44
arr[4] = 333
subArr := arr[1:4]
subArr[1] = 77
修改了subArr同樣修改了底層數組,這就是為什么在Golang中切片使用廣泛的原因。
在使用內置函數append()對切片進行添加元素時,但在內部它做了很多復雜的工作,來進行內存分配。
arr := make([]int, 5, 6)
subArr := arr[:]
arr = append(arr, 1, 2)
使用append() 時會檢查該切片是否有未使用的容器個數,如果沒有,則分配更多的內存。 分配內存是一個相當昂貴的操作,因此append嘗試對該操作進行預估,一次增加原始容量的兩倍。 一次分配較多的內存通常比多次分配較少的內存更高效和更快。
分配更多的內存通常意味著分配新內存并從舊數組拷貝數據到新數組(導致地址值的改變)。
可以看出會有兩個不同的底層數組,這對初學者來說可能不經意中出錯。
協程與通道使用
協程與通道我認為是Golang的核心,為了能大家通俗易懂了解,防止在編寫代碼出錯,我接下來會以一些例子來講解核心部分。
無緩沖與有緩沖channel有什么區別?
一個是同步。
一個是非同步。
簡單點理解:
無緩沖 使用通道發送數據,必須有此通道類型的協程接收數據,才能繼續發送數據,要不然進入永久阻塞(死鎖)。
有緩沖 如果緩沖大小是1,在通道發送數據時,只有當放第二個值的時候,第一個還沒被人拿走,這時候才會阻塞。
下面三個例子更好說明了同步與非同步:
data := make(chan string)
//因為data沒有值,所以會選擇default執行(發送接收數據都
//是一樣的道理),這里就達成看一個無阻塞的效果。
select {
case msg := <-data:
fmt.Println("received data", msg)
default:
fmt.Println("no data.....")
}
執行結果:no data.....
data := make(chan string, 1) //給通道加上緩沖的話,會怎么選擇?
data <- "wuxiao"
//猜測:data已經有值,所以會選擇 received data 執行。
select {
case msg := <-data:
fmt.Println("received data", msg)
default:
fmt.Println("no data.....")
}
執行結果:received data wuxiao
- 如上面所說,在緩沖未裝滿時,給一個帶緩沖的緩存發送數據是不會阻塞的,而從緩沖讀取數據也不會阻塞。
data := make(chan string)//沒有緩沖了
data <- "wuxiao" //因為該channels沒有緩沖,發送數據,導致死鎖(有的書上寫為永遠阻塞)
select {
case msg := <-data:
fmt.Println("received data", msg)
default:
fmt.Println("no data.....")
}
執行結果: fatal error: all goroutines are asleep - deadlock!
上面為什么會報錯?
官方解釋到: Unbuffered channels combine communication—the exchange of a value—with synchronization—guaranteeing that two calculations (goroutines) are in a known state
無緩沖的信道進行通信,保證兩個協程處于已知狀態。
流水線模式
開發中我們可以使用協程與通道達到并發的效果,而且還可以根據自己需求使用并發設計模式來提高效率,并發設計模式有扇入和扇出,流水線等。
流水線模式可以簡單理解由多個階段組成的,相鄰的兩個階段由 channel 進行連接;每個階段是都有自己goroutine 。每個階段都會執行下面三個操作(除了第一個和最后一個階段):
1. 通過 channel 接收數據上流的數據
2. 對接收到的數據進行操作
3. 將新生成的數據通過 channels 發送數據給下游
顯然,第一個階段只有發送管道,而最后一個階段只有接收管道.
通常稱第一個階段可以理解為"生產者",稱最后一個階段理解為"消費者"。
//第一階段是為gen 函數,首先啟動一個 goroutine,
//通過goroutine 把數字發送到 channel,當數字發送完時關閉channel。
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
//第二階段是 sq 函數,它從第一階段返回的通道來接受整數,把
//所接收的整數發送自己創建的通道中并返回給下游,并且等第一
//階段通道數字全部發給下游會關閉了上流通道,然后在關閉自己創建的管道。
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
//main 函數為流水線的最后一個階段。
//會從第二階段接收數字,并逐個打印出來,直到來自于上游的接收管道關閉
func main() {
//由于 sq 函數的channel 類型一樣,所以組合任意個 sq 函數
for n := range sq(sq(gen(2, 4, 5)) ){
fmt.Println(n)
}
}
如果我把上面最后一個階段改成
out := range sq(sq(gen(2, 4, 5)) )
fmt.Println(<-out) // 16 or 256 , 625
這里存在資源泄漏。一方面goroutine 消耗內存和運行時資源,另一方面goroutine 棧中的堆引用會阻止 gc 執行回收操作。 既然goroutine 不能被回收,那么他們必須自己退出。
那么如何解決這個問題?
使用顯式取消。
在Go語言中,我們可以通過關閉一個channel 實現,因為在一個已關閉 channel 上執行接收操作數據總是能夠立即返回,返回值是對應類型的零值。
簡單講,對一個管道的關閉操作事實上是對所有接收者進行廣播信號。
func main() {
// 當關閉 done channel時
//給所有 goroutine發送信號
// 接收到后都會正常退出。
done := make(chan struct{})
defer close(done)
in := gen(done, 2, 4, 5)
c1 := sq(done, in)
out := sq(done, c1)
fmt.Println(<-out)
}
func sq(done <-chan struct{}, in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
select {
case out <- n * n:
case <-done:
return
}
}
}()
return out
}
其實講上面的模式就是為了,提醒我們使用協程和通道的時候小心資源泄漏,
發送完數據以后進行close關閉,以防死鎖。
其他一些模式感興趣可以點擊查看模式學習地址一和模式學習地址二
并發這塊我還很多不足,希望大家能多多討論.共同進步