热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

golang学习笔记---数组/字符串/切片

数组数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。数组的长度是数组类型的组成部分。因为数组的长度是数组类型的一个部分,不同长度或不同类型的数据组成的数组都是
  数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元
素组成。数组的长度是数组类型的组成部分。因为数组的长度是数组类型的一个部
分,不同长度或不同类型的数据组成的数组都是不同的类型,因此在Go语言中很少
直接使用数组(不同长度的数组因为类型不同无法直接赋值)。

定义方式:

var a [3]int // 定义一个长度为3的int类型数组, 元素全部为0
var b = [...]int{1, 2, 3} // 定义一个长度为3的int类型数组, 元素为 1, 2, 3
var c = [...]int{2: 3, 1: 2} // 定义一个长度为3的int类型数组, 元素为 0, 2, 3
var d = [...]int{1, 2, 4: 5, 6} // 定义一个长度为6的int类型数组, 元素为 1, 2, 0, 0, 5, 6

  

Go语言中数组是值语义。一个数组变量即表示整个数组,它并不是隐式的指向第一
个元素的指针(比如C语言的数组),而是一个完整的值。当一个数组变量被赋值

或者被传递的时候,实际上会复制整个数组。如果数组较大的话,数组的赋值也会
有较大的开销。为了避免复制数组带来的开销,可以传递一个指向数组的指针,但
是数组指针并不是数组。

var a = [...]int{1, 2, 3} // a 是一个数组
var b = &a // b 是指向数组的指针
fmt.Println(a[0], a[1]) // 打印数组的前2个元素
fmt.Println(b[0], b[1]) // 通过数组指针访问数组元素的方式和数组类似
for i, v := range b { // 通过数组指针迭代数组的元素
fmt.Println(i, v)
}

对于数组类型来说, len 和 cap 函数返回的结果始终是一
样的,都是对应数组类型的长度。

遍历数组:

for i := range a {
fmt.Printf("a[%d]: %d\n", i, a[i])
}
for i, v := range b {
fmt.Printf("b[%d]: %d\n", i, v)
}
for i := 0; i  

用 for range 方式迭代的性能可能会更好一些,因为这种迭代可以保证不会出现
数组越界的情形,每轮迭代对数组元素的访问时可以省去对下标越界的判断。

用 for range 方式迭代,还可以忽略迭代时的下标:

var times [5][0]int
for range times {
fmt.Println("hello")
}

其中 times 对应一个 [5][0]int 类型的数组,虽然第一维数组有长度,但是数
组的元素 [0]int 大小是0,因此整个数组占用的内存大小依然是0。没有付出额外
的内存代价,我们就通过 for range 方式实现了 times 次快速迭代。

数组不仅仅可以用于数值类型,还可以定义字符串数组、结构体数组、函数数组、
接口数组、管道数组等等:

// 字符串数组
var s1 = [2]string{"hello", "world"}
var s2 = [...]string{"你好", "世界"}
var s3 = [...]string{1: "世界", 0: "你好", }
// 结构体数组
var line1 [2]image.Point
var line2 = [...]image.Point{image.Point{X: 0, Y: 0}, image.Poin
t{X: 1, Y: 1}}
var line3 = [...]image.Point{{0, 0}, {1, 1}}
// 图像解码器数组
var decoder1 [2]func(io.Reader) (image.Image, error)
var decoder2 = [...]func(io.Reader) (image.Image, error){
png.Decode,
jpeg.Decode,
}
// 接口数组
var unknown1 [2]interface{}
var unknown2 = [...]interface{}{123, "你好"}
// 管道数组
var chanList = [2]chan int{}

空的数组:

var d [0]int // 定义一个长度为0的数组
var e = [0]int{} // 定义一个长度为0的数组
var f = [...]int{} // 定义一个长度为0的数组

长度为0的数组在内存中并不占用空间。空数组虽然很少直接使用,但是可以用于
强调某种特有类型的操作时避免分配额外的内存空间,比如用于管道的同步操作:

c1 := make(chan [0]int)
go func() {
fmt.Println("c1")
c1 <- [0]int{}
}()
<-c1

在这里,我们并不关心管道中传输数据的真实类型,其中管道接收和发送操作只是
用于消息的同步。对于这种场景,我们用空数组来作为管道类型可以减少管道元素
赋值时的开销。当然一般更倾向于用无类型的匿名结构体代替:

c2 := make(chan struct{})
go func() {
fmt.Println("c2")
c2 <- struct{}{} // struct{}部分是类型, {}表示对应的结构体值
}()
<-c2

 

字符串 

一个字符串是一个不可改变的字节序列,字符串通常是用来包含人类可读的文本数
据。和数组不同的是,字符串的元素不可修改,是一个只读的字节数组。每个字符
串的长度虽然也是固定的,但是字符串的长度并不是字符串类型的一部分。

Go语言字符串的底层结构在 reflect.StringHeader 中定义:

type StringHeader struct {
Data uintptr
Len int
}

字符串结构由两个信息组成:第一个是字符串指向的底层字节数组,第二个是字符
串的字节的长度。字符串其实是一个结构体,因此字符串的赋值操作也就
是 reflect.StringHeader 结构体的复制过程,并不会涉及底层字节数组的复
制。

我们可以看看字符串“Hello, world”本身对应的内存结构:

 

 

字符串虽然不是切片,但是支持切片操作,不同位置的切片底层也访问的同一块内
存数据(因为字符串是只读的,相同的字符串面值常量通常是对应同一个字符串常
量):

s := "hello, world"
hello := s[:5]
world := s[7:]
s1 := "hello, world"[:5]
s2 := "hello, world"[7:]

  

字符串和数组类似,内置的 len 函数返回字符串的长度。也可以通
过 reflect.StringHeader 结构访问字符串的长度

fmt.Println("len(s):", (*reflect.StringHeader)(unsafe.Pointer(&s
)).Len) // 12
fmt.Println("len(s1):", (*reflect.StringHeader)(unsafe.Pointer(&
s1)).Len) // 5
fmt.Println("len(s2):", (*reflect.StringHeader)(unsafe.Pointer(&
s2)).Len) // 5

 

如果不想解码UTF8字符串,想直接遍历原始的字节码,可以将字符串强制转
为 []byte 字节序列后再行遍历(这里的转换一般不会产生运行时开销):

for i, c := range []byte("世界abc") {
fmt.Println(i, c)
}

  

Go语言除了 for range 语法对UTF8字符串提供了特殊支持外,还对字符串
和 []rune 类型的相互转换提供了特殊的支持。

fmt.Printf("%#v\n", []rune("世界")) // []int32{19990
, 30028}
fmt.Printf("%#v\n", string([]rune{'世', '界'})) // 世界

  

从上面代码的输出结果来看,我们可以发现 []rune 其实是 []int32 类型,这里
的 rune 只是 int32 类型的别名,并不是重新定义的类型。 rune 用于表示每个
Unicode码点,目前只使用了21个bit位。

字符串相关的强制类型转换主要涉及到 []byte 和 []rune 两种类型。每个转换
都可能隐含重新分配内存的代价,最坏的情况下它们的运算时间复杂度都
是 O(n) 。不过字符串和 []rune 的转换要更为特殊一些,因为一般这种强制类
型转换要求两个类型的底层内存结构要尽量一致,显然它们底层对应
的 []byte 和 []int32 类型是完全不同的内部布局,因此这种转换可能隐含重新
分配内存的操作。

 

切片(slice)

切片就是一种简化版的动态数组。因为动态数组的长度是不固定,切片
的长度自然也就不能是类型的组成部分了。

切片的结构定义, reflect.SliceHeader :

type SliceHeader struct {
Data uintptr
Len int
Cap int
}

  

可以看出切片的开头部分和Go字符串是一样的,但是切片多了一个 Cap 成员表示
切片指向的内存空间的最大容量(对应元素的个数,而不是字节数)。

下图是 x= []int{2,3,5,7,11} 和 y := x[1:3] 两个切片对应的内存结构。

 

 

 让我们看看切片有哪些定义方式:

var (
a []int // nil切片, 和 nil 相等, 一般用来表示一个不存在的切片
b = []int{} // 空切片, 和 nil 不相等, 一般用来表示一个空的集合
c = []int{1, 2, 3} // 有3个元素的切片, len和cap都为3
d = c[:2] // 有2个元素的切片, len为2, cap为3
e = c[0:2:cap(c)] // 有2个元素的切片, len为2, cap为3
f = c[:0] // 有0个元素的切片, len为0, cap为3
g = make([]int, 3) // 有3个元素的切片, len和cap都为3
h = make([]int, 2, 3) // 有2个元素的切片, len为2, cap为3
i = make([]int, 0, 3) // 有0个元素的切片, len为0, cap为3
)

 

和数组一样,内置的 len 函数返回切片中有效元素的长度,内置的 cap 函数返回
切片容量大小,容量必须大于或等于切片的长度。也可以通
过 reflect.SliceHeader 结构访问切片的信息(只是为了说明切片的结构,并不
是推荐的做法)。切片可以和 nil 进行比较,只有当切片底层数据指针为空时切
片本身为 nil ,这时候切片的长度和容量信息将是无效的。如果有切片的底层数
据指针为空,但是长度和容量不为0的情况,那么说明切片本身已经被损坏了(比
如直接通过 reflect.SliceHeader 或 unsafe 包对切片作了不正确的修改)。

遍历切片的方式和遍历数组的方式类似:

for i := range a {
fmt.Printf("a[%d]: %d\n", i, a[i])
}
for i, v := range b {
fmt.Printf("b[%d]: %d\n", i, v)
}
for i := 0; i  

  

在对切片本身赋值或参数传
递时,和数组指针的操作方式类似,只是复制切片头信息
( reflect.SliceHeader ),并不会复制底层的数据。对于类型,和数组的最大
不同是,切片的类型和长度信息无关,只要是相同类型元素构成的切片均对应相同
的切片类型。

添加切片元素
内置的泛型函数 append 可以在切片的尾部追加 N 个元素:

var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包

  

不过要注意的是,在容量不足的情况下, append 的操作会导致重新分配内存,可
能导致巨大的内存分配和复制数据代价。即使容量足够,依然需要用 append 函数
的返回值来更新切片本身,因为新切片的长度已经发生了变化。
除了在切片的尾部追加,我们还可以在切片的开头添加元素:

var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片

  

在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制1次。因
此,从切片的开头添加元素的性能一般要比从尾部追加元素的性能差很多。

 

由于 append 函数返回新的切片,也就是它支持链式操作。我们可以将多
个 append 操作组合起来,实现在切片中间插入元素:

var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片

  

每个添加操作中的第二个 append 调用都会创建一个临时切片,并将 a[i:] 的内
容复制到新创建的切片中,然后将临时创建的切片再追加到 a[:i] 。
可以用 copy 和 append 组合可以避免创建中间的临时切片,同样是完成添加元
素的操作:

a = append(a, 0) // 切片扩展1个空间
copy(a[i+1:], a[i:]) // a[i:]向后移动1个位置
a[i] = x // 设置新添加的元素

  

第一句 append 用于扩展切片的长度,为要插入的元素留出空间。第二
句 copy 操作将要插入位置开始之后的元素向后挪动一个位置。第三句真实地将新
添加的元素赋值到对应的位置。操作语句虽然冗长了一点,但是相比前面的方法,
可以减少中间创建的临时切片。
用 copy 和 append 组合也可以实现在中间位置插入多个元素(也就是插入一个切
片):

a = append(a, x...) // 为x切片扩展足够的空间
copy(a[i+len(x):], a[i:]) // a[i:]向后移动len(x)个位置
copy(a[i:], x) // 复制新添加的切片

  

稍显不足的是,在第一句扩展切片容量的时候,扩展空间部分的元素复制是没有必
要的。没有专门的内置函数用于扩展切片的容量, append 本质是用于追加元素而
不是扩展容量,扩展切片容量只是 append 的一个副作用。

删除切片元素
根据要删除元素的位置有三种情况:从开头位置删除,从中间位置删除,从尾部删
除。其中删除切片尾部的元素最快:

a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素

删除开头的元素可以直接移动数据指针:

a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素

 

删除开头的元素也可以不移动数据指针,但是将后面的数据向开头移动。可以
用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完
成,不会导致内存空间结构的变化):

a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素

也可以用 copy 完成删除开头的元素:

a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素

对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以
用 append 或 copy 原地完成:

a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素

删除开头的元素和删除尾部的元素都可以认为是删除中间元素操作的特殊情况。

切片内存技巧

切片高效操作的要点是要降低内存分配的次数,尽量保证 append 操作不会超
出 cap 的容量,降低触发内存分配的次数和每次分配内存大小。

避免切片内存泄漏

可以将感兴趣的数据复制到一个新的切片中(数据的传值是Go语
言编程的一个哲学,虽然传值有一定的代价,但是换取的好处是切断了对原始数据
的依赖):

假设切片里存放的是指针对象,那么
下面删除末尾的元素后,被删除的元素依然被切片底层数组引用,从而导致不能及
时被自动垃圾回收器回收(这要依赖回收器的实现方式):

var a []*int{ ... }
a = a[:len(a)-1] // 被删除的最后一个元素依然被引用, 可能导致GC操作被阻碍

 

保险的方式是先将需要自动内存回收的元素设置为 nil ,保证自动回收器可以发
现需要回收的对象,然后再进行切片的删除操作:

var a []*int{ ... }
a[len(a)-1] = nil // GC回收最后一个元素内存
a = a[:len(a)-1] // 从切片删除最后一个元素

  

当然,如果切片存在的周期很短的话,可以不用刻意处理这个问题。因为如果切片
本身已经可以被GC回收的话,切片对应的每个元素自然也就是可以被回收的了。

 

Go语言实现中非0大小数组的长度不得超过
2GB,因此需要针对数组元素的类型大小计算数组的最大长度范围( []uint8 最
大2GB, []uint16 最大1GB,以此类推,但是 []struct{} 数组的长度可以超
过2GB)。

 


推荐阅读
  • C语言常量与变量的深入理解及其影响
    本文深入讲解了C语言中常量与变量的概念及其深入实质,强调了对常量和变量的理解对于学习指针等后续内容的重要性。详细介绍了常量的分类和特点,以及变量的定义和分类。同时指出了常量和变量在程序中的作用及其对内存空间的影响,类似于const关键字的只读属性。此外,还提及了常量和变量在实际应用中可能出现的问题,如段错误和野指针。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • This article discusses the efficiency of using char str[] and char *str and whether there is any reason to prefer one over the other. It explains the difference between the two and provides an example to illustrate their usage. ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • 本文介绍了使用哈夫曼树实现文件压缩和解压的方法。首先对数据结构课程设计中的代码进行了分析,包括使用时间调用、常量定义和统计文件中各个字符时相关的结构体。然后讨论了哈夫曼树的实现原理和算法。最后介绍了文件压缩和解压的具体步骤,包括字符统计、构建哈夫曼树、生成编码表、编码和解码过程。通过实例演示了文件压缩和解压的效果。本文的内容对于理解哈夫曼树的实现原理和应用具有一定的参考价值。 ... [详细]
  • ejava,刘聪dejava
    本文目录一览:1、什么是Java?2、java ... [详细]
  • 千万不要错过的后端[纯干货]面试知识点整理 I I
    千万不要错过的后端【纯干货】面试知识点整理IIc++内存管理上次分享整理的面试知识点I,今天我们来继续分享面试知识点整理IIlinuxkernel内核空间、内存管理、进程管理设备、 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 本文介绍了在CentOS上安装Python2.7.2的详细步骤,包括下载、解压、编译和安装等操作。同时提供了一些注意事项,以及测试安装是否成功的方法。 ... [详细]
  • C语言判断正整数能否被整除的程序
    本文介绍了使用C语言编写的判断正整数能否被整除的程序,包括输入一个三位正整数,判断是否能被3整除且至少包含数字3的方法。同时还介绍了使用qsort函数进行快速排序的算法。 ... [详细]
  • 合并列值-合并为一列问题需求:createtabletab(Aint,Bint,Cint)inserttabselect1,2,3unionallsel ... [详细]
  • 全面介绍Windows内存管理机制及C++内存分配实例(四):内存映射文件
    本文旨在全面介绍Windows内存管理机制及C++内存分配实例中的内存映射文件。通过对内存映射文件的使用场合和与虚拟内存的区别进行解析,帮助读者更好地理解操作系统的内存管理机制。同时,本文还提供了相关章节的链接,方便读者深入学习Windows内存管理及C++内存分配实例的其他内容。 ... [详细]
  • 本文分析了Wince程序内存和存储内存的分布及作用。Wince内存包括系统内存、对象存储和程序内存,其中系统内存占用了一部分SDRAM,而剩下的30M为程序内存和存储内存。对象存储是嵌入式wince操作系统中的一个新概念,常用于消费电子设备中。此外,文章还介绍了主电源和后备电池在操作系统中的作用。 ... [详细]
  • 本文介绍了GTK+中的GObject对象系统,该系统是基于GLib和C语言完成的面向对象的框架,提供了灵活、可扩展且易于映射到其他语言的特性。其中最重要的是GType,它是GLib运行时类型认证和管理系统的基础,通过注册和管理基本数据类型、用户定义对象和界面类型来实现对象的继承。文章详细解释了GObject系统中对象的三个部分:唯一的ID标识、类结构和实例结构。 ... [详细]
author-avatar
kenvilen_106
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有