吹拉弹唱


  • Home
  • Archive
  • Categories
  • Tags
  • Books
  •  

© 2022 Kleon

Theme Typography by Makito

Proudly published with Hexo

烤面筋 - Go

Posted at 2021-04-26Updated at 2021-05-19 面筋  面筋 

基础问题

https://github.com/lifei6671/interview-go

https://segmentfault.com/a/1190000038922260

# Context

golang context的理解,context主要用于父子任务之间的同步取消信号,本质上是一种协程调度的方式。另外在使用context时有两点值得注意:上游任务仅仅使用context通知下游任务不再需要,但不会直接干涉和中断下游任务的执行,由下游任务自行决定后续的处理操作,也就是说context的取消操作是无侵入的;context是线程安全的,因为context本身是不可变的(immutable),因此可以放心地在多个协程中传递使用。

Context的用法

# 切片和数组

slice和array

扩容机制

slice,len,cap,共享,扩容

# GPM 🌟

channel,process

CSP(communicating sequential processes)

GPM

Goroutine/Processor/M Thread

https://my.oschina.net/aom/blog/4279175

Processor(P):

根据用户设置的 GoMAXPROCS 值来创建一批小车§。

Goroutine(G):

通过 Go 关键字就是用来创建一个 Goroutine,也就相当于制造一块砖(G),然后将这块砖(G)放入当前这辆小车§中。

Machine (M):

地鼠(M)不能通过外部创建出来,只能砖(G)太多了,地鼠(M)又太少了,实在忙不过来,刚好还有空闲的小车§没有使用,那就从别处再借些地鼠(M)过来直到把小车§用完为止。

这里有一个地鼠(M)不够用,从别处借地鼠(M)的过程,这个过程就是创建一个内核线程(M)。

需要注意的是:地鼠(M) 如果没有小车§是没办法运砖的,小车§的数量决定了能够干活的地鼠(M)数量,在 Go 程序里面对应的是活动线程数;

P 代表可以“并行”运行的逻辑处理器,每个 P 都被分配到一个系统线程 M,G 代表 Go 协程。

Go 调度器中有两个不同的运行队列:全局运行队列(GRQ)和本地运行队列(LRQ)。

每个 P 都有一个 LRQ,用于管理分配给在 P 的上下文中执行的 Goroutines,这些 Goroutine 轮流被和 P 绑定的 M 进行上下文切换。GRQ 适用于尚未分配给 P 的 Goroutines。

从上图可以看出,G 的数量可以远远大于 M 的数量,换句话说,Go 程序可以利用少量的内核级线程来支撑大量 Goroutine 的并发。多个 Goroutine 通过用户级别的上下文切换来共享内核线程 M 的计算资源,但对于操作系统来说并没有线程上下文切换产生的性能损耗。

调度策略:

  1. 任务窃取(work-stealing),每个P的G不均衡,可以从GRQ或LRQ中获取G执行。
  2. 减少阻塞
  3. 由于原子、互斥量或通道操作调用导致 Goroutine 阻塞,调度器将把当前阻塞的 Goroutine 切换出去,重新调度 LRQ 上的其他 Goroutine;
  4. 由于网络请求和 IO 操作导致 Goroutine 阻塞,Go 程序提供了**网络轮询器(NetPoller)**来处理网络请求和 IO 操作的问题,其后台通过 kqueue(MacOS),epoll(Linux)或 iocp(Windows)来实现 IO 多路复用。
  5. 当调用一些系统方法(比如文件IO)的时候,如果系统方法调用的时候发生阻塞,这种情况下,网络轮询器(NetPoller)无法使用,而进行系统调用的 Goroutine 将阻塞当前 M。调度器介入后:识别出 G1 已导致 M1 阻塞,此时,调度器将 M1 与 P 分离,同时也将 G1 带走。然后调度器引入新的 M2 来服务 P。此时,可以从 LRQ 中选择 G2 并在 M2 上进行上下文切换。阻塞的系统调用完成后:G1 可以移回 LRQ 并再次由 P 执行。如果这种情况再次发生,M1 将被放在旁边以备将来重复使用。
  6. 如果在 Goroutine 去执行一个 sleep 操作,导致 M 被阻塞了。Go 程序后台有一个监控线程 sysmon,它监控那些长时间运行的 G 任务然后设置可以强占的标识符,别的 Goroutine 就可以抢先进来执行。只要下次这个 Goroutine 进行函数调用,那么就会被强占,同时也会保护现场,然后重新放入 P 的本地队列里面等待下次执行。

底层复用多路IO,调度器分离阻塞G

https://segmentfault.com/a/1190000022871460

goroutine的保存和恢复只需要三个寄存器:程序计数器、栈指针和DX寄存器。因为goroutine之间共享堆空间,不共享栈空间,所以只需把goroutine的栈指针和程序执行到那里的信息保存和恢复即可,花费很低。

当线程阻塞时,其它的线程进可能被执行,这叫做线程的切换。切换的时候,调度器需要保存当前阻塞的线程的状态,恢复要执行的线程状态,包括所有的寄存器,16个通用寄存器、程序计数器、栈指针、段寄存器、16个XMM寄存器、FP协处理器、16个 AVX寄存器、所有的MSR等等

https://developer.aliyun.com/article/611313

Do not communicate by sharing memory; instead, share memory by communicating.

# 线程

kernel threads, user threads, and fibers

kernel thread是由操作系统内核支持的线程,也是基本意义上的线程,kernel thread切换由内核完成.一般程序不会直接使用kernel thread,而是使用kernel thread的一种高级接口-轻量级进程(Light Weight Process)一个LWP对应于一个kernel thread.

user thread指不需要内核支持而在用户程序中实现的线程,它不依赖于内核,用户自己实现user thread的创建,同步,调度,管理等功能.这里一个LWP对应多个user thread,简单点说 一个进程拥有多个LWP,一个LWP又拥有多个user thread.,这里的用户一般指的是编程语言

Go使用了user thread的线程模型,在Go中称之为goroutine,这是Go称为并发型语言的基础.
Go语言中有逻辑处理器(Logic Processor)的概念,通过它完成user thread的调度.

# 逃逸分析 🌟

逃逸分配在堆上,否则分配到栈上。

https://www.cnblogs.com/itbsl/p/10476674.html

go build -gcflags ‘-m -l’ main.go

写一个

1、堆上动态分配内存比栈上静态分配内存,开销大很多。

2、变量分配在栈上需要能在编译期确定它的作用域,否则会分配到堆上。

3、Go编译器会在编译期对考察变量的作用域,并作一系列检查,如果它的作用域在运行期间对编译器一直是可知的,那么就会分配到栈上。简单来说,编译器会根据变量是否被外部引用来决定是否逃逸。

4、对于Go程序员来说,编译器的这些逃逸分析规则不需要掌握,我们只需通过go build -gcflags '-m’命令来观察变量逃逸情况就行了。

5、不要盲目使用变量的指针作为函数参数,虽然它会减少复制操作。但其实当参数为变量自身的时候,复制是在栈上完成的操作,开销远比变量逃逸后动态地在堆上分配内存少的多。

6、逃逸分析在编译阶段完成的。

  • 知道golang的内存逃逸吗?什么情况下会发生内存逃逸?

https://juejin.cn/post/6844904176481206285

golang程序变量会携带有一组校验数据,用来证明它的整个生命周期是否在运行时完全可知。如果变量通过了这些校验,它就可以在栈上分配。否则就说它 逃逸 了,必须在堆上分配。
能引起变量逃逸到堆上的典型情况:

  1. 在方法内把局部变量指针返回 局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出。
  2. 发送指针或带有指针的值到 channel 中。 在编译时,是没有办法知道哪个 goroutine 会在 channel 上接收数据。所以编译器没法知道变量什么时候才会被释放。
  3. 在一个切片上存储指针或带指针的值。 一个典型的例子就是 []*string 。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。
  4. slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )。 slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。
  5. 在 interface 类型上调用方法。 在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道。想像一个 io.Reader 类型的变量 r , 调用 r.Read(b) 会使得 r 的值和切片b 的背后存储都逃逸掉,所以会在堆上分配。

# sync.Map

原生map非线程安全,加锁以及sync.Map{}的实现

优点:是官方出的,是亲儿子;通过空间换时间的方式;读写分离;
缺点:不适用于大量写的场景,这样会导致read map读不到数据而进一步加锁读取,同时dirty map也会一直晋升为read map,整体性能较差。
适用场景:大量读,少量写

这个实现类似于一个线程安全的 map[interface{}]interface{} . 这个map的优化主要适用了以下场景:

(1)给定key的键值对只写了一次,但是读了很多次,比如在只增长的缓存中;

(2)当多个goroutine读取、写入和覆盖的key值不相交时。

https://github.com/orcaman/concurrent-map

# sync.WaitGroup

# sync.RWMutex

# sync.Mutex

# channel

channel no buffer以及buffer的区别
channel一定记得close。

# goroutine

goroutine记得return或者中断,不然容易造成goroutine占用大量CPU。

  1. 主动自己退出: 经过多少时间后主动退出(channel) func main() { var wg sync. WaitGroup quit := time. …
  2. 外部被动退出: 我们可以通过外部操作接收信号方式来退出: func main() { var wg sync.

# goroutine泄露,或是内存泄漏

协程泄露是指协程创建后,长时间得不到释放,并且还在不断地创建新的协程,最终导致内存耗尽,程序崩溃。常见的导致协程泄露的场景有以下几种:

  • 缺少接收器,导致发送阻塞
    这个例子中,每执行一次 query,则启动1000个协程向信道 ch 发送数字 0,但只接收了一次,导致 999 个协程被阻塞,不能退出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func query() int {
ch := make(chan int)
for i := 0; i < 1000; i++ {
go func() { ch <- 0 }()
}
return <-ch
}

func main() {
for i := 0; i < 4; i++ {
query()
fmt.Printf("goroutines: %d\n", runtime.NumGoroutine())
}
}
// goroutines: 1001
// goroutines: 2000
// goroutines: 2999
// goroutines: 3998
  • 缺少发送器,导致接收阻塞

那同样的,如果启动 1000 个协程接收信道的信息,但信道并不会发送那么多次的信息,也会导致接收协程被阻塞,不能退出。

  • 死锁(dead lock)

两个或两个以上的协程在执行过程中,由于竞争资源或者由于彼此通信而造成阻塞,这种情况下,也会导致协程被阻塞,不能退出。

  • 无限循环(infinite loops)

这个例子中,为了避免网络等问题,采用了无限重试的方式,发送 HTTP 请求,直到获取到数据。那如果 HTTP 服务宕机,永远不可达,导致协程不能退出,发生泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func request(url string, wg sync.WaitGroup) {
for {
if _, err := http.Get(url); err == nil {
// write to db
break
}
time.Sleep(time.Second)
}
wg.Done()
}

func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go request(fmt.Sprintf("exampe.com/%d", i), wg)
}
wg.Wait()
}

# interface

interface nil 比较

# init

init函数先于main函数自动执行,不能被其他函数调用; … 每个包可以有多个init函数; 包的每个源文件也可以有多个init函数,这点比较特殊; 同一个包的init执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序。

go init 的执行顺序,注意是不按导入规则的(这里是编译时按文件名的顺序执行的)

# defer

go中多个defer的执行顺序, FILO

# Slice

从slice创建slice的时候,注意原slice的操作可能导致底层数组变化。

如果你要创建一个很长的slice,尽量创建成一个slice里存引用,这样可以分批释放,避免gc在低配机器上stop the world

# Socket

# 优劣

优势:容易学习,生产力,并发,动态语法。劣势:包管理,错误处理,缺乏框架。

# 框架

beego
go-micro
gin

# GC 🌟

优缺点

三色并发标记法

https://zhuanlan.zhihu.com/p/334999060

https://juejin.cn/post/6844903793855987719

  1. 程序创建的对象都标记为白色。
  2. gc开始:扫描所有可直接到达的对象,标记为灰色
  3. 从灰色对象中找到其引用对象标记为灰色,把灰色对象本身标记为黑色
  4. 监视对象中的内存修改,并持续上一步的操作,直到灰色标记的对象不存在
  5. gc回收白色对象。
  6. 最后,将所有黑色对象变为白色,并重复以上所有过程。

Go是如何解决标记-清除(mark and sweep)算法中的卡顿(stw,stop the world)问题的呢?

  1. STW,stop the world;让程序暂停,程序出现卡顿。
  2. 标记需要扫描整个heap
  3. 清除数据会产生heap碎片

标记-清除(mark and sweep)算法的STW(stop the world)操作,就是runtime把所有的线程全部冻结掉,所有的线程全部冻结意味着用户逻辑是暂停的。这样所有的对象都不会被修改了,这时候去扫描是绝对安全的。
Go如何减短这个过程呢?标记-清除(mark and sweep)算法包含两部分逻辑:标记和清除。
我们知道Golang三色标记法中最后只剩下的黑白两种对象,黑色对象是程序恢复后接着使用的对象,如果不碰触黑色对象,只清除白色的对象,肯定不会影响程序逻辑。所以:清除操作和用户逻辑可以并发。
标记操作和用户逻辑也是并发的,用户逻辑会时常生成对象或者改变对象的引用,那么标记和用户逻辑如何并发呢?

process新生成对象的时候,GC该如何操作呢?不会乱吗?
我们看如下图,在此状态下:process程序又新生成了一个对象,我们设想会变成这样:

但是这样显然是不对的,因为按照三色标记法的步骤,这样新生成的对象A最后会被清除掉,这样会影响程序逻辑。

Golang为了解决这个问题,引入了写屏障这个机制。 写屏障:该屏障之前的写操作和之后的写操作相比,先被系统其它组件感知。 通俗的讲:就是在gc跑的过程中,可以监控对象的内存修改,并对对象进行重新标记。(实际上也是超短暂的stw,然后对对象进行标记)

那么,灰色或者黑色对象的引用改为白色对象的时候,Golang是该如何操作的?
看如下图,一个黑色对象引用了曾经标记的白色对象。

这时候,写屏障机制被触发,向GC发送信号,GC重新扫描对象并标位灰色。

因此,gc一旦开始,无论是创建对象还是对象的引用改变,都会先变为灰色。

# 写屏障和混合写屏障

https://www.huaweicloud.com/articles/9aa423940e224bc6ff57a0c63e2615fa.html

# Go内存(tcmalloc)

https://zhuanlan.zhihu.com/p/29216091

# 定长分配

首先是基本问题,如何分配定长记录?例如,我们有一个 Page 的内存,大小为 4KB,现在要以 N 字节为单位进行分配。为了简化问题,就以 16 字节为单位进行分配。

解法有很多,比如,bitmap。4KB / 16 / 8 = 32, 用 32 字节做 bitmap即可,实现也相当简单。

出于最大化内存利用率的目的,我们使用另一种经典的方式,freelist。将 4KB 的内存划分为 16 字节的单元,每个单元的前8个字节作为节点指针,指向下一个单元。初始化的时候把所有指针指向下一个单元;分配时,从链表头分配一个对象出去;释放时,插入到链表。

由于链表指针直接分配在待分配内存中,因此不需要额外的内存开销,而且分配速度也是相当快。

# 变长分配

我们把所有的变长记录进行“取整”,例如分配7字节,就分配8字节,31字节分配32字节,得到多种规格的定长记录。这里带来了内部内存碎片的问题,即分配出去的空间不会被完全利用,有一定浪费。为了减少内部碎片,分配规则按照 8, 16, 32, 48, 64, 80这样子来。注意到,这里并不是简单地使用2的幂级数,因为按照2的幂级数,内存碎片会相当严重,分配65字节,实际会分配128字节,接近50%的内存碎片。而按照这里的分配规格,只会分配80字节,一定程度上减轻了问题。

# 多Page

这里提出了 Span 的概念,也就是多个连续的 Page 会组成一个 Span,在 Span 中记录起始 Page 的编号,以及 Page 数量。

分配对象时,大的对象直接分配 Span,小的对象从 Span 中分配。

还是用多种定长 Page 来实现变长 Page 的分配,初始时只有 128 Page 的 Span,如果要分配 1 个 Page 的 Span,就把这个 Span 分裂成两个,1 + 127,把127再记录下来。对于 Span 的回收,需要考虑Span的合并问题,否则在分配回收多次之后,就只剩下很小的 Span 了,也就是带来了外部碎片 问题。

为此,释放 Span 时,需要将前后的空闲 Span 进行合并,当然,前提是它们的 Page 要连续。

问题来了,如何知道前后的 Span 在哪里?

由于 Span 中记录了起始 Page,也就是知道了从 Span 到 Page 的映射,那么我们只要知道从 Page 到 Span 的映射,就可以知道前后的Span 是什么了。

最简单的一种方式,用一个数组记录每个Page所属的 Span,而数组索引就是 Page ID。这种方式虽然简洁明了,但是在 Page 比较少的时候会有很大的空间浪费。

为此,我们可以使用 RadixTree 这种数据结构,用较少的空间开销,和不错的速度来完成这件事:

乍一看可能有点懵,这个跟 RadixTree 能扯上关系吗?可以把 RadixTree 理解成压缩过的前缀树(trie),所谓压缩,就是在一条路径上的节点都只有一个子节点,就把这条路径合并到父节点去,因此内部节点最少会有 Radix 个字节点。具体的分析可以参考一下 wikipedia 。

实现时,可以通过一定的空间换来时间,也就是减少层数,比如说3层。每层都是一个数组,用一个地址的前 1/3 的bit 索引数组,剩下的 bit 对下一层进行寻址。实际的寻址也可以非常快。

# PageHeap

到这里,我们已经实现了 PageHeap,对所有 Page进行管理。

既然有了基于 Page 的对象分配,和Page本身的管理,我们把它们串起来就可以得到一个简单的内存分配器了:

按照我们之前设计的,每种规格的对象,都从不同的 Span 进行分配;每种规则的对象都有一个独立的内存分配单元:CentralCache。在一个CentralCache 内存,我们用链表把所有 Span 组织起来,每次需要分配时就找一个 Span 从中分配一个 Object;当没有空闲的 Span 时,就从 PageHeap 申请 Span。

看起来基本满足功能,但是这里有一个严重的问题,在多线程的场景下,所有线程都从CentralCache 分配的话,竞争可能相当激烈。

到这里 ThreadCache 便呼之欲出了:

每个线程都一个线程局部的 ThreadCache,按照不同的规格,维护了对象的链表;如果ThreadCache 的对象不够了,就从 CentralCache 进行批量分配;如果 CentralCache 依然没有,就从PageHeap申请Span;如果 PageHeap没有合适的 Page,就只能从操作系统申请了。

在释放内存的时候,ThreadCache依然遵循批量释放的策略,对象积累到一定程度就释放给 CentralCache;CentralCache发现一个 Span的内存完全释放了,就可以把这个 Span 归还给 PageHeap;PageHeap发现一批连续的Page都释放了,就可以归还给操作系统。

至此,TCMalloc 的大体结构便呈现在我们眼前了。

# recover

# sql pool

# 多态

语言基础是否学习完整?
例:除了 mutex 以外还有那些方式安全读写共享变量?

语言基础是否足够扎实?
例:无缓冲 chan 的发送和接收是否同步?

语言细节是否有过了解?
例:golang 采用什么并发模型?体现在哪里?

语言生态是否进行关注?
例:在 Vendor 特性之前包管理工具是怎么实现的?

考察基础经验,
例:说说 golang 中常用的并发模式?

考察实际经验,例:JSON 标准库对 nil slice 和 空 slice 的处理是一致的吗?

考察学习历程,例:学习 Go 的主要途径?读过那些书?

# go defer / for defer

# select

# map

顺序读取

# set

# 消息队列

# 大文件排序

# Go for range 踩坑

  • 闭包变量捕获
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var ss [5]struct{}

//第一种情况,很正常,输出0,1,2,3,4
for i := range ss {
fmt.Println(i)
}

//第二种情况,典型的Go语言闭包,它捕获了变量i,但是要注意的是它持有的是引用不是拷贝,当for循环结束时,i=4
//所以闭包输出的结果都是4
for i := range ss {
defer func() {
fmt.Println(i)
}()
}

//第三种情况,这种情况下的闭包,它并没有捕获变量i,而是通过传参的方式,这种情况下它得到的是拷贝
//所以其结果是4,3,2,1,0
for i := range ss {
defer func(i int) {
fmt.Println(i)
}(i)
}
  • 遍历取不到所有元素指针
1
2
3
4
5
6
7
8
arr := [2]int{1, 2}
res := []*int{}
for _, v := range arr {
res = append(res, &v)
}
//expect: 1 2
fmt.Println(*res[0],*res[1])
//but output: 2 2

通过查看go编译源码可以了解到, for-range其实是语法糖,内部调用还是for循环,初始化会拷贝带遍历的列表(如array,slice,map),然后每次遍历的v都是对同一个元素的遍历赋值。 也就是说如果直接对v取地址,最终只会拿到一个地址,而对应的值就是最后遍历的那个元素所附给v的值。对应伪代码如下:

1
2
3
4
5
6
7
8
// len_temp := len(range)
// range_temp := range
// for index_temp = 0; index_temp < len_temp; index_temp++ {
// value_temp = range_temp[index_temp]
// index = index_temp
// value = value_temp
// original body
// }
  1. 那么怎么改? 有两种 - 使用局部变量拷贝v
1
2
3
4
5
for _, v := range arr {
//局部变量v替换了v,也可用别的局部变量名
v := v
res = append(res, &v)
}
  1. 直接索引获取原来的元素
1
2
3
4
//这种其实退化为for循环的简写
for k := range arr {
res = append(res, &arr[k])
}
  • 遍历会停止么?
1
2
3
4
v := []int{1, 2, 3}
for i := range v {
v = append(v, i)
}

答案是【会】,因为遍历前对v做了拷贝,所以期间对原来v的修改不会反映到遍历中

  • 对大数组这样遍历有啥问题?
1
2
3
4
5
6
7
//假设值都为1,这里只赋值3个
var arr = [102400]int{1, 1, 1}
for i, n := range arr {
//just ignore i and n for simplify the example
_ = i
_ = n
}

答案是【有问题】!遍历前的拷贝对内存是极大浪费啊 怎么优化?有两种

  • 对数组取地址遍历 for i, n := range &arr
  • 对数组做切片引用 for i, n := range arr[:]

反思题:对大量元素的slice和map遍历为啥不会有内存浪费问题? (提示,底层数据结构是否被拷贝)

  • 对大数组这样重置效率高么?
1
2
3
4
5
//假设值都为1,这里只赋值3个
var arr = [102400]int{1, 1, 1}
for i, _ := range &arr {
arr[i] = 0
}

答案是【高】,这个要理解得知道go对这种重置元素值为默认值的遍历是有优化的, 详见go源码:memclrrange

1
2
3
4
5
6
7
8
9
// Lower n into runtime·memclr if possible, for
// fast zeroing of slices and arrays (issue 5373).
// Look for instances of
//
// for i := range a {
// a[i] = zero
// }
//
// in which the evaluation of a is side-effect-free.
  • 对map遍历时删除元素能遍历到么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var m = map[int]int{1: 1, 2: 2, 3: 3}
//only del key once, and not del the current iteration key
var o sync.Once
for i := range m {
o.Do(func() {
for _, key := range []int{1, 2, 3} {
if key != i {
fmt.Printf("when iteration key %d, del key %d\n", i, key)
delete(m, key)
break
}
}
})
fmt.Printf("%d%d ", i, m[i])
}

答案是【不会】 map内部实现是一个链式hash表,为保证每次无序,初始化时会随机一个遍历开始的位置, 这样,如果删除的元素开始没被遍历到(上边once.Do函数内保证第一次执行时删除未遍历的一个元素),那就后边就不会出现。

  • 对map遍历时新增元素能遍历到么?
1
2
3
4
5
var m = map[int]int{1:1, 2:2, 3:3}
for i, _ := range m {
m[4] = 4
fmt.Printf("%d%d ", i, m[i])
}

答案是【可能会】,输出中可能会有44。原因同上一个, 可以用以下代码验证

1
2
3
4
5
6
7
8
9
10
11
12
var createElemDuringIterMap = func() {
var m = map[int]int{1: 1, 2: 2, 3: 3}
for i := range m {
m[4] = 4
fmt.Printf("%d%d ", i, m[i])
}
}
for i := 0; i < 50; i++ {
//some line will not show 44, some line will
createElemDuringIterMap()
fmt.Println()
}
  • 这样遍历中起goroutine可以么?
1
2
3
4
5
6
7
8
var m = []int{1, 2, 3}
for i := range m {
go func() {
fmt.Print(i)
}()
}
//block main 1ms to wait goroutine finished
time.Sleep(time.Millisecond)

答案是【不可以】。预期输出0,1,2的某个组合,如012,210… 结果是222. 同样是拷贝的问题 怎么解决 - 以参数方式传入

1
2
3
4
5
for i := range m {
go func(i int) {
fmt.Print(i)
}(i)
}

使用局部变量拷贝

1
2
3
4
5
6
for i := range m {
i := i
go func() {
fmt.Print(i)
}()
}

# 高级特性

  • var _ io.Writer = (*myWriter)(nil)

编译器会由此检查 *myWriter 类型是否实现了 io.Writer 接口。

var _ io.Writer= myWriter{}

  • interface比较

1、判断类型是否一样

reflect.TypeOf(a).Kind() == reflect.TypeOf(b).Kind()
2、判断两个interface{}是否相等

reflect.DeepEqual(a, b interface{})
3、将一个interface{}赋值给另一个interface{}

reflect.ValueOf(a).Elem().Set(reflect.ValueOf(b))

  • 内存对齐

https://ms2008.github.io/2019/08/01/golang-memory-alignment/

# 高并发

https://blog.csdn.net/weixin_42117918/article/details/107561920

队列 + 工作池

# 定时器

NewTimer NewTicker AfterFunc Sleep

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go func() {
timer := time.NewTimer(time.Minute * 1)
for {
select {
case <-timer.C:
o.lock.Lock()
for k, v := range o.visitIPs {
if time.Now().Sub(v) >= time.Minute*1 {
delete(o.visitIPs, k)
}
}
o.lock.Unlock()
timer.Reset(time.Minute * 1)
case <-ctx.Done():
return
}
}
}()
1
2
3
4
5
6
go func() {
for {
LoadConfigFromABParam()
time.Sleep(time.Minute)
}
}

# 避免拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import (
"reflect"
"unsafe"
)

func String(b []byte) (s string) {
pbytes := (*reflect.SliceHeader)(unsafe.Pointer(&b))
pstring := (*reflect.StringHeader)(unsafe.Pointer(&s))
pstring.Data = pbytes.Data
pstring.Len = pbytes.Len
return
}

func Slice(s string) (b []byte) {
pbytes := (*reflect.SliceHeader)(unsafe.Pointer(&b))
pstring := (*reflect.StringHeader)(unsafe.Pointer(&s))
pbytes.Data = pstring.Data
pbytes.Len = pstring.Len
pbytes.Cap = pstring.Len
return
}

最佳实践之Golang错误处理

1
2
3
4
5
6
7
func doSomething() (err error) {
defer func() {
p := recover()
err = fmt.Errorf("FATAL ERROR: %s", p)
}()
panic("Oops!!")
}

[Go Testing&Coverage](https://hedzr.com/golang/testing/golang-testing-2]

【Go语言】小白也能看懂的context包详解:从入门到精通

gRPC优缺点

Share 

 Previous post: 烤面筋 - 后端 Next post: 烤面筋 - 简历 

© 2022 Kleon

Theme Typography by Makito

Proudly published with Hexo