字符串
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做以下幾件事:
- 調用 runtime.getg 獲取發送數據使用的 Goroutine;
- 執行 runtime.acquireSudog 函數獲取 runtime.sudog 結構體並設置這一次阻塞發送的相關信息,例如發送的 Channel、是否在 Select 控制結構中和待發送數據的內存地址等;
- 將剛剛創建並初始化的 runtime.sudog 加入發送等待隊列,並設置到當前 Goroutine 的 waiting 上,表示 Goroutine 正在等待該 sudog 准備就緒;
- 調用 runtime.goparkunlock 函數將當前的 Goroutine 陷入沉睡等待喚醒;
- 被調度器喚醒后會執行一些收尾工作,將一些屬性置零並且釋放 runtime.sudog 結構體;
讀channel
讀channel對應runtime.chanrecv函數。
從nil值的channel接收,會調用gopark讓出處理器的使用權
如果channel已經關閉,且緩沖區不存在數據,則清除ep指針中的數據並立即返回。ep指針應該指向接收方變量。
當 Channel 的 sendq 隊列不為空,調用 runtime.recv 函數:
- 如果 Channel 不存在緩沖區;
- 調用 runtime.recvDirect 函數將 sendq 隊列中 Goroutine 存儲的 elem 數據拷貝到目標內存地址中;
- 如果 Channel 存在緩沖區;
- 將緩沖區隊列頭的數據拷貝到接收方的內存地址;
- 將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函數分別做這個事情。