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()
}
  • 結論:
    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指向再次寫入時插入的位置