Go读书笔记总结

笔记内容来自雨痕的《go学习笔记》。

变量

类型决定了变量内存的长度和存储格式,我们只能修改变量值,无法改变类型。

用var声明变量,类型放在变量名后

var x int //        会被自动初始化为其零值
var y = false //  显式提供初始化值,可省略变量类型
var x, y int //    同类型的多个变量
var a, s = 100, "abc" //   不同类型的初始化值
x := 100 //简短的声明变量的方法,只能在函数内部使用, 对于多个变量的简短模式,部分可能是赋值,但必须有至少一个是新变量被定义

x, y = y+3, x+2 // 赋值时首先计算出所有右值,然后依次完成赋值操作
x, _ := strconv.Atoi("12") //   和Python类似,也有个_占位符,空标志符可用来临时规避对未使用变量和导入包的错误检查,它是预置成员,不能重新定义

支持用汉字等Unicode字符命名,但不推荐。

常量

常量值必须是编译期可确定的字符、字符串、数字或布尔值。
声明方式:const x = 123,常量组中如不指定类型和初始化值,则与上一行非空常量右值相同。

const (
    x uint = 120
    y // 与上面x相同

)

枚举

const (
    x =iota // 0
    y // 1
    z // 2
    )

const (
    _, _ = iota, iota * 10 // 0, 0*10
    a, b // 1, 1*10
    c, d // 2, 2*10
    )

如中断iota自增,必须显式恢复,且后续自增值按行序递增。

基本类型

byte和rune都是别名。
byte:alias for uint8(1字节)
rune: alias for int32 (4字节 代表unicode的code point)

基本类型:

屏幕快照 2019-02-16 下午8.25.09.png
屏幕快照 2019-02-16 下午8.26.12.png

引用类型

所谓引用类型特指slice、map、channel这三种预定义类型,引用类型要使用make函数创建,以完成内存分配和属性初始化。

类型转换

除常量、别名类型以及未命名类型外,Go强制要求使用显式类型转换。

自定义类型

使用type 关键字可以定义用户自定义类型

type flags byte
type (  // 多个定义可以合并成组,可在函数或代码块内定义局部类型
    user struct {
        name string
          age uint8 
     }
    event func(string) bool
)
type data int
var d data = 10
var x int = d //   错误: cannot use d (type data) as type int in assignment

基础类型相同但仍然是两种完全不同的类型,不能视作别名,无法进行比较以及隐士转换。

未命名类型

array、slice、map、channel等类型与具体元素的类型或长度等属性有关,称为未命名类型,可用type为其提供命名以变为命名类型。

屏幕快照 2019-02-22 上午10.42.30.png

屏幕快照 2019-02-22 上午10.46.19.png

指针

内存地址是内存中每个字节单元的唯一编号,指针是一个实体,相当于一个专门用来保存地址的数字变量。

零长度的对象的地址不等于nil.

var a, b struct{}
println(&a == &b, &a == nil) // 结果是 true false

指针运算符*为左值时,用于更新目标对象状态,为右值时用于获取目标对象状态

流控制

// if else,支持初始化语句
if num:=9;num<0{
    fmt.Println(num,"is negative")
}else if num<10{
    fmt.Println(num,"has 1 digit")
}else{
    fmt.Println(num,"has multiple digits")
}

// switch 语句
v := 3
switch  {
case v % 2 == 0:
    fmt.Println("v is even")
case v % 2 == 1:
    fmt.Println("v is odd")
}

// for循环,有几种形式,
// for init; condition; post { }
// for condition { }
for i:= 0; i < 10; i++ {
    if i % 2 == 0 {
        continue
    }
    fmt.Println(i)
}

还可以用for range对数组、切片、map等结构进行迭代,返回索引、键值等数据。

注意普通for循环及range迭代,定义的局部变量会重复使用

data := [3]string{"a", "b", "c"}
for i, s := range data {
    fmt.Println(&i, &s)

输出:

0xc82003fe98 0xc82003fec8
0xc82003fe98 0xc82003fec8
0xc82003fe98 0xc82003fec8

range会复制目标数据,array的话复制开销会较大。

延迟调用

向当前函数注册稍后执行的函数调用,直到当前函数执行结束前才被调用,常用于资源释放、错误处理等,多个延迟注册按先进后出顺序执行。延迟调用开销较大,影响性能。

函数

函数是第一类对象,第一类对象指可在运行期创建,可用作函数参数或返回值,可存入变量的实体。

不管是指针、引用类型还是其他类型参数,都是值拷贝类型,无非是拷贝目标对象还是拷贝指针自身而已。

变参本质是slice, 需放在参数的最后。

闭包, 是函数和引用环境组合体,可以引用其环境变量。

可在函数内部定义匿名函数,形成类似函数嵌套的效果

错误处理

panic会立即中断当前函数流程,出发执行延迟调用,在延迟调用函数中,recover可以捕获并返回panic提交的错误。recover函数必须在延迟调用函数中才起作用。

除非是导致系统无法正常工作的错误,否则不建议使用panic.

字符串

字符串是不可变字节序列,

type stringStruct struct {
    str unsafe.Pointer
    len int
}

内置函数len返回字节数组长度。

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

s := "hi你"
for i:=0; i < len(s); i++ {  // 按字节迭代
    fmt.Printf("%d: %c\n", i, s[i])
}

for i, c := range s {  // 使用for range来按字符迭代
    fmt.Printf("%d: %c\n", i, c)
}

输出:

0: h
1: i
2: ä
3: ½
4:  
0: h
1: i
2: 你

类型rune专门用来存储Unicode Code Point, 它是int32的别名,使用单引号的字面量,其默认类型就是rune.

要修改字符串,需要先将其转换为可变类型([]byte或者[]rune),这都需要重新分配内存,并复制数据。

动态构建字符串容易造成性能问题,用加法操作符每次都需重新分配内存。改进方法是用strings.Join().

数组

数组长度是其类型的一部分,也就是说[2]int和[3]int类型不同。

多维数组中,len和cap都返回第一纬度长度。

Go数组是值类型,赋值和传参都会复制整个数组内容,可改用指针或slice避免。

切片

切片内部通过指针引用底层数组,当需要时会申请更大内存,将当前数据复制过去,以实现类似动态数组的功能。

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

属性cap表示切片所引用数组片段真实长度,len用于限定可读写数量。不支持比较操作,仅能判断是否为nil.

将切片作为切片的数据源,不能超出cap,新建切片对象依旧指向原底层数组,也就是说修改对所有关联可见。

append, 向切片尾部添加数据,并返回新的切片对象,数据被追加到原底层数组,如超出cap限制,则为新切片对象重新分配数组。

copy, 在两个切片对象间复制数据,允许指向同一地层数组,允许目标区间重叠,最终所复制的长度以较短的切片len为准。还可直接从字符串复制数据到[]byte.

b := make([]byte, 3)
n := copy(b, "abcde")

字典

是引用类型,使用make函数或初始化表达式创建。键值必须是支持相等运算符(==、!=)的数据类型,例如数字、字符串、指针、数组、结构体,及其对应接口类型。

m := map[string]int{
        "a": 1,
        "b": 2, 
    }
m["a"] = 10 //修改
m["c"] = 30 //新增
v, ok := m["d"] // 判断键是否存在
delete(m, "d") // 删除键值对,不存在时不报错

访问不存在的键值,默认返回零值,不会报错。不能修改值成员(值为结构或数组的其中的成员)

内容为空的字典与nil是不同的。

var m1 map[string]int
m2 := map[string]int{}
fmt.Println(m1 == nil, m2 == nil) // true false

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

运行时会对字典并发操作作出检测,如某个goroutine正在对字典进行写操作,则其他goroutine就不能对该字典执行并发操作(读写、删除), 否则会导致进程崩溃。可使用sync.RWMutex实现并发同步,避免读写同时发生,

在创建时预先准备足够空间有助于提升性能,减少扩张时内存分配和重新哈希操作。例如make(map[int]int, 100).

结构

结构体将多个不同类型命名字段序列打包成一个复合类型。字段名必须唯一,可用_补位,支持使用自身类型的指针成员。字段名、排列顺序属类型组成部分。可按顺序初始化全部字段,或使用命名方式初始化指定字段。推荐使用命名初始化方式,这样在扩充结构字段或调整字段顺序时,不会导致初始化语句出错。

可直接定义匿名结构类型变量,或用作字段类型。近在字段类型全部支持时,才可做相等操作。可使用指针直接操作结构字段。

空结构(struct{})是指没有字段的结构类型,它比较特殊,是因为无论其自身,还是作为数组元素类型,其长度都为零。

var a struct{}
var b [100]struct{}
fmt.Println(unsafe.Sizeof(a), unsafe.Sizeof(b)) // 0 0

匿名字段是指没有名字仅有类型的字段,也被称为嵌入字段或嵌入类型。从编译器角度看,这是隐式以类型标识作名字的字段。不能讲基础类型和其指针类型同时嵌入,因为两者字段名字相同。

字段标签(tag)并不是注释,而是对字段进行描述的元数据,是类型的组成部分。在运行期,可用反射获取标签信息,常被用作格式校验,数据库关系映射等。

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

方法

方法与函数的区别在于有个前置的实例接收参数。可使用实例值或指针调用方法。

如何确定方法的receiver类型?

  • 要修改实例状态,用 *T
  • 无需修改状态,小对象或固定值,建议用T
  • 大对象建议用*T, 减少复制成本
  • 引用类型、字符串、函数等指针包装对象,直接用T
  • 包含Mutex等同步字段,用*T,避免复制造成锁操作无效
  • 实在搞不清,就都用*T

类型有一个与之相关的方法集合(method set), 这决定了它是否实现某个接口。

method value被赋值给变量或作为参数传递时,会立即计算并复制该方法执行时的receiver对象,与其绑定,以便在稍后执行时,能隐式传入receiver参数。

接口

接口代表一种调用契约,是多个方法声明的集合。只要目标类型方法集内包含接口的全部方法就被视为实现了该接口,无需显示声明。

接口习惯以er作为名称后缀,方法名是声明组成部分,参数名可不同或省略。

空接口可被赋值为任何类型的对象。可以嵌入其他接口,相当于将其声明的方法集导入。嵌入的接口不能有同名方法,不能嵌入自身或循环嵌入。

执行机制,略。

类型转换,略。

并发

并发:逻辑上具备处理多个同时性任务的能力
并行:物理上同一时刻执行多个并发任务

在函数调用前添加go关键字即可创建并发任务,新建任务被放置在系统队列中,等待调度器安排合适的系统线程去获取执行,当前流程不会阻塞,且运行时不保证并发任务执行次序。

每个任务单元除保存函数指针、调用参数外,还会分配执行所需的栈内存空间,相比系统默认MB级别的线程栈,goroutine自定义栈初始仅需2KB,所以才能创建成千上万的并发任务。与defer一样,goroutine也会因延迟执行而立即计算并复制执行参数。

进程退出时不会等待并发任务结束,可用通道阻塞,然后发出退出信号。如要等待多个任务结束,推荐使用sync.WaitGroup, 通过设定计数器,让每个goroutine在退出前递减,直至归零时解除阻塞。

exit := make(chan struct{})
go func() {
    time.Sleep(time.Second)
    fmt.Println("goroutine done.")
    close(exit) //关闭通道,发出信号
}()
fmt.Println("main ...")
<-exit // 如通道关闭,立即解除阻塞
fmt.Println("main exit.")

运行时可能会创建很多线程,但任何时候仅有限的几个参与并发任务的执行,默认与处理器核数相等,可用runtime.GOMAXPROCS函数修改。

Gosched, 暂停,释放线程去执行其他任务,当前任务被放回队列,等待下次调度时恢复执行。

Goexit, 立即终止当前任务,运行时确保所有已注册延迟调用被执行。如果让main goroutine调用Goexit, 它会等待其他任务结束,然后让进程直接崩溃。

通道

Go鼓励使用CSP通道,以通讯来代替内存共享,实现并发安全。

内置函数cap和len返回缓冲区大小和当前已缓冲数量。

对于closed或nil通道,发送和接收有如下规则:

  • 向closed channel发送数据,引发panic
  • 从closed channel接收数据,返回已缓冲数据或零值
  • 无论收发,nil channel都会阻塞

select, 略。