<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      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言嗎?

      是的,也不是。原因是:

      1. Go 有類型和方法,并且允許面向?qū)ο蟮木幊田L(fēng)格,但沒有類型層次。
      2. Go 中的 "接口 "概念提供了一種不同的方法,我們認(rèn)為這種方法易于使用,而且在某些方面更加通用。還有一些方法可以將類型嵌入到其他類型中,以提供類似的東西,但不等同于子類。
      3. Go 中的方法比 C++ 或 Java 中的方法更通用:它們可以為任何類型的數(shù)據(jù)定義,甚至是內(nèi)置類型,如普通的、"未裝箱的 "整數(shù)。它們并不局限于結(jié)構(gòu)(類)。
      4. 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)景:

      1. 由于原子、互斥量或通道操作調(diào)用導(dǎo)致 Goroutine 阻塞,調(diào)度器將把當(dāng)前阻塞的 Goroutine 切換出去,重新調(diào)度 LRQ 上的其他 Goroutine;
      2. 由于網(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ī)制“模擬”出來的。
      3. 當(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。
      4. 如果在 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里面的 _

      1. 忽略返回值
        1. 比如某個(gè)函數(shù)返回三個(gè)參數(shù),但是我們只需要其中的兩個(gè),另外一個(gè)參數(shù)可以忽略,這樣的話代碼可以這樣寫:
      v1, v2, _ := function(...)
      v1, _, _ := function(...)
      
      1. 用在變量(特別是接口斷言)
      type T struct{}
      var _ X = T{}
      //其中 I為interface
      

      上面用來判斷 type T是否實(shí)現(xiàn)了X,用作類型斷言,如果T沒有實(shí)現(xiàn)接口X,則編譯錯(cuò)誤.

      1. 用在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ī)范?

      1. ** 單元測(cè)試文件命名規(guī)則 :**

      單元測(cè)試需要?jiǎng)?chuàng)建單獨(dú)的測(cè)試文件,不能在原有文件中書寫,名字規(guī)則為 xxx_test.go。這個(gè)規(guī)則很好理解。

      1. **單元測(cè)試包命令規(guī)則 **

      單元測(cè)試文件的包名為原文件的包名添加下劃線接test,舉例如下:

      // 原文件包名:
      
      package xxx
      
      // 單元測(cè)試文件包名:
      
      package xxx_test
      
      1. ** 單元測(cè)試方法命名規(guī)則 **

      單元測(cè)試文件中的測(cè)試方法和原文件中的待測(cè)試的方法名相對(duì)應(yīng),以Test開頭,舉例如下:

      // 原文件方法:
      func Xxx(name string) error 
       
      // 單元測(cè)試文件方法:
      func TestXxx()
      
      1. **單元測(cè)試方法參數(shù) **

      單元測(cè)試方法的參數(shù)必須是t *testing.T,舉例如下:

      func TestZipFiles(t *testing.T) { ...
      

      單步調(diào)試?

      golang單步調(diào)試神器delve

      導(dǎo)入一個(gè)go的工程,有些依賴找不到,改怎么辦?

      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

      1. 定義
        • goroutine 是 Go 語言中的一種輕量級(jí)線程,由 Go 運(yùn)行時(shí)管理。
      2. 使用方法
        • 使用 go 關(guān)鍵字啟動(dòng)一個(gè)新的 goroutine。例如:go 函數(shù)名(參數(shù)列表)
      3. 優(yōu)勢(shì)
        • goroutine 的創(chuàng)建和銷毀開銷非常小,可以高效地創(chuàng)建成千上萬個(gè) goroutine。
        • goroutine 是并發(fā)執(zhí)行的,可以提高程序的執(zhí)行效率。
      4. 調(diào)度
        • 由 Go 運(yùn)行時(shí)調(diào)度和管理,無需手動(dòng)管理線程。
      5. 通信
        • goroutine 之間通過 channel 進(jìn)行通信,確保數(shù)據(jù)傳遞的安全性和同步性。
      6. 典型應(yīng)用
        • 適用于并發(fā)任務(wù)處理,如網(wǎng)絡(luò)請(qǐng)求處理、并發(fā)計(jì)算等。
      7. 示例
        • 在 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ù)組

      1. 當(dāng)使用數(shù)組作為參數(shù)和返回值的時(shí)候,傳進(jìn)去的是值,在函數(shù)內(nèi)部對(duì)數(shù)組進(jìn)行修改并不會(huì)影響原數(shù)據(jù)
      2. 當(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ì)被修改
      3. 數(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ò)容:

      1. 如果期望容量大于當(dāng)前容量的兩倍就會(huì)使用期望容量
      2. 如果當(dāng)前切片的長度小于 1024 就會(huì)將容量翻倍
      3. 如果當(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ò)容:

      1. 如果期望容量大于當(dāng)前容量的兩倍就會(huì)使用期望容量
      2. 當(dāng)原 slice 容量 < threshold(閾值默認(rèn) 256) 的時(shí)候,新 slice 容量變成原來的 2 倍
      3. 當(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的類型的例子:

      1. 整數(shù)類型:
      m := make(map[int]string)  
      m[1] = "one"
      
      1. 字符串類型:
      m := make(map[string]int)  
      m["one"] = 1
      
      1. 布爾類型:
      m := make(map[bool]string)  
      m[true] = "yes"  
      m[false] = "no"
      
      1. 結(jié)構(gòu)體類型(只要結(jié)構(gòu)體的所有字段都是可比較的):
      type Point struct {  
          X, Y int  
      }  
        
      m := make(map[Point]string)  
      m[Point{1, 2}] = "1,2"
      
      1. 數(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)

      1. key的唯一性:map中的每個(gè)key必須是唯一的。如果嘗試使用已存在的key插入新值,則會(huì)覆蓋舊值。
      2. key的不可變性:作為key的類型必須是可比較的,這通常意味著它們應(yīng)該是不可變的。例如,在Go語言中,切片、映射和函數(shù)類型因?yàn)榘勺儬顟B(tài),所以不能直接作為map的key。
      3. 初始化和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)行初始化。
      4. 遍歷順序:map的遍歷順序是不確定的,每次遍歷的結(jié)果可能不同。如果需要按照特定順序處理map中的元素,應(yīng)該先對(duì)key進(jìn)行排序。
      5. 并發(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,可以采取以下幾種策略:
        1. 使用互斥鎖(sync.Mutex):在讀寫map的操作前后加鎖,確保同一時(shí)間只有一個(gè)goroutine可以訪問map。
        2. 使用讀寫互斥鎖(sync.RWMutex):如果讀操作遠(yuǎn)多于寫操作,可以使用讀寫鎖來提高性能。讀寫鎖允許多個(gè)goroutine同時(shí)讀取map,但在寫入時(shí)需要獨(dú)占訪問。
        3. 使用并發(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)記與垃圾回收

      1. 刪除操作:使用<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的一部分。
      2. 內(nèi)存標(biāo)記:刪除操作后,如果沒有任何其他變量或數(shù)據(jù)結(jié)構(gòu)引用被刪除的值,那么這些值就變成了垃圾回收器的目標(biāo)。Go語言的垃圾回收器(Garbage Collector, GC)會(huì)定期掃描內(nèi)存,標(biāo)記那些不再被使用的內(nèi)存區(qū)域。
      3. 內(nèi)存釋放:在垃圾回收過程中,被標(biāo)記為垃圾的內(nèi)存區(qū)域會(huì)被釋放回堆內(nèi)存,供后續(xù)的內(nèi)存分配使用。然而,這個(gè)過程并不是立即發(fā)生的,而是由垃圾回收器的觸發(fā)條件和回收策略決定的。

      注意事項(xiàng)

      1. 內(nèi)存釋放時(shí)機(jī):由于垃圾回收器的非確定性,刪除map中的key后,其內(nèi)存釋放的時(shí)機(jī)是不確定的。因此,不能依賴刪除操作來立即釋放內(nèi)存。
      2. 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ù)制過去。
      3. 并發(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è):

      1. 負(fù)載因子 > 6.5時(shí),也即平均每個(gè)bucket存儲(chǔ)的鍵值對(duì)達(dá)到6.5個(gè)。
      2. 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ì)提高訪問效率。

      查找過程

      查找過程如下:

      1. 根據(jù)key值算出哈希值
      2. 取哈希值低位與hmap.B取模確定bucket位置
      3. 取哈希值高位在tophash數(shù)組中查詢
      4. 如果tophash[i]中存儲(chǔ)值也哈希值相等,則去找到該bucket中的key值進(jìn)行比較
      5. 當(dāng)前bucket沒有找到,則繼續(xù)從下個(gè)overflow的bucket中查找。
      6. 如果當(dāng)前處于搬遷過程,則優(yōu)先從oldbuckets查找

      注:如果查找不到,也不會(huì)返回空值,而是返回相應(yīng)類型的0值。

      插入過程

      新元素插入過程如下:

      1. 根據(jù)key值算出哈希值
      2. 取哈希值低位與hmap.B取模確定bucket位置
      3. 查找該key是否已經(jīng)存在,如果存在則直接更新值
      4. 如果沒找到將key,將key插入

      增刪查的時(shí)間復(fù)雜度 O(1)

      1. 在Go語言中,對(duì)于map的查找、插入和刪除操作,在大多數(shù)情況下,它們的時(shí)間復(fù)雜度都可以視為O(1),即常數(shù)時(shí)間復(fù)雜度。
      2. map的讀寫效率之所以在平均情況下能達(dá)到O(1),是因?yàn)镚o語言的map實(shí)現(xiàn)采用了哈希表的方式,通過哈希函數(shù)將鍵映射到哈希表的某個(gè)位置(哈希桶)上,從而在常數(shù)時(shí)間內(nèi)完成讀寫操作。
      3. 然而,需要明確的是,這個(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)鍵原理:

      1. 讀寫分離sync.Map 通過讀寫分離來提升性能。它內(nèi)部維護(hù)了兩種數(shù)據(jù)結(jié)構(gòu):一個(gè)只讀的只讀字典 (read),一個(gè)讀寫字典 (dirty)。讀操作優(yōu)先訪問只讀字典,只有在只讀字典中找不到數(shù)據(jù)時(shí)才會(huì)訪問讀寫字典。
      2. 延遲寫入:寫操作并不立即更新只讀字典(read),而是更新讀寫字典 (dirty)。只有在讀操作發(fā)現(xiàn)只讀字典的數(shù)據(jù)過時(shí)(即 misses 計(jì)數(shù)器超過閾值)時(shí),才會(huì)將讀寫字典中的數(shù)據(jù)同步到只讀字典。這種策略減少了寫操作對(duì)讀操作的影響。
      3. 原子操作:讀操作大部分是無鎖的,因?yàn)樗鼈冎饕L問只讀的 read map,并通過原子操作 (atomic.Value) 來保護(hù)讀操作;寫操作會(huì)加鎖(使用 sync.Mutex)保護(hù)寫操作,以確保對(duì) dirty map 的并發(fā)安全 ,確保高并發(fā)環(huán)境下的安全性。
      4. 條目淘汰:當(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.Mutexsync.RWMutex)加上 map 的方式有一些關(guān)鍵區(qū)別:

      自己使用鎖和 map

      1. 全局鎖
        • 你需要自己管理鎖,通常是一個(gè)全局的 sync.Mutexsync.RWMutex
        • 對(duì)于讀多寫少的場(chǎng)景,使用 sync.RWMutex 可以允許多個(gè)讀操作同時(shí)進(jìn)行,但寫操作依然會(huì)阻塞所有讀操作。
      2. 手動(dòng)處理
        • 你需要自己編寫代碼來處理加鎖、解鎖、讀寫操作。
        • 錯(cuò)誤使用鎖可能導(dǎo)致死鎖、競(jìng)態(tài)條件等問題。
      3. 簡(jiǎn)單直觀
        • 實(shí)現(xiàn)簡(jiǎn)單,容易理解和調(diào)試。

      **<u>sync.Map</u>**

      1. 讀寫分離
        • sync.Map 內(nèi)部使用讀寫分離的策略,通過只讀和讀寫兩個(gè) map 提高讀操作的性能。
        • 讀操作大部分情況下是無鎖的,只有在只讀 map 中找不到數(shù)據(jù)時(shí),才會(huì)加鎖訪問讀寫 map。
      2. 延遲寫入
        • 寫操作更新讀寫 map(dirty),但不會(huì)立即更新只讀 map(read)。只有當(dāng)讀操作發(fā)現(xiàn)只讀 map 中的數(shù)據(jù)過時(shí)時(shí),才會(huì)將讀寫 map 的數(shù)據(jù)同步到只讀 map 中。
      3. 內(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.Mutexsync.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)換的原因必然是類型兼容。

      1. 具體類型轉(zhuǎn)空接口時(shí),_type 字段直接復(fù)制源類型的 _type;調(diào)用 mallocgc 獲得一塊新內(nèi)存,把值復(fù)制進(jìn)去,data 再指向這塊新內(nèi)存。
      2. 具體類型轉(zhuǎn)非空接口時(shí),入?yún)?tab 是編譯器在編譯階段預(yù)先生成好的,新接口 tab 字段直接指向入?yún)?tab 指向的 itab;調(diào)用 mallocgc 獲得一塊新內(nèi)存,把值復(fù)制進(jìn)去,data 再指向這塊新內(nèi)存。
      3. 而對(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):

      1. 一種類型具有多種類型的能力
      2. 允許不同的對(duì)象對(duì)同一消息做出靈活的反應(yīng)
      3. 以一種通用的方式對(duì)待個(gè)使用的對(duì)象
      4. 非動(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-地鼠文檔

      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 常見的用途:

      1. 取消信號(hào)
        • context 可以用來向多個(gè) goroutine 傳遞取消信號(hào)。當(dāng)一個(gè) goroutine 需要取消其他 goroutine 時(shí),可以調(diào)用 contextCancelFunc
        • 例如,在處理 HTTP 請(qǐng)求時(shí),如果客戶端關(guān)閉了連接,可以使用 context 取消所有相關(guān)的后臺(tái)操作。
      2. 截止時(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í)間的等待。
      3. 傳遞請(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ù)。

      具體示例

      1. 創(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)
      }
      
      1. 創(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)
      }
      
      1. 傳遞請(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 是否線程安全?鎖用在什么地方?

      1. Golang的Channel,發(fā)送一個(gè)數(shù)據(jù)到Channel 和 從Channel接收一個(gè)數(shù)據(jù) 都是 原子性的。
      2. 而且Go的設(shè)計(jì)思想就是:不要通過共享內(nèi)存來通信,而是通過通信來共享內(nèi)存,前者就是傳統(tǒng)的加鎖,后者就是Channel。
      3. 也就是說,設(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)如下 :

      chan data structure

      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)單過程如下:

      1. 如果等待接收隊(duì)列recvq不為空,說明緩沖區(qū)中沒有數(shù)據(jù)或者沒有緩沖區(qū),此時(shí)直接從recvq取出G,并把數(shù)據(jù)寫入,最后把該G喚醒,結(jié)束發(fā)送過程;
      2. 如果緩沖區(qū)中有空余位置,將數(shù)據(jù)寫入緩沖區(qū),結(jié)束發(fā)送過程;
      3. 如果緩沖區(qū)中沒有空余位置,將待發(fā)送數(shù)據(jù)寫入G,將當(dāng)前G加入sendq,進(jìn)入睡眠,等待被讀goroutine喚醒;

      簡(jiǎn)單流程圖如下:

      接收流程:

      從一個(gè)channel讀數(shù)據(jù)簡(jiǎn)單過程如下:

      1. 如果等待發(fā)送隊(duì)列sendq不為空,且沒有緩沖區(qū),直接從sendq中取出G,把G中數(shù)據(jù)讀出,最后把G喚醒,結(jié)束讀取過程;
      2. 如果等待發(fā)送隊(duì)列sendq不為空,此時(shí)說明緩沖區(qū)已滿,從緩沖區(qū)中首部讀出數(shù)據(jù),把G中數(shù)據(jù)寫入緩沖區(qū)尾部,把G喚醒,結(jié)束讀取過程;
      3. 如果緩沖區(qū)中有數(shù)據(jù),則從緩沖區(qū)取出數(shù)據(jù),結(jié)束讀取過程;
      4. 將當(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)景還有:

      1. 關(guān)閉值為nil的channel
      2. 關(guān)閉已經(jīng)被關(guān)閉的channel
      3. 向已經(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)度流程:

      1. 創(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ì)列。
      2. 調(diào)度與執(zhí)行
        • 每個(gè) M 與一個(gè) P 綁定,M 從 P 的本地隊(duì)列中獲取一個(gè) G 來執(zhí)行。
        • 如果 P 的本地隊(duì)列為空,M 會(huì)嘗試從全局隊(duì)列或其他 P 的本地隊(duì)列中偷取(work stealing)任務(wù)執(zhí)行。
      3. 系統(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)歷如上圖所示的過程:

      1. runtime創(chuàng)建最初的線程m0和goroutine g0,并把2者關(guān)聯(lián)。
      2. 調(diào)度器初始化:初始化m0、棧、垃圾回收,以及創(chuàng)建和初始化由GOMAXPROCS個(gè)P構(gòu)成的P列表。
      3. 示例代碼中的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ì)列。
      4. 啟動(dòng)m0,m0已經(jīng)綁定了P,會(huì)從P的本地隊(duì)列獲取G,獲取到main goroutine。
      5. G擁有棧,M根據(jù)G中的棧信息和調(diào)度信息設(shè)置運(yùn)行環(huán)境
      6. M運(yùn)行G
      7. 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-地鼠文檔
      rwmutex-地鼠文檔

      除了 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)正常模式

      1. 當(dāng)前的mutex只有一個(gè)goruntine來獲取,那么沒有競(jìng)爭(zhēng),直接返回。
      2. 新的goruntine進(jìn)來,如果當(dāng)前mutex已經(jīng)被獲取了,則該goruntine進(jìn)入一個(gè)先入先出的waiter隊(duì)列,在mutex被釋放后,waiter按照先進(jìn)先出的方式獲取鎖。該goruntine會(huì)處于自旋狀態(tài)(不掛起,繼續(xù)占有cpu)。
      3. 新的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)換成正常模式:

      1. 此 waiter 已經(jīng)是隊(duì)列中的最后一個(gè) waiter 了,沒有其它的等待鎖的 goroutine 了;
      2. 此 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è)例子中,

      1. Spinlock 結(jié)構(gòu)體有一個(gè) int32 類型的字段 locked,用于表示鎖的狀態(tài)。
      2. Lock 方法使用 atomic.CompareAndSwapInt32 原子操作來嘗試將 locked 從0(未鎖定)更改為1(已鎖定)。如果鎖已經(jīng)被另一個(gè)goroutine持有(即 locked 為1),則 CompareAndSwapInt32 會(huì)返回 false,并且循環(huán)會(huì)繼續(xù)。
      3. 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)化:

      1. 減少自旋鎖的使用
        評(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),從而降低自旋的需求。
      2. 優(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。這可以在一定程度上緩解自旋鎖帶來的資源占用問題。
      3. 監(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)。
      4. 利用 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/
      這篇文章的目錄是:

      1. 為什么需要協(xié)程池?
      2. 簡(jiǎn)單的協(xié)程池
      3. go-playground/pool
      4. 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)

      1. 減少內(nèi)存分配和垃圾回收(GC)壓力:通過復(fù)用已經(jīng)分配的對(duì)象,<font style="color:rgb(5, 7, 59);">sync.Pool</font> 可以顯著減少內(nèi)存分配的次數(shù),從而減輕 GC 的壓力,提高程序的性能。
      2. 并發(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ì)象,而無需額外的同步措施。
      3. 自動(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ì)象

      使用方法

      1. 創(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 為你的類型  
          },  
      }
      
      1. 獲取對(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)行類型斷言
      
      1. 使用對(duì)象:獲取到對(duì)象后,你可以像使用普通對(duì)象一樣使用它。
      2. 放回對(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)

      1. 對(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)。
      2. 自動(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ì)象。
      3. 不適合所有場(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)。
      • 分代收集按照對(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)記的目的

      1. 主要是利用Tracing GC(Tracing GC 是垃圾回收的一個(gè)大類,另外一個(gè)大類是引用計(jì)數(shù)) 做增量式垃圾回收,降低最大暫停時(shí)間
      2. 原生Tracing GC只有黑色和白色,沒有中間的狀態(tài),這就要求GC掃描過程必須一次性完成,得到最后的黑色和白色對(duì)象。在前面增量式GC中介紹到了,這種方式會(huì)存在較大的暫停時(shí)間
      3. 三色標(biāo)記增加了中間狀態(tài)灰色,增量式GC運(yùn)行過程中,應(yīng)用線程的運(yùn)行可能改變了對(duì)象引用樹,只要讓黑色對(duì)象直接引用白色對(duì)象,GC就可以增量式的運(yùn)行,減少停頓時(shí)間

      什么是三色標(biāo)記?

      三色標(biāo)記,通過字面意思我們就可以知道它由3種顏色組成:

      1. 黑色 Black:表示對(duì)象是可達(dá)的,即使用中的對(duì)象,黑色是已經(jīng)被掃描的對(duì)象
      2. 灰色 Gary:表示被黑色對(duì)象直接引用的對(duì)象,但還沒對(duì)它進(jìn)行掃描
      3. 白色 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)化描述為以下步驟:

      1. GC 開始與根集合標(biāo)記
        • GC啟動(dòng)時(shí),會(huì)先進(jìn)行一次短暫的停頓(STW),以初始化GC的內(nèi)部狀態(tài)。
        • 在這次停頓中,所有活躍的對(duì)象(如全局變量、活躍棧幀中的指針等)被識(shí)別為根對(duì)象,并標(biāo)記為灰色,表示它們需要被進(jìn)一步掃描以確定其可達(dá)性。
      2. 并發(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ò)誤地回收。
      3. 棧上對(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>
      4. 標(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)存。
      5. 對(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é)。

      1. Golang程序啟動(dòng)時(shí)申請(qǐng)一大塊內(nèi)存,并劃分成spans、bitmap、arena區(qū)域
      2. arena區(qū)域按頁劃分成一個(gè)個(gè)小塊
      3. span管理一個(gè)或多個(gè)頁
      4. mcentral管理多個(gè)span供線程申請(qǐng)使用
      5. 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ù)變量是否被外部引用來決定是否逃逸:

      1. 如果函數(shù)外部沒有引用,則優(yōu)先放到棧中;
      2. 如果函數(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

      理解

      1. Gin 是一個(gè)用 Go 語言編寫的輕量級(jí) Web 框架,專注于高效的 HTTP 路由和中間件管理。它以簡(jiǎn)潔易用的 API 和極高的性能著稱,適合開發(fā) RESTful API 和 Web 服務(wù)
      2. Gin 的核心是路由機(jī)制,通過將 HTTP 請(qǐng)求路由到相應(yīng)的處理函數(shù)來實(shí)現(xiàn)。它支持路由分組,便于組織和管理復(fù)雜的路由結(jié)構(gòu)
      3. 同時(shí),Gin 提供了一套強(qiáng)大的中間件機(jī)制,允許在請(qǐng)求到達(dá)處理函數(shù)之前進(jìn)行預(yù)處理,如日志記錄、認(rèn)證、錯(cuò)誤處理等
      4. 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)景。

      特性

      1. 快速
        1. 基于 Radix 樹的路由,小內(nèi)存占用。沒有反射。可預(yù)測(cè)的 API 性能。
      2. 支持中間件
        1. 傳入的 HTTP 請(qǐng)求可以由一系列中間件和最終操作來處理。 例如:Logger,Authorization,GZIP,最終操作 DB。
      3. Crash 處理
        1. Gin 可以 catch 一個(gè)發(fā)生在 HTTP 請(qǐng)求中的 panic 并 recover 它。這樣,你的服務(wù)器將始終可用。例如,你可以向 Sentry 報(bào)告這個(gè) panic!
      4. JSON 驗(yàn)證
        1. Gin 可以解析并驗(yàn)證請(qǐng)求的 JSON,例如檢查所需值的存在。
      5. 路由組
        1. 更好地組織路由。是否需要授權(quán),不同的 API 版本…… 此外,這些組可以無限制地嵌套而不會(huì)降低性能
        2. Gin 使用基于樹狀結(jié)構(gòu)的路由匹配算法,能夠快速地匹配 URL 路徑
      6. 錯(cuò)誤管理
        1. Gin 提供了一種方便的方法來收集 HTTP 請(qǐng)求期間發(fā)生的所有錯(cuò)誤。最終,中間件可以將它們寫入日志文件,數(shù)據(jù)庫并通過網(wǎng)絡(luò)發(fā)送。
      7. 內(nèi)置渲染
        1. Gin 為 JSON,XML 和 HTML 渲染提供了易于使用的 API。
      8. 可擴(kuò)展性
        1. 新建一個(gè)中間件非常簡(jiǎn)單,去查看示例代碼吧。

      Gin路由機(jī)制

      Gin 的路由機(jī)制非常靈活和高效,主要有以下幾個(gè)方面:

      1. 路由定義
        Gin 使用 *gin.Engine 對(duì)象來定義路由。可以通過 GETPOST 等方法為不同的 HTTP 請(qǐng)求定義處理函數(shù)。例如:
      r := gin.Default()
      r.GET("/ping", func(c *gin.Context) {
          c.String(http.StatusOK, "pong")
      })
      r.Run()
      
      1. 路由組
        Gin 支持通過 Group 方法創(chuàng)建路由組,方便管理相關(guān)的路由。例如:
      v1 := r.Group("/v1")
      {
          v1.GET("/users", getUsers)
          v1.GET("/posts", getPosts)
      }
      
      1. 路由參數(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)
      })
      
      1. 路由方法
        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ù)。
      2. 路由優(yōu)先級(jí)
        更具體的路由定義優(yōu)先匹配,例如帶有路徑參數(shù)的路由會(huì)比通用的路由更優(yōu)先匹配。
      3. 中間件
        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è)步驟:

      1. 請(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 的核心組件。
      2. 路由匹配
        Gin 根據(jù)請(qǐng)求的 URL 和 HTTP 方法(如 GET、POST)來匹配路由。框架會(huì)查找定義的路由規(guī)則,并找到與請(qǐng)求最匹配的處理函數(shù)(Handler)
      3. 中間件處理
        在執(zhí)行路由處理函數(shù)之前,Gin 會(huì)依次執(zhí)行與該路由關(guān)聯(lián)的中間件。中間件可以用于請(qǐng)求的預(yù)處理,如認(rèn)證、日志記錄等。
      4. 執(zhí)行處理函數(shù)
        中間件執(zhí)行完畢后,Gin 會(huì)調(diào)用匹配的路由處理函數(shù)。處理函數(shù)可以訪問請(qǐng)求數(shù)據(jù)、處理業(yè)務(wù)邏輯,并準(zhǔn)備響應(yīng)數(shù)據(jù)。
      5. 生成響應(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() 等。
      6. 響應(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ì)之初參考了其他開源框架 fasthttpginecho 的優(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)支持 ThriftKitex ProtobufgRPC。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é)議有 TTHeaderHTTP2。TTHeader 可以和 Thrift、Kitex Protobuf 結(jié)合使用;HTTP2 目前主要是結(jié)合 gRPC 協(xié)議使用,后續(xù)也會(huì)支持 Thrift。
      • 多種消息類型支持 PingPongOneway雙向 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)置代碼生成工具,可支持生成 ThriftProtobuf 以及腳手架代碼。

      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):

      1. 代碼生成:通過分析數(shù)據(jù)庫表結(jié)構(gòu),自動(dòng)生成對(duì)應(yīng)的 Golang 結(jié)構(gòu)體和數(shù)據(jù)庫操作代碼。這些代碼通常包括基礎(chǔ)的增刪改查方法,還可以根據(jù)需求生成自定義查詢。
      2. 自動(dòng)更新:當(dāng)數(shù)據(jù)庫表結(jié)構(gòu)發(fā)生變化時(shí),GORM Gen 可以重新生成代碼,保證數(shù)據(jù)訪問層與數(shù)據(jù)庫結(jié)構(gòu)的一致性,減少手動(dòng)維護(hù)代碼的麻煩。
      3. 增強(qiáng)的類型安全:生成的代碼類型更為安全,避免了常見的類型轉(zhuǎn)換錯(cuò)誤。
      4. 支持復(fù)雜查詢GORM Gen 可以生成支持復(fù)雜查詢的代碼,比如聯(lián)合查詢、條件查詢、分頁等,減少了開發(fā)者手寫復(fù)雜 SQL 的負(fù)擔(dān)

      使用 GORM Gen 的流程:

      1. 安裝:首先需要通過 go install 安裝 GORM Gen 工具。
      2. 配置:使用 YAML 文件或在代碼中配置數(shù)據(jù)庫連接等相關(guān)信息。
      3. 生成代碼:通過運(yùn)行 GORM Gen 工具,自動(dòng)生成數(shù)據(jù)庫表對(duì)應(yīng)的 Golang 結(jié)構(gòu)體和 DAO 層代碼。
      4. 使用生成的代碼:在項(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
        使用 leaktestgoleak 工具檢測(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í))

      posted @ 2025-10-28 00:04  Lucas_coming  閱讀(23)  評(píng)論(0)    收藏  舉報(bào)
      主站蜘蛛池模板: 国产综合色在线精品| 亚洲国产午夜理论片不卡| 国产小受被做到哭咬床单GV| 61精品人妻一区二区三区| 亚洲av麻豆aⅴ无码电影| 午夜无码免费福利视频网址| 成在线人视频免费视频| 国产嫩草精品网亚洲av| 成年女人免费碰碰视频| 国产欧美日韩综合精品一区二区| 亚洲人妻精品一区二区| 成人福利国产午夜AV免费不卡在线 | 亚洲区欧美区综合区自拍区| 亚洲精品区午夜亚洲精品区| 深夜释放自己在线观看| 国产日韩成人内射视频| 日韩精品一区二区三区激情| 久久国产精品乱子乱精品| 99热国产这里只有精品9| 泾川县| 日韩高清视频 一区二区| 国产熟女50岁一区二区| 亚洲成人动漫在线| 99热精品毛片全部国产无缓冲 | 午夜成人精品福利网站在线观看| 香港日本三级亚洲三级| 久久精品国产国产精品四凭| 人妻中文字幕一区二区视频| 激情综合色综合啪啪开心| 免费大片av手机看片高清 | 免费现黄频在线观看国产| 和林格尔县| 国产成人无码免费视频麻豆| 国产在线98福利播放视频| 国产91色综合久久免费| 激情自拍校园春色中文| 惠来县| AV人摸人人人澡人人超碰| 亚洲AV天天做在线观看| 中文字幕人乱码中文| 99久久精品国产熟女拳交|