数组和切片

数组

var a [3]int                    // 定义长度为3的int型数组, 元素全部为0
var b = [...]int{1, 2, 3}       // 定义长度为3的int型数组, 元素为 1, 2, 3
var c = [...]int{2: 3, 1: 2}    // 定义长度为3的int型数组, 元素为 0, 2, 3
var d = [...]int{1, 2, 4: 5, 6} // 定义长度为6的int型数组, 元素为 1, 2, 0, 0, 5, 6
  • 第一种方式是定义一个数组变量的最基本的方式,数组的长度明确指定,数组中的每个元素都以零值初始化。
  • 第二种方式定义数组,可以在定义的时候顺序指定全部元素的初始化值,数组的长度根据初始化元素的数目自动计算。
  • 第三种方式是以索引的方式来初始化数组的元素,因此元素的初始化值出现顺序比较随意。数组的长度以出现的最大的索引为准,没有明确初始化的元素依然用0值初始化。
  • 第四种方式是混合了第二种和第三种的初始化方式,前面两个元素采用顺序初始化,第三第四个元素零值初始化,第五个元素通过索引初始化,最后一个元素跟在前面的第五个元素之后采用顺序初始化。

值传递

当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。如果数组较大的话,数组的赋值也会有较大的开销。

  • copy 关键字可以完成值传递,另辟一片新的内存空间给新值

引用传递

  • 为了避免复制数组带来的开销,可以传递一个指向数组的指针,但是数组指针并不是数组,而是指向同一片内存区间的指针
  • 切片赋值,直接用切片赋值,传递的也是指针
  • 引用传递实际指向的是同一片内存空间,因此相同引用的变量之间会受到互相更新的影响
func main() {
  var a = [...]int{1, 2, 3} // a 是一个数组
  var b = a                 // b 是值传递赋值数组
  var c = a[:2]             // c 是引用传递的切片数组
  var d = &a                // d 是指向源数组的指针
  fmt.Println(a, b, c, d)   // 打印数组元素
  //[1 2 3] [1 2 3] [1 2] &[1 2 3]
  a[0] = 6
  fmt.Println(a, b, c, d)
  //[6 2 3] [1 2 3] [6 2] &[6 2 3]
  b[0] = 7
  fmt.Println(a, b, c, d)
  //[6 2 3] [7 2 3] [6 2] &[6 2 3]
  c[1] = 8
  fmt.Println(a, b, c, d)
  //[6 8 3] [7 2 3] [6 8] &[6 8 3]
  d[2] = 9
  fmt.Println(a, b, c, d)
  //[6 8 9] [7 2 3] [6 8] &[6 8 9]
}

String

值传递

直接赋值或通过切片赋值都是值传递,都会新开辟一段内存空间给新值

	s := "hello, world"
	h := s[:5]
	fmt.Println(s, h)
	//hello, world hello
	s = "ertgg"
	fmt.Println(s, h)
	//ertgg hello
	h = "asd"
	fmt.Println(s, h)
	//ertgg asd

通过指针引用内存

	s := "hello, world"
	h := &s
	fmt.Println(s, *h)
	//hello, world hello
	s = "ertgg"
	fmt.Println(s, *h)
	//ertgg ertgg

[]Rune

fmt.Printf("%#v\n", []rune("世界"))      		// []int32{19990, 30028}
fmt.Printf("%#v\n", string([]rune{'世', '界'})) // 世界
  • 这里的 rune 只是 int32 类型的别名,并不是重新定义的类型。
  • string 是只读的 byte 数据,string 字符使用 utf-8 编码,每个字符占位 1~3 bytes
  • 对于英文字符,stringrune 类型没有区别
  • 对于中文字符,rune 类型占位 4 bytes 字符,用于操作中文字符不会出现乱码

Examples:

// string & rune compare,
package main

import "fmt"

// string & rune compare,
func stringAndRuneCompare() {
  // string,
  s := "hello你好"

  fmt.Printf("%s, type: %T, len: %d\n", s, s, len(s))
  fmt.Printf("s[%d]: %v, type: %T\n", 0, s[0], s[0])
  li := len(s) - 1 // last index,
  fmt.Printf("s[%d]: %v, type: %T\n\n", li, s[li], s[li])

  // []rune
  rs := []rune(s)
  fmt.Printf("%v, type: %T, len: %d\n", rs, rs, len(rs))
}

func main() {
  stringAndRuneCompare()
}

OutPut:

hello你好, type: string, len: 11

s[0]: 104, type: uint8

s[10]: 189, type: uint8

[104 101 108 108 111 20320 22909], type: []int32, len: 7

Analysis:

  1. hello你好 占位 11 bytes(5 * 1 + 2 * 3 = 11)
  2. stringrune 时,一共 7 个 utf-8 字符,因此转换成 size = 7 的 []rune

数组切片

切片可以和 nil 进行比较,只有当切片底层数据指针为空时切片本身为 nil

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

数组切片由三部分组成:

  • 内存引用地址
  • 长度,返回切片中有效元素的长度
  • 容量,返回切片容量大小,容量必须大于或等于切片的长度,在容量使用完后会进行扩容

定义方式

var (
	a []int               // nil切片, 和 nil 相等, 一般用来表示一个不存在的切片
	b = []int{}           // 空切片, 和 nil 不相等, 一般用来表示一个空的集合
	c = []int{1, 2, 3}    // 有3个元素的切片, len和cap都为3
	d = c[:2]             // 有2个元素的切片, len为2, cap为3
	e = c[0:2:cap(c)]     // 有2个元素的切片, len为2, cap为3
	f = c[:0]             // 有0个元素的切片, len为0, cap为3
	g = make([]int, 3)    // 有3个元素的切片, len和cap都为3
	h = make([]int, 2, 3) // 有2个元素的切片, len为2, cap为3
	i = make([]int, 0, 3) // 有0个元素的切片, len为0, cap为3
)
  • 切片定义– 数组不定长
    • 通过 make 分配内存,返回的是引用类型本身
      • make是生成一个可变大小的内存块,并返回一个它的引用
    • 通过 new 分配内存,返回的是指向类型的指针,并且内存置为0
      • new 可以申请任何类型变量内存块并返回一个指针指向它

值传递

copy 复制切片数组

  • copy 值传递,实现单独的内存分配,复制出的切片和原切片处于独立的内存区,两部分数据互不影响
  • copy(dst, src) 从源数据拷贝 min(len(dst), len(src))个元素
  • 因此,拷贝的 dst 数组需要初始化长度
	a := make([]int, 0, 10)
    a = append([]int{1, 2}, a...)    //在开头添加1个切片
    a = append(a, []int{3, 4, 5}...) // 在最后插入切片

    var b = make([]int, len(a)-1) //
    copy(b[:], a[:])
    fmt.Println(a, b)
    //[1 2 3 4 5] [1 2 3 4]
    a[0] = 9
    b[1] = 10
    fmt.Println(a, b)
    //[9 2 3 4 5] [1 10 3 4]

切片通过 append 在末尾增加数据

  • 切片之间通过 append 值传递,两部分数据互不影响
  • append 时没必要初始化新切片的 lenappend 时会自动将数据依次加到新切片末尾
	a := make([]int, 0, 6) //默认0值为空 容量3
	a = append(append(append(a, 1), 2), 3)
	fmt.Printf("alen=%d acap=%d aslice=%v \n", len(a), cap(a), a)
	// alen=3 acap=6 aslice=[1 2 3]
	b := make([]int, 0) //声明一个长度为a 切片指针变量b
	b = append(b, a...)
	fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b)
	//alen=3 acap=6 aslice=[1 2 3] blen=3 bcap=3 bslice=[1 2 3]

	// append赋值值传递,两部分数据互不影响
	a[0] = 99
	fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b)
	//alen=3 acap=6 aslice=[99 2 3] blen=3 bcap=3 bslice=[1 2 3]
	b[1] = 100
	fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b)
	//alen=3 acap=6 aslice=[99 2 3] blen=3 bcap=3 bslice=[1 100 3]

赋值/切片 引用传递

容量

定义新切片时,默认采用旧切片的容量,因此已经提前分配好一片内存空间

在未超过原始内存的容量区间内,两个切片是引用传递

	// -------------------- 切片 直接引用 复制 -----------------
	a := make([]int, 0, 6) //默认0值为空 容量3
	a = append(append(append(a, 1), 2), 3)
	fmt.Printf("alen=%d acap=%d aslice=%v \n", len(a), cap(a), a)
	// alen=3 acap=6 aslice=[1 2 3]

	// 引用复制,在未超过旧切片的 cap 时,两部分指向相同的地址
	b := a
	fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b)
	//alen=3 acap=6 aslice=[1 2 3] blen=3 bcap=3 bslice=[1 2 3]

	a[0] = 99
	fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b)
	//alen=3 acap=6 aslice=[99 2 3] blen=3 bcap=6 bslice=[99 2 3]
	b[1] = 100
	fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b)
	//alen=3 acap=6 aslice=[99 100 3] blen=3 bcap=6 bslice=[99 100 3]

	// 再次新增数据,超出旧切片的容量
	b = append(append(append(b, 4), 5), 6)
	fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b)
	//alen=3 acap=6 aslice=[99 100 3] blen=6 bcap=6 bslice=[99 100 3 4 5 6]

	a[2] = 101
	b[1] = 98
	fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b)
	//alen=3 acap=6 aslice=[99 98 101] blen=6 bcap=6 bslice=[99 98 101 4 5 6]

	// a 也扩容到相同的内存空间
	a = append(a, 7)
	fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b)
	//alen=4 acap=6 aslice=[99 98 101 7] blen=6 bcap=6 bslice=[99 98 101 7 5 6]

    b = append(append(b, 7), 8)
    a[0] = 555
    b[1] = 666
    fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b)
    //alen=4 acap=6 aslice=[555 98 101 7] blen=7 bcap=12 bslice=[99 666 101 7 6 7 8]

切片扩容

当切片超出 cap 容量的时候,就会重新分配内存扩容,此时,两部分切片将不在引用同一个内存区间

	array := [6]int{0, 1, 2, 3, 4, 5}
	slice := array[1:3:4]
	fmt.Println("slice:", slice)
	//slice: [1 2]

	slice = append(slice, 6)
	fmt.Println("slice after appending 6:", slice, unsafe.SliceData(slice))
	fmt.Println("array now:", array)
	//slice after appending 6: [1 2 6] 0xc0003172c8
	//array now: [0 1 2 6 4 5]

	slice = append(slice, 7)
	fmt.Println("slice after appending 7:", slice, unsafe.SliceData(slice))
	fmt.Println("array now:", array)
	//slice after appending 7: [1 2 6 7] 0xc000317320
	//array now: [0 1 2 6 4 5]

扩容

容量增长表


Cap	[]int8	[]int32	[]int64
0	0	0	0
1			1
2		2	2
4		4	4
8	8	8	8
16	16	16	16
32	32	32	32
64	64	64	64
128	128	128	128
256	256	256	256
512	512	512	512
848			848
864		864	
896	896		
1280			1280
1344		1344	
1408	1408		
1792			1792
2048	2048	2048	
2560			2560
3072	3072	3072	3072
3408			3408
4096	4096	4096	4096
5120			5120
5376	5376		
5440		5440	
6912	6912		
7168		7168	7168
9216			9216
9472	9472		
10240		10240	10240
12288	12288		12288

当切片容量达到 512 时,扩容速度变缓 oldCap + (oldCap + 3*256) / 4

Preference

https://victoriametrics.com/blog/go-slice/ https://go.dev/src/runtime/slice.go https://cloud.tencent.com/edu/learning/course-2412-38275