一文總結(jié)Go語言切片核心知識點和坑
一. 切片結(jié)構(gòu)說明
切片(slice),是Go語言中對可變數(shù)組的抽象,相較于數(shù)組,具有可動態(tài)追加元素,可動態(tài)擴容等更加實用的特性。切片在Go語言中對應的結(jié)構(gòu)體源碼如下。
type slice struct {
array unsafe.Pointer
len int
cap int
}字段含義說明如下。
- array,指向數(shù)組內(nèi)存地址的指針。切片底層存儲數(shù)據(jù)的結(jié)構(gòu)就是數(shù)組,切片使用一個指向數(shù)組的指針來操作這個數(shù)組;
- len,切片的長度。len表示切片中可用的元素個數(shù),所謂可用,就是內(nèi)存空間被分配,并且通過當前切片能夠訪問;
- cap,切片的容量。cap表示切片最大的元素個數(shù),通常cap大于等于len,如果cap大于len,表示當前切片有部分元素不可用,而不可用的意思就是內(nèi)存空間被分配,但是當前切片無法訪問,訪問這些不可用元素會導致panic。
一個切片的示意圖如下。

二. 切片創(chuàng)建
切片的創(chuàng)建有多種方式,本節(jié)結(jié)合示例和圖示,對切片的不同創(chuàng)建方式進行說明。
1. 直接創(chuàng)建切片
示例代碼如下所示。
// 直接創(chuàng)建切片
func directlyCreate() {
slice := []int{1, 2, 3, 4, 5, 6, 7}
fmt.Printf("len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p\n",
len(slice), cap(slice), &slice, slice)
}上述示例代碼運行打印如下。
len=7, cap=7, sliceAddress=0xc000004078, sliceArrayAddress=0xc000014240
此時創(chuàng)建出來的切片對應圖示如下。

直接創(chuàng)建切片時,會為切片的底層數(shù)組開辟內(nèi)存空間并使用指定的元素對數(shù)組完成初始化,且創(chuàng)建出來的切片的len等于cap等于初始化元素的個數(shù)。
2. 從整個數(shù)組切得到切片
切片,顧名思義,其實就是可以從已經(jīng)存在的數(shù)組上切一段作為切片,本小節(jié)演示直接切整個數(shù)組得到切片,示例代碼如下所示。
// 從整個數(shù)組切得到切片
func sliceFromWholeArray() {
originArray := [7]int{0, 1, 2, 3, 4, 5, 6}
slice := originArray[:]
fmt.Printf("originArrayAddress=%p\n", &originArray)
fmt.Printf("len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p\n",
len(slice), cap(slice), &slice, slice)
}上述示例代碼運行打印如下。
originArrayAddress=0xc000014240 len=7, cap=7, sliceAddress=0xc000004078, sliceArrayAddress=0xc000014240
此時創(chuàng)建出來的切片對應圖示如下。

從整個數(shù)組切,實際就是切片直接使用了這個數(shù)組作為底層的數(shù)組。
3. 從前到后切數(shù)組得到切片
本小節(jié)演示從前到后切數(shù)組得到切片,示例代碼如下所示。
// 從前到后切數(shù)組得到切片
func sliceFromArrayFrontToBack() {
originArray := [7]int{0, 1, 2, 3, 4, 5, 6}
slice := originArray[:4]
fmt.Printf("origin array address is: [%p]\n", &originArray)
fmt.Printf("len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p\n",
len(slice), cap(slice), &slice, slice)
}上述在切數(shù)組時,沒有指定數(shù)組的開始索引,表示從索引0開始切(inclusive),指定了數(shù)組的結(jié)束索引,表示切到結(jié)束索引的位置(exclusive),運行示例代碼,打印如下。
originArrayAddress=0xc000014240 len=4, cap=7, sliceAddress=0xc000004078, sliceArrayAddress=0xc000014240
此時創(chuàng)建出來的切片對應圖示如下。

從前到后切數(shù)組得到的切片,len等于切的范圍的長度,對應示例中索引0(inclusive)到索引4(exclusive)的長度4,而cap等于切的開始位置(inclusive)到數(shù)組末尾(inclusive)的長度7。
4. 從數(shù)組中間切到最后得到切片
本小節(jié)演示從數(shù)組中間切到最后得到切片,示例代碼如下所示。
// 從數(shù)組中間切到最后得到切片
func sliceFromArrayMiddleToLast() {
originArray := [7]int{0, 1, 2, 3, 4, 5, 6}
slice := originArray[4:]
fmt.Printf("originArrayAddress=%p\n", &originArray)
fmt.Printf("len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p\n",
len(slice), cap(slice), &slice, slice)
}上述在切數(shù)組時,指定了數(shù)組的開始索引,表示從索引4(inclusive)開始切,沒有指定數(shù)組的結(jié)束索引,表示切到數(shù)組的末尾(inclusive),運行示例代碼,打印如下。
originArrayAddress=0xc000014240 len=3, cap=3, sliceAddress=0xc000004078, sliceArrayAddress=0xc000014260
此時創(chuàng)建出來的切片對應圖示如下。

從數(shù)組中間切到最后得到的切片,len等于cap等于切的范圍的長度,對應示例中索引4(inclusive)到數(shù)組末尾(inclusive)的長度3。并且由上述圖示可以看出,切片使用的底層數(shù)組其實還是被切的數(shù)組,只不過使用的是被切數(shù)組的一部分。
5. 從數(shù)組切一段得到切片
本小節(jié)演示從數(shù)組切一段得到切片,示例代碼如下所示。
// 從數(shù)組切一段得到切片
func sliceFromSelectionOfArray() {
originArray := [7]int{0, 1, 2, 3, 4, 5, 6}
slice := originArray[2:4]
fmt.Printf("originArrayAddress=%p\n", &originArray)
fmt.Printf("len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p\n",
len(slice), cap(slice), &slice, slice)
}上述在切數(shù)組時,指定了數(shù)組的開始索引,表示從索引2(inclusive)開始切,也指定了數(shù)組的結(jié)束索引,表示切到數(shù)組的索引4的位置(exclusive),運行示例代碼,打印如下。
originArrayAddress=0xc000014240 len=2, cap=5, sliceAddress=0xc000004078, sliceArrayAddress=0xc000014250
此時創(chuàng)建出來的切片對應圖示如下。

從數(shù)組切一段得到的切片,len等于切的范圍的長度,對應示例中索引2(inclusive)到索引4(exclusive)的長度2,cap等于切的開始位置(inclusive)到數(shù)組末尾(inclusive)的長度5。并且,切片使用的底層數(shù)組還是被切數(shù)組的一部分。
6. 從切片切得到切片
除了切數(shù)組得到切片,還能切切片來得到切片,示例代碼如下所示。
// 從切片切得到切片
func sliceFromSlice() {
originArray := [7]int{0, 1, 2, 3, 4, 5, 6}
originSlice := originArray[:]
derivedSlice := originSlice[2:4]
fmt.Printf("originArrayAddress=%p\n", &originArray)
fmt.Printf("originSlice: len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p\n",
len(originSlice), cap(originSlice), &originSlice, originSlice)
fmt.Printf("derivedSlice: len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p\n",
len(derivedSlice), cap(derivedSlice), &derivedSlice, derivedSlice)
}上述示例代碼中,originSlice是切數(shù)組originArray得到的切片,derivedSlice是切切片originSlice得到的切片,運行示例代碼,打印如下。
originArrayAddress=0xc000014240 originSlice: len=7, cap=7, sliceAddress=0xc000004078, sliceArrayAddress=0xc000014240 derivedSlice: len=2, cap=5, sliceAddress=0xc000004090, sliceArrayAddress=0xc000014250
此時創(chuàng)建出來的切片對應圖示如下。

從切片切得到切片后,兩個切片會使用同一個底層數(shù)組,區(qū)別就是可能使用的是底層數(shù)組的不同區(qū)域,因此如果其中一個切片更改了數(shù)據(jù),而這個數(shù)據(jù)恰好另一個切片可用訪問,那么另一個切片訪問該數(shù)據(jù)時就會發(fā)現(xiàn)數(shù)據(jù)發(fā)生了更改。但是請注意,雖然兩個切片使用同一個底層數(shù)組,但是切片的len和cap都是獨立的,也就是假如其中一個切片通過類似于append() 函數(shù)導致len或者cap發(fā)生了更改,此時另一個切片的len或者cap是不會受影響的。
7. 使用make函數(shù)得到切片
make() 函數(shù)專門用于為slice,map和chan這三種引用類型分配內(nèi)存并完成初始化,make() 函數(shù)返回的就是引用類型對應的底層結(jié)構(gòu)體本身,使用make() 函數(shù)創(chuàng)建slice的示例代碼如下所示。
// 使用make函數(shù)得到切片
func initializeByMake() {
slice := make([]int, 5, 7)
fmt.Printf("len=%d, cap=%d, slice=%v\n",
len(slice), cap(slice), slice)
}上述示例代碼中,會使用make() 函數(shù)創(chuàng)建一個int類型的切片,并指定len為5(第二個參數(shù)指定),cap為7(第三個參數(shù)指定),其中可以不指定cap,此時cap會取值為len。運行示例代碼,打印如下。
len=5, cap=7, slice=[0 0 0 0 0]
此時訪問索引5或索引6的元素,會引發(fā)panic,示例代碼修改如下。
// 使用make函數(shù)得到切片
func initializeByMake() {
slice := make([]int, 5, 7)
fmt.Printf("len=%d, cap=%d, slice=%v\n",
len(slice), cap(slice), slice)
// 訪問索引5或索引6會引發(fā)panic
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
fmt.Println(slice[5])
}運行示例代碼,打印如下。
len=5, cap=7, slice=[0 0 0 0 0] runtime error: index out of range [5] with length 5
那么索引5和索引6的元素怎么才能使用呢,就需要使用到內(nèi)建函數(shù)append(),示例代碼修改如下。
// 使用make函數(shù)得到切片
func initializeByMake() {
slice := make([]int, 5, 7)
fmt.Printf("len=%d, cap=%d, slice=%v\n",
len(slice), cap(slice), slice)
slice = append(slice, 1)
slice = append(slice, 2)
fmt.Printf("len=%d, cap=%d, slice=%v\n",
len(slice), cap(slice), slice)
}運行示例代碼,打印如下。
len=5, cap=7, slice=[0 0 0 0 0]
len=7, cap=7, slice=[0 0 0 0 0 1 2]
append() 函數(shù)的使用,會在本文后面章節(jié)進行說明。
三. 聲明切片占用內(nèi)存分析
使用如下方式聲明切片
var slice []int
然后使用如下方式打印僅聲明的切片占用的內(nèi)存大小。
fmt.Printf("memory=%d\n", unsafe.Sizeof(slice))占用內(nèi)存大小打印如下。
memory=24
也就是僅聲明的切片會占用24字節(jié)的內(nèi)存大小,這個大小實際就是切片底層結(jié)構(gòu)體占用的大小,如下所示。
type slice struct {
array unsafe.Pointer // 64位操作系統(tǒng)下占8字節(jié)
len int // 64位操作系統(tǒng)下占8字節(jié)
cap int // 64位操作系統(tǒng)下占8字節(jié)
}四. append函數(shù)
append() 函數(shù)用于將元素附加到切片的末尾,比如一個切片len為5,cap為7,此時可以使用append() 函數(shù)附加一個元素到切片的末尾使得切片的len變?yōu)?(cap此時不會改變),從而切片索引為5的位置的元素也可以讀和寫了。本節(jié)將對append() 函數(shù)的使用進行說明。
1. 基本使用
append() 函數(shù)聲明如下所示。
func append(slice []Type, elems ...Type) []Type
append() 函數(shù)會將附加了元素后的切片返回,所以我們需要用切片變量來存儲append() 函數(shù)的返回值。
append() 函數(shù)的基本使用示例如下所示。
// 向切片附加元素
func appendItem() {
slice := make([]int, 2, 7)
fmt.Printf("len=%d, cap=%d, slice=%v\n",
len(slice), cap(slice), slice)
slice = append(slice, 10)
fmt.Printf("len=%d, cap=%d, slice=%v\n",
len(slice), cap(slice), slice)
}運行示例代碼,打印如下。
len=2, cap=7, slice=[0 0]
len=3, cap=7, slice=[0 0 10]
對應圖示如下所示。

2. 觸發(fā)擴容
已知切片有len和cap,len表示可以讀和寫的元素個數(shù),cap表示當前切片最大的元素個數(shù),那么cap減去len就是當前切片還沒有使用的元素個數(shù),這部分元素需要通過append() 函數(shù)來附加到切片上。
當附加元素到切片上后,會讓len加1,但是如果附加元素到切片之前len已經(jīng)等于cap,那么此時會先觸發(fā)擴容再附加元素,擴容的流程簡圖如下所示。

一旦觸發(fā)擴容,會創(chuàng)建新容量大小的數(shù)組,然后將老數(shù)組的數(shù)據(jù)拷貝到新數(shù)組上,再然后將附加元素添加到新數(shù)組中,最后切片的array指向新數(shù)組。也就是說,切片擴容會導致切片使用的底層數(shù)組地址發(fā)生變更,如下是示例代碼。
// 擴容導致切片數(shù)組地址變更
func expansionCausedArrayAddressChange() {
// 原始數(shù)組
originArray := [7]int{0, 1, 2, 3, 4, 5, 6}
// 原始切片
originSlice := originArray[0:6]
// 打印原始切片和原始數(shù)組的信息
fmt.Printf("len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p, originArrayAddress=%p\n",
len(originSlice), cap(originSlice), &originSlice, originSlice, &originArray)
// 第一次append不會觸發(fā)擴容
firstAppendSlice := append(originSlice, 7)
// 打印第一次Append后的切片和原始數(shù)組的信息
fmt.Printf("len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p, originArrayAddress=%p\n",
len(firstAppendSlice), cap(firstAppendSlice), &firstAppendSlice, firstAppendSlice, &originArray)
// 第二次append會觸發(fā)擴容
secondAppendSlice := append(firstAppendSlice, 8)
// 打印第二次Append后的切片和原始數(shù)組的信息
fmt.Printf("len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p, originArrayAddress=%p\n",
len(secondAppendSlice), cap(secondAppendSlice), &secondAppendSlice, secondAppendSlice, &originArray)
}運行示例代碼,打印如下。
len=6, cap=7, sliceAddress=0xc000004078, sliceArrayAddress=0xc000014240, originArrayAddress=0xc000014240
len=7, cap=7, sliceAddress=0xc0000040a8, sliceArrayAddress=0xc000014240, originArrayAddress=0xc000014240
len=8, cap=14, sliceAddress=0xc0000040d8, sliceArrayAddress=0xc0000100e0, originArrayAddress=0xc000014240
在示例代碼中,切數(shù)組originArray得到的切片如下所示。

第一次append元素后,切片如下所示。

第二次append元素時,會觸發(fā)擴容,擴容后的切片如下所示。

可見,擴容后切片使用了另外一個數(shù)組作為了底層數(shù)組。
五. 切片注意事項
1. 問題演示
如下是一個踩坑的反面案例,代碼如下所示。
// 將slice作為參數(shù)傳遞到函數(shù)中
func appendTriggerExpansionCausingProblems() {
outerSlice := make([]int, 6, 7)
fmt.Printf("OuterSlice: len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p\n",
len(outerSlice), cap(outerSlice), &outerSlice, outerSlice)
helpAppendNums(outerSlice)
fmt.Printf("OuterSlice: len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p\n",
len(outerSlice), cap(outerSlice), &outerSlice, outerSlice)
}
func helpAppendNums(innerSlice []int) {
for i := 0; i < 3; i++ {
innerSlice = append(innerSlice, i)
}
fmt.Printf("InnerSlice: len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p\n",
len(innerSlice), cap(innerSlice), &innerSlice, innerSlice)
}運行示例代碼,打印如下。
OuterSlice: len=6, cap=7, sliceAddress=0xc000004078, sliceArrayAddress=0xc000014240
InnerSlice: len=9, cap=14, sliceAddress=0xc0000040a8, sliceArrayAddress=0xc0000100e0
OuterSlice: len=6, cap=7, sliceAddress=0xc000004078, sliceArrayAddress=0xc000014240
反面案例的代碼實現(xiàn)中,把一個len為6,cap為7的切片outterSlice作為參數(shù)傳遞到了一個helpAppendNums() 函數(shù)中,并在這個函數(shù)中進行了3次append元素的操作,然后發(fā)現(xiàn)outterSlice的len,cap和底層數(shù)組的地址都沒有發(fā)生改變。
下面將結(jié)合圖示,對反面案例代碼整個流程進行說明。
outerSlice創(chuàng)建出來時,outerSlice結(jié)構(gòu)如下所示。

因為Go語言中都是值傳遞,且切片變量本質(zhì)就是一個結(jié)構(gòu)體,所以把outerSlice作為實參調(diào)用helpAppendNums() 函數(shù)時,會把outerSlice值拷貝給到helpAppendNums() 函數(shù)的形參innerSlice,此時兩個切片的結(jié)構(gòu)如下所示。

這里其實就是一個容易踩坑特別是Go語言新手(我)容易踩坑的地方,因為在學習Go中的數(shù)據(jù)類型時,各種資料總是會告訴我在Go語言中有值類型和引用類型的區(qū)分,而引用類型就是slice切片,map字典,interface接口,func函數(shù)和chan管道,此時如果這個Go語言新手(我)還會一點Java,那么就會認為Go里面的slice和Java中的List一樣,畢竟都是引用嘛,那么把一個引用類型的slice傳遞到一個函數(shù)中,當然在函數(shù)中操作的slice就應該是傳遞的這個slice嘛。
上面用刪除線劃掉的部分,如果你真的有這樣的認識,要么就是對Go的引用類型理解得有偏差(我),要么就是對Java中聲明一個指向?qū)ο蟮淖兞繒r的內(nèi)存結(jié)構(gòu)沒有清晰的認識(我),或者都是(我)。先說Java中聲明一個指向?qū)ο蟮淖兞?,就像下面這樣。
List list = new ArrayList();
那么按照我們的正常認知,上面的list是一個引用對吧,但是這里的list實際是一個指向堆上一個ArrayList對象的指針,存在于線程棧幀的局部變量表里,我們無論如何傳遞list變量(Java中也是值傳遞),實際都是傳遞的指向堆上ArrayList對象的指針,最終操作的都是堆上的ArrayList對象。
回到Go中的切片,我們聲明并創(chuàng)建一個切片,就像下面這樣。
slice := []int{1, 2, 3, 4, 5, 6, 7}上面的slice字段就是一個切片結(jié)構(gòu)體,那么把這個slice字段傳遞到函數(shù)中時,是會直接拷貝這個切片給到函數(shù)的形參,此時實參和形參實際是兩個不同的切片,在內(nèi)存中有自己的空間和地址,只不過底層使用的是同一個數(shù)組而已,這里的拷貝,也稱作淺拷貝。
上面的坑踩完后,再回到反面案例代碼中,在helpAppendNums() 函數(shù)中會執(zhí)行3次append元素的操作,在執(zhí)行完第一次append操作后,outerSlice和innerSlice的結(jié)構(gòu)如下所示。

執(zhí)行完第一次append操作后,對于innerSlice來說len已經(jīng)等于cap了,又因為append的操作目標是innerSlice,所以盡管底層數(shù)組數(shù)據(jù)發(fā)生了變更,但outerSlice的len是沒有發(fā)生變動的,并且也無法訪問索引為6的元素。
執(zhí)行完第二次append操作后,outerSlice和innerSlice的結(jié)構(gòu)如下所示。

因為第一次append后,innerSlice的len已經(jīng)和cap相等,所以第二次append時,innerSlice使用的底層數(shù)組是一個新的且容量翻倍的數(shù)組,那么從這時起,outerSlice和innerSlice使用的底層數(shù)組也不同了。
執(zhí)行完第三次append操作后,outerSlice和innerSlice的結(jié)構(gòu)如下所示。

2. 解決方式
既然把切片直接傳到函數(shù)中存在一些坑,那么相應的就需要一些手段來解決。
第一種解決方式就是不直接傳遞切片,而是傳遞切片的指針,改進代碼如下所示。
// 將slice作為參數(shù)傳遞到函數(shù)中
func appendTriggerExpansionCausingProblems() {
outerSlice := make([]int, 6, 7)
fmt.Printf("OuterSlice: len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p\n",
len(outerSlice), cap(outerSlice), &outerSlice, outerSlice)
helpAppendNumsUsePointer(&outerSlice)
fmt.Printf("OuterSlice: len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p\n",
len(outerSlice), cap(outerSlice), &outerSlice, outerSlice)
}
func helpAppendNumsUsePointer(innerSlice *[]int) {
for i := 0; i < 3; i++ {
*innerSlice = append(*innerSlice, i)
}
fmt.Printf("InnerSlice: len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p\n",
len(*innerSlice), cap(*innerSlice), innerSlice, *innerSlice)
}運行改進代碼,打印如下。
OuterSlice: len=6, cap=7, sliceAddress=0xc000004078, sliceArrayAddress=0xc000014200
InnerSlice: len=9, cap=14, sliceAddress=0xc000004078, sliceArrayAddress=0xc0000100e0
OuterSlice: len=9, cap=14, sliceAddress=0xc000004078, sliceArrayAddress=0xc0000100e0
這種方式的好處從內(nèi)存的角度來說,僅進行了一次指針的值傳遞,對內(nèi)存更友好。
第二種解決方式就是在函數(shù)中將處理后的切片返回,改進代碼如下所示。
// 將slice作為參數(shù)傳遞到函數(shù)中
func appendTriggerExpansionCausingProblems() {
outerSlice := make([]int, 6, 7)
fmt.Printf("OuterSlice: len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p\n",
len(outerSlice), cap(outerSlice), &outerSlice, outerSlice)
outerSlice = helpAppendNumsAndReturn(outerSlice)
fmt.Printf("OuterSlice: len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p\n",
len(outerSlice), cap(outerSlice), &outerSlice, outerSlice)
}
func helpAppendNumsAndReturn(innerSlice []int) []int {
for i := 0; i < 3; i++ {
innerSlice = append(innerSlice, i)
}
fmt.Printf("InnerSlice: len=%d, cap=%d, sliceAddress=%p, sliceArrayAddress=%p\n",
len(innerSlice), cap(innerSlice), &innerSlice, innerSlice)
return innerSlice
}運行改進代碼,打印如下。
OuterSlice: len=6, cap=7, sliceAddress=0xc000004078, sliceArrayAddress=0xc000014200
InnerSlice: len=9, cap=14, sliceAddress=0xc0000040a8, sliceArrayAddress=0xc0000100e0
OuterSlice: len=9, cap=14, sliceAddress=0xc000004078, sliceArrayAddress=0xc0000100e0
相較于第一種方式,由指針的值傳遞變更為了結(jié)構(gòu)體的值傳遞,內(nèi)存相對不友好。
總結(jié)
一圖流。

后記
切片的結(jié)構(gòu)簡潔明了,名字形象生動,使用靈活方便,完美符合Go語言的設計原則,但是為啥在我手里就用出了這么多簍子呢。
我一再反思,發(fā)現(xiàn)根因還是對于Go語言中的引用類型不夠理解,以及缺失對切片的底層源碼實現(xiàn)的了解,所以本文只能從現(xiàn)象入手討論切片的一些淺層次的使用,后續(xù)還是要深入源碼,結(jié)合Go語言的內(nèi)存模型詳細學習切片。
以上就是一文總結(jié)Go語言切片核心知識點和坑的詳細內(nèi)容,更多關(guān)于Go語言切片知識點和坑的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang 經(jīng)典校驗庫 validator 用法解析
這篇文章主要為大家介紹了Golang 經(jīng)典校驗庫 validator 用法解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08
golang中sync.Once只執(zhí)行一次的原理解析
在某些場景下,我們希望某個操作或者函數(shù)僅被執(zhí)行一次,比如單例模式的初始化,一些資源配置的加載等,golang中的sync.Once就實現(xiàn)了這個功能,本文就和大家一起解析sync.Once只執(zhí)行一次的原理,需要的朋友可以參考下2023-09-09
golang中的select關(guān)鍵字用法總結(jié)
這篇文章主要介紹了golang中的select關(guān)鍵字用法總結(jié),本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06

