Go語言切片??嫉拿嬖囌骖}解析
前言
哈嘍,大家好,我是asong。最近沒事在看八股文,總結(jié)了幾道常考的切片八股文,以問答的方式總結(jié)出來,希望對正在面試的你們有用~
本文題目不全,關(guān)于切片的面試真題還有哪些?歡迎評論區(qū)補(bǔ)充~
01. 數(shù)組和切片有什么區(qū)別?
Go語言中數(shù)組是固定長度的,不能動(dòng)態(tài)擴(kuò)容,在編譯期就會(huì)確定大小,聲明方式如下:
var buffer [255]int buffer := [255]int{0}
切片是對數(shù)組的抽象,因?yàn)閿?shù)組的長度是不可變的,在某些場景下使用起來就不是很方便,所以Go語言提供了一種靈活,功能強(qiáng)悍的內(nèi)置類型切片("動(dòng)態(tài)數(shù)組"),與數(shù)組相比切片的長度是不固定的,可以追加元素。切片是一種數(shù)據(jù)結(jié)構(gòu),切片不是數(shù)組,切片描述的是一塊數(shù)組,切片結(jié)構(gòu)如下:
我們可以直接聲明一個(gè)未指定大小的數(shù)組來定義切片,也可以使用make()函數(shù)來創(chuàng)建切片,聲明方式如下:
var slice []int // 直接聲明 slice := []int{1,2,3,4,5} // 字面量方式 slice := make([]int, 5, 10) // make創(chuàng)建 slice := array[1:5] // 截取下標(biāo)的方式 slice := *new([]int) // new一個(gè)
切片可以使用append追加元素,當(dāng)cap不足時(shí)進(jìn)行動(dòng)態(tài)擴(kuò)容。
02. 拷貝大切片一定比拷貝小切片代價(jià)大嗎?
這道題比較有意思,原文地址:Are large slices more expensive than smaller ones?
這道題本質(zhì)是考察對切片本質(zhì)的理解,Go語言中只有值傳遞,所以我們以傳遞切片為例子:
func main() { param1 := make([]int, 100) param2 := make([]int, 100000000) smallSlice(param1) largeSlice(param2) } func smallSlice(params []int) { // .... } func largeSlice(params []int) { // .... }
切片param2要比param1大1000000個(gè)數(shù)量級,在進(jìn)行值拷貝的時(shí)候,是否需要更昂貴的操作呢?
實(shí)際上不會(huì),因?yàn)榍衅举|(zhì)內(nèi)部結(jié)構(gòu)如下:
type SliceHeader struct { Data uintptr Len int Cap int }
切片中的第一個(gè)字是指向切片底層數(shù)組的指針,這是切片的存儲(chǔ)空間,第二個(gè)字段是切片的長度,第三個(gè)字段是容量。將一個(gè)切片變量分配給另一個(gè)變量只會(huì)復(fù)制三個(gè)機(jī)器字,大切片跟小切片的區(qū)別無非就是Len 和 Cap的值比小切片的這兩個(gè)值大一些,如果發(fā)生拷貝,本質(zhì)上就是拷貝上面的三個(gè)字段。
03. 切片的深淺拷貝
深淺拷貝都是進(jìn)行復(fù)制,區(qū)別在于復(fù)制出來的新對象與原來的對象在它們發(fā)生改變時(shí),是否會(huì)相互影響,本質(zhì)區(qū)別就是復(fù)制出來的對象與原對象是否會(huì)指向同一個(gè)地址。在Go語言,切片拷貝有三種方式:
使用=操作符拷貝切片,這種就是淺拷貝
使用[:]下標(biāo)的方式復(fù)制切片,這種也是淺拷貝
使用Go語言的內(nèi)置函數(shù)copy()進(jìn)行切片拷貝,這種就是深拷貝,
04. 零切片、空切片、nil切片是什么
為什么問題中這么多種切片呢?因?yàn)樵贕o語言中切片的創(chuàng)建方式有五種,不同方式創(chuàng)建出來的切片也不一樣;
- 零切片
我們把切片內(nèi)部數(shù)組的元素都是零值或者底層數(shù)組的內(nèi)容就全是 nil的切片叫做零切片,使用make創(chuàng)建的、長度、容量都不為0的切片就是零值切片:
slice := make([]int,5) // 0 0 0 0 0 slice := make([]*int,5) // nil nil nil nil nil
- nil切片
nil切片的長度和容量都為0,并且和nil比較的結(jié)果為true,采用直接創(chuàng)建切片的方式、new創(chuàng)建切片的方式都可以創(chuàng)建nil切片:
var slice []int var slice = *new([]int)
- 空切片
空切片的長度和容量也都為0,但是和nil的比較結(jié)果為false,因?yàn)樗械目涨衅臄?shù)據(jù)指針都指向同一個(gè)地址 0xc42003bda0;使用字面量、make可以創(chuàng)建空切片:
var slice = []int{} var slice = make([]int, 0)
空切片指向的 zerobase 內(nèi)存地址是一個(gè)神奇的地址,從 Go 語言的源代碼中可以看到它的定義:
// base address for all 0-byte allocations var zerobase uintptr // 分配對象內(nèi)存 func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ... if size == 0 { return unsafe.Pointer(&zerobase) } ... }
05. 切片的擴(kuò)容策略
這個(gè)問題是一個(gè)高頻考點(diǎn),我們通過源碼來解析一下切片的擴(kuò)容策略,切片的擴(kuò)容都是調(diào)用growslice方法,截取部分重要源代碼:
// runtime/slice.go // et:表示slice的一個(gè)元素;old:表示舊的slice;cap:表示新切片需要的容量; func growslice(et *_type, old slice, cap int) slice { if cap < old.cap { panic(errorString("growslice: cap out of range")) } if et.size == 0 { // append should not create a slice with nil pointer but non-zero len. // We assume that append doesn't need to preserve old.array in this case. return slice{unsafe.Pointer(&zerobase), old.len, cap} } newcap := old.cap // 兩倍擴(kuò)容 doublecap := newcap + newcap // 新切片需要的容量大于兩倍擴(kuò)容的容量,則直接按照新切片需要的容量擴(kuò)容 if cap > doublecap { newcap = cap } else { // 原 slice 容量小于 1024 的時(shí)候,新 slice 容量按2倍擴(kuò)容 if old.cap < 1024 { newcap = doublecap } else { // 原 slice 容量超過 1024,新 slice 容量變成原來的1.25倍。 // Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap { newcap += newcap / 4 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } } // 后半部分還對 newcap 作了一個(gè)內(nèi)存對齊,這個(gè)和內(nèi)存分配策略相關(guān)。進(jìn)行內(nèi)存對齊之后,新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍。 var overflow bool var lenmem, newlenmem, capmem uintptr // Specialize for common values of et.size. // For 1 we don't need any division/multiplication. // For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant. // For powers of 2, use a variable shift. switch { case et.size == 1: lenmem = uintptr(old.len) newlenmem = uintptr(cap) capmem = roundupsize(uintptr(newcap)) overflow = uintptr(newcap) > maxAlloc newcap = int(capmem) case et.size == sys.PtrSize: lenmem = uintptr(old.len) * sys.PtrSize newlenmem = uintptr(cap) * sys.PtrSize capmem = roundupsize(uintptr(newcap) * sys.PtrSize) overflow = uintptr(newcap) > maxAlloc/sys.PtrSize newcap = int(capmem / sys.PtrSize) case isPowerOfTwo(et.size): var shift uintptr if sys.PtrSize == 8 { // Mask shift for better code generation. shift = uintptr(sys.Ctz64(uint64(et.size))) & 63 } else { shift = uintptr(sys.Ctz32(uint32(et.size))) & 31 } lenmem = uintptr(old.len) << shift newlenmem = uintptr(cap) << shift capmem = roundupsize(uintptr(newcap) << shift) overflow = uintptr(newcap) > (maxAlloc >> shift) newcap = int(capmem >> shift) default: lenmem = uintptr(old.len) * et.size newlenmem = uintptr(cap) * et.size capmem, overflow = math.MulUintptr(et.size, uintptr(newcap)) capmem = roundupsize(capmem) newcap = int(capmem / et.size) } }
通過源代碼可以總結(jié)切片擴(kuò)容策略:
切片在擴(kuò)容時(shí)會(huì)進(jìn)行內(nèi)存對齊,這個(gè)和內(nèi)存分配策略相關(guān)。進(jìn)行內(nèi)存對齊之后,新 slice 的容量是要 大于等于老 slice 容量的 2倍或者1.25倍,當(dāng)原 slice 容量小于 1024 的時(shí)候,新 slice 容量變成原來的 2 倍;原 slice 容量超過 1024,新 slice 容量變成原來的1.25倍。
07. 參數(shù)傳遞切片和切片指針有什么區(qū)別?
我們都知道切片底層就是一個(gè)結(jié)構(gòu)體,里面有三個(gè)元素:
type SliceHeader struct { Data uintptr Len int Cap int }
分別表示切片底層數(shù)據(jù)的地址,切片長度,切片容量。
當(dāng)切片作為參數(shù)傳遞時(shí),其實(shí)就是一個(gè)結(jié)構(gòu)體的傳遞,因?yàn)镚o語言參數(shù)傳遞只有值傳遞,傳遞一個(gè)切片就會(huì)淺拷貝原切片,但因?yàn)榈讓訑?shù)據(jù)的地址沒有變,所以在函數(shù)內(nèi)對切片的修改,也將會(huì)影響到函數(shù)外的切片,舉例:
func modifySlice(s []string) { s[0] = "song" s[1] = "Golang" fmt.Println("out slice: ", s) } func main() { s := []string{"asong", "Golang夢工廠"} modifySlice(s) fmt.Println("inner slice: ", s) } // 運(yùn)行結(jié)果 out slice: [song Golang] inner slice: [song Golang]
不過這也有一個(gè)特例,先看一個(gè)例子:
func appendSlice(s []string) { s = append(s, "快關(guān)注?。?) fmt.Println("out slice: ", s) } func main() { s := []string{"asong", "Golang夢工廠"} appendSlice(s) fmt.Println("inner slice: ", s) } // 運(yùn)行結(jié)果 out slice: [asong Golang夢工廠 快關(guān)注?。 inner slice: [asong Golang夢工廠]
因?yàn)榍衅l(fā)生了擴(kuò)容,函數(shù)外的切片指向了一個(gè)新的底層數(shù)組,所以函數(shù)內(nèi)外不會(huì)相互影響,因此可以得出一個(gè)結(jié)論,當(dāng)參數(shù)直接傳遞切片時(shí),如果指向底層數(shù)組的指針被覆蓋或者修改(copy、重分配、append觸發(fā)擴(kuò)容),此時(shí)函數(shù)內(nèi)部對數(shù)據(jù)的修改將不再影響到外部的切片,代表長度的len和容量cap也均不會(huì)被修改。
參數(shù)傳遞切片指針就很容易理解了,如果你想修改切片中元素的值,并且更改切片的容量和底層數(shù)組,則應(yīng)該按指針傳遞。
08. range遍歷切片有什么要注意的?
Go語言提供了range關(guān)鍵字用于for 循環(huán)中迭代數(shù)組(array)、切片(slice)、通道(channel)或集合(map)的元素,有兩種使用方式:
for k,v := range _ { } for k := range _ { }
第一種是遍歷下標(biāo)和對應(yīng)值,第二種是只遍歷下標(biāo),使用range遍歷切片時(shí)會(huì)先拷貝一份,然后在遍歷拷貝數(shù)據(jù):
s := []int{1, 2} for k, v := range s { } 會(huì)被編譯器認(rèn)為是 for_temp := s len_temp := len(for_temp) for index_temp := 0; index_temp < len_temp; index_temp++ { value_temp := for_temp[index_temp] _ = index_temp value := value_temp }
不知道這個(gè)知識(shí)點(diǎn)的情況下很容易踩坑,例如下面這個(gè)例子:
package main import ( "fmt" ) type user struct { name string age uint64 } func main() { u := []user{ {"asong",23}, {"song",19}, {"asong2020",18}, } for _,v := range u{ if v.age != 18{ v.age = 20 } } fmt.Println(u) } // 運(yùn)行結(jié)果 [{asong 23} {song 19} {asong2020 18}]
因?yàn)槭褂胷ange遍歷切片u,變量v是拷貝切片中的數(shù)據(jù),修改拷貝數(shù)據(jù)不會(huì)對原切片有影響。
之前寫了一個(gè)對for-range踩坑總結(jié),可以讀一下:面試官:go中for-range使用過嗎?這幾個(gè)問題你能解釋一下原因嗎?
總結(jié)
本文總結(jié)了8道切片相關(guān)的面試真題,切片一直是面試中的重要考點(diǎn),把本文這幾個(gè)知識(shí)點(diǎn)弄會(huì),應(yīng)對面試官就會(huì)變的輕松自如。
到此這篇關(guān)于Go語言切片面試??嫉奈恼戮徒榻B到這了,更多相關(guān)Go語言切片面試內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Go如何基于現(xiàn)有的context創(chuàng)建新的context
在?Golang?中,context?包提供了創(chuàng)建和管理上下文的功能,那么在GO語言中如何基于現(xiàn)有的context創(chuàng)建新的context,下面小編就來和大家詳細(xì)聊聊2024-01-01GoFrame框架gcache的緩存控制淘汰策略實(shí)踐示例
這篇文章主要為大家介紹了GoFrame框架gcache的緩存控制淘汰策略的實(shí)踐示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06golang語言如何將interface轉(zhuǎn)為int, string,slice,struct等類型
這篇文章主要介紹了golang語言如何將interface轉(zhuǎn)為int, string,slice,struct等類型,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12詳解golang中?work與?module?的區(qū)別與聯(lián)系
Go?模塊通常由一個(gè)項(xiàng)目或庫組成,并包含一組隨后一起發(fā)布的?Go?包,Go?模塊通過允許用戶將項(xiàng)目代碼放在他們選擇的目錄中并為每個(gè)模塊指定依賴項(xiàng)的版本,解決了原始系統(tǒng)的許多問題,本文將給大家介紹一下golang中?work與?module?的區(qū)別與聯(lián)系,需要的朋友可以參考下2023-09-09goland把go項(xiàng)目打包進(jìn)docker鏡像的全過程記錄
golang編譯的應(yīng)用是不需要依賴其他運(yùn)行環(huán)境的,下面這篇文章主要給大家介紹了關(guān)于goland把go項(xiàng)目打包進(jìn)docker鏡像的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2022-08-08goland遠(yuǎn)程調(diào)試k8s上容器的實(shí)現(xiàn)
本文主要介紹了goland遠(yuǎn)程調(diào)試k8s上容器的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02