go面試題-基礎類
go基礎類
1. go優勢
* 天生支持並發,性能高
* 單一的標準代碼格式,比其它語言更具可讀性
* 自動垃圾收集比java和python更有效,因為它與程序同時執行
go數據類型
int string float bool array slice map channel pointer struct interface method
go程序中的包是什麼
* 項目中包含go源文件以及其它包的目錄,源文件中的函數、變量、類型都存儲在該包中
* 每個源文件都屬於一個包,該包在文件頂部使用 package packageName 聲明
* 我們在源文件中需要導入第三方包時需要使用 import packageName
go支持什麼形式的類型轉換?將整數轉換為浮點數
* go支持顯示類型轉換,以滿足嚴格的類型要
* a := 15
* b := float64(a)
* fmt.Println(b, reflect.TypeOf(b))
什麼是 goroutine,你如何停止它?
* goroutine是協程/輕量級線程/用戶態線程,不同於傳統的內核態線程
* 佔用資源特別少,創建和銷毀只在用戶態執行不會到內核態,節省時間
* 創建goroutine需要使用go關鍵字
* 可以向goroutine發送一個信號通道來停止它,goroutine內部需要檢查信號通道
例子:
```
func main() {
var wg sync.WaitGroup
var exit = make(chan bool)
wg.Add(1)
go func() {
for {
select {
case <-exit: // 接收到信號後return退出當前goroutine
fmt.Println("goroutine接收到信號退出了!")
wg.Done()
return
default:
fmt.Println("還沒有接收到信號")
}
}
}()
exit <- true
wg.Wait()
}
```
如何在運行時檢查變量類型
* 類型開關(Type Switch)是在運行時檢查變量類型的最佳方式。
* 類型開關按類型而不是值來評估變量。每個 Switch 至少包含一個 case 用作條件語句
* 如果沒有一個 case 為真,則執行 default。
go兩個接口之間可以存在什麼關係
* 如果兩個接口有相同的方法列表,那麼他倆就是等價的,可以相互賦值
* 接口A可以嵌套到接口B裏面,那麼接口B就有了自己的方法列表+接口A的方法列表
go中同步鎖(互斥鎖)有什麼特點,作用是什麼?何時使用互斥鎖,何時使用讀寫鎖?
* 當一個goroutine獲得了Mutex後,其它goroutine就只能乖乖等待,除非該goroutine釋放Mutex
* RWMutext在讀鎖佔用的情況下會阻止寫,但不會阻止讀,在寫鎖佔用的情況下,會阻止任何其它goroutine進來
* 無論是讀還是寫,整個鎖相當於由該goroutine獨佔
* 作用:保證資源在使用時的獨有性,不會因為並發導致數據錯亂,保證系統穩定性
* 案例:
```
package main
import (
"fmt"
"sync"
"time"
)
var (
num = 0
lock = sync.RWMutex{} // 耗時:100+毫秒
//lock = sync.Mutex{} // 耗時:50+毫秒
)
func main() {
start := time.Now()
go func() {
for i := 0; i < 100000; i++{
lock.Lock()
//fmt.Println(num)
num++
lock.Unlock()
}
}()
for i := 0; i < 100000; i++{
lock.Lock()
//fmt.Println(num)
num++
lock.Unlock()
}
fmt.Println(num)
fmt.Println(time.Now().Sub(start))
}
```
// 結論:
// 1. 如果對數據寫的比較多,使用Mutex同步鎖/互斥鎖性能更高
// 2. 如果對數據讀的比較多,使用RWMutex讀寫鎖性能更高
goroutine案例(兩個goroutine,一個負責輸出數字,另一個負責輸出26個英文字母,格式如下:12ab34cd56ef78gh … yz)
package main
import (
"fmt"
"sync"
"unicode/utf8"
)
// 案例:兩個goroutine,一個負責輸出數字,另一個負責輸出26個英文字母,格式如下:12ab34cd56ef78gh ... yz
var (
wg = sync.WaitGroup{}
chNum = make(chan bool)
chAlpha = make(chan bool)
)
func main() {
go func() {
i := 1
for {
<-chNum
fmt.Printf("%v%v", i, i + 1)
i += 2
chAlpha <- true
}
}()
wg.Add(1)
go func() {
str := "abcdefghigklmnopqrstuvwxyz"
i := 0
for {
<-chAlpha
fmt.Printf("%v", str[i:i+2])
i += 2
if i >= utf8.RuneCountInString(str){
wg.Done()
return
}
chNum <- true
}
}()
chNum <- true
wg.Wait()
}
go語言中,channel通道有什麼特點,需要注意什麼?
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var ch chan int
var ch1 = make(chan int)
fmt.Println(ch, ch1) // <nil> 0xc000086060
wg.Add(1)
go func() {
//ch <- 15 // 如果給一個nil的channel發送數據會造成永久阻塞
//<-ch // 如果從一個nil的channel中接收數據也會造成永久阻塞
ret := <-ch1
fmt.Println(ret)
ret = <-ch1 // 從一個已關閉的通道中接收數據,如果緩衝區中為空,則返回該類型的零值
fmt.Println(ret)
wg.Done()
}()
go func() {
//close(ch1)
ch1 <- 15 // 給一個已關閉通道發送數據就會包panic錯誤
close(ch1)
}()
wg.Wait()
}
- 結論:
- 給一個nil channel發送數據時會一直堵塞
- 從一個nil channel接收數據時會一直阻塞
- 給一個已關閉的channel發送數據時會panic
- 從一個已關閉的channel中讀取數據時,如果channel為空,則返回通道中類型的領零值
go中channel緩衝有什麼特點?
go中的cap函數可以作用於哪些內容?
- 可作用於的類型有
- 數組(array)
- 切片(slice)
- 通道(channel)
go convey是什麼,一般用來做什麼?
- go convey是一個支持golang的單元測試框架
- 能夠自動監控文件修改並啟動測試,並可以將測試結果實時輸出到web界面
- 提供了豐富的斷言簡化測試用例的編寫
go語言中new的作用是什麼?
- 使用new函數來分配內存空間
- 傳遞給new函數的是一個類型,而不是一個值
- 返回值是指向這個新分配的地址的指針
go語言中的make作用是什麼?
- 分配內存空間並進行初始化, 返回值是該類型的實例而不是指針
- make只能接收三種類型當做參數:slice、map、channel
總結new和make的區別?
- new可以接收任意內置類型當做參數,返回的是對應類型的指針
- make只能接收slice、map、channel當做參數,返回值是對應類型的實例
Printf、Sprintf、FprintF都是格式化輸出,有什麼不同?
- 雖然這三個函數都是格式化輸出,但是輸出的目標不一樣
- Printf輸出到控制台
- Sprintf結果賦值給返回值
- FprintF輸出到指定的io.Writer接口中
例如:
func main() {
var a int = 15
file, _ := os.OpenFile("test.log", os.O_CREATE|os.O_APPEND, 0644)
// 格式化字符串並輸出到文件
n, _ := fmt.Fprintf(file, "%T:%v:%p", a, a, &a)
fmt.Println(n)
}
go語言中的數組和切片的區別是什麼?
- 數組:
- 數組固定長度,數組長度是數組類型的一部分,所以[3]int和[4]int是兩種不同的數組類型
- 數組類型需要指定大小,不指定也會根據初始化,自動推算出大小,大小不可改變,數組是通過值傳遞的
- 切片:
- 切片的長度可改變,切片是輕量級的數據結構,三個屬性:指針、長度、容量
- 不要指定切片的大小,切片也是值傳遞只不過切片的一個屬性指針指向的數據不變,所以看起來像引用傳遞
- 切片可以通過數組來初始化也可以通過make函數來初始化,初始化時的len和cap相等,然後進行擴容
- 切片擴容的時候會導致底層的數組複製,也就是切片中的指針屬性會發生變化
- 切片也是拷貝,在不發生擴容時,底層使用的是同一個數組,當對其中一個切片append的時候, 該切片長度會增加
但是不會影響另外一個切片的長度
- copy函數將原切片拷貝到目標切片,會導致底層數組複製,因為目標切片需要通過make函數來聲明初始化內存,然後
將原切片指向的數組元素拷貝到新切片指向的數組元素
- 重點:數組保存真正的數據,切片值保存數組的指針和該切片的長度和容量
- append函數如果切片容量足夠的話,只會影響當前切片的長度,數組底層不會複製,不會影響與數組關聯的其它切片的長度
- copy直接會導致數組底層複製
go語言中值傳遞和地址傳遞(引用傳遞)如何運行?有什麼區別?舉例說明
- 值傳遞會把參數的值複製一份放到對應的函數里,兩個變量的地址不同,不可互相修改
- 地址傳遞會把參數的地址複製一份放到對應的函數里,兩個變量的地址相同,可以互相修改
- 例如:數組傳遞就是值傳遞,而切片傳遞就是數組的地址傳遞(本質上切片值傳遞,只不過是保存的數據地址相同)
go中數組和切片在傳遞時有什麼區別?
- 數組是值傳遞
- 切片地址傳遞(引用傳遞)
go中是如何實現切片擴容的?
- 當容量小於1024時,每次擴容容量翻倍,當容量大於1024時,每次擴容加25%
func main() {
s1 := make([]int, 0)
for i := 0; i < 3000; i++{
fmt.Println("len =", len(s1), "cap = ", cap(s1))
s1 = append(s1, i)
}
}
看下面代碼defer的執行順序是什麼?defer的作用和特點是什麼?
- 在普通函數或方法前加上defer關鍵字,就完成了defer所需要的語法,當defer語句被執行時,跟在defer語句後的函數會被延遲執行
- 知道包含該defer語句的函數執行完畢,defer語句後的函數才會執行,無論包含defer語句的函數是通過return正常結束,還是通過panic導致的異常結束
- 可以在一個函數中執行多條defer語句,由於在棧中存儲,所以它的執行順序和聲明順序相反
defer語句中通過recover捕獲panic例子
func main() {
defer func() {
err := recover()
fmt.Println(err)
}()
defer fmt.Println("first defer")
defer fmt.Println("second defer")
defer fmt.Println("third defer")
fmt.Println("哈哈哈哈")
panic("abc is an error")
}
go中的25個關鍵字
- 程序聲明2
package import
- 程序實體聲明和定義8
var const type func struct map chan interface
- 程序流程控制15
for range continue break select switch case default if else fallthrough defer go goto return
寫一個定時任務,每秒執行一次
func main() {
t1 := time.NewTicker(time.Second * 1)
var i = 1
for {
if i == 10{
break
}
select {
case <-t1.C: // 一秒執行一次的定時任務
task1(i)
i++
}
}
}
func task1(i int) {
fmt.Println("task1執行了---", i)
}
switch case fallthrough default使用場景
func main() {
var a int
for i := 0; i < 10; i++{
a = rand.Intn(100)
switch {
case a >= 80:
fmt.Println("優秀", a)
fallthrough
case a >= 60:
fmt.Println("及格", a)
fallthrough
default:
fmt.Println("不及格", a)
}
}
}
defer的常用場景
- defer語句經常被用於處理成對的操作打開/關閉,鏈接/斷開連接,加鎖/釋放鎖
- 通過defer機制,不論函數邏輯多複雜,都能保證在任何執行路徑下,資源被釋放
- 釋放資源的defer語句應該直接跟在請求資源處理錯誤之後
- 注意:defer一定要放在請求資源處理錯誤之後
go中slice的底層實現
- 切片是基於數組實現的,它的底層是數組,它本身非常小,它可以理解為對底層數組的抽閑
- 因為基於數組實現,所以它的底層內存是連續分配的,效率非常高,還可以通過索引獲取數據
- 切片本身並不是動態數組或數組指針,它內部實現的數據結構體通過指針引用底層數組
- 設定相關屬性將讀寫操作限定在指定的區域內,切片本身是一個只讀對象,其工作機制類似於數組指針的一種封裝
- 切片對象非常小,因為它只有三個字段的數據結構:指向底層數組的指針、切片的長度、切片的容量
go中slice的擴容機制,有什麼注意點?
- 首先判斷,如果新申請的容量大於2倍的舊容量,最終容量就是新申請的容量
- 否則判斷,如果舊切片的長度小於1024,最終容量就是舊容量的兩倍
- 否則判斷,如果舊切片的長度大於等於1024,則最終容量從舊容量開始循環增加原來的1/4,直到最終容量大於新申請的容量
- 如果最終容量計算值溢出,則最終容量就是新申請的容量
擴容前後的slice是否相同?
- 情況一:
- 原來數組還有容量可以擴容(實際容量沒有填充完),這種情況下,擴容之後的切片還是指向原來的數組
- 對一個切片的操作可能影響多個指針指向相同地址的切片
- 情況二:
- 原來數組的容量已經達到了最大值,在擴容,go默認會先開闢一塊內存區域,把原來的值拷貝過來
- 然後再執行append操作,這種情況絲毫不影響原數組
- 注意:要複製一個slice最好使用copy函數
go中的參數傳遞、引用傳遞
- go語言中的所有的傳參都是值傳遞(傳值),都是一個副本,一個拷貝,
- 因為拷貝的內容有時候是非引用類型(int, string, struct)等,這樣在函數中就無法修改原內容數據
- 有的是引用類型(指針、slice、map、chan),這樣就可以修改原內容數據
- go中的引用類型包含slice、map、chan,它們有複雜的內部結構,除了申請內存外,還需要初始化相關屬性
- 內置函數new計算類型大小,為其分配零值內存,返回指針。
- 而make會被編譯器翻譯成具體的創建函數,由其分配內存並初始化成員結構,返回對象而非指針
哈希概念講解
- 哈希表又稱為散列表,由一個直接尋址表和一個哈希函數組成
- 由於哈希表的大小是有限的而要存儲的數值是無限的,因此對於任何哈希函數,
- 都會出現兩個不同元素映射到相同位置的情況,這種情況叫做哈希衝突
- 通過拉鏈法解決哈希衝突:
* 哈希表每個位置都連接一個鏈表,當衝突發生是,衝突的元素將會被加到該位置鏈表的最後
- 哈希表的查找速度起決定性作用的就是哈希函數: 除法哈希發、乘法哈希法、全域哈希法
- 哈希表的應用?
- 字典與集合都是通過哈希表來實現的
- md5曾經是密碼學中常用的哈希函數,可以吧任意長度的數據映射為128位的哈希值
go中的map底層實現
- go中map的底層實現就是一個散列表,因此實現map的過程實際上就是實現散列表的過程
- 在這個散列表中,主要出現的結構體由兩個,一個是hmap、一個是bmap
- go中也有一個哈希函數,用來對map中的鍵生成哈希值
- hash結果的低位用於把k/v放到bmap數組中的哪個bmap中
- 高位用於key的快速預覽,快速試錯
go中的map如何擴容
- 翻倍擴容:如果map中的鍵值對個數/桶的個數>6.5,就會引發翻倍擴容
- 等量擴容:當B<=15時,如果溢出桶的個數>=2的B次方就會引發等量擴容
- 當B>15時,如果溢出桶的個數>=2的15次方時就會引發等量擴容
go中map的查找
- go中的map採用的是哈希查找表,由哈希函數通過key和哈希因此計算出哈希值,
- 根據hamp中的B來確定放到哪個桶中,如果B=5,那麼就根據哈希值的後5位確定放到哪個桶中
- 在用哈希值的高8位確定桶中的位置,如果當前的bmap中未找到,則去對應的overflow bucket中查找
- 如果當前map處於數據搬遷狀態,則優先從oldbuckets中查找
介紹一下channel
- go中不要通過共享內存來通信,而要通過通信實現共享內存
- go中的csp並發模型,中文名通信順序進程,就是通過goroutine和channel實現的
- channel收發遵循先進先出,分為有緩衝通道(異步通道),無緩衝通道(同步通道)
go中channel的特性
- 給一個nil的channel發送數據,會造成永久阻塞
- 從一個nil的channel接收數據,會造成永久阻塞
- 給一個已經關閉的channel發送數據,會造成panic
- 從一個已經關閉的channel接收數據,如果緩衝區為空,會返回零值
- 無緩衝的channel是同步的,有緩衝的channel是異步的
- 關閉一個nil channel會造成panic
channel中ring buffer的實現
- channel中使用了ring buffer(環形緩衝區)來緩存寫入數據,
- ring buffer有很多好處,而且非常適合實現FiFo的固定長度隊列
- channel中包含buffer、sendx、recvx
- recvx指向最早被讀取的位置,sendx指向再次寫入時插入的位置