go数组Array和切片Slice全面简述理解

小聊:本文是小白刚学习 golang 时候的总结,在基本学习了解之上的特性比较与讨论,go的数组有什么不同?切片又有什么好处?怎么去区分它们的使用?外加一些知识的拓展,有利于加深对 goArraySlice 的理解,敬请阅览哦!

目录



1. go 数组Array

1.1. go Array的定义

  • 基本定义方式
var arr1 [3]int = [3]int{
    
    1, 2, 3}
var arr2 = [3]int{
    
    1, 2, 3}	 // 可省去前面[3]int,var会自动识别类型
arr3 := [3]int{
    
    1, 2, 3}      //局部定义(函数内)
arr4 := [...]int{
    
    1, 2, 3}    //初始化后会自动计算数组长度并给它,所以无区别
arr5 := [...]int{
    
    1: 1, 5: 5} //指定索引序号初始化赋值,其它索引处是int的默认值0
arr6 := [...]struct {
    
    
    name string
}{
    
    {
    
    name: "Alice"}} //结构体数组,也必须在初始化时赋予初始,可省略属性名name(注意,这是一个匿名结构体)
var arr7 *[3]int = &[3]int{
    
    } // 数组指针
var arr8 [3]*int = [3]*int{
    
    } // 指针数组
  • 多维数组
arr := [2][2]int{
    
    {
    
    1, 2}, {
    
    3, 4}}
brr := [...][2]int{
    
    {
    
    1, 2}, {
    
    3, 4}, {
    
    5, 6}}

1.2. go Array的有趣之处

(1)goArray 数组一般使用和其它语言无异,但在类型定义上有个很大的不同:数组初始化的长度也作为组成数组类型的一部分。就比如说,var arr [2]intvar brr [3]int 是不同的类型。

package main

import "fmt"

func main() {
    
    
	var arr [2]int = [2]int{
    
    }
	var brr [3]int = [3]int{
    
    }

	// 输出他们的类型
	fmt.Printf("arr: %T\n", arr) // arr: [2]int
	fmt.Printf("brr: %T\n", brr) // brr: [3]int
}

(2)go 支持 使用 ==!= 来比较两个数组。我们知道,go 数组是值类型,初始化时已经是一个固定长度的数据序列,被保存在内存中。所以 go 也可以不用遍历就直接 Println 输出整个数组的值。至此,那么它可以直接被比较不就也很好理解了么。当然,不同类型之间无法进行比较,会编译报错,比如 var a [2]intvar d [3]int

package main

import "fmt"

func main() {
    
    
	a := [2]int{
    
    1, 2}
	b := [2]int{
    
    1, 2}
	c := [2]int{
    
    3, 4}
	// d := [3]int{1, 2}

	if a == b {
    
    
		fmt.Println("a == b") // 输出:a == b
	} else {
    
    
		fmt.Println("a != b")
	}

	if a == c {
    
    
		fmt.Println("a == c")
	} else {
    
    
		fmt.Println("a != c") // 输出:a != c
	}
	// 编译报错
	// if a == d {
    
    
	// 	fmt.Println("a == d")
	// } else {
    
    
	// 	fmt.Println("a != d")
	// }
}

2. go 切片Slice

2.1. go Array的定义

  • 基本定义方式
package main
import "fmt"

func main() {
    
    
	// 创建切片方式
	s1 := []int{
    
    }
	fmt.Printf("s1: %v, 类型: %T\n", s1, &s1)                       // s1: [], 类型: *[]int
	var s2 []int = make([]int, 2)
	fmt.Printf("s2: %v, len: %v, cap: %v\n", s2, len(s2), cap(s2)) // s2: [0 0], len: 2, cap: 2
	var s3 []int = make([]int, 2, 4)
	fmt.Printf("s3: %v, len: %v, cap: %v\n", s3, len(s3), cap(s3)) // s3: [0 0], len: 2, cap: 4

	// 利用数组进行切片初始化
	arr := [...]int{
    
    0, 1, 2, 3, 4}
	var s4 []int = arr[0:2]
	fmt.Printf("s4: %v\n", s4) //s4: [0 1]
	var s5 []int = arr[:2]
	fmt.Printf("s5: %v\n", s5) //s5: [0 1]
	var s6 []int = arr[2:]
	fmt.Printf("s6: %v\n", s6) //s6: [2 3 4]
	var s7 []int = arr[:]
	fmt.Printf("s7: %v\n", s7) //s7: [0 1 2 3 4]
}

在数组的基础上初始化切片的操作:

操作 含义
var s int[] := arr[n] 切片s中索引位置为n的项
var s int[] := arr[:] 从切片s的索引位置0到 len(s)-1处所获得的切片
var s int[] := arr[low:] 从切片s的索引位置low到len(s)-1处所获得的切片
var s int[] := arr[:high] 从切片s 的索引位置О到 high 处所获得的切片,len=high
var s int[] := arr[low:high] 从切片s的索引位置low到high 处所获得的切片,len=high-low
var s int[] := arr[low:high:max] 从切片s 的索引位置 low到high 处所获得的切片,len=high-low,cap=max-low
  • 切片数组
package main

import "fmt"

func main() {
    
    
	s8 := make([][]int, 2, 4)
	fmt.Printf("s8: %v, len: %v, cap: %v\n", s8, len(s8), cap(s8)) // s8: [[] []], len: 2, cap: 4
	s9 := [][]int{
    
    
		[]int{
    
    1, 2},
		[]int{
    
    3, 4},
	}
	fmt.Printf("s9: %v, len: %v, cap: %v\n", s9, len(s9), cap(s9)) // s9: [[1 2] [3 4]], len: 2, cap: 2
}

2.2. go Slice的有趣之处

因为 切片是 go 独有的定义,内容会详细一点,我们先介绍它,然后再说怎么用,顺便讲原理,还会说到它和数组的关系。

大家都应该简单了解过切片,这里总结一下。
slice 类似于数组,可以很像数组一样使用,相比最明显的不同:它是动态数组,append 追加元素时可以自动增长。诶,有同学就会说这不就类似于Java的集合、Python 的 列表嘛。确实,语言都有共通性,但它还是有它的特点。

  • (1)Slice 根据内部策略分配长度和容量,然后实现变长方案,因此,切片可以作为一个可变的数组使用。
s := []int{
    
    1, 2}
fmt.Printf("s: %v, len: %v, cap: %v\n", s, len(s), cap(s)) // s: [1 2], len: 2, cap: 2
s = append(s, 3)
fmt.Printf("s: %v, len: %v, cap: %v\n", s, len(s), cap(s)) // s: [1 2 3], len: 3, cap: 4
  • (2)切片是引用类型,不是值类型,它是数组 Array 的一个引用。这个很重要,有关于自身的特性和与 Array 的联系。

什么是引用类型,和指针类似但不是指针,学过 c++ 好理解,就是它可以使用类似于指针的方式去操作数组,为什么是操作数组?因为切片初始化的时候底层就是数组。所以我们定义切片时,可以在数组的基础上定义切片类型,而这个切片实际操作的就是那个数组,因为切片是引用类型,它引用了这个数组。如果我们这个时候要对这个切片内的元素进行修改,它将会影响数组的内容,因为它们的内存物理地址是一样的。当然,我们使用 make 的方式创建的切片是新的数组啦。不过在固长的数组上初始化切片还是有限制的,后面会有介绍。【戳这里:什么是引用类型】

package main
import "fmt"

func main() {
    
    
	// 我们举个数组初始化的例子
	arr := [3]int{
    
    1, 1, 1}
	fmt.Printf("初始数组arr : %v\n", arr) // 初始数组arr : [1 1 1]
	s := arr[:]
	fmt.Printf("初始化后的切片s2 : %v\n", s) // 初始化后的切片s2 : [1 1 1]
	s[0] = 2
	fmt.Printf("修改后的切片s2 : %v\n", arr)  // 修改后的切片s2 : [2 1 1]
	fmt.Printf("修改后的数组arr : %v\n", arr) // 修改后的数组arr : [2 1 1]
	// 然而我们再来打印索引为0的地址确认一下:
	fmt.Printf("数组arr索引为0的地址: %p\n", &arr[0]) // 数组arr索引为0的地址: 0xc000016150
	fmt.Printf("切片s2索引为0的地址: %p\n", &s[0])    // 切片s2索引为0的地址: 0xc000016150
}
  • 切片定义时的参数有 []typelencap。比如:var s []int = make([]int, 2, 4)

如果不设置 cap 的话默认和 len 的值一样。关于长度 len、容量 cap 和源码的加长策略之间的关系,一句话:当切片 append 元素之后长度超过 cap,就会触发扩容,扩容机制源码解释正常情况是:当目前总容量小于1024时,一次触发扩容的是增加1倍当前 cap 大小,当超过1024时,一次触发扩容的是增加0.25倍当前 cap 大小。

package main
import "fmt"

func main() {
    
    
	s := make([]int, 2)
	fmt.Println("初始切片情况:")
	fmt.Printf("val: %v\nlen(s): %v,cap(s): %v\n", s, len(s), cap(s)) // len(s): 2,cap(s): 2
	fmt.Println("第一次扩容:")
	s = append(s, 100, 200)
	fmt.Printf("val: %v\nlen(s): %v,cap(s): %v\n", s, len(s), cap(s)) // len(s): 4,cap(s): 4
	fmt.Println("第二次扩容:")
	s = append(s, 300, 400)
	fmt.Printf("val: %v\nlen(s): %v,cap(s): %v\n", s, len(s), cap(s)) // len(s): 6,cap(s): 8
	fmt.Println("第三次扩容:")
	s = append(s, 500, 600, 700)
	fmt.Printf("val: %v\nlen(s): %v,cap(s): %v\n", s, len(s), cap(s)) // len(s): 9,cap(s): 16
}
  • 在数组的基础上初始化切片时,不能超出原数组固长限制。即 0 <= len(slice) <= len(array),其中 arrayslice 引用的数组。

在上面的知识中我们已经知道,在数组的基础上去初始化切片,该切片就是原数组的引用,他们共享操作数据。
那么就会有这样一个问题:我切片理论是可以无限扩容增长的,但是数组不是可变的呀,所以基于数组初始化的时候长度的范围是多少?切片增加数据的时候会不会受数组固定长度的限制呢?
答案是:初始化切片长度 len 最大为数组长度;切片扩容不会受限制,扩容的的区域是新开辟的空间,独属于切片使用。使用 a := []int{} 或者 a := make([]int, 0) 的方式就正常,因为他们是创建新的底层数组,没有初始化固长限制。

package main

import "fmt"

func main() {
    
    
	arr := [4]int{
    
    1, 2}
	fmt.Printf("数组arr:val: %v,len(s): %v,cap(s): %v\n", arr, len(arr), cap(arr))
	var s []int = arr[:]
	fmt.Printf("切片s:val: %v,len(s): %v,cap(s): %v\n", s, len(s), cap(s)) 
	s = append(s, 3, 4)
	fmt.Printf("扩容后:切片s:val: %v,len(s): %v,cap(s): %v\n", s, len(s), cap(s))         
	fmt.Printf("扩容后:数组arr:val: %v,len(s): %v,cap(s): %v\n", arr, len(arr), cap(arr)) 
}

// 输出
数组arr:val: [1 2 0 0]len(s): 4cap(s): 4
切片s:val: [1 2 0 0]len(s): 4cap(s): 4
扩容后:切片s:val: [1 2 0 0 3 4]len(s): 6cap(s): 8
扩容后:数组arr:val: [1 2 0 0]len(s): 4cap(s): 4

3. 数组和切片的使用区别

通过上面的知识梳理,说到区别这块差不多能想到、理解下面的问题和例子了。

  • 数组与数组之间的赋值是复制整个数组数据,切片与数组或者切片与切片是复制引用,不复制整个数据Java 的数组赋值却是拷贝引用
package main
import "fmt"

func main() {
    
    
	arr := [3]int{
    
    1, 2, 3}
	brr := arr
	fmt.Printf("arr[0]的地址: %p\n", &arr[0]) // arr[0]的地址: 0xc000016150
	fmt.Printf("brr[0]的地址: %p\n", &brr[0]) // brr[0]的地址: 0xc000016168
	slice1 := make([]int, 5)
	slice2 := slice1
	fmt.Printf("slice1[0]的地址: %p\n", &slice1[0]) // slice1[0]的地址: 0xc000010480
	fmt.Printf("slice2[0]的地址: %p\n", &slice2[0]) // slice2[0]的地址: 0xc000010480
}
  • 同理,函数传参时数组时值传递,切片是引用传递,所以大部分情况下使用切片(当然指针也行)会节约很多内存的消耗
package main
import "fmt"

func change1(arr [5]int) [5]int {
    
    
	for i := 0; i < 5; i++ {
    
    
		arr[i] = i
	}
	return arr
}

func change2(s []int) []int {
    
    
	for i := 0; i < 5; i++ {
    
    
		s[i] = i
	}
	return s
}

func main() {
    
    
	arr := [5]int{
    
    }
	slice := make([]int, 5)
	fmt.Printf("初始值:arr: %v\n", arr)     // 初始值:arr: [0 0 0 0 0]
	fmt.Printf("初始值:slice: %v\n", slice) // 初始值:slice: [0 0 0 0 0]
	change1(arr)
	change2(slice)
	fmt.Printf("修改后值:arr: %v\n", arr)     // 修改后值:arr: [0 0 0 0 0]
	fmt.Printf("修改后值:slice: %v\n", slice) // 修改后值:slice: [0 1 2 3 4]
}


拓展

4.1. 简单理解引用

定义:引用类型 由类型的实际值引用(类似于指针)表示的数据类型。如果为某个变量分配一个引用类型),则该变量将引用(或“指向”)原始值。不创建任何副本。引用类型包括类、接口、委托和装箱值类型。

因为指针本身也是一种引用,本来指针和引用可以合并讨论。但由于引用屏蔽了实现细节,使得程序员不一定知道对引用的操作,作用的具体是哪一部分,也就比透明的指针多了更多的意外情况需要指出。

我举个简单点的例子:“一个班里,当老师要让某个同学起来回答问题,那让大家怎么知道是哪个同学?一:叫他的名字;二:用教鞭指着他。”
那么,所有同学是内存数据,这里的名字就是指引用,教鞭就是指针。区别就是,当我要操作内存时,引用是初始化时给目标内存取好的名字,固定了就是他,而指针就是指向内存数据的物理位置,指向哪里就代表要访问哪里的数据,不过指针可以移动,指向不同内存地址拿不同的数据,而引用不行,就像名字一样初始化生出来就固定了。但是他们的作用都类似,就是找到目标数据。【戳这里跳回去


4.2. go值传递、引用传递和指针传递

以调用方法的传参为例:

值传递:拷贝了值,修改形参的值,也不会影响原来的值。

引用传递:拷贝了引用的信息,但底层的内存数据没有拷贝,操作的是同一数据。

指针传递:拷贝了指针指向的物理地址,操作同一物理地址的值。

注意:由于 Go 不允许对指针进行运算,不存在意外改变指针的情况。而如果是给指针赋新的值,后续的修改当然不再影响旧值指向的值。由于指针的机制透明,这点很好理解。



随笔

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_48489737/article/details/127178400