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 ,最后一个元素还是能被访问,没有被回收。

文章目录