小聊:本文是小白刚学习
golang
时候的总结,在基本学习了解之上的特性比较与讨论,go的数组有什么不同?切片又有什么好处?怎么去区分它们的使用?外加一些知识的拓展,有利于加深对go
的Array
和Slice
的理解,敬请阅览哦!
目录
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)go
的 Array
数组一般使用和其它语言无异,但在类型定义上有个很大的不同:数组初始化的长度也作为组成数组类型的一部分。就比如说,var arr [2]int
和 var 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]int
和 var 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
}
- 切片定义时的参数有
[]type
,len
和cap
。比如: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)
,其中array
是slice
引用的数组。
在上面的知识中我们已经知道,在数组的基础上去初始化切片,该切片就是原数组的引用,他们共享操作数据。
那么就会有这样一个问题:我切片理论是可以无限扩容增长的,但是数组不是可变的呀,所以基于数组初始化的时候长度的范围是多少?切片增加数据的时候会不会受数组固定长度的限制呢?
答案是:初始化切片长度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): 4,cap(s): 4
切片s:val: [1 2 0 0],len(s): 4,cap(s): 4
扩容后:切片s:val: [1 2 0 0 3 4],len(s): 6,cap(s): 8
扩容后:数组arr:val: [1 2 0 0],len(s): 4,cap(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 不允许对指针进行运算,不存在意外改变指针的情况。而如果是给指针赋新的值,后续的修改当然不再影响旧值指向的值。由于指针的机制透明,这点很好理解。