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

 

Golang面試問題彙總

一般咱們去面試確定會有些不錯的Golang的面試題目的,因此總結下,讓其餘Golang開發者也能夠查看到,同時也用來檢測本身的能力和提醒本身的不足之處,歡迎你們補充和提交新的面試題目.php

Golang面試問題彙總:html

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

Golang中Goroutine 能夠經過 Channel 進行安全讀寫共享變量。java

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

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

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

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

Golang中channel 是被單首創建而且能夠在進程之間傳遞,它的通訊模式相似於 boss-worker 模式的,一個實體經過將消息發送到channel 中,而後又監聽這個 channel 的實體處理,兩個實體之間是匿名的,這個就實現實體中間的解耦,其中 channel 是同步的一個消息被髮送到 channel 中,最終是必定要被另外的實體消費掉的,在實現原理上其實相似一個阻塞的消息隊列。python

Goroutine 是Golang實際併發執行的實體,它底層是使用協程(coroutine)實現併發,coroutine是一種運行在用戶態的用戶線程,相似於 greenthread,go底層選擇使用coroutine的出發點是由於,它具備如下特色:mysql

  • 用戶空間 避免了內核態和用戶態的切換致使的成本。
  • 能夠由語言和框架層進行調度。
  • 更小的棧空間容許建立大量的實例。

Golang中的Goroutine的特性:linux

Golang內部有三個對象: P對象(processor) 表明上下文(或者能夠認爲是cpu),M(work thread)表明工做線程,G對象(goroutine).ios

正常狀況下一個cpu對象啓一個工做線程對象,線程去檢查並執行goroutine對象。碰到goroutine對象阻塞的時候,會啓動一個新的工做線程,以充分利用cpu資源。 全部有時候線程對象會比處理器對象多不少.c++

咱們用以下圖分別表示P、M、G:git

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';

注意:這裏只是使用基於數據庫的一種方法,使用數據庫實現分佈式鎖還有不少其餘的用法能夠實現!

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

一、由於是基於數據庫實現的,數據庫的可用性和性能將直接影響分佈式鎖的可用性及性能,因此,數據庫須要雙機部署、數據同步、主備切換;

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

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

四、不具有阻塞鎖特性,獲取不到鎖直接返回失敗,因此須要優化獲取邏輯,循環屢次去獲取。

五、在實施的過程當中會遇到各類不一樣的問題,爲了解決這些問題,實現方式將會愈來愈複雜;依賴數據庫須要必定的資源開銷,性能問題須要考慮。

  • 基於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

相關文章
相關標籤/搜索