1、数组
Go的数组是有固定个相同类型元素的数据结构,底层采用连续的内存空间存放,数组一旦声明后大小就不可改变了。
注意:Go中的数组是一种基本类型,数组的类型不仅包括其元素类型,也包括其大小,[2]int和[5]int是两个完全不同的数组类型。
创建数组
- 声明时通过字面量进行初始化
- 直接声明,不显示地进行初始化
a := [3]int{1, 2, 3}
b := [...]int{1, 2, 3}
c := [3]int{1:1, 2:3}
var d [3]int
数组名无论作为函数实参,还是作为struct嵌入字段,或者数组之间的直接赋值,都是值拷贝,不像C语言数组名因场景不同,可能是值拷贝,也可能是指针传递:C语言数组名作为函数实参传递时,直接退化为指针,int a[10]、int a[]、int *a在C语言中都是一个意思,就是一个指向int类型的指针;但是,当数组内嵌到C的struct里面时,又表现的是值拷贝的语义。
Go语言的数组不存在这种歧义,数组的一切传递都是值拷贝,体现在以下三个方面:
- 数组间的直接赋值。
- 数组作为函数参数。
- 数组内嵌到struct中。
下面以一个示例来证明这三条:
package main
import "fmt"
func main() {
a := [3]int{1, 2, 3}
b := a
a[2] = 4
fmt.Printf("%p,%v\n", &a, a)
fmt.Printf("%p,%v\n", &b, b)
f(a)
c := struct {
s [3]int
}{s: a}
d := c
c.s[2] = 20
d.s[2] = 20
fmt.Printf("%p,%v\n", &a, a)
fmt.Printf("%p,%v\n", &b, b)
fmt.Printf("%p,%v\n", &c, c)
}
func f(a [3]int) {
a[2] = 0
fmt.Printf("%p,%v\n", &a, a)
}
2、切片
切片的创建
- 通过数组创建。
array[b:e]创建一个包括e-b个元素的切片,第一个元素是array[b],最后一个元素是array[e-l]。 - make。
通过内置的make函数创建,make([]T,len,cap)中的T是切片元素类型,len是长度,cap是底层数组的容量,cap是可选参数。 - 直接声明。
可以直接声明一个切片,也可以在声明切片的过程中使用字面量进行初始化,直接声明但不进行初始化的切片其值为nil。例如:
var a []int
var a []int []int(1,2,3,4}
切片数据结构
通常我们说切片是一种类似的引用类型,原因是其存放数据的数组是通过指针间接引用的。所以切片名作为函数参数和指针传递是一样的效果。切片的底层数据结构如下:
可以看到切片的数据结构有三个成员,分别是指向底层数组的指针、切片的当前大小和底层数组的大小。当len增长超过cap时,会申请一个更大容量的底层数组,并将数据从老数组复制到新申请的数组中。
nil切片和空切片
make([]int,0)
与var a []int
创建的切片是有区别的。前者的切片指针有分配,后者的内部指针为0。示例如下:
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
var a []int
b := make([]int, 0)
if a == nil {
fmt.Println("a is nil")
} else {
fmt.Println("a is not nil")
}
if b == nil {
fmt.Println("b is nil")
} else {
fmt.Println("b is not nil")
}
as := (*reflect.SliceHeader)(unsafe.Pointer(&a))
bs := (*reflect.SliceHeader)(unsafe.Pointer(&b))
fmt.Printf("len=%d,cap=%d,type=%d\n", len(a), cap(a), as.Data)
fmt.Printf("len=%d,cap=%d,type=%d\n", len(b), cap(b), bs.Data)
}
可以看到var a[]int 创建的切片是一个nil切片(底层数据没有分配,指针指向nil),数据结构如下:
可以看到make([]int, 0)创建的是一个空切片(底层数组指针费控,但底层数据是空的),数据结构如下:
看一下makeslice底层实现代码,就是到为什么make([]int, 0)创建的是一个空切片:
func makeslice(et *_type, len, cap int) slice {
maxElements := maxSliceCap(et.size)
if len < 0 || uintptr(len) > maxElements {
panic(errorString("makeslice: len out of range"))
}
if cap < len || uintptr(cap) > maxElements {
panic(errorString("makeslice: cap out of range"))
}
p := mallocgc(et.size*uintptr(cap), et, true)
return slice{p, len, cap}
}
接下来看一下len和cap是0的情况下,mallocgc的代码片段
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
if gcphase == _GCmarktermination {
throw("mallocgc called with gcphase == _GCmarktermination")
}
if size == 0 {
return unsafe.Pointer(&zerobase)
}
...
}
多个切片引用同一个底层数组引发的混乱
切片可以由数组创建,一个底层数组可以创建多个切片,这些切片共享底层数组,使用append扩展切片过程中可能修改底层数组的元素,间接地影响其他切片的值,也可能发生数组复制重建,共用底层数组的切片,由于其行为不明朗,不推荐使用。接下来看一个示例:
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
a := []int{0, 1, 2, 3, 4, 5, 6}
b := a[0:4]
as := (*reflect.SliceHeader)(unsafe.Pointer(&a))
bs := (*reflect.SliceHeader)(unsafe.Pointer(&b))
fmt.Printf("a=%v,len=%d,cap=%d,type=%d\n", a, len(a), cap(a), as.Data)
fmt.Printf("b=%v,len=%d,cap=%d,type=%d\n", b, len(b), cap(b), bs.Data)
b = append(b, 10, 11, 12)
fmt.Printf("a=%v,len=%d,cap=%d\n", a, len(a), cap(a))
fmt.Printf("b=%v,len=%d,cap=%d\n", b, len(b), cap(b))
b = append(b, 13, 14)
as = (*reflect.SliceHeader)(unsafe.Pointer(&a))
bs = (*reflect.SliceHeader)(unsafe.Pointer(&b))
fmt.Printf("a=%v,len=%d,cap=%d,type=%d\n", a, len(a), cap(a), as.Data)
fmt.Printf("a=%v,len=%d,cap=%d,type=%d\n", b, len(b), cap(b), bs.Data)
}
a=[0 1 2 3 4 5 6],len=7,cap=7,type=842350559488
b=[0 1 2 3],len=4,cap=7,type=842350559488
a=[0 1 2 3 10 11 12],len=7,cap=7
b=[0 1 2 3 10 11 12],len=7,cap=7
a=[0 1 2 3 10 11 12],len=7,cap=7,type=842350559488
a=[0 1 2 3 10 11 12 13 14],len=9,cap=14,type=842350837872
问题总结:多个切片共享一个底层数组,其中一个切片的append操作可能引发如下两种情况。
- append追加的元素没有超过底层数组的容量,此种append操作会直接操作共享的底层数组,如果其他切片有引用数组被覆盖的元素,则会导致其他切片的值也隐式地发生变化。
- append追加的元素加上原来的元素如果超出底层数组的容量,则此种append操作会重新申请新数组,并将原来数组值复制到新数组。由于有这种二义性,所以在使用切片的过程中应该尽量避免多个切面共享底层数组,可以使用copy进行显式的复制。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)