关于golang:Go语言SliceHeaderslice-如何高效处理数据

46次阅读

共计 4232 个字符,预计需要花费 11 分钟才能阅读完成。

数组

Go 语言中,数组类型包含两局部:数组大小、数组外部元素类型

a1 := [1]string("微客鸟窝")
a2 := [2]string("微客鸟窝")

示例中变量 a1 的类型是 [1]string,变量 a2 的类型是 [2]string,因为它们大小不统一,所以不是同一类型。

数组局限性

  • 数组被申明之后,它的大小和外部元素的类型就不能再被扭转
  • 因为在 Go 语言中,函数之间的参数传递是 值传递 ,数组作为参数的时候,会将其复制一份,如果它十分大, 会造成大量的内存节约

正是因为数组有这些局限性,Go 又设计了 slice!

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
}
  • Data 用来指向存储切片元素的数组。
  • Len 代表切片的长度。
  • Cap 代表切片的容量。
    通过这三个字段,就能够把一个数组形象成一个切片,所以不同切片对应的底层 Data 指向的可能是同一个数组。
    示例:

    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

能够发现:

  • 同一个数组传到 arrayData 函数中指针产生了变动,阐明数组在传参的时候被复制了,产生了一个新的数组。
  • 切片作为参数传递给 sliceData 函数,指针没有发生变化,因为 slice 切片的底层 Data 是一样的,切片共用的是一个底层数组,底层数组没有被复制。

string 和 []byte 互转

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
  1. b1 和 b 的内容是一样的,不一样的是 b1 没有申请新内存,而是和变量 s 应用同一块内存,因为它们底层的 Data 字段雷同,所以也节约了内存。
  2. 通过 unsafe.Pointer 把 string 转为 []byte 后,不能对 []byte 批改,比方不能够进行 b1[0]=10 这种操作,会报异样,导致程序解体。因为在 Go 语言中 string 内存是只读的。

正文完
 0