一文總結(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)容,更多關于Go語言切片知識點和坑的資料請關注腳本之家其它相關文章!
相關文章
Golang 經(jīng)典校驗庫 validator 用法解析
這篇文章主要為大家介紹了Golang 經(jīng)典校驗庫 validator 用法解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08golang中sync.Once只執(zhí)行一次的原理解析
在某些場景下,我們希望某個操作或者函數(shù)僅被執(zhí)行一次,比如單例模式的初始化,一些資源配置的加載等,golang中的sync.Once就實現(xiàn)了這個功能,本文就和大家一起解析sync.Once只執(zhí)行一次的原理,需要的朋友可以參考下2023-09-09