最新要闻

广告

手机

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

家电

GO语言学习笔记-数据篇 Study for Go ! Chapter four - Data

来源:博客园

持续更新 Go 语言学习进度中 ......

  1. GO语言学习笔记-类型篇 Study for Go! Chapter one - Type - slowlydance2me - 博客园 (cnblogs.com)
  2. GO语言学习笔记-表达式篇 Study for Go ! Chapter two - Expression - slowlydance2me - 博客园 (cnblogs.com)
  3. GO语言学习笔记-函数篇 Study for Go ! Chapter three - Function - slowlydance2me - 博客园 (cnblogs.com)

2023-03-08

Study for Go ! Chapter four - Data

1. String

  • 字符串是不可变字节 (byte)序列,其本身是一个复合结构


    (资料图片仅供参考)

  • 头部指针指向字节数组,但没有 NULL 结尾,默认以 UTF-8 编码 存储 Unicode 字符, 字面量里允许使用十六进制、八进制和 UTF 编码格式

  • 字符串默认值不是 nil, 而是 “”

  • 使用 “ ` ” 定义不做转义处理的原始字符串 ( raw string ),支持跨行

  • 编译器不会解析原始字符串内的注释语句,且前置缩进空格也属于字符串内容

  • 支持 “ != 、==、<、>、+、+=”操作符

  • 允许以索引号访问字节数组 (非字符),但不能获取元素地址

  • 以切片语法 (起始和结束索引号)返回字串时,其内部依旧指向原字节数组

  • 使用 for 遍历字符串时,分 byte 和 rune 两种方式

转换

  • 要修改字符串,需要将其转化为可变类型 ([ ]rune 或 [ ]byte),待完成后再转换回来,但不管如何转换都需要重新分配内存,并复制数据

  • 某些时候,转换操作会拖累算法性能,可尝试用“ 非安全 ” 方法进行改善

  • 用 append 函数, 可将 string 直接追加到 [ ]byte 内

  • 考虑到字符串只读特征,转换时复制数据到新分配内存是可以理解的。当然,性能同样重要,编译器会为某些场合进行专门优化,避免格外分配和复制操作

性能

  • 除了类型转换外,动态构建字符串也容易造成性能问题

  • 用加法操作符拼接字符串时,每次都需重新分配内存。如此,构建 “ 超大 ” 字符串时,性能就显得很差

  • 改进思路是预分配足够的内存空间。常用的方法是用 “ string. Join ”, 它会统计所有参数的长度,并一次性完成内存分配操作

  • bytes. Buffer 也能完成类似操作,且性能相当

  • 对于数量较小的字符串格式化拼接,可用 fmt. Sprintf、text/template 等方法

  • 字符串操作通常在对上分配内存,这回对 Web 等高并发应用会造成较大影响,会由大量字符串对象要做垃圾回收。建议使用 [ ]byte 缓存池,或在栈上自行拼装等方式来实现zero-garbage

Unicode

  • 类型 rune 专门用来存储 Unicode 码点 ( code point ),他是 int32 的别名, 相当于 UCS-4/UTF-32 编码格式。使用单引号的字面量,其默认类型就是 rune

  • 除 [ ]rune 外, 还可直接在 rune、byte、string 间进行转换

  • 字符串存储的字节数组,不一定就是合法的 UTF-8 文本

  • 标准库 Unicode 里提供了丰富的操作函数。除了验证函数以外,还可以用 RuneCountInString 代替 len 返回准确的 Unicode 字符数量

2. Array

  • 定义数组类型时,数组长度必须是非负整数常量表达式,长度是类型组成部分。也就是说,元素类型相同,但长度不同的数组不属于同一类型

  • 对于结构等复合类型,可省略元素初始化类型标签

  • 在定义多维数组时,仅第一维度允许使用 “ ... ”

  • 内置函数 len 和 cap 都返回第一维度长度

  • 如果元素类型支持 “ ==、!= ” 操作符,那么数组也支持此操作

指针

  • 要分清指针数组和数组指针的区别,指针数组是指元素为指针类型的数组,数组指针是获取数组变量的地址

复制

  • 与C数组变量隐式作为指针使用不同,golang 数组是值类型,赋值和传参操作都会赋值整个数组数据

  • 如果需要可以改用 指针或者 切片,以此避免数据赋值

3. Slice

  • 切片本身并非动态数组或数组指针,它内部通过指针引用底层数组,设定相关属性将数据读写操作限定在指定区域内

  • 切片本身是个只读对象,其工作机制类似于数组指针的一种包装

  • 可基于数组或者数组指针创建切片,以开始和结束索引位置确定所引用的数组片段。不支持反向索引,实际范围是一个右半开区间

  • 属性 cap 表示切片所引用数组片段的真实长度,len 用于限定可读的写元素数量。另外,数组必须addressable ,否则会引发错误。

  • 和数组一样,切片同样使用索引号访问元素内容。起始索引为0,而非对应的底层数组真实索引位置

  • 可以直接创建切片对象,不需要预先准备数组,因为是引用类型,需要使用 make 函数或显式初始化语句,它会自动完成底层数组内存分配

Attention:

注意下面两种定义方式的区别,前者仅定义了一个[ ]int类型变量,并未执行初始化操作,而后者则用初始化表达式完成了全部创建过程。

  • 切片不支持比较操作,计算元素类型支持也不行,仅能判断是否为 nil

  • 切片可获取元素地址,但不能像数组那样直接用指针访问内容

  • 入宫元素类型也是切片,那么就可以实现类似交错数组 ( jagged array )功能

  • 显然,切片是很小的结构体对象,用来代替数组传参可避免复制开销,还有,make函数允许在运行期动态指定数组长度,绕开了数组类型必须使用编译期常量的限制

  • 但是,并非所有时候都适合用切片代替数组,以为切片底层数组可能会在堆上分配内存,而且小数组在栈上拷贝的消耗也未必就比 make 代价大

reslice

  • 将切片视作 [ cap ]slice 数据源,据此创建新的切片对象,不能超出 cap ,但不受 len 限制

  • 新建切片对象依旧指向原底层数组,也就是说修改对所有关联切片可见

  • 利用 reslice 操作很容易实现一个栈式数据结构

append

  • 向切片尾部 ( slice[ len ] )添加数据,返回新的切片对象

  • 数据被追加到原底层数组。如果超出 cap 限制,则为新切片对象重新分配数组

Attention:

  • 是超出切片 cap 限制,而非底层数组长度限制,因为 cap 可小于数组长度

  • 新分配数组长度是原 cap 的 2 倍,而非原数组的 2 倍 ( 也并非总是 2 倍,对于较大的切片,会尝试扩容 1/4,以节约内存 )

  • 向 nil 切片追加数据时,会为其分配底层数组内存

  • 正因为存在重新分配底层数组的缘故,在某些场合建议预留足够多的空间,避免中途内存分配和数据复制开销

copy

  • 在两个切片对象间复制数据,允许指向同一底层数组,允许目标区间重叠,最终所复制长度以较短的切片长度 ( len )为准

  • 还可直接从字符串中复制数据到 [ ]byte

  • 如果切片长时间引用大数组中很小的片段,那么建议新建独立切片,复制出所需数据,以便原数组内存可被及时回收。

4. Map

  • 字典 (哈希表)是一种使用频率极高的数据结构,将其作为语言内置类型,从运行时层面进行优化,可获得更高校的性能

  • 作为无序键值对集合、字典要求 key 必须是支持相等运算符 (==、!=)的数据类型,比如,数字、字符串、指针、数组、结构体,以及对应接口类型

  • 字典是引用类型,使用 make 函数或初始化表达语句来创建

  • 访问不存在的键值,会默认返回零值,不会引发错误。但推荐使用 ok-idiom 模式,毕竟通过零值无法判断键值是否存在,或许存储的 value 本就是零

  • 对字典进行迭代,每次返回的键值次序都不相同

  • 函数 len 返回当前键值对数量,cap 不接受字典类型。另外因内存访问安全和哈希算法等问题,字典被设计成 “ not addressable ”,所以不能直接修改 value 成员 (结构或函数)

  • 正确的做法是返回整个 value,待修改后再设置字典键值,或直接用指针类型

  • 不能对 nil 字典进行写操作,但却能读

  • 内容为空的字典 ≠ nil

安全

  • 在迭代期间删除或新增键值是安全的

  • 运行时会对字典并发操作做出检测,如果某个任务正在对字典进行写操作,那么其他任务就不能对该字典执行并发操作 (读、写、删除),否则会导致进程崩溃

  • 上述问题可用 数据竞争 ( data race )检查,它会输出详细检测信息

性能

  • 字典对象本身是指针包装,传参时无需再次取地址

  • 在创建时预先准备足够的空间有助于提升性能,减少扩张时的内存分配和重新哈希操作

  • 对于海量小对象,应直接用字典存储键值数据拷贝,而非指针,这有助于减少需要扫描的对象数量,大幅缩短垃圾回收时间。另外,字典不会收缩内存,所以适当替换成新对象时必要的

5. Struct

  • 结构体将多个不同类型命名字段(field)序列打包成一个符合类型

  • 字段名必须唯一,可以用 “ _ ” 补位,支持使用自身指针类型成员。字段名,排列顺序属于类型组成成分。除对齐处理外,编译器不会优化,调整内存布局

  • 可以按照顺序初始化全部字段,或使用命名方式初始化指定字段

  • 可以直接定义匿名结构类型变量,或用作字段类型,但因其缺少类型标识,在作为字段类型时无法直接初始化,稍显麻烦

  • 只有在所有字段类型全部支持时,才可做相等操作

  • 可使用指针直接操作结构字段,但不能是多级指针

空结构(struct{ })

  • 空结构是指没有字段的结构类型,它比较特殊,因为无论是其自身还是作为数组元素类型,其长度都为0

  • 尽管没有分配数组内存,但依然可以操作元素,对应切片 len、cap 属性也正常

  • 实际上,这类”长度“为 0 的对象通常都指向 runtime.zerobase 变量

  • 空结构可作为通道元素类型,用于事件通知

匿名字段( anonymous field )

  • 匿名字段是指没有名字、仅有类型的字段,也被称作嵌入字段或者嵌入类型

  • 从编译器角度看,这只是隐式地以类型名作为字段名字,可直接引用匿名字段的成员,但初始化时须当作独立字段

  • 如嵌入其他包中的类型,则隐式字段名字不包括包名

  • 不仅仅时结构体,除接口指针和多级指针以外的任何命名类型都可作为匿名字段

  • 不能将基础类型和其指针类型同时嵌入,因为两者隐式名字相同

  • 虽然可以像普通字段那样访问匿名字段成员,但会存在重名问题,默认情况下,编译器从当前显式命名字段开始,逐步向内查找匿名字段成员,如果匿名字段成员被外层同名字段遮蔽,那么必须使用显式字段名

  • 如果多个相同层级的匿名字段成员重名,就只能使用显式字段名访问,因为编译器无法确定目标

  • 严格来说,golang 并不是传统意义上的面向对象编程语言,或者说仅实现了最小面向对象机制,匿名嵌入不是继承,无法实现多态处理。虽然配合方法集,可用接口来实现一些类似的操作,但是其本质是完全不同的

字段标签 ( tag )

  • 字段标签并不是注释,而是用来对字段进行描述的元数据,尽管它不属于数据成员,但是确实类型的组成成分

  • 在运行期,可用反射获取标签信息,它常被用做格式校验,数据库关系映射等

内存布局

  • 不管结构体中包含多少字段,其内存总是一次性分配的,各字段在相邻的地址空间按定义顺序排列。

  • 对于引用类型、字符串和指针,结构内存中只包含其基本(头部)数据

  • 包含匿名字段成员

  • 借助 unsafe 包中的相关函数,可输出所有字段的偏移量和长度

  • 分配内存时,字段必须做对齐处理,通常以所有字段中最长的基础类型宽度为标准

  • 比较特殊的时空结构类型字段,如果它是最后一个字段,那么编译器将其作为长度为 1 的类型做类型处理,以便地址不会越界,避免引发垃圾回收错误

  • 如果仅有一个空结构字段,那么同样按 1 对齐,只不过长度为 0 ,且指向 runtime.zerobase 变量

  • 对齐的原因于硬件平台,以及访问效率有关,某些平台只能访问特定地址,比如只能是偶数地址。而另一方面,CPU 访问自然对齐的数据所需的读周期最少,还可以避免拼接数据

关键词: