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
2
type A B  // A is an another type
type A = B // A is an alias of B

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package typesys

type (
MyInt int
Age MyInt
)

type (
IntSlice []int
MyIntSlice []MyInt
AgeSlice []Age
)

type Ages []Age

func f1(v int) {
panic("implement me")
}

func f2([]Age) {

}

func f3([]int) {

}

func Call1() {
var v MyInt = 114514
//cannot call
//f1(v)
f1(int(v))

var s []Age
f2(s)
var s2 AgeSlice
f2(s2)

// cannot call
//f3(s)
}

注意,类型别名中

1
2
3
4
5
type A = B
type C = B

type AA = struct {}
type BB = struct {}

AA, BB 都是同一种类型

value

一个类型有:

  • 不同的 value
  • 一个零值

我们可以用 unsafe 标准库包中的 Sizeof 函数来取得任何一个值的 memory size。

裹在一个接口值中的非接口 值称为此接口值的动态值。此动态值的类型称为此接口值的动态类型。 一个什么也没包裹的接口值为 一个零值接口值。零值接口值的动态值和动态类型均为不存在。

一个接口类型可以指定若干个(可以是零个)方法,这些方法形成了此接口类型的方法集。

指针

这个概念类似 C 的 pointer, 但是有 gc 兜底,所以我总觉得 Go 里面指针怪怪的

可寻址

return &func() 可能会给你报错 “不可寻址”,这一点我总觉得很离谱… 但是我们总得搞明白为什么。

一个可寻址的值是指被放置在内存中某固定位置处的一个值(但放置在某固定位置处的一个 值并非一定是可寻址的)。 目前,我们只需知道所有变量都是可以寻址的;但是所有常量、函数返回 值和强制转换结果都是不可寻址的。 当一个变量被声明的时候,Go运行时将为此变量开辟一段内存。 此内存的起始地址即为此变量的地址。

*p 类似 C,对 nil deref 会 panic.

47D191C9293076F9A6775E0FA2546192

这个写的比较迷,我个人理解是:

  • 如果是 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. slice: https://github.com/golang/go/blob/master/src/runtime/slice.go
1
2
3
4
5
type slice struct {
array unsafe.Pointer
len int
cap int
}
  1. map: https://github.com/golang/go/blob/master/src/runtime/map.go

这些具体 parse 的时候都是类似:

1
type _map *hashtableImpl

这样的引用对象。

比较需要注意的是 interface

一个非空接口类型的值的dynamicTypeInfo字段的methods字段引用着一个方法列表。 此列表中的每 一项为此接口值的动态类型上定义的一个方法,此方法对应着此接口类型所指定的一个的同原型的方 法。

所以:

  • 在赋值中,底层间接值部将不会被复制
  • 在使用 unsafe.Sizeof 函数计算一个值的尺寸的 时候,此值的间接部分所占内存空间未被计算在内。

需要理解引用这个词,在 Go 里面实际上没有很好的 context

在Go中,只有切片、映射、通道和函数类型属于引用类型。 (如果我们确实需要引用类型这个术 语,那么我们不应把其它指针持有者类型排除在引用类型之外。) 一些函数调用的参数是通过引用来传递的。 (对不起,在Go中,所有的函数调用的参数都是通过 值复制的方式来传递的。)

array slice map

  • 一个映射类型的键值类型必须为一个可比较类型
  • 数组和切片类型的键类型均为内 置类型int。 一个数组或切片的一个元素对应的键值总是一个非负整数下标,此非负整数表示该元素在 该数组或切片所有元素中的顺序位置。此非负整数下标亦常称为一个元素索引(index)。
  • 每个容器值有一个长度属性
  • 每个数组值仅由一个直接部分组成,而一个切片或者映射值是由一个直接 部分和一个可能的被此直接部分引用着的间接部分组成。

一个数组或者切片的所有元素紧挨着存放在一块连续的内存中。一个数组中的所有元素均存放在此数组 值的直接部分,一个切片中的所元素均存放在此切片值的间接部分。 在官方标准编译器和运行时中, 映射是使用哈希表算法来实现的。所以一个映射中的所有元素也均存放在一块连续的内存中,但是映射 中的元素并不一定紧挨着存放。

对于这三种容器,元素访问的时间复杂度均为O(1)。

零值

  1. A{} —> []int{}

  2. 和指针一样,所有切片和映射类型的零值均用预声明的标识符 nil 来表示。

  3. 即使一个数组变量在声明的时候未指定初始值,它的元素所占的内存空间也已经被开辟出

    来。 但是一个nil切片或者映射值的元素的内存空间尚未被开辟出来。

  4. []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
2
3
4
5
type slice struct {
array unsafe.Pointer
len int
cap int
}

当一个切片被用做一个 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

func main() {
s1 := []int { 1, 1, 4, 5, 1, 4}
fmt.Printf("len: %d cap: %d\n", len(s1), cap(s1))
s1 = append(s1, 4)
fmt.Printf("len: %d cap: %d\n", len(s1), cap(s1))
s2 := append(s1, 5)
fmt.Printf("len: %d cap: %d\n", len(s1), cap(s1))
fmt.Printf("len: %d cap: %d\n", len(s2), cap(s2))

s1[2] = 5
// s1: 5 s2: 5
fmt.Printf("s1: %d s2: %d\n", s1[2], s2[2])

s3 := append(s1, 6)
// s2: 6 s3: 6
fmt.Printf("s2: %d s3: %d\n", s2[len(s2) - 1], s3[len(s3) - 1])

sptr := &s1[5]
*sptr = 114514
// s2: 114514 s3: 114514
fmt.Printf("s2: %d s3: %d\n", s2[5], s3[5])
}

所以我个人觉得,还是把 slice 当成“有内部所有权的指针”吧

(我局的挺傻逼的,真的)

创建容器可以:

  • new
  • make
  • 字面量

容器元素:寻址

可寻址的数组的元素也是可寻址的。

不可寻址的数组的元素也是不可寻址的。 原因很简单,因为 一个数组中的所有元素均处于此数组的直接部分。

一个切片值的任何元素都是可寻址的,即使此切片本身是不可寻址的。 这是因为一个切片的底层 元素总是存储在一个被开辟出来的内存片段上。 任何映射元素都是不可寻址的。原因详见此条问答(第51章)。

如果一个映射类型的元素类型为一个结构体类型,则我们无法修改此映射类型的值中的每个结构 体元素的单个字段。 我们必须整体地同时修改所有结构体字段。

如果一个映射类型的元素类型为一个数组类型,则我们无法修改此映射类型的值中的每个数组元 素的单个元素。 我们必须整体地同时修改所有数组元素。

slice copy

copy 函数形式是 copy(dest, src). 在 dest 拷贝到 len 为止,返回拷贝的长度。

for-loop

1
2
3
for key, element = range aContainer { 
// 使用key和element ...
}

在 cpp 中,我们会有(实际上加入 range 之后会更好使):

1
2
for (auto& p: container) {}
for (const auto& p: container) {}

感觉 Go 的 loop 功能孱弱很多:

  • 如果你需要修改值,其实还是要借助 key
  1. 如果 aContainer 是一个数组,那么在遍历过程中对此数组元素的修改不会体现到循环变量 中。 原因是此数组的副本(被真正遍历的容器)和此数组不共享任何元素。
  2. 如果 aContainer 是一个切片(或者映射),那么在遍历过程中对此切片(或者映射)元素 的修改将体现到循环变量中。 原因是此切片(或者映射)的副本和此切片(或者映射)共 享元素(或条目)。
  3. 在遍历中的每个循环步, aContainer 副本中的一个键值元素对将被赋值(复制)给循环变量。 所以对循环变量的直接部分的修改将不会体现在aContainer中的对应元素中。 (因为这个原 因,并且 for-range 循环是遍历映射条目的唯一途径,所以最好不要使用大尺寸的映射键值和元 素类型,以避免较大的复制负担。)

这里有一些细节:

映射中的条目的遍历顺序是不确定的(可以认为是随机的)。或者说,同一个映射中的条目的两 次遍历中,条目的顺序很可能是不一致的,即使在这两次遍历之间,此映射并未发生任何改变。

如果在一个映射中的条目的遍历过程中,一个还没有被遍历到的条目被删除了,则此条目保证不 会被遍历出来。

如果在一个映射中的条目的遍历过程中,一个新的条目被添加入此映射,则此条目并不保证将在 此遍历过程中被遍历出来。

循环变量是单个的,实际上你可以试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func main() {
// 可以使用 persons[:] 或者 &persons 避免开销
for i, p := range persons {
fmt.Println(i, p)
// Note: 不改变遍历过程中的值
persons[1].name = "Jack"
p.age = 31
}
fmt.Println("persons:", &persons)

persons[1].name = "nmsl.xt"

var personSlice []Person
personSlice = persons[:]


for i, p := range personSlice {
fmt.Println(i, p)
// Note: 不改变遍历过程中的值
personSlice[1].name = "Jack"
p.age = 31
}
fmt.Println("persons:", &personSlice)
}

type Person struct {
name string
age int
}

请注意,上述所有各种容器操作的内部实现都未进行同步。如果不使用今后将要介绍的各种并发同步技 术,在没有协程修改一个容器值和它的元素的时候,多个协程并发读取此容器值和它的元素是安全的。 但是并发修改同一个容器值则是不安全的。

需要注意的一点是:

1
2
3
for k, v := range container {
slice = append(slice, &v)
}

你会发现,如同那个经典的并发错误一样,你 append 了同一个值。