欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

GO中的slice使用簡介(源碼分析slice)

 更新時間:2023年06月15日 10:56:36   作者:daliucheng  
slice(切片)是go中常見和強大的類型,這篇文章不是slice使用簡介,從源碼角度來分析slice的實現(xiàn),slice的一些迷惑的使用方式,感興趣的朋友跟隨小編一起看看吧

slice表示切片(分片),例如對一個數組進行切片,取出數組中的一部分值。在現(xiàn)代編程語言中,slice(切片)幾乎成為一種必備特性,它可以從一個數組(列表)中取出任意長度的子數組(列表),為操作數據結構帶來非常大的便利性,如python、perl等都支持對數組的slice操作,甚至perl還支持對hash數據結構的slice。

但Go中的slice和這些語言的slice不太一樣,前面所說的語言中,slice是一種切片的操作,切片后返回一個新的數據對象。而Go中的slice不僅僅是一種切片動作,還是一種數據結構(就像數組一樣)。

GO-slice詳解

簡介

slice(切片)是go中常見和強大的類型,這篇文章不是slice使用簡介,從源碼角度來分析slice的實現(xiàn),slice的一些迷惑的使用方式,同時也講清楚一些問題。

slice的底層實現(xiàn)是數組,是對數組的抽象

官方有slice相關的博客

https://go.dev/blog/slices-introhttps://go.dev/blog/slices

slice分析

數據結構

源碼:https://github.com/golang/go/blob/master/src/runtime/slice.go#L14

slice結構如下:

type slice struct {
	array unsafe.Pointer // 數組指針
	len   int  //切片長度
	cap   int // 切片容量
}

array:為是底層數組的指針

len:切片中已有元素的個數

cap:底層數組的長度

原理概述:

切片的底層實現(xiàn)是數組,len是切片的個數,cap是底層數組的長度,當往切片中追加元素的時候,len++,如果len>cap,就會觸發(fā)切片擴容,擴容邏輯是算一個新的cap,并且創(chuàng)建一個新的底層數組,將原來的數據copy過來。并且創(chuàng)建一個新的切片(slice)。

可以從一個切片中創(chuàng)建一個新的切片,底層數組是同用的,修改切片元素的時候會影響到新的切片。

如果所示:

var s = make([]byte,5)對應的邏輯是,創(chuàng)建一個長度和容量為5的數組,如果所示

切片的創(chuàng)建方式

先看切片的創(chuàng)建方式,說這個問題之前,先看看切片的創(chuàng)建方式

聲明

var vocabList []uint64

聲明了一個[]uint64類型的切片,vocabList為切片的0值,

這得說一下go中nil

在 Go 中,nil 是指針、接口、映射、切片、通道和函數類型的零值,表示未初始化的值。

具體的可以看:https://go101.org/article/nil.html

回到代碼,這表示nil值,它的len和cap都是0,和nil比較結果為true,這里要說,nil值對應的具體的類型是在上下文中編譯器推導出來的

package main
func main() {
	var vocabList []uint64
	println(vocabList == nil)
}
// output:
// true

通過new創(chuàng)建

var vocabList = *new([]uint64)

new是內建函數,用來分配指定類型的內容,返回指向內存地址的指針,并且給此地址分配此類型的0值。

字面量創(chuàng)建

var vocabList = []uint64{1,2,3,4}

make

var vocabList = make([]uint64,10)

make接受三個參數,在創(chuàng)建的時候指定類型,長度,容量,不指定容量,默認和長度一樣

代碼如下:

func main() {
	var vocabList = make([]uint64,10)
	fmt.Printf("slice:%v,len:%d,cap:%d",vocabList,len(vocabList),cap(vocabList))
}
// output:
// slice:[0 0 0 0 0 0 0 0 0 0],len:10,cap:10

從切片或數組截取

var vocabList = resList[3:5]

兩個切片公用一個底層數組,但如果新創(chuàng)建的切片擴容了,就不共用了。

問題分析

主要分析幾個問題

nil切片和空切片的差異

nil切片是通過 new 和聲明方式創(chuàng)建的切片,go會給他們nil值,如下面的代碼所示:

var vocabList []int
vocabList == nil //true

空切片是通過make,字面量方式創(chuàng)建的長度為0的切片,

vocabList := make([]int,0)
vocabList == nil // false
vocabList1 = []int{}
vocabList1 == nil //false

具體可以看這篇文章:http://www.dbjr.com.cn/jiaoben/288490px1.htm

我下面的代碼和內容來于這篇文章

通過unsafe.Pointer可以將對應地址中的數據轉換為任何符合go中類型的變量

可以看到,空切片的是有底層數組的,并且底層數組都一樣,其實也可以說空切片執(zhí)行了一個指定的地址空間,

這個地址空間在源碼中有定義,當分配的大小為0的時候會返回這個地址,要說明的是這個地址空間不是固定的,不是寫死的一個數,在不同的機器上運行會有不同的值。

源碼:https://github.com/golang/go/blob/master/src/runtime/malloc.go#L948

兩者的差異:

嵌套在結構體中不容易發(fā)現(xiàn)

package main
type Word struct {
	SenseIds []int
}
func main() {
	word := Word{}
	println(word.SenseIds == nil) //true
	word1 := Word{
		SenseIds: make([]int,0),
	}
	println(word1.SenseIds == nil) //false
}

json序列化

package main
import "encoding/json"
type Word struct {
	SenseIds []int `json:"sense_ids" `
}
func main() {
	word := Word{}
	marshal, err := json.Marshal(word)
	if err != nil {
		return
	}
	println(string(marshal)) //{"sense_ids":null}
	word1 := Word{
		SenseIds: []int{},
	}
	marshal1, err := json.Marshal(word1)
	if err != nil {
		return
	}
	println(string(marshal1)) //{"sense_ids":[]}
}

這個問題我深有體會

在做一個需求的時候,看到編輯器報黃色提示,提示我將 var a = []int{} 改為var a []int,當然,go官方也是這么建議的。我就改了,然后一個接口就報錯了。給我一頓找,發(fā)現(xiàn)json返回了null。

除此之外,沒有別的區(qū)別。

切片共用底層數組

在做截取的時候,會創(chuàng)建一個新的slice,截取語法如下

bSlice := aSlice[start:stop:capacityIndex]
// satrt <= stop <= capacityIndex
//capacityIndex不是必須的,默認=原來切片的cap-startIndex
// 如果指定 新切片的容量為 capacityIndex-start

如圖所示:

有了上面的例子,可以看如下代碼

package main
import (
	"fmt"
)
func main() {
	slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s1 := slice[2:5]  
	fmt.Println("============= 1 ==============")
	fmt.Printf("%v len:%d  cap:%d \n",slice,len(slice),cap(slice))
	fmt.Printf("%v len:%d  cap:%d \n",s1,len(s1),cap(s1))
	s2 := s1[2:6:7]
	fmt.Println("============= 2 ==============")
	fmt.Printf("%v len:%d  cap:%d \n",slice,len(slice),cap(slice))
	fmt.Printf("%v len:%d  cap:%d \n",s1,len(s1),cap(s1))
	fmt.Printf("%v len:%d  cap:%d \n",s2,len(s2),cap(s2))
    // 到這里是正確的切片操作,slice,s1,s2通用底層數組
	s2 = append(s2, 100) //s2追加100,此時s2中l(wèi)en=cap,還沒有觸發(fā)擴容操作
	fmt.Println("============= 3 ==============")
	fmt.Printf("%v len:%d  cap:%d \n",slice,len(slice),cap(slice))
	fmt.Printf("%v len:%d  cap:%d \n",s1,len(s1),cap(s1))
	fmt.Printf("%v len:%d  cap:%d \n",s2,len(s2),cap(s2))
	s2 = append(s2, 200)// 200加不進去了,觸發(fā)擴容操作,此時s2的底層數組和s1,slice不一樣了
	fmt.Println("============= 3 ==============")
	fmt.Printf("%v len:%d  cap:%d \n",slice,len(slice),cap(slice))
	fmt.Printf("%v len:%d  cap:%d \n",s1,len(s1),cap(s1))
	fmt.Printf("%v len:%d  cap:%d \n",s2,len(s2),cap(s2))
	s1[2] = 20 // s1和slice底層數組還是一樣的
	fmt.Println("============= 3 ==============")
	fmt.Printf("%v len:%d  cap:%d \n",slice,len(slice),cap(slice))
	fmt.Printf("%v len:%d  cap:%d \n",s1,len(s1),cap(s1))
	fmt.Printf("%v len:%d  cap:%d \n",s2,len(s2),cap(s2))
}
輸出如下:
============= 1 ==============
[0 1 2 3 4 5 6 7 8 9] len:10  cap:10 
[2 3 4] len:3  cap:8 
============= 2 ==============
[0 1 2 3 4 5 6 7 8 9] len:10  cap:10 
[2 3 4] len:3  cap:8 
[4 5 6 7] len:4  cap:5 
============= 3 ==============
[0 1 2 3 4 5 6 7 100 9] len:10  cap:10 
[2 3 4] len:3  cap:8 
[4 5 6 7 100] len:5  cap:5 
============= 3 ==============
[0 1 2 3 4 5 6 7 100 9] len:10  cap:10 
[2 3 4] len:3  cap:8 
[4 5 6 7 100 200] len:6  cap:10 
============= 3 ==============
[0 1 2 3 20 5 6 7 100 9] len:10  cap:10 
[2 3 20] len:3  cap:8 
[4 5 6 7 100 200] len:6  cap:10 

源碼分析

make創(chuàng)建切片

使用dlv或者go提供的匯編工具可以看到 make調用了什么函數

源碼:https://github.com/golang/go/blob/master/src/runtime/slice.go#LL88C18-L88C18

切片的擴容規(guī)則

版本不同,擴容規(guī)則可能不一樣,例子中go版本為:

代碼如下:

package main
import (
	"fmt"
	"unsafe"
)
func main() {
	ints := make([]int, 0) // 創(chuàng)建了一個長度為0的切片
	i := *(*[3]int)(unsafe.Pointer(&ints)) // 利用Pointer將slice轉換為長度為3的int數組,此操作可以查看slice結構體中各個字段的數值
	fmt.Printf("slice1:%v \n",i)
	fmt.Printf("slice:%v \n",ints)
	var ints1 = append(ints, 1) // 追加一個元素
	i2 := *(*[3]int)(unsafe.Pointer(&ints1))
	fmt.Printf("slice2 %v \n",i2)   // slice對應的底層數組發(fā)生了擴容操作,底層數組已經變了
	fmt.Printf("slice2:%v \n",ints)
}

用dlv 查看它的匯編代碼,看擴容操作調用了那些函數

源碼鏈接:https://github.com/golang/go/blob/master/src/runtime/slice.go#LL157C10-L157C10

// 函數入參說明如下
//1. et 類型
//2. old 老切片
//3. cap 需要分配的指定容量,為了方便期間,調用這個函數的時候cap傳遞的都是老的slice的cap
func growslice(et *_type, old slice, cap int) slice { 
	if raceenabled { // 是否啟動競爭檢測
		callerpc := getcallerpc()
		racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, abi.FuncPCABIInternal(growslice))
	}
	if msanenabled { //內存檢查,確保沒有未初始化的內存被使用
		msanread(old.array, uintptr(old.len*int(et.size)))
	}
	if asanenabled { // 檢查內存訪問是否越界
		asanread(old.array, uintptr(old.len*int(et.size)))
	}
	if cap < old.cap {
		panic(errorString("growslice: cap out of range"))
	}
	if et.size == 0 {
	    // 正常是不會這樣的,但為了安全還是處理了0的情況
		return slice{unsafe.Pointer(&zerobase), old.len, cap}
	}
	// 開始計算新的cap
	newcap := old.cap
	doublecap := newcap + newcap // 2倍
	if cap > doublecap { // 新的cap要是老的2倍
		newcap = cap
	} else {
		const threshold = 256
		if old.cap < threshold { // cap小于256,newCap為oldCap的兩倍
			newcap = doublecap
		} else {
			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.
				// 這個公式可以當超過256之后i,可以實現(xiàn)1.25到2倍的平滑過渡
				newcap += (newcap + 3*threshold) / 4  // 這個公式化簡一下 newCap = oldCap*1.25 + 192(3/4*256)
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 { // 防止溢出
				newcap = cap
			}
		}
	}
    // 下面的邏輯是對上面計算出來的newCap來做對齊操作,上面的計算不是真正的結果,下面還需要做內存對齊操作。
	var overflow bool
	var lenmem, newlenmem, capmem uintptr
	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 == goarch.PtrSize:
		lenmem = uintptr(old.len) * goarch.PtrSize
		newlenmem = uintptr(cap) * goarch.PtrSize
		capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
		overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
		newcap = int(capmem / goarch.PtrSize)
	case isPowerOfTwo(et.size):
		var shift uintptr
		if goarch.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)
	}
	// The check of overflow in addition to capmem > maxAlloc is needed
	// to prevent an overflow which can be used to trigger a segfault
	// on 32bit architectures with this example program:
	//
	// type T [1<<27 + 1]int64
	//
	// var d T
	// var s []T
	//
	// func main() {
	//   s = append(s, d, d, d, d)
	//   print(len(s), "\n")
	// }
	if overflow || capmem > maxAlloc {
		panic(errorString("growslice: cap out of range"))
	}
	var p unsafe.Pointer
    // 下面是分配新的數組
	if et.ptrdata == 0 { // 原slice底層數組為0,也就是nil切片,
		p = mallocgc(capmem, nil, false)
		// The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
		// Only clear the part that will not be overwritten.
		memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem) // 然后使用memclrNoHeapPointers函數來清除新分配的內存
	} else {
		// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
		p = mallocgc(capmem, et, true) // 分配新的底層數組
		if lenmem > 0 && writeBarrier.enabled { // 之前有數據,并且寫屏障已經開啟
			// Only shade the pointers in old.array since we know the destination slice p
			// only contains nil pointers because it has been cleared during alloc.
			bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
		}
	}
	// copy元素
	memmove(p, old.array, lenmem)
   // 創(chuàng)建新的切片返回
	return slice{p, old.len, newcap}
}

總結如下:

  • 確定新容量,cap小于256,直接2倍,大于256,新容量=老容量*1.25 * 3/4 * 256
  • 用新容量來做內存對齊操作
  • 分配新數組
  • copy數組
  • 創(chuàng)建切片返回

還有一點:

append的操作匯編并沒有調用函數,在匯編層面就做了,直接往底層數組添加元素,只有數組已經滿的情況下才會觸發(fā)擴容操作

內存對齊相關東西之后在說

我們來一個例子來驗證一下上面的代碼邏輯:

package main
import "fmt"
func main() {
	var s = []int{}
	oldCap := cap(s)
	for i := 0; i < 2048; i++ {
		s = append(s, i)
		newCap := cap(s)
		if newCap != oldCap {
			// 追加元素,當容量發(fā)生變化的時候,打印,擴容之前的元素,cap,導致擴容的元素,和擴容之后的cap
			fmt.Printf("[%d -> %4d] cap = %-4d    after append %-4d  cap = %-4d\n", 0, i-1, oldCap, i, newCap)
			oldCap = newCap
		}
	}
}
// outPut
[0 ->   -1] cap = 0     |  after append 0     cap = 1   
[0 ->    0] cap = 1     |  after append 1     cap = 2   
[0 ->    1] cap = 2     |  after append 2     cap = 4   
[0 ->    3] cap = 4     |  after append 4     cap = 8   
[0 ->    7] cap = 8     |  after append 8     cap = 16  
[0 ->   15] cap = 16    |  after append 16    cap = 32  
[0 ->   31] cap = 32    |  after append 32    cap = 64  
[0 ->   63] cap = 64    |  after append 64    cap = 128 
[0 ->  127] cap = 128   |  after append 128   cap = 256    // 256之前都是2倍
[0 ->  255] cap = 256   |  after append 256   cap = 512    // 1.25 * 256 + 192 = 512
[0 ->  511] cap = 512   |  after append 512   cap = 848    // 1.25 * 512 + 192 = 832
[0 ->  847] cap = 848   |  after append 848   cap = 1280
[0 -> 1279] cap = 1280  |  after append 1280  cap = 1792
[0 -> 1791] cap = 1792  |  after append 1792  cap = 2560

copy函數的使用

copy函數底層調用的是

底層數組共用,那copy函數就可以完成一下幾種操作

移動slice中的元素

func main() {
	var vocabs = []int{1,2,3,4,5,6,7,8,9}
	// 現(xiàn)在將5去除掉,將5之后的移動到前面 
	copy(vocabs[4:],vocabs[5:])
	fmt.Printf("%v",vocabs)
}
//outPut
[1 2 3 4 6 7 8 9 9]

slice合并

func main() {
	ints := make([]int, 10)
	i1 := make([]int,0, 5)
	for i := 0; i < 5; i++ {
		i1 = append(i1, i)
	}
	i2 := make([]int,0, 5)
	for i := 5; i < 10; i++ {
		i2 = append(i2, i)
	}
	copy(ints,i1) // 從i1全部復制到ints中
	copy(ints[len(i1):],i2) // 將i2復制到ints的len(i1)位置開始一直到結束的數組中
	fmt.Printf("%v\n",i1)
	fmt.Printf("%v\n",i2)
	fmt.Printf("%v\n",ints)
}
// output
[0 1 2 3 4]
[5 6 7 8 9]
[0 1 2 3 4 5 6 7 8 9]

長度不夠的copy,依dist為準

package main
import "fmt"
func main() {
	ints := make([]int, 3)
	i1 := make([]int,0, 10)
	for i := 0; i < 10; i++ {
		i1 = append(i1, i)
	}
	copy(ints,i1)
	fmt.Printf("%v",ints)
}
//outPut:
[0 1 2]

問題解答

nil 切片可以添加元素嗎?

可以

nil切片就是切片聲明,追加的時候切片長度為0,會引發(fā)擴容操作,擴容的時候會給分配一個新的數組。

nil切片和空切片有區(qū)別嗎?

nil切片有兩種方式

聲明new創(chuàng)建

空切片有兩種:

字面量創(chuàng)建但沒有任何的元素make創(chuàng)建長度指定為0

使用方式除了下面兩點沒有別的區(qū)別:

嵌套結構體,不顯性創(chuàng)建為niljson序列化會為null

slice擴容規(guī)則

說到前面:它在確定cap之后有內存對齊操作

小于256,是原cap的2倍大于256,是原來的1.25倍+3/4*256

到這里就結束了。

到此這篇關于GO中的slice詳解的文章就介紹到這了,更多相關go slice詳解內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • Go語言基礎學習之數組的使用詳解

    Go語言基礎學習之數組的使用詳解

    數組相必大家都很熟悉,各大語言也都有數組的身影。Go 語言也提供了數組類型的數據結構。本文就來通過一些簡單的示例帶大家了解一下Go語言中數組的使用,希望對大家有所幫助
    2022-12-12
  • 利用systemd部署golang項目的實現(xiàn)方法

    利用systemd部署golang項目的實現(xiàn)方法

    這篇文章主要介紹了利用systemd部署golang項目的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-11-11
  • 解決golang編譯提示dial tcp 172.217.160.113:443: connectex: A connection attempt failed(推薦)

    解決golang編譯提示dial tcp 172.217.160.113:443: con

    這篇文章主要介紹了解決golang編譯提示dial tcp 172.217.160.113:443: connectex: A connection attempt failed,此問題完美解決,需要的朋友可以參考下
    2023-02-02
  • go slice 數組和切片使用區(qū)別示例解析

    go slice 數組和切片使用區(qū)別示例解析

    這篇文章主要為大家介紹了go slice 數組和切片使用區(qū)別示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-01-01
  • Golang中interface轉string輸出打印方法

    Golang中interface轉string輸出打印方法

    這篇文章主要給大家介紹了關于Golang中interface轉string輸出打印的相關資料,在go語言中interface轉string可以直接使用fmt提供的fmt函數,文中通過代碼介紹的非常詳細,需要的朋友可以參考下
    2024-02-02
  • 淺析Go 字符串指紋

    淺析Go 字符串指紋

    這篇文章主要介紹了Go 字符串指紋的相關資料,幫助大家更好的理解和學習go語言,感興趣的朋友可以了解下
    2020-09-09
  • go語言中讀取配置文件的方法總結

    go語言中讀取配置文件的方法總結

    這篇文章主要為大家詳細介紹了go語言中讀取配置文件的幾個常見方法,文中的示例代碼講解詳細,具有一定的借鑒價值,需要的小伙伴可以參考下
    2023-08-08
  • Go語言的管道Channel用法實例

    Go語言的管道Channel用法實例

    這篇文章主要介紹了Go語言的管道Channel用法,實例分析了Go語言中管道的原理與使用技巧,具有一定參考借鑒價值,需要的朋友可以參考下
    2015-02-02
  • Golang異常處理之defer,panic,recover的使用詳解

    Golang異常處理之defer,panic,recover的使用詳解

    這篇文章主要為大家介紹了Go語言異常處理機制中defer、panic和recover三者的使用方法,文中示例代碼講解詳細,需要的朋友可以參考下
    2022-05-05
  • go語言實現(xiàn)markdown解析庫的方法示例

    go語言實現(xiàn)markdown解析庫的方法示例

    這篇文章主要介紹了go語言實現(xiàn)markdown解析庫的方法示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-02-02

最新評論