利用Go語言實(shí)現(xiàn)Raft日志同步
在raft中,選取成功后集群就可以正常工作,一次正常的客戶端提案過程如下:
- 客戶端連接到leader,發(fā)起提案
- leader收到提案后將提案,包裝為一條日志
- leader將日志緩存到待提交日志
- leader發(fā)送日志到集群其他節(jié)點(diǎn)
- follower收到日志后,將日志緩存到待提交日志,并響應(yīng)leader請(qǐng)求
- leader收到follower響應(yīng),檢查是否集群大多數(shù)節(jié)點(diǎn)已響應(yīng)
- 集群中大多數(shù)節(jié)點(diǎn)已緩存日志后,leader提交日志,發(fā)送空日志要求follower提交日志
- leader響應(yīng)客戶端提案已接收
raft中日志規(guī)則如下:
1.如果在不同節(jié)點(diǎn)日志中的兩條日志記錄有相同的任期、編號(hào),則這兩條日志記錄具有相同的內(nèi)容
- leader在單個(gè)任期中最多創(chuàng)建一個(gè)給定編號(hào)的日志記錄
- leader不會(huì)改變?nèi)罩居涗浰诘奈恢茫╨eader不會(huì)覆蓋、刪除日志記錄)
2.如果在不同節(jié)點(diǎn)日志中的兩條日志記錄有相同的任期、編號(hào),則這兩條日志之前的所有日志相同
leader發(fā)送日志記錄到follower會(huì)包含上次日志記錄編號(hào)、任期
follower在收到追加日志時(shí)請(qǐng)求時(shí)會(huì)進(jìn)行一致性檢查,如檢查失敗,follower會(huì)拒絕追加請(qǐng)求,保證了日志與leader一致,一致性檢查失敗時(shí)leader會(huì)強(qiáng)迫follower接受leader日志,從而保證日志一致性。一致性檢查失敗分兩種情況,
a.follower日志存在缺失,收到失敗響應(yīng)后leader會(huì)將發(fā)送日志編號(hào)減一(follower檢查失敗時(shí)也可以返回當(dāng)前最新日志編號(hào)),發(fā)送follower缺失的日志
當(dāng)節(jié)點(diǎn)下線過一段時(shí)間/網(wǎng)絡(luò)異常消息丟失會(huì)出現(xiàn)日志缺失
b.follower有多余/不一致的日志,收到失敗響應(yīng)后leader會(huì)將發(fā)送日志編號(hào)減一(follower檢查失敗時(shí)也可以返回當(dāng)前最后提交的日志編號(hào)),找到兩份日志中最新的一致記錄編號(hào),刪除follower中不一致的部分
節(jié)點(diǎn)當(dāng)選leader收到消息只追加到本地尚未同步到集群便異常下線,后續(xù)上線會(huì)出現(xiàn)內(nèi)存中有不一致的日志,已提交日志不會(huì)出現(xiàn)不一致
日志在節(jié)點(diǎn)間的同步分兩步完成,
1.leader通過rpc將日志復(fù)制到其他節(jié)點(diǎn)
2.當(dāng)leader確認(rèn)日志已被復(fù)制到集群中大多數(shù)節(jié)點(diǎn)后,將日志持久化到磁盤,leader追蹤已提交的日志的最大編號(hào),通過日志rpc(含心跳)發(fā)送該編號(hào)告知follower需提交日志
日志提交時(shí)會(huì)持久化當(dāng)前編號(hào)及該編號(hào)之前的未提交日志
每條日志到包含客戶端的實(shí)際提案內(nèi)容、leader任期、日志編號(hào),定義日志記錄為如下結(jié)構(gòu):
- type 日志類型,當(dāng)前只有一種日志,客戶端的提案
- term 日志產(chǎn)生的任期,編號(hào)和任期相同代表為同一條日志
- index 日志編號(hào),單調(diào)增加
- data 提案的實(shí)際內(nèi)容
enum EntryType { NORMAL = 0; } message LogEntry { EntryType type = 1; uint64 term = 2; uint64 index = 3; bytes data = 4; }
定義日志整體結(jié)構(gòu)
未提交日志,保存在內(nèi)存切片中
日志提交時(shí),取出切片待提交部分,進(jìn)行持久化
type WaitApply struct { done bool index uint64 ch chan struct{} } type RaftLog struct { logEnties []*pb.LogEntry // 未提交日志 storage Storage // 已提交日志存儲(chǔ) commitIndex uint64 // 提交進(jìn)度 lastAppliedIndex uint64 // 最后提交日志 lastAppliedTerm uint64 // 最后提交日志任期 lastAppendIndex uint64 // 最后追加日志 logger *zap.SugaredLogger }
定義日志持久化接口,實(shí)際存儲(chǔ)實(shí)現(xiàn)由外部提供
type Storage interface { Append(entries []*pb.LogEntry) GetEntries(startIndex, endIndex uint64) []*pb.LogEntry GetTerm(index uint64) uint64 GetLastLogIndexAndTerm() (uint64, uint64) Close() }
實(shí)現(xiàn)一致性檢查
已持久化的日志必然與leader一致,檢查一致時(shí)只需檢查內(nèi)存中日志切片,存在以下幾種情況:
1.節(jié)點(diǎn)日志中有找到leader上次追加日志
I.為節(jié)點(diǎn)追加的最后一條日志
II.為節(jié)點(diǎn)內(nèi)存切片中的某條日志
- 節(jié)點(diǎn)網(wǎng)絡(luò)波動(dòng),導(dǎo)致未響應(yīng)leader,leader重發(fā)了記錄,清除重復(fù)日志記錄
- 節(jié)點(diǎn)作為leader期間有部分日志未同步到其他節(jié)點(diǎn)就失效,集群重新選舉,導(dǎo)致后續(xù)日志不一致,清除沖突日志(內(nèi)存中后續(xù)日志)
III.為節(jié)點(diǎn)最后提交日志
如內(nèi)存中存在日志記錄,則內(nèi)存中的記錄皆不一致,清除內(nèi)存日志記錄
2.節(jié)點(diǎn)未找到leader上次追加日志
I.存在相同日志編號(hào)記錄,任不相同
節(jié)點(diǎn)作為leader期間有部分日志未同步到其他節(jié)點(diǎn)就失效,集群重新選舉,導(dǎo)致使用了相同日志編號(hào),清除沖突日志(相同任期的日志),從節(jié)點(diǎn)未沖突部分開始重發(fā)
II.沒有相同日志編號(hào)記錄
日志缺失,需從最后提交開始重發(fā)
func (l *RaftLog) HasPrevLog(lastIndex, lastTerm uint64) bool { if lastIndex == 0 { return true } var term uint64 size := len(l.logEnties) if size > 0 { lastlog := l.logEnties[size-1] if lastlog.Index == lastIndex { term = lastlog.Term } else if lastlog.Index > lastIndex { // 檢查最后提交 if lastIndex == l.lastAppliedIndex { // 已提交日志必然一致 l.logEnties = l.logEnties[:0] return true } else if lastIndex > l.lastAppliedIndex { // 檢查未提交日志 for i, entry := range l.logEnties[:size] { if entry.Index == lastIndex { term = entry.Term // 將leader上次追加后日志清理 // 網(wǎng)絡(luò)異常未收到響應(yīng)導(dǎo)致leader重發(fā)日志/leader重選舉使舊leader未同步數(shù)據(jù)失效 l.logEnties = l.logEnties[:i+1] break } } } } } else if lastIndex == l.lastAppliedIndex { return true } b := term == lastTerm if !b { l.logger.Debugf("最新日志: %d, 任期: %d ,本地記錄任期: %d", lastIndex, lastTerm, term) if term != 0 { // 當(dāng)日志與leader不一致,刪除內(nèi)存中不一致數(shù)據(jù)同任期日志記錄 for i, entry := range l.logEnties { if entry.Term == term { l.logEnties = l.logEnties[:i] break } } } } return b }
實(shí)現(xiàn)日志追加,將新的日志添加到內(nèi)存切片,更新最后追加日志編號(hào)
func (l *RaftLog) AppendEntry(entry []*pb.LogEntry) { ??????? size := len(entry) if size == 0 { return } l.logEnties = append(l.logEnties, entry...) l.lastAppendIndex = entry[size-1].Index }
實(shí)現(xiàn)日志提交
- follower可能未同步全部日志,同步時(shí)如節(jié)點(diǎn)日志已同步全部待提交日志,則提交待提交日志,否則提交索引已追加日志
- 取出日志中待提交部分,添加到持久化存儲(chǔ),更新提交進(jìn)度、內(nèi)存切片
func (l *RaftLog) Apply(lastCommit, lastLogIndex uint64) { // 更新可提交索引 if lastCommit > l.commitIndex { if lastLogIndex > lastCommit { l.commitIndex = lastCommit } else { l.commitIndex = lastLogIndex } } // 提交索引 if l.commitIndex > l.lastAppliedIndex { n := 0 for i, entry := range l.logEnties { if l.commitIndex >= entry.Index { n = i } else { break } } entries := l.logEnties[:n+1] l.storage.Append(entries) l.lastAppliedIndex = l.logEnties[n].Index l.lastAppliedTerm = l.logEnties[n].Term l.logEnties = l.logEnties[n+1:] l.NotifyReadIndex() } }
定義新建函數(shù),創(chuàng)建實(shí)例時(shí)需提供存儲(chǔ)實(shí)現(xiàn)
func NewRaftLog(storage Storage, logger *zap.SugaredLogger) *RaftLog { lastIndex, lastTerm := storage.GetLastLogIndexAndTerm() return &RaftLog{ logEnties: make([]*pb.LogEntry, 0), storage: storage, commitIndex: lastIndex, lastAppliedIndex: lastIndex, lastAppliedTerm: lastTerm, lastAppendIndex: lastIndex, logger: logger, } }
實(shí)現(xiàn)了日志的一致性檢查、追加、提交,接下實(shí)現(xiàn)raft中日志處理邏輯,首先我們需要在leader節(jié)點(diǎn)中保存集群其他節(jié)點(diǎn)的日志同步進(jìn)度
節(jié)點(diǎn)在切換為leader時(shí)會(huì)將進(jìn)度重置
- 投票響應(yīng)中會(huì)返回節(jié)點(diǎn)最新日志信息
- 未收到投票響應(yīng)的,使用leader最新日志,在一致性檢查后動(dòng)態(tài)更新
在集群使用中通過第一條消息確認(rèn)網(wǎng)絡(luò)可用,后續(xù)假設(shè)網(wǎng)絡(luò)正常,消息發(fā)送即成功,不等待節(jié)點(diǎn)響應(yīng)消息,直到出現(xiàn)同步失敗
- prevResp 記錄上次發(fā)送結(jié)果,初始時(shí)為flase
- pending 中記錄未發(fā)送完成的日志編號(hào)
- 消息發(fā)送時(shí)如 !prevResp && len(pending) 為true,表示上次發(fā)送未完成,延遲后續(xù)信息發(fā)送
- 一次消息發(fā)送成功后,prevResp標(biāo)記為true,后續(xù)有待發(fā)送日志都直接發(fā)送
type ReplicaProgress struct { MatchIndex uint64 // 已接收日志 NextIndex uint64 // 下次發(fā)送日志 pending []uint64 // 未發(fā)送完成日志 prevResp bool // 上次日志發(fā)送結(jié)果 maybeLostIndex uint64 // 可能丟失的日志,記上次發(fā)送未完以重發(fā) }
leader將日志記錄追加到本地,再廣播到集群
func (r *Raft) BroadcastAppendEntries() { r.cluster.Foreach(func(id uint64, _ *ReplicaProgress) { if id == r.id { return } r.SendAppendEntries(id) }) } func (r *Raft) SendAppendEntries(to uint64) { p := r.cluster.progress[to] if p == nil || p.IsPause() { return } nextIndex := r.cluster.GetNextIndex(to) lastLogIndex := nextIndex - 1 lastLogTerm := r.raftlog.GetTerm(lastLogIndex) maxSize := MAX_LOG_ENTRY_SEND if !p.prevResp { maxSize = 1 } // var entries []*pb.LogEntry entries := r.raftlog.GetEntries(nextIndex, maxSize) size := len(entries) if size > 0 { r.cluster.AppendEntry(to, entries[size-1].Index) } r.send(&pb.RaftMessage{ MsgType: pb.MessageType_APPEND_ENTRY, Term: r.currentTerm, From: r.id, To: to, LastLogIndex: lastLogIndex, LastLogTerm: lastLogTerm, LastCommit: r.raftlog.commitIndex, Entry: entries, }) }
- 從日志中取得最新日志編號(hào),遍歷待追加日志,設(shè)置日志編號(hào)
- 追加日志到內(nèi)存切片
- 更新leader追加進(jìn)度
- 廣播日志到集群
func (r *Raft) AppendEntry(entries []*pb.LogEntry) { lastLogIndex, _ := r.raftlog.GetLastLogIndexAndTerm() for i, entry := range entries { entry.Index = lastLogIndex + 1 + uint64(i) entry.Term = r.currentTerm } r.raftlog.AppendEntry(entries) r.cluster.UpdateLogIndex(r.id, entries[len(entries)-1].Index) r.BroadcastAppendEntries() } func (c *Cluster) UpdateLogIndex(id uint64, lastIndex uint64) { p := c.progress[id] if p != nil { p.NextIndex = lastIndex p.MatchIndex = lastIndex + 1 } }
廣播日志與之前廣播心跳一致,遍歷集群信息發(fā)送到每個(gè)節(jié)點(diǎn),發(fā)送按下述流程
檢查發(fā)送狀態(tài),如上次發(fā)送未完成,暫緩發(fā)送
func (rp *ReplicaProgress) IsPause() bool { return (!rp.prevResp && len(rp.pending) > 0) }
從節(jié)點(diǎn)同步進(jìn)度中取得當(dāng)前需發(fā)送日志編號(hào)
func (c *Cluster) GetNextIndex(id uint64) uint64 { p := c.progress[id] if p != nil { return p.NextIndex } return 0 }
從leader的日志中取到要發(fā)送的日志
func (l *RaftLog) GetEntries(index uint64, maxSize int) []*pb.LogEntry { // 請(qǐng)求日志已提交,從存儲(chǔ)獲取 if index <= l.lastAppliedIndex { endIndex := index + MAX_APPEND_ENTRY_SIZE if endIndex >= l.lastAppliedIndex { endIndex = l.lastAppliedIndex + 1 } return l.storage.GetEntries(index, endIndex) } else { // 請(qǐng)求日志未提交,從數(shù)組獲取 var entries []*pb.LogEntry for i, entry := range l.logEnties { if entry.Index == index { if len(l.logEnties)-i > maxSize { entries = l.logEnties[i : i+maxSize] } else { entries = l.logEnties[i:] } break } } return entries } }
更新節(jié)點(diǎn)發(fā)送進(jìn)度,將節(jié)點(diǎn)待發(fā)送日志編號(hào)加一,將發(fā)送的日志編號(hào)加入未發(fā)送完成切片
上次發(fā)送成功時(shí),假設(shè)本次也會(huì)成功,如發(fā)送失敗再回退發(fā)送進(jìn)度
func (c *Cluster) AppendEntry(id uint64, lastIndex uint64) { p := c.progress[id] if p != nil { p.AppendEntry(lastIndex) } } func (rp *ReplicaProgress) AppendEntry(lastIndex uint64) { rp.pending = append(rp.pending, lastIndex) if rp.prevResp { rp.NextIndex = lastIndex + 1 } }
日志發(fā)送后,是follower收到日志進(jìn)行處理
進(jìn)行一致性檢查
- 檢查成功,將日志追加到follower內(nèi)存中,標(biāo)記追加成功
- 檢查失敗,一致性檢查中已處理沖突日志,直接標(biāo)記追加失敗
嘗試提交日志,每次日志消息都會(huì)包含leader提交進(jìn)度,按leader提交進(jìn)度,提交follower日志
響應(yīng)leader本次追加結(jié)果
func (r *Raft) ReciveAppendEntries(mLeader, mTerm, mLastLogTerm, mLastLogIndex, mLastCommit uint64, mEntries []*pb.LogEntry) { var accept bool if !r.raftlog.HasPrevLog(mLastLogIndex, mLastLogTerm) { // 檢查節(jié)點(diǎn)日志是否與leader一致 r.logger.Infof("節(jié)點(diǎn)未含有上次追加日志: Index: %d, Term: %d ", mLastLogIndex, mLastLogTerm) accept = false } else { r.raftlog.AppendEntry(mEntries) accept = true } lastLogIndex, lastLogTerm := r.raftlog.GetLastLogIndexAndTerm() r.raftlog.Apply(mLastCommit, lastLogIndex) r.send(&pb.RaftMessage{ MsgType: pb.MessageType_APPEND_ENTRY_RESP, Term: r.currentTerm, From: r.id, To: mLeader, LastLogIndex: lastLogIndex, LastLogTerm: lastLogTerm, Success: accept, }) }
leader處理follower日志追加響應(yīng),響應(yīng)分為日志追加成功、日志追加失敗
func (r *Raft) ReciveAppendEntriesResult(from, term, lastLogIndex uint64, success bool) { leaderLastLogIndex, _ := r.raftlog.GetLastLogIndexAndTerm() if success { r.cluster.AppendEntryResp(from, lastLogIndex) if lastLogIndex > r.raftlog.commitIndex { // 取已同步索引更新到lastcommit if r.cluster.CheckCommit(lastLogIndex) { prevApplied := r.raftlog.lastAppliedIndex r.raftlog.Apply(lastLogIndex, lastLogIndex) r.BroadcastAppendEntries() } } else if len(r.raftlog.waitQueue) > 0 { r.raftlog.NotifyReadIndex() } if r.cluster.GetNextIndex(from) <= leaderLastLogIndex { r.SendAppendEntries(from) } } else { r.logger.Infof("節(jié)點(diǎn) %s 追加日志失敗, Leader記錄節(jié)點(diǎn)最新日志: %d ,節(jié)點(diǎn)最新日志: %d ", strconv.FormatUint(from, 16), r.cluster.GetNextIndex(from)-1, lastLogIndex) ??????? r.cluster.ResetLogIndex(from, lastLogIndex, leaderLastLogIndex) r.SendAppendEntries(from) } }
日志追加成功時(shí)
更新同步進(jìn)度,更新節(jié)點(diǎn)已接收進(jìn)度,從未發(fā)送完成切片中清除已發(fā)送部分,標(biāo)記上次發(fā)送成功
func (c *Cluster) AppendEntryResp(id uint64, lastIndex uint64) { p := c.progress[id] if p != nil { p.AppendEntryResp(lastIndex) } } func (rp *ReplicaProgress) AppendEntryResp(lastIndex uint64) { if rp.MatchIndex < lastIndex { rp.MatchIndex = lastIndex } idx := -1 for i, v := range rp.pending { if v == lastIndex { idx = i } } // 標(biāo)記前次日志發(fā)送成功,更新下次發(fā)送 if !rp.prevResp { rp.prevResp = true rp.NextIndex = lastIndex + 1 } if idx > -1 { // 清除之前發(fā)送 rp.pending = rp.pending[idx+1:] } }
檢查follower數(shù)據(jù)同步進(jìn)度,判斷響應(yīng)對(duì)應(yīng)日志編號(hào)是否在集群中大多數(shù)節(jié)點(diǎn)已同步
func (c *Cluster) CheckCommit(index uint64) bool { // 集群達(dá)到多數(shù)共識(shí)才允許提交 incomingLogged := 0 for id := range c.progress { if index <= c.progress[id].MatchIndex { incomingLogged++ } } incomingCommit := incomingLogged >= len(c.progress)/2+1 return incomingCommit }
集群達(dá)成多數(shù)共識(shí)時(shí),提交日志,繼續(xù)廣播日志
當(dāng)響應(yīng)follower待發(fā)送日志編號(hào)小于leader最新日志時(shí)繼續(xù)發(fā)送日志
當(dāng)日志追加失敗時(shí)
按follower響應(yīng)的日志進(jìn)度重置日志同步進(jìn)度,標(biāo)記上次發(fā)送失敗,以延緩發(fā)送起始日志編號(hào)與follower不一致的日志,直到日志正確追加
func (c *Cluster) ResetLogIndex(id uint64, lastIndex uint64, leaderLastIndex uint64) { p := c.progress[id] if p != nil { p.ResetLogIndex(lastIndex, leaderLastIndex) } } func (rp *ReplicaProgress) ResetLogIndex(lastLogIndex uint64, leaderLastLogIndex uint64) { // 節(jié)點(diǎn)最后日志小于leader最新日志按節(jié)點(diǎn)更新進(jìn)度,否則按leader更新進(jìn)度 if lastLogIndex < leaderLastLogIndex { rp.NextIndex = lastLogIndex + 1 rp.MatchIndex = lastLogIndex } else { rp.NextIndex = leaderLastLogIndex + 1 rp.MatchIndex = leaderLastLogIndex } if rp.prevResp { rp.prevResp = false rp.pending = nil } }
按更新后同步進(jìn)度重發(fā)日志
修改raft新建函數(shù),參數(shù)中加入存儲(chǔ)接口
func NewRaft(id uint64, storage Storage, peers map[uint64]string, logger *zap.SugaredLogger) *Raft { raftlog := NewRaftLog(storage, logger) ... }
raft日志同步邏輯基本實(shí)現(xiàn),接下來實(shí)現(xiàn)raftNode中的提案方法以追加日志,在raftNode主循環(huán)中已實(shí)現(xiàn)讀取recv通道,調(diào)用raft消息處理方法,當(dāng)為leader時(shí)會(huì)將提案追加到日志,當(dāng)前只需要將提案消息加入recv通道
當(dāng)前l(fā)eader將提案加入讀寫通道后視為寫入成功,暫不實(shí)現(xiàn)集群多數(shù)共識(shí)后響應(yīng)客戶端
要實(shí)現(xiàn)多數(shù)響應(yīng)后通知,可添加一個(gè)新的結(jié)構(gòu)含RaftMessage和一個(gè)channel,在raftlog添加一個(gè)等待隊(duì)列,當(dāng)raft處理追加消息時(shí),將日志最后一條記錄的日志編號(hào)通過channel返回給提案方法,提案方法再將channel放入raftlog中等待隊(duì)列,提交日志檢查等待隊(duì)列待通知對(duì)象,通過channel通知提案方法指定日志編號(hào)已提交
func (n *RaftNode) Propose(ctx context.Context, entries []*pb.LogEntry) error { msg := &pb.RaftMessage{ MsgType: pb.MessageType_PROPOSE, Term: n.raft.currentTerm, Entry: entries, } return n.Process(ctx, msg) }
修改raftNode新建函數(shù)添加存儲(chǔ)接口,存儲(chǔ)實(shí)現(xiàn)在下篇lsm中實(shí)現(xiàn)
func NewRaftNode(id uint64, storage Storage, peers map[uint64]string, logger *zap.SugaredLogger) *RaftNode { node := &RaftNode{ raft: NewRaft(id, storage, peers, logger), ... } ... }
修改raft server中批量發(fā)送消息方法,將多個(gè)日志記錄合并到一個(gè)raft meassage進(jìn)行發(fā)送
func (p *Peer) SendBatch(msgs []*pb.RaftMessage) { p.wg.Add(1) var appEntryMsg *pb.RaftMessage var propMsg *pb.RaftMessage for _, msg := range msgs { if msg.MsgType == pb.MessageType_APPEND_ENTRY { if appEntryMsg == nil { appEntryMsg = msg } else { size := len(appEntryMsg.Entry) if size == 0 || len(msg.Entry) == 0 || appEntryMsg.Entry[size-1].Index+1 == msg.Entry[0].Index { appEntryMsg.LastCommit = msg.LastCommit appEntryMsg.Entry = append(appEntryMsg.Entry, msg.Entry...) } else if appEntryMsg.Entry[0].Index >= msg.Entry[0].Index { appEntryMsg = msg } } } else if msg.MsgType == pb.MessageType_PROPOSE { if propMsg == nil { propMsg = msg } else { propMsg.Entry = append(propMsg.Entry, msg.Entry...) } } else { p.send(msg) } } if appEntryMsg != nil { p.send(appEntryMsg) } if propMsg != nil { p.send(propMsg) } p.wg.Done() }
通過上述代碼實(shí)現(xiàn)了提案到leader,leader包裝為日志,同步到集群的過程,后續(xù)將通過lsm實(shí)現(xiàn)日志落盤并將raft server作為一個(gè)簡(jiǎn)單的kv數(shù)據(jù)庫。
參考:https://github.com/etcd-io/etcd
到此這篇關(guān)于利用Go語言實(shí)現(xiàn)Raft日志同步的文章就介紹到這了,更多相關(guān)Go Raft日志同步內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言字符串操作指南:簡(jiǎn)單易懂的實(shí)戰(zhàn)技巧
本文將介紹Go語言中字符串的實(shí)戰(zhàn)操作,通過本文的學(xué)習(xí),讀者將掌握Go語言中字符串的常用操作,為實(shí)際開發(fā)提供幫助,需要的朋友可以參考下2023-10-10go module構(gòu)建項(xiàng)目的實(shí)現(xiàn)
本文主要介紹了go module構(gòu)建項(xiàng)目的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03Golang 處理浮點(diǎn)數(shù)遇到的精度問題(使用decimal)
本文主要介紹了Golang 處理浮點(diǎn)數(shù)遇到的精度問題,不使用decimal會(huì)出大問題,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02一文帶你掌握Go語言I/O操作中的io.Reader和io.Writer
在?Go?語言中,io.Reader?和?io.Writer?是兩個(gè)非常重要的接口,它們?cè)谠S多標(biāo)準(zhǔn)庫中都扮演著關(guān)鍵角色,下面就跟隨小編一起學(xué)習(xí)一下它們的使用吧2025-01-01GoLang語法之標(biāo)準(zhǔn)庫fmt.Printf的使用
fmt包實(shí)現(xiàn)了類似C語言printf和scanf的格式化I/O,主要分為向外輸出內(nèi)容和獲取輸入內(nèi)容兩大部分,本文就來介紹一下GoLang語法之標(biāo)準(zhǔn)庫fmt.Printf的使用,感興趣的可以了解下2023-10-10golang中sync.Map并發(fā)創(chuàng)建、讀取問題實(shí)戰(zhàn)記錄
這篇文章主要給大家介紹了關(guān)于golang中sync.Map并發(fā)創(chuàng)建、讀取問題的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07