“ 上一篇用gdb分析了golang的數據結構,這一期分析golang的語法。”
Golang語法到底是怎麼回事?
golang關鍵字編譯之後是什麼樣子,會展開成什麼樣。
range
range其實展開本質上和普通的for循環展開是一樣的。只不過邊界條件的判斷稍微有點不一樣。
for 初始化; 判斷條件; 遞進 {
}
只不過編譯器幫你來做了判斷條件和遞進(旁白:還是那句話,Golang那麼高級,是因爲編譯器幫你幹了好多事)。下面分別看幾個類型遇到 range 是怎麼回事,最主要的抓住邊界條件是啥即可。
array / slice
邊界條件:是否超過數組長度(len)
編譯器做了什麼?拿slice變量或者array變量來說。
- 取出連續內存元素長度(數組是靜態編譯就知道的,slice是從len變量裏取的)
- 每次循環判斷是否超出長度
- 遞進
解析下這段代碼就知道了
3 func main() {
4 var s []int = []int{11,12,13}
5
6 for i, n:= range s {
7 println(i, n)
8 }
反彙編看下:
…
// 比較
0x000000000044ec4f <+159>: mov 0x20(%rsp),%rax。// 0x20($rsp) 存的就是 len字段
0x000000000044ec54 <+164>: cmp %rax,0x28(%rsp) // 遞進的數字index: 0x28($rsp) 和len 比較
// 跳轉分支
0x000000000044ec59 <+169>: jl 0x44ec5d <main.main+173>
0x000000000044ec5b <+171>: jmp 0x44ecc8 <main.main+280>
// 業務邏輯
// 遞進
0x000000000044ecbe <+270>: inc %rax
map
邊界條件:是否還有下一個值。mapiternext -> hiter != nil
map遇到range稍微有點不一樣,是通過runtime.mapiternext來獲取邊界值,並且判斷邊界值是通過這個調用是否爲0來判斷的。
// 初始化迭代器
0x0000000000450157 <+487>: callq 0x40bb70 <runtime.mapiterinit>
0x000000000045015c <+492>: jmp 0x45015e <main.main+494>
// 判斷是否有元素可以繼續迭代
0x000000000045015e <+494>: cmpq $0x0,0xb8(%rsp)
0x0000000000450167 <+503>: jne 0x45016e <main.main+510> // 非0,還有元素,可以繼續迭代
0x0000000000450169 <+505>: jmpq 0x450207 <main.main+663>. // 跳出循環
0x000000000045016e <+510>: mov 0xc0(%rsp),%rax
// 業務邏輯
// 獲取到下一個值
0x00000000004501fd <+653>: callq 0x40be30 <runtime.mapiternext>
channel
邊界調節:是否close
對於channel是調用runtime.chanrecv1展開的,邊界值是channel關閉,所以這裏如果沒有close,就會永遠阻塞。
// 迭代開始
// 賦值chanrecv2的參數
0x000000000044ec89 <+153>: mov 0x38(%rsp),%rax
0x000000000044ec8e <+158>: mov %rax,(%rsp)
0x000000000044ec92 <+162>: lea 0x28(%rsp),%rax
0x000000000044ec97 <+167>: mov %rax,0x8(%rsp)
=> 0x000000000044ec9c <+172>: callq 0x404d60 <runtime.chanrecv2> // channel未關閉就有可能是阻塞在這裏
// 判讀是否滿足邊界條件
0x000000000044eca6 <+182>: mov %al,0x1f(%rsp)
0x000000000044ecaa <+186>: test %al,%al // 判斷是否滿足邊界條件close
0x000000000044ecac <+188>: jne 0x44ecb0 <main.main+192>
0x000000000044ecae <+190>: jmp 0x44ece4 <main.main+244>
// 業務邏輯
// 直接調到153開始
0x000000000044ece2 <+242>: jmp 0x44ec89 <main.main+153>
// chanrecv receives on channel c and writes the received data to ep.
// ep may be nil, in which case received data is ignored.
// If block == false and no elements are available, returns (false, false).
// Otherwise, if c is closed, zeros *ep and returns (true, false).
// Otherwise, fills in *ep with an element and returns (true, true).
// A non-nil ep must point to the heap or the caller's stack.
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
select
-
select 展開成 selectgo . 有幾個需要注意的:
-
select運行一次其實就是調用了一次 selectgo
-
調用selectgo之前需要計算參數,表達式會計算出值
-
每個case傳到selectgo函數裏的一定是io操作;出來之後可以進行賦值操作。但是注意了,chan的io操作一定是在selectgo內部進行的
爲什麼能得到以上的幾個結論: -
因爲每次selectgo調用是需要傳參數的,傳參數是需要構造變量的,這個時候必須計算出來。這個變量類型就是scase類型。
-
看selectgo的邏輯和彙編代碼的生成,所有的channel io操作均在selectgo內部,涉及外部的賦值操作在外部
-
selectgo返回的是case的index,外部根據這個判斷執行哪個case的邏輯
package main
func main() {
c1 := make(chan int, 2)
c2 := make(chan int, 2)
c1<-1
c2<-2
select {
case <-c1:
println("1\n")
case <-c2:
println("2\n")
}
}
挑重點:
對應關係:
chan<-
runtime.chansend1
<-chan
runtime.chanrecv1
函數
函數
- 函數的調用慣例
- 閉包到底做了什麼
函數的調用慣例
所有的參數和返回值都是通過棧來傳遞。這個和c不同,c是前6個參數按照慣例用寄存器rdi,rsi,rdx,rcx,r8,r9. 參數溢出之後放在棧上,返回值存rax。go的傳參這樣設計,性能比c差點,但是複雜性大大降低。並且返回值還能統一起來,並且容易支持多參數。
閉包到底做了什麼
閉包就是 帶環境上下文的函數(funcval結構)。在編譯的流程,有一步是專門分析變量捕捉的(分析出哪些變量會被捕捉,會和函數指針構成一個數據結構),然後纔是函數編譯。這樣函數調用的時候,就能直接去上下文地址取變量的值了。
那麼這裏就要注意下,這裏就有引用和值的區別,如果是和函數捆綁的是引用,那麼取值的時候,就是通過反引用來取值的,修改的話也會導致這個原變量的值修改。如果是值,那麼就是完全clone出來的一個變量對象。和原來的不相關。那麼究竟是值,還是引用,這個要看我們業務代碼怎麼寫,編譯器纔會怎麼分析判斷。
舉個例子:
package main
func main () {
var i int = 0
for i = 0; i< 3; i++ {
go func () {
println(i) // 編譯器捕捉分析,按照引用取值
}()
}
}
彙編代碼
000000000044ec60 <main.main.func1>:
44ec60: 64 48 8b 0c 25 f8 ff mov %fs:0xfffffffffffffff8,%rcx
44ec67: ff ff
44ec69: 48 3b 61 10 cmp 0x10(%rcx),%rsp
44ec6d: 76 42 jbe 44ecb1 <main.main.func1+0x51>
44ec6f: 48 83 ec 18 sub $0x18,%rsp
44ec73: 48 89 6c 24 10 mov %rbp,0x10(%rsp)
44ec78: 48 8d 6c 24 10 lea 0x10(%rsp),%rbp
44ec7d: 48 8b 44 24 20 mov 0x20(%rsp),%rax // 變量地址
44ec82: 48 8b 00 mov (%rax),%rax // 反引用取值
44ec85: 48 89 44 24 08 mov %rax,0x8(%rsp)
44ec8a: e8 b1 3f fd ff callq 422c40 <runtime.printlock>
44ec8f: 48 8b 44 24 08 mov 0x8(%rsp),%rax
44ec94: 48 89 04 24 mov %rax,(%rsp)
44ec98: e8 13 47 fd ff callq 4233b0 <runtime.printint>
44ec9d: e8 1e 42 fd ff callq 422ec0 <runtime.printnl>
44eca2: e8 19 40 fd ff callq 422cc0 <runtime.printunlock>
44eca7: 48 8b 6c 24 10 mov 0x10(%rsp),%rbp
44ecac: 48 83 c4 18 add $0x18,%rsp
44ecb0: c3 retq
44ecb1: e8 7a 82 ff ff callq 446f30 <runtime.morestack_noctxt>
44ecb6: eb a8 jmp 44ec60 <main.main.func1>
另一個例子
package main
func main () {
var i int = 0
for i = 0; i< 3; i++ {
v := i
go func () {
println(v). // 編譯器捕捉分析,直接copy值,和func綁定。運行的時候,直接取值。
}()
}
}
彙編
000000000044ec40 <main.main.func1>:
44ec40: 64 48 8b 0c 25 f8 ff mov %fs:0xfffffffffffffff8,%rcx
44ec47: ff ff
44ec49: 48 3b 61 10 cmp 0x10(%rcx),%rsp
44ec4d: 76 35 jbe 44ec84 <main.main.func1+0x44>
44ec4f: 48 83 ec 10 sub $0x10,%rsp
44ec53: 48 89 6c 24 08 mov %rbp,0x8(%rsp)
44ec58: 48 8d 6c 24 08 lea 0x8(%rsp),%rbp
44ec5d: e8 de 3f fd ff callq 422c40 <runtime.printlock>
44ec62: 48 8b 44 24 18 mov 0x18(%rsp),%rax // 取值
44ec67: 48 89 04 24 mov %rax,(%rsp)
44ec6b: e8 40 47 fd ff callq 4233b0 <runtime.printint>
44ec70: e8 4b 42 fd ff callq 422ec0 <runtime.printnl>
44ec75: e8 46 40 fd ff callq 422cc0 <runtime.printunlock>
44ec7a: 48 8b 6c 24 08 mov 0x8(%rsp),%rbp
44ec7f: 48 83 c4 10 add $0x10,%rsp
44ec83: c3 retq
第一個例子:用的是外面的變量,編譯器假設你可能有讀取,修改這個變量的值,其他人也是看的到的,那麼自然是用引用的方式。
第二個例子:v是一個局部變量,每一輪循環都是新的變量值,是一個非常小的作用域。直接傳值的話,沒有問題,因爲只有這個閉包關注這個值。
堅持思考,方向比努力更重要。關注我:奇伢雲存儲