go面試題-基礎類

專職發表於2022-01-23

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()
}
  • 結論:
    1. 給一個nil channel傳送資料時會一直堵塞
    2. 從一個nil channel接收資料時會一直阻塞
    3. 給一個已關閉的channel傳送資料時會panic
    4. 從一個已關閉的channel中讀取資料時,如果channel為空,則返回通道中型別的領零值

go中channel緩衝有什麼特點?

  • 無緩衝的通道是同步的,有緩衝的通道是非同步的

go中的cap函式可以作用於哪些內容?

  • 可作用於的型別有
    1. 陣列(array)
    2. 切片(slice)
    3. 通道(channel)

go convey是什麼,一般用來做什麼?

  1. go convey是一個支援golang的單元測試框架
  2. 能夠自動監控檔案修改並啟動測試,並可以將測試結果實時輸出到web介面
  3. 提供了豐富的斷言簡化測試用例的編寫

go語言中new的作用是什麼?

  1. 使用new函式來分配記憶體空間
  2. 傳遞給new函式的是一個型別,而不是一個值
  3. 返回值是指向這個新分配的地址的指標

go語言中的make作用是什麼?

  • 分配記憶體空間並進行初始化, 返回值是該型別的例項而不是指標
  • make只能接收三種型別當做引數:slice、map、channel

總結new和make的區別?

  1. new可以接收任意內建型別當做引數,返回的是對應型別的指標
  2. make只能接收slice、map、channel當做引數,返回值是對應型別的例項

Printf、Sprintf、FprintF都是格式化輸出,有什麼不同?

  • 雖然這三個函式都是格式化輸出,但是輸出的目標不一樣
    1. Printf輸出到控制檯
    2. Sprintf結果賦值給返回值
    3. 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語言中的陣列和切片的區別是什麼?

  • 陣列:
    1. 陣列固定長度,陣列長度是陣列型別的一部分,所以[3]int和[4]int是兩種不同的陣列型別
    2. 陣列型別需要指定大小,不指定也會根據初始化,自動推算出大小,大小不可改變,陣列是通過值傳遞的
  • 切片:
    1. 切片的長度可改變,切片是輕量級的資料結構,三個屬性:指標、長度、容量
    2. 不要指定切片的大小,切片也是值傳遞只不過切片的一個屬性指標指向的資料不變,所以看起來像引用傳遞
    3. 切片可以通過陣列來初始化也可以通過make函式來初始化,初始化時的len和cap相等,然後進行擴容
    4. 切片擴容的時候會導致底層的陣列複製,也就是切片中的指標屬性會發生變化
    5. 切片也是拷貝,在不發生擴容時,底層使用的是同一個陣列,當對其中一個切片append的時候, 該切片長度會增加
      但是不會影響另外一個切片的長度
    6. copy函式將原切片拷貝到目標切片,會導致底層陣列複製,因為目標切片需要通過make函式來宣告初始化記憶體,然後
      將原切片指向的陣列元素拷貝到新切片指向的陣列元素
  • 重點:陣列儲存真正的資料,切片值儲存陣列的指標和該切片的長度和容量
  • append函式如果切片容量足夠的話,只會影響當前切片的長度,陣列底層不會複製,不會影響與陣列關聯的其它切片的長度
  • copy直接會導致陣列底層複製

go語言中值傳遞和地址傳遞(引用傳遞)如何執行?有什麼區別?舉例說明

  1. 值傳遞會把引數的值複製一份放到對應的函式裡,兩個變數的地址不同,不可互相修改
  2. 地址傳遞會把引數的地址複製一份放到對應的函式裡,兩個變數的地址相同,可以互相修改
  3. 例如:陣列傳遞就是值傳遞,而切片傳遞就是陣列的地址傳遞(本質上切片值傳遞,只不過是儲存的資料地址相同)

go中陣列和切片在傳遞時有什麼區別?

  1. 陣列是值傳遞
  2. 切片地址傳遞(引用傳遞)

go中是如何實現切片擴容的?

  1. 當容量小於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的作用和特點是什麼?

  1. 在普通函式或方法前加上defer關鍵字,就完成了defer所需要的語法,當defer語句被執行時,跟在defer語句後的函式會被延遲執行
  2. 知道包含該defer語句的函式執行完畢,defer語句後的函式才會執行,無論包含defer語句的函式是通過return正常結束,還是通過panic導致的異常結束
  3. 可以在一個函式中執行多條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的底層實現

  1. 切片是基於陣列實現的,它的底層是陣列,它本身非常小,它可以理解為對底層陣列的抽閒
  2. 因為基於陣列實現,所以它的底層記憶體是連續分配的,效率非常高,還可以通過索引獲取資料
  3. 切片本身並不是動態陣列或陣列指標,它內部實現的資料結構體通過指標引用底層陣列
  4. 設定相關屬性將讀寫操作限定在指定的區域內,切片本身是一個只讀物件,其工作機制類似於陣列指標的一種封裝
  5. 切片物件非常小,因為它只有三個欄位的資料結構:指向底層陣列的指標、切片的長度、切片的容量

go中slice的擴容機制,有什麼注意點?

  1. 首先判斷,如果新申請的容量大於2倍的舊容量,最終容量就是新申請的容量
  2. 否則判斷,如果舊切片的長度小於1024,最終容量就是舊容量的兩倍
  3. 否則判斷,如果舊切片的長度大於等於1024,則最終容量從舊容量開始迴圈增加原來的1/4,直到最終容量大於新申請的容量
  4. 如果最終容量計算值溢位,則最終容量就是新申請的容量

擴容前後的slice是否相同?

  • 情況一:
    1. 原來陣列還有容量可以擴容(實際容量沒有填充完),這種情況下,擴容之後的切片還是指向原來的陣列
    2. 對一個切片的操作可能影響多個指標指向相同地址的切片
  • 情況二:
    1. 原來陣列的容量已經達到了最大值,在擴容,go預設會先開闢一塊記憶體區域,把原來的值拷貝過來
    2. 然後再執行append操作,這種情況絲毫不影響原陣列
  • 注意:要複製一個slice最好使用copy函式

go中的引數傳遞、引用傳遞

  1. go語言中的所有的傳參都是值傳遞(傳值),都是一個副本,一個拷貝,
  2. 因為拷貝的內容有時候是非引用型別(int, string, struct)等,這樣在函式中就無法修改原內容資料
  3. 有的是引用型別(指標、slice、map、chan),這樣就可以修改原內容資料
  • go中的引用型別包含slice、map、chan,它們有複雜的內部結構,除了申請記憶體外,還需要初始化相關屬性
  • 內建函式new計算型別大小,為其分配零值記憶體,返回指標。
  • 而make會被編譯器翻譯成具體的建立函式,由其分配記憶體並初始化成員結構,返回物件而非指標

雜湊概念講解

  1. 雜湊表又稱為雜湊表,由一個直接定址表和一個雜湊函式組成
  2. 由於雜湊表的大小是有限的而要儲存的數值是無限的,因此對於任何雜湊函式,
  3. 都會出現兩個不同元素對映到相同位置的情況,這種情況叫做雜湊衝突
  4. 通過拉鍊法解決雜湊衝突:
    * 雜湊表每個位置都連線一個連結串列,當衝突發生是,衝突的元素將會被加到該位置連結串列的最後
  5. 雜湊表的查詢速度起決定性作用的就是雜湊函式: 除法雜湊發、乘法雜湊法、全域雜湊法
  6. 雜湊表的應用?
  7. 字典與集合都是通過雜湊表來實現的
  8. md5曾經是密碼學中常用的雜湊函式,可以吧任意長度的資料對映為128位的雜湊值

go中的map底層實現

  1. go中map的底層實現就是一個雜湊表,因此實現map的過程實際上就是實現雜湊表的過程
  2. 在這個雜湊表中,主要出現的結構體由兩個,一個是hmap、一個是bmap
  3. go中也有一個雜湊函式,用來對map中的鍵生成雜湊值
  4. hash結果的低位用於把k/v放到bmap陣列中的哪個bmap中
  5. 高位用於key的快速預覽,快速試錯

go中的map如何擴容

  1. 翻倍擴容:如果map中的鍵值對個數/桶的個數>6.5,就會引發翻倍擴容
  2. 等量擴容:當B<=15時,如果溢位桶的個數>=2的B次方就會引發等量擴容
  3. 當B>15時,如果溢位桶的個數>=2的15次方時就會引發等量擴容

go中map的查詢

  1. go中的map採用的是雜湊查詢表,由雜湊函式通過key和雜湊因此計算出雜湊值,
  2. 根據hamp中的B來確定放到哪個桶中,如果B=5,那麼就根據雜湊值的後5位確定放到哪個桶中
  3. 在用雜湊值的高8位確定桶中的位置,如果當前的bmap中未找到,則去對應的overflow bucket中查詢
  4. 如果當前map處於資料搬遷狀態,則優先從oldbuckets中查詢

介紹一下channel

  1. go中不要通過共享記憶體來通訊,而要通過通訊實現共享記憶體
  2. go中的csp併發模型,中文名通訊順序程式,就是通過goroutine和channel實現的
  3. channel收發遵循先進先出,分為有緩衝通道(非同步通道),無緩衝通道(同步通道)

go中channel的特性

  1. 給一個nil的channel傳送資料,會造成永久阻塞
  2. 從一個nil的channel接收資料,會造成永久阻塞
  3. 給一個已經關閉的channel傳送資料,會造成panic
  4. 從一個已經關閉的channel接收資料,如果緩衝區為空,會返回零值
  5. 無緩衝的channel是同步的,有緩衝的channel是非同步的
  6. 關閉一個nil channel會造成panic

channel中ring buffer的實現

  1. channel中使用了ring buffer(環形緩衝區)來快取寫入資料,
  2. ring buffer有很多好處,而且非常適合實現FiFo的固定長度佇列
  3. channel中包含buffer、sendx、recvx
  4. recvx指向最早被讀取的位置,sendx指向再次寫入時插入的位置

相關文章