大廠Golang語(yǔ)法50問(wèn)!

前言
Golang 這門語(yǔ)言想必大家都不陌生,現(xiàn)在也比較火,學(xué)習(xí)的人也比較多。作為一款性能還算不錯(cuò)的語(yǔ)言,現(xiàn)在很多大廠的新項(xiàng)目都選擇了Golang。
這期針對(duì)大家的疑惑,總結(jié)了大廠系列Golang語(yǔ)法50問(wèn),供大家面試和學(xué)習(xí)用,下面看一下提綱。


1.使用值為 nil 的 slice、map會(huì)發(fā)生啥
允許對(duì)值為 nil 的 slice 添加元素,但對(duì)值為 nil 的 map 添加元素,則會(huì)造成運(yùn)行時(shí) panic。
// map 錯(cuò)誤示例
func main() {
var m map[string]int
m["one"] = 1 // error: panic: assignment to entry in nil map
// m := make(map[string]int)// map 的正確聲明,分配了實(shí)際的內(nèi)存
}
// slice 正確示例
func main() {
var s []int
s = append(s, 1)
}
2.訪問(wèn) map 中的 key,需要注意啥
當(dāng)訪問(wèn) map 中不存在的 key 時(shí),Go 則會(huì)返回元素對(duì)應(yīng)數(shù)據(jù)類型的零值,比如 nil、'' 、false 和 0,取值操作總有值返回,故不能通過(guò)取出來(lái)的值,來(lái)判斷 key 是不是在 map 中。
檢查 key 是否存在可以用 map 直接訪問(wèn),檢查返回的第二個(gè)參數(shù)即可。
// 錯(cuò)誤的 key 檢測(cè)方式
func main() {
x := map[string]string{"one": "2", "two": "", "three": "3"}
if v := x["two"]; v == "" {
fmt.Println("key two is no entry") // 鍵 two 存不存在都會(huì)返回的空字符串
}
}
// 正確示例
func main() {
x := map[string]string{"one": "2", "two": "", "three": "3"}
if _, ok := x["two"]; !ok {
fmt.Println("key two is no entry")
}
}
3.string 類型的值可以修改嗎
不能,嘗試使用索引遍歷字符串,來(lái)更新字符串中的個(gè)別字符,是不允許的。
string 類型的值是只讀的二進(jìn)制 byte slice,如果真要修改字符串中的字符,將 string 轉(zhuǎn)為 []byte 修改后,再轉(zhuǎn)為 string 即可。
// 修改字符串的錯(cuò)誤示例
func main() {
x := "text"
x[0] = "T" // error: cannot assign to x[0]
fmt.Println(x)
}
// 修改示例
func main() {
x := "text"
xBytes := []byte(x)
xBytes[0] = 'T' // 注意此時(shí)的 T 是 rune 類型
x = string(xBytes)
fmt.Println(x) // Text
}
4.switch 中如何強(qiáng)制執(zhí)行下一個(gè) case 代碼塊
switch 語(yǔ)句中的 case 代碼塊會(huì)默認(rèn)帶上 break,但可以使用 fallthrough 來(lái)強(qiáng)制執(zhí)行下一個(gè) case 代碼塊。
func main() {
isSpace := func(char byte) bool {
switch char {
case ' ': // 空格符會(huì)直接 break,返回 false // 和其他語(yǔ)言不一樣
// fallthrough // 返回 true
case '\t':
return true
}
return false
}
fmt.Println(isSpace('\t')) // true
fmt.Println(isSpace(' ')) // false
}
5.你是如何關(guān)閉 HTTP 的響應(yīng)體的
直接在處理 HTTP 響應(yīng)錯(cuò)誤的代碼塊中,直接關(guān)閉非 nil 的響應(yīng)體;手動(dòng)調(diào)用 defer 來(lái)關(guān)閉響應(yīng)體。
// 正確示例
func main() {
resp, err := http.Get("http://www.baidu.com")
// 關(guān)閉 resp.Body 的正確姿勢(shì)
if resp != nil {
defer resp.Body.Close()
}
checkError(err)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
checkError(err)
fmt.Println(string(body))
}
6.你是否主動(dòng)關(guān)閉過(guò)http連接,為啥要這樣做
有關(guān)閉,不關(guān)閉會(huì)程序可能會(huì)消耗完 socket 描述符。有如下2種關(guān)閉方式:
直接設(shè)置請(qǐng)求變量的 Close 字段值為 true,每次請(qǐng)求結(jié)束后就會(huì)主動(dòng)關(guān)閉連接。
設(shè)置 Header 請(qǐng)求頭部選項(xiàng) Connection: close,然后服務(wù)器返回的響應(yīng)頭部也會(huì)有這個(gè)選項(xiàng),此時(shí) HTTP 標(biāo)準(zhǔn)庫(kù)會(huì)主動(dòng)斷開連接
// 主動(dòng)關(guān)閉連接
func main() {
req, err := http.NewRequest("GET", "http://golang.org", nil)
checkError(err)
req.Close = true
//req.Header.Add("Connection", "close") // 等效的關(guān)閉方式
resp, err := http.DefaultClient.Do(req)
if resp != nil {
defer resp.Body.Close()
}
checkError(err)
body, err := ioutil.ReadAll(resp.Body)
checkError(err)
fmt.Println(string(body))
}
你可以創(chuàng)建一個(gè)自定義配置的 HTTP transport 客戶端,用來(lái)取消 HTTP 全局的復(fù)用連接。
func main() {
tr := http.Transport{DisableKeepAlives: true}
client := http.Client{Transport: &tr}
resp, err := client.Get("https://golang.google.cn/")
if resp != nil {
defer resp.Body.Close()
}
checkError(err)
fmt.Println(resp.StatusCode) // 200
body, err := ioutil.ReadAll(resp.Body)
checkError(err)
fmt.Println(len(string(body)))
}7.解析 JSON 數(shù)據(jù)時(shí),默認(rèn)將數(shù)值當(dāng)做哪種類型
在 encode/decode JSON 數(shù)據(jù)時(shí),Go 默認(rèn)會(huì)將數(shù)值當(dāng)做 float64 處理。
func main() {
var data = []byte(`{"status": 200}`)
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
log.Fatalln(err)
}
解析出來(lái)的 200 是 float 類型。
8.如何從 panic 中恢復(fù)
在一個(gè) defer 延遲執(zhí)行的函數(shù)中調(diào)用 recover ,它便能捕捉/中斷 panic。
// 錯(cuò)誤的 recover 調(diào)用示例
func main() {
recover() // 什么都不會(huì)捕捉
panic("not good") // 發(fā)生 panic,主程序退出
recover() // 不會(huì)被執(zhí)行
println("ok")
}
// 正確的 recover 調(diào)用示例
func main() {
defer func() {
fmt.Println("recovered: ", recover())
}()
panic("not good")
}
9.簡(jiǎn)短聲明的變量需要注意啥
簡(jiǎn)短聲明的變量只能在函數(shù)內(nèi)部使用
struct 的變量字段不能使用 := 來(lái)賦值
不能用簡(jiǎn)短聲明方式來(lái)單獨(dú)為一個(gè)變量重復(fù)聲明, := 左側(cè)至少有一個(gè)新變量,才允許多變量的重復(fù)聲明
10.range 迭代 map是有序的嗎
無(wú)序的。Go 的運(yùn)行時(shí)是有意打亂迭代順序的,所以你得到的迭代結(jié)果可能不一致。但也并不總會(huì)打亂,得到連續(xù)相同的 5 個(gè)迭代結(jié)果也是可能的。
11.recover的執(zhí)行時(shí)機(jī)
無(wú),recover 必須在 defer 函數(shù)中運(yùn)行。recover 捕獲的是祖父級(jí)調(diào)用時(shí)的異常,直接調(diào)用時(shí)無(wú)效。
func main() {
recover()
panic(1)
}
直接 defer 調(diào)用也是無(wú)效。
func main() {
defer recover()
panic(1)
}
defer 調(diào)用時(shí)多層嵌套依然無(wú)效。
func main() {
defer func() {
func() { recover() }()
}()
panic(1)
}
必須在 defer 函數(shù)中直接調(diào)用才有效。
func main() {
defer func() {
recover()
}()
panic(1)
}
12.閉包錯(cuò)誤引用同一個(gè)變量問(wèn)題怎么處理
在每輪迭代中生成一個(gè)局部變量 i 。如果沒有 i := i 這行,將會(huì)打印同一個(gè)變量。
func main() {
for i := 0; i < 5; i++ {
i := i
defer func() {
println(i)
}()
}
}
或者是通過(guò)函數(shù)參數(shù)傳入 i 。
func main() {
for i := 0; i < 5; i++ {
defer func(i int) {
println(i)
}(i)
}
}
13.在循環(huán)內(nèi)部執(zhí)行defer語(yǔ)句會(huì)發(fā)生啥
defer 在函數(shù)退出時(shí)才能執(zhí)行,在 for 執(zhí)行 defer 會(huì)導(dǎo)致資源延遲釋放。
func main() {
for i := 0; i < 5; i++ {
func() {
f, err := os.Open("/path/to/file")
if err != nil {
log.Fatal(err)
}
defer f.Close()
}()
}
}
func 是一個(gè)局部函數(shù),在局部函數(shù)里面執(zhí)行 defer 將不會(huì)有問(wèn)題。
14.說(shuō)出一個(gè)避免Goroutine泄露的措施
可以通過(guò) context 包來(lái)避免內(nèi)存泄漏。
func main() {
ctx, cancel := context.WithCancel(context.Background())
ch := func(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
for i := 0; ; i++ {
select {
case <- ctx.Done():
return
case ch <- i:
}
}
} ()
return ch
}(ctx)
for v := range ch {
fmt.Println(v)
if v == 5 {
cancel()
break
}
}
}
下面的 for 循環(huán)停止取數(shù)據(jù)時(shí),就用 cancel 函數(shù),讓另一個(gè)協(xié)程停止寫數(shù)據(jù)。如果下面 for 已停止讀取數(shù)據(jù),上面 for 循環(huán)還在寫入,就會(huì)造成內(nèi)存泄漏。
15.如何跳出for select 循環(huán)
通常在for循環(huán)中,使用break可以跳出循環(huán),但是注意在go語(yǔ)言中,for select配合時(shí),break 并不能跳出循環(huán)。
func testSelectFor2(chExit chan bool){
EXIT:
for {
select {
case v, ok := <-chExit:
if !ok {
fmt.Println("close channel 2", v)
break EXIT//goto EXIT2
}
fmt.Println("ch2 val =", v)
}
}
//EXIT2:
fmt.Println("exit testSelectFor2")
}
16.如何在切片中查找
go中使用 sort.searchXXX 方法,在排序好的切片中查找指定的方法,但是其返回是對(duì)應(yīng)的查找元素不存在時(shí),待插入的位置下標(biāo)(元素插入在返回下標(biāo)前)。
可以通過(guò)封裝如下函數(shù),達(dá)到目的。
func IsExist(s []string, t string) (int, bool) {
iIndex := sort.SearchStrings(s, t)
bExist := iIndex!=len(s) && s[iIndex]==t
return iIndex, bExist
}
17.如何初始化帶嵌套結(jié)構(gòu)的結(jié)構(gòu)體
go 的哲學(xué)是組合優(yōu)于繼承,使用 struct 嵌套即可完成組合,內(nèi)嵌的結(jié)構(gòu)體屬性就像外層結(jié)構(gòu)的屬性即可,可以直接調(diào)用。
注意初始化外層結(jié)構(gòu)體時(shí),必須指定內(nèi)嵌結(jié)構(gòu)體名稱的結(jié)構(gòu)體初始化,如下看到 s1方式報(bào)錯(cuò),s2 方式正確。
type stPeople struct {
Gender bool
Name string
}
type stStudent struct {
stPeople
Class int
}
//嘗試4 嵌套結(jié)構(gòu)的初始化表達(dá)式
//var s1 = stStudent{false, "JimWen", 3}
var s2 = stStudent{stPeople{false, "JimWen"}, 3}
fmt.Println(s2.Gender, s2.Name, s2.Class)
18.切片和數(shù)組的區(qū)別
數(shù)組是具有固定長(zhǎng)度,且擁有零個(gè)或者多個(gè),相同數(shù)據(jù)類型元素的序列。數(shù)組的長(zhǎng)度是數(shù)組類型的一部分,所以[3]int 和 [4]int 是兩種不同的數(shù)組類型。
數(shù)組需要指定大小,不指定也會(huì)根據(jù)初始化的自動(dòng)推算出大小,不可改變;數(shù)組是值傳遞。
數(shù)組是內(nèi)置類型,是一組同類型數(shù)據(jù)的集合,它是值類型,通過(guò)從0開始的下標(biāo)索引訪問(wèn)元素值。在初始化后長(zhǎng)度是固定的,無(wú)法修改其長(zhǎng)度。
當(dāng)作為方法的參數(shù)傳入時(shí)將復(fù)制一份數(shù)組而不是引用同一指針。數(shù)組的長(zhǎng)度也是其類型的一部分,通過(guò)內(nèi)置函數(shù)len(array)獲取其長(zhǎng)度。
數(shù)組定義:
var array [10]int
var array =[5]int{1,2,3,4,5}
切片表示一個(gè)擁有相同類型元素的可變長(zhǎng)度的序列。切片是一種輕量級(jí)的數(shù)據(jù)結(jié)構(gòu),它有三個(gè)屬性:指針、長(zhǎng)度和容量。
切片不需要指定大小;切片是地址傳遞;切片可以通過(guò)數(shù)組來(lái)初始化,也可以通過(guò)內(nèi)置函數(shù)make()初始化 。初始化時(shí)len=cap,在追加元素時(shí)如果容量cap不足時(shí)將按len的2倍擴(kuò)容。
切片定義:
var slice []type = make([]type, len)
19.new和make的區(qū)別
new 的作用是初始化一個(gè)指向類型的指針 (*T) 。
new 函數(shù)是內(nèi)建函數(shù),函數(shù)定義:func new(Type) *Type。
使用 new 函數(shù)來(lái)分配空間。傳遞給 new 函數(shù)的是一個(gè)類型,不是一個(gè)值。返回值是指向這個(gè)新分配的零值的指針。
make 的作用是為 slice,map 或 chan 初始化并返回引用 (T)。
make 函數(shù)是內(nèi)建函數(shù),函數(shù)定義:func make(Type, size IntegerType) Type;第一個(gè)參數(shù)是一個(gè)類型,第二個(gè)參數(shù)是長(zhǎng)度;返回值是一個(gè)類型。
make(T, args) 函數(shù)的目的與 new(T) 不同。它僅僅用于創(chuàng)建 Slice, Map 和 Channel,并且返回類型是 T(不是T*)的一個(gè)初始化的(不是零值)的實(shí)例。
20.Printf()、Sprintf()、Fprintf()函數(shù)的區(qū)別用法是什么
都是把格式好的字符串輸出,只是輸出的目標(biāo)不一樣。
Printf(),是把格式字符串輸出到標(biāo)準(zhǔn)輸出(一般是屏幕,可以重定向)。Printf() 是和標(biāo)準(zhǔn)輸出文件 (stdout) 關(guān)聯(lián)的,F(xiàn)printf 則沒有這個(gè)限制。
Sprintf(),是把格式字符串輸出到指定字符串中,所以參數(shù)比printf多一個(gè)char*。那就是目標(biāo)字符串地址。
Fprintf(),是把格式字符串輸出到指定文件設(shè)備中,所以參數(shù)比 printf 多一個(gè)文件指針 FILE*。主要用于文件操作。Fprintf() 是格式化輸出到一個(gè)stream,通常是到文件。
21.說(shuō)說(shuō)go語(yǔ)言中的for循環(huán)
for 循環(huán)支持 continue 和 break 來(lái)控制循環(huán),但是它提供了一個(gè)更高級(jí)的break,可以選擇中斷哪一個(gè)循環(huán) for 循環(huán)不支持以逗號(hào)為間隔的多個(gè)賦值語(yǔ)句,必須使用平行賦值的方式來(lái)初始化多個(gè)變量。
22.Array 類型的值作為函數(shù)參數(shù)
在 C/C++ 中,數(shù)組(名)是指針。將數(shù)組作為參數(shù)傳進(jìn)函數(shù)時(shí),相當(dāng)于傳遞了數(shù)組內(nèi)存地址的引用,在函數(shù)內(nèi)部會(huì)改變?cè)摂?shù)組的值。
在 Go 中,數(shù)組是值。作為參數(shù)傳進(jìn)函數(shù)時(shí),傳遞的是數(shù)組的原始值拷貝,此時(shí)在函數(shù)內(nèi)部是無(wú)法更新該數(shù)組的。
// 數(shù)組使用值拷貝傳參
func main() {
x := [3]int{1,2,3}
func(arr [3]int) {
arr[0] = 7
fmt.Println(arr) // [7 2 3]
}(x)
fmt.Println(x) // [1 2 3] // 并不是你以為的 [7 2 3]
}
想改變數(shù)組,直接傳遞指向這個(gè)數(shù)組的指針類型。
// 傳址會(huì)修改原數(shù)據(jù)
func main() {
x := [3]int{1,2,3}
func(arr *[3]int) {
(*arr)[0] = 7
fmt.Println(arr) // &[7 2 3]
}(&x)
fmt.Println(x) // [7 2 3]
}
直接使用 slice:即使函數(shù)內(nèi)部得到的是 slice 的值拷貝,但依舊會(huì)更新 slice 的原始數(shù)據(jù)(底層 array)
// 錯(cuò)誤示例
func main() {
x := []string{"a", "b", "c"}
for v := range x {
fmt.Println(v) // 1 2 3
}
}
// 正確示例
func main() {
x := []string{"a", "b", "c"}
for _, v := range x { // 使用 _ 丟棄索引
fmt.Println(v)
}
}說(shuō)。go語(yǔ)言中的for循
23.說(shuō)說(shuō)go語(yǔ)言中的switch語(yǔ)句
單個(gè) case 中,可以出現(xiàn)多個(gè)結(jié)果選項(xiàng)。只有在 case 中明確添加 fallthrough關(guān)鍵字,才會(huì)繼續(xù)執(zhí)行緊跟的下一個(gè) case。
24.說(shuō)說(shuō)go語(yǔ)言中有沒有隱藏的this指針
方法施加的對(duì)象顯式傳遞,沒有被隱藏起來(lái)。
golang 的面向?qū)ο蟊磉_(dá)更直觀,對(duì)于面向過(guò)程只是換了一種語(yǔ)法形式來(lái)表達(dá)
方法施加的對(duì)象不需要非得是指針,也不用非得叫 this。
25.go語(yǔ)言中的引用類型包含哪些
數(shù)組切片、字典(map)、通道(channel)、接口(interface)。
26.go語(yǔ)言中指針運(yùn)算有哪些
可以通過(guò)“&”取指針的地址;可以通過(guò)“*”取指針指向的數(shù)據(jù)。
26.說(shuō)說(shuō)go語(yǔ)言的main函數(shù)
main 函數(shù)不能帶參數(shù);main 函數(shù)不能定義返回值。
main 函數(shù)所在的包必須為 main 包;main 函數(shù)中可以使用 flag 包來(lái)獲取和解析命令行參數(shù)。
27.go語(yǔ)言觸發(fā)異常的場(chǎng)景有哪些
空指針解析
下標(biāo)越界
除數(shù)為0
調(diào)用 panic 函數(shù)
28.說(shuō)說(shuō)go語(yǔ)言的beego框架
beego 是一個(gè) golang 實(shí)現(xiàn)的輕量級(jí)HTTP框架
beego 可以通過(guò)注釋路由、正則路由等多種方式完成 url 路由注入
可以使用 bee new 工具生成空工程,然后使用 bee run 命令自動(dòng)熱編譯
29.說(shuō)說(shuō)go語(yǔ)言的goconvey框架
goconvey 是一個(gè)支持 golang 的單元測(cè)試框架
goconvey 能夠自動(dòng)監(jiān)控文件修改并啟動(dòng)測(cè)試,并可以將測(cè)試結(jié)果實(shí)時(shí)輸出到web界面
goconvey 提供了豐富的斷言簡(jiǎn)化測(cè)試用例的編寫
30.GoStub的作用是什么
GoStub 可以對(duì)全局變量打樁
GoStub 可以對(duì)函數(shù)打樁
GoStub 不可以對(duì)類的成員方法打樁
GoStub 可以打動(dòng)態(tài)樁,比如對(duì)一個(gè)函數(shù)打樁后,多次調(diào)用該函數(shù)會(huì)有不同的行為
31.go語(yǔ)言編程的好處是什么
編譯和運(yùn)行都很快。
在語(yǔ)言層級(jí)支持并行操作。
有垃圾處理器。
內(nèi)置字符串和 maps。
函數(shù)是 go 語(yǔ)言的最基本編程單位。
32.說(shuō)說(shuō)go語(yǔ)言的select機(jī)制
select 機(jī)制用來(lái)處理異步 IO 問(wèn)題
select 機(jī)制最大的一條限制就是每個(gè) case 語(yǔ)句里必須是一個(gè) IO 操作
golang 在語(yǔ)言級(jí)別支持 select 關(guān)鍵字
33.解釋一下go語(yǔ)言中的靜態(tài)類型聲明
靜態(tài)類型聲明是告訴編譯器不需要太多的關(guān)注這個(gè)變量的細(xì)節(jié)。
靜態(tài)變量的聲明,只是針對(duì)于編譯的時(shí)候, 在連接程序的時(shí)候,編譯器還要對(duì)這個(gè)變量進(jìn)行實(shí)際的聲明。
34.go的接口是什么
在 go 語(yǔ)言中,interface 也就是接口,被用來(lái)指定一個(gè)對(duì)象。接口具有下面的要素:
一系列的方法
具體應(yīng)用中并用來(lái)表示某個(gè)數(shù)據(jù)類型
在 go 中使用 interface 來(lái)實(shí)現(xiàn)多態(tài)
35.Go語(yǔ)言里面的類型斷言是怎么回事
類型斷言是用來(lái)從一個(gè)接口里面讀取數(shù)值給一個(gè)具體的類型變量。
類型轉(zhuǎn)換是指轉(zhuǎn)換兩個(gè)不相同的數(shù)據(jù)類型。
36.go語(yǔ)言中局部變量和全局變量的缺省值是什么
全局變量的缺省值是與這個(gè)類型相關(guān)的零值。
37.go語(yǔ)言編程的好處是什么
編譯和運(yùn)行都很快。
在語(yǔ)言層級(jí)支持并行操作。
有垃圾處理器。
內(nèi)置字符串和 maps。
函數(shù)是 go 語(yǔ)言的最基本編程單位。
38.解釋一下go語(yǔ)言中的靜態(tài)類型聲明
靜態(tài)類型聲明是告訴編譯器不需要太多的關(guān)注這個(gè)變量的細(xì)節(jié)。
靜態(tài)變量的聲明,只是針對(duì)于編譯的時(shí)候, 在連接程序的時(shí)候,編譯器還要對(duì)這個(gè)變量進(jìn)行實(shí)際的聲明。
39.模塊化編程是怎么回事
模塊化編程是指把一個(gè)大的程序分解成幾個(gè)小的程序。這么做的目的是為了減少程序的復(fù)雜度,易于維護(hù),并且達(dá)到最高的效率。
碼字不易,請(qǐng)不吝點(diǎn)贊,隨手關(guān)注,更多精彩,自動(dòng)送達(dá)。
40.Golang的方法有什么特別之處
函數(shù)的定義聲明沒有接收者。
方法的聲明和函數(shù)類似,他們的區(qū)別是:方法在定義的時(shí)候,會(huì)在func和方法名之間增加一個(gè)參數(shù),這個(gè)參數(shù)就是接收者,這樣我們定義的這個(gè)方法就和接收者綁定在了一起,稱之為這個(gè)接收者的方法。
Go語(yǔ)言里有兩種類型的接收者:值接收者和指針接收者。
使用值類型接收者定義的方法,在調(diào)用的時(shí)候,使用的其實(shí)是值接收者的一個(gè)副本,所以對(duì)該值的任何操作,不會(huì)影響原來(lái)的類型變量。-------相當(dāng)于形式參數(shù)。
如果我們使用一個(gè)指針作為接收者,那么就會(huì)其作用了,因?yàn)橹羔樈邮照邆鬟f的是一個(gè)指向原值指針的副本,指針的副本,指向的還是原來(lái)類型的值,所以修改時(shí),同時(shí)也會(huì)影響原來(lái)類型變量的值。
41.Golang可變參數(shù)
func main() {
print("1","2","3")
}
func print (a ...interface{}){
for _,v:=range a{
fmt.Print(v)
}
fmt.Println()
}
例子中我們自己定義了一個(gè)接受可變參數(shù)的函數(shù),效果和fmt.Println()一樣。
42.Golang Slice的底層實(shí)現(xiàn)
切片是基于數(shù)組實(shí)現(xiàn)的,它的底層是數(shù)組,它自己本身非常小,可以理解為對(duì)底層數(shù)組的抽象。因?yàn)榛跀?shù)組實(shí)現(xiàn),所以它的底層的內(nèi)存是連續(xù)分配的,效率非常高,還可以通過(guò)索引獲得數(shù)據(jù),可以迭代以及垃圾回收優(yōu)化。
切片本身并不是動(dòng)態(tài)數(shù)組或者數(shù)組指針。它內(nèi)部實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu)通過(guò)指針引用底層數(shù)組,設(shè)定相關(guān)屬性將數(shù)據(jù)讀寫操作限定在指定的區(qū)域內(nèi)。切片本身是一個(gè)只讀對(duì)象,其工作機(jī)制類似數(shù)組指針的一種封裝。
切片對(duì)象非常小,是因?yàn)樗侵挥?個(gè)字段的數(shù)據(jù)結(jié)構(gòu):
指向底層數(shù)組的指針
切片的長(zhǎng)度
切片的容量
這3個(gè)字段,就是Go語(yǔ)言操作底層數(shù)組的元數(shù)據(jù)。

43.Golang Slice的擴(kuò)容機(jī)制,有什么注意點(diǎn)
Go 中切片擴(kuò)容的策略是這樣的:
首先判斷,如果新申請(qǐng)容量大于 2 倍的舊容量,最終容量就是新申請(qǐng)的容量。
否則判斷,如果舊切片的長(zhǎng)度小于 1024,則最終容量就是舊容量的兩倍。
否則判斷,如果舊切片長(zhǎng)度大于等于 1024,則最終容量從舊容量開始循環(huán)增加原來(lái)的 1/4 , 直到最終容量大于等于新申請(qǐng)的容量。
如果最終容量計(jì)算值溢出,則最終容量就是新申請(qǐng)容量。
情況一:
原數(shù)組還有容量可以擴(kuò)容(實(shí)際容量沒有填充完),這種情況下,擴(kuò)容以后的數(shù)組還是指向原來(lái)的數(shù)組,對(duì)一個(gè)切片的操作可能影響多個(gè)指針指向相同地址的Slice。
情況二:
原來(lái)數(shù)組的容量已經(jīng)達(dá)到了最大值,再想擴(kuò)容, Go 默認(rèn)會(huì)先開一片內(nèi)存區(qū)域,把原來(lái)的值拷貝過(guò)來(lái),然后再執(zhí)行 append() 操作。這種情況絲毫不影響原數(shù)組。
要復(fù)制一個(gè)Slice,最好使用Copy函數(shù)。
44.Golang Map底層實(shí)現(xiàn)
Golang 中 map 的底層實(shí)現(xiàn)是一個(gè)散列表,因此實(shí)現(xiàn) map 的過(guò)程實(shí)際上就是實(shí)現(xiàn)散表的過(guò)程。
在這個(gè)散列表中,主要出現(xiàn)的結(jié)構(gòu)體有兩個(gè),一個(gè)叫hmap(a header for a go map),一個(gè)叫bmap(a bucket for a Go map,通常叫其bucket)。
hmap如下所示:

圖中有很多字段,但是便于理解 map 的架構(gòu),你只需要關(guān)心的只有一個(gè),就是標(biāo)紅的字段:buckets 數(shù)組。Golang 的 map 中用于存儲(chǔ)的結(jié)構(gòu)是 bucket數(shù)組。而 bucket(即bmap)的結(jié)構(gòu)是怎樣的呢?
bucket:

相比于 hmap,bucket 的結(jié)構(gòu)顯得簡(jiǎn)單一些,標(biāo)橙的字段依然是“核心”,我們使用的 map 中的 key 和 value 就存儲(chǔ)在這里。
"高位哈希值"數(shù)組記錄的是當(dāng)前 bucket 中 key 相關(guān)的"索引",稍后會(huì)詳細(xì)敘述。還有一個(gè)字段是一個(gè)指向擴(kuò)容后的 bucket 的指針,使得 bucket 會(huì)形成一個(gè)鏈表結(jié)構(gòu)。
整體的結(jié)構(gòu)應(yīng)該是這樣的:

Golang 把求得的哈希值按照用途一分為二:高位和低位。低位用于尋找當(dāng)前 key屬于 hmap 中的哪個(gè) bucket,而高位用于尋找 bucket 中的哪個(gè) key。
需要特別指出的一點(diǎn)是:map中的key/value值都是存到同一個(gè)數(shù)組中的。這樣做的好處是:在key和value的長(zhǎng)度不同的時(shí)候,可以消除padding帶來(lái)的空間浪費(fèi)。

Map 的擴(kuò)容:
當(dāng) Go 的 map 長(zhǎng)度增長(zhǎng)到大于加載因子所需的 map 長(zhǎng)度時(shí),Go 語(yǔ)言就會(huì)將產(chǎn)生一個(gè)新的 bucket 數(shù)組,然后把舊的 bucket 數(shù)組移到一個(gè)屬性字段 oldbucket中。
注意:并不是立刻把舊的數(shù)組中的元素轉(zhuǎn)義到新的 bucket 當(dāng)中,而是,只有當(dāng)訪問(wèn)到具體的某個(gè) bucket 的時(shí)候,會(huì)把 bucket 中的數(shù)據(jù)轉(zhuǎn)移到新的 bucket 中。
45. JSON 標(biāo)準(zhǔn)庫(kù)對(duì) nil slice 和 空 slice 的處理是一致的嗎
首先 JSON 標(biāo)準(zhǔn)庫(kù)對(duì) nil slice 和 空 slice 的處理是不一致。
通常錯(cuò)誤的用法,會(huì)報(bào)數(shù)組越界的錯(cuò)誤,因?yàn)橹皇锹暶髁藄lice,卻沒有給實(shí)例化的對(duì)象。
var slice []int
slice[1] = 0此時(shí)slice的值是nil,這種情況可以用于需要返回slice的函數(shù),當(dāng)函數(shù)出現(xiàn)異常的時(shí)候,保證函數(shù)依然會(huì)有nil的返回值。
empty slice 是指slice不為nil,但是slice沒有值,slice的底層的空間是空的,此時(shí)的定義如下:
slice := make([]int,0)
slice := []int{}當(dāng)我們查詢或者處理一個(gè)空的列表的時(shí)候,這非常有用,它會(huì)告訴我們返回的是一個(gè)列表,但是列表內(nèi)沒有任何值。
總之,nil slice 和 empty slice是不同的東西,需要我們加以區(qū)分的。
46.Golang的內(nèi)存模型,為什么小對(duì)象多了會(huì)造成gc壓力
通常小對(duì)象過(guò)多會(huì)導(dǎo)致 GC 三色法消耗過(guò)多的GPU。優(yōu)化思路是,減少對(duì)象分配。
47.Data Race問(wèn)題怎么解決?能不能不加鎖解決這個(gè)問(wèn)題
同步訪問(wèn)共享數(shù)據(jù)是處理數(shù)據(jù)競(jìng)爭(zhēng)的一種有效的方法。
golang在 1.1 之后引入了競(jìng)爭(zhēng)檢測(cè)機(jī)制,可以使用 go run -race 或者 go build -race來(lái)進(jìn)行靜態(tài)檢測(cè)。其在內(nèi)部的實(shí)現(xiàn)是,開啟多個(gè)協(xié)程執(zhí)行同一個(gè)命令, 并且記錄下每個(gè)變量的狀態(tài)。
競(jìng)爭(zhēng)檢測(cè)器基于C/C++的ThreadSanitizer 運(yùn)行時(shí)庫(kù),該庫(kù)在Google內(nèi)部代碼基地和Chromium找到許多錯(cuò)誤。這個(gè)技術(shù)在2012年九月集成到Go中,從那時(shí)開始,它已經(jīng)在標(biāo)準(zhǔn)庫(kù)中檢測(cè)到42個(gè)競(jìng)爭(zhēng)條件。現(xiàn)在,它已經(jīng)是我們持續(xù)構(gòu)建過(guò)程的一部分,當(dāng)競(jìng)爭(zhēng)條件出現(xiàn)時(shí),它會(huì)繼續(xù)捕捉到這些錯(cuò)誤。
競(jìng)爭(zhēng)檢測(cè)器已經(jīng)完全集成到Go工具鏈中,僅僅添加-race標(biāo)志到命令行就使用了檢測(cè)器。
$ go test -race mypkg // 測(cè)試包
$ go run -race mysrc.go // 編譯和運(yùn)行程序 $ go build -race mycmd
// 構(gòu)建程序 $ go install -race mypkg // 安裝程序48.在 range 迭代 slice 時(shí),你怎么修改值的
在 range 迭代中,得到的值其實(shí)是元素的一份值拷貝,更新拷貝并不會(huì)更改原來(lái)的元素,即是拷貝的地址并不是原有元素的地址。
func main() {
data := []int{1, 2, 3}
for _, v := range data {
v *= 10 // data 中原有元素是不會(huì)被修改的
}
fmt.Println("data: ", data) // data: [1 2 3]
}
如果要修改原有元素的值,應(yīng)該使用索引直接訪問(wèn)。
func main() {
data := []int{1, 2, 3}
for i, v := range data {
data[i] = v * 10
}
fmt.Println("data: ", data) // data: [10 20 30]
}
如果你的集合保存的是指向值的指針,需稍作修改。依舊需要使用索引訪問(wèn)元素,不過(guò)可以使用 range 出來(lái)的元素直接更新原有值。
func main() {
data := []*struct{ num int }{{1}, {2}, {3},}
for _, v := range data {
v.num *= 10 // 直接使用指針更新
}
fmt.Println(data[0], data[1], data[2]) // &{10} &{20} &{30}
}
49.nil interface 和 nil interface 的區(qū)別
func main() {
var data *byte
var in interface{}
fmt.Println(data, data == nil) // <nil> true
fmt.Println(in, in == nil) // <nil> true
in = data
fmt.Println(in, in == nil) // <nil> false // data 值為 nil,但 in 值不為 nil
}
// 正確示例
func main() {
doIt := func(arg int) interface{} {
var result *struct{} = nil
if arg > 0 {
result = &struct{}{}
} else {
return nil // 明確指明返回 nil
}
return result
}
if res := doIt(-1); res != nil {
fmt.Println("Good result: ", res)
} else {
fmt.Println("Bad result: ", res) // Bad result: <nil>
}
}
50.select可以用于什么
常用語(yǔ)gorotine的完美退出。
golang 的 select 就是監(jiān)聽 IO 操作,當(dāng) IO 操作發(fā)生時(shí),觸發(fā)相應(yīng)的動(dòng)作
每個(gè)case語(yǔ)句里必須是一個(gè)IO操作,確切的說(shuō),應(yīng)該是一個(gè)面向channel的IO操作。
結(jié)尾
本文總結(jié)了常見的 Go 語(yǔ)法問(wèn)題,且在工作和面試中都容易出錯(cuò)的。可以供大家學(xué)習(xí)和面試用。
Golang 的開源社區(qū)在不斷的壯大,是一門值得學(xué)習(xí)的語(yǔ)言。我公眾號(hào)有Golang 學(xué)習(xí)的全面資料,可以領(lǐng)取,祝大家學(xué)習(xí)愉快!
·················· END ··················
點(diǎn)擊關(guān)注公眾號(hào),免費(fèi)領(lǐng)學(xué)習(xí)資料
點(diǎn)“贊”和“在看”哦
