91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

go數組和切片的概念及用法

發布時間:2021-07-10 10:48:01 來源:億速云 閱讀:209 作者:chen 欄目:編程語言

這篇文章主要介紹“go數組和切片的概念及用法”,在日常操作中,相信很多人在go數組和切片的概念及用法問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”go數組和切片的概念及用法”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

1.容器類型和容器值

每個容器(值)用來表示和存儲一個元素(element)序列或集合。一個容器中的所有元素的類型是相同的。此相同的類型稱為此容器的類型的元素類型(或簡稱此容器的元素類型)。

存儲在一個容器中的每個元素值都關聯著著一個鍵值(key)。每個元素可以通過它的鍵值而被訪問到。 一個映射類型的鍵值類型必須為一個可比較類型

可比較類型即指支持使用==和!=運算標識符比較的類型,在Go中,除了切片類型、映射類型、函數類型、任何包含有不可比較類型的字段的結構體類型、任何元素類型為不可比較類型的數組類型之外的其它類型稱為可比較類型。

數組和切片類型的鍵值類型均為內置類型int。 一個數組或切片的一個元素對應的鍵值總是一個非負整數下標,此非負整數表示該元素在該數組或切片所有元素中的順序位置。此非負整數下標亦常稱為一個元素索引(index)。

每個容器值有一個長度屬性,用來表明此容器中當前存儲了多少個元素。 一個數組或切片中的每個元素所關聯的非負整數索引鍵值的合法取值范圍為左閉右開區間[0, 此數組或切片的長度)

2.數組和切片的異同

2.1.異同點

  • slice 的底層數據是數組,slice 是對數組的封裝,它描述一個數組的片段。兩者都可以通過下標來訪問單個元素。

  • 數組是定長的,長度定義好之后,不能再更改。在 Go 中,數組是不常見的,因為其長度是類型的一部分,限制了它的表達能力,比如 [3]int 和 [4]int 就是不同的類型。

  • 而切片則非常靈活,它可以動態地擴容。切片的類型和長度無關。

  • 數組就是一片連續的內存, slice 實際上是一個結構體,包含三個字段:長度、容量、底層數組。

2.2.切片結構及示例

slice 的數據結構如下:

// runtime/slice.go
type slice struct {
    array unsafe.Pointer // 元素指針
    len   int // 長度 
    cap   int // 容量
}

或者用圖表示:

go數組和切片的概念及用法

注意:底層數組是可以被多個 slice 同時指向的,因此對一個 slice 的元素進行操作是有可能影響到其他 slice 的。

1. 示例一

[3]int 和 [4]int 是同一個類型嗎?

不是。因為數組的長度是類型的一部分,這是與 slice 不同的一點。

2. 示例二

Go中對nil的Slice和空Slice的處理是一致的嗎?

2.1. 首先Go的JSON 標準庫對 nil slice 和 空 slice 的處理是不一致的:

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

func main(){
	var s1 []int
	s2 := []int{}
	b, err := json.Marshal(s1)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(b))
	b, err = json.Marshal(s2)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(b))
}

輸出結果:

null
[]

2.2.通常錯誤的用法,會報數組越界的錯誤,因為只是聲明了slice,卻沒有給實例化的對象:

var slice []int
slice[1] = 0

此時slice的值是nil,這種情況可以用于需要返回slice的函數,當函數出現異常的時候,保證函數依然會有nil的返回值。

2.3.empty slice 是指slice不為nil,但是slice沒有值,slice的底層的空間是空的,此時的定義如下:

slice := make([]int,0)
slice := []int{}

當我們查詢或者處理一個空的列表的時候,這非常有用,它會告訴我們返回的是一個列表,但是列表內沒有任何值。

總之,nil slice empty slice是不同的東西,需要我們加以區分的.

3. 示例三

下面的代碼輸出是什么?

package main

import (
	"fmt"
)

func main() {
	slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s1 := slice[2:5]
	/*
	對于切片截取操作s[low:high:max]:
	1. 第一個數(low)表示下標的起點(從該位置開始截取),
	如果low取值為0表示從第一個元素開始截取;
	2. 第二個數(high)表示取到哪結束,也就是下標的終點(不包含該位置),
	根據公式計算(len=high-low),也就是第二個數減去第一個數,差就是數據長度。
	在這里可以將長度理解成取出的數據的個數。
	3. 第三個數用來計算容量,所謂容量:是指切片目前可容納的最多元素個數。
	通過公式計算(cap=max-low),也就是第三個數減去第一個數。
	*/
	s2 := s1[2:6:7]

	s2 = append(s2, 100)
	s2 = append(s2, 200)

	s1[2] = 20

	fmt.Println(s1)
	fmt.Println(s2)
	fmt.Println(slice)

}

輸出結果:

[2 3 20]
[4 5 6 7 100 200]
[0 1 2 3 20 5 6 7 100 9]

s1slice 索引2(閉區間)到索引5(開區間,元素真正取到索引4),長度為3,容量默認到數組結尾,為8。 s2s1 的索引2(閉區間)到索引6(開區間,元素真正取到索引5),容量到索引7(開區間,真正到索引6),為5。

go數組和切片的概念及用法

接著,向s2尾部追加一個元素 100:

s2 = append(s2, 100)

s2 容量剛好夠,直接追加。不過,這會修改原始數組對應位置的元素。這一改動,數組和 s1 都可以看得到。

go數組和切片的概念及用法

再次向 s2 追加元素200:

s2 = append(s2, 100)

這時,s2 的容量不夠用,該擴容了。于是,s2 另起爐灶,將原來的元素復制到新的位置,擴大自己的容量。并且為了應對未來可能的 append 帶來的再一次擴容,s2 會在此次擴容的時候多留一些 buffer,將新的容量擴大為原始容量的2倍,也就是10了。

go數組和切片的概念及用法

最后,修改 s1 索引為2位置的元素:

s1[2] = 20

這次只會影響原始數組相應位置的元素。它影響不到 s2 了,人家已經遠走高飛了。

go數組和切片的概念及用法

再提一點,打印 s1 的時候,只會打印出 s1 長度以內的元素。所以,只會打印出3個元素,雖然它的底層數組不止3個元素。

3.切片作為函數參數傳遞

前面我們說到,slice 其實是一個結構體,包含了三個成員:len, cap, array。分別表示切片長度,容量,底層數據的地址。

當 slice 作為函數參數時,就是一個普通的結構體。其實很好理解:若直接傳 slice,在調用者看來,實參 slice 并不會被函數中的操作改變;若傳的是 slice 的指針,在調用者看來,是會被改變原 slice 的。

值的注意的是,不管傳的是 slice 還是 slice 指針,如果改變了 slice 底層數組的數據,會反應到實參 slice 的底層數據。為什么能改變底層數組的數據?很好理解:底層數據在 slice 結構體里是一個指針,僅管 slice 結構體自身不會被改變,也就是說底層數據地址不會被改變; 但是通過指向底層數據的指針,可以改變切片的底層數據,沒有問題。

通過 slice 的 array 字段就可以拿到數組的地址。在代碼里,是直接通過類似 s[i]=10 這種操作改變 slice 底層數組元素值。

另外,值得注意的是,Go 語言的函數參數傳遞,只有值傳遞,沒有引用傳遞。

來看一個代碼片段:

package main

func main() {
    s := []int{1, 1, 1}
    f(s)
    fmt.Println(s)
}

func f(s []int) {
    // i只是一個副本,不能改變s中元素的值
    /*for _, i := range s {
        i++
    }
    */

    for i := range s {
        s[i] += 1
    }
}

運行一下,程序輸出:

[2 2 2]

通過下標引用的方式果真改變了原始 slice 的底層數據。

這里調用 f 函數時傳遞的是一個 slice 的副本,即在 f 函數中,s 只是 main 函數中 s 的一個拷貝。在f 函數內部,對 s 的作用并不會改變外層 main 函數的 s,正如上面所說的,在 f 函數中slice 結構體自身不會改變,即使我們通過下標引用的方式改變了切片的底層數據,其底層數據地址是不變的。

要想真的改變外層 slice,只有將返回的新的 slice 賦值到原始 slice,或者向函數傳遞一個指向 slice 的指針。我們再來看一個例子:

package main

import "fmt"

func myAppend(s []int) []int {
    // 這里 s 雖然改變了,但并不會影響外層函數的 s
    s = append(s, 100)
    return s
}

func myAppendPtr(s *[]int) {
    // 會改變外層 s 本身
    *s = append(*s, 100)
    return
}

func main() {
    s := []int{1, 1, 1}
    newS := myAppend(s)

    fmt.Println(s)
    fmt.Println(newS)

    s = newS

    myAppendPtr(&s)
    fmt.Println(s)
}

運行結果:

[1 1 1]
[1 1 1 100]
[1 1 1 100 100]

myAppend 函數里,雖然改變了 s,但它只是一個值傳遞,并不會影響外層的 s,因此第一行打印出來的結果仍然是 [1 1 1]

newS 是一個新的 slice,它是基于 s 得到的。因此它打印的是追加了一個 100 之后的結果: [1 1 1 100]

最后,將 newS 賦值給了 ss 這時才真正變成了一個新的slice。之后,再給 myAppendPtr 函數傳入一個 s 指針,這回它就真的被改變了:[1 1 1 100 100]

4.切片的擴容

一般都是在向 slice 追加了元素之后,才會引起擴容。追加元素調用的是 append 函數。

先來看看 append 函數的原型:

func append(slice []Type, elems ...Type) []Type

append 函數的參數長度可變,因此可以追加多個值到 slice 中,還可以用 ... 傳入 slice,直接追加一個切片:

slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)

append函數返回值是一個新的slice,Go編譯器不允許調用了 append 函數后不使用返回值:

append(slice, elem1, elem2)
append(slice, anotherSlice...)

所以上面的用法是錯的,不能編譯通過。

使用 append 可以向 slice 追加元素,實際上是往底層數組添加元素。但是底層數組的長度是固定的,如果索引 len-1 所指向的元素已經是底層數組的最后一個元素,就沒法再添加了。

這時,slice 會遷移到新的內存位置,新底層數組的長度也會增加,這樣就可以放置新增的元素。同時,為了應對未來可能再次發生的 append 操作,新的底層數組的長度,也就是新 slice 的容量是留了一定的 buffer 的。否則,每次添加元素的時候,都會發生遷移,成本太高。

新 slice 預留的 buffer 大小是有一定規律的。網上大多數的文章都是這樣描述的:

當原 slice 容量小于 1024 的時候,新 slice 容量變成原來的 2 倍;原 slice 容量超過 1024,新 slice 容量變成原來的1.25倍。

這里先說結論:以上描述是錯誤的。

為了說明上面的規律是錯誤的,看下面這段代碼:

package main

import "fmt"

func main() {
	s := make([]int, 0)

	oldCap := cap(s)

	for i := 0; i < 2048; i++ {
		s = append(s, i)

		newCap := cap(s)

		if newCap != oldCap {
			fmt.Printf("[%d -> %4d] cap = %-4d  |  after append %-4d  cap = %-4d\n",
				0, i-1, oldCap, i, newCap)
			oldCap = newCap
		}
	}
}

先創建了一個空的 slice,然后,在一個循環里不斷往里面 append 新的元素。然后記錄容量的變化,并且每當容量發生變化的時候,記錄下老的容量,以及添加完元素之后的容量,同時記下此時 slice 里的元素。這樣,我就可以觀察,新老 slice 的容量變化情況,從而找出規律。

運行結果:

[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
[0 ->  255] cap = 256   |  after append 256   cap = 512
[0 ->  511] cap = 512   |  after append 512   cap = 1024
[0 -> 1023] cap = 1024  |  after append 1024  cap = 1280
[0 -> 1279] cap = 1280  |  after append 1280  cap = 1696
[0 -> 1695] cap = 1696  |  after append 1696  cap = 2304

在老 slice 容量小于1024的時候,新 slice 的容量的確是老 slice 的2倍。目前還算正確。

但是,當老 slice 容量大于等于 1024 的時候,情況就有變化了。當向 slice 中添加元素 1280 的時候,老 slice 的容量為 1280,之后變成了 1696,兩者并不是 1.25 倍的關系 (1696/1280=1.325)。添加完 1696 后,新的容量 2304 當然也不是 16961.25 倍。

可見,現在網上各種文章中的擴容策略并不正確。我們直接搬出源碼:

向 slice 追加元素的時候,若容量不夠,會調用 growslice 函數,所以我們直接看它的代碼。

// go 1.14.6 src/runtime/slice.go:76
// et:slice元素類型,old:舊的slice,cap:所需的新最小容量
func growslice(et *_type, old slice, cap int) slice {
	// ……
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		if old.len < 1024 {
			newcap = doublecap
		} else {
			// 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 的規律是對的。現實是,后半部分還對 newcap 作了一個內存對齊,這個和內存分配策略相關。進行內存對齊之后,新slice的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍。

之后,向 Go 內存管理器申請內存,將老 slice 中的數據復制過去,并且將 append 的元素添加到新的底層數組中。

最后,向 growslice 函數調用者返回一個新的 slice,這個 slice 的長度并沒有變化,而容量卻增大了。

下面來看幾個示例:

1. 示例一

package main

import "fmt"

func main() {
    s := []int{5}
    s = append(s, 7)
    s = append(s, 9)
    x := append(s, 11)
    y := append(s, 12)
    fmt.Println(s, x, y)
}

解釋:

代碼切片對應狀態
s := []int{5}s 只有一個元素,[5]
s = append(s, 7)s 擴容,容量變為2,[5, 7]
s = append(s, 9)s 擴容,容量變為4,[5, 7, 9]。注意,這時 s 長度是3,只有3個元素
x := append(s, 11)由于 s 的底層數組仍然有空間,因此并不會擴容。這樣,底層數組就變成了 [5, 7, 9, 11]。注意,此時 s = [5, 7, 9],容量為4;x = [5, 7, 9, 11],容量為4。這里 s 不變
y := append(s, 12)這里還是在 s 元素的尾部追加元素,由于 s 的長度為3,容量為4,所以直接在底層數組索引為3的地方填上12。結果:s = [5, 7, 9],y = [5, 7, 9, 12],x = [5, 7, 9, 12],x,y 的長度均為4,容量也均為4

所以最后程序的執行結果是:

[5 7 9] [5 7 9 12] [5 7 9 12]

這里要注意的是,append函數執行完后,返回的是一個全新的 slice,并且對傳入的 slice 并不影響。

2. 示例二

關于 append,我們來看這個例子:

package main

import "fmt"

func main() {
    s := []int{1,2}
    s = append(s,4,5,6)
    fmt.Printf("len=%d, cap=%d",len(s),cap(s))
}

運行結果是:

len=5, cap=6

如果按網上各種文章中總結的那樣:小于原 slice 長度小于 1024 的時候,容量每次增加 1 倍。添加元素 4 的時候,容量變為4;添加元素 5 的時候不變;添加元素 6 的時候容量增加 1 倍,變成 8。 那上面代碼的運行結果應該是:

len=5, cap=8

這顯然是錯誤的,我們來仔細看看,為什么會這樣,再次搬出源碼:

// go 1.14.6 src/runtime/slice.go:76
// et:slice元素類型,old:舊的slice,cap:所需的新最小容量
func growslice(et *_type, old slice, cap int) slice {
	// ……
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		// ……
	}
	// ……
	// 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:
		// ……
	case et.size == sys.PtrSize:
		// ……
		capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
		// ……
		newcap = int(capmem / sys.PtrSize)
	case isPowerOfTwo(et.size):
		// ……
	default:
		// ……
	}
}

這個函數的參數依次是 元素的類型,老的 slice,新 slice 所需的最小容量。

例子中s原來只有 2 個元素,len 和 cap 都為 2,append 了三個元素后,長度變為 5,容量最小要變成 5,即調用 growslice 函數時,傳入的第三個參數應該為 5。即 cap=5。而一方面,doublecap 是原 slice容量的 2 倍,等于 4。滿足第一個 if 條件,所以 newcap 變成了 5

接著調用了 roundupsize 函數,傳入 40。(代碼中ptrSize是指一個指針的大小,int類型在64位機上大小是8

我們再看內存對齊,搬出 roundupsize 函數的代碼:

// Returns size of the memory block that mallocgc will allocate if you ask for the size.
func roundupsize(size uintptr) uintptr {
	if size < _MaxSmallSize {
		if size <= smallSizeMax-8 {
			return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])
		} else {
			//……
		}
	}
	//……
}

const _MaxSmallSize = 32768
const smallSizeMax = 1024
const smallSizeDiv = 8

很明顯,我們最終將返回這個式子的結果:

class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]

這是 Go 源碼中有關內存分配的兩個 sliceclass_to_size通過 spanClass獲取 span劃分的 object大小。而 size_to_class8 表示通過 size 獲取它的 spanClass

var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31}

var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}

我們傳進去的 size 等于 40。所以 (size+smallSizeDiv-1)/smallSizeDiv = 5;獲取 size_to_class8 數組中索引為5的元素為 4;獲取 class_to_size 中索引為 4 的元素為 48。 最終,新的 slice 的容量為 6

newcap = int(capmem / ptrSize) // 6

至于上面的兩個魔法數組的由來,就不展開了。

3. 示例三

向一個nil的slice添加元素會發生什么?為什么?

其實 nil slice 或者empty slice都是可以通過調用 append 函數來獲得底層數組的擴容。最終都是調用 mallocgc 來向 Go 的內存管理器申請到一塊內存,然后再賦給原來的nil sliceempty slice,然后搖身一變,成為“真正”的 slice 了。

綜上,本篇文章介紹了數組和切片的相關知識,并著重分析了切片的擴容原理,了解這些對于我們恰當使用切片進行開發并規避一些如底層數組改變而引發的切片問題也是有所幫助的。

參考文章:

https://qcrao91.gitbook.io/go/shu-zu-he-qie-pian

https://gfw.go101.org/article/container.html

到此,關于“go數組和切片的概念及用法”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

go
AI

陇川县| 榕江县| 石台县| 涡阳县| 饶平县| 高安市| 芜湖市| 司法| 布拖县| 聊城市| 成安县| 昭平县| 玉屏| 三台县| 峨山| 桐城市| 海门市| 永靖县| 武定县| 固原市| 台南市| 张掖市| 宁蒗| 巧家县| 延吉市| 十堰市| 鹿邑县| 桦甸市| 滦南县| 康定县| 永春县| 大庆市| 蓝田县| 社旗县| 措勤县| 萍乡市| 大英县| 江达县| 昆山市| 西峡县| 通江县|