Golang 語法到底是怎麼回事?gdb調一調?

“ 上一篇用gdb分析了golang的數據結構,這一期分析golang的語法。”

Golang語法到底是怎麼回事?

golang關鍵字編譯之後是什麼樣子,會展開成什麼樣。

range

range其實展開本質上和普通的for循環展開是一樣的。只不過邊界條件的判斷稍微有點不一樣。

for 初始化; 判斷條件; 遞進 {
}

只不過編譯器幫你來做了判斷條件和遞進(旁白:還是那句話,Golang那麼高級,是因爲編譯器幫你幹了好多事)。下面分別看幾個類型遇到 range 是怎麼回事,最主要的抓住邊界條件是啥即可。

array / slice

邊界條件:是否超過數組長度(len)

編譯器做了什麼?拿slice變量或者array變量來說。

  1. 取出連續內存元素長度(數組是靜態編譯就知道的,slice是從len變量裏取的)
  2. 每次循環判斷是否超出長度
  3. 遞進

解析下這段代碼就知道了

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

  1. select 展開成 selectgo . 有幾個需要注意的:

  2. select運行一次其實就是調用了一次 selectgo

  3. 調用selectgo之前需要計算參數,表達式會計算出值

  4. 每個case傳到selectgo函數裏的一定是io操作;出來之後可以進行賦值操作。但是注意了,chan的io操作一定是在selectgo內部進行的
    在這裏插入圖片描述
    爲什麼能得到以上的幾個結論:

  5. 因爲每次selectgo調用是需要傳參數的,傳參數是需要構造變量的,這個時候必須計算出來。這個變量類型就是scase類型。

  6. 看selectgo的邏輯和彙編代碼的生成,所有的channel io操作均在selectgo內部,涉及外部的賦值操作在外部

  7. 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 

函數

函數

  1. 函數的調用慣例
  2. 閉包到底做了什麼

函數的調用慣例

所有的參數和返回值都是通過棧來傳遞。這個和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是一個局部變量,每一輪循環都是新的變量值,是一個非常小的作用域。直接傳值的話,沒有問題,因爲只有這個閉包關注這個值。


堅持思考,方向比努力更重要。關注我:奇伢雲存儲

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章