基 本 概 念 \red{基本概念} 基本概念:
- go跟在特定符号后的换行符会被转换成分号,所以go要求有一定格式
- go编译器判断变量使用堆或栈内存时,并不从new var判断,是从引用树来判断。
- 在往string迭代添加字符时,使用str+=char性能很慢(大量gc),尽量替换成strings.Join
- string内容不可被修改,数组可以,但数组长度不能改变,切片都可以。
- iota在多常量定义时非常好用
- slice和vector类似,超出容量后2倍扩容。
- go外层结构体变量会覆盖匿名成员中同名变量,同样外层方法会覆盖内层方法
- 结构体的json tag在没有设定时,默认域名的markdown格式。
- 同一个作用域的函数不能重名
- 在打印自定义err时,使用fmt.Errorf()可以格式化输出,errors.New()只能输出特定字符串。
- 动词"%*s"需要传入两个参数,数量a和重复元素b,可以格式化输出比如多个连续空格。
- 类的nil指针也可以调用其方法,但用nil时,不能调用匿名成员的方法
- go接口类型,成员内嵌和类型断言很好支持面向对象特性
- 通道在容量满时再发送会阻塞,无缓冲通道发送会立即阻塞,尽量保证在bug时,chan可以关闭(defer),同样,可以通过关闭chan来向所有消费者goroutinue广播。
- 单向通道类型保证了函数不会误操作管道
- 对于chan,len的价值很低,变化很快。cap则有价值,在内存中如果无法提供该cap的chan,会导致死锁。
- 尽管chan和队列很像,但不要用chan充当simple queue。chan与go调度器深度关联,有永久阻塞的风险。同理select用来监听chan,switch是简单的多分支。
- goroutine 并发数量会受到,CPU核数,IO操作数,网络带宽,web服务本身的容量四个方面的限制,所以goroutine过度并发也不会提高性能,一般通过goroutine数量阈值来限制(令牌chan)。
- go也存在三种竞态问题,死锁(资源互斥,占有且等待,不可抢占,循环等待),活锁(不同于死锁,可能自行解开),饿死(级别太低)
- go中map不是并发安全的,但只读map是并发安全的(一般都会用上RWMutex,支持高并发读),如果需要频繁读写map,一般会增加一个唯一goroutine+chan来封装map,例如读,写,原子操作等。
- 互斥锁的本质是一个容量为1的缓冲chan,同理defer+unlock是很好的习惯。
- goroutine没有标识,go调度器通过GOMAXPROCS参数来确定使用多少个OS线程,一般这个值是CPU的核数。
- 在运行时动态获取一个变量的类型和值,就叫反射。断言一般用于已经结构的变量,而反射用于未知结构的变量。反射很难调试,容易出错且性能开销大,谨慎使用。
闭 包 \red{闭包} 闭包:
- 闭包即函数变量,在将已声明的函数赋值给函数变量时,会完成函数内的计算,只是没有return而已。
- 闭包的条件是用来赋值的函数不能是匿名函数,如例3
// 例1
func A() func() int {
var x int
fmt.Println("x",x)
return func() int {
x++
return x*x
}
}
func main(){
f := A()
fmt.Println("start")
fmt.Println(f())
fmt.Println(f())
}
//输出
/*
x 0
start
1
4
*/
// 例2
func B() func() int {
var x int
fmt.Println("Bx:",x)
return func() int {
x++
return x*x
}
}
func A() func() int {
var x int
fmt.Println("Ax:",x)
f := B()
fmt.Println("B",f()) //
return func() int {
x++
return x*x
}
}
func main(){
f := A()
fmt.Println("start")
fmt.Println("A",f())
fmt.Println("A",f())
}
// 输出
/*
Ax: 0
Bx: 0
B 1
start
A 1
A 4
*/
//例3
func main(){
f := func() func() int {
var x int
fmt.Println("x", x)
return func() int {
x++
return x * x
}
}
fmt.Println("start")
fmt.Println(f()())
fmt.Println(f()())
}
// 输出
/*
start
x 0
1
x 0
1
*/
捕 获 迭 代 量 陷 阱 \red{捕获迭代量陷阱} 捕获迭代量陷阱:
- 因为作用域的原因,for循环内部i被分配在栈中同一块内存,在每一次循环都更新这块内存。
- 而不经过函数形参直接在函数内部使用i,其实是一种引用,引用的地址就是i在栈中那块地址。
// 例1
func main(){
numList := []int{1,2,3}
fList := make([]func()int,0)
for _,i := range numList {
fList = append(fList, func() int {
return i*i
})
}
for _,f := range fList {
fmt.Println(f())
}
}
// 输出
/*
9
9
9
*/
// 例2
func main(){
numList := []int{1,2,3}
fList := make([]func()int,0)
for _,i := range numList {
f := func() int {
fmt.Println("f",i)
return i*i
}
fList = append(fList, f)
}
fmt.Println("start")
for _,f := range fList {
fmt.Println(f())
}
}
// 输出
/*
start
f 3
9
f 3
9
f 3
9
*/
捕 获 p a n i c \red{捕获panic} 捕获panic:
为什么要捕获panic呢?因为要在goroutine里panic的时候捕获堆栈信息
func main(){
defer func() {
if p := recover() ; p == "A"{
fmt.Println("panic")
}
}()
panic("A")
}
// 输出
// panic
并 发 锁 \red{并发锁} 并发锁:
- sync.Mutex互斥锁
- sync.RWMutex读写锁,当读上锁是共享锁,写上锁是互斥锁
- sync.Once初始化锁,包含一个bool和互斥锁,bool来标记是否初始化完成,lock来保护初始化变量。
重 复 抑 制 \red{重复抑制} 重复抑制:
在goroutine并发调用慢函数时,需要对慢函数缓存返回结果。但是在慢函数第一次计算过程中,需要对慢函数进行互斥,否则可能会有多个慢函数都在“第一次执行”,这中场景即重复抑制(duplicate suppression)。
常用操作是,并发+重复抑制(chan广播实现)+非阻塞
type result struct {
value interface{}
err error
}
type Func func(key string)(interface{}, error)
type entry struct {
res result
ready chan struct{}
}
func New(f Func)*Memo {
return &Memo{f: f, cache: make(map[string]*entry)}
}
type Memo struct {
f Func
mu sync.Mutex
cache map[string]*entry
}
func (memo *Memo) Get(key string)(value interface{}, err error){
memo.mu.Lock()
e := memo.cache[key]
if e == nil {
e = &entry{ready:make(chan struct{})}
memo.cache[key] = e
memo.mu.Unlock()
e.res.value, e.res.err = memo.f(key)
close(e.ready) // 广播
}else{
memo.mu.Unlock()
<-e.ready
}
return e.res.value, e.res.err
}
g o r o u t i n e V S 线 程 \red{goroutine\ VS\ 线程} goroutine VS 线程:
-
内存:
thread:线程在操作系统中有固定大小栈内存(注意,是栈内存),通常为2MB,用于保存函数调用期间的局部变量。
goroutine:在生命周期开始只有2KB,但最大可以扩到1GB -
调度:
thread:线程通过OS内核来调度,一般定期隔几毫秒会通过 硬 件 时 钟 \red{硬件时钟} 硬件时钟触发。
goroutine:使用Go自己的调度器,把m个goroutine映射到n个线程上,同时go的调度器并不通过硬件时钟触发,而通过特定go语言结构触发,比如goroutine在阻塞时会被切走。 -
映射关系:
休眠或阻塞的goroutine不会被映射到线程上,而阻塞在I/O或其他非go语言的模块上会占用一个独立的OS线程,但这个线程不计算在GOMAXPROCS内。 -
标识:
thread:程序员可以拿到thread唯一标识
goroutine:设计所然,程序员拿不到唯一标识
管 道 \red{管道} 管道:
写管道:
读管道:
关闭channel时会把recvq中的G全部唤醒,本该写入G的数据位置为nil。把sendq中的G全部唤醒,但这些G会
panic。
m a p \red{map} map:
每个哈希表的实现对负载因子容忍程度不同,比如Redis实现中负载因子大于1时就会触发rehash,而Go则在在负载 因子达到6.5时才会触发rehash,因为Redis的每个bucket只能存1个键值对,而Go的bucket可能存8个键值对, 所以Go可以容忍更高的负载因子。
i
t
o
a
\red{itoa}
itoa:
iota代表了const声明块的行索引(下标从0开始)
d
e
f
e
r
延
迟
函
数
的
参
数
\red{defer延迟函数的参数}
defer延迟函数的参数:
在传入时就已经确定。