中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當(dāng)前位置: 首頁 > news >正文

以網(wǎng)站域名做郵箱怎樣做企業(yè)宣傳推廣

以網(wǎng)站域名做郵箱,怎樣做企業(yè)宣傳推廣,哪個公司做企業(yè)網(wǎng)站好,溫江建設(shè)網(wǎng)站文章目錄 Golang面試題總結(jié)一、基礎(chǔ)知識1、defer相關(guān)2、rune 類型3、context包4、Go 競態(tài)、內(nèi)存逃逸分析5、Goroutine 和線程的區(qū)別6、Go 里面并發(fā)安全的數(shù)據(jù)類型7、Go 中常用的并發(fā)模型8、Go 中安全讀寫共享變量方式9、Go 面向?qū)ο笫侨绾螌崿F(xiàn)的10、make 和 new 的區(qū)別11、Go 關(guān)…

文章目錄

    • Golang面試題總結(jié)
    • 一、基礎(chǔ)知識
      • 1、defer相關(guān)
      • 2、rune 類型
      • 3、context包
      • 4、Go 競態(tài)、內(nèi)存逃逸分析
      • 5、Goroutine 和線程的區(qū)別
      • 6、Go 里面并發(fā)安全的數(shù)據(jù)類型
      • 7、Go 中常用的并發(fā)模型
      • 8、Go 中安全讀寫共享變量方式
      • 9、Go 面向?qū)ο笫侨绾螌崿F(xiàn)的
      • 10、make 和 new 的區(qū)別
      • 11、Go 關(guān)閉(退出) goroutine 的方式
      • 12、Gorm更新空值問題
      • 13、Go語言優(yōu)勢和缺點
      • 14、Go 為什么要使用協(xié)程?
      • 15、內(nèi)存對齊
      • 16、反射
      • 17、go中一個地址占多少位?
      • 18、go 包循環(huán)引用?怎么避免?
      • 19、go 閉包調(diào)用?怎么避免?
      • 20、結(jié)構(gòu)體比較
      • 21、go 語言中的可比較類型和不可比較類型
      • 22、interface 接口
      • 23、空結(jié)構(gòu)體 struct 應(yīng)用場景
      • 24、go 內(nèi)存泄漏
      • 25、協(xié)程泄露
      • 26、值傳遞和地址傳遞(引用傳遞)
      • 27、go 語言中棧的空間有多大?
      • 28、并發(fā)情況下的數(shù)據(jù)處理,避免并發(fā)情況下數(shù)據(jù)競爭問題?
    • 二、channel 通道
      • 1、底層數(shù)據(jù)結(jié)構(gòu)
      • 2、channel為什么能做到線程安全?
      • 3、無緩沖的 channel 和 有緩沖的 channel 的區(qū)別?
      • 4、channel 死鎖的場景
      • 5、操作 channel 的情況總結(jié)
    • 三、map 哈希表
      • 1、map 的底層數(shù)據(jù)結(jié)構(gòu)是什么?
      • 2、map的擴(kuò)容
      • 3、從未初始化的 map 讀數(shù)據(jù)會發(fā)生什么?
      • 4、map 中的 key 為什么是無序的?怎么實現(xiàn)有序?
      • 5、map并發(fā)訪問安全嗎?怎么解決?可以邊遍歷邊刪除嗎?
      • 6、map元素可以取地址嗎?
      • 7、map 中刪除一個 key,它的內(nèi)存會釋放么?
      • 8、什么樣的類型可以做 map 的鍵 key?
      • 9、如何比較兩個 map 相等?
      • 10、map怎么解決哈希沖突?
      • 11、map 使用中注意的點?
      • 12、map 創(chuàng)建、賦值、刪除、查詢的過程?
    • 四、slice 切片
      • 1、數(shù)組和切片的區(qū)別
      • 2、slice 底層數(shù)據(jù)結(jié)構(gòu)
      • 3、slice 的擴(kuò)容
      • 4、slice 的拷貝
      • 5、append 函數(shù)
      • 6、切片作為函數(shù)參數(shù)?
      • 7、切片 slice 使用時注意的點?
      • 8、slice 內(nèi)存泄露情況
      • 9、slice 并發(fā)不安全
      • 10、從未初始化的 slice讀數(shù)據(jù)會發(fā)生什么?

Golang面試題總結(jié)

一、基礎(chǔ)知識

1、defer相關(guān)

??defer是Go語言中的一個關(guān)鍵字,延遲調(diào)用。

  • 作用
    ??釋放資源、釋放鎖、關(guān)閉文件、關(guān)閉鏈接、捕獲panic等收尾工作。

  • 執(zhí)行順序
    ??多個 defer 調(diào)用順序是 LIFO(后入先出),defer后的操作可以理解為壓入棧中,函數(shù)參數(shù)會被拷?下來。
    ??defer聲明時,對應(yīng)的參數(shù)會實時解析。
    ??defer、return、返回值三者的執(zhí)行邏輯:return最先執(zhí)行,return負(fù)責(zé)將結(jié)果寫入返回值中;接著defer開始執(zhí)行一些收尾工作;最后函數(shù)攜帶當(dāng)前返回值(可能和最初的返回值不相同)退出。
    ??defer與panic的執(zhí)行邏輯:在panic語句后面的defer語句不被執(zhí)行,在panic語句前的defer語句會被執(zhí)行(早于panic),panic觸發(fā)defer出棧??梢栽赿efer中使用recover捕獲異常,panic 后依然有效。panic僅有最后一個可以被revover捕獲。

  • defer與recover
    ??recover(異常捕獲)可以讓程序在引發(fā)panic的時候不會崩潰退出。在引發(fā)panic的時候,panic會停掉當(dāng)前正在執(zhí)?的程序,但是,在這之前,它會有序的執(zhí)?完當(dāng)前goroutine的defer列表中的語句。
    ??我們通常在defer??掛?個recover,防?程序直接掛掉,類似于try…catch,但絕對不能像try…catch這樣使?,因為panic的作?不是為了捕獲異常。recover函數(shù)只在defer的上下?中才有效,如果直接調(diào)?recover,會返回nil。
    ??recover不能跨協(xié)程捕獲panic信息。recover只能恢復(fù)同一個協(xié)程中的panic,所以必須與可能發(fā)生panic的協(xié)程在同一個協(xié)程中才生效。panic在子協(xié)程中,而recover在主協(xié)程中,最終會導(dǎo)致所有的協(xié)程全部掛掉,程序會整體退出。

  • defer可以修改函數(shù)最終返回值
    ??修改時機(jī):有名返回值或者函數(shù)返回指針。
    ??無名返回(返回值沒有指定命名),執(zhí)行Return語句后,Go會創(chuàng)建一個臨時變量保存返回值,defer修改的是臨時變量,沒有修改返回值。
    ??有名返回(指定返回值命名func test() (t int)),執(zhí)行 return 語句時,并不會再創(chuàng)建臨時變量保存,defer修改的是返回值。
    ??函數(shù)返回值為指針,指向變量所在的地址,defer修改變量,指針指向的地址不變,地址對應(yīng)的內(nèi)容發(fā)生了改變,返回值改變。

??特殊例子:defer沒有修改有名返回值,因為 r 作為參數(shù),傳入defer 內(nèi)部時會發(fā)生值拷貝,地址會變,defer修改的是新地址的變量,不是原來的返回值。

func f() (r int) {defer func(r int) {r = r + 5}(r)return r
}
//0

為什么defer要按照定義的順序逆序執(zhí)行?
??后?定義的函數(shù)可能會依賴前?的資源,所以要先執(zhí)?。如果前?先執(zhí)?,釋放掉這個依賴,那后?的函數(shù)就找不到它的依賴了。

3、defer函數(shù)定義時,對外部變量的引??式有兩種
分別是函數(shù)參數(shù)以及作為閉包引?。
在作為函數(shù)參數(shù)的時候,在defer定義時就把值傳遞給defer,并被緩存起來。
如果是作為閉包引?,則會在defer真正調(diào)?的時候,根據(jù)整個上下?去確定當(dāng)前的值。

4、defer后?的語句在執(zhí)?的時候,函數(shù)調(diào)?的參數(shù)會被保存起來,也就是復(fù)制。在真正執(zhí)?的時候,實際上?到的是復(fù)制的變量,也就是說,如果這個變量是?個"值類型",那他就和定義的時候是?致的,如果是?個"引?",那么就可能和定義的時候的值不?致。

2、rune 類型

??Go語言的字符有以下兩種:

  • byte 等同于uint8,常用來處理 ASCII 碼;
  • rune 等同于int32,常用來處理 unicode 或 utf-8 字符。當(dāng)需要處理中文、日文或者其他復(fù)合字符時,使用 rune 類型。

??在 Go 語言中,字符串默認(rèn)使用 UTF-8 編碼,UTF8 的好處在于,如果基本是英文,每個字符占 1 byte,和 ASCII 編碼是一樣的,非常節(jié)省空間,如果是中文,一般占3字節(jié)。中文字符在unicode下占2個字節(jié),在utf-8編碼下占3個字節(jié)。

在Go中,字符串是以UTF-8編碼格式進(jìn)行存儲的。在UTF-8編碼中,一個漢字通常占用24位(3字節(jié))。英文字符(包括英文標(biāo)點)通常占用8位(1字節(jié))1個字節(jié)。

??字符串的底層表示是 byte (8 bit) 序列,而非 rune (32 bit) 序列。包含中文的字符串,正確的處理方式是將 string 轉(zhuǎn)為 rune 數(shù)組,轉(zhuǎn)換成 []rune 類型后,字符串中的每個字符,無論占多少個字節(jié)都用 int32 來表示,因而可以正確處理中文。

func main() {str := "我愛GO"fmt.Println(reflect.TypeOf(str[0]).Kind()) //uint8fmt.Println(len(str))                      //8runeStr := []rune(str)runeStr[0] = '你'fmt.Println(reflect.TypeOf(runeStr[0]).Kind()) //int32fmt.Println(string(runeStr))                   //你愛GOfmt.Println(len(runeStr))                      //4
}
// reflect.TypeOf().Kind() 可以知道某個變量的類型

3、context包

??context是Golang常用的并發(fā)控制技術(shù),context的作用就是在不同的goroutine之間同步請求特定的數(shù)據(jù)、取消信號以及處理請求的截止日期(設(shè)置超時時間)。目前我們常用的一些庫都是支持context的,例如gin、database/sql等庫都是支持context的,這樣更方便我們做并發(fā)控制了,只要在服務(wù)器入口創(chuàng)建一個context上下文,不斷透傳下去即可。

??context可以用來在goroutine之間傳遞上下文信息,相同的context可以傳遞給運行在不同goroutine中的函數(shù),上下文對于多個goroutine同時使用是安全的,它與WaitGroup最大的不同點是context對于派生goroutine有更強(qiáng)的控制力,它可以控制多級的goroutine。

??context包定義了上下文類型,可以使用context.Background()、context.TODO()創(chuàng)建一個上下文,也可以使用WithDeadline()、WithTimeout()、WithCancel() 或 WithValue() 創(chuàng)建。

??Go 的 Context 的數(shù)據(jù)結(jié)構(gòu)包含 Deadline(),Done(),Err(),Value()方法:

  • Deadline() 方法返回一個time.Time和一個bool值。time.Time表示此上下文被取消的時間,也就是完成工作的截至日期。布爾值表示是否設(shè)置截止日期,當(dāng)沒有設(shè)置截止日期時,bool值返回ok==false,此時截止日期為一個初始值的time.Time值。對Deadline的連續(xù)調(diào)用返回相同的結(jié)果。
  • Done() 方法返回一個channel。 這個 Channel 會在當(dāng)前工作完成或者context 被取消之后關(guān)閉,告訴給 context 相關(guān)的函數(shù)要停止當(dāng)前工作然后返回了。對Done的連續(xù)調(diào)用返回相同的值。需要在select-case語句中使用,如”case <-context.Done():”。當(dāng)context關(guān)閉后,Done()返回一個被關(guān)閉的管道,關(guān)閉的管道仍然是可讀的,據(jù)此goroutine可以收到關(guān)閉請求;當(dāng)context還未關(guān)閉時,Done()返回nil。
  • Err() 方法返回一個error。表示 context 被關(guān)閉的原因,關(guān)閉原因由context實現(xiàn)控制,不需要用戶設(shè)置。當(dāng)context關(guān)閉后,Err()返回context的關(guān)閉原因;當(dāng)context還未關(guān)閉時,Err()返回nil。Context 被取消,返回 “context canceled” 錯誤;超時,返回“context deadline exceeded”錯誤。
  • Value() 方法,參數(shù)為key,返回 Context 中 key 對應(yīng)的值,如果沒有值與鍵相關(guān)聯(lián),返回nil。對于同一個 Context 來說,多次調(diào)用Value() 并傳入相同的Key,會返回相同的結(jié)果,這個功能可以用來傳遞特定的數(shù)據(jù)。

4、Go 競態(tài)、內(nèi)存逃逸分析

??競態(tài):資源競爭,就是在程序中,同一塊內(nèi)存同時被多個 goroutine 訪問。我們使用 go build、go run、go test 命令時,可以添加 -race 標(biāo)識檢查代碼中是否存在資源競爭。

??解決方案:給資源進(jìn)行加鎖,讓其在同一時刻只能被一個協(xié)程來操作。
??sync.Mutex
??sync.RWMutex

??內(nèi)存逃逸分析:是go的編譯器在編譯期間,對代碼進(jìn)行分析,根據(jù)變量的類型和作用域,確定變量是分配在堆上還是棧上,如果變量需要分配在堆上,則稱作內(nèi)存逃逸了。簡單來說,本該分配到棧上的變量,跑到了堆上,這就導(dǎo)致了內(nèi)存逃逸。go的編譯器提供了逃逸分析的工具,只需要在編譯的時候加上 -gcflags=-m 就可以看到逃逸分析的結(jié)果了。

??為什么需要逃逸分析?
??因為go語言是自動內(nèi)存管理的,也就是有GC的。開發(fā)者在寫代碼的時候不需要關(guān)心考慮內(nèi)存釋放的問題,這樣編譯器和go運行時(runtime)就需要準(zhǔn)確分配和管理內(nèi)存,所以編譯器在編譯期間要確定變量是放在堆空間和??臻g。
??棧是高地址到低地址,棧上的變量,函數(shù)結(jié)束后變量會跟著回收掉,不會有額外性能的開銷。變量從棧逃逸到堆上,如果要回收掉,需要進(jìn)行 gc,那么 gc 一定會帶來額外的性能開銷。編程語言不斷優(yōu)化 gc 算法,主要目的都是為了減少 gc 帶來的額外性能開銷,變量一旦逃逸會導(dǎo)致性能開銷變大。

??出現(xiàn)內(nèi)存逃逸的場景:

  • 指針逃逸
    ??返回局部變量的指針、向 channel 發(fā)送指針或帶有指針的數(shù)據(jù)、在 slice 或 map 中存儲指針或帶有指針的值。
  • 動態(tài)類型逃逸
    ??當(dāng)函數(shù)傳遞的變量類型是 interface{} 類型的時候,因為編譯器無法推斷運行時變量的實際類型,所以也會發(fā)生逃逸。
  • ??臻g不足逃逸
    ??切片(擴(kuò)容后)長度太大,因為棧的空間是有限的,所以在分配大塊內(nèi)存時,會考慮??臻g內(nèi)否存下,如果棧空間存不下,會分配到堆上。
  • 閉包引用對象逃逸
    ??在閉包中引用包外的值。

5、Goroutine 和線程的區(qū)別

  • 進(jìn)程:進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個獨立單位,分配完整獨立的地址空間,擁有自己獨立的堆和棧,既不共享堆,亦不共享棧,進(jìn)程的切換只發(fā)生在內(nèi)核態(tài),由操作系統(tǒng)調(diào)度,進(jìn)程間的切換開銷(棧、寄存器、虛擬內(nèi)存、文件句柄等)比較大,但相對比較穩(wěn)定安全。不同進(jìn)程通過進(jìn)程間通信來通信。

  • 線程:是CPU調(diào)度和分派的基本單位,和其它本進(jìn)程的線程共享地址空間,擁有自己獨立的棧和共享的堆,共享堆,不共享棧,線程的切換一般也由操作系統(tǒng)調(diào)度(標(biāo)準(zhǔn)線程是的)。 線程間通信主要通過共享內(nèi)存,上下文切換很快,資源開銷較少,但相比進(jìn)程不夠穩(wěn)定容易丟失數(shù)據(jù)。

  • 協(xié)程:協(xié)程是一種用戶態(tài)的輕量級線程,協(xié)程的調(diào)度完全由用戶控制。共享堆,不共享棧。協(xié)程擁有自己的寄存器上下文和棧。協(xié)程調(diào)度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復(fù)先前保存的寄存器上下文和棧,直接操作棧則基本沒有內(nèi)核切換的開銷,可以不加鎖的訪問全局變量,所以上下文的切換非常快。

??進(jìn)程和線程的切換主要依賴于時間片的控制,而協(xié)程的切換則主要依賴于自身。

??goroutine是非常輕量級的,它就是一段代碼,一個函數(shù)入口,以及在堆上為其分配的一個堆棧(在64位機(jī)器上,初始化大小為2KB,最大為1GB,會隨著程序的執(zhí)行自動增長刪除),所以它非常廉價,我們可以很輕松的創(chuàng)建上萬個goroutine。

??在 go 語言中,每個 goroutine 默認(rèn)使用比較小的??臻g(通常只有幾 kb),用于保存其執(zhí)行狀態(tài)、臨時變量等數(shù)據(jù)。如果需要更大的??臻g,則會動態(tài)地進(jìn)行擴(kuò)容,最終實現(xiàn)在堆上分配內(nèi)存,并將原來的棧數(shù)據(jù)復(fù)制到新的堆內(nèi)存位置中。

??因此,可以說 goroutine 在創(chuàng)建時先分配在棧上,當(dāng)彈出棧無法滿足內(nèi)存需求時,將重新分配在堆上。但從整體上看,go 運行時負(fù)責(zé)管理所有 goroutine 的內(nèi)存分配,具體實現(xiàn)方式對于開發(fā)者來說可能并不透明或關(guān)鍵。

6、Go 里面并發(fā)安全的數(shù)據(jù)類型

(1)由一條機(jī)器指令完成賦值的類型并發(fā)賦值是安全的,這些類型有:字節(jié)型,布爾型、整型、浮點型、字符型、指針、函數(shù)。
(2)數(shù)組由一個或多個元素組成,大部分情況并發(fā)不安全。注意:當(dāng)位寬不大于 64 位且是 2 的整數(shù)次冪(8,16,32,64),那么其并發(fā)賦值是安全的。
(3)struct 或底層是 struct 的類型并發(fā)賦值大部分情況并發(fā)不安全,這些類型有:復(fù)數(shù)、字符串、 數(shù)組、切片 slice、字典 map、接口 interface。

??注意:當(dāng) struct 賦值時退化為單個字段由一個機(jī)器指令完成賦值時,并發(fā)賦值又是安全的。這種情況有:
(a)實部或虛部相同的復(fù)數(shù)的并發(fā)賦值;
(b)等長字符串的并發(fā)賦值;
(c)同長度同容量切片的并發(fā)賦值;
(d)同一種具體類型不同值并發(fā)賦給接口。

7、Go 中常用的并發(fā)模型

  • 通過channel通知實現(xiàn)并發(fā)控制

  • 通過sync包中的WaitGroup實現(xiàn)并發(fā)控制
    ??Goroutine是異步執(zhí)行的,有的時候為了防止在結(jié)束mian函數(shù)的時候結(jié)束掉Goroutine,所以需要同步等待,這個時候就需要用 WaitGroup了,在 sync 包中,提供了 WaitGroup ,它會等待它收集的所有 goroutine 任務(wù)全部完成。在WaitGroup里主要有三個方法:Add()可以添加或減少 goroutine的數(shù)量,Done()相當(dāng)于Add(-1),Wait()執(zhí)行后會堵塞主線程,直到WaitGroup 里的值減至0。
    ??在主 goroutine 中 Add(delta int) 索要等待goroutine 的數(shù)量,在每一個 goroutine 完成后 Done() 表示這一個goroutine 已經(jīng)完成,當(dāng)所有的 goroutine 都完成后,在主 goroutine 中 WaitGroup 返回返回。

  • 在Go 1.7 以后引進(jìn)的強(qiáng)大的Context上下文,實現(xiàn)并發(fā)控制

8、Go 中安全讀寫共享變量方式

go中除了加Mutex鎖以外還有哪些方式安全讀寫共享變量?
??go 中 Goroutine 可以通過 Channel 進(jìn)行安全讀寫共享變量,而且官網(wǎng)建議使用這種方式,此方式的并發(fā)是由官方進(jìn)行保證的。

map
在并發(fā)情況下,只讀是線程安全的,同時讀寫是線程不安全的。
1、在寫操作的時候增加鎖
2、sync.Map包

數(shù)組
指定索引進(jìn)行讀寫時,數(shù)組是支持并發(fā)讀寫索引區(qū)的數(shù)據(jù)的,但是索引區(qū)的數(shù)據(jù)在并發(fā)時會被覆蓋的;
1、加鎖
2、控制并發(fā)順序

切片
指定索引進(jìn)行讀寫:是支持并發(fā)讀寫索引區(qū)的數(shù)據(jù)的,但是索引區(qū)的數(shù)據(jù)在并發(fā)時可能會被覆蓋的;
發(fā)生切片動態(tài)擴(kuò)容:并發(fā)場景下擴(kuò)容可能會被覆蓋。
1、加互斥鎖
2、使用channel串行化操作
3、使用sync.map代替切片

9、Go 面向?qū)ο笫侨绾螌崿F(xiàn)的

??Go實現(xiàn)面向?qū)ο蟮膬蓚€關(guān)鍵是struct和interface。

??封裝:對于同一個包,對象對包內(nèi)的文件可見;對不同的包,需要將對象以大寫字母開頭才是可見的。

??繼承:繼承是編譯時特征,go語言通過結(jié)構(gòu)體組合來實現(xiàn)繼承,在struct內(nèi)加入所需要繼承的類即可。Go支持多重繼承,就是在類型中嵌入所有必要的父類型。

type A struct{}
type B struct{A
}

??多態(tài):多態(tài)是運行時特征,Go多態(tài)通過interface來實現(xiàn)。類型和接口是松耦合的,某個類型的實例可以賦給它所實現(xiàn)的任意接口類型的變量。

10、make 和 new 的區(qū)別

  • 相同點:
    (1)都是給變量分配內(nèi)存;
    (2)都是在堆上分配內(nèi)存。

  • 不同點:
    (1)作用變量類型不同,new給string,int和數(shù)組分配內(nèi)存,make給切片,map,channel分配內(nèi)存;
    (2)返回類型不一樣,new返回指向變量的指針,make返回變量本身;
    (3)new 分配的空間被清零,make 分配空間后,會進(jìn)行初始化;

11、Go 關(guān)閉(退出) goroutine 的方式

  • 向退出通道發(fā)送退出信號(退出一個 goroutine)
  • 關(guān)閉退出通道(退出多個 goroutine)
  • 使用 context.WithCancel() 方法,手動調(diào)用 cancel() 方法退出 goroutine
  • 使用 context.WithTimeout() 方法,手動調(diào)用 cancel() 方法,在超時之前退出 goroutine
  • 使用 context.WithDeadLine() 方法,在指定的時間退出 goroutine

12、Gorm更新空值問題

問題:在使用gorm將一個字段更新為空的時候,發(fā)現(xiàn)并不生效?

原因:通過 struct 結(jié)構(gòu)體變量更新字段值,gorm 會忽略零值字段,如果更新后的值為0, nil, “”, false,就不會更新該字段,而是只更新非空的其他字段。

解決方案:
(1)使用 map 類型替代 struct 結(jié)構(gòu)體,更新傳值的時候通過 map 來指定,key為字符串,value為 interface{} 類型,方便保存任意值。
(2)采用save的方式,先take獲取源數(shù)據(jù),然后在save進(jìn)行保存。
(3)修改gorm的源碼包,讓它支持自定義是否可以設(shè)置為空值。

13、Go語言優(yōu)勢和缺點

優(yōu)勢:
1、并發(fā)編程:Go語言天生支持并發(fā),內(nèi)置了輕量級的協(xié)程(goroutine)和通信機(jī)制(channel),使得并發(fā)編程變得非常簡單。這種并發(fā)模型是Go語言最大的特點之一,也是其在網(wǎng)絡(luò)編程、高并發(fā)處理等領(lǐng)域得以廣泛應(yīng)用的原因。
2、高效性能:Go語言使用靜態(tài)編譯,可以生成本地代碼,且具有快速的垃圾回收機(jī)制,使得其在性能上有很好的表現(xiàn)。
3、簡單易學(xué):Go語言的語法簡潔清晰,易于學(xué)習(xí)和理解,并且沒有像C++或Java那樣復(fù)雜的繼承、多態(tài)等概念。
4、跨平臺支持:Go語言提供了跨平臺的編譯工具,可以在不同操作系統(tǒng)和硬件上編譯出可執(zhí)行文件。
5、開源社區(qū)支持:Go語言擁有一個龐大的開源社區(qū),提供了大量的第三方庫和工具,可以方便地實現(xiàn)各種功能。

性能高、編譯快、開發(fā)效率和運行效率高:Go性能與 Java 或 C++相似,比C++開發(fā)效率高,比php和python快,與Java比,更簡明的類型系統(tǒng),go在語法簡明和類型系統(tǒng)設(shè)計上優(yōu)于python。
豐富的標(biāo)準(zhǔn)庫:Go目前已經(jīng)內(nèi)置了大量的庫,特別是網(wǎng)絡(luò)庫非常強(qiáng)大。
內(nèi)置強(qiáng)大的工具:內(nèi)置了很多工具鏈,Go擁有強(qiáng)大的編譯檢查、嚴(yán)格的編碼規(guī)范和完整的軟件生命周期工具,具有很強(qiáng)的穩(wěn)定性,Go提供了軟件生命周期(開發(fā)、測試、部署、維護(hù)等等)的各個環(huán)節(jié)的工具,如go tool、gofmt、go test。

缺點:
1、生態(tài)系統(tǒng)相對不夠完善:雖然Go語言擁有龐大的開源社區(qū),但相對其他成熟的編程語言如Python和Java,其生態(tài)系統(tǒng)還不夠完善,一些開發(fā)工具和庫的支持還不夠全面。
2、語言特性相對較少:為了保持簡潔,Go語言在一些高級特性上犧牲了部分靈活性。例如,Go語言沒有泛型、繼承、異常等概念,這些限制可能會影響一些特定領(lǐng)域的開發(fā)。
3、不適合大型項目:雖然Go語言在小型或中型項目中表現(xiàn)優(yōu)異,但由于其缺乏一些高級特性和完善的生態(tài)系統(tǒng)支持,不適合用于大型復(fù)雜項目的開發(fā)。

14、Go 為什么要使用協(xié)程?

??協(xié)程是一種用戶態(tài)的輕量級線程,協(xié)程的調(diào)度完全由用戶控制,由于協(xié)程運行在用戶態(tài),能夠大大減少上下文切換帶來的開銷,并且協(xié)程調(diào)度器把可運行的協(xié)程逐個調(diào)度到線程中執(zhí)行,同時及時把阻塞的協(xié)程調(diào)度出線程,從而有效的避免了線程的頻繁切換,達(dá)到了使用少量的線程實現(xiàn)高并發(fā)的效果,但是對于一個線程來說每一時刻只能運行一個協(xié)程。

??高并發(fā)+高擴(kuò)展性+低成本:一個CPU支持上萬的協(xié)程都不是問題。所以很適合用于高并發(fā)處理。

15、內(nèi)存對齊

1、什么是內(nèi)存對齊?
??編譯器會按照特定的規(guī)則,把數(shù)據(jù)安排到合適的存儲地址上,并占用合適的地址長度。每種類型的對齊值就是它的對齊邊界,內(nèi)存對齊要求數(shù)據(jù)存儲地址以及占用的字節(jié)數(shù)都要是它的對齊邊界的倍數(shù)。所以下述的int32要錯開兩個字節(jié),從4開始存,卻不能緊接著從2開始。
在這里插入圖片描述

2、為什么要內(nèi)存對齊?
??內(nèi)存對齊是為了減少CPU訪問內(nèi)存的次數(shù),加大 CPU 訪問內(nèi)存的吞吐量,提高CPU讀取內(nèi)存數(shù)據(jù)的效率,可以讓CPU快速從內(nèi)存中讀取到數(shù)據(jù),保證程序高效的運行,避免資源浪費。如果內(nèi)存不對齊,訪問相同的數(shù)據(jù)需要多次的訪問內(nèi)存。

??CPU 不會以一個字節(jié)一個字節(jié)的去讀取和寫入內(nèi)存,CPU 讀取內(nèi)存是一塊一塊讀取的,一塊內(nèi)存可以是2、4、8、16個字節(jié),塊大小稱為內(nèi)存訪問粒度,內(nèi)存訪問粒度跟機(jī)器字長有關(guān),32位CPU訪問粒度是4個字節(jié),64位CPU訪問粒度是8個字節(jié)。

??平臺原因(移植原因):不是所有的硬件平臺都能訪問任意地址上的任意數(shù)據(jù)的;某些硬件平臺只能在某些地址處取某些特定類型的數(shù)據(jù),否則拋出硬件異常。
??性能原因:數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應(yīng)該盡可能地在自然邊界上對齊。原因在于,為了訪問未對齊的內(nèi)存,處理器需要作兩次內(nèi)存訪問;而對齊的內(nèi)存訪問僅需要一次訪問。

3、內(nèi)存對齊規(guī)則
??起始的存儲地址 必須是 內(nèi)存對齊邊界 的倍數(shù)。
??整體占用字節(jié)數(shù) 必須是 內(nèi)存對齊邊界 的倍數(shù)。

16、反射

??Go語言的反射是指在運行時動態(tài)地獲取類型信息和操作對象的能力。這意味著程序可以檢查變量的類型、值,以及調(diào)用它們的方法。Go語言中的反射由reflect包提供支持,包括了一些常用的函數(shù)和數(shù)據(jù)類型,如TypeOf()、ValueOf()等。使用反射可以實現(xiàn)一些靈活的功能,如實現(xiàn)通用的序列化和反序列化,或者通過動態(tài)調(diào)用方法來實現(xiàn)類似于插件的機(jī)制。但是需要注意的是,過度使用反射可能會影響代碼的可讀性和性能,因此應(yīng)該謹(jǐn)慎使用。

17、go中一個地址占多少位?

??在管理內(nèi)存地址的硬件/操作系統(tǒng)上,Go語言中的指針通常使用64位(8字節(jié))來表示。

18、go 包循環(huán)引用?怎么避免?

1、為什么會出現(xiàn)循環(huán)引用問題?怎么發(fā)現(xiàn)?如何避免?
??程序結(jié)構(gòu)沒設(shè)計好,包沒有規(guī)劃好。
??有一個項目引用可視化的工具 godepgraph,可以查看包引用的關(guān)系,生成引用圖。
??項目框架構(gòu)建的時候,將各個模塊設(shè)計好,規(guī)劃好包。嘗試分層的設(shè)計,高層依賴于低層,低層不依賴于高層,嚴(yán)格規(guī)范單向調(diào)用鏈,如控制層->業(yè)務(wù)層->數(shù)據(jù)層。

2、go 為什么不允許循環(huán)引用?

  • 加快編譯速度;
  • 規(guī)范框架設(shè)計,使項目結(jié)構(gòu)更加清晰明了。

3、怎么解決?

  • 對于軟相互依賴,利用分包的方法就能解決,有些函數(shù)導(dǎo)致的相互依賴只能通過分包解決;分包能細(xì)化包的功能;

軟相互依賴可以通過將方法拆分到另一個包的方式來解決;在拆分包的過程中,可能會將結(jié)構(gòu)體的方法轉(zhuǎn)化為普通的函數(shù);

  • 對于硬相互依賴只能通過定義接口的方法解決;定義接口能提高包的獨立性,同時也提高了追蹤代碼調(diào)用關(guān)系的難度。

在 package b 中 定義 a interface ; 將 b 所有使用到結(jié)構(gòu)體 a 的變量和方法的地方全部轉(zhuǎn)化成 使用接口 a 的方法;在 a interface 中補充缺少的方法;

19、go 閉包調(diào)用?怎么避免?

1、什么是閉包調(diào)用?
??閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù),創(chuàng)建閉包的常見方式就是在一個函數(shù)內(nèi)部創(chuàng)建另一個函數(shù), 內(nèi)函數(shù)可以訪問外函數(shù)的變量。
??注意:閉包里作用域返回的局部變量不會被立刻銷毀回收,可能會占用更多內(nèi)存,過度使用閉包會導(dǎo)致性能下降。

2、帶來的問題?
??由于閉包會在其生命周期內(nèi)保留對環(huán)境變量的引用,因此可能會導(dǎo)致一些問題,例如:

  • 內(nèi)存泄漏:如果閉包持有對某些資源的引用,但又沒有及時釋放這些資源,就會導(dǎo)致內(nèi)存泄漏。
  • 競態(tài)條件:如果多個閉包同時訪問和修改同一個共享變量,就可能出現(xiàn)競態(tài)條件(race condition)的問題。
  • 函數(shù)返回值不確定:如果閉包引用了函數(shù)內(nèi)部的變量,那么當(dāng)函數(shù)返回后,在閉包被調(diào)用之前這些變量的值可能已經(jīng)被修改,從而導(dǎo)致閉包產(chǎn)生意外的行為。

3、怎么避免?

  • 聲明新變量,閉包函數(shù)使用新變量
  • 將變量通過函數(shù)參數(shù)形式傳遞進(jìn)閉包函數(shù)

20、結(jié)構(gòu)體比較

  • 2 個 interface 可以比較嗎 ?

??Go 語言中,接口(interface)是對非接口值(例如指針,struct 等)的封裝,內(nèi)部實現(xiàn)包含了 2 個字段,類型 T 和 值 V。接口類型的比較就演變成了結(jié)構(gòu)體比較。
??兩個接口類型比較時,會先比較 T,再比較 V。接口類型與非接口類型比較時,會先將非接口類型嘗試轉(zhuǎn)換為接口類型,再按接口比較的規(guī)則進(jìn)行比較。如果兩個接口變量底層類型和值完全相同(或同為 nil)則兩個變量相等,否則不等。

??接口類型比較時,如果底層類型不可比較,則會發(fā)生 panic。

package mainimport "fmt"type Animal interface {Speak() string
}type Duck struct {Name string
}func (a Duck) Speak() string {return "I'm " + a.Name
}type Cat struct {Name string
}func (a Cat) Speak() string {return "I'm " + a.Name
}type Bird struct {Name      stringSpeakFunc func() string
}func (a Bird) Speak() string {return "I'm " + a.SpeakFunc()
}// Animal 為接口類型,Duck 和 Cat 分別實現(xiàn)了該接口。
func main() {var d1, d2, c1 Animald1 = Duck{Name: "Donald Duck"}d2 = Duck{Name: "Donald Duck"}c1 = Cat{Name: "Donald Duck"}fmt.Println(d1 == d2) // 輸出 truefmt.Println(d1 == c1) // 輸出 false// 接口變量 d1、d2 底層類型同為 Duck 并且底層值相同,所以 d1 和 d2 相等。// 接口變量 c1 底層類型為 Cat,盡管底層值相同,但類型不同,c1 與 d1 也不相等。var animal Animalanimal = Duck{Name: "Donald Duck"}var duck Duckduck = Duck{Name: "Donald Duck"}fmt.Println(animal == duck) // 輸出 true// 當(dāng) struct 和接口進(jìn)行比較時,可以簡單地把 struct 轉(zhuǎn)換成接口然后再按接口比較的規(guī)則進(jìn)行判定。// animal 為接口變量,而 duck 為 struct 變量,底層類型同為 Duck 并且底層值相同,二者判定為相等。var b1 Animal = Bird{Name: "bird",SpeakFunc: func() string {return "I'm Poly"}}var b2 Animal = Bird{Name: "bird",SpeakFunc: func() string {return "I'm eagle"}}fmt.Println(b1 == b2)// panic: runtime error: comparing uncomparable type main.Bird// 結(jié)構(gòu)體 Bird 也實現(xiàn)了 Animal 接口,但結(jié)構(gòu)體中增加了一個不可比較的函數(shù)類型成員 SpeakFunc,// 因此 Bird 變成了不可比較類型,接口類型變量 b1 和 b2 底層類型為 Bird,在比較時會觸發(fā) panic。
}
  • 2 個 nil 可能不相等嗎?

??2 個 nil 類型可能不相等,兩個nil 只有在類型相同時才相等。例如,interface 在運行時綁定值,只有值為 nil 接口值才為 nil,但是與指針的 nil 不相等。

func main() {var p *int = nilvar i interface{}fmt.Println(p == nil) // 輸出 truefmt.Println(i == nil) // 輸出 truefmt.Println(i == p)   // 輸出 false
}
  • 結(jié)構(gòu)體比較

??結(jié)構(gòu)體是可以比較的,但前提是結(jié)構(gòu)體成員字段全部可以比較,并且結(jié)構(gòu)體成員字段類型、個數(shù)、順序也需要相同,當(dāng)結(jié)構(gòu)體成員全部相等時,兩個結(jié)構(gòu)體相等。

??特別注意的點,如果結(jié)構(gòu)體成員字段的順序不相同,那么結(jié)構(gòu)體也是不可以比較的。如果結(jié)構(gòu)體成員字段中有不可以比較的類型,如map、slice、function 等,那么結(jié)構(gòu)體也是不可以比較的。

func main() {sn1 := struct {age  intname string}{age: 11, name: "Zhang San"}sn2 := struct {age  intname string}{age: 11, name: "Zhang San"}fmt.Println(sn1 == sn2) // 輸出 truesn3 := struct {name stringage  int}{age: 11, name: "Zhang San"}fmt.Println(sn1 == sn3)// 錯誤提示:Invalid operation: sn1 == sn3 (mismatched types struct {...} and struct {...})sn4 := struct {name stringage  intgrade map[string]int}{age: 11, name: "Zhang San"}sn5 := struct {name stringage  intgrade map[string]int}{age: 11, name: "Zhang San"}fmt.Println(sn4 == sn5)// 錯誤提示:Invalid operation: sn4 == sn5 (the operator == is not defined on struct {...})
}

21、go 語言中的可比較類型和不可比較類型

??比較操作符分為等值操作符(== 和 !=)和排序操作符(<、<=、> 、 >=),等值操作符作用的操作數(shù)必須是可比較的,排序操作符作用的操作數(shù)必須是可排序的。

操作符變量類型
等值操作符 (==、!=)整型、浮點型、字符串、布爾型、復(fù)數(shù)、指針、管道、接口、結(jié)構(gòu)體、數(shù)組
排序操作符 (<、<=、> 、 >=)整型、浮點型、字符串
不可比較類型map、slice、function

??管道是可以比較的,管道本質(zhì)上是個指針,make 語句生成的是一個管道的指針,所以管道的比較規(guī)則與指針相同,兩個管道變量如果是同一個 make 語句聲明(或同為 nil)則兩個管道相等,否則不等。

cha := make(chan int, 10)
chb := make(chan int, 10)
chc := cha
fmt.Println(cha == chb) // 輸出 false
fmt.Println(cha == chc) // 輸出 true
// 管道 cha 和 chb 雖然類型和空間完全相同,但由于出自不同的 make 語句,所以兩個管道不相等
// 但管道 chc 由于獲得了管道 cha 的地址,所以管道 cha 和 chc 相等fmt.Println(cha < chc)
// 錯誤提示:Invalid operation: cha < chc (the operator < is not defined on chan int)
  • map、slice、function 為什么不能直接用 == 比較?使用什么可以比較?

??至于這三種類型為什么不可比較,Golang 社區(qū)沒有給出官方解釋,經(jīng)過分析,可能是因為 比較的維度不好衡量,難以定義一種沒有爭議的比較規(guī)則。所以 go 官方并沒有定義比較運算符(==和!=),而是只能與nil進(jìn)行比較。

??比如兩個 slice 類型相同、長度相同并且元素值也相同算不算相等?如果說相等,那么如果兩個 slice 地址不同,還算不算相等呢?答案就可能無法統(tǒng)一了。至于 map 也是同樣的道理。另外再看 function,兩個函數(shù)實現(xiàn)功能一樣,但實現(xiàn)邏輯不一樣算不算相等呢?可見,這三種類型的比較容易引入歧義。

??使用 reflect.TypeOf(value).Comparable() 判斷可否進(jìn)行比較, 使用 reflect.DeepEqual(value 1, value 2) 進(jìn)行比較,當(dāng)然也有特殊情況,例如 []byte,通過 bytes. Equal 函數(shù)進(jìn)行比較。但是反射非常影響性能。

func main() {s := "Hello World"aMap := make(map[string]int)bMap := make(map[string]int)fmt.Println(reflect.TypeOf(s).Comparable())    // 輸出 truefmt.Println(reflect.TypeOf(aMap).Comparable()) // 輸出 falsefmt.Println(reflect.TypeOf(bMap).Comparable()) // 輸出 falsefmt.Println(reflect.DeepEqual(aMap, bMap))     // 輸出 trueaMap["s"] = 1fmt.Println(reflect.DeepEqual(aMap, bMap)) // 輸出 false
}

22、interface 接口

  • 在Go語言中,接口(interface)是方法的集合,它允許我們定義一組方法但不實現(xiàn)它們,任何類型只要實現(xiàn)了這些方法,就被認(rèn)為是實現(xiàn)了該接口。接口更重要的作用在于多態(tài)實現(xiàn),它允許程序以多態(tài)的方式處理不同類型的值。接口體現(xiàn)了程序設(shè)計的多態(tài)和高內(nèi)聚、低耦合的思想。

  • 使用的注意事項
    (1)interface 類型默認(rèn)是一個指針,引用類型。
    (2)interface 接口不能包含任何變量。
    (3)一個自定義類型可以實現(xiàn)多個接口。
    (4)一個接口可以嵌套(繼承)多個別的接口。
    (5)接口的實現(xiàn)是隱式的,不需要顯式聲明。
    (6)空接口沒有任何方法,能被任意數(shù)據(jù)類型實現(xiàn)。
    (7)結(jié)構(gòu)體類型(structs)、類型別名(type aliases)、其他接口、自定義類型、變量等都可以實現(xiàn)接口。

  • 接口分為侵入式和非侵入式
    ??GO語言的接口是非侵入式接口。
    ??侵入式接口的實現(xiàn)是顯式聲明的,必須顯式的表明我要繼承那個接口,必須通過特定的關(guān)鍵字(?如Java中的implements)?來聲明實現(xiàn)關(guān)系。?
    ??非侵入式接口的實現(xiàn)是隱式聲明的,不需要通過任何關(guān)鍵字聲明類型與接口之間的實現(xiàn)關(guān)系。?只要一個類型實現(xiàn)了接口的所有方法,?那么這個類型就實現(xiàn)了這個接口。

  • 應(yīng)用場景
    Go接口的應(yīng)用場景包括多態(tài)性、?解耦、?擴(kuò)展性、?代碼復(fù)用、API設(shè)計、?單元測試、?插件系統(tǒng)、?依賴注入。?
    類型轉(zhuǎn)換、類型判斷、實現(xiàn)多態(tài)功能、作為函數(shù)參數(shù)或返回值。

  • 空接口的應(yīng)用場景
    (1)用空接口可以讓函數(shù)和方法接受任意類型、任意數(shù)量的函數(shù)參數(shù),空接口切片還可以用于函數(shù)的可選參數(shù)。
    (2)空接口還可以作為函數(shù)的返回值,但是極不推薦這樣干,因為代碼的維護(hù)、拓展與重構(gòu)將會變得極為痛苦。
    (3)空接口可以實現(xiàn)保存任意類型值的字典 (map)。

23、空結(jié)構(gòu)體 struct 應(yīng)用場景

空結(jié)構(gòu)體在Go語言中通常用于需要一個空的值的場景。

  • 作為channel的通知信號。通知型 channel,使用空結(jié)構(gòu)體當(dāng)做通知信號,不會帶來額外的內(nèi)存開銷。
  • 作為map的值。map + 空 struct 實現(xiàn)集合 set,節(jié)省內(nèi)存。
  • 作為方法接收器。在業(yè)務(wù)場景下,我們需要將方法組合起來,代表其是一個 ”分組“ 的,便于后續(xù)拓展和維護(hù)。在該場景下,使用空結(jié)構(gòu)體是最合適的,易拓展,省空間,最結(jié)構(gòu)化。
  • 作為一個標(biāo)記或占位符。表示某個動作已經(jīng)發(fā)生,某個元素已被處理。
  • 作為接口的實現(xiàn)??战Y(jié)構(gòu)體可以實現(xiàn)一個或多個接口,而不需要存儲任何字段。這允許你創(chuàng)建符合特定接口的對象,而不需要為這些對象分配額外的內(nèi)存空間。這在某些設(shè)計模式(如工廠模式、單例模式等)中特別有用。

空結(jié)構(gòu)體主要有以下幾個特點:
(1)零內(nèi)存占用 :空結(jié)構(gòu)體不占用任何內(nèi)存空間,?這使得空結(jié)構(gòu)體在內(nèi)存優(yōu)化方面非常有用。?
(2)地址相同:?無論創(chuàng)建多少個空結(jié)構(gòu)體,?它們所指向的地址都是相同的,這意味著所有空結(jié)構(gòu)體實例共享同一個內(nèi)存位置。
(3)無狀態(tài):?由于空結(jié)構(gòu)體沒有數(shù)據(jù)成員,?因此它不包含任何狀態(tài)信息。?

24、go 內(nèi)存泄漏

??內(nèi)存泄漏是指在程序執(zhí)行過程中,已不再使用的內(nèi)存空間沒有被及時釋放或者釋放時出現(xiàn)了錯誤,導(dǎo)致這些內(nèi)存無法被使用,直到程序結(jié)束這些內(nèi)存才被釋放。

??如果出現(xiàn)內(nèi)存泄漏問題,程序?qū)驗檎紦?jù)大量內(nèi)存而變得異常緩慢,嚴(yán)重時可能會導(dǎo)致程序崩潰。在go語言中,可以通過runtime包里的freeosmemory()函數(shù)來進(jìn)行內(nèi)存回收和清理。此外,也可以使用一些工具來檢測內(nèi)存泄漏問題,例如go pprof等。需要注意的是,內(nèi)存泄漏不是語言本身的問題,而通常是程序編寫者忘記釋放內(nèi)存或者處理內(nèi)存時出現(xiàn)錯誤導(dǎo)致的。

??在Go中內(nèi)存泄露分為暫時性內(nèi)存泄露和永久性內(nèi)存泄露。

  • 暫時性內(nèi)存泄露

??臨時性泄露,指的是該釋放的內(nèi)存資源沒有及時釋放,對應(yīng)的內(nèi)存資源仍然有機(jī)會在更晚些時候被釋放,即便如此在內(nèi)存資源緊張情況下,也會是個問題。這類主要是 string、slice 底層 buffer 的錯誤共享,導(dǎo)致無用數(shù)據(jù)對象無法及時釋放,或者 defer 函數(shù)導(dǎo)致的資源沒有及時釋放。

1、獲取長字符串中的一段導(dǎo)致長字符串未釋放
2、獲取長slice中的一段導(dǎo)致長slice未釋放
3、獲取指針切片slice中的一段
4、defer 導(dǎo)致的內(nèi)存泄露

  • 永久性內(nèi)存泄露

??永久性泄露,指的是在進(jìn)程后續(xù)生命周期內(nèi),泄露的內(nèi)存都沒有機(jī)會回收,如 goroutine 內(nèi)部預(yù)期之外的for-loop或者chan select-case導(dǎo)致的無法退出的情況,導(dǎo)致協(xié)程棧及引用內(nèi)存永久泄露問題。

1、goroutine 協(xié)程阻塞,無法退出,導(dǎo)致內(nèi)存泄漏;

??Go運行時不會殺死掛起的goroutines,因此分配給掛起goroutines的資源(以及所引用的內(nèi)存塊)永遠(yuǎn)不會被垃圾回收。

  • channel 阻塞導(dǎo)致 goroutine 阻塞
  • select 導(dǎo)致 goroutine 阻塞
  • 互斥鎖沒有釋放,互斥鎖死鎖
  • 申請過多的goroutine來不及釋放導(dǎo)致內(nèi)存泄漏

2、定時器使用不當(dāng),time.Ticker未關(guān)閉導(dǎo)致內(nèi)存泄漏;

time.After在定時器到達(dá)時,會自動內(nèi)回收。然后time.Ticker 鐘擺不使用時,一定要Stop,不然會造成真內(nèi)存泄露。

3、不正確地使用終結(jié)器(Finalizers)導(dǎo)致內(nèi)存泄漏

25、協(xié)程泄露

??協(xié)程泄露是指協(xié)程創(chuàng)建后,長時間得不到釋放,并且還在不斷地創(chuàng)建新的協(xié)程,最終導(dǎo)致內(nèi)存耗盡,程序崩潰。

??常見的導(dǎo)致協(xié)程泄露的場景有以下幾種:
??1、缺少接收方,導(dǎo)致發(fā)送方阻塞。啟動1000個協(xié)程向信道發(fā)送數(shù)字,但只接收了一次,導(dǎo)致 999 個協(xié)程阻塞,不能退出。
??2、缺少發(fā)送方,導(dǎo)致接收方阻塞。啟動 1000 個協(xié)程接收信道的信息,但信道并不會發(fā)送那么多次的信息,也會導(dǎo)致接收協(xié)程被阻塞,不能退出。
??3、死鎖。多個協(xié)程由于競爭資源或者彼此通信而造成阻塞,不能退出。
??4、select操作。select里也是channel操作,如果所有case上的操作阻塞,且沒有default分支進(jìn)行處理,goroutine也無法繼續(xù)執(zhí)行。

26、值傳遞和地址傳遞(引用傳遞)

??Go語言函數(shù)傳參時,默認(rèn)使用值傳遞。

??值傳遞是指當(dāng)我們調(diào)用一個方法并將參數(shù)傳遞給它時,實際上是把變量的一個副本傳遞給了函數(shù),而非原始變量自己,兩個變量的地址不同,不可相互修改。 在函數(shù)內(nèi)部,對于這個參數(shù)所做的任何更改,只會影響副本的值,不會影響原始變量。

??地址傳遞(引用傳遞)是指傳遞給函數(shù)的是變量的指針或者地址(變量本身),函數(shù)可以通過修改這個地址上的值來更改變量的值,對該變量的修改在所有使用它的地方都是可見的。

區(qū)別值傳遞引用傳遞
傳參變量的副本變量本身(指針或者地址)
影響范圍只影響副本,不影響原始變量影響原始變量
使用場景處理較小的變量時速度更快避免復(fù)制大塊的內(nèi)存內(nèi)容

27、go 語言中棧的空間有多大?

??多數(shù)架構(gòu)上默認(rèn)棧大小都在 2 ~ 4 MB 左右。在Go語言中,??臻g大小并不是固定的,?而是根據(jù)程序的運行需求動態(tài)調(diào)整的,?Goroutine 的初始棧大小降低到了 2KB。在64位操作系統(tǒng)上,?Go中的??臻g最大可以擴(kuò)展到1GB。?

早期版本的Go可能將最小棧內(nèi)存設(shè)置為4KB或8KB,而后來為了優(yōu)化性能,又可能將其調(diào)整回2KB。

28、并發(fā)情況下的數(shù)據(jù)處理,避免并發(fā)情況下數(shù)據(jù)競爭問題?

??1、使用互斥鎖(mutex):在操作共享資源之前,加鎖;完成操作后,解鎖。

??2、使用讀寫互斥鎖(RWMutex):讀不阻塞,寫阻塞。允許多個goroutine同時訪問共享變量,寫入時就必須等到其他goroutine不再讀寫了才能進(jìn)行。這種方法適合在讀多寫少的場景。

??3、使用通道(channel)串行化操作:使用通道來實現(xiàn)數(shù)據(jù)同步和互斥訪問,將需要訪問的數(shù)據(jù)發(fā)送到一個通道中,可以保證同一時間只有一個goroutine能夠訪問該數(shù)據(jù)。類似于生產(chǎn)者消費者模式。

??4、使用原子操作(atomic):如果只是對一個共享變量進(jìn)行簡單的增加或減少,就可以使用原子操作。原子操作是一個CPU指令,它保證在一個時刻內(nèi)執(zhí)行所有的增量或減量,避免了并發(fā)情況下的數(shù)據(jù)競爭問題。

二、channel 通道

??在Go語言中,channel是一種用于在goroutine之間傳遞數(shù)據(jù)的安全通信機(jī)制。它可以被看做是一種特殊類型的隊列,其中的數(shù)據(jù)只能被一個goroutine讀取而另一個goroutine寫入。要創(chuàng)建一個channel,可以使用內(nèi)置的make函數(shù)。

1、底層數(shù)據(jù)結(jié)構(gòu)

type hchan struct {// chan 里元素數(shù)量qcount   uint// chan 底層循環(huán)數(shù)組的長度dataqsiz uint// 指向底層循環(huán)數(shù)組的指針// 只針對有緩沖的 channelbuf      unsafe.Pointer// chan 中元素大小elemsize uint16// chan 是否被關(guān)閉的標(biāo)志closed   uint32// chan 中元素類型elemtype *_type // element type// 已發(fā)送元素在循環(huán)數(shù)組中的索引sendx    uint   // send index// 已接收元素在循環(huán)數(shù)組中的索引recvx    uint   // receive index// 等待接收的 goroutine 隊列recvq    waitq  // list of recv waiters// 等待發(fā)送的 goroutine 隊列sendq    waitq  // list of send waiters// 保護(hù) hchan 中所有字段lock mutex
}

從channel中讀數(shù)據(jù):
1、若等待發(fā)送隊列 sendq 不為空,且沒有緩沖區(qū),直接從 sendq 中取出 G ,把 G 中數(shù)據(jù)讀出,最后把 G 喚醒,結(jié)束讀取過程。
2、如果等待發(fā)送隊列 sendq 不為空,說明緩沖區(qū)已滿,從緩沖區(qū)中首部讀出數(shù)據(jù),把 G 中數(shù)據(jù)寫入緩沖區(qū)尾部,把 G 喚醒,結(jié)束讀取過程。
3、如果緩沖區(qū)中有數(shù)據(jù),則從緩沖區(qū)取出數(shù)據(jù),結(jié)束讀取過程。將當(dāng)前 goroutine 加入 recvq ,進(jìn)入睡眠,等待被寫 goroutine 喚醒。

往channel中寫數(shù)據(jù)
1、若等待接收隊列 recvq 不為空,則緩沖區(qū)中無數(shù)據(jù)或無緩沖區(qū),將直接從 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 喚醒。

關(guān)閉 channel
關(guān)閉 channel 時會將 recvq 中的 G 全部喚醒,本該寫入 G 的數(shù)據(jù)位置為 nil 。將 sendq 中的 G 全部喚醒,但是這些 G 會 panic。

2、channel為什么能做到線程安全?

??channel可以理解是一個先進(jìn)先出的循環(huán)隊列,通過管道進(jìn)行通信,發(fā)送一個數(shù)據(jù)到Channel和從Channel接收一個數(shù)據(jù)都是原子性的。不要通過共享內(nèi)存來通信,而是通過通信來共享內(nèi)存,前者就是傳統(tǒng)的加鎖,后者就是Channel。設(shè)計Channel的主要目的就是在多任務(wù)間傳遞數(shù)據(jù)的,本身就是安全的。

3、無緩沖的 channel 和 有緩沖的 channel 的區(qū)別?

??阻塞與否是分別針對發(fā)送方、接收方而言的,可以類比生產(chǎn)者與消費者問題。

??對于無緩沖的 channel,發(fā)送方將阻塞該信道,直到接收方從該信道接收到數(shù)據(jù)為止,而接收方也將阻塞該信道,直到發(fā)送方將數(shù)據(jù)發(fā)送到該信道中為止。

??對于有緩存的 channel,發(fā)送方在緩沖區(qū)滿的時候阻塞,接收方不阻塞;接收方在緩沖區(qū)為空的時候阻塞,發(fā)送方不阻塞。

4、channel 死鎖的場景

1、當(dāng)一個channel中沒有數(shù)據(jù),而直接讀取時,會發(fā)生死鎖;
2、當(dāng)channel數(shù)據(jù)滿了,再嘗試寫數(shù)據(jù)會造成死鎖;
3、向一個關(guān)閉的channel寫數(shù)據(jù)。

??解決方案是采用select語句,再default放默認(rèn)處理方式。
1、Go的select語句是一種僅能用于channl發(fā)送和接收消息的專用語句,此語句運行期間是阻塞的;當(dāng)select中沒有case語句的時候,會阻塞當(dāng)前groutine。
2、select是Golang在語言層面提供的I/O多路復(fù)用的機(jī)制,其專門用來檢測多個channel是否準(zhǔn)備完畢:可讀或可寫。
3、select語句中除default外,每個case操作一個channel,要么讀要么寫。
4、select語句中除default外,各case執(zhí)行順序是隨機(jī)的。
5、select語句中如果沒有default語句,則會阻塞等待任一case。
6、select語句中讀操作要判斷是否成功讀取,關(guān)閉的channel也可以讀取。

5、操作 channel 的情況總結(jié)

操作nil channel(未初始化)closed channelnot nil, not closed channel
closepanicpanic正常關(guān)閉
讀 <- ch阻塞讀到對應(yīng)類型的零值阻塞或正常讀取數(shù)據(jù)。非緩沖型 channel 沒有等待發(fā)送者或緩沖型 channel buf為空時會阻塞
寫 ch <-阻塞panic阻塞或正常寫入數(shù)據(jù)。非緩沖型 channel 沒有等待接收者或緩沖型 channel buf 滿時會被阻塞

總結(jié)一下,發(fā)生 panic 的情況有三種:向一個關(guān)閉的 channel 進(jìn)行寫操作;關(guān)閉一個未初始化的 channel;重復(fù)關(guān)閉一個 channel。

讀、寫一個未初始化的channel 都會被阻塞。

三、map 哈希表

1、map 的底層數(shù)據(jù)結(jié)構(gòu)是什么?

?? 源碼位于 src\runtime\map.go 中。

??golang 中 map 底層使用的是哈希查找表,用鏈表來解決哈希沖突。每個 map 的底層結(jié)構(gòu)是 hmap,是有若干個結(jié)構(gòu)為 bmap 的 bucket 組成的數(shù)組,每個 bucket 底層都采用鏈表結(jié)構(gòu)。

hmap的結(jié)構(gòu):

type hmap struct {count      int            // map中元素的數(shù)量,調(diào)用len()直接返回此值flags      uint8          // 狀態(tài)標(biāo)識符,key和value是否包指針、是否正在擴(kuò)容、是否已經(jīng)被迭代B          uint8          // map中桶數(shù)組的數(shù)量,桶數(shù)組的長度的對數(shù),len(buckets) == 2^B,可以最多容納 6.5 * 2 ^ B 個元素,6.5為裝載因子noverflow  uint16         // 溢出桶的大概數(shù)量,當(dāng)B小于16時是準(zhǔn)確值,大于等于16時是大概的值hash0      uint32         // 哈希種子,用于計算哈希值,為哈希函數(shù)的結(jié)果引入一定的隨機(jī)性,降低哈希沖突的概率buckets    unsafe.Pointer // 指向桶數(shù)組的指針,長度為 2^B ,如果元素個數(shù)為0,就為 niloldbuckets unsafe.Pointer // 指向一個舊桶數(shù)組,用于擴(kuò)容,它的長度是當(dāng)前桶數(shù)組的一半nevacuate  uintptr        // 搬遷進(jìn)度,小于此地址的桶數(shù)組遷移完成extra      *mapextra      // 可選字段,用于gc,指向所有的溢出桶,避免gc時掃描整個map,僅掃描所有溢出桶就足夠了
}

bmap的結(jié)構(gòu):

type bmap struct {tophash [bucketCnt]uint8    // bucketCnt=8,存放key哈希值的高8位,用于決定kv鍵值對放在桶內(nèi)的哪個位置
}

??buckets是一個bmap數(shù)組,數(shù)組的長度就是 2^B。每個bucket固定包含8個key和value,實現(xiàn)上面是一個固定的大小連續(xù)內(nèi)存塊,分成四部分:tophash 值,8個key值,8個value值,指向下個bucket的指針。

??tophash 值用于快速查找key是否在該bucket中,當(dāng)插入和查詢運行時都會使用哈希哈數(shù)對key做哈希運算,獲取一個hashcode,取高8位存放在bmap tophash字段中。

??桶結(jié)構(gòu)中,所有的key放一起,所有的value放一起,而不是key/value一對一存放,目的是省去 pad 字段,節(jié)省內(nèi)存空間。由于內(nèi)存對齊的原因,key/value一對一的形式可能需要更多的補齊空間。

??每個 bucket 設(shè)計成最多只能放 8 個 key-value 對,如果有第 9 個 key-value 落入當(dāng)前的 bucket,那就需要再構(gòu)建一個溢出桶,通過指針連接起來。

key 定位過程

??key 經(jīng)過哈希計算后得到哈希值,共 64 個 bit 位(64位機(jī)),
??低 B 位:后 B 位,決定在哪個桶。用于尋找當(dāng)前key屬于哪個bucket,桶的編號;
??高 8 位:前 8 位,決定在桶的的哪個位置。找到此 key 在 bucket 中的位置,第幾個槽位,key 和 value 也對應(yīng)第幾個。最開始桶內(nèi)還沒有 key,新加入的 key 會找到第一個空位,放入。

2、map的擴(kuò)容

1、裝載因子(平均每個桶存儲的元素個數(shù))

??Go的裝載因子閾值常量:6.5,map 最多可容納 6.5*2^B 個元素。
??裝載因子等于 map中元素的個數(shù) / map的容量,即len(map) / 2^B。裝載因子用來表示空閑位置的情況,裝載因子越大,表明空閑位置越少,沖突也越多,散列表的性能會下降。

為什么裝載因子是6.5?不是8?不是1?
??裝載因子是哈希表中的一個重要指標(biāo),主要目的是為了平衡 buckets 的存儲空間大小和查找元素時的性能高低。
??Go 官方發(fā)現(xiàn):裝載因子越大,填入的元素越多,空間利用率就越高,但發(fā)生沖突的幾率就變大;反之,裝數(shù)因子越小,填入的元素越少,沖突發(fā)生的幾率減小,但空間利用率低,而且還會提高擴(kuò)容操作的次數(shù)。根據(jù)測試結(jié)果和討論,Go 官方取了一個相對適中的值6.5。

2、觸發(fā) map 擴(kuò)容的時機(jī)(插入、刪除key):

??觸發(fā)擴(kuò)容的時機(jī)是新增操作,搬遷的時機(jī)是賦值和刪除操作,每次最多搬遷兩個bucket。

擴(kuò)容分為等量擴(kuò)容和2倍增量擴(kuò)容。

條件1:
??當(dāng)元素個數(shù)超過負(fù)載,元素個數(shù) > 6.5 * 桶個數(shù),擴(kuò)容一倍,屬于增量擴(kuò)容;

條件2:
??當(dāng)使用的溢出桶過多時,重新分配一樣大的內(nèi)存空間,屬于等量擴(kuò)容;(實際上沒有擴(kuò)容,主要是為了回收空閑的溢出桶,提高 map 的查找和插入效率)

如何定義溢出桶是否太多需要等量擴(kuò)容呢?兩種情況:

  • 當(dāng)B小于15時,溢出桶的數(shù)量超過2^B桶總數(shù),屬于溢出桶數(shù)量太多,需要等量擴(kuò)容;
  • 當(dāng)B大于等于15時,溢出桶數(shù)量超過2^15,屬于溢出桶數(shù)量太多,需要等量擴(kuò)容。

??條件2是對條件1的補充,例如不停地插入、刪除元素,導(dǎo)致創(chuàng)建很多的溢出桶,但裝載因子不高,達(dá)不到條件1的臨界值,不能觸發(fā)擴(kuò)容來緩解這種情況。溢出桶數(shù)量太多,桶使用率低,導(dǎo)致 key 會很分散,查找插入效率低,空間利用率低。

3、擴(kuò)容策略(怎么擴(kuò)容?)

??Go 會創(chuàng)建一個新的 buckets 數(shù)組,新的 buckets 數(shù)組的容量是舊buckets數(shù)組的兩倍(或者和舊桶容量相同),將原始桶數(shù)組中的所有元素重新散列到新的桶數(shù)組中。這樣做的目的是為了使每個桶中的元素數(shù)量盡可能平均分布,以提高查詢效率。舊的buckets數(shù)組不會被直接刪除,而是會把原來對舊數(shù)組的引用去掉,讓GC來清除內(nèi)存。

??擴(kuò)容過程是漸進(jìn)式的,主要是防止一次擴(kuò)容要搬遷的元素太多引發(fā)性能問題。

??在map進(jìn)行擴(kuò)容遷移的期間,不會觸發(fā)第二次擴(kuò)容。只有在前一個擴(kuò)容遷移工作完成后,map才能進(jìn)行下一次擴(kuò)容操作。

4、搬遷策略

??由于map擴(kuò)容需要將原有的kv鍵值對搬遷到新的內(nèi)存地址,如果一下子全部搬完,會非常的影響性能。go 中 map 的擴(kuò)容采用漸進(jìn)式的搬遷策略,原有的 key 并不會一次性搬遷完畢,一次性搬遷會造成比較大的延時,每次最多只會搬遷 2 個 bucket,將搬遷的O(N)開銷均攤到O(1)的賦值和刪除操作上。

??插入或修改、刪除 key 的時候,都會嘗試進(jìn)行搬遷 buckets 的工作。

3、從未初始化的 map 讀數(shù)據(jù)會發(fā)生什么?

??從未初始化的 map 讀取數(shù)據(jù),則會返回該值類型的零值。當(dāng)給未初始化的 map 賦值時,會出現(xiàn)運行時錯誤 “panic: assignment to entry in nil map” ,這是因為未經(jīng)初始化的 map 是一個 nil 值,并且不能對 nil map 進(jìn)行賦值操作。因此,在使用一個 map 之前,必須確保它已經(jīng)正確地初始化。

操作nil map (未初始化)空 map (長度為 0)
賦值panic不會報錯
打印不會報錯,打印 map[]不會報錯
讀取不會報錯,讀到對應(yīng)類型的零值不會報錯
刪除不會報錯不會報錯
func main() {var a map[int]int  // 未初始化 mapb := map[int]int{} // 空 mapa[1] = 1           // panic: assignment to entry in nil mapb[1] = 1fmt.Println(a)    // map[]fmt.Println(b)    // map[1:1]fmt.Println(a[1]) // 0fmt.Println(b[1]) // 1delete(a, 1)delete(b, 1)
}

4、map 中的 key 為什么是無序的?怎么實現(xiàn)有序?

無序的原因
??(1)map底層的擴(kuò)容與搬遷:map在擴(kuò)容后,會發(fā)生key的搬遷,原來在同一個桶的key,搬遷后,有可能就不處于同一個桶了,而遍歷map的過程,就是遍歷這些桶,桶里的元素發(fā)生了變化,map遍歷當(dāng)然就是無序的。
??(2)隨機(jī) bucket 隨機(jī)序號:Go 中遍歷 map 時,并不是固定地從 0 號 bucket 開始遍歷,每次都是從一個隨機(jī)值序號的 bucket 開始遍歷,并且是從這個 bucket 的一個隨機(jī)序號的 cell 開始遍歷。這樣,即使你是一個寫死的 map,僅僅只是遍歷它,也不太可能會返回一個固定序列的 key/value 對了。

實現(xiàn)有序
??(1)使用第三方庫,如 github.com/iancoleman/orderedmap 或者 github.com/wkhere/ordered_map,這些庫提供了類似于標(biāo)準(zhǔn)庫中的 map 操作,同時也保持了元素的順序。
??(2)使用 sort 包。使key有序,對key排序,再遍歷key輸出value;使value有序,用struct存放key和value,實現(xiàn)sort接口,調(diào)用sort.Sort進(jìn)行排序。

5、map并發(fā)訪問安全嗎?怎么解決?可以邊遍歷邊刪除嗎?

??map 在并發(fā)情況下,只讀是線程安全的,同時讀寫是線程不安全的。在并發(fā)訪問下,多個goroutine同時讀寫同一個map會導(dǎo)致數(shù)據(jù)競爭(data race)問題,這可能導(dǎo)致不可預(yù)期的結(jié)果和程序崩潰。并發(fā)讀寫的時候運行時會有檢查,在查找、賦值、遍歷、刪除的過程中都會進(jìn)行寫保護(hù)檢測,檢測寫標(biāo)志,一旦發(fā)現(xiàn)寫標(biāo)志置位(等于1),則直接 panic。賦值和刪除函數(shù)在檢測完寫標(biāo)志是復(fù)位之后,先將寫標(biāo)志位置位,才會進(jìn)行之后的操作。

??go 官方認(rèn)為,map 更應(yīng)適配典型使用場景(不需要從多個 goroutine 中進(jìn)行安全訪問),而不是為了小部分情況(并發(fā)訪問),導(dǎo)致大部分程序付出加鎖的代價,影響性能,所以決定了不支持。

解決方法:
(1)使用讀寫鎖 sync.RWMutex,讀之前調(diào)用 RLock() 函數(shù),讀完之后調(diào)用 RUnlock() 函數(shù)解鎖;寫之前調(diào)用 Lock() 函數(shù),寫完之后,調(diào)用 Unlock() 解鎖。
(2)使用 sync.Map ,并發(fā)安全的 map。使用 Store() 函數(shù)賦值,使用 Load() 函數(shù)獲取值。

Go map和sync.Map誰的性能好,為什么?
??sync.Map 性能好,空間換時間機(jī)制,冗余的數(shù)據(jù)結(jié)構(gòu)就是dirty和read,發(fā)生鎖競爭的頻率小,減少了加鎖對性能的影響。適合讀多寫少的場景,寫多的場景,需要加鎖,性能會下降。

遍歷操作:只需遍歷read即可,而read是并發(fā)讀安全的,沒有鎖,相比于加鎖方案,性能大為提升
查找操作:先在read中查找,read中找不到再去dirty中找

邊遍歷邊刪除——同時讀寫?
(1)多個協(xié)程同時讀寫同一個 map 會直接 panic。
(2)如果在同一個協(xié)程內(nèi)邊遍歷邊刪除,并不會 panic,但是,遍歷的結(jié)果就可能不會是相同的了,有可能結(jié)果集中包含了刪除的 key,也有可能不包含,這取決于刪除 key 的時間:是在遍歷到 key 所在的 bucket 時刻前或者后。

6、map元素可以取地址嗎?

??無法對 map 的 key 或 value 進(jìn)行取址,因為擴(kuò)容后map元素的地址會發(fā)生變化,歸根結(jié)底還是map底層的擴(kuò)容與搬遷。

7、map 中刪除一個 key,它的內(nèi)存會釋放么?

??不會釋放,因為刪除只是將桶對應(yīng)位置的tophash置空而已,如果kv存儲的是指針,那么會清理指針指向的內(nèi)存,否則不會真正回收內(nèi)存,內(nèi)存占用并不會減少。

??在大多數(shù)情況下,刪除 Map 中的 key 不會立即釋放內(nèi)存。這是因為在大多數(shù)語言中,Map 內(nèi)部實現(xiàn)使用哈希表或紅黑樹等數(shù)據(jù)結(jié)構(gòu)來存儲鍵值對,而刪除一個鍵值對只是將該鍵值對的引用從內(nèi)部數(shù)據(jù)結(jié)構(gòu)中刪除,并不會立即釋放與其相關(guān)的內(nèi)存。

8、什么樣的類型可以做 map 的鍵 key?

??Go 語言中只要是可比較的類型都可以作為 key。除開 slice,map,functions 這幾種類型,其他類型都是 OK 的。具體包括:布爾值、數(shù)字、字符串、指針、通道、接口類型、結(jié)構(gòu)體、只包含上述類型的數(shù)組。這些類型的共同特征是支持 == 和 != 操作符,k1 == k2 時,可認(rèn)為 k1 和 k2 是同一個 key。如果是結(jié)構(gòu)體,只有 hash 后的值相等以及字面值相等,才被認(rèn)為是相同的 key。很多字面值相等的,hash出來的值不一定相等,比如引用。

??任何類型都可以作為 value,包括 map 類型。

float 型可以作為 key,但是由于精度的問題,會導(dǎo)致一些詭異的問題,慎用之。
2.4 == 2.4000000000000000000000001
當(dāng)用 float64 作為 key 的時候,先要將其轉(zhuǎn)成 unit64 類型,再插入 key 中。2.4 和 2.4000000000000000000000001 經(jīng)過 math.Float64bits() 函數(shù)轉(zhuǎn)換后的結(jié)果是一樣的,所以認(rèn)為同一個 key 。

NAN != NAN
hash(NAN) != hash(NAN)
NAN 是從一個常量解析得來的,為什么插入 map 時,會被認(rèn)為是不同的 key?
哈希函數(shù)針對 NAN,會再加一個隨機(jī)數(shù),所以認(rèn)為是不同的key。

9、如何比較兩個 map 相等?

map 深度相等的條件:
1、都為 nil
2、非空、長度相等,指向同一個 map 實體對象
3、相應(yīng)的 key 指向的 value “深度”相等

??直接將使用 map1 == map2 是錯誤的,編譯不通過。== 只能比較 map 是否為 nil。因此只能是遍歷map 的每個元素,比較元素是否都是深度相等。使用 reflect.DeepEqual 進(jìn)行比較。

10、map怎么解決哈希沖突?

??在Go語言中,map是通過哈希表來實現(xiàn)的,當(dāng)多個鍵映射到哈希表的同一個桶時,就會發(fā)生哈希沖突。Go語言使用 鏈地址法(拉鏈法)解決哈希沖突。

解決哈希沖突的方法:

  • 開放尋址法
    ??如果發(fā)生哈希沖突,從發(fā)生沖突的那個單元起,按一定的次序,不斷重復(fù),從哈希表中尋找一個空閑的單元,將該鍵值對存儲在該單元中。具體的實現(xiàn)方式包括線性探測法、平方探測法、隨機(jī)探測法和雙重哈希法等。開放尋址法需要的表長度要大于等于所需要存放的元素數(shù)量。
  • 鏈地址法(拉鏈法)
    ??基于數(shù)組 + 鏈表 實現(xiàn)哈希表,數(shù)組中每個元素都是一個鏈表,將每個桶都指向一個鏈表,當(dāng)哈希沖突發(fā)生時,新的鍵值對會按順序添加到該桶對應(yīng)的鏈表的尾部。在查找特定鍵值對時,可以遍歷該鏈表以查找與之匹配的鍵值對。

兩種方案的比較:
(1)對于鏈地址法,基于 數(shù)組 + 鏈表 進(jìn)行存儲,鏈表節(jié)點可以在需要時再創(chuàng)建,開放尋址法需要事先申請好足夠內(nèi)存,因此鏈地址法對內(nèi)存的利用率高。
(2)鏈地址法對裝載因子的容忍度會更高,適合存儲大對象、大數(shù)據(jù)量的哈希表,而且相較于開放尋址法,它更加靈活,支持更多的優(yōu)化策略,比如可采用紅黑樹代替鏈表。但是鏈地址法需要額外的空間來存儲指針。
(3)對于開放尋址法,它只有數(shù)組一種數(shù)據(jù)結(jié)構(gòu)就可完成存儲,繼承了數(shù)組的優(yōu)點,對CPU緩存友好,易于序列化操作,但是它對內(nèi)存的利用率不高,且發(fā)生沖突時代價更高。當(dāng)數(shù)據(jù)量明確、裝載因子小,適合采用開放尋址法。

11、map 使用中注意的點?

(1)一定要先初始化,再使用,否則panic;
(2)map 不是線程安全的;
(3)map 的 key 必須是可比較的;
(4)map是無序的。

12、map 創(chuàng)建、賦值、刪除、查詢的過程?

??寫保護(hù)檢測,查找、賦值、遍歷、刪除的過程中都會檢測寫標(biāo)志位 flags,一旦發(fā)現(xiàn) flags 的寫標(biāo)志位被置為1,則直接 panic,因為這表明有其他協(xié)程同時在進(jìn)行寫操作。查找,賦值,刪除這些操作一個很核心的內(nèi)容都是如何定位key的位置。

創(chuàng)建 map

??創(chuàng)建 map 底層調(diào)用的是 makemap() 函數(shù),主要做的工作就是初始化 hmap 結(jié)構(gòu)體的各種字段,例如計算 B 的大小、設(shè)置哈希種子 hash0 、分配桶空間等。

默認(rèn)會創(chuàng)建2^B個bucket,如果b大于等于4,會預(yù)先創(chuàng)建一些溢出桶,b小于4的情況可能用不到溢出桶,沒必要預(yù)先創(chuàng)建

??其中的關(guān)鍵點在于哈希函數(shù)的選擇,在程序啟動時,會檢測 cpu 是否支持 aes,如果支持,則使用 aes hash,否則使用 memhash。這是在函數(shù) alginit() 中完成,位于路徑:src/runtime/alg.go 下。

hash 函數(shù),有加密型和非加密型。
加密型的一般用于加密數(shù)據(jù)、數(shù)字摘要等,典型代表就是 md5、sha1、sha256、aes256 這種;
非加密型的一般就是查找。在 map 的應(yīng)用場景中,用的是查找。
選擇 hash 函數(shù)主要考察的是兩點:性能、碰撞概率。

map 的賦值(修改)過程

??向 map 中插入或者修改 key,底層調(diào)用的是 mapassign() 函數(shù),根據(jù) key 類型的不同,編譯器會將其優(yōu)化為相應(yīng)的“快速函數(shù)”。流程:對 key 計算 hash 值,根據(jù) hash 值按照之前的流程,找到要賦值的位置(可能是插入新 key,也可能是更新老 key),對相應(yīng)位置進(jìn)行賦值。

??核心還是一個雙層循環(huán),外層遍歷 bucket 和它的 overflow bucket,內(nèi)層遍歷整個 bucket 的各個 cell。

map 的刪除過程

??底層調(diào)用的是 mapdelete() 函數(shù),根據(jù) key 類型的不同,刪除操作會被優(yōu)化成更具體的函數(shù)。它首先會檢查 h.flags 標(biāo)志,如果發(fā)現(xiàn)寫標(biāo)位是 1,直接 panic,因為這表明有其他協(xié)程同時在進(jìn)行寫操作。計算 key 的哈希值,找到落入的 bucket。檢查此 map 如果正在擴(kuò)容的過程中,直接觸發(fā)一次搬遷操作。

??刪除操作同樣是兩層循環(huán),核心還是找到 key 的具體位置。尋找過程都是類似的,在 bucket 中挨個 cell 尋找。找到對應(yīng)位置后,對 key 或者 value 進(jìn)行“清零”操作。最后,將 count 值減 1,將對應(yīng)位置的 tophash 值置成 Empty。

刪除key僅僅只是將其對應(yīng)的tohash值置空,如果kv存儲的是指針,那么會清理指針指向的內(nèi)存,否則不會真正回收內(nèi)存,內(nèi)存占用并不會減少。
如果正在擴(kuò)容,并且操作的bucket沒有搬遷完,那么會搬遷bucket。

map 的查詢過程

??底層調(diào)用的是 mapaccess1()、mapaccess2() 函數(shù),mapaccess2() 函數(shù)返回值多了一個 bool 型變量,兩者的代碼也是完全一樣的,只是在返回值后面多加了一個 false 或者 true。根據(jù) key 的不同類型,編譯器用更具體的函數(shù)替換,以優(yōu)化效率。流程:計算hash值并根據(jù)hash值找到桶,遍歷桶和桶串聯(lián)的溢出桶,尋找 key。

??需要注意的地方:如果根據(jù)hash值定位到桶正在進(jìn)行搬遷,并且這個bucket還沒有搬遷到新桶中,那么就從老的桶中找。在bucket中進(jìn)行順序查找,使用高八位進(jìn)行快速過濾,高八位相等,再比較key是否相等,找到就返回value。如果當(dāng)前bucket找不到,就往下找溢出桶,都沒有就返回零值。

四、slice 切片

1、數(shù)組和切片的區(qū)別

  • 相同點:

(1)都是只能存儲一組相同類型的數(shù)據(jù)結(jié)構(gòu);
(2)下標(biāo)都是從0開始的,可以通過下標(biāo)來訪問單個元素;
(3)有容量、長度,長度通過 len 獲取,容量通過 cap 獲取。

  • 不同點:

(1)數(shù)組是定長的,長度定義好之后,不能再更改,長度是類型的一部分,訪問和復(fù)制不能超過數(shù)組定義的長度,否則就會下標(biāo)越界。切片長度和容量可以自動擴(kuò)容,切片的類型和長度無關(guān)。

在 Go 中,數(shù)組是不常見的,因為其長度是類型的一部分,限制了它的表達(dá)能力,比如 [3]int 和 [4]int 就是不同的類型。

(2)數(shù)組是值類型。切片是引用類型,每個切片都引用了一個底層數(shù)組,切片本身不能存儲任何數(shù)據(jù),都是底層數(shù)組存儲數(shù)據(jù),修改切片的時候修改的是底層數(shù)組中的數(shù)據(jù),切片一旦擴(kuò)容,會指向一個新的底層數(shù)組,內(nèi)存地址也就隨之改變。

2、slice 底層數(shù)據(jù)結(jié)構(gòu)

?? 源碼位于 src\runtime\slice.go 中。

??golang 中 slice 實際上是一個結(jié)構(gòu)體,包含三個字段:長度、容量、底層數(shù)組。

type slice struct {array unsafe.Pointer // 指向底層數(shù)組的指針len   int // 長度 cap   int // 容量
}

??注意,底層數(shù)組是可以被多個 slice 同時指向的,因此對一個 slice 的元素進(jìn)行操作是有可能影響到其他 slice 的。

??創(chuàng)建 slice 底層調(diào)用的是 makeslice() 函數(shù),主要工作是向 Go 內(nèi)存管理器申請內(nèi)存(在堆上分配),返回指向底層數(shù)組的指針。

32KB = 32768 字節(jié)。小對象是從per-P緩存的空閑列表中分配的。大型對象(>32kB)是直接從堆中分配的。

??擴(kuò)容,底層調(diào)用的是 growslice() 函數(shù),里面包括擴(kuò)容規(guī)則、內(nèi)存對齊、申請新內(nèi)存、拷貝舊數(shù)據(jù)。

??拷貝,底層調(diào)用的是 slicecopy() 函數(shù),切片中全部元素通過memmove或者數(shù)組指針的方式將整塊內(nèi)存中的內(nèi)容拷貝到目標(biāo)的內(nèi)存區(qū)域,所以大切片拷貝需要注意性能影響,不過比一個個的復(fù)制要有更好的性能。

3、slice 的擴(kuò)容

1、觸發(fā)擴(kuò)容的時機(jī)
??向 slice 追加元素,如果底層數(shù)組的容量不夠(即便底層數(shù)組并未填滿),就會觸發(fā)擴(kuò)容。追加元素調(diào)用的是 append 函數(shù)。

2、擴(kuò)容規(guī)則

Go <= 1.17

1、首先判斷,如果新申請容量(cap)大于2倍的舊容量(old.cap),最終容量(newcap)就是新申請的容量(cap)。
2、否則判斷,如果舊切片的長度小于1024,則最終容量(newcap)就是舊容量(old.cap)的 2 倍。
3、否則判斷,如果舊切片長度大于等于1024,則最終容量(newcap)就是舊容量(old.cap)按照 1.25 倍循環(huán)遞增,也就是每次加上 cap / 4。
4、如果最終容量(cap)計算值溢出,則最終容量(cap)就是新申請容量(cap)。

Go1.18之后

??引入了新的擴(kuò)容規(guī)則,首先 1024 的邊界不復(fù)存在,取而代之的常量是 256 。超出256的情況,也不是直接擴(kuò)容25%,而是設(shè)計了一個平滑過渡的計算方法,隨著容量增大,擴(kuò)容比例逐漸從100%平滑降低到25%,從 2 倍平滑過渡到 1.25 倍。

為什么要這樣設(shè)計?
??避免追加過程中頻繁擴(kuò)容,減少內(nèi)存分配和數(shù)據(jù)復(fù)制開銷,有助于性能提升。

3、內(nèi)存對齊

??計算出了新容量之后,還沒有完,出于內(nèi)存的高效利用考慮,還要進(jìn)行內(nèi)存對齊。進(jìn)行內(nèi)存對齊之后,新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍。

4、完整過程

??向 slice 追加元素的時候,若容量不夠,會觸發(fā)擴(kuò)容,會調(diào)用 growslice 函數(shù)。首先,根據(jù)擴(kuò)容規(guī)則,計算出新的容量,然后進(jìn)行內(nèi)存對齊,之后,向 Go 內(nèi)存管理器申請內(nèi)存,將老 slice 中的數(shù)據(jù)整個復(fù)制過去,并且將追加的元素添加到新的底層數(shù)組中。

4、slice 的拷貝

1、淺拷貝

??淺拷貝,拷貝的是地址,淺拷貝只復(fù)制了指向底層數(shù)據(jù)結(jié)構(gòu)的指針,而不是復(fù)制整個底層數(shù)據(jù)結(jié)構(gòu),修改新對象的值會影響原對象值。對于引用類型,如切片和字典等都是淺拷貝。

slice2 := slice1

??slice1和slice2指向的都是同一個底層數(shù)組,任何一個數(shù)組元素被改變,都可能會影響兩個slice。在slice觸發(fā)擴(kuò)容操作前,slice1和slice2指向的都是相同數(shù)組,但在觸發(fā)擴(kuò)容操作后,二者指向的就不一定是相同的底層數(shù)組了。

2、深拷貝

??深拷貝,拷貝的是數(shù)據(jù)本身,完全復(fù)制了底層數(shù)據(jù)結(jié)構(gòu),而不是復(fù)制指向底層數(shù)據(jù)結(jié)構(gòu)的指針,會創(chuàng)建一個新對象,新對象和原對象不共享內(nèi)存,它們是完全獨立的,修改新對象的值不會影響原對象值,內(nèi)存地址不同,釋放內(nèi)存地址時,可以分別釋放。

copy(slice2, slice1)  

??把 slice1 的數(shù)據(jù)復(fù)制到 slice2 中,修改 slice2 的數(shù)據(jù),不會影響到 slice1 。如果 slice2 的長度和容量小于 slice1 的,那么只會復(fù)制 slice2 長度的數(shù)據(jù)。

5、append 函數(shù)

??使用 append 可以向 slice 追加元素,實際上是往底層數(shù)組添加元素,如果底層數(shù)組的容量不夠,會觸發(fā)擴(kuò)容。append 函數(shù)的參數(shù)長度可變,因此可以追加多個值到 slice 中,還可以用 … 傳入 slice,直接追加一個切片。

??append函數(shù)返回值是一個新的slice,Go編譯器不允許調(diào)用了 append 函數(shù)后不使用返回值。

??注意:append不會修改傳參進(jìn)來的slice(len和cap),只會在不夠用的時候新分配一個array,并把之前的slice依賴的array數(shù)據(jù)拷貝過來;所以對同一個slice 重復(fù) append,只要不超過cap,都是修改的同一個array,后面的會覆蓋前面。

func main() {a := []int{1, 2, 3, 4, 5}b := append(a, 100)fmt.Println(b) // [1 2 3 4 5 100]c := append(a, 200)fmt.Println(c) // [1 2 3 4 5 200]
}

6、切片作為函數(shù)參數(shù)?

??當(dāng) slice 作為函數(shù)參數(shù)時,是值傳遞,函數(shù)內(nèi)部對 slice 的作用并不會改變外層的 slice ,要想真的改變外層 slice,只有將返回的新的 slice 賦值到原始 slice,或者向函數(shù)傳遞一個指向 slice 的指針。slice 結(jié)構(gòu)體自身不會被改變,指針指向的底層數(shù)組的地址也不會被改變,改變的是數(shù)組中的數(shù)據(jù)。

??傳slice和傳slice的引用,其實開銷區(qū)別不大。Go 語言的函數(shù)參數(shù)傳遞,只有值傳遞,沒有引用傳遞。

7、切片 slice 使用時注意的點?

(1)創(chuàng)建slice時應(yīng)根據(jù)實際需要預(yù)分配容量,避免追加過程中頻繁擴(kuò)容,有助于性能提升;在大批量添加數(shù)據(jù)時,建議?次性分配足夠大的空間,以減少內(nèi)存分配和數(shù)據(jù)復(fù)制開銷;
(2)slice是非并發(fā)安全的,如要實現(xiàn)并發(fā)安全,請采用鎖或channle;
(3)大數(shù)組作為函數(shù)參數(shù)時,會復(fù)制整個數(shù)組,消耗過多內(nèi)存,建議采用slice或指針;
(4)如果只用到大的slice或數(shù)組的一部分,建議將需要部分復(fù)制到新的slice中取,以便釋放大的slice底層數(shù)組內(nèi)存,減少內(nèi)存占用;
(5)多個slice指向相同的底層數(shù)組時,修改其中一個slice,可能會影響其他slice的值;
(6)slice作為參數(shù)傳遞時,比數(shù)組更為高效,因為slice本身的結(jié)構(gòu)就比較小,所以你參數(shù)傳遞時,傳slice和傳slice的引用,其實開銷區(qū)別不大;
(7)slice在擴(kuò)容時,可能會發(fā)生底層數(shù)組的變更和數(shù)據(jù)拷貝;
(8)及時釋放不再使用的 slice 對象,避免持有過期數(shù)組,造成 GC 無法回收。

8、slice 內(nèi)存泄露情況

??當(dāng) slice2 的底層數(shù)組很大,但 slice1 使用 slice2 中很小的一段,slice1 和 slice2 共用一個底層數(shù)組,底層數(shù)組占據(jù)的大部分空間都是被浪費的,沒法被回收,造成了內(nèi)存泄露。

解決方法:
??不再引用 slice2 數(shù)組,將需要的數(shù)據(jù)復(fù)制到一個新的slice中,這樣新slice的底層數(shù)組,就和 slice2 數(shù)組無任何關(guān)系了。

func main() {slice2 := make([]int, 1000)// 錯誤用法slice1 := slice2[:1] // slice1 和 slice2 共用一個底層數(shù)組// 正確用法copy(slice1, slice2[:1])return
}

9、slice 并發(fā)不安全

??在Go語言中,slice是并發(fā)不安全的,主要有以下兩個原因:數(shù)據(jù)競爭、內(nèi)存重分配。slice底層的結(jié)構(gòu)體包含一個指向底層數(shù)組的指針和該數(shù)組的長度,當(dāng)多個協(xié)程并發(fā)訪問同一個slice時,有可能會出現(xiàn)數(shù)據(jù)競爭的問題。例如,一個協(xié)程在修改slice的長度,而另一個協(xié)程同時在讀取或修改slice的內(nèi)容。在向slice中追加元素時,可能會觸發(fā)slice的擴(kuò)容操作,在這個過程中,如果有其他協(xié)程訪問了slice,就會導(dǎo)致指向底層數(shù)組的指針出現(xiàn)異常。

??要并發(fā)安全,有兩種方法:加互斥鎖、使用channel串行化操作。加互斥鎖適合于對性能要求不高的場景,畢竟鎖的粒度太大,這種方式屬于通過共享內(nèi)存來實現(xiàn)通信。channle 適合于對性能要求大的場景,channle就是專用于goroutine間通信的,這種方式屬于通過通信來實現(xiàn)共享內(nèi)存。

10、從未初始化的 slice讀數(shù)據(jù)會發(fā)生什么?

??從未初始化的 slice 上讀取數(shù)據(jù),給未初始化的 slice 賦值,會發(fā)生運行時錯誤(panic),未初始化的 slice 沒有分配底層數(shù)組,指向底層數(shù)組的指針為 nil,因此不能存儲元素,讀取和賦值會 panic: runtime error: index out of range [0] with length 0。因此,在使用一個slice之前,必須確保它已經(jīng)正確地初始化。

http://www.risenshineclean.com/news/43868.html

相關(guān)文章:

  • 黃頁88網(wǎng)全自動錄播系統(tǒng)寧波百度推廣優(yōu)化
  • 如何給網(wǎng)站添加搜索關(guān)鍵字網(wǎng)絡(luò)營銷有哪些方式
  • web畢業(yè)設(shè)計題目西安seo王塵宇
  • 百度網(wǎng)站做防水補漏seo01
  • 醫(yī)療類網(wǎng)站源碼網(wǎng)絡(luò)推廣網(wǎng)上營銷
  • 網(wǎng)頁創(chuàng)建網(wǎng)站如何免費自己創(chuàng)建網(wǎng)站
  • asp.net旅游網(wǎng)站管理系統(tǒng)代碼軟文推廣多少錢一篇
  • 做網(wǎng)站專題需要什么軟件湖南靠譜關(guān)鍵詞優(yōu)化
  • 長治做網(wǎng)站公司網(wǎng)絡(luò)服務(wù)公司
  • 購物網(wǎng)站 服務(wù)器 帶寬 多大360搜索引擎地址
  • 網(wǎng)站開發(fā)怎么使用sharepoint網(wǎng)站推廣優(yōu)化外包便宜
  • 企業(yè)做的網(wǎng)站推廣方案的步驟深圳網(wǎng)站建設(shè)哪家好
  • 公司網(wǎng)站建設(shè)需要什么資質(zhì)購物網(wǎng)站頁面設(shè)計
  • 怎么選擇一家好的網(wǎng)站建設(shè)公司360優(yōu)化大師
  • 網(wǎng)站制作哪家專業(yè)微商怎么找客源人脈
  • 公司企業(yè)網(wǎng)站免費建設(shè)網(wǎng)絡(luò)營銷促銷方案
  • 做極速賽車網(wǎng)站公眾號推廣
  • 在百度網(wǎng)站備案查詢上顯示未備案是什么意思網(wǎng)頁設(shè)計素材
  • 所有政府網(wǎng)站必須做等保嗎sem運營是什么意思
  • 政務(wù)服務(wù) 網(wǎng)站 建設(shè)方案朋友圈推廣平臺
  • 網(wǎng)站收錄低的原因百度云網(wǎng)頁版登錄入口
  • 住房城鄉(xiāng)建設(shè)部辦公廳網(wǎng)站口碑營銷公司
  • 番禺區(qū)網(wǎng)站設(shè)計線上推廣的方式有哪些
  • 關(guān)于做美食的小視頻網(wǎng)站晚上免費b站軟件
  • 石家莊個人誰做網(wǎng)站廈門百度關(guān)鍵詞推廣
  • 網(wǎng)站優(yōu)化怎樣做網(wǎng)絡(luò)營銷整合推廣
  • 個人工作室可以做哪些項目win優(yōu)化大師怎么樣
  • 北京網(wǎng)站建設(shè)招聘網(wǎng)站域名查詢系統(tǒng)
  • wordpress 刪除略縮圖關(guān)鍵詞seo優(yōu)化公司
  • 做旅游銷售網(wǎng)站平臺ppt模板網(wǎng)頁設(shè)計的流程