GO開發:GIN面試題

GO開發:GIN面試題

Go語言面試問得最多的面試題

new 和 make 的區別

首先我們得知道,Go分為資料型別分為值型別和引用型別,其中

值型別是 int、float、string、bool、struct和array,它們直接儲存值,分配棧的記憶體空間,它們被函式呼叫完之後會釋放

引用型別是 slice、map、chan和值型別對應的指標 它們儲存是一個地址(或者理解為指標),指標指向記憶體中真正儲存資料的首地址,記憶體通常在堆分配,透過GC回收

區別

new 的引數要求傳入一個型別,而不是一個值,它會申請該型別的記憶體大小空間,並初始化為對應的零值,返回該指向型別空間的一個指標

make 也用於記憶體分配,但它只用於引用物件 slice、map、channel的記憶體建立,返回的型別是型別本身

值傳遞和指標傳遞有什麼區別

值傳遞:會建立一個新的副本並將其傳遞給所呼叫函式或方法 指標傳遞:將建立相同記憶體地址的新副本

需要改變傳入引數本身的時候用指標傳遞,否則值傳遞

另外,如果函式內部返回指標,會發生記憶體逃逸

聊聊記憶體逃逸分析

Go的逃逸分析是一種確定指標動態範圍的方法,可以分析程式在哪些可以訪問到指標,它涉及到指標分析和狀態分析。

當一個變數(或物件)在子程式中被分配時,一個指向變數的指標可能逃逸到其它程式,或者去呼叫子程式。

如果使用尾遞迴最佳化(通常函數語言程式設計是需要的),物件也可能逃逸到被呼叫程式中。如果一個子程式分配一個物件並返回一個該物件的指標,該物件可能在程式中的任何一個地方都可以訪問。

如果指標儲存在全域性變數或者其它資料結構中,它們也可能發生逃逸,這種情況就是當前程式的指標逃逸。逃逸分析需要確定指標所有可以儲存的地方,保證指標的生命週期只在當前程序或執行緒中。

導致記憶體逃逸的情況比較多(有些可能官方未能夠實現精確的逃逸分析情況的bug),通常來講就是如果變數的作用域不會擴大並且行為或者大小能夠在其編譯時確定,一般情況下都分配棧上,否則就可能發生記憶體逃逸到堆上。

引用記憶體逃逸的典型情況: *

在函式內部返回把區域性變數指標返回

區域性變數原本應該在棧中分配,在棧中回收。但是由於返回時被外部引用,因此生命週期大於棧,則溢位

傳送指標或帶有指標的值到channel中

在編譯時,是沒辦法知道哪個 goroutine 會在 channel上接受資料,所以編譯器沒辦法知道變數什麼時候釋放。

在一個切片上儲存指標或帶指標的值

一個典型的例子就是 []*string,這會導致切片的內容逃逸,儘管其後面的陣列在棧上分配,但其引用值一定是在堆上

slice 的背後陣列被重新分配了

因為 append 時可能會超出其容量( cap )。 slice 初始化的地方在編譯時是可以知道的,它最開始會在棧上分配。如果切片背後的儲存要基於執行時的資料進行擴充,就會在堆上分配。

在 interface 型別上呼叫方法

在 interface 型別上呼叫方法都是動態排程的 —— 方法的真正實現只能在執行時知道。想像一個 io。Reader 型別的變數 r , 呼叫 r。Read(b) 會使得 r 的值和切片b 的背後儲存都逃逸掉,所以會在堆上分配。

瞭解過golang的記憶體管理嗎

記憶體池概述

Go語言的記憶體分配器採用了跟

tcmalloc

庫相同的實現,是一個帶記憶體池的分配器,底層直接呼叫作業系統的 mmpa 等函式。

作為一個記憶體池,它的基本部分包括以下幾部分:

首先,它會想作業系統申請大塊記憶體,自己管理這部分記憶體

然後,它是一個池子,當上層釋放記憶體時它不實際歸還給作業系統,而是放回池子重複利用

接著,記憶體管理中必然會考慮的就是記憶體碎片問題,如果儘量避免記憶體碎片,提高記憶體利用率,像作業系統中的首次適應,最佳適應,最差適應,夥伴演算法都是一些相關的知識背景。

另外,Go語言是一個支援 goroutine 這種多執行緒的語言,所以它的記憶體管理系統必須要考慮在多執行緒下的穩定性和效率問題。

在多執行緒方面

很自然的做法就是每條執行緒都有自己的本地的記憶體,然後有一個全域性的分配鏈,當某個執行緒中的記憶體不足後就向全域性分配鏈中申請記憶體。這樣就避免了多執行緒同時訪問共享變數的加鎖。

在避免記憶體碎片方面,大塊記憶體直接按頁為單位分配,小塊記憶體會切成各種不同的固定大小的塊,申請做任意位元組記憶體時會向上取整到最接近的塊,將整塊分配給申請者以避免隨意切割。

在避免記憶體碎片方面

大塊記憶體直接按頁為單位分配,小塊記憶體會切成各種不同的固定大小的塊,申請做任意位元組記憶體時會向上取整到最接近的塊,將整塊分配給申請者以避免隨意切割。

Go語言中為每個系統執行緒分配一個本地的 MCahe,少量的地址分配就直接從 MCache 中分配,並且定期做垃圾回收,將執行緒的 MCache 中的空閒記憶體返回給全域性控制堆。小於 32K為小物件,大物件直接從全域性控制堆上以頁(4k)為單位進行分配,也就是說大物件總是以頁對齊的。一個頁可以存入一些相同大小的小物件,小物件從本地記憶體連結串列中分配,大物件從中心記憶體對分配。

大約有 100 種記憶體塊類別,每一個類別都有自己物件的空閒連結串列。小於 32KB 的記憶體分配被向上取整到對應的尺寸類別,從相應的空閒連結串列中分配。一頁記憶體只可以被分裂成同一種尺寸類別的物件,然後由空間連結串列分配管理器。

大約有 100 種記憶體塊類別,每一個類別都有自己物件的空閒連結串列。小於 32kB 的記憶體分配被向上取整到對應的尺寸類別,從相應的空閒連結串列中分配。一頁記憶體只可以被分裂成同一種尺寸類別的物件,然後由空閒連結串列分配器管理。

分配器的資料結構包括:

FixAlloc:固定大小(128kB)的物件的空閒鏈分配器,被分配器用於管理儲存;

MHeap:分配堆,按頁的粒度進行管理(4kB);

MSpan:一些由 MHeap 管理的頁;

MCentral:對於給定尺寸類別的共享的 free list; * MCache:用於小物件的每 M 一個的 cache。

我們可以將Go語言的記憶體管理看成一個兩級的記憶體管理結構 MHeap 和 MCache。上面一級管理的基本單位是頁,用於分配大物件,每次分配都是若干連續的頁,也就是若干個 4KB 的大小。使用的資料結構是 MHeap 和 MSpan,用 BestFit 演算法做分配,用位示圖做回收。下面一級管理的基本單位是不同型別的固定大小的物件,更像一個物件池而不是記憶體池,用引用計數做回收。下面這一級使用的資料結構是 MCache。

執行緒有幾種模型?Goroutine 的原理你瞭解過嗎,將一下實現和原理

執行緒模型有n

核心執行緒模型

使用者級執行緒模型

混合型執行緒模型

Linux歷史上執行緒的3種實現模型: 執行緒的實現曾有3種模型:

多對一(M:1)的使用者級執行緒模型

一對一(1:1)的核心級執行緒模型

多對多(M:N)的兩級執行緒模型

goroutine的原理

基於CSP併發模型開發了GMP排程器,其中 *

G(Goroutine)

: 每個 Goroutine 對應一個 G 結構體,G 儲存 Goroutine 的執行堆疊、狀態以及任務函式

M(Machine)

: 對OS核心級執行緒的封裝,數量對應真實的CPU數(真正幹活的物件)。

P (Processor)

: 邏輯處理器,即為G和M的排程物件,用來排程G和M之間的關聯關係,其數量可透過 GOMAXPROCS()來設定,預設為核心數。

在單核情況下,所有Goroutine執行在同一個執行緒(M0)中,每一個執行緒維護一個上下文(P),任何時刻,一個上下文中只有一個Goroutine,其他Goroutine在runqueue中等待。

一個Goroutine執行完自己的時間片後,讓出上下文,自己回到runqueue中(如下圖所示)。

GO開發:GIN面試題

當正在執行的G0阻塞的時候(可以需要IO),會再建立一個執行緒(M1),P轉到新的執行緒中去執行。

當M0返回時,它會嘗試從其他執行緒中“偷”一個上下文過來,如果沒有偷到,會把Goroutine放到Global runqueue中去,然後把自己放入執行緒快取中。 上下文會定時檢查Global runqueue。

goroutine的優勢

上下文切換代價小

:從GMP排程器可以看出,避免了使用者態和核心態執行緒切換,所以上下文切換代價小

記憶體佔用少

:執行緒棧空間通常是 2M,Goroutine 棧空間最小 2K;

goroutine 什麼時候發生阻塞

channel 在等待網路請求或者資料操作的IO返回的時候會發生阻塞

發生一次系統呼叫等待返回結果的時候

goroutine進行sleep操作的時候

在GPM排程模型,goroutine 有哪幾種狀態?執行緒呢?

有9種狀態

_Gidle

:剛剛被分配並且還沒有被初始化

_Grunnable

:沒有執行程式碼,沒有棧的所有權,儲存在執行佇列中

_Grunning

:可以執行程式碼,擁有棧的所有權,被賦予了核心執行緒 M 和處理器 P

_Gsyscall

:正在執行系統呼叫,擁有棧的所有權,沒有執行使用者程式碼,被賦予了核心執行緒 M 但是不在執行佇列上

_Gwaiting

:由於執行時而被阻塞,沒有執行使用者程式碼並且不在執行佇列上,但是可能存在於 Channel 的等待佇列上

_Gdead

:沒有被使用,沒有執行程式碼,可能有分配的棧

_Gcopystack

:棧正在被複製,沒有執行程式碼,不在執行佇列上

_Gpreempted

:由於搶佔而被阻塞,沒有執行使用者程式碼並且不在執行佇列上,等待喚醒

_Gscan

:GC 正在掃描棧空間,沒有執行程式碼,可以與其他狀態同時存在

去搶佔 G 的時候,會有一個自旋和非自旋的狀態

執行緒和協程記憶體多少

執行緒一般是2M,協程一般是2K

如果 goroutine 一直佔用資源怎麼辦,GMP模型怎麼解決這個問題

如果有一個goroutine一直佔用資源的話,GMP模型會從正常模式轉為飢餓模式,透過訊號協作強制處理在最前的 goroutine 去分配使用

如果若干個執行緒發生OOM,會發生什麼?Goroutine中記憶體洩漏的發現與排查?專案出現過OOM嗎,怎麼解決

執行緒

如果執行緒發生OOM,也就是記憶體溢位,發生OOM的執行緒會被kill掉,其它執行緒不受影響。

Goroutine中記憶體洩漏的發現與排查

go中的記憶體洩漏一般都是goroutine洩露,就是goroutine沒有被關閉,或者沒有新增超時控制,讓goroutine一隻處於阻塞狀態,不能被GC。

場景

在Go中記憶體洩露分為暫時性記憶體洩露和永久性記憶體洩露

暫時性記憶體洩露

獲取長字串中的一段導致長字串未釋放

獲取長slice中的一段導致長slice未釋放

在長slice新建slice導致洩漏

string相比切片少了一個容量的cap欄位,可以把string當成一個只讀的切片型別。獲取長string或者切片中的一段內容,由於新生成的物件和老的string或者切片共用一個記憶體空間,會導致老的string和切片資源暫時得不到釋放,造成短暫的記憶體洩漏

永久性記憶體洩露

goroutine永久阻塞而導致洩漏

time。Ticker未關閉導致洩漏

不正確使用Finalizer導致洩漏

使用pprof排查

Go的垃圾回收演算法

Go 1。5 後,採取的是併發標記和併發清除,三色標記的演算法

Go 中的 gc 基本上是標記清除的過程:

GO開發:GIN面試題

Go 的垃圾回收是基於標記清除演算法,這種演算法需要進行 STW (stop the world),這個過程就是會導致程式是卡頓的,頻繁的 GC 會嚴重影響程式效能

Go 在此基礎上進行了改進,透過三色標記清除掃法與寫屏障來減少 STW 的時間

GC 的過程一共分為四個階段:

棧掃描(STW),所有物件開始都是白色

從 root 開始找到所有可達物件(所有可以找到的物件),標記灰色,放入待處理佇列

遍歷灰色物件佇列,將其引用物件標記為灰色放入待處理佇列,自身標記為黑色

清除(併發)迴圈步驟3 直到灰色佇列為空為止,此時所有引用物件都被標記為黑色,所有不可達的物件依然為白色,白色的就是需要進行回收的物件。三色標記法相對於普通標記清除,減少了 STW 時間。這主要得益於標記過程是 “on-the-fly”的,在標記過程中是不需要 STW的,它與程式是併發執行的,這就大大縮短了 STW 的時間。

Go GC 最佳化的核心就是儘量使得 STW(Stop The World) 的時間越來越短。

寫屏障:

當標記和程式是併發執行的,這就會造成一個問題。 在標記過程中,有新的引用產生,可能會導致誤清掃。

清掃開始前,標記為黑色的物件引用了一個新申請的物件,它肯定是白色的,而黑色物件不會被再次掃描,那麼這個白色物件無法被掃描變成灰色、黑色,它就會最終被清掃,而實際它不應該被清掃。

這就需要用到屏障技術,golang採用了寫屏障,其作用就是為了避免這類誤清掃問題。 寫屏障即在記憶體寫操作前,維護一個約束,從而確保清掃開始前,黑色的物件不能引用白色物件。

Go資料競爭怎麼解決

Data Race 問題可以使用互斥鎖解決,或者也可以透過CAS無鎖併發解決

中使用同步訪問共享資料或者CAS無鎖併發是處理資料競爭的一種有效的方法。

golang在1。1之後引入了競爭檢測機制,可以使用 go run -race 或者 go build -race來進行靜態檢測。

其在內部的實現是,開啟多個協程執行同一個命令, 並且記錄下每個變數的狀態。

競爭檢測器基於C/C++的ThreadSanitizer執行時庫,該庫在Google內部程式碼基地和Chromium找到許多錯誤。這個技術在2012年九月整合到Go中,從那時開始,它已經在標準庫中檢測到42個競爭條件。現在,它已經是我們持續構建過程的一部分,當競爭條件出現時,它會繼續捕捉到這些錯誤。

競爭檢測器已經完全整合到Go工具鏈中,僅僅新增-race標誌到命令列就使用了檢測器。

$ go test -race mypkg // 測試包$ go run -race mysrc。go // 編譯和執行程式$ go build -race mycmd // 構建程式$ go install -race mypkg // 安裝程式

要想解決資料競爭的問題可以使用互斥鎖sync。Mutex,解決資料競爭(Data race),也可以使用管道解決,使用管道的效率要比互斥鎖高。

Go:反射之用字串函式名呼叫函式

package mainimport ( “fmt” “reflect”)type Animal struct {}func (m *Animal) Eat() { fmt。Println(“Eat”)}func main() { animal := Animal{} value := reflect。ValueOf(&animal) f := value。MethodByName(“Eat”) //透過反射獲取它對應的函式,然後透過call來呼叫 f。Call([]reflect。Value{})}

開發用過gin框架嗎?引數檢驗怎麼做的?中間使怎麼使用的

gin框架使用http://github。com/go-playground/validator進行引數校驗 在 struct 結構體新增 binding tag,然後呼叫 ShouldBing 方法,下面是一個示例

type SignUpParam struct { Age uint8 `json:“age” binding:“gte=1,lte=130”` Name string `json:“name” binding:“required”` Email string `json:“email” binding:“required,email”` Password string `json:“password” binding:“required”` RePassword string `json:“re_password” binding:“required,eqfield=Password”`}func main() { r := gin。Default() r。POST(“/signup”, func(c *gin。Context) { var u SignUpParam if err := c。ShouldBind(&u); err != nil { c。JSON(http。StatusOK, gin。H{ “msg”: err。Error(), }) return } // 儲存入庫等業務邏輯程式碼。。。 c。JSON(http。StatusOK, “success”) }) _ = r。Run(“:8999”)}

中介軟體使用use方法,Gin的中介軟體其實就是一個HandlerFunc,那麼只要我們自己實現一個HandlerFunc,下面是一個示例

func costTime() gin。HandlerFunc { return func(c *gin。Context) { //請求前獲取當前時間 nowTime := time。Now() //請求處理 c。Next() //處理後獲取消耗時間 costTime := time。Since(nowTime) url := c。Request。URL。String() fmt。Printf(“the request URL %s cost %v\n”, url, costTime) }}

以上我們就實現了一個Gin中介軟體,比較簡單,而且有註釋加以說明,這裡要注意的是c。Next方法,這個是執行後續中介軟體請求處理的意思(含沒有執行的中介軟體和我們定義的GET方法處理),這樣我們才能獲取執行的耗時。也就是在c。Next方法前後分別記錄時間,就可以得出耗時。

goroutine的鎖機制瞭解過嗎?Mutex有哪幾種模式?Mutex 鎖底層如何實現

互斥鎖的加鎖是靠 sync。Mutex。Lock 方法完成的, 當鎖的狀態是 0 時,將 mutexLocked 位置成 1:

// Lock locks m。// If the lock is already in use, the calling goroutine// blocks until the mutex is available。func (m *Mutex) Lock() { // Fast path: grab unlocked mutex。 if atomic。CompareAndSwapInt32(&m。state, 0, mutexLocked) { if race。Enabled { race。Acquire(unsafe。Pointer(m)) } return } // Slow path (outlined so that the fast path can be inlined) m。lockSlow()}

Mutex:正常模式和飢餓模式

在正常模式下,鎖的等待者會按照先進先出的順序獲取鎖。

但是剛被喚起的 Goroutine 與新建立的 Goroutine 競爭時,大機率會獲取不到鎖,為了減少這種情況的出現,一旦 Goroutine 超過 1ms 沒有獲取到鎖,它就會將當前互斥鎖切換飢餓模式,防止部分 Goroutine 被餓死。

飢餓模式是在 Go 語言 1。9 版本引入的最佳化的,引入的目的是保證互斥鎖的公平性(Fairness)。

在飢餓模式中,互斥鎖會直接交給等待佇列最前面的 Goroutine。新的 Goroutine 在該狀態下不能獲取鎖、也不會進入自旋狀態,它們只會在佇列的末尾等待。

如果一個 Goroutine 獲得了互斥鎖並且它在佇列的末尾或者它等待的時間少於 1ms,那麼當前的互斥鎖就會被切換回正常模式。

相比於飢餓模式,正常模式下的互斥鎖能夠提供更好地效能,飢餓模式的能避免 Goroutine 由於陷入等待無法獲取鎖而造成的高尾延時。