GoLang切片相關(guān)問題梳理講解
1.數(shù)組和切片有什么區(qū)別
Go語言中數(shù)組是固定長(zhǎng)度的,不能動(dòng)態(tài)擴(kuò)容,在編譯期就會(huì)確定大小,聲明方式如下:
var buffer [255]int
buffer := [255]int{0}
切片是對(duì)數(shù)組的抽象,因?yàn)閿?shù)組的長(zhǎng)度是不可變的,在某些場(chǎng)景下使用起來就不是很方便,所以Go語言提供了一種靈活,功能強(qiáng)悍的內(nèi)置類型切片(“動(dòng)態(tài)數(shù)組”),與數(shù)組相比切片的長(zhǎng)度是不固定的,可以追加元素。切片是一種數(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ò)容。
2.拷貝大切片一定比拷貝小切片代價(jià)大嗎
這道題本質(zhì)是考察對(duì)切片本質(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í),在進(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è)字段是切片的長(zhǎng)度,第三個(gè)字段是容量。將一個(gè)切片變量分配給另一個(gè)變量只會(huì)復(fù)制三個(gè)機(jī)器字,大切片跟小切片的區(qū)別無非就是 Len 和 Cap的值比小切片的這兩個(gè)值大一些,如果發(fā)生拷貝,本質(zhì)上就是拷貝上面的三個(gè)字段。
3.切片的深淺拷貝
深淺拷貝都是進(jìn)行復(fù)制,區(qū)別在于復(fù)制出來的新對(duì)象與原來的對(duì)象在它們發(fā)生改變時(shí),是否會(huì)相互影響,本質(zhì)區(qū)別就是復(fù)制出來的對(duì)象與原對(duì)象是否會(huì)指向同一個(gè)地址。在Go語言,切片拷貝有三種方式:
1.使用=操作符拷貝切片,這種就是淺拷貝
2.使用[:]下標(biāo)的方式復(fù)制切片,這種也是淺拷貝
3.使用Go語言的內(nèi)置函數(shù)copy()進(jìn)行切片拷貝,這種就是深拷貝
4.零切片 空切片 nil切片是什么
4.1零切片
我們把切片內(nèi)部數(shù)組的元素都是零值或者底層數(shù)組的內(nèi)容就全是 nil的切片叫做零切片,使用make創(chuàng)建的、長(zhǎng)度、容量都不為0的切片就是零值切片:
slice := make([]int,5) // 0 0 0 0 0 slice := make([]*int,5) // nil nil nil nil nil
4.2nil切片
nil切片的長(zhǎng)度和容量都為0,并且和nil比較的結(jié)果為true,采用直接創(chuàng)建切片的方式、new創(chuàng)建切片的方式都可以創(chuàng)建nil切片:
var slice []int var slice = *new([]int)
4.3空切片
空切片的長(zhǎng)度和容量也都為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
// 分配對(duì)象內(nèi)存
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
if size == 0 {
return unsafe.Pointer(&zerobase)
}
...
}
5.切片的擴(kuò)容策略
這個(gè)問題是一個(gè)高頻考點(diǎn),我們通過源碼來解析一下切片的擴(kuò)容策略,切片的擴(kuò)容都是調(diào)用growslice方法,截取部分重要源代碼:
1.17之前
代碼的擴(kuò)容策略可以簡(jiǎn)述為以下三個(gè)規(guī)則:
1.當(dāng)期望容量 > 兩倍的舊容量時(shí),直接使用期望容量作為新切片的容量
2.如果舊容量 < 1024(注意這里單位是元素個(gè)數(shù)),那么直接翻倍舊容量
3.如果舊容量 > 1024,那么會(huì)進(jìn)入一個(gè)循環(huán),每次增加25%直到大于期望容量
可以看到,原來的go對(duì)于切片擴(kuò)容后的容量判斷有一個(gè)明顯的magic number:1024,在1024之前,增長(zhǎng)的系數(shù)是2,而1024之后則變?yōu)?.25。關(guān)于為什么會(huì)這么設(shè)計(jì),社區(qū)的相關(guān)討論1給出了幾點(diǎn)理由:
1.如果只選擇翻倍的擴(kuò)容策略,那么對(duì)于較大的切片來說,現(xiàn)有的方法可以更好的節(jié)省內(nèi)存。
2.如果只選擇每次系數(shù)為1.25的擴(kuò)容策略,那么對(duì)于較小的切片來說擴(kuò)容會(huì)很低效。
3.之所以選擇一個(gè)小于2的系數(shù),在擴(kuò)容時(shí)被釋放的內(nèi)存塊會(huì)在下一次擴(kuò)容時(shí)更容易被重新利用
// 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
}
}
}
// 后半部分還對(duì) newcap 作了一個(gè)內(nèi)存對(duì)齊,這個(gè)和內(nèi)存分配策略相關(guān)。進(jìn)行內(nèi)存對(duì)齊之后,新 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)
}
}
1.18之后
到了Go1.18時(shí),又改成不和1024比較了,而是和256比較;并且擴(kuò)容的增量也有所變化,不再是每次擴(kuò)容1/4,如下代碼所示:
//1.18
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
const threshold = 256
if old.cap < threshold {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
newcap += (newcap + 3*threshold) / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}

在1.18中,優(yōu)化了切片擴(kuò)容的策略2,讓底層數(shù)組大小的增長(zhǎng)更加平滑:
通過減小閾值并固定增加一個(gè)常數(shù),使得優(yōu)化后的擴(kuò)容的系數(shù)在閾值前后不再會(huì)出現(xiàn)從2到1.25的突變,該commit作者給出了幾種原始容量下對(duì)應(yīng)的“擴(kuò)容系數(shù)”:

6. 參數(shù)傳遞切片和切片指針有什么區(qū)別
我們都知道切片底層就是一個(gè)結(jié)構(gòu)體,里面有三個(gè)元素:
分別表示切片底層數(shù)據(jù)的地址,切片長(zhǎng)度,切片容量。
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
當(dāng)切片作為參數(shù)傳遞時(shí),其實(shí)就是一個(gè)結(jié)構(gòu)體的傳遞,因?yàn)镚o語言參數(shù)傳遞只有值傳遞,傳遞一個(gè)切片就會(huì)淺拷貝原切片,但因?yàn)榈讓訑?shù)據(jù)的地址沒有變,所以在函數(shù)內(nèi)對(duì)切片的修改,也將會(huì)影響到函數(shù)外的切片,舉例:
func modifySlice(s []string) {
s[0] = "song"
s[1] = "Golang"
fmt.Println("out slice: ", s)
}
func main() {
s := []string{"asong", "Golang夢(mèng)工廠"}
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夢(mèng)工廠"}
appendSlice(s)
fmt.Println("inner slice: ", s)
}// 運(yùn)行結(jié)果
out slice: [asong Golang夢(mèng)工廠 快關(guān)注?。
inner slice: [asong Golang夢(mèng)工廠]
因?yàn)榍衅l(fā)生了擴(kuò)容,函數(shù)外的切片指向了一個(gè)新的底層數(shù)組,所以函數(shù)內(nèi)外不會(huì)相互影響,因此可以得出一個(gè)結(jié)論,當(dāng)參數(shù)直接傳遞切片時(shí),如果指向底層數(shù)組的指針被覆蓋或者修改(重分配、append觸發(fā)擴(kuò)容),此時(shí)函數(shù)內(nèi)部對(duì)數(shù)據(jù)的修改將不再影響到外部的切片,代表長(zhǎng)度的len和容量cap也均不會(huì)被修改
參數(shù)傳遞切片指針就很容易理解了,如果你想修改切片中元素的值,并且更改切片的容量和底層數(shù)組,則應(yīng)該按指針傳遞。
7.range遍歷切片有什么要注意的
Go語言提供了range關(guān)鍵字用于for 循環(huán)中迭代數(shù)組(array)、切片(slice)、通道(channel)或集合(map)的元素,有兩種使用方式:
for k,v := range _ { }
for k := range _ { }
第一種是遍歷下標(biāo)和對(duì)應(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]
k := index_temp
v := 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ì)對(duì)原切片有影響。
到此這篇關(guān)于GoLang切片相關(guān)問題梳理講解的文章就介紹到這了,更多相關(guān)GoLang切片內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用Gin框架搭建一個(gè)Go Web應(yīng)用程序的方法詳解
在本文中,我們將要實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 Web 應(yīng)用程序,通過 Gin 框架來搭建,主要支持用戶注冊(cè)和登錄,用戶可以通過注冊(cè)賬戶的方式創(chuàng)建自己的賬號(hào),并通過登錄功能進(jìn)行身份驗(yàn)證,感興趣的同學(xué)跟著小編一起來看看吧2023-08-08
Golang中的select語句及其應(yīng)用實(shí)例
本文將介紹Golang中的select語句的使用方法和作用,并通過代碼示例展示其在并發(fā)編程中的實(shí)際應(yīng)用,此外,還提供了一些與select相關(guān)的面試題,幫助讀者更好地理解和應(yīng)用select語句2023-12-12
golang實(shí)現(xiàn)http服務(wù)器處理靜態(tài)文件示例
這篇文章主要介紹了golang實(shí)現(xiàn)http服務(wù)器處理靜態(tài)文件的方法,涉及Go語言基于http協(xié)議處理文件的相關(guān)技巧,需要的朋友可以參考下2016-07-07
Windows10系統(tǒng)下安裝Go環(huán)境詳細(xì)步驟
Go語言是谷歌推出的一款全新的編程語言,可以在不損失應(yīng)用程序性能的情況下極大的降低代碼的復(fù)雜性,這篇文章主要給大家介紹了關(guān)于Windows10系統(tǒng)下安裝Go環(huán)境的詳細(xì)步驟,需要的朋友可以參考下2023-11-11
Golang等多種語言轉(zhuǎn)數(shù)組成字符串舉例詳解
今天寫代碼遇到數(shù)組轉(zhuǎn)換成字符串操作,下面這篇文章主要給大家介紹了關(guān)于Golang等多種語言轉(zhuǎn)數(shù)組成字符串的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-05-05

