Go 语言中,数组类型包括两部分:「数组大小、数组内部元素类型」。
a1 := [1]string("微客鸟窝")
a2 := [2]string("微客鸟窝")
示例中变量 a1 的类型是 [1]string,变量 a2 的类型是 [2]string,因为它们大小不一致,所以不是同一类型。
正是因为数组有这些局限性,Go 又设计了 slice !
slice 切片的底层数据是存储在数组中的,可以说是数组的改良版,slice 是对数组的抽象和封装,它可以动态的添加元素,容量不足时可以自动扩容。
通过内置的 append 方法,可以对切片追加任意多个元素:
func main() {
s := []string{"微客鸟窝","无尘"}
s = append(s,"wucs")
fmt.Println(s) //[微客鸟窝 无尘 wucs]
}
append 方法追加元素时,如果切片的容量不够,会自动进行扩容:
func main() {
s := []string{"微客鸟窝","无尘"}
fmt.Println("切片长度:",len(s),";切片容量:",cap(s))
s = append(s,"wucs")
fmt.Println("切片长度:",len(s),";切片容量:",cap(s))
fmt.Println(s) //
}
运行结果:
切片长度: 2 ;切片容量: 2
切片长度: 3 ;切片容量: 4
[微客鸟窝 无尘 wucs]
通过运行结果我们发现,在调用 append 之前,容量是 2,调用之后容量是 4,说明自动扩容了。 扩容原理是新建一个底层数组,把原来切片内的元素拷贝到新的数组中,然后返回一个指向新数组的切片。
切片其实是一个结构体,它的定义如下:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
func main() {
a1 := [2]string{"微客鸟窝","无尘"}
s1 := a1[0:1]
s2 := a1[:]
fmt.Println((*reflect.SliceHeader)(unsafe.Pointer(&s1)).Data)
fmt.Println((*reflect.SliceHeader)(unsafe.Pointer(&s2)).Data)
}
运行结果:
824634892120
824634892120
我们发现打印出s1和s2的Data值是一样的,说明两个切片共用一个数组。所以在对切片进行操作时,使用的还是同一个数组,没有复制原来的元素,减少内存的占用,提高效率。 多个切片共用一个底层数组虽然可以减少内存占用,但是如果一个切片修改了内部元素,其他切片也会受到影响,所以切片作为参数传递的时候要小心,尽可能不要修改远切片内的元素。切片的本质是 SliceHeader,又因为函数的参数是值传递,所以传递的是 SliceHeader 的副本,而不是底层数组的副本,这样就可以大大减少内存的使用。 获取切片数组结果的三个字段的值,除了使用 SliceHeader,也可以自定义一个结构体,只有包子字段和 SliceHeader 一样就可以了:
func main() {
s := []string{"微客鸟窝","无尘","wucs"}
s1 := (*any)(unsafe.Pointer(&s))
fmt.Println(s1.Data,s1.Len,s1.Cap) //824634892104 3 3
}
type any struct {
Data uintptr
Len int
Cap int
}
对于Go 语言中的集合类型:数组、切片、map,数组和切片的取值和赋值操作相比 map 要更高效,因为它们是连续的内存操作,可以通过索引就能快速地找到元素存储的地址。在函数传参中,切片相比数组要高效,因为切片作为参数,不会把所有的元素都复制一遍,只是复制 SliceHeader 的三个字段,共用的仍是同一个底层数组。 示例:
func main() {
a := [2]string{"微客鸟窝", "无尘"}
fmt.Printf("函数main数组指针:%p\n", &a)
arrayData(a)
s := a[0:1]
fmt.Println((*reflect.SliceHeader)(unsafe.Pointer(&s)).Data)
sliceData(s)
}
func arrayData(a [2]string) {
fmt.Printf("函数arrayData数组指针:%p\n", &a)
}
func sliceData(s []string) {
fmt.Println("函数sliceData数组指针:", (*reflect.SliceHeader)(unsafe.Pointer(&s)).Data)
}
运行结果:
函数main数组指针:0xc0000503c0
函数arrayData数组指针:0xc000050400
824634049472
函数sliceData数组指针: 824634049472
可以发现:
string 底层结构 StringHeader:
// StringHeader is the runtime representation of a string.
type StringHeader struct {
Data uintptr
Len int
}
StringHeader 和 SliceHeader 一样,代表的是字符串在程序运行时的真实结构,可以看到字段仅比切片少了一个Cap属性。 []byte(s) 和 string(b) 强制转换:
func main() {
s := "微客鸟窝"
fmt.Printf("s的内存地址:%d\n", (*reflect.StringHeader)(unsafe.Pointer(&s)).Data)
b := []byte(s)
fmt.Printf("b的内存地址:%d\n", (*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)
c := string(b)
fmt.Printf("c的内存地址:%d\n", (*reflect.StringHeader)(unsafe.Pointer(&c)).Data)
}
运行结果:
s的内存地址:8125426
b的内存地址:824634892016
c的内存地址:824634891984
通过上面示例发现打印出的内存地址都不一样,可以看出[]byte(s) 和 string(b) 这种强制转换会重新拷贝一份字符串。若字符串非常大,这样重新拷贝的方式会很影响性能。
[]byte 转 string,就等于通过 unsafe.Pointer 把 *SliceHeader 转为 *StringHeader,也就是 *[]byte 转 *string。 零拷贝示例:
func main() {
s := "微客鸟窝"
fmt.Printf("s的内存地址:%d\n", (*reflect.StringHeader)(unsafe.Pointer(&s)).Data)
b := []byte(s)
fmt.Printf("b的内存地址:%d\n", (*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)
//c1 :=string(b)
c2 := *(*string)(unsafe.Pointer(&b))
fmt.Printf("c2的内存地址:%d\n", (*reflect.StringHeader)(unsafe.Pointer(&c2)).Data)
}
运行结果:
s的内存地址:1899506
b的内存地址:824634597104
c2的内存地址:824634597104
示例中,c1 和 c2 的内容是一样的,不一样的是 c2 没有申请新内存(零拷贝),c2 和变量b使用的是同一块内存,因为它们的底层 Data 字段值相同,这样就节约了内存,也达到了 []byte 转 string 的目的。 SliceHeader 有 Data、Len、Cap 三个字段,StringHeader 有 Data、Len 两个字段,所以 *SliceHeader 通过 unsafe.Pointer 转为 *StringHeader 的时候没有问题,但是反过来却不行了,因为 *StringHeader 缺少 *SliceHeader 所需的 Cap 字段,需要我们自己补上一个默认值:
func main() {
s := "微客鸟窝"
fmt.Printf("s的内存地址:%d\n", (*reflect.StringHeader)(unsafe.Pointer(&s)).Data)
b := []byte(s)
fmt.Printf("b的内存地址:%d\n", (*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)
sh := (*reflect.SliceHeader)(unsafe.Pointer(&s))
sh.Cap = sh.Len
b1 := *(*[]byte)(unsafe.Pointer(sh))
fmt.Printf("b1的内存地址:%d\n", (*reflect.StringHeader)(unsafe.Pointer(&b1)).Data)
}
运行结果:
s的内存地址:1309682
b的内存地址:824634892008
b1的内存地址:1309682