笔记内容来自雨痕的《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)
基本类型:
引用类型
所谓引用类型特指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为其提供命名以变为命名类型。
指针
内存地址是内存中每个字节单元的唯一编号,指针是一个实体,相当于一个专门用来保存地址的数字变量。
零长度的对象的地址不等于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, 略。