go101 笔记

#Go 类型系统概述

  • 预声明类型:17 个内置基本类型
  • 从 Go 1.9 到 Go 1.17,Go 白皮书曾经把预声明类型视为定义类型。 但是从 Go 1.18 开始,Go 白皮书明确说明预声明类型不再属于定义类型。

在 Go 中,每个类型都有一个底层类型。规则:

  • 一个内置类型的底层类型为它自己。
  • unsafe 标准库包中定义的 Pointer 类型的底层类型是它自己。(至少我们可以认为是这样。事实上,关于 unsafe.Pointer 类型的底层类型,官方文档中并没有清晰的说明。我们也可以认为 unsafe.Pointer 类型的底层类型为*T,其中 T 表示一个任意类型。) unsafe.Pointer 也被视为一个内置类型。
  • 一个无名类型(必为一个组合类型)的底层类型为它自己。
  • 在一个类型声明中,新声明的类型和源类型共享底层类型。
  • 一般说来,一个可寻址的值是指被放置在内存中某固定位置处的一个值(但放置在某固定位置处的一个值并非一定是可寻址的)。 目前,我们只需知道所有变量都是可以寻址的;但是所有常量、函数返回值和强制转换结果都是不可寻址的。 当一个变量被声明的时候,Go 运行时将为此变量开辟一段内存。此内存的起始地址即为此变量的地址。
  • 解引用操作符*的优先级都高于自增++和自减–操作符。
  • 一个结构体类型中的字段标签和字段的声明顺序对此结构体类型的身份识别很重要。 如果两个无名结构体类型的各个对应字段声明都相同(按照它们的出现顺序),则此两个无名结构体类型是等同的。 两个字段声明只有在它们的名称、类型和标签都等同的情况下才相同。
  • 两个声明在不同的代码包中的非导出字段将总被认为是不同的字段。
  • 当一个(源)结构体值被赋值给另外一个(目标)结构体值时,其效果和逐个将源结构体值的各个字段赋值给目标结构体值的各个对应字段的效果是一样的。
1
2
3
4
5
6
7
8
9
10
func f() {
book1 := Book{pages: 300}
book2 := Book{"Go语言101", "老貘", 256}

book2 = book1
// 上面这行和下面这三行是等价的。
book2.title = book1.title
book2.author = book1.author
book2.pages = book1.pages
}
  • 选择器中的属性选择操作符.的优先级比取地址操作符&的优先级要高。
  • 组合字面量不可寻址但可被取地址。
1
2
3
4
5
6
7
8
9
10
package main

func main() {
type Book struct {
Pages int
}
// Book{100}是不可寻址的,但是它可以被取地址。
p := &Book{100} // <=> tmp := Book{100}; p := &tmp
p.Pages = 200
}
  • 在字段选择器中,属主结构体值可以是指针,它将被隐式解引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

func main() {
type Book struct {
pages int
}
book1 := &Book{100} // book1是一个指针
book2 := new(Book) // book2是另外一个指针
// 像使用结构值一样来使用结构体值的指针。
book2.pages = book1.pages
// 上一行等价于下一行。换句话说,上一行
// 两个选择器中的指针属主将被自动解引用。
(*book2).pages = (*book1).pages
}

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

1
2
3
4
5
6
7
type _interface struct {
dynamicTypeInfo *struct {
dynamicType *_type // 引用着接口值的动态类型
methods []*_function // 引用着动态类型的对应方法列表
}
dynamicValue unsafe.Pointer // 引用着动态值
}

NOTE: 所以是 interface 定义的方法列表还是类型的方法列表?

  • 因为一个间接值部可能并不专属于任何一个值,所以在使用unsafe.Sizeof函数计算一个值的尺寸的时候,此值的间接部分所占内存空间未被计算在内。
  • 在运行时刻,即使一个数组变量在声明的时候未指定初始值,它的元素所占的内存空间也已经被开辟出来。 但是一个 nil 切片或者映射值的元素的内存空间尚未被开辟出来。
  • []T{}表示类型[]T的一个空切片值,它和[]T(nil)是不等价的。 同样,map[K]T{}map[K]T(nil)也是不等价的。
  • 容器字面量是不可寻址的但可以被取地址
  • 任意两个映射值(或切片值)是不能相互比较的。
  • 大多数数组类型都是可比较类型,除了元素类型为不可比较类型的数组类型。

#defer

1
2
3
4
5
6
7
func main() {
defer fmt.Println("此行可以被执行到")
var f func() // f == nil
defer f() // 将产生一个恐慌
fmt.Println("此行可以被执行到")
f = func() {} // 此行不会阻止恐慌产生
}

main 函数第一行依旧可以被执行的原因是:

在恐慌传播到调用者之前,所有已注册的 defer 语句仍然会被执行。

  • 在循环中 defer 关闭可以通过构造一个匿名函数的形式,更优雅。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func writeManyFiles(files []File) error {
for _, file := range files {
if err := func() error {
f, err := os.Open(file.path)
if err != nil {
return err
}
defer f.Close() // 将在此循环步内执行

_, err = f.WriteString(file.content)
if err != nil {
return err
}

return f.Sync()
}(); err != nil {
return err
}
}

return nil
}
  • 这仿佛不是一个能看懂的句式,尤其最后一段话。

#稍后再看

#并发

一个WaitGroup可以在它的一个Wait方法返回之后被重用。 但是请注意,当一个WaitGroup值维护的基数为零时,它的带有正整数实参的Add方法调用不能和它的Wait方法调用并发运行,否则将可能出现数据竞争。

#context

在 db 侧都有一个检测超时的代码段:

1
2
3
4
5
select {
default:
case <-ctx.Done():
return nil, ctx.Err()
}

主动控制超时

请注意,到目前为止(Go 1.22),一个 64 位字(int64 或 uint64 值)的原子操作要求此 64 位字的内存地址必须是 8 字节对齐的。 对于 Go 1.19 引入的原子方法操作,此要求无论在 32-bit 还是 64-bit 架构上总是会得到满足,但是对于 32-bit 架构上的原子函数操作,此要求并非总会得到满足。 请阅读关于 Go 值的内存布局一文获取详情。

#内存顺序保证

下面列出的是通道操作做出的基本顺序保证:

  1. 一个通道上的第**n次成功发送操作的开始发生在此通道上的第n**次成功接收操作完成之前,无论此通道是缓冲的还是非缓冲的。
  2. 一个容量为**m通道上的第n次成功接收操作的开始发生在此通道上的第n+m次发送操作完成之前。 特别地,如果此通道是非缓冲的(m == 0),则此通道上的第n次成功接收操作的开始发生在此通道上的第n**次发送操作完成之前。
  3. 一个通道的关闭操作发生在任何因为此通道被关闭而从此通道接收到了零值的操作完成之前。

#内存块

  • 一个内存块同时可能承载着不同 Go 值的若干值部,但是一个值部在内存中绝不会跨内存块存储,无论此值部的尺寸有多大。
  • 在运行时刻,每一个仍在被使用中的逃逸到堆上的值部肯定被至少一个开辟在栈上的值部所引用着。 如果一个逃逸到堆上的值是一个被声明为T类型的局部变量,则在运行时,一个*T类型的隐式指针将被创建在栈上。 此指针存储着此T类型的局部变量的在堆上的地址,从而形成了一个从栈到堆的引用关系。 另外,编译器还将所有对此局部变量的使用替换为对此指针的解引用。 此*T值可能从今后的某一时刻不再被使用从而使得此引用关系不再存在。
  • 如果一个结构体值的一个字段逃逸到了堆上,则此整个结构体值也逃逸到了堆上。
  • 如果一个数组的某个元素逃逸到了堆上,则此整个数组也逃逸到了堆上。
  • 如果一个切片的某个元素逃逸到了堆上,则此切片中的所有元素都将逃逸到堆上,但此切片值的直接部分可能开辟在栈上。
  • 如果一个值部v被一个逃逸到了堆上的值部所引用,则此值部v也将逃逸到堆上。

#nil

1
2
3
4
func main() {
var a map[int64]string
fmt.Println(a[1])
}

空map的访问不会panic,我之前一直以为会,然后以为只要接了v, ok :=就不会panic,没想到直接索引也不会。