golang 面試題(從基礎到高級)


 

Golang面試問題匯總

通常我們去面試肯定會有些不錯的Golang的面試題目的,所以總結下,讓其他Golang開發者也可以查看到,同時也用來檢測自己的能力和提醒自己的不足之處,歡迎大家補充和提交新的面試題目.

Golang面試問題匯總:

1. Golang中除了加Mutex鎖以外還有哪些方式安全讀寫共享變量?

Golang中Goroutine 可以通過 Channel 進行安全讀寫共享變量。

2. 無緩沖 Chan 的發送和接收是否同步?

ch := make(chan int) 無緩沖的channel由於沒有緩沖發送和接收需要同步. ch := make(chan int, 2) 有緩沖channel不要求發送和接收操作同步. 
  • channel無緩沖時,發送阻塞直到數據被接收,接收阻塞直到讀到數據。
  • channel有緩沖時,當緩沖滿時發送阻塞,當緩沖空時接收阻塞。

3. go語言的並發機制以及它所使用的CSP並發模型.

CSP模型是上個世紀七十年代提出的,不同於傳統的多線程通過共享內存來通信,CSP講究的是“以通信的方式來共享內存”。用於描述兩個獨立的並發實體通過共享的通訊 channel(管道)進行通信的並發模型。 CSP中channel是第一類對象,它不關注發送消息的實體,而關注與發送消息時使用的channel。

Golang中channel 是被單獨創建並且可以在進程之間傳遞,它的通信模式類似於 boss-worker 模式的,一個實體通過將消息發送到channel 中,然后又監聽這個 channel 的實體處理,兩個實體之間是匿名的,這個就實現實體中間的解耦,其中 channel 是同步的一個消息被發送到 channel 中,最終是一定要被另外的實體消費掉的,在實現原理上其實類似一個阻塞的消息隊列。

Goroutine 是Golang實際並發執行的實體,它底層是使用協程(coroutine)實現並發,coroutine是一種運行在用戶態的用戶線程,類似於 greenthread,go底層選擇使用coroutine的出發點是因為,它具有以下特點:

  • 用戶空間 避免了內核態和用戶態的切換導致的成本。
  • 可以由語言和框架層進行調度。
  • 更小的棧空間允許創建大量的實例。

Golang中的Goroutine的特性:

Golang內部有三個對象: P對象(processor) 代表上下文(或者可以認為是cpu),M(work thread)代表工作線程,G對象(goroutine).

正常情況下一個cpu對象啟一個工作線程對象,線程去檢查並執行goroutine對象。碰到goroutine對象阻塞的時候,會啟動一個新的工作線程,以充分利用cpu資源。 所有有時候線程對象會比處理器對象多很多.

我們用如下圖分別表示P、M、G:

G(Goroutine) :我們所說的協程,為用戶級的輕量級線程,每個Goroutine對象中的sched保存着其上下文信息.

M(Machine) :對內核級線程的封裝,數量對應真實的CPU數(真正干活的對象).

P(Processor) :即為G和M的調度對象,用來調度G和M之間的關聯關系,其數量可通過GOMAXPROCS()來設置,默認為核心數.

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

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

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

當M0返回時,它會嘗試從其他線程中“偷”一個上下文過來,如果沒有偷到,會把Goroutine放到Global runqueue中去,然后把自己放入線程緩存中。 上下文會定時檢查Global runqueue。

Golang是為並發而生的語言,Go語言是為數不多的在語言層面實現並發的語言;也正是Go語言的並發特性,吸引了全球無數的開發者。

Golang的CSP並發模型,是通過Goroutine和Channel來實現的。

Goroutine 是Go語言中並發的執行單位。有點抽象,其實就是和傳統概念上的”線程“類似,可以理解為”線程“。 Channel是Go語言中各個並發結構體(Goroutine)之前的通信機制。通常Channel,是各個Goroutine之間通信的”管道“,有點類似於Linux中的管道。

通信機制channel也很方便,傳數據用channel <- data,取數據用<-channel。

在通信過程中,傳數據channel <- data和取數據<-channel必然會成對出現,因為這邊傳,那邊取,兩個goroutine之間才會實現通信。

而且不管傳還是取,必阻塞,直到另外的goroutine傳或者取為止。

4. Golang 中常用的並發模型?

Golang 中常用的並發模型有三種:

  • 通過channel通知實現並發控制

無緩沖的通道指的是通道的大小為0,也就是說,這種類型的通道在接收前沒有能力保存任何值,它要求發送 goroutine 和接收 goroutine 同時准備好,才可以完成發送和接收操作。

從上面無緩沖的通道定義來看,發送 goroutine 和接收 gouroutine 必須是同步的,同時准備后,如果沒有同時准備好的話,先執行的操作就會阻塞等待,直到另一個相對應的操作准備好為止。這種無緩沖的通道我們也稱之為同步通道。

func main() { ch := make(chan struct{}) go func() { fmt.Println("start working") time.Sleep(time.Second * 1) ch <- struct{}{} }() <-ch fmt.Println("finished") }

當主 goroutine 運行到 <-ch 接受 channel 的值的時候,如果該 channel 中沒有數據,就會一直阻塞等待,直到有值。 這樣就可以簡單實現並發控制

  • 通過sync包中的WaitGroup實現並發控制

Goroutine是異步執行的,有的時候為了防止在結束mian函數的時候結束掉Goroutine,所以需要同步等待,這個時候就需要用 WaitGroup了,在 sync 包中,提供了 WaitGroup ,它會等待它收集的所有 goroutine 任務全部完成。在WaitGroup里主要有三個方法:

  • Add, 可以添加或減少 goroutine的數量.
  • Done, 相當於Add(-1).
  • Wait, 執行后會堵塞主線程,直到WaitGroup 里的值減至0.

在主 goroutine 中 Add(delta int) 索要等待goroutine 的數量。 在每一個 goroutine 完成后 Done() 表示這一個goroutine 已經完成,當所有的 goroutine 都完成后,在主 goroutine 中 WaitGroup 返回返回。

func main(){ var wg sync.WaitGroup var urls = []string{ "http://www.golang.org/", "http://www.google.com/", } for _, url := range urls { wg.Add(1) go func(url string) { defer wg.Done() http.Get(url) }(url) } wg.Wait() }

在Golang官網中對於WaitGroup介紹是A WaitGroup must not be copied after first use,在 WaitGroup 第一次使用后,不能被拷貝

應用示例:

func main(){ wg := sync.WaitGroup{} for i := 0; i < 5; i++ { wg.Add(1) go func(wg sync.WaitGroup, i int) { fmt.Printf("i:%d", i) wg.Done() }(wg, i) } wg.Wait() fmt.Println("exit") } 

運行:

i:1i:3i:2i:0i:4fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]: sync.runtime_Semacquire(0xc000094018) /home/keke/soft/go/src/runtime/sema.go:56 +0x39 sync.(*WaitGroup).Wait(0xc000094010) /home/keke/soft/go/src/sync/waitgroup.go:130 +0x64 main.main() /home/keke/go/Test/wait.go:17 +0xab exit status 2

它提示所有的 goroutine 都已經睡眠了,出現了死鎖。這是因為 wg 給拷貝傳遞到了 goroutine 中,導致只有 Add 操作,其實 Done操作是在 wg 的副本執行的。

因此 Wait 就死鎖了。

這個第一個修改方式:將匿名函數中 wg 的傳入類型改為 *sync.WaitGrou,這樣就能引用到正確的WaitGroup了。 這個第二個修改方式:將匿名函數中的 wg 的傳入參數去掉,因為Go支持閉包類型,在匿名函數中可以直接使用外面的 wg 變量

  • 在Go 1.7 以后引進的強大的Context上下文,實現並發控制

通常,在一些簡單場景下使用 channel 和 WaitGroup 已經足夠了,但是當面臨一些復雜多變的網絡並發場景下 channel 和 WaitGroup 顯得有些力不從心了。 比如一個網絡請求 Request,每個 Request 都需要開啟一個 goroutine 做一些事情,這些 goroutine 又可能會開啟其他的 goroutine,比如數據庫和RPC服務。 所以我們需要一種可以跟蹤 goroutine 的方案,才可以達到控制他們的目的,這就是Go語言為我們提供的 Context,稱之為上下文非常貼切,它就是goroutine 的上下文。 它是包括一個程序的運行環境、現場和快照等。每個程序要運行時,都需要知道當前程序的運行狀態,通常Go 將這些封裝在一個 Context 里,再將它傳給要執行的 goroutine 。

context 包主要是用來處理多個 goroutine 之間共享數據,及多個 goroutine 的管理。

context 包的核心是 struct Context,接口聲明如下:

// A Context carries a deadline, cancelation signal, and request-scoped values // across API boundaries. Its methods are safe for simultaneous use by multiple // goroutines. type Context interface { // Done returns a channel that is closed when this `Context` is canceled // or times out. Done() <-chan struct{} // Err indicates why this Context was canceled, after the Done channel // is closed. Err() error // Deadline returns the time when this Context will be canceled, if any. Deadline() (deadline time.Time, ok bool) // Value returns the value associated with key or nil if none. Value(key interface{}) interface{} }

Done() 返回一個只能接受數據的channel類型,當該context關閉或者超時時間到了的時候,該channel就會有一個取消信號

Err() 在Done() 之后,返回context 取消的原因。

Deadline() 設置該context cancel的時間點

Value() 方法允許 Context 對象攜帶request作用域的數據,該數據必須是線程安全的。

Context 對象是線程安全的,你可以把一個 Context 對象傳遞給任意個數的 gorotuine,對它執行 取消 操作時,所有 goroutine 都會接收到取消信號。

一個 Context 不能擁有 Cancel 方法,同時我們也只能 Done channel 接收數據。 其中的原因是一致的:接收取消信號的函數和發送信號的函數通常不是一個。 典型的場景是:父操作為子操作操作啟動 goroutine,子操作也就不能取消父操作。

5. JSON 標准庫對 nil slice 和 空 slice 的處理是一致的嗎? 

首先JSON 標准庫對 nil slice 和 空 slice 的處理是不一致.

通常錯誤的用法,會報數組越界的錯誤,因為只是聲明了slice,卻沒有給實例化的對象。

var slice []int slice[1] = 0

此時slice的值是nil,這種情況可以用於需要返回slice的函數,當函數出現異常的時候,保證函數依然會有nil的返回值。

empty slice 是指slice不為nil,但是slice沒有值,slice的底層的空間是空的,此時的定義如下:

slice := make([]int,0) slice := []int{}

當我們查詢或者處理一個空的列表的時候,這非常有用,它會告訴我們返回的是一個列表,但是列表內沒有任何值。

總之,nil slice 和 empty slice是不同的東西,需要我們加以區分的.

6. 協程,線程,進程的區別。

  • 進程

進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位。每個進程都有自己的獨立內存空間,不同進程通過進程間通信來通信。由於進程比較重量,占據獨立的內存,所以上下文進程間的切換開銷(棧、寄存器、虛擬內存、文件句柄等)比較大,但相對比較穩定安全。

  • 線程

線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。線程間通信主要通過共享內存,上下文切換很快,資源開銷較少,但相比進程不夠穩定容易丟失數據。

  • 協程

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

7. 互斥鎖,讀寫鎖,死鎖問題是怎么解決。

  • 互斥鎖

互斥鎖就是互斥變量mutex,用來鎖住臨界區的.

條件鎖就是條件變量,當進程的某些資源要求不滿足時就進入休眠,也就是鎖住了。當資源被分配到了,條件鎖打開,進程繼續運行;讀寫鎖,也類似,用於緩沖區等臨界資源能互斥訪問的。

  • 讀寫鎖

通常有些公共數據修改的機會很少,但其讀的機會很多。並且在讀的過程中會伴隨着查找,給這種代碼加鎖會降低我們的程序效率。讀寫鎖可以解決這個問題。

注意:寫獨占,讀共享,寫鎖優先級高

  • 死鎖

一般情況下,如果同一個線程先后兩次調用lock,在第二次調用時,由於鎖已經被占用,該線程會掛起等待別的線程釋放鎖,然而鎖正是被自己占用着的,該線程又被掛起而沒有機會釋放鎖,因此就永遠處於掛起等待狀態了,這叫做死鎖(Deadlock)。 另外一種情況是:若線程A獲得了鎖1,線程B獲得了鎖2,這時線程A調用lock試圖獲得鎖2,結果是需要掛起等待線程B釋放鎖2,而這時線程B也調用lock試圖獲得鎖1,結果是需要掛起等待線程A釋放鎖1,於是線程A和B都永遠處於掛起狀態了。

死鎖產生的四個必要條件:

  1. 互斥條件:一個資源每次只能被一個進程使用
  2. 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
  3. 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
  4. 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關系。 這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。

a. 預防死鎖

可以把資源一次性分配:(破壞請求和保持條件)

然后剝奪資源:即當某進程新的資源未滿足時,釋放已占有的資源(破壞不可剝奪條件)

資源有序分配法:系統給每類資源賦予一個編號,每一個進程按編號遞增的順序請求資源,釋放則相反(破壞環路等待條件)

b. 避免死鎖

預防死鎖的幾種策略,會嚴重地損害系統性能。因此在避免死鎖時,要施加較弱的限制,從而獲得 較滿意的系統性能。由於在避免死鎖的策略中,允許進程動態地申請資源。因而,系統在進行資源分配之前預先計算資源分配的安全性。若此次分配不會導致系統進入不安全狀態,則將資源分配給進程;否則,進程等待。其中最具有代表性的避免死鎖算法是銀行家算法。

c. 檢測死鎖

首先為每個進程和每個資源指定一個唯一的號碼,然后建立資源分配表和進程等待表.

d. 解除死鎖

當發現有進程死鎖后,便應立即把它從死鎖狀態中解脫出來,常采用的方法有.

e. 剝奪資源

從其它進程剝奪足夠數量的資源給死鎖進程,以解除死鎖狀態.

f. 撤消進程

可以直接撤消死鎖進程或撤消代價最小的進程,直至有足夠的資源可用,死鎖狀態.消除為止.所謂代價是指優先級、運行代價、進程的重要性和價值等。

8. Golang的內存模型,為什么小對象多了會造成gc壓力。

通常小對象過多會導致GC三色法消耗過多的GPU。優化思路是,減少對象分配.

9. Data Race問題怎么解決?能不能不加鎖解決這個問題?

同步訪問共享數據是處理數據競爭的一種有效的方法.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),也可以使用管道解決,使用管道的效率要比互斥鎖高.

10. 什么是channel,為什么它可以做到線程安全?

Channel是Go中的一個核心類型,可以把它看成一個管道,通過它並發核心單元就可以發送或者接收數據進行通訊(communication),Channel也可以理解是一個先進先出的隊列,通過管道進行通信。

Golang的Channel,發送一個數據到Channel 和 從Channel接收一個數據 都是 原子性的。而且Go的設計思想就是:不要通過共享內存來通信,而是通過通信來共享內存,前者就是傳統的加鎖,后者就是Channel。也就是說,設計Channel的主要目的就是在多任務間傳遞數據的,這當然是安全的。

11. Epoll原理.

開發高性能網絡程序時,windows開發者們言必稱Iocp,linux開發者們則言必稱Epoll。大家都明白Epoll是一種IO多路復用技術,可以非常高效的處理數以百萬計的Socket句柄,比起以前的Select和Poll效率提高了很多。

先簡單了解下如何使用C庫封裝的3個epoll系統調用。

int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); 

使用起來很清晰,首先要調用epoll_create建立一個epoll對象。參數size是內核保證能夠正確處理的最大句柄數,多於這個最大數時內核可不保證效果。 epoll_ctl可以操作上面建立的epoll,例如,將剛建立的socket加入到epoll中讓其監控,或者把 epoll正在監控的某個socket句柄移出epoll,不再監控它等等。

epoll_wait在調用時,在給定的timeout時間內,當在監控的所有句柄中有事件發生時,就返回用戶態的進程。

從調用方式就可以看到epoll相比select/poll的優越之處是,因為后者每次調用時都要傳遞你所要監控的所有socket給select/poll系統調用,這意味着需要將用戶態的socket列表copy到內核態,如果以萬計的句柄會導致每次都要copy幾十幾百KB的內存到內核態,非常低效。而我們調用epoll_wait時就相當於以往調用select/poll,但是這時卻不用傳遞socket句柄給內核,因為內核已經在epoll_ctl中拿到了要監控的句柄列表。

所以,實際上在你調用epoll_create后,內核就已經在內核態開始准備幫你存儲要監控的句柄了,每次調用epoll_ctl只是在往內核的數據結構里塞入新的socket句柄。

在內核里,一切皆文件。所以,epoll向內核注冊了一個文件系統,用於存儲上述的被監控socket。當你調用epoll_create時,就會在這個虛擬的epoll文件系統里創建一個file結點。當然這個file不是普通文件,它只服務於epoll。

epoll在被內核初始化時(操作系統啟動),同時會開辟出epoll自己的內核高速cache區,用於安置每一個我們想監控的socket,這些socket會以紅黑樹的形式保存在內核cache里,以支持快速的查找、插入、刪除。這個內核高速cache區,就是建立連續的物理內存頁,然后在之上建立slab層,通常來講,就是物理上分配好你想要的size的內存對象,每次使用時都是使用空閑的已分配好的對象。

static int __init eventpoll_init(void) { ... ... /* Allocates slab cache used to allocate "struct epitem" items */ epi_cache = kmem_cache_create("eventpoll_epi", sizeof(struct epitem), 0, SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL); /* Allocates slab cache used to allocate "struct eppoll_entry" */ pwq_cache = kmem_cache_create("eventpoll_pwq", sizeof(struct eppoll_entry), 0, EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL); ... ... }

epoll的高效就在於,當我們調用epoll_ctl往里塞入百萬個句柄時,epoll_wait仍然可以飛快的返回,並有效的將發生事件的句柄給我們用戶。這是由於我們在調用epoll_create時,內核除了幫我們在epoll文件系統里建了個file結點,在內核cache里建了個紅黑樹用於存儲以后epoll_ctl傳來的socket外,還會再建立一個list鏈表,用於存儲准備就緒的事件,當epoll_wait調用時,僅僅觀察這個list鏈表里有沒有數據即可。有數據就返回,沒有數據就sleep,等到timeout時間到后即使鏈表沒數據也返回。所以,epoll_wait非常高效。

而且,通常情況下即使我們要監控百萬計的句柄,大多一次也只返回很少量的准備就緒句柄而已,所以,epoll_wait僅需要從內核態copy少量的句柄到用戶態而已,因此就會非常的高效!

然而,這個准備就緒list鏈表是怎么維護的呢?當我們執行epoll_ctl時,除了把socket放到epoll文件系統里file對象對應的紅黑樹上之外,還會給內核中斷處理程序注冊一個回調函數,告訴內核,如果這個句柄的中斷到了,就把它放到准備就緒list鏈表里。所以,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中后就來把socket插入到准備就緒鏈表里了。

如此,一個紅黑樹,一張准備就緒句柄鏈表,少量的內核cache,就幫我們解決了大並發下的socket處理問題。執行epoll_create時,創建了紅黑樹和就緒鏈表,執行epoll_ctl時,如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹干上,然后向內核注冊回調函數,用於當中斷事件來臨時向准備就緒鏈表中插入數據。執行epoll_wait時立刻返回准備就緒鏈表里的數據即可。

最后看看epoll獨有的兩種模式LT和ET。無論是LT和ET模式,都適用於以上所說的流程。區別是,LT模式下,只要一個句柄上的事件一次沒有處理完,會在以后調用epoll_wait時每次返回這個句柄,而ET模式僅在第一次返回。

當一個socket句柄上有事件時,內核會把該句柄插入上面所說的准備就緒list鏈表,這時我們調用epoll_wait,會把准備就緒的socket拷貝到用戶態內存,然后清空准備就緒list鏈表,最后,epoll_wait需要做的事情,就是檢查這些socket,如果不是ET模式(就是LT模式的句柄了),並且這些socket上確實有未處理的事件時,又把該句柄放回到剛剛清空的准備就緒鏈表了。所以,非ET的句柄,只要它上面還有事件,epoll_wait每次都會返回。而ET模式的句柄,除非有新中斷到,即使socket上的事件沒有處理完,也是不會每次從epoll_wait返回的。

因此epoll比select的提高實際上是一個用空間換時間思想的具體應用.對比阻塞IO的處理模型, 可以看到采用了多路復用IO之后, 程序可以自由的進行自己除了IO操作之外的工作, 只有到IO狀態發生變化的時候由多路復用IO進行通知, 然后再采取相應的操作, 而不用一直阻塞等待IO狀態發生變化,提高效率.

12. Golang GC 時會發生什么?

首先我們先來了解下垃圾回收.什么是垃圾回收?

內存管理是程序員開發應用的一大難題。傳統的系統級編程語言(主要指C/C++)中,程序開發者必須對內存小心的進行管理操作,控制內存的申請及釋放。因為稍有不慎,就可能產生內存泄露問題,這種問題不易發現並且難以定位,一直成為困擾程序開發者的噩夢。如何解決這個頭疼的問題呢?

過去一般采用兩種辦法:

  • 內存泄露檢測工具。這種工具的原理一般是靜態代碼掃描,通過掃描程序檢測可能出現內存泄露的代碼段。然而檢測工具難免有疏漏和不足,只能起到輔助作用。

  • 智能指針。這是 c++ 中引入的自動內存管理方法,通過擁有自動內存管理功能的指針對象來引用對象,是程序員不用太關注內存的釋放,而達到內存自動釋放的目的。這種方法是采用最廣泛的做法,但是對程序開發者有一定的學習成本(並非語言層面的原生支持),而且一旦有忘記使用的場景依然無法避免內存泄露。

為了解決這個問題,后來開發出來的幾乎所有新語言(java,python,php等等)都引入了語言層面的自動內存管理 – 也就是語言的使用者只用關注內存的申請而不必關心內存的釋放,內存釋放由虛擬機(virtual machine)或運行時(runtime)來自動進行管理。而這種對不再使用的內存資源進行自動回收的行為就被稱為垃圾回收。

常用的垃圾回收的方法:

  • 引用計數(reference counting)

這是最簡單的一種垃圾回收算法,和之前提到的智能指針異曲同工。對每個對象維護一個引用計數,當引用該對象的對象被銷毀或更新時被引用對象的引用計數自動減一,當被引用對象被創建或被賦值給其他對象時引用計數自動加一。當引用計數為0時則立即回收對象。

這種方法的優點是實現簡單,並且內存的回收很及時。這種算法在內存比較緊張和實時性比較高的系統中使用的比較廣泛,如ios cocoa框架,php,python等。

但是簡單引用計數算法也有明顯的缺點:

  1. 頻繁更新引用計數降低了性能。

一種簡單的解決方法就是編譯器將相鄰的引用計數更新操作合並到一次更新;還有一種方法是針對頻繁發生的臨時變量引用不進行計數,而是在引用達到0時通過掃描堆棧確認是否還有臨時對象引用而決定是否釋放。等等還有很多其他方法,具體可以參考這里。

  1. 循環引用。

當對象間發生循環引用時引用鏈中的對象都無法得到釋放。最明顯的解決辦法是避免產生循環引用,如cocoa引入了strong指針和weak指針兩種指針類型。或者系統檢測循環引用並主動打破循環鏈。當然這也增加了垃圾回收的復雜度。

  • 標記-清除(mark and sweep)

標記-清除(mark and sweep)分為兩步,標記從根變量開始迭代得遍歷所有被引用的對象,對能夠通過應用遍歷訪問到的對象都進行標記為“被引用”;標記完成后進行清除操作,對沒有標記過的內存進行回收(回收同時可能伴有碎片整理操作)。這種方法解決了引用計數的不足,但是也有比較明顯的問題:每次啟動垃圾回收都會暫停當前所有的正常代碼執行,回收是系統響應能力大大降低!當然后續也出現了很多mark&sweep算法的變種(如三色標記法)優化了這個問題。

  • 分代搜集(generation)

java的jvm 就使用的分代回收的思路。在面向對象編程語言中,絕大多數對象的生命周期都非常短。分代收集的基本思想是,將堆划分為兩個或多個稱為代(generation)的空間。新創建的對象存放在稱為新生代(young generation)中(一般來說,新生代的大小會比 老年代小很多),隨着垃圾回收的重復執行,生命周期較長的對象會被提升(promotion)到老年代中(這里用到了一個分類的思路,這個是也是科學思考的一個基本思路)。

因此,新生代垃圾回收和老年代垃圾回收兩種不同的垃圾回收方式應運而生,分別用於對各自空間中的對象執行垃圾回收。新生代垃圾回收的速度非常快,比老年代快幾個數量級,即使新生代垃圾回收的頻率更高,執行效率也仍然比老年代垃圾回收強,這是因為大多數對象的生命周期都很短,根本無需提升到老年代。

Golang GC 時會發生什么?

Golang 1.5后,采取的是“非分代的、非移動的、並發的、三色的”標記清除垃圾回收算法。

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

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

  1. 棧掃描(開始時STW)
  2. 第一次標記(並發)
  3. 第二次標記(STW)
  4. 清除(並發)

整個進程空間里申請每個對象占據的內存可以視為一個圖,初始狀態下每個內存對象都是白色標記。

  1. 先STW,做一些准備工作,比如 enable write barrier。然后取消STW,將掃描任務作為多個並發的goroutine立即入隊給調度器,進而被CPU處理
  2. 第一輪先掃描root對象,包括全局指針和 goroutine 棧上的指針,標記為灰色放入隊列
  3. 第二輪將第一步隊列中的對象引用的對象置為灰色加入隊列,一個對象引用的所有對象都置灰並加入隊列后,這個對象才能置為黑色並從隊列之中取出。循環往復,最后隊列為空時,整個圖剩下的白色內存空間即不可到達的對象,即沒有被引用的對象;
  4. 第三輪再次STW,將第二輪過程中新增對象申請的內存進行標記(灰色),這里使用了write barrier(寫屏障)去記錄

Golang gc 優化的核心就是盡量使得 STW(Stop The World) 的時間越來越短。

詳細的Golang的GC介紹可以參看Golang垃圾回收.

13. Golang 中 Goroutine 如何調度?

goroutine是Golang語言中最經典的設計,也是其魅力所在,goroutine的本質是協程,是實現並行計算的核心。 goroutine使用方式非常的簡單,只需使用go關鍵字即可啟動一個協程,並且它是處於異步方式運行,你不需要等它運行完成以后在執行以后的代碼。

go func()//通過go關鍵字啟動一個協程來運行函數

協程:

協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。 因此,協程能保留上一次調用時的狀態(即所有局部狀態的一個特定組合),每次過程重入時,就相當於進入上一次調用的狀態,換種說法:進入上一次離開時所處邏輯流的位置。 線程和進程的操作是由程序觸發系統接口,最后的執行者是系統;協程的操作執行者則是用戶自身程序,goroutine也是協程。

groutine能擁有強大的並發實現是通過GPM調度模型實現.

Go的調度器內部有四個重要的結構:M,P,S,Sched,如上圖所示(Sched未給出).

  • M:M代表內核級線程,一個M就是一個線程,goroutine就是跑在M之上的;M是一個很大的結構,里面維護小對象內存cache(mcache)、當前執行的goroutine、隨機數發生器等等非常多的信息
  • G:代表一個goroutine,它有自己的棧,instruction pointer和其他信息(正在等待的channel等等),用於調度。
  • P:P全稱是Processor,處理器,它的主要用途就是用來執行goroutine的,所以它也維護了一個goroutine隊列,里面存儲了所有需要它來執行的goroutine
  • Sched:代表調度器,它維護有存儲M和G的隊列以及調度器的一些狀態信息等。

調度實現:

從上圖中可以看到,有2個物理線程M,每一個M都擁有一個處理器P,每一個也都有一個正在運行的goroutine。P的數量可以通過GOMAXPROCS()來設置,它其實也就代表了真正的並發度,即有多少個goroutine可以同時運行。

圖中灰色的那些goroutine並沒有運行,而是出於ready的就緒態,正在等待被調度。P維護着這個隊列(稱之為runqueue),Go語言里,啟動一個goroutine很容易:go function 就行,所以每有一個go語句被執行,runqueue隊列就在其末尾加入一個goroutine,在下一個調度點,就從runqueue中取出(如何決定取哪個goroutine?)一個goroutine執行。

當一個OS線程M0陷入阻塞時,P轉而在運行M1,圖中的M1可能是正被創建,或者從線程緩存中取出。

當MO返回時,它必須嘗試取得一個P來運行goroutine,一般情況下,它會從其他的OS線程那里拿一個P過來, 如果沒有拿到的話,它就把goroutine放在一個global runqueue里,然后自己睡眠(放入線程緩存里)。所有的P也會周期性的檢查global runqueue並運行其中的goroutine,否則global runqueue上的goroutine永遠無法執行。

另一種情況是P所分配的任務G很快就執行完了(分配不均),這就導致了這個處理器P很忙,但是其他的P還有任務,此時如果global runqueue沒有任務G了,那么P不得不從其他的P里拿一些G來執行。

通常來說,如果P從其他的P那里要拿任務的話,一般就拿run queue的一半,這就確保了每個OS線程都能充分的使用。

14. 並發編程概念是什么?

並行是指兩個或者多個事件在同一時刻發生;並發是指兩個或多個事件在同一時間間隔發生。

並行是在不同實體上的多個事件,並發是在同一實體上的多個事件。在一台處理器上“同時”處理多個任務,在多台處理器上同時處理多個任務。如hadoop分布式集群

並發偏重於多個任務交替執行,而多個任務之間有可能還是串行的。而並行是真正意義上的“同時執行”。

並發編程是指在一台處理器上“同時”處理多個任務。並發是在同一實體上的多個事件。多個事件在同一時間間隔發生。並發編程的目標是充分的利用處理器的每一個核,以達到最高的處理性能。

15. 負載均衡原理是什么?

負載均衡Load Balance)是高可用網絡基礎架構的關鍵組件,通常用於將工作負載分布到多個服務器來提高網站、應用、數據庫或其他服務的性能和可靠性。負載均衡,其核心就是網絡流量分發,分很多維度。

負載均衡(Load Balance)通常是分攤到多個操作單元上進行執行,例如Web服務器、FTP服務器、企業關鍵應用服務器和其它關鍵任務服務器等,從而共同完成工作任務。

負載均衡是建立在現有網絡結構之上,它提供了一種廉價有效透明的方法擴展網絡設備和服務器的帶寬、增加吞吐量、加強網絡數據處理能力、提高網絡的靈活性和可用性。

通過一個例子詳細介紹:

  • 沒有負載均衡 web 架構

在這里用戶是直連到 web 服務器,如果這個服務器宕機了,那么用戶自然也就沒辦法訪問了。 另外,如果同時有很多用戶試圖訪問服務器,超過了其能處理的極限,就會出現加載速度緩慢或根本無法連接的情況。

而通過在后端引入一個負載均衡器和至少一個額外的 web 服務器,可以緩解這個故障。 通常情況下,所有的后端服務器會保證提供相同的內容,以便用戶無論哪個服務器響應,都能收到一致的內容。

  • 有負載均衡 web 架構

用戶訪問負載均衡器,再由負載均衡器將請求轉發給后端服務器。在這種情況下,單點故障現在轉移到負載均衡器上了。 這里又可以通過引入第二個負載均衡器來緩解。

那么負載均衡器的工作方式是什么樣的呢,負載均衡器又可以處理什么樣的請求?

負載均衡器的管理員能主要為下面四種主要類型的請求設置轉發規則:

  • HTTP (七層)
  • HTTPS (七層)
  • TCP (四層)
  • UDP (四層)

負載均衡器如何選擇要轉發的后端服務器?

負載均衡器一般根據兩個因素來決定要將請求轉發到哪個服務器。首先,確保所選擇的服務器能夠對請求做出響應,然后根據預先配置的規則從健康服務器池(healthy pool)中進行選擇。

因為,負載均衡器應當只選擇能正常做出響應的后端服務器,因此就需要有一種判斷后端服務器是否健康的方法。為了監視后台服務器的運行狀況,運行狀態檢查服務會定期嘗試使用轉發規則定義的協議和端口去連接后端服務器。 如果,服務器無法通過健康檢查,就會從池中剔除,保證流量不會被轉發到該服務器,直到其再次通過健康檢查為止。

負載均衡算法

負載均衡算法決定了后端的哪些健康服務器會被選中。 其中常用的算法包括:

  • Round Robin(輪詢):為第一個請求選擇列表中的第一個服務器,然后按順序向下移動列表直到結尾,然后循環。
  • Least Connections(最小連接):優先選擇連接數最少的服務器,在普遍會話較長的情況下推薦使用。
  • Source:根據請求源的 IP 的散列(hash)來選擇要轉發的服務器。這種方式可以一定程度上保證特定用戶能連接到相同的服務器。

如果你的應用需要處理狀態而要求用戶能連接到和之前相同的服務器。可以通過 Source 算法基於客戶端的 IP 信息創建關聯,或者使用粘性會話(sticky sessions)。

除此之外,想要解決負載均衡器的單點故障問題,可以將第二個負載均衡器連接到第一個上,從而形成一個集群。

16. LVS相關了解.

LVS是 Linux Virtual Server 的簡稱,也就是Linux虛擬服務器。這是一個由章文嵩博士發起的一個開源項目,它的官方網站是LinuxVirtualServer現在 LVS 已經是 Linux 內核標准的一部分。使用 LVS 可以達到的技術目標是:通過 LVS 達到的負載均衡技術和 Linux 操作系統實現一個高性能高可用的 Linux 服務器集群,它具有良好的可靠性、可擴展性和可操作性。 從而以低廉的成本實現最優的性能。LVS 是一個實現負載均衡集群的開源軟件項目,LVS架構從邏輯上可分為調度層、Server集群層和共享存儲。

LVS的基本工作原理:

  1. 當用戶向負載均衡調度器(Director Server)發起請求,調度器將請求發往至內核空間
  2. PREROUTING鏈首先會接收到用戶請求,判斷目標IP確定是本機IP,將數據包發往INPUT鏈
  3. IPVS是工作在INPUT鏈上的,當用戶請求到達INPUT時,IPVS會將用戶請求和自己已定義好的集群服務進行比對,如果用戶請求的就是定義的集群服務,那么此時IPVS會強行修改數據包里的目標IP地址及端口,並將新的數據包發往POSTROUTING鏈
  4. POSTROUTING鏈接收數據包后發現目標IP地址剛好是自己的后端服務器,那么此時通過選路,將數據包最終發送給后端的服務器

LVS的組成:

LVS 由2部分程序組成,包括 ipvs 和 ipvsadm

  1. ipvs(ip virtual server):一段代碼工作在內核空間,叫ipvs,是真正生效實現調度的代碼。
  2. ipvsadm:另外一段是工作在用戶空間,叫ipvsadm,負責為ipvs內核框架編寫規則,定義誰是集群服務,而誰是后端真實的服務器(Real Server)

詳細的LVS的介紹可以參考LVS詳解.

17. 微服務架構是什么樣子的?

通常傳統的項目體積龐大,需求、設計、開發、測試、部署流程固定。新功能需要在原項目上做修改。

但是微服務可以看做是對大項目的拆分,是在快速迭代更新上線的需求下產生的。新的功能模塊會發布成新的服務組件,與其他已發布的服務組件一同協作。 服務內部有多個生產者和消費者,通常以http rest的方式調用,服務總體以一個(或幾個)服務的形式呈現給客戶使用。

微服務架構是一種思想對微服務架構我們沒有一個明確的定義,但簡單來說微服務架構是:

采用一組服務的方式來構建一個應用,服務獨立部署在不同的進程中,不同服務通過一些輕量級交互機制來通信,例如 RPC、HTTP 等,服務可獨立擴展伸縮,每個服務定義了明確的邊界,不同的服務甚至可以采用不同的編程語言來實現,由獨立的團隊來維護。

Golang的微服務框架kit中有詳細的微服務的例子,可以參考學習.

微服務架構設計包括:

  1. 服務熔斷降級限流機制 熔斷降級的概念(Rate Limiter 限流器,Circuit breaker 斷路器).
  2. 框架調用方式解耦方式 Kit 或 Istio 或 Micro 服務發現(consul zookeeper kubeneters etcd ) RPC調用框架.
  3. 鏈路監控,zipkin和prometheus.
  4. 多級緩存.
  5. 網關 (kong gateway).
  6. Docker部署管理 Kubenetters.
  7. 自動集成部署 CI/CD 實踐.
  8. 自動擴容機制規則.
  9. 壓測 優化.
  10. Trasport 數據傳輸(序列化和反序列化).
  11. Logging 日志.
  12. Metrics 指針對每個請求信息的儀表盤化.

微服務架構介紹詳細的可以參考:

18. 分布式鎖實現原理,用過嗎?

在分析分布式鎖的三種實現方式之前,先了解一下分布式鎖應該具備哪些條件:

  1. 在分布式系統環境下,一個方法在同一時間只能被一個機器的一個線程執行;
  2. 高可用的獲取鎖與釋放鎖;
  3. 高性能的獲取鎖與釋放鎖;
  4. 具備可重入特性;
  5. 具備鎖失效機制,防止死鎖;
  6. 具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗。

分布式的CAP理論告訴我們“任何一個分布式系統都無法同時滿足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多只能同時滿足兩項。”所以,很多系統在設計之初就要對這三者做出取舍。在互聯網領域的絕大多數的場景中,都需要犧牲強一致性來換取系統的高可用性,系統往往只需要保證“最終一致性”,只要這個最終時間是在用戶可以接受的范圍內即可。

通常分布式鎖以單獨的服務方式實現,目前比較常用的分布式鎖實現有三種:

  • 基於數據庫實現分布式鎖。
  • 基於緩存(redis,memcached,tair)實現分布式鎖。
  • 基於Zookeeper實現分布式鎖。

盡管有這三種方案,但是不同的業務也要根據自己的情況進行選型,他們之間沒有最好只有更適合!

  • 基於數據庫的實現方式

基於數據庫的實現方式的核心思想是:在數據庫中創建一個表,表中包含方法名等字段,並在方法名字段上創建唯一索引,想要執行某個方法,就使用這個方法名向表中插入數據,成功插入則獲取鎖,執行完成后刪除對應的行數據釋放鎖。

創建一個表:

DROP TABLE IF EXISTS `method_lock`; CREATE TABLE `method_lock` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `method_name` varchar(64) NOT NULL COMMENT '鎖定的方法名', `desc` varchar(255) NOT NULL COMMENT '備注信息', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';

想要執行某個方法,就使用這個方法名向表中插入數據:

INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '測試的methodName');

因為我們對method_name做了唯一性約束,這里如果有多個請求同時提交到數據庫的話,數據庫會保證只有一個操作可以成功,那么我們就可以認為操作成功的那個線程獲得了該方法的鎖,可以執行方法體內容。

成功插入則獲取鎖,執行完成后刪除對應的行數據釋放鎖:

delete from method_lock where method_name ='methodName';

注意:這里只是使用基於數據庫的一種方法,使用數據庫實現分布式鎖還有很多其他的用法可以實現!

使用基於數據庫的這種實現方式很簡單,但是對於分布式鎖應該具備的條件來說,它有一些問題需要解決及優化:

1、因為是基於數據庫實現的,數據庫的可用性和性能將直接影響分布式鎖的可用性及性能,所以,數據庫需要雙機部署、數據同步、主備切換;

2、不具備可重入的特性,因為同一個線程在釋放鎖之前,行數據一直存在,無法再次成功插入數據,所以,需要在表中新增一列,用於記錄當前獲取到鎖的機器和線程信息,在再次獲取鎖的時候,先查詢表中機器和線程信息是否和當前機器和線程相同,若相同則直接獲取鎖;

3、沒有鎖失效機制,因為有可能出現成功插入數據后,服務器宕機了,對應的數據沒有被刪除,當服務恢復后一直獲取不到鎖,所以,需要在表中新增一列,用於記錄失效時間,並且需要有定時任務清除這些失效的數據;

4、不具備阻塞鎖特性,獲取不到鎖直接返回失敗,所以需要優化獲取邏輯,循環多次去獲取。

5、在實施的過程中會遇到各種不同的問題,為了解決這些問題,實現方式將會越來越復雜;依賴數據庫需要一定的資源開銷,性能問題需要考慮。

  • 基於Redis的實現方式

選用Redis實現分布式鎖原因:

  1. Redis有很高的性能;
  2. Redis命令對此支持較好,實現起來比較方便

主要實現方式:

  1. SET lock currentTime+expireTime EX 600 NX,使用set設置lock值,並設置過期時間為600秒,如果成功,則獲取鎖;
  2. 獲取鎖后,如果該節點掉線,則到過期時間ock值自動失效;
  3. 釋放鎖時,使用del刪除lock鍵值;

使用redis單機來做分布式鎖服務,可能會出現單點問題,導致服務可用性差,因此在服務穩定性要求高的場合,官方建議使用redis集群(例如5台,成功請求鎖超過3台就認為獲取鎖),來實現redis分布式鎖。詳見RedLock。

優點:性能高,redis可持久化,也能保證數據不易丟失,redis集群方式提高穩定性。

缺點:使用redis主從切換時可能丟失部分數據。

  • 基於ZooKeeper的實現方式

ZooKeeper是一個為分布式應用提供一致性服務的開源組件,它內部是一個分層的文件系統目錄樹結構,規定同一個目錄下只能有一個唯一文件名。基於ZooKeeper實現分布式鎖的步驟如下:

  1. 創建一個目錄mylock;
  2. 線程A想獲取鎖就在mylock目錄下創建臨時順序節點;
  3. 獲取mylock目錄下所有的子節點,然后獲取比自己小的兄弟節點,如果不存在,則說明當前線程順序號最小,獲得鎖;
  4. 線程B獲取所有節點,判斷自己不是最小節點,設置監聽比自己次小的節點;
  5. 線程A處理完,刪除自己的節點,線程B監聽到變更事件,判斷自己是不是最小的節點,如果是則獲得鎖。

這里推薦一個Apache的開源庫Curator,它是一個ZooKeeper客戶端,Curator提供的InterProcessMutex是分布式鎖的實現,acquire方法用於獲取鎖,release方法用於釋放鎖。

優點:具備高可用、可重入、阻塞鎖特性,可解決失效死鎖問題。

缺點:因為需要頻繁的創建和刪除節點,性能上不如Redis方式。

上面的三種實現方式,沒有在所有場合都是完美的,所以,應根據不同的應用場景選擇最適合的實現方式。

在分布式環境中,對資源進行上鎖有時候是很重要的,比如搶購某一資源,這時候使用分布式鎖就可以很好地控制資源。

19. Etcd怎么實現分布式鎖?

首先思考下Etcd是什么?可能很多人第一反應可能是一個鍵值存儲倉庫,卻沒有重視官方定義的后半句,用於配置共享和服務發現。

A highly-available key value store for shared configuration and service discovery.

實際上,etcd 作為一個受到 ZooKeeper 與 doozer 啟發而催生的項目,除了擁有與之類似的功能外,更專注於以下四點。

  • 簡單:基於 HTTP+JSON 的 API 讓你用 curl 就可以輕松使用。
  • 安全:可選 SSL 客戶認證機制。
  • 快速:每個實例每秒支持一千次寫操作。
  • 可信:使用 Raft 算法充分實現了分布式。

但是這里我們主要講述Etcd如何實現分布式鎖?

因為 Etcd 使用 Raft 算法保持了數據的強一致性,某次操作存儲到集群中的值必然是全局一致的,所以很容易實現分布式鎖。鎖服務有兩種使用方式,一是保持獨占,二是控制時序。

  • 保持獨占即所有獲取鎖的用戶最終只有一個可以得到。etcd 為此提供了一套實現分布式鎖原子操作 CAS(CompareAndSwap)的 API。通過設置prevExist值,可以保證在多個節點同時去創建某個目錄時,只有一個成功。而創建成功的用戶就可以認為是獲得了鎖。

  • 控制時序,即所有想要獲得鎖的用戶都會被安排執行,但是獲得鎖的順序也是全局唯一的,同時決定了執行順序。etcd 為此也提供了一套 API(自動創建有序鍵),對一個目錄建值時指定為POST動作,這樣 etcd 會自動在目錄下生成一個當前最大的值為鍵,存儲這個新的值(客戶端編號)。同時還可以使用 API 按順序列出所有當前目錄下的鍵值。此時這些鍵的值就是客戶端的時序,而這些鍵中存儲的值可以是代表客戶端的編號。

在這里Ectd實現分布式鎖基本實現原理為:

  1. 在ectd系統里創建一個key
  2. 如果創建失敗,key存在,則監聽該key的變化事件,直到該key被刪除,回到1
  3. 如果創建成功,則認為我獲得了鎖

應用示例:

package etcdsync

import ( "fmt" "io" "os" "sync" "time" "github.com/coreos/etcd/client" "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context" ) const ( defaultTTL = 60 defaultTry = 3 deleteAction = "delete" expireAction = "expire" ) // A Mutex is a mutual exclusion lock which is distributed across a cluster. type Mutex struct { key string id string // The identity of the caller client client.Client kapi client.KeysAPI ctx context.Context ttl time.Duration mutex *sync.Mutex logger io.Writer } // New creates a Mutex with the given key which must be the same // across the cluster nodes. // machines are the ectd cluster addresses func New(key string, ttl int, machines []string) *Mutex { cfg := client.Config{ Endpoints: machines, Transport: client.DefaultTransport, HeaderTimeoutPerRequest: time.Second, } c, err := client.New(cfg) if err != nil { return nil } hostname, err := os.Hostname() if err != nil { return nil } if len(key) == 0 || len(machines) == 0 { return nil } if key[0] != '/' { key = "/" + key } if ttl < 1 { ttl = defaultTTL } return &Mutex{ key: key, id: fmt.Sprintf("%v-%v-%v", hostname, os.Getpid(), time.Now().Format("20060102-15:04:05.999999999")), client: c, kapi: client.NewKeysAPI(c), ctx: context.TODO(), ttl: time.Second * time.Duration(ttl), mutex: new(sync.Mutex), } } // Lock locks m. // If the lock is already in use, the calling goroutine // blocks until the mutex is available. func (m *Mutex) Lock() (err error) { m.mutex.Lock() for try := 1; try <= defaultTry; try++ { if m.lock() == nil { return nil } m.debug("Lock node %v ERROR %v", m.key, err) if try < defaultTry { m.debug("Try to lock node %v again", m.key, err) } } return err } func (m *Mutex) lock() (err error) { m.debug("Trying to create a node : key=%v", m.key) setOptions := &client.SetOptions{ PrevExist:client.PrevNoExist, TTL: m.ttl, } resp, err := m.kapi.Set(m.ctx, m.key, m.id, setOptions) if err == nil { m.debug("Create node %v OK [%q]", m.key, resp) return nil } m.debug("Create node %v failed [%v]", m.key, err) e, ok := err.(client.Error) if !ok { return err } if e.Code != client.ErrorCodeNodeExist { return err } // Get the already node's value. resp, err = m.kapi.Get(m.ctx, m.key, nil) if err != nil { return err } m.debug("Get node %v OK", m.key) watcherOptions := &client.WatcherOptions{ AfterIndex : resp.Index, Recursive:false, } watcher := m.kapi.Watcher(m.key, watcherOptions) for { m.debug("Watching %v ...", m.key) resp, err = watcher.Next(m.ctx) if err != nil { return err } m.debug("Received an event : %q", resp) if resp.Action == deleteAction || resp.Action == expireAction { return nil } } } // Unlock unlocks m. // It is a run-time error if m is not locked on entry to Unlock. // // A locked Mutex is not associated with a particular goroutine. // It is allowed for one goroutine to lock a Mutex and then // arrange for another goroutine to unlock it. func (m *Mutex) Unlock() (err error) { defer m.mutex.Unlock() for i := 1; i <= defaultTry; i++ { var resp *client.Response resp, err = m.kapi.Delete(m.ctx, m.key, nil) if err == nil { m.debug("Delete %v OK", m.key) return nil } m.debug("Delete %v falied: %q", m.key, resp) e, ok := err.(client.Error) if ok && e.Code == client.ErrorCodeKeyNotFound { return nil } } return err } func (m *Mutex) debug(format string, v ...interface{}) { if m.logger != nil { m.logger.Write([]byte(m.id)) m.logger.Write([]byte(" ")) m.logger.Write([]byte(fmt.Sprintf(format, v...))) m.logger.Write([]byte("\n")) } } func (m *Mutex) SetDebugLogger(w io.Writer) { m.logger = w } 

其實類似的實現有很多,但目前都已經過時,使用的都是被官方標記為deprecated的項目。且大部分接口都不如上述代碼簡單。 使用上,跟Golang官方sync包的Mutex接口非常類似,先New(),然后調用Lock(),使用完后調用Unlock(),就三個接口,就是這么簡單。示例代碼如下:

package main

import ( "github.com/zieckey/etcdsync" "log" ) func main() { //etcdsync.SetDebug(true) log.SetFlags(log.Ldate|log.Ltime|log.Lshortfile) m := etcdsync.New("/etcdsync", "123", []string{"http://127.0.0.1:2379"}) if m == nil { log.Printf("etcdsync.NewMutex failed") } err := m.Lock() if err != nil { log.Printf("etcdsync.Lock failed") } else { log.Printf("etcdsync.Lock OK") } log.Printf("Get the lock. Do something here.") err = m.Unlock() if err != nil { log.Printf("etcdsync.Unlock failed") } else { log.Printf("etcdsync.Unlock OK") } }

20. Redis的數據結構有哪些,以及實現場景?

Redis的數據結構有五種:

  • string 字符串

String 數據結構是簡單的 key-value 類型,value 不僅可以是 String,也可以是數字(當數字類型用 Long 可以表示的時候encoding 就是整型,其他都存儲在 sdshdr 當做字符串)。使用 Strings 類型,可以完全實現目前 Memcached 的功能,並且效率更高。還可以享受 Redis 的定時持久化(可以選擇 RDB 模式或者 AOF 模式),操作日志及 Replication 等功能。

除了提供與 Memcached 一樣的 get、set、incr、decr 等操作外,Redis 還提供了下面一些操作:

  1. LEN niushuai:O(1)獲取字符串長度.
  2. APPEND niushuai redis:往字符串 append 內容,而且采用智能分配內存(每次2倍).
  3. 設置和獲取字符串的某一段內容.
  4. 設置及獲取字符串的某一位(bit).
  5. 批量設置一系列字符串的內容.
  6. 原子計數器.
  7. GETSET 命令的妙用,請於清空舊值的同時設置一個新值,配合原子計數器使用.
  • Hash 字典

在 Memcached 中,我們經常將一些結構化的信息打包成 hashmap,在客戶端序列化后存儲為一個字符串的值(一般是 JSON 格式),比如用戶的昵稱、年齡、性別、積分等。這時候在需要修改其中某一項時,通常需要將字符串(JSON)取出來,然后進行反序列化,修改某一項的值,再序列化成字符串(JSON)存儲回去。簡單修改一個屬性就干這么多事情,消耗必定是很大的,也不適用於一些可能並發操作的場合(比如兩個並發的操作都需要修改積分)。而 Redis 的 Hash 結構可以使你像在數據庫中 Update 一個屬性一樣只修改某一項屬性值。

Hash可以用來存儲、讀取、修改用戶屬性。

  • List 列表

List 說白了就是鏈表(redis 使用雙端鏈表實現的 List),相信學過數據結構知識的人都應該能理解其結構。使用 List 結構,我們可以輕松地實現最新消息排行等功能(比如新浪微博的 TimeLine )。List 的另一個應用就是消息隊列,可以利用 List 的 *PUSH 操作,將任務存在 List 中,然后工作線程再用 POP 操作將任務取出進行執行。

Redis 還提供了操作 List 中某一段元素的 API,你可以直接查詢,刪除 List 中某一段的元素。

List 列表應用:

  1. 微博 TimeLine.
  2. 消息隊列.
  • Set 集合

Set 就是一個集合,集合的概念就是一堆不重復值的組合。利用 Redis 提供的 Set 數據結構,可以存儲一些集合性的數據。比如在微博應用中,可以將一個用戶所有的關注人存在一個集合中,將其所有粉絲存在一個集合。因為 Redis 非常人性化的為集合提供了求交集、並集、差集等操作,那么就可以非常方便的實現如共同關注、共同喜好、二度好友等功能,對上面的所有集合操作,你還可以使用不同的命令選擇將結果返回給客戶端還是存集到一個新的集合中。

Set 集合應用:

  1. 共同好友、二度好友
  2. 利用唯一性,可以統計訪問網站的所有獨立 IP.
  3. 好友推薦的時候,根據 tag 求交集,大於某個 threshold 就可以推薦.
  • Sorted Set有序集合

和Sets相比,Sorted Sets是將 Set 中的元素增加了一個權重參數 score,使得集合中的元素能夠按 score 進行有序排列,比如一個存儲全班同學成績的 Sorted Sets,其集合 value 可以是同學的學號,而 score 就可以是其考試得分,這樣在數據插入集合的時候,就已經進行了天然的排序。另外還可以用 Sorted Sets 來做帶權重的隊列,比如普通消息的 score 為1,重要消息的 score 為2,然后工作線程可以選擇按 score 的倒序來獲取工作任務。讓重要的任務優先執行。

Sorted Set有序集合應用:

1.帶有權重的元素,比如一個游戲的用戶得分排行榜. 2.比較復雜的數據結構,一般用到的場景不算太多.

Redis 其他功能使用場景:

  • 訂閱-發布系統

Pub/Sub 從字面上理解就是發布(Publish)與訂閱(Subscribe),在 Redis 中,你可以設定對某一個 key 值進行消息發布及消息訂閱,當一個 key 值上進行了消息發布后,所有訂閱它的客戶端都會收到相應的消息。這一功能最明顯的用法就是用作實時消息系統,比如普通的即時聊天,群聊等功能。

  • 事務——Transactions

誰說 NoSQL 都不支持事務,雖然 Redis 的 Transactions 提供的並不是嚴格的 ACID 的事務(比如一串用 EXEC 提交執行的命令,在執行中服務器宕機,那么會有一部分命令執行了,剩下的沒執行),但是這個 Transactions 還是提供了基本的命令打包執行的功能(在服務器不出問題的情況下,可以保證一連串的命令是順序在一起執行的,中間有會有其它客戶端命令插進來執行)。Redis 還提供了一個 Watch 功能,你可以對一個 key 進行 Watch,然后再執行 Transactions,在這過程中,如果這個 Watched 的值進行了修改,那么這個 Transactions 會發現並拒絕執行。

21. Mysql高可用方案有哪些?

Mysql高可用方案包括:

  1. 主從復制方案

這是MySQL自身提供的一種高可用解決方案,數據同步方法采用的是MySQL replication技術。MySQL replication就是從服務器到主服務器拉取二進制日志文件,然后再將日志文件解析成相應的SQL在從服務器上重新執行一遍主服務器的操作,通過這種方式保證數據的一致性。為了達到更高的可用性,在實際的應用環境中,一般都是采用MySQL replication技術配合高可用集群軟件keepalived來實現自動failover,這種方式可以實現95.000%的SLA。

  1. MMM/MHA高可用方案

MMM提供了MySQL主主復制配置的監控、故障轉移和管理的一套可伸縮的腳本套件。在MMM高可用方案中,典型的應用是雙主多從架構,通過MySQL replication技術可以實現兩個服務器互為主從,且在任何時候只有一個節點可以被寫入,避免了多點寫入的數據沖突。同時,當可寫的主節點故障時,MMM套件可以立刻監控到,然后將服務自動切換到另一個主節點,繼續提供服務,從而實現MySQL的高可用。

  1. Heartbeat/SAN高可用方案

在這個方案中,處理failover的方式是高可用集群軟件Heartbeat,它監控和管理各個節點間連接的網絡,並監控集群服務,當節點出現故障或者服務不可用時,自動在其他節點啟動集群服務。在數據共享方面,通過SAN(Storage Area Network)存儲來共享數據,這種方案可以實現99.990%的SLA。

  1. Heartbeat/DRBD高可用方案

這個方案處理failover的方式上依舊采用Heartbeat,不同的是,在數據共享方面,采用了基於塊級別的數據同步軟件DRBD來實現。DRBD是一個用軟件實現的、無共享的、服務器之間鏡像塊設備內容的存儲復制解決方案。和SAN網絡不同,它並不共享存儲,而是通過服務器之間的網絡復制數據。

  1. NDB CLUSTER高可用方案

國內用NDB集群的公司非常少,貌似有些銀行有用。NDB集群不需要依賴第三方組件,全部都使用官方組件,能保證數據的一致性,某個數據節點掛掉,其他數據節點依然可以提供服務,管理節點需要做冗余以防掛掉。缺點是:管理和配置都很復雜,而且某些SQL語句例如join語句需要避免。

22. Go語言的棧空間管理是怎么樣的?

Go語言的運行環境(runtime)會在goroutine需要的時候動態地分配棧空間,而不是給每個goroutine分配固定大小的內存空間。這樣就避免了需要程序員來決定棧的大小。

分塊式的棧是最初Go語言組織棧的方式。當創建一個goroutine的時候,它會分配一個8KB的內存空間來給goroutine的棧使用。我們可能會考慮當這8KB的棧空間被用完的時候該怎么辦?

為了處理這種情況,每個Go函數的開頭都有一小段檢測代碼。這段代碼會檢查我們是否已經用完了分配的棧空間。如果是的話,它會調用morestack函數。morestack函數分配一塊新的內存作為棧空間,並且在這塊棧空間的底部填入各種信息(包括之前的那塊棧地址)。在分配了這塊新的棧空間之后,它會重試剛才造成棧空間不足的函數。這個過程叫做棧分裂(stack split)。

在新分配的棧底部,還插入了一個叫做lessstack的函數指針。這個函數還沒有被調用。這樣設置是為了從剛才造成棧空間不足的那個函數返回時做准備的。當我們從那個函數返回時,它會跳轉到lessstacklessstack函數會查看在棧底部存放的數據結構里的信息,然后調整棧指針(stack pointer)。這樣就完成了從新的棧塊到老的棧塊的跳轉。接下來,新分配的這個塊棧空間就可以被釋放掉了。

分塊式的棧讓我們能夠按照需求來擴展和收縮棧的大小。 Go開發者不需要花精力去估計goroutine會用到多大的棧。創建一個新的goroutine的開銷也不大。當 Go開發者不知道棧會擴展到多少大時,它也能很好的處理這種情況。

這一直是之前Go語言管理棧的的方法。但這個方法有一個問題。縮減棧空間是一個開銷相對較大的操作。如果在一個循環里有棧分裂,那么它的開銷就變得不可忽略了。一個函數會擴展,然后分裂棧。當它返回的時候又會釋放之前分配的內存塊。如果這些都發生在一個循環里的話,代價是相當大的。 這就是所謂的熱分裂問題(hot split problem)。它是Go語言開發者選擇新的棧管理方法的主要原因。新的方法叫做棧復制法(stack copying)

棧復制法一開始和分塊式的棧很像。當goroutine運行並用完棧空間的時候,與之前的方法一樣,棧溢出檢查會被觸發。但是,不像之前的方法那樣分配一個新的內存塊並鏈接到老的棧內存塊,新的方法會分配一個兩倍大的內存塊並把老的內存塊內容復制到新的內存塊里。這樣做意味着當棧縮減回之前大小時,我們不需要做任何事情。棧的縮減沒有任何代價。而且,當棧再次擴展時,運行環境也不需要再做任何事。它可以重用之前分配的空間。

棧的復制聽起來很容易,但實際操作並非那么簡單。存儲在棧上的變量的地址可能已經被使用到。也就是說程序使用到了一些指向棧的指針。當移動棧的時候,所有指向棧里內容的指針都會變得無效。然而,指向棧內容的指針自身也必定是保存在棧上的。這是為了保證內存安全的必要條件。否則一個程序就有可能訪問一段已經無效的棧空間了。

因為垃圾回收的需要,我們必須知道棧的哪些部分是被用作指針了。當我們移動棧的時候,我們可以更新棧里的指針讓它們指向新的地址。所有相關的指針都會被更新。我們使用了垃圾回收的信息來復制棧,但並不是任何使用棧的函數都有這些信息。因為很大一部分運行環境是用C語言寫的,很多被調用的運行環境里的函數並沒有指針的信息,所以也就不能夠被復制了。當遇到這種情況時,我們只能退回到分塊式的棧並支付相應的開銷。

這也是為什么現在運行環境的開發者正在用Go語言重寫運行環境的大部分代碼。無法用Go語言重寫的部分(比如調度器的核心代碼和垃圾回收器)會在特殊的棧上運行。這個特殊棧的大小由運行環境的開發者設置。

這些改變除了使棧復制成為可能,它也允許我們在將來實現並行垃圾回收。

另外一種不同的棧處理方式就是在虛擬內存中分配大內存段。由於物理內存只是在真正使用時才會被分配,因此看起來好似你可以分配一個大內存段並讓操 作系統處理它。下面是這種方法的一些問題

首先,32位系統只能支持4G字節虛擬內存,並且應用只能用到其中的3G空間。由於同時運行百萬goroutines的情況並不少見,因此你很可 能用光虛擬內存,即便我們假設每個goroutine的stack只有8K。

第二,然而我們可以在64位系統中分配大內存,它依賴於過量內存使用。所謂過量使用是指當你分配的內存大小超出物理內存大小時,依賴操作系統保證 在需要時能夠分配出物理內存。然而,允許過量使用可能會導致一些風險。由於一些進程分配了超出機器物理內存大小的內存,如果這些進程使用更多內存 時,操作系統將不得不為它們補充分配內存。這會導致操作系統將一些內存段放入磁盤緩存,這常常會增加不可預測的處理延遲。正是考慮到這個原因,一 些新系統關閉了對過量使用的支持。

23. Goroutine和Channel的作用分別是什么?

進程是內存資源管理和cpu調度的執行單元。為了有效利用多核處理器的優勢,將進程進一步細分,允許一個進程里存在多個線程,這多個線程還是共享同一片內存空間,但cpu調度的最小單元變成了線程。

那協程又是什么呢,以及與線程的差異性??

協程,可以看作是輕量級的線程。但與線程不同的是,線程的切換是由操作系統控制的,而協程的切換則是由用戶控制的。

最早支持協程的程序語言應該是lisp方言scheme里的continuation(續延),續延允許scheme保存任意函數調用的現場,保存起來並重新執行。Lua,C#,python等語言也有自己的協程實現。

Go中的goroutinue就是協程,可以實現並行,多個協程可以在多個處理器同時跑。而協程同一時刻只能在一個處理器上跑(可以把宿主語言想象成單線程的就好了)。 然而,多個goroutine之間的通信是通過channel,而協程的通信是通過yield和resume()操作。

goroutine非常簡單,只需要在函數的調用前面加關鍵字go即可,例如:

go elegance()

我們也可以啟動5個goroutines分別打印索引。

func main() { for i:=1;i<5;i++ { go func(i int) { fmt.Println(i) }(i) } // 停歇5s,保證打印全部結束 time.Sleep(5*time.Second) }

在分析goroutine執行的隨機性和並發性,啟動了5個goroutine,再加上main函數的主goroutine,總共有6個goroutines。由於goroutine類似於”守護線程“,異步執行的,如果主goroutine不等待片刻,可能程序就沒有輸出打印了。

在Golang中channel則是goroutinues之間進行通信的渠道。

可以把channel形象比喻為工廠里的傳送帶,一頭的生產者goroutine往傳輸帶放東西,另一頭的消費者goroutinue則從輸送帶取東西。channel實際上是一個有類型的消息隊列,遵循先進先出的特點。

  1. channel的操作符號

ch <- data 表示data被發送給channel ch;

data <- ch 表示從channel ch取一個值,然后賦給data。

  1. 阻塞式channel

channel默認是沒有緩沖區的,也就是說,通信是阻塞的。send操作必須等到有消費者accept才算完成。

應用示例:

func main() { ch1 := make(chan int) go pump(ch1) // pump hangs fmt.Println(<-ch1) // prints only 1 } func pump(ch chan int) { for i:= 1; ; i++ { ch <- i } }

在函數pump()里的channel在接受到第一個元素后就被阻塞了,直到主goroutinue取走了數據。最終channel阻塞在接受第二個元素,程序只打印 1。

沒有緩沖(buffer)的channel只能容納一個元素,而帶有緩沖(buffer)channel則可以非阻塞容納N個元素。發送數據到緩沖(buffer) channel不會被阻塞,除非channel已滿;同樣的,從緩沖(buffer) channel取數據也不會被阻塞,除非channel空了。

24. 怎么查看Goroutine的數量?

GOMAXPROCS中控制的是未被阻塞的所有Goroutine,可以被Multiplex到多少個線程上運行,通過GOMAXPROCS可以查看Goroutine的數量。

25. 說下Go中的鎖有哪些?三種鎖,讀寫鎖,互斥鎖,還有map的安全的鎖?

Go中的三種鎖包括:互斥鎖,讀寫鎖,sync.Map的安全的鎖.

  • 互斥鎖

Go並發程序對共享資源進行訪問控制的主要手段,由標准庫代碼包中sync中的Mutex結構體表示。

//Mutex 是互斥鎖, 零值是解鎖的互斥鎖, 首次使用后不得復制互斥鎖。 type Mutex struct { state int32 sema uint32 }

sync.Mutex包中的類型只有兩個公開的指針方法Lock和Unlock。

//Locker表示可以鎖定和解鎖的對象。 type Locker interface { Lock() Unlock() } //鎖定當前的互斥量 //如果鎖已被使用,則調用goroutine //阻塞直到互斥鎖可用。 func (m *Mutex) Lock() //對當前互斥量進行解鎖 //如果在進入解鎖時未鎖定m,則為運行時錯誤。 //鎖定的互斥鎖與特定的goroutine無關。 //允許一個goroutine鎖定Mutex然后安排另一個goroutine來解鎖它。 func (m *Mutex) Unlock()

聲明一個互斥鎖:

var mutex sync.Mutex

不像C或Java的鎖類工具,我們可能會犯一個錯誤:忘記及時解開已被鎖住的鎖,從而導致流程異常。但Go由於存在defer,所以此類問題出現的概率極低。關於defer解鎖的方式如下:

var mutex sync.Mutex func Write() { mutex.Lock() defer mutex.Unlock() }

如果對一個已經上鎖的對象再次上鎖,那么就會導致該鎖定操作被阻塞,直到該互斥鎖回到被解鎖狀態.

fpackage main

import (
	"fmt" "sync" "time" ) func main() { var mutex sync.Mutex fmt.Println("begin lock") mutex.Lock() fmt.Println("get locked") for i := 1; i <= 3; i++ { go func(i int) { fmt.Println("begin lock ", i) mutex.Lock() fmt.Println("get locked ", i) }(i) } time.Sleep(time.Second) fmt.Println("Unlock the lock") mutex.Unlock() fmt.Println("get unlocked") time.Sleep(time.Second) }

我們在for循環之前開始加鎖,然后在每一次循環中創建一個協程,並對其加鎖,但是由於之前已經加鎖了,所以這個for循環中的加鎖會陷入阻塞直到main中的鎖被解鎖, time.Sleep(time.Second) 是為了能讓系統有足夠的時間運行for循環,輸出結果如下:

> go run mutex.go begin lock get locked begin lock 3 begin lock 1 begin lock 2 Unlock the lock get unlocked get locked 3

這里可以看到解鎖后,三個協程會重新搶奪互斥鎖權,最終協程3獲勝。

互斥鎖鎖定操作的逆操作並不會導致協程阻塞,但是有可能導致引發一個無法恢復的運行時的panic,比如對一個未鎖定的互斥鎖進行解鎖時就會發生panic。避免這種情況的最有效方式就是使用defer。

我們知道如果遇到panic,可以使用recover方法進行恢復,但是如果對重復解鎖互斥鎖引發的panic卻是無用的(Go 1.8及以后)。

package main

import ( "fmt" "sync" ) func main() { defer func() { fmt.Println("Try to recover the panic") if p := recover(); p != nil { fmt.Println("recover the panic : ", p) } }() var mutex sync.Mutex fmt.Println("begin lock") mutex.Lock() fmt.Println("get locked") fmt.Println("unlock lock") mutex.Unlock() fmt.Println("lock is unlocked") fmt.Println("unlock lock again") mutex.Unlock() }

運行:

> go run mutex.go begin lock get locked unlock lock lock is unlocked unlock lock again fatal error: sync: unlock of unlocked mutex goroutine 1 [running]: runtime.throw(0x4bc1a8, 0x1e) /home/keke/soft/go/src/runtime/panic.go:617 +0x72 fp=0xc000084ea8 sp=0xc000084e78 pc=0x427ba2 sync.throw(0x4bc1a8, 0x1e) /home/keke/soft/go/src/runtime/panic.go:603 +0x35 fp=0xc000084ec8 sp=0xc000084ea8 pc=0x427b25 sync.(*Mutex).Unlock(0xc00001a0c8) /home/keke/soft/go/src/sync/mutex.go:184 +0xc1 fp=0xc000084ef0 sp=0xc000084ec8 pc=0x45f821 main.main() /home/keke/go/Test/mutex.go:25 +0x25f fp=0xc000084f98 sp=0xc000084ef0 pc=0x486c1f runtime.main() /home/keke/soft/go/src/runtime/proc.go:200 +0x20c fp=0xc000084fe0 sp=0xc000084f98 pc=0x4294ec runtime.goexit() /home/keke/soft/go/src/runtime/asm_amd64.s:1337 +0x1 fp=0xc000084fe8 sp=0xc000084fe0 pc=0x450ad1 exit status 2

這里試圖對重復解鎖引發的panic進行recover,但是我們發現操作失敗,雖然互斥鎖可以被多個協程共享,但還是建議將對同一個互斥鎖的加鎖解鎖操作放在同一個層次的代碼中。

  • 讀寫鎖

讀寫鎖是針對讀寫操作的互斥鎖,可以分別針對讀操作與寫操作進行鎖定和解鎖操作 。

讀寫鎖的訪問控制規則如下:

① 多個寫操作之間是互斥的 ② 寫操作與讀操作之間也是互斥的 ③ 多個讀操作之間不是互斥的

在這樣的控制規則下,讀寫鎖可以大大降低性能損耗。

在Go的標准庫代碼包中sync中的RWMutex結構體表示為:

// RWMutex是一個讀/寫互斥鎖,可以由任意數量的讀操作或單個寫操作持有。 // RWMutex的零值是未鎖定的互斥鎖。 //首次使用后,不得復制RWMutex。 //如果goroutine持有RWMutex進行讀取而另一個goroutine可能會調用Lock,那么在釋放初始讀鎖之前,goroutine不應該期望能夠獲取讀鎖定。 //特別是,這種禁止遞歸讀鎖定。 這是為了確保鎖最終變得可用; 阻止的鎖定會阻止新讀操作獲取鎖定。 type RWMutex struct { w Mutex //如果有待處理的寫操作就持有 writerSem uint32 // 寫操作等待讀操作完成的信號量 readerSem uint32 //讀操作等待寫操作完成的信號量 readerCount int32 // 待處理的讀操作數量 readerWait int32 // number of departing readers }

sync中的RWMutex有以下幾種方法:

//對讀操作的鎖定 func (rw *RWMutex) RLock() //對讀操作的解鎖 func (rw *RWMutex) RUnlock() //對寫操作的鎖定 func (rw *RWMutex) Lock() //對寫操作的解鎖 func (rw *RWMutex) Unlock() //返回一個實現了sync.Locker接口類型的值,實際上是回調rw.RLock and rw.RUnlock. func (rw *RWMutex) RLocker() Locker

Unlock方法會試圖喚醒所有想進行讀鎖定而被阻塞的協程,而 RUnlock方法只會在已無任何讀鎖定的情況下,試圖喚醒一個因欲進行寫鎖定而被阻塞的協程。若對一個未被寫鎖定的讀寫鎖進行寫解鎖,就會引發一個不可恢復的panic,同理對一個未被讀鎖定的讀寫鎖進行讀寫鎖也會如此。

由於讀寫鎖控制下的多個讀操作之間不是互斥的,因此對於讀解鎖更容易被忽視。對於同一個讀寫鎖,添加多少個讀鎖定,就必要有等量的讀解鎖,這樣才能其他協程有機會進行操作。

package main

import ( "fmt" "sync" "time" ) func main() { var rwm sync.RWMutex for i := 0; i < 5; i++ { go func(i int) { fmt.Println("try to lock read ", i) rwm.RLock() fmt.Println("get locked ", i) time.Sleep(time.Second * 2) fmt.Println("try to unlock for reading ", i) rwm.RUnlock() fmt.Println("unlocked for reading ", i) }(i) } time.Sleep(time.Millisecond * 1000) fmt.Println("try to lock for writing") rwm.Lock() fmt.Println("locked for writing") }

運行:

> go run rwmutex.go try to lock read 0 get locked 0 try to lock read 4 get locked 4 try to lock read 3 get locked 3 try to lock read 1 get locked 1 try to lock read 2 get locked 2 try to lock for writing try to unlock for reading 0 unlocked for reading 0 try to unlock for reading 2 unlocked for reading 2 try to unlock for reading 1 unlocked for reading 1 try to unlock for reading 3 unlocked for reading 3 try to unlock for reading 4 unlocked for reading 4 locked for writing

這里可以看到創建了五個協程用於對讀寫鎖的讀鎖定與讀解鎖操作。在 rwm.Lock()種會對main中協程進行寫鎖定,但是for循環中的讀解鎖尚未完成,因此會造成mian中的協程阻塞。當for循環中的讀解鎖操作都完成后就會試圖喚醒main中阻塞的協程,main中的寫鎖定才會完成。

  • sync.Map安全鎖

golang中的sync.Map是並發安全的,其實也就是sync包中golang自定義的一個名叫Map的結構體。

應用示例:

package main
import ( "sync" "fmt" ) func main() { //開箱即用 var sm sync.Map //store 方法,添加元素 sm.Store(1,"a") //Load 方法,獲得value if v,ok:=sm.Load(1);ok{ fmt.Println(v) } //LoadOrStore方法,獲取或者保存 //參數是一對key:value,如果該key存在且沒有被標記刪除則返回原先的value(不更新)和true;不存在則store,返回該value 和false if vv,ok:=sm.LoadOrStore(1,"c");ok{ fmt.Println(vv) } if vv,ok:=sm.LoadOrStore(2,"c");!ok{ fmt.Println(vv) } //遍歷該map,參數是個函數,該函數參的兩個參數是遍歷獲得的key和value,返回一個bool值,當返回false時,遍歷立刻結束。 sm.Range(func(k,v interface{})bool{ fmt.Print(k) fmt.Print(":") fmt.Print(v) fmt.Println() return true }) }

運行 :

a
a
c
1:a
2:c

sync.Map的數據結構:

 type Map struct { // 該鎖用來保護dirty mu Mutex // 存讀的數據,因為是atomic.value類型,只讀類型,所以它的讀是並發安全的 read atomic.Value // readOnly //包含最新的寫入的數據,並且在寫的時候,會把read 中未被刪除的數據拷貝到該dirty中,因為是普通的map存在並發安全問題,需要用到上面的mu字段。 dirty map[interface{}]*entry // 從read讀數據的時候,會將該字段+1,當等於len(dirty)的時候,會將dirty拷貝到read中(從而提升讀的性能)。 misses int }

read的數據結構是:

type readOnly struct { m map[interface{}]*entry // 如果Map.dirty的數據和m 中的數據不一樣是為true amended bool }

entry的數據結構:

type entry struct { //可見value是個指針類型,雖然read和dirty存在冗余情況(amended=false),但是由於是指針類型,存儲的空間應該不是問題 p unsafe.Pointer // *interface{} }

Delete 方法:

func (m *Map) Delete(key interface{}) { read, _ := m.read.Load().(readOnly) e, ok := read.m[key] //如果read中沒有,並且dirty中有新元素,那么就去dirty中去找 if !ok && read.amended { m.mu.Lock() //這是雙檢查(上面的if判斷和鎖不是一個原子性操作) read, _ = m.read.Load().(readOnly) e, ok = read.m[key] if !ok && read.amended { //直接刪除 delete(m.dirty, key) } m.mu.Unlock() } if ok { //如果read中存在該key,則將該value 賦值nil(采用標記的方式刪除!) e.delete() } } func (e *entry) delete() (hadValue bool) { for { p := atomic.LoadPointer(&e.p) if p == nil || p == expunged { return false } if atomic.CompareAndSwapPointer(&e.p, p, nil) { return true } } }

Store 方法:

func (m *Map) Store(key, value interface{}) { // 如果m.read存在這個key,並且沒有被標記刪除,則嘗試更新。 read, _ := m.read.Load().(readOnly) if e, ok := read.m[key]; ok && e.tryStore(&value) { return } // 如果read不存在或者已經被標記刪除 m.mu.Lock() read, _ = m.read.Load().(readOnly) if e, ok := read.m[key]; ok { //如果entry被標記expunge,則表明dirty沒有key,可添加入dirty,並更新entry if e.unexpungeLocked() { //加入dirty中 m.dirty[key] = e } //更新value值 e.storeLocked(&value) //dirty 存在該key,更新 } else if e, ok := m.dirty[key]; ok { e.storeLocked(&value) //read 和dirty都沒有,新添加一條 } else { //dirty中沒有新的數據,往dirty中增加第一個新鍵 if !read.amended { //將read中未刪除的數據加入到dirty中 m.dirtyLocked() m.read.Store(readOnly{m: read.m, amended: true}) } m.dirty[key] = newEntry(value) } m.mu.Unlock() } //將read中未刪除的數據加入到dirty中 func (m *Map) dirtyLocked() { if m.dirty != nil { return } read, _ := m.read.Load().(readOnly) m.dirty = make(map[interface{}]*entry, len(read.m)) //read如果較大的話,可能影響性能 for k, e := range read.m { //通過此次操作,dirty中的元素都是未被刪除的,可見expunge的元素不在dirty中 if !e.tryExpungeLocked() { m.dirty[k] = e } } } //判斷entry是否被標記刪除,並且將標記為nil的entry更新標記為expunge func (e *entry) tryExpungeLocked() (isExpunged bool) { p := atomic.LoadPointer(&e.p) for p == nil { // 將已經刪除標記為nil的數據標記為expunged if atomic.CompareAndSwapPointer(&e.p, nil, expunged) { return true } p = atomic.LoadPointer(&e.p) } return p == expunged } //對entry 嘗試更新 func (e *entry) tryStore(i *interface{}) bool { p := atomic.LoadPointer(&e.p) if p == expunged { return false } for { if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) { return true } p = atomic.LoadPointer(&e.p) if p == expunged { return false } } } //read里 將標記為expunge的更新為nil func (e *entry) unexpungeLocked() (wasExpunged bool) { return atomic.CompareAndSwapPointer(&e.p, expunged, nil) } //更新entry func (e *entry) storeLocked(i *interface{}) { atomic.StorePointer(&e.p, unsafe.Pointer(i)) }

因此,每次操作先檢查read,因為read 並發安全,性能好些;read不滿足,則加鎖檢查dirty,一旦是新的鍵值,dirty會被read更新。

Load方法:

Load方法是一個加載方法,查找key。

func (m *Map) Load(key interface{}) (value interface{}, ok bool) { //因read只讀,線程安全,先查看是否滿足條件 read, _ := m.read.Load().(readOnly) e, ok := read.m[key] //如果read沒有,並且dirty有新數據,那從dirty中查找,由於dirty是普通map,線程不安全,這個時候用到互斥鎖了 if !ok && read.amended { m.mu.Lock() // 雙重檢查 read, _ = m.read.Load().(readOnly) e, ok = read.m[key] // 如果read中還是不存在,並且dirty中有新數據 if !ok && read.amended { e, ok = m.dirty[key] // mssLocked()函數是性能是sync.Map 性能得以保證的重要函數,目的講有鎖的dirty數據,替換到只讀線程安全的read里 m.missLocked() } m.mu.Unlock() } if !ok { return nil, false } return e.load() } //dirty 提升至read 關鍵函數,當misses 經過多次因為load之后,大小等於len(dirty)時候,講dirty替換到read里,以此達到性能提升。 func (m *Map) missLocked() { m.misses++ if m.misses < len(m.dirty) { return } //原子操作,耗時很小 m.read.Store(readOnly{m: m.dirty}) m.dirty = nil m.misses = 0 }

sync.Map是通過冗余的兩個數據結構(read、dirty),實現性能的提升。為了提升性能,load、delete、store等操作盡量使用只讀的read;為了提高read的key擊中概率,采用動態調整,將dirty數據提升為read;對於數據的刪除,采用延遲標記刪除法,只有在提升dirty的時候才刪除。

26. 讀寫鎖或者互斥鎖讀的時候能寫嗎?

Go中讀寫鎖包括讀鎖和寫鎖,多個讀線程可以同時訪問共享數據;寫線程必須等待所有讀線程都釋放鎖以后,才能取得鎖;同樣的,讀線程必須等待寫線程釋放鎖后,才能取得鎖,也就是說讀寫鎖要確保的是如下互斥關系,可以同時讀,但是讀-寫,寫-寫都是互斥的。

27. 怎么限制Goroutine的數量.

在Golang中,Goroutine雖然很好,但是數量太多了,往往會帶來很多麻煩,比如耗盡系統資源導致程序崩潰,或者CPU使用率過高導致系統忙不過來。所以我們可以限制下Goroutine的數量,這樣就需要在每一次執行go之前判斷goroutine的數量,如果數量超了,就要阻塞go的執行。第一時間想到的就是使用通道。每次執行的go之前向通道寫入值,直到通道滿的時候就阻塞了,

package main

import "fmt" var ch chan int func elegance(){ <-ch fmt.Println("the ch value receive",ch) } func main(){ ch = make(chan int,5) for i:=0;i<10;i++{ ch <-1 fmt.Println("the ch value send",ch) go elegance() fmt.Println("the result i",i) } } 

運行:

> go run goroutine.go the ch value send 0xc00009c000 the result i 0 the ch value send 0xc00009c000 the result i 1 the ch value send 0xc00009c000 the result i 2 the ch value send 0xc00009c000 the result i 3 the ch value send 0xc00009c000 the result i 4 the ch value send 0xc00009c000 the result i 5 the ch value send 0xc00009c000 the ch value receive 0xc00009c000 the result i 6 the ch value receive 0xc00009c000 the ch value send 0xc00009c000 the result i 7 the ch value send 0xc00009c000 the result i 8 the ch value send 0xc00009c000 the result i 9 the ch value send 0xc00009c000 the ch value receive 0xc00009c000 the ch value receive 0xc00009c000 the ch value receive 0xc00009c000 the result i 10 the ch value send 0xc00009c000 the result i 11 the ch value send 0xc00009c000 the result i 12 the ch value send 0xc00009c000 the result i 13 the ch value send 0xc00009c000 the ch value receive 0xc00009c000 the ch value receive 0xc00009c000 the ch value receive 0xc00009c000 the ch value receive 0xc00009c000 the result i 14 the ch value receive 0xc00009c000
> go run goroutine.go 
the ch value send 0xc00007e000
the result i 0
the ch value send 0xc00007e000
the result i 1
the ch value send 0xc00007e000
the result i 2
the ch value send 0xc00007e000
the result i 3
the ch value send 0xc00007e000
the ch value receive 0xc00007e000
the result i 4
the ch value send 0xc00007e000
the ch value receive 0xc00007e000
the result i 5
the ch value send 0xc00007e000
the ch value receive 0xc00007e000
the result i 6
the ch value send 0xc00007e000
the result i 7
the ch value send 0xc00007e000
the ch value receive 0xc00007e000
the ch value receive 0xc00007e000
the ch value receive 0xc00007e000
the result i 8
the ch value send 0xc00007e000
the result i 9

這樣每次同時運行的goroutine就被限制為5個了。但是新的問題於是就出現了,因為並不是所有的goroutine都執行完了,在main函數退出之后,還有一些goroutine沒有執行完就被強制結束了。這個時候我們就需要用到sync.WaitGroup。使用WaitGroup等待所有的goroutine退出。

package main

import ( "fmt" "runtime" "sync" "time" ) // Pool Goroutine Pool type Pool struct { queue chan int wg *sync.WaitGroup } // New 新建一個協程池 func NewPool(size int) *Pool{ if size <=0{ size = 1 } return &Pool{ queue:make(chan int,size), wg:&sync.WaitGroup{}, } } // Add 新增一個執行 func (p *Pool)Add(delta int){ // delta為正數就添加 for i :=0;i<delta;i++{ p.queue <-1 } // delta為負數就減少 for i:=0;i>delta;i--{ <-p.queue } p.wg.Add(delta) } // Done 執行完成減一 func (p *Pool) Done(){ <-p.queue p.wg.Done() } // Wait 等待Goroutine執行完畢 func (p *Pool) Wait(){ p.wg.Wait() } func main(){ // 這里限制5個並發 pool := NewPool(5) fmt.Println("the NumGoroutine begin is:",runtime.NumGoroutine()) for i:=0;i<20;i++{ pool.Add(1) go func(i int) { time.Sleep(time.Second) fmt.Println("the NumGoroutine continue is:",runtime.NumGoroutine()) pool.Done() }(i) } pool.Wait() fmt.Println("the NumGoroutine done is:",runtime.NumGoroutine()) }

運行:

the NumGoroutine begin is: 1 the NumGoroutine continue is: 6 the NumGoroutine continue is: 7 the NumGoroutine continue is: 6 the NumGoroutine continue is: 6 the NumGoroutine continue is: 6 the NumGoroutine continue is: 6 the NumGoroutine continue is: 6 the NumGoroutine continue is: 6 the NumGoroutine continue is: 6 the NumGoroutine continue is: 6 the NumGoroutine continue is: 6 the NumGoroutine continue is: 6 the NumGoroutine continue is: 6 the NumGoroutine continue is: 6 the NumGoroutine continue is: 6 the NumGoroutine continue is: 6 the NumGoroutine continue is: 6 the NumGoroutine continue is: 6 the NumGoroutine continue is: 3 the NumGoroutine continue is: 2 the NumGoroutine done is: 1

其中,Go的GOMAXPROCS默認值已經設置為CPU的核數, 這里允許我們的Go程序充分使用機器的每一個CPU,最大程度的提高我們程序的並發性能。runtime.NumGoroutine函數在被調用后,會返回系統中的處於特定狀態的Goroutine的數量。這里的特指是指Grunnable\Gruning\Gsyscall\Gwaition。處於這些狀態的Groutine即被看做是活躍的或者說正在被調度。

這里需要注意下:垃圾回收所在Groutine的狀態也處於這個范圍內的話,也會被納入該計數器。

28. Channel是同步的還是異步的.

Channel是異步進行的。

channel存在3種狀態:

  • nil,未初始化的狀態,只進行了聲明,或者手動賦值為nil
  • active,正常的channel,可讀或者可寫
  • closed,已關閉,千萬不要誤認為關閉channel后,channel的值是nil

29. 說一下異步和非阻塞的區別?

  • 異步和非阻塞的區別:
  1. 異步:調用在發出之后,這個調用就直接返回,不管有無結果;異步是過程。
  2. 非阻塞:關注的是程序在等待調用結果(消息,返回值)時的狀態,指在不能立刻得到結果之前,該調用不會阻塞當前線程。
  • 同步和異步的區別:
  1. 步:一個服務的完成需要依賴其他服務時,只有等待被依賴的服務完成后,才算完成,這是一種可靠的服務序列。要么成功都成功,失敗都失敗,服務的狀態可以保持一致。
  2. 異步:一個服務的完成需要依賴其他服務時,只通知其他依賴服務開始執行,而不需要等待被依賴的服務完成,此時該服務就算完成了。被依賴的服務是否最終完成無法確定,一次它是一個不可靠的服務序列。
  • 消息通知中的同步和異步:
  1. 同步:當一個同步調用發出后,調用者要一直等待返回消息(或者調用結果)通知后,才能進行后續的執行。
  2. 異步:當一個異步過程調用發出后,調用者不能立刻得到返回消息(結果)。在調用結束之后,通過消息回調來通知調用者是否調用成功。
  • 阻塞與非阻塞的區別:
  1. 阻塞:阻塞調用是指調用結果返回之前,當前線程會被掛起,一直處於等待消息通知,不能夠執行其他業務,函數只有在得到結果之后才會返回。
  2. 非阻塞:非阻塞和阻塞的概念相對應,指在不能立刻得到結果之前,該函數不會阻塞當前線程,而會立刻返回。

同步與異步是對應的,它們是線程之間的關系,兩個線程之間要么是同步的,要么是異步的。

阻塞與非阻塞是對同一個線程來說的,在某個時刻,線程要么處於阻塞,要么處於非阻塞。

阻塞是使用同步機制的結果,非阻塞則是使用異步機制的結果。

30. Log包線程安全嗎?

Golang的標准庫提供了log的機制,但是該模塊的功能較為簡單(看似簡單,其實他有他的設計思路)。在輸出的位置做了線程安全的保護。

31. Goroutine和線程的區別?

從調度上看,goroutine的調度開銷遠遠小於線程調度開銷。

OS的線程由OS內核調度,每隔幾毫秒,一個硬件時鍾中斷發到CPU,CPU調用一個調度器內核函數。這個函數暫停當前正在運行的線程,把他的寄存器信息保存到內存中,查看線程列表並決定接下來運行哪一個線程,再從內存中恢復線程的注冊表信息,最后繼續執行選中的線程。這種線程切換需要一個完整的上下文切換:即保存一個線程的狀態到內存,再恢復另外一個線程的狀態,最后更新調度器的數據結構。某種意義上,這種操作還是很慢的。

Go運行的時候包涵一個自己的調度器,這個調度器使用一個稱為一個M:N調度技術,m個goroutine到n個os線程(可以用GOMAXPROCS來控制n的數量),Go的調度器不是由硬件時鍾來定期觸發的,而是由特定的go語言結構來觸發的,他不需要切換到內核語境,所以調度一個goroutine比調度一個線程的成本低很多。

從棧空間上,goroutine的棧空間更加動態靈活。

每個OS的線程都有一個固定大小的棧內存,通常是2MB,棧內存用於保存在其他函數調用期間哪些正在執行或者臨時暫停的函數的局部變量。這個固定的棧大小,如果對於goroutine來說,可能是一種巨大的浪費。作為對比goroutine在生命周期開始只有一個很小的棧,典型情況是2KB, 在go程序中,一次創建十萬左右的goroutine也不罕見(2KB*100,000=200MB)。而且goroutine的棧不是固定大小,它可以按需增大和縮小,最大限制可以到1GB。

goroutine沒有一個特定的標識。

在大部分支持多線程的操作系統和編程語言中,線程有一個獨特的標識,通常是一個整數或者指針,這個特性可以讓我們構建一個線程的局部存儲,本質是一個全局的map,以線程的標識作為鍵,這樣每個線程可以獨立使用這個map存儲和獲取值,不受其他線程干擾。

goroutine中沒有可供程序員訪問的標識,原因是一種純函數的理念,不希望濫用線程局部存儲導致一個不健康的超距作用,即函數的行為不僅取決於它的參數,還取決於運行它的線程標識。

32. 滑動窗口的概念以及應用?

滑動窗口概念不僅存在於數據鏈路層,也存在於傳輸層,兩者有不同的協議,但基本原理是相近的。其中一個重要區別是,一個是針對於幀的傳送,另一個是字節數據的傳送。

滑動窗口(Sliding window)是一種流量控制技術。早期的網絡通信中,通信雙方不會考慮網絡的擁擠情況直接發送數據。由於大家不知道網絡擁塞狀況,同時發送數據,導致中間節點阻塞掉包,誰也發不了數據,所以就有了滑動窗口機制來解決此問題。參見滑動窗口如何根據網絡擁塞發送數據仿真視頻。

滑動窗口協議是用來改善吞吐量的一種技術,即容許發送方在接收任何應答之前傳送附加的包。接收方告訴發送方在某一時刻能送多少包(稱窗口尺寸)。

CP中采用滑動窗口來進行傳輸控制,滑動窗口的大小意味着接收方還有多大的緩沖區可以用於接收數據。發送方可以通過滑動窗口的大小來確定應該發送多少字節的數據。當滑動窗口為0時,發送方一般不能再發送數據報,但有兩種情況除外,一種情況是可以發送緊急數據,例如,允許用戶終止在遠端機上的運行進程。另一種情況是發送方可以發送一個1字節的數據報來通知接收方重新聲明它希望接收的下一字節及發送方的滑動窗口大小。

33. 怎么做彈性擴縮容,原理是什么?

彈性伸縮(Auto Scaling)根據您的業務需求和伸縮策略,為您自動調整計算資源。您可設置定時、周期或監控策略,恰到好處地增加或減少CVM實例,並完成實例配置,保證業務平穩健康運行。在需求高峰期時,彈性伸縮自動增加CVM實例的數量,以保證性能不受影響;當需求較低時,則會減少CVM實例數量以降低成本。彈性伸縮既適合需求穩定的應用程序,同時也適合每天、每周、每月使用量不停波動的應用程序。

34. 讓你設計一個web框架,你要怎么設計,說一下步驟.

35. 說一下中間件原理.

中間件(middleware)是基礎軟件的一大類,屬於可復用軟件的范疇。中間件處於操作系統軟件與用戶的應用軟件的中間。中間件在操作系統、網絡和數據庫之上,應用軟件的下層,總的作用是為處於自己上層的應用軟件提供運行與開發的環境,幫助用戶靈活、高效地開發和集成復雜的應用軟件  IDC的定義是:中間件是一種獨立的系統軟件或服務程序,分布式應用軟件借助這種軟件在不同的技術之間共享資源,中間件位於客戶機服務器的操作系統之上,管理計算資源和網絡通信。

中間件解決的問題是:

在中間件產生以前,應用軟件直接使用操作系統、網絡協議和數據庫等開發,這些都是計算機最底層的東西,越底層越復雜,開發者不得不面臨許多很棘手的問題,如操作系統的多樣性,繁雜的網絡程序設計、管理,復雜多變的網絡環境,數據分散處理帶來的不一致性問題、性能和效率、安全,等等。這些與用戶的業務沒有直接關系,但又必須解決,耗費了大量有限的時間和精力。於是,有人提出能不能將應用軟件所要面臨的共性問題進行提煉、抽象,在操作系統之上再形成一個可復用的部分,供成千上萬的應用軟件重復使用。這一技術思想最終構成了中間件這類的軟件。中間件屏蔽了底層操作系統的復雜性,使程序開發人員面對一個簡單而統一的開發環境,減少程序設計的復雜性,將注意力集中在自己的業務上,不必再為程序在不同系統軟件上的移植而重復工作,從而大大減少了技術上的負擔。

36. 怎么設計orm,讓你寫,你會怎么寫?

37. 用過原生的http包嗎?

38. 一個非常大的數組,讓其中兩個數想加等於1000怎么算?

39. 各個系統出問題怎么監控報警.

40. 常用測試工具,壓測工具,方法?

goconvey,vegeta

41. 復雜的單元測試怎么測試,比如有外部接口mysql接口的情況

42. redis集群,哨兵,持久化,事務

43. mysql和redis區別是什么?

44. 高可用軟件是什么?

45. 怎么搞一個並發服務程序?

46. 講解一下你做過的項目,然后找問題問實現細節。

47. mysql事務說下。

48. 怎么做一個自動化配置平台系統?

49. grpc遵循什么協議?

50. grpc內部原理是什么?

51. http2的特點是什么,與http1.1的對比。

| HTTP1.1                    | HTTP2       | QUIC                        |
| -------------------------- | ----------- | --------------------------- |
| 持久連接                       | 二進制分幀       | 基於UDP的多路傳輸(單連接下)            |
| 請求管道化                      | 多路復用(或連接共享) | 極低的等待時延(相比於TCP的三次握手)        |
| 增加緩存處理(新的字段如cache-control) | 頭部壓縮        | QUIC為 傳輸層 協議 ,成為更多應用層的高性能選擇 |
| 增加Host字段、支持斷點傳輸等(把文件分成幾部分) | 服務器推送       |                             |
  1. Go的調度原理.
  1. go struct能不能比較
  • 相同struct類型的可以比較

  • 不同struct類型的不可以比較,編譯都不過,類型不匹配

package main
import "fmt" func main() { type A struct { a int } type B struct { a int } a := A{1} //b := A{1} b := B{1} if a == b { fmt.Println("a == b") }else{ fmt.Println("a != b") } } // output // command-line-arguments [command-line-arguments.test] // ./.go:14:7: invalid operation: a == b (mismatched types A and B) 
  1. go defer(for defer)
  1. select可以用於什么?

Go的select主要是處理多個channel的操作.

  1. context包的用途是什么?

godoc: https://golang.org/pkg/context/

  1. client如何實現長連接?
  1. 主協程如何等其余協程完再操作?
  1. slice,len,cap,共享,擴容.

  2. map如何順序讀取?

可以通過sort中的排序包進行對map中的key進行排序

package main

import ( "fmt" "sort" ) func main() { var m = map[string]int{ "hello": 0, "morning": 1, "my": 2, "girl": 3, } var keys []string for k := range m { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { fmt.Println("Key:", k, "Value:", m[k]) } }
  1. 實現set

根據go中map的keys的無序性和唯一性,可以將其作為set

  1. 實現消息隊列(多生產者,多消費者)

根據Goroutine和channel的讀寫可以實現消息隊列,

  1. 大文件排序

64.基本排序,哪些是穩定的

選擇排序、快速排序、希爾排序、堆排序不是穩定的排序算法,

冒泡排序、插入排序、歸並排序和基數排序是穩定的排序算法

  1. Http get跟head

get:獲取由Request-URI標識的任何信息(以實體的形式),如果Request-URI引用某個數據處理過程,則應該以它產生的數據作為在響應中的實體,而不是該過程的源代碼文本,除非該過程碰巧輸出該文本。

head: 除了服務器不能在響應中返回消息體,HEAD方法與GET相同。用來獲取暗示實體的元信息,而不需要傳輸實體本身。常用於測試超文本鏈接的有效性、可用性和最近的修改。

  1. Http 401,403

401 Unauthorized: 該HTTP狀態碼表示認證錯誤,它是為了認證設計的,而不是為了授權設計的。收到401響應,表示請求沒有被認證—壓根沒有認證或者認證不正確—但是請重新認證和重試。(一般在響應頭部包含一個WWW-Authenticate來描述如何認證)。通常由web服務器返回,而不是web應用。從性質上來說是臨時的東西。(服務器要求客戶端重試)

403 Forbidden:該HTTP狀態碼是關於授權方面的。從性質上來說是永久的東西,和應用的業務邏輯相關聯。它比401更具體,更實際。收到403響應表示服務器完成認證過程,但是客戶端請求沒有權限去訪問要求的資源。

總的來說,401 Unauthorized響應應該用來表示缺失或錯誤的認證;403 Forbidden響應應該在這之后用,當用戶被認證后,但用戶沒有被授權在特定資源上執行操作。

67.Http keep-alive

  1. Http能不能一次連接多次請求,不等后端返回

  2. TCP 和 UDP 有什么區別,適用場景

  • TCP 是面向連接的,UDP 是面向無連接的;故 TCP 需要建立連接和斷開連接,UDP 不需要。

  • TCP 是流協議,UDP 是數據包協議;故 TCP 數據沒有大小限制,UDP 數據報有大小限制(UDP 協議本身限制、數據鏈路層的 MTU、緩存區大小)。

  • TCP 是可靠協議,UDP 是不可靠協議;故 TCP 會處理數據丟包重發以及亂序等情況,UDP 則不會處理。

UDP 的特點及使用場景:

UDP 不提供復雜的控制機制,利用 IP 提供面向無連接的通信服務,隨時都可以發送數據,處理簡單且高效,經常用於以下場景:

包總量較小的通信(DNS、SNMP)

視頻、音頻等多媒體通信(即時通信)

廣播通信

TCP 的特點及使用場景:

相對於 UDP,TCP 實現了數據傳輸過程中的各種控制,可以進行丟包時的重發控制,還可以對次序亂掉的分包進行順序控制。

在對可靠性要求較高的情況下,可以使用 TCP,即不考慮 UDP 的時候,都可以選擇 TCP。

  1. time-wait的作用
  1. 數據庫如何建索引
  1. 孤兒進程,僵屍進程
  • 孤兒進程:一個父進程退出,而它的一個或多個子進程還在運行,那么那些子進程將成為孤兒進程。孤兒進程將被init進程(進程號為1)所收養,並由init進程對它們完成狀態收集工作。

  • 僵屍進程:一個進程使用fork創建子進程,如果子進程退出,而父進程並沒有調用wait或waitpid獲取子進程的狀態信息,那么子進程的進程描述符仍然保存在系統中。這種進程稱之為僵死進程。

  1. 死鎖條件,如何避免

  2. linux命令,查看端口占用,cpu負載,內存占用,如何發送信號給一個進程

  3. git文件版本,使用順序,merge跟rebase

  4. 通常一般會用到哪些數據結構?

  5. 鏈表和數組相比, 有什么優缺點?

  6. 如何判斷兩個無環單鏈表有沒有交叉點?

  7. 如何判斷一個單鏈表有沒有環, 並找出入環點?

  8. 描述一下 TCP 四次揮手的過程中

  9. TCP 有哪些狀態?

  10. TCP 的 LISTEN 狀態是什么?

  11. TCP 的 CLOSE_WAIT 狀態是什么?

  12. 建立一個 socket 連接要經過哪些步驟?

  13. 常見的 HTTP 狀態碼有哪些?

  14. 301和302有什么區別?

  15. 504和500有什么區別?

  16. HTTPS 和 HTTP 有什么區別?

  17. 算法題: 手寫一個快速排序

快速排序:

func main() { var arr = []int{19,8,16,15,23,34,6,3,1,0,2,9,7} quickAscendingSort(arr, 0, len(arr)-1) fmt.Println("quickAscendingSort:",arr) quickDescendingSort(arr, 0, len(arr)-1) fmt.Println("quickDescendingSort:",arr) } //升序 func quickAscendingSort(arr []int, start, end int) { if (start < end) { i, j := start, end key := arr[(start + end)/2] for i <= j { for arr[i] < key { i++ } for arr[j] > key { j-- } if i <= j { arr[i], arr[j] = arr[j], arr[i] i++ j-- } } if start < j { quickAscendingSort(arr, start, j) } if end > i { quickAscendingSort(arr, i, end) } } } //降序 func quickDescendingSort(arr []int, start, end int) { if (start < end) { i, j := start, end key := arr[(start + end)/2] for i <= j { for arr[i] > key { i++ } for arr[j] < key { j-- } if i <= j { arr[i], arr[j] = arr[j], arr[i] i++ j-- } } if start < j { quickDescendingSort(arr, start, j) } if end > i { quickDescendingSort(arr, i, end) } } }
  1. Golang 里的逃逸分析是什么?怎么避免內存逃逸?

  2. 配置中心如何保證一致性?

  3. Golang 的GC觸發時機是什么?

  4. Redis 里數據結構的實現熟悉嗎?

  5. Etcd的Raft一致性算法原理?

  6. 微服務概念.

  7. SLB原理.

  8. 分布式一直性原則.

  9. 如何保證服務宕機造成的分布式服務節點處理問題?

  10. 服務發現怎么實現的.

Golang面試參考

文章來自 https://github.com/KeKe-Li/golang-interview-questions/blob/master/src/chapter05/golang.01.md


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM