小小程知识库 小小程知识库
首页
Golang
MySQL
归档
GitHub (opens new window)

xxcheng

记录美好生活~
首页
Golang
MySQL
归档
GitHub (opens new window)
  • 语言基础

    • 内置函数
    • 数组、切片、字符串
      • 1.关系
      • 2.数组
        • 定义
        • 空数组
        • 基本格式
      • 3.字符串
        • 底层实现
        • 编码
        • 升级
      • 4. 切片
        • 底层
        • 添加元素
        • 删除元素
        • 利用切片减少内存分配
  • GORM 框架

  • go-zero 微服务

  • 专题学习

  • 未整理的学习笔记

  • Golang
  • 语言基础
xxcheng
2023-07-12
目录

数组、切片、字符串

# 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])
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      [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
      
      1
      2
      3
      4
      5
      6
      7
  • # 空数组

    就是长度为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)
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      [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
      
      1
      2
      3
      4
      5
      6

# 3.字符串

字符串一个不可改变的字节序列,字符串通常是用来包含人类可读的文本数据

  • # 底层实现

    底层是一个只读属性长度固定的字节数组,使用 reflect.StringHeader 结构体封装。

    实现代码:

    type StringHeader struct {
        Data uintptr
        Len  int
    }
    
    1
    2
    3
    4
    • 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))
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    [root@CentOS upday]# go test -run Test_A_03
    HGNUé»
          åå¸èå­¦é¢
    HGNU黄冈师范学院
    22
    PASS
    ok     upday    0.002s
    
    1
    2
    3
    4
    5
    6
    7
  • # 升级

    在官方文档 (opens new window)中说,在新代码中,将会把 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
    }
    
    1
    2
    3
    4
    5
    • Date 存储的是一个地址值,指向存储数据的数组的地址值
    • Len 表示元素的个数
    • Cap 表示实际占用的存储空间

    在复制的时候会复制一份新的结构体,但是其内部这些值还是原来的的,Data 指向的地址值还是原来的地址值。

  • # 添加元素

    使用内置函数 append (opens new window)

    如果超出最大存储内存 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))
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [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
    
    1
    2
    3
    4
    5
    6
    7
  • # 删除元素

    先取值然后重新赋值

    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))
    }
    
    1
    2
    3
    4
    5
    6
    [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
    
    1
    2
    3
    4
    5

    # 利用切片减少内存分配

    原理:前面提到了一个空数组的概念,切片中也有类似的空切片,当我们有这么一个需求,把这个切片序列 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)
    	}
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    [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
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    明显的看出,两次运行结果的差异,利用空切片,在原有的切片上面操作,减少内存分配。

    同时,在原有切片上面操作的话,还有可能导致内存无法被回收的问题。

    看下面的例子:

    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)
    }
    
    1
    2
    3
    4
    5
    6
    7
    [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
    
    1
    2
    3
    4
    5

    当我在原切片上面删除最后一个元素重新赋值给 a 后,然后再赋值给 b ,最后一个元素还是能被访问,没有被回收。

#golang#字符串#切片#数组
上次更新: 2023/07/12, 18:28:56
内置函数
CRUD 接口

← 内置函数 CRUD 接口→

最近更新
01
Go:GMP模型深入理解
01-10
02
rpc学习:进阶到gRPC
01-04
03
配置
12-12
更多文章>
Theme by Vdoing | Copyright © 2019-2024 xxcheng | 浙ICP备19024050号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式