Golang进阶: 数组、切片、字符串
1.关系
数组、切片和字符串有着密切的关系。切片和字符串的底层都是基于数组实现的。
2.数组
-
定义
-
固定长度的相同数据类型的元素组成的;
-
长度是数组类型的组成部分,比如
[3]int
和[5]int
不是相同的数据类型; -
长度不同,其对应的指针类型也不同;
-
Go
语言中数组是值语义。一个数组变量即表示整个数组,它并不是隐式的指向第一个元素的指针(比如 C 语言的数组),而是一个完整的值。这句话我不是很理解,我执行下面这段示例,发现数组的地址值就是指向的数组元素第一个的地址值
func Test_A_01(t *testing.T) { a := [3]int{} b := [5]int{} aa := &a bb := &b aaa := a fmt.Printf("%T,%T,%T,%T\n", a, aa, b, bb) fmt.Printf("%p,%p,%p,%p\n", &a, &a[0], &a[1], &a[2]) fmt.Printf("%p,%p,%p,%p\n", &aa, &aa[0], &aa[1], &aa[2]) fmt.Printf("%p,%p,%p,%p\n", &aaa, &aaa[0], &aaa[1], &aaa[2]) }
[root@CentOS upday]# go test -run Test_A_01 [3]int,*[3]int,[5]int,*[5]int 0xc00001a120,0xc00001a120,0xc00001a128,0xc00001a130 0xc000012068,0xc00001a120,0xc00001a128,0xc00001a130 0xc00001a138,0xc00001a138,0xc00001a140,0xc00001a148 PASS ok upday 0.002s
-
-
空数组
就是长度为0的数组,在内存中不占用空间,用于一些特殊的操作,比如管道同步。
-
基本格式
func Test_A_02(t *testing.T) { a := [0]int{} b := make(chan [0]int) c := make(chan struct{}) go func() { res := <-b fmt.Printf("res[b]:%T,%p\n", res, &res) }() go func() { res := <-c fmt.Printf("res[c]:%T,%p\n", res, &res) }() go func() { b <- [0]int{} c <- struct{}{} // struct{} 表示数据类型 {} 表示数据的值 }() fmt.Printf("%T,%p,%d,%d\n", a, &a, cap(a), len(a)) time.Sleep(time.Second * 3) }
[root@CentOS upday]# go test -run Test_A_02 [0]int,0x63f888,0,0 res[b]:[0]int,0x63f888 res[c]:struct {},0x63f888 PASS ok upday 3.002s
-
3.字符串
字符串一个不可改变的字节序列,字符串通常是用来包含人类可读的文本数据
-
底层实现
底层是一个只读属性长度固定的字节数组,使用
reflect.StringHeader
结构体封装。实现代码:
type StringHeader struct { Data uintptr Len int }
Date
存储的是一个地址值,指向存储字符串的字节数组的地址值Len
存储的是字节长度,不等于字符个数
-
编码
Go
使用的是UTF-8
编码,在遍历的时候最好使用for range
,否则可能会有乱码风险。func Test_A_03(t *testing.T) { str := "HGNU黄冈师范学院" for i := 0; i < len(str); i++ { fmt.Printf("%c", str[i]) } fmt.Println() for _, c := range str { fmt.Printf("%c", c) } fmt.Println() fmt.Println(len(str)) }
[root@CentOS upday]# go test -run Test_A_03 HGNUé» åå¸èå¦é¢ HGNU黄冈师范学院 22 PASS ok upday 0.002s
-
升级
在官方文档中说,在新代码中,将会把
StringHeader
替换为unsafe.Slice
或者unsafe.SliceData
In new code, use unsafe.Slice or unsafe.SliceData instead.
4. 切片
切片就是长度不固定的数组
-
底层
由一个
reflect.SliceHeader
结构体封装,相对于字符串的结构体,多了一个Cap
属性实现代码:
type SliceHeader struct { Data uintptr Len int Cap int }
Date
存储的是一个地址值,指向存储数据的数组的地址值Len
表示元素的个数Cap
表示实际占用的存储空间
在复制的时候会复制一份新的结构体,但是其内部这些值还是原来的的,
Data
指向的地址值还是原来的地址值。 -
添加元素
使用内置函数
append
如果超出最大存储内存
cap
,将会重新分配内存。func Test_A_06(t *testing.T) { a := make([]int, 4, 5) b := a[:2] fmt.Printf("%#v,%T,%p,%p,%d\n", a, a, &a, &a[0], cap(a)) a = append(a, 999) fmt.Printf("%#v,%T,%p,%p,%d\n", a, a, &a, &a[0], cap(a)) a = append(a, 888) // 这里超出了超出容量了,重新分配内存 fmt.Printf("%#v,%T,%p,%p,%d\n", a, a, &a, &a[0], cap(a)) fmt.Printf("%#v,%T,%p,%p,%d\n", b, b, &b, &b[0], cap(b)) }
[root@CentOS upday]# go test -run Test_A_06 []int{0, 0, 0, 0},[]int,0xc0000100c0,0xc00001c120,5 []int{0, 0, 0, 0, 999},[]int,0xc0000100c0,0xc00001c120,5 []int{0, 0, 0, 0, 999, 888},[]int,0xc0000100c0,0xc0000a2000,10 []int{0, 0},[]int,0xc0000100d8,0xc00001c120,5 PASS ok upday 0.002s
-
删除元素
先取值然后重新赋值
func Test_A_07(t *testing.T) { a := []int{111, 222, 333, 444, 555, 666, 777, 888, 999} fmt.Printf("%#v,%T,%p,%p,%d\n", a, a, &a, &a[0], cap(a)) a = a[2:5] fmt.Printf("%#v,%T,%p,%p,%d\n", a, a, &a, &a[0], cap(a)) }
[root@CentOS upday]# go test -run Test_A_07 []int{111, 222, 333, 444, 555, 666, 777, 888, 999},[]int,0xc0000100c0,0xc0000a2000,9 []int{333, 444, 555},[]int,0xc0000100c0,0xc0000a2010,7 PASS ok upday 0.002s
利用切片减少内存分配
原理:前面提到了一个空数组的概念,切片中也有类似的空切片,当我们有这么一个需求,把这个切片序列
a
[]byte{65, 66, 67, 0, 97, 98, 99}
中的0
删除。正常操作的话,就是定义一个新的变量,分配一个新的切片,但是分配新的切片就会有内存分配的操作。我们可以将原切片赋值给新变量,但是初始化长度为0
,然后依次判断添加。示例:
func TrimSpace(s []byte) []byte { b := s[:0] for _, x := range s { if x != 0 { b = append(b, x) } } return b } func Benchmark_A_08(b *testing.B) { for i := 0; i < b.N; i++ { a := []byte{65, 66, 67, 0, 97, 98, 99} a = TrimSpace(a) } } func TrimSpace2(s []byte) []byte { b := make([]byte, len(s)) for _, x := range s { if x != 0 { b = append(b, x) } } return b } func Benchmark_A_09(b *testing.B) { for i := 0; i < b.N; i++ { a := []byte{65, 66, 67, 0, 97, 98, 99} a = TrimSpace2(a) } }
[root@CentOS upday]# go test b_test.go -bench=A_09 -benchmem goos: linux goarch: amd64 cpu: AMD EPYC Processor Benchmark_A_09 18057982 66.92 ns/op 24 B/op 2 allocs/op PASS ok command-line-arguments 1.279s [root@CentOS upday]# go test b_test.go -bench=A_08 -benchmem goos: linux goarch: amd64 cpu: AMD EPYC Processor Benchmark_A_08 93468324 12.35 ns/op 0 B/op 0 allocs/op PASS ok command-line-arguments 1.170s
明显的看出,两次运行结果的差异,利用空切片,在原有的切片上面操作,减少内存分配。
同时,在原有切片上面操作的话,还有可能导致内存无法被回收的问题。
看下面的例子:
func Test_A_09(t *testing.T) { a := []int{111, 222, 333, 444, 555, 666, 777, 888, 999} a = a[:len(a)-1] b := a[:len(a)+1] fmt.Println(a) fmt.Println(b) }
[root@CentOS upday]# go test -run Test_A_09 [111 222 333 444 555 666 777 888] [111 222 333 444 555 666 777 888 999] PASS ok upday 0.002s
当我在原切片上面删除最后一个元素重新赋值给
a
后,然后再赋值给b
,最后一个元素还是能被访问,没有被回收。
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
gopher