Go實現Raft第二篇:選舉
今天小編為大家分享一篇關於Golang實現Raft的文章,本篇文章為系列中的第二篇,對Raft中的選舉機制進行介紹並使用go進行實現。希望能對大家有所幫助。
本篇文章為Raft系列文章中的第二篇,Raft中的選舉。在該篇中,我們將解釋Raft實現的一般結構,並著重介紹該演算法的領導者選舉相關內容。該部分的程式碼以及相關的測試會在文章結尾給出,其中包含一些系統測試。 儘管不能響應客戶請求,也不能維護日誌。所有這些都將在下一部分中新增。
Go實現Raft第一篇:介紹
程式碼結構
關於Raft實現結構方式的相關資訊;適用於系列文章中的所有部分。
通常,Raft是作為引入到某些服務中的物件實現的。由於我們不在這裡開發服務,而是僅研究Raft本身,因此我建立了一個簡單的 Server 型別,該型別包裹 ConsensusModule 型別以儘可能地隔離程式碼中更感興趣的部分:
共識模組(CM)實現了Raft演算法的核心,位於 raft.go 檔案中。它完全從與叢集中其他副本的網路和連線的細節中抽象出來。ConsensusModule中與網路相關的唯一欄位是:
// CM中的伺服器ID
id int
// 叢集中節點的ID列表
peerIds []int
// 包含CM的伺服器,處理節點間的RPC通訊
server *Server
在實現中,每個Raft副本將群集中的其他副本稱為”端點“。叢集中的每個端點都有唯一的數字ID,以及所有端點的ID列表。server 欄位是指向包含 Server 的指標(在server.go中實現),使 ConsensusModule 可以將訊息傳送給端點。稍後我們將看到它是如何實現的。
這樣設計的目標是將所有網路細節都排除掉,把重點放在Raft演算法上。通常,要將Raft論文對映到此實現上,只需要 ConsensusModule 型別及其方法。伺服器程式碼是一個相當簡單的Go網路框架,有一些小的複雜之處來支援嚴格的測試。在本系列文章中,我們不會花時間,但是如果有不清楚的地方,可以留言提問。
Raft伺服器狀態
從總體上講,Raft CM是一個具有3種狀態的狀態機:
這可能有點迷惑,因為上一部分花費了大量時間來解釋Raft如何幫助實現狀態機。通常情況下,狀態一詞在這裡是過載的。Raft是用於實現任意複製狀態機的演算法,但它內部也有一個小型狀態機。後面,該狀態意味著從上下文中可以清楚地確定,不明確的地方我們也會指出該狀態。
在一個典型的穩定狀態場景中,群集中的一臺伺服器是領導者,而其他所有伺服器都是跟隨者。儘管我們不希望出問題,但Raft的目標就是容錯,因此我們將花費大部分時間來討論非典型場景,失敗情況,某些伺服器崩潰,其他伺服器斷開連線等。
正如前面所說,Raft使用了一個強大的領導模型。領導者響應客戶端的請求,將新條目新增到日誌中,並將其複製到跟隨者。萬一領導者失敗或停止響應,每個跟隨者都隨時準備接管領導權。這是圖中從“跟隨者”到“候選者”的“響應超時,開始選舉”的過渡。
任期
就像常規選舉一樣,在Raft中也有任期。任期是某個伺服器擔任領導者的時間段。新的選舉觸發一個新的任期,並且Raft演算法可確保給定的任期只有一個領導者。
但是,這個類別有點牽強,因為Raft選舉與真實選舉有很大差別。在Raft中,選舉更加合作;候選者的目標不是贏得選舉,而是在任何一個特定的任期內有合適的候選者贏得選舉。我們稍後將詳細討論“合適”的含義。
選舉計時器
Raft演算法的關鍵組成部分是選舉計時器。 這是每個跟隨者連續執行的計時器,每次收到當前領導者的訊息就會重新啟動它。領導者傳送週期性的心跳,因此當這些心跳停止到達時,跟隨者會認為領導者已經崩潰或斷開連線,並開始選舉(切換到候選狀態)。
問:不是所有的跟隨者都會同時成為候選人?
選舉計時器是隨機的,這是Raft簡單化的關鍵之一。Raft使用此隨機方法來降低多個關注者同時進行選舉的機會。但是,即使他們確實同時成為候選人,在任何給定的任期中也只有一個當選為領導者。在極少數情況下,如果投票分裂,以致沒有候選人能贏得選舉,將進行新的選舉(有新任期)。從理論上講,永久地重新進行選舉是可行的,但在每一輪選舉中發生這種情況的可能性都大大降低。
問:如果跟隨者與叢集斷開連線(分割槽)怎麼辦?它不會因為沒有聽到領導者的聲音而開始選舉嗎?
答:這是網路分割槽的隱患,因為跟隨者無法區分誰被分割槽。它確實將開始選舉。但是,如果是這個跟隨者被斷開,那麼這次選舉將無濟於事-因為它無法與其他端點聯絡,所以不會獲得任何投票。它可能會繼續保持候選狀態(每隔一段時間重新啟動一次新選舉),直到重新連線到叢集。稍後我們將更詳細地研究這種情況。
對等RPC
Raft有兩種RPC在端點之間互相傳送。有關這些RPC的詳細引數和規則,參見圖2。簡要討論它們的目標:
RequestVotes(RV):僅在候選狀態下使用;候選人使用它來請求選舉中的端點投票。答覆中包含是否批准投票的指示。
AppendEntries(AE):僅在領導者狀態下使用;領導者使用此RPC將日誌條目複製到跟隨者,也傳送心跳。即使沒有新的日誌條目要複製,該RPC也會定期傳送給每個跟隨者。
從以上內容可以推斷出跟隨者沒有傳送任何RPC。這是對的;跟隨者不會向其他端點發起RPC,但是他們在後臺執行選舉計時器。如果在沒有當前領導者的通訊的情況下經過了此計時器,則跟隨者將成為候選者並開始傳送RV。
實現選舉計時器
現在開始深入研究程式碼。以下程式碼檔案會在文章末尾給出。關於 ConsensusModule 結構的欄位的完整列表,可以在程式碼檔案中檢視。
我們的 CM 透過在 goroutine 中執行以下功能來實現選舉計時器:
func (cm *ConsensusModule) runElectionTimer() {
timeoutDuration := cm.electionTimeout()
cm.mu.Lock()
termStarted := cm.currentTerm
cm.mu.Unlock()
cm.dlog("election timer started (%v), term=%d", timeoutDuration, termStarted)
// This loops until either:
// - we discover the election timer is no longer needed, or
// - the election timer expires and this CM becomes a candidate
// In a follower, this typically keeps running in the background for the
// duration of the CM's lifetime.
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
for {
<-ticker.C
cm.mu.Lock()
if cm.state != Candidate && cm.state != Follower {
cm.dlog("in election timer state=%s, bailing out", cm.state)
cm.mu.Unlock()
return
}
if termStarted != cm.currentTerm {
cm.dlog("in election timer term changed from %d to %d, bailing out", termStarted, cm.currentTerm)
cm.mu.Unlock()
return
}
// Start an election if we haven't heard from a leader or haven't voted for
// someone for the duration of the timeout.
if elapsed := time.Since(cm.electionResetEvent); elapsed >= timeoutDuration {
cm.startElection()
cm.mu.Unlock()
return
}
cm.mu.Unlock()
}
}
首先透過呼叫 cm.electionTimeout 選擇一個偽隨機的選舉超時時間。正如論文中的建議,我們在這裡使用的範圍是150到300毫秒。和 ConsensusModule 的大多數方法一樣,runElectionTimer 在訪問欄位時會鎖定結構。這是必須要做的,因為實現嘗試儘可能地保持同步,這也是Go的優勢之一。這意味著順序程式碼是...順序執行的,並且不會拆分為多個事件處理程式。但是,RPC仍然同時發生,因此我們必須保護共享資料結構。我們很快就會講到RPC處理程式。
這個方法的主迴圈執行一個10毫秒的程式碼。有更有效的方法來等待事件,但是這種習慣用法程式碼最為簡單。每次迴圈迭代都在10毫秒之後進行。從理論上講,這可以使整個選舉超時,但隨後響應速度會變慢,並且在日誌中進行除錯/跟蹤會更加困難。我們檢查狀態是否仍然如預期且任期未更改。如果有任何關閉,我們終止選舉計時器。
如果自上次“選舉重置事件”以來已經過去了足夠的時間,則此端點開始選舉併成為候選人。這是什麼選舉重置事件?可以終止選舉的任何因素-例如,收到有效的心跳,或給另一個候選人投票。
成為候選人
上面可以看到,一旦經過足夠的時間而沒有跟隨者收到領導者或其他候選人的訊息,它將開始選舉。在檢視程式碼之前,我們考慮一下進行選舉所需的事情:
將狀態切換為候選項,並增加條件項,因為這是演算法為每次選舉指定的條件。
將RV RPC傳送給所有端點,要求他們在這次選舉中為我們投票。
等待對這些RPC的答覆,然後計數是否獲得足夠的選票成為領導者。
在Go中,所有這些邏輯都可以在一個函式中實現:
func (cm *ConsensusModule) startElection() {
cm.state = Candidate
cm.currentTerm += 1
savedCurrentTerm := cm.currentTerm
cm.electionResetEvent = time.Now()
cm.votedFor = cm.id
cm.dlog("becomes Candidate (currentTerm=%d); log=%v", savedCurrentTerm, cm.log)
var votesReceived int32 = 1
// Send RequestVote RPCs to all other servers concurrently.
for _, peerId := range cm.peerIds {
go func(peerId int) {
args := RequestVoteArgs{
Term: savedCurrentTerm,
CandidateId: cm.id,
}
var reply RequestVoteReply
cm.dlog("sending RequestVote to %d: %+v", peerId, args)
if err := cm.server.Call(peerId, "ConsensusModule.RequestVote", args, &reply); err == nil {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.dlog("received RequestVoteReply %+v", reply)
if cm.state != Candidate {
cm.dlog("while waiting for reply, state = %v", cm.state)
return
}
if reply.Term > savedCurrentTerm {
cm.dlog("term out of date in RequestVoteReply")
cm.becomeFollower(reply.Term)
return
} else if reply.Term == savedCurrentTerm {
if reply.VoteGranted {
votes := int(atomic.AddInt32(&votesReceived, 1))
if votes*2 > len(cm.peerIds)+1 {
// Won the election!
cm.dlog("wins election with %d votes", votes)
cm.startLeader()
return
}
}
}
}
}(peerId)
}
// Run another election timer, in case this election is not successful.
go cm.runElectionTimer()
}
候選人首先為自己投票-將 voiceReceived 初始化為1並設定 cm.votedFor =cm.id。
然後,它與所有其他端點並行發出RPC。每個RPC都在自己的goroutine中完成,因為我們的RPC呼叫是同步的-它們會阻塞直到收到響應為止,這可能需要一段時間。
rpc實現:
cm.server.Call(peer, "ConsensusModule.RequestVote", args, &reply)
我們使用 ConsensusModule.server 欄位中包含的Server指標發出遠端呼叫,使用 ConsensusModule.RequestVotes 作為遠端方法名稱。最終呼叫第一個引數中給出的端點的RequestVote方法。
如果RPC成功,已經過了一段時間,因此我們必須檢查狀態,看看有哪些選項。如果我們的狀態不再是候選人,就退出。什麼時候會發生這種情況?例如,我們可能贏得了選舉,因為其他RPC呼叫中有足夠的選票。或者收到其他RPC呼叫中的一個具有更高的任期,所以我們切換成跟隨者。重要的是,在網路不穩定的情況下,RPC可能需要很長時間才能到達-當我們收到答覆時,其餘程式碼可能會繼續進行,因此在這種情況下妥協放棄非常重要。
如果響應返回時我們仍然是候選人,我們將檢查響應的任期,並將其與傳送請求時的原始任期進行比較。如果答覆的任期較高,我們將恢復為跟隨者狀態。例如,如果其他候選人在我們收集選票時贏得了選舉,就會發生這種情況。
如果該任期與我們發出的任期相同,請檢查是否已投票。我們使用一個原子投票變數來安全地從多個goroutine中收集投票。如果此伺服器擁有多數表決權(包括它自己授予的表決權),它將成為領導者。
請注意,startElection方法不會阻塞。它更新一些狀態,啟動一堆goroutines並返回。因此,它還應該在goroutine中啟動一個新的選舉計數器-在最後一行進行。這樣可以確保如果這次選舉沒有任何用處,則超時後將開始新的選舉。這也解釋了runElectionTimer中的狀態檢查:如果此選舉確實將端點轉變為領導者,則併發執行的runElecionTimer將在觀察它不希望進入的狀態時才返回。
成為領導者
當投票記錄顯示該端點獲勝時,我們已經在startElection中看到了startLeader呼叫。
func (cm *ConsensusModule) startLeader() {
cm.state = Leader
cm.dlog("becomes Leader; term=%d, log=%v", cm.currentTerm, cm.log)
go func() {
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()
// Send periodic heartbeats, as long as still leader.
for {
cm.leaderSendHeartbeats()
<-ticker.C
cm.mu.Lock()
if cm.state != Leader {
cm.mu.Unlock()
return
}
cm.mu.Unlock()
}
}()
}
這實際上是一個相當簡單的方法:所有操作都是執行心跳計時器-一個goroutine,只要此CM仍然是領導者,它將每50毫秒呼叫一次 leaderSendHeartbeats。
func (cm *ConsensusModule) leaderSendHeartbeats() {
cm.mu.Lock()
savedCurrentTerm := cm.currentTerm
cm.mu.Unlock()
for _, peerId := range cm.peerIds {
args := AppendEntriesArgs{
Term: savedCurrentTerm,
LeaderId: cm.id,
}
go func(peerId int) {
cm.dlog("sending AppendEntries to %v: ni=%d, args=%+v", peerId, 0, args)
var reply AppendEntriesReply
if err := cm.server.Call(peerId, "ConsensusModule.AppendEntries", args, &reply); err == nil {
cm.mu.Lock()
defer cm.mu.Unlock()
if reply.Term > savedCurrentTerm {
cm.dlog("term out of date in heartbeat reply")
cm.becomeFollower(reply.Term)
return
}
}
}(peerId)
}
}
有點類似於startElection,從某種意義上說,它為每個對等點啟動了一個goroutine以傳送RPC。這次RPC是沒有日誌內容的AppendEntries(AE),在Raft中起著心跳的作用。
與處理RV響應類似,如果RPC返回的任期高於我們的任期,則此端點切換為跟隨者。
func (cm *ConsensusModule) becomeFollower(term int) {
cm.dlog("becomes Follower with term=%d; log=%v", term, cm.log)
cm.state = Follower
cm.currentTerm = term
cm.votedFor = -1
cm.electionResetEvent = time.Now()
go cm.runElectionTimer()
}
它將CM的狀態設定為跟隨者,並重置其條件和其他重要狀態欄位。啟動一個新的選舉計時器。
答覆RPC
目前為止,我們已經實現了活動部分-初始化RPC、計時器和狀態轉換。在我們看到伺服器方法之前,演示還不完整。 從RequestVote開始:
func (cm *ConsensusModule) RequestVote(args RequestVoteArgs, reply *RequestVoteReply) error {
cm.mu.Lock()
defer cm.mu.Unlock()
if cm.state == Dead {
return nil
}
cm.dlog("RequestVote: %+v [currentTerm=%d, votedFor=%d]", args, cm.currentTerm, cm.votedFor)
if args.Term > cm.currentTerm {
cm.dlog("... term out of date in RequestVote")
cm.becomeFollower(args.Term)
}
if cm.currentTerm == args.Term &&
(cm.votedFor == -1 || cm.votedFor == args.CandidateId) {
reply.VoteGranted = true
cm.votedFor = args.CandidateId
cm.electionResetEvent = time.Now()
} else {
reply.VoteGranted = false
}
reply.Term = cm.currentTerm
cm.dlog("... RequestVote reply: %+v", reply)
return nil
}
注意檢查是否為“死亡”狀態。我們稍後再討論。
檢查該任期是否過時併成為跟隨者。如果已經是跟隨者,則狀態不會更改,但其他狀態欄位將重置。
否則,如果呼叫者的任期與該任期一致,而我們還沒有投票給其他候選人,將進行投票。不會對較舊的RPC投票。
func (cm *ConsensusModule) AppendEntries(args AppendEntriesArgs, reply *AppendEntriesReply) error {
cm.mu.Lock()
defer cm.mu.Unlock()
if cm.state == Dead {
return nil
}
cm.dlog("AppendEntries: %+v", args)
if args.Term > cm.currentTerm {
cm.dlog("... term out of date in AppendEntries")
cm.becomeFollower(args.Term)
}
reply.Success = false
if args.Term == cm.currentTerm {
if cm.state != Follower {
cm.becomeFollower(args.Term)
}
cm.electionResetEvent = time.Now()
reply.Success = true
}
reply.Term = cm.currentTerm
cm.dlog("AppendEntries reply: %+v", *reply)
return nil
}
該邏輯也與圖2的選擇部分保持一致。需要了解的一個複雜情況是:
if cm.state != Follower {
cm.becomeFollower(args.Term)
}
問:如果此端點是領導者怎麼辦?為什麼它成為另一個領導者的跟隨者?
答:Raft在任何給定的任期內都保證只有一個領導者存在。如果仔細地遵循RequestVote的邏輯以及傳送RV的startElection中的程式碼,將看到在叢集中不能使用相同的任期存在兩個領導者。對於發現其他端點贏得選舉的候選人而言,這一條件很重要。
狀態和goroutine
回顧一下CM可能處於的所有狀態,並在其中執行不同的goroutine:
跟隨者:將CM初始化為跟隨者,並且在對beginFollower的每次呼叫中,一個新的goroutine開始執行runElectionTimer。注意,在短時間內一次可以執行多個。假設跟隨者在較高的任期內從領導者那裡獲得了RV;將觸發另一個beginFollower呼叫,該呼叫將啟動新的計時器goroutine。但是,舊的一旦發現任期發生變化,將不做任何事情直接退出。
候選人:也同時具有選舉goroutine的計時器,除此之外,還有許多goroutines傳送RPC。具有與跟隨者相同的保護措施,可以在新的執行程式停止執行時停止“舊的”選舉程式。請記住,RPC goroutine可能需要很長時間才能完成,因此,如果他們注意到RPC呼叫返回時它們已過時,必須安靜地退出,這一點至關重要。
領導者:領導者沒有選舉goroutine,但有心跳goroutine每50毫秒執行一次。
程式碼中還有一個附加狀態-死亡狀態。是為了有序地關閉CM。呼叫Stop會將狀態設定為Dead,所有goroutine會在觀察到該狀態後立即退出。
使所有這些goroutine執行可能會令人擔憂-如果其中一些仍在後臺執行怎麼辦?或更糟糕的是,它們反覆洩漏,其數量無邊無際地增長?這就是洩漏檢查的目的,並且一些測試啟用了洩漏檢查。
伺服器失控和任期增加
總結以上部分,我們研究一個可能發生的特殊場景以及Raft如何應對。這個例子非常有趣並且很有啟發性。在這裡,我試圖將其呈現為一個故事,但是您可能希望使用一張紙來跟蹤不同伺服器的狀態。 如果您不能遵循該示例-請給我傳送電子郵件-我們將很樂意對其進行修復以使其更加清晰。
設想一個有三臺伺服器的群集:A,B和C。假設A是領導者,起始項為1,並且該群集執行正常。A每隔50毫秒向B和C傳送一次心跳AE RPC,並在幾毫秒內獲得快速響應;每個這樣的AE都會重置B和C的 eletementResetEvent,因此它們仍然是的跟隨者。
在某個時間點,由於網路路由器出現故障,伺服器B從A和C中分割槽了。A仍每50毫秒向其傳送一次AE,但是這些AE要麼立即出錯,要麼在底層RPC引擎超時出錯。A對此無能為力,但這沒什麼大不了的。我們還沒有討論日誌複製,但是由於三臺伺服器中的兩臺還處於活動狀態,因此群集具有提交客戶端命令的數量。
B呢?假設當斷開連線時,其選舉超時設定為200毫秒。斷開連線大約200毫秒後,B的runElectionTimer goroutine意識到沒有收到來自領導者的選舉超時訊息。B無法區分誰存在,所以它將成為候選人並開始新的選舉。
因此,B的任期將變為2(而A和C的項仍為1)。B會將RV RPC傳送給A和C,要求他們投票。當然,這些RPC在B的網路中丟失了。B的startElection在開始時就啟動了另一個runElectionTimer goroutine,該goroutine等待250毫秒(超時範圍在150-300毫秒之間是隨機的),檢視是否由於上次選舉而發生了重要的事情。B沒做任何事情,因為它仍然是完全隔離的。因此,runElectionTimer開始另一個新的選舉,將期限增加到3。
很長時間,B的路由器需要花費幾秒鐘的時間來重置並恢復線上狀態。同時,B偶爾會重新選舉一次,其任期已到8。
此時,網路分割槽恢復,並且B重新連線到A和C。
然後,AE RPC從A到達。回想一下A一直每50 ms傳送一次,儘管B暫時沒有回覆。
B的 AppendEntries 被執行,並以term = 8傳送回一個答覆。
A在 LeaderSendHeartbeats 中獲得了此答覆,檢查了答覆的任期,並發現其高於其本身。它將自己的任期更新為8,併成為跟隨者。叢集暫時失去領導者。
現在可能會發生多種情況。B是候選者,但它可能在網路恢復之前已經傳送了RV。C是跟隨者,但在其自身的選舉超時時間內,它將成為候選者,因為它不再從A接收定期的AE。A成為跟隨者,並且還將在其選舉超時時間內成為候選者。
因此,這三臺伺服器中的任何一臺都可以贏得下一次選舉。這僅是因為我們實際上未在此處複製任何日誌。正如我們將在下一部分中看到,在實際情況下,A和C可能會在B不在時新增一些新的客戶端命令,因此它們的日誌將是最新的。因此,B不能成為新的領導者-將會發生新的選舉,由A或C贏得;我們將在下一部分中再次討論該場景。
假設自從B斷開連線以來未新增任何新命令,則由於重新連線而導致更換領導者也是完全可以的。
這看起來效率很低。因為領導者的更換並不是真正必要的,因為在整個場景中A都非常健康。但是,在個別情況下以不降低效率為代價來使不變數保持簡單是Raft做出的設計選擇之一。在最常見的情況下(沒有任何中斷),效率才是關鍵,因為99.9%的時間叢集都是在正常狀態。
下一步
本系列的下一部分中將描述一個更完整的Raft實現,包括實際處理客戶端命令並在整個叢集中複製。敬請關注!
Raft參考:
程式碼參考:
說明:有小夥伴留言說原文是外網寫的,我們直接拿來發了,是否是盜用。實際上小編也是經過原作者Eli同意了的。發此係列文章的目的也很簡單,僅僅是為了能讓更多的小夥伴看到優質的文章,可以學習提升自己。
以下為Eli的郵件回覆:
360雲端計算
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69966971/viewspace-2681615/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Go 實現 Raft 第二篇:選舉GoRaft
- Go實現Raft第一篇:介紹GoRaft
- Go 實現 Raft 第一篇:介紹GoRaft
- Raft演算法系列教程1:Leader選舉Raft演算法
- Go 實現 Raft 第四篇:持久化和調優GoRaft持久化
- 實現 Raft 協議Raft協議
- Go 實現 Raft 第三篇:命令和日誌複製GoRaft
- 小技巧分享:在 Go 如何實現列舉?Go
- 談談raft fig8 —— 迷惑的提交條件和選舉條件Raft
- cornerstone中RAFT的buffer的實現Raft
- Cloudflare分散式系統中的拜占庭式失敗與Raft選舉問題 - cloudflareCloud分散式Raft
- 怎樣用Nacos實現Raft演算法Raft演算法
- consul 原始碼解析(一)raft 協議實現原始碼Raft協議
- GO實現Redis:GO實現Redis叢集(5)GoRedis
- tikv/raft-rs:在 Rust 中實現的 Raft 分散式共識演算法原始碼RaftRust分散式演算法原始碼
- zab選舉
- etcd學習(6)-etcd實現raft原始碼解讀Raft原始碼
- go 實現btcGo
- GO實現Redis:GO實現Redis的AOF持久化(4)GoRedis持久化
- 副本集選舉
- Flutter簡單實現手寫瀑布流 第二篇Flutter
- Spark UDAF實現舉例 -- average poolingSpark
- Python如何實現窮舉搜尋?Python
- Go-grpc 實現GoRPC
- GO 實現快速排序Go排序
- go RWMutex 的實現GoMutex
- Go 實現 LeetCode 全集GoLeetCode
- Go interface實現分析Go
- Go語言實現excel匯入無限級選單結構GoExcel
- 是列舉?還是常量?其實很好選擇!
- ZooKeeper 工作、選舉 原理
- zk選舉過程
- 遞迴實現指數型列舉遞迴
- Java 利用列舉實現單例模式Java單例模式
- c++11 實現列舉值到列舉名的轉換C++
- OC中列舉寫法 以及 字串型別列舉實現探索字串型別
- Go 如何實現多型Go多型
- Go能實現AOP嗎?Go