POSIX线程API（实现了这个api的库常被称作pthreads）在很大程度上可以看做是对现有Unix进程模型的逻辑延伸，线程和进程有很多相似处。线程有自己的信号掩码，可以分配与CPU关联，可以被放入cgroups，可以被查询使用了哪些资源。所有这些控制的特性都增加了开销，当你的程序中有10万个线程的时候，这些开销将会非常巨大。另一个问题是，操作系统在Go模型下不能做出好的调度决策。举例来说，Go垃圾回收器在执行一次回收时，需要所有线程都停止，使得内存处于一致性状态。这导致需要等待所有运行着的线程到达某一个我们可以确定内存一致的时刻。当你有很多线程被调度运行时，你必须等待它们达到一致性状态。而Go调度器可以决策出只调度到它可以确认内存是一致的时刻即可。这意味着当我们停下来做垃圾回收时，我们只需要等待那些正在CPU上运行的活跃的线程。Go GC的时候需要全局内存一致性，也即全局都停下来不操作内存，Go的调度器更容易做到这一点。比如所有空闲系统线程不用管，所有运行时的系统线程都在执行完当前协程后暂停。Go语言在并发编程方面有强大的能力，通过协程（Goroutine）实现，由调度器来管理执行。与操作系统提供的线程相比，去掉无用特性，使用一个小的可伸缩的栈，更短的上下文切换，显得十分轻量。调度器通过使用与CPU数量相等的线程减少线程频繁切换的内存开销，同时在每一个线程上执行额外开销更低的Goroutine来降低操作系统和硬件的负载。一、Go调度器今天的Go语言调度器有着优异的性能，最初的调度器不仅实现非常简陋，也无法支撑高并发的服务，调度器经过几个大版本的迭代才有今天的优异性能，几个不同版本的调度器引入了不同的改进，也存在不同的缺陷。单线程调度器 0.x只包含40多行代码；程序中只能存在一个活跃线程，由G-M模型组成；多线程调度器 1.0允许运行多线程的程序；全局锁导致竞争严重；任务窃取调度器 1.1引入了处理器P，构成了目前的G-M-P模型；在处理器P的基础上实现了基于工作窃取的调度器；在某些情况下，Goroutine不会让出线程，进而造成饥饿问题；时间过长的垃圾回收（Stop-the-world，STW）会导致程序长时间无法工作；抢占式调度器 1.2~至今基于协作的抢占式调度器 1.2~1.13通过编译器在函数调用时插入抢占检查指令，在函数调用时检查当前Goroutine是否发起了抢占请求，实现基于协作的抢占式调度；Goroutine可能会因为垃圾回收和循环长时间占用资源导致程序暂停；基于信号的抢占式调度器 1.14~至今实现基于信号的真抢占式调度；垃圾回收在扫描栈时会触发抢占调度；抢占的时间点不够多，还不能覆盖全部的边缘情况；非均匀存储访问调度器 提案对运行时的各种资源进行分区；实现非常复杂，到今天还没有提上日程1、单线程调度器0.x版本调度器只包含表示Goroutine的G和表示线程的M两种结构，全局也只有一个线程。在这时Go语言的调度器还是由C语言实现的，调度函数runtime.schedule也只包含40多行代码。该函数会遵循如下的过程调度Goroutine：获取调度器的全局锁；调用runtime.gosave保存栈寄存器和程序计数器；调用runtime.nextgandunlock获取下一个需要运行的Goroutine并解锁调度器；修改全局线程m上要执行的Goroutine；调用runtime.gogo函数运行最新的Goroutine；显然这个单线程调度器的唯一优点就是能运行。2、多线程调度器Go语言在1.0版本正式发布时就支持了多线程的调度器，与上一个版本几乎不可用的调度器相比，Go语言团队在这一阶段实现了从不可用到可用的跨越。多线程版本的调度函数runtime.schedule包含70多行代码。整体的逻辑与单线程调度器没有太多区别，因为我们的程序中可能同时存在多个活跃线程，所以多线程调度器引入了GOMAXPROCS变量帮助我们灵活控制程序中的最大处理器数，即活跃线程数。多线程调度器的主要问题是调度时的锁竞争会严重浪费资源。该调度器有以下问题需要解决：调度器和锁是全局资源，所有的调度状态都是中心化存储的，锁竞争问严重；线程需要经常互相传递可运行的Goroutine，引入了大量的延迟；每个线程都需要处理内存缓存，导致大量的内存占用并影响数据局部性（Data locality）；系统调用频繁阻塞和解除阻塞正在运行的线程，增加了额外开销；3、任务窃取调度器在现有多线程调度器基础上提出了两个改进的手段：在当前的G-M模型中引入了处理器P，增加中间层；在处理器P的基础上实现基于工作窃取的调度器；使用了新的G-M-P模型，调度器的runtime.schedule函数在这个版本的调度器中反而更简单了：static void schedule(void) { G *gp; top: if(runtime·gcwaiting) { gcstopm(); goto top; } gp = runqget(m->p); if(gp == nil) gp = findrunnable(); ... execute(gp); } 如果当前运行时在等待垃圾回收，调用runtime.gcstopm函数；调用runtime.runqget和runtime.findrunnable从本地或者全局的运行队列中获取待执行的Goroutine；调用runtime.execute函数在当前线程M上运行Goroutine；当前处理器本地的运行队列中不包含Goroutine时，调用findrunnable函数会触发工作窃取，从其它的处理器的队列中随机获取一些Goroutine。运行时G-M-P模型中引入的处理器P是线程M和线程G的中间层。基于工作窃取的多线程调度器将每一个线程绑定到了独立的CPU上，这些线程会被不同处理器管理，不同的处理器通过工作窃取对任务进行再分配实现任务的平衡，也能提升调度器和Go语言程序的整体性能。4、抢占式调度器对Go语言并发模型的修改提升了调度器的性能，但是1.1版本中的调度器仍然不支持抢占式调度，程序只能依靠Goroutine主动让出CPU资源才能触发调度。Go语言的调度器在1.2版本中引入基于协作的抢占式调度解决下面的问题：某些Goroutine可以长时间占用线程，造成其它Goroutine的饥饿；垃圾回收需要暂停整个程序（Stop-the-world，STW），最长可能需要几分钟的时间6，导致整个程序无法工作；1.2版本的抢占式调度虽然能够缓解这个问题，但是它实现的抢占式调度是基于协作的，在之后很长的一段时间里Go语言的调度器都有一些无法被抢占的边缘情况，例如：for循环或者垃圾回收长时间占用线程，这些问题中的一部分直到1.14才被基于信号的抢占式调度解决。基于协作的抢占式调度：Go语言在分段栈的机制上实现抢占调度，利用编译器在分段栈上插入的函数，所有Goroutine在函数调用时都有机会进入运行时执行检查函数判断是否需要执行抢占。基于协作的抢占式调度的工作原理：编译器会在调用函数前插入runtime.morestack；Go语言运行时会在垃圾回收暂停程序、系统监控发现Goroutine运行超过10ms时发出抢占请求StackPreempt；当发生函数调用时，可能会执行编译器插入的runtime.morestack函数，它调用的runtime.newstack会检查Goroutine的stackguard0字段是否为StackPreempt；如果stackguard0是StackPreempt，就会触发抢占让出当前线程；这种实现方式虽然增加了运行时的复杂度，但是实现相对简单，也没有带来过多的额外开销，总体来看还是比较成功的实现。因为这里的抢占是通过编译器插入函数实现的，还是需要函数调用作为入口才能触发抢占，所以这是一种协作式的抢占式调度。基于信号的抢占式调度：Go语言在1.14版本中实现了非协作的抢占式调度，在实现的过程中重构已有的逻辑并为Goroutine增加新的状态和字段来支持抢占。目前的抢占式调度也只会在垃圾回收扫描任务时触发，调度过程：程序启动时，在runtime.sighandler函数中注册SIGURG信号的处理函数runtime.doSigPreempt；在触发垃圾回收的栈扫描时会调用runtime.suspendG挂起Goroutine，该函数会执行下面的逻辑：将_Grunning状态的Goroutine标记成可以被抢占，即将preemptStop设置成true；调用runtime.preemptM触发抢占；runtime.preemptM会调用runtime.signalM向线程发送信号SIGURG；操作系统会中断正在运行的线程并执行预先注册的信号处理函数runtime.doSigPreempt；runtime.doSigPreempt函数会处理抢占信号，获取当前的SP和PC寄存器并调用runtime.sigctxt.pushCall；runtime.sigctxt.pushCall会修改寄存器并在程序回到用户态时执行runtime.asyncPreempt；汇编指令 runtime.asyncPreempt会调用运行时函数runtime.asyncPreempt2；runtime.asyncPreempt2会调用runtime.preemptPark；runtime.preemptPark会修改当前Goroutine的状态到_Gpreempted并调用runtime.schedule让当前函数陷入休眠并让出线程，调度器会选择其它的Goroutine继续执行；根据以下的四个原因选择SIGURG作为触发异步抢占的信号；该信号需要被调试器透传；该信号不会被内部的libc库使用并拦截；该信号可以随意出现并且不触发任何后果；需要处理多个平台上的不同信号；STW和栈扫描是一个可以抢占的安全点（Safe-points），所以Go语言会在这里先加入抢占功能。5、非均匀内存访问调度器非均匀内存访问（Non-uniform memory access，NUMA）调度器现在只是Go语言的提案，因为该提案过于复杂，而目前的调度器的性能已经足够优异，所以我们暂时没有实现该提案。该提案的原理就是通过拆分全局资源，让各个处理器能够就近获取，减少锁竞争并增加数据的局部性。在目前的运行时中，线程、处理器、网络轮询器、运行队列、全局内存分配器状态、内存分配缓存和垃圾收集器都是全局资源。运行时没有保证本地化，也不清楚系统的拓扑结构，部分结构可以提供一定的局部性，但是从全局来看没有这种保证。二、GMP模型运行时调度器有三个重要组成部分：G：代表一个Goroutine对象，每次go调用的时候，都会创建一个G对象，它包括栈、指令指针以及对于调用goroutines很重要的其它信息，比如阻塞它的任何channelM：表示一个操作系统的线程，由操作系统的调度器调度和管理；每次创建一个M的时候，都会有一个底层线程创建，所有的G任务，最终还是在M上执行P：表示一个处理器，每一个运行的M都必须绑定一个P，就像线程必须在一个CPU核上执行一样，由P来调度G在M上的运行，P的个数是GOMAXPROCS。1、GG（Gorotuine）就是Go语言调度器中待执行的任务，它在运行时调度器中的地位与线程在操作系统中差不多，但是它占用了更小的内存空间，也降低了上下文切换的开销。Goroutine只存在于Go语言的运行时，它是Go语言在用户态提供的线程，作为一种粒度更细的资源调度单元，如果使用得当能够在高并发的场景下更高效地利用机器的CPU。Goroutine在运行时里使用私有结构体runtime.g表示。这个私有结构体非常复杂，总共包含40多个用于表示各种状态的成员变量，首先是与栈相关的两个字段：type g struct { stack stack stackguard0 uintptr } 其中stack字段描述了当前Goroutine的栈内存范围[stack.lo, stack.hi)，另一个字段stackguard0可以用于调度器抢占式调度。除了stackguard0之外，Goroutine中还包含另外三个与抢占密切相关的字段：type g struct { preempt bool // 抢占信号 preemptStop bool // 抢占时将状态修改成 `_Gpreempted` preemptShrink bool // 在同步安全点收缩栈 } Goroutine与我们在前面章节提到的defer和panic也有千丝万缕的联系，每一个Goroutine上都持有两个分别存储defer和panic对应结构体的链表：type g struct { _panic *_panic // 最内侧的 panic 结构体 _defer *_defer // 最内侧的延迟函数结构体 } 结构体runtime.g的atomicstatus字段就存储了当前Goroutine的状态。除了几个已经不被使用的以及与GC相关的状态之外，Goroutine可能处于以下9个状态：状态描述_Gidle刚刚被分配并且还没有被初始化_Grunnable没有执行代码，没有栈的所有权，存储在运行队列中_Grunning可以执行代码，拥有栈的所有权，被赋予了内核线程 M 和处理器 P_Gsyscall正在执行系统调用，拥有栈的所有权，没有执行用户代码，被赋予了内核线程 M 但是不在运行队列上_Gwaiting由于运行时而被阻塞，没有执行用户代码并且不在运行队列上，但是可能存在于 Channel 的等待队列上_Gdead没有被使用，没有执行代码，可能有分配的栈_Gcopystack栈正在被拷贝，没有执行代码，不在运行队列上_Gpreempted由于抢占而被阻塞，没有执行用户代码并且不在运行队列上，等待唤醒_Gscan GC正在扫描栈空间，没有执行代码，可以与其他状态同时存在虽然Goroutine在运行时中定义的状态及触发迁移的方法非常多而且复杂，但是我们可以将这些不同的状态聚合成最终的三种：等待中、可运行、运行中，在运行期间我们会在这三种不同的状态来回切换：等待中：Goroutine正在等待某些条件满足，例如：系统调用结束等，包括_Gwaiting、_Gsyscall和_Gpreempted几个状态；可运行：Goroutine已经准备就绪，可以在线程运行，如果当前程序中有非常多的Goroutine，每个Goroutine就可能会等待更多的时间，即_Grunnable；运行中：Goroutine正在某个线程上运行，即_Grunning；2、MGo语言并发模型中的M（Machine）是操作系统线程。调度器最多可以创建10000个线程，但是其中大多数的线程都不会执行用户代码（可能陷入系统调用），最多只会有GOMAXPROCS个活跃线程能够正常运行。在默认情况下，运行时会将GOMAXPROCS设置成当前机器的核数，我们也可以使用runtime.GOMAXPROCS来改变程序中最大的线程数。在大多数情况下，我们都会使用Go的默认设置，也就是线程数等于CPU个数，在这种情况下不会触发操作系统的线程调度和上下文切换，所有的调度都会发生在用户态，由Go语言调度器触发，能够减少非常多的额外开销。操作系统线程在Go语言中会使用私有结构体runtime.m来表示，这个结构体中也包含了几十个私有的字段，与Goroutine直接相关的字段：type m struct { g0 *g curg *g ... } 其中g0是持有调度栈的Goroutine，curg是在当前线程上运行的用户Goroutine，这也是操作系统线程唯一关心的两个Goroutine。g0是一个运行时中比较特殊的Goroutine，它会深度参与运行时的调度过程，包括Goroutine的创建、大内存分配和 CGO函数的执行。runtime.m结构体中还存在着三个处理器字段，它们分别表示正在运行代码的处理器p、暂存的处理器nextp和执行系统调用之前的使用线程的处理器oldp：type m struct { p puintptr nextp puintptr oldp puintptr } 3、P调度器中的处理器P(Processor)是线程和Goroutine的中间层，它能提供线程需要的上下文环境，也会负责调度线程上的等待队列，通过处理器P的调度，每一个内核线程都能够执行多个Goroutine，它能在Goroutine进行一些I/O操作时及时切换，提高线程的利用率。因为调度器在启动时就会创建GOMAXPROCS个处理器，所以Go语言程序的处理器数量一定会等于GOMAXPROCS，这些处理器会绑定到不同的内核线程上并利用线程的计算资源运行Goroutine。runtime.p是处理器的运行时表示，作为调度器的内部实现，它包含的字段也非常多，其中包括与性能追踪、垃圾回收和计时器相关的字段，主要关注其中的线程和运行队列：type p struct { m muintptr runqhead uint32 runqtail uint32 runq [256]guintptr runnext guintptr ... } 反向存储的线程维护着线程与处理器之间的关系，而runhead、runqtail和runq三个字段表示处理器持有的运行队列，其中存储着待执行的Goroutine列表，runnext中是线程下一个需要执行的Goroutine。runtime.p结构体中的状态status字段会是以下五种中的一种：状态描述_Pidle处理器没有运行用户代码或者调度器，被空闲队列或者改变其状态的结构持有，运行队列为空_Prunning被线程 M 持有，并且正在执行用户代码或者调度器_Psyscall没有执行用户代码，当前线程陷入系统调用_Pgcstop被线程 M 持有，当前处理器由于垃圾回收被停止_Pdead当前处理器已经不被使用通过分析处理器P的状态，我们能够对处理器的工作过程有一些简单理解，例如处理器在执行用户代码时会处于_Prunning状态，在当前线程执行I/O操作时会陷入_Psyscall状态。三、调度运行时有一个重要的全局变量sched，可以看做一个全局的调度者，数据类型是schedt。type schedt struct { ... lock mutex midle muintptr // idle状态的m nmidle int32 // idle状态的m个数 nmidlelocked int32 // lockde状态的m个数 mcount int32 // 创建的m的总数 maxmcount int32 // m允许的最大个数 ngsys uint32 // 系统中goroutine的数目，会自动更新 // 全局的可运行的g队列 runqhead guintptr runqtail guintptr runqsize int32 // dead的G的全局缓存 gflock mutex gfreeStack *g gfreeNoStack *g ngfree int32 ... } 大多数需要的信息都已放在了结构体M、G和P中，schedt结构体只是一个壳。可以看到，其中有M的idle队列，P的idle队列，以及一个全局的就绪的G队列。schedt结构体中的Lock是非常必须的，如果M或P等做一些非局部的操作，它们一般需要先锁住调度器。1、调度器启动运行时通过runtime.schedinit函数初始化调度器：func schedinit() { _g_ := getg() ... sched.maxmcount = 10000 ... sched.lastpoll = uint64(nanotime()) procs := ncpu if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 { procs = n } if procresize(procs) != nil { throw("unknown runnable goroutine during bootstrap") } } 在调度器初始函数执行的过程中会将maxmcount设置成10000，这也就是一个Go语言程序能够创建的最大线程数，虽然最多可以创建10000个线程，但是可以同时运行的线程还是由GOMAXPROCS变量控制。启动完成之后，等待用户创建运行新的Goroutine并为Goroutine调度处理器资源。2、创建Goroutine想要启动一个新的Goroutine来执行任务时，需要使用Go语言中的go关键字。编译器会将其转换成runtime.newproc函数，该函数会接收大小和表示函数的指针funcval。在这个函数中会获取Goroutine以及调用方的程序计数器，然后调用runtime.newproc1函数：func newproc(siz int32, fn *funcval) { argp := add(unsafe.Pointer(&fn), sys.PtrSize) gp := getg() pc := getcallerpc() newproc1(fn, (*uint8)(argp), siz, gp, pc) } runtime.newproc1会根据传入参数初始化一个g，该函数分成以下几个步骤：获取或者创建新的Goroutine结构体；将传入的参数移到Goroutine的栈上；更新Goroutine调度相关的属性；将Goroutine加入处理器的运行队列；Go语言中有两个运行队列，其中一个是处理器本地的运行队列，另一个是调度器持有的全局运行队列，只有在本地运行队列没有剩余空间时才会使用全局队列。3、调度循环调度器启动之后，会调用runtime.schedule进入调度循环：func schedule() { _g_ := getg() top: var gp *g var inheritTime bool if gp == nil { if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 { lock(&sched.lock) gp = globrunqget(_g_.m.p.ptr(), 1) unlock(&sched.lock) } } if gp == nil { gp, inheritTime = runqget(_g_.m.p.ptr()) } if gp == nil { gp, inheritTime = findrunnable() } execute(gp, inheritTime) } runtime.schedule函数的会从不同地方查找待执行的Goroutine：为了保证公平，当全局运行队列中有待执行的Goroutine时，通过schedtick保证有一定几率会从全局的运行队列中查找对应的Goroutine；从处理器本地的运行队列中查找待执行的Goroutine；如果前两种方法都没有找到Goroutine，就会通过runtime.findrunnable进行阻塞地查找Goroutine；runtime.findrunnable通过以下的过程获取可运行的Goroutine，且一定会返回一个可执行的 Goroutine，如果当前不存在就会阻塞等待。从本地运行队列、全局运行队列中查找；从网络轮询器中查找是否有Goroutine等待运行；通过runtime.runqsteal函数尝试从其他随机的处理器中窃取待运行的 Goroutine，在该过程中还可能窃取处理器中的计时器；接下来由runtime.execute函数执行获取的Goroutine，做好准备工作后，它会通过runtime.gogo将Goroutine调度到当前线程上执行。func execute(gp *g, inheritTime bool) { _g_ := getg() _g_.m.curg = gp gp.m = _g_.m casgstatus(gp, _Grunnable, _Grunning) gp.waitsince = 0 gp.preempt = false gp.stackguard0 = gp.stack.lo + _StackGuard if !inheritTime { _g_.m.p.ptr().schedtick++ } gogo(&gp.sched)}最后会调用runtime.goexit0函数，该函数会将Goroutine转换会_Gdead状态、清理其中的字段、移除Goroutine和线程的关联并调用runtime.gfput重新加入处理器的Goroutine空闲列表gFree。func goexit0(gp *g) { _g_ := getg() casgstatus(gp, _Grunning, _Gdead) gp.m = nil ... gp.param = nil gp.labels = nil gp.timer = nil dropg() gfput(_g_.m.p.ptr(), gp) schedule() } 在最后runtime.goexit0函数会重新调用runtime.schedule触发新的Goroutine调度，我们可以认为调度循环永远都不会返回。Go语言中的运行时调度循环会从runtime.schedule函数开始，最终又回到runtime.schedule；上述介绍的是Goroutine正常执行并退出的逻辑，实际情况会复杂得多，多数情况下Goroutine的执行的过程中都会经历协作式或者抢占式调度，这时会让出线程的使用权等待调度器的唤醒。
编程语言通常会使用手动和自动两种方式管理内存，C、C++ 以及Rust等编程语言使用手动的方式管理内存，工程师需要主动申请或者释放内存；而Python、Ruby、Java和Go等语言使用自动的垃圾回收（Garbage Collection）机制，简称GC，GC会自动检测程序运行过程中的垃圾并回收它。优点：自动回收内存，避免忘记释放内存空间而发生的内存泄露和“悬垂指针”导致严重的无法预期的BUG。程序员就不用再去担心因为忘了释放内存，从而大大减轻了负担。也不用再去头疼费事的内存管理，让程序员告别恼人的内存管理，把精力集中在更本质的编程工作上。缺点：程序运行垃圾收集回收系统总是消耗一定的资源，不管怎么优化，对程序性能还是有一定的影响，包括：暂停程序、额外的存储开销、额外的计算开销、内存读写速度下降等等。一、基本算法1960年首次发布了GC算法，直到现在为止，垃圾回收算法都是基于以下三种算法的组合或应用。1、标记清除标记清除算法由标记阶段和清除阶段构成。标记阶段是把所有活动对象都做上标记的阶段。清除阶段是把那些没有标记的对象，也就是非活动对象回收的阶段，通过这两个阶段，就可以令不能利用的内存空间重新得到利用。标记阶段需要暂停程序（Stop the world，STW），扫描所有的对象，如果对象很多，暂停程序时间就很长，但是实现简单，搭配其他一些算法可以实现高效的GC算法。标记阶段：从根对象（全局对象和栈对象）出发查找并标记堆中所有存活的对象；清除阶段：遍历堆中的全部对象，回收未被标记的垃圾对象并将回收的内存加入空闲链表；2、引用计数给每个对象添加一个引用计数属性，新增一个引用时计数加1，引用释放时计数减1，当被引用数的值为0时，对象马上就会把自己作为空闲空间连接到空闲链表。也就是说，各个对象在变成垃圾的同时就会立刻被回收。内存管理的开销分布于整个应用程序运行期间，无需挂起应用程序的运行来做垃圾回收。虽然优点佷明显，但是局限性很多，包括：频繁的更新计数，每一个对象的操作都可能增加或减少计数，然而程序中对象操作很多计数器需要占用很多位，如果是64位系统，地址数量即最大的对象数量是2^64，就需要一个64位的存储空间算法简单但是实现复杂，每一个对象都要插入计数器，且无法处理循环引用3、复制算法只把某个空间里的活动对象复制到其他空间，把原空间里的所有对象都回收掉。复制活动对象的原空间称为From空间，将粘贴活动对象的新空间称为To空间。GC复制算法是利用From空间进行分配的。当From空间被完全占满时，GC会将活动对象全部复制到To空间。当复制完成后，该算法会把From空间和To空间互换，GC也就结束了。From空间和To空间大小必须一致。这是为了保证能把From空间中的所有活动对象都收纳到To空间里。可实现内存高速分配且不会产生碎片化，但只能利用一半的内存空间，如果使用递归调用的方式复制对象则可能存在栈溢出的情况，迭代复制更高效而且不存在栈溢出，只是需要一点额外的内存作为迭代缓存。二、优化算法传统的垃圾收集算法会在垃圾收集的执行期间暂停应用程序，一旦触发垃圾收集，垃圾收集器就会抢占CPU的使用权占据大量的计算资源以完成标记和清除工作，然而很多追求实时的应用程序无法接受长时间的STW。垃圾收集器一旦开始执行就会浪费大量的计算资源，为了减少应用程序暂停的最长时间和垃圾收集的总暂停时间，我们会使用下面的策略优化现代的垃圾收集器：增量垃圾收集：增量地标记和清除垃圾，降低应用程序暂停的最长时间；并发垃圾收集：利用多核的计算资源，在用户程序执行时并发标记和清除垃圾；因为增量和并发两种方式都可以与用户程序交替运行，所以需要使用屏障技术保证垃圾收集的正确性；与此同时，应用程序也不能等到内存溢出时触发垃圾收集，因为当内存不足时，应用程序已经无法分配内存，这与直接暂停程序没有什么区别，增量和并发的垃圾收集需要提前触发并在内存不足前完成整个循环，避免程序的长时间暂停。增量式（Incremental）的垃圾收集是减少程序最长暂停时间的一种方案，它可以将原本时间较长的暂停时间切分成多个更小的GC时间片，虽然从垃圾收集开始到结束的时间更长了，但是这也减少了应用程序暂停的最大时间。增量式的垃圾收集需要与三色标记法一起使用，为了保证垃圾收集的正确性，我们需要在垃圾收集开始前打开写屏障，这样用户程序对内存的修改都会先经过写屏障的处理，保证了堆内存中对象关系的强三色不变性或者弱三色不变性。虽然增量式的垃圾收集能够减少最大的程序暂停时间，但是增量式收集也会增加一次GC循环的总时间。并发式（Concurrent）的垃圾收集不仅能够减少程序的最长暂停时间，还能减少整个垃圾收集阶段的时间，通过开启读写屏障、利用多核优势与用户程序并行执行，并发垃圾收集器确实能够减少垃圾收集对应用程序的影响。虽然并发收集器能够与用户程序一起运行，但是并不是所有阶段都可以与用户程序一起运行，部分阶段还是需要暂停用户程序的，不过与传统的算法相比，并发的垃圾收集可以将能够并发执行的工作尽量并发执行。1、三色标记将程序中的对象分为以下三类：白色：还未搜索过的对象灰色：正在搜索的对象黑色：搜索完成的对象GC开始运行前所有的对象都是白色。GC一开始运行，所有从根能到达的对象（全局对象和栈对象）都会被标记，然后被堆到栈里。GC只是发现了这样的对象，但还没有搜索完它们，所以这些对象就成了灰色对象。灰色对象会被依次从栈中取出，其子对象也会被涂成灰色。当其所有的子对象都被涂成灰色时，对象就会被涂成黑色。当GC结束时已经不存在灰色对象了，活动对象全部为黑色，垃圾则为白色。当STW时，对象间的引用是不会发生变化的，可以轻松完成标记。而当需要支持并发标记时，即标记期间应用线程还在继续运行，对象间的引用可能发生变化，多标和漏标的情况就有可能发生。假设已经遍历到E（变为灰色了），此时应用执行了objD.fieldE = null，此刻之后，对象E/F/G是“应该”被回收的。然而因为E已经变为灰色了，其仍会被当作存活对象继续遍历下去。最终的结果是：这部分对象仍会被标记为存活，即本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存，被称之为“浮动垃圾”。浮动垃圾并不会影响垃圾回收的正确性，只是需要等到下一轮垃圾回收中才被清除。假设GC线程已经遍历到E（变为灰色了），此时应用线程执行了：var G = objE.fieldG; objE.fieldG = null; // 灰色E 断开引用 白色G objD.fieldG = G; // 黑色D 引用 白色G 此时GC继续执行，因为E已经没有对G的引用了，所以不会将G放到灰色集合；尽管因为D重新引用了G，但因为D已经是黑色了，不会再重新做遍历处理。最终导致的结果是：G会一直停留在白色集合中，最后被当作垃圾进行清除。这直接影响到了应用程序的正确性，是不可接受的。不难分析，漏标只有同时满足以下两个条件时才会发生：条件一：灰色对象断开了白色对象的引用；即灰色对象原来成员变量的引用发生了变化。条件二：黑色对象重新引用了该白色对象；即黑色对象成员变量增加了新的引用。2、屏障技术因为用户程序可能在标记执行的过程中修改对象的指针，所以三色标记清除算法本身是不可以并发或者增量执行的，它仍然需要STW，想要并发或者增量地标记对象还是需要使用屏障技术。想要在并发或者增量的标记算法中保证正确性，我们需要达成以下两种三色不变性（Tri-color invariant）中的任意一种：强三色不变性：黑色对象不会指向白色对象，只会指向灰色对象或者黑色对象；弱三色不变性：黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径；遵循上述两个不变性中的任意一个，我们都能保证垃圾收集算法的正确性，而屏障技术就是在并发或者增量标记过程中，给新添加的和被修改的对象实时上色，保证对象不会被意外清理掉（即使多标也没有关系，下一次GC会清理的）。屏障技术更像是一个钩子方法，它是在用户程序读取对象、创建新对象以及更新对象指针时执行的一段代码，根据操作类型的不同，我们可以将它们分成读屏障（Read barrier）和写屏障（Write barrier）两种，因为读屏障需要在读操作中加入代码片段，对用户程序的性能影响很大，所以编程语言往往都会采用写屏障保证三色不变性。插入写屏障：每当我们执行类似*slot = ptr的表达式时，我们会执行写屏障尝试改变指针的颜色。如果ptr指针是白色的，那么该函数会将该对象设置成灰色，其他情况则保持不变。它是一种相对保守的屏障技术，它会将有存活可能的对象都标记成灰色以满足强三色不变性。删除写屏障：白色对象的引用被删除时，将对象涂成灰色，这样删除写屏障就可以保证弱三色不变性，对象引用的下游对象一定可以被灰色对象引用。删除写屏障通过对对象的着色，保证了对象和下游的对象能够在这一次垃圾收集的循环中存活。避免发生悬挂指针以保证用户程序的正确性。三、Go垃圾收集器Go语言的垃圾收集器从诞生的第一天起就一直在演进，除了少数几个版本没有大更新之外，几乎每次发布的小版本都会提升垃圾收集的性能，而与性能一同提升的还有垃圾收集器代码的复杂度。v1.0：完全串行的标记和清除过程，需要暂停整个程序；v1.1：在多核主机并行执行垃圾收集的标记和清除阶段；v1.3：运行时基于只有指针类型的值包含指针的假设增加了对栈内存的精确扫描支持，实现了真正精确的垃圾收集；将unsafe.Pointer类型转换成整数类型的值认定为不合法的，可能会造成悬挂指针等严重问题；v1.5：实现了基于三色标记清扫的并发垃圾收集器；大幅度降低垃圾收集的延迟从几百ms降低至10ms以下；计算垃圾收集启动的合适时间并通过并发加速垃圾收集的过程；v1.6：实现了去中心化的垃圾收集协调器；基于显式的状态机使得任意Goroutine都能触发垃圾收集的状态迁移；使用密集的位图替代空闲链表表示的堆内存，降低清除阶段的CPU占用；v1.7：通过并行栈收缩将垃圾收集的时间缩短至2ms以内；v1.8：使用混合写屏障将垃圾收集的时间缩短至0.5ms以内；v1.9：彻底移除暂停程序的重新扫描栈的过程；v1.10：更新了垃圾收集调频器的实现，分离软硬堆大小的目标；v1.12：使用新的标记终止算法简化垃圾收集器的几个阶段；v1.13：通过新的Scavenger解决瞬时内存占用过高的应用程序向操作系统归还内存的问题；v1.14：使用全新的页分配器优化内存分配的速度；从Go语言垃圾收集器的演进能够看到该组件的的实现和算法变得越来越复杂，最开始的垃圾收集器还是不精确的单线程STW收集器，但是最新版本的垃圾收集器却支持并发垃圾收集、去中心化协调等特性。1、并发垃圾收集Go语言在v1.5中引入了并发的垃圾收集器，该垃圾收集器使用了我们上面提到的三色抽象和写屏障技术保证垃圾收集器执行的正确性。并发垃圾收集器会在扫描对象之前暂停程序做一些标记对象的准备工作，其中包括启动后台标记的垃圾收集器以及开启写屏障，如果在后台执行的垃圾收集器不够快，应用程序申请内存的速度超过预期，运行时就会让申请内存的应用程序辅助完成垃圾收集的扫描阶段，在标记和标记终止阶段结束之后就会进入异步的清理阶段，将不用的内存增量回收。v1.5版本实现的并发垃圾收集策略由专门的Goroutine负责在处理器之间同步和协调垃圾收集的状态。当其他的Goroutine发现需要触发垃圾收集时，它们需要将该信息通知给负责修改状态的主Goroutine，然而这个通知的过程会带来一定的延迟，这个延迟的时间窗口很可能是不可控的，用户程序会在这段时间分配界面很多内存空间。v1.6引入了去中心化的垃圾收集协调机制，将垃圾收集器变成一个显式的状态机，任意的Goroutine都可以调用方法触发状态的迁移。2、回收堆目标STW的垃圾收集器虽然需要暂停程序，但是它能够有效地控制堆内存的大小，Go语言运行时的默认配置会在堆内存达到上一次垃圾收集的2倍时，触发新一轮的垃圾收集，这个行为可以通过环境变量GOGC调整，在默认情况下它的值为100，即增长100%的堆内存才会触发GC。因为并发垃圾收集器会与程序一起运行，所以它无法准确的控制堆内存的大小，并发收集器需要在达到目标前触发垃圾收集，这样才能够保证内存大小的可控，并发收集器需要尽可能保证垃圾收集结束时的堆内存与用户配置的GOGC一致。Go语言v1.5引入并发垃圾收集器的同时使用垃圾收集调步（Pacing）算法计算触发的垃圾收集的最佳时间，确保触发的时间既不会浪费计算资源，也不会超出预期的堆大小。优化堆的增长速度和垃圾收集器的CPU利用率。3、混合写屏障在Go语言v1.7版本之前，运行时会使用插入写屏障保证强三色不变性，但是运行时并没有在所有的垃圾收集根对象上开启插入写屏障。因为Go语言的应用程序可能包含成百上千的Goroutine，而垃圾收集的根对象一般包括全局变量和栈对象，如果运行时需要在几百个Goroutine的栈上都开启写屏障，会带来巨大的额外开销，所以Go团队在实现上选择了在标记阶段完成时暂停程序、将所有栈对象标记为灰色并重新扫描，在活跃Goroutine非常多的程序中，重新扫描的过程需要占用10 ~ 100ms的时间。Go语言在v1.8组合插入写屏障和删除写屏障构成了混合写屏障，该屏障会将被覆盖的对象标记成灰色和新对象也标记成灰色，为了移除栈的重扫描过程，除了引入混合写屏障之外，在垃圾收集的标记阶段，还需要将栈上创建的所有新对象都标记成黑色，防止新分配的栈内存和堆内存中的对象被错误地回收，因为栈内存在标记阶段最终都会变为黑色，所以不再需要重新扫描栈空间。4、回收过程Go语言的垃圾收集可以分成清除终止、标记、标记终止和清除四个不同阶段，它们分别完成了不同的工作。清理终止阶段；暂停程序，所有的处理器在这时会进入安全点（Safe point）；如果当前垃圾收集循环是强制触发的，我们还需要处理还未被清理的内存管理单元；标记阶段；将状态切换至_GCmark、开启写屏障、用户程序协助并将根对象入队；恢复执行程序，标记进程和用于协助的用户程序会开始并发标记内存中的对象，写屏障会将被覆盖的指针和新指针都标记成灰色，而所有新创建的对象都会被直接标记成黑色；开始扫描根对象，包括所有Goroutine的栈、全局对象以及不在堆中的运行时数据结构，扫描Goroutine栈期间会暂停当前处理器；依次处理灰色队列中的对象，将对象标记成黑色并将它们指向的对象标记成灰色；使用分布式的终止算法检查剩余的工作，发现标记阶段完成后进入标记终止阶段；标记终止阶段；暂停程序、将状态切换至_GCmarktermination并关闭辅助标记的用户程序；清理处理器上的线程缓存；清理阶段；将状态切换至_GCoff开始清理阶段，初始化清理状态并关闭写屏障；恢复用户程序，所有新创建的对象会标记成白色；后台并发清理所有的内存管理单元，当Goroutine申请新的内存管理单元时就会触发清理；
应用程序的内存一般会分成堆区和栈区两个部分。栈区的内存一般由编译器自动进行分配和释放，其中存储着函数的入参以及局部变量，这些参数会随着函数的创建而创建，函数的返回而消亡，一般不会在程序中长期存在，这种线性的内存分配策略有着极高地效率，但是工程师也往往不能控制栈内存的分配，这部分工作基本都是由编译器自动完成的。一、线程栈寄存器是中央处理器（CPU）中的稀缺资源，它的存储能力非常有限，但是能提供最快的读写速度，充分利用寄存器的效率可以构建高性能的应用程序。寄存器在物理机上非常有限，然而栈区的操作就会使用到两个以上的寄存器，这足以说明栈内存在应用程序的重要性。栈寄存器在是 CPU 寄存器中的一种，它的主要作用是跟踪函数的调用栈，Go 语言的汇编代码中包含 BP 和 SP 两个栈寄存器，它们分别存储了栈的基址指针和栈顶的地址，栈内存与函数调用的关系非常紧密。由于历史的设计问题，目前的栈区内存都是从高地址向低地址扩展的，当应用程序申请或者释放栈内存时只需要修改 SP 寄存器的值，这种线性的内存分配方式与堆内存相比更加快速，占用极少的额外开销。如果我们在Linux操作系统中执行pthread_create系统调用，进程会启动一个新的线程，如果用户没有通过软资源限制RLIMIT_STACK指定线程栈的大小，那么操作系统会根据架构选择不同的默认栈大小。多数架构上默认栈大小都在2 ~ 4MB左右，极少数架构会使用32MB作为默认大小，用户程序可以在分配的栈上存储函数参数和局部变量。然而这个固定的栈大小在某些场景下可能不是一个合适的值，如果一个程序需要同时运行几百个甚至上千个线程，那么这些线程中的绝大部分都只会用到很少的栈空间，而如果函数的调用栈非常深，固定的栈大小也无法满足用户程序的需求。线程和进程都是代码执行的上下文（Context of Execution），但是如果一个应用程序中包含成百上千个执行上下文并且每个上下文都是线程，就会占用大量的内存空间并带来其他的额外开销，Go语言在设计时认为执行上下文应该是轻量级的，所以在它实现了用户级的Goroutine作为执行上下文。二、逃逸分析在 C 语言和 C++ 这类需要手动管理内存的编程语言中，将对象或者结构体分配到栈上或者堆上是由工程师自主决定的，这也为工程师的工作带来的挑战，如果工程师能够精准地为每一个变量分配最合理的空间，那么整个程序的运行效率和内存使用效率一定是最高的，但是手动分配内存会导致如下的两个问题：不需要分配到堆上的对象分配到了堆上，浪费内存空间；需要分配到堆上的对象分配到了栈上，悬挂指针、影响内存安全；与悬挂指针相比，浪费的内存空间反而是小问题。如果一个该分配到堆上的变量分配到栈上，函数调用返回时这个局部变量会被回收，调用方获取的是危险的悬挂指针，我们不确定当前指针指向的值是否合法，这种问题在大型项目中是比较难以发现和定位的。int *dangling_pointer() { int i = 2; return &i; } 在编译器优化中，逃逸分析（Escape analysis）是用来决定指针动态作用域的方法。Go语言的编译器使用逃逸分析决定哪些变量应该在栈上分配，哪些变量应该在堆上分配，其中包括使用new、make和字面量等方法隐式分配的内存，Go语言的逃逸分析遵循以下两个不变性：指向栈对象的指针不能存在于堆中；指向栈对象的指针不能在栈对象回收后存活；逃逸分析是静态分析的一种，在编译器解析了Go语言源文件后，它可以获得整个程序的抽象语法树（Abstract syntax tree，AST），编译器可以根据抽象语法树分析静态的数据流，通过以下几个步骤实现静态分析的全过程（src/cmd/compile/internal/gc/escape.go）：构建带权重的有向图，其中顶点表示被分配的变量，边表示变量之间的分配关系，权重表示寻址和取地址的次数；遍历对象分配图并查找违反两条不变性的变量分配关系，如果堆上的变量指向了栈上的变量，那么栈上的变量就需要分配在堆上；记录从函数的调用参数到堆以及返回值的数据流，增强函数参数的逃逸分析；决定变量是在栈上还是堆上虽然重要，但是这是一个定义相对清晰的问题，我们可以通过编译器在统一作出决策。为了保证内存的绝对安全，编译器可能会将一些变量错误地分配到堆上，但是因为这些对也会被垃圾收集器处理，所以不会造成内存泄露以及悬挂指针等安全问题，解放了工程师的生产力。三、栈内存空间Go语言使用用户态线程Goroutine作为执行上下文，它的额外开销和默认栈大小都比线程小很多，然而Goroutine的栈内存空间和栈结构也在早期几个版本中发生过一些变化：v1.0 ~ v1.1：最小栈内存空间为4KB；v1.2：将最小栈内存提升到了8KB；v1.3：使用连续栈替换之前版本的分段栈；v1.4：将最小栈内存降低到了2KB；Goroutine的初始栈内存在最初的几个版本中多次修改，从4KB提升到8KB是临时的解决方案，其目的是为了减轻分段栈的栈分裂问题对程序造成的性能影响；在v1.3版本引入连续栈之后，Goroutine的初始栈大小降低到了2KB，进一步减少了Goroutine占用的内存空间。1、分段栈分段栈是Go语言在v1.3版本之前的实现，所有Goroutine在初始化时都会调用runtime.stackalloc分配一块固定大小的内存空间，这块内存的大小由StackMin表示，在v1.2版本中为8KB。当Goroutine调用的函数层级或者局部变量需要的越来越多时，运行时会调用runtime.morestack和runtime.newstack创建一个新的栈空间，这些栈空间虽然不连续，但是当前Goroutine的多个栈空间会以链表的形式串联起来，运行时会通过指针找到连续的栈片段。分段栈机制虽然能够按需为当前Goroutine分配内存并且及时减少内存的占用，但是它也存在两个比较大的问题：如果当前Goroutine的栈几乎充满，那么任意的函数调用都会触发栈的扩容，当函数返回后又会触发栈的收缩，如果在一个循环中调用函数，栈的分配和释放就会造成巨大的额外开销，这被称为热分裂问题（Hot split）；一旦Goroutine使用的内存越过了分段栈的扩缩容阈值，运行时就会触发栈的扩容和缩容，带来额外的工作量；2、连续栈连续栈可以解决分段栈中存在的两个问题，其核心原理就是每当程序的栈空间不足时，初始化一片更大的栈空间并将原栈中的所有值都迁移到新的栈中，新的局部变量或者函数调用就有了充足的内存空间。使用连续栈机制时，栈空间不足导致的扩容会经历以下几个步骤：在内存空间中分配更大的栈内存空间；将旧栈中的所有内容复制到新的栈中；将指向旧栈对应变量的指针重新指向新栈；销毁并回收旧栈的内存空间；在扩容的过程中，最重要的是调整指针的第三步，这一步能够保证指向栈的指针的正确性，因为栈中的所有变量内存都会发生变化，所以原本指向栈中变量的指针也需要调整。经过逃逸分析的Go语言程序的遵循以下不变性：指向栈对象的指针不能存在于堆中，所以指向栈中变量的指针只能在栈上，只需要调整栈中的所有变量就可以保证内存的安全了。因为需要拷贝变量和调整指针，连续栈增加了栈扩容时的额外开销，但是通过合理栈缩容机制就能避免热分裂带来的性能问题，在GC期间如果Goroutine使用了栈内存的四分之一，那就将其内存减少一半，这样在栈内存几乎充满时也只会扩容一次，不会因为函数调用频繁扩缩容。四、栈操作Go语言中的执行栈由runtime.stack结构体表示，该结构体中只包含两个字段，分别表示栈的顶部和栈的底部，每个栈结构体都表示范围[lo, hi)的内存空间：type stack struct { lo uintptr hi uintptr } 栈管理分为编译期间和运行时两个阶段：编译器会在编译阶段会通过cmd/internal/obj/x86.stacksplit在调用函数前插入runtime.morestack或者runtime.morestack_noctxt函数；运行时在创建新的Goroutine时会在runtime.malg函数中调用runtime.stackalloc申请新的栈内存，并在编译器插入的 runtime.morestack中检查栈空间是否充足；Go语言的编译器不会为所有的函数插入runtime.morestack，它只会在必要时插入指令以减少运行时的额外开销，编译指令 nosplit可以跳过栈溢出的检查，虽然这能降低一些开销，不过固定大小的栈也存在溢出的风险。1、栈初始化栈空间在运行时中包含两个重要的全局变量，分别是runtime.stackpool和runtime.stackLarge，这两个变量分别表示全局的栈缓存和大栈缓存，前者可以分配小于32KB的内存，后者用来分配大于32KB的栈空间。var stackpool [_NumStackOrders]struct { item stackpoolItem _ [cpu.CacheLinePadSize - unsafe.Sizeof(stackpoolItem{})%cpu.CacheLinePadSize]byte } type stackpoolItem struct { mu mutex span mSpanList } var stackLarge struct { lock mutex free [heapAddrBits - pageShift]mSpanList } 这两个用于分配空间的全局变量都与内存单元runtime.mspan有关，我们可以认为Go语言的栈内存都是分配在堆上的，运行时初始化时调用的runtime.stackinit函数会在初始化这些全局变量。如果运行时只使用全局变量来分配栈内存的话，势必会造成线程之间的锁竞争进而影响程序的执行效率，栈内存由于与线程关系比较密切，所以在每一个线程缓存runtime.mcache中都加入了栈缓存减少锁竞争影响。运行时使用全局的runtime.stackpool 和线程缓存中的空闲链表分配32KB以下的栈内存，使用全局的runtime.stackLarge和堆内存分配32KB以上的栈内存，提高本地分配栈内存的性能。2、栈分配运行时会在Goroutine的初始化函数runtime.malg中调用runtime.stackalloc分配一个大小足够栈内存空间，根据线程缓存和申请栈的大小，该函数会通过三种不同的方法分配栈空间。如果栈空间较小，使用全局栈缓存或者线程缓存上固定大小的空闲链表分配内存；如果栈空间较大，从全局的大栈缓存runtime.stackLarge中获取内存空间；如果栈空间较大并且runtime.stackLarge空间不足，在堆上申请一片大小足够内存空间；runtime.stackpoolalloc函数会在全局的栈缓存池runtime.stackpool中获取新的内存，如果栈缓存池中不包含剩余的内存，运行时会从堆上申请一片内存空间；如果线程缓存中包含足够的空间，我们可以从线程本地的缓存中获取内存，一旦发现空间不足就会调用runtime.stackcacherefill从堆上获取新的内存。3、栈扩容编译器会在cmd/internal/obj/x86.stacksplit函数中为函数调用插入runtime.morestack运行时检查，它会在几乎所有的函数调用之前检查当前Goroutine的栈内存是否充足，如果当前栈需要扩容，会保存一些栈的相关信息并调用runtime.newstack创建新的栈。runtime.newstack会先做一些准备工作并检查当前Goroutine是否发出了抢占请求。如果发出了抢占请求：当前线程可以被抢占时，直接调用runtime.gogo触发调度器的调度；如果当前Goroutine在垃圾回收被runtime.scanstack函数标记成了需要收缩栈，调用runtime.shrinkstack；如果当前Goroutine被runtime.suspendG函数挂起，调用runtime.preemptPark被动让出当前处理器的控制权并将Goroutine的状态修改至_Gpreempted；调用runtime.gopreempt_m主动让出当前处理器的控制权；如果当前Goroutine不需要被抢占：也就意味着我们需要新的栈空间来支持函数调用和本地变量的初始化，运行时会先检查目标大小的栈是否会溢出。如果目标栈的大小没有超出程序的限制，我们会将Goroutine切换至_Gcopystack 状态并调用runtime.copystack开始栈的拷贝，在拷贝栈的内存之前，运行时会通过runtime.stackalloc函数分配新的栈空间。新栈的初始化和数据的复制是一个比较简单的过程，复杂的是需要将指向源栈中内存指向新的栈。所有的指针都被调整后，我们就可以更新Goroutine的几个变量并通过runtime.stackfree释放原始栈的内存空间了。4、栈缩容runtime.shrinkstack是用于栈缩容的函数，该函数的实现原理非常简单，其中大部分都是检查是否满足缩容前置条件的代码。如果要触发栈的缩容，新栈的大小会使原始栈的一半，不过如果新栈的大小低于程序的最地限制 2KB，那么缩容的过程就会停止。运行时只会在栈内存使用不足1/4时进行缩容，缩容也会调用扩容时使用的runtime.copystack函数开辟新的栈空间。func shrinkstack(gp *g) { ... oldsize := gp.stack.hi - gp.stack.lo newsize := oldsize / 2 if newsize < _FixedStack { return } avail := gp.stack.hi - gp.stack.lo if used := gp.stack.hi - gp.sched.sp + _StackLimit; used >= avail/4 { return } copystack(gp, newsize) }
程序中的数据和变量都会被分配到程序所在的虚拟内存中，内存空间包含两个重要区域栈区（Stack）和堆区（Heap）。函数调用的参数、返回值以及局部变量大都会被分配到栈上，这部分内存会由编译器进行管理；不同编程语言使用不同的方法管理堆区的内存，C++等编程语言会由工程师主动申请和释放内存，Go等编程语言会由工程师和编译器共同管理，堆中的对象由内存分配器分配并由垃圾收集器回收。Go语言程序内置运行时，内存分配器是运行时的一个组件。一、分配方法编程语言的内存分配器一般包含两种分配方法，一种是线性分配器（Sequential Allocator，Bump Allocator），另一种是空闲链表分配器（Free-List Allocator）。1、线性分配器线性分配（Bump Allocator）是一种高效的内存分配方法，但是有较大的局限性。当我们在编程语言中使用线性分配器，我们只需要在内存中维护一个指向内存特定位置的指针，当用户程序申请内存时，分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置。根据线性分配器的原理，我们可以推测它有较快的执行速度，以及较低的实现复杂度；但是线性分配器无法在内存被释放时重用内存。正是因为线性分配器的这种特性，我们需要合适的垃圾回收算法配合使用。标记压缩（Mark-Compact）、复制回收（Copying GC）和分代回收（Generational GC）等算法可以通过拷贝的方式整理存活对象的碎片，将空闲内存定期合并，这样就能利用线性分配器的效率提升内存分配器的性能了。因为线性分配器的使用需要配合具有拷贝特性的垃圾回收算法，所以C和C++等需要直接对外暴露指针的语言就无法使用该策略。2、空闲链表分配器空闲链表分配器（Free-List Allocator）可以重用已经被释放的内存，它在内部会维护一个类似链表的数据结构。当用户程序申请内存时，空闲链表分配器会依次遍历空闲的内存块，找到足够大的内存，然后申请新的资源并修改链表。因为不同的内存块以链表的方式连接，所以使用这种方式分配内存的分配器可以重新利用回收的资源，但是因为分配内存时需要遍历链表，所以它的时间复杂度就是O(n)。空闲链表分配器可以选择不同的策略在链表中的内存块中进行选择，最常见的就是以下四种方式：首次适应（First-Fit）：从链表头开始遍历，选择第一个大小大于申请内存的内存块；循环首次适应（Next-Fit）：从上次遍历的结束位置开始遍历，选择第一个大小大于申请内存的内存块；最优适应（Best-Fit）：从链表头遍历整个链表，选择最合适的内存块；隔离适应（Segregated-Fit）：将内存分割成多个链表，每个链表中的内存块大小相同，申请内存时先找到满足条件的链表，再从链表中选择合适的内存块；Go语言采用的是隔离适应的内存分配策略，将内存分割成由4、8、16、32字节的内存块组成的链表，当我们向内存分配器申请8字节的内存时，会在8字节内存块的链表找到空闲的内存块并返回。隔离适应的分配策略减少了需要遍历的内存块数量，提高了内存分配的效率。二、分级分配线程缓存分配（Thread-Caching Malloc，TCMalloc）是用于分配内存的的机制，它比glibc中的malloc函数还要快很多。Go 语言的内存分配器就借鉴了TCMalloc的设计实现高速的内存分配，它的核心理念是使用多级缓存根据将对象根据大小分类，并按照类别实施不同的分配策略。Go语言会根据申请分配的内存大小选择不同的处理逻辑，将对象分成微对象（0, 16B）、小对象[16B, 32KB]和大对象（32KB, +∞）三种。因为程序中的绝大多数对象的大小都在32KB以下，而申请的内存大小影响Go语言运行时分配内存的过程和开销，所以分别处理大对象和小对象有利于提高内存分配器的性能。内存分配器不仅会区别对待大小不同的对象，还会将内存分成不同的级别分别管理，Go内存分配器引入线程缓存（Thread Cache）、中心缓存（Central Cache）和页堆（Page Heap）三个组件分级管理内存。线程缓存属于每一个独立的线程，它能够满足线程上绝大多数的内存分配需求，因为不涉及多线程，所以也不需要使用互斥锁来保护内存，这能够减少锁竞争带来的性能损耗。当线程缓存不能满足需求时，就会使用中心缓存作为补充解决小对象的内存分配问题；在遇到32KB以上的对象时，内存分配器就会选择页堆直接分配大量的内存。这种多层级的内存分配设计与计算机操作系统中的多级缓存也有些类似，因为多数的对象都是小对象，我们可以通过线程缓存和中心缓存提供足够的内存空间，发现资源不足时就从上一级组件中获取更多的内存资源。注：go语言是没有线程的，这里的线程指的是go语言的协程。三、内存划分在Go语言1.10以前的版本，堆区的内存空间都是连续的；但是在1.11版本，Go团队使用稀疏的堆内存空间替代了连续的内存，解决了连续内存带来的限制以及在特殊场景下可能出现的问题。1、线性内存Go语言程序的1.10版本在启动时会初始化整片虚拟内存区域，分为三个区域spans、bitmap和arena分别预留了512MB、16GB以及512GB的内存空间。spans区域存储了指向内存单元runtime.mspan的指针，每个内存单元会管理几页的内存空间，每页大小为8KB；bitmap用于标识arena区域中的那些地址保存了对象，位图中的每个字节都会表示堆区中的32字节是否包含空闲；arena区域是真正的堆区，运行时会将8KB看做一页，这些内存页中存储了所有在堆上初始化的对象；对于任意一个地址，我们都可以根据arena的基地址计算该地址所在的页数并通过spans数组获得管理该片内存单元 runtime.mspan，spans数组中多个连续的位置可能对应同一个runtime.mspan。Go 语言在垃圾回收时会根据指针的地址判断对象是否在堆中，并通过上一段中介绍的过程找到管理该对象的runtime.mspan。这些都建立在堆区的内存是连续的这一假设上。这种设计虽然简单并且方便，但是在C和Go混合使用时会导致程序崩溃：分配的内存地址会发生冲突，导致堆的初始化和扩容失败；没有被预留的大块内存可能会被分配给C语言的二进制，导致扩容后的堆不连续；线性的堆内存需要预留大块的内存空间，但是申请大块的内存空间而不使用是不切实际的，不预留内存空间却会在特殊场景下造成程序崩溃。虽然连续内存的实现比较简单，但是这些问题我们也没有办法忽略。2、稀疏内存稀疏内存是Go语言在1.11中提出的方案，使用稀疏的内存布局不仅能移除堆大小的上限，还能解决C和Go混合使用时的地址空间冲突问题。不过因为基于稀疏内存的内存管理失去了内存的连续性这一假设，这也使内存管理变得更加复杂。运行时使用二维的runtime.heapArena数组管理所有的内存，每个单元都会管理64MB的内存空间：type heapArena struct { bitmap [heapArenaBitmapBytes]byte spans [pagesPerArena]*mspan pageInUse [pagesPerArena / 8]uint8 pageMarks [pagesPerArena / 8]uint8 zeroedBase uintptr } 该结构体中的bitmap和spans与线性内存中的bitmap和spans区域一一对应，zeroedBase字段指向了该结构体管理的内存的基地址。这种设计将原有的连续大内存切分成稀疏的小内存，而用于管理这些内存的元信息也被切分成了小块。不同平台和架构的二维数组大小可能完全不同，在Linux的x86-64架构上，二维数组的一维大小会是1，而二维大小是4,194,304，因为每一个指针占用8字节的内存空间，所以元信息的总大小为32MB。由于每个runtime.heapArena都会管理 64MB的内存，整个堆区最多可以管理256TB的内存，这比之前的512GB多好几个数量级。四、地址空间因为所有的内存最终都是要从操作系统中申请的，所以Go语言的运行时构建了操作系统的内存管理抽象层，该抽象层将运行时管理的地址空间分成以下的四种状态：状态解释None内存没有被保留或者映射，是地址空间的默认状态Reserved运行时持有该地址空间，但是访问该内存会导致错误Prepared内存被保留，一般没有对应的物理内存访问该片内存的行为是未定义的可以快速转换到Ready状态Ready可以被安全访问每一个不同的操作系统都会包含一组特定的方法，这些方法可以让内存地址空间在不同的状态之间做出转换。下面是Linux操作系统的方法。runtime.sysAlloc 会从操作系统中获取一大块可用的内存空间，可能为几百KB或者几MB；runtime.sysFree 会在程序发生内存不足（Out-of Memory，OOM）时调用并无条件地返回内存；runtime.sysReserve 会保留操作系统中的一片内存区域，对这片内存的访问会触发异常；runtime.sysMap 保证内存区域可以快速转换至准备就绪；runtime.sysUsed 通知操作系统应用程序需要使用该内存区域，需要保证内存区域可以安全访问；runtime.sysUnused 通知操作系统虚拟内存对应的物理内存已经不再需要了，它可以重用物理内存；runtime.sysFault 将内存区域转换成保留状态，主要用于运行时的调试；运行时使用Linux提供的mmap、munmap和madvise等系统调用实现了操作系统的内存管理抽象层，抹平了不同操作系统的差异，为运行时提供了更加方便的接口，除了Linux之外，运行时还实现了BSD、Darwin、Plan9以及Windows等平台上抽象层。五、内存布局Go语言的内存分配器包含内存单元、线程缓存、中心缓存和页堆几个重要组件，分别对应运行时的数据结构runtime.mspan、runtime.mcache、runtime.mcentral和runtime.mheap。所有的Go语言程序都会在启动时初始化如上图所示的内存布局，每一个线程都会被分配一个线程缓存runtime.mcache用于处理微对象和小对象的分配，它们会持有内存单元runtime.mspan。每个类别的内存单元都会管理特定大小的对象，当内存管理单元中不存在空闲对象时，它们会从runtime.mheap持有的134个中心缓存runtime.mcentral中获取新的内存单元，中心缓存属于全局的堆结构体runtime.mheap，它会从操作系统中申请内存。在amd64的Linux 操作系统上，runtime.mheap会持有4,194,304runtime.heapArena。runtime.mspan是Go语言内存管理的基本单元，该结构体中包含next和prev两个字段，它们分别指向了前一个和后一个runtime.mspan：type mspan struct { next *mspan prev *mspan ... } 串联后的上述结构体会构成如下双向链表，运行时会使用runtime.mSpanList存储双向链表的头结点和尾节点并在线程缓存以及中心缓存中使用。因为相邻的管理单元会互相引用，所以我们可以从任意一个结构体访问双向链表中的其他节点。每个runtime.mspan都管理npages个大小为8KB的页，这里的页不是操作系统中的内存页，它们是操作系统内存页的整数倍，该结构体会使用下面的这些字段来管理内存页的分配和回收：type mspan struct { startAddr uintptr // 起始地址 npages uintptr // 页数 freeindex uintptr allocBits *gcBits gcmarkBits *gcBits allocCache uint64 ... } startAddr和npages：确定该结构体管理的多个页所在的内存，每个页的大小都是8KB；freeindex：扫描页中空闲对象的初始索引；allocBits和gcmarkBits：分别用于标记内存的占用和回收情况；allocCache：allocBits的补码，可以用于快速查找内存中未被使用的内存；当内存单元runtime.mspan管理的内存不足时，运行时会以页为单位向堆申请内存。当程序向runtime.mspan申请内存时，该结构会使用allocCache字段以对象为单位在管理的内存中快速查找待分配的空间。如果我们能在内存中找到空闲的内存单元，就会直接返回，当内存中不包含空闲的内存时，上一级的组件runtime.mcache可能会为该结构体添加更多的内存页以满足为更多对象分配内存的需求。运行时会使用runtime.mSpanStateBox结构体存储内存单元的状态runtime.mSpanState：type mspan struct { ... state mSpanStateBox ... } 该状态可能处于mSpanDead、mSpanInUse、mSpanManual和mSpanFree四种情况。当runtime.mspan在空闲堆中，它会处于mSpanFree状态；当runtime.mspan已经被分配时，它会处于mSpanInUse、mSpanManual状态，这些状态会在遵循以下规则发生转换：在垃圾回收的任意阶段，可能从 mSpanFree 转换到 mSpanInUse 和 mSpanManual；在垃圾回收的清除阶段，可能从 mSpanInUse 和 mSpanManual 转换到 mSpanFree；在垃圾回收的标记阶段，不能从 mSpanInUse 和 mSpanManual 转换到 mSpanFree；设置runtime.mspan结构体状态的读写操作必须是原子性的避免垃圾回收造成的线程竞争问题。runtime.spanClass是runtime.mspan结构体的类别，它决定了内存单元中存储的对象大小和个数。本质是一个uint8类型的整数，它的前7位存储着内存单元mspan种类的ID，最后一位表示是否包含指针，该类型提供的两个方法能够帮我们快速获取对应的字段。type mspan struct { ... spanclass spanClass ...}Go语言的内存管理模块中一共包含67类的内存单元mspan，每一类内存单元都会存储特定大小的对象并且包含特定数量的页数以及对象，所有的数据都会被预选计算好并存储在runtime.class_to_size和runtime.class_to_allocnpages等变量中。运行时中还包含ID为0的特殊跨度类，它能够管理大于32KB的特殊对象。1、线程缓存runtime.mcache是Go语言中的线程缓存，主要用来缓存线程申请的微小对象。每一个线程缓存都持有67*2个runtime.mspan，这些内存管理单元都存储在结构体的alloc字段中。线程缓存在刚刚被初始化时是不包含runtime.mspan的，只有当用户程序申请内存时才会从上一级组件获取新的runtime.mspan满足内存分配的需求。运行时在初始化处理器时会调用runtime.allocmcache初始化线程缓存，该函数会在系统栈中使用runtime.mheap中的线程缓存分配器初始化新的runtime.mcache结构体。初始化后的runtime.mcache中的所有runtime.mspan都是空的占位符emptymspan。runtime.mcache.refill方法会为线程缓存获取一个指定类别的内存单元，被替换的单元不能包含空闲的内存空间，而获取的单元中需要至少包含一个空闲对象用于分配内存。该函数会从中心缓存中申请新的runtime.mspan存储到线程缓存中，这也是向线程缓存中插入内存管理单元的唯一方法。线程缓存中还包含几个用于分配微对象的字段，下面的这三个字段组成了微对象分配器，专门为16字节以下的对象申请和管理内存：type mcache struct { tiny uintptr tinyoffset uintptr local_tinyallocs uintptr } 微分配器只会用于分配非指针类型的内存，上述三个字段中tiny会指向堆中的一篇内存，tinyOffset是下一个空闲内存所在的偏移量，最后的local_tinyallocs会记录内存分配器中分配的对象个数。2、中心缓存runtime.mcentral是内存分配器的中心缓存，与线程缓存不同，访问中心缓存中的内存单元需要使用互斥锁：type mcentral struct { lock mutex spanclass spanClass nonempty mSpanList empty mSpanList nmalloc uint64 } 每一个中心缓存都会管理某个类别的内存单元，它会同时持有两个runtime.mSpanList，分别存储包含空闲对象的列表和不包含空闲对象的链表。该结构体在初始化时，两个链表都不包含任何内存，程序运行时会扩容结构体持有的两个链表，nmalloc字段也记录了该结构体中分配的对象个数。线程缓存会通过中心缓存的runtime.mcentral.cacheSpan方法获取新的内存管理单元，可以将其分成以下几个部分：从有空闲对象的runtime.mspan链表中查找可以使用的内存单元；从没有空闲对象的runtime.mspan链表中查找可以使用的内存单元；调用runtime.mcentral.grow从堆中申请新的内存单元；更新内存管理单元的allocCache等字段帮助快速分配内存；无论通过哪种方法获取到了内存单元，该方法的最后都会对内存单元的allocBits和allocCache等字段进行更新，让运行时在分配内存时能够快速找到空闲的对象。中心缓存的扩容方法runtime.mcentral.grow会根据预先计算的class_to_allocnpages和class_to_size获取待分配的页数以及spanClass并调用runtime.mheap.alloc获取新的runtime.mspan结构。3、页堆runtime.mheap是内存分配的核心结构体，Go语言程序只会存在一个全局的结构，而堆上初始化的所有对象都由该结构体统一管理，该结构体中包含两组非常重要的字段，其中一个是全局的中心缓存列表central，另一个是管理堆区内存区域的arenas以及相关字段。页堆中包含一个长度为134的runtime.mcentral数组，其中67个为不同类别的scan的中心缓存，另外的67个是noscan的中心缓存，通过runtime.spanClass索引。堆区的初始化会使用runtime.mheap.init方法，该方法初始化了非常多的结构体和字段，不过其中初始化的两类变量比较重要：spanalloc、cachealloc以及 arenaHintAlloc等runtime.fixalloc类型的空闲链表分配器；central切片中runtime.mcentral类型的中心缓存；该方法中初始化所有的中心缓存，这些中心缓存会维护全局的内存单元，各个线程会通过中心缓存获取新的内存单元。运行时会通过它的runtime.mheap.alloc方法在系统栈中获取新的runtime.mspan。runtime.mheap.grow方法会向操作系统申请更多的内存空间，传入的页数经过对齐可以得到期望的内存大小。六、内存分配堆上所有的对象都会通过调用runtime.newobject函数分配内存，该函数会调用runtime.mallocgc分配指定大小的内存空间，这也是用户程序向堆上申请内存空间的必经函数。func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { mp := acquirem() mp.mallocing = 1 c := gomcache() var x unsafe.Pointer noscan := typ == nil || typ.ptrdata == 0 if size <= maxSmallSize { if noscan && size < maxTinySize { // 微对象分配 } else { // 小对象分配 } } else { // 大对象分配 } publicationBarrier() mp.mallocing = 0 releasem(mp) return x } 使用runtime.gomcache获取了线程缓存并通过类型判断类型是否为指针类型。runtime.mallocgc会根据对象的大小执行不同的分配逻辑，运行时根据对象大小将它们分成微对象、小对象和大对象，这里会根据大小选择不同的分配逻辑：微对象 (0, 16B)：先使用微型分配器，再依次尝试线程缓存、中心缓存和堆分配内存；小对象 [16B, 32KB]：依次尝试使用线程缓存、中心缓存和堆分配内存；大对象 (32KB, +∞)：直接在堆上分配内存；运行时对于大于32KB的大对象会单独处理，不会从线程缓存或者中心缓存中获取内存管理单元，而是直接在系统的栈中调用runtime.largeAlloc函数分配大片的内存，函数会计算分配该对象所需要的页数，它会按照8KB的倍数为对象在堆上申请内存。
应用程序的交互，即数据的交互，永远离不开序列化，常见的数据交互格式是json。一、Go语言中的序列化和反序列化go语言中序列化对比：方式优点缺点binary性能高不支持不确定大小类型 int、slice、stringprotobuf支持多种类型，性能高需要单独存放结构，如果结构变动需要重新生成 .pb.go 文件gob支持多种类型性能低json支持多种类型性能低于 binary 和 protobufxml支持多种类型性能低于 json二、jsonjson格式数据是现在数据交互用的最多的数据格式，go一般通过json.Marshal()进行序列化，通过json.Unmarshal()反序列化。type Message struct { Id uint64 `json:"id"` Size uint64 `json:"size"` Data string `json:"data"` } func JsonEncodeDecode() { m1 := Message{3, 1024, "json"} var buf []byte var err error if buf, err = json.Marshal(m1); err != nil { log.Fatal("json marshal error:", err) } var m2 Message if err = json.Unmarshal(buf, &m2); err != nil { log.Fatal("json unmarshal error:", err) } }三、GobGob（Go binary 的缩写）是Go语言自己以二进制形式序列化和反序列化程序数据的格式，其方法在encoding中，只能在go程序之间进行数据交互。type Message struct { Id uint64 Size uint64 Data string } func GobEncodeDecode() { m1 := Message{2, 1024, "gob"} var buf bytes.Buffer enc := gob.NewEncoder(&buf) dec := gob.NewDecoder(&buf) if err := enc.Encode(m1); err != nil { log.Fatal("encode error:", err) } var m2 Message if err := dec.Decode(&m2); err != nil { log.Fatal("decode error:", err) } }四、Binaryendoding包中的binnary主要用于二进制数据序列化，但是局限性较高。注意：数据类型必须是固定大小，如果字段中有不确定大小的类型，如int，slice，string等，则会报错。字节序列方式（order）：binary.BigEndian（大端模式）：内存的低地址存放着数据高位binary.LittleEndian(小端模式)：内存的低地址存放着数据地位type Message struct { Id uint64 Size uint64 } func BinaryRW() { m1 := Message{1, 1024} buf := new(bytes.Buffer) if err := binary.Write(buf, binary.LittleEndian, m1); err != nil { log.Fatal("binary write error:", err) } var m2 Message if err := binary.Read(buf, binary.LittleEndian, &m2); err != nil { log.Fatal("binary read error:", err) } }五、ProtobufProtoBuf并不是go语言中包自带的，需要自行安装protoc编译器，以及protoc库以及生成相关的类syntax = "proto2"; package example; message Message { required uint64 id = 1; required uint64 size = 2; required string data = 3; }func ProtoEncodeDecode() { m1 := &example.Message{ Id: proto.Uint64(4), Size: proto.Uint64(1024), Data: proto.String("proto"), } buf, err := proto.Marshal(m1) if err != nil { log.Fatal("proto marshal error:", err) } var m2 example.Message if err = proto.Unmarshal(buf, &m2); err != nil { log.Fatal("proto unmarshal error:", err) } fmt.Println(m2.GetId(), m2.GetSize(), m2.GetData()) }六、XMLGo语言内置的encoding/xml包可以用在结构体和XML格式之间进行编解码，其方式跟encoding/json包类似type Message struct { Id uint64 Size uint64 Data string } func XmlEncodeDecode() { m1 := Message{2, 1024, "gob"} var buf bytes.Buffer enc := xml.NewEncoder(&buf) dec := xml.NewDecoder(&buf) if err := enc.Encode(m1); err != nil { log.Fatal("encode error:", err) } var m2 Message if err := dec.Decode(&m2); err != nil { log.Fatal("decode error:", err) } }
目录和文件管理方法由Go语言标准库的os包提供一、目录管理相关函数：func Mkdir(name string, perm FileMode) error // 创建名称为name的目录 func MkdirAll(path string, perm FileMode) error // 根据path创建多级子目录 func Remove(name string) error // 删除名称为name的目录或文件，当目录下有文件或目录时出错 func RemoveAll(path string) error // 根据path删除多级子目录，该目录下的子目录全部删除文件权限说明：采用linux系统的权限管理方式，在go语言里以0开头来8进制的数字来表示文件权限，故全权限用0777表示实例：package main import ( "fmt" "os" ) func main() { os.Mkdir("test", 0777) os.MkdirAll("test/test1/test2", 0777) err := os.Remove("test") if err != nil { fmt.Println(err) } os.RemoveAll("test") }二、文件管理相关函数：func Create(name string) (file *File, err Error) // 根据提供的文件名创建新的文件 func NewFile(fd uintptr, name string) *File //根据文件描述符和文件名创建相应的文件 func Remove(name string) Error //删除文件，和删除文件夹是同一个函数文件描述符：go程序并不是直接操作文件，而是通过操作系统提供的接口去操作文件（其它编程语言都是这样的），打开现存文件或新建文件时，操作系统内核会返回一个文件描述符（实质是一个非负整数），读写文件也需要使用文件描述符来指定待读写的文件。实例：package main import ( "fmt" "os" ) func main() { fileName := "testFile" currFile,err:=os.Create(fileName) if err!= nil { fmt.Println(err,currFile); } Close(currFile) }Go程序中有三个默认已经打开的文件：os.Stdin: 标准输入os.Stdout: 标准输出os.Stderr: 标准错误输出三、文件打开和关闭相关函数：func Open(name string) (*File, error) //Open打开一个文件用于读取 func OpenFile(name string, flag int, perm FileMode) (*File, error) //OpenFile是一个更一般性的文件打开函数 func (f *File) Close() error //Close关闭文件f，使文件不能用于读写文件打开模式：const ( O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件 O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件 O_RDWR int = syscall.O_RDWR // 读写模式打开文件 O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部 O_CREATE int = syscall.O_CREAT // 如果不存在将创建一个新文件 O_EXCL int = syscall.O_EXCL // 和O_CREATE配合使用，文件必须不存在 O_SYNC int = syscall.O_SYNC // 打开文件用于同步I/O O_TRUNC int = syscall.O_TRUNC // 如果可能，打开时清空文件 )实例：package main import ( "os" ) func main() { file, err := os.Open("/home/test.txt") //只读打开 if err != nil { fmt.Println("open file error: ", err) return } file.Close() //关闭文件 }目录也是可以打开和关闭的，和文件一样的四、文件操作方法详细说明os.File是一个结构体，其封装了诸多操作文件的方法： func Create(name string) (*File, error) //Create采用模式0666（任何人都可读写，不可执行）创建一个名为name的文件，如果文件已存在会截断它（为空文件）。如果成功，返回的文件对象可用于I/O；对应的文件描述符具有O_RDWR模式。如果出错，错误底层类型是*PathError。 func NewFile(fd uintptr, name string) *File //NewFile使用给出的Unix文件描述符和名称创建一个文件。 func Open(name string) (*File, error) //Open打开一个文件用于读取。如果操作成功，返回的文件对象的方法可用于读取数据；对应的文件描述符具有O_RDONLY模式。如果出错，错误底层类型是*PathError。 func OpenFile(name string, flag int, perm FileMode) (*File, error) //OpenFile是一个更一般性的文件打开函数，大多数调用者都应用Open或Create代替本函数。它会使用指定的选项（如O_RDONLY等）、指定的模式（如0666等）打开指定名称的文件。如果操作成功，返回的文件对象可用于I/O。如果出错，错误底层类型是*PathError。 func Pipe() (r *File, w *File, err error) //Pipe返回一对关联的文件对象。从r的读取将返回写入w的数据。本函数会返回两个文件对象和可能的错误。 func (f *File) Chdir() error //Chdir将当前工作目录修改为f，f必须是一个目录。如果出错，错误底层类型是*PathError。 func (f *File) Chmod(mode FileMode) error //Chmod修改文件权限。如果出错，错误底层类型是*PathError。 func (f *File) Chown(uid, gid int) error //修改文件文件用户id和组id func (f *File) Close() error //Close关闭文件f，使文件不能用于读写。它返回可能出现的错误。 func (f *File) Fd() uintptr //Fd返回与文件f对应的整数类型的Unix文件描述符。 func (f *File) Name() string //Name方法返回（提供给Open/Create等方法的）文件名称。 func (f *File) Read(b []byte) (n int, err error) //Read方法从f中读取最多len(b)字节数据并写入b。它返回读取的字节数和可能遇到的任何错误。文件终止标志是读取0个字节且返回值err为io.EOF。 func (f *File) ReadAt(b []byte, off int64) (n int, err error) //ReadAt从指定的位置（相对于文件开始位置）读取len(b)字节数据并写入b。它返回读取的字节数和可能遇到的任何错误。当n<len(b)时，本方法总是会返回错误；如果是因为到达文件结尾，返回值err会是io.EOF。 func (f *File) Readdir(n int) ([]FileInfo, error) //Readdir读取目录f的内容，返回一个有n个成员的[]FileInfo，这些FileInfo是被Lstat返回的，采用目录顺序。对本函数的下一次调用会返回上一次调用剩余未读取的内容的信息。如果n>0，Readdir函数会返回一个最多n个成员的切片。这时，如果Readdir返回一个空切片，它会返回一个非nil的错误说明原因。如果到达了目录f的结尾，返回值err会是io.EOF。如果n<=0，Readdir函数返回目录中剩余所有文件对象的FileInfo构成的切片。此时，如果Readdir调用成功（读取所有内容直到结尾），它会返回该切片和nil的错误值。如果在到达结尾前遇到错误，会返回之前成功读取的FileInfo构成的切片和该错误。 func (f *File) Readdirnames(n int) (names []string, err error) //Readdir读取目录f的内容，返回一个有n个成员的[]string，切片成员为目录中文件对象的名字，采用目录顺序。对本函数的下一次调用会返回上一次调用剩余未读取的内容的信息。如果n>0，Readdir函数会返回一个最多n个成员的切片。这时，如果Readdir返回一个空切片，它会返回一个非nil的错误说明原因。如果到达了目录f的结尾，返回值err会是io.EOF。如果n<=0，Readdir函数返回目录中剩余所有文件对象的名字构成的切片。此时，如果Readdir调用成功（读取所有内容直到结尾），它会返回该切片和nil的错误值。如果在到达结尾前遇到错误，会返回之前成功读取的名字构成的切片和该错误。 func (f *File) Seek(offset int64, whence int) (ret int64, err error) //Seek设置下一次读/写的位置。offset为相对偏移量，而whence决定相对位置：0为相对文件开头，1为相对当前位置，2为相对文件结尾。它返回新的偏移量（相对开头）和可能的错误。 func (f *File) SetDeadline(t time.Time) error // 设置文件读取和写入时间，超时返回错误 func (f *File) SetReadDeadline(t time.Time) error //设置文件读取时间 func (f *File) SetWriteDeadline(t time.Time) error // 设置文件写入时间 func (f *File) Stat() (FileInfo, error) //Stat返回描述文件f的FileInfo类型值。如果出错，错误底层类型是*PathError。 func (f *File) Sync() error //Sync递交文件的当前内容进行稳定的存储。一般来说，这表示将文件系统的最近写入的数据在内存中的拷贝刷新到硬盘中稳定保存。 func (f *File) Truncate(size int64) error //Truncate改变文件的大小，它不会改变I/O的当前位置。 如果截断文件，多出的部分就会被丢弃。如果出错，错误底层类型是*PathError。 func (f *File) Write(b []byte) (n int, err error) //Write向文件中写入len(b)字节数据。它返回写入的字节数和可能遇到的任何错误。如果返回值n!=len(b)，本方法会返回一个非nil的错误。 func (f *File) WriteAt(b []byte, off int64) (n int, err error) //将len(b)字节写入文件，从字节偏移开始。它返回写入的字节数和错误，写的时候返回一个错误，当n != len（b） func (f *File) WriteString(s string) (n int, err error) //WriteString类似Write，参数为字符串。
nil是Go中的一个使用频率很高的预声明标识符。很多种类的类型的零值都用nil表示。很多有其它语言编程经验的程序员在初学Go语言的时候常将nil看成是其它语言中的null或者NULL。这种看法只是部分上正确的，但是Go中的nil和其它语言中的null或者NULL也是有很大的区别的。一、nil是一个预声明的标识符在Go中，预声明的nil可以表示下列种类（kind）的类型的零值：指针类型（包括类型安全和非类型安全指针）映射类型切片类型函数类型通道类型接口类型二、预声明标识符nil没有默认类型Go中其它的预声明标识符都有各自的默认类型，比如预声明标识符true和false的默认类型均为内置类型bool。预声明标识符iota的默认类型为内置类型int。 但是，预声明标识符nil没有一个默认类型，尽管它有很多潜在的可能类型。事实上，预声明标识符nil是Go中唯一一个没有默认类型的类型不确定值。我们必须在代码中提供足够的信息以便让编译器能够推断出一个类型不确定的nil值的期望类型。package main func main() { // 代码中必须提供充足的信息来让编译器推断出某个nil的类型。 _ = (*struct{})(nil) _ = []int(nil) _ = map[int]bool(nil) _ = chan string(nil) _ = (func())(nil) _ = interface{}(nil) // 下面这一组和上面这一组等价。 var _ *struct{} = nil var _ []int = nil var _ map[int]bool = nil var _ chan string = nil var _ func() = nil var _ interface{} = nil // 下面这行编译不通过。 var _ = nil }三、nil值尺寸不同种类的类型的nil值的尺寸很可能不相同,一个类型的所有值的内存布局都是一样的，此类型nil值也不例外（假设此类型的零值使用nil表示）。所以同一个类型的nil值和非nil值的尺寸是一样的。但是不同类型的nil值的尺寸可能是不一样的。package main import ( "fmt" "unsafe" ) func main() { var p *struct{} = nil fmt.Println( unsafe.Sizeof( p ) ) // 8 var s []int = nil fmt.Println( unsafe.Sizeof( s ) ) // 24 var m map[int]bool = nil fmt.Println( unsafe.Sizeof( m ) ) // 8 var c chan string = nil fmt.Println( unsafe.Sizeof( c ) ) // 8 var f func() = nil fmt.Println( unsafe.Sizeof( f ) ) // 8 var i interface{} = nil fmt.Println( unsafe.Sizeof( i ) ) // 16 }上例中的输出是使用官方标准编译器编译并在64位的系统架构上运行的结果。在32位的系统架构上，这些输出值将减半。四、nil值比较两个不同类型的nil值可能不能相互比较// error: 类型不匹配 var _ = (*int)(nil) == (*bool)(nil) // error: 类型不匹配 var _ = (chan int)(nil) == (chan bool)(nil)同一个类型的两个nil值可能不能相互比较在Go中，映射类型、切片类型和函数类型是不支持比较类型。比较同一个不支持比较的类型的两个值（包括nil值）是非法的。// 下面的几个比较都编译不通过 var _ = ([]int)(nil) == ([]int)(nil) var _ = (map[string]int)(nil) == (map[string]int)(nil) var _ = (func())(nil) == (func())(nil)但是，映射类型、切片类型和函数类型的任何值都可以和类型不确定的裸nil标识符比较。// 这几行编译都没问题。 var _ = ([]int)(nil) == nil var _ = (map[string]int)(nil) == nil var _ = (func())(nil) == nil两个nil值可能并不相等如果可被比较的两个nil值中的一个的类型为接口类型，而另一个不是，则比较结果总是false。原因是，在进行此比较之前，此非接口nil值将被转换为另一个nil值的接口类型，从而将此比较转化为两个接口值的比较。从接口一文中，我们得知每个接口值可以看作是一个包裹非接口值的盒子。一个非接口值被转换为一个接口类型的过程可以看作是用一个接口值将此非接口值包裹起来的过程。一个nil接口值中什么也没包裹，但是一个包裹了nil非接口值的接口值并非什么都没包裹。一个什么都没包裹的接口值和一个包裹了一个非接口值（即使它是nil）的接口值是不相等的。fmt.Println( (interface{})(nil) == (*int)(nil) ) // false五、nil值访问访问nil映射值的条目不会产生恐慌访问一个nil映射将得到此映射的类型的元素类型的零值。fmt.Println( (map[string]int)(nil)["key"] ) // 0 fmt.Println( (map[int]bool)(nil)[123] ) // false fmt.Println( (map[int]*int64)(nil)[123] ) // <nil>range关键字后可以跟随nil通道、nil映射、nil切片和nil数组指针遍历nil映射和nil切片的循环步数均为零。遍历一个nil数组指针的循环步数为对应数组类型的长度。（但是，如果此数组类型的长度不为零并且第二个循环变量未被舍弃或者忽略，则对应for-range循环将导致一个恐慌。）遍历一个nil通道将使当前协程永久阻塞。比如，下面的代码将输出0、1、2、3和4后进入阻塞状态。Hello、world和Bye不会被输出。for range []int(nil) { fmt.Println("Hello") } for range map[string]string(nil) { fmt.Println("world") } for i := range (*[5]int)(nil) { fmt.Println(i) } for range chan bool(nil) { // 阻塞在此 fmt.Println("Bye") }六、nil值的非接口属主方法调用不会造成恐慌package main type Slice []bool func (s Slice) Length() int { return len(s) } func (s Slice) Modify(i int, x bool) { s[i] = x // panic if s is nil } func (p *Slice) DoNothing() { } func (p *Slice) Append(x bool) { *p = append(*p, x) // 如果p为空指针，则产生一个恐慌。 } func main() { // 下面这几行中的选择器不会造成恐慌。 _ = ((Slice)(nil)).Length _ = ((Slice)(nil)).Modify _ = ((*Slice)(nil)).DoNothing _ = ((*Slice)(nil)).Append // 这两行也不会造成恐慌。 _ = ((Slice)(nil)).Length() ((*Slice)(nil)).DoNothing() // 下面这两行都会造成恐慌。但是恐慌不是因为nil // 属主实参造成的。恐慌都来自于这两个方法内部的 // 对空指针的解引用操作。 /* ((Slice)(nil)).Modify(0, true) ((*Slice)(nil)).Append(true) */ }事实上，上面的Append方法实现不完美。我们应该像下面这样实现之：func (p *Slice) Append(x bool) { if p == nil { *p = []bool{x} return } *p = append(*p, x) }七、总结在Go中，为了简单和方便，nil被设计成一个可以表示成很多种类型的零值的预声明标识符。换句话说，它可以表示很多内存布局不同的值，而不仅仅是一个值。