# Golang 数组和切片

# 一、Golang 数组简介

Go 语言提供了数组类型的数据结构。

数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。

数组元素可以通过索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。数组的下标取值范围是从 0 开始,到长度减 1。

数组一旦定义后,大小不能更改。

# 二、Golang 数组声明

基本格式:

var variable_name [SIZE] variable_type
  • 指定数组大小

    var arr [5]float64  //等价于 var arr = [4]float{}
    //默认初始化为对应数据类型的“零值”
    fmt.Println(arr) //[0 0 0 0 0]
    
  • 指定数组大小并赋初始值

    var arr = [5] string{"ruby", "hedon", "rose"}  // {} 中元素个数不能大于 [] 中指定的大小
    fmt.Println(arr) // [ruby hedon rose  ] 后面有 2 个空字符串
    
    var arr = [5] int{'A', 'B', 'C', 'D', 'E'} // byte
    fmt.Println(arr) // [65 66 67 68 69]
    
  • 指定数组大小并按索引赋初始值

    arr := [5] int{4: 100}   //表示 arr[4] 初始化为 100
    fmt.Println(arr)// [0 0 0 0 100]
    
  • 根据元素个数,自动识别数组大小

    var arr = [...]int{1,2,3,4,5}
    fmt.Println(len(arr)) //5
    
  • 根据最大的索引赋值来确定数组大小

    arr := [...]int{0:1, 4:1, 9:1}  //会根据最后一个 9:1 来判断数组大小为 10
    fmt.Println(len(arr))  //10
    fmt.Println(arr) //[1 0 0 0 1 0 0 0 0 1]
    
  • 多维数组

    arr := [3][4]int{
      {1,2,3,4},
      {5,6,7,8},
      {9,10,11,12},  //需要逗号
    }
    
    fmt.Println(arr[0][1]) //2
    

# 三、Golang 数组遍历

# 1. for ... i

arr := [5]int{1,2,3,4,5}
for i := 0; i < len(arr); i++{
  fmt.Println(arr[i])
}
/*
  1
  2
  3
  4
  5
*/

# 2. for ... range

arr := [5]int{1,2,3,4,5}
for i, v := range arr {
  fmt.Printf("index=%d, value=%\n", i, v)
}

/*
	index=0, value=1
  index=1, value=2
  index=2, value=3
  index=3, value=4
  index=4, value=5
*/

arr := [5]int{1,2,3,4,5}
for _, v := range arr {  //如果不需要用到索引,则可以使用 _ 来忽略索引
   //...
}

# 四、Golang 数组是值类型

Go 中的数组是值类型,而不是引用类型。这意味着:

  1. 当它们被分配给一个新变量时,将把原始数组的副本分配给新变量。如果对新变量进行了更改,则不会在原始数组中反映。

    arr1 := [5]int{1,2,3,4,5}
    arr2 := arr1
    arr2[0] = 10086
    fmt.Println(arr1) //[1 2 3 4 5]
    fmt.Println(arr2) //[10086 2 3 4 5]
    
  2. 函数或方法传参时传一个数组,在函数或方法内部对数组进行修改,是不会影响外面的数组的值的。

    func main() {
    	arr := [5]int{1,2,3,4,5}
    	fmt.Println("main 传参前:", arr)
    	test(arr)
    	fmt.Println("main 传参后:", arr)
    }
    
    func test(arr [5]int)  {
    	arr[0] = 10086
    	fmt.Println("test 内部修改后:", arr)
    }
    

    输出:

    main 传参前:    [1 2 3 4 5]
    test 内部修改后: [10086 2 3 4 5]
    main 传参后:    [1 2 3 4 5]
    
  3. 数组的大小是类型的一部分,因此 [5]int 和 [3]int 是不同的类型。因此数组的大小的不能调整的。这非常鸡肋,所以一般不怎么会用数组,一般都是用切片 slice。

# 五、Golang 切片简介

Go 语言切片是对数组的抽象。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片(动态数组),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

切片是一种方便、灵活且强大的包装器。切片本身没有任何数据。它们只是对现有数组的引用

切片与数组相比,不需要设定长度,在 [] 中不用设定值,相对来说比较自由。

从概念上面来说 slice 像一个结构体,这个结构体包含了三个元素:

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}
  1. 指针 ptr:指向数组中 slice 指定的开始位置
  2. 长度 len:即 slice 的长度
  3. 最大长度 cap:也就是 slice 开始位置到数组的最后位置的长度

img

# 六、Golang 切片声明

基本格式:

var slice = []type 										//此时 slice 为 nil
var slice = []type{}  								//此时 slice 不为 nil
var slice []type = make([]type, len) 	//此时 slice 不为 nil
slice := make([]type, len)
slice := make([]type, len, cap)

slice := arr[startIndex:endIndex]  	//从一个数组中开一个切片,不包括 endIndex
slice := arr[:endIndex] 	 					//从头到 endIndex,不包括 endIndex
slice := arr[startIndex:] 					//从 startIndex 到尾

slice2 := slice1[startIndex:endIndex]  //从一个切片中再开一个切片

示例:

package main

import "fmt"

func main() {
	arr := [7]int{1,2,3,4,5,6,7}
	slice := arr[2:4]
	fmt.Println(slice) //[3,4]
  
  //虽然 len(slice)=2,但是 cap(slice)=5,所以 slice[0:4] 还是可以取到值的
  slice2 := slice[0:4]
  fmt.Println(slice2) //[3,4,5,6]
}

# 七、Golang 切片是数组的引用

slice 本质上是 array 的一个引用,所以对 slice 的修改都会反映到其引用到的 array、以及其他引用该 array 的 slice。

package main

import "fmt"

func main() {
	arr := [7]int{1,2,3,4,5,6,7}
	fmt.Println("修改前:")
	fmt.Println("arr:", arr)
	slice1 := arr[2:4]
	slice2 := slice1[0:4]
	slice1[0] = 10086
	fmt.Println("修改后:")
	fmt.Println("arr:", arr)
	fmt.Println("slice1:", slice1)
	fmt.Println("slice2:", slice2)
}

输出:

修改前:
arr: [1 2 3 4 5 6 7]
修改后:
arr: [1 2 10086 4 5 6 7]
slice1: [10086 4]
slice2: [10086 4 5 6]

# 八、Golang 切片常用方法

# 1. len

返回 slice 中元素的数量。

arr := [5]int{1,2,3,4,5}
slice := arr[2:4]
fmt.Println(len(slice)) //2

# 2. cap

切片的容量是从创建切片的索引开始的底层数组中元素的数量。

arr := [5]int{1,2,3,4,5}
slice := arr[2:4]
fmt.Println(cap(slice)) //3

# 3. append

append 向 slice 里面追加一个或者多个元素,然后返回一个和 slice 一样类型的 slice。

append 函数会改变 slice 所引用的数组的内容,从而影响到引用同一数组的其它 slice。

但当 slice 中没有剩余空间,即 cap-len == 0 时,此时将动态分配新的数组空间。返回的 slice 数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的 slice 则不受影响。

package main

import "fmt"

func main() {
   var numbers []int
   printSlice(numbers)

   /* 允许追加空切片 */
   numbers = append(numbers, 0)
   printSlice(numbers)

   /* 向切片添加一个元素 */
   numbers = append(numbers, 1)
   printSlice(numbers)

   /* 同时添加多个元素 */
   numbers = append(numbers, 2,3,4)
   printSlice(numbers)

	 /* 添加另外一个切片 */
   slice2 := []int {10,11,12,13}
   numbers = append(numbers, slice2...)
   printSlice(numbers)
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

输出:

len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=9 cap=12 slice=[0 1 2 3 4 10 11 12 13]

# 4. copy

copy 从源 slice 的 src 中复制元素到目标 dist,并且返回复制的元素的个数。

package main

import "fmt"

func main() {
	var numbers []int
	printSlice(numbers)

	/* 追加一个切片 */
	numbers = append(numbers, []int{1,2,3}...)
	printSlice(numbers)

	/* 声明一个新切片 */
	numbers1 := make([]int, len(numbers), cap(numbers) * 2)
	printSlice(numbers1)

	/* 将 numbers 拷贝到 numbers1 */
	copy(numbers1, numbers)
	printSlice(numbers1)
}

func printSlice(x []int){
	fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

输出:

len=0 cap=0 slice=[]
len=3 cap=3 slice=[1 2 3]
len=3 cap=6 slice=[0 0 0]
len=3 cap=6 slice=[1 2 3]

numbers1 与numbers 两者不存在联系,numbers 发生变化时,numbers1 是不会随着变化的。

也就是说 copy 方法是不会建立两个切片的联系的。

# 5. sort

sort 包中内含支持各种类型切片的排序函数。

image-20210721002447168

package main

import (
	"fmt"
	"sort"
)

func main() {
	s1 := []int{5,7,2,3,1,67}
	fmt.Println("排序前:", s1)
  //内置的排序算法
	sort.Ints(s1)
	fmt.Println("排序后:", s1)
  //降序,第二个参数传排序规则
	sort.Slice(s1, func(i, j int) bool {
		return i > j
	})
	fmt.Println("反排序:", s1)
}

输出:

排序前: [5 7 2 3 1 67]
排序后: [1 2 3 5 7 67]
反排序: [67 7 5 3 2 1]

# 九、Golang 切片原理

# 1. 扩容机制

  • append 单个元素,或者 append 少量的多个元素,这里的少量指 double 之后的容量能容纳:

    • 当 cap < 1024 的时候,每次 × 2
    • 当 cap >= 1024 的时候,每次 × 1.25

    当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的 1.25 倍。 这个结论是不对的~

    growslice 方法里面会调用 roundupsize 方法内存对齐,进行内存对齐之后,新 slice 的容量是要 大于等于 老 slice 容量的 2 倍或者 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("before cap = %-4d  |  after append %-4d  cap = %-4d\n",  oldCap, i, newCap)
    			oldCap = newCap
    		}
    	}
    }
    

    输出:

    before cap = 0     |  after append 0     cap = 1
    before cap = 1     |  after append 1     cap = 2
    before cap = 2     |  after append 2     cap = 4
    before cap = 4     |  after append 4     cap = 8
    before cap = 8     |  after append 8     cap = 16
    before cap = 16    |  after append 16    cap = 32
    before cap = 32    |  after append 32    cap = 64
    before cap = 64    |  after append 64    cap = 128
    before cap = 128   |  after append 128   cap = 256
    before cap = 256   |  after append 256   cap = 512
    before cap = 512   |  after append 512   cap = 1024
    before cap = 1024  |  after append 1024  cap = 1280
    before cap = 1280  |  after append 1280  cap = 1696 //这里开始就不是 1.25倍增长,大于1.25倍。
    before cap = 1696  |  after append 1696  cap = 2304
    
  • 若是 append 多个元素且 double 后的容量不能容纳,直接使用预估的容量

    var s []int
    s = append(s, 0, 1, 2) //len=3, cap=3
    
上次更新: 10/12/2021, 7:12:39 AM