数组的拷贝是 深拷贝(只有长度相同的数组之间才能相互拷贝),切片的拷贝是 浅拷贝。
数组与切片在内存形式上的区别:
数组只有“体”,切片除了“体”之外,还有“头”部。切片的头部和内容体是分离的,使用指针关联起来。
1. 数组:
Go语言里面的数组(array)其实很不常用,这是因为数组是定长的、静态的,一旦定义好长度就无法更改,而且不同长度的数组属于不同的类型,之间不能相互转换相互赋值,用起来多有不方便之处。
切片(slice)是动态的数组,是可以扩充内容增加长度的数组。
当长度不变时,它用起来和普通数组一样。当长度不同时,它们也属于相同的类型,之间可以相互赋值。
这就决定了数组的应用领域都广泛的被切片取代了。
切片是数组的一个包装,数组是切片的底层实现,切片的特殊语法隐藏了内部的细节,让用户不能直接看到内部隐藏的数组。
1.1 数组的定义:
package main
import "fmt"
func main() {
var a [9]int //默认初始化为零值
fmt.Println(a)
}
------
[0 0 0 0 0 0 0 0 0]
数组的另外三种定义方式:
func main() {
var a [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
var b [9]int = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
c := [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
}
1.2 数组的访问:(通过 数组下标 访问)
package main
import "fmt"
func main() {
var squares [9]int
for i := 0; i < len(squares); i++ {
squares[i] = i + 1
}
fmt.Println(squares)
}
------
[1 2 3 4 5 6 7 8 9]
1.3 数组的下标越界检查:
package main
import "fmt"
func main() {
var a = [5]int{1, 2, 3, 4, 5}
a[101] = 255
fmt.Println(a)
}
------
> go run test.go
# command-line-arguments
./2test.go:6:3: invalid array index 101 (out of bounds for 5-element array)
上述示例程序在编译时会报错 数组越界,说明Go语言会对数组访问下标越界进行编译器检查。
如果数组下标是常量,则在编译期间即可检查出数组越界;但如果数组下标是变量,则需要在执行器才会检查数据越界,这种情况下Go会在 编译后 的代码中插入下标检查的逻辑,所以数组的下标访问效率是要打折扣的,比不得C语言的数据访问性能。
package main
import "fmt"
func main() {
var a = [5]int{1, 2, 3, 4, 5}
var b int = 101
a[b] = 255
fmt.Println(a)
}
------
go build test.go //没问题,顺利编译通过
./test //执行,则会报错
panic: runtime error: index out of range [101] with length 5
goroutine 1 [running]:
main.main()
/Users/xuesong/Documents/TEST/test.go:7 +0x1d
1.4 数组拷贝:
只有长度相同的数组之间才能相互赋值(深拷贝);长度不同的数组之间是禁止相互赋值的,因为它们属于不同的类型。
package main
import "fmt"
func main() {
var a [9]int = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
var b [9]int
b = a
a[0] = 12345
fmt.Println(a)
fmt.Println(b)
}
------
12345 2 3 4 5 6 7 8 9]
[1 2 3 4 5 6 7 8 9]
1.5 数组的遍历:(range)
除了可以使用下标对数组进行遍历之外,还可以使用 range
关键字来遍历。
range有两种使用形式:
package main
import "fmt"
func main() {
var a [5]int = [5]int{1, 2, 3, 4, 5}
for index := range a {
fmt.Println(index, a[index])
}
for index, value := range a {
fmt.Println(index, value)
}
}
2. 切片:
一个切片变量包含三个域,分别是:
底层数组的指针、切片的长度(length)、切片的容量(capacity)。
切片支持append操作 可以将新的内容追加到底层数组,即下图中灰色的各自(尚未被填充的切片容量空间)。如果格子满了,即切片的空间已全被使用时,底层的数组就会更换。
2.1 切片的创建:(make函数)
package main
import "fmt"
func main() {
var s1 []int = make([]int, 5, 8)
var s2 []int = make([]int, 8) //满容切片
var s3 []int = []int{1, 2, 3, 4, 5} //不使用make函数,此时创建的切片是满容的
fmt.Println(s1, len(s1), cap(s1))
fmt.Println(s2, len(s2), cap(s2))
fmt.Println(s3, len(s3), cap(s3))
}
------
[0 0 0 0 0] 5 8
[0 0 0 0 0 0 0 0] 8 8
[1 2 3 4 5] 5 5
make函数创建切片,需要提供三个参数,分别是:切片的类型、切片的长度、切片的容量。
其中,第三个参数可选,如果不提供第三个参数,则长度和容量相等,即切片是满容的。
make的作用在于可以创建 非满容 的切片,即给切片预留一部分空间,避免扩容重新申请内存、数组拷贝造成的性能影响。 如果不使用make 则第二种方式创建的切片只能是满容的。
Go语言提供了内置函数 len()
和 cap()
可以直接获得切片的 长度 和 容量 属性。
2.3 切片的复制:
切片的复制是 浅拷贝的操作。
切片的底层是一个数组,切片的表层是一个包含三个变量的结构体,当我们将一个切片赋值给另一个切片时,本质上是对切片表层结构体的浅拷贝。
结构体中的第一个变量是一个指针,指向底层的数组,另外两个变量分别是切片的长度和容量。
type slice struct {
array unsafe.Pointer
length int
capacity int
}
2.4 切片的追加:
切片是动态的数组,长度不固定,可以对其进行“追加”操作。
如果追加后底层数组没有扩容,那么追加前后的两个切片变量共享底层数组;
如果追加后底层数组发生扩容,那么会导致数组的分离,即新建一个数组,追加前后的两个切片变量的底层数组 不同享。
package main
import "fmt"
func main() {
var s1 = []int{1,2,3,4,5}
fmt.Println(s1, len(s1), cap(s1))
var s2 = append(s1, 6) //对满容切片s1追加新元素,会导致底层的数组分离,
即新建一个数组,s2与s1分别指向两个独立的底层数组
fmt.Println(s1, len(s1), cap(s1))
fmt.Println(s2, len(s2), cap(s2)) //s2的容量是10
var s3 = append(s2, 7) //对非满容切片s2追加新元素,不会导致底层数组分离
,s2与s3指向同一个底层数组
fmt.Println(s2, len(s2), cap(s2))
fmt.Println(s3, len(s3), cap(s3))
}
------
[1 2 3 4 5] 5 5
[1 2 3 4 5] 5 5
[1 2 3 4 5 6] 6 10
[1 2 3 4 5 6] 6 10
[1 2 3 4 5 6 7] 7 10
注意:
追加操作需要将append函数的返回值重新赋给原切片,否则追加操作不会生效:
func main() {
var s1 []int = []int{1,2,3}
s1 = append(s1, 4) //正确
append(s1, 5) //错误!编译报错:
//append(s1, 5) evaluated but not used
}
2.5 切片的切割:
既然是叫“切片”,自然是要支持切割操作。
切片的切割可以类比字符串的子串,子切片只是母切片的一个片段,子切片与母切片共享底层数组。
package main
import "fmt"
func main() {
var s1 []int = []int{1,2,3,4,5,6}
var s2 []int = s1[0:2]
var s3 []int = s1[:]
var s4 []int = s1[2:4]
fmt.Println(s1, len(s1), cap(s1))
fmt.Println(s2, len(s2), cap(s2))
fmt.Println(s3, len(s3), cap(s3))
fmt.Println(s4, len(s4), cap(s4))
}
------
[1 2 3 4 5 6] 6 6
[1 2] 2 6
[1 2 3 4 5 6] 6 6
[3 4] 2 4
关于切片的切割有几点需要注意:
(1)s1[:] 与普通的切片复制效果相同,同样都是共享底层数组,同样都是浅拷贝;
(2)虽然子切片与母切片共享底层数组,但是二者的capacity容量可能不同:
s1[0:2] : 如果子切片从母切片的起始位置开始,则二者容量是相同的,与子切片指定的结束位置无关;
s1[2:4] : 如果子切片的起始位置非0,比如从2开始,则子切片底层数组的指针指向2的位置,二者容量不同,如下图所示:
2.6 数组转切片:
对数组进行切割可以转换成切片,切片将原数组作为内部底层数组。也就是说修改了原数组会影响到新切片,对切片的修改也会影响到原数组。
package main
import "fmt"
func main() {
var s1 [6]int = [6]int{1, 2, 3, 4, 5, 6}
var s2 []int = s1[2:4]
fmt.Println(s1, len(s1)) //6
fmt.Println(s2, len(s2), cap(s2)) //2 4
}
------
[1 2 3 4 5 6] 6
[3 4] 2 4
2.7 切片的扩容点:
当比较短的切片扩容时,系统会多分配 100% 的空间,即扩容后的切片容量是原切片的 2倍;
当切片的长度超过 1024 时,扩容策略调整为多分配 25%(并不是一个严格的数字,需要看具体的运行环境) 的空间,这是为了避免空间的过多浪费。
package main
import "fmt"
func main() {
s1 := make([]int, 6)
s2 := make([]int, 1024)
s1 = append(s1, 1)
s2 = append(s2, 1)
fmt.Println(len(s1), cap(s1))
fmt.Println(len(s2), cap(s2))
}
------
7 12
1025 1280
2.8 空切片:
package main
import (
"fmt"
"unsafe"
)
func main() {
var s1 []int
var s2 []int = []int{}
var s3 []int = make([]int, 0)
var s4 []int = *new([]int)
fmt.Println(s1, s2, s3, s4)
fmt.Println(len(s1), len(s2), len(s3), len(s4))
fmt.Println(cap(s1), cap(s2), cap(s3), cap(s4))
var a1 [3]int = *(*[3]int)(unsafe.Pointer(&s1)) //*[3]int: 切片类型相当于是一个含有3个元素的结构体类型,可以将这个结构体看成长度为3的整型
数组[3]int,所以指向s1的指针类型是 *[3]int
var a2 [3]int = *(*[3]int)(unsafe.Pointer(&s2))
var a3 [3]int = *(*[3]int)(unsafe.Pointer(&s3))
var a4 [3]int = *(*[3]int)(unsafe.Pointer(&s4))
fmt.Println(a1)
fmt.Println(a2)
fmt.Println(a3)
fmt.Println(a4)
}
------
[] [] [] []
0 0 0 0
0 0 0 0
[0 0 0]
[18400112 0 0]
[18400112 0 0]
[0 0 0]
s1 与 s4 这两种形式创建的切片是 “nil切片”, s2 与 s3 这两种形式创建的切片是 “空切片”。
切片类型结构体中的第一个元素是切片底层数组的地址,18400112 这个值(另说值为824634199592)是一个特殊的内存地址,所有类型的 空切片 都共享这一内存地址。
用图来表示“空切片”和“nil切片”如下:
nil切片 和 空切片 在使用上的区别在于类型判断上,为了避免出现异常错误,最好的办法是不要创建 空切片,这是官方的标准建议:
The former declares a nil slice value, while the latter is non-nil but zero-length.
They are functionally equivalant -- their len and cap are both zero
-- but the nil slices is the preferred style.
package main
import "fmt"
func main() {
var s1 []int //nil
var s2 []int = []int{} //空
fmt.Println(s1 == nil)
fmt.Println(s2 == nil)
fmt.Printf("%#v\n", s1)
fmt.Printf("%#v\n", s2)
}
------
true
false
[]int(nil)
[]int{}
参考内容:
https://zhuanlan.zhihu.com/p/48927056
https://zhuanlan.zhihu.com/p/49415852
https://zhuanlan.zhihu.com/p/49529590