Go實戰面試備忘錄
原文地址://blog.likeli.top/posts/面試/go面試備忘錄/
一個小廠的面試,記錄一下,答案不對的,請幫忙更正下
go部分
map底層實現
map底層通過哈希表實現
slice和array的區別
array是固定長度的數組,使用前必須確定數組長度
array特點:
- go的數組是值類型,也就是說一個數組賦值給另一個數組,那麼實際上就是真箇數組拷貝了一份,需要申請額外的記憶體空間
- 如果go中的數組做為函數的參數,那麼實際傳遞的參數是一份數組的拷貝,而不是數組的指針
- array的長度也是Type的一部分,這樣就說明[10]int和[20]int是不一樣的
slice特點:
- slice是一個引用類型,是一個動態的指向數組切片的指針
- slice是一個不定長的,總是指向底層的數組array的數據結構
區別:
- 聲明時:array需要聲明長度或者…
- 做為函數參數時:array傳遞的是數組的副本,slice傳遞的是指針
struct和OOP使用中有什麼區別
首先OOP的特點:繼承、封裝、多態
繼承
概念:一個對象獲得另一個對象的屬性的過程
- java只有單繼承,介面多實現
- go可以實現多繼承
- 一個struct嵌套了另一個匿名struct,那麼這個struct可以直接訪問匿名機構提的方法,從而實現集成
- 一個struct嵌套了另一個命名的struct,那麼這個模式叫做組合
- 一個struct嵌套了多個匿名struct,那麼這個結構可以直接訪問多個匿名struct的方法,從而實現多重繼承
封裝
概念:自包含的黑盒子,有私有和公有的部分,公有可以被訪問,私有的外部不能訪問
- java中訪問許可權控制通過public、protected、private、default關鍵字控制
- go通過約定來實現許可權控制。變數名首字母大寫,相當於public,首字母小寫,相當於private。在同一個包中訪問,相當於default。由於在go中沒有繼承,所以就沒有protected
多態
概念:允許用一個介面在訪問同一類動作的特性
- java中的多態是通過
extends class
或implements interface
實現 - go中的interface通過
合約
方式實現,只要某個struct實現了某個interface中的所有方法,那麼它就隱式的實現了該介面
聊聊你對channel的理解
channel是一個通訊機制,它可以讓一個goroutine通過它給另一個goroutine發送值資訊。每個channel都有一個特殊的類型,也就是channel可發送數據的類型。
channel有哪些狀態
channel有三種狀態:
- nil,未初始化的狀態,只進行了聲明,或者手動賦值為nil
- active,正常的channel,可讀可寫
- closed,已關閉
channel可進行三種操作:
- 讀
- 寫
- 關閉
這三種操作和狀態可以組合出九種情況:
操作 | nil的channel | 正常channel | 已關閉channel |
---|---|---|---|
<-ch (讀) | 阻塞 | 成功或阻塞 | 讀到零值 |
ch<- (寫) | 阻塞 | 成功或阻塞 | panic |
close(ch) (關閉) | panic | 成功 | panic |
在並髮狀態下map如何保證執行緒安全
go的map並發訪問是不安全的,會出現未定義行為,導致程式退出。
go1.6之前,內置的map類型是部分goroutine安全的,並發的讀沒有問題,並發的寫可能有問題。go1.6之後,並發的讀寫map會報錯。
對比一下Java的
ConcurrentHashMap
的實現,在map的數據非常大的情況下,一把鎖會導致大並發的客戶端爭搶一把鎖,Java的解決方案是shard
,內部使用多個鎖,每個區間共享一把鎖,這樣減少了數據共享一把鎖的性能影響
go1.9之前,一般情況下通過sync.RWMutex
實現對map的並發訪問控制,或者單獨使用鎖都可以。
go1.9之後,實現了sync.Map
,類似於Java的ConcurrentHashMap
。
sync.Map
的實現有幾個優化點:
- 空間換時間。通過冗餘的兩個數據結構(read,dirty),實現加鎖對性能的影響
- 使用只讀數據(read),避免讀寫衝突
- 動態調整,miss次數多了之後,將dirty數據提升為read
- double-checking
- 延遲刪除。刪除一個鍵值只是打標記,只有在提升dirty的時候才清理刪除的數據
- 優先從read讀取、更新、刪除,因為對read的讀取不需要鎖
聊聊你對gc的理解
記憶體管理
go實現的記憶體管理簡單的說就是維護一塊大的全局記憶體,每個執行緒(go中為P)維護一塊小的私有記憶體,私有記憶體不足再從全局申請。
- go程式啟動時申請一塊大記憶體,並劃分成spans、bitmap、arena區域
- arean區域按頁劃分成一個個小塊
- span管理一個或多個頁
- mcentral管理多個span供執行緒申請使用
- mcache作為執行緒私有資源,資源來源於mcentral
更多說明參閱引用說明[1]
垃圾回收
常見的垃圾回收演算法:
- 引用計數:對每個對象維護一個引用計數,當引用該對象的對象被銷毀時,引用計數減一,當引用計數為0時回收該對象。
- 優點:對象可以很快地被回收,不會出現記憶體耗盡或達到某個閾值時才回收。
- 缺點:不能很好的處理循環引用,而且實時的維護引用計數,也有一定的代價。
- 代表語言:Python、PHP、Swift
- 標記-清除:從根變數遍歷所有引用的對象,引用對象標記為」被引用「,沒有被標記的進行回收。
- 優點:解決了引用計數的缺點
- 缺點:需要STW(Stop The World),就是停掉所有的goroutine,專心做垃圾回收,待垃圾回收結束後再恢復goroutine,這回導致程式短時間的暫停。
- 代表語言:Go(三色標記法)
- 分代收集:按照對象生命周期的長短劃分不同的代空間,生命周期長的放入老年代,而短的放入新生代,不同代有不同的回收演算法和回收頻率。
- 優點:回收性能好
- 缺點:回收演算法複雜
- 代表語言:Java
Go垃圾回收的三色標記法
三色標記法只是為了描述方便抽象出來的一種說法,實際上對象並沒有顏色之分。這裡的三色對應了垃圾回收過程中對象的三種狀態:
- 灰色:對象還在標記隊列中等待
- 黑色:對象已被標記,gcmarkBits對應的位為1(對象不會在本次GC中被清理)
- 白色:對象未被標記,gcmarkBits對應的位為0(對象將會在本次GC中被清理)
垃圾回收優化[2]
寫屏障(Write Barrier)
前面說過STW目的是防止GC掃描時記憶體變化而停掉goroutine,而寫屏障就是讓goroutine與GC同時運行的手段。雖然寫屏障不能完全消除STW,但是可以大大減少STW的時間。
寫屏障類似一種開關,在GC的特定時機開啟,開啟後指針傳遞時會把指針標記,即本輪不回收,下次GC時再確定。
GC過程中新分配的記憶體會被立即標記,用的並不是寫屏障技術,也即GC過程中分配的記憶體不會在本輪GC中回收。
輔助GC(Mutator Assist)
為了防止記憶體分配過快,在GC執行過程中,如果goroutine需要分配記憶體,那麼這個goroutine會參與一部分GC的工作,即幫助GC做一部分的工作,這個機制叫做Mutator Assist。
垃圾回收觸發時機[3]
記憶體分配量達到閾值出發GC
每次記憶體分配時都會檢查當前記憶體分配量是否已達到閾值,如果達到閾值則立即啟動GC。
閾值 = 上次GC記憶體分配量 * 記憶體增長率
記憶體增長率由環境變數GOGC
控制,默認為100,即每當記憶體擴大一倍時啟動GC。
定期觸發GC
默認情況下,最長2分鐘觸發一次GC,這個間隔在src/runtime/proc.go:forcegcperiod
變數中被聲明:
// forcegcperiod is the maximum time in nanoseconds between garbage
// collections. If we go this long without a garbage collection, one
// is forced to run.
//
// This is a variable for testing purposes. It normally doesn't change.
var forcegcperiod int64 = 2 * 60 * 1e9
手動觸發
程式程式碼中也可以使用runtime.GC()
來手動觸發GC,這主要用於GC性能測試和統計。
GC性能優化
GC性能與對象數量負相關,對象越多GC性能越差,對程式影響越大。
所以GC性能優化的思路之一就是減少對象分配個數,比如對象復用或使用大對象組合多個小對象等等。
另外,由於記憶體逃逸現象,有些隱式的記憶體分配也會產生,也有可能成為GC的負擔。
記憶體逃逸現象[4]:變數分配在棧上需要能在編譯器確定它的作用域,否則就會被分配到堆上。而堆上動態分配記憶體比棧上靜態分配記憶體,開銷大很多。
go通過go build -gcflags=m
命令來觀察變數逃逸情況[5]
更多逃逸場景:逃逸場景
逃逸分析的作用:
- 逃逸分析的好處是為了減少GC的壓力,不逃逸的對象分配在棧上,當函數返回時就回收了資源,不需要GC標記清除。
- 逃逸分析完後可以確定哪些變數可以分配在棧上,棧的分配比堆快,性能好(逃逸的局部變數會分配在堆上,而沒有發生逃逸的則由編譯器分配到棧上)
- 同步消除,如果你定義的對象在方法上有同步鎖,但在運行時,卻只有一個執行緒在訪問,此時逃逸分析後的機器碼,會去掉同步鎖運行
逃逸總結
- 棧上分配記憶體比在堆中分配記憶體有更高的效率
- 棧上分配的記憶體不需要GC處理
- 堆上分配的記憶體使用完畢會交給GC處理
- 逃逸分析的目的是決定記憶體分配到堆還是棧
- 逃逸分析在編譯階段完成
go方法傳參比起python、java有什麼區別
參考文檔:go的參數傳遞細節
go中的函數的參數傳遞採用的是值傳遞
gin
聊聊你對gin的理解
gin是一個go的微框架,封裝優雅,API友好。快速靈活。容錯方便等特點。
其實對於go而言,對web框架的依賴遠比Python、Java之類的小。本身的net/http
足夠簡單,而且性能也非常不錯,大部分的框架都是對net/http
的高階封裝。所以gin框架更像是一些常用函數或者工具的集合。使用gin框架發開發,可以提升效率,並同意團隊的編碼風格。
gin的路由組件為什麼高性能
路由樹
gin使用高性能路由庫httprouter
[6]
在Gin框架中,路由規則被分成了9課前綴樹,每一個HTTP Method對應一顆前綴樹,樹的節點按照URL中的 / 符號進行層級劃分
gin.RouterGroup
RouterGroup是對路由樹的包裝,所有的路由規則最終都是由它來進行管理。Engine結構體繼承了RouterGroup,所以Engine直接具備了RouterGroup所有的路由管理功能。
gin數據綁定
gin提供了很方便的數據綁定功能,可以將用戶傳過來的參數自動跟我們定義的結構體綁定在一起。
這是也我選擇用gin的重要原因。
gin數據驗證
在上面數據綁定的基礎上,gin還提供了數據校驗的方法。gin的數據驗證是和數據綁定結合在一起的。只需要在數據綁定的結構體成員變數的標籤添加binding
規則即可。這又省了大量的驗證工作,對用慣AspCoreMVC、Spring MVC的程式設計師來說是完美的替代框架。
gin的中間件
gin中間件利用函數調用棧後進先出
的特點,完成中間件在自定義處理函數完成後的處理操作。
redis
為什麼redis高性能
- 純記憶體操作,記憶體的讀寫速度非常快。
- 單執行緒[7],省避免了不必要的上下文切換和競爭條件,也不存在多進程或者多執行緒導致的切換而消耗CPU,不用去考慮各種鎖的問題,不存在加鎖釋放鎖的操作,因為沒有出現死鎖。
- 高效的數據結構,Redis的數據結構是專門進行設計的
- 使用多路I/O復用模型,非阻塞IO
- 使用底層模型不同,它們之間底層實現方式以及與客戶端之間通訊的應用協議不一樣,Redis直接自己構建了VM機制,因為一般的系統調用系統函數的話,會需要一定的時間去移動和請求;
為什麼redis要採用單執行緒
官方答覆:因為Redis是基於記憶體的操作,CPU不是Redis的瓶頸,Redis的瓶頸最有可能是機器的記憶體大小或者網路頻寬。既然單執行緒容易實現,而且CPU不會成為瓶頸,那就順理成章的採用單執行緒方案了
多路I/O復用模型,非阻塞IO
Linux下的select、poll和epoll就是干這個的。目前最先進的就是epoll。將用戶socket對應的fd註冊進epoll,然後epoll幫你監聽那些socket上有消息到達,這樣就避免了大量的無用操作。此時的socket採用非阻塞的模式。這樣,這個過程只在調用epoll的時候才會阻塞,收發客戶端消息是不會阻塞的,整個進程或執行緒就被充分利用起來,這也就是事件驅動。
常用的5數據結構
- String:快取、計數器、分散式鎖等
- List:鏈表、隊列、微博關注人時間軸列表等
- Hash:用戶資訊、Hash表等
- Set:去重、贊、共同好友等
- Zset:訪問量排行、點擊量排行榜等
redis作為消息隊列的可靠性如何保證
參照RabbitMQ的ACK機制,消費端提供消費回饋。
RabbitMQ
RabbitMQ如何保證消息的可靠性
生產端
有兩種方案:事務消息、消息確認
事務消息會嚴重損耗RabbitMQ的性能,所以基本不會使用。所以一般使用非同步的消息確認方式保證發送的消息一定到達RabbitMQ
消費端
消息確認(ACK),當Customer使用autoAck=true
的方式訂閱RabbitMQ節點消息的時候,可能由於網路原因也可能由於Customer處理消息的時候出現異常,亦或是伺服器宕機,都有可能丟失消息。
而當autoAck=true
的時候,RabbitMQ會自動把發出去的消息設置為確認,然後從記憶體(或者磁碟)中刪除,而不管消費者是否真正的消費到了這些消息。
為了避免這種情況下丟失消息,RabbitMQ提供了消費端確認的方式處理消息,所以需要設置autoAck=false
MQ本身
以上都是應用級別保證消息的可靠性,雖然已經極大的提高了應用的安全性,但是當RabbitMQ節點重啟、宕機等情況依舊會導致消息丟失,所以還需要設置隊列的持久性。消息的持久性,保證節點宕機或者重啟後能恢復消息。
如果出現單點問題,消息還是會丟失。所以可以對於關鍵的消息設置鏡像隊列和集群保證消息服務的高可用。
MongoDB
MongoDB是一個通用的、面向文檔的分散式資料庫
MongoDB索引的數據結構
MongoDB的默認引擎WiredTiger
使用B樹
做為索引底層的數據結構,但是除了B樹外,還支援LSM樹做為可選的底層數據存儲結構。
MongoDB索引是特殊的數據結構,索引存儲在一個易於遍歷讀取的數據集合中,索引是對資料庫表中一列或多列的值進行排序的一種結構。
MongoDB為什麼默認選擇B樹而不是MySQL默認的B+樹
首先是應用場景:
- 做為非關係型資料庫,MongoDB對於遍曆數據的需求沒有關係型資料庫那麼強,它追求的是讀寫單個記錄的性能
- 大多數的資料庫面對的都是讀多寫少的場景,B樹與LSM樹在該場景下有更大的優勢
MySQL中使用的B+樹是因為B+樹只有葉結點會存儲數據,將樹種的每一個葉結點通過指針連接起來就能實現順序遍歷,而遍歷資料庫在關係型資料庫中非常常見
MongoDB和MySQL在多個不同數據結構之間選擇的最終目的就是減少查詢需要的隨機IO次數,MySQL認為遍曆數據的查詢是非常常見的,所以它選擇B+樹作為底層數據結構。而MongoDB認為查詢單個數據記錄遠比遍曆數據更加常見,由於B樹的非葉結點也可以存儲數據,所以查詢一條數據所需要的平均隨機IO次數會比B+樹少,使用B樹的MongoDB在類似的場景中的查詢速度就會比MySQL快。這裡並不是說MongoDB並不能對數據進行遍歷,我們在MongoDB中也可以使用範圍查詢來查詢一批滿足對應條件的記錄,只是需要的時間會比MySQL長一些。
MongoDB作為非關係型的資料庫,它從集合的設計上就使用了完全不同的方法,如果我們仍然使用傳統的關係型資料庫的表設計思路來思考MongoDB中集合的設計,寫出的查詢可能會帶來相對比較差的性能。
MongoDB中推薦的設計方法,是使用嵌入文檔[8]。
MongoDB的索引有哪些,區別是什麼
MongoDB支援多種類型的索引,包括單欄位索引、複合索引、多key索引、文本索引等,每種類型的索引有不同的使用場景。
- 單欄位索引:能加速對指定欄位的各種查詢請求,是最常見的索引形式,MongoDB默認創建的id索引也是這種類型。
- 複合索引:是單欄位索引的升級版,它針對多個欄位聯合創建索引,先按第一個欄位排序,第一個欄位相同的文檔第二個欄位排序,以此類推。
- 多key索引:當索引的欄位為數組時,創建出的索引稱為多key索引,多key索引會為數組的每個元素建立一條索引。
- 哈希索引:是指按照某個欄位的hash值來建立索引,目前主要用於MongoDB Sharded Cluster的Hash分片,hash索引只能滿足欄位完全匹配的查詢,不能滿足範圍查詢等。
- 地理位置索引:能很好的解決O2O的應用場景,比如『查找附近的美食』、『查找某個區域內的車站』等。
- 文本索引:能解決快速文本查找的需求,比如有一個部落格文章集合,需要根據部落格的內容來快速查找,則可以針對部落格內容建立文本索引。
Nginx
文檔://www.aosabook.org/en/nginx.html
引用文章[9]
為什麼Nginx高性能
Nginx運行過程
- 多進程:一個 Master 進程、多個 Worker 進程
- Master 進程:管理 Worker 進程
- 對外介面:接收外部的操作(訊號)
- 對內轉發:根據外部的操作的不同,通過訊號管理 Worker
- 監控:監控 worker 進程的運行狀態,worker 進程異常終止後,自動重啟 worker 進程
- Worker 進程:所有 Worker 進程都是平等的
- 實際處理:網路請求,由 Worker 進程處理;
- Worker 進程數量:在 nginx.conf 中配置,一般設置為核心數,充分利用 CPU 資源,同時,避免進程數量過多,避免進程競爭 CPU 資源,增加上下文切換的損耗。
HTTP連接建立和請求處理過程
- Nginx啟動時,Master進程,載入配置文件
- Master進程,初始化監聽的socket
- Master進程,fork出多個Worker進程
- Worker進程,競爭新的連接,獲勝方通過三次握手,建立Socket連接,並處理請求
TCP/UDP
TCP
Tcp三次握手
圖片來自於《圖解HTTP》
- 客戶端-發送帶有SYN標誌的數據包 – 一次握手-服務端
- 第一次握手:Client什麼都不能確認;Server確認了對方發送正常,自己接收正常
- 服務端-發送帶有SYN/ACK標誌的數據包 – 二次握手-客戶端
- 第二次握手:Client確認了:自己發送、接收正常、對方發送、接收正常;Server確認了;對方發送、自己接收正常
- 客戶端-發送帶有ACK標誌的數據包 – 三次握手-服務端
- 第三次握手:Client確認了:自己發送、接收正常,對方發送、接收正常;Server確認了:自己發送、接收正常,對方發送、接收正常
所以需要三次握手才能確認雙方收發功能都正常。
Tcp四次揮手
斷開一個TCP連接則需要「四次揮手」:
- 客戶端-發送一個FIN,用來關閉客戶端到伺服器的數據傳送
- 伺服器-收到這個FIN,它發回一個ACK,確認序號為收到的序號加1。和SYN一樣,一個FIN將佔用一個序號
- 伺服器-關閉與客戶端的連接,發送一個FIN給客戶端
- 客戶端-發回ACK報文確認,並將確認序號設置為收到序號加1
任何一方都可以在數據傳送結束後發出連接釋放的通知,待對方確認或進入半關閉狀態。
當另一方也沒有數據再發送的時候,則發出連接釋放通知,對方確認後就完全關閉了TCP連接。
UDP
UDP在傳送數據之前不需要先建立連接,遠程主機在收到UDP報文後,不需要給出任何確認。雖然UDP不提供可靠交付,但在某些情況下UDP是一種最有效的工作方式(一般用於即時通訊,比如:QQ語言、QQ影片、直播等等)
長連接/短連接[10]
TCP本身沒有長短連接的區別,長短與否,取決於我們怎麼用它。
- 短連接:每次通訊時,創建Socket;一次通訊結束,調用
socket.close()
,這就是一般意義上的短連接。- 短連接的好處是管理起來比較簡單,存在的連接都是可用的連接,不需要額外的控制手段。
- 長連接:每次通訊完畢後,不會關閉連接,這樣可以做到連接的復用。
- 長連接的好處是省去了創建連接的耗時,性能好。
-
//rainbowmango.gitbook.io/go/chapter04/4.1-memory_alloc#4-zong-jie ↩︎
-
//rainbowmango.gitbook.io/go/chapter04/4.2-garbage_collection#4-la-ji-hui-shou-you-hua ↩︎
-
//rainbowmango.gitbook.io/go/chapter04/4.2-garbage_collection#5-la-ji-hui-shou-chu-fa-shi-ji ↩︎
-
redis的單執行緒指的是網路請求模組使用了一個執行緒,即一個執行緒處理所有的網路請求,其他模組仍用了多個執行緒 ↩︎