Go语言:SliceHeader,slice 如何高效处理数据?

简介: 数组被声明之后,它的大小和内部元素的类型就不能再被改变因为在 Go 语言中,函数之间的参数传递是值传递,数组作为参数的时候,会将其复制一份,如果它非常大,会造成大量的内存浪费

数组

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 内存是只读的。
相关文章
|
5天前
|
存储 JSON 监控
Viper,一个Go语言配置管理神器!
Viper 是一个功能强大的 Go 语言配置管理库,支持从多种来源读取配置,包括文件、环境变量、远程配置中心等。本文详细介绍了 Viper 的核心特性和使用方法,包括从本地 YAML 文件和 Consul 远程配置中心读取配置的示例。Viper 的多来源配置、动态配置和轻松集成特性使其成为管理复杂应用配置的理想选择。
23 2
|
3天前
|
Go 索引
go语言中的循环语句
【11月更文挑战第4天】
12 2
|
3天前
|
Go C++
go语言中的条件语句
【11月更文挑战第4天】
14 2
|
7天前
|
程序员 Go
go语言中的控制结构
【11月更文挑战第3天】
84 58
|
6天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
3天前
|
Go
go语言中的 跳转语句
【11月更文挑战第4天】
10 4
|
3天前
|
JSON 安全 Go
Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!
本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。
14 1
|
7天前
|
Go 数据处理 API
Go语言在微服务架构中的应用与优势
本文摘要采用问答形式,以期提供更直接的信息获取方式。 Q1: 为什么选择Go语言进行微服务开发? A1: Go语言的并发模型、简洁的语法和高效的编译速度使其成为微服务架构的理想选择。 Q2: Go语言在微服务架构中有哪些优势? A2: 主要优势包括高性能、高并发处理能力、简洁的代码和强大的标准库。 Q3: 文章将如何展示Go语言在微服务中的应用? A3: 通过对比其他语言和展示Go语言在实际项目中的应用案例,来说明其在微服务架构中的优势。
|
7天前
|
Go 数据处理 调度
探索Go语言的并发模型:Goroutines与Channels的协同工作
在现代编程语言中,Go语言以其独特的并发模型脱颖而出。本文将深入探讨Go语言中的Goroutines和Channels,这两种机制如何协同工作以实现高效的并发处理。我们将通过实际代码示例,展示如何在Go程序中创建和管理Goroutines,以及如何使用Channels进行Goroutines之间的通信。此外,本文还将讨论在使用这些并发工具时可能遇到的常见问题及其解决方案,旨在为Go语言开发者提供一个全面的并发编程指南。
|
5天前
|
Go 调度 开发者
探索Go语言中的并发模式:goroutine与channel
在本文中,我们将深入探讨Go语言中的核心并发特性——goroutine和channel。不同于传统的并发模型,Go语言的并发机制以其简洁性和高效性著称。本文将通过实际代码示例,展示如何利用goroutine实现轻量级的并发执行,以及如何通过channel安全地在goroutine之间传递数据。摘要部分将概述这些概念,并提示读者本文将提供哪些具体的技术洞见。