6. GO之array(数组)、slice(切片)、make和new操作、map(字典)

1.array(数组)

数组是已经确定长度的。已经确定好了所使用的内存,当数组长度不够时,并不会动态的去扩容和自动的申请新的内存。

数组的声明和格式:

//一维数组
var a = [5]int{12, 34, 56, 78, 90}
//也可以这样写
b := [5]int{21, 43, 65}
fmt.Println("a的长度和容量:", len(a), cap(a))
fmt.Println("b的长度和容量:", len(b), cap(b))
fmt.Println("a数组为:", a, "b数组为:", b)

//二维数组
var c = [4][5]int{
   
   {1, 2, 3}, {4, 5, 6}}
//也可以这样写
d := [4][5]int{[5]int{1, 2, 3}, [5]int{4, 5, 6}}
fmt.Println("d的行的长度和容量:", len(d), cap(d))
fmt.Println("d的某行中列的长度和容量", len(d[1]), cap(d[1]))
fmt.Println("c数组为:", c, "d数组为:", d)

output
a的长度和容量: 5 5
b的长度和容量: 5 5
a数组为: [12 34 56 78 90] b数组为: [21 43 65 0 0]
d的行的长度和容量: 4 4
d的某行中列的长度和容量 5 5
c数组为: [[1 2 3 0 0] [4 5 6 0 0] [0 0 0 0 0] [0 0 0 0 0]] d数组为: [[1 2 3 0 0] [4 5 6 0 0] [0 0 0 0 0] [0 0 0 0 0]]

我们可以清晰的看出数组的长度和容量是一致的。数组的特性是声明即定长,无法原地进行扩容,所以Golang中一般来说需要使用到数组的场合都会使用切片来替代。

2. slice(切片)

切片的声明和数组的声明一直,只不过切片可以动态扩容。

slice声明:

var a []int
//也可以
a:=[]int{1,2,3}

切片的本质是引用底层数组头指针+当前切片长度+底层数组大小:即array、len和cap:

slice的结构:

type slice struct {
   array unsafe.Pointer    // 底层数组头指针
   len   int               // 当前切片长度 
   cap   int               // 底层数组最大容量
}

举例:

我们设置u为底层数组,s1为第一次切片。

	u := [10]byte{11, 12, 13, 14, 15, 16, 17, 18, 19, 20} // 长度为10的数组
	s1 := u[3:7]                                          // 从数组的第3个元素至第6个元素建立切片(前闭后开区间)
	fmt.Println(len(s1), cap(s1))
	fmt.Println("u=", u, "s1=", s1)

output
4 7
u= [11 12 13 14 15 16 17 18 19 20] s1= [14 15 16 17]

在这里插入图片描述

 2.我们再用该底层数组进行分片,为s2。

在这里插入图片描述

	//修改s1的值
    s1[0] = 45
	s1[3] = 89
	s2 := u[2:8]
	fmt.Println(len(s2), cap(s2))
	fmt.Println("u=", u, "s1=", s1, "s2=", s2)

output
6 8
u= [11 12 13 45 15 16 89 18 19 20] s1= [45 15 16 89] s2= [13 45 15 16 89 18]

 通过输出结果我们可以发现,两个切片作用在用一个底层数组上时,是相互影响的。

扩容机制:

看到这里你有没有这样的疑惑,既然在初始化的时候已经指定数据的大小了,那为什么还要指定预留的大小呢?这是因为 make() 使用的是一种动态数组算法,一开始先向操作系统申请一小块内存,这个就是 cap,等 cap 被 len 占用满以后就需要扩容,扩容就是动态数组再去向操作系统申请当前长度的两倍的内存,然后将旧数据复制到新内存空间中。

用代码举例:

var data []int
for i, n := 0, 20; i < n; i++ {
	data = append(data, 1)
	fmt.Printf("len=%d cap=%d\n", len(data), cap(data))
}

//output
len=1 cap=1		# 第一次扩容
len=2 cap=2		# 第二次扩容
len=3 cap=4		# 第三次扩容
len=4 cap=4
len=5 cap=8		# 第四次扩容
len=6 cap=8
len=7 cap=8
len=8 cap=8
len=9 cap=16	# 第五次扩容
len=10 cap=16
len=11 cap=16
len=12 cap=16
len=13 cap=16
len=14 cap=16
len=15 cap=16
len=16 cap=16
len=17 cap=32	# 第六次扩容
len=18 cap=32
len=19 cap=32
len=20 cap=32

通过上面的结果我们可以发现,当容量大于我们设置的最大容量时,将会以当前最大容量的二倍进行扩展,但是当扩展到一定程度上时,将会少于2倍的扩展。

slice的内置函数

  • cap():slice的最大容量,上例中的 Slice_a和Slice_b的cap()值分别为8和5。正式由于Slice_a容量为8,所以给Slice_b的赋值是有效的
  • len():slice的长度,上例已有介绍,Slice_a和Slice_b的len()值分别为3和2
  • append():向 slice 里面追加一个或者多个元素,然后返回一个和 slice 一样类型的slice
  • copy():函数 copy 从源 slice 的 src 中复制元素到目标 dst ,并且返回复制的元素的个数

3. make和new操作

make跟new都可以用于内存分配。

  • new用于各种类型的内存分配,本质上跟java的new的功能是一样的。new(T)返回了一个指针,指向新分配的类型 T 的零值。
  • make只能用于给slice、map和channel类型的内存分配,并且返回一个有初始值(非零)的 T 类型,而不是指针 T。

make声明为:

make(Type, len, cap)

其参数有三个:

  • Type:数据类型,必要参数,Type 的值只能是 slice、 map、 channel 这三种数据类型。
  • len:数据类型实际占用的内存空间长度,map、 channel 是可选参数,slice 是必要参数。
  • cap:为数据类型提前预留的内存空间长度,可选参数。所谓的提前预留是当前为数据类型申请内存空间的时候,提前申请好额外的内存空间,这样可以避免二次分配内存带来的开销,大大提高程序的性能。

 扩容存在损耗,所以尽可能在已知数据最长度的情况下声明切片的cap,避免频繁库容切换底层数组带来的性能损耗:

//没有设置cap
func BenchmarkAppendZeroSlice(b *testing.B) {
	maxNum := 10000
	for i := 0; i < b.N; i++ {
		// 空切片
		var zeroSlice []int
		for j := 0; j < maxNum; j++ {
			zeroSlice = append(zeroSlice, j)
		}
	}
}

//设置了cap
func BenchmarkAppendCapSlice(b *testing.B) {
	maxNum := 10000
	for i := 0; i < b.N; i++ {
		// 声明cap的切片
		capSlice := make([]int, 0, maxNum)
		for j := 0; j < maxNum; j++ {
			capSlice = append(capSlice, j)
		}
	}
}

其测试速度如下:

PS D:\Project\GoLang\test> go test -bench .
goos: windows
goarch: amd64
pkg: test
cpu: AMD Ryzen 5 5600U with Radeon Graphics
BenchmarkAppendZeroSlice-12        28065             41915 ns/op
BenchmarkAppendCapSlice-12        108114             11399 ns/op
  • BenchmarkAppendZeroSlice-12 意思为这个函数用的cpu核数为12,
  • 28065  是运行的次数
  • 41915 ns/op 是每次平均时间

从结果我们可以看出提前分配好cap运行一次为11399ns,而没有提前设置的话为41915ns,有很大的性能差距。所以我们应该尽量的先设置cap。

4. map

 map声明:

a := make(map[string][]int)
a["tom"] = append(a["tom"], 56, 78, 90)
a["jack"] = append(a["jack"], 67, 77, 34)
fmt.Println(a)


output
map[jack:[67 77 34] tom:[56 78 90]]

借鉴连接:

Golang原理分析:切片(slice)原理及扩容机制_go语言切片扩容原理_pbrong的博客-CSDN博客

 Go语言基础:array、slice、make和new操作、map_go语言array := make_悠悠豆的博客-CSDN博客

Golang 内置函数:make()_golang make_极客点儿的博客-CSDN博客 

猜你喜欢

转载自blog.csdn.net/qq_48480384/article/details/129944976