Types In Golang: part1
Golang 的类型分为:
- basic type:
- string
- bool
- int/uint/float
- byte and rune is uint8 and int32
- complex
- composite type
- pointer
- struct
- function
- container
- slice
- array
- map
- channel
- interface
每种 type 都有一个 kind
1 | type A B // A is an another type |
underlying type
- 每个 type 都有 underlying type
- 内置类型 underlying type 为自身
- unsafe.Pointer undefined
- 一个 undefined type 即
=
构成的 composite type - 一个类型声明中,两个类型共享 underlying type
上面的内容4/5写的很乱,其实就是下面:
AgeSlice
底层类型是[]Age
Age
的底层类型是MyInt
,MyInt
底层类型是int
,int
底层类型是自己
1 | package typesys |
注意,类型别名中
1 | type A = B |
AA, BB 都是同一种类型
value
一个类型有:
- 不同的 value
- 一个零值
我们可以用 unsafe 标准库包中的 Sizeof 函数来取得任何一个值的 memory size。
裹在一个接口值中的非接口 值称为此接口值的动态值。此动态值的类型称为此接口值的动态类型。 一个什么也没包裹的接口值为 一个零值接口值。零值接口值的动态值和动态类型均为不存在。
一个接口类型可以指定若干个(可以是零个)方法,这些方法形成了此接口类型的方法集。
指针
这个概念类似 C 的 pointer, 但是有 gc 兜底,所以我总觉得 Go 里面指针怪怪的
可寻址
return &func()
可能会给你报错 “不可寻址”,这一点我总觉得很离谱… 但是我们总得搞明白为什么。
一个可寻址的值是指被放置在内存中某固定位置处的一个值(但放置在某固定位置处的一个 值并非一定是可寻址的)。 目前,我们只需知道所有变量都是可以寻址的;但是所有常量、函数返回 值和强制转换结果都是不可寻址的。 当一个变量被声明的时候,Go运行时将为此变量开辟一段内存。 此内存的起始地址即为此变量的地址。
*p
类似 C,对 nil
deref 会 panic.
这个写的比较迷,我个人理解是:
- 如果是 underline type 相同,可以 implicit 转
- 如果关系多层,需要 explict 的转
指针只能与 nil 比较
(unsafe.Pointer 类似 C Pointer, 可以让我们大展宏图)
结构体
- 当一个(源)结构体值被赋值给另外一个(目标)结构体值时,其效果和逐个将源结构体值的各个字段 赋值给目标结构体值的各个对应字段的效果是一样的。
&Struct{}
是一个语法糖,(&Struct{}.field)
不可以,但是&Struct{}
本身的 field 是可以续命的(想起 C++ 用&&
续命了)
值与引用
Go 里面有两种指针:
- 类型安全的 pointer
- 不安全的 unsafe.Pointer
一个指针值存储着另一个值的地址,除非此指针值是一个nil空指针。 我们可以说此指针引用着(第15 章)另外一个值,或者说另外一个值正被此指针所引用。
实际上,如果传参,参数是 slice/map/string, 然后你把整个 对象 copy 了一遍,显然是不合理的。很显然这需要 pass 一个 pointer/引用一样的对象进去。
实际上我们可以找到一些对象定义:
1 | type slice struct { |
这些具体 parse 的时候都是类似:
1 | type _map *hashtableImpl |
这样的引用对象。
比较需要注意的是 interface
一个非空接口类型的值的dynamicTypeInfo字段的methods字段引用着一个方法列表。 此列表中的每 一项为此接口值的动态类型上定义的一个方法,此方法对应着此接口类型所指定的一个的同原型的方 法。
所以:
- 在赋值中,底层间接值部将不会被复制
- 在使用 unsafe.Sizeof 函数计算一个值的尺寸的 时候,此值的间接部分所占内存空间未被计算在内。
需要理解引用这个词,在 Go 里面实际上没有很好的 context
在Go中,只有切片、映射、通道和函数类型属于引用类型。 (如果我们确实需要引用类型这个术 语,那么我们不应把其它指针持有者类型排除在引用类型之外。) 一些函数调用的参数是通过引用来传递的。 (对不起,在Go中,所有的函数调用的参数都是通过 值复制的方式来传递的。)
array slice map
- 一个映射类型的键值类型必须为一个可比较类型
- 数组和切片类型的键类型均为内 置类型int。 一个数组或切片的一个元素对应的键值总是一个非负整数下标,此非负整数表示该元素在 该数组或切片所有元素中的顺序位置。此非负整数下标亦常称为一个元素索引(index)。
- 每个容器值有一个长度属性
- 每个数组值仅由一个直接部分组成,而一个切片或者映射值是由一个直接 部分和一个可能的被此直接部分引用着的间接部分组成。
一个数组或者切片的所有元素紧挨着存放在一块连续的内存中。一个数组中的所有元素均存放在此数组 值的直接部分,一个切片中的所元素均存放在此切片值的间接部分。 在官方标准编译器和运行时中, 映射是使用哈希表算法来实现的。所以一个映射中的所有元素也均存放在一块连续的内存中,但是映射 中的元素并不一定紧挨着存放。
对于这三种容器,元素访问的时间复杂度均为O(1)。
零值
A{}
—>[]int{}
和指针一样,所有切片和映射类型的零值均用预声明的标识符 nil 来表示。
即使一个数组变量在声明的时候未指定初始值,它的元素所占的内存空间也已经被开辟出
来。 但是一个nil切片或者映射值的元素的内存空间尚未被开辟出来。
[]T{}
表示类型[]T
的一个空切片值,它和[]T(nil)
是不等价的。 同样,map[K]T{}
和map[K]T(nil)
也是不等价的。
以上内容是很显然的,但是实际使用的时候,因为 slice 我们通常和 append 联用,所以var v []int
然后再对它不断 append
比较
- 同类型数组是可比较的,类似 C 里面 strcmp
- slice map 可以同 nil 比较
len cap
- slice: len cap
- map: len, 它的 cap 是无限大
- array: len cap 都是自身的 size
元素的 r/w
在 C++ 中,我们可以想象:
const T& operator[](int v)
和T& operator[](int v)
, 保证你拿出来是个 ref
在 Go 的 v[k]
中:
如果是个 slice/array
- v 是一个 nil slice: panic
- slice/array 中必须有长度的限制
如果是个 map
- 如果 k 是一个动态类型为不可比较类型的接口值,则 v[k] 在运行时刻将造成一个 Panic (比如你定义了一个 map[interface{}]xxx, 然后传了一个不可以 cmp 的 key, 它会 panic 说这玩意不是 hashable 的)
- 如果
v[k] = xxx
,且v
is nil, 会 panic v[k]
读取时,无论如何不会 panic, 如果不存在会返回 Value 类型的 零值
Slice 的结构与操作语义
了解了 slice 的语义,我们才能不瞎几把用。首先要知道 Slice 并不是 immutable + Persistent 的结构,也就是说,实际上这些操作是很可能有副作用的
1 | type slice struct { |
当一个切片被用做一个 append 函数调用中的基础切片,
如果添加的元素数量大于此(基础)切片的冗余元素槽位的数量,则一个新的底层内存片段将被 开辟出来并用来存放结果切片的元素。 这时,基础切片和结果切片不共享任何底层元素。 否则,不会有底层内存片段被开辟出来。这时,基础切片中的所有元素也同时属于结果切片。两 个切片的元素都存放于同一个内存片段上。
实际上,如果不注意的话,很容易发生多个 slice 共享底层内存的情况,请注意。
- slice 赋值改变 slice 的引用对象
- array 赋值 copy 全部元素
对于 map 来说,增改/删分别是:
v[k] = s
delete(v, k)
map 上操作似乎都是泛型的,(Go 没有泛型就是sb,哎)。
注意,在Go 1.12之前,映射打印结果中的条目顺序并不固定,两次打印结果可能并不相同。
至于 slice, 通常靠 append, 但是 slice 又不是 immutable 的,所以实际上 append()
可能会返回一个改变 len 而不改变 cap 的:
1 | package main |
所以我个人觉得,还是把 slice 当成“有内部所有权的指针”吧
(我局的挺傻逼的,真的)
创建容器可以:
new
make
- 字面量
容器元素:寻址
可寻址的数组的元素也是可寻址的。
不可寻址的数组的元素也是不可寻址的。 原因很简单,因为 一个数组中的所有元素均处于此数组的直接部分。
一个切片值的任何元素都是可寻址的,即使此切片本身是不可寻址的。 这是因为一个切片的底层 元素总是存储在一个被开辟出来的内存片段上。 任何映射元素都是不可寻址的。原因详见此条问答(第51章)。
如果一个映射类型的元素类型为一个结构体类型,则我们无法修改此映射类型的值中的每个结构 体元素的单个字段。 我们必须整体地同时修改所有结构体字段。
如果一个映射类型的元素类型为一个数组类型,则我们无法修改此映射类型的值中的每个数组元 素的单个元素。 我们必须整体地同时修改所有数组元素。
slice copy
copy
函数形式是 copy(dest, src)
. 在 dest 拷贝到 len 为止,返回拷贝的长度。
for-loop
1 | for key, element = range aContainer { |
在 cpp 中,我们会有(实际上加入 range 之后会更好使):
1 | for (auto& p: container) {} |
感觉 Go 的 loop 功能孱弱很多:
- 如果你需要修改值,其实还是要借助 key
- 如果 aContainer 是一个数组,那么在遍历过程中对此数组元素的修改不会体现到循环变量 中。 原因是此数组的副本(被真正遍历的容器)和此数组不共享任何元素。
- 如果 aContainer 是一个切片(或者映射),那么在遍历过程中对此切片(或者映射)元素 的修改将体现到循环变量中。 原因是此切片(或者映射)的副本和此切片(或者映射)共 享元素(或条目)。
- 在遍历中的每个循环步, aContainer 副本中的一个键值元素对将被赋值(复制)给循环变量。 所以对循环变量的直接部分的修改将不会体现在aContainer中的对应元素中。 (因为这个原 因,并且 for-range 循环是遍历映射条目的唯一途径,所以最好不要使用大尺寸的映射键值和元 素类型,以避免较大的复制负担。)
这里有一些细节:
映射中的条目的遍历顺序是不确定的(可以认为是随机的)。或者说,同一个映射中的条目的两 次遍历中,条目的顺序很可能是不一致的,即使在这两次遍历之间,此映射并未发生任何改变。
如果在一个映射中的条目的遍历过程中,一个还没有被遍历到的条目被删除了,则此条目保证不 会被遍历出来。
如果在一个映射中的条目的遍历过程中,一个新的条目被添加入此映射,则此条目并不保证将在 此遍历过程中被遍历出来。
循环变量是单个的,实际上你可以试试:
1 | func main() { |
请注意,上述所有各种容器操作的内部实现都未进行同步。如果不使用今后将要介绍的各种并发同步技 术,在没有协程修改一个容器值和它的元素的时候,多个协程并发读取此容器值和它的元素是安全的。 但是并发修改同一个容器值则是不安全的。
需要注意的一点是:
1 | for k, v := range container { |
你会发现,如同那个经典的并发错误一样,你 append 了同一个值。