golang底層 數據結構


 

字符串

type _string struct {
    elements *byte // 引用着底層的字節
    len      int   // 字符串中的字節數,獲取長度O(1)
}

 

對於字符串比較,編譯器有兩個優化:

若長度不相等,則字符串不相等,O(1)

若指針相等,長度大的字符串大,O(1)

 

 

slice

slice由指針、長度、容量三部分組成

type SliceHeader struct {
    Data     uintptr
    Len      int
    Cap      int
}

slice 和 array 做 len() 和 cap() 操作,會被直接展開為 sl->len 和 sl->cap 。

slice擴容規則是:

  • 如果新的大小是當前大小2倍以上,則大小增長為新大小
  • 否則循環以下操作:如果當前大小小於1024,按每次2倍增長,否則每次按當前大小1.25倍增長。直到增長的大小超過或等於新大小。

 

var x []int

go func(){ x=make([]int, 10) } ()

go func(){ x=make([]int, 10000) } ()

可能會出現 x的指針指向第一個make創建的底層數組,而x的長度為第二個make里的10000

 

map

map是用哈希表實現的,

for range遍歷是隨機的,應該是先隨機找到一個桶,遍歷該桶和溢出桶內的元素,然后按順序遍歷其他的所有桶

加載因子超過0.65或者使用了過多的溢出桶會擴容,當哈希函數選的不好,或者頻繁的添加然后刪除,就會導致加載因子小,卻使用了過多的溢出桶,這種情況下擴容(大概)不會增加哈希表的大小,而是新建相同大小的哈希表,並整理桶內的數據。加載因子超過0.65而引發的擴容會擴大到上次大小的2倍

采取增量擴容,每次添加或刪除時,將當前的桶擴展成兩個桶,並轉移桶內的元素

 

哈希值對2^B取模,低B位作為buckets數組的index,找到對應的桶。將哈希值的高8位和桶內的tophash數組里的值依次比較,若和tophash[i]相等,則比較第i個key與給定的key是否相等,若相等,返回第i個value。桶內的8個元素都不相同則去overflow里繼續尋找。

type hmap struct {
    count             int                // 當前元素數量
    flags             uint8
    B                 uint8            // bucket數組的長度為2^B
    noverflow     uint16
    hash0             uint32            // 哈希種子

    buckets        unsafe.Pointer             // bucket數組,數組長度為2^B
 // 老的buckets,長度為新buckets的一半,只有當正在擴容時才不為空
    oldbuckets     unsafe.Pointer      
    nevacuate      uintptr

    extra         *mapextra
}
type bmap struct {    // 桶,存儲8個鍵值對
 tophash          [bucketCnt]uint8    // hash值的高8位   bucketCnt默認為8
    topbits          [8]uint8
    keys             [8]keytype
    values           [8]valuetype
    pad              uintptr
    overflow         uintptr
}

 

 

 

channel

疑問:復制一個channel,是復制了hchan結構體嗎?如果是,那么對一個channel操作,另一個hchan里的qcount等屬性,是怎么被更新的?

hchan內部的修改,都要獲取lock嗎?

 

目前的 Channel 收發操作均遵循了先入先出(FIFO)的設計,具體規則如下:

  • 先從 Channel 讀取數據的 Goroutine 會先接收到數據;
  • 先向 Channel 發送數據的 Goroutine 會得到先發送數據的權利;
type hchan struct {
    qcount     uint         //隊列中目前的元素數量
    dataqsiz     uint         //環形隊列的總大小,make(chan int, 10) 里面的 10
    buf         unsafe.Pointer     // 指向大小為 dataqsiz 的數組
    elemsize     uint16    // 元素大小
    closed         uint32    // 是否已被關閉
    elemtype     *_type     // runtime._type,代表 channel 中的元素類型的 runtime 結構體
    sendx         uint         // send index
    recvx         uint         // receive index
    recvq         waitq     // 接收 goroutine 對應的 sudog 隊列
    sendq         waitq     // 發送 goroutine 對應的 sudog 隊列
    lock         mutex
}

 

 

recvq和sendq兩個鏈表,一個是因讀這個通道而導致阻塞的goroutine,另一個是因為寫這個通道而阻塞的goroutine。WaitQ是鏈表的定義,包含一個頭結點和一個尾結點:

type waitq struct { // 等待隊列 sudog 雙向隊列
    first *sudog
    last  *sudog
}

隊列中的每個成員是一個SudoG結構體變量。

struct    SudoG
{
    G*    g;                    // g and selgen constitute
    uint32    selgen;            // a weak pointer to g
    SudoG*    link;
    int64    releasetime;
    byte*    elem;            // data element
};

 

該結構中主要的就是一個g和一個elem。elem用於存儲goroutine的數據。讀通道時,數據會從Hchan的隊列中拷貝到SudoG的elem域。寫通道時,數據則是由SudoG的elem域拷貝到Hchan的隊列中。

 

channel

channel對應runtime.chansend函數。

 

recvq不為空時,

調用 runtime.sendDirect 函數將發送的數據直接拷貝到 x = <-c 表達式中變量 x 所在的內存地址上;

調用 runtime.goready 將等待接收數據的 Goroutine 標記成可運行狀態 Grunnable 並把該 Goroutine 放到發送方所在的P的 runnext 上等待執行,該P在下一次調度時就會立刻喚醒數據的接收方;

 

recvq為空時,

緩沖區不滿時不會阻塞寫者,而是將數據放到channel的緩沖區中,調用者返回。

阻塞的情況下,chansend做以下幾件事:

  1. 調用 runtime.getg 獲取發送數據使用的 Goroutine;
  2. 執行 runtime.acquireSudog 函數獲取 runtime.sudog 結構體並設置這一次阻塞發送的相關信息,例如發送的 Channel、是否在 Select 控制結構中和待發送數據的內存地址等;
  3. 將剛剛創建並初始化的 runtime.sudog 加入發送等待隊列,並設置到當前 Goroutine 的 waiting 上,表示 Goroutine 正在等待該 sudog 准備就緒;
  4. 調用 runtime.goparkunlock 函數將當前的 Goroutine 陷入沉睡等待喚醒;
  5. 被調度器喚醒后會執行一些收尾工作,將一些屬性置零並且釋放 runtime.sudog 結構體;

 

channel

channel對應runtime.chanrecv函數。

 

nil值的channel接收,會調用gopark讓出處理器的使用權

如果channel已經關閉,且緩沖區不存在數據,則清除ep指針中的數據並立即返回。ep指針應該指向接收方變量。

 

Channel 的 sendq 隊列不為空,調用 runtime.recv 函數:

  • 如果 Channel 不存在緩沖區;
  1. 調用 runtime.recvDirect 函數將 sendq 隊列中 Goroutine 存儲的 elem 數據拷貝到目標內存地址中;
  • 如果 Channel 存在緩沖區;
  1. 將緩沖區隊列頭的數據拷貝到接收方的內存地址;
  2. sendq隊列頭的數據拷貝到緩沖區中,釋放一個阻塞的發送方;

無論發生哪種情況,運行時都會調用 runtime.goready 函數將當前處理器的 runnext 設置成發送數據的 Goroutine,在調度器下一次調度時將阻塞的發送方喚醒。

 

sendq隊列為空,緩沖區不為空時,直接獲取緩沖區內的數據

 

sendq隊列為空,且緩沖區無數據或不存在緩沖區時,接收方會阻塞,並使用 runtime.sudog 結構體將當前 g 包裝成一個處於等待狀態的 g 並將其加入到接收隊列中。然后調用 runtime.goparkunlock 函數觸發 Goroutine 的調度,讓出處理器的使用權

 

 

 

close

close channel時,會鎖channel,然后將阻塞在channel上的g添加到一個gList上,然后釋放鎖,最后喚醒所有reader和writer。喚醒的reader會返回零值,喚醒的writer會panic?

 

 

 

接口

接口是一個結構體,包含兩個成員:類型,和指向數據的指針

type eface struct {            // 不包含方法的接口
    _type     *_type                 // 動態類型
    data      unsafe.Pointer     // 接口所指向的具體類型值的地址
}
type _type struct {
    size       uintptr             // 類型大小
    kind       uint8               // 所代表的具體類型
    hash      uint32            // 類型的哈希,可快速判斷類型是否相等
    ...
}

type iface struct {            // 包含方法的接口
    tab      *itab                  // 包含接口的靜態類型信息、數據的動態類型信息、函數表的結構
    data     unsafe.Pointer     // 接口所指向的具體類型值的地址
}
type itab struct {
    inter     *interfacetype         // 接口類型
    _type     *_type            // 動態類型
    hash      uint32                 // _type.hash 的 copy,用於類型的判斷
    _         [4]byte
    fun       [1]uintptr             // 可變大小,func[0]==0 意味着 _type 沒有實現相關接口函數
}

 

 

類型斷言會比較itab里的hash和目標類型_type里的hash,hash相同則是同一個類型

 

接口的方法調用

對象的方法調用,等價於普通函數調用,函數地址是在編譯時就可以確定的。而接口的方法調用,函數地址要在運行時才能確定。將具體值賦值給接口時,會將Type中的方法表復制到接口的方法表中,然后接口方法的函數地址才會確定下來。因此,接口的方法調用的代價比普通函數調用和對象的方法調用略高,多了幾條指令。

 

將具體類型轉換為空接口類型,過程比較簡單,就是返回一個Eface,將Eface中的data指針指向原型數據,type指針會指向數據的Type結構體。

將具體類型轉換為帶方法的接口時,會在編譯期比較具體類型的方法表和接口類型的方法表,這兩處方法表都是排序過的,只需要一遍順序掃描,就可以知道Type中否實現了接口中聲明的所有方法。最后會將Type方法表中的函數指針,拷貝到Itab的fun字段中。

 

 

這里提到了三個方法表,有點容易把人搞暈,所以要解釋一下。

Type的UncommonType中有一個Method方法表,某個具體類型實現的所有方法都會被收集到這張表中。reflect包中的Method和MethodByName方法都是通過查詢這張表實現的。

Iface的Itab的InterfaceType中也有一張方法表,這張方法表中是接口所聲明的方法。其中每一項是一個IMethod,里面只有聲明沒有實現。

Iface中的Itab的func域也是一張方法表,這張表中的每一項就是一個函數指針,也就是只有實現沒有聲明。

類型轉換時的檢測就是看Type中的方法表是否包含了InterfaceType的方法表中的所有方法,並把Type方法表中的實現部分拷到Itab的func那張表中。

 

reflect

reflect就是給定一個接口類型的數據,得到它的具體類型的類型信息,它的Value等。reflect包中的TypeOf和ValueOf函數分別做這個事情。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM