go面經(jīng)
go與其他語言
面向?qū)ο?/a>
在了解 Go 語言是不是面向?qū)ο螅ê?jiǎn)稱:OOP) 之前,我們必須先知道 OOP 是啥,得先給他 “下定義”
根據(jù) Wikipedia 的定義,我們梳理出 OOP 的幾個(gè)基本認(rèn)知:
- 面向?qū)ο缶幊蹋∣OP)是一種基于 “對(duì)象” 概念的編程范式,它可以包含數(shù)據(jù)和代碼:數(shù)據(jù)以字段的形式存在(通常稱為屬性或?qū)傩裕a以程序的形式存在(通常稱為方法)。
- 對(duì)象自己的程序可以訪問并經(jīng)常修改自己的數(shù)據(jù)字段。
- 對(duì)象經(jīng)常被定義為類的一個(gè)實(shí)例。
- 對(duì)象利用屬性和方法的私有/受保護(hù)/公共可見性,對(duì)象的內(nèi)部狀態(tài)受到保護(hù),不受外界影響(被封裝)。
基于這幾個(gè)基本認(rèn)知進(jìn)行一步延伸出,面向?qū)ο蟮娜蠡咎匦裕?/p>
- 封裝
- 繼承
- 多態(tài)
Go語言和Java有什么區(qū)別?
1、Go上不允許函數(shù)重載,必須具有方法和函數(shù)的唯一名稱,而Java允許函數(shù)重載。
2、在速度方面,Go的速度要比Java快。
3、Java默認(rèn)允許多態(tài),而Go沒有。
4、Go語言使用HTTP協(xié)議進(jìn)行路由配置,而Java使用Akka.routing.ConsistentHashingRouter和Akka.routing.ScatterGatherFirstCompletedRouter進(jìn)行路由配置。
5、Go代碼可以自動(dòng)擴(kuò)展到多個(gè)核心,而Java并不總是具有足夠的可擴(kuò)展性。
6、Go語言的繼承通過匿名組合完成,基類以Struct的方式定義,子類只需要把基類作為成員放在子類的定義中,支持多繼承;而Java的繼承通過extends關(guān)鍵字完成,不支持多繼承。
Go 是面向?qū)ο蟮恼Z言嗎?
是的,也不是。原因是:
- Go 有類型和方法,并且允許面向?qū)ο蟮木幊田L(fēng)格,但沒有類型層次。
- Go 中的 "接口 "概念提供了一種不同的方法,我們認(rèn)為這種方法易于使用,而且在某些方面更加通用。還有一些方法可以將類型嵌入到其他類型中,以提供類似的東西,但不等同于子類。
- Go 中的方法比 C++ 或 Java 中的方法更通用:它們可以為任何類型的數(shù)據(jù)定義,甚至是內(nèi)置類型,如普通的、"未裝箱的 "整數(shù)。它們并不局限于結(jié)構(gòu)(類)。
- Go 由于缺乏類型層次,Go 中的 "對(duì)象 "比 C++ 或 Java 等語言更輕巧。
封裝
面向?qū)ο笾械?“封裝” 指的是可以隱藏對(duì)象的內(nèi)部屬性和實(shí)現(xiàn)細(xì)節(jié),僅對(duì)外提供公開接口調(diào)用,這樣子用戶就不需要關(guān)注你內(nèi)部是怎么實(shí)現(xiàn)的。
在 Go 語言中的屬性訪問權(quán)限,通過首字母大小寫來控制:
- 首字母大寫,代表是公共的、可被外部訪問的。
- 首字母小寫,代表是私有的,不可以被外部訪問。
Go 語言的例子如下:
type Animal struct {
name string
}
func NewAnimal() *Animal {
return &Animal{}
}
func (p *Animal) SetName(name string) {
p.name = name
}
func (p *Animal) GetName() string {
return p.name
}
在上述例子中,我們聲明了一個(gè)結(jié)構(gòu)體 Animal,其屬性 name 為小寫。沒法通過外部方法,在配套上存在 Setter 和 Getter 的方法,用于統(tǒng)一的訪問和設(shè)置控制。
以此實(shí)現(xiàn)在 Go 語言中的基本封裝。
繼承
面向?qū)ο笾械?“繼承” 指的是子類繼承父類的特征和行為,使得子類對(duì)象(實(shí)例)具有父類的實(shí)例域和方法,或子類從父類繼承方法,使得子類具有父類相同的行為。

從實(shí)際的例子來看,就是動(dòng)物是一個(gè)大父類,下面又能細(xì)分為 “食草動(dòng)物”、“食肉動(dòng)物”,這兩者會(huì)包含 “動(dòng)物” 這個(gè)父類的基本定義。
從實(shí)際的例子來看,就是動(dòng)物是一個(gè)大父類,下面又能細(xì)分為 “食草動(dòng)物”、“食肉動(dòng)物”,這兩者會(huì)包含 “動(dòng)物” 這個(gè)父類的基本定義。
在 Go 語言中,是沒有類似 extends 關(guān)鍵字的這種繼承的方式,在語言設(shè)計(jì)上采取的是組合的方式:
type Animal struct {
Name string
}
type Cat struct {
Animal
FeatureA string
}
type Dog struct {
Animal
FeatureB string
}
在上述例子中,我們聲明了 Cat 和 Dog 結(jié)構(gòu)體,其在內(nèi)部匿名組合了 Animal 結(jié)構(gòu)體。因此 Cat 和 Dog 的實(shí)例都可以調(diào)用 Animal 結(jié)構(gòu)體的方法:
func main() {
p := NewAnimal()
p.SetName("我是搬運(yùn)工,去給煎魚點(diǎn)贊~")
dog := Dog{Animal: *p}
fmt.Println(dog.GetName())
}
同時(shí) Cat 和 Dog 的實(shí)例可以擁有自己的方法:
func (dog *Dog) HelloWorld() {
fmt.Println("腦子進(jìn)煎魚了")
}
func (cat *Cat) HelloWorld() {
fmt.Println("煎魚進(jìn)腦子了")
}
上述例子能夠正常包含調(diào)用 Animal 的相關(guān)屬性和方法,也能夠擁有自己的獨(dú)立屬性和方法,在 Go 語言中達(dá)到了類似繼承的效果。
多態(tài)
多態(tài)
面向?qū)ο笾械?“多態(tài)” 指的同一個(gè)行為具有多種不同表現(xiàn)形式或形態(tài)的能力,具體是指一個(gè)類實(shí)例(對(duì)象)的相同方法在不同情形有不同表現(xiàn)形式。
多態(tài)也使得不同內(nèi)部結(jié)構(gòu)的對(duì)象可以共享相同的外部接口,也就是都是一套外部模板,內(nèi)部實(shí)際是什么,只要符合規(guī)格就可以。
在 Go 語言中,多態(tài)是通過接口來實(shí)現(xiàn)的:
type AnimalSounder interface {
MakeDNA()
}
func MakeSomeDNA(animalSounder AnimalSounder) { // 參數(shù)是AnimalSounder接口類型
animalSounder.MakeDNA()
}
在上述例子中,我們聲明了一個(gè)接口類型 AnimalSounder,配套一個(gè) MakeSomeDNA 方法,其接受 AnimalSounder 接口類型作為入?yún)ⅰ?br> 因此在 Go 語言中。只要配套的 Cat 和 Dog 的實(shí)例也實(shí)現(xiàn)了 MakeSomeDNA 方法,那么我們就可以認(rèn)為他是 AnimalSounder 接口類型:
type AnimalSounder interface {
MakeDNA()
}
func MakeSomeDNA(animalSounder AnimalSounder) {
animalSounder.MakeDNA()
}
func (c *Cat) MakeDNA() {
fmt.Println("煎魚是煎魚")
}
func (c *Dog) MakeDNA() {
fmt.Println("煎魚其實(shí)不是煎魚")
}
func main() {
MakeSomeDNA(&Cat{})
MakeSomeDNA(&Dog{})
}
當(dāng) Cat 和 Dog 的實(shí)例實(shí)現(xiàn)了 AnimalSounder 接口類型的約束后,就意味著滿足了條件,他們?cè)?Go 語言中就是一個(gè)東西。能夠作為入?yún)魅?MakeSomeDNA 方法中,再根據(jù)不同的實(shí)例實(shí)現(xiàn)多態(tài)行為。
在日常工作中,基本了解這些概念就可以了。若是面試,可以針對(duì)三大特性:“封裝、繼承、多態(tài)” 和 五大原則 “單一職責(zé)原則(SRP)、開放封閉原則(OCP)、里氏替換原則(LSP)、依賴倒置原則(DIP)、接口隔離原則(ISP)” 進(jìn)行深入理解和說明。
go語言和python的區(qū)別:
1、范例
Python是一種基于面向?qū)ο缶幊痰亩喾妒剑钍胶秃瘮?shù)式編程語言。它堅(jiān)持這樣一種觀點(diǎn),即如果一種語言在某些情境中表現(xiàn)出某種特定的方式,理想情況下它應(yīng)該在所有情境中都有相似的作用。但是,它又不是純粹的OOP語言,它不支持強(qiáng)封裝,這是OOP的主要原則之一。
Go是一種基于并發(fā)編程范式的過程編程語言,它與C具有表面相似性。實(shí)際上,Go更像是C的更新版本。
2、類型化
Python是動(dòng)態(tài)類型語言,而Go是一種靜態(tài)類型語言,它實(shí)際上有助于在編譯時(shí)捕獲錯(cuò)誤,這可以進(jìn)一步減少生產(chǎn)后期的嚴(yán)重錯(cuò)誤。
3、并發(fā)
Python沒有提供內(nèi)置的并發(fā)機(jī)制,而Go有內(nèi)置的并發(fā)機(jī)制。
4、安全性
Python是一種強(qiáng)類型語言,它是經(jīng)過編譯的,因此增加了一層安全性。Go具有分配給每個(gè)變量的類型,因此,它提供了安全性。但是,如果發(fā)生任何錯(cuò)誤,用戶需要自己運(yùn)行整個(gè)代碼。
5、管理內(nèi)存
Go允許程序員在很大程度上管理內(nèi)存。而,Python中的內(nèi)存管理完全自動(dòng)化并由Python VM管理;它不允許程序員對(duì)內(nèi)存管理負(fù)責(zé)。
6、庫
與Go相比,Python提供的庫數(shù)量要大得多。然而,Go仍然是新的,并且還沒有取得很大進(jìn)展。
7、語法
Python的語法使用縮進(jìn)來指示代碼塊。Go的語法基于打開和關(guān)閉括號(hào)。
8、詳細(xì)程度
為了獲得相同的功能,Golang代碼通常需要編寫比Python代碼更多的字符。
go 與 node.js
深入對(duì)比Node.js和Golang 到底誰才是NO.1 : https://zhuanlan.zhihu.com/p/421352168
從 Node 到 Go:一個(gè)粗略的比較 : https://zhuanlan.zhihu.com/p/29847628
基礎(chǔ)部分
為什么選擇golang
0、高性能-協(xié)程
golang 源碼級(jí)別支持協(xié)程,實(shí)現(xiàn)簡(jiǎn)單;對(duì)比進(jìn)程和線程,協(xié)程占用資源少,能夠簡(jiǎn)潔高效地處理高并發(fā)問題。
1、學(xué)習(xí)曲線容易-代碼極簡(jiǎn)
Go語言語法簡(jiǎn)單,包含了類C語法。因?yàn)镚o語言容易學(xué)習(xí),所以一個(gè)普通的大學(xué)生花幾個(gè)星期就能寫出來可以上手的、高性能的應(yīng)用。在國內(nèi)大家都追求快,這也是為什么國內(nèi)Go流行的原因之一。
Go 語言的語法特性簡(jiǎn)直是太簡(jiǎn)單了,簡(jiǎn)單到你幾乎玩不出什么花招,直來直去的,學(xué)習(xí)曲線很低,上手非常快。
2、效率:快速的編譯時(shí)間,開發(fā)效率和運(yùn)行效率高
開發(fā)過程中相較于 Java 和 C++呆滯的編譯速度,Go 的快速編譯時(shí)間是一個(gè)主要的效率優(yōu)勢(shì)。Go擁有接近C的運(yùn)行效率和接近PHP的開發(fā)效率。
C 語言的理念是信任程序員,保持語言的小巧,不屏蔽底層且底層友好,關(guān)注語言的執(zhí)行效率和性能。而 Python 的姿態(tài)是用盡量少的代碼完成盡量多的事。于是我能夠感覺到,Go 語言想要把 C 和 Python 統(tǒng)一起來,這是多棒的一件事啊。
3、出身名門、血統(tǒng)純正
之所以說Go出身名門,從Go語言的創(chuàng)造者就可見端倪,Go語言絕對(duì)血統(tǒng)純正。其次Go語言出自Google公司,Google在業(yè)界的知名度和實(shí)力自然不用多說。Google公司聚集了一批牛人,在各種編程語言稱雄爭(zhēng)霸的局面下推出新的編程語言,自然有它的戰(zhàn)略考慮。而且從Go語言的發(fā)展態(tài)勢(shì)來看,Google對(duì)它這個(gè)新的寵兒還是很看重的,Go自然有一個(gè)良好的發(fā)展前途。
4、自由高效:組合的思想、無侵入式的接口
Go語言可以說是開發(fā)效率和運(yùn)行效率二者的完美融合,天生的并發(fā)編程支持。Go語言支持當(dāng)前所有的編程范式,包括過程式編程、面向?qū)ο缶幊獭⒚嫦蚪涌诰幊獭⒑瘮?shù)式編程。程序員們可以各取所需、自由組合、想怎么玩就怎么玩。
5、強(qiáng)大的標(biāo)準(zhǔn)庫-生態(tài)
背靠谷歌,生態(tài)豐富,輕松 go get 獲取各種高質(zhì)量輪子。用戶可以專注于業(yè)務(wù)邏輯,避免重復(fù)造輪子。
這包括互聯(lián)網(wǎng)應(yīng)用、系統(tǒng)編程和網(wǎng)絡(luò)編程。Go里面的標(biāo)準(zhǔn)庫基本上已經(jīng)是非常穩(wěn)定了,特別是我這里提到的三個(gè),網(wǎng)絡(luò)層、系統(tǒng)層的庫非常實(shí)用。Go 語言的 lib 庫麻雀雖小五臟俱全。Go 語言的 lib 庫中基本上有絕大多數(shù)常用的庫,雖然有些庫還不是很好,但我覺得不是問題,因?yàn)槲蚁嘈旁谖磥淼陌l(fā)展中會(huì)把這些問題解決掉。
6、部署方便:二進(jìn)制文件,Copy部署
部署簡(jiǎn)單,源碼編譯成執(zhí)行文件后,可以直接運(yùn)行,減少了對(duì)其它插件依賴。不像其它語言,執(zhí)行文件依賴各種插件,各種庫,研發(fā)機(jī)器運(yùn)行正常,部署到生產(chǎn)環(huán)境,死活跑不起來 。
7、簡(jiǎn)單的并發(fā)
并行和異步編程幾乎無痛點(diǎn)。Go 語言的 Goroutine 和 Channel 這兩個(gè)神器簡(jiǎn)直就是并發(fā)和異步編程的巨大福音。像 C、C++、Java、Python 和 JavaScript 這些語言的并發(fā)和異步方式太控制就比較復(fù)雜了,而且容易出錯(cuò),而 Go 解決這個(gè)問題非常地優(yōu)雅和流暢。這對(duì)于編程多年受盡并發(fā)和異步折磨的編程者來說,完全就是讓人眼前一亮的感覺。Go 是一種非常高效的語言,高度支持并發(fā)性。Go是為大數(shù)據(jù)、微服務(wù)、并發(fā)而生的一種編程語言。
Go 作為一門語言致力于使事情簡(jiǎn)單化。它并未引入很多新概念,而是聚焦于打造一門簡(jiǎn)單的語言,它使用起來異常快速并且簡(jiǎn)單。其唯一的創(chuàng)新之處是 goroutines 和通道。Goroutines 是 Go 面向線程的輕量級(jí)方法,而通道是 goroutines 之間通信的優(yōu)先方式。
創(chuàng)建 Goroutines 的成本很低,只需幾千個(gè)字節(jié)的額外內(nèi)存,正由于此,才使得同時(shí)運(yùn)行數(shù)百個(gè)甚至數(shù)千個(gè) goroutines 成為可能。可以借助通道實(shí)現(xiàn) goroutines 之間的通信。Goroutines 以及基于通道的并發(fā)性方法使其非常容易使用所有可用的 CPU 內(nèi)核,并處理并發(fā)的 IO。相較于 Python/Java,在一個(gè) goroutine 上運(yùn)行一個(gè)函數(shù)需要最小的代碼。
8、穩(wěn)定性
Go擁有強(qiáng)大的編譯檢查、嚴(yán)格的編碼規(guī)范和完整的軟件生命周期工具,具有很強(qiáng)的穩(wěn)定性,穩(wěn)定壓倒一切。那么為什么Go相比于其他程序會(huì)更穩(wěn)定呢?這是因?yàn)镚o提供了軟件生命周期(開發(fā)、測(cè)試、部署、維護(hù)等等)的各個(gè)環(huán)節(jié)的工具,如go tool、gofmt、go test。
9、跨平臺(tái)
很多語言都支持跨平臺(tái),把這個(gè)優(yōu)點(diǎn)單獨(dú)拿出來,貌似沒有什么值得稱道的,但是結(jié)合上述優(yōu)點(diǎn),它的綜合能力就非常強(qiáng)了。
golang 缺點(diǎn)
①右大括號(hào)不允許換行,否則編譯報(bào)錯(cuò)
②不允許有未使用的包或變量
③錯(cuò)誤處理原始,雖然引入了defer、panic、recover處理出錯(cuò)后的邏輯,函數(shù)可以返回多個(gè)值,但基本依靠返回錯(cuò)誤是否為空來判斷函數(shù)是否執(zhí)行成功,if err != nil語句較多,比較繁瑣,程序沒有java美觀。(官方解釋:提供了多個(gè)返回值,處理錯(cuò)誤方便,如加入異常機(jī)制會(huì)要求記住一些常見異常,例如IOException,go的錯(cuò)誤Error類型較統(tǒng)一方便)
④[]interface{}不支持下標(biāo)操作
⑤struct沒有構(gòu)造和析構(gòu),一些資源申請(qǐng)和釋放動(dòng)作不太方便
⑥仍然保留C/C++的指針操作,取地址&,取值*
golang 中 make 和 new 的區(qū)別?(基本必問)
共同點(diǎn):給變量分配內(nèi)存
不同點(diǎn):
1)作用變量類型不同,new給string,int和數(shù)組分配內(nèi)存,make給切片,map,channel分配內(nèi)存;
2)返回類型不一樣,new返回指向變量的指針,make返回變量本身;
3)new 分配的空間被清零。make 分配空間后,會(huì)進(jìn)行初始化;
4) 字節(jié)的面試官還說了另外一個(gè)區(qū)別,就是分配的位置,在堆上還是在棧上?這塊我比較模糊,大家可以自己探究下,我搜索出來的答案是golang會(huì)弱化分配的位置的概念,因?yàn)榫幾g的時(shí)候會(huì)自動(dòng)內(nèi)存逃逸處理,懂的大佬幫忙補(bǔ)充下:make、new內(nèi)存分配是在堆上還是在棧上?
區(qū)別匯總2
- 返回值:new 返回指針,make 返回值本身。
- 類型限制:new 用于所有類型;make 僅用于 slice/map/chan。
- 初始化:new 為零值;make 在零值后再做結(jié)構(gòu)化初始化(可用)。
- 分配位置:由逃逸分析決定,不在棧即在堆。
- 何時(shí)使用:new 用于指針;make 用于需要直接使用的 slice/map/chan。
IO多路復(fù)用
for range 的時(shí)候它的地址會(huì)發(fā)生變化么?
答:在 for a,b := range c 遍歷中, a 和 b 在內(nèi)存中只會(huì)存在一份,即之后每次循環(huán)時(shí)遍歷到的數(shù)據(jù)都是以值覆蓋的方式賦給 a 和 b,a,b 的內(nèi)存地址始終不變。由于有這個(gè)特性,for 循環(huán)里面如果開協(xié)程,不要直接把 a 或者 b 的地址傳給協(xié)程。解決辦法:在每次循環(huán)時(shí),創(chuàng)建一個(gè)臨時(shí)變量。
go defer,多個(gè) defer 的順序,defer 在什么時(shí)機(jī)會(huì)修改返回值?
Golang中的Defer必掌握的7知識(shí)點(diǎn)-地鼠文檔
作用:defer延遲函數(shù),釋放資源,收尾工作;如釋放鎖,關(guān)閉文件,關(guān)閉鏈接;捕獲panic;
避坑指南:defer函數(shù)緊跟在資源打開后面,否則defer可能得不到執(zhí)行,導(dǎo)致內(nèi)存泄露。
多個(gè) defer 調(diào)用順序是 LIFO(后入先出),defer后的操作可以理解為壓入棧中
defer,return,return value(函數(shù)返回值) 執(zhí)行順序:首先return,其次return value,最后defer。defer可以修改函數(shù)最終返回值,修改時(shí)機(jī):有名返回值或者函數(shù)返回指針 參考:
【Golang】Go語言defer用法大總結(jié)(含return返回機(jī)制)__奶酪的博客-CSDN博客blog.csdn.net/Cassie_zkq/article/details/108567205
有名返回值
func b() (i int) {
defer func() {
i++
fmt.Println("defer2:", i)
}()
defer func() {
i++
fmt.Println("defer1:", i)
}()
return i
//或者直接寫成
return
}
func main() {
fmt.Println("return:", b())
}
函數(shù)返回指針
func c() *int {
var i int
defer func() {
i++
fmt.Println("defer2:", i)
}()
defer func() {
i++
fmt.Println("defer1:", i)
}()
return &i
}
func main() {
fmt.Println("return:", *(c()))
}
uint 類型溢出問題
超過最大存儲(chǔ)值如uint8最大是255
var a uint8 =255
var b uint8 =1
a+b = 0總之類型溢出會(huì)出現(xiàn)難以意料的事

能介紹下 rune 類型嗎?
相當(dāng)int32
golang中的字符串底層實(shí)現(xiàn)是通過byte數(shù)組的,中文字符在unicode下占2個(gè)字節(jié),在utf-8編碼下占3個(gè)字節(jié),而golang默認(rèn)編碼正好是utf-8
byte 等同于int8,常用來處理ascii字符
rune 等同于int32,常用來處理unicode或utf-8字符

golang 中解析 tag 是怎么實(shí)現(xiàn)的?反射原理是什么?(中高級(jí)肯定會(huì)問,比較難,需要自己多去總結(jié))
參考如下連接
golang中struct關(guān)于反射tag_paladinosment的博客-CSDN博客_golang 反射tagblog.csdn.net/paladinosment/article/details/42570937
type User struct { name string json:name-field age int } func main() { user := &User{"John Doe The Fourth", 20} field, ok := reflect.TypeOf(user).Elem().FieldByName("name") if !ok { panic("Field not found") } fmt.Println(getStructTag(field)) } func getStructTag(f reflect.StructField) string { return string(f.Tag) }
Go 中解析的 tag 是通過反射實(shí)現(xiàn)的,反射是指計(jì)算機(jī)程序在運(yùn)行時(shí)(Run time)可以訪問、檢測(cè)和修改它本身狀態(tài)或行為的一種能力或動(dòng)態(tài)知道給定數(shù)據(jù)對(duì)象的類型和結(jié)構(gòu),并有機(jī)會(huì)修改它。反射將接口變量轉(zhuǎn)換成反射對(duì)象 Type 和 Value;反射可以通過反射對(duì)象 Value 還原成原先的接口變量;反射可以用來修改一個(gè)變量的值,前提是這個(gè)值可以被修改;tag是啥:結(jié)構(gòu)體支持標(biāo)記,name string json:name-field 就是 json:name-field 這部分
gorm json yaml gRPC protobuf gin.Bind()都是通過反射來實(shí)現(xiàn)的
調(diào)用函數(shù)傳入結(jié)構(gòu)體時(shí),應(yīng)該傳值還是指針? (Golang 都是傳值)
Go 的函數(shù)參數(shù)傳遞都是值傳遞。所謂值傳遞:指在調(diào)用函數(shù)時(shí)將實(shí)際參數(shù)復(fù)制一份傳遞到函數(shù)中,這樣在函數(shù)中如果對(duì)參數(shù)進(jìn)行修改,將不會(huì)影響到實(shí)際參數(shù)。參數(shù)傳遞還有引用傳遞,所謂引用傳遞是指在調(diào)用函數(shù)時(shí)將實(shí)際參數(shù)的地址傳遞到函數(shù)中,那么在函數(shù)中對(duì)參數(shù)所進(jìn)行的修改,將影響到實(shí)際參數(shù)
因?yàn)?Go 里面的 map,slice,chan 是引用類型。變量區(qū)分值類型和引用類型。所謂值類型:變量和變量的值存在同一個(gè)位置。所謂引用類型:變量和變量的值是不同的位置,變量的值存儲(chǔ)的是對(duì)值的引用。但并不是 map,slice,chan 的所有的變量在函數(shù)內(nèi)都能被修改,不同數(shù)據(jù)類型的底層存儲(chǔ)結(jié)構(gòu)和實(shí)現(xiàn)可能不太一樣,情況也就不一樣。
goroutine什么情況下會(huì)阻塞
在 Go 里面阻塞主要分為以下 4 種場(chǎng)景:
- 由于原子、互斥量或通道操作調(diào)用導(dǎo)致 Goroutine 阻塞,調(diào)度器將把當(dāng)前阻塞的 Goroutine 切換出去,重新調(diào)度 LRQ 上的其他 Goroutine;
- 由于網(wǎng)絡(luò)請(qǐng)求和 IO 操作導(dǎo)致 Goroutine 阻塞。Go 程序提供了網(wǎng)絡(luò)輪詢器(NetPoller)來處理網(wǎng)絡(luò)請(qǐng)求和 IO 操作的問題,其后臺(tái)通過 kqueue(MacOS),epoll(Linux)或 iocp(Windows)來實(shí)現(xiàn) IO 多路復(fù)用。通過使用 NetPoller 進(jìn)行網(wǎng)絡(luò)系統(tǒng)調(diào)用,調(diào)度器可以防止 Goroutine 在進(jìn)行這些系統(tǒng)調(diào)用時(shí)阻塞 M。這可以讓 M 執(zhí)行 P 的 LRQ 中其他的 Goroutines,而不需要?jiǎng)?chuàng)建新的 M。執(zhí)行網(wǎng)絡(luò)系統(tǒng)調(diào)用不需要額外的 M,網(wǎng)絡(luò)輪詢器使用系統(tǒng)線程,它時(shí)刻處理一個(gè)有效的事件循環(huán),有助于減少操作系統(tǒng)上的調(diào)度負(fù)載。用戶層眼中看到的 Goroutine 中的“block socket”,實(shí)現(xiàn)了 goroutine-per-connection 簡(jiǎn)單的網(wǎng)絡(luò)編程模式。實(shí)際上是通過 Go runtime 中的 netpoller 通過 Non-block socket + I/O 多路復(fù)用機(jī)制“模擬”出來的。
- 當(dāng)調(diào)用一些系統(tǒng)方法的時(shí)候(如文件 I/O),如果系統(tǒng)方法調(diào)用的時(shí)候發(fā)生阻塞,這種情況下,網(wǎng)絡(luò)輪詢器(NetPoller)無法使用,而進(jìn)行系統(tǒng)調(diào)用的 G1 將阻塞當(dāng)前 M1。調(diào)度器引入 其它M 來服務(wù) M1 的P。
- 如果在 Goroutine 去執(zhí)行一個(gè) sleep 操作,導(dǎo)致 M 被阻塞了。Go 程序后臺(tái)有一個(gè)監(jiān)控線程 sysmon,它監(jiān)控那些長時(shí)間運(yùn)行的 G 任務(wù)然后設(shè)置可以強(qiáng)占的標(biāo)識(shí)符,別的 Goroutine 就可以搶先進(jìn)來執(zhí)行。
goroutine 的阻塞場(chǎng)景
1) Channel 操作
ch := make(chan int)
go func() {
data := <-ch // 等待 channel 有數(shù)據(jù) → 阻塞
}()
ch <- 10 // 如果沒有接收者等待 → 阻塞
2) 互斥鎖/讀寫鎖
var mu sync.Mutex
go func() {
mu.Lock() // 獲取不到鎖 → 阻塞
// ...
mu.Unlock()
}()
3) 同步原語
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
wg.Wait() // 等待計(jì)數(shù)器歸零 → 阻塞
}()
cond := sync.NewCond(&mu)
cond.Wait() // 等待信號(hào) → 阻塞
4) 定時(shí)器
time.Sleep(5 * time.Second) // 阻塞指定時(shí)間
timer := time.After(5 * time.Second)
<-timer // 等待定時(shí)器觸發(fā) → 阻塞
ticker := time.NewTicker(1 * time.Second)
<-ticker.C // 等待下次觸發(fā) → 阻塞
5) IO(網(wǎng)絡(luò)/文件)
conn, _ := net.Dial("tcp", "example.com:80")
conn.Read(buffer) // 等待數(shù)據(jù)到達(dá) → 阻塞
resp, _ := http.Get("https://example.com") // 等待響應(yīng) → 阻塞
file, _ := os.Open("data.txt")
file.Read(buffer) // 等待文件IO → 阻塞
fmt.Scanln(&input) // 等待用戶輸入 → 阻塞
6) 系統(tǒng)調(diào)用
// 執(zhí)行外部命令時(shí)阻塞
cmd := exec.Command("sleep", "10")
cmd.Run()
// 內(nèi)存分配過大,GC 會(huì)阻塞
data := make([]byte, 10*1024*1024*1024)
7) Context 等待
ctx := context.WithTimeout(context.Background(), 5*time.Second)
<-ctx.Done() // 等待超時(shí)或取消 → 阻塞
select {
case <-ctx.Done():
return ctx.Err()
case <-someChannel:
// 處理數(shù)據(jù)
}
8) Select 語句(全部 channel 不可用)
select {
case data := <-ch1:
// 處理 ch1 數(shù)據(jù)
case data := <-ch2:
// 處理 ch2 數(shù)據(jù)
// 如果兩個(gè) channel 都阻塞,select 本身阻塞
}
阻塞會(huì)導(dǎo)致的問題
- 無法接收新的請(qǐng)求
- 可能發(fā)生死鎖
ch := make(chan int)
<-ch // 主 goroutine 阻塞,等待數(shù)據(jù)
ch <- 10 // 永遠(yuǎn)執(zhí)行不到 → 死鎖
如何避免長時(shí)間阻塞
// 方法 1: 使用帶緩沖的 channel
ch := make(chan int, 10) // 緩沖 10 個(gè)元素
ch <- 1 // 不會(huì)阻塞(除非緩沖區(qū)滿)
// 方法 2: 使用 select 帶超時(shí)
select {
case data := <-ch:
fmt.Println(data)
case <-time.After(5 * time.Second):
fmt.Println("超時(shí)")
}
// 方法 3: 使用 context 控制超時(shí)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
select {
case data := <-ch:
fmt.Println(data)
case <-ctx.Done():
fmt.Println("被取消或超時(shí)")
}
總結(jié)
| 操作類型 | 何時(shí)阻塞 | 如何避免 |
|---|---|---|
| Channel 操作 | 無緩沖 channel 或緩沖區(qū)滿/空 | 使用緩沖 channel、select |
| 鎖操作 | 鎖被占用 | 使用帶超時(shí)的上下文 |
| IO 操作 | 等待數(shù)據(jù) | 使用異步 IO、context |
| Sleep/Timer | 等待時(shí)間到達(dá) | 使用 context 控制 |
| WaitGroup | 計(jì)數(shù)器未歸零 | 確保所有 Done 被調(diào)用 |
阻塞是并發(fā)編程的常見行為;應(yīng)明確哪些是可接受的,哪些需要超時(shí)或取消控制。
講講 Go 的 select 底層數(shù)據(jù)結(jié)構(gòu)和一些特性?(難點(diǎn),沒有項(xiàng)目經(jīng)常可能說不清,面試一般會(huì)問你項(xiàng)目中怎么使用select)
答:go 的 select 為 golang 提供了多路 IO 復(fù)用機(jī)制,和其他 IO 復(fù)用一樣,用于檢測(cè)是否有讀寫事件是否 ready。linux 的系統(tǒng) IO 模型有 select,poll,epoll,go 的 select 和 linux 系統(tǒng) select 非常相似。
select 結(jié)構(gòu)組成主要是由 case 語句和執(zhí)行的函數(shù)組成 select 實(shí)現(xiàn)的多路復(fù)用是:每個(gè)線程或者進(jìn)程都先到注冊(cè)和接受的 channel(裝置)注冊(cè),然后阻塞,然后只有一個(gè)線程在運(yùn)輸,當(dāng)注冊(cè)的線程和進(jìn)程準(zhǔn)備好數(shù)據(jù)后,裝置會(huì)根據(jù)注冊(cè)的信息得到相應(yīng)的數(shù)據(jù)。
select 的特性
1)select 操作至少要有一個(gè) case 語句,出現(xiàn)讀寫 nil 的 channel 該分支會(huì)忽略,在 nil 的 channel 上操作則會(huì)報(bào)錯(cuò)。
2)select 僅支持管道,而且是單協(xié)程操作。
3)每個(gè) case 語句僅能處理一個(gè)管道,要么讀要么寫。
4)多個(gè) case 語句的執(zhí)行順序是隨機(jī)的。
5)存在 default 語句,select 將不會(huì)阻塞,但是存在 default 會(huì)影響性能。
講講 Go 的 defer 底層數(shù)據(jù)結(jié)構(gòu)和一些特性?
答:每個(gè) defer 語句都對(duì)應(yīng)一個(gè)_defer 實(shí)例,多個(gè)實(shí)例使用指針連接起來形成一個(gè)單連表,保存在 gotoutine 數(shù)據(jù)結(jié)構(gòu)中,每次插入_defer 實(shí)例,均插入到鏈表的頭部,函數(shù)結(jié)束再一次從頭部取出,從而形成后進(jìn)先出的效果。
defer 的規(guī)則總結(jié):
延遲函數(shù)的參數(shù)是 defer 語句出現(xiàn)的時(shí)候就已經(jīng)確定了的。
延遲函數(shù)執(zhí)行按照后進(jìn)先出的順序執(zhí)行,即先出現(xiàn)的 defer 最后執(zhí)行。
延遲函數(shù)可能操作主函數(shù)的返回值。
申請(qǐng)資源后立即使用 defer 關(guān)閉資源是個(gè)好習(xí)慣。
單引號(hào),雙引號(hào),反引號(hào)的區(qū)別?
單引號(hào),表示byte類型或rune類型,對(duì)應(yīng) uint8和int32類型,默認(rèn)是 rune 類型。byte用來強(qiáng)調(diào)數(shù)據(jù)是raw data,而不是數(shù)字;而rune用來表示Unicode的code point。
雙引號(hào),才是字符串,實(shí)際上是字符數(shù)組。可以用索引號(hào)訪問某字節(jié),也可以用len()函數(shù)來獲取字符串所占的字節(jié)長度。
反引號(hào),表示字符串字面量,但不支持任何轉(zhuǎn)義序列。字面量 raw literal string 的意思是,你定義時(shí)寫的啥樣,它就啥樣,你有換行,它就換行。你寫轉(zhuǎn)義字符,它也就展示轉(zhuǎn)義字符。
go出現(xiàn)panic的場(chǎng)景
Go出現(xiàn)panic的場(chǎng)景
- 數(shù)組/切片越界
- 空指針調(diào)用。比如訪問一個(gè) nil 結(jié)構(gòu)體指針的成員
- 過早關(guān)閉 HTTP 響應(yīng)體
- 除以 0
- 向已經(jīng)關(guān)閉的 channel 發(fā)送消息
- 重復(fù)關(guān)閉 channel
- 關(guān)閉未初始化的 channel
- 未初始化 map。注意訪問 map 不存在的 key 不會(huì) panic,而是返回 map 類型對(duì)應(yīng)的零值,但是不能直接賦值
- 跨協(xié)程的 panic 處理
- sync 計(jì)數(shù)為負(fù)數(shù)。
- 類型斷言不匹配。
var a interface{} = 1; fmt.Println(a.(string))會(huì) panic,建議用s,ok := a.(string)
go是否支持while循環(huán),如何實(shí)現(xiàn)這種機(jī)制
https://blog.csdn.net/chengqiuming/article/details/115573947
go里面如何實(shí)現(xiàn)set?
Go中是不提供Set類型的,Set是一個(gè)集合,其本質(zhì)就是一個(gè)List,只是List里的元素不能重復(fù)。
Go提供了map類型,但是我們知道,map類型的key是不能重復(fù)的,因此,我們可以利用這一點(diǎn),來實(shí)現(xiàn)一個(gè)set。那value呢?value我們可以用一個(gè)常量來代替,比如一個(gè)空結(jié)構(gòu)體,實(shí)際上空結(jié)構(gòu)體不占任何內(nèi)存,使用空結(jié)構(gòu)體,能夠幫我們節(jié)省內(nèi)存空間,提高性能
代碼實(shí)現(xiàn):https://blog.csdn.net/haodawang/article/details/80006059
go如何實(shí)現(xiàn)類似于java當(dāng)中的繼承機(jī)制?
兩分鐘讓你明白Go中如何繼承
說到繼承我們都知道,在Go中沒有extends關(guān)鍵字,也就意味著Go并沒有原生級(jí)別的繼承支持。這也是為什么我在文章開頭用了偽繼承這個(gè)詞。本質(zhì)上,Go使用interface實(shí)現(xiàn)的功能叫組合,Go是使用組合來實(shí)現(xiàn)的繼承,說的更精確一點(diǎn),是使用組合來代替的繼承,舉個(gè)很簡(jiǎn)單的例子:
通過組合實(shí)現(xiàn)了繼承:
type Animal struct {
Name string
}
func (a *Animal) Eat() {
fmt.Printf("%v is eating", a.Name)
fmt.Println()
}
type Cat struct {
*Animal
}
cat := &Cat{
Animal: &Animal{
Name: "cat",
},
}
cat.Eat() // cat is eating
首先,我們實(shí)現(xiàn)了一個(gè)Animal的結(jié)構(gòu)體,代表動(dòng)物類。并聲明了Name字段,用于描述動(dòng)物的名字。
然后,實(shí)現(xiàn)了一個(gè)以Animal為receiver的Eat方法,來描述動(dòng)物進(jìn)食的行為。
最后,聲明了一個(gè)Cat結(jié)構(gòu)體,組合了Cat字段。再實(shí)例化一個(gè)貓,調(diào)用Eat方法,可以看到會(huì)正常的輸出。
可以看到,Cat結(jié)構(gòu)體本身沒有Name字段,也沒有去實(shí)現(xiàn)Eat方法。唯一有的就是組合了Animal父類,至此,我們就證明了已經(jīng)通過組合實(shí)現(xiàn)了繼承。
總結(jié):
- 如果一個(gè) struct 嵌套了另一個(gè)匿名結(jié)構(gòu)體,那么這個(gè)結(jié)構(gòu)可以直接訪問匿名結(jié)構(gòu)體的屬性和方法,從而實(shí)現(xiàn)繼承。
- 如果一個(gè) struct 嵌套了另一個(gè)有名的結(jié)構(gòu)體,那么這個(gè)模式叫做組合。
- 如果一個(gè) struct 嵌套了多個(gè)匿名結(jié)構(gòu)體,那么這個(gè)結(jié)構(gòu)可以直接訪問多個(gè)匿名結(jié)構(gòu)體的屬性和方法,從而實(shí)現(xiàn)多重繼承。
怎么去復(fù)用一個(gè)接口的方法?
怎么在golang中通過接口嵌套實(shí)現(xiàn)復(fù)用 - 開發(fā)技術(shù) - 億速云
go里面的 _
- 忽略返回值
- 比如某個(gè)函數(shù)返回三個(gè)參數(shù),但是我們只需要其中的兩個(gè),另外一個(gè)參數(shù)可以忽略,這樣的話代碼可以這樣寫:
v1, v2, _ := function(...)
v1, _, _ := function(...)
- 用在變量(特別是接口斷言)
type T struct{}
var _ X = T{}
//其中 I為interface
上面用來判斷 type T是否實(shí)現(xiàn)了X,用作類型斷言,如果T沒有實(shí)現(xiàn)接口X,則編譯錯(cuò)誤.
- 用在import package
import _ "test/food"
引入包時(shí),會(huì)先調(diào)用包中的初始化函數(shù),這種使用方式僅讓導(dǎo)入的包做初始化,而不使用包中其他功能
goroutine創(chuàng)建的時(shí)候如果要傳一個(gè)參數(shù)進(jìn)去有什么要注意的點(diǎn)?
http://www.rzrgm.cn/waken-captain/p/10496454.html
注:Golang1.22 版本對(duì)于for loop進(jìn)行了修改,詳見 Fixing For Loops in Go 1.22
寫go單元測(cè)試的規(guī)范?
- ** 單元測(cè)試文件命名規(guī)則 :**
單元測(cè)試需要?jiǎng)?chuàng)建單獨(dú)的測(cè)試文件,不能在原有文件中書寫,名字規(guī)則為 xxx_test.go。這個(gè)規(guī)則很好理解。
- **單元測(cè)試包命令規(guī)則 **
單元測(cè)試文件的包名為原文件的包名添加下劃線接test,舉例如下:
// 原文件包名:
package xxx
// 單元測(cè)試文件包名:
package xxx_test
- ** 單元測(cè)試方法命名規(guī)則 **
單元測(cè)試文件中的測(cè)試方法和原文件中的待測(cè)試的方法名相對(duì)應(yīng),以Test開頭,舉例如下:
// 原文件方法:
func Xxx(name string) error
// 單元測(cè)試文件方法:
func TestXxx()
- **單元測(cè)試方法參數(shù) **
單元測(cè)試方法的參數(shù)必須是t *testing.T,舉例如下:
func TestZipFiles(t *testing.T) { ...
單步調(diào)試?
導(dǎo)入一個(gè)go的工程,有些依賴找不到,改怎么辦?
值拷貝 與 引用拷貝,深拷貝 與 淺拷貝
map,slice,chan 是引用拷貝;引用拷貝 是 淺拷貝
其余的,都是 值拷貝;值拷貝 是 深拷貝
深淺拷貝的本質(zhì)區(qū)別:
是否真正獲取對(duì)象實(shí)體,而不是引用
深拷貝:
拷貝的是數(shù)據(jù)本身,創(chuàng)造一個(gè)新的對(duì)象,并在內(nèi)存中開辟一個(gè)新的內(nèi)存地址,與原對(duì)象是完全獨(dú)立的,不共享內(nèi)存,修改新對(duì)象時(shí)不會(huì)影響原對(duì)象的值。釋放內(nèi)存時(shí),也沒有任何關(guān)聯(lián)。
值拷貝:
接收的是 整個(gè)array的值拷貝,所以方法對(duì)array中元素的重新賦值不起作用。
package main
import "fmt"
func modify(a [3]int) {
a[0] = 4
fmt.Println("modify",a) // modify [4 2 3]
}
func main() {
a := [3]int{1, 2, 3}
modify(a)
fmt.Println("main",a) // main [1 2 3]
}
淺拷貝:
拷貝的是數(shù)據(jù)地址,只復(fù)制指向的對(duì)象的指針,新舊對(duì)象的內(nèi)存地址是一樣的,修改一個(gè)另一個(gè)也會(huì)變。釋放內(nèi)存時(shí),同時(shí)釋放。
引用拷貝:
函數(shù)的引用拷貝與原始的引用指向同一個(gè)數(shù)組,所以對(duì)數(shù)組中元素的修改,是有效的
package main
import "fmt"
func modify(s []int) {
s[0] = 4
fmt.Println("modify",s) // modify [4 2 3]
}
func main() {
s := []int{1, 2, 3}
modify(s)
fmt.Println("main",s) // main [4 2 3]
}
精通Golang項(xiàng)目依賴Go modules
Go 多返回值怎么實(shí)現(xiàn)的?
答:Go 傳參和返回值是通過 FP+offset 實(shí)現(xiàn),并且存儲(chǔ)在調(diào)用函數(shù)的棧幀中。FP 棧底寄存器,指向一個(gè)函數(shù)棧的頂部;PC 程序計(jì)數(shù)器,指向下一條執(zhí)行指令;SB 指向靜態(tài)數(shù)據(jù)的基指針,全局符號(hào);SP 棧頂寄存器。
Go 語言中不同的類型如何比較是否相等?
答:像 string,int,float interface 等可以通過 reflect.DeepEqual 和等于號(hào)進(jìn)行比較,像 slice,struct,map 則一般使用 reflect.DeepEqual 來檢測(cè)是否相等。
Go中init 函數(shù)的特征?
答:一個(gè)包下可以有多個(gè) init 函數(shù),每個(gè)文件也可以有多個(gè) init 函數(shù)。多個(gè) init 函數(shù)按照它們的文件名順序逐個(gè)初始化。應(yīng)用初始化時(shí)初始化工作的順序是,從被導(dǎo)入的最深層包開始進(jìn)行初始化,層層遞出最后到 main 包。不管包被導(dǎo)入多少次,包內(nèi)的 init 函數(shù)只會(huì)執(zhí)行一次。應(yīng)用初始化時(shí)初始化工作的順序是,從被導(dǎo)入的最深層包開始進(jìn)行初始化,層層遞出最后到 main 包。但包級(jí)別變量的初始化先于包內(nèi) init 函數(shù)的執(zhí)行。
Go中 uintptr和 unsafe.Pointer 的區(qū)別?
答:unsafe.Pointer 是通用指針類型,它不能參與計(jì)算,任何類型的指針都可以轉(zhuǎn)化成 unsafe.Pointer,unsafe.Pointer 可以轉(zhuǎn)化成任何類型的指針,uintptr 可以轉(zhuǎn)換為 unsafe.Pointer,unsafe.Pointer 可以轉(zhuǎn)換為 uintptr。uintptr 是指針運(yùn)算的工具,但是它不能持有指針對(duì)象(意思就是它跟指針對(duì)象不能互相轉(zhuǎn)換),unsafe.Pointer 是指針對(duì)象進(jìn)行運(yùn)算(也就是 uintptr)的橋梁。
什么是goroutine
- 定義:
- goroutine 是 Go 語言中的一種輕量級(jí)線程,由 Go 運(yùn)行時(shí)管理。
- 使用方法:
- 使用
go關(guān)鍵字啟動(dòng)一個(gè)新的 goroutine。例如:go 函數(shù)名(參數(shù)列表)。
- 使用
- 優(yōu)勢(shì):
- goroutine 的創(chuàng)建和銷毀開銷非常小,可以高效地創(chuàng)建成千上萬個(gè) goroutine。
- goroutine 是并發(fā)執(zhí)行的,可以提高程序的執(zhí)行效率。
- 調(diào)度:
- 由 Go 運(yùn)行時(shí)調(diào)度和管理,無需手動(dòng)管理線程。
- 通信:
- goroutine 之間通過 channel 進(jìn)行通信,確保數(shù)據(jù)傳遞的安全性和同步性。
- 典型應(yīng)用:
- 適用于并發(fā)任務(wù)處理,如網(wǎng)絡(luò)請(qǐng)求處理、并發(fā)計(jì)算等。
- 示例:
- 在 Web 服務(wù)器中,每個(gè)請(qǐng)求可以由一個(gè)單獨(dú)的 goroutine 處理,從而提高并發(fā)處理能力。
這樣回答簡(jiǎn)潔明了,可以幫助面試官快速了解你對(duì) goroutine 的理解。
slice
數(shù)組和切片的區(qū)別 (基本必問)
相同點(diǎn):
1)只能存儲(chǔ)一組相同類型的數(shù)據(jù)結(jié)構(gòu)
2)都是通過下標(biāo)來訪問,并且有容量長度,長度通過 len 獲取,容量通過 cap 獲取
區(qū)別:
1)數(shù)組是定長,訪問和復(fù)制不能超過數(shù)組定義的長度,否則就會(huì)下標(biāo)越界,切片長度和容量可以自動(dòng)擴(kuò)容
2)數(shù)組是值類型,切片是引用類型,每個(gè)切片都引用了一個(gè)底層數(shù)組,切片本身不能存儲(chǔ)任何數(shù)據(jù),都是這底層數(shù)組存儲(chǔ)數(shù)據(jù),所以修改切片的時(shí)候修改的是底層數(shù)組中的數(shù)據(jù)。切片一旦擴(kuò)容,指向一個(gè)新的底層數(shù)組,內(nèi)存地址也就隨之改變
簡(jiǎn)潔的回答:
1)定義方式不一樣 2)初始化方式不一樣,數(shù)組需要指定大小,大小不改變 3)在函數(shù)傳遞中,數(shù)組切片都是值傳遞。
數(shù)組的定義
var a1 [3]int
var a2 [...]int{1,2,3}
切片的定義
var a1 []int
var a2 :=make([]int,3,5)
數(shù)組的初始化
a1 := [...]int{1,2,3}
a2 := [5]int{1,2,3}
切片的初始化
b:= make([]int,3,5)
數(shù)組和切片有什么異同 - 碼農(nóng)桃花源
【引申1】 [3]int 和 [4]int 是同一個(gè)類型嗎?
不是。因?yàn)閿?shù)組的長度是類型的一部分,這是與 slice 不同的一點(diǎn)。
****講講 Go 的 slice 底層數(shù)據(jù)結(jié)構(gòu)和一些特性?
答:Go 的 slice 底層數(shù)據(jù)結(jié)構(gòu)是由一個(gè) array 指針指向底層數(shù)組,len 表示切片長度,cap 表示切片容量。slice 的主要實(shí)現(xiàn)是擴(kuò)容。對(duì)于 append 向 slice 添加元素時(shí),假如 slice 容量夠用,則追加新元素進(jìn)去,slice.len++,返回原來的 slice。當(dāng)原容量不夠,則 slice 先擴(kuò)容,擴(kuò)容之后 slice 得到新的 slice,將元素追加進(jìn)新的 slice,slice.len++,返回新的 slice。對(duì)于切片的擴(kuò)容規(guī)則:當(dāng)切片比較小時(shí)(容量小于 1024),則采用較大的擴(kuò)容倍速進(jìn)行擴(kuò)容(新的擴(kuò)容會(huì)是原來的 2 倍),避免頻繁擴(kuò)容,從而減少內(nèi)存分配的次數(shù)和數(shù)據(jù)拷貝的代價(jià)。當(dāng)切片較大的時(shí)(原來的 slice 的容量大于或者等于 1024),采用較小的擴(kuò)容倍速(新的擴(kuò)容將擴(kuò)大大于或者等于原來 1.25 倍),主要避免空間浪費(fèi),網(wǎng)上其實(shí)很多總結(jié)的是 1.25 倍,那是在不考慮內(nèi)存對(duì)齊的情況下,實(shí)際上還要考慮內(nèi)存對(duì)齊,擴(kuò)容是大于或者等于 1.25 倍。
注:Go的切片擴(kuò)容源代碼在runtime下的growslice函數(shù)
(關(guān)于剛才問的 slice 為什么傳到函數(shù)內(nèi)可能被修改,如果 slice 在函數(shù)內(nèi)沒有出現(xiàn)擴(kuò)容,函數(shù)外和函數(shù)內(nèi) slice 變量指向是同一個(gè)數(shù)組,則函數(shù)內(nèi)復(fù)制的 slice 變量值出現(xiàn)更改,函數(shù)外這個(gè) slice 變量值也會(huì)被修改。如果 slice 在函數(shù)內(nèi)出現(xiàn)擴(kuò)容,則函數(shù)內(nèi)變量的值會(huì)新生成一個(gè)數(shù)組(也就是新的 slice,而函數(shù)外的 slice 指向的還是原來的 slice,則函數(shù)內(nèi)的修改不會(huì)影響函數(shù)外的 slice。)
golang中數(shù)組和slice作為參數(shù)的區(qū)別?slice作為參數(shù)傳遞有什么問題?
golang數(shù)組和切片作為參數(shù)和返回值_weixin_44387482的博客-CSDN博客_golang 返回?cái)?shù)組
- 當(dāng)使用數(shù)組作為參數(shù)和返回值的時(shí)候,傳進(jìn)去的是值,在函數(shù)內(nèi)部對(duì)數(shù)組進(jìn)行修改并不會(huì)影響原數(shù)據(jù)
- 當(dāng)切片作為參數(shù)的時(shí)候穿進(jìn)去的是值,也就是值傳遞,但是當(dāng)我在函數(shù)里面修改切片的時(shí)候,我們發(fā)現(xiàn)源數(shù)據(jù)也會(huì)被修改,這是因?yàn)槲覀冊(cè)谇衅牡讓泳S護(hù)這一個(gè)匿名的數(shù)組,當(dāng)我們把切片當(dāng)成參數(shù)的時(shí)候,會(huì)重現(xiàn)創(chuàng)建一個(gè)切片,但是創(chuàng)建的這個(gè)切片和我們?cè)瓉淼臄?shù)據(jù)是共享數(shù)據(jù)源的,所以在函數(shù)內(nèi)被修改,源數(shù)據(jù)也會(huì)被修改
- 數(shù)組還是切片,在函數(shù)中傳遞的時(shí)候如果沒有指定為指針傳遞的話,都是值傳遞,但是切片在傳遞的過程中,有著共享底層數(shù)組的風(fēng)險(xiǎn),所以如果在函數(shù)內(nèi)部進(jìn)行了更改的時(shí)候,會(huì)修改到源數(shù)據(jù),所以我們需要根據(jù)不同的需求來處理,如果我們不希望源數(shù)據(jù)被修改話的我們可以使用copy函數(shù)復(fù)制切片后再傳入,如果希望源數(shù)據(jù)被修改的話我們應(yīng)該使用指針傳遞的方式
從數(shù)組中取一個(gè)相同大小的slice有成本嗎?
在Go語言中,從數(shù)組中取一個(gè)相同大小的slice(切片)實(shí)際上是一個(gè)非常低成本的操作。這是因?yàn)閟lice在Go中是一個(gè)引用類型,它內(nèi)部包含了指向數(shù)組的指針、切片的長度以及切片的容量。當(dāng)你從一個(gè)數(shù)組創(chuàng)建一個(gè)相同大小的slice時(shí),你實(shí)際上只是創(chuàng)建了一個(gè)新的slice結(jié)構(gòu)體,它包含了指向原數(shù)組的指針、原數(shù)組的長度作為切片的長度,以及原數(shù)組的長度作為切片的容量。
這個(gè)操作的成本主要在于內(nèi)存的分配(為新的slice結(jié)構(gòu)體分配內(nèi)存),但這個(gè)成本是非常小的,因?yàn)樗皇欠峙淞艘粋€(gè)很小的結(jié)構(gòu)體,而不是復(fù)制數(shù)組的內(nèi)容。數(shù)組的內(nèi)容仍然是共享的,即新的slice和原數(shù)組指向相同的內(nèi)存區(qū)域。
因此,從數(shù)組中取一個(gè)相同大小的slice是一個(gè)低成本的操作,它允許你高效地操作數(shù)組的部分或全部元素,而不需要復(fù)制這些元素。
新舊擴(kuò)容策略
1.18之前
Go 1.18版本 之前擴(kuò)容原理
在分配內(nèi)存空間之前需要先確定新的切片容量,運(yùn)行時(shí)根據(jù)切片的當(dāng)前容量選擇不同的策略進(jìn)行擴(kuò)容:
- 如果期望容量大于當(dāng)前容量的兩倍就會(huì)使用期望容量;
- 如果當(dāng)前切片的長度小于 1024 就會(huì)將容量翻倍;
- 如果當(dāng)前切片的長度大于等于 1024 就會(huì)每次增加 25% 的容量,直到新容量大于期望容量;
注:解釋一下第一條:
比如 nums := []int{1, 2} nums = append(nums, 2, 3, 4),這樣期望容量為2+3 = 5,而5 > 2*2,故使用期望容量(這只是不考慮內(nèi)存對(duì)齊的情況下)
1.18版本 之后擴(kuò)容原理
和之前版本的區(qū)別,主要在擴(kuò)容閾值,以及這行源碼:newcap += (newcap + 3*threshold) / 4。
在分配內(nèi)存空間之前需要先確定新的切片容量,運(yùn)行時(shí)根據(jù)切片的當(dāng)前容量選擇不同的策略進(jìn)行擴(kuò)容:
- 如果期望容量大于當(dāng)前容量的兩倍就會(huì)使用期望容量;
- 當(dāng)原 slice 容量 < threshold(閾值默認(rèn) 256) 的時(shí)候,新 slice 容量變成原來的 2 倍;
- 當(dāng)原 slice 容量 > threshold(閾值默認(rèn) 256),進(jìn)入一個(gè)循環(huán),每次容量增加(舊容量+3*threshold)/4。

下對(duì)應(yīng)的“擴(kuò)容系數(shù)”:
| oldcap | 擴(kuò)容系數(shù) |
|---|---|
| 256 | 2.0 |
| 512 | 1.63 |
| 1024 | 1.44 |
| 2048 | 1.35 |
| 4096 | 1.30 |
可以看到,Go1.18的擴(kuò)容策略中,隨著容量的增大,其擴(kuò)容系數(shù)是越來越小的,可以更好地節(jié)省內(nèi)存
總的來說
Go的設(shè)計(jì)者不斷優(yōu)化切片擴(kuò)容的機(jī)制,其目的只有一個(gè):就是控制讓小的切片容量增長速度快一點(diǎn),減少內(nèi)存分配次數(shù),而讓大切片容量增長率小一點(diǎn),更好地節(jié)省內(nèi)存。
如果只選擇翻倍的擴(kuò)容策略,那么對(duì)于較大的切片來說,現(xiàn)有的方法可以更好的節(jié)省內(nèi)存。
如果只選擇每次系數(shù)為1.25的擴(kuò)容策略,那么對(duì)于較小的切片來說擴(kuò)容會(huì)很低效。
之所以選擇一個(gè)小于2的系數(shù),在擴(kuò)容時(shí)被釋放的內(nèi)存塊會(huì)在下一次擴(kuò)容時(shí)更容易被重新利用。
map相關(guān)
什么類型可以作為map 的key
在Go語言中,map的key可以是任何可以比較的類型。這包括所有的基本類型,如整數(shù)、浮點(diǎn)數(shù)、字符串和布爾值,以及結(jié)構(gòu)體和數(shù)組,只要它們沒有被定義為包含不可比較的類型(如切片、映射或函數(shù))。
以下是一些可以作為map key的類型的例子:
- 整數(shù)類型:
m := make(map[int]string)
m[1] = "one"
- 字符串類型:
m := make(map[string]int)
m["one"] = 1
- 布爾類型:
m := make(map[bool]string)
m[true] = "yes"
m[false] = "no"
- 結(jié)構(gòu)體類型(只要結(jié)構(gòu)體的所有字段都是可比較的):
type Point struct {
X, Y int
}
m := make(map[Point]string)
m[Point{1, 2}] = "1,2"
- 數(shù)組類型(數(shù)組的元素類型必須是可比較的):
m := make(map[[2]int]string)
m[[2]int{1, 2}] = "1,2"
注意,切片、映射和函數(shù)類型是不可比較的,因此不能作為map的key。如果你需要一個(gè)包含這些類型的key,你可以考慮使用一個(gè)指向這些類型的指針,或者將它們封裝在一個(gè)可比較的結(jié)構(gòu)體中,并確保結(jié)構(gòu)體不包含任何不可比較的類型。
map 使用注意的點(diǎn),是否并發(fā)安全?
map使用的注意點(diǎn)
- key的唯一性:map中的每個(gè)key必須是唯一的。如果嘗試使用已存在的key插入新值,則會(huì)覆蓋舊值。
- key的不可變性:作為key的類型必須是可比較的,這通常意味著它們應(yīng)該是不可變的。例如,在Go語言中,切片、映射和函數(shù)類型因?yàn)榘勺儬顟B(tài),所以不能直接作為map的key。
- 初始化和nil map:在Go語言中,聲明一個(gè)map變量不會(huì)自動(dòng)初始化它。未初始化的map變量的零值是nil,對(duì)nil map進(jìn)行讀寫操作會(huì)引發(fā)panic。因此,在使用map之前,應(yīng)該使用
<font style="color:rgb(5, 7, 59);">make</font>函數(shù)進(jìn)行初始化。 - 遍歷順序:map的遍歷順序是不確定的,每次遍歷的結(jié)果可能不同。如果需要按照特定順序處理map中的元素,應(yīng)該先對(duì)key進(jìn)行排序。
- 并發(fā)安全性:默認(rèn)情況下,map并不是并發(fā)安全的。在并發(fā)環(huán)境下對(duì)同一個(gè)map進(jìn)行讀寫操作可能會(huì)導(dǎo)致競(jìng)態(tài)條件和數(shù)據(jù)不一致性。
并發(fā)安全性
Go語言中的map并發(fā)安全性:
- Go語言中的map類型并不是并發(fā)安全的。這意味著,如果有多個(gè)goroutine嘗試同時(shí)讀寫同一個(gè)map,可能會(huì)導(dǎo)致競(jìng)態(tài)條件和數(shù)據(jù)損壞。
- 為了在并發(fā)環(huán)境下安全地使用map,可以采取以下幾種策略:
- 使用互斥鎖(sync.Mutex):在讀寫map的操作前后加鎖,確保同一時(shí)間只有一個(gè)goroutine可以訪問map。
- 使用讀寫互斥鎖(sync.RWMutex):如果讀操作遠(yuǎn)多于寫操作,可以使用讀寫鎖來提高性能。讀寫鎖允許多個(gè)goroutine同時(shí)讀取map,但在寫入時(shí)需要獨(dú)占訪問。
- 使用并發(fā)安全的map(sync.Map):從Go 1.9版本開始,標(biāo)準(zhǔn)庫中的
<font style="color:rgb(5, 7, 59);">sync</font>包提供了<font style="color:rgb(5, 7, 59);">sync.Map</font>類型,這是一個(gè)專為并發(fā)環(huán)境設(shè)計(jì)的map。它提供了一系列方法來安全地在多個(gè)goroutine之間共享數(shù)據(jù)。
結(jié)論:
在使用map時(shí),需要注意其key的唯一性和不可變性,以及初始化和并發(fā)安全性的問題。特別是在并發(fā)環(huán)境下,應(yīng)該采取適當(dāng)?shù)拇胧﹣泶_保map的安全訪問,以避免競(jìng)態(tài)條件和數(shù)據(jù)不一致性。在Go語言中,可以通過使用互斥鎖、讀寫互斥鎖或并發(fā)安全的map(<font style="color:rgb(5, 7, 59);">sync.Map</font>)來實(shí)現(xiàn)這一點(diǎn)。
map 循環(huán)是有序的還是無序的?
在Go語言中,map的循環(huán)(遍歷)是無序的。這意味著當(dāng)你遍歷map時(shí),每次遍歷的順序可能都不同。Go語言的map是基于哈希表的,因此元素的存儲(chǔ)順序是不確定的,并且可能會(huì)隨著元素的添加、刪除等操作而改變。
如果你需要按照特定的順序處理map中的元素,你應(yīng)該先將key提取到一個(gè)切片中,對(duì)切片進(jìn)行排序,然后按照排序后的順序遍歷切片,并從map中取出對(duì)應(yīng)的值。這樣,你就可以按照特定的順序處理map中的元素了。
map 中刪除一個(gè) key,它的內(nèi)存會(huì)釋放么?
在Go語言中,從map中刪除一個(gè)key時(shí),其內(nèi)存釋放的行為并非直觀且立即的,這涉及到Go語言的內(nèi)存管理機(jī)制。具體來說,刪除map中的key后,其內(nèi)存釋放情況如下:
內(nèi)存標(biāo)記與垃圾回收
- 刪除操作:使用
<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">delete</font>函數(shù)從map中刪除一個(gè)key時(shí),該key及其關(guān)聯(lián)的值會(huì)被從map的內(nèi)部數(shù)據(jù)結(jié)構(gòu)中移除。此時(shí),這些值在邏輯上不再屬于map的一部分。 - 內(nèi)存標(biāo)記:刪除操作后,如果沒有任何其他變量或數(shù)據(jù)結(jié)構(gòu)引用被刪除的值,那么這些值就變成了垃圾回收器的目標(biāo)。Go語言的垃圾回收器(Garbage Collector, GC)會(huì)定期掃描內(nèi)存,標(biāo)記那些不再被使用的內(nèi)存區(qū)域。
- 內(nèi)存釋放:在垃圾回收過程中,被標(biāo)記為垃圾的內(nèi)存區(qū)域會(huì)被釋放回堆內(nèi)存,供后續(xù)的內(nèi)存分配使用。然而,這個(gè)過程并不是立即發(fā)生的,而是由垃圾回收器的觸發(fā)條件和回收策略決定的。
注意事項(xiàng)
- 內(nèi)存釋放時(shí)機(jī):由于垃圾回收器的非確定性,刪除map中的key后,其內(nèi)存釋放的時(shí)機(jī)是不確定的。因此,不能依賴刪除操作來立即釋放內(nèi)存。
- map底層存儲(chǔ)不變:刪除操作只是邏輯上移除了key-value對(duì),但map底層分配的內(nèi)存(如哈希表的桶和溢出桶)并不會(huì)立即減小。這是因?yàn)閙ap的設(shè)計(jì)優(yōu)先考慮的是訪問速度,而不是空間效率。如果需要釋放大量?jī)?nèi)存,一種方法是創(chuàng)建一個(gè)新的map,并將舊map中需要保留的元素復(fù)制過去。
- 并發(fā)安全:如果map在多個(gè)goroutine之間共享,那么刪除操作需要考慮并發(fā)安全問題。可以使用互斥鎖(如
<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync.Mutex</font>)來保護(hù)對(duì)map的訪問,或者使用Go 1.9引入的<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync.Map</font>,它提供了內(nèi)置的并發(fā)安全機(jī)制。
結(jié)論
從map中刪除一個(gè)key后,其內(nèi)存并不會(huì)立即釋放。內(nèi)存釋放取決于Go語言的垃圾回收器何時(shí)觸發(fā)回收過程。在大多數(shù)情況下,開發(fā)者不需要過于擔(dān)心內(nèi)存釋放的問題,因?yàn)镚o的內(nèi)存管理機(jī)制相當(dāng)智能。然而,在處理大量數(shù)據(jù)時(shí),了解這些內(nèi)存管理的細(xì)節(jié)對(duì)于優(yōu)化程序性能是非常有幫助的。
怎么處理對(duì) map 進(jìn)行并發(fā)訪問?有沒有其他方案? 區(qū)別是什么?
處理對(duì)map進(jìn)行并發(fā)訪問的問題,主要需要確保在多個(gè)goroutine同時(shí)訪問map時(shí)不會(huì)出現(xiàn)競(jìng)態(tài)條件和數(shù)據(jù)不一致的情況。以下是幾種處理并發(fā)訪問map的方案及其區(qū)別:
使用互斥鎖(sync.Mutex)
方案描述:
使用<font style="color:rgb(5, 7, 59);">sync.Mutex</font>或<font style="color:rgb(5, 7, 59);">sync.RWMutex</font>(讀寫互斥鎖)來控制對(duì)map的訪問。在訪問map之前加鎖,訪問完成后釋放鎖。這樣可以保證在同一時(shí)間內(nèi)只有一個(gè)goroutine可以訪問map。
優(yōu)點(diǎn):
- 實(shí)現(xiàn)簡(jiǎn)單,容易理解。
- 對(duì)于寫操作頻繁的場(chǎng)景,能夠較好地保證數(shù)據(jù)一致性。
缺點(diǎn):
- 在讀多寫少的場(chǎng)景下,性能可能不是最優(yōu)的,因?yàn)樽x操作也需要獲取鎖。
- 鎖的粒度較大,可能會(huì)影響并發(fā)性能。
使用讀寫互斥鎖(sync.RWMutex)
方案描述:
與<font style="color:rgb(5, 7, 59);">sync.Mutex</font>類似,但<font style="color:rgb(5, 7, 59);">sync.RWMutex</font>允許多個(gè)goroutine同時(shí)讀取map,只有在寫入時(shí)才需要獨(dú)占訪問。
優(yōu)點(diǎn):
- 在讀多寫少的場(chǎng)景下,性能優(yōu)于
<font style="color:rgb(5, 7, 59);">sync.Mutex</font>,因?yàn)樽x操作不需要獲取寫鎖。
缺點(diǎn):
- 寫入操作仍然需要獨(dú)占訪問,可能會(huì)影響并發(fā)寫入的性能。
- 實(shí)現(xiàn)略復(fù)雜于
<font style="color:rgb(5, 7, 59);">sync.Mutex</font>,需要區(qū)分讀寫操作。
使用并發(fā)安全的map(sync.Map)
方案描述:
從Go 1.9版本開始,標(biāo)準(zhǔn)庫中的<font style="color:rgb(5, 7, 59);">sync</font>包提供了<font style="color:rgb(5, 7, 59);">sync.Map</font>類型,它是一個(gè)專為并發(fā)環(huán)境設(shè)計(jì)的map。<font style="color:rgb(5, 7, 59);">sync.Map</font>內(nèi)部使用了讀寫鎖和其他同步機(jī)制來保證并發(fā)訪問的安全性。
優(yōu)點(diǎn):
- 無需顯式加鎖,簡(jiǎn)化了并發(fā)編程。
- 針對(duì)讀多寫少的場(chǎng)景進(jìn)行了優(yōu)化,如讀寫分離等,提高了并發(fā)性能。
- 提供了特定的方法(如
<font style="color:rgb(5, 7, 59);">Load</font>、<font style="color:rgb(5, 7, 59);">Store</font>、<font style="color:rgb(5, 7, 59);">Delete</font>等)來安全地訪問map。
缺點(diǎn):
- 在某些情況下,性能可能不如使用
<font style="color:rgb(5, 7, 59);">sync.RWMutex</font>的自定義map(尤其是在寫入操作頻繁時(shí))。 <font style="color:rgb(5, 7, 59);">sync.Map</font>的API與內(nèi)置map不同,可能需要適應(yīng)新的使用方式。
區(qū)別總結(jié)
| 方案 | 實(shí)現(xiàn)復(fù)雜度 | 性能(讀多寫少) | 性能(寫多) | 使用場(chǎng)景 |
|---|---|---|---|---|
| sync.Mutex | 低 | 中等 | 中等 | 寫操作頻繁,對(duì)并發(fā)性能要求不高 |
| sync.RWMutex | 中等 | 高 | 中等 | 讀多寫少,需要較高并發(fā)讀性能 |
| sync.Map | 低(API不同) | 高 | 中等偏下 | 讀多寫少,追求簡(jiǎn)潔的并發(fā)編程模型 |
注意事項(xiàng)
- 在選擇方案時(shí),需要根據(jù)實(shí)際的應(yīng)用場(chǎng)景(如讀寫比例、并發(fā)級(jí)別等)來決定使用哪種方案。
- 如果并發(fā)級(jí)別不高,且對(duì)性能要求不高,也可以考慮使用簡(jiǎn)單的鎖機(jī)制(如
<font style="color:rgb(5, 7, 59);">sync.Mutex</font>)來簡(jiǎn)化實(shí)現(xiàn)。 - 對(duì)于性能要求極高的場(chǎng)景,可能需要考慮更復(fù)雜的并發(fā)數(shù)據(jù)結(jié)構(gòu)或算法來優(yōu)化性能。
綜上所述,處理對(duì)map的并發(fā)訪問需要根據(jù)具體情況選擇合適的方案,并在實(shí)際使用中不斷優(yōu)化和調(diào)整以達(dá)到最佳性能。
nil map 和空 map 有何不同?
在Go語言中,nil map和空map之間存在一些關(guān)鍵的不同點(diǎn),主要體現(xiàn)在它們的初始狀態(tài)、對(duì)增刪查操作的影響以及內(nèi)存占用等方面。
初始狀態(tài)與內(nèi)存占用
- nil map:未初始化的map的零值是nil。這意味著map變量被聲明后,如果沒有通過
<font style="color:rgb(5, 7, 59);">make</font>函數(shù)或其他方式顯式初始化,它將保持nil狀態(tài)。nil map不占用實(shí)際的內(nèi)存空間來存儲(chǔ)鍵值對(duì),因?yàn)樗鼪]有底層的哈希表結(jié)構(gòu)。 - 空map:空map是通過
<font style="color:#DF2A3F;">make</font>函數(shù)或其他方式初始化但沒有添加任何鍵值對(duì)的map。空map已經(jīng)分配了底層的哈希表結(jié)構(gòu),但表中沒有存儲(chǔ)任何鍵值對(duì)。因此,空map占用了一定的內(nèi)存空間,盡管這個(gè)空間相對(duì)較小。
對(duì)增刪查操作的影響
- nil map:
- 添加操作:向nil map中添加鍵值對(duì)將導(dǎo)致運(yùn)行時(shí)panic,因?yàn)閚il map沒有底層的哈希表來存儲(chǔ)數(shù)據(jù)。
- 刪除操作:在早期的Go版本中,嘗試從nil map中刪除鍵值對(duì)也可能導(dǎo)致panic,但在最新的Go版本中,這一行為可能已經(jīng)被改變(具體取決于Go的版本),但通常不建議對(duì)nil map執(zhí)行刪除操作。
- 查找操作:從nil map中查找鍵值對(duì)不會(huì)引發(fā)panic,但會(huì)返回對(duì)應(yīng)類型的零值,表示未找到鍵值對(duì)。
- 空map:
- 添加操作:向空map中添加鍵值對(duì)是安全的,鍵值對(duì)會(huì)被添加到map中。
- 刪除操作:從空map中刪除鍵值對(duì)是一個(gè)空操作,不會(huì)引發(fā)panic,因?yàn)閙ap中原本就沒有該鍵值對(duì)。
- 查找操作:從空map中查找不存在的鍵值對(duì)也會(huì)返回對(duì)應(yīng)類型的零值,表示未找到鍵值對(duì)。
總結(jié)
nil map和空map的主要區(qū)別在于它們的初始狀態(tài)和對(duì)增刪查操作的影響。nil map未初始化且不能用于存儲(chǔ)鍵值對(duì),而空map已初始化且可以安全地用于增刪查操作。在編寫Go程序時(shí),應(yīng)根據(jù)需要選擇使用nil map還是空map,并注意處理nil map可能引發(fā)的panic。
map 的數(shù)據(jù)結(jié)構(gòu)是什么?
map-地鼠文檔
答:golang 中 map 是一個(gè) kv 對(duì)集合。底層使用 hash table,用鏈表來解決沖突 ,出現(xiàn)沖突時(shí),不是每一個(gè) key 都申請(qǐng)一個(gè)結(jié)構(gòu)通過鏈表串起來,而是以 bmap 為最小粒度掛載,一個(gè) bmap 可以放 8 個(gè) kv。在哈希函數(shù)的選擇上,會(huì)在程序啟動(dòng)時(shí),檢測(cè) cpu 是否支持 aes,如果支持,則使用 aes hash,否則使用 memhash。每個(gè) map 的底層結(jié)構(gòu)是 hmap,是有若干個(gè)結(jié)構(gòu)為 bmap 的 bucket 組成的數(shù)組。每個(gè) bucket 底層都采用鏈表結(jié)構(gòu)。
hmap 的結(jié)構(gòu)如下:
type hmap struct {
count int // 元素個(gè)數(shù)
flags uint8
B uint8 // 擴(kuò)容常量相關(guān)字段B是buckets數(shù)組的長度的對(duì)數(shù) 2^B
noverflow uint16 // 溢出的bucket個(gè)數(shù)
hash0 uint32 // hash seed
buckets unsafe.Pointer // buckets 數(shù)組指針
oldbuckets unsafe.Pointer // 結(jié)構(gòu)擴(kuò)容的時(shí)候用于賦值的buckets數(shù)組
nevacuate uintptr // 搬遷進(jìn)度
extra *mapextra // 用于擴(kuò)容的指針
}
下圖展示一個(gè)擁有4個(gè)bucket的map:

本例中, hmap.B=2, 而hmap.buckets長度是2^B為4. 元素經(jīng)過哈希運(yùn)算后會(huì)落到某個(gè)bucket中進(jìn)行存儲(chǔ)。查找過程類似。
bucket很多時(shí)候被翻譯為桶,所謂的哈希桶實(shí)際上就是bucket。
bucket數(shù)據(jù)結(jié)構(gòu)
bucket數(shù)據(jù)結(jié)構(gòu)由runtime/map.go:bmap定義:
type bmap struct {
tophash [8]uint8 //存儲(chǔ)哈希值的高8位
data byte[1] //key value數(shù)據(jù):key/key/key/.../value/value/value...
overflow *bmap //溢出bucket的地址
}
每個(gè)bucket可以存儲(chǔ)8個(gè)鍵值對(duì)。
- tophash是個(gè)長度為8的數(shù)組,哈希值相同的鍵(準(zhǔn)確的說是哈希值低位相同的鍵)存入當(dāng)前bucket時(shí)會(huì)將哈希值的高位存儲(chǔ)在該數(shù)組中,以方便后續(xù)匹配。
- data區(qū)存放的是key-value數(shù)據(jù),存放順序是key/key/key/…value/value/value,如此存放是為了節(jié)省字節(jié)對(duì)齊帶來的空間浪費(fèi)。
- overflow 指針指向的是下一個(gè)bucket,據(jù)此將所有沖突的鍵連接起來。
注意:上述中data和overflow并不是在結(jié)構(gòu)體中顯示定義的,而是直接通過指針運(yùn)算進(jìn)行訪問的。
下圖展示bucket存放8個(gè)key-value對(duì):

解決哈希沖突(四種方法)
哈希沖突
當(dāng)有兩個(gè)或以上數(shù)量的鍵被哈希到了同一個(gè)bucket時(shí),我們稱這些鍵發(fā)生了沖突。Go使用鏈地址法來解決鍵沖突。
由于每個(gè)bucket可以存放8個(gè)鍵值對(duì),所以同一個(gè)bucket存放超過8個(gè)鍵值對(duì)時(shí)就會(huì)再創(chuàng)建一個(gè)鍵值對(duì),用類似鏈表的方式將bucket連接起來。
下圖展示產(chǎn)生沖突后的map:

bucket數(shù)據(jù)結(jié)構(gòu)指示下一個(gè)bucket的指針稱為overflow bucket,意為當(dāng)前bucket盛不下而溢出的部分。事實(shí)上哈希沖突并不是好事情,它降低了存取效率,好的哈希算法可以保證哈希值的隨機(jī)性,但沖突過多也是要控制的,后面會(huì)再詳細(xì)介紹。
鏈地址法:
將所有哈希地址相同的記錄都鏈接在同一鏈表中。
- 當(dāng)兩個(gè)不同的鍵通過哈希函數(shù)計(jì)算得到相同的哈希值時(shí),Go的map并不直接覆蓋舊的值,而是將這些具有相同哈希值的鍵值對(duì)存儲(chǔ)在同一個(gè)桶(bucket)中的鏈表中。這樣,即使哈希值相同,也可以通過遍歷鏈表來找到對(duì)應(yīng)的鍵值對(duì)。
- 當(dāng)桶中的鏈表長度超過一定閾值時(shí)(通常是8個(gè)元素),Go的map會(huì)進(jìn)行擴(kuò)容和重新哈希,以減少哈希沖突,并優(yōu)化查找、插入和刪除操作的性能。
負(fù)載因子
負(fù)載因子用于衡量一個(gè)哈希表沖突情況,公式為:
負(fù)載因子 = 鍵數(shù)量/bucket數(shù)量
例如,對(duì)于一個(gè)bucket數(shù)量為4,包含4個(gè)鍵值對(duì)的哈希表來說,這個(gè)哈希表的負(fù)載因子為1.
哈希表需要將負(fù)載因子控制在合適的大小,超過其閥值需要進(jìn)行rehash,也即鍵值對(duì)重新組織:
- 哈希因子過小,說明空間利用率低
- 哈希因子過大,說明沖突嚴(yán)重,存取效率低
每個(gè)哈希表的實(shí)現(xiàn)對(duì)負(fù)載因子容忍程度不同,比如Redis實(shí)現(xiàn)中負(fù)載因子大于1時(shí)就會(huì)觸發(fā)rehash,而Go則在在負(fù)載因子達(dá)到6.5時(shí)才會(huì)觸發(fā)rehash,因?yàn)镽edis的每個(gè)bucket只能存1個(gè)鍵值對(duì),而Go的bucket可能存8個(gè)鍵值對(duì),所以Go可以容忍更高的負(fù)載因子。
是怎么實(shí)現(xiàn)擴(kuò)容?
map 的容量大小
底層調(diào)用 makemap 函數(shù),計(jì)算得到合適的 B,map 容量最多可容納 6.52B 個(gè)元素,6.5 為裝載因子閾值常量。裝載因子的計(jì)算公式是:裝載因子=填入表中的元素個(gè)數(shù)/散列表的長度,裝載因子越大,說明空閑位置越少,沖突越多,散列表的性能會(huì)下降。底層調(diào)用 makemap 函數(shù),計(jì)算得到合適的 B,map 容量最多可容納 6.52B 個(gè)元素,6.5 為裝載因子閾值常量。裝載因子的計(jì)算公式是:裝載因子=填入表中的元素個(gè)數(shù)/散列表的長度,裝載因子越大,說明空閑位置越少,沖突越多,散列表的性能會(huì)下降。
觸發(fā) map 擴(kuò)容的條件
為了保證訪問效率,當(dāng)新元素將要添加進(jìn)map時(shí),都會(huì)檢查是否需要擴(kuò)容,擴(kuò)容實(shí)際上是以空間換時(shí)間的手段。
觸發(fā)擴(kuò)容的條件有二個(gè):
- 負(fù)載因子 > 6.5時(shí),也即平均每個(gè)bucket存儲(chǔ)的鍵值對(duì)達(dá)到6.5個(gè)。
- overflow數(shù)量 > 2^15時(shí),也即overflow數(shù)量超過32768時(shí)。
增量擴(kuò)容
當(dāng)負(fù)載因子過大時(shí),就新建一個(gè)bucket,新的bucket長度是原來的2倍,然后舊bucket數(shù)據(jù)搬遷到新的bucket。
考慮到如果map存儲(chǔ)了數(shù)以億計(jì)的key-value,一次性搬遷將會(huì)造成比較大的延時(shí),Go采用逐步搬遷策略,即每次訪問map時(shí)都會(huì)觸發(fā)一次搬遷,每次搬遷2個(gè)鍵值對(duì)。
下圖展示了包含一個(gè)bucket滿載的map(為了描述方便,圖中bucket省略了value區(qū)域):

當(dāng)前map存儲(chǔ)了7個(gè)鍵值對(duì),只有1個(gè)bucket。此地負(fù)載因子為7。再次插入數(shù)據(jù)時(shí)將會(huì)觸發(fā)擴(kuò)容操作,擴(kuò)容之后再將新插入鍵寫入新的bucket。
當(dāng)?shù)?個(gè)鍵值對(duì)插入時(shí),將會(huì)觸發(fā)擴(kuò)容,擴(kuò)容后示意圖如下:

hmap數(shù)據(jù)結(jié)構(gòu)中oldbuckets成員指身原bucket,而buckets指向了新申請(qǐng)的bucket。新的鍵值對(duì)被插入新的bucket中。
后續(xù)對(duì)map的訪問操作會(huì)觸發(fā)遷移,將oldbuckets中的鍵值對(duì)逐步的搬遷過來。當(dāng)oldbuckets中的鍵值對(duì)全部搬遷完畢后,刪除oldbuckets。
搬遷完成后的示意圖如下:

數(shù)據(jù)搬遷過程中原bucket中的鍵值對(duì)將存在于新bucket的前面,新插入的鍵值對(duì)將存在于新bucket的后面。
實(shí)際搬遷過程中比較復(fù)雜,將在后續(xù)源碼分析中詳細(xì)介紹。
等量擴(kuò)容
所謂等量擴(kuò)容,實(shí)際上并不是擴(kuò)大容量,buckets數(shù)量不變,重新做一遍類似增量擴(kuò)容的搬遷動(dòng)作,把松散的鍵值對(duì)重新排列一次,以使bucket的使用率更高,進(jìn)而保證更快的存取。
在極端場(chǎng)景下,比如不斷地增刪,而鍵值對(duì)正好集中在一小部分的bucket,這樣會(huì)造成overflow的bucket數(shù)量增多,但負(fù)載因子又不高,從而無法執(zhí)行增量搬遷的情況,如下圖所示:

上圖可見,overflow的bucket中大部分是空的,訪問效率會(huì)很差。此時(shí)進(jìn)行一次等量擴(kuò)容,即buckets數(shù)量不變,經(jīng)過重新組織后overflow的bucket數(shù)量會(huì)減少,即節(jié)省了空間又會(huì)提高訪問效率。
查找過程
查找過程如下:
- 根據(jù)key值算出哈希值
- 取哈希值低位與hmap.B取模確定bucket位置
- 取哈希值高位在tophash數(shù)組中查詢
- 如果tophash[i]中存儲(chǔ)值也哈希值相等,則去找到該bucket中的key值進(jìn)行比較
- 當(dāng)前bucket沒有找到,則繼續(xù)從下個(gè)overflow的bucket中查找。
- 如果當(dāng)前處于搬遷過程,則優(yōu)先從oldbuckets查找
注:如果查找不到,也不會(huì)返回空值,而是返回相應(yīng)類型的0值。
插入過程
新元素插入過程如下:
- 根據(jù)key值算出哈希值
- 取哈希值低位與hmap.B取模確定bucket位置
- 查找該key是否已經(jīng)存在,如果存在則直接更新值
- 如果沒找到將key,將key插入
增刪查的時(shí)間復(fù)雜度 O(1)
- 在Go語言中,對(duì)于map的查找、插入和刪除操作,在大多數(shù)情況下,它們的時(shí)間復(fù)雜度都可以視為O(1),即常數(shù)時(shí)間復(fù)雜度。
- map的讀寫效率之所以在平均情況下能達(dá)到O(1),是因?yàn)镚o語言的map實(shí)現(xiàn)采用了哈希表的方式,通過哈希函數(shù)將鍵映射到哈希表的某個(gè)位置(哈希桶)上,從而在常數(shù)時(shí)間內(nèi)完成讀寫操作。
- 然而,需要明確的是,這個(gè)O(1)的復(fù)雜度是基于平均情況或假設(shè)哈希函數(shù)分布均勻的前提下的。在實(shí)際應(yīng)用中,如果哈希函數(shù)設(shè)計(jì)不當(dāng)或發(fā)生了大量的哈希沖突,那么這些操作的時(shí)間復(fù)雜度可能會(huì)受到影響,甚至退化為O(n),其中n是map中元素的數(shù)量。但在正常、合理的使用場(chǎng)景下,這種極端情況是非常罕見的。
可以對(duì)map里面的一個(gè)元素取地址嗎
在Go語言中,你不能直接對(duì)map中的元素取地址,因?yàn)閙ap的元素并不是固定的內(nèi)存位置。當(dāng)你從map中獲取一個(gè)元素的值時(shí),你實(shí)際上得到的是該值的一個(gè)副本,而不是它的實(shí)際存儲(chǔ)位置的引用。這意味著,即使你嘗試獲取這個(gè)值的地址,你也只是得到了這個(gè)副本的地址,而不是map中原始元素的地址。
例如,考慮以下代碼:
m := make(map[string]int)
m["key"] = 42
value := m["key"]
fmt.Println(&value) // 打印的是value變量的地址,而不是map中元素的地址
在這個(gè)例子中,<font style="color:rgb(5, 7, 59);">&value</font> 是變量 <font style="color:rgb(5, 7, 59);">value</font> 的地址,它包含了從map中檢索出來的值的副本。如果你修改了 <font style="color:rgb(5, 7, 59);">value</font>,map中的原始值是不會(huì)改變的。
如果你需要修改map中的值,你應(yīng)該直接通過map的鍵來設(shè)置新的值:
m["key"] = newValue
這樣,你就會(huì)直接修改map中存儲(chǔ)的值,而不是修改一個(gè)副本。
如果你確實(shí)需要引用map中的值,并且希望這個(gè)引用能夠反映map中值的改變,你可以使用指針類型的值作為map的元素。這樣,你就可以存儲(chǔ)和修改指向?qū)嶋H數(shù)據(jù)的指針了。例如:
m := make(map[string]*int)
m["key"] = new(int)
*m["key"] = 42
fmt.Println(*m["key"]) // 輸出42
在這個(gè)例子中,map的值是指向int的指針,所以你可以通過指針來修改map中的實(shí)際值。
sync.map
sync.Map 是 Go 語言標(biāo)準(zhǔn)庫中提供的并發(fā)安全的 Map 類型,它適用于讀多寫少的場(chǎng)景。以下是 sync.Map 的一些關(guān)鍵原理:
- 讀寫分離:
sync.Map通過讀寫分離來提升性能。它內(nèi)部維護(hù)了兩種數(shù)據(jù)結(jié)構(gòu):一個(gè)只讀的只讀字典 (read),一個(gè)讀寫字典 (dirty)。讀操作優(yōu)先訪問只讀字典,只有在只讀字典中找不到數(shù)據(jù)時(shí)才會(huì)訪問讀寫字典。 - 延遲寫入:寫操作并不立即更新只讀字典(
read),而是更新讀寫字典 (dirty)。只有在讀操作發(fā)現(xiàn)只讀字典的數(shù)據(jù)過時(shí)(即misses計(jì)數(shù)器超過閾值)時(shí),才會(huì)將讀寫字典中的數(shù)據(jù)同步到只讀字典。這種策略減少了寫操作對(duì)讀操作的影響。 - 原子操作:讀操作大部分是無鎖的,因?yàn)樗鼈冎饕L問只讀的
readmap,并通過原子操作 (atomic.Value) 來保護(hù)讀操作;寫操作會(huì)加鎖(使用sync.Mutex)保護(hù)寫操作,以確保對(duì)dirtymap 的并發(fā)安全 ,確保高并發(fā)環(huán)境下的安全性。 - 條目淘汰:當(dāng)一個(gè)條目被刪除時(shí),它只從讀寫字典中刪除。只有在下一次數(shù)據(jù)同步時(shí),該條目才會(huì)從只讀字典中刪除。
通過這種設(shè)計(jì),sync.Map 在讀多寫少的場(chǎng)景下能夠提供較高的性能,同時(shí)保證并發(fā)安全。
sync.map的鎖機(jī)制跟你自己用鎖加上map有區(qū)別么
sync.Map 的鎖機(jī)制和自己使用鎖(如 sync.Mutex 或 sync.RWMutex)加上 map 的方式有一些關(guān)鍵區(qū)別:
自己使用鎖和 map
- 全局鎖:
- 你需要自己管理鎖,通常是一個(gè)全局的
sync.Mutex或sync.RWMutex。 - 對(duì)于讀多寫少的場(chǎng)景,使用
sync.RWMutex可以允許多個(gè)讀操作同時(shí)進(jìn)行,但寫操作依然會(huì)阻塞所有讀操作。
- 你需要自己管理鎖,通常是一個(gè)全局的
- 手動(dòng)處理:
- 你需要自己編寫代碼來處理加鎖、解鎖、讀寫操作。
- 錯(cuò)誤使用鎖可能導(dǎo)致死鎖、競(jìng)態(tài)條件等問題。
- 簡(jiǎn)單直觀:
- 實(shí)現(xiàn)簡(jiǎn)單,容易理解和調(diào)試。
**<u>sync.Map</u>**
- 讀寫分離:
sync.Map內(nèi)部使用讀寫分離的策略,通過只讀和讀寫兩個(gè) map 提高讀操作的性能。- 讀操作大部分情況下是無鎖的,只有在只讀 map 中找不到數(shù)據(jù)時(shí),才會(huì)加鎖訪問讀寫 map。
- 延遲寫入:
- 寫操作更新讀寫 map(
dirty),但不會(huì)立即更新只讀 map(read)。只有當(dāng)讀操作發(fā)現(xiàn)只讀 map 中的數(shù)據(jù)過時(shí)時(shí),才會(huì)將讀寫 map 的數(shù)據(jù)同步到只讀 map 中。
- 寫操作更新讀寫 map(
- 內(nèi)置優(yōu)化:
sync.Map內(nèi)部有各種優(yōu)化措施,如原子操作、延遲寫入等,使得它在讀多寫少的場(chǎng)景下性能更高。
區(qū)別總結(jié)
- 并發(fā)性能:
sync.Map通過讀寫分離和延遲寫入在讀多寫少的場(chǎng)景下提供更高的并發(fā)性能,而使用全局鎖的 map 在讀寫頻繁時(shí)性能較低。 - 復(fù)雜性和易用性:
sync.Map封裝了復(fù)雜的并發(fā)控制邏輯,使用起來更簡(jiǎn)單,而自己管理鎖和 map 需要處理更多的并發(fā)控制細(xì)節(jié)。 - 適用場(chǎng)景:
**<font style="color:#DF2A3F;">sync.Map</font>**適用于讀多寫少的場(chǎng)景,而使用全局鎖的 map 適用于讀寫操作較均衡或者對(duì)性能要求不高的場(chǎng)景。
如果你的應(yīng)用場(chǎng)景是讀多寫少且對(duì)性能要求較高,sync.Map 會(huì)是一個(gè)更好的選擇。而對(duì)于簡(jiǎn)單的并發(fā)訪問控制,使用 sync.Mutex 或 sync.RWMutex 加上 map 也可以滿足需求。
接口
Go 語言與鴨子類型的關(guān)系
總結(jié)一下,鴨子類型是一種動(dòng)態(tài)語言的風(fēng)格,在這種風(fēng)格中,一個(gè)對(duì)象有效的語義,不是由繼承自特定的類或?qū)崿F(xiàn)特定的接口,而是由它"當(dāng)前方法和屬性的集合"決定。Go 作為一種靜態(tài)語言,通過接口實(shí)現(xiàn)了 鴨子類型,實(shí)際上是 Go 的編譯器在其中作了隱匿的轉(zhuǎn)換工作。
值接收者和指針接收者的區(qū)別
方法
方法能給用戶自定義的類型添加新的行為。它和函數(shù)的區(qū)別在于方法有一個(gè)接收者,給一個(gè)函數(shù)添加一個(gè)接收者,那么它就變成了方法。接收者可以是值接收者,也可以是指針接收者。
在調(diào)用方法的時(shí)候,值類型既可以調(diào)用值接收者的方法,也可以調(diào)用指針接收者的方法;指針類型既可以調(diào)用指針接收者的方法,也可以調(diào)用值接收者的方法。
也就是說,不管方法的接收者是什么類型,該類型的值和指針都可以調(diào)用,不必嚴(yán)格符合接收者的類型。
實(shí)際上,當(dāng)類型和方法的接收者類型不同時(shí),其實(shí)是編譯器在背后做了一些工作,用一個(gè)表格來呈現(xiàn):
| - | 值接收者 | 指針接收者 |
|---|---|---|
| 值類型調(diào)用者 | 方法會(huì)使用調(diào)用者的一個(gè)副本,類似于“傳值” | 使用值的引用來調(diào)用方法,上例中,qcrao.growUp() 實(shí)際上是 (&qcrao).growUp() |
| 指針類型調(diào)用者 | 指針被解引用為值,上例中,stefno.howOld() 實(shí)際上是 (*stefno).howOld() | 實(shí)際上也是“傳值”,方法里的操作會(huì)影響到調(diào)用者,類似于指針傳參,拷貝了一份指針 |
值接收者和指針接收者
前面說過,不管接收者類型是值類型還是指針類型,都可以通過值類型或指針類型調(diào)用,這里面實(shí)際上通過語法糖起作用的。
先說結(jié)論:實(shí)現(xiàn)了接收者是值類型的方法,相當(dāng)于自動(dòng)實(shí)現(xiàn)了接收者是指針類型的方法;而實(shí)現(xiàn)了接收者是指針類型的方法,不會(huì)自動(dòng)生成對(duì)應(yīng)接收者是值類型的方法。
所以,當(dāng)實(shí)現(xiàn)了一個(gè)接收者是值類型的方法,就可以自動(dòng)生成一個(gè)接收者是對(duì)應(yīng)指針類型的方法,因?yàn)閮烧叨疾粫?huì)影響接收者。但是,當(dāng)實(shí)現(xiàn)了一個(gè)接收者是指針類型的方法,如果此時(shí)自動(dòng)生成一個(gè)接收者是值類型的方法,原本期望對(duì)接收者的改變(通過指針實(shí)現(xiàn)),現(xiàn)在無法實(shí)現(xiàn),因?yàn)橹殿愋蜁?huì)產(chǎn)生一個(gè)拷貝,不會(huì)真正影響調(diào)用者。
最后,只要記住下面這點(diǎn)就可以了:
如果實(shí)現(xiàn)了接收者是值類型的方法,會(huì)隱含地也實(shí)現(xiàn)了接收者是指針類型的方法。
兩者分別在何時(shí)使用
如果方法的接收者是值類型,無論調(diào)用者是對(duì)象還是對(duì)象指針,修改的都是對(duì)象的副本,不影響調(diào)用者;如果方法的接收者是指針類型,則調(diào)用者修改的是指針指向的對(duì)象本身。
使用指針作為方法的接收者的理由:
- 方法能夠修改接收者指向的值。
- 避免在每次調(diào)用方法時(shí)復(fù)制該值,在值的類型為大型結(jié)構(gòu)體時(shí),這樣做會(huì)更加高效。
是使用值接收者還是指針接收者,不是由該方法是否修改了調(diào)用者(也就是接收者)來決定,而是應(yīng)該基于該類型的本質(zhì)。
如果類型具備“原始的本質(zhì)”,也就是說它的成員都是由 Go 語言里內(nèi)置的原始類型,如字符串,整型值等,那就定義值接收者類型的方法。像內(nèi)置的引用類型,如 slice,map,interface,channel,這些類型比較特殊,聲明他們的時(shí)候,實(shí)際上是創(chuàng)建了一個(gè) header, 對(duì)于他們也是直接定義值接收者類型的方法。這樣,調(diào)用函數(shù)時(shí),是直接 copy 了這些類型的 header,而 header 本身就是為復(fù)制設(shè)計(jì)的。
如果類型具備非原始的本質(zhì),不能被安全地復(fù)制,這種類型總是應(yīng)該被共享,那就定義指針接收者的方法。比如 go 源碼里的文件結(jié)構(gòu)體(struct File)就不應(yīng)該被復(fù)制,應(yīng)該只有一份實(shí)體。
iface 和 eface 的區(qū)別是什么
iface 和 eface 都是 Go 中描述接口的底層結(jié)構(gòu)體,區(qū)別在于 iface 描述的接口包含方法,而 eface 則是不包含任何方法的空接口:interface{}。
從源碼層面看一下:
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
link *itab
hash uint32 // copy of _type.hash. Used for type switches.
bad bool // type does not implement interface
inhash bool // has this itab been added to hash?
unused [2]byte
fun [1]uintptr // variable sized
}
iface 內(nèi)部維護(hù)兩個(gè)指針,tab 指向一個(gè) itab 實(shí)體, 它表示接口的類型以及賦給這個(gè)接口的實(shí)體類型。data 則指向接口具體的值,一般而言是一個(gè)指向堆內(nèi)存的指針。
再來仔細(xì)看一下 itab 結(jié)構(gòu)體:_type 字段描述了實(shí)體的類型,包括內(nèi)存對(duì)齊方式,大小等;inter 字段則描述了接口的類型。fun 字段放置和接口方法對(duì)應(yīng)的具體數(shù)據(jù)類型的方法地址,實(shí)現(xiàn)接口調(diào)用方法的動(dòng)態(tài)分派,一般在每次給接口賦值發(fā)生轉(zhuǎn)換時(shí)會(huì)更新此表,或者直接拿緩存的 itab。
這里只會(huì)列出實(shí)體類型和接口相關(guān)的方法,實(shí)體類型的其他方法并不會(huì)出現(xiàn)在這里。
另外,你可能會(huì)覺得奇怪,為什么 fun 數(shù)組的大小為 1,要是接口定義了多個(gè)方法可怎么辦?實(shí)際上,這里存儲(chǔ)的是第一個(gè)方法的函數(shù)指針,如果有更多的方法,在它之后的內(nèi)存空間里繼續(xù)存儲(chǔ)。從匯編角度來看,通過增加地址就能獲取到這些函數(shù)指針,沒什么影響。順便提一句,這些方法是按照函數(shù)名稱的字典序進(jìn)行排列的。
再看一下 interfacetype 類型,它描述的是接口的類型:
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
可以看到,它包裝了 _type 類型,_type 實(shí)際上是描述 Go 語言中各種數(shù)據(jù)類型的結(jié)構(gòu)體。我們注意到,這里還包含一個(gè) mhdr 字段,表示接口所定義的函數(shù)列表, pkgpath 記錄定義了接口的包名。
這里通過一張圖來看下 iface 結(jié)構(gòu)體的全貌:

接著來看一下 eface 的源碼:
type eface struct {
_type *_type
data unsafe.Pointer
}
相比 iface,eface 就比較簡(jiǎn)單了。只維護(hù)了一個(gè) _type 字段,表示空接口所承載的具體的實(shí)體類型。data 描述了具體的值。

接口的動(dòng)態(tài)類型和動(dòng)態(tài)值
從源碼里可以看到:iface包含兩個(gè)字段:tab 是接口表指針,指向類型信息;data 是數(shù)據(jù)指針,則指向具體的數(shù)據(jù)。它們分別被稱為動(dòng)態(tài)類型和動(dòng)態(tài)值。而接口值包括動(dòng)態(tài)類型和動(dòng)態(tài)值。
【引申1】接口類型和 nil 作比較
接口值的零值是指動(dòng)態(tài)類型和動(dòng)態(tài)值都為 nil。當(dāng)僅且當(dāng)這兩部分的值都為 nil 的情況下,這個(gè)接口值就才會(huì)被認(rèn)為 接口值 == nil。
編譯器自動(dòng)檢測(cè)類型是否實(shí)現(xiàn)接口
接口的構(gòu)造過程是怎樣的
類型轉(zhuǎn)換和斷言的區(qū)別
我們知道,Go 語言中不允許隱式類型轉(zhuǎn)換,也就是說 = 兩邊,不允許出現(xiàn)類型不相同的變量。
類型轉(zhuǎn)換、類型斷言本質(zhì)都是把一個(gè)類型轉(zhuǎn)換成另外一個(gè)類型。不同之處在于,類型斷言是對(duì)接口變量進(jìn)行的操作。
類型轉(zhuǎn)換
對(duì)于類型轉(zhuǎn)換而言,轉(zhuǎn)換前后的兩個(gè)類型要相互兼容才行。類型轉(zhuǎn)換的語法為:
<結(jié)果類型> := <目標(biāo)類型> ( <表達(dá)式> )
func main() {
var i int = 9
var f float64
f = float64(i)
fmt.Printf("%T, %v\n", f, f)
f = 10.8
a := int(f)
fmt.Printf("%T, %v\n", a, a)
}
斷言
前面說過,因?yàn)榭战涌?interface{} 沒有定義任何函數(shù),因此 Go 中所有類型都實(shí)現(xiàn)了空接口。當(dāng)一個(gè)函數(shù)的形參是 interface{},那么在函數(shù)中,需要對(duì)形參進(jìn)行斷言,從而得到它的真實(shí)類型。
斷言的語法為:
<目標(biāo)類型的值>,<布爾參數(shù)> := <表達(dá)式>.( 目標(biāo)類型 ) // 安全類型斷言
<目標(biāo)類型的值> := <表達(dá)式>.( 目標(biāo)類型 ) //非安全類型斷言
類型轉(zhuǎn)換和類型斷言有些相似,不同之處,在于類型斷言是對(duì)接口進(jìn)行的操作。
type Student struct {
Name string
Age int
}
func main() {
var i interface{} = new(Student)
s, ok := i.(Student)
if ok {
fmt.Println(s)
}
}
斷言其實(shí)還有另一種形式,就是用在利用 switch 語句判斷接口的類型。每一個(gè) case 會(huì)被順序地考慮。當(dāng)命中一個(gè) case 時(shí),就會(huì)執(zhí)行 case 中的語句,因此 case 語句的順序是很重要的,因?yàn)楹苡锌赡軙?huì)有多個(gè) case 匹配的情況。
接口轉(zhuǎn)換的原理
通過前面提到的 iface 的源碼可以看到,實(shí)際上它包含接口的類型 interfacetype 和 實(shí)體類型的類型 _type,這兩者都是 iface 的字段 itab 的成員。也就是說生成一個(gè) itab 同時(shí)需要接口的類型和實(shí)體的類型。
<interface 類型, 實(shí)體類型> ->itable
當(dāng)判定一種類型是否滿足某個(gè)接口時(shí),Go 使用類型的方法集和接口所需要的方法集進(jìn)行匹配,如果類型的方法集完全包含接口的方法集,則可認(rèn)為該類型實(shí)現(xiàn)了該接口。
例如某類型有 m 個(gè)方法,某接口有 n 個(gè)方法,則很容易知道這種判定的時(shí)間復(fù)雜度為 O(mn),Go 會(huì)對(duì)方法集的函數(shù)按照函數(shù)名的字典序進(jìn)行排序,所以實(shí)際的時(shí)間復(fù)雜度為 O(m+n)。
這里我們來探索將一個(gè)接口轉(zhuǎn)換給另外一個(gè)接口背后的原理,當(dāng)然,能轉(zhuǎn)換的原因必然是類型兼容。
- 具體類型轉(zhuǎn)空接口時(shí),_type 字段直接復(fù)制源類型的 _type;調(diào)用 mallocgc 獲得一塊新內(nèi)存,把值復(fù)制進(jìn)去,data 再指向這塊新內(nèi)存。
- 具體類型轉(zhuǎn)非空接口時(shí),入?yún)?tab 是編譯器在編譯階段預(yù)先生成好的,新接口 tab 字段直接指向入?yún)?tab 指向的 itab;調(diào)用 mallocgc 獲得一塊新內(nèi)存,把值復(fù)制進(jìn)去,data 再指向這塊新內(nèi)存。
- 而對(duì)于接口轉(zhuǎn)接口,itab 調(diào)用 getitab 函數(shù)獲取。只用生成一次,之后直接從 hash 表中獲取。
如何用 interface 實(shí)現(xiàn)多態(tài)
Go 語言并沒有設(shè)計(jì)諸如虛函數(shù)、純虛函數(shù)、繼承、多重繼承等概念,但它通過接口卻非常優(yōu)雅地支持了面向?qū)ο蟮奶匦浴?br> 多態(tài)是一種運(yùn)行期的行為,它有以下幾個(gè)特點(diǎn):
- 一種類型具有多種類型的能力
- 允許不同的對(duì)象對(duì)同一消息做出靈活的反應(yīng)
- 以一種通用的方式對(duì)待個(gè)使用的對(duì)象
- 非動(dòng)態(tài)語言必須通過繼承和接口的方式來實(shí)現(xiàn)
main 函數(shù)里先生成 Student 和 Programmer 的對(duì)象,再將它們分別傳入到函數(shù) whatJob 和 growUp。函數(shù)中,直接調(diào)用接口函數(shù),實(shí)際執(zhí)行的時(shí)候是看最終傳入的實(shí)體類型是什么,調(diào)用的是實(shí)體類型實(shí)現(xiàn)的函數(shù)。于是,不同對(duì)象針對(duì)同一消息就有多種表現(xiàn),多態(tài)就實(shí)現(xiàn)了。
Go 接口與 C++ 接口有何異同
接口定義了一種規(guī)范,描述了類的行為和功能,而不做具體實(shí)現(xiàn)。
C++ 的接口是使用抽象類來實(shí)現(xiàn)的,如果類中至少有一個(gè)函數(shù)被聲明為純虛函數(shù),則這個(gè)類就是抽象類。純虛函數(shù)是通過在聲明中使用 “= 0” 來指定的。例如:
class Shape
{
public:
// 純虛函數(shù)
virtual double getArea() = 0;
private:
string name; // 名稱
};
設(shè)計(jì)抽象類的目的,是為了給其他類提供一個(gè)可以繼承的適當(dāng)?shù)幕悺3橄箢惒荒鼙挥糜趯?shí)例化對(duì)象,它只能作為接口使用。
派生類需要明確地聲明它繼承自基類,并且需要實(shí)現(xiàn)基類中所有的純虛函數(shù)。
C++ 定義接口的方式稱為“侵入式”,而 Go 采用的是 “非侵入式”,不需要顯式聲明,只需要實(shí)現(xiàn)接口定義的函數(shù),編譯器自動(dòng)會(huì)識(shí)別。
C++ 和 Go 在定義接口方式上的不同,也導(dǎo)致了底層實(shí)現(xiàn)上的不同。C++ 通過虛函數(shù)表來實(shí)現(xiàn)基類調(diào)用派生類的函數(shù);而 Go 通過 itab 中的 fun 字段來實(shí)現(xiàn)接口變量調(diào)用實(shí)體類型的函數(shù)。C++ 中的虛函數(shù)表是在編譯期生成的;而 Go 的 itab 中的 fun 字段是在運(yùn)行期間動(dòng)態(tài)生成的。原因在于,Go 中實(shí)體類型可能會(huì)無意中實(shí)現(xiàn) N 多接口,很多接口并不是本來需要的,所以不能為類型實(shí)現(xiàn)的所有接口都生成一個(gè) itab, 這也是“非侵入式”帶來的影響;這在 C++ 中是不存在的,因?yàn)榕缮枰@示聲明它繼承自哪個(gè)基類。
context相關(guān)
context 結(jié)構(gòu)是什么樣的?context 使用場(chǎng)景和用途?
(難,也常常問你項(xiàng)目中怎么用,光靠記答案很難讓面試官滿意,反正有各種結(jié)合實(shí)際的問題)
參考鏈接:
go context詳解 - 卷毛狒狒 - 博客園www.rzrgm.cn/juanmaofeifei/p/14439957.html
答:Go 的 Context 的數(shù)據(jù)結(jié)構(gòu)包含 Deadline,Done,Err,Value。Deadline 方法返回一個(gè) time.Time,表示當(dāng)前 Context 應(yīng)該結(jié)束的時(shí)間,ok 則表示有結(jié)束時(shí)間,Done 方法當(dāng) Context 被取消或者超時(shí)時(shí)候返回的一個(gè) close 的 channel,告訴給 context 相關(guān)的函數(shù)要停止當(dāng)前工作然后返回了,Err 表示 context 被取消的原因,Value 方法表示 context 實(shí)現(xiàn)共享數(shù)據(jù)存儲(chǔ)的地方,是協(xié)程安全的。context 在業(yè)務(wù)中是經(jīng)常被使用的,
其主要的應(yīng)用 :
1:上下文控制,2:多個(gè) goroutine 之間的數(shù)據(jù)交互等,3:超時(shí)控制:到某個(gè)時(shí)間點(diǎn)超時(shí),過多久超時(shí)。
context在go中一般可以用來做什么?
在 Go 語言中,context 包提供了一種管理多個(gè) goroutine 之間的截止時(shí)間、取消信號(hào)和請(qǐng)求范圍數(shù)據(jù)的方法。以下是 context 常見的用途:
- 取消信號(hào):
context可以用來向多個(gè) goroutine 傳遞取消信號(hào)。當(dāng)一個(gè) goroutine 需要取消其他 goroutine 時(shí),可以調(diào)用context的CancelFunc。- 例如,在處理 HTTP 請(qǐng)求時(shí),如果客戶端關(guān)閉了連接,可以使用
context取消所有相關(guān)的后臺(tái)操作。
- 截止時(shí)間/超時(shí)控制:
context可以設(shè)置一個(gè)截止時(shí)間或超時(shí)。當(dāng)超過這個(gè)時(shí)間或超時(shí)發(fā)生時(shí),context會(huì)自動(dòng)取消操作。- 例如,在數(shù)據(jù)庫查詢或網(wǎng)絡(luò)請(qǐng)求時(shí),可以使用
context設(shè)置一個(gè)超時(shí)時(shí)間,以防止長時(shí)間的等待。
- 傳遞請(qǐng)求范圍的數(shù)據(jù):
context可以在多個(gè) goroutine 之間傳遞請(qǐng)求范圍的數(shù)據(jù),例如請(qǐng)求的唯一 ID、用戶認(rèn)證信息等。- 例如,在處理 HTTP 請(qǐng)求時(shí),可以將請(qǐng)求的元數(shù)據(jù)存儲(chǔ)在
context中,并在各個(gè)處理函數(shù)之間傳遞這些數(shù)據(jù)。
具體示例
- 創(chuàng)建帶取消功能的 context:
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
// 執(zhí)行一些操作
// 在需要取消操作時(shí)調(diào)用 cancel
cancel()
}()
select {
case <-ctx.Done():
fmt.Println("操作取消")
case result := <-someOperation():
fmt.Println("操作結(jié)果:", result)
}
- 創(chuàng)建帶超時(shí)的 context:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
select {
case <-ctx.Done():
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("操作超時(shí)")
}
case result := <-someOperation():
fmt.Println("操作結(jié)果:", result)
}
- 傳遞請(qǐng)求范圍的數(shù)據(jù):
ctx := context.WithValue(context.Background(), "requestID", "12345")
go func(ctx context.Context) {
requestID := ctx.Value("requestID").(string)
fmt.Println("處理請(qǐng)求ID:", requestID)
}(ctx)
常用函數(shù)
context.Background(): 返回一個(gè)空的Context,通常用于根Context。context.TODO(): 返回一個(gè)空的Context,用于暫時(shí)不知道該使用什么Context的情況。context.WithCancel(parent Context) (Context, CancelFunc): 創(chuàng)建一個(gè)可以取消的Context。context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc): 創(chuàng)建一個(gè)帶超時(shí)的Context。context.WithDeadline(parent Context, d time.Time) (Context, CancelFunc): 創(chuàng)建一個(gè)帶截止時(shí)間的Context。context.WithValue(parent Context, key, val interface{}) Context: 創(chuàng)建一個(gè)攜帶值的Context。
通過這些功能,context 在 Go 中為管理 goroutine 的生命周期和跨 goroutine 傳遞數(shù)據(jù)提供了便利和強(qiáng)大的支持。
channel相關(guān)
channel 是否線程安全?鎖用在什么地方?
- Golang的Channel,發(fā)送一個(gè)數(shù)據(jù)到Channel 和 從Channel接收一個(gè)數(shù)據(jù) 都是 原子性的。
- 而且Go的設(shè)計(jì)思想就是:不要通過共享內(nèi)存來通信,而是通過通信來共享內(nèi)存,前者就是傳統(tǒng)的加鎖,后者就是Channel。
- 也就是說,設(shè)計(jì)Channel的主要目的就是在多任務(wù)間傳遞數(shù)據(jù)的,這當(dāng)然是安全的
go channel 的底層實(shí)現(xiàn)原理 (數(shù)據(jù)結(jié)構(gòu))
Go面試題(五):圖解 Golang Channel 的底層原理 - 掘金
chan-地鼠文檔
數(shù)據(jù)結(jié)構(gòu)
type hchan struct {
//channel分為無緩沖和有緩沖兩種。
//對(duì)于有緩沖的channel存儲(chǔ)數(shù)據(jù),借助的是如下循環(huán)數(shù)組的結(jié)構(gòu)
qcount uint // 循環(huán)數(shù)組中的元素?cái)?shù)量
dataqsiz uint // 循環(huán)數(shù)組的長度
buf unsafe.Pointer // 指向底層循環(huán)數(shù)組的指針
elemsize uint16 //能夠收發(fā)元素的大小
closed uint32 //channel是否關(guān)閉的標(biāo)志
elemtype *_type //channel中的元素類型
//有緩沖channel內(nèi)的緩沖數(shù)組會(huì)被作為一個(gè)“環(huán)型”來使用。
//當(dāng)下標(biāo)超過數(shù)組容量后會(huì)回到第一個(gè)位置,所以需要有兩個(gè)字段記錄當(dāng)前讀和寫的下標(biāo)位置
sendx uint // 已發(fā)送元素在循環(huán)數(shù)組中的索引
recvx uint // 已接收元素在循環(huán)數(shù)組中的索引
//當(dāng)循環(huán)數(shù)組中沒有數(shù)據(jù)時(shí),收到了接收請(qǐng)求,那么接收數(shù)據(jù)的變量地址將會(huì)寫入讀等待隊(duì)列
//當(dāng)循環(huán)數(shù)組中數(shù)據(jù)已滿時(shí),收到了發(fā)送請(qǐng)求,那么發(fā)送數(shù)據(jù)的變量地址將寫入寫等待隊(duì)列
recvq waitq // 讀等待隊(duì)列
sendq waitq // 寫等待隊(duì)列
lock mutex //互斥鎖,保證讀寫channel時(shí)不存在并發(fā)競(jìng)爭(zhēng)問題
}

總結(jié)hchan結(jié)構(gòu)體的主要組成部分有四個(gè):
- 用來保存goroutine之間傳遞數(shù)據(jù)的循環(huán)鏈表。=====> buf。
- 用來記錄此循環(huán)鏈表當(dāng)前發(fā)送或接收數(shù)據(jù)的下標(biāo)值。=====> sendx和recvx。
- 用于保存向該chan發(fā)送和從改chan接收數(shù)據(jù)的goroutine的隊(duì)列。=====> sendq 和 recvq
- 保證channel寫入和讀取數(shù)據(jù)時(shí)線程安全的鎖。 =====> lock
<font style="color:rgb(18, 29, 48);">waitq</font> 是 <font style="color:rgb(18, 29, 48);">sudog</font> 的一個(gè)雙向鏈表,而 <font style="color:rgb(18, 29, 48);">sudog</font> 實(shí)際上是對(duì) goroutine 的一個(gè)封裝:
type waitq struct {
first *sudog
last *sudog
}
例如,創(chuàng)建一個(gè)容量為 6 的,元素為 int 型的 channel 數(shù)據(jù)結(jié)構(gòu)如下 :

nil、關(guān)閉的 channel、有數(shù)據(jù)的 channel,再進(jìn)行讀、寫、關(guān)閉會(huì)怎么樣?(各類變種題型,重要)
Channel讀寫特性(15字口訣)
首先,我們先復(fù)習(xí)一下Channel都有哪些特性?
- 給一個(gè) nil channel 發(fā)送數(shù)據(jù),造成永遠(yuǎn)阻塞
- 從一個(gè) nil channel 接收數(shù)據(jù),造成永遠(yuǎn)阻塞
- 給一個(gè)已經(jīng)關(guān)閉的 channel 發(fā)送數(shù)據(jù),引起 panic
- 從一個(gè)已經(jīng)關(guān)閉的 channel 接收數(shù)據(jù),如果緩沖區(qū)中為空,則返回一個(gè)零值
- 無緩沖的channel是同步的,而有緩沖的channel是非同步的
以上5個(gè)特性是死東西,也可以通過口訣來記憶:“空讀寫阻塞,寫關(guān)閉異常,讀關(guān)閉空零”。
向 channel 發(fā)送數(shù)據(jù)和從 channel 讀數(shù)據(jù)的流程是什么樣的?
發(fā)送流程:
向一個(gè)channel中寫數(shù)據(jù)簡(jiǎn)單過程如下:
- 如果等待接收隊(duì)列recvq不為空,說明緩沖區(qū)中沒有數(shù)據(jù)或者沒有緩沖區(qū),此時(shí)直接從recvq取出G,并把數(shù)據(jù)寫入,最后把該G喚醒,結(jié)束發(fā)送過程;
- 如果緩沖區(qū)中有空余位置,將數(shù)據(jù)寫入緩沖區(qū),結(jié)束發(fā)送過程;
- 如果緩沖區(qū)中沒有空余位置,將待發(fā)送數(shù)據(jù)寫入G,將當(dāng)前G加入sendq,進(jìn)入睡眠,等待被讀goroutine喚醒;
簡(jiǎn)單流程圖如下:

接收流程:
從一個(gè)channel讀數(shù)據(jù)簡(jiǎn)單過程如下:
- 如果等待發(fā)送隊(duì)列sendq不為空,且沒有緩沖區(qū),直接從sendq中取出G,把G中數(shù)據(jù)讀出,最后把G喚醒,結(jié)束讀取過程;
- 如果等待發(fā)送隊(duì)列sendq不為空,此時(shí)說明緩沖區(qū)已滿,從緩沖區(qū)中首部讀出數(shù)據(jù),把G中數(shù)據(jù)寫入緩沖區(qū)尾部,把G喚醒,結(jié)束讀取過程;
- 如果緩沖區(qū)中有數(shù)據(jù),則從緩沖區(qū)取出數(shù)據(jù),結(jié)束讀取過程;
- 將當(dāng)前goroutine加入recvq,進(jìn)入睡眠,等待被寫goroutine喚醒;
簡(jiǎn)單流程圖如下:

關(guān)閉channel
關(guān)閉channel時(shí)會(huì)把recvq中的G全部喚醒,本該寫入G的數(shù)據(jù)位置為nil。把sendq中的G全部喚醒,但這些G會(huì)panic。
除此之外,panic出現(xiàn)的常見場(chǎng)景還有:
- 關(guān)閉值為nil的channel
- 關(guān)閉已經(jīng)被關(guān)閉的channel
- 向已經(jīng)關(guān)閉的channel寫數(shù)據(jù)
講講 Go 的 chan 底層數(shù)據(jù)結(jié)構(gòu)和主要使用場(chǎng)景
答:channel 的數(shù)據(jù)結(jié)構(gòu)包含 qccount 當(dāng)前隊(duì)列中剩余元素個(gè)數(shù),dataqsiz 環(huán)形隊(duì)列長度,即可以存放的元素個(gè)數(shù),buf 環(huán)形隊(duì)列指針,elemsize 每個(gè)元素的大小,closed 標(biāo)識(shí)關(guān)閉狀態(tài),elemtype 元素類型,sendx 隊(duì)列下表,指示元素寫入時(shí)存放到隊(duì)列中的位置,recv 隊(duì)列下表,指示元素從隊(duì)列的該位置讀出。recvq 等待讀消息的 goroutine 隊(duì)列,sendq 等待寫消息的 goroutine 隊(duì)列,lock 互斥鎖,chan 不允許并發(fā)讀寫。
無緩沖和有緩沖區(qū)別: 管道沒有緩沖區(qū),從管道讀數(shù)據(jù)會(huì)阻塞,直到有協(xié)程向管道中寫入數(shù)據(jù)。同樣,向管道寫入數(shù)據(jù)也會(huì)阻塞,直到有協(xié)程從管道讀取數(shù)據(jù)。管道有緩沖區(qū)但緩沖區(qū)沒有數(shù)據(jù),從管道讀取數(shù)據(jù)也會(huì)阻塞,直到協(xié)程寫入數(shù)據(jù),如果管道滿了,寫數(shù)據(jù)也會(huì)阻塞,直到協(xié)程從緩沖區(qū)讀取數(shù)據(jù)。
channel 的一些特點(diǎn) 1)、讀寫值 nil 管道會(huì)永久阻塞 2)、關(guān)閉的管道讀數(shù)據(jù)仍然可以讀數(shù)據(jù) 3)、往關(guān)閉的管道寫數(shù)據(jù)會(huì) panic 4)、關(guān)閉為 nil 的管道 panic 5)、關(guān)閉已經(jīng)關(guān)閉的管道 panic
向 channel 寫數(shù)據(jù)的流程: 如果等待接收隊(duì)列 recvq 不為空,說明緩沖區(qū)中沒有數(shù)據(jù)或者沒有緩沖區(qū),此時(shí)直接從 recvq 取出 G,并把數(shù)據(jù)寫入,最后把該 G 喚醒,結(jié)束發(fā)送過程; 如果緩沖區(qū)中有空余位置,將數(shù)據(jù)寫入緩沖區(qū),結(jié)束發(fā)送過程; 如果緩沖區(qū)中沒有空余位置,將待發(fā)送數(shù)據(jù)寫入 G,將當(dāng)前 G 加入 sendq,進(jìn)入睡眠,等待被讀 goroutine 喚醒;
向 channel 讀數(shù)據(jù)的流程: 如果等待發(fā)送隊(duì)列 sendq 不為空,且沒有緩沖區(qū),直接從 sendq 中取出 G,把 G 中數(shù)據(jù)讀出,最后把 G 喚醒,結(jié)束讀取過程; 如果等待發(fā)送隊(duì)列 sendq 不為空,此時(shí)說明緩沖區(qū)已滿,從緩沖區(qū)中首部讀出數(shù)據(jù),把 G 中數(shù)據(jù)寫入緩沖區(qū)尾部,把 G 喚醒,結(jié)束讀取過程; 如果緩沖區(qū)中有數(shù)據(jù),則從緩沖區(qū)取出數(shù)據(jù),結(jié)束讀取過程;將當(dāng)前 goroutine 加入 recvq,進(jìn)入睡眠,等待被寫 goroutine 喚醒;
使用場(chǎng)景: 消息傳遞、消息過濾,信號(hào)廣播,事件訂閱與廣播,請(qǐng)求、響應(yīng)轉(zhuǎn)發(fā),任務(wù)分發(fā),結(jié)果匯總,并發(fā)控制,限流,同步與異步
有緩存channel和無緩存channel
Go語言進(jìn)階--有緩存channel和無緩存channel
無緩存channel適用于數(shù)據(jù)要求同步的場(chǎng)景,而有緩存channel適用于無數(shù)據(jù)同步的場(chǎng)景。可以根據(jù)實(shí)現(xiàn)項(xiàng)目需求選擇。
channel 在什么情況下會(huì)引起資源泄漏
Channel 可能會(huì)引發(fā) goroutine 泄漏。
泄漏的原因是 goroutine 操作 channel 后,處于發(fā)送或接收阻塞狀態(tài),而 channel 處于滿或空的狀態(tài),一直得不到改變。同時(shí),垃圾回收器也不會(huì)回收此類資源,進(jìn)而導(dǎo)致 gouroutine 會(huì)一直處于等待隊(duì)列中,不見天日。
另外,程序運(yùn)行過程中,對(duì)于一個(gè) channel,如果沒有任何 goroutine 引用了,gc 會(huì)對(duì)其進(jìn)行回收操作,不會(huì)引起內(nèi)存泄漏。
GMP相關(guān)
進(jìn)程、線程、協(xié)程有什么區(qū)別?(必問)
進(jìn)程:是應(yīng)用程序的啟動(dòng)實(shí)例,每個(gè)進(jìn)程都有獨(dú)立的內(nèi)存空間,不同的進(jìn)程通過進(jìn)程間的通信方式來通信。
線程:從屬于進(jìn)程,每個(gè)進(jìn)程至少包含一個(gè)線程,線程是 CPU 調(diào)度的基本單位,多個(gè)線程之間可以共享進(jìn)程的資源并通過共享內(nèi)存等線程間的通信方式來通信。
協(xié)程:為輕量級(jí)線程,與線程相比,協(xié)程不受操作系統(tǒng)的調(diào)度,協(xié)程的調(diào)度器由用戶應(yīng)用程序提供,協(xié)程調(diào)度器按照調(diào)度策略把協(xié)程調(diào)度到線程中運(yùn)行
什么是 GMP?(必問)
答:G 代表著 goroutine,P 代表著上下文處理器,M 代表 thread 線程,
在 GPM 模型,有一個(gè)全局隊(duì)列(Global Queue):存放等待運(yùn)行的 G,還有一個(gè) P 的本地隊(duì)列:也是存放等待運(yùn)行的 G,但數(shù)量有限,不超過 256 個(gè)。
調(diào)度流程:
- 創(chuàng)建 Goroutine:
- 當(dāng)通過
go func()創(chuàng)建新的 Goroutine 時(shí),G 會(huì)首先被加入到與當(dāng)前 P 關(guān)聯(lián)的本地隊(duì)列中。 - 如果 P 的本地隊(duì)列已滿(超過 256 個(gè) G),則新的 G 會(huì)被放入全局隊(duì)列。
- 當(dāng)通過
- 調(diào)度與執(zhí)行:
- 每個(gè) M 與一個(gè) P 綁定,M 從 P 的本地隊(duì)列中獲取一個(gè) G 來執(zhí)行。
- 如果 P 的本地隊(duì)列為空,M 會(huì)嘗試從全局隊(duì)列或其他 P 的本地隊(duì)列中偷取(work stealing)任務(wù)執(zhí)行。
- 系統(tǒng)調(diào)用與阻塞:
- 當(dāng) G 執(zhí)行過程中發(fā)生阻塞或系統(tǒng)調(diào)用,M 也會(huì)被阻塞。這時(shí),P 會(huì)解綁當(dāng)前的 M,并嘗試尋找或創(chuàng)建新的 M 來繼續(xù)執(zhí)行其他 G。
- 阻塞結(jié)束后,原來的 M 會(huì)嘗試重新綁定一個(gè) P 繼續(xù)執(zhí)行。
G,P,M 的個(gè)數(shù)問題:
G(Goroutine)的個(gè)數(shù)
- 理論上無限制:G的數(shù)量在理論上是沒有上限的,只要系統(tǒng)的內(nèi)存足夠,就可以創(chuàng)建大量的goroutine。這是因?yàn)間oroutine比線程更輕量級(jí),它們共享相同的地址空間,并且在堆上分配的內(nèi)存相對(duì)較少。
- 實(shí)際受內(nèi)存限制:盡管理論上goroutine的數(shù)量沒有限制,但實(shí)際上它們會(huì)受到系統(tǒng)可用內(nèi)存的限制。每個(gè)goroutine都需要分配一定的棧空間(盡管棧的大小可以動(dòng)態(tài)調(diào)整),而且goroutine之間共享的數(shù)據(jù)結(jié)構(gòu)(如全局變量、通道等)也會(huì)占用內(nèi)存。
P(Processor)的個(gè)數(shù)
- 通常設(shè)置為邏輯CPU數(shù)的兩倍:P的數(shù)量通常建議設(shè)置為邏輯CPU核心數(shù)的兩倍,這是為了提高調(diào)度的并行性和效率。每個(gè)P都可以綁定到一個(gè)M上執(zhí)行g(shù)oroutine,而設(shè)置更多的P可以使得在某些M阻塞時(shí),其他M仍然可以執(zhí)行P上的goroutine,從而減少等待時(shí)間。
- 由
**<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">GOMAXPROCS</font>**決定:P的實(shí)際數(shù)量由環(huán)境變量<font style="color:#DF2A3F;background-color:rgb(253, 253, 254);">GOMAXPROCS</font>(或在Go程序中通過<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">runtime.GOMAXPROCS</font>函數(shù)設(shè)置)決定。這個(gè)值限制了同時(shí)運(yùn)行的goroutine的數(shù)量,即在任何給定時(shí)間,最多只有<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">GOMAXPROCS</font>個(gè)goroutine在CPU上執(zhí)行。
M(Machine/Thread)的個(gè)數(shù)
- 動(dòng)態(tài)創(chuàng)建和銷毀:M的數(shù)量是動(dòng)態(tài)變化的,Go運(yùn)行時(shí)根據(jù)需要?jiǎng)?chuàng)建和銷毀M。當(dāng)一個(gè)M上的所有g(shù)oroutine都阻塞時(shí),該M可能會(huì)被銷毀,而當(dāng)有g(shù)oroutine等待執(zhí)行但沒有可用的M時(shí),會(huì)創(chuàng)建新的M。
- 默認(rèn)和最大限制:Go程序啟動(dòng)時(shí),會(huì)設(shè)置一個(gè)M的最大數(shù)量(默認(rèn)通常是10000,但這個(gè)值可能因Go版本和操作系統(tǒng)而異),但這個(gè)限制很少達(dá)到,因?yàn)椴僮飨到y(tǒng)本身就有線程/進(jìn)程數(shù)量的限制。此外,通過
<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">runtime/debug</font>包中的<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">SetMaxThreads</font>函數(shù)可以設(shè)置M的最大數(shù)量,但這個(gè)函數(shù)主要用于調(diào)試目的,不建議在生產(chǎn)環(huán)境中隨意更改。 - 與P的關(guān)系:M與P之間沒有絕對(duì)的固定關(guān)系。一個(gè)M可以綁定到任意P上執(zhí)行g(shù)oroutine,而當(dāng)M阻塞時(shí),它會(huì)釋放其綁定的P,P隨后會(huì)嘗試綁定到其他空閑的M上。因此,即使P的數(shù)量較少,也可能因?yàn)楣ぷ髁扛`取和M的動(dòng)態(tài)創(chuàng)建而有大量的M存在(盡管這些M中的大多數(shù)可能在等待中)。
關(guān)鍵機(jī)制
work stealing(工作量竊取) 機(jī)制:會(huì)優(yōu)先從全局隊(duì)列里進(jìn)行竊取,之后會(huì)從其它的P隊(duì)列里竊取一半的G,放入到本地P隊(duì)列里。
hand off (移交)機(jī)制:M 被阻塞時(shí),P 會(huì)被移交給其他空閑的 M,或者創(chuàng)建新的 M 來執(zhí)行任務(wù)。
為什么要有 P?
帶來什么改變
加了 P 之后會(huì)帶來什么改變呢?我們?cè)俑@式的講一下。
- 每個(gè) P 有自己的本地隊(duì)列,大幅度的減輕了對(duì)全局隊(duì)列的直接依賴,所帶來的效果就是鎖競(jìng)爭(zhēng)的減少。而 GM 模型的性能開銷大頭就是鎖競(jìng)爭(zhēng)。
- 每個(gè) P 相對(duì)的平衡上,在 GMP 模型中也實(shí)現(xiàn)了 Work Stealing (工作量竊取機(jī)制)算法,如果 P 的本地隊(duì)列為空,則會(huì)從全局隊(duì)列或其他 P 的本地隊(duì)列中竊取可運(yùn)行的 G 來運(yùn)行,減少空轉(zhuǎn),提高了資源利用率。
為什么要有 P
這時(shí)候就有小伙伴會(huì)疑惑了,如果是想實(shí)現(xiàn)本地隊(duì)列、Work Stealing 算法,那為什么不直接在 M 上加呢,M 也照樣可以實(shí)現(xiàn)類似的組件。為什么又再加多一個(gè) P 組件?
結(jié)合 M(系統(tǒng)線程) 的定位來看,若這么做,有以下問題:
- 一般來講,M 的數(shù)量都會(huì)多于 P。像在 Go 中,M 的數(shù)量默認(rèn)是 10000,P 的默認(rèn)數(shù)量的 CPU 核數(shù)。另外由于 M 的屬性,也就是如果存在系統(tǒng)阻塞調(diào)用,阻塞了 M,又不夠用的情況下,M 會(huì)不斷增加。
- M 不斷增加的話,如果本地隊(duì)列掛載在 M 上,那就意味著本地隊(duì)列也會(huì)隨之增加。這顯然是不合理的,因?yàn)楸镜仃?duì)列的管理會(huì)變得復(fù)雜,且 Work Stealing 性能會(huì)大幅度下降。
- M 被系統(tǒng)調(diào)用阻塞后,我們是期望把他既有未執(zhí)行的任務(wù)分配給其他繼續(xù)運(yùn)行的,而不是一阻塞就導(dǎo)致全部停止。
因此使用 M 是不合理的,那么引入新的組件 P,把本地隊(duì)列關(guān)聯(lián)到 P 上,就能很好的解決這個(gè)問題。
調(diào)度器的設(shè)計(jì)策略
復(fù)用線程:避免頻繁的創(chuàng)建、銷毀線程,而是對(duì)線程的復(fù)用。
1)work stealing(工作量竊取)機(jī)制
當(dāng)本線程無可運(yùn)行的G時(shí),嘗試從其他線程綁定的P偷取G,而不是銷毀線程。
2)hand off(移交)機(jī)制
當(dāng)本線程因?yàn)镚進(jìn)行系統(tǒng)調(diào)用阻塞時(shí),線程釋放綁定的P,把P轉(zhuǎn)移給其他空閑的線程執(zhí)行。
利用并行:GOMAXPROCS設(shè)置P的數(shù)量,最多有GOMAXPROCS個(gè)線程分布在多個(gè)CPU上同時(shí)運(yùn)行。GOMAXPROCS也限制了并發(fā)的程度,比如GOMAXPROCS = 核數(shù)/2,則最多利用了一半的CPU核進(jìn)行并行。
搶占:在coroutine中要等待一個(gè)協(xié)程主動(dòng)讓出CPU才執(zhí)行下一個(gè)協(xié)程,在Go中,一個(gè)goroutine最多占用CPU 10ms,防止其他goroutine被餓死,這就是goroutine不同于coroutine的一個(gè)地方。
全局G隊(duì)列:在新的調(diào)度器中依然有全局G隊(duì)列,但功能已經(jīng)被弱化了,當(dāng)M執(zhí)行work stealing從其他P偷不到G時(shí),它可以從全局G隊(duì)列獲取G。
搶占式調(diào)度是如何搶占的?
基于協(xié)作式搶占
基于信號(hào)量搶占
就像操作系統(tǒng)要負(fù)責(zé)線程的調(diào)度一樣,Go的runtime要負(fù)責(zé)goroutine的調(diào)度。現(xiàn)代操作系統(tǒng)調(diào)度線程都是搶占式的,我們不能依賴用戶代碼主動(dòng)讓出CPU,或者因?yàn)镮O、鎖等待而讓出,這樣會(huì)造成調(diào)度的不公平。基于經(jīng)典的時(shí)間片算法,當(dāng)線程的時(shí)間片用完之后,會(huì)被時(shí)鐘中斷給打斷,調(diào)度器會(huì)將當(dāng)前線程的執(zhí)行上下文進(jìn)行保存,然后恢復(fù)下一個(gè)線程的上下文,分配新的時(shí)間片令其開始執(zhí)行。這種搶占對(duì)于線程本身是無感知的,系統(tǒng)底層支持,不需要開發(fā)人員特殊處理。
基于時(shí)間片的搶占式調(diào)度有個(gè)明顯的優(yōu)點(diǎn),能夠避免CPU資源持續(xù)被少數(shù)線程占用,從而使其他線程長時(shí)間處于饑餓狀態(tài)。goroutine的調(diào)度器也用到了時(shí)間片算法,但是和操作系統(tǒng)的線程調(diào)度還是有些區(qū)別的,因?yàn)檎麄€(gè)Go程序都是運(yùn)行在用戶態(tài)的,所以不能像操作系統(tǒng)那樣利用時(shí)鐘中斷來打斷運(yùn)行中的goroutine。也得益于完全在用戶態(tài)實(shí)現(xiàn),goroutine的調(diào)度切換更加輕量。
上面這兩段文字只是對(duì)調(diào)度的一個(gè)概括,具體的協(xié)作式調(diào)度、信號(hào)量調(diào)度大家還需要去詳細(xì)了解,這偏底層了,大廠或者中高級(jí)開發(fā)會(huì)問。(字節(jié)就問了)
調(diào)度器的生命周期

特殊的M0和G0
M0
M0是啟動(dòng)程序后的編號(hào)為0的主線程,這個(gè)M對(duì)應(yīng)的實(shí)例會(huì)在全局變量runtime.m0中,不需要在heap上分配,M0負(fù)責(zé)執(zhí)行初始化操作和啟動(dòng)第一個(gè)G, 在之后M0就和其他的M一樣了。
G0
G0是每次啟動(dòng)一個(gè)M都會(huì)第一個(gè)創(chuàng)建的goroutine,G0僅用于負(fù)責(zé)調(diào)度的G,G0不指向任何可執(zhí)行的函數(shù), 每個(gè)M都會(huì)有一個(gè)自己的G0。在調(diào)度或系統(tǒng)調(diào)用時(shí)會(huì)使用G0的棧空間, 全局變量的G0是M0的G0。
我們來跟蹤一段代碼
package main
import "fmt"
func main() {
fmt.Println("Hello world")
}
接下來我們來針對(duì)上面的代碼對(duì)調(diào)度器里面的結(jié)構(gòu)做一個(gè)分析。
也會(huì)經(jīng)歷如上圖所示的過程:
- runtime創(chuàng)建最初的線程m0和goroutine g0,并把2者關(guān)聯(lián)。
- 調(diào)度器初始化:初始化m0、棧、垃圾回收,以及創(chuàng)建和初始化由GOMAXPROCS個(gè)P構(gòu)成的P列表。
- 示例代碼中的main函數(shù)是main.main,runtime中也有1個(gè)main函數(shù)——runtime.main,代碼經(jīng)過編譯后,runtime.main會(huì)調(diào)用main.main,程序啟動(dòng)時(shí)會(huì)為runtime.main創(chuàng)建goroutine,稱它為main goroutine吧,然后把main goroutine加入到P的本地隊(duì)列。
- 啟動(dòng)m0,m0已經(jīng)綁定了P,會(huì)從P的本地隊(duì)列獲取G,獲取到main goroutine。
- G擁有棧,M根據(jù)G中的棧信息和調(diào)度信息設(shè)置運(yùn)行環(huán)境
- M運(yùn)行G
- G退出,再次回到M獲取可運(yùn)行的G,這樣重復(fù)下去,直到main.main退出,runtime.main執(zhí)行Defer和Panic處理,或調(diào)用runtime.exit退出程序。
調(diào)度器的生命周期幾乎占滿了一個(gè)Go程序的一生,runtime.main的goroutine執(zhí)行之前都是為調(diào)度器做準(zhǔn)備工作,runtime.main的goroutine運(yùn)行,才是調(diào)度器的真正開始,直到runtime.main結(jié)束而結(jié)束。
鎖相關(guān)
除了 mutex 以外還有那些方式安全讀寫共享變量?
- 將共享變量的讀寫放到一個(gè) goroutine 中,其它 goroutine 通過 channel 進(jìn)行讀寫操作。
- 可以用個(gè)數(shù)為 1 的信號(hào)量(semaphore)實(shí)現(xiàn)互斥
- 通過 Mutex 鎖實(shí)現(xiàn)
Go 如何實(shí)現(xiàn)原子操作?
答:原子操作就是不可中斷的操作,外界是看不到原子操作的中間狀態(tài),要么看到原子操作已經(jīng)完成,要么看到原子操作已經(jīng)結(jié)束。在某個(gè)值的原子操作執(zhí)行的過程中,CPU 絕對(duì)不會(huì)再去執(zhí)行其他針對(duì)該值的操作,那么其他操作也是原子操作。
Go 語言的標(biāo)準(zhǔn)庫代碼包 sync/atomic 提供了原子的讀取(Load 為前綴的函數(shù))或?qū)懭耄⊿tore 為前綴的函數(shù))某個(gè)值(這里細(xì)節(jié)還要多去查查資料)。
原子操作與互斥鎖的區(qū)別
1)、互斥鎖是一種數(shù)據(jù)結(jié)構(gòu),用來讓一個(gè)線程執(zhí)行程序的關(guān)鍵部分,完成互斥的多個(gè)操作。
2)、原子操作是針對(duì)某個(gè)值的單個(gè)互斥操作。
Mutex 是悲觀鎖還是樂觀鎖?悲觀鎖、樂觀鎖是什么?
悲觀鎖
悲觀鎖:當(dāng)要對(duì)數(shù)據(jù)庫中的一條數(shù)據(jù)進(jìn)行修改的時(shí)候,為了避免同時(shí)被其他人修改,最好的辦法就是直接對(duì)該數(shù)據(jù)進(jìn)行加鎖以防止并發(fā)。這種借助數(shù)據(jù)庫鎖機(jī)制,在修改數(shù)據(jù)之前先鎖定,再修改的方式被稱之為悲觀并發(fā)控制【Pessimistic Concurrency Control,縮寫“PCC”,又名“悲觀鎖”】。
樂觀鎖
樂觀鎖是相對(duì)悲觀鎖而言的,樂觀鎖假設(shè)數(shù)據(jù)一般情況不會(huì)造成沖突,所以在數(shù)據(jù)進(jìn)行提交更新的時(shí)候,才會(huì)正式對(duì)數(shù)據(jù)的沖突與否進(jìn)行檢測(cè),如果沖突,則返回給用戶異常信息,讓用戶決定如何去做。樂觀鎖適用于讀多寫少的場(chǎng)景,這樣可以提高程序的吞吐量
Mutex 有幾種模式?
1)正常模式
- 當(dāng)前的mutex只有一個(gè)goruntine來獲取,那么沒有競(jìng)爭(zhēng),直接返回。
- 新的goruntine進(jìn)來,如果當(dāng)前mutex已經(jīng)被獲取了,則該goruntine進(jìn)入一個(gè)先入先出的waiter隊(duì)列,在mutex被釋放后,waiter按照先進(jìn)先出的方式獲取鎖。該goruntine會(huì)處于自旋狀態(tài)(不掛起,繼續(xù)占有cpu)。
- 新的goruntine進(jìn)來,mutex處于空閑狀態(tài),將參與競(jìng)爭(zhēng)。新來的 goroutine 有先天的優(yōu)勢(shì),它們正在 CPU 中運(yùn)行,可能它們的數(shù)量還不少,所以,在高并發(fā)情況下,被喚醒的 waiter 可能比較悲劇地獲取不到鎖,這時(shí),它會(huì)被插入到隊(duì)列的前面。如果 waiter 獲取不到鎖的時(shí)間超過閾值 1 毫秒,那么,這個(gè) Mutex 就進(jìn)入到了饑餓模式。
2)饑餓模式
在饑餓模式下,Mutex 的擁有者將直接把鎖交給隊(duì)列最前面的 waiter。新來的 goroutine 不會(huì)嘗試獲取鎖,即使看起來鎖沒有被持有,它也不會(huì)去搶,也不會(huì) spin(自旋),它會(huì)乖乖地加入到等待隊(duì)列的尾部。 如果擁有 Mutex 的 waiter 發(fā)現(xiàn)下面兩種情況的其中之一,它就會(huì)把這個(gè) Mutex 轉(zhuǎn)換成正常模式:
- 此 waiter 已經(jīng)是隊(duì)列中的最后一個(gè) waiter 了,沒有其它的等待鎖的 goroutine 了;
- 此 waiter 的等待時(shí)間小于 1 毫秒。
sync.Mutex
<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync.Mutex</font> 是 Go 語言標(biāo)準(zhǔn)庫 <font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync</font> 包中的一個(gè)互斥鎖類型,用于在多個(gè) goroutine 之間同步對(duì)共享資源的訪問。當(dāng)多個(gè) goroutine 需要訪問同一個(gè)資源時(shí),使用 <font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync.Mutex</font> 可以確保在任何時(shí)刻只有一個(gè) goroutine 能夠訪問該資源,從而避免數(shù)據(jù)競(jìng)爭(zhēng)和不一致性的問題。
主要特點(diǎn)
- 互斥性:在任何時(shí)刻,只有一個(gè) goroutine 可以持有
<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync.Mutex</font>的鎖。如果多個(gè) goroutine 嘗試同時(shí)獲取鎖,那么除了第一個(gè)成功獲取鎖的 goroutine 之外,其他 goroutine 將被阻塞,直到鎖被釋放。 - 非重入性:如果一個(gè) goroutine 已經(jīng)持有了
<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync.Mutex</font>的鎖,那么它不能再次請(qǐng)求這個(gè)鎖,這會(huì)導(dǎo)致死鎖。
方法
<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync.Mutex</font> 提供了兩個(gè)主要方法:
<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">Lock()</font>:嘗試獲取鎖。如果鎖已經(jīng)被其他 goroutine 持有,則調(diào)用者將阻塞,直到鎖被釋放。<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">Unlock()</font>:釋放鎖。調(diào)用此方法之前必須先成功調(diào)用<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">Lock()</font>。如果在一個(gè)沒有鎖的<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync.Mutex</font>上調(diào)用<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">Unlock()</font>,將會(huì)導(dǎo)致 panic。
使用場(chǎng)景
<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync.Mutex</font> 適用于需要嚴(yán)格互斥訪問共享資源的場(chǎng)景。例如,在并發(fā)編程中,如果有多個(gè) goroutine 需要修改同一個(gè)數(shù)據(jù)結(jié)構(gòu)或訪問同一個(gè)文件,就應(yīng)該使用 <font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync.Mutex</font> 來確保操作的原子性和數(shù)據(jù)的一致性。
sync.RWMutex
<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync.RWMutex</font> 是 Go 語言標(biāo)準(zhǔn)庫 <font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync</font> 包中的一個(gè)類型,它實(shí)現(xiàn)了讀寫互斥鎖(Reader-Writer Mutex)。與普通的互斥鎖(如 <font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync.Mutex</font>)相比,<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync.RWMutex</font> 允許多個(gè)讀操作同時(shí)進(jìn)行,但寫操作會(huì)完全互斥。這意味著在任何時(shí)刻,可以有多個(gè) goroutine 同時(shí)讀取某個(gè)資源,但寫入資源時(shí),必須保證沒有其他 goroutine 在讀取或?qū)懭朐撡Y源。
主要特點(diǎn)
- 多個(gè)讀者,單一寫者:允許多個(gè)讀操作并發(fā)執(zhí)行,但寫操作會(huì)阻塞所有其他讀寫操作。
- 優(yōu)化讀性能:通過允許多個(gè)讀操作同時(shí)進(jìn)行,提高了讀操作的并發(fā)性能。
- 寫操作獨(dú)占性:寫操作在執(zhí)行時(shí)會(huì)阻止所有其他讀寫操作,確保數(shù)據(jù)的一致性和完整性。
方法
<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync.RWMutex</font> 提供了以下主要方法:
<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">Lock()</font>:加寫鎖。如果鎖已被其他 goroutine 獲取(無論是讀鎖還是寫鎖),則調(diào)用者將阻塞,直到鎖被釋放。<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">Unlock()</font>:釋放寫鎖。調(diào)用此方法之前必須先成功調(diào)用<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">Lock()</font>。<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">RLock()</font>:加讀鎖。如果鎖已被其他 goroutine 獲取為寫鎖,則調(diào)用者將阻塞,但如果有其他 goroutine 持有讀鎖,則調(diào)用者可以立即獲取讀鎖。<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">RUnlock()</font>:釋放讀鎖。調(diào)用此方法之前必須先成功調(diào)用<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">RLock()</font>。
使用場(chǎng)景
<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync.RWMutex</font> 適用于讀多寫少的場(chǎng)景,可以顯著提高程序的并發(fā)性能。例如,在緩存系統(tǒng)、配置管理系統(tǒng)等場(chǎng)景中,讀操作遠(yuǎn)多于寫操作,使用 <font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">sync.RWMutex</font> 可以在保證數(shù)據(jù)一致性的同時(shí),提高讀操作的并發(fā)性。
什么是自旋鎖
自旋鎖是指當(dāng)一個(gè)線程(在 Go 中是 Goroutine)在獲取鎖的時(shí)候,如果鎖已經(jīng)被其他線程獲取,那么該線程將循環(huán)等待(自旋),不斷判斷鎖是否已經(jīng)被釋放,而不是進(jìn)入睡眠狀態(tài)。這種行為在某些情況下可能會(huì)導(dǎo)致資源的過度占用,特別是當(dāng)鎖持有時(shí)間較長或者自旋的 Goroutine 數(shù)量較多時(shí)。
自旋鎖的核心思想是,如果預(yù)期鎖很快就會(huì)被釋放(即鎖持有時(shí)間很短),那么讓線程持續(xù)運(yùn)行并檢查鎖的狀態(tài),而不是進(jìn)入睡眠和喚醒的昂貴操作,可能會(huì)更加高效。然而,如果鎖被長時(shí)間持有,或者多個(gè)線程同時(shí)競(jìng)爭(zhēng)鎖,自旋鎖可能會(huì)導(dǎo)致大量的CPU時(shí)間被浪費(fèi)在無效的循環(huán)等待上,這種情況稱為“自旋”。
在Go語言中,雖然標(biāo)準(zhǔn)庫中沒有直接提供自旋鎖的實(shí)現(xiàn),但開發(fā)者可以通過原子操作和其他同步原語來實(shí)現(xiàn)自定義的自旋鎖。然而,由于自旋鎖可能導(dǎo)致CPU資源的過度占用,因此在決定使用自旋鎖之前,應(yīng)該仔細(xì)考慮其適用性和潛在的性能影響。在許多情況下,使用互斥鎖或其他更高級(jí)的同步機(jī)制可能是更好的選擇。
go里面怎么實(shí)現(xiàn)一個(gè)自旋鎖
在Go語言中,實(shí)現(xiàn)一個(gè)自旋鎖通常涉及使用原子操作來確保對(duì)鎖狀態(tài)的并發(fā)訪問是安全的。下面是一個(gè)簡(jiǎn)單的自旋鎖實(shí)現(xiàn)的例子:
package main
import (
"sync/atomic"
"time"
)
type Spinlock struct {
locked int32
}
func (s *Spinlock) Lock() {
for !atomic.CompareAndSwapInt32(&s.locked, 0, 1) {
// 這里可以添加一些退避策略,比如隨機(jī)等待一段時(shí)間,以避免過多的CPU占用
// time.Sleep(time.Nanosecond) // 注意:實(shí)際使用中可能不需要或想要這樣的退避
}
}
func (s *Spinlock) Unlock() {
atomic.StoreInt32(&s.locked, 0)
}
func main() {
var lock Spinlock
// 示例:使用自旋鎖
go func() {
lock.Lock()
// 執(zhí)行一些操作...
lock.Unlock()
}()
// 在另一個(gè)goroutine中嘗試獲取鎖
go func() {
lock.Lock()
// 執(zhí)行一些操作...
lock.Unlock()
}()
// 等待足夠的時(shí)間以確保goroutines完成
time.Sleep(time.Second)
}
在這個(gè)例子中,
Spinlock結(jié)構(gòu)體有一個(gè)int32類型的字段locked,用于表示鎖的狀態(tài)。Lock方法使用atomic.CompareAndSwapInt32原子操作來嘗試將locked從0(未鎖定)更改為1(已鎖定)。如果鎖已經(jīng)被另一個(gè)goroutine持有(即locked為1),則CompareAndSwapInt32會(huì)返回false,并且循環(huán)會(huì)繼續(xù)。Unlock方法使用atomic.StoreInt32原子操作將locked設(shè)置回0,表示鎖已被釋放。
需要注意的是,在實(shí)際應(yīng)用中,自旋鎖可能會(huì)導(dǎo)致CPU資源的過度占用,特別是在鎖被長時(shí)間持有或存在大量競(jìng)爭(zhēng)的情況下。因此,在使用自旋鎖之前,應(yīng)該仔細(xì)考慮其適用性和潛在的性能影響。在許多情況下,使用互斥鎖(sync.Mutex)或其他更高級(jí)的同步機(jī)制可能是更好的選擇。
什么情況下會(huì)更改失敗
在自旋鎖的實(shí)現(xiàn)中,更改失敗通常指的是嘗試獲取鎖時(shí)未能成功將鎖的狀態(tài)從“未鎖定”更改為“已鎖定”。這種情況通常發(fā)生在以下幾種情境中:
鎖已被其他線程持有
- 當(dāng)一個(gè)線程嘗試通過自旋鎖獲取對(duì)共享資源的訪問權(quán)時(shí),如果該鎖當(dāng)前已被另一個(gè)線程持有,那么嘗試更改鎖狀態(tài)的原子操作(如
atomic.CompareAndSwap)將失敗,因?yàn)闂l件(鎖為未鎖定狀態(tài))不滿足。
競(jìng)爭(zhēng)條件
- 在多線程環(huán)境中,多個(gè)線程可能幾乎同時(shí)嘗試獲取同一個(gè)自旋鎖。由于CPU調(diào)度和線程執(zhí)行的并發(fā)性,這些嘗試可能幾乎同時(shí)發(fā)生,導(dǎo)致多個(gè)線程在鎖被釋放后立即嘗試獲取它。盡管自旋鎖設(shè)計(jì)用于快速響應(yīng)鎖狀態(tài)的更改,但在高競(jìng)爭(zhēng)條件下,仍然可能存在多個(gè)線程同時(shí)看到鎖為未鎖定狀態(tài)的情況,從而導(dǎo)致多個(gè)更改嘗試中只有一個(gè)成功。
解決方案
- 設(shè)置自旋次數(shù)限制:為了避免在鎖被長時(shí)間持有時(shí)浪費(fèi)CPU資源,可以為自旋鎖設(shè)置自旋次數(shù)的限制。一旦達(dá)到該限制,嘗試獲取鎖的線程將放棄自旋并進(jìn)入睡眠狀態(tài),等待鎖被釋放。
- 使用退避策略:在自旋期間,可以嘗試使用退避策略(如指數(shù)退避),以減少CPU的占用率并提高系統(tǒng)的整體性能。
- 考慮使用其他同步機(jī)制:如果自旋鎖不適用于特定場(chǎng)景(如鎖持有時(shí)間較長、競(jìng)爭(zhēng)激烈等),則可以考慮使用其他同步機(jī)制(如互斥鎖、讀寫鎖等)。
總之,自旋鎖更改失敗通常是由于鎖已被其他線程持有、競(jìng)爭(zhēng)條件、系統(tǒng)或硬件限制以及編程錯(cuò)誤等原因?qū)е碌摹榱私鉀Q這個(gè)問題,可以采取設(shè)置自旋次數(shù)限制、使用退避策略以及考慮使用其他同步機(jī)制等措施。
goroutine 的自旋占用資源如何解決
Goroutine 的自旋占用資源問題主要涉及到 Goroutine 在等待鎖或其他資源時(shí)的一種行為模式,即自旋鎖(spinlock)。自旋鎖是指當(dāng)一個(gè)線程(在 Go 中是 Goroutine)在獲取鎖的時(shí)候,如果鎖已經(jīng)被其他線程獲取,那么該線程將循環(huán)等待(自旋),不斷判斷鎖是否已經(jīng)被釋放,而不是進(jìn)入睡眠狀態(tài)。這種行為在某些情況下可能會(huì)導(dǎo)致資源的過度占用,特別是當(dāng)鎖持有時(shí)間較長或者自旋的 Goroutine 數(shù)量較多時(shí)。
針對(duì) Goroutine 的自旋占用資源問題,可以從以下幾個(gè)方面進(jìn)行解決或優(yōu)化:
- 減少自旋鎖的使用
評(píng)估必要性:首先評(píng)估是否真的需要使用自旋鎖。在許多情況下,互斥鎖(mutex)已經(jīng)足夠滿足需求,因?yàn)榛コ怄i在資源被占用時(shí)會(huì)讓調(diào)用者進(jìn)入睡眠狀態(tài),從而減少對(duì) CPU 的占用。
優(yōu)化鎖的設(shè)計(jì):考慮使用更高級(jí)的同步機(jī)制,如讀寫鎖(rwmutex),它允許多個(gè)讀操作同時(shí)進(jìn)行,而寫操作則是互斥的。這可以顯著減少鎖的競(jìng)爭(zhēng),從而降低自旋的需求。 - 優(yōu)化自旋鎖的實(shí)現(xiàn)
設(shè)置自旋次數(shù)限制:在自旋鎖的實(shí)現(xiàn)中加入自旋次數(shù)的限制,當(dāng)自旋達(dá)到一定次數(shù)后,如果仍未獲取到鎖,則讓 Goroutine 進(jìn)入睡眠狀態(tài)。這樣可以避免長時(shí)間的無效自旋,浪費(fèi) CPU 資源。
利用 Go 的調(diào)度器特性:Go 的調(diào)度器在檢測(cè)到 Goroutine 長時(shí)間占用 CPU 而沒有進(jìn)展時(shí),會(huì)主動(dòng)進(jìn)行搶占式調(diào)度,將 Goroutine 暫停并讓出 CPU。這可以在一定程度上緩解自旋鎖帶來的資源占用問題。 - 監(jiān)控和調(diào)整系統(tǒng)資源
監(jiān)控系統(tǒng)性能:通過工具(如 pprof、statsviz 等)監(jiān)控 Go 程序的運(yùn)行時(shí)性能,包括 CPU 使用率、內(nèi)存占用等指標(biāo)。這有助于及時(shí)發(fā)現(xiàn)和解決資源占用過高的問題。
調(diào)整 Goroutine 數(shù)量:根據(jù)系統(tǒng)的負(fù)載情況動(dòng)態(tài)調(diào)整 Goroutine 的數(shù)量。例如,在高并發(fā)場(chǎng)景下適當(dāng)增加 Goroutine 的數(shù)量以提高處理能力,但在負(fù)載降低時(shí)及時(shí)減少 Goroutine 的數(shù)量以避免資源浪費(fèi)。 - 利用 Go 的并發(fā)特性
充分利用多核 CPU:通過設(shè)置 runtime.GOMAXPROCS 來指定 Go 運(yùn)行時(shí)使用的邏輯處理器數(shù)量,使其盡可能接近或等于物理 CPU 核心數(shù),從而充分利用多核 CPU 的并行處理能力。
使用 Channel 進(jìn)行通信:Go 鼓勵(lì)使用 Channel 進(jìn)行 Goroutine 之間的通信和同步,而不是直接使用鎖。Channel 可以有效地避免死鎖和競(jìng)態(tài)條件,并且減少了鎖的使用,從而降低了資源占用的風(fēng)險(xiǎn)。
綜上所述,解決 Goroutine 的自旋占用資源問題需要從多個(gè)方面入手,包括減少自旋鎖的使用、優(yōu)化自旋鎖的實(shí)現(xiàn)、監(jiān)控和調(diào)整系統(tǒng)資源以及充分利用 Go 的并發(fā)特性等。通過這些措施的綜合應(yīng)用,可以有效地降低 Goroutine 在自旋過程中對(duì)系統(tǒng)資源的占用。
并發(fā)相關(guān)
Go 中主協(xié)程如何等待其余協(xié)程退出?
答:Go 的 sync.WaitGroup 是等待一組協(xié)程結(jié)束,sync.WaitGroup 只有 3 個(gè)方法,Add()是添加計(jì)數(shù),Done()減去一個(gè)計(jì)數(shù),Wait()阻塞直到所有的任務(wù)完成。Go 里面還能通過有緩沖的 channel 實(shí)現(xiàn)其阻塞等待一組協(xié)程結(jié)束,這個(gè)不能保證一組 goroutine 按照順序執(zhí)行,可以并發(fā)執(zhí)行協(xié)程。Go 里面能通過無緩沖的 channel 實(shí)現(xiàn)其阻塞等待一組協(xié)程結(jié)束,這個(gè)能保證一組 goroutine 按照順序執(zhí)行,但是不能并發(fā)執(zhí)行。
啰嗦一句:循環(huán)智能二面,手寫代碼部分時(shí),三個(gè)協(xié)程按交替順序打印數(shù)字,最后題目做出來了,問我代碼中Add()是什么意思,我回答的不是很清晰,這家公司就沒有然后了。Add()表示協(xié)程計(jì)數(shù),可以一次Add多個(gè),如Add(3),可以多次Add(1);然后每個(gè)子協(xié)程必須調(diào)用done(),這樣才能保證所有子協(xié)程結(jié)束,主協(xié)程才能結(jié)束。
怎么控制并發(fā)數(shù)?
第一,有緩沖通道
根據(jù)通道中沒有數(shù)據(jù)時(shí)讀取操作陷入阻塞和通道已滿時(shí)繼續(xù)寫入操作陷入阻塞的特性,正好實(shí)現(xiàn)控制并發(fā)數(shù)量。
func main() {
count := 10 // 最大支持并發(fā)
sum := 100 // 任務(wù)總數(shù)
wg := sync.WaitGroup{} //控制主協(xié)程等待所有子協(xié)程執(zhí)行完之后再退出。
c := make(chan struct{}, count) // 控制任務(wù)并發(fā)的chan
defer close(c)
for i := 0; i < sum; i++ {
wg.Add(1)
c <- struct{}{} // 作用類似于waitgroup.Add(1)
go func(j int) {
defer wg.Done()
fmt.Println(j)
<-c // 執(zhí)行完畢,釋放資源
}(i)
}
wg.Wait()
}
第二,三方庫實(shí)現(xiàn)的協(xié)程池
import (
"github.com/Jeffail/tunny"
"log"
"time"
)
func main() {
pool := tunny.NewFunc(10, func(i interface{}) interface{} {
log.Println(i)
time.Sleep(time.Second)
return nil
})
defer pool.Close()
for i := 0; i < 500; i++ {
go pool.Process(i)
}
time.Sleep(time.Second * 4)
}
多個(gè) goroutine 對(duì)同一個(gè) map 寫會(huì) panic,異常是否可以用 defer 捕獲?
可以捕獲異常,但是只能捕獲一次,Go語言,可以使用多值返回來返回錯(cuò)誤。不要用異常代替錯(cuò)誤,更不要用來控制流程。在極個(gè)別的情況下,才使用Go中引入的Exception處理:defer, panic, recover Go中,對(duì)異常處理的原則是:多用error包,少用panic
defer func() {
if err := recover(); err != nil {
// 打印異常,關(guān)閉資源,退出此函數(shù)
fmt.Println(err)
}
}()
如何優(yōu)雅的實(shí)現(xiàn)一個(gè) goroutine 池
(百度、手寫代碼,本人面?zhèn)饕艨毓杀粏柕溃赫?qǐng)求數(shù)大于消費(fèi)能力怎么設(shè)計(jì)協(xié)程池)
這一塊能啃下來,offer滿天飛,這應(yīng)該是保證高并發(fā)系統(tǒng)穩(wěn)定性、高可用的核心部分之一。
建議參考:
Golang學(xué)習(xí)篇--協(xié)程池_Word哥的博客-CSDN博客_golang協(xié)程池blog.csdn.net/finghting321/article/details/106492915/
這篇文章的目錄是:
- 為什么需要協(xié)程池?
- 簡(jiǎn)單的協(xié)程池
- go-playground/pool
- ants(推薦)
所以直接研究ants底層吧,省的造輪子。
golang實(shí)現(xiàn)多并發(fā)請(qǐng)求(發(fā)送多個(gè)get請(qǐng)求)
在go語言中其實(shí)有兩種方法進(jìn)行協(xié)程之間的通信。一個(gè)是共享內(nèi)存、一個(gè)是消息傳遞
共享內(nèi)存(互斥鎖)
//基本的GET請(qǐng)求
package main
import (
"fmt"
"io/ioutil"
"net/http"
"time"
"sync"
"runtime"
)
// 計(jì)數(shù)器
var counter int = 0
func httpget(lock *sync.Mutex){
lock.Lock()
counter++
resp, err := http.Get("http://localhost:8000/rest/api/user")
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
fmt.Println(resp.StatusCode)
if resp.StatusCode == 200 {
fmt.Println("ok")
}
lock.Unlock()
}
func main() {
start := time.Now()
lock := &sync.Mutex{}
for i := 0; i < 800; i++ {
go httpget(lock)
}
for {
lock.Lock()
c := counter
lock.Unlock()
runtime.Gosched()
if c >= 800 {
break
}
}
end := time.Now()
consume := end.Sub(start).Seconds()
fmt.Println("程序執(zhí)行耗時(shí)(s):", consume)
}
問題
我們可以看到共享內(nèi)存的方式是可以做到并發(fā),但是我們需要利用共享變量來進(jìn)行協(xié)程的通信,也就需要使用互斥鎖來確保數(shù)據(jù)安全性,導(dǎo)致代碼啰嗦,復(fù)雜話,不易維護(hù)。我們后續(xù)使用go的消息傳遞方式避免這些問題。
消息傳遞(管道)
//基本的GET請(qǐng)求
package main
import (
"fmt"
"io/ioutil"
"net/http"
"time"
)
// HTTP get請(qǐng)求
func httpget(ch chan int){
resp, err := http.Get("http://localhost:8000/rest/api/user")
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
fmt.Println(resp.StatusCode)
if resp.StatusCode == 200 {
fmt.Println("ok")
}
ch <- 1
}
// 主方法
func main() {
start := time.Now()
// 注意設(shè)置緩沖區(qū)大小要和開啟協(xié)程的個(gè)人相等
chs := make([]chan int, 2000)
for i := 0; i < 2000; i++ {
chs[i] = make(chan int)
go httpget(chs[i])
}
for _, ch := range chs {
<- ch
}
end := time.Now()
consume := end.Sub(start).Seconds()
fmt.Println("程序執(zhí)行耗時(shí)(s):", consume)
}
總結(jié):
我們通過go語言的管道channel來實(shí)現(xiàn)并發(fā)請(qǐng)求,能夠解決如何避免傳統(tǒng)共享內(nèi)存實(shí)現(xiàn)并發(fā)的很多問題而且效率會(huì)高于共享內(nèi)存的方法。
sync.pool
<font style="color:rgb(5, 7, 59);">sync.Pool</font> 是 Go 語言在標(biāo)準(zhǔn)庫 <font style="color:rgb(5, 7, 59);">sync</font> 包中提供的一個(gè)類型,它主要用于存儲(chǔ)和復(fù)用臨時(shí)對(duì)象,以減少內(nèi)存分配的開銷,提高性能。以下是對(duì) <font style="color:rgb(5, 7, 59);">sync.Pool</font> 的詳細(xì)解析:
基本概念
<font style="color:rgb(5, 7, 59);">sync.Pool</font> 是一個(gè)可以存儲(chǔ)任意類型的臨時(shí)對(duì)象的集合。當(dāng)你需要一個(gè)新的對(duì)象時(shí),可以先從 <font style="color:#DF2A3F;">sync.Pool</font> 中嘗試獲取;如果 <font style="color:#DF2A3F;">sync.Pool</font> 中有可用的對(duì)象,則直接返回該對(duì)象;如果沒有,則需要自行創(chuàng)建。使用完對(duì)象后,可以將其放回 <font style="color:#DF2A3F;">sync.Pool</font> 中,以供后續(xù)再次使用。
主要特點(diǎn)
- 減少內(nèi)存分配和垃圾回收(GC)壓力:通過復(fù)用已經(jīng)分配的對(duì)象,
<font style="color:rgb(5, 7, 59);">sync.Pool</font>可以顯著減少內(nèi)存分配的次數(shù),從而減輕 GC 的壓力,提高程序的性能。 - 并發(fā)安全:
<font style="color:rgb(5, 7, 59);">sync.Pool</font>是 Goroutine 并發(fā)安全的,多個(gè) Goroutine 可以同時(shí)從<font style="color:rgb(5, 7, 59);">sync.Pool</font>中獲取和放回對(duì)象,而無需額外的同步措施。 - 自動(dòng)清理:Go 的垃圾回收器在每次垃圾回收時(shí),都會(huì)清除
<font style="color:rgb(5, 7, 59);">sync.Pool</font>中的所有對(duì)象。因此,你不能假設(shè)一個(gè)對(duì)象被放入<font style="color:rgb(5, 7, 59);">sync.Pool</font>后就會(huì)一直存在。
使用場(chǎng)景
<font style="color:rgb(5, 7, 59);">sync.Pool</font> 適用于以下場(chǎng)景:
- 對(duì)象實(shí)例創(chuàng)建開銷較大的場(chǎng)景,如數(shù)據(jù)庫連接、大型數(shù)據(jù)結(jié)構(gòu)等。
- 需要頻繁創(chuàng)建和銷毀臨時(shí)對(duì)象的場(chǎng)景,如 HTTP 處理函數(shù)中頻繁創(chuàng)建和銷毀的請(qǐng)求上下文對(duì)象。
使用方法
- 創(chuàng)建 Pool 實(shí)例:首先,你需要?jiǎng)?chuàng)建一個(gè)
<font style="color:rgb(5, 7, 59);">sync.Pool</font>的實(shí)例,并配置<font style="color:rgb(5, 7, 59);">New</font>方法。<font style="color:rgb(5, 7, 59);">New</font>方法是一個(gè)無參函數(shù),用于在<font style="color:rgb(5, 7, 59);">sync.Pool</font>中沒有可用對(duì)象時(shí)創(chuàng)建一個(gè)新的對(duì)象。
var pool = &sync.Pool{
New: func() interface{} {
return new(YourType) // 替換 YourType 為你的類型
},
}
- 獲取對(duì)象:使用
<font style="color:rgb(5, 7, 59);">Get</font>方法從<font style="color:rgb(5, 7, 59);">sync.Pool</font>中獲取對(duì)象。<font style="color:rgb(5, 7, 59);">Get</font>方法會(huì)返回<font style="color:rgb(5, 7, 59);">sync.Pool</font>中已經(jīng)存在的對(duì)象(如果存在的話),或者調(diào)用<font style="color:rgb(5, 7, 59);">New</font>方法創(chuàng)建一個(gè)新的對(duì)象。
obj := pool.Get().(*YourType) // 替換 YourType 為你的類型,并進(jìn)行類型斷言
- 使用對(duì)象:獲取到對(duì)象后,你可以像使用普通對(duì)象一樣使用它。
- 放回對(duì)象:使用完對(duì)象后,使用
<font style="color:rgb(5, 7, 59);">Put</font>方法將對(duì)象放回<font style="color:rgb(5, 7, 59);">sync.Pool</font>中,以供后續(xù)再次使用。
pool.Put(obj)
注意事項(xiàng)
- 對(duì)象狀態(tài)未知:從
<font style="color:rgb(5, 7, 59);">sync.Pool</font>中獲取的對(duì)象的狀態(tài)是未知的。因此,在使用對(duì)象之前,你應(yīng)該將其重置到適當(dāng)?shù)某跏紶顟B(tài)。 - 自動(dòng)清理:由于 Go 的垃圾回收器會(huì)清理
<font style="color:rgb(5, 7, 59);">sync.Pool</font>中的對(duì)象,因此你不能依賴<font style="color:rgb(5, 7, 59);">sync.Pool</font>來長期存儲(chǔ)對(duì)象。 - 不適合所有場(chǎng)景:
<font style="color:rgb(5, 7, 59);">sync.Pool</font>并不適合所有需要對(duì)象池的場(chǎng)景。特別是對(duì)于那些需要精確控制對(duì)象生命周期的場(chǎng)景,你可能需要實(shí)現(xiàn)自定義的對(duì)象池。
總的來說,<font style="color:rgb(5, 7, 59);">sync.Pool</font> 是 Go 語言提供的一個(gè)非常有用的工具,它可以幫助你減少內(nèi)存分配和垃圾回收的開銷,提高程序的性能。然而,在使用時(shí)需要注意其特性和局限,以免發(fā)生不可預(yù)見的問題。
垃圾回收-GC
垃圾回收原理-地鼠文檔
Golang三色標(biāo)記+混合寫屏障GC模式全分析-地鼠文檔
- 算法:Golang采用三色標(biāo)記清掃法進(jìn)行垃圾回收,以減少STW(Stop The World)的時(shí)間。寫屏障技術(shù)被用來避免在并發(fā)標(biāo)記過程中產(chǎn)生的誤清掃問題。
- 觸發(fā)條件:垃圾回收的觸發(fā)條件包括內(nèi)存分配達(dá)到一定比例、長時(shí)間未觸發(fā)GC、手動(dòng)調(diào)用runtime.GC()等。
GC 算法有四種:
- 引用計(jì)數(shù):對(duì)每個(gè)對(duì)象維護(hù)一個(gè)引用計(jì)數(shù),當(dāng)引用該對(duì)象的對(duì)象被銷毀時(shí),引用計(jì)數(shù)減1,當(dāng)引用計(jì)數(shù)器為0時(shí)回收該對(duì)象。
- 優(yōu)點(diǎn):對(duì)象可以很快地被回收,不會(huì)出現(xiàn)內(nèi)存耗盡或達(dá)到某個(gè)閥值時(shí)才回收。
- 缺點(diǎn):不能很好地處理循環(huán)引用,而且實(shí)時(shí)維護(hù)引用計(jì)數(shù),也有一定的代價(jià)。
- 代表語言:Python、PHP、Swift
- 標(biāo)記-清除:從根變量開始遍歷所有引用的對(duì)象,引用的對(duì)象標(biāo)記為”被引用”,沒有被標(biāo)記的進(jìn)行回收。
- 優(yōu)點(diǎn):解決了引用計(jì)數(shù)的缺點(diǎn)。
- 缺點(diǎn):需要STW,即要暫時(shí)停掉程序運(yùn)行。
- 代表語言:Golang(其采用三色標(biāo)記法)
- 節(jié)點(diǎn)復(fù)制:節(jié)點(diǎn)復(fù)制也是基于追蹤的算法。其將整個(gè)堆等分為兩個(gè)半?yún)^(qū)(semi-space),一個(gè)包含現(xiàn)有數(shù)據(jù),另一個(gè)包含已被廢棄的數(shù)據(jù)。節(jié)點(diǎn)復(fù)制式垃圾收集從切換(flip)兩個(gè)半?yún)^(qū)的角色開始,然后收集器在老的半?yún)^(qū),也就是 Fromspace 中遍歷存活的數(shù)據(jù)結(jié)構(gòu),在第一次訪問某個(gè)單元時(shí)把它復(fù)制到新半?yún)^(qū),也就是 Tospace 中去。 在 Fromspace 中所有存活單元都被訪問過之后,收集器在 Tospace 中建立一個(gè)存活數(shù)據(jù)結(jié)構(gòu)的副本,用戶程序可以重新開始運(yùn)行了。
- 優(yōu)點(diǎn):
- 所有存活的數(shù)據(jù)結(jié)構(gòu)都縮并地排列在 Tospace 的底部,這樣就不會(huì)存在內(nèi)存碎片的問題
- 獲取新內(nèi)存可以簡(jiǎn)單地通過遞增自由空間指針來實(shí)現(xiàn)。
- 缺點(diǎn):內(nèi)存得不到充分利用,總有一半的內(nèi)存空間處于浪費(fèi)狀態(tài)。
- 優(yōu)點(diǎn):
- 分代收集:按照對(duì)象生命周期長短劃分不同的代空間,生命周期長的放入老年代,而短的放入新生代,不同代有不同的回收算法和回收頻率。
- 優(yōu)點(diǎn):回收性能好
- 缺點(diǎn):算法復(fù)雜
- 代表語言: JAVA
Go 垃圾回收機(jī)制的演變
Go 的 GC 回收有三次演進(jìn)過程,Go V1.3 之前普通標(biāo)記清除(mark and sweep)方法,整體過程需要啟動(dòng) STW,效率極低。GoV1.5 三色標(biāo)記法,堆空間啟動(dòng)寫屏障,棧空間不啟動(dòng),全部掃描之后,需要重新掃描一次棧(需要 STW),效率普通。GoV1.8 三色標(biāo)記法,混合寫屏障機(jī)制:棧空間不啟動(dòng)(全部標(biāo)記成黑色),堆空間啟用寫屏障,整個(gè)過程不要 STW,效率高。
Go 1.3 及之前
- Stop-the-World (STW): 在這些早期版本中,垃圾回收是全局停止的,即在進(jìn)行垃圾回收時(shí),所有應(yīng)用程序的 goroutine 都會(huì)暫停。這種方式導(dǎo)致較長的停頓時(shí)間,對(duì)性能有顯著影響。
Go 1.5
- 三色標(biāo)記清除****和并發(fā)標(biāo)記: 引入了并發(fā)標(biāo)記階段,使得標(biāo)記過程可以與應(yīng)用程序的執(zhí)行并發(fā)進(jìn)行。雖然依然有停頓,但停頓時(shí)間顯著減少。
- 寫屏障: 實(shí)現(xiàn)了寫屏障技術(shù),確保在并發(fā)標(biāo)記過程中對(duì)活動(dòng)對(duì)象的準(zhǔn)確追蹤。
Go 1.8
- Hybrid Barrier: 引入混合屏障機(jī)制,結(jié)合了寫屏障和標(biāo)記終止,進(jìn)一步減少了 STW 時(shí)間。
- 非阻塞回收: 清除過程變?yōu)橥耆l(fā),減少了垃圾回收對(duì)應(yīng)用程序的影響。
Go 1.9
- Pacer 改進(jìn): 對(duì)垃圾回收的節(jié)奏器(Pacer)進(jìn)行了改進(jìn),使得垃圾回收周期更均勻,減少了突發(fā)性停頓。
- Sweep Termination 改進(jìn): 改進(jìn)了清除終止階段的并發(fā)性,進(jìn)一步減少停頓時(shí)間。
Go 1.10
- 并行清除: 增加了對(duì)并行清除的支持,多個(gè)清除操作可以并行執(zhí)行,提高了清除效率。
Go 1.11
- 更好的內(nèi)存分配器: 優(yōu)化了內(nèi)存分配器,減少了垃圾回收壓力。
- 對(duì)象再利用: 增強(qiáng)了對(duì)對(duì)象再利用的支持,減少了內(nèi)存分配次數(shù),從而降低了垃圾回收頻率。
Go 1.12
- 內(nèi)存剖析器改進(jìn): 引入新的內(nèi)存剖析器,提供更精確的內(nèi)存使用報(bào)告,幫助開發(fā)者更好地優(yōu)化內(nèi)存使用。
Go 1.13 - Go 1.15
- 進(jìn)一步優(yōu)化 Pacer: 持續(xù)改進(jìn) Pacer,確保垃圾回收的平滑進(jìn)行,減少對(duì)應(yīng)用程序的影響。
- 優(yōu)化并發(fā)性: 增強(qiáng)了垃圾回收的并發(fā)性,進(jìn)一步減少了停頓時(shí)間。
Go 1.16 及以后
- 內(nèi)存使用優(yōu)化: 持續(xù)改進(jìn)內(nèi)存分配和垃圾回收算法,提高內(nèi)存使用效率。
- 低延遲 GC: 目標(biāo)是在保持高吞吐量的同時(shí),將垃圾回收的停頓時(shí)間進(jìn)一步降低。
- 更智能的內(nèi)存管理: 引入更多智能化的內(nèi)存管理策略,動(dòng)態(tài)調(diào)整垃圾回收參數(shù),以適應(yīng)不同的工作負(fù)載。
總結(jié)
Go 語言的垃圾回收機(jī)制隨著版本的演變不斷優(yōu)化,從早期的全局停止到現(xiàn)在的并發(fā)標(biāo)記和清除,逐步減少了垃圾回收對(duì)應(yīng)用程序性能的影響。未來的版本將繼續(xù)致力于降低垃圾回收的停頓時(shí)間和提高內(nèi)存管理效率,為開發(fā)者提供更高效、更穩(wěn)定的運(yùn)行環(huán)境。
三色標(biāo)記法的流程
為什么需要三色標(biāo)記?
三色標(biāo)記的目的:
- 主要是利用Tracing GC(Tracing GC 是垃圾回收的一個(gè)大類,另外一個(gè)大類是引用計(jì)數(shù)) 做增量式垃圾回收,降低最大暫停時(shí)間。
- 原生Tracing GC只有黑色和白色,沒有中間的狀態(tài),這就要求GC掃描過程必須一次性完成,得到最后的黑色和白色對(duì)象。在前面增量式GC中介紹到了,這種方式會(huì)存在較大的暫停時(shí)間。
- 三色標(biāo)記增加了中間狀態(tài)灰色,增量式GC運(yùn)行過程中,應(yīng)用線程的運(yùn)行可能改變了對(duì)象引用樹,只要讓黑色對(duì)象直接引用白色對(duì)象,GC就可以增量式的運(yùn)行,減少停頓時(shí)間。
什么是三色標(biāo)記?
三色標(biāo)記,通過字面意思我們就可以知道它由3種顏色組成:
- 黑色 Black:表示對(duì)象是可達(dá)的,即使用中的對(duì)象,黑色是已經(jīng)被掃描的對(duì)象。
- 灰色 Gary:表示被黑色對(duì)象直接引用的對(duì)象,但還沒對(duì)它進(jìn)行掃描。
- 白色 White:白色是對(duì)象的初始顏色,如果掃描完成后,對(duì)象依然還是白色的,說明此對(duì)象是垃圾對(duì)象。
三色標(biāo)記規(guī)則
黑色不能指向白色對(duì)象。即黑色可以指向灰色,灰色可以指向白色。
三色標(biāo)記法,主要流程如下:
三色標(biāo)記算法是對(duì)標(biāo)記階段的改進(jìn),原理如下:
- 起初所有對(duì)象都是白色****。
- 從根出發(fā)掃描所有可達(dá)對(duì)象,標(biāo)記為灰色,放入待處理隊(duì)列。
- 從隊(duì)列取出灰色對(duì)象,將其引用對(duì)象標(biāo)記為灰色放入隊(duì)列,自身標(biāo)記為黑色。
- 重復(fù) 3,直到灰色對(duì)象隊(duì)列為空。此時(shí)白色對(duì)象即為垃圾,進(jìn)行回收****。
三色法標(biāo)記主要是第一部分是掃描所有對(duì)象進(jìn)行三色標(biāo)記,標(biāo)記為黑色、灰色和白色,標(biāo)記完成后只有黑色和白色對(duì)象,黑色代表使用中對(duì)象,白色對(duì)象代表垃圾,灰色是白色過渡到黑色的中間臨時(shí)狀態(tài),第二部分是清掃垃圾,即清理白色對(duì)象。
第一部分包含了棧掃描、標(biāo)記和標(biāo)記結(jié)束3個(gè)階段。在棧掃描之前有2個(gè)重要的準(zhǔn)備:STW(Stop The World)和開啟寫屏障(WB,Write Barrier)。
STW是Stop The World,指會(huì)暫停所有正在執(zhí)行的用戶線程/協(xié)程,進(jìn)行垃圾回收的操作,在這之前會(huì)進(jìn)行一些準(zhǔn)備工作,比如開啟Write Barrier,把全局變量,以及每個(gè)goroutine中的 Root對(duì)象 收集起來,Root對(duì)象是標(biāo)記掃描的源頭,可以從Root對(duì)象依次索引到使用中的對(duì)象,STW為垃圾對(duì)象的掃描和標(biāo)記提供了必要的條件。
每個(gè)P都有一個(gè) mcache ,每個(gè) mcache 都有1個(gè)Span用來存放 TinyObject,TinyObject 都是不包含指針的對(duì)象,所以這些對(duì)象可以直接標(biāo)記為黑色,然后關(guān)閉 STW。
每個(gè)P都有1個(gè)進(jìn)行掃描標(biāo)記的 goroutine,可以進(jìn)行并發(fā)標(biāo)記,關(guān)閉STW后,這些 goroutine 就變成可運(yùn)行狀態(tài),接收 Go Scheduler 的調(diào)度,被調(diào)度時(shí)執(zhí)行1輪標(biāo)記,它負(fù)責(zé)第1部分任務(wù):棧掃描、標(biāo)記和標(biāo)記結(jié)束。
棧掃描階段就是把前面搜集的Root對(duì)象找出來,標(biāo)記為黑色,然后把它們引用的對(duì)象也找出來,標(biāo)記為灰色,并且加入到gcWork隊(duì)列,gcWork隊(duì)列保存了灰色的對(duì)象,每個(gè)灰色的對(duì)象都是一個(gè)Work。
后面可以進(jìn)入標(biāo)記階段,它是一個(gè)循環(huán),不斷的從gcWork隊(duì)列中取出work,所指向的對(duì)象標(biāo)記為黑色,該對(duì)象指向的對(duì)象標(biāo)記為灰色,然后加入隊(duì)列,直到隊(duì)列為空。 然后進(jìn)入標(biāo)記結(jié)束階段,再次開啟STW,不同的版本處理方式是不同的。
在Go1.7的版本是Dijkstra寫屏障,這個(gè)寫屏障只監(jiān)控堆上指針數(shù)據(jù)的變動(dòng),由于成本原因,沒有監(jiān)控棧上指針的變動(dòng),由于應(yīng)用goroutine和GC的標(biāo)記goroutine都在運(yùn)行,當(dāng)棧上的指針指向的對(duì)象變更為白色對(duì)象時(shí),這個(gè)白色對(duì)象應(yīng)當(dāng)標(biāo)記為黑色,需要再次掃描全局變量和棧,以免釋放這類不該釋放的對(duì)象。
在Go1.8及以后的版本引入了混合寫屏障,這個(gè)寫屏障依然不監(jiān)控棧上指針的變動(dòng),但是它的策略,使得無需再次掃描棧和全局變量,但依然需要STW然后進(jìn)行一些檢查。
標(biāo)記結(jié)束階段的最后會(huì)關(guān)閉寫屏障,然后關(guān)閉STW,喚醒熟睡已久的負(fù)責(zé)清掃垃圾的goroutine。
清掃goroutine是應(yīng)用啟動(dòng)后立即創(chuàng)建的一個(gè)后臺(tái)goroutine,它會(huì)立刻進(jìn)入睡眠,等待被喚醒,然后執(zhí)行垃圾清理:把白色對(duì)象挨個(gè)清理掉,清掃goroutine和應(yīng)用goroutine是并發(fā)進(jìn)行的。清掃完成之后,它再次進(jìn)入睡眠狀態(tài),等待下次被喚醒。
最后執(zhí)行一些數(shù)據(jù)統(tǒng)計(jì)和狀態(tài)修改的工作,并且設(shè)置好觸發(fā)下一輪GC的閾值,把GC狀態(tài)設(shè)置為Off。
這寫基本是Go垃圾回收的流程,但是在go1.12的源碼稍微有一些不同,例如在標(biāo)記結(jié)束后,就開始設(shè)置各種狀態(tài)數(shù)據(jù)以及把GC狀態(tài)成了Off,在開啟一輪GC時(shí),會(huì)自動(dòng)檢測(cè)當(dāng)前是否處于Off,如果不是Off,則當(dāng)前goroutine會(huì)調(diào)用清掃函數(shù),幫助清掃goroutine一起清掃span,實(shí)際的Go垃圾回收流程以源碼為準(zhǔn)。
這里需要提下go的對(duì)象大小定義:
- 大對(duì)象是大于32KB的.
- 小對(duì)象16KB到32KB的.
- Tiny對(duì)象指大小在1Byte到16Byte之間并且不包含指針的對(duì)象.

三色標(biāo)記的一個(gè)明顯好處是能夠讓用戶程序和 mark 并發(fā)的進(jìn)行.
混合寫屏障規(guī)則是(GoV1.8 )
在Go語言的垃圾回收(GC)機(jī)制中,對(duì)象的標(biāo)記和寫屏障的應(yīng)用過程可以優(yōu)化描述為以下步驟:
- GC 開始與根集合標(biāo)記:
- GC啟動(dòng)時(shí),會(huì)先進(jìn)行一次短暫的停頓(STW),以初始化GC的內(nèi)部狀態(tài)。
- 在這次停頓中,所有活躍的對(duì)象(如全局變量、活躍棧幀中的指針等)被識(shí)別為根對(duì)象,并標(biāo)記為灰色,表示它們需要被進(jìn)一步掃描以確定其可達(dá)性。
- 并發(fā)標(biāo)記階段:
- GC與用戶程序并發(fā)運(yùn)行,從根集合開始,遞歸地掃描并標(biāo)記所有可達(dá)的對(duì)象。
- 插入寫屏障:當(dāng)對(duì)象A新增一個(gè)指向?qū)ο驜的指針時(shí),如果對(duì)象B是白色(即未被標(biāo)記),則將其標(biāo)記為灰色。這確保了新增的引用不會(huì)導(dǎo)致對(duì)象B被錯(cuò)誤地回收。
- 刪除寫屏障(在某些Go版本或?qū)崿F(xiàn)中可能涉及,但具體行為可能有所不同):當(dāng)對(duì)象A刪除一個(gè)指向?qū)ο驜的指針時(shí),如果對(duì)象B是灰色或白色,則將其重新標(biāo)記為灰色(如果是白色,則直接標(biāo)記為灰色;如果是灰色,則保持灰色狀態(tài))。這樣做可以確保在后續(xù)掃描中,對(duì)象B仍然會(huì)被訪問到,從而防止其被錯(cuò)誤地回收。
- 棧上對(duì)象的處理:
- 在GC期間,棧上創(chuàng)建的新對(duì)象最初是未標(biāo)記的(即白色的),但由于它們是活躍對(duì)象,因此它們會(huì)很快被GC識(shí)別并處理。具體來說,當(dāng)GC的標(biāo)記器遍歷到包含這些新對(duì)象的棧幀時(shí),它們會(huì)被標(biāo)記為灰色,并在后續(xù)的掃描過程中變?yōu)楹谏?/font>
- 標(biāo)記完成與清理:
- 當(dāng)并發(fā)標(biāo)記階段完成足夠的工作量或達(dá)到預(yù)定條件后,GC會(huì)再次執(zhí)行STW,以完成剩余的標(biāo)記工作,并準(zhǔn)備進(jìn)入清理階段。
- 清理階段與用戶程序并發(fā)進(jìn)行,回收所有未被標(biāo)記為可達(dá)(即黑色和灰色之外的對(duì)象)的內(nèi)存。
- 對(duì)象刪除與可達(dá)性:
- 需要注意的是,對(duì)象被刪除(即其引用被移除)并不直接導(dǎo)致其被標(biāo)記為灰色或任何其他特定顏色。相反,對(duì)象的可達(dá)性是通過GC的掃描過程來確定的。如果一個(gè)對(duì)象在掃描過程中沒有被任何可達(dá)對(duì)象引用,則它最終會(huì)被識(shí)別為不可達(dá),并在清理階段被回收。
綜上所述,Go語言的GC機(jī)制通過并發(fā)標(biāo)記、寫屏障和清理階段來優(yōu)化內(nèi)存管理,減少STW時(shí)間,并提高程序的性能和響應(yīng)速度。關(guān)于對(duì)象的標(biāo)記和寫屏障的具體行為,需要根據(jù)Go的當(dāng)前版本和具體實(shí)現(xiàn)來準(zhǔn)確理解。
Go V1.8 引入的混合寫屏障(Hybrid Write Barrier)是一種優(yōu)化垃圾回收(GC)性能的機(jī)制,它結(jié)合了插入寫屏障(Insert Write Barrier)和刪除寫屏障(Delete Write Barrier)的優(yōu)點(diǎn),以減少垃圾回收過程中的停頓時(shí)間(STW,Stop The World)。
插入寫屏障規(guī)則
插入寫屏障在對(duì)象A新增一個(gè)指向?qū)ο驜的指針時(shí)觸發(fā)。具體規(guī)則如下:
- 標(biāo)記階段:當(dāng)對(duì)象A新增一個(gè)指向?qū)ο驜的指針時(shí),如果對(duì)象B是白色(未被標(biāo)記),則將其標(biāo)記為灰色(表示其需要被進(jìn)一步掃描)。這樣做可以確保在標(biāo)記過程中不會(huì)遺漏任何可達(dá)對(duì)象。
- 目的:防止在并發(fā)標(biāo)記過程中,由于新增的指針導(dǎo)致原本應(yīng)該被回收的對(duì)象(白色對(duì)象)被錯(cuò)誤地保留下來。
刪除寫屏障規(guī)則
刪除寫屏障在對(duì)象A刪除一個(gè)指向?qū)ο驜的指針時(shí)觸發(fā)。具體規(guī)則如下:
- 標(biāo)記階段:當(dāng)對(duì)象A刪除一個(gè)指向?qū)ο驜的指針時(shí),如果對(duì)象B是灰色或白色,則將其重新標(biāo)記為灰色(如果是白色,則直接標(biāo)記為灰色;如果是灰色,則保持灰色狀態(tài))。這樣做可以確保在后續(xù)掃描中,對(duì)象B仍然會(huì)被訪問到,從而防止其被錯(cuò)誤地回收。
- 清除階段:在清除階段開始時(shí),所有在堆上的灰色對(duì)象都會(huì)被視為可達(dá)對(duì)象,因此不會(huì)被回收。刪除寫屏障確保了在并發(fā)修改指針的情況下,對(duì)象的可達(dá)性狀態(tài)能夠正確地被維護(hù)。
混合寫屏障的優(yōu)勢(shì)
- 減少STW時(shí)間:通過并發(fā)標(biāo)記和寫屏障機(jī)制,Go V1.8 能夠顯著減少垃圾回收過程中的STW時(shí)間,從而提高程序的并發(fā)性能和響應(yīng)速度。
- 提高內(nèi)存使用效率:寫屏障機(jī)制有助于更準(zhǔn)確地識(shí)別垃圾對(duì)象,減少內(nèi)存碎片的產(chǎn)生,提高內(nèi)存的使用效率。
- 增強(qiáng)并發(fā)安全性:在并發(fā)環(huán)境下,寫屏障機(jī)制能夠確保垃圾回收過程的安全性和正確性,防止由于并發(fā)修改導(dǎo)致的內(nèi)存錯(cuò)誤。
總之,Go V1.8 的混合寫屏障規(guī)則通過結(jié)合插入寫屏障和刪除寫屏障的優(yōu)點(diǎn),有效地優(yōu)化了垃圾回收的性能和安全性,為Go語言的高并發(fā)特性提供了堅(jiān)實(shí)的支撐。
GC 的觸發(fā)時(shí)機(jī)?
初級(jí)必問,分為系統(tǒng)觸發(fā)和主動(dòng)觸發(fā)。
1)gcTriggerHeap:當(dāng)所分配的堆大小達(dá)到閾值(由控制器計(jì)算的觸發(fā)堆的大小)時(shí),將會(huì)觸發(fā)。
2)gcTriggerTime:當(dāng)距離上一個(gè) GC 周期的時(shí)間超過一定時(shí)間時(shí),將會(huì)觸發(fā)。時(shí)間周期以runtime.forcegcperiod 變量為準(zhǔn),默認(rèn) 2 分鐘。
3)gcTriggerCycle:如果沒有開啟 GC,則啟動(dòng) GC。
4)手動(dòng)觸發(fā)的 runtime.GC 方法。
內(nèi)存相關(guān)
內(nèi)存分配原理
垃圾回收原理
逃逸分析
Go語言的內(nèi)存模型及堆的分配管理
談?wù)剝?nèi)存泄露,什么情況下內(nèi)存會(huì)泄露?怎么定位排查內(nèi)存泄漏問題?
答:go 中的內(nèi)存泄漏一般都是 goroutine 泄漏,就是 goroutine 沒有被關(guān)閉,或者沒有添加超時(shí)控制,讓 goroutine 一只處于阻塞狀態(tài),不能被 GC。
內(nèi)存泄露有下面一些情況
1)如果 goroutine 在執(zhí)行時(shí)被阻塞而無法退出,就會(huì)導(dǎo)致 goroutine 的內(nèi)存泄漏,一個(gè) goroutine 的最低棧大小為 2KB,在高并發(fā)的場(chǎng)景下,對(duì)內(nèi)存的消耗也是非常恐怖的。
2)互斥鎖未釋放或者造成死鎖會(huì)造成內(nèi)存泄漏
3)time.Ticker 是每隔指定的時(shí)間就會(huì)向通道內(nèi)寫數(shù)據(jù)。作為循環(huán)觸發(fā)器,必須調(diào)用 stop 方法才會(huì)停止,從而被 GC 掉,否則會(huì)一直占用內(nèi)存空間。
4)字符串的截取引發(fā)臨時(shí)性的內(nèi)存泄漏
func main() {
var str0 = "12345678901234567890"
str1 := str0[:10]
}
5)切片截取引起子切片內(nèi)存泄漏
func main() {
var s0 = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := s0[:3]
}
6)函數(shù)數(shù)組傳參引發(fā)內(nèi)存泄漏【如果我們?cè)诤瘮?shù)傳參的時(shí)候用到了數(shù)組傳參,且這個(gè)數(shù)組夠大(我們假設(shè)數(shù)組大小為 100 萬,64 位機(jī)上消耗的內(nèi)存約為 800w 字節(jié),即 8MB 內(nèi)存),或者該函數(shù)短時(shí)間內(nèi)被調(diào)用 N 次,那么可想而知,會(huì)消耗大量?jī)?nèi)存,對(duì)性能產(chǎn)生極大的影響,如果短時(shí)間內(nèi)分配大量?jī)?nèi)存,而又來不及 GC,那么就會(huì)產(chǎn)生臨時(shí)性的內(nèi)存泄漏,對(duì)于高并發(fā)場(chǎng)景相當(dāng)可怕。】
排查方式:
一般通過 pprof 是 Go 的性能分析工具,在程序運(yùn)行過程中,可以記錄程序的運(yùn)行信息,可以是 CPU 使用情況、內(nèi)存使用情況、goroutine 運(yùn)行情況等,當(dāng)需要性能調(diào)優(yōu)或者定位 Bug 時(shí)候,這些記錄的信息是相當(dāng)重要。
當(dāng)然你能說說具體的分析指標(biāo)更加分咯,有的面試官就喜歡他問什么,你簡(jiǎn)潔的回答什么,不喜歡巴拉巴拉詳細(xì)解釋一通,比如蝦P面試官,不過他考察的內(nèi)容特別多,可能是為了節(jié)約時(shí)間。
golang 的內(nèi)存逃逸嗎?什么情況下會(huì)發(fā)生內(nèi)存逃逸?(必問)
答:1)本該分配到棧上的變量,跑到了堆上,這就導(dǎo)致了內(nèi)存逃逸。2)棧是高地址到低地址,棧上的變量,函數(shù)結(jié)束后變量會(huì)跟著回收掉,不會(huì)有額外性能的開銷。3)變量從棧逃逸到堆上,如果要回收掉,需要進(jìn)行 gc,那么 gc 一定會(huì)帶來額外的性能開銷。編程語言不斷優(yōu)化 gc 算法,主要目的都是為了減少 gc 帶來的額外性能開銷,變量一旦逃逸會(huì)導(dǎo)致性能開銷變大。
內(nèi)存逃逸的情況如下:
1)方法內(nèi)返回局部變量指針。
2)向 channel 發(fā)送指針數(shù)據(jù)。
3)在閉包中引用包外的值。
4)在 slice 或 map 中存儲(chǔ)指針。
5)切片(擴(kuò)容后)長度太大。
6)在 interface 類型上調(diào)用方法。
請(qǐng)簡(jiǎn)述 Go 是如何分配內(nèi)存的?
Golang內(nèi)存分配是個(gè)相當(dāng)復(fù)雜的過程,其中還摻雜了GC的處理,這里僅僅對(duì)其關(guān)鍵數(shù)據(jù)結(jié)構(gòu)進(jìn)行了說明,了解其原理而又不至于深陷實(shí)現(xiàn)細(xì)節(jié)。
- Golang程序啟動(dòng)時(shí)申請(qǐng)一大塊內(nèi)存,并劃分成spans、bitmap、arena區(qū)域
- arena區(qū)域按頁劃分成一個(gè)個(gè)小塊
- span管理一個(gè)或多個(gè)頁
- mcentral管理多個(gè)span供線程申請(qǐng)使用
- mcache作為線程私有資源,資源來源于mcentral
go內(nèi)存分配器
Channel 分配在棧上還是堆上?哪些對(duì)象分配在堆上,哪些對(duì)象分配在棧上?
Channel 被設(shè)計(jì)用來實(shí)現(xiàn)協(xié)程間通信的組件,其作用域和生命周期不可能僅限于某個(gè)函數(shù)內(nèi)部,所以 golang 直接將其分配在堆上
準(zhǔn)確地說,你并不需要知道。Golang 中的變量只要被引用就一直會(huì)存活,存儲(chǔ)在堆上還是棧上由內(nèi)部實(shí)現(xiàn)決定而和具體的語法沒有關(guān)系。
知道變量的存儲(chǔ)位置確實(shí)和效率編程有關(guān)系。如果可能,Golang 編譯器會(huì)將函數(shù)的局部變量分配到函數(shù)棧幀(stack frame)上。然而,如果編譯器不能確保變量在函數(shù) return 之后不再被引用,編譯器就會(huì)將變量分配到堆上。而且,如果一個(gè)局部變量非常大,那么它也應(yīng)該被分配到堆上而不是棧上。
當(dāng)前情況下,如果一個(gè)變量被取地址,那么它就有可能被分配到堆上,然而,還要對(duì)這些變量做逃逸分析,如果函數(shù) return 之后,變量不再被引用,則將其分配到棧上。
介紹一下大對(duì)象小對(duì)象,為什么小對(duì)象多了會(huì)造成 gc 壓力?
小于等于 32k 的對(duì)象就是小對(duì)象,其它都是大對(duì)象。一般小對(duì)象通過 mspan 分配內(nèi)存;大對(duì)象則直接由 mheap 分配內(nèi)存。通常小對(duì)象過多會(huì)導(dǎo)致 GC 三色法消耗過多的 CPU。優(yōu)化思路是,減少對(duì)象分配。
小對(duì)象:如果申請(qǐng)小對(duì)象時(shí),發(fā)現(xiàn)當(dāng)前內(nèi)存空間不存在空閑跨度時(shí),將會(huì)需要調(diào)用 nextFree 方法獲取新的可用的對(duì)象,可能會(huì)觸發(fā) GC 行為。
大對(duì)象:如果申請(qǐng)大于 32k 以上的大對(duì)象時(shí),可能會(huì)觸發(fā) GC 行為。
編譯
逃逸分析是怎么進(jìn)行的
在編譯原理中,分析指針動(dòng)態(tài)范圍的方法稱之為逃逸分析。通俗來講,當(dāng)一個(gè)對(duì)象的指針被多個(gè)方法或線程引用時(shí),我們稱這個(gè)指針發(fā)生了逃逸。
Go語言的逃逸分析是編譯器執(zhí)行靜態(tài)代碼分析后,對(duì)內(nèi)存管理進(jìn)行的優(yōu)化和簡(jiǎn)化,它可以決定一個(gè)變量是分配到堆還棧上。
寫過C/C++的同學(xué)都知道,調(diào)用著名的malloc和new函數(shù)可以在堆上分配一塊內(nèi)存,這塊內(nèi)存的使用和銷毀的責(zé)任都在程序員。一不小心,就會(huì)發(fā)生內(nèi)存泄露。
Go語言里,基本不用擔(dān)心內(nèi)存泄露了。雖然也有new函數(shù),但是使用new函數(shù)得到的內(nèi)存不一定就在堆上。堆和棧的區(qū)別對(duì)程序員“模糊化”了,當(dāng)然這一切都是Go編譯器在背后幫我們完成的。
Go語言逃逸分析最基本的原則是:如果一個(gè)函數(shù)返回對(duì)一個(gè)變量的引用,那么它就會(huì)發(fā)生逃逸。
簡(jiǎn)單來說,編譯器會(huì)分析代碼的特征和代碼生命周期,Go中的變量只有在編譯器可以證明在函數(shù)返回后不會(huì)再被引用的,才分配到棧上,其他情況下都是分配到堆上。
Go語言里沒有一個(gè)關(guān)鍵字或者函數(shù)可以直接讓變量被編譯器分配到堆上,相反,編譯器通過分析代碼來決定將變量分配到何處。
對(duì)一個(gè)變量取地址,可能會(huì)被分配到堆上。但是編譯器進(jìn)行逃逸分析后,如果考察到在函數(shù)返回后,此變量不會(huì)被引用,那么還是會(huì)被分配到棧上。
編譯器會(huì)根據(jù)變量是否被外部引用來決定是否逃逸:
- 如果函數(shù)外部沒有引用,則優(yōu)先放到棧中;
- 如果函數(shù)外部存在引用,則必定放到堆中;
Go的垃圾回收,讓堆和棧對(duì)程序員保持透明。真正解放了程序員的雙手,讓他們可以專注于業(yè)務(wù),“高效”地完成代碼編寫。把那些內(nèi)存管理的復(fù)雜機(jī)制交給編譯器,而程序員可以去享受生活。
逃逸分析這種“騷操作”把變量合理地分配到它該去的地方。即使你是用new申請(qǐng)到的內(nèi)存,如果我發(fā)現(xiàn)你竟然在退出函數(shù)后沒有用了,那么就把你丟到棧上,畢竟棧上的內(nèi)存分配比堆上快很多;反之,即使你表面上只是一個(gè)普通的變量,但是經(jīng)過逃逸分析后發(fā)現(xiàn)在退出函數(shù)之后還有其他地方在引用,那我就把你分配到堆上。
如果變量都分配到堆上,堆不像棧可以自動(dòng)清理。它會(huì)引起Go頻繁地進(jìn)行垃圾回收,而垃圾回收會(huì)占用比較大的系統(tǒng)開銷(占用CPU容量的25%)。
堆和棧相比,堆適合不可預(yù)知大小的內(nèi)存分配。但是為此付出的代價(jià)是分配速度較慢,而且會(huì)形成內(nèi)存碎片。棧內(nèi)存分配則會(huì)非常快。棧分配內(nèi)存只需要兩個(gè)CPU指令:“PUSH”和“RELEASE”,分配和釋放;而堆分配內(nèi)存首先需要去找到一塊大小合適的內(nèi)存塊,之后要通過垃圾回收才能釋放。
通過逃逸分析,可以盡量把那些不需要分配到堆上的變量直接分配到棧上,堆上的變量少了,會(huì)減輕分配堆內(nèi)存的開銷,同時(shí)也會(huì)減少gc的壓力,提高程序的運(yùn)行速度。
GoRoot 和 GoPath 有什么用
GoRoot 是 Go 的安裝路徑。mac 或 unix 是在 /usr/local/go 路徑上,來看下這里都裝了些什么:

bin 目錄下面:

pkg 目錄下面:

Go 工具目錄如下,其中比較重要的有編譯器 compile,鏈接器 link:

GoPath 的作用在于提供一個(gè)可以尋找 .go 源碼的路徑,它是一個(gè)工作空間的概念,可以設(shè)置多個(gè)目錄。Go 官方要求,GoPath 下面需要包含三個(gè)文件夾:
src
pkg
bin
src 存放源文件,pkg 存放源文件編譯后的庫文件,后綴為 .a;bin 則存放可執(zhí)行文件。
Go 編譯鏈接過程概述
Go 程序并不能直接運(yùn)行,每條 Go 語句必須轉(zhuǎn)化為一系列的低級(jí)機(jī)器語言指令,將這些指令打包到一起,并以二進(jìn)制磁盤文件的形式存儲(chǔ)起來,也就是可執(zhí)行目標(biāo)文件。
從源文件到可執(zhí)行目標(biāo)文件的轉(zhuǎn)化過程:

完成以上各個(gè)階段的就是 Go 編譯系統(tǒng)。你肯定知道大名鼎鼎的 GCC(GNU Compile Collection),中文名為 GNU 編譯器套裝,它支持像 C,C++,Java,Python,Objective-C,Ada,F(xiàn)ortran,Pascal,能夠?yàn)楹芏嗖煌臋C(jī)器生成機(jī)器碼。
可執(zhí)行目標(biāo)文件可以直接在機(jī)器上執(zhí)行。一般而言,先執(zhí)行一些初始化的工作;找到 main 函數(shù)的入口,執(zhí)行用戶寫的代碼;執(zhí)行完成后,main 函數(shù)退出;再執(zhí)行一些收尾的工作,整個(gè)過程完畢。
在接下來的文章里,我們將探索編譯和運(yùn)行的過程。
Go 源碼里的編譯器源碼位于 src/cmd/compile 路徑下,鏈接器源碼位于 src/cmd/link 路徑下。
Go 編譯相關(guān)的命令詳解
和編譯相關(guān)的命令主要是:
go build
go install
go run
go build
go build 用來編譯指定 packages 里的源碼文件以及它們的依賴包,編譯的時(shí)候會(huì)到 $GoPath/src/package 路徑下尋找源碼文件。go build 還可以直接編譯指定的源碼文件,并且可以同時(shí)指定多個(gè)。
通過執(zhí)行 go help build 命令得到 go build 的使用方法:
usage: go build [-o output] [-i] [build flags] [packages]
-o 只能在編譯單個(gè)包的時(shí)候出現(xiàn),它指定輸出的可執(zhí)行文件的名字。
-i 會(huì)安裝編譯目標(biāo)所依賴的包,安裝是指生成與代碼包相對(duì)應(yīng)的 .a 文件,即靜態(tài)庫文件(后面要參與鏈接),并且放置到當(dāng)前工作區(qū)的 pkg 目錄下,且?guī)煳募哪夸泴蛹?jí)和源碼層級(jí)一致。
至于 build flags 參數(shù),build, clean, get, install, list, run, test 這些命令會(huì)共用一套:
| 參數(shù) | 作用 |
|---|---|
| -a | 強(qiáng)制重新編譯所有涉及到的包,包括標(biāo)準(zhǔn)庫中的代碼包,這會(huì)重寫 /usr/local/go 目錄下的 .a |
| 文件 | |
| -n | 打印命令執(zhí)行過程,不真正執(zhí)行 |
| -p n | 指定編譯過程中命令執(zhí)行的并行數(shù),n 默認(rèn)為 CPU 核數(shù) |
| -race | 檢測(cè)并報(bào)告程序中的數(shù)據(jù)競(jìng)爭(zhēng)問題 |
| -v | 打印命令執(zhí)行過程中所涉及到的代碼包名稱 |
| -x | 打印命令執(zhí)行過程中所涉及到的命令,并執(zhí)行 |
| -work | 打印編譯過程中的臨時(shí)文件夾。通常情況下,編譯完成后會(huì)被刪除 |
我們知道,Go 語言的源碼文件分為三類:命令源碼、庫源碼、測(cè)試源碼。
命令源碼文件:是 Go 程序的入口,包含
func main()函數(shù),且第一行用package main聲明屬于 main 包。
庫源碼文件:主要是各種函數(shù)、接口等,例如工具類的函數(shù)。
測(cè)試源碼文件:以
_test.go為后綴的文件,用于測(cè)試程序的功能和性能。
注意,go build 會(huì)忽略 *_test.go 文件。
go install
go install 用于編譯并安裝指定的代碼包及它們的依賴包。相比 go build,它只是多了一個(gè)“安裝編譯后的結(jié)果文件到指定目錄”的步驟。
還是使用之前 hello-world 項(xiàng)目的例子,我們先將 pkg 目錄刪掉,在項(xiàng)目根目錄執(zhí)行:
go install src/main.go
或者
go install util
兩者都會(huì)在根目錄下新建一個(gè) pkg 目錄,并且生成一個(gè) util.a 文件。
并且,在執(zhí)行前者的時(shí)候,會(huì)在 GOBIN 目錄下生成名為 main 的可執(zhí)行文件。
所以,運(yùn)行 go install 命令,庫源碼包對(duì)應(yīng)的 .a 文件會(huì)被放置到 pkg 目錄下,命令源碼包生成的可執(zhí)行文件會(huì)被放到 GOBIN 目錄。
go install 在 GoPath 有多個(gè)目錄的時(shí)候,會(huì)產(chǎn)生一些問題,具體可以去看郝林老師的 Go 命令教程,這里不展開了。
go run
go run 用于編譯并運(yùn)行命令源碼文件。
Go 程序啟動(dòng)過程是怎樣的
框架
Gin
文檔:https://gin-gonic.com/zh-cn/docs/introduction/
Gin框架介紹及使用-李文周:https://www.liwenzhou.com/posts/Go/Gin_framework/#autoid-0-0-0
Gin源碼閱讀與分析:https://www.yuque.com/iveryimportantpig/huchao/zd24cb3z2bco5304
理解
- Gin 是一個(gè)用 Go 語言編寫的輕量級(jí) Web 框架,專注于高效的 HTTP 路由和中間件管理。它以簡(jiǎn)潔易用的 API 和極高的性能著稱,適合開發(fā) RESTful API 和 Web 服務(wù)。
- Gin 的核心是路由機(jī)制,通過將 HTTP 請(qǐng)求路由到相應(yīng)的處理函數(shù)來實(shí)現(xiàn)。它支持路由分組,便于組織和管理復(fù)雜的路由結(jié)構(gòu)。
- 同時(shí),Gin 提供了一套強(qiáng)大的中間件機(jī)制,允許在請(qǐng)求到達(dá)處理函數(shù)之前進(jìn)行預(yù)處理,如日志記錄、認(rèn)證、錯(cuò)誤處理等。
- Gin 的另一個(gè)亮點(diǎn)是它的 JSON 解析和響應(yīng)處理能力,通過內(nèi)置的
c.JSON方法,可以輕松地將數(shù)據(jù)結(jié)構(gòu)序列化為 JSON 格式返回給客戶端。
總的來說,Gin 適合用于開發(fā)性能要求高的 Web 應(yīng)用,尤其是對(duì)于需要處理大量并發(fā)請(qǐng)求的場(chǎng)景。
特性
- 快速
- 基于 Radix 樹的路由,小內(nèi)存占用。沒有反射。可預(yù)測(cè)的 API 性能。
- 支持中間件
- 傳入的 HTTP 請(qǐng)求可以由一系列中間件和最終操作來處理。 例如:Logger,Authorization,GZIP,最終操作 DB。
- Crash 處理
- Gin 可以 catch 一個(gè)發(fā)生在 HTTP 請(qǐng)求中的 panic 并 recover 它。這樣,你的服務(wù)器將始終可用。例如,你可以向 Sentry 報(bào)告這個(gè) panic!
- JSON 驗(yàn)證
- Gin 可以解析并驗(yàn)證請(qǐng)求的 JSON,例如檢查所需值的存在。
- 路由組
- 更好地組織路由。是否需要授權(quán),不同的 API 版本…… 此外,這些組可以無限制地嵌套而不會(huì)降低性能。
- Gin 使用基于樹狀結(jié)構(gòu)的路由匹配算法,能夠快速地匹配 URL 路徑
- 錯(cuò)誤管理
- Gin 提供了一種方便的方法來收集 HTTP 請(qǐng)求期間發(fā)生的所有錯(cuò)誤。最終,中間件可以將它們寫入日志文件,數(shù)據(jù)庫并通過網(wǎng)絡(luò)發(fā)送。
- 內(nèi)置渲染
- Gin 為 JSON,XML 和 HTML 渲染提供了易于使用的 API。
- 可擴(kuò)展性
- 新建一個(gè)中間件非常簡(jiǎn)單,去查看示例代碼吧。
Gin路由機(jī)制
Gin 的路由機(jī)制非常靈活和高效,主要有以下幾個(gè)方面:
- 路由定義:
Gin 使用*gin.Engine對(duì)象來定義路由。可以通過GET、POST等方法為不同的 HTTP 請(qǐng)求定義處理函數(shù)。例如:
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
r.Run()
- 路由組:
Gin 支持通過Group方法創(chuàng)建路由組,方便管理相關(guān)的路由。例如:
v1 := r.Group("/v1")
{
v1.GET("/users", getUsers)
v1.GET("/posts", getPosts)
}
- 路由參數(shù):
可以在路由中使用動(dòng)態(tài)參數(shù),Gin 會(huì)自動(dòng)提取這些參數(shù)。例如:
r.GET("/user/:id", func(c *gin.Context) {
id := c.Param("id")
c.String(http.StatusOK, "User ID: %s", id)
})
- 路由方法:
Gin 支持 HTTP 的各種請(qǐng)求方法,包括<font style="color:#DF2A3F;">GET</font>、<font style="color:#DF2A3F;">POST</font>、<font style="color:#DF2A3F;">PUT</font>、<font style="color:#DF2A3F;">DELETE</font>等,通過對(duì)應(yīng)的方法定義不同的路由處理函數(shù)。 - 路由優(yōu)先級(jí):
更具體的路由定義優(yōu)先匹配,例如帶有路徑參數(shù)的路由會(huì)比通用的路由更優(yōu)先匹配。 - 中間件:
Gin 允許為路由定義中間件,用于處理請(qǐng)求的預(yù)處理、認(rèn)證、日志記錄等任務(wù)。例如:
r.Use(gin.Logger())
r.Use(gin.Recovery())
總結(jié):Gin 的路由機(jī)制通過提供清晰的路由定義、靈活的路由分組、動(dòng)態(tài)參數(shù)支持、方法匹配和中間件支持,使得構(gòu)建高效、結(jié)構(gòu)化的 Web 應(yīng)用變得簡(jiǎn)單和高效。
請(qǐng)求打入到響應(yīng)的一個(gè)過程
Gin 框架的請(qǐng)求處理過程大致分為以下幾個(gè)步驟:
- 請(qǐng)求接收:
當(dāng) HTTP 請(qǐng)求到達(dá) Gin 應(yīng)用時(shí),Gin 框架會(huì)首先接收到請(qǐng)求。這些請(qǐng)求會(huì)被<font style="color:#DF2A3F;">*gin.Engine</font>對(duì)象處理,Engine是 Gin 的核心組件。 - 路由匹配:
Gin 根據(jù)請(qǐng)求的 URL 和 HTTP 方法(如 GET、POST)來匹配路由。框架會(huì)查找定義的路由規(guī)則,并找到與請(qǐng)求最匹配的處理函數(shù)(Handler)。 - 中間件處理:
在執(zhí)行路由處理函數(shù)之前,Gin 會(huì)依次執(zhí)行與該路由關(guān)聯(lián)的中間件。中間件可以用于請(qǐng)求的預(yù)處理,如認(rèn)證、日志記錄等。 - 執(zhí)行處理函數(shù):
中間件執(zhí)行完畢后,Gin 會(huì)調(diào)用匹配的路由處理函數(shù)。處理函數(shù)可以訪問請(qǐng)求數(shù)據(jù)、處理業(yè)務(wù)邏輯,并準(zhǔn)備響應(yīng)數(shù)據(jù)。 - 生成響應(yīng):
處理函數(shù)會(huì)通過<font style="color:#DF2A3F;">*gin.Context</font>對(duì)象生成響應(yīng)。可以設(shè)置響應(yīng)狀態(tài)碼、響應(yīng)頭以及響應(yīng)體。Gin 提供了多種方法來構(gòu)造響應(yīng),比如c.String()、<font style="color:#DF2A3F;">c.JSON()</font>、c.XML()等。 - 響應(yīng)返回:
最終,Gin 將響應(yīng)數(shù)據(jù)發(fā)送回客戶端,完成請(qǐng)求-響應(yīng)周期。
總結(jié):Gin 框架處理請(qǐng)求的流程從接收請(qǐng)求開始,經(jīng)過路由匹配和中間件處理,執(zhí)行處理函數(shù),生成并返回響應(yīng)。整個(gè)過程高效且結(jié)構(gòu)清晰,幫助開發(fā)者快速構(gòu)建 Web 應(yīng)用。
gin目錄結(jié)構(gòu)
文檔:https://blog.csdn.net/qq_34877350/article/details/107959381
├── gin
│ ├── Router
│ └── router.go
│ ├── Middlewares
│ └── corsMiddleware.go
│ ├── Controllers
│ └── testController.go
│ ├── Services
│ └── testService.go
│ ├── Models
│ └── testModel.go
│ ├── Databases
│ └── mysql.go
│ ├── Sessions
│ └── session.go
└── main.go
- 使用gorm訪問數(shù)據(jù)庫
- gin 為項(xiàng)目根目錄
- main.go 為入口文件
- Router 為路由目錄
- Middlewares 為中間件目錄
- Controllers 為控制器目錄(MVC)
- Services 為服務(wù)層目錄,這里把DAO邏輯也寫入其中,如果分開也可以
- Models 為模型目錄
- Databases 為數(shù)據(jù)庫初始化目錄
- Sessions 為session初始化目錄
- 文件 引用順序 大致如下:
- main.go(在main中關(guān)閉數(shù)據(jù)庫) - router(Middlewares) - Controllers - Services(sessions) - Models - Databases
go-zero
文檔:https://go-zero.dev/cn/docs/introduction
go-zero 是一個(gè)集成了各種工程實(shí)踐的 web 和 rpc 框架。通過彈性設(shè)計(jì)保障了大并發(fā)服務(wù)端的穩(wěn)定性,經(jīng)受了充分的實(shí)戰(zhàn)檢驗(yàn)。
go-zero 包含極簡(jiǎn)的 API 定義和生成工具 goctl,可以根據(jù)定義的 api 文件一鍵生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代碼,并可直接運(yùn)行。
使用 go-zero 的好處:
- 輕松獲得支撐千萬日活服務(wù)的穩(wěn)定性
- 內(nèi)建級(jí)聯(lián)超時(shí)控制、限流、自適應(yīng)熔斷、自適應(yīng)降載等微服務(wù)治理能力,無需配置和額外代碼
- 微服務(wù)治理中間件可無縫集成到其它現(xiàn)有框架使用
- 極簡(jiǎn)的 API 描述,一鍵生成各端代碼
- 自動(dòng)校驗(yàn)客戶端請(qǐng)求參數(shù)合法性
- 大量微服務(wù)治理和并發(fā)工具包
字節(jié)-CloudWeGo
文檔:https://www.cloudwego.io/zh/docs/
HTTP-Hertz
文檔:https://www.cloudwego.io/zh/docs/hertz/overview/
是一個(gè) Golang 微服務(wù) HTTP 框架,在設(shè)計(jì)之初參考了其他開源框架 fasthttp、gin、echo 的優(yōu)勢(shì), 并結(jié)合字節(jié)跳動(dòng)內(nèi)部的需求,使其具有高易用性、高性能、高擴(kuò)展性等特點(diǎn),目前在字節(jié)跳動(dòng)內(nèi)部已廣泛使用。 如今越來越多的微服務(wù)選擇使用 Golang,如果對(duì)微服務(wù)性能有要求,又希望框架能夠充分滿足內(nèi)部的可定制化需求,Hertz 會(huì)是一個(gè)不錯(cuò)的選擇。
特點(diǎn)
- 高易用性在開發(fā)過程中,快速寫出來正確的代碼往往是更重要的。因此,在 Hertz 在迭代過程中,積極聽取用戶意見,持續(xù)打磨框架,希望為用戶提供一個(gè)更好的使用體驗(yàn),幫助用戶更快的寫出正確的代碼。
- 高性能Hertz 默認(rèn)使用自研的高性能網(wǎng)絡(luò)庫 Netpoll,在一些特殊場(chǎng)景相較于 go net,Hertz 在 QPS、時(shí)延上均具有一定優(yōu)勢(shì)。關(guān)于性能數(shù)據(jù),可參考下圖 Echo 數(shù)據(jù)。四個(gè)框架的對(duì)比:
三個(gè)框架的對(duì)比:
關(guān)于詳細(xì)的性能數(shù)據(jù),可參考 https://github.com/cloudwego/hertz-benchmark。 - 高擴(kuò)展性Hertz 采用了分層設(shè)計(jì),提供了較多的接口以及默認(rèn)的擴(kuò)展實(shí)現(xiàn),用戶也可以自行擴(kuò)展。同時(shí)得益于框架的分層設(shè)計(jì),框架的擴(kuò)展性也會(huì)大很多。目前僅將穩(wěn)定的能力開源給社區(qū),更多的規(guī)劃參考 RoadMap。
- 多協(xié)議支持Hertz 框架原生提供 HTTP1.1、ALPN 協(xié)議支持。除此之外,由于分層設(shè)計(jì),Hertz 甚至支持自定義構(gòu)建協(xié)議解析邏輯,以滿足協(xié)議層擴(kuò)展的任意需求。
- 網(wǎng)絡(luò)層切換能力Hertz 實(shí)現(xiàn)了 Netpoll 和 Golang 原生網(wǎng)絡(luò)庫 間按需切換能力,用戶可以針對(duì)不同的場(chǎng)景選擇合適的網(wǎng)絡(luò)庫,同時(shí)也支持以插件的方式為 Hertz 擴(kuò)展網(wǎng)絡(luò)庫實(shí)現(xiàn)。
RPC-Kitex
文檔:https://www.cloudwego.io/zh/docs/kitex/overview/
字節(jié)跳動(dòng)內(nèi)部的 Golang 微服務(wù) RPC 框架,具有高性能、強(qiáng)可擴(kuò)展的特點(diǎn),在字節(jié)內(nèi)部已廣泛使用。如果對(duì)微服務(wù)性能有要求,又希望定制擴(kuò)展融入自己的治理體系,Kitex 會(huì)是一個(gè)不錯(cuò)的選擇。
框架特點(diǎn)
- 高性能使用自研的高性能網(wǎng)絡(luò)庫 Netpoll,性能相較 go net 具有顯著優(yōu)勢(shì)。
- 擴(kuò)展性提供了較多的擴(kuò)展接口以及默認(rèn)擴(kuò)展實(shí)現(xiàn),使用者也可以根據(jù)需要自行定制擴(kuò)展,具體見下面的框架擴(kuò)展。
- 多消息協(xié)議RPC 消息協(xié)議默認(rèn)支持 Thrift、Kitex Protobuf、gRPC。Thrift 支持 Buffered 和 Framed 二進(jìn)制協(xié)議;Kitex Protobuf 是 Kitex 自定義的 Protobuf 消息協(xié)議,協(xié)議格式類似 Thrift;gRPC 是對(duì) gRPC 消息協(xié)議的支持,可以與 gRPC 互通。除此之外,使用者也可以擴(kuò)展自己的消息協(xié)議。
- 多傳輸協(xié)議傳輸協(xié)議封裝消息協(xié)議進(jìn)行 RPC 互通,傳輸協(xié)議可以額外透?jìng)髟畔ⅲ糜诜?wù)治理,Kitex 支持的傳輸協(xié)議有 TTHeader、HTTP2。TTHeader 可以和 Thrift、Kitex Protobuf 結(jié)合使用;HTTP2 目前主要是結(jié)合 gRPC 協(xié)議使用,后續(xù)也會(huì)支持 Thrift。
- 多種消息類型支持 PingPong、Oneway、雙向 Streaming。其中 Oneway 目前只對(duì) Thrift 協(xié)議支持,雙向 Streaming 只對(duì) gRPC 支持,后續(xù)會(huì)考慮支持 Thrift 的雙向 Streaming。
- 服務(wù)治理支持服務(wù)注冊(cè)/發(fā)現(xiàn)、負(fù)載均衡、熔斷、限流、重試、監(jiān)控、鏈路跟蹤、日志、診斷等服務(wù)治理模塊,大部分均已提供默認(rèn)擴(kuò)展,使用者可選擇集成。
- 代碼生成Kitex 內(nèi)置代碼生成工具,可支持生成 Thrift、Protobuf 以及腳手架代碼。
ORM
GORM
GORM 是一個(gè)強(qiáng)大的 Golang ORM(對(duì)象關(guān)系映射)庫,它能夠簡(jiǎn)化數(shù)據(jù)庫操作,使開發(fā)者能夠通過 Golang 代碼與數(shù)據(jù)庫進(jìn)行交互,而不需要直接編寫 SQL 語句。GORM 支持自動(dòng)映射數(shù)據(jù)庫表結(jié)構(gòu)到 Golang 結(jié)構(gòu)體,并提供了豐富的鏈?zhǔn)秸{(diào)用方法來進(jìn)行增刪改查操作。
使用 GORM 時(shí),我們可以通過結(jié)構(gòu)體字段標(biāo)簽(例如 <font style="color:#DF2A3F;">gorm:"column:name"</font>)來指定數(shù)據(jù)庫表的列名、數(shù)據(jù)類型、索引等。它還支持事務(wù)、預(yù)加載、關(guān)聯(lián)關(guān)系(如一對(duì)一、一對(duì)多、多對(duì)多)等高級(jí)特性,適合構(gòu)建復(fù)雜的業(yè)務(wù)系統(tǒng)。
在性能方面,GORM 的操作雖然較為直觀和簡(jiǎn)潔,但它會(huì)帶來一定的性能開銷,特別是在處理大批量數(shù)據(jù)或高并發(fā)場(chǎng)景時(shí),需要注意優(yōu)化查詢語句或選擇適當(dāng)?shù)臄?shù)據(jù)庫操作方式,比如使用原生 SQL 語句。
總的來說,GORM 適合大多數(shù)應(yīng)用場(chǎng)景,特別是對(duì)于中小型項(xiàng)目或者需要快速開發(fā)的項(xiàng)目來說,能顯著提高開發(fā)效率。
GORM GEN
GORM Gen 是 GORM 的一個(gè)插件,它可以根據(jù)數(shù)據(jù)庫的表結(jié)構(gòu)自動(dòng)生成 Golang 的結(jié)構(gòu)體代碼和數(shù)據(jù)訪問層(DAO)代碼。這對(duì)于那些需要頻繁操作數(shù)據(jù)庫的大型項(xiàng)目非常有幫助,因?yàn)?font style="color: rgba(223, 42, 63, 1)">它可以減少手寫代碼的時(shí)間,提高開發(fā)效率,并確保生成的代碼與數(shù)據(jù)庫表結(jié)構(gòu)保持一致。
GORM Gen 的主要特點(diǎn):
- 代碼生成:通過分析數(shù)據(jù)庫表結(jié)構(gòu),自動(dòng)生成對(duì)應(yīng)的 Golang 結(jié)構(gòu)體和數(shù)據(jù)庫操作代碼。這些代碼通常包括基礎(chǔ)的增刪改查方法,還可以根據(jù)需求生成自定義查詢。
- 自動(dòng)更新:當(dāng)數(shù)據(jù)庫表結(jié)構(gòu)發(fā)生變化時(shí),GORM Gen 可以重新生成代碼,保證數(shù)據(jù)訪問層與數(shù)據(jù)庫結(jié)構(gòu)的一致性,減少手動(dòng)維護(hù)代碼的麻煩。
- 增強(qiáng)的類型安全:生成的代碼類型更為安全,避免了常見的類型轉(zhuǎn)換錯(cuò)誤。
- 支持復(fù)雜查詢:GORM Gen 可以生成支持復(fù)雜查詢的代碼,比如聯(lián)合查詢、條件查詢、分頁等,減少了開發(fā)者手寫復(fù)雜 SQL 的負(fù)擔(dān)。
使用 GORM Gen 的流程:
- 安裝:首先需要通過
go install安裝 GORM Gen 工具。 - 配置:使用 YAML 文件或在代碼中配置數(shù)據(jù)庫連接等相關(guān)信息。
- 生成代碼:通過運(yùn)行 GORM Gen 工具,自動(dòng)生成數(shù)據(jù)庫表對(duì)應(yīng)的 Golang 結(jié)構(gòu)體和 DAO 層代碼。
- 使用生成的代碼:在項(xiàng)目中直接調(diào)用生成的代碼來執(zhí)行數(shù)據(jù)庫操作,而無需手寫 SQL。
適用場(chǎng)景:
- 對(duì)于擁有大量數(shù)據(jù)庫表的大型項(xiàng)目,使用 GORM Gen 能夠顯著提高開發(fā)效率。
- 當(dāng)項(xiàng)目的數(shù)據(jù)庫結(jié)構(gòu)頻繁變化時(shí),GORM Gen 可以幫助開發(fā)者快速更新代碼,保持?jǐn)?shù)據(jù)庫與代碼的同步。
總的來說,GORM Gen 適合那些需要高效、穩(wěn)定的數(shù)據(jù)庫操作代碼的項(xiàng)目,通過減少重復(fù)勞動(dòng)和手動(dòng)錯(cuò)誤來提高開發(fā)質(zhì)量。
場(chǎng)景
有沒有遇到過cpu不高但是內(nèi)存高的場(chǎng)景?怎么排查的
在實(shí)際開發(fā)中,遇到 CPU 使用率不高但內(nèi)存占用很高的情況并不少見。這種現(xiàn)象通常表明程序中存在內(nèi)存泄漏、內(nèi)存占用過大、或者內(nèi)存管理不當(dāng)的問題。下面是一個(gè)排查的步驟:
在實(shí)際開發(fā)中,遇到 CPU 使用率不高但內(nèi)存占用很高的情況并不少見。這種現(xiàn)象通常表明程序中存在內(nèi)存泄漏、內(nèi)存占用過大、或者內(nèi)存管理不當(dāng)?shù)膯栴}。下面是一個(gè)排查的步驟:
檢查內(nèi)存占用情況
- 工具:
top**,htop, **ps
使用這些系統(tǒng)工具查看內(nèi)存占用較高的進(jìn)程,確認(rèn)是否是你的 Go 程序?qū)е碌膬?nèi)存消耗。 pmap
使用pmap <PID>查看進(jìn)程的內(nèi)存分布,確定是哪個(gè)內(nèi)存段占用最大(如 heap、stack)。
分析 Go 程序的內(nèi)存使用
- 內(nèi)存分配情況:
pprof
使用 Go 的<font style="color:#DF2A3F;">pprof</font>工具生成內(nèi)存快照(heap profile):
go tool pprof http://localhost:6060/debug/pprof/heap
分析結(jié)果可以幫助你識(shí)別哪些對(duì)象在堆上占用最多的內(nèi)存。
- 查看 Goroutine 狀態(tài)
使用<font style="color:#DF2A3F;">pprof</font>中的 Goroutine 分析工具:
go tool pprof http://localhost:6060/debug/pprof/goroutine
查看是否存在大量 Goroutine 導(dǎo)致內(nèi)存占用。
檢查內(nèi)存泄漏
- 是否有未釋放的內(nèi)存
檢查代碼中是否存在未釋放的資源,如未關(guān)閉的文件描述符、數(shù)據(jù)庫連接、未清理的緩存等。 - 工具:
leaktest**, **goleak
使用leaktest或goleak工具檢測(cè) Goroutine 泄漏,這些泄漏可能會(huì)導(dǎo)致內(nèi)存無法被回收。
優(yōu)化內(nèi)存使用
- 減少對(duì)象分配
盡量復(fù)用內(nèi)存,如使用<font style="color:#DF2A3F;">sync.Pool</font>來管理重復(fù)使用的對(duì)象,避免頻繁的內(nèi)存分配和 GC 壓力。 - 優(yōu)化數(shù)據(jù)結(jié)構(gòu)
檢查是否使用了不必要的大型數(shù)據(jù)結(jié)構(gòu)(如 map, slice),考慮更合適的替代方案。
通過以上步驟,通常可以較為全面地排查和解決 CPU 不高但內(nèi)存高的問題。
怎么實(shí)時(shí)查看k8s內(nèi)存占用的
要實(shí)時(shí)查看 Kubernetes 集群中 Pod 的內(nèi)存占用情況,有幾種常見的方法:
使用 kubectl top 命令
**<font style="color:#DF2A3F;">kubectl top</font>** 是 Kubernetes 提供的一個(gè)工具,可以實(shí)時(shí)查看 Pod 和節(jié)點(diǎn)的資源使用情況(包括 CPU 和內(nèi)存)。
# 查看某個(gè)命名空間下所有 Pod 的資源使用情況
kubectl top pod -n <namespace>
# 查看指定 Pod 的資源使用情況
kubectl top pod <pod-name> -n <namespace>
# 查看集群中所有節(jié)點(diǎn)的資源使用情況
kubectl top nodes
這個(gè)命令會(huì)顯示每個(gè) Pod 當(dāng)前的 CPU 和內(nèi)存使用量,以及節(jié)點(diǎn)的總資源消耗。
使用 kubectl describe pod 命令
kubectl describe 命令可以查看單個(gè) Pod 的詳細(xì)信息,包括資源請(qǐng)求和限制:
kubectl describe pod <pod-name> -n <namespace>
這不會(huì)直接顯示實(shí)時(shí)內(nèi)存使用情況,但你可以看到 Pod 所請(qǐng)求和限制的內(nèi)存資源。
使用 Kubernetes Dashboard
Kubernetes Dashboard 是一個(gè) web 界面的 UI,可以查看集群中各種資源的使用情況,包括實(shí)時(shí)的內(nèi)存消耗。
- 安裝 Kubernetes Dashboard:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0/aio/deploy/recommended.yaml
- 訪問 Dashboard:
kubectl proxy
然后在瀏覽器中打開 http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/。
在 Dashboard 中,你可以查看各個(gè) Pod 的詳細(xì)資源使用情況,包括實(shí)時(shí)的內(nèi)存和 CPU 使用。
使用 Prometheus + Grafana 監(jiān)控
如果你的集群已經(jīng)配置了 Prometheus 和 Grafana,你可以使用它們來實(shí)時(shí)監(jiān)控內(nèi)存使用情況:
- Prometheus:收集和存儲(chǔ) Kubernetes 中的指標(biāo)數(shù)據(jù)。
- Grafana:提供豐富的儀表盤,可以實(shí)時(shí)顯示集群中各個(gè)資源的使用情況。
在 Grafana 中,你可以創(chuàng)建或使用現(xiàn)有的儀表盤來監(jiān)控 Pod 和節(jié)點(diǎn)的內(nèi)存使用情況。
直接查看容器內(nèi)的內(nèi)存使用
如果你想直接查看某個(gè)容器的內(nèi)存使用情況,可以進(jìn)入容器內(nèi)部,然后使用 <font style="color:#DF2A3F;">top</font> 或 <font style="color:#DF2A3F;">free</font> 等命令:
kubectl exec -it <pod-name> -n <namespace> -- bash
# 在容器內(nèi)使用 top 或 free 命令
top
free -m
6. 使用 kubectl get --raw 命令
你可以直接通過 Kubernetes API 獲取內(nèi)存使用情況,返回結(jié)果為 JSON 格式:
kubectl get --raw /apis/metrics.k8s.io/v1beta1/namespaces/<namespace>/pods/<pod-name>
這個(gè)方法適合進(jìn)行腳本化或編程訪問資源使用數(shù)據(jù)。
通過以上這些方法,你可以實(shí)時(shí)查看 Kubernetes 中的內(nèi)存使用情況,并及時(shí)了解資源的分配與消耗。
參考并致謝
1、可可醬 可可醬:Golang常見面試題
2、Bel_Ami同學(xué) golang 面試題(從基礎(chǔ)到高級(jí))

三個(gè)框架的對(duì)比:
關(guān)于詳細(xì)的性能數(shù)據(jù),可參考
浙公網(wǎng)安備 33010602011771號(hào)