You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dive-into-go-v2/public/en.search-data.min.9af27c93...

1 line
343 KiB
JSON

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

[{"id":0,"href":"/memory/","title":"Memory","section":"简介","content":" 内存管理 # 一张一弛,文武之道。\n内存分配器 GC "},{"id":1,"href":"/memory/allocator/","title":"Allocator","section":"Memory","content":" 内存分配器 # 概述 # Golang内存分配管理策略是按照不同大小的对象和不同的内存层级来分配管理内存。通过这种多层级分配策略形成无锁化或者降低锁的粒度以及尽量减少内存碎片来提高内存分配效率。\nGolang中内存分配管理的对象按照大小可以分为\n类别 大小 微对象 tiny object (0, 16B) 小对象 small object [16B, 32KB] 大对象 large object (32KB, +∞) Golang中内存管理的层级从最下到最上可以分为mspan -\u0026gt; mcache -\u0026gt; mcentral -\u0026gt; mheap -\u0026gt; heapArena。golang中对象的内存分配流程如下\n小于16个字节的对象使用mcache的微对象分配器进行分配内存 大小在16个字节到32k字节之间的对象首先计算出需要使用的span大小规格然后使用mcache中相同大小规格的mspan分配 如果对应的大小规格在mcache中没有可用的mspan则向mcentral申请 如果mcentral中没有可用的mspan则向mheap申请并根据BestFit算法找到最合适的mspan。如果申请到的mspan超出申请大小将会根据需求进行切分以返回用户所需的页数剩余的页构成一个新的mspan放回mheap的空闲列表 如果mheap中没有可用span则向操作系统申请一系列新的页最小 1MB 对于大于32K的大对象直接从mheap分配 mspan # mspan是一个双向链表结构。mspan是golang中内存分配管理的基本单位。\n// file: mheap.go type mspan struct { next *mspan // 指向下一个mspan prev *mspan // 指向上一个mspan startAddr uintptr // 该span在arena区域起始地址 npages uintptr // 该span在arena区域中占用page个数 manualFreeList gclinkptr // 空闲对象列表 freeindex uintptr // 下一个空闲span的索引freeindex大小介于0到nelems当freeindex == nelem表明该span中没有空余对象空间了 // freeindex之前的元素均是已经被使用的freeindex之后的元素可能被使用也可能没被使用 // freeindex 和 allocCache配合使用来定位出可用span的位置 nelems uintptr // span链表中元素个数 allocCache uint64 // 初始值为2^64-1位值置为1假定该位的位置是pos的表明该span链表中对应的freeindex+pos位置的span未使用 allocBits *gcBits // 标识该span中所有元素的使用分配情况位值置为1则标识span链表中对应位置的span已被分配 gcmarkBits *gcBits // 用来sweep过程进行标记垃圾对象的用于后续gc。 allocCount uint16 // 已分配的对象个数 spanclass spanClass // span类别 state mSpanState // mspaninuse etc needzero uint8 // needs to be zeroed before allocation elemsize uintptr // 能存储的对象大小 } // file: mheap.go type spanClass uint8 // span规格类型 span大小一共有67个规格。规格列表如下 其中class = 0 是特殊的span用于大于32kb对象分配是直接从mheap上分配的\n# file: sizeclasses.go // class bytes/obj bytes/span objects tail waste max waste // 1 8 8192 1024 0 87.50% // 2 16 8192 512 0 43.75% // 3 32 8192 256 0 46.88% // 4 48 8192 170 32 31.52% // 5 64 8192 128 0 23.44% ... // 64 27264 81920 3 128 10.00% // 65 28672 57344 2 0 4.91% // 66 32768 32768 1 0 12.50% class - 规格id即spanClass bytes/obj - 能够存储的对象大小对应的是mspan的elemsize字段 bytes/span - 每个span的大小大小等于页数*页大小即8k * npages object - 每个span能够存储的objects个数即nelems也等于(bytes/span)/(bytes/obj) tail waste - 每个span产生的内存碎片bytes/span%bytes/obj max waste - 最大浪费比例bytes/obj-span最小使用量*objects/(bytes/span)*100,比如class =2时span运行的最小使用量是9bytes则max waste=16-9512/8192100=43.75% mcache # mcache持有一系列不同大小的mspan。mcache属于per-P cache由于M运行G时候必须绑定一个P这样当G中申请从mcache分配对象内存时候无需加锁处理。\n// file: mcache.go type mcache struct { next_sample uintptr // trigger heap sample after allocating this many bytes local_scan uintptr // bytes of scannable heap allocated // 微对象分配器对象大小需要小于16byte tiny uintptr // 微对象起始地址 tinyoffset uintptr // 从tiny开始的偏移值 local_tinyallocs uintptr // tiny对象的个数 // 大小为134的指针数组数组元素指向mspanSpanClasses一共有67种为了满足指针对象和非指针对象这里为每种规格的span同时准备scan和noscan两个分别用于存储指针对象和非指针对象 alloc [numSpanClasses]*mspan stackcache [_NumStackOrders]stackfreelist // 栈缓存 // Local allocator stats, flushed during GC. local_largefree uintptr // 大对象释放的字节数 local_nlargefree uintptr // 释放的大对象个数 local_nsmallfree [_NumSizeClasses]uintptr // 大小为64的数组每种规格span是否的小对象个数 flushGen uint32 // 扫描计数 } // file: malloc.go if size \u0026lt;= maxSmallSize { // 如果size \u0026lt;= 32k if noscan \u0026amp;\u0026amp; size \u0026lt; maxTinySize { // 不需要扫描且size\u0026lt;16 if size\u0026amp;7 == 0 { off = round(off, 8) } else if size\u0026amp;3 == 0 { off = round(off, 4) } else if size\u0026amp;1 == 0 { off = round(off, 2) } if off+size \u0026lt;= maxTinySize \u0026amp;\u0026amp; c.tiny != 0 { // The object fits into existing tiny block. x = unsafe.Pointer(c.tiny + off) c.tinyoffset = off + size c.local_tinyallocs++ mp.mallocing = 0 releasem(mp) return x } // Allocate a new maxTinySize block. span := c.alloc[tinySpanClass] v := nextFreeFast(span) if v == 0 { v, _, shouldhelpgc = c.nextFree(tinySpanClass) } x = unsafe.Pointer(v) (*[2]uint64)(x)[0] = 0 (*[2]uint64)(x)[1] = 0 // See if we need to replace the existing tiny block with the new one // based on amount of remaining free space. if size \u0026lt; c.tinyoffset || c.tiny == 0 { c.tiny = uintptr(x) c.tinyoffset = size } size = maxTinySize } else { // 16b ~ 32kb var sizeclass uint8 if size \u0026lt;= smallSizeMax-8 { sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv] } else { sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv] } size = uintptr(class_to_size[sizeclass]) spc := makeSpanClass(sizeclass, noscan) span := c.alloc[spc] v := nextFreeFast(span) if v == 0 { v, span, shouldhelpgc = c.nextFree(spc) } x = unsafe.Pointer(v) if needzero \u0026amp;\u0026amp; span.needzero != 0 { memclrNoHeapPointers(unsafe.Pointer(v), size) } } } else {// \u0026gt; 32kb var s *mspan shouldhelpgc = true systemstack(func() { s = largeAlloc(size, needzero, noscan) }) s.freeindex = 1 s.allocCount = 1 x = unsafe.Pointer(s.base()) size = s.elemsize } mcentral # 当mcache的中没有可用的span时候会向mcentral申请mcetral结构如下\ntype mcentral struct { lock mutex // 锁由于每个p关联的mcache都可能会向mcentral申请空闲的span所以需要加锁 spanclass spanClass // mcentral负责的span规格 nonempty mSpanList // 空闲span列表 empty mSpanList // 已经使用的span列表 nmalloc uint64 // mcentral已分配的span计数 } 一个mecentral只负责一个规格span规格类型记录在mcentral的spanClass字段中。mcentral维护着两个双向链表nonempty表示链表里还有空闲的mspan待分配。empty表示这条链表里的mspan都被分配了object。mcache从mcentrl中获取和归还span流程如下\n获取时候先加锁先从nonempty中获取一个没有分配使用的span将其从nonempty中删除并将span加入empty链表mcache获取之后释放锁。 归还时候先加锁先将span加入nonempty链表中并从empty链表中删除最后释放锁。 mheap # 当mecentral没有可用的span时候会向mheap申请。\ntype mheap struct { // lock must only be acquired on the system stack, otherwise a g // could self-deadlock if its stack grows with the lock held. lock mutex free mTreap // 空闲的并且没有被os收回的二叉树堆大对象用 sweepgen uint32 // 扫描计数值每次gc后自增2 sweepdone uint32 // all spans are swept sweepers uint32 // number of active sweepone calls allspans []*mspan // 所有的span sweepSpans [2]gcSweepBuf _ uint32 // align uint64 fields on 32-bit for atomics pagesInUse uint64 // pages of spans in stats mSpanInUse; R/W with mheap.lock pagesSwept uint64 // pages swept this cycle; updated atomically pagesSweptBasis uint64 // pagesSwept to use as the origin of the sweep ratio; updated atomically sweepHeapLiveBasis uint64 // value of heap_live to use as the origin of sweep ratio; written with lock, read without sweepPagesPerByte float64 // proportional sweep ratio; written with lock, read without // TODO(austin): pagesInUse should be a uintptr, but the 386 // compiler can\u0026#39;t 8-byte align fields. scavengeTimeBasis int64 scavengeRetainedBasis uint64 scavengeBytesPerNS float64 scavengeRetainedGoal uint64 scavengeGen uint64 // incremented on each pacing update reclaimIndex uint64 reclaimCredit uintptr // Malloc stats. largealloc uint64 // bytes allocated for large objects nlargealloc uint64 // number of large object allocations largefree uint64 // bytes freed for large objects (\u0026gt;maxsmallsize) nlargefree uint64 // number of frees for large objects (\u0026gt;maxsmallsize) nsmallfree [_NumSizeClasses]uint64 // number of frees for small objects (\u0026lt;=maxsmallsize) arenas [1 \u0026lt;\u0026lt; arenaL1Bits]*[1 \u0026lt;\u0026lt; arenaL2Bits]*heapArena heapArenaAlloc linearAlloc arenaHints *arenaHint arena linearAlloc allArenas []arenaIdx sweepArenas []arenaIdx curArena struct { base, end uintptr } _ uint32 // ensure 64-bit alignment of central // 各个尺寸的central central [numSpanClasses]struct { mcentral mcentral pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte } spanalloc fixalloc // allocator for span* cachealloc fixalloc // allocator for mcache* treapalloc fixalloc // allocator for treapNodes* specialfinalizeralloc fixalloc // allocator for specialfinalizer* specialprofilealloc fixalloc // allocator for specialprofile* speciallock mutex // lock for special record allocators. arenaHintAlloc fixalloc // allocator for arenaHints unused *specialfinalizer // never set, just here to force the specialfinalizer type into DWARF } heapArena # heapArenaBytes = 1 \u0026lt;\u0026lt; logHeapArenaBytes logHeapArenaBytes = (6+20)*(_64bit*(1-sys.GoosWindows)*(1-sys.GoosAix)*(1-sys.GoarchWasm)) + (2+20)*(_64bit*sys.GoosWindows) + (2+20)*(1-_64bit) + (8+20)*sys.GoosAix + (2+20)*sys.GoarchWasm // heapArenaBitmapBytes is the size of each heap arena\u0026#39;s bitmap. heapArenaBitmapBytes = heapArenaBytes / (sys.PtrSize * 8 / 2) pagesPerArena = heapArenaBytes / pageSize type heapArena struct { bitmap [heapArenaBitmapBytes]byte spans [pagesPerArena]*mspan pageInUse [pagesPerArena / 8]uint8 pageMarks [pagesPerArena / 8]uint8 } heapArena中arena区域是真正的堆区所有分配的span都是从这个地方分配。arena区域管理的单元大小是pagepage页数为pagesPerArena。\n在64位linux系统runtime.mheap会持有 4,194,304 runtime.heapArena每个 runtime.heapArena 都会管理 64MB 的内存所有golang的内存上限是256TB。\n"},{"id":2,"href":"/memory/gc/","title":"Gc","section":"Memory","content":" GC # 三色标记清除算法 # Golang中采用 三色标记清除算法tricolor mark-and-sweep algorithm 进行GC。由于支持写屏障write barrier)了GC过程和程序可以并发运行。\n三色标记清除算核心原则就是根据每个对象的颜色分到不同的颜色集合中对象的颜色是在标记阶段完成的。三色是黑白灰三种颜色每种颜色的集合都有特别的含义\n黑色集合\n该集合下的对象没有引用任何白色对象即该对象没有指针指向白色对象\n白色集合\n扫描标记结束之后白色集合里面的对象就是要进行垃圾回收的该对象允许有指针指向黑色对象。\n灰色集合\n可能有指针指向白色对象。它是一个中间状态只有该集合下不在存在任何对象时候才能进行最终的清除操作。\n过程 # 标记清除算法核心不变要素是没有黑色的对象能够指向白色集合对象。当垃圾回收开始,全部对象标记为白色,然后垃圾回收器会遍历所有根对象并把它们标记为灰色。根对象就是程序能直接访问到的对象,包括全局变量以及栈、寄存器上的里面的变量。在这之后,垃圾回收器选取一个灰色的对象,首先把它变为黑色,然后开始寻找去确定这个对象是否有指针指向白色集合的对象,若找到则把找到的对象由标记为灰色,并将其白色集合中移入到灰色集合中。就这样持续下去,直到灰色集合中没有任何对象为止。\n为了支持能够并发进行垃圾回收Golang在垃圾回收过程中采用写屏障每次堆中的指针被修改时候写屏障都会执行写屏障会将该指针指向的对象标记为灰色然后放入灰色集合因为才对象现在是可触达的了然后继续扫描该对象。\n举个例子说明写屏障的重要性\n假定标记完成的瞬间A对象是黑色B是白色然后A的对象指针字段f由空指针改成指向B若没有写屏障的话清除阶段B就会被清除掉那边A的f字段就变成了悬浮指针这是有问题的。若存在写屏障那么f字段改变的时候f指向的B就会放入到灰色集合中然后继续扫描B最终也会变成黑色的那么清除阶段它也就不会被清除了。\n"},{"id":3,"href":"/analysis-tools/dlv/","title":"Delve is a debugger for the Go programming language","section":"Go语言分析工具","content":" Delve # Delve1 是使用Go语言实现的专门用来调试Go程序的工具。它跟 GDB 工具类似,相比 GDB它简单易用能更好的理解Go语言数据结构和语言特性。它支持打印 goroutine 以及 defer 函数等Go特有语法特性。Delve 简称 dlv后文将以 dlv 代称 Delve.\n安装 # go get -u github.com/go-delve/delve/cmd/dlv 使用 # 开始调试 # dlv 使用 debug 命令进入调试界面:\ndlv debug main.go 如果当前目录是 main 包所在目录时候,可以不用指定 main.go 文件这个参数的。假定项目结构如下:\n. ├── github.com/me/foo ├── cmd │ └── foo │ └── main.go ├── pkg │ └── baz │ ├── bar.go │ └── bar_test.go 如果当前已在 cmd/foo 目录下,我们可以直接执行 dlv debug 命令开始调试。在任何目录下我们可以使用 dlv debug github.com/me/foo/cmd/foo 开始调试。\n如果已构建成二进制可执行文件我们可以使用 dlv exec 命令开始调试:\ndlv exec /youpath/go_binary_file 对于需要命令行参数才能启动的程序,我们可以通过--来传递命令行参数,比如如下:\ndlv debug github.com/me/foo/cmd/foo -- -arg1 value dlv exec /mypath/binary -- --config=config.toml 对于已经运行的程序,可以使用 attach 命令,进行跟踪调试指定 pid 的Go应用\ndlv attach pid 除了上面调试 main 包外dlv 通过 test 子命令还支持调试 test 文件:\ndlv test github.com/me/foo/pkg/baz 接下来我们可以使用 help 命令查看 dlv 支持的命令有哪些:\n(dlv) help The following commands are available: Running the program: call ------------------------ Resumes process, injecting a function call (EXPERIMENTAL!!!) continue (alias: c) --------- Run until breakpoint or program termination. next (alias: n) ------------- Step over to next source line. rebuild --------------------- Rebuild the target executable and restarts it. It does not work if the executable was not built by delve. restart (alias: r) ---------- Restart process. step (alias: s) ------------- Single step through program. step-instruction (alias: si) Single step a single cpu instruction. stepout (alias: so) --------- Step out of the current function. Manipulating breakpoints: break (alias: b) ------- Sets a breakpoint. breakpoints (alias: bp) Print out info for active breakpoints. clear ------------------ Deletes breakpoint. clearall --------------- Deletes multiple breakpoints. condition (alias: cond) Set breakpoint condition. on --------------------- Executes a command when a breakpoint is hit. trace (alias: t) ------- Set tracepoint. Viewing program variables and memory: args ----------------- Print function arguments. display -------------- Print value of an expression every time the program stops. examinemem (alias: x) Examine memory: locals --------------- Print local variables. print (alias: p) ----- Evaluate an expression. regs ----------------- Print contents of CPU registers. set ------------------ Changes the value of a variable. vars ----------------- Print package variables. whatis --------------- Prints type of an expression. Listing and switching between threads and goroutines: goroutine (alias: gr) -- Shows or changes current goroutine goroutines (alias: grs) List program goroutines. thread (alias: tr) ----- Switch to the specified thread. threads ---------------- Print out info for every traced thread. Viewing the call stack and selecting frames: deferred --------- Executes command in the context of a deferred call. down ------------- Move the current frame down. frame ------------ Set the current frame, or execute command on a different frame. stack (alias: bt) Print stack trace. up --------------- Move the current frame up. Other commands: config --------------------- Changes configuration parameters. disassemble (alias: disass) Disassembler. edit (alias: ed) ----------- Open where you are in $DELVE_EDITOR or $EDITOR exit (alias: quit | q) ----- Exit the debugger. funcs ---------------------- Print list of functions. help (alias: h) ------------ Prints the help message. libraries ------------------ List loaded dynamic libraries list (alias: ls | l) ------- Show source code. source --------------------- Executes a file containing a list of delve commands sources -------------------- Print list of source files. types ---------------------- Print list of types Type help followed by a command for full documentation. 接下来我们将以下面代码作为示例演示如何dlv进行调试。\npackage main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;go\u0026#34;) } 设置断点 # 当我们使用 dlv debug main.go 命令进行 dlv 调试之后,我们可以设置断点。\n(dlv) b main.main # 在main函数处设置断点 Breakpoint 1 set at 0x4adf8f for main.main() ./main.go:5 继续执行 # 设置断点之后,我们可以通过 continue 命令,可以简写成 c ,继续执行到我们设置的断点处。\n(dlv) c \u0026gt; main.main() ./main.go:5 (hits goroutine(1):1 total:1) (PC: 0x4adf8f) 1:\tpackage main 2: 3:\timport \u0026#34;fmt\u0026#34; 4: =\u0026gt; 5:\tfunc main() { 6:\tfmt.Println(\u0026#34;go\u0026#34;) 7:\t} 注意不同于 GDB 需要执行 run 命令启动应用之后,才能执行 continue 命令。而 dlv 在进入调试界面之后,已经指向程序的入口地址处,可以直接执行 continue 命令\n执行下一条指令 # 我们可以通过next命令可以简写成n来执行下一行源码。同 GDB 一样next 命令是 Step over 操作,遇到函数时不会进入函数内部一行行代码执行,而是直接执行函数,然后跳过到函数下面的一行代码。\n(dlv) n go \u0026gt; main.main() ./main.go:7 (PC: 0x4adfff) 2: 3:\timport \u0026#34;fmt\u0026#34; 4: 5:\tfunc main() { 6:\tfmt.Println(\u0026#34;go\u0026#34;) =\u0026gt; 7:\t} 打印栈信息 # 通过 stack 命令,我们可以查看函数栈信息:\n(dlv) stack 0 0x00000000004adfff in main.main at ./main.go:7 1 0x0000000000436be8 in runtime.main at /usr/lib/go/src/runtime/proc.go:203 2 0x0000000000464621 in runtime.goexit at /usr/lib/go/src/runtime/asm_amd64.s:1373 打印gorountine信息 # 通过goroutines命令可以简写成grs我们可以查看所有 goroutine\n(dlv) goroutines * Goroutine 1 - User: ./main.go:7 main.main (0x4adfff) (thread 14358) Goroutine 2 - User: /usr/lib/go/src/runtime/proc.go:305 runtime.gopark (0x436f9b) Goroutine 3 - User: /usr/lib/go/src/runtime/proc.go:305 runtime.gopark (0x436f9b) Goroutine 4 - User: /usr/lib/go/src/runtime/proc.go:305 runtime.gopark (0x436f9b) Goroutine 5 - User: /usr/lib/go/src/runtime/mfinal.go:161 runtime.runfinq (0x418f80) [5 goroutines] goroutine 命令,可以简写成 gr用来显示当前 goroutine 信息:\n(dlv) goroutine Thread 14358 at ./main.go:7 Goroutine 1: Runtime: ./main.go:7 main.main (0x4adfff) User: ./main.go:7 main.main (0x4adfff) Go: /usr/lib/go/src/runtime/asm_amd64.s:220 runtime.rt0_go (0x462594) Start: /usr/lib/go/src/runtime/proc.go:113 runtime.main (0x436a20) 查看汇编代码 # 通过 disassemble 命令,可以简写成 disass ,我们可以查看汇编代码:\n(dlv) disass TEXT main.main(SB) /tmp/dlv/main.go main.go:5\t0x4adf80\t64488b0c25f8ffffff\tmov rcx, qword ptr fs:[0xfffffff8] main.go:5\t0x4adf89\t483b6110\tcmp rsp, qword ptr [rcx+0x10] main.go:5\t0x4adf8d\t767a\tjbe 0x4ae009 main.go:5\t0x4adf8f*\t4883ec68\tsub rsp, 0x68 main.go:5\t0x4adf93\t48896c2460\tmov qword ptr [rsp+0x60], rbp main.go:5\t0x4adf98\t488d6c2460\tlea rbp, ptr [rsp+0x60] main.go:6\t0x4adf9d\t0f57c0\txorps xmm0, xmm0 main.go:6\t0x4adfa0\t0f11442438\tmovups xmmword ptr [rsp+0x38], xmm0 main.go:6\t0x4adfa5\t488d442438\tlea rax, ptr [rsp+0x38] main.go:6\t0x4adfaa\t4889442430\tmov qword ptr [rsp+0x30], rax main.go:6\t0x4adfaf\t8400\ttest byte ptr [rax], al main.go:6\t0x4adfb1\t488d0d28ed0000\tlea rcx, ptr [rip+0xed28] main.go:6\t0x4adfb8\t48894c2438\tmov qword ptr [rsp+0x38], rcx main.go:6\t0x4adfbd\t488d0dcce10300\tlea rcx, ptr [rip+0x3e1cc] main.go:6\t0x4adfc4\t48894c2440\tmov qword ptr [rsp+0x40], rcx main.go:6\t0x4adfc9\t8400\ttest byte ptr [rax], al main.go:6\t0x4adfcb\teb00\tjmp 0x4adfcd main.go:6\t0x4adfcd\t4889442448\tmov qword ptr [rsp+0x48], rax main.go:6\t0x4adfd2\t48c744245001000000\tmov qword ptr [rsp+0x50], 0x1 main.go:6\t0x4adfdb\t48c744245801000000\tmov qword ptr [rsp+0x58], 0x1 main.go:6\t0x4adfe4\t48890424\tmov qword ptr [rsp], rax main.go:6\t0x4adfe8\t48c744240801000000\tmov qword ptr [rsp+0x8], 0x1 main.go:6\t0x4adff1\t48c744241001000000\tmov qword ptr [rsp+0x10], 0x1 main.go:6\t0x4adffa\te811a1ffff\tcall $fmt.Println =\u0026gt;\tmain.go:7\t0x4adfff\t488b6c2460\tmov rbp, qword ptr [rsp+0x60] main.go:7\t0x4ae004\t4883c468\tadd rsp, 0x68 main.go:7\t0x4ae008\tc3\tret main.go:5\t0x4ae009\te8e247fbff\tcall $runtime.morestack_noctxt \u0026lt;autogenerated\u0026gt;:1\t0x4ae00e\te96dffffff\tjmp $main.main dlv 默认显示的是 intel 风格汇编代码,我们可以通过 config 命令设置 gnu 或者 go 风格代码:\n(dlv) config disassemble-flavor go 这种方式更改的配置只会对此次调试有效若保证下次调试一样有效我们需要将其配置到配置文件中。dlv 默认配置文件是 HOME/.config/dlv/config.yml。我们只需要在配置文件加入以下内容\ndisassemble-flavor: go https://github.com/go-delve/delve\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n"},{"id":4,"href":"/feature/for-range/","title":"for-range语法","section":"语言特性","content":" 遍历 - for-range语法 # for-range语法可以用来遍历数组、指向数组的指针切片、字符串、映射和通道。\n遍历数组 # 当遍历一个数组a时候循环范围会从0到len(a) -1\nfunc main() { var a [3]int for i, v := range a { fmt.Println(i, v) } for i, v := range \u0026amp;a { fmt.Println(i, v) } } 遍历切片 # 当遍历一个切片s时候循环范围会从0到len(s) -1若切片是nil则迭代次数是0次\nfunc main() { a := make([]int, 3) for i, v := range a { fmt.Println(i, v) } a = nil for i, v := range a { fmt.Println(i, v) } } for-range切片时候可以边遍历边append吗 # 当遍历切片时候可以边遍历边append操作这并不会造成死循环。因为遍历之前已经确定了循环范围遍历操作相当如下伪代码\nlen_temp := len(range) // 循环上界 range_temp := range for index_temp = 0; index_temp \u0026lt; len_temp; index_temp++ { value_temp = range_temp[index_temp] index = index_temp value = value_temp original body } for-range切片时候返回的是值拷贝 # 无论遍历数组还是切片,返回都是数组或切片中的值拷贝:\nfunc main() { users := []User{ { Name: \u0026#34;a1\u0026#34;, Age: 100, }, { Name: \u0026#34;a2\u0026#34;, Age: 101, }, { Name: \u0026#34;a2\u0026#34;, Age: 102, }, } fmt.Println(\u0026#34;before: \u0026#34;, users) for _, v := range users { v.Age = v.Age + 10 // 想给users中所有用户年龄增加10岁 } fmt.Println(\u0026#34;after: \u0026#34;, users) } 执行上面代码,输入以下内容:\nbefore: [{a1 100} {a2 101} {a2 102}] after: [{a1 100} {a2 101} {a2 102}] 解决办法可以通过索引访问原切片或数组:\nfunc main() { users := []User{ { Name: \u0026#34;a1\u0026#34;, Age: 100, }, { Name: \u0026#34;a2\u0026#34;, Age: 101, }, { Name: \u0026#34;a2\u0026#34;, Age: 102, }, } fmt.Println(\u0026#34;before: \u0026#34;, users) for i := range users { users[i].Age = users[i].Age + 10 } fmt.Println(\u0026#34;after: \u0026#34;, users) } 遍历字符串 # 当遍历字符串时候返回的是rune类型rune类型是int32类型的别名一个rune就是一个码点(code point)\n// rune is an alias for int32 and is equivalent to int32 in all ways. It is // used, by convention, to distinguish character values from integer values. type rune = int32 由于遍历字符串时候返回的是码点所以索引并不总是依次增加1的\nfunc main() { var str = \u0026#34;hello你好\u0026#34; var buf [100]byte for i, v := range str { vl := utf8.RuneLen(v) si := i + vl copy(buf[:], str[i:si]) fmt.Printf(\u0026#34;索引%2d: %q\\t 码点: %#6x\\t 码点转换成字节: %#v\\n\u0026#34;, i, v, v, buf[:vl]) } } 执行上面代码将输出以下内容:\n索引 0: \u0026#39;h\u0026#39;\t码点: 0x68\t码点转换成字节: []byte{0x68} 索引 1: \u0026#39;e\u0026#39;\t码点: 0x65\t码点转换成字节: []byte{0x65} 索引 2: \u0026#39;l\u0026#39;\t码点: 0x6c\t码点转换成字节: []byte{0x6c} 索引 3: \u0026#39;l\u0026#39;\t码点: 0x6c\t码点转换成字节: []byte{0x6c} 索引 4: \u0026#39;o\u0026#39;\t码点: 0x6f\t码点转换成字节: []byte{0x6f} 索引 5: \u0026#39;\u0026#39;\t码点: 0xff0c\t码点转换成字节: []byte{0xef, 0xbc, 0x8c} 索引 8: \u0026#39;你\u0026#39;\t码点: 0x4f60\t码点转换成字节: []byte{0xe4, 0xbd, 0xa0} 索引11: \u0026#39;好\u0026#39;\t码点: 0x597d\t码点转换成字节: []byte{0xe5, 0xa5, 0xbd} 遍历映射 # 当遍历切片时候Go语言是不会保证遍历顺序的为了明确强调这一点Go语言在实现的时候故意随机地选择一个桶开始遍历。当映射通道为nil时候遍历次数为0次。\nfunc main() { m := map[int]int{ 1: 10, 2: 20, 3: 30, } for i, v := range m { fmt.Println(i, v) } m = nil for i, v := range m { fmt.Println(i, v) } } for-range映射时候可以边遍历边新增或删除吗 # 若在一个Goroutine里面边遍历边新增、删除理论上是可以的不会触发写检测的新增的key-value可能会被访问到也可能不会。\n若多个Goroutine中进行遍历、新增、删除操作的话是不可以的是可能触发写检测的然后直接panic。\n遍历通道 # 当遍历通道时直到通道关闭才会终止若通道是nil则会永远阻塞。遍历通道源码分析请见 从通道中读取数据。\n进一步阅读 # Go Range Loop Internals For statements "},{"id":5,"href":"/gmp/","title":"G-M-P调度机制","section":"简介","content":" G-M-P调度机制 # 其曲弥高,其和弥寡。\n调度机制概述 调度器 "},{"id":6,"href":"/analysis-tools/gdb/","title":"GDB","section":"Go语言分析工具","content":" GDB # GDBGNU symbolic Debugger是Linux系统下的强大的调试工具可以用来调试ada, c, c++, asm, minimal, d, fortran, objective-c, go, java,pascal 等多种语言。\n我们以调试 go 代码为示例来介绍GDB的使用。源码内容如下\npackage main import \u0026#34;fmt\u0026#34; func add(a, b int) int { sum := 0 sum = a + b return sum } func main() { sum := add(10, 20) fmt.Println(sum) } 构建二进制应用:\ngo build -gcflags=\u0026#34;-N -l\u0026#34; -o test main.go 启动调试 # gdb ./test # 启动调试 gdb --args ./test arg1 arg2 # 指定参数启动调试 进入gdb调试界面之后执行 run 命令运行程序。若程序已经运行,我们可以 attach 该程序的进程id进行调试:\n$ gdb (gdb) attach 1785 当执行 attach 命令的时候GDB首先会在当前工作目录下查找进程的可执行程序如果没有找到接着会用源代码文件搜索路径。我们也可以用file命令来加载可执行文件。\n或者通过命令设置进程id:\ngdb test 1785 gdb test --pid 1785 若已运行的进程不含调试信息,我们可以使用同样代码编译出一个带调试信息的版本,然后使用 file 和 attach 命令进行运行调试。\n$ gdb (gdb) file test Reading symbols from test...done. (gdb) attach 1785 可视化窗口 # GDB也支持多窗口图形启动运行一个窗口显示源码信息一个窗口显示调试信息\ngdb test -tui GDB支持在运行过程中使用 Crtl+X+A 组合键进入多窗口图形界面, GDB支持的快捷操作有\nCrtl+X+A // 多窗口与单窗口界面切换 Ctrl + X + 2 // 显示两个窗口 Ctrl + X + 1 // 显示一个窗口 运行程序 # 通过 run 命令运行程序, run 命令可以简写成 r\n(gdb) run 除了启动GDB时候设置程序的命令行参数外我们也可以在启动GDB后再指定程序的命令行参数\n(gdb) run arg1 arg2 或者通过 set 命令设置命令行参数:\n(gdb) set args arg1 arg2 (gdb) run 除了 run 命令外,我们也可以使用 start 命令运行程序。start 命令会在在 main 函数的第一条语句前面停下来。\n(gdb) start start 命令相当于在Go程序的入口函数 main.main (main.main 代表 main 包的 main 函数)处设置断点,然后运行 run 命令:\n(gdb) b main.main (gdb) run 断点的设置、查看、删除、禁用 # 设置断点 # GDB中是通过 break 命令来设置断点(BreakPoint)break 可以简写成 b。\nbreak function\n在指定函数出设置断点设置断点后程序会在进入指定函数时停住\nbreak linenum\n在指定行号处设置断点\nbreak +offset/-offset\n在当前行号的前面或后面的offset行处设置断点。offset为自然数\nbreak filename:linenum\n在源文件filename的linenum行处设置断点\nbreak filename:function\n在源文件filename的function函数的入口处设置断点\nbreak *address\n在程序运行的内存地址处设置断点\nbreak\nbreak命令没有参数时表示在下一条指令处停住。\nbreak \u0026hellip; if \u0026hellip;可以是上述的参数condition表示条件在条件成立时停住。比如在循环境体中可以设置break if i=100表示当i为100时停住程序\n查看断点 # 我们可以通过 info 命令查看断点:\n(gdb) info breakpoint # 查看所有断点 (gdb) info breakpoint 3 # 查看3号断点 删除断点 # 删除断点是通过 delete 命令删除的delete 命令可以简写成 d\n(gdb) delete 3 # 删除3号断点 断点启用与禁用 # (gdb) disable 3 # 禁用3号断点 (gdb) enable 3 # 启用3号断点 调试 # 单步执行 # next 用于单步执行,会一行行执行代码,运到函数时候,不会进入到函数内部,跳过该函数,但会执行该函数,即 step over。可以简写成 n。\n(gdb) next 单步进入 # step 用于单步进入执行,跟 next 命令类似,但是遇到函数时候,会进入到函数内部一步步执行,即 step into。可以简写成 s。\n(gdb) step 与 step 相关的命令 stepi用于每次执行每次执行一条机器指令。可以简写成 si。\n继续执行到下一个断点 # continue 命令会继续执行程序,直到再次遇到断点处。可以简写成 c:\n(gdb) continue (gdb) continue 3 # 跳过3个断点 继续运行到指定位置 # until 命令可以帮助我们实现运行到某一行停住,可以简写成 u\n(gdb) until 5 跳过执行 # skip 命令可以在step时跳过一些不想关注的函数或者某个文件的代码:\n(gdb) skip function add # step时跳过add函数 (gdb) info skip # 查看skip列表 其他相关的命令:\nskip delete [num] 删除skip skip enable [num] 启动skip skip disable [num] 关闭skip 注意: 当不带skip号时候是针对所有skip进行设置。\n执行完成当前函数 # finish 命令用来将当前函数执行完成并打印函数返回时的堆栈地址、返回值、参数值等信息即step out 。\n(gdb) finish 查看源码 # GDB中的 list 命令用来显示源码信息。list 命令可以简写成 l。\nlist\n从第一行开始显示源码继续输入list可列出后面的源码\nlist linenum\n列出linenum行附近的源码\nlist function\n列出函数function的代码\nlist filename:linenum\n列出文件filename文件中linenum行出的代码\nlist filename:function\n列出文件filename中函数function的代码\nlist +offset/-offset\n列出在当前行号的前面或后面的offset行附近的代码。offset为自然数。\nlist +/-\n列出当前行后面或者前面的代码\nlist linenum1, linenum2\n列出行linenum1和linenum2之间的代码\n查看信息 # info 命令用来显示信息,可以简写成 i。\ninfo files\n显示当前的调试的文件包含程序入口地址内存分段布局位置信息等\ninfo breakpoints\n显示当前设置的断点列表\ninfo registers\n显示当前寄存器的值可以简写成 i r。指定寄存器名称可以查看具体寄存器信息i r rsp\ninfo all-registers\n显示所有寄存器的值。GDB提供四个标准寄存器pc 是程序计数器寄存器sp 是堆栈指针。fp 用于记录当前堆栈帧的指针ps 用于记录处理器状态的寄存器。GDB会处理好不同架构系统寄存器不一致问题比如对于 amd64 架构pc 对应就是 rip 寄存器。\n引用寄存器内容是将寄存器名前置 $ 符作为变量来用。比如 $pc 就是程序计数器寄存器值。\ninfo args\n显示当前函数参数\ninfo locals\n显示当前局部变量\ninfo frame\n查看当前栈帧的详细信息包括 rip 信息,正在运行的指令所在文件位置\ninfo variables\n查看程序中的变量符号\ninfo functions\n查看程序中的函数符号\ninfo functions regexp\n通过正则匹配来查看程序中的函数符号\ninfo goroutines\n显示当前执行的 goroutine 列表,带 * 的表示当前执行的。注意需要加载 go runtime 支持。\ninfo stack\n查看栈信息\ninfo proc mappings\n可以简写成 i proc m。用来查看应用内存映射\ninfo proc [procid]\n显示进程信息\ninfo proc status\n显示进程相关信息包括user id和group id进程内有多少线程虚拟内存的使用情况挂起的信号阻塞的信号忽略的信号TTY消耗的系统和用户时间堆栈大小nice值\ninfo display\ninfo watchpoints\n列出当前所设置了的所有观察点\ninfo line [linenum]\n查看第 linenum 的代码指令地址信息,不带 linenum 时,显示的是当前位置的指令地址信息\ninfo source\n显示此源代码的源代码语言\ninfo sources\n显示程序中所有有调试信息的源文件名一共显示两个列表一个是其符号信息已经读过的一个是还未读取过的\ninfo types\n显示程序中所有类型符号\ninfo types regexp\n通过正则匹配来查看程序中的类型符号\n其他类似命令有\nshow args\n查看命令行参数\nshow environment [envname]\n查看环境变量信息\nshow paths\n查看程序的运行路径\nwhatis var1\n显示变量var1类型\nptype var1\n显示变量 var1 类型,若是 var1 结构体类型,会显示该结构体定义信息。\n查看调用栈 # 通过 where 可以查看调用栈信息:\n(gdb) where #0 _rt0_amd64 () at /usr/lib/go/src/runtime/asm_amd64.s:15 #1 0x0000000000000001 in ?? () #2 0x00007fffffffdd2c in ?? () #3 0x0000000000000000 in ?? () 设置观察点 # 通过 watch 命令,可以设置观察点。当观察点的变量发生变化时,程序会停下来。可以简写成 wa\n(gdb) watch sum 查看汇编代码 # 我们可以通过开启 disassemble-next-line 自动显示汇编代码。\n(gdb) set disassemble-next-line on 当面我们可以查看指定函数的汇编代码:\n(gdb) disassemble main.main disassemble 可以简写成 disas。我们也可以将源代码和汇编代码一一映射起来后进行查看\n(gdb) disas /m main.main GDB默认显示汇编指令格式是 AT\u0026amp;T 格式,我们可以改成 intel 格式:\n(gdb) set disassembly-flavor intel 自动显示变量值 # display 命令支持自动显示变量值功能。当进行 next 或者 step 等调试操作时候GDB会自动显示 display 所设置的变量或者地址的值信息。\ndisplay 命令格式:\ndisplay \u0026lt;expr\u0026gt; display /\u0026lt;fmt\u0026gt; \u0026lt;expr\u0026gt; display /\u0026lt;fmt\u0026gt; \u0026lt;addr\u0026gt; expr是一个表达式 fmt表示显示的格式 addr表示内存地址 其他相关命令:\nundisplay [num]: 不显示 delete display [num]: 删除 disable display [num]: 关闭自动显示 enable display [num] 开启自动显示 info display: 查看display信息 注意: 当不带display号时候是针对所有display进行设置。\n显示将要执行的汇编指令 # 我们可以通过 display 命令可以实现当程序停止时,查看将要执行的汇编指令:\n(gdb) display /i $pc (gdb) display /3i $pc # 一次性显示3条指令 取消显示可以用 undisplay 命令进行操作。\n查看backtrace信息 # backtrace 命令用来查看栈帧信息。可以简写成 bt。\n(gdb) backtrace # 显示当前函数的栈帧以及局部变量信息 (gdb) backtrace full # 显示各个函数的栈帧以及局部变量值 (gdb) backtrace full n # 从内向外显示n个栈桢及其局部变量 (gdb) backtrace full -n # 从外向内显示n个栈桢及其局部变量 切换栈帧信息 # frame 命令可以切换栈帧信息:\n(gdb) frame n # 其中n是层数最内层的函数帧为第0帧 其他相关命令:\ninfo frame: 查看栈帧列表 调试多线程 # GDB中有一组命令能够辅助多线程的调试\ninfo threads\n显示当前可调式的所有线程线程 ID 前有 “*” 表示当前被调试的线程。\nthread threadid\n切换线程到线程threadid\nset scheduler-locking [on|off|step]\n多线程环境下会存在多个线程运行这会影响调试某个线程的结果这个命令可以设置调试的时候多个线程的运行情况on 表示只有当前调试的线程会继续执行off 表示不屏蔽任何线程所有线程都可以执行step 表示在单步执行时,只有当前线程会执行。\nthread apply [threadid] [all] args\n对线程列表执行命令。比如通过 thread apply all bt full 可以查看所有线程的局部变量信息。\n查看运行时变量 # print 命令可以用来查看变量的值。print 命令可以简写成 p。print 命令格式如下:\nprint [\u0026lt;/format\u0026gt;] \u0026lt;expr\u0026gt; format 用来设置显示变量的格式,是可选的选项。其可用值如下所示:\nx 按十六进制格式显示变量 d 按十进制格式显示变量 u 按十六进制格式显示无符号整型 o 按八进制格式显示变量 t 按二进制格式显示变量 a 按十六进制格式显示变量 c 按字符格式显示变量 f 按浮点数格式显示变量 z 按十六进制格式显示变量,左侧填充零 expr 可以是一个变量,也可以是表达式,也可以是寄存器:\n(gdb) p var1 # 打印变量var1 (gdb) p \u0026amp;var1 # 打印变量var1地址 (gdb) p $rsp # 打印rsp寄存器地址 (gdb) p $rsp + 8 # 打印rsp加8后的地址信息 (gdb) p 0xc000068fd0 # 打印0xc000068fd0转换成10进制格式 (gdb) p /x 824634150864 # 打印824634150864转换成16进制格式 print 也支持查看连续内存,@ 操作符用于查看连续内存,@ 的左边是第一个内存的地址的值,@ 的右边则想查看内存的长度。\n例如对于如下代码int arr[] = {2, 4, 6, 8, 10};,可以通过如下命令查看 arr 前三个单元的数据:\n(gdb) p *arr@3 $2 = {2, 4, 6} 查看内存中的值 # examine 命令用来查看内存地址中的值,可以简写成 x。examine 命令的语法如下所示:\nexamine /\u0026lt;n/f/u\u0026gt; \u0026lt;addr\u0026gt; n 表示显示字段的长度,也就是说从当前地址向后显示几个地址的内容。\nf 表示显示的格式\nd 数字 decimal u 无符号数字 unsigned decimal s 字符串 string c 字符 char u 无符号整数 unsigned integer t 二进制 binary o 八进制格式 octal x 十六进制格式 hex f 浮点数格式 float i 指令 instruction a 地址 address z 十六进制格式,左侧填充零 hex, zero padded on the left u 表示从当前地址往后请求的字节数默认是4个bytes\nb 一个字节 byte h 两个字节 halfword w 四个字节 word g 八个字节 giantword 示例:\n(gdb) x/10c 0x4005d4 # 打印前10个字符 (gdb) x/16xb a # 以16进制格式打印数组前a16个byte的值 (gdb) x/16ub a # 以无符号10进制格式打印数组a前16个byte的值 (gdb) x/16tb a # 以2进制格式打印数组前16个abyte的值 (gdb) x/16xw a # 以16进制格式打印数组a前16个word4个byte的值 (gdb) x $rsp # 打印rsp寄存器执行的地址的值 (gdb) x $rsp + 8 # 打印rsp加8后的地址指向的值 (gdb) x 0xc000068fd0 # 打印内存0xc000068fd0指向的值 (gdb) x/5i schedule # 打印函数schedule前5条指令 修改变量或寄存器值 # set 命令支持修改变量以及寄存器的值:\n(gdb) set var var1=123 # 设置变量var1值为123 (gdb) set var $rax=123 # 设置寄存器值为123 (gdb) set environment envname1=123 # 设置环境变量envname1值为123 查看命令帮助信息 # help 命令支持查看GDB命令帮助信息。\n(gdb) help status # 查看所有命令使用示例 (gdb) help x # 查看x命令使用帮助 搜索源文件 # search 命令支持在当前文件中使用正则表达式搜索内容。search 等效于 forward-search 命令,是从当前位置向前搜索,可以简写成 fo。reverse-search 命令功能跟 forward-search 恰好相反,其可以简写成 rev。\n(gdb) search func add # 从当前位置向前搜索add方法 (gdb) rev func add # 从当前为向后搜索add方法 执行shell命令 # 我们可以通过 shell 指令来执行shell命令。\n(gdb) shell cat /proc/27889/maps # 查看进程27889的内存映射。若想查看当前进程id可以使用info proc命令获取 (gdb) shell ls -alh GDB对go runtime支持 # runtime.Breakpoint():触发调试器断点。 runtime/debug.PrintStack():显示调试堆栈。 log适合替代 print显示调试信息 为系统调用设置捕获点 # GDB支持为系统调用设置捕获点(catchpoint),我们可以通过 catch 指令,后面加上 系统调用号(syscall numbers)1 或者系统调用助记符(syscall mnemonic names也称为系统调用名称) 来设置捕获点。如果不指定系统调用的话,默认是捕获所有系统调用。\n(gdb) catch syscall 231 Catchpoint 1 (syscall \u0026#39;exit_group\u0026#39; [231]) (gdb) catch syscall exit_group Catchpoint 2 (syscall \u0026#39;exit_group\u0026#39; [231]) (gdb) catch syscall Catchpoint 3 (any syscall) 设置源文件查找路径 # 在程序调试过程中构建程序的源文件位置更改之后gdb不能找到源文件位置我们可以使用 directory命令设置查找源文件的路径。\ndirectory ~/www/go/src/github.com/go-delve/ directory 命令只使用相对路径下的源文件,若绝对路径下源文件找不到,我们可以使用 set substitute-path 设置路径替换。\nset substitute-path ~/www/go/src/github.com/go-delve/ ~/www/go/src/github.com/go-delve2/ 批量执行命令 # GDB支持以脚本形式运行命令我们可以使用下面的选项\n-ex选项可以用来指定执行命令 -iex选来用来指定加载应用程序之前需执行的命令 -x 选项用来从指定文件中加载命令 -batch类似-q支持安静模式会指示GDB在所有命令执行完成之后退出 # 1. 打印提示语 2. 在main.main出设置断点 3. 运行程序 4. 执行完成程序退出gdb gdb -iex \u0026#39;echo 开始执行:\\n\u0026#39; -ex \u0026#34;b main.main\u0026#34; -ex \u0026#34;run\u0026#34; -batch ./main # 设置exit/exit_group系统调用追踪点然后运行程序最后打印backtrace信息 gdb -ex \u0026#34;catch syscall exit exit_group\u0026#34; -ex \u0026#34;run\u0026#34; -ex \u0026#34;bt\u0026#34; -batch ./main # 从文件中加载命令 gdb -batch -x /tmp/cmds --args executablename arg1 arg2 arg3 GDB增强插件 # gdbinit gdb-dashboard pwndbg peda 进一步阅读 # Beej\u0026rsquo;s Quick Guide to GDB 100个gdb小技巧 https://x64.syscall.sh/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n"},{"id":7,"href":"/function/first-class/","title":"Go函数是一等公民","section":"函数","content":" 一等公民 # Go语言中函数是一等公民first class)因为它既可以作为变量也可作为函数参数函数返回值。Go语言还支持匿名函数闭包函数返回多个值。\n一等公民特征 # 函数赋值给一个变量 # func add(a, b int) int { return a + b } func main() { fn := add fmt.Println(fn(1, 2)) // 3 } 函数作为返回值 # func pow(a int) func(int) int { return func(b int) int { result := 1 for i := 0; i \u0026lt; b; i++ { result *= a } return result } } func main() { powOfTwo := pow(2) // 2的x次幂 fmt.Println(powOfTwo(3)) // 8 fmt.Println(powOfTwo(4)) // 16 powOfThree := pow(3) // 3的x次幂 fmt.Println(powOfThree(3)) // 27 fmt.Println(powOfThree(4)) // 81 } 函数作为函数参数传递 # 下面示例中使用匿名函数作为函数参数传递另外一个函数。\nfunc filter(a []int, fn func(int) bool) (result []int) { for _, v := range a { if fn(v) { result = append(result, v) } } return result } func main() { data := []int{1, 2, 3, 4, 5} // 传递奇数过滤器函子,过滤出奇数 fmt.Println(filter(data, func(a int) bool { return a\u0026amp;1 == 1 })) // 1, 3, 5 // 过滤出偶数 fmt.Println(filter(data, func(a int) bool { return a\u0026amp;1 == 0 })) // 2, 4 } 使用闭包函数构建一个生成器 # 生成器指的是每次调用时候总是返回下一序列值。下面演示一个整数的生成器:\nfunc generateInteger() func() int { ch := make(chan int) count := 0 go func() { for { ch \u0026lt;- count count++ } }() return func() int { return \u0026lt;-ch } } func main() { generate := generateInteger() fmt.Println(generate()) // 0 fmt.Println(generate()) // 1 fmt.Println(generate()) // 2 } 函数式编程 # 函数式编程functional programming是一种编程范式其核心思想是将复杂的操作采用函数嵌套、组合调用方式来处理。函数式编程一大特征是函数是一等公民Go语言中函数是一等公民但是由于其不支持泛型Go语言中采用函数式编程有时候是无法通用性的。比如上面的过滤器示例当想要支持过滤int64类型的就需要重写一遍或者传递interface{}参数。\n高阶函数 # 高阶函数Higher-order function指的是至少满足下列一个条件的函数\n接受一个或多个函数作为输入 输出一个函数 高阶函数是函数式编程中常用范式,常见使用案例有:\n过滤器 apply函数 排序函数 回调函数 函数柯里化 合成函数 进一步阅读 # 7 Easy functional programming techniques in Go "},{"id":8,"href":"/function/call-stack/","title":"Go函数调用栈","section":"函数","content":" 调用栈 # 这一章节延续前面《 准备篇-Go汇编 》那一章节。这一章节将从一个实例出发详细分析Go 语言中函数调用栈。这一章节会涉及callercallee寄存器相关概念如果还不太了解可以去《 准备篇-Go汇编 》查看了解。\n在详细分析函数栈之前我们先复习以下几个概念。\ncaller 与 callee # 如果一个函数调用另外一个函数那么该函数被称为调用者函数也叫做caller而被调用的函数称为被调用者函数也叫做callee。比如函数main中调用sum函数那么main就是caller而sum函数就是callee。\n栈帧 # 栈帧stack frame指的是未完成函数所持有的独立连续的栈区域用来保存其局部变量返回地址等信息。\n函数调用约定 # 函数调用约定(Calling Conventions)是 ABI(Application Binary Interface) 的组成部分,它描述了:\n如何将执行控制权交给callee以及返还给caller 如何保存和恢复caller的状态 如何将参数传递个callee 如何从callee获取返回值 简而言之,一句话就是函数调用约定指的是约定了函数调用时候,函数参数如何传递,函数栈由谁完成平衡,以及函数返回值如何返回的。\n在Go语言中函数的参数和返回值的存储空间是由其caller的栈帧提供。这也为Go语言为啥支持多返回值以及总是值传递的原因。从Go汇编层面看在callee中访问其参数和返回值是通过FP寄存器来操作的在实现层面是通过SP寄存器访问的。Go语言中函数参数入栈顺序是从右到左入栈的。\n函数调用时候会为其分配栈空间用来存放临时变量返回值等信息当完成调用后这些栈空间应该进行回收以恢复调用以前的状态。这个过程就是栈平衡。栈平衡工作可以由被调用者本身(callee)完成,也可以由其调用者(caller)完成。在Go语言中是由callee来完成栈平衡的。\n函数栈 # 当前函数作为caller其本身拥有的栈帧以及其所有callee的栈帧可以称为该函数的函数栈也称函数调用栈。C语言中函数栈大小是固定的如果超出栈空间就会栈溢出异常。比如递归求斐波拉契这时候可以使用尾调用来优化。由于Go 语言栈可以自动进行分裂扩容,栈空间不够时候,可以自动进行扩容。当用火焰图分析性能时候,火焰越高,说明栈越深。\nGo 语言中函数栈全景图如下:\nGo语言函数调用栈 接下来的函数调用栈分析,都是基于函数栈的全景图出发。知道该全景图每一部分含义也就了解函数调用栈。\n实例分析 # 我们将分析如下代码。\npackage main func sum(a, b int) int { sum := 0 sum = a + b return sum } func main() { a := 3 b := 5 print(sum(a, b)) } 参照前面的函数栈全景图我们画出main函数调用sum函数时的函数调用栈图\nmain函数调用栈 从栈底往栈顶,我们依次可以看到:\nmain函数的caller的基址Base Pointer)。这部分是黄色区域。 main函数局部变量a,b。我们看到a,b变量按照他们出现的顺序依次入栈在实际指令中可能出现指令重排a,b变量入栈顺序可能相反但这个不影响最终结果。这部分是蓝色区域。 接下来是绿色区域这部分是用来存放sum函数返回值的。这部分空间是提前分配好了。由于sum函数返回值只有一个且是int类型那么绿色区域大小是8字节64位系统下int占用8字节。在sum函数内部是通过FP寄存器访问这个栈空间的。 在下来就是浅黄色区域这个是存放sum函数实参的。从上面介绍中我们知道Go语言中函数参数是从右到左入栈的sum函数的签名是func sum(a, b int) int那么b=5会先入栈a=3接着入栈。 接下来是粉红色区域这部分存放的是return address。main函数调用sum函数时候会将sum函数后面的一条指令入栈。从main函数caller的基址空间到此处都属于main的函数栈帧。 接下来就是sum函数栈帧空间部分。首先同main函数栈帧空间一样其存放的sum函数caller的基址由于sum函数的caller就是main函数所以这个地方存放就是main栈帧的栈底地址。 \u0026hellip;. 从汇编的角度观察 # 接下来我们从Go 汇编角度查看main函数调用sum函数时的函数调用栈。\nGo语言中函数的栈帧空间是提前分配好的分配的空间用来存放函数局部变量被调用函数参数被调用函数返回值返回地址等信息。我们来看下main函数和sum函数的汇编定义\nTEXT\t\u0026#34;\u0026#34;.main(SB), ABIInternal, $56-0 // main函数定义 TEXT\t\u0026#34;\u0026#34;.sum(SB), NOSPLIT|ABIInternal, $16-24 // sum函数定义 从上面函数定义可以看出来给main函数分配的栈帧空间大小是56字节大小这里面的56字节大小是不包括返回地址空间的实际上main函数的栈帧大小是56+8(返回地址占用8字节空间大小) = 64字节大小由于main函数没有参数和返回值所以参数和返回值这部分大小是0。给sum函数分配的栈帧空间大小是16字节大小sum函数参数有2个且都是int类型返回值是int类型所以参数和返回值大小是24字节。\n关于函数声明时每个字段的含义可以去《 准备篇-Go汇编-函数声明 》 查看:\n需要注意的有两点\n函数分配的栈空间足以放下所有被调用者信息如果一个函数会调用很多其他函数那么它的栈空间是按照其调用函数中最大空间要求来分配的。 函数栈空间是可以split。当栈空间不足时候会进行split重新找一块2倍当前栈空间的内存空间将当前栈帧信息拷贝过去这个叫栈分裂。Go语言在栈分裂基础上实现了抢占式调度这个我们会在后续篇章详细探讨。我们可以使用 //go:nosplit 这个编译指示强制函数不进行栈分裂。从sum函数定义可以看出来其没有进行栈分裂处理。 接下来我们分析main函数的汇编代码\n0x0000 00000 (main.go:9)\tTEXT\t\u0026#34;\u0026#34;.main(SB), ABIInternal, $56-0 # main函数定义 0x0000 00000 (main.go:9)\tMOVQ\t(TLS), CX # 将本地线程存储信息保存到CX寄存器中 0x0009 00009 (main.go:9)\tCMPQ\tSP, 16(CX) # 比较当前栈顶地址(SP寄存器存放的)与本地线程存储的栈顶地址 0x000d 00013 (main.go:9)\tPCDATA\t$0, $-2 # PCDATAFUNCDATA用于Go汇编额外信息不必关注 0x000d 00013 (main.go:9)\tJLS\t114 # 如果当前栈顶地址(SP寄存器存放的)小于本地线程存储的栈顶地址则跳到114处代码处进行栈分裂扩容操作 0x000f 00015 (main.go:9)\tPCDATA\t$0, $-1 0x000f 00015 (main.go:9)\tSUBQ\t$56, SP # 提前分配好56字节空间作为main函数的栈帧注意此时的SP寄存器指向会往下移动了56个字节 0x0013 00019 (main.go:9)\tMOVQ\tBP, 48(SP) # BP寄存器存放的是main函数caller的基址movq这条指令是将main函数caller的基址入栈。对应就是上图中我们看到的main函数栈帧的黄色区域。 0x0018 00024 (main.go:9)\tLEAQ\t48(SP), BP # 将main函数的基址存放到到BP寄存器 0x001d 00029 (main.go:9)\tPCDATA\t$0, $-2 0x001d 00029 (main.go:9)\tPCDATA\t$1, $-2 0x001d 00029 (main.go:9)\tFUNCDATA\t$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d 00029 (main.go:9)\tFUNCDATA\t$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d 00029 (main.go:9)\tFUNCDATA\t$2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d 00029 (main.go:10)\tPCDATA\t$0, $0 0x001d 00029 (main.go:10)\tPCDATA\t$1, $0 0x001d 00029 (main.go:10)\tMOVQ\t$3, \u0026#34;\u0026#34;.a+32(SP) # main函数局部变量a入栈 0x0026 00038 (main.go:11)\tMOVQ\t$5, \u0026#34;\u0026#34;.b+24(SP) # main函数局部变量b入栈 0x002f 00047 (main.go:12)\tMOVQ\t\u0026#34;\u0026#34;.a+32(SP), AX # 将局部变量a保存到AX寄存中 0x0034 00052 (main.go:12)\tMOVQ\tAX, (SP) # sum函数第二个参数 0x0038 00056 (main.go:12)\tMOVQ\t$5, 8(SP) # sum函数第一个参数 0x0041 00065 (main.go:12)\tCALL\t\u0026#34;\u0026#34;.sum(SB) # 通过call指令调用sum函数。此时会隐式进行两个操作1. 将当前指令的下一条指令的地址入栈。当前指令下一条指令就是MOVQ 16(SP), AX其相对地址是0x0046。2. IP指令寄存器指向了sum函数指令入库地址。 0x0046 00070 (main.go:12)\tMOVQ\t16(SP), AX #将sum函数值保存AX寄存中。16(SP) 存放的是sum函数的返回值 0x004b 00075 (main.go:12)\tMOVQ\tAX, \u0026#34;\u0026#34;..autotmp_2+40(SP) 0x0050 00080 (main.go:12)\tCALL\truntime.printlock(SB) 0x0055 00085 (main.go:12)\tMOVQ\t\u0026#34;\u0026#34;..autotmp_2+40(SP), AX 0x005a 00090 (main.go:12)\tMOVQ\tAX, (SP) 0x005e 00094 (main.go:12)\tCALL\truntime.printint(SB) 0x0063 00099 (main.go:12)\tCALL\truntime.printunlock(SB) 0x0068 00104 (main.go:13)\tMOVQ\t48(SP), BP 0x006d 00109 (main.go:13)\tADDQ\t$56, SP 0x0071 00113 (main.go:13)\tRET 0x0072 00114 (main.go:13)\tNOP 0x0072 00114 (main.go:9)\tPCDATA\t$1, $-1 0x0072 00114 (main.go:9)\tPCDATA\t$0, $-2 0x0072 00114 (main.go:9)\tCALL\truntime.morestack_noctxt(SB) # 调用栈分裂处理函数 0x0077 00119 (main.go:9)\tPCDATA\t$0, $-1 0x0077 00119 (main.go:9)\tJMP\t0 结合汇编,我们最终画出 main 函数调用栈图:\nmain函数调用栈 "},{"id":9,"href":"/go-assembly/","title":"Go汇编语法","section":"简介","content":" Go汇编 # 本节将介绍Go语言所使用到的汇编知识。在介绍Go汇编之前我们先了解一些汇编语言寄存器 AT\u0026amp;T 汇编语法内存布局等前置知识点。这些知识点与Go汇编或多或少有关系了解这些才能更好的帮助我们去看懂Go汇编代码。\n前置知识 # 机器语言 # 机器语言是机器指令的集合。计算机的机器指令是一系列二进制数字。计算机将之转换为一系列高低电平脉冲信号来驱动硬件工作的。\n汇编语言 # 机器指令是由0和1组成的二进制指令难以编写与记忆。汇编语言是二进制指令的文本形式与机器指令一一对应相当于机器指令的助记码。比如加法的机器指令是00000011写成汇编语言就是ADD。汇编的指令格式由操作码和操作数组成。\n将助记码标准化后称为assembly language缩写为asm中文译为汇编语言。\n汇编语言大致可以分为两类\n基于x86架构处理器的汇编语言\nIntel 汇编 DOS(8086处理器), Windows Windows 派系 -\u0026gt; VC 编译器 AT\u0026amp;T 汇编 Linux, Unix, Mac OS, iOS(模拟器) Unix派系 -\u0026gt; GCC编译器 基于ARM 架构处理器的汇编语言\nARM 汇编 数据单元大小 # 汇编中数据单元大小可分为:\n位 bit 半字节 Nibble 字节 Byte 字 Word 相当于两个字节 双字 Double Word 相当于2个字4个字节 四字 Quadword 相当于4个字8个字节 寄存器 # 寄存器是CPU中存储数据的器件起到数据缓存作用。内存按照内存层级(memory hierarchy)依次分为寄存器L1 Cache, L2 Cache, L3 Cache其读写延迟依次增加实现成本依次降低。\n内存层级结构 寄存器分类 # 一个CPU中有多个寄存器。每一个寄存器都有自己的名称。寄存器按照种类分为通用寄存器和控制寄存器。其中通用寄存器有可细分为数据寄存器指针寄存器以及变址寄存器。\n1979年因特尔推出8086架构的CPU开始支持16位。为了兼容之前8008架构的8位CPU8086架构中AX寄存器高8位称为AH低8位称为AL用来对应8008架构的8位的A寄存器。后来随着x86以及x86-64 架构的CPU推出开始支持32位以及64位为了兼容并保留了旧名称16位处理器的AX寄存器拓展成EAX(E代表拓展Extended的意思)。对于64位处理器的寄存器相应的RAX(R代表寄存器Register的意思)。其他指令也类似。\n各个寄存器功能介绍\n寄存器 功能 AX A代表累加器AccumulatorX是八位寄存器AH和AL的中H和L的占位符表示AX由AH和AL组成。AX一般用于算术与逻辑运算以及作为函数返回值 BX B代表BaseBX一般用于保存中间地址(hold indirect addresses) CX C代表CountCX一般用于计数比如使用它来计算循环中的迭代次数或指定字符串中的字符数 DX D代表DataDX一般用于保存某些算术运算的溢出并且在访问80x86 I/O总线上的数据时保存I/O地址 DI DI代表Destination IndexDI一般用于指针 SI SI代表Source IndexSI用途同DI一样 SP SP代表Stack Pointer是栈指针寄存器存放着执行函数对应栈帧的栈顶地址且始终指向栈顶 BP BP代表Base Pointer是栈帧基址指针寄存器存放这执行函数对应栈帧的栈底地址一般用于访问栈中的局部变量和参数 IP IP代表Instruction Pointer是指令寄存器指向处理器下条等待执行的指令地址(代码段内的偏移量)每次执行完相应汇编指令IP值就会增加IP是个特殊寄存器不能像访问通用寄存器那样访问它。IP可被jmp、call和ret等指令隐含地改变 进程在虚拟内存中布局 # 32位系统下虚拟内存空间大小为4G每一个进程独立的运行在该虚拟内存空间上。从0x00000000开始的3G空间属于用户空间剩下1G空间属于内核空间。\n用户空间还可以进一步细分每一部分叫做段(section),大致可以分为以下几段:\nStack 栈空间:用于函数调用中存储局部变量、返回地址、返回值等,向下增长,变量存储和使用过程叫做入栈和出栈过程 Heap 堆空间用于动态申请的内存比如c语言通过malloc函数调用分配内存其向上增长。指针型变量指向的一般就是这里面的空间。存储此空间的数据需要GC的。栈上变量scope是函数级的而堆上变量属于进程级的 Bss段未初始化数据区存储未初始化的全局变量或静态变量 Data段初始化数据区存储已经初始化的全局变量或静态变量 Text段代码区存储的是源码编译后二进制指令 在32位系统中进程空间(即用户空间范围为0x00000000 ~ 0xbfffffff内核空间范围为0xc0000000 ~ 0xffffffff, 实际上分配的进程空间并不是从0x00000000开始的而是从0x08048000开始到0xbfffffff结束。另外进程实际的esp指向的地址并不是从0xbfffffff开始的因为linux系统会在程序初始化前将一些命令行参数及环境变量以及ELF辅助向量ELF Auxiliary Vectors)等信息放到栈上。进程启动时,其空间布局如下所示(注意图示中地址是从低地址到高地址的):\nstack pointer -\u0026gt; [ argc = number of args ] 4 [ argv[0] (pointer) ] 4 (program name) [ argv[1] (pointer) ] 4 [ argv[..] (pointer) ] 4 * x [ argv[n - 1] (pointer) ] 4 [ argv[n] (pointer) ] 4 (= NULL) [ envp[0] (pointer) ] 4 [ envp[1] (pointer) ] 4 [ envp[..] (pointer) ] 4 [ envp[term] (pointer) ] 4 (= NULL) [ auxv[0] (Elf32_auxv_t) ] 8 [ auxv[1] (Elf32_auxv_t) ] 8 [ auxv[..] (Elf32_auxv_t) ] 8 [ auxv[term] (Elf32_auxv_t) ] 8 (= AT_NULL vector) [ padding ] 0 - 16 [ argument ASCIIZ strings ] \u0026gt;= 0 [ environment ASCIIZ strings ] \u0026gt;= 0 [ program name ASCIIZ strings ] \u0026gt;= 0 (0xbffffffc) [ end marker ] 4 (= NULL) (0xc0000000) \u0026lt; bottom of stack \u0026gt; 0 (virtual) 进程空间起始位置处存放命令行参数个数与参数信息,我们将在后面章节有讨论到。\ncaller 与 callee # 如果一个函数调用另外一个函数那么该函数被称为调用者函数也叫做caller而被调用的函数称为被调用者函数也叫做callee。比如函数main中调用sum函数那么main就是caller而sum函数就是callee。\n栈帧 # 栈帧即stack frame即未完成函数所持有的独立连续的栈区域用来保存其局部变量返回地址等信息。\n函数栈 # 当前函数作为caller其本身拥有的栈帧以及其所有callee的栈帧可以称为该函数的函数栈。一般情况下函数栈大小是固定的如果超出栈空间就会栈溢出异常。比如递归求斐波拉契这时候可以使用尾调用来优化。用火焰图分析性能时候火焰越高说明栈越深。\nAT\u0026amp;T 汇编语法 # ATT汇编语法是类Unix的系统上的标准汇编语法比如gcc、gdb中默认都是使用AT\u0026amp;T汇编语法。AT\u0026amp;T汇编的指令格式如下\ninstruction src dst 其中instruction是指令助记符也叫操作码比如mov就是一个指令助记符src是源操作数dst是目的操作。\n当引用寄存器时候应在寄存器名称加前缀%,对于常数,则应加前缀 $。\n指令分类 # 数据传输指令 # 汇编指令 逻辑表达式 含义 mov $0x05, %ax R[ax] = 0x05 将数值5存储到寄存器ax中 mov %ax, -4(%bp) mem[R[bp] -4] = R[ax] 将ax寄存器中存储的数据存储到bp寄存器存的地址减去4之后的内存地址中 mov -4(%bp), %ax R[ax] = mem[R[bp] -4] bp寄存器存储的地址减去4值然后改地址对应的内存存储的信息存储到ax寄存器中 mov $0x10, (%sp) mem[R[sp]] = 0x10 将16存储到sp寄存器存储的地址对应的内存 push $0x03 mem[R[sp]] = 0x03 R[sp] = R[sp] - 4 将数值03入栈然后sp寄存器存储的地址减去4 pop R[sp] = R[sp] + 4 将当前sp寄存器指向的地址的变量出栈并将sp寄存器存储的地址加4 call func1 \u0026mdash; 调用函数func1 ret \u0026mdash; 函数返回将返回值存储到寄存器中或caller栈中并将return address弹出到ip寄存器中 当使用mov指令传递数据时数据的大小由mov指令的后缀决定。\nmovb $123, %eax // 1 byte movw $123, %eax // 2 byte movl $123, %eax // 4 byte movq $123, %eax // 8 byte 算术运算指令 # 指令 含义 subl $0x05, %eax R[eax] = R[eax] - 0x05 subl %eax, -4(%ebp) mem[R[ebp] -4] = mem[R[ebp] -4] - R[eax] subl -4(%ebp), %eax R[eax] = R[eax] - mem[R[ebp] -4] 跳转指令 # 指令 含义 cmpl %eax %ebx 计算 R[eax] - R[ebx], 然后设置flags寄存器 jmp location 无条件跳转到location je location 如果flags寄存器设置了相等标志则跳转到location jg, jge, jl, gle, jnz, \u0026hellip; location 如果flags寄存器设置了\u0026gt;, \u0026gt;=, \u0026lt;, \u0026lt;=, != 0等标志则跳转到location 栈与地址管理指令 # 指令 含义 等同操作 pushl %eax 将R[eax]入栈 subl $4, %esp; movl %eax, (%esp) popl %eax 将栈顶数据弹出然后存储到R[eax] movl (%esp), %eax addl $4, %esp leave Restore the callers stack pointer movl %ebp, %esp pop %ebp lea\t8(%esp), %esi 将R[esp]存放的地址加8然后存储到R[esi] R[esi] = R[esp] + 8 lea 是load effective address的缩写用于将一个内存地址直接赋给目的操作数。\n函数调用指令 # 指令 含义 call label 调用函数,并将返回地址入栈 ret 从栈中弹出返回地址,并跳转至该返回地址 leave 恢复调用者者栈指针 注意:\n以上指令分类并不规范和完整比如 call , ret 都可以算作无条件跳转指令,这里面是按照功能放在函数调用这一分类了。\nGo 汇编 # Go语言汇编器采用Plan9 汇编语法该汇编语言是由贝尔实验推出来的。下面说的Go汇编也就是Plan9 汇编。 不同于C语言汇编中汇编指令的寄存器都是代表硬件寄存器Go汇编中的寄存器使用的是伪寄存器可以把Go汇编考虑成是底层硬件汇编之上的抽象。\n伪寄存器 # Go汇编一共有4个伪寄存器\nFP: Frame pointer: arguments and locals.\n使用形如 symbol+offset(FP) 的方式,引用函数的输入参数。例如 arg0+0(FP)arg1+8(FP) offset是正值 PC: Program counter: jumps and branches.\nPC寄存器在 x86 平台下对应 ip 寄存器amd64 上则是 rip SB: Static base pointer: global symbols.\n全局静态基指针一般用来声明函数或全局变量 SP: Stack pointer: top of stack.\nSP寄存器指向当前栈帧的局部变量的开始位置使用形如 symbol+offset(SP) 的方式,引用函数的局部变量。 offset是负值offset 的合法取值是 [-framesize, 0)。 手写汇编代码时,如果是 symbol+offset(SP) 形式,则表示伪寄存器 SP。如果是 offset(SP) 则表示硬件寄存器 SP。对于编译输出(go tool compile -S / go tool objdump)的代码来讲,所有的 SP 都是硬件寄存器 SP无论是否带 symbol。 函数声明 # 参数大小+返回值大小 | TEXT pkgname·add(SB),NOSPLIT,$32-16 | | | 包名 函数名 栈帧大小 TEXT指令声明了pagname.add是在.text段\npkgname·add中的·是一个 unicode 的中点。在程序被链接之后,所有的中点·都会被替换为点号.,所以通过 GDB 调试打断点时候,应该是 b pagname.add\n(SB): SB 是一个虚拟寄存器,保存了静态基地址(static-base) 指针,即我们程序地址空间的开始地址。 \u0026quot;\u0026quot;.add(SB) 表明我们的符号add位于某个固定的相对地址空间起始处的偏移位置\nobjdump -j .text -t test | grep \u0026#39;main.add\u0026#39; # 可获得main.add的绝对地址 NOSPLIT: 表明该函数内部不进行栈分裂逻辑处理可以避免CPU资源浪费。关于栈分裂会在调度器章节介绍\n$32-16: $32代表即将分配的栈帧大小而$16指定了传入的参数与返回值的大小\n函数调用栈 # Go汇编中函数调用的参数以及返回值都是由栈传递和保存的这部分空间由caller在其栈帧(stack frame)上提供。Go汇编中没有使用PUSH/POP指令进行栈的伸缩处理所有栈的增长和收缩是通过在栈指针寄存器SP上分别执行加减指令来实现的。\ncaller +------------------+ | | +----------------------\u0026gt; |------------------| | | caller parent BP | | |------------------| \u0026lt;--------- BP(pseudo SP) | | local Var0 | | |------------------| | | ......... | | |------------------| | | local VarN | | |------------------| | | temporarily | | unused space | caller stack frame |------------------| | callee retN | | |------------------| | | ......... | | |------------------| | | callee ret0 | | |------------------| | | callee argN | | |------------------| | | ......... | | |------------------| | | callee arg0 | | |------------------| \u0026lt;--------- FP(virtual register) | | return addr | +----------------------\u0026gt; |------------------| \u0026lt;----------------------+ | caller BP | | BP(pseudo SP) ------\u0026gt; |------------------| | | local Var0 | | |------------------| | | local Var1 | |------------------| callee stack frame | ......... | |------------------| | | local VarN | | SP(Real Register) ------\u0026gt; |------------------| | | | | | | | +------------------+ \u0026lt;----------------------+ callee 关于Go汇编进一步知识我们将在 《 基础篇-函数-函数调用栈 》 章节详细探讨说明,此处我们只需要大致了解下函数声明、调用栈概念即可。\n获取Go汇编代码 # go代码示例\npackage main import \u0026#34;fmt\u0026#34; //go:noinline func add(a, b int) int { return a + b } func main() { c := add(3, 5) fmt.Println(c) } go tool compile # go tool compile -N -l -S main.go GOOS=linux GOARCH=amd64 go tool compile -N -l -S main.go # 指定系统和架构 -N选项指示禁止优化 -l选项指示禁止内联 -S选项指示打印出汇编代码 若要禁止指定函数内联优化也可以在函数定义处加上noinline编译指示\n//go:noinline func add(a, b int) int { return a + b } go tool objdump # 方法1 根据目标文件反编译出汇编代码\ngo tool compile -N -l main.go # 生成main.o go tool objdump main.o go tool objdump -s \u0026#34;main.(main|add)\u0026#34; ./test # objdump支持搜索特定字符串 方法2 根据可执行文件反编译出汇编代码\ngo build -gcflags=\u0026#34;-N -l\u0026#34; main.go -o test go tool objdump main.o go build -gcflags -S # go build -gcflags=\u0026#34;-N -l -S\u0026#34; main.go 其他方法 # objdump命令 go编译器gccgo 在线转换汇编代码godbolt 进一步阅读 # Go官方A Quick Guide to Go\u0026rsquo;s Assembler plan9 assembly 完全解析 x86 Assembly book What is an ABI? "},{"id":10,"href":"/compiler/","title":"Go编译流程","section":"简介","content":" 编译流程 # Go语言是一门编译型语言程序运行时需要先编译成相应平台的可执行文件。在介绍Go语言编译流程之前我们来了解下编译器编译整个流程。\n编译六阶段 # 编译器工作目标是完成从高级语言high-level langue到机器码machine code的输出。整个编译流程可分为两部分每个部分又可以细分为三个阶段也就是说整个编译流程分为六个阶段。编译流程的两部分别是分析部分Analysis part以及合成部分Synthesis part也称为编译前端和编译后端。编译六阶段如下\n词法分析Lexical analysis) 语法分析Syntax analysis 语义分析Semantic analysis 中间码生成Intermediate code generator 代码优化Code optimizer 机器代码生成Code generator 词法分析 # 词法分析最终生成的是Tokens。词法分析时编译器扫描源代码从当前行最左端开始到最右端然后将扫描到的字符进行分组标记。编译器会将扫描到的词法单位Lexemes归类到常量、保留字、运算符等标记Tokens中。我们以c = a+b*5为例:\nLexemes Tokens c identifier = assignment symbol a identifier + + (addition symbol) b identifier * * (multiplication symbol) 5 5 (number) 语法分析 # 词法分析阶段接收上一阶段生成的Tokens序列基于特定编程语言的规则生成抽象语法树Abstract Syntax Tree。\n抽象语法树 # 抽象语法树Abstract Syntax Tree简称AST是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构树上的每个节点都表示源代码中的一种结构。\n以(a+b)*c为例最终生成的抽象语法树如下\n语义分析阶段 # 语义分析阶段用来检查代码的语义一致性。它使用前一阶段的语法树以及符号表来验证给定的源代码在语义上是一致的。它还检查代码是否传达了适当的含义。\n中间码生成阶段 # 中间代码介是于高级语言和机器语言之间,具有跨平台特性。使用中间代码可以易于跨平台转换为特定类型目标机器代码。\n代码优化阶段 # 代码优化阶段主要是改进中间代码,删除不必要的代码,以调整代码序列以生成速度更快和空间更少的中间代码。\n机器码生成阶段 # 机器码生成阶段是编译器工作的最后阶段。此阶段会基于中间码生成汇编代码,汇编器根据汇编代码生成目标文件,目标文件经过链接器处理最终生成可执行文件。\nGo语言编译流程 # 上面介绍了编译器工作整个流程Go语言编译器编译也符合上面流程\nGo语言编译流程 我们执行go build命令时候带上-n选项可以观察编译流程所执行所有的命令\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # # command-line-arguments # mkdir -p $WORK/b001/ cat \u0026gt;$WORK/b001/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config packagefile runtime=/usr/lib/go/pkg/linux_amd64/runtime.a EOF cd /home/vagrant/dive-into-go /usr/lib/go/pkg/tool/linux_amd64/compile -o $WORK/b001/_pkg_.a -trimpath \u0026#34;$WORK/b001=\u0026gt;\u0026#34; -p main -complete -buildid aJhlsTb17ElgWQeF76b5/aJhlsTb17ElgWQeF76b5 -goversion go1.14.13 -D _/home/vagrant/dive-into-go -importcfg $WORK/b001/importcfg -pack ./empty_string.go /usr/lib/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/_pkg_.a # internal cat \u0026gt;$WORK/b001/importcfg.link \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal packagefile command-line-arguments=$WORK/b001/_pkg_.a packagefile runtime=/usr/lib/go/pkg/linux_amd64/runtime.a packagefile internal/bytealg=/usr/lib/go/pkg/linux_amd64/internal/bytealg.a packagefile internal/cpu=/usr/lib/go/pkg/linux_amd64/internal/cpu.a packagefile runtime/internal/atomic=/usr/lib/go/pkg/linux_amd64/runtime/internal/atomic.a packagefile runtime/internal/math=/usr/lib/go/pkg/linux_amd64/runtime/internal/math.a packagefile runtime/internal/sys=/usr/lib/go/pkg/linux_amd64/runtime/internal/sys.a EOF mkdir -p $WORK/b001/exe/ cd . /usr/lib/go/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=FoylCipvV-SPkhyi2PJs/aJhlsTb17ElgWQeF76b5/aJhlsTb17ElgWQeF76b5/FoylCipvV-SPkhyi2PJs -extld=gcc $WORK/b001/_pkg_.a /usr/lib/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal mv $WORK/b001/exe/a.out empty_string 从上面命令输出的内容可以看到:\nGo编译器首先会创建一个任务输出临时目录mkdir -p $WORK/b001/。b001是root task的工作目录每次构建都是由一系列task完成它们构成 action graph\n接着将empty_string.go中依赖的包: /usr/lib/go/pkg/linux_amd64/runtime.a 写入到importcfg中\n接着会使用compile命令并指定importcfg文件将主程序empty_string.go编译成_pkg.a文件/usr/lib/go/pkg/tool/linux_amd64/compile -o $WORK/b001/pkg.a -trimpath \u0026ldquo;$WORK/b001=\u0026gt;\u0026rdquo; -p main -complete -buildid aJhlsTb17ElgWQeF76b5/aJhlsTb17ElgWQeF76b5 -goversion go1.14.13 -D _/home/vagrant/dive-into-go -importcfg $WORK/b001/importcfg -pack ./empty_string.go。\n程序依赖的包都写到importcfg.link这个文件中Go编译器连接阶段中链接器会使用该文件找到所有依赖的包文件将其连接到程序中/usr/lib/go/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=FoylCipvV-SPkhyi2PJs/aJhlsTb17ElgWQeF76b5/aJhlsTb17ElgWQeF76b5/FoylCipvV-SPkhyi2PJs -extld=gcc $WORK/b001/pkg.a /usr/lib/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal。\n将编译成功的二进制文件移动到输出目录中mv $WORK/b001/exe/a.out empty_string。\n为了详细查看go build整个详细过程我们可以使用go build -work -a -p 1 -x empty_string.go命令来观察整个过程它比go build -n提供了更详细的信息:\n-work选项指示编译器编译完成后保留编译临时工作目录 -a选项强制编译所有包。我们使用go build -n时候只看到main包编译过程这是因为其他包已经编译过了不会再编译。我们可以使用这个选项强制编译所有包。 -p选项用来指定编译过程中线程数这里指定为1是为观察编译的顺序性 -x选项可以指定编译参数 输出内容摘要如下:\nvagrant@vagrant:~/dive-into-go$ go build -work -a -p 1 -x empty_string.go WORK=/tmp/go-build871888098 mkdir -p $WORK/b004/ cat \u0026gt;$WORK/b004/go_asm.h \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal EOF cd /usr/lib/go/src/internal/cpu /usr/lib/go/pkg/tool/linux_amd64/asm -trimpath \u0026#34;$WORK/b004=\u0026gt;\u0026#34; -I $WORK/b004/ -I /usr/lib/go/pkg/include -D GOOS_linux -D GOARCH_amd64 -gensymabis -o $WORK/b004/symabis ./cpu_x86.s cat \u0026gt;$WORK/b004/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config EOF /usr/lib/go/pkg/tool/linux_amd64/compile -o $WORK/b004/_pkg_.a -trimpath \u0026#34;$WORK/b004=\u0026gt;\u0026#34; -p internal/cpu -std -+ -buildid 8F_1bll3rU7d1mo74DFt/8F_1bll3rU7d1mo74DFt -goversion go1.14.13 -symabis $WORK/b004/symabis -D \u0026#34;\u0026#34; -importcfg $WORK/b004/importcfg -pack -asmhdr $WORK/b004/go_asm.h ./cpu.go ./cpu_amd64.go ./cpu_x86.go /usr/lib/go/pkg/tool/linux_amd64/asm -trimpath \u0026#34;$WORK/b004=\u0026gt;\u0026#34; -I $WORK/b004/ -I /usr/lib/go/pkg/include -D GOOS_linux -D GOARCH_amd64 -o $WORK/b004/cpu_x86.o ./cpu_x86.s /usr/lib/go/pkg/tool/linux_amd64/pack r $WORK/b004/_pkg_.a $WORK/b004/cpu_x86.o # internal /usr/lib/go/pkg/tool/linux_amd64/buildid -w $WORK/b004/_pkg_.a # internal cp $WORK/b004/_pkg_.a /home/vagrant/.cache/go-build/e2/e20b6a590621cff911735ea491492b992b429df9b0b579155aecbfdffdf7ec74-d # internal mkdir -p $WORK/b003/ cat \u0026gt;$WORK/b003/go_asm.h \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal EOF cd /usr/lib/go/src/internal/bytealg /usr/lib/go/pkg/tool/linux_amd64/asm -trimpath \u0026#34;$WORK/b003=\u0026gt;\u0026#34; -I $WORK/b003/ -I /usr/lib/go/pkg/include -D GOOS_linux -D GOARCH_amd64 -gensymabis -o $WORK/b003/symabis ./compare_amd64.s ./count_amd64.s ./equal_amd64.s ./index_amd64.s ./indexbyte_amd64.s cat \u0026gt;$WORK/b003/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config packagefile internal/cpu=$WORK/b004/_pkg_.a EOF /usr/lib/go/pkg/tool/linux_amd64/compile -o $WORK/b003/_pkg_.a -trimpath \u0026#34;$WORK/b003=\u0026gt;\u0026#34; -p internal/bytealg -std -+ -buildid I0-Z7SEGCaTIz2BZXZCm/I0-Z7SEGCaTIz2BZXZCm -goversion go1.14.13 -symabis $WORK/b003/symabis -D \u0026#34;\u0026#34; -importcfg $WORK/b003/importcfg -pack -asmhdr $WORK/b003/go_asm.h ./bytealg.go ./compare_native.go ./count_native.go ./equal_generic.go ./equal_native.go ./index_amd64.go ./index_native.go ./indexbyte_native.go /usr/lib/go/pkg/tool/linux_amd64/asm -trimpath \u0026#34;$WORK/b003=\u0026gt;\u0026#34; -I $WORK/b003/ -I /usr/lib/go/pkg/include -D GOOS_linux -D GOARCH_amd64 -o $WORK/b003/compare_amd64.o ./compare_amd64.s /usr/lib/go/pkg/tool/linux_amd64/asm -trimpath \u0026#34;$WORK/b003=\u0026gt;\u0026#34; -I $WORK/b003/ -I /usr/lib/go/pkg/include -D GOOS_linux -D GOARCH_amd64 -o $WORK/b003/count_amd64.o ./count_amd64.s /usr/lib/go/pkg/tool/linux_amd64/asm -trimpath \u0026#34;$WORK/b003=\u0026gt;\u0026#34; -I $WORK/b003/ -I /usr/lib/go/pkg/include -D GOOS_linux -D GOARCH_amd64 -o $WORK/b003/equal_amd64.o ./equal_amd64.s /usr/lib/go/pkg/tool/linux_amd64/asm -trimpath \u0026#34;$WORK/b003=\u0026gt;\u0026#34; -I $WORK/b003/ -I /usr/lib/go/pkg/include -D GOOS_linux -D GOARCH_amd64 -o $WORK/b003/index_amd64.o ./index_amd64.s /usr/lib/go/pkg/tool/linux_amd64/asm -trimpath \u0026#34;$WORK/b003=\u0026gt;\u0026#34; -I $WORK/b003/ -I /usr/lib/go/pkg/include -D GOOS_linux -D GOARCH_amd64 -o $WORK/b003/indexbyte_amd64.o ./indexbyte_amd64.s /usr/lib/go/pkg/tool/linux_amd64/pack r $WORK/b003/_pkg_.a $WORK/b003/compare_amd64.o $WORK/b003/count_amd64.o $WORK/b003/equal_amd64.o $WORK/b003/index_amd64.o $WORK/b003/indexbyte_amd64.o # internal /usr/lib/go/pkg/tool/linux_amd64/buildid -w $WORK/b003/_pkg_.a # internal cp $WORK/b003/_pkg_.a /home/vagrant/.cache/go-build/42/42c362e050cb454a893b15620b72fbb75879ac0a1fdd13762323eec247798a43-d # internal mkdir -p $WORK/b006/ cat \u0026gt;$WORK/b006/go_asm.h \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal EOF cd /usr/lib/go/src/runtime/internal/atomic /usr/lib/go/pkg/tool/linux_amd64/asm -trimpath \u0026#34;$WORK/b006=\u0026gt;\u0026#34; -I $WORK/b006/ -I /usr/lib/go/pkg/include -D GOOS_linux -D GOARCH_amd64 -gensymabis -o $WORK/b006/symabis ./asm_amd64.s cat \u0026gt;$WORK/b006/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config EOF /usr/lib/go/pkg/tool/linux_amd64/compile -o $WORK/b006/_pkg_.a -trimpath \u0026#34;$WORK/b006=\u0026gt;\u0026#34; -p runtime/internal/atomic -std -+ -buildid uI0THQvFtr7yRsGPOXDw/uI0THQvFtr7yRsGPOXDw -goversion go1.14.13 -symabis $WORK/b006/symabis -D \u0026#34;\u0026#34; -importcfg $WORK/b006/importcfg -pack -asmhdr $WORK/b006/go_asm.h ./atomic_amd64.go ./stubs.go /usr/lib/go/pkg/tool/linux_amd64/asm -trimpath \u0026#34;$WORK/b006=\u0026gt;\u0026#34; -I $WORK/b006/ -I /usr/lib/go/pkg/include -D GOOS_linux -D GOARCH_amd64 -o $WORK/b006/asm_amd64.o ./asm_amd64.s /usr/lib/go/pkg/tool/linux_amd64/pack r $WORK/b006/_pkg_.a $WORK/b006/asm_amd64.o # internal /usr/lib/go/pkg/tool/linux_amd64/buildid -w $WORK/b006/_pkg_.a # internal cp $WORK/b006/_pkg_.a /home/vagrant/.cache/go-build/6b/6b2c5449e17d9b0e34bfe37a77dc16b9675ffb657fbe9277a1067fa8ca5179ab-d # internal mkdir -p $WORK/b008/ cat \u0026gt;$WORK/b008/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config EOF cd /usr/lib/go/src/runtime/internal/sys /usr/lib/go/pkg/tool/linux_amd64/compile -o $WORK/b008/_pkg_.a -trimpath \u0026#34;$WORK/b008=\u0026gt;\u0026#34; -p runtime/internal/sys -std -+ -complete -buildid AZJ761JYi_ToDiYI_5UA/AZJ761JYi_ToDiYI_5UA -goversion go1.14.13 -D \u0026#34;\u0026#34; -importcfg $WORK/b008/importcfg -pack ./arch.go ./arch_amd64.go ./intrinsics.go ./intrinsics_common.go ./stubs.go ./sys.go ./zgoarch_amd64.go ./zgoos_linux.go ./zversion.go /usr/lib/go/pkg/tool/linux_amd64/buildid -w $WORK/b008/_pkg_.a # internal cp $WORK/b008/_pkg_.a /home/vagrant/.cache/go-build/f7/f706a1321f01a45857a441e80fd50709a700a9d304543d534a953827021222c1-d # internal mkdir -p $WORK/b007/ cat \u0026gt;$WORK/b007/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config packagefile runtime/internal/sys=$WORK/b008/_pkg_.a EOF cd /usr/lib/go/src/runtime/internal/math /usr/lib/go/pkg/tool/linux_amd64/compile -o $WORK/b007/_pkg_.a -trimpath \u0026#34;$WORK/b007=\u0026gt;\u0026#34; -p runtime/internal/math -std -+ -complete -buildid NxqylyDav-hCzDju1Kr1/NxqylyDav-hCzDju1Kr1 -goversion go1.14.13 -D \u0026#34;\u0026#34; -importcfg $WORK/b007/importcfg -pack ./math.go /usr/lib/go/pkg/tool/linux_amd64/buildid -w $WORK/b007/_pkg_.a # internal cp $WORK/b007/_pkg_.a /home/vagrant/.cache/go-build/f6/f6dcba7ea64d64182a26bcda498c1888786213b0b5560d9bde92cfff323be7df-d # internal ... 从上面可以看到编译器工作目录是/tmp/go-build871888098cd进去之后我们可以看到多个子目录每个子目录都是用编译子task使用存放的都是编译后的包\nvagrant@vagrant:/tmp/go-build871888098$ ls b001 b002 b003 b004 b006 b007 b008 其中b001目录用于main包编译是任务图的root节点。b001目录下面的importcfg.link文件存放都是程序所有依赖的包地址它们指向的都是b002,b003\u0026hellip;这些目录下的_pkg_.a文件。\nGo 编译器 # Go 编译器英文名称是Go compiler简称gc。gc是Go命令的一部分包含在每次Go发行版本中。Go命令是由Go语言编写的而Go 语言编写的程序需要Go命令来编译也就是自己编译自己这就出现了“先有鸡还是先有蛋”的问题。Go gc如何做到自己编译自己呢要解答这个问题我们先来了解下自举概念。\n自举 # 自举英文名称是Bootstrapping。自举指的是用要编译的程序的编程语言来编写其编译器。自举步骤一般如下假定要编译的程序语言是A\n先使用程序语言B实现A的编译器假定为compiler0 接着使用A语言实现A的编译器之后使用步骤1中的compiler0编译器编译得到编译器compiler1 最后我们就可以使用compiler1来编译A语言写的程序这样实现了自己编译自己 通过自举方式解决了上面说的“先有鸡还是先有蛋”的问题实现了自己编译自己。Go语言最开始是使用C语言实现的编译器go1.4是最后一个C语言实现的编译器版本。自go1.5开始Go实现了自举功能go1.5的gc是由go语言实现的它是由go1.4版本的C语言实现编译器编译出来的详细内容可以参见Go 自举的设计文档: Go 1.3+ Compiler Overhaul。\n除了 Go 语言实现的 gc 外Go 官方还维护了一个基于 gcc 实现的 Go 编译器 gccgo。与 gc 相比gccgo 编译代码较慢,但支持更强大的优化,因此由 gccgo 构建的 CPU 密集型(CPU-bound)程序通常会运行得更快。此外 gccgo 比 gc 支持更多的操作系统如果交叉编译gc不支持的操作系统可以考虑使用gccgo。\n源码安装 # Go 源码安装需要系统先有一个bootstrap toolchain该toolchain可以从下面三种方式获取\n从官网下载Go二进制发行包 使用gccgo工具编译 基于Go1.4版本的工具链 从官网下载发行包 # 第一种方式是从Go发行包中获取Go二进制应用比如要源码编译go1.14.13,我们可以去 官网下载已经编译好的go1.13设置好GOROOT_BOOTSTRAP环境变量就可以源码编译了。\nwget https://golang.org/dl/go1.13.15.linux-amd64.tar.gz tar xzvf go1.13.15.linux-amd64.tar.gz mv go go1.13.15 export GOROOT_BOOTSTRAP=/tmp/go1.13.15 # 设置GOROOT_BOOTSTRAP环境变量指向bootstrap toolchain的目录 cd /tmp git clone -b go1.14.13 https://go.googlesource.com/go go1.14.13 cd go1.14.13/src ./make.bash 使用gccgo工具编译 # 第二种方式是使用gccgo来编译\nsudo apt-get install gccgo-5 sudo update-alternatives --set go /usr/bin/go-5 export GOROOT_BOOTSTRAP=/usr cd /tmp git clone -b go1.14.13 https://go.googlesource.com/go go1.14.13 cd go1.14.13/src ./make.bash 基于go1.14版本工具链编译 # 第三种方式是先编译出go1.4版本然后使用go1.4版本去编译其他版本。\ncd /tmp git clone -b go1.4.3 https://go.googlesource.com/go go1.4 cd go1.4/src ./all.bash # go1.4版本是c语言实现的编译器 export GOROOT_BOOTSTRAP=/tmp/go1.4 git clone -b go1.14.13 https://go.googlesource.com/go go1.14.13 cd go1.14.13/src ./all.bash 进一步阅读 # Go: Overview of the Compiler How a Go Program Compiles down to Machine Code 编译原理 Compiler appearance - Phases of Compiler Introduction to the Go compiler How “go build” Works Go 1.3+ Compiler Overhaul Installing Go from source GcToolchainTricks Bootstrapping Go Gccgo in GCC 4.7.1 "},{"id":11,"href":"/analysis-tools/go-buildin-tools/","title":"Go语言内置分析工具","section":"Go语言分析工具","content":" Go 内置分析工具 # 这一章节将介绍Go 内置分析工具。通过这些工具我们可以分析、诊断、跟踪竞态GMP调度CPU耗用等问题。\ngo build # go build命令用来编译Go 程序。go build重要的命令行选项有以下几个\ngo build -n # -n选项用来显示编译过程中所有执行的命令不会真正执行。通过该选项我们可以查看编译器连接器如何工作的\n# # _/home/vagrant/dive-into-go # mkdir -p $WORK/b001/ cat \u0026gt;$WORK/b001/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config packagefile fmt=/usr/lib/go/pkg/linux_amd64/fmt.a packagefile runtime=/usr/lib/go/pkg/linux_amd64/runtime.a EOF cd /home/vagrant/dive-into-go /usr/lib/go/pkg/tool/linux_amd64/compile -o $WORK/b001/_pkg_.a -trimpath \u0026#34;$WORK/b001=\u0026gt;\u0026#34; -p main -complete -buildid RcHLBQbXBa2gQVsMR6P0/RcHLBQbXBa2gQVsMR6P0 -goversion go1.14.13 -D _/home/vagrant/dive-into-go -importcfg $WORK/b001/importcfg -pack ./empty_string.go ./string.go /usr/lib/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/_pkg_.a # internal cat \u0026gt;$WORK/b001/importcfg.link \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal packagefile _/home/vagrant/dive-into-go=$WORK/b001/_pkg_.a packagefile fmt=/usr/lib/go/pkg/linux_amd64/fmt.a packagefile runtime=/usr/lib/go/pkg/linux_amd64/runtime.a packagefile errors=/usr/lib/go/pkg/linux_amd64/errors.a packagefile internal/fmtsort=/usr/lib/go/pkg/linux_amd64/internal/fmtsort.a packagefile io=/usr/lib/go/pkg/linux_amd64/io.a packagefile math=/usr/lib/go/pkg/linux_amd64/math.a packagefile os=/usr/lib/go/pkg/linux_amd64/os.a packagefile reflect=/usr/lib/go/pkg/linux_amd64/reflect.a packagefile strconv=/usr/lib/go/pkg/linux_amd64/strconv.a packagefile sync=/usr/lib/go/pkg/linux_amd64/sync.a packagefile unicode/utf8=/usr/lib/go/pkg/linux_amd64/unicode/utf8.a packagefile internal/bytealg=/usr/lib/go/pkg/linux_amd64/internal/bytealg.a packagefile internal/cpu=/usr/lib/go/pkg/linux_amd64/internal/cpu.a packagefile runtime/internal/atomic=/usr/lib/go/pkg/linux_amd64/runtime/internal/atomic.a packagefile runtime/internal/math=/usr/lib/go/pkg/linux_amd64/runtime/internal/math.a packagefile runtime/internal/sys=/usr/lib/go/pkg/linux_amd64/runtime/internal/sys.a packagefile internal/reflectlite=/usr/lib/go/pkg/linux_amd64/internal/reflectlite.a packagefile sort=/usr/lib/go/pkg/linux_amd64/sort.a packagefile math/bits=/usr/lib/go/pkg/linux_amd64/math/bits.a packagefile internal/oserror=/usr/lib/go/pkg/linux_amd64/internal/oserror.a packagefile internal/poll=/usr/lib/go/pkg/linux_amd64/internal/poll.a packagefile internal/syscall/execenv=/usr/lib/go/pkg/linux_amd64/internal/syscall/execenv.a packagefile internal/syscall/unix=/usr/lib/go/pkg/linux_amd64/internal/syscall/unix.a packagefile internal/testlog=/usr/lib/go/pkg/linux_amd64/internal/testlog.a packagefile sync/atomic=/usr/lib/go/pkg/linux_amd64/sync/atomic.a packagefile syscall=/usr/lib/go/pkg/linux_amd64/syscall.a packagefile time=/usr/lib/go/pkg/linux_amd64/time.a packagefile unicode=/usr/lib/go/pkg/linux_amd64/unicode.a packagefile internal/race=/usr/lib/go/pkg/linux_amd64/internal/race.a EOF mkdir -p $WORK/b001/exe/ cd . /usr/lib/go/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=nR64Q3qx-0ZdNI4_-qJS/RcHLBQbXBa2gQVsMR6P0/RcHLBQbXBa2gQVsMR6P0/nR64Q3qx-0ZdNI4_-qJS -extld=gcc $WORK/b001/_pkg_.a /usr/lib/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal mv $WORK/b001/exe/a.out dive-into-go go build -race # -race选项用来检查代码中是否存在竞态问题。-race可以用在多个子命令中\ngo test -race mypkg go run -race mysrc.go go build -race mycmd go install -race mypkg 下面是来自Go语言官方博客的一个示例1在该示例中演示了使用-race选项检查代码中的竞态问题\nfunc main() { start := time.Now() var t *time.Timer t = time.AfterFunc(randomDuration(), func() { fmt.Println(time.Now().Sub(start)) t.Reset(randomDuration()) }) time.Sleep(5 * time.Second) } func randomDuration() time.Duration { return time.Duration(rand.Int63n(1e9)) } 上面代码完成的功能是通过time.AfterFunc创建定时器该定时器会在randomDuration()时候打印消息此外还会通过Rest()方法重置该定时器,以达到重复利用该定时器目的。\n当我们使用-race选项执行检查时候可以发现上面代码是存在竞态问题的\n$ go run -race main.go ================== WARNING: DATA RACE Read by goroutine 5: main.func·001() race.go:14 +0x169 Previous write by goroutine 1: main.main() race.go:15 +0x174 Goroutine 5 (running) created at: time.goFunc() src/pkg/time/sleep.go:122 +0x56 timerproc() src/pkg/runtime/ztime_linux_amd64.c:181 +0x189 ================== go build -gcflags # -gcflags选项用来设置编译器编译时参数支持的参数有\n-N选项指示禁止优化 -l选项指示禁止内联 -S选项指示打印出汇编代码 -m选项指示打印出变量变量逃逸信息-m -m可以打印出更丰富的变量逃逸信息 -gcflags支持只在编译特定包时候才传递编译参数此时的-gcflags格式为包名=参数列表。\ngo build -gcflags=\u0026#34;-N -l -S\u0026#34; main.go // 打印出main.go对应的汇编代码 go build -gcflags=\u0026#34;log=-N -l\u0026#34; main.go // 只对log包进行禁止优化禁止内联操作 go tool compile # go tool compile命令用于汇编处理Go 程序文件。go tool compile支持常见选项有\n-N选项指示禁止优化 -l选项指示禁止内联 -S选项指示打印出汇编代码 -m选项指示打印出变量内存逃逸信息 go tool compile -N -l -S main.go # 打印出main.go对应的汇编代码 GOOS=linux GOARCH=amd64 go tool compile -N -l -S main.go # 打印出针对特定系统和CPU架构的汇编代码 go tool nm # go tool nm命令用来查看Go 二进制文件中符号表信息。\ngo tool nm ./main | grep \u0026#34;runtime.zerobase\u0026#34; go tool objdump # go tool objdump命令用来根据目标文件或二进制文件反编译出汇编代码。该命令支持两个选项\n-S选项指示打印汇编代码 -s选项指示搜索相关的汇编代码 go tool compile -N -l main.go # 生成main.o go tool objdump main.o # 打印所有汇编代码 go tool objdump -s \u0026#34;main.(main|add)\u0026#34; ./test # objdump支持搜索特定字符串 go tool trace # GODEBUG环境变量 # GODEBUG是控制运行时调试的变量其参数以逗号分隔格式为name=val。GODEBUG可以用来观察GMP调度和GC过程。\nGMP调度 # 与GMP调度相关的两个参数\nschedtrace设置 schedtrace=X 参数可以使运行时在每 X 毫秒输出一行调度器的摘要信息到标准 err 输出中。\nscheddetail设置 schedtrace=X 和 scheddetail=1 可以使运行时在每 X 毫秒输出一次详细的多行信息信息内容主要包括调度程序、处理器、OS 线程 和 Goroutine 的状态。\n我们以下面代码为例\npackage main import ( \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) func main() { var wg sync.WaitGroup for i := 0; i \u0026lt; 2000; i++ { wg.Add(1) go func() { a := 0 for i := 0; i \u0026lt; 1e6; i++ { a += 1 } wg.Done() }() time.Sleep(100 * time.Millisecond) } wg.Wait() } 执行以下代码获取GMP调度信息\nGODEBUG=schedtrace=1000 go run ./test.go 笔者本人电脑输出以下内容:\nSCHED 0ms: gomaxprocs=8 idleprocs=6 threads=4 spinningthreads=1 idlethreads=0 runqueue=0 [0 0 0 0 0 0 0 0] SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=3 spinningthreads=1 idlethreads=0 runqueue=0 [1 0 0 0 0 0 0 0] SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=5 spinningthreads=1 idlethreads=0 runqueue=0 [0 0 0 0 0 0 0 0] SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=5 spinningthreads=2 idlethreads=0 runqueue=0 [0 0 0 0 0 0 0 0] SCHED 1007ms: gomaxprocs=8 idleprocs=8 threads=16 spinningthreads=0 idlethreads=9 runqueue=0 [0 0 0 0 0 0 0 0] SCHED 1000ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0] SCHED 2018ms: gomaxprocs=8 idleprocs=8 threads=16 spinningthreads=0 idlethreads=9 runqueue=0 [0 0 0 0 0 0 0 0] 上面输出内容解释说明:\nSCHED XXms: SCHED是调度日志输出标志符。XXms是自程序启动之后到输出当前行时间 gomaxprocs P的数量等于当前的 CPU 核心数或者GOMAXPROCS环境变量的值 idleprocs 空闲P的数量与gomaxprocs的差值即运行中P的数量 threads 线程数量即M的数量 spinningthreads自旋状态线程的数量。当M没有找到可供其调度执行的 Goroutine 时,该线程并不会销毁,而是出于自旋状态 idlethreads空闲线程的数量 runqueue全局队列中G的数量 [0 0 0 0 0 0 0 0]表示P本地队列下G的数量有几个P中括号里面就会有几个数字 GC # 与GC相关的参数是gctrace当设置为1时候会输出gc信息到标准err输出中。使用方式示例如下\nGODEBUG=gctrace=1 godoc -http=:8080 GC时候输出的内容格式如下\ngc# @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #-\u0026gt;#-\u0026gt;# MB, # MB goal, #P\n格式解释说明如下\ngc#GC 执行次数的编号,每次叠加。 @#s自程序启动后到当前的具体秒数。 #%自程序启动以来在GC中花费的时间百分比。 #+\u0026hellip;+#GC 的标记工作共使用的 CPU 时间占总 CPU 时间的百分比。 #-\u0026gt;#-\u0026gt;# MB分别表示 GC 启动时, GC 结束时, GC 活动时的堆大小. #MB goal下一次触发 GC 的内存占用阈值。 #P当前使用的处理器 P 的数量。 比如对于以下输出内容:\ngc 100 @0.904s 11%: 0.043+2.8+0.029 ms clock, 0.34+3.4/5.4/0+0.23 ms cpu, 10-\u0026gt;11-\u0026gt;6 MB, 12 MB goal, 8 P\ngc 100第 100 次 GC\n@0.904s当前时间是程序启动后的0.904s\n11%:程序启动后到现在共花费 11% 的时间在 GC 上\n0.043+2.8+0.029 ms clock\n0.043:表示单个 P 在 mark 阶段的 STW 时间 2.8:表示所有 P 的 mark concurrent并发标记所使用的时间 0.029:表示单个 P 的 markTermination 阶段的 STW 时间 0.34+3.4/5.4/0+0.23 ms cpu:\n0.34:表示整个进程在 mark 阶段 STW 停顿的时间一共0.34秒 3.4/5.4/03.4 表示 mutator assist 占用的时间5.4 表示 dedicated + fractional 占用的时间0 表示 idle 占用的时间 0.23 ms0.23 表示整个进程在 markTermination 阶段 STW 时间 10-\u0026gt;11-\u0026gt;6 MB:\n10表示开始 mark 阶段前的 heap_live 大小 11表示开始 markTermination 阶段前的 heap_live 大小 6表示被标记对象的大小 12 MB goal表示下一次触发 GC 回收的阈值是 12 MB\n8 P本次 GC 一共涉及8 P\nGOGC参数 # Go语言GC相关的另外一个参数是GOGC。GOGC 用于控制GC的处发频率 其值默认为100, 这意味着直到自上次垃圾回收后heap size已经增长了100%时GC才触发运行live heap size每增长一倍GC触发运行一次。若设定GOGC=200, 则live heap size 自上次垃圾回收后增长2倍时GC触发运行 总之其值越大则GC触发运行频率越低 反之则越高。如果GOGC=off 则关闭GC。\n# 表示当前应用占用的内存是上次GC时占用内存的两倍时触发GC export GOGC=100 进一步阅读 # Go 大杀器之跟踪剖析 trace go runtime Environment Variables Introducing the Go Race Detector\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n"},{"id":12,"href":"/analysis-tools/","title":"Go语言分析工具","section":"简介","content":" 分析工具 # 工欲善其事,必先利其器。\nGDB Delve Go 内置工具 "},{"id":13,"href":"/type/nil/","title":"nil类型","section":"数据类型与数据结构","content":" nil # 在探究 nil 之前,我们先看看零值的概念。\n零值 # 零值(zero value)1 指的是当声明变量且未显示初始化时Go语言会自动给变量赋予一个默认初始值。对于值类型变量来说不同值类型有不同的零值比如整数型零值是 0字符串类型是 \u0026quot;\u0026quot;,布尔类型是 false。对于引用类型变量其零值都是 nil。\n类型 零值 数值类型 0 字符串 \u0026quot;\u0026quot; 布尔类型 false 指针类型 nil 通道 nil 函数 nil 接口 nil 映射 nil 切片 nil 结构体 每个结构体字段对应类型的零值 自定义类型 其底层类型的对应的零值 从零值的定义可以看出Go语言引入 nil 概念,是为了将其作为引用类型变量的零值而存在。\nnil # nil 是Go语言中的一个变量是预先声明的标识符用来作为引用类型变量的零值。\n// nil is a predeclared identifier representing the zero value for a // pointer, channel, func, interface, map, or slice type. var nil Type // Type must be a pointer, channel, func, interface, map, or slice type nil 不能通过:=方式赋值给一个变量,下面代码是编译不通过的:\na := nil 上面代码编译不通过是因为Go语言是无法通过 nil 自动推断出a的类型而Go语言是强类型的每个变量都必须明确其类型。将 nil 赋值一个变量是可以的:\nvar a chan int a = nil b := make([]int, 5) b = nil 与nil进行比较 # nil 与 nil比较 # nil 是不能和 nil 比较的:\nfunc main() { fmt.Println(nil == nil) // 报错invalid operation: nil == nil (operator == not defined on nil) } nil 与 指针类型变量、通道、切片、函数、映射比较 # nil 是可以和指针类型变量,通道、切片、函数、映射比较的。\n对于指针类型变量只有其未指向任何对象时候才能等于 nil func main() { var p *int println(p == nil) // true a := 100 p = \u0026amp;a println(p == nil) // false } 对于通道、切片、映射只有 var t T 或者手动赋值为nil时候(t = nil)才能等于nil: func main() { // 通道 var ch chan int println(ch == nil) // true ch = make(chan int, 0) println(ch == nil) // false ch1 := make(chan int, 0) println(ch1 == nil) // false ch1 = nil println(ch1 == nil) // true // 切片 var s []int // 此时s是nil slice println(s == nil) // true s = make([]int, 0, 0) // 此时s是empty slice println(s == nil) // false // 映射 var m map[int]int // 此时m是nil map println(m == nil) // true m = make(map[int]int, 0) println(m == nil) // false // 函数 var fn func() println(fn == nil) fn = func() { } println(fn == nil) } 从上面可以看到通过make函数初始化的变量都不等于 nil。\nnil 与 接口比较 # 接口类型变量包含两个基础属性Type 和 ValueType 指的是接口类型变量的底层类型Value 指的是接口类型变量的底层值。接口类型变量是可以比较的。当它们具有相同的底层类型且相等的底层值时候或者两者都为nil时候这两个接口值是相等的。\n当 nil 与接口比较时候,需要接口的 Type 和 Value都是 nil 时候,两者才相等:\nfunc main() { var p *int var i interface{} // (T=nil, V=nil) println(p == nil) // true println(i == nil) // true var pi interface{} = interface{}(p) // (T=*int, V= nil) println(pi == nil) // false println(pi == i) // fasle println(p == i) // false。跟上面强制转换p一样。当变量和接口比较时候会隐式将其转换成接口 var a interface{} = nil // (T=nil, V=nil) println(a == nil) // true var a2 interface{} = (*interface{})(nil) // (T=*interface{}, V=nil) println(a2 == nil) // false var a3 interface{} = (interface{})(nil) // (T=nil, V=nil) println(a3 == nil) // true } nil 和接口比较最容易出错的场景是使用error接口时候。Go官方文档举了一个例子 Why is my nil error value not equal to nil?:\ntype MyError int func (e *MyError) Error() string { return \u0026#34;errCode \u0026#34; + string(int) } func returnError() error { var p *MyError = nil if bad() { // 出现错误时候返回MyError p = \u0026amp;MyError(401) } // println(p == nil) // 输出true return p } func checkError(err error) { if err == nil { println(\u0026#34;nil\u0026#34;) return } println(\u0026#34;not nil\u0026#34;) } err := returnError() // 假定returnsError函数中bad()返回false println(err == nil) // false checkError(err) // 输出not nil 我们可以看到上面代码中 checkError 函数输出的并不是 nil而是 not nil。这是因为接口类型变量 err 的底层类型是 (T=*MyError, V=nil),不再是 (T=nil, V=nil)。解决办法是当需返回 nil 时候,直接返回 nil。\nfunc returnError() error { if bad() { // 出现错误时候返回MyError return \u0026amp;MyError(401) } return p } 几个值为nil的特别变量 # nil通道 # 通道类型变量的零值是 nil对于等于 nil 的通道称为 nil通道。当从 nil通道 读取或写入数据时候会发生永久性阻塞若关闭则会发生恐慌。nil通道 存在的意义可以参考 Why are there nil channels in Go?\nnil切片 # 对 nil切片 进行读写操作时候会发生恐慌。但对 nil切片 进行 append 操作时候是可以的这是因为Go语言对append操作做了特殊处理。\nvar s []int s[0] = 1 // panic: runtime error: index out of range [0] with length 0 println(s[0]) // panic: runtime error: index out of range [0] with length 0 s = append(s, 100) // ok nil映射 # 我们可以对 nil映射 进行读取和删除操作,当进行读取操作时候会返回映射的零值。当进行写操作时候会发生恐慌。\nfunc main() { var m map[int]int println(m[100]) // print 0 delete(m, 1) m[100] = 100 // panic: assignment to entry in nil map } nil接收者 # 值为 nil 的变量可以作为函数的接收者:\nconst defaultPath = \u0026#34;/usr/bin/\u0026#34; type Config struct { path string } func (c *Config) Path() string { if c == nil { return defaultPath } return c.path } func main() { var c1 *Config var c2 = \u0026amp;Config{ path: \u0026#34;/usr/local/bin/\u0026#34;, } fmt.Println(c1.Path(), c2.Path()) } nil函数 # nil函数 可以用来处理默认值情况:\nfunc NewServer(logger function) { if logger == nil { logger = log.Printf // default } logger.DoSomething... } 参考资料 # Golang 零值、空值与空结构 Why are there nil channels in Go? Go官方语法指南零值的定义\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n"},{"id":14,"href":"/feature/panic-recover/","title":"panic与recover机制","section":"语言特性","content":" 恐慌与恢复 - panic/recover # 我们知道Go语言中许多错误会在编译时暴露出来直接编译不通过但对于空指针访问元素切片/数组越界访问之类的运行时错误,只会在运行时引发 panic 异常暴露出来。这种由Go语言自动的触发的 panic 异常属于运行时panic(Run-time panics)1。当发生 panic 时候Go会运行所有已经注册的延迟函数若延迟函数中未进行panic异常捕获处理那么最终Go进程会终止并打印堆栈信息。此外Go中还内置了 panic 函数可以用于用户手动触发panic。\nGo语言中内置的 recover 函数可以用来捕获 panic异常但 recover 函数只能放在延迟函数调用中,才能起作用。我们从之前的章节《 基础篇-语言特性-defer函数 》了解到多个延迟函数会组成一个链表。Go在发生panic过程中会依次遍历该链表并检查链表中的延迟函数是否调用了 recover 函数调用,若调用了则 panic 异常会被捕获而不会继续向上抛出,否则会继续向上抛出异常和执行延迟函数,直到该 panic 没有被捕获进程异常终止这个过程叫做panicking。我们需要知道的是即使panic被延迟函数链表中某个延迟函数捕获处理了但其他的延迟函数还是会继续执行的只是panic异常不在继续抛出。\n接下来我们来将深入了解下panic和recover底层的实现机制。在开始之前我们来看下下面的测试题。\n测试题下面哪些panic异常将会捕获 # case 1:\nfunc main() { recover() panic(\u0026#34;it is panic\u0026#34;) // not recover } case 2:\nfunc main() { defer func() { recover() }() panic(\u0026#34;it is panic\u0026#34;) // recover } case 3:\nfunc main() { defer recover() panic(\u0026#34;it is panic\u0026#34;) // not recover } case 4:\nfunc main() { defer func() { defer recover() }() panic(\u0026#34;it is panic\u0026#34;) // recover } case 5:\nfunc main() { defer func() { defer func() { recover() }() }() panic(\u0026#34;it is panic\u0026#34;) // not recover } case 6:\nfunc main() { defer doRecover() panic(\u0026#34;it is panic\u0026#34;) // recover } func doRecover() { recover() fmt.Println(\u0026#34;hello\u0026#34;) } case 7:\nfunc main() { defer doRecover() panic(\u0026#34;it is panic\u0026#34;) // recover } func doRecover() { defer recover() } 简单说明下上面几个案例运行结果:\ncase 1中recover函数调用不是在defer延迟函数里面肯定不会捕获panic异常。 case 2中是panic异常捕获的标准操作是可以捕获panic异常的case 6跟case 2是一样的只不过一个是匿名延迟函数一个是具名延迟函数同样可以捕获panic异常。 case 3中recover函数作为延迟函数没有在其他延迟函数中调用它也是不起作用的。 case 4中recover函数被一个延迟函数调用且recover函数本身作为一个延迟函数这个情况下也是可以正常捕获panic异常的case 7跟case 4是一样的只不过一个是匿名延迟函数一个是具名延迟函数同样可以捕获panic异常。 case 5中尽管recover函数被延迟函数调用但它却无法捕获panic异常。 从上面案例中可以看出来使用recover函数进行panic异常捕获也要使用正确才能起作用。下面会分析源码探讨panic-recover实现机制也能更好帮助你理解为什么case 2,case 4可以起作用而case 3和case 5为啥没有起作用。\n源码分析 # 我们先分析case 2案例我们可以通过go tool compile -N -l -S case2.go获取 汇编代码来查看panic和recover在底层真正的实现\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 main_pc0: TEXT \u0026#34;\u0026#34;.main(SB), ABIInternal, $104-0 MOVQ (TLS), CX CMPQ SP, 16(CX) JLS main_pc113 SUBQ $104, SP MOVQ BP, 96(SP) LEAQ 96(SP), BP MOVL $0, \u0026#34;\u0026#34;..autotmp_1+16(SP) LEAQ \u0026#34;\u0026#34;.main.func1·f(SB), AX MOVQ AX, \u0026#34;\u0026#34;..autotmp_1+40(SP) LEAQ \u0026#34;\u0026#34;..autotmp_1+16(SP), AX MOVQ AX, (SP) CALL runtime.deferprocStack(SB) TESTL AX, AX JNE main_pc97 JMP main_pc69 main_pc69: LEAQ type.string(SB), AX MOVQ AX, (SP) LEAQ \u0026#34;\u0026#34;..stmp_0(SB), AX MOVQ AX, 8(SP) CALL runtime.gopanic(SB) main_pc97: XCHGL AX, AX CALL runtime.deferreturn(SB) MOVQ 96(SP), BP ADDQ $104, SP RET main_pc113: NOP CALL runtime.morestack_noctxt(SB) JMP main_pc0 main_func1_pc0: TEXT \u0026#34;\u0026#34;.main.func1(SB), ABIInternal, $32-0 MOVQ (TLS), CX CMPQ SP, 16(CX) JLS main_func1_pc53 SUBQ $32, SP MOVQ BP, 24(SP) LEAQ 24(SP), BP LEAQ \u0026#34;\u0026#34;..fp+40(SP), AX MOVQ AX, (SP) CALL runtime.gorecover(SB) MOVQ 24(SP), BP ADDQ $32, SP RET main_func1_pc53: NOP CALL runtime.morestack_noctxt(SB) JMP main_func1_pc0 从上面汇编代码中,可以看出 panic 函数底层实现 runtime.gopanicrecover 函数底层实现是 runtime.gorecover。\npanic函数底层实现的 runtime.gopanic 源码如下:\nfunc gopanic(e interface{}) { gp := getg() ... // 一些判断当前g是否允许在用户栈是否正在内存分配的代码略 var p _panic // panic底层数据结构是_panic p.arg = e // e是panic函数的参数对应case2中的: it is panic p.link = gp._panic gp._panic = (*_panic)(noescape(unsafe.Pointer(\u0026amp;p))) // 将当前panic挂到g上面 atomic.Xadd(\u0026amp;runningPanicDefers, 1) // 记录正在执行panic的goroutine数量防止main groutine返回时候 // 其他goroutine的panic栈信息未打印出来。@see https://github.com/golang/go/blob/go1.14.13/src/runtime/proc.go#L208-L220 // 对于open-coded defer实现的延迟函数需要扫描FUNCDATA_OpenCodedDeferInfo信息 // 获取延迟函数的sp/pc信息并创建_defer结构将其插入gp._defer链表中 // 这是也是在defer函数章节中提到的为啥open-coded defer提升了延迟函数的性能而panic性能却降低的原因 addOneOpenDeferFrame(gp, getcallerpc(), unsafe.Pointer(getcallersp())) for { // 开始遍历defer链表 d := gp._defer if d == nil { break } // 当延迟函数里面再次抛出panic或者调用runtime.Goexit时候 // 会再次进入同一个延迟函数此时d.started已经设置为true状态 if d.started { if d._panic != nil { // 标记上一个_panic状态为aborted d._panic.aborted = true } d._panic = nil if !d.openDefer { // 对于非open-coded defer函数我们需要将_defer从gp._defer链表中溢出去防止继续重复执行 d.fn = nil gp._defer = d.link freedefer(d) continue } } // 标记当前defer开始执行这样当g栈增长时候或者垃圾回收时候可以更新defer的参数栈帧 d.started = true // 记录当前的_panic信息到_defer结构中这样当该defer函数再次发生panic时候可以标记d._panic为aborted状态 d._panic = (*_panic)(noescape(unsafe.Pointer(\u0026amp;p))) done := true if d.openDefer { // 如果该延迟函数是open-coded defer函数 done = runOpenDeferFrame(gp, d) // 运行open-coded defer函数如果当前栈下面没有其他延迟函数则返回true if done \u0026amp;\u0026amp; !d._panic.recovered { // 如果当前栈下面没有其他open-coded defer函数了且panic也未recover // 那么继续当前的open-coded defer函数的sp作为基址继续扫描funcdata获取open-coded defer函数。 // 之所以这么做是因为open-coded defer里面也存在defer函数的情况例如case4 addOneOpenDeferFrame(gp, 0, nil) } } else {// 非open-coded defer实现的defer函数 // getargp返回其caller的保存callee参数的地址。 // 之前介绍过了Go语言中函数调用约定callee的参数存储是由caller的栈空间提供。 p.argp = unsafe.Pointer(getargp(0)) // 这里面p.argp保存的gopanic函数作为caller时候保存callee参数的地址。 // 之所以要_panic.argp保存gopanic的callee参数地址 // 这是因为调用gorecover会通过此检查其caller的caller是不是gopanic。 // 这也是case5等不能捕获panic异常的原因。 // 调用defer函数 reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) } p.argp = nil // reflectcall did not panic. Remove d. if gp._defer != d { throw(\u0026#34;bad defer entry in panic\u0026#34;) } d._panic = nil pc := d.pc sp := unsafe.Pointer(d.sp) if done { // 从gp._defer链表清除掉当前defer函数 d.fn = nil gp._defer = d.link freedefer(d) } if p.recovered { gp._panic = p.link if gp._panic != nil \u0026amp;\u0026amp; gp._panic.goexit \u0026amp;\u0026amp; gp._panic.aborted { // A normal recover would bypass/abort the Goexit. Instead, // we return to the processing loop of the Goexit. gp.sigcode0 = uintptr(gp._panic.sp) gp.sigcode1 = uintptr(gp._panic.pc) mcall(recovery) throw(\u0026#34;bypassed recovery failed\u0026#34;) // mcall should not return } atomic.Xadd(\u0026amp;runningPanicDefers, -1) if done { // panic已经被recover处理掉了那么移除掉上面通过addOneOpenDeferFrame添加到gp._defer中的open-coded defer函数。 // 因为这些open-coded defer是通过inline方式执行的从gp._defer链表中移除掉不影响它们继续的执行 d := gp._defer var prev *_defer for d != nil { if d.openDefer { if d.started { break } if prev == nil { gp._defer = d.link } else { prev.link = d.link } newd := d.link freedefer(d) d = newd } else { prev = d d = d.link } } } gp._panic = p.link // 无用代码,上面已经操作过了 // Aborted panics are marked but remain on the g.panic list. // Remove them from the list. for gp._panic != nil \u0026amp;\u0026amp; gp._panic.aborted { gp._panic = gp._panic.link } if gp._panic == nil { // must be done with signal gp.sig = 0 } // Pass information about recovering frame to recovery. gp.sigcode0 = uintptr(sp) gp.sigcode1 = pc mcall(recovery) throw(\u0026#34;recovery failed\u0026#34;) // mcall should not return } } preprintpanics(gp._panic) fatalpanic(gp._panic) // should not return *(*int)(nil) = 0 // not reached } 对于基于open-coded defer方式实现的延迟函数中处理panic recover逻辑比如addOneOpenDeferFramerunOpenDeferFrame等函数这里不再深究。这里主要分析通过链表实现的延迟函数中处理panic recover逻辑。\n接下来我们看下recover函数底层实现runtime.gorecover源码\nfunc gorecover(argp uintptr) interface{} { gp := getg() p := gp._panic if p != nil \u0026amp;\u0026amp; !p.goexit \u0026amp;\u0026amp; !p.recovered \u0026amp;\u0026amp; argp == uintptr(p.argp) { p.recovered = true return p.arg } return nil } Go官方语法指南运行时恐慌\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n"},{"id":15,"href":"/concurrency/sync-once/","title":"一次性操作 - sync.Once","section":"并发编程","content":" 一次性操作 - sync.Once # sync.Once用来完成一次性操作比如配置加载单例对象初始化等。\n源码分析 # sync.Once定义如下\ntype Once struct { done uint32 // 用来标志操作是否操作 m Mutex // 锁,用来第一操作时候,加锁处理 } 接下来看剩下的全部代码:\nfunc (o *Once) Do(f func()) { if atomic.LoadUint32(\u0026amp;o.done) == 0 {// 原子性加载o.done若值为1说明已完成操作若为0说明未完成操作 o.doSlow(f) } } func (o *Once) doSlow(f func()) { o.m.Lock() // 加锁 defer o.m.Unlock() if o.done == 0 { // 再次进行o.done是否等于0判断因为存在并发调用doSlow的情况 defer atomic.StoreUint32(\u0026amp;o.done, 1) // 将o.done值设置为1用来标志操作完成 f() // 执行操作 } } "},{"id":16,"href":"/concurrency/context/","title":"上下文 - context","section":"并发编程","content":" 上下文 - context # Context是由Golang官方开发的并发控制包一方面可以用于当请求超时或者取消时候相关的goroutine马上退出释放资源另一方面Context本身含义就是上下文其可以在多个goroutine或者多个处理函数之间传递共享的信息。\n创建一个新的context必须基于一个父context新的context又可以作为其他context的父context。所有context在一起构造成一个context树。\nContext使用示例 # Context一大用处就是超时控制。我们先看一个简单用法。\nfunc main() { ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second) defer cancel() go SlowOperation(ctx) go func() { for { time.Sleep(300 * time.Millisecond) fmt.Println(\u0026#34;goroutine:\u0026#34;, runtime.NumGoroutine()) } }() time.Sleep(4 * time.Second) } func SlowOperation(ctx context.Context) { done := make(chan int, 1) go func() { // 模拟慢操作 dur := time.Duration(rand.Intn(5)+1) * time.Second time.Sleep(dur) done \u0026lt;- 1 }() select { case \u0026lt;-ctx.Done(): fmt.Println(\u0026#34;SlowOperation timeout:\u0026#34;, ctx.Err()) case \u0026lt;-done: fmt.Println(\u0026#34;Complete work\u0026#34;) } } 上面代码会不停打印当前groutine数量可以观察到SlowOperation函数执行超时之后goroutine数量由4个变成2个相关goroutetine退出了。源码可以去 go playground查看。\n再看一个关于超时处理的例子 源码可以去 go playground查看\n// // 根据github仓库统计信息接口查询某个仓库信息 func QueryFrameworkStats(ctx context.Context, framework string) \u0026lt;-chan string { stats := make(chan string) go func() { repos := \u0026#34;https://api.github.com/repos/\u0026#34; + framework req, err := http.NewRequest(\u0026#34;GET\u0026#34;, repos, nil) if err != nil { return } req = req.WithContext(ctx) client := \u0026amp;http.Client{} resp, err := client.Do(req) if err != nil { return } data, err := ioutil.ReadAll(resp.Body) if err != nil { return } defer resp.Body.Close() stats \u0026lt;- string(data) }() return stats } func main() { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() framework := \u0026#34;gin-gonic/gin\u0026#34; select { case \u0026lt;-ctx.Done(): fmt.Println(ctx.Err()) case statsInfo := \u0026lt;-QueryFrameworkStats(ctx, framework): fmt.Println(framework, \u0026#34; fork and start info : \u0026#34;, statsInfo) } } Context另外一个用途就是传递上下文信息。从WithValue方法我们可以创建一个可以储存键值的context\nContext源码分析 # Context接口 # 首先我们来看下Context接口\ntype Context interface { Deadline() (deadline time.Time, ok bool) Done() \u0026lt;-chan struct{} Err() error Value(key interface{}) interface{} } Context接口一共包含四个方法\nDeadline返回绑定该context任务的执行超时时间若未设置则ok等于false Done返回一个只读通道当绑定该context的任务执行完成并调用cancel方法或者任务执行超时时候该通道会被关闭 Err返回一个错误如果Done返回的通道未关闭则返回nil,如果context如果被取消返回Canceled错误如果超时则会返回DeadlineExceeded错误 Value根据key返回存储在context中k-v数据 实现Context接口的类型 # Context一共有4个类型实现了Context接口, 分别是emptyCtx, cancelCtx,timerCtx,valueCtx。每个类型都关联一个创建方法。\nemptyCtx # emptyCtx是int类型emptyCtx实现了Context接口是一个空context只能作为根context。\ntype emptyCtx int // func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { return } func (*emptyCtx) Done() \u0026lt;-chan struct{} { return nil } func (*emptyCtx) Err() error { return nil } func (*emptyCtx) Value(key interface{}) interface{} { return nil } func (e *emptyCtx) String() string { switch e { case background: return \u0026#34;context.Background\u0026#34; case todo: return \u0026#34;context.TODO\u0026#34; } return \u0026#34;unknown empty Context\u0026#34; } Background/TODO\ncontext包还提供两个函数返回emptyCtx类型。\nvar ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo } Background用于创建根context一般用于主函数、初始化和测试中我们创建的context一般都是基于Bacground创建的。TODO用于当我们不确定使用什么样的context的时候使用。\ncancelCtx # cancelCtx支持取消操作取消同时也会对实现了canceler接口的子代进行取消操作。我们来看下cancelCtx结构体和cancelceler接口\ntype cancelCtx struct { Context mu sync.Mutex done chan struct{} children map[canceler]struct{} err error } type canceler interface { cancel(removeFromParent bool, err error) Done() \u0026lt;-chan struct{} } cancelCtx:\nContext变量存储其父context done变量定义了一个通道并且只在第一次取消调用才关闭此通道。该通道是惰性创建的 children是一个映射类型用来存储其子代context中实现的canceler当该context取消时候会遍历该映射来让子代context进行取消操作 err记录错误信息默认是nil仅当第一次cancel调用时候才会设置。 我们分别来看下cancelCtx实现的Done,Err,cancel方法。\nfunc (c *cancelCtx) Done() \u0026lt;-chan struct{} { c.mu.Lock() // 加锁 if c.done == nil { // done通道惰性创建只有调用Done方法时候才会创建 c.done = make(chan struct{}) } d := c.done c.mu.Unlock() return d } func (c *cancelCtx) Err() error { c.mu.Lock() err := c.err c.mu.Unlock() return err } func (c *cancelCtx) cancel(removeFromParent bool, err error) { if err == nil { // 取消操作时候一定要传递err信息 panic(\u0026#34;context: internal error: missing cancel error\u0026#34;) } c.mu.Lock() if c.err != nil { // 只允许第一次cancel调用操作下一次进来直接返回 c.mu.Unlock() return } c.err = err if c.done == nil { // 未先进行Done调用而先行调用Cancel, 此时done是nil // 这时候复用全局已关闭的通道 c.done = closedchan } else { // 关闭Done返回的通道发送关闭信号 close(c.done) } // 子级context依次进行取消操作 for child := range c.children { child.cancel(false, err) } c.children = nil c.mu.Unlock() if removeFromParent { // 将当前context从其父级context中children map中移除掉父级Context与该Context脱钩。 // 这样当父级Context进行Cancel操作时候不会再改Context进行取消操作了。因为再取消也没有意义了因为该Context已经取消过了 removeChild(c.Context, c) } } func removeChild(parent Context, child canceler) { p, ok := parentCancelCtx(parent) if !ok { return } p.mu.Lock() if p.children != nil { delete(p.children, child) } p.mu.Unlock() } func parentCancelCtx(parent Context) (*cancelCtx, bool) { for { switch c := parent.(type) { case *cancelCtx: return c, true case *timerCtx: return \u0026amp;c.cancelCtx, true case *valueCtx: // 当父级context是不支持cancel操作的ValueCtx类型时候向上一直查找 parent = c.Context default: return nil, false } } } 注意parentCancelCtx找到的节点不一定是就是父context有可能是其父辈的context。可以参考下面这种图:\nWithCancel\n接下来看cancelCtx类型Context的创建。WithCancel会创一个cancelCtx以及它关联的取消函数。\ntype CancelFunc func() func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { // 根据父context创建新的cancelCtx类型的context c := newCancelCtx(parent) // 向上递归找到父辈并将新context的canceler添加到父辈的映射中 propagateCancel(parent, \u0026amp;c) return \u0026amp;c, func() { c.cancel(true, Canceled) } } func newCancelCtx(parent Context) cancelCtx { return cancelCtx{Context: parent} } func propagateCancel(parent Context, child canceler) { if parent.Done() == nil { // parent.Done()返回nil表明父Context不支持取消操作 // 大部分情况下该父context已是根context // 该父context是通过context.Background()或者context.ToDo()创建的 return } if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() if p.err != nil { // 父conext已经取消操作过 // 子context立即进行取消操作并传递父级的错误信息 child.cancel(false, p.err) } else { if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} // 将当前context的取消添加到父context中 } p.mu.Unlock() } else { // 如果parent是不可取消的则监控parent和child的Done()通道 go func() { select { case \u0026lt;-parent.Done(): child.cancel(false, parent.Err()) case \u0026lt;-child.Done(): } }() } } timerCtx # timerCtx是基于cancelCtx的context类型它支持过期取消。\ntype timerCtx struct { cancelCtx timer *time.Timer deadline time.Time } func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { return c.deadline, true } func (c *timerCtx) String() string { return contextName(c.cancelCtx.Context) + \u0026#34;.WithDeadline(\u0026#34; + c.deadline.String() + \u0026#34; [\u0026#34; + time.Until(c.deadline).String() + \u0026#34;])\u0026#34; } func (c *timerCtx) cancel(removeFromParent bool, err error) { c.cancelCtx.cancel(false, err) if removeFromParent { // 删除与父辈context的关联 removeChild(c.cancelCtx.Context, c) } c.mu.Lock() if c.timer != nil { // 停止timer并回收 c.timer.Stop() c.timer = nil } c.mu.Unlock() } WithDeadline\nWithDeadline会创建一个timerCtx以及它关联的取消函数\nfunc WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if cur, ok := parent.Deadline(); ok \u0026amp;\u0026amp; cur.Before(d) { // 如果父context过期时间早于当前context过期时间则创建cancelCtx return WithCancel(parent) } c := \u0026amp;timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c) dur := time.Until(d) if dur \u0026lt;= 0 { // 如果新创建的timerCtx正好过期了则取消操作并传递DeadlineExceeded c.cancel(true, DeadlineExceeded) return c, func() { c.cancel(false, Canceled) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { // 创建定时器时间一到执行context取消操作 c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) } } WithTimeout\nWithTimeout用来创建超时就会取消的context内部实现就是WithDealine传递给WithDealine的过期时间就是当前时间加上timeout时间\nfunc WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) } valueCtx # valueCtx是可以传递共享信息的context。\ntype valueCtx struct { Context key, val interface{} } func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { // 当前context存在当前的key return c.val } // 当前context不存在则会沿着context树向上递归查找直到根context如果一直未找到则会返回nil return c.Context.Value(key) } 如果当前context不存在该key则会沿着context树向上递归查找直到查找到根context最后返回nil WithValue\nWithValue用来创建valueCtx。如果key是不可以比较的时候则会发生恐慌。可以比较类型可以参考 Comparison_operators。key应该是不导出变量防止冲突。\nfunc WithValue(parent Context, key, val interface{}) Context { if key == nil { panic(\u0026#34;nil key\u0026#34;) } if !reflectlite.TypeOf(key).Comparable() { panic(\u0026#34;key is not comparable\u0026#34;) } return \u0026amp;valueCtx{parent, key, val} } 总结 # 实现Context接口的类型 # Context一共有4个类型实现了Context接口, 分别是emptyCtx,\tcancelCtx,timerCtx,valueCtx。\n它们的功能与创建方法如下\n类型 创建方法 功能 emptyCtx Background()/TODO() 用做context树的根节点 cancelCtx WithCancel() 可取消的context timerCtx WithDeadline()/WithTimeout() 可取消的context过期或超时会自动取消 valueCtx WithValue() 可存储共享信息的context Context实现两种递归 # Context实现两种方向的递归操作。\n递归方向 目的 向下递归 当对父Context进去手动取消操作或超时取消时候向下递归处理对实现了canceler接口的后代进行取消操作 向上队规 当对Context查询Key信息时候若当前Context没有当前K-V信息时候则向父辈递归查询一直到查询到跟节点的emptyCtx返回nil为止 Context使用规范 # 使用Context的是应该准守以下原则来保证在不同包中使用时候的接口一致性以及能让静态分析工具可以检查context的传播\n不要将Context作为结构体的一个字段存储相反而应该显示传递Context给每一个需要它的函数Context应该作为函数的第一个参数并命名为ctx 不要传递一个nil Context给一个函数即使该函数能够接受它。如果你不确定使用哪一个Context那你就传递context.TODO context是并发安全的相同的Context能够传递给运行在不同goroutine的函数 参考资料 # 深入理解Golang之context Go Concurrency Patterns: Context Using Goroutines, Channels, Contexts, Timers, WaitGroups and Errgroups "},{"id":17,"href":"/concurrency/sync-mutex/","title":"互斥锁 - sync.Mutex","section":"并发编程","content":" 互斥锁 - sync.Mutex # "},{"id":18,"href":"/function/pass-by-value/","title":"值传递","section":"函数","content":" 值传递 # 函数传参有三种方式分别是值传递pass by value)、引用传递pass by reference)以及指针传递pass by pointer)。指针传递也称为地址传递本质上也属于值传递它只不过传递的值是地址而已。所以按照广义的函数传递来分分为值传递和引用传递。Go语言中函数传参值传递不支持引用传递。但是由于切片通道映射等具有引用传递的某些特性往往令人疑惑其应该是引用传递。这个章节我们就来探究下Go语言中函数传递的问题。\n在探究Go语言中函数传递的问题我们先研究C++语言下的引用传递和指针传递是怎么回事。\nC++中指针传递 # #include \u0026lt;stdio.h\u0026gt; void swap(int* a,int *b){ printf(\u0026#34;交换中变量a值%d 地址:%p 变量b值%d地址%p\\n\u0026#34;, *a, \u0026amp;a, *b, \u0026amp;b); int temp = *a; *a = *b; *b = temp; } int main() { int a = 1; int b = 2; printf(\u0026#34;交换前变量a值%d 地址:%p 变量b值%d地址%p\\n\u0026#34;, a, \u0026amp;a, b, \u0026amp;b); swap(\u0026amp;a,\u0026amp;b); printf(\u0026#34;交换后变量a值%d 地址:%p 变量b值%d地址%p\\n\u0026#34;, a, \u0026amp;a, b, \u0026amp;b); return 0; } C++中引用传递 # #include \u0026lt;stdio.h\u0026gt; void swap(int \u0026amp;a, int \u0026amp;b){ printf(\u0026#34;交换中变量a值%d 地址:%p 变量b值%d地址%p\\n\u0026#34;, a, \u0026amp;a, b, \u0026amp;b); int temp = a; a = b; b = temp; } int main() { int a = 1; int b = 2; printf(\u0026#34;交换前变量a值%d 地址:%p 变量b值%d地址%p\\n\u0026#34;, a, \u0026amp;a, b, \u0026amp;b); swap(a,b); printf(\u0026#34;交换后变量a值%d 地址:%p 变量b值%d地址%p\\n\u0026#34;, a, \u0026amp;a, b, \u0026amp;b); return 0; } 进一步阅读 # When are function parameters passed by value? "},{"id":19,"href":"/concurrency/memory-model/","title":"内存模型 - memroy model","section":"并发编程","content":" 内存模型 # Go语言中的内存模型规定了多个goroutine读取变量时候变量的可见性情况。注意本章节的内存模型并不是内存对象分配、管理、回收的模型准确的说这里面的内存模型是内存一致性模型。\nHappens Before原则 # Happens Before原则的定义是如果一个操作e1先于操作e2发生那么我们就说e1 happens before e2也可以描述成e2 happens after e2此时e1操作的变量结果对e2都是可见的。如果e1操作既不先于e2发生又不晚于e2发生我们说e1操作与e2操作并发发生。\nHappens Before具有传导性如果操作e1 happens before 操作e2e3 happends before e1那么e3一定也 happends before e2。\n由于存在指令重排和多核CPU并发访问情况我们代码中变量顺序和实际方法顺序并不总是一致的。考虑下面一种情况\na := 1 b := 2 c := a + 1 上面代码中是先给变量a赋值然后给变量b赋值最后给编程c赋值。但是在底层实现指令时候可能发生指令重排变量b赋值在前变量a赋值在后最后变量c赋值。对于依赖于a变量的c变量的赋值不管怎样指令重排Go语言都会保证变量a赋值操作 happends before c变量赋值操作。\n上面代码运行是运行在同一goroutine中Go语言时能够保证happends before原则的实现正确的变量可见性。但对于多个goroutine共享数据时候Go语言是无法保证Happens Before原则的这时候就需要我们采用锁、通道等同步手段来保证数据一致性。考虑下面场景\nvar a, b int // goroutine A go func() { a = 1 b = 2 }() // goroutine B go func() { if b == 2 { print(a) } }() 当执行goroutine B打印变量a时并不一定打印出来1有可能打印出来的是0。这是因为goroutine A中可能存在指令重排先将b变量赋值2若这时候接着执行goroutine B那么就会打印出来0\nGo语言中保证的 happens-before 场景 # Go语言提供了某些场景下面的happens-before原则保证。详细内容可以阅读文章末尾进一步阅读中提供的Go官方资料。\n初始化 # 当进行包初始化或程序初始化时候会保证下面的happens-before:\n如果包p导入了包q则q的init函数的happens before在任何p的开始之前。 所有init函数happens before 入口函数main.main goroutine # 与goroutine有关的happens-before保证场景有\ngoroutine的创建happens before其执行 goroutine的完成不保证happens-before任何代码 对于第一条场景,考虑下面代码:\nvar a string func f() { print(a) // 3 } func hello() { a = \u0026#34;hello, world\u0026#34; // 1 go f() // 2 } 根据goroutine的创建happens before其执行我们知道操作2 happens before 操作3。又因为在同一goroutine中先书写的代码一定会happens before后面代码注意即使发生了执行重排其并不会影响happends before操作1 happends before 操作3那么操作1 happends before 操作3所以最终一定会打印出hello, world不可能出现打印空字符串情况。\n注意goroutine f()的执行完成并不能保证hello()返回之前其有可能是在hello返回之后执行完成。\n对于第二条场景考虑下面代码\nvar a string func hello() { go func() { a = \u0026#34;hello\u0026#34; }() // 1 print(a) // 2 } 由于goroutine的完成不保证happens-before任何代码那么操作1和操作2无法确定谁先执行谁后执行那么最终可能打印出hello也有可能打印出空字符串。\n通道通信 # 对于缓冲通道向通道发送数据happens-before从通道接收到数据 var c = make(chan int, 10) var a string func f() { a = \u0026#34;hello, world\u0026#34; // 4 c \u0026lt;- 0 // 5 } func main() { go f() // 1 \u0026lt;-c // 2 print(a) // 3 } c是一个缓存通道操作5 happens before 操作2所以最终会打印hello, world\n对于无缓冲通道从通道接收数据happens-before向通道发送数据 var c = make(chan int) var a string func f() { a = \u0026#34;hello, world\u0026#34; // 4 \u0026lt;-c // 5 } func main() { go f() // 1 c \u0026lt;- 0 // 2 print(a) // 3 } c是无缓存通道操作5 happens before 操作2所以最终会打印hello, world。\n对于上面通道的两种happens before场景下打印数据结果我们都可以通过通道特性得出相关结果。\n锁 # 对于任意的sync.Mutex或者sync.RWMutexn次Unlock()调用happens before m次Lock()调用其中n\u0026lt;m var l sync.Mutex var a string func f() { a = \u0026#34;hello, world\u0026#34; l.Unlock() // 2 } func main() { l.Lock() // 1 go f() l.Lock() // 3 print(a) } 操作2 happends before 操作3所以最终一定会打印出来hello,world。\n对于这种情况我们可以从锁的机制方面理解操作3一定会阻塞到操作为2完成释放锁那么最终一定会打印hello, world。\n进一步阅读 # The Go Memory Model "},{"id":20,"href":"/function/","title":"函数","section":"简介","content":" 函数 # 博观而约取,厚积而薄发。\n一等公民 函数调用栈 值传递 闭包 方法 "},{"id":21,"href":"/type/slice/","title":"切片","section":"数据类型与数据结构","content":" 切片 # 切片是Go语言中最常用的数据类型之一它类似数组但相比数组它更加灵活高效由于它本身的特性往往也更容易用错。\n不同于数组是值类型而切片是引用类型。虽然两者作为函数参数传递时候都是值传递pass by value但是切片传递的包含数据指针可以细分为pass by pointer如果切片使用不当会产生意想不到的副作用。\n初始化 # 切片的初始化方式可以分为三种:\n使用make函数创建切片\nmake函数语法格式为make([]T, length, capacity)capacity可以省略默认等于length\n使用字面量创建切片\n从数组或者切片派生(reslice)出新切片\nGo支持从数组、指向数组的指针、切片类型变量再reslice一个新切片。\nreslice操作语法可以是[]T[low : high],也可以是[]T[low : high : max]。其中low,high,max都可以省略low默认值是0high默认值cap([]T)max默认值cap([]T)。low,hight,max取值范围是0 \u0026lt;= low \u0026lt;= high \u0026lt;= max \u0026lt;= cap([]T)其中high-low是新切片的长度max-low是新切片的容量。\n对于[]T[low : high],其包含的元素是[]T中下标low开始到high结束不含high所在位置的相当于左闭右开[low, high)的元素元素个数是high - low个容量是cap([]T) - low。\nfunc main() { slice1 := make([]int, 0) slice2 := make([]int, 1, 3) slice3 := []int{} slice4 := []int{1: 2, 3} arr := []int{1, 2, 3} slice5 := arr[1:2] slice6 := arr[1:2:2] slice7 := arr[1:] slice8 := arr[:1] slice9 := arr[3:] slice10 := slice2[1:2] fmt.Printf(\u0026#34;%s = %v,\\t len = %d, cap = %d\\n\u0026#34;, \u0026#34;slice1\u0026#34;, slice1, len(slice1), cap(slice1)) fmt.Printf(\u0026#34;%s = %v,\\t len = %d, cap = %d\\n\u0026#34;, \u0026#34;slice2\u0026#34;, slice2, len(slice2), cap(slice2)) fmt.Printf(\u0026#34;%s = %v,\\t len = %d, cap = %d\\n\u0026#34;, \u0026#34;slice3\u0026#34;, slice3, len(slice3), cap(slice3)) fmt.Printf(\u0026#34;%s = %v,\\t len = %d, cap = %d\\n\u0026#34;, \u0026#34;slice4\u0026#34;, slice4, len(slice4), cap(slice4)) fmt.Printf(\u0026#34;%s = %v,\\t len = %d, cap = %d\\n\u0026#34;, \u0026#34;slice5\u0026#34;, slice5, len(slice5), cap(slice5)) fmt.Printf(\u0026#34;%s = %v,\\t len = %d, cap = %d\\n\u0026#34;, \u0026#34;slice6\u0026#34;, slice6, len(slice6), cap(slice6)) fmt.Printf(\u0026#34;%s = %v,\\t len = %d, cap = %d\\n\u0026#34;, \u0026#34;slice7\u0026#34;, slice7, len(slice7), cap(slice7)) fmt.Printf(\u0026#34;%s = %v,\\t len = %d, cap = %d\\n\u0026#34;, \u0026#34;slice8\u0026#34;, slice8, len(slice8), cap(slice8)) fmt.Printf(\u0026#34;%s = %v,\\t len = %d, cap = %d\\n\u0026#34;, \u0026#34;slice9\u0026#34;, slice9, len(slice9), cap(slice9)) fmt.Printf(\u0026#34;%s = %v,\\t len = %d, cap = %d\\n\u0026#34;, \u0026#34;slice10\u0026#34;, slice10, len(slice10), cap(slice10)) } 上面代码输出一下内容:\nslice1 = [],\tlen = 0, cap = 0 slice2 = [0],\tlen = 1, cap = 3 slice3 = [],\tlen = 0, cap = 0 slice4 = [0 2 3],\tlen = 3, cap = 3 slice5 = [2],\tlen = 1, cap = 2 slice6 = [2],\tlen = 1, cap = 1 slice7 = [2 3],\tlen = 2, cap = 2 slice8 = [1],\tlen = 1, cap = 3 slice9 = [],\tlen = 0, cap = 0 slice10 = [0],\tlen = 1, cap = 2 注意:\n我们使用arr[3]访问切片元素时候会报 index out of range [3] with length 错误而使用arr[3:]来初始化slice9却是可以的。因为这是Go语言故意为之的。具体原因可以参见 Why slice not painc 这个issue。\n接下来我们来看看切片的底层数据结构。\n数据结构 # Go语言中切片的底层数据结构是 runtime.slice runtime/slice.go其中包含了指向数据数组的指针切片长度以及切片容量\ntype slice struct { array unsafe.Pointer // 底层数据数组的指针 len int // 切片长度 cap int // 切片容量 } 注意:\n切片底层数据结构也可以说成是 reflect.SliceHeader两者没有冲突。reflect.SliceHeader 是暴露出来的类型,可以被用户程序代码直接使用。\n我们来看看下面切片如何共用同一个底层数组的\nfunc main() { a := []byte{\u0026#39;h\u0026#39;, \u0026#39;e\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;o\u0026#39;} b := a[2:3] c := a[2:3:3] fmt.Println(string(a), string(b), string(c)) // 输出 hello l l } Go语言切片底层结构示意图 在前面 《 基础篇-字符串 》 章节,我们使用了 GDB 工具验证了字符串的数据结构,这一次我们使用另外一种方式验证切片的数据结构。我们通过打印切片的底层结构信息来验证:\nfunc main() { type sliceHeader struct { array unsafe.Pointer // 底层数据数组的指针 len int // 切片长度 cap int // 切片容量 } a := []byte{\u0026#39;h\u0026#39;, \u0026#39;e\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;o\u0026#39;} b := a[2:3] c := a[2:3:3] ptrA := (*sliceHeader)(unsafe.Pointer(\u0026amp;a)) ptrB := (*sliceHeader)(unsafe.Pointer(\u0026amp;b)) ptrC := (*sliceHeader)(unsafe.Pointer(\u0026amp;c)) fmt.Printf(\u0026#34;切片%s: 底层数组地址=0x%x, 长度=%d, 容量=%d\\n\u0026#34;, \u0026#34;a\u0026#34;, ptrA.array, ptrA.len, ptrA.cap) fmt.Printf(\u0026#34;切片%s: 底层数组地址=0x%x, 长度=%d, 容量=%d\\n\u0026#34;, \u0026#34;b\u0026#34;, ptrB.array, ptrB.len, ptrB.cap) fmt.Printf(\u0026#34;切片%s: 底层数组地址=0x%x, 长度=%d, 容量=%d\\n\u0026#34;, \u0026#34;c\u0026#34;, ptrC.array, ptrC.len, ptrC.cap) } 上面代码输出以下内容:\n切片a: 底层数组地址=0xc00009400b, 长度=5, 容量=5 切片b: 底层数组地址=0xc00009400d, 长度=1, 容量=3 切片c: 底层数组地址=0xc00009400d, 长度=1, 容量=1 从输出内容可以看到切片变量 b和 c 都指向同一个底层数组地址 0xc00009400d它们与切片变量 a 指向的底层数组地址 0xc00009400b 恰好相差2个字节这两个字节大小的内存空间存在的是 h 和 e 字符。\n副作用 # 由于切片底层结构的特殊性,当我们使用切片的时候需要特别留心,防止产生副作用(side effect)。\n示例1append操作产生副作用 # func main() { slice1 := []byte{\u0026#39;h\u0026#39;, \u0026#39;e\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;o\u0026#39;} slice2 := slice1[2:3] slice2 = append(slice2, \u0026#39;g\u0026#39;) fmt.Println(string(slice2)) // lg fmt.Println(string(slice1)) // 输出helgeslice1的值也变了。 } 上面代码本意是将切片slice2追加g字符却产生副作用即也修改了slice1的值\nGo语言append切片时产生副作用 解决append产生的副作用 # 解决由于append产生的副作用有两种解决办法\nreslice时候指定max边界 使用copy函数拷贝出一个副本 reslice时候指定max边界 # func main() { slice1 := []byte{\u0026#39;h\u0026#39;, \u0026#39;e\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;o\u0026#39;} slice2 := slice1[2:3:3] slice2 = append(slice2, \u0026#39;g\u0026#39;) // 此时slice2容量扩大到8 fmt.Println(string(slice2)) // 输出lg fmt.Println(string(slice1)) // 输出hello } 通过slice2 := slice1[2:3:3] 方式进行reslice之后slice2的长度和容量一样若对slice2再进行append操作其一定会发送扩容操作此后slice2和slice1之间就没有任何关系了。\nreslice时候指定max边界 使用copy函数拷贝出一个副本 # func main() { slice1 := []byte{\u0026#39;h\u0026#39;, \u0026#39;e\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;o\u0026#39;} slice2 := make([]byte, 1) copy(slice2, slice1[2:3]) slice2 = append(slice2, \u0026#39;g\u0026#39;) fmt.Println(string(slice2)) // 输出lg fmt.Println(string(slice1)) // 输出hello } 示例2指针类型变量引用切片产生副作用 # type User struct { Likes int } func main() { users := make([]User, 1) pFirstUser := \u0026amp;users[0] pFirstUser.Likes++ fmt.Println(\u0026#34;所有用户:\u0026#34;) for i := range users { fmt.Printf(\u0026#34;User: %d Likes: %d\\n\\n\u0026#34;, i, users[i].Likes) } users = append(users, User{}) // 添加一个新用户到集合中 pFirstUser.Likes++ // 第一个用户的Likes次数加一 fmt.Println(\u0026#34;所有用户:\u0026#34;) for i := range users { fmt.Printf(\u0026#34;User: %d Likes: %d\\n\u0026#34;, i, users[i].Likes) } } 指向上面代码输出以下内容:\n所有用户 User: 0 Likes: 1 所有用户: User: 0 Likes: 1 User: 1 Likes: 0 代码本意是通过User类型指针变量pUsers进行第一个用户Likes更新操作没想到切片进行append之后产生了副作用pUsers指向切片已经与切片变量users不一样了。\n引用切片变量产生副作用 避免切片副作用黄金法则 # 在边界处拷贝切片,这里面的边界指的是函数接受切片参数或返回切片的时候。 永远不要使用一个变量来引用切片数据 扩容策略 # 当对切片进行append操作时候若切片容量不够时候会进行扩容处理。当切片进行扩容时候会先调用runtime.growslice函数该函数返回一个新的slice底层结构体该结构体array字段指向新的底层数组地址cap字段是新切片的容量len字段是旧切片的长度旧切片的内容会拷贝到新切片中最后再把要追加的数据复制到新切片中并更新切片len长度。\n// et是slice元素类型 // old是旧的slice // cap是新slice最低要求容量大小。是旧的slice的长度加上append函数中追加的元素的个数 // 比如s := []int{1, 2, 3}s = append(s, 4, 5); 此时growslice中的cap参数值为5 func growslice(et *_type, old slice, cap int) slice { if cap \u0026lt; old.cap { panic(errorString(\u0026#34;growslice: cap out of range\u0026#34;)) } if et.size == 0 { return slice{unsafe.Pointer(\u0026amp;zerobase), old.len, cap} } newcap := old.cap doublecap := newcap + newcap if cap \u0026gt; doublecap { // 最小cap要求大于旧slice的cap两倍大小 newcap = cap } else { if old.len \u0026lt; 1024 { // 当旧slice的len小于1024, 扩容一倍 newcap = doublecap } else { // 否则每次扩容25% for 0 \u0026lt; newcap \u0026amp;\u0026amp; newcap \u0026lt; cap { newcap += newcap / 4 } if newcap \u0026lt;= 0 { newcap = cap } } } var overflow bool var lenmem, newlenmem, capmem uintptr switch { case et.size == 1: // 元素大小 lenmem = uintptr(old.len) newlenmem = uintptr(cap) capmem = roundupsize(uintptr(newcap)) overflow = uintptr(newcap) \u0026gt; maxAlloc newcap = int(capmem) // 调整newcap大小 case et.size == sys.PtrSize: lenmem = uintptr(old.len) * sys.PtrSize newlenmem = uintptr(cap) * sys.PtrSize capmem = roundupsize(uintptr(newcap) * sys.PtrSize) overflow = uintptr(newcap) \u0026gt; maxAlloc/sys.PtrSize newcap = int(capmem / sys.PtrSize) case isPowerOfTwo(et.size): var shift uintptr if sys.PtrSize == 8 { // Mask shift for better code generation. shift = uintptr(sys.Ctz64(uint64(et.size))) \u0026amp; 63 } else { shift = uintptr(sys.Ctz32(uint32(et.size))) \u0026amp; 31 } lenmem = uintptr(old.len) \u0026lt;\u0026lt; shift newlenmem = uintptr(cap) \u0026lt;\u0026lt; shift capmem = roundupsize(uintptr(newcap) \u0026lt;\u0026lt; shift) overflow = uintptr(newcap) \u0026gt; (maxAlloc \u0026gt;\u0026gt; shift) newcap = int(capmem \u0026gt;\u0026gt; shift) default: lenmem = uintptr(old.len) * et.size newlenmem = uintptr(cap) * et.size capmem, overflow = math.MulUintptr(et.size, uintptr(newcap)) capmem = roundupsize(capmem) newcap = int(capmem / et.size) } if overflow || capmem \u0026gt; maxAlloc { panic(errorString(\u0026#34;growslice: cap out of range\u0026#34;)) } var p unsafe.Pointer if et.ptrdata == 0 { // 切片元素中没有指针类型数据,不用考虑写屏障问题 p = mallocgc(capmem, nil, false) memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem) } else { p = mallocgc(capmem, et, true) if lenmem \u0026gt; 0 \u0026amp;\u0026amp; writeBarrier.enabled { bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem) } } // 涉及到slice扩容都会有内存移动操作 memmove(p, old.array, lenmem) return slice{p, old.len, newcap} } 从上面代码中可以总结出切片扩容的策略是:\n若切片容量小于1024会扩容一倍 若切片容量大于等于1024会扩容1/4大小由于考虑内存对齐最终实际扩容大小可能会大于1/4 从上面代码中可以看到,切片进行扩容时一定会进行内存拷贝,这是成本较大操作。所以切片一大优化点就是在使用之前尽量指定好切片所需容量,避免出现扩容情况。\nstring类型与[]byte类型如何实现zero-copy互相转换 # 什么是零拷贝(zero-copy) # 零拷贝(zero-copy) 指的是CPU不需要先将数据从某处内存复制到另一个特定区域。当应用程序读取文件需要从磁盘中加载内核区域然后将内核区域内容复制到应用内存区域这就涉及到内存拷贝。若采用mmap技术可以文件映射到特定内存中只需加载一次应用程序和内核都可以共享内存中文件数据这就实现了zero-copy。或者当应用程序需要发送文件给远程时候可以采用sendfile技术实现零拷贝若未实现零拷贝则需要进行四次拷贝过程:\n磁盘\u0026mdash;DMA copy\u0026ndash;\u0026gt; 系统内核 \u0026ndash;\u0026gt; 应用程序区域 \u0026ndash;\u0026gt; 系统内核(socket) \u0026mdash;DMA copy\u0026mdash;\u0026gt; 网卡\n使用[]byte(string) 和 string([]byte)方式进行字符串和字节切片互转时候会不会发生内存拷贝? # package main func byteArrayToString(b []byte) string { return string(b) } func stringToByteArray(s string) []byte { return []byte(s) } func main() { } 我们来看下上面代码中的底层实现\ngo tool compile -N -l -S main.go 执行上面命名,输出以下内容:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 \u0026#34;\u0026#34;.byteArrayToString STEXT size=117 args=0x28 locals=0x38 0x0000 00000 (main.go:3)\tTEXT\t\u0026#34;\u0026#34;.byteArrayToString(SB), ABIInternal, $56-40 0x0000 00000 (main.go:3)\tMOVQ\t(TLS), CX 0x0009 00009 (main.go:3)\tCMPQ\tSP, 16(CX) 0x000d 00013 (main.go:3)\tPCDATA\t$0, $-2 0x000d 00013 (main.go:3)\tJLS\t110 0x000f 00015 (main.go:3)\tPCDATA\t$0, $-1 0x000f 00015 (main.go:3)\tSUBQ\t$56, SP 0x0013 00019 (main.go:3)\tMOVQ\tBP, 48(SP) 0x0018 00024 (main.go:3)\tLEAQ\t48(SP), BP ... 0x003c 00060 (main.go:4)\tMOVQ\tAX, 8(SP) 0x0041 00065 (main.go:4)\tMOVQ\tCX, 16(SP) 0x0046 00070 (main.go:4)\tMOVQ\tDX, 24(SP) 0x004b 00075 (main.go:4)\tCALL\truntime.slicebytetostring(SB) 0x0050 00080 (main.go:4)\tMOVQ\t40(SP), AX .... \u0026#34;\u0026#34;.stringToByteArray STEXT size=144 args=0x28 locals=0x50 0x0000 00000 (main.go:7)\tTEXT\t\u0026#34;\u0026#34;.stringToByteArray(SB), ABIInternal, $80-40 0x0000 00000 (main.go:7)\tMOVQ\t(TLS), CX 0x0009 00009 (main.go:7)\tCMPQ\tSP, 16(CX) ... 0x0040 00064 (main.go:8)\tMOVQ\tAX, 8(SP) 0x0045 00069 (main.go:8)\tMOVQ\tCX, 16(SP) 0x004a 00074 (main.go:8)\tCALL\truntime.stringtoslicebyte(SB) 0x004f 00079 (main.go:8)\tMOVQ\t32(SP), AX 0x0054 00084 (main.go:8)\tMOVQ\t40(SP), CX .... 从上面汇编代码可以看到 string([]byte) 底层调用的是 runtime.slicebytetostring[]byte(string) 底层调用的是 runtime.stringtoslicebyte。查看这两个底层函数实现可以看到两者都是先创建一段内存空间然后使用 memmove 函数拷贝内存,将数据拷贝到新内存空间。这也就是说 []byte(string) 和 string([]byte) 进行转换时候需要内存拷贝。\nstring类型与[]byte类型 zero-copy转换实现 # 那么能不能实现不需要内存拷贝的字符串和字节切片的转换呢?答案是可以的。\n根据前面 《 基础篇-字符串 》 章节和本章节我们可以看到字符串和字节切片底层结构很相似它们相同部分都有指向底层数据指针和记录底层数据长度len字段而字节切片额外多了一个字段cap记录底层数据的容量。我们只要转换时候让它们共享底层数据就能实现zero-copy。让我们再看看字符串和切片的数组结构\ntype StringHeader struct { Data uintptr Len int } type SliceHeader struct { Data uintptr Len int Cap int } 我们来看下网上比较常见zero-copy的实现方式它是有bug的\nfunc string2bytes(s string) []byte { return *(*[]byte)(unsafe.Pointer(\u0026amp;s)) } func bytes2string(b []byte) string{ return *(*string)(unsafe.Pointer(\u0026amp;b)) } 我们来测试一下:\nfunc main() { a := \u0026#34;hello\u0026#34; b := string2bytes(a) fmt.Println(string(b), len(b), cap(b)) } 上面代码输出以下内容:\nhello 5 824634122328 从上面输入内容,我们可以看到字符串转换成字节切片后的容量明显是有问题的。让我们来分析下具体原因。\n上面两个函数借助 非安全指针类型 强制转换类型实现的。对于字节切片转换字符串使用这种方式是可以的字节切片多余的cap字段会自动溢出掉而反过来由于字符串没有记录容量字段那么将其强制转换成字节切片时候字节切片的cap字段是未知的这有可能导致非常严重问题。所以将字符串转换成字节切片时候需要保证字节切片的cap设置正确。\n正确的字符串转字节切片实现如下\nfunc StringToBytes(s string) (b []byte) { sh := *(*reflect.StringHeader)(unsafe.Pointer(\u0026amp;s)) bh := (*reflect.SliceHeader)(unsafe.Pointer(\u0026amp;b)) bh.Data, bh.Len, bh.Cap = sh.Data, sh.Len, sh.Len return b } 或者\nfunc StringToBytes(s string) []byte { return *(*[]byte)(unsafe.Pointer( \u0026amp;struct { string Cap int }{s, len(s)}, )) } 进一步阅读 # The Go Programming Language Specification: Slice expressions Uber Go Style Guide: Copy Slices and Maps at Boundaries "},{"id":22,"href":"/concurrency/atomic/","title":"原子操作-atomic","section":"并发编程","content":" 原子操作 - atomic # atomic是Go内置原子操作包。下面是官方说明\nPackage atomic provides low-level atomic memory primitives useful for implementing synchronization algorithms. atomic包提供了用于实现同步机制的底层原子内存原语。\nThese functions require great care to be used correctly. Except for special, low-level applications, synchronization is better done with channels or the facilities of the sync package. Share memory by communicating; don\u0026rsquo;t communicate by sharing memory. 使用这些功能需要非常小心。除了特殊的底层应用程序外最好使用通道或sync包来进行同步。通过通信来共享内存不要通过共享内存来通信。\natomic包提供的操作可以分为三类\n对整数类型T的操作 # T类型是int32、int64、uint32、uint64、uintptr其中一种。\nfunc AddT(addr *T, delta T) (new T) func CompareAndSwapT(addr *T, old, new T) (swapped bool) func LoadT(addr *T) (val T) func StoreT(addr *T, val T) func SwapT(addr *T, new T) (old T) 对于unsafe.Pointer类型的操作 # func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool) func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer) atomic.Value类型提供Load/Store操作 # atomic提供了atomic.Value类型用来原子性加载和存储类型一致的值consistently typed value。atomic.Value提供了对任何类型的原则性操作。\nfunc (v *Value) Load() (x interface{}) // 原子性返回刚刚存储的值若没有值返回nil func (v *Value) Store(x interface{}) // 原子性存储值xx可以是nil但需要每次存的值都必须是同一个具体类型。 用法 # 用法示例1原子性增加值 # package main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;sync/atomic\u0026#34; ) func main() { var count int32 var wg sync.WaitGroup for i := 0; i \u0026lt; 10; i++ { wg.Add(1) go func() { atomic.AddInt32(\u0026amp;count, 1) // 原子性增加值 wg.Done() }() go func() { fmt.Println(atomic.LoadInt32(\u0026amp;count)) // 原子性加载 }() } wg.Wait() fmt.Println(\u0026#34;count: \u0026#34;, count) } 用法示例2简易自旋锁实现 # package main import ( \u0026#34;sync/atomic\u0026#34; ) type spin int64 func (l *spin) lock() bool { for { if atomic.CompareAndSwapInt64((*int64)(l), 0, 1) { return true } continue } } func (l *spin) unlock() bool { for { if atomic.CompareAndSwapInt64((*int64)(l), 1, 0) { return true } continue } } func main() { s := new(spin) for i := 0; i \u0026lt; 5; i++ { s.lock() go func(i int) { println(i) s.unlock() }(i) } for { } } 用法示例3 无符号整数减法操作 # 对于Uint32和Uint64类型Add方法第二个参数只能接受相应的无符号整数atomic包没有提供减法SubstractT操作\nfunc AddUint32(addr *uint32, delta uint32) (new uint32) func AddUint64(addr *uint64, delta uint64) (new uint64) 对于无符号整数V我们可以传递-V给AddT方法第二个参数就可以实现减法操作。\npackage main import ( \u0026#34;sync/atomic\u0026#34; ) func main() { var i uint64 = 100 var j uint64 = 10 var k = 5 atomic.AddUint64(\u0026amp;i, -j) println(i) atomic.AddUint64(\u0026amp;i, -uint64(k)) println(i) // 下面这种操作是不可以的会发生恐慌constant -5 overflows uint64 // atomic.AddUint64(\u0026amp;i, -uint64(5)) } 源码分析 # atomic包提供的三类操作的前两种都是直接通过汇编源码实现的 sync/atomic/asm.s):\n#include \u0026#34;textflag.h\u0026#34; TEXT ·SwapInt32(SB),NOSPLIT,$0 JMP\truntimeinternalatomic·Xchg(SB) TEXT ·SwapUint32(SB),NOSPLIT,$0 JMP\truntimeinternalatomic·Xchg(SB) ... TEXT ·StoreUintptr(SB),NOSPLIT,$0 JMP\truntimeinternalatomic·Storeuintptr(SB) 从上面汇编代码可以看出来atomic操作通过JMP操作跳到runtime/internal/atomic目录下面的汇编实现。我们把目标转移到runtime/internal/atomic目录下面。\n该目录包含针对不同平台的atomic汇编实现asm_xxx.s。这里面我们只关注amd64平台asm_amd64.s( runtime/internal/atomic/asm_amd64.s)和atomic_amd64.go( runtime/internal/atomic/atomic_amd64.go)。\n函数 底层实现 SwapInt32 / SwapUint32 runtimeinternalatomic·Xchg SwapInt64 / SwapUint64 / SwapUintptr runtimeinternalatomic·Xchg64 CompareAndSwapInt32 / CompareAndSwapUint32 runtimeinternalatomic·Cas CompareAndSwapUintptr / CompareAndSwapInt64 / CompareAndSwapUint64 runtimeinternalatomic·Cas64 AddInt32 / AddUint32 runtimeinternalatomic·Xadd AddUintptr / AddInt64 / AddUint64 runtimeinternalatomic·Xadd64 LoadInt32 / LoadUint32 runtimeinternalatomic·Load LoadInt64 / LoadUint64 / LoadUint64/ LoadUintptr runtimeinternalatomic·Load64 LoadPointer runtimeinternalatomic·Loadp StoreInt32 / StoreUint32 runtimeinternalatomic·Store StoreInt64 / StoreUint64 / StoreUintptr runtimeinternalatomic·Store64 Add操作 # AddUintptr 、 AddInt64 以及 AddUint64都是由方法runtimeinternalatomic·Xadd64实现:\nTEXT runtimeinternalatomic·Xadd64(SB), NOSPLIT, $0-24 MOVQ\tptr+0(FP), BX // 第一个参数保存到BX MOVQ\tdelta+8(FP), AX // 第二个参数保存到AX MOVQ\tAX, CX // 将第二个参数临时存到CX寄存器中 LOCK\t// LOCK指令进行锁住操作实现对共享内存独占访问 XADDQ\tAX, 0(BX) // xaddq指令实现寄存器AX的值与BX指向的内存存的值互换 // 并将这两个值的和存在BX指向的内存中此时AX寄存器存的是第一个参数指向的值 ADDQ\tCX, AX // 此时AX寄存器的值是Add操作之后的值和0(BX)值一样 MOVQ\tAX, ret+16(FP) # 返回值 RET LOCK指令是一个指令前缀其后是读-写性质的指令在多处理器环境中LOCK指令能够确保在执行LOCK随后的指令时处理器拥有对数据的独占使用。若对应数据已经在cache line里也就不用锁定总线仅锁住缓存行即可否则需要锁住总线来保证独占性。\nXADDQ指令用于交换加操作会将源操作数与目的操作数互换并将两者的和保存到源操作数中。\nAddInt32 、 AddUint32 都是由方法runtimeinternalatomic·Xadd实现实现逻辑和runtimeinternalatomic·Xadd64一样只是Xadd中相关数据操作指令后缀是L\nTEXT runtimeinternalatomic·Xadd(SB), NOSPLIT, $0-20 MOVQ\tptr+0(FP), BX // 注意第一个参数是一个指针类型是64位所以还是MOVQ指令 MOVL\tdelta+8(FP), AX // 第二个参数32位的所以是MOVL指令 MOVL\tAX, CX LOCK XADDL\tAX, 0(BX) ADDL\tCX, AX MOVL\tAX, ret+16(FP) RET Store操作 # StoreInt64、StoreUint64、StoreUintptr三个是runtimeinternalatomic·Store64方法实现:\nTEXT runtimeinternalatomic·Store64(SB), NOSPLIT, $0-16 MOVQ\tptr+0(FP), BX // 第一个参数保存到BX MOVQ\tval+8(FP), AX // 第二个参数保存到AX XCHGQ\tAX, 0(BX) // 将AX寄存器与BX寄存指向内存的值互换 // 那么第一个参数指向的内存存的值为第二个参数 RET XCHGQ指令是交换指令用于交换源操作数和目的操作数。\nStoreInt32、StoreUint32是由runtimeinternalatomic·Store方法实现与runtimeinternalatomic·Store64逻辑一样这里不在赘述。\nCompareAndSwap操作 # CompareAndSwapUintptr、CompareAndSwapInt64和CompareAndSwapUint64都是由runtimeinternalatomic·Cas64实现\nTEXT runtimeinternalatomic·Cas64(SB), NOSPLIT, $0-25 MOVQ\tptr+0(FP), BX // 将第一个参数保存到BX MOVQ\told+8(FP), AX // 将第二个参数保存到AX MOVQ\tnew+16(FP), CX // 将第三个参数保存CX LOCK\t// LOCK指令进行上锁操作 CMPXCHGQ\tCX, 0(BX) // BX寄存器指向的内存的值与AX寄存器值进行比较若相等则把CX寄存器值存储到BX寄存器指向的内存中 SETEQ\tret+24(FP) RET CMPXCHGQ指令是比较并交换指令它的用法是将目的操作数和累加寄存器AX进行比较若相等则将源操作数复制到目的操作数中否则将目的操作复制到累加寄存器中。\nSwap操作 # SwapInt64、SwapUint64、SwapUintptr实现的方法是runtimeinternalatomic·Xchg64SwapInt32和SwapUint32底层实现是runtimeinternalatomic·Xchg这里面只分析64的操作\nTEXT runtimeinternalatomic·Xchg64(SB), NOSPLIT, $0-24 MOVQ\tptr+0(FP), BX // 第一个参数保存到BX MOVQ\tnew+8(FP), AX // 第一个参数保存到AX中 XCHGQ\tAX, 0(BX) // XCHGQ指令交互AX值到0(BX)中 MOVQ\tAX, ret+16(FP) // 将旧值返回 RET Load操作 # LoadInt32、LoadUint32、LoadInt64 、 LoadUint64 、 LoadUint64、 LoadUintptr、LoadPointer实现都是Go实现的\n//go:linkname Load //go:linkname Loadp //go:linkname Load64 //go:nosplit //go:noinline func Load(ptr *uint32) uint32 { return *ptr } //go:nosplit //go:noinline func Loadp(ptr unsafe.Pointer) unsafe.Pointer { return *(*unsafe.Pointer)(ptr) } //go:nosplit //go:noinline func Load64(ptr *uint64) uint64 { return *ptr } 最后我们来分析atomic.Value类型提供Load/Store操作。\natomic.Value类型的Load/Store操作 # atomic.Value类型定义如下\ntype Value struct { v interface{} } // ifaceWords是空接口底层表示 type ifaceWords struct { typ unsafe.Pointer data unsafe.Pointer } atomic.Value底层存储的是空接口类型空接口底层结构如下\ntype eface struct { _type *_type // 空接口持有的类型 data unsafe.Pointer // 指向空接口持有类型变量的指针 } atomic.Value内存布局如下所示\n从上图可以看出来atomic.Value内部分为两部分第一个部分是_type类型指针第二个部分是unsafe.Pointer类型两个部分大小都是8字节64系统下。我们可以通过以下代码进行测试\ntype Value struct { v interface{} } type ifaceWords struct { typ unsafe.Pointer data unsafe.Pointer } func main() { func main() { val := Value{v: 123456} t := (*ifaceWords)(unsafe.Pointer(\u0026amp;val)) dp := (*t).data // dp是非安全指针类型变量 fmt.Println(*((*int)(dp))) // 输出123456 var val2 Value t = (*ifaceWords)(unsafe.Pointer(\u0026amp;val2)) fmt.Println(t.typ) // 输出nil } 接下来我们看下Store方法\nfunc (v *Value) Store(x interface{}) { if x == nil { // atomic.Value类型变量不能是nil panic(\u0026#34;sync/atomic: store of nil value into Value\u0026#34;) } vp := (*ifaceWords)(unsafe.Pointer(v)) // 将指向atomic.Value类型指针转换成*ifaceWords类型 xp := (*ifaceWords)(unsafe.Pointer(\u0026amp;x)) // xp是*faceWords类型指针指向传入参数x for { typ := LoadPointer(\u0026amp;vp.typ) // 原子性返回vp.typ if typ == nil { // 第一次调用Store时候atomic.Value底层结构体第一部分是nil // 我们可以从上面测试代码可以看出来 runtime_procPin() // pin process处理防止M被抢占 if !CompareAndSwapPointer(\u0026amp;vp.typ, nil, unsafe.Pointer(^uintptr(0))) { // 通过cas操作将atomic.Value的第一部分存储为unsafe.Pointer(^uintptr(0)),若没操作成功,继续操作 runtime_procUnpin() // unpin process处理释放对当前M的锁定 continue } // vp.data == xp.data // vp.typ == xp.typ StorePointer(\u0026amp;vp.data, xp.data) StorePointer(\u0026amp;vp.typ, xp.typ) runtime_procUnpin() return } if uintptr(typ) == ^uintptr(0) { // 此时说明第一次的Store操作未完成正在处理中此时其他的Store等待第一次操作完成 continue } if typ != xp.typ { // 再次Store操作时进行typ类型校验确保每次Store数据对象都必须是同一类型 panic(\u0026#34;sync/atomic: store of inconsistently typed value into Value\u0026#34;) } StorePointer(\u0026amp;vp.data, xp.data) // vp.data == xp.data return } } 总结上面Store流程\n每次调用Store方法时候会将传入参数转换成interface{}类型。当第一次调用Store方法时候分两部分操作分别将传入参数空接口类型的_typ和data存储到Value类型中。 当再次调用Store类型时候进行传入参数空接口类型的_type和Value的_type比较若不一致直接panic若一致则将data存储到Value类型中 从流程2可以看出来每次调用Store方法时传入参数都必须是同一类型的变量。当Store完成之后实现了“鸠占鹊巢”atomic.Value底层存储的实际上是(interface{})x。\n最后我们看看atomic.Value的Load操作\nfunc (v *Value) Load() (x interface{}) { vp := (*ifaceWords)(unsafe.Pointer(v)) // 将指向v指针转换成*ifaceWords类型 typ := LoadPointer(\u0026amp;vp.typ) if typ == nil || uintptr(typ) == ^uintptr(0) { // typ == nil 说明Store方法未调用过 // uintptr(typ) == ^uintptr(0) 说明第一Store方法调用正在进行中 return nil } data := LoadPointer(\u0026amp;vp.data) xp := (*ifaceWords)(unsafe.Pointer(\u0026amp;x)) xp.typ = typ xp.data = data return } "},{"id":23,"href":"/type-system/reflect/","title":"反射","section":"类型系统","content":" 反射 # "},{"id":24,"href":"/type/string/","title":"字符串","section":"数据类型与数据结构","content":" 字符串 # 我们知道C语言中的字符串是使用字符数组 char[] 表示,字符数组的最后一位元素是 \\0用来标记字符串的结束。C语言中字符串的结构简单但获取字符串长度时候需要遍历字符数组才能完成。\nGo语言中字符串的底层结构中也包含了字符数组该字符数组是完整的字符串内容它不同于C语言字符数组中没有标记字符串结束的标记。为了记录底层字符数组的大小Go语言使用了额外的一个长度字段来记录该字符数组的大小字符数组的大小也就是字符串的长度。\n数据结构 # Go语言字符串的底层数据结构是 reflect.StringHeader( reflect/value.go),它包含了指向字节数组的指针,以及该指针指向的字符数组的大小:\ntype StringHeader struct { Data uintptr Len int } 字符串复制 # 当将一个字符串变量赋值给另外一个变量时候,他们 StringHeader.Data 都指向同一个内存地址,不会发生字符串拷贝:\na := \u0026#34;hello\u0026#34; b := a 从上图中我们可以看到a变量和b变量的Data字段存储的都是0x1234而0x1234是字符数组的起始地址。\n接来下我们借助 GDB 工具来验证Go语言中字符串数据结构是不是按照上面说的那样。\npackage main import ( \u0026#34;fmt\u0026#34; ) func main() { a := \u0026#34;hello\u0026#34; b := a fmt.Printf(\u0026#34;a变量地址%p\\n\u0026#34;, \u0026amp;a) fmt.Printf(\u0026#34;b变量地址%p\\n\u0026#34;, \u0026amp;b) print(\u0026#34;断点打在这里\u0026#34;) } 将上面代码构建二进制应用, 然后使用 GDB 调试一下:\ngo build -o string string.go # 构建二进制应用 gdb ./string # GDB调试 调试流程如下:\nlen(str) == 0 和 str == \u0026quot;\u0026quot;有区别吗? # 判断一个字符串是否是空字符串我们既可以使用len判断其长度是0也可以判断其是否等于空字符串 \u0026quot;\u0026quot;。那么它们有什么区别吗?这个问题的答案是二者没有区别。因为他们底层实现是一样的。\n让我们来探究一下。源代码如下\npackage main func isEmptyStr(str string) bool { return len(str) == 0 } func isEmtpyStr2(str string) bool { return str == \u0026#34;\u0026#34; } func main() { } 接下来我们来查看下上面代码的底层汇编:\ngo tool compile -S empty_string.go # 查看底层汇编代码 从下图中,我们可以发现两种方式的实现是一样的:\n注意\n当我们编译时候开启了禁止内联禁止优化时候可以发现 len(str) == 0 和 str == \u0026quot;\u0026quot; 的实现是不同的前者的执行效率是不如后者的。在默认情况下Go编译器是开启了优化选项的len(str) == 0 会优化成跟 str == \u0026quot;\u0026quot; 的实现一样。 [3]string类型的变量占用多大空间 # 对于这个问题,直觉上觉得[3]string类型变量由3个字符串组成而字符串长度是不确定的所以对于类似[n]string类型变量占用多大的空间是不确定。\n首先明确的是Go语言中提供了 unsafe.Sizeof 函数来确定一个类型变量占用空间大小,这个大小是不含它引用的内存大小。比如某结构体中一个字段是个指针类型,这个字段指向的内存是不计算进去的,只会计算该字段本身的大小。\n字符串底层结构是 reflect.StringHeader 一共占用16个字节空间所以我们对于[n]string的大小计算伪代码如下\nunsafe.Sizeof([n]string) == n * 16 那么问题[3]string类型的变量占用多大空间的答案是48。\n如何高效的进行字符串拼接 # 字符串进行拼接有多种方法:\n使用拼接字符 + 拼接字符串\n效率低每次拼接会产生临时字符串适合少量字符串拼接。使用起来最简单。\n使用 fmt.Printf() 来拼接字符\n由于需要将字符串转换成空接口类型效率差这里面不再讨论\n使用 strings.Join() 来拼接字符串\n其底层其实使用的是 strings.Builder ,效率高,适合字符串数组。\n使用 bytes.Buffer 来拼接字符串\n效率高可以复用\n使用 strings.Builder 来拼接字符串\n效率高每次Reset()之后,其底层缓冲会被清除,不适合复用。\n使用拼接符 + 进行拼接 # package main import ( \u0026#34;fmt\u0026#34; \u0026#34;reflect\u0026#34; \u0026#34;unsafe\u0026#34; ) func main() { strSlices := []string{\u0026#34;h\u0026#34;, \u0026#34;e\u0026#34;, \u0026#34;l\u0026#34;, \u0026#34;l\u0026#34;, \u0026#34;o\u0026#34;} var all string for _, str := range strSlices { all += str sh := (*reflect.StringHeader)(unsafe.Pointer(\u0026amp;all)) fmt.Printf(\u0026#34;str地址%pall地址%pall底层字节数组地址=0x%x\\n\u0026#34;, \u0026amp;str, \u0026amp;all, sh.Data) } } 上面代码输出一下内容:\nstr地址0xc000010250all地址0xc000010240all底层字节数组地址=0x4bc8f7 str地址0xc000010250all地址0xc000010240all底层字节数组地址=0xc000018048 str地址0xc000010250all地址0xc000010240all底层字节数组地址=0xc000018068 str地址0xc000010250all地址0xc000010240all底层字节数组地址=0xc000018078 str地址0xc000010250all地址0xc000010240all底层字节数组地址=0xc000018088 从上面输出中可以发现str和all地址一直没有变但是all的底层字节数组地址一直在变化这说明拼接符 + 在拼接字符串时候,会创建许多临时字符串,临时字符串意味着内存分配,指向效率不会太高。\n使用 bytes.Buffer 拼接字符串 # package main import \u0026#34;bytes\u0026#34; func main() { strSlices := []string{\u0026#34;h\u0026#34;, \u0026#34;e\u0026#34;, \u0026#34;l\u0026#34;, \u0026#34;l\u0026#34;, \u0026#34;o\u0026#34;} var bf bytes.Buffer for _, str := range strSlices { bf.WriteString(str) } print(bf.String()) } bytes.Buffer 底层结构包含内存缓冲最少缓冲大小是64个字节当进行字符串拼接时候由于利用到了缓冲拼接效率相比拼接符 + 大大提升:\ntype Buffer struct { buf []byte // 内存缓冲是字节切片类型 off int // buf已读索引下次读取从buf[off]开始 lastRead readOp } func (b *Buffer) String() string { if b == nil { // Special case, useful in debugging. return \u0026#34;\u0026lt;nil\u0026gt;\u0026#34; } return string(b.buf[b.off:]) } 注意:\nbytes.Buffer是可以复用的。当进行reset时候并不会销毁内存缓冲。\n使用 strings.Builder 拼接字符串 # package main import \u0026#34;strings\u0026#34; func main() { strSlices := []string{\u0026#34;h\u0026#34;, \u0026#34;e\u0026#34;, \u0026#34;l\u0026#34;, \u0026#34;l\u0026#34;, \u0026#34;o\u0026#34;} var strb strings.Builder for _, str := range strSlices { strb.WriteString(str) } print(strb.String()) } strings.Builder 同 bytes.Buffer 一样都是用内存缓冲,最大限度地减少了内存复制:\ntype Builder struct { addr *Builder // 用来运行时检测是否违背nocopy机制 buf []byte // 内存缓冲,类型是字节数组 } func (b *Builder) String() string { return *(*string)(unsafe.Pointer(\u0026amp;b.buf)) } 从上面可以看到 string.Builder 的 String 方法使用 unsafe.Pointer 将字节数组转换成字符串。而bytes.Buffer的 String 方法使用的 string([]byte)将字节数组转换成字符串,后者由于涉及内存分配和拷贝,相比之下它的执行效率低。\n为什么bytes.Buffer的 String 方法的效率比较低,可以查看《 基础篇-切片-string类型与[]byte类型如何实现zero-copy互相转换》。\n字符串拼接基准测试 # 下面我们进行基准测试下:\n// 使用拼接符拼接字符串 func BenchmarkJoinStringUsePlus(b *testing.B) { strSlices := []string{\u0026#34;h\u0026#34;, \u0026#34;e\u0026#34;, \u0026#34;l\u0026#34;, \u0026#34;l\u0026#34;, \u0026#34;o\u0026#34;} for i := 0; i \u0026lt; b.N; i++ { for j := 0; j \u0026lt; 10000; j++ { var all string for _, str := range strSlices { all += str } _ = all } } } // 复用bytes.Buffer结构 func BenchmarkJoinStringUseBytesBufWithReuse(b *testing.B) { strSlices := []string{\u0026#34;h\u0026#34;, \u0026#34;e\u0026#34;, \u0026#34;l\u0026#34;, \u0026#34;l\u0026#34;, \u0026#34;o\u0026#34;} var bf bytes.Buffer for i := 0; i \u0026lt; b.N; i++ { for j := 0; j \u0026lt; 10000; j++ { var all string for _, str := range strSlices { bf.WriteString(str) } all = bf.String() _ = all bf.Reset() } } } // 使用bytes.Buffer未进行复用 func BenchmarkJoinStringUseBytesBufWithoutReuse(b *testing.B) { strSlices := []string{\u0026#34;h\u0026#34;, \u0026#34;e\u0026#34;, \u0026#34;l\u0026#34;, \u0026#34;l\u0026#34;, \u0026#34;o\u0026#34;} for i := 0; i \u0026lt; b.N; i++ { for j := 0; j \u0026lt; 10000; j++ { var all string var bf bytes.Buffer for _, str := range strSlices { bf.WriteString(str) } all = bf.String() _ = all bf.Reset() } } } // 使用strings.Builder func BenchmarkJoinStringUseStringBuilder(b *testing.B) { strSlices := []string{\u0026#34;h\u0026#34;, \u0026#34;e\u0026#34;, \u0026#34;l\u0026#34;, \u0026#34;l\u0026#34;, \u0026#34;o\u0026#34;} for i := 0; i \u0026lt; b.N; i++ { for j := 0; j \u0026lt; 10000; j++ { all := \u0026#34;\u0026#34; var strb strings.Builder for _, str := range strSlices { strb.WriteString(str) } all = strb.String() _ = all strb.Reset() } } } 基准测试结果如下:\nBenchmarkJoinStringUsePlus 703\t1633439 ns/op\t160000 B/op\t40000 allocs/op BenchmarkJoinStringUseBytesBufWithReuse 2130\t471368 ns/op\t0 B/op\t0 allocs/op BenchmarkJoinStringUseBytesBufWithoutReuse 1209\t883053 ns/op\t640000 B/op\t10000 allocs/op BenchmarkJoinStringUseStringBuilder 1830\t548350 ns/op\t80000 B/op\t10000 allocs/op 字符串拼接效率总结 # 从上面结果可以分析得到字符串拼接效率其中strings.Builder的效率最高拼接字符+效率最低:\nstrings.Builder \u0026gt; bytes.Buffer \u0026gt; 拼接字符+ 但是由于bytes.Buffer可以复用若在需要多此执行字符串拼接的场景下推荐使用它。\n"},{"id":25,"href":"/concurrency/sync-map/","title":"并发Map - sync.Map","section":"并发编程","content":" 并发Map - sync.Map # 源码分析 # sync.Map的结构\ntype Map struct { mu Mutex // 排他锁用于对dirty map操作时候加锁处理 read atomic.Value // read map // dirty map。新增key时候只写入dirty map中需要使用mu dirty map[interface{}]*entry // 用来记录从read map中读取key时miss的次数 misses int } sync.Map结构体中read字段是atomic.Value类型底层是readOnly结构体\ntype readOnly struct { m map[interface{}]*entry amended bool // 当amended为true时候表示sync.Map中的key也存在dirty map中 } read map和dirty map的value类型是*entry entry结构体定义如下\n// expunged用来标记从dirty map删除掉了 var expunged = unsafe.Pointer(new(interface{})) type entry struct { // 如果p == nil 说明对应的entry已经被删除掉了 且m.dirty == nil // 如果 p == expunged 说明对应的entry已经被删除了但m.dirty != nil且该entry不存在m.dirty中 // 上述两种情况外entry则是合法的值并且在m.read.m[key]中存在 // 如果m.dirty != nilentry也会在m.dirty[key]中 // p指针指向sync.Map中key对应的Value p unsafe.Pointer // *interface{} } 对Map的操作可以分为四类\nAdd key-value 新增key-value Update key-value 更新key对应的value值 Get Key-value 获取Key对应的Value值 Delete Key 删除key 我们来看看新增和更新操作:\n// Store用来新增和更新操作 func (m *Map) Store(key, value interface{}) { read, _ := m.read.Load().(readOnly) // 如果read map存在该key且该key对应的value不是expunged时(准确的说key对应的value, value是*entry类型entry的p字段指向不是expunged时) // 则使用cas更新value此操作是原子性的 if e, ok := read.m[key]; ok \u0026amp;\u0026amp; e.tryStore(\u0026amp;value) { return } m.mu.Lock() // 先加锁然后重新读取一次read map目的是防止dirty map升级到read map并发Load操作时候read map更改了。 read, _ = m.read.Load().(readOnly) if e, ok := read.m[key]; ok { // 若read map存在此key此时就是map的更新操作 if e.unexpungeLocked() { // 将value由expunged更改成nil // 若成功则表明dirty map中不存在此key把key-value添加到dirty map中 m.dirty[key] = e } e.storeLocked(\u0026amp;value) // 更改value。value是指针类型(*entry)read map和dirty map的value都指向该值。 } else if e, ok := m.dirty[key]; ok {// 若dirty map存在该key则直接更改value e.storeLocked(\u0026amp;value) } else { // 若read map和dirty map中都不存在该key其实就是map的新增key-value操作 if !read.amended {// amended为true时表示sync.Map部分key存在dirty map中 // dirtyLocked()做两件事情: // 1. 若dirty map等于nil则初始化dirty map。 // 2. 遍历read map将read map中的key-value复制到dirty map中从read map中复制的key-value时value是nil或expunged的(因为nil和expunged是key删除了的不进行复制。 // 同时若value值为nil则顺便更改成expunged用来标记dirty map不包含此key) // 思考🤔为啥dirtyLocked()要干事情2即将read map的key-value复制到dirty map中 m.dirtyLocked() // 该新增key-value将添加dirty map中所以将read map的amended设置为true。当amended为true时候从sync.Map读取key时候优先从read map中读取若read map读取时候不到时候会从dirty map中读取 m.read.Store(readOnly{m: read.m, amended: true}) } // 添加key-value到dirty map中 m.dirty[key] = newEntry(value) } // 释放锁 m.mu.Unlock() } func (e *entry) tryStore(i *interface{}) bool { for { p := atomic.LoadPointer(\u0026amp;e.p) if p == expunged { return false } if atomic.CompareAndSwapPointer(\u0026amp;e.p, p, unsafe.Pointer(i)) { return true } } } func (e *entry) unexpungeLocked() (wasExpunged bool) { return atomic.CompareAndSwapPointer(\u0026amp;e.p, expunged, nil) } func (e *entry) storeLocked(i *interface{}) { atomic.StorePointer(\u0026amp;e.p, unsafe.Pointer(i)) } func (m *Map) dirtyLocked() { if m.dirty != nil { return } read, _ := m.read.Load().(readOnly) m.dirty = make(map[interface{}]*entry, len(read.m)) for k, e := range read.m { if !e.tryExpungeLocked() { m.dirty[k] = e } } } func (e *entry) tryExpungeLocked() (isExpunged bool) { p := atomic.LoadPointer(\u0026amp;e.p) for p == nil { if atomic.CompareAndSwapPointer(\u0026amp;e.p, nil, expunged) { return true } p = atomic.LoadPointer(\u0026amp;e.p) } return p == expunged } 接下来看看Map的Get操作\n// Load方法用来获取key对应的value值返回的ok表名key是否存在sync.Map中 func (m *Map) Load(key interface{}) (value interface{}, ok bool) { read, _ := m.read.Load().(readOnly) e, ok := read.m[key] if !ok \u0026amp;\u0026amp; read.amended { // 若key不存在read map中且dirty map包含sync.Map中key情况下 m.mu.Lock() // 加锁 read, _ = m.read.Load().(readOnly) // 再次从read map读取key e, ok = read.m[key] if !ok \u0026amp;\u0026amp; read.amended { e, ok = m.dirty[key] // 从dirty map中读取key // missLocked() 首先将misses计数加1misses用来表明read map读取key没有命中的次数。 // 若misses次数多于dirty map中元素个数时候则将dirty map升级为read mapdirty map设置为nil, amended置为false m.missLocked() } m.mu.Unlock() } if !ok { // read map 和 dirty map中都不存在该key return nil, false } // 加载value值 return e.load() } func (e *entry) load() (value interface{}, ok bool) { p := atomic.LoadPointer(\u0026amp;e.p) if p == nil || p == expunged { // 若value值是nil或expunged返回nil, false表示key不存在 return nil, false } return *(*interface{})(p), true } func (m *Map) missLocked() { m.misses++ if m.misses \u0026lt; len(m.dirty) { return } // 新创建一个readOnly对象其中amended为false, 并将m.dirty直接赋值给该对象的m字段 // 这也是上面思考中的dirtyLocked为什么要干事情2的原因因为通过2操作之后m.dirty已包含read map中的所有key可以直接拿来创建readOnly。 m.read.Store(readOnly{m: m.dirty}) m.dirty = nil m.misses = 0 } 在接着看看Map的删除操作\n// Delete用于删除key func (m *Map) Delete(key interface{}) { read, _ := m.read.Load().(readOnly) e, ok := read.m[key] if !ok \u0026amp;\u0026amp; read.amended { m.mu.Lock() read, _ = m.read.Load().(readOnly) e, ok = read.m[key] // 若read map不存在该key但dirty map中存在该key。则直接调用delete删除dirty map中该key if !ok \u0026amp;\u0026amp; read.amended { delete(m.dirty, key) } m.mu.Unlock() } if ok { e.delete() } } func (e *entry) delete() (hadValue bool) { for { p := atomic.LoadPointer(\u0026amp;e.p) if p == nil || p == expunged { // 若entry中p已经是nil或者expunged则直接返回 return false } if atomic.CompareAndSwapPointer(\u0026amp;e.p, p, nil) { // 将entry中的p设置为nil return true } } } sync.Map还提供遍历key-value功能\n// Range方法接受一个迭代回调函数用来处理遍历的key和value func (m *Map) Range(f func(key, value interface{}) bool) { read, _ := m.read.Load().(readOnly) if read.amended { // 若dirty map中包含sync.Map中key时候 m.mu.Lock() read, _ = m.read.Load().(readOnly) if read.amended {// 加锁之后再次判断是为了防止并发调用Load方法时候dirty map升级为read map时候amended为false情况 // read.amended为true的时候m.dirty包含sync.Map中所有的key read = readOnly{m: m.dirty} m.read.Store(read) m.dirty = nil m.misses = 0 } m.mu.Unlock() } for k, e := range read.m { v, ok := e.load() if !ok { continue } if !f(k, v) { //执行迭代回调函数当返回false时候停止迭代 break } } } 为什么不使用sync.Mutex+map实现并发的map呢 # 这个问题可以换个问法就是sync.Map相比sync.Mutex+map实现并发map有哪些优势\nsync.Map优势在于当key存在read map时候如果进行Store操作可以使用原子性操作更新而sync.Mutex+map形式每次写操作都要加锁这个成本更高。\n另外并发读写两个不同的key时候写操作需要加锁而读操作是不需要加锁的。\n读少写多情况下并发map应该怎么设计 # 这种情况下可以使用分片锁跟据key进行hash处理后找到其对应读写锁然后进行锁定处理。通过分片锁机制可以降低锁的粒度来实现读少写多情况下高并发。可以参见 orcaman/concurrent-map实现。\n总结 # sync.Map是不能值传递狭义的的 sync.Map采用空间换时间策略。其底层结构存在两个map分别是read map和dirty map。当读取操作时候优先从read map中读取是不需要加锁的若key不存在read map中时候再从dirty map中读取这个过程是加锁的。当新增key操作时候只会将新增key添加到dirty map中此操作是加锁的但不会影响read map的读操作。当更新key操作时候如果key已存在read map中时候只需无锁更新更新read map就行负责加锁处理在dirty map中情况了。总之sync.Map会优先从read map中读取、更新、删除因为对read map的读取不需要锁 当sync.Map读取key操作时候若从read map中一直未读到若dirty map中存在read map中不存在的keys时则会把dirty map升级为read map这个过程是加锁的。这样下次读取时候只需要考虑从read map读取且读取过程是无锁的 延迟删除机制删除一个键值时只是打上删除标记只有在提升dirty map为read map的时候才清理删除的数据 sync.Map中的dirty map要么是nil要么包含read map中所有未删除的key-value。 sync.Map适用于读多写少场景。根据 包官方文档介绍它特别适合这两个场景1. 一个key只写入一次但读取多次时比如在只会增长的缓存中2. 当多个goroutine读取、写入和更新不相交的键值对时。 "},{"id":26,"href":"/concurrency/","title":"并发编程","section":"简介","content":" 并发编程 # 学然后知不足,教然后知困。\n内存模型 上下文 - context 通道 - channel 原子操作 - atomic 并发Map - sync.Map 等待组 - sync.WaitGroup 一次性操作 - sync.Once 缓冲池 - sync.Pool 条件变量 - sync.Cond 互斥锁 - sync.Mutex 读写锁 - sync.RWMutex "},{"id":27,"href":"/feature/defer/","title":"延迟执行 - defer语法","section":"语言特性","content":" 延迟执行 - defer语法 # defer 语法支持是Go 语言中一大特性,通过 defer 关键字,我们可以声明一个延迟执行函数,当调用者返回之前开始执行该函数,一般用来完成资源、锁、连接等释放工作,或者 recover 可能发生的panic。\n三大特性 # defer延迟执行语法有三大特性\ndefer函数的传入参数在定义时就已经明确 # func main() { i := 1 defer fmt.Println(i) i++ return } 上面代码输出1而不是2。\ndefer函数是按照后进先出的顺序执行 # func main() { for i := 1; i \u0026lt;= 5; i++ { defer fmt.Print(i) } } 上面代码输出54321而不是12345。\ndefer函数可以读取和修改函数的命名返回值 # func main() { fmt.Println(test()) } func test() (i int) { defer func() { i++ }() return 100 } 上面代码输出输出101而不是100或者1。\n白话defer原理 # defer函数底层数据结构是_defer结构体多个defer函数会构建成一个_defer链表后面加入的defer函数会插入链表的头部该链表链表头部会链接到G上。当函数执行完成返回的时候会从_defer链表头部开始依次执行defer函数。这也就是defer函数执行时会LIFO的原因。_defer链接结构示意图如下\ndefer原理示意图 创建_defer结构体是需要进行内存分配的为了减少分配_defer结构体时资源消耗Go底层使用了defer缓冲池defer pool用来缓存上次使用完的_defer结构体这样下次可以直接使用不必再重新分配内存了。defer缓冲池一共有两级per-P级defer缓冲池和全局defer缓冲池。当创建_defer结构体时候优先从当前M关联的P的缓冲池中取得_defer结构体即从per-P缓冲池中获取这个过程是无锁操作。如果per-P缓冲池中没有则在尝试从全局defer缓冲池获取若也没有获取到则重新分配一个新的_defer结构体。\n当defer函数执行完成之后Go底层会将分配的_defer结构体进行回收先存放在per-P级defer缓冲池中若已存满则存放在全局defer缓冲池中。\n源码分析 # 我们以下代码作为示例分析defer实现机制\npackage main func main() { defer greet(\u0026#34;friend\u0026#34;) println(\u0026#34;welcome\u0026#34;) } func greet(text string) { print(\u0026#34;hello \u0026#34; + text) } 在分析之前我们先来看下defer结构体\ntype _defer struct { siz int32 // 参数和返回值共占用空间大小这段空间会在_defer结构体后面用于defer注册时候保存参数并在执行时候拷贝到调用者参数与返回值空间。 started bool // 标记defer是否已经执行 heap bool // 标记该_defer结构体是否分配在堆上 openDefer bool // 标志是否使用open coded defer方式处理defer sp uintptr // 调用者栈指针执行时会根据sp判断该defer是否是当前执行调用者注册的 pc uintptr // deferprocStack或deferproc的返回地址 fn *funcval // defer函数是funcval类型 _panic *_panic // panic链表用于panic处理 link *_defer // 链接到下一个_defer结构体即该在_defer之前注册的_defer结构体 fd unsafe.Pointer // funcdata for the function associated with the frame varp uintptr // value of varp for the stack frame framepc uintptr } _defer结构体中siz字段记录着defer函数参数和返回值大小如果defer函数拥有参数则Go会把其参数拷贝到该defer函数对应的_defer结构体后面的内存块中。\n_defer结构体中fn字段是指向一个funcval类型的指针funcval结构体的fn字段字段指向defer函数的入口地址。对应上面示例代码中就是greet函数的入口地址\n上面示例代码中编译后的Go汇编代码如下 点击在线查看汇编代码:\nmain_pc0: TEXT \u0026#34;\u0026#34;.main(SB), ABIInternal, $40-0 MOVQ (TLS), CX CMPQ SP, 16(CX) JLS main_pc151 SUBQ $40, SP MOVQ BP, 32(SP) LEAQ 32(SP), BP FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) FUNCDATA $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB) PCDATA $2, $0 PCDATA $0, $0 MOVL $16, (SP) PCDATA $2, $1 LEAQ \u0026#34;\u0026#34;.greet·f(SB), AX PCDATA $2, $0 MOVQ AX, 8(SP) PCDATA $2, $1 LEAQ go.string.\u0026#34;friend\u0026#34;(SB), AX PCDATA $2, $0 MOVQ AX, 16(SP) MOVQ $6, 24(SP) CALL runtime.deferproc(SB) TESTL AX, AX JNE main_pc135 JMP main_pc84 main_pc84: CALL runtime.printlock(SB) PCDATA $2, $1 LEAQ go.string.\u0026#34;welcome\\n\u0026#34;(SB), AX PCDATA $2, $0 MOVQ AX, (SP) MOVQ $8, 8(SP) CALL runtime.printstring(SB) CALL runtime.printunlock(SB) XCHGL AX, AX CALL runtime.deferreturn(SB) MOVQ 32(SP), BP ADDQ $40, SP RET main_pc135: XCHGL AX, AX CALL runtime.deferreturn(SB) MOVQ 32(SP), BP ADDQ $40, SP RET 需要注意的是上面汇编代码是go1.12版本的汇编代码。\n从上面汇编代码我们可以发现defer实现有两个阶段第一个阶段使用runtime.deferproc函数进行defer注册阶段。这一阶段主要工作是创建defer结构然后将其注册到defer链表中。在注册完成之后会根据runtime.deferproc函数返回结果进行下一步处理若是1则说明defer函数有panic处理则直接跳过defer后面的代码直接去执行runtime.deferreturn对应就是上面汇编代码JNE main_pc135逻辑若是0则是正常流程则继续后面的代码对应上面汇编代码就是 JMP main_pc84。\n第二个阶段是调用runtime.deferreturn函数执行defer执行阶段。这个阶段遍历defer链表获取defer结构然后执行defer结构中存放的defer函数信息。\ndefer注册阶段 # defer注册阶段是调用deferproc函数将创建defer结构体并将其注册到defer链表中。\nfunc deferproc(siz int32, fn *funcval) { if getg().m.curg != getg() { // 判断当前G是否处在用户栈空间上若不是则抛出异常 throw(\u0026#34;defer on system stack\u0026#34;) } sp := getcallersp() argp := uintptr(unsafe.Pointer(\u0026amp;fn)) + unsafe.Sizeof(fn) // 获取defer函数参数起始地址 callerpc := getcallerpc() d := newdefer(siz) if d._panic != nil { throw(\u0026#34;deferproc: d.panic != nil after newdefer\u0026#34;) } d.fn = fn d.pc = callerpc d.sp = sp switch siz { case 0: // Do nothing. case sys.PtrSize: // defer函数等于8字节大小64位系统下则直接将_defer结构体后面8字节空间 *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp)) default: memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz)) } return0() } 上面代码中getcallersp()返回调用者SP地址。deferproc的调用者是main函数getcallersp()返回的SP地址指向的deferproc的return address。\ngetcallerpc()返回调用者PC此时PC指向的CALL runtime.deferproc(SB)指令的下一条指令即TESTL AX, AX。\n结合汇编和deferproc代码我们画出defer注册时状态图\n接下来我们来看下newdefer函数是如何分配defer结构体的。\nfunc newdefer(siz int32) *_defer { var d *_defer sc := deferclass(uintptr(siz)) // 根据defer函数参数大小计算出应该使用上面规格的defer缓冲池 gp := getg() if sc \u0026lt; uintptr(len(p{}.deferpool)) { // defer缓冲池只支持5种缓冲池从0到4若sc规格不小于5(说明defer参数大小大于64字节) // 则无法使用缓冲池,则需从内存中分配 pp := gp.m.p.ptr() // pp指向当前M关联的P if len(pp.deferpool[sc]) == 0 \u0026amp;\u0026amp; sched.deferpool[sc] != nil { // 若当前P的defer缓冲池为空且全局缓冲池有可用的defer那么先从全局缓冲拿一点过来存放在P的缓冲池中 systemstack(func() { lock(\u0026amp;sched.deferlock) for len(pp.deferpool[sc]) \u0026lt; cap(pp.deferpool[sc])/2 \u0026amp;\u0026amp; sched.deferpool[sc] != nil { d := sched.deferpool[sc] sched.deferpool[sc] = d.link d.link = nil pp.deferpool[sc] = append(pp.deferpool[sc], d) } unlock(\u0026amp;sched.deferlock) }) } if n := len(pp.deferpool[sc]); n \u0026gt; 0 { d = pp.deferpool[sc][n-1] pp.deferpool[sc][n-1] = nil pp.deferpool[sc] = pp.deferpool[sc][:n-1] } } if d == nil { // 若果需要的defer缓冲池不满足所需的规格或者缓冲池中没有可用的时候切换到系统栈上进行defer结构内存分配。 systemstack(func() { total := roundupsize(totaldefersize(uintptr(siz))) d = (*_defer)(mallocgc(total, deferType, true)) }) } d.siz = siz d.heap = true // 标记分配到堆上 d.link = gp._defer // 插入到链表头部 gp._defer = d return d } 总结下newdefer函数逻辑\n首先根据defer函数的参数大小使用deferclass计算出相应所需要的defer规格如果defer缓冲池支持该规格则尝试从defer缓冲池取出对应的defer结构体。 从defer缓冲池中取可用defer结构体时候会首先从per-P defer缓冲池中取若per-P defer缓冲池为空则尝试从全局缓冲池中取一些可用defer结构体然后放在per-P缓冲池然后再从per-P缓冲池中取。 若defer缓冲池不支持该规格或者缓冲池无可用缓冲则切换到系统栈上进行defer结构分配。 defer缓冲池规格 # defer缓冲池是按照defer函数参数大小范围分为五种规格若不在五种规格之类则不提供缓冲池功能那么每次defer注册时候时候都必须进行内存分配创建defer结构体\n缓冲池规格 defer函数参数大小范围 对应per-P缓冲池位置 对应全局缓冲池位置 class0 0 p.deferpool[0] sched.deferpool[0] class1 [1, 16] p.deferpool[1] sched.deferpool[1] class2 [17, 32] p.deferpool[2] sched.deferpool[2] class3 [33, 48] p.deferpool[3] sched.deferpool[3] class4 [49, 64] p.deferpool[4] sched.deferpool[4] defer函数参数大小与缓冲池规格转换是通过deferclass函数转换的\nfunc deferclass(siz uintptr) uintptr { if siz \u0026lt;= minDeferArgs { // minDeferArgs是个常量值是0 return 0 } return (siz - minDeferArgs + 15) / 16 } per-P级defer缓冲池与全局级defer缓冲池结构 # per-P级defer缓冲池结构使用两个字段deferpool和deferpoolbuf构成缓冲池\ntype p struct { ... deferpool [5][]*_defer // pool of available defer structs of different sizes (see panic.go) deferpoolbuf [5][32]*_defer ... } p结构体中deferpool数组的元素是_defer指针类型的切片该切片的底层数组是deferpoolbuf数组的元素\nfunc (pp *p) init(id int32) { ... for i := range pp.deferpool { pp.deferpool[i] = pp.deferpoolbuf[i][:0] } ... } 全局级defer缓冲池保存在全局sched的deferpool字段中sched是schedt类型变量deferpool是由5个_defer类型指针构成链表组成的数组\ntype schedt struct { ... deferlock mutex // 由于存在多个P并发的从全局缓冲池中获取defer结构体所以需要一个锁 deferpool [5]*_defer ... } defer执行阶段 # 当函数返回之前Go会调用deferreturn函数开始执行defer函数。总之defer流程可以简单概括为Go语言通过先注册通过调用deferproc函数然后函数返回之前执行defer函数通过调用deferreturn函数实现了defer延迟执行功能。\nfunc deferreturn(arg0 uintptr) { gp := getg() d := gp._defer if d == nil { // defer链表为空直接返回。deferreturn是一个递归调用每次调用都会从defer链表弹出一个defer进行执行当defer链表为空时候说明所有defer都已经执行完成 return } sp := getcallersp() if d.sp != sp { // defer保存的sp与当前调用deferreturn的调用者栈顶sp不一致则直接返回 return } switch d.siz { case 0: case sys.PtrSize: // 若defer参数大小是8字节则直接将defer参数复制给arg0 *(*uintptr)(unsafe.Pointer(\u0026amp;arg0)) = *(*uintptr)(deferArgs(d)) default: // 否则进行内存移动将defer的参数复制到arg0中此后arg0存放的是延迟函数的参数 memmove(unsafe.Pointer(\u0026amp;arg0), deferArgs(d), uintptr(d.siz)) } fn := d.fn d.fn = nil gp._defer = d.link freedefer(d) jmpdefer(fn, uintptr(unsafe.Pointer(\u0026amp;arg0))) } deferreturn函数通过jmpdefer实现递归调用jmpdefer是通过汇编实现的jmpdefer函数完成两个功能调用defer函数和deferreturn再次调用。deferreturn递归调用时候递归终止条件有两个1. defer链表为空。2. defer保存的sp与当前调用deferreturn调用者栈顶sp不一致。第一个条件很好了解第二个循环终止条件存在原因我们稍后探究。\n我们需要理解arg0这个变量用途。arg0看似是deferreturn的参数实际上是用来存储延迟函数的参数。\n在调用jmpdefer之前会先调用freedefer将当前defer结构释放回收\nfunc freedefer(d *_defer) { if d._panic != nil { // freedefer调用时_panic一定是nil freedeferpanic() // freedeferpanic作用是抛出异常freedefer with d._panic != nil } if d.fn != nil { // freedefer调用时fn一定已经置为nil freedeferfn() // freedeferfn作用是抛出异常freedefer with d.fn != nil } if !d.heap { // defer结构不是在堆上分配则无需进行回收 return } sc := deferclass(uintptr(d.siz)) // 根据defer参数和返回值大小判断规格以便决定放在哪种规格defer缓冲池中 if sc \u0026gt;= uintptr(len(p{}.deferpool)) { return } pp := getg().m.p.ptr() if len(pp.deferpool[sc]) == cap(pp.deferpool[sc]) { // 当前P的defer缓冲池已满则将P的defer缓冲池defer取出一般放在全局defer缓冲池中 systemstack(func() { var first, last *_defer for len(pp.deferpool[sc]) \u0026gt; cap(pp.deferpool[sc])/2 { n := len(pp.deferpool[sc]) d := pp.deferpool[sc][n-1] pp.deferpool[sc][n-1] = nil pp.deferpool[sc] = pp.deferpool[sc][:n-1] if first == nil { first = d } else { last.link = d } last = d } lock(\u0026amp;sched.deferlock) last.link = sched.deferpool[sc] sched.deferpool[sc] = first unlock(\u0026amp;sched.deferlock) }) } // 重置defer参数 d.siz = 0 d.started = false d.sp = 0 d.pc = 0 d.link = nil pp.deferpool[sc] = append(pp.deferpool[sc], d) // 将当前defer放入P的defer缓冲池中 } 我们来看下jmpdefer实现\nTEXT runtime·jmpdefer(SB), NOSPLIT, $0-16 MOVQ\tfv+0(FP), DX\t# DX寄存器存储jmpdefer第一个参数fnfn是funcval类型指针 MOVQ\targp+8(FP), BX\t# BX寄存器存储jmpdefer第二个参数该参数是个指针类型指向arg0 LEAQ\t-8(BX), SP\t# 将BX存放的arg0的地址减少8获取得到调用deferreturn时栈顶地址此时栈顶存放的是deferreturn的return address最后将该地址存放在SP寄存器中 MOVQ\t-8(SP), BP\t# 重置BP寄存器 SUBQ\t$5, (SP)\t# 此时SP寄存器指向的是deferreturn的return address。该指令是将调用deferreturn的return address减少5 # 而减少5之后return adderss恰好指向了`CALL runtime.deferreturn(SB)`这就实现了deferreturn递归调用 MOVQ 0(DX), BX # DX存储的是fn其是funcval类型指针所以获取真正函数入口地址需要0(DX)该指令等效于BX = Mem[R[DX] + 0]。 # 寄存器逻辑操作不了解的话可以参看前面Go汇编章节 JMP\tBX\t# 通过JMP指令调用延迟函数 从上面代码可以看出来jmpdefer通过汇编更改了延迟函数调用的return address使return address指向deferreturn入口地址这样当延迟函数执行完成之后会继续调用deferreturn函数从而实现了deferreturn递归调用。deferreturn和jmpdefer最后实现的逻辑的伪代码如下\nfunction deferreturn() { var arg int for _, d := range deferLinkList { arg = d.arg d.fn(arg) deferreturn() } } 画出deferreturn调用内存和栈的状态图帮助理解\n最后我们来探究一下deferreturn第二个终止条件考虑下面的场景\nfunc A() { defer B() defer C() } func C() { defer D() } 将上面代码转换成成底层实现的伪代码如下:\nfunc A() { deferproc(B) // 注册延迟函数B deferproc(C) // 注册延迟函数C deferreturn() // 开始执行延迟函数 } func C() { deferproc(D) // 注册延迟函数C deferreturn() // 开始执行延迟函数 } 当调用A函数的deferreturn函数时会从defer链表中取出延迟函数C进行执行当执行C函数时其内部也有一个defer函数C函数最后也会调用deferreturn函数当C函数中调用deferreturn函数时defer链表结构如下\nsp指向C的栈顶 sp指向A的栈顶 | | | | v v g._defer ---------\u0026gt; D --------\u0026gt; B 当C中的deferreturn执行完defer链表中延迟函数D之后开始执行B的时候由于B的sp指向的是A的栈顶不等于C的栈顶此时满足终止条件2C中的deferreturn会退出执行此时A的deferreturn开始继续执行A的deferreturn调用其C的deferreturn函数相当于一个大循环里面套一个小循环现在是小循环退出了大循环还是会继续的此时由于B的sp指向就是A的栈顶B函数会执行。\ndeferreturn循环终止第二个条件就是为了解决诸于此类的场景。\n优化历程 # 上面我们分析的代码中defer结构是分配到堆上其实为了优化defer语法性能Go在实现过程可能会将defer结构分配在栈上。我们来看看Go各个版本对defer都做了哪些优化\npackage main func main() { defer greet() } func greet() { print(\u0026#34;hello\u0026#34;) } 我们以上面代码为例看看其在go1.12、go1.13、go1.14这几个版本下的核心汇编代码:\ngo1.12版本:\n1 2 3 4 5 6 7 8 9 leaq \u0026#34;\u0026#34;.greet·f(SB), AX pcdata $2, $0 movq AX, 8(SP) call runtime.deferproc(SB) testl AX, AX jne main_pc73 .loc 1 5 0 xchgl AX, AX call runtime.deferreturn(SB) go1.12版本中通过调用 runtime.deferproc 函数将defer函数包装成 _defer 结构并注册到defer链表中该 _defer 结构体是分配在堆内存中,需要进行垃圾回收的。\ngo1.13版本:\n1 2 3 4 5 6 7 8 9 10 11 12 13 leaq \u0026#34;\u0026#34;.greet·f(SB), AX pcdata $0, $0 movq AX, \u0026#34;\u0026#34;..autotmp_0+32(SP) pcdata $0, $1 leaq \u0026#34;\u0026#34;..autotmp_0+8(SP), AX pcdata $0, $0 movq AX, (SP) call runtime.deferprocStack(SB) testl AX, AX jne main_pc83 .loc 1 5 0 xchgl AX, AX call runtime.deferreturn(SB) go1.13版本中通过调用 runtime.deferprocStack 函数将defer函数包装成 _defer 结构并注册到defer链表中该 _defer 结构体是分配在栈上不需要进行垃圾回收处理这个地方就是go1.13相比go1.12所做的优化点。\ngo1.14版本:\n1 2 3 4 5 6 7 8 9 10 11 leaq \u0026#34;\u0026#34;.greet·f(SB), AX pcdata $0, $0 pcdata $1, $1 movq AX, \u0026#34;\u0026#34;..autotmp_1+8(SP) .loc 1 5 0 movb $0, \u0026#34;\u0026#34;..autotmp_0+7(SP) call \u0026#34;\u0026#34;.greet(SB) movq 16(SP), BP addq $24, SP ret call runtime.deferreturn(SB) go1.14版本不再调用deferproc/deferprocStack 函数来处理,而是在 return 返回之前直接调用该 defer函数即inline方式性能相比go1.13又得到进一步提升go官方把这种处理方式称为open-coded defer。实际上go1.14中禁止优化和内联之后defer函数其底层实现方式就和go1.13一样了。\n需要注意的是 open-coded defer 使用是有限制的它不能用于for循环中的defer函数还有就是defer的数量也是有限制的 最多支持8个defer函数对于for循环或者数量过的defer将使用deferproc/deferprocStack方式实现。关于 open-coded defer 设计细节可以参见官方设计文档: Proposal: Low-cost defers through inline code, and extra funcdata to manage the panic case\n此外 open-coded defer 虽大大提高了 defer 函数执行的性能,但 panic 的 recover 的执行性能会大大变慢,这是因为 panic 处理过程中会扫描 open-coded defer 的栈帧。具体参见open-coded defer的 代码提交记录。open-coded defer带来的好处的是明显毕竟panic是比较少发生的。\ngo1.14也增加了 -d defer 编译选项可以查看defer实现时候使用哪一种方式:\ngo build -gcflags=\u0026#34;-d defer\u0026#34; main.go 总结一下defer优化历程\n版本 优化内容 Go1.12及以前 defer分配到堆上是heap-allocated defer Go1.13 支持在栈上分配defer结构减少堆上分配和GC的开销是stack-allocated defer G01.14 支持开放式编码defer不再使用defer结构直接在函数尾部调用延迟函数是open-coded defer 进一步阅读 # What is a defer? And how many can you run? "},{"id":28,"href":"/type/pointer/","title":"指针","section":"数据类型与数据结构","content":" 指针 # Golang支持指针但是不能像C语言中那样进行算术运算。对于任意类型T其对应的的指针类型是*T类型T称为指针类型*T的基类型。\n引用与解引用 # 一个指针类型*T变量B存储的是类型T变量A的内存地址我们称该指针类型变量B引用(reference)了A。从指针类型变量B获取或者称为访问A变量的值的过程叫解引用。解引用是通过解引用操作符*操作的。\nfunc main() { var A int = 100 var B *int = \u0026amp;A fmt.Println(A == *B) } 转换和可比较性 # 对于指针类型变量能不能够比较和显示转换需要满足以下规则:\n指针类型*T1和*T2相应的基类型T1和T2的底层类型必须一致。 type MyInt int type PInt *int type PMyInt *MyInt func main() { p1 := new(int) var p2 PInt = p1 // p2底层类型是*int p3 := new(MyInt) var p4 PMyInt = p3 // p4底层类型是*MyInt fmt.Println(p1, p2, p3, p4) } uintptr # uintptr是一个足够大的整数类型能够存放任何指针。不同C语言Go语言中普通类型指针不能进行算术运算我们可以将普通类型指针转换成uintptr然后进行运算但普通类型指针不能直接转换成uintptr必须先转换成unsafe.Pointer类型之后再转换成uintptr。\n// uintptr is an integer type that is large enough to hold the bit pattern of // any pointer. type uintptr uintptr unsafe.Pointer # unsafe标准库包提供了unsafe.Pointer类型unsafe.Pointer类型称为非安全指针类型。\ntype ArbitraryType int type Pointer *ArbitraryType unsafe标准库包中也提供了三个函数\nfunc Alignof(variable ArbitraryType) uintptr // 用来获取变量variable的对齐保证 func Offsetof(selector ArbitraryType) uintptr // 用来获取结构体值中的某个字段的地址相对于此结构体值地址的偏移 func Sizeof(variable ArbitraryType) uintptr // 用来获取变量variable变量的大小尺寸 任何指针类型都可以转换成unsafe.Pointer类型即unsafe.Pointer可以指向任何类型arbitrary type但是该类型值是不能够解引用(dereferenced)的。unsafe.Pointer类型的零值是nil。反过来unsafe.Pointer也可以转换成任何指针类型。\nunsafe.Pointer类型变量可以显示转换成内置的uintptr类型变量uintptr变量是整数可以进行算术运算也可以反向转换成unsafe.Pointer。\n安全类型指针(普通类型指针) \u0026lt;\u0026mdash;-\u0026gt; unsafe.Pointer \u0026lt;\u0026mdash;\u0026ndash;\u0026gt; uintptr\n如何正确地使用非类型安全指针 # unsafe包中列出 6种正确使用unsafe.Pointer的模式。\nCode not using these patterns is likely to be invalid today or to become invalid in the future 在代码中不使用这些模式可能现在无效,或者将来也会变成无效的。\n通过非安全类型指针将T1转换成T2 # func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(\u0026amp;f)) } 此时unsafe.Pointer充当桥梁注意T2类型的尺寸不应该大于T1否则会出现溢出异常\n将非安全类型指针转换成uintptr类型 # type MyInt int func main() { a := 100 fmt.Printf(\u0026#34;%p\\n\u0026#34;, \u0026amp;a) fmt.Printf(\u0026#34;%x\\n\u0026#34;, uintptr(unsafe.Pointer(\u0026amp;a))) } 将非安全类型指针转换成uintptr类型并进行算术运算 # 这种模式常用来访问结构体字段或者数组的地址。\ntype MyType struct { f1 uint8 f2 int f3 uint64 } func main() { s := MyType{f1: 10, f2: 20, f3: 30} f2UintPtr := uintptr(unsafe.Pointer(uintptr(unsafe.Pointer(\u0026amp;s)) + unsafe.Offsetof(s.f2))) fmt.Printf(\u0026#34;%p\\n\u0026#34;, \u0026amp;s) fmt.Printf(\u0026#34;%x\\n\u0026#34;, f2UintPtr) // f2UintPtr = s地址 + 8 arr := [3]int{} fmt.Printf(\u0026#34;%p\\n\u0026#34;, \u0026amp;arr) for i := 0; i \u0026lt; 3; i++ { addr := uintptr(unsafe.Pointer(uintptr(unsafe.Pointer(\u0026amp;arr[0])) + uintptr(i)*unsafe.Sizeof(arr[0]))) fmt.Printf(\u0026#34;%x\\n\u0026#34;, addr) } } 通过指针移动到变量内存地址的末尾是无效的:\n// INVALID: end points outside allocated space. var s thing end = unsafe.Pointer(uintptr(unsafe.Pointer(\u0026amp;s)) + unsafe.Sizeof(s)) // INVALID: end points outside allocated space. b := make([]byte, n) end = unsafe.Pointer(uintptr(unsafe.Pointer(\u0026amp;b[0])) + uintptr(n)) .. warning:: 当将uintptr转换回unsafe.Pointer时其不能赋值给一个变量进行中转。 我们来看看下面这个例子:\ntype MyType struct { f1 uint8 f2 int f3 uint64 } func main() { // 方式1 s := MyType{f1: 10, f2: 20, f3: 30} ptr := uintptr(unsafe.Pointer(\u0026amp;s)) + unsafe.Offsetof(s.f2) f2Ptr := (*int)(unsafe.Pointer(ptr)) fmt.Println(*f2Ptr) // 方式2 f2Ptr2 := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(\u0026amp;s)) + unsafe.Offsetof(s.f2))) fmt.Println(*f2Ptr2) } 上面代码中方式1是不安全的尽管大多数情况结果是符合我们期望的但是由于将uintptr赋值给ptr时变量s已不再被引用这时候若恰好进行GC变量s会被回收处理。这会造成此后的操作都是非法访问内存地址。所以对于uintptr转换成unsafe.Pointer的场景我们应该采用方式2将其写在一行里面。\n将非类型安全指针值转换为uintptr值然后传递给syscall.Syscall函数 # 如果unsafe.Pointer参数必须转换为uintptr才能作为参数使用这个转换必须出现在调用表达式中\nsyscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n)) 将unsafe.Pointer转换成uintptr后传参时无法保证执行函数时其执行的内存未回收。只有将这个转换放在函数调用表达时候才能保证函数能够安全的访问该内存这个是编译器进行安全保障实现的。\n将reflect.Value.Pointer或reflect.Value.UnsafeAddr方法的uintptr返回值转换为非类型安全指针 # reflect标准库包中的Value类型的Pointer和UnsafeAddr方法都返回uintptr类型值而不是unsafe.Pointer类型值是为了避免用户在不引用unsafe包情况下就可以将这两个方法的返回值转换为任何类型安全指针类型。\n调用reflect.Value.Pointer或reflect.Value.UnsafeAddr方法获取uintptr并转换unsafe.Pointer必须放在一行表达式中\np := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer())) 下面这种形式是非法:\n// INVALID: uintptr cannot be stored in variable // before conversion back to Pointer. u := reflect.ValueOf(new(int)).Pointer() p := (*int)(unsafe.Pointer(u)) 将reflect.SliceHeader或reflect.StringHeader的Data字段转换成非安全类型或反之操作 # 正确的转换操作如下:\nvar s string hdr := (*reflect.StringHeader)(unsafe.Pointer(\u0026amp;s)) // 模式1 hdr.Data = uintptr(unsafe.Pointer(p)) // 模式6 hdr.Len = n 下面操作是存在bug的\nvar hdr reflect.StringHeader hdr.Data = uintptr(unsafe.Pointer(p)) // 当执行下面代码时候hdr.Data指向的内存可以已经被回收了 hdr.Len = n s := *(*string)(unsafe.Pointer(\u0026amp;hdr)) "},{"id":29,"href":"/type-system/interface/","title":"接口","section":"类型系统","content":" 接口 # "},{"id":30,"href":"/type/","title":"数据类型与数据结构","section":"简介","content":" 数据类型与数据结构 # 绳锯木断,水滴石穿。\n字符串 数组 切片 nil 空结构体 指针 映射 "},{"id":31,"href":"/type/array/","title":"数组","section":"数据类型与数据结构","content":" 数组 # 数组是Go语言中常见的数据结构相比切片数组我们使用的比较少。\n初始化 # Go语言数组有两个声明初始化方式一种需要显示指明数组大小另一种使用 ... 保留字, 数组的长度将由编译器在编译阶段推断出来:\narr1 := [3]int{1, 2, 3} // 使用[n]T方式 arr2 := [...]int{1, 2, 3} // 使用[...]T方式 arr3 := [3]int{2: 3} // 使用[n]T方式 arr4 := [...]int{2: 3} // 使用[...]T方式 注意:\n上面代码中 arr3 和 arr4 的初始化方式是指定数组索引对应的值。实际使用中这种方式并不常见。\n可比较性 # 数组大小是数组类型的一部分,只有数组大小和数组元素类型一样的数组才能够进行比较。\nfunc main() { var a1 [3]int var a2 [3]int var a3 [5]int fmt.Println(a1 == a2) // 输出true fmt.Println(a1 == a3) // 不能够比较,会报编译错误: invalid operation: a1 == a3 (mismatched types [3]int and [5]int) } 值类型 # Go语言中数组是一个值类型变量将一个数组作为函数参数传递是拷贝原数组形成一个新数组传递在函数里面对数组做任何更改都不会影响原数组\nfunc passArr(arr [3]int) { arr[0] = arr[0] * 100 } func main() { myArr := [3]int{1, 3, 5} passArr(myArr) fmt.Println(myArr[0]) // 输出1 } 空间局部性与时间局部性 # CPU访问数据时候趋于访问同一片内存区域的数据这个称为 局部性原理principle of locality。局部性原理可以为细分为 空间局部性Spatial Locality 和 时间局部性Temporal Locality。\n空间局部性\n指的是如果一个位置的数据被访问那么它周围的数据也有可能被访问到。\n时间局部性\n指的是如果一个位置的数据被访问到那么它下一次还是很有可能被访问到。所以我们可以把最近访问的数据缓存起来内存淘汰算法LRU就是基于这个原理。\n我们知道数组内存空间是连续分配的比如对于[3][5]int类型数组其内存空间分配使用如下图所示\n二维数组内存布局 观察上面的二维数组的内存布局,我们可以得出对于 [m][n]T 类型的数组中任一个元素内存地址的计算公式是:\n数组元素的内存地址 = 第一个数组元素的内存地址 + 该元素跨过了多少行 * 元素类型大小 + 该元素在当前行的位置 * 元素类型大小 转换成伪码的实现如下:\naddress(arr[x][y]) = address(arr[0][0]) + x * n * sizeof(T) + y * sizeof(T) = address(arr[0][0]) + (x * n + y) * sizeof(T) 下面我们根据上面公式来访问数组中元素,下面代码中使用到了 uintptr 和 unsafe.Pointer如果不太了解的话可以看本书的 《 基础篇-指针》 那一章节:\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;unsafe\u0026#34; ) func main() { arr := [2][3]int{{1, 2, 3}, {4, 5, 6}} for i := 0; i \u0026lt; 2; i++ { for j := 0; j \u0026lt; 3; j++ { addr := uintptr(unsafe.Pointer(\u0026amp;arr[0][0])) + uintptr(i*3*8) + uintptr(j*8) // 地址 fmt.Printf(\u0026#34;arr[%d][%d]: 地址 = 0x%x值 = %d\\n\u0026#34;, i, j, addr, *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(\u0026amp;arr[0][0])) + uintptr(i*3*8) + uintptr(j*8)))) } } } 上面代码运行结果如下:\narr[0][0]: 地址 = 0xc000068ef0值 = 1 arr[0][1]: 地址 = 0xc000068ef8值 = 2 arr[0][2]: 地址 = 0xc000068f00值 = 3 arr[1][0]: 地址 = 0xc000068f08值 = 4 arr[1][1]: 地址 = 0xc000068f10值 = 5 arr[1][2]: 地址 = 0xc000068f18值 = 6 空间局部性示例 # 对于数组的访问,我们可以一行行访问,也可以一列列访问,根据上面分析我们可以得出一行行访问可以有很好的空间局部性,有更好的执行效率的结论。因为一行行访问时,下一次访问的就是当前元素挨着的元素,而一列列访问则是需要跨过数组列数个元素:\n二位数组的访问 最后我们来进行下基准测试验证一下:\nfunc BenchmarkAccessArrayByRow(b *testing.B) { var myArr [3][5]int b.ReportAllocs() b.ResetTimer() for k := 0; k \u0026lt; b.N; k++ { for i := 0; i \u0026lt; 3; i++ { for j := 0; j \u0026lt; 5; j++ { myArr[i][j] = i*i + j*j } } } } func BenchmarkAccessArrayByCol(b *testing.B) { var myArr [3][5]int b.ReportAllocs() b.ResetTimer() for k := 0; k \u0026lt; b.N; k++ { for i := 0; i \u0026lt; 5; i++ { for j := 0; j \u0026lt; 3; j++ { myArr[j][i] = i*i + j*j } } } } 本人电脑中基准测试结果如下:\ngoos: linux goarch: amd64 BenchmarkAccessArrayByRow 121336255\t10.3 ns/op\t0 B/op\t0 allocs/op BenchmarkAccessArrayByCol 82772149\t13.2 ns/op\t0 B/op\t0 allocs/op PASS 从上面结果可以看出来我们可以发现按行访问10.3 ns/op快于按列访问13.2 ns/op符合我们预测的结论。\n如何实现随机访问数组的全部元素 # 这里将介绍两种实现方法。这两种实现方法都是Go语言底层使用到的算法。\n第一种方法用在Go调度器部分。G-M-P调度模型中当M关联的P的本地队列中没有可以执行的G时候M会从其他P的本地可运行G队列中偷取G所有P存储一个全局切片中为了随机性选择P来偷取这就需要随机的访问数组。该算法具体叫什么未找到相关文档。由于该算法实现上使用到素数和取模运算姑且称之素数取模随机法。\n第二种方法使用算法FisherYates shuffleGo语言用它来随机性处理通道选择器select中case语句。\n素数取模随机法 # 该算法实现逻辑是:对于一个数组[n]T随机的从小于n的素数集合中选择一个素数假定是p接着从数组0到n-1位置中随机选择一个位置开始假定是m那么此时(m + p)%n = i位置处的数组元素就是我们要访问的第一个元素。第二次要访问的元素是(上一次位置+p)%n处元素这里面就是(i+p)%n以此类推访问n次就可以访问完全部数组元素。\n举个具体例子来说明比如对于[8]int数组a其素数集合是{1, 3, 5, 7}。假定选择的素数是5从位置1开始。\n第一次访问元素是 (1 + 5)%8 = 6处元素即a[6] 第二次访问元素是 (6 + 5)%8 = 3处元素即a[3] 第三次访问元素是 (3 + 5)%8 = 0处元素即a[0] 第四次访问元素是 (0 + 5)%8 = 5处元素即a[5] 第五次访问元素是 (5 + 5)%8 = 2处元素即a[2] 第六次访问元素是 (2 + 5)%8 = 7处元素即a[7] 第七次访问元素是 (7 + 5)%8 = 4处元素即a[4] 第八次访问元素是 (4 + 5)%8 = 1处元素即a[1] 从上面例子可以看出来访问8次即可遍历完所有数组元素由于素数和开始位置是随机的那么访问也能做到随机性。\n该算法实现如下代码来自Go源码 runtime/proc.go\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;math/rand\u0026#34; ) type randomOrder struct { count uint32 coprimes []uint32 } type randomEnum struct { i uint32 count uint32 pos uint32 inc uint32 } func (ord *randomOrder) reset(count uint32) { ord.count = count ord.coprimes = ord.coprimes[:0] for i := uint32(1); i \u0026lt;= count; i++ { // 初始化素数集合 if gcd(i, count) == 1 { ord.coprimes = append(ord.coprimes, i) } } } func (ord *randomOrder) start(i uint32) randomEnum { return randomEnum{ count: ord.count, pos: i % ord.count, inc: ord.coprimes[i%uint32(len(ord.coprimes))], } } func (enum *randomEnum) done() bool { return enum.i == enum.count } func (enum *randomEnum) next() { enum.i++ enum.pos = (enum.pos + enum.inc) % enum.count } func (enum *randomEnum) position() uint32 { return enum.pos } func gcd(a, b uint32) uint32 { // 辗转相除法取最大公约数 for b != 0 { a, b = b, a%b } return a } func main() { arr := [8]int{1, 2, 3, 4, 5, 6, 7, 8} var order randomOrder order.reset(uint32(len(arr))) fmt.Println(\u0026#34;====第一次随机遍历====\u0026#34;) for enum := order.start(rand.Uint32()); !enum.done(); enum.next() { fmt.Println(arr[enum.position()]) } fmt.Println(\u0026#34;====第二次随机遍历====\u0026#34;) for enum := order.start(rand.Uint32()); !enum.done(); enum.next() { fmt.Println(arr[enum.position()]) } } FisherYates shuffle # 进一步阅读 # Locality of reference FisherYates_shuffle "},{"id":32,"href":"/function/method/","title":"方法","section":"函数","content":" 方法 # Go 语言中具有接收者的函数即为方法。若函数的接收者类型是T那么我们可以说该函数是类型T的方法。那么方法底层实现是怎么样的和函数有什么区别呢这一章节我们将探讨这个。\n方法的本质就是普通函数 # 我们来看下如下的代码:\ntype A struct { name string } func (a A) Name() string { a.name = \u0026#34;Hi \u0026#34; + a.name return a.name } func main() { a := A{name: \u0026#34;new world\u0026#34;} println(a.Name()) println(A.Name(a)) } func NameofA(a A) string { a.name = \u0026#34;Hi \u0026#34; + a.name return a.name } 上面代码中a.Name()表示的是调用对象a的Name方法。它实际上是一个语法糖等效于A.Name(a)其中a就是方法接收者。我们可以通过以下代码证明两者是相等的\nt1 := reflect.TypeOf(A.Name) t2 := relect.TypeOf(NameOfA) fmt.Println(t1 == t2) // true 我们在看下a.Name()底层实现是怎么样的,点击 在线查看:\nLEAQ go.string.\u0026#34;new world\u0026#34;(SB), AX MOVQ AX, \u0026#34;\u0026#34;.a+32(SP) MOVQ $9, \u0026#34;\u0026#34;.a+40(SP) PCDATA $0, $0 MOVQ AX, (SP) MOVQ $9, 8(SP) CALL \u0026#34;\u0026#34;.A.Name(SB) a.Name()底层其实调用的就是A.Name函数只不过传递的第一参数就是对象a。\n综上所述方法本质就是普通的函数方法的接收者就是隐含的第一个参数。对于其他面向对象的语言来说类对象就是相应的函数的第一个参数。\n值接收者和指针接收者混合的方法 # 比如以下代码中,展示的值接收者和指针接收者混合的方法\ntype A struct { name string } func (a A) GetName() string { return a.name } func (pa *A) SetName() string { pa.name = \u0026#34;Hi \u0026#34; + p.name return pa.name } func main() { a := A{name: \u0026#34;new world\u0026#34;} pa := \u0026amp;a println(pa.GetName()) // 通过指针调用定义的值接收者方法 println(a.SetName()) // 通过值调用定义的指针接收者方法 } 上面代码中通过指针调用值接收者方法和通过值调用指针接收者方法都能够正常运行。这是因为两者都是语法糖Go 语言会在编译阶段会将两者转换如下形式:\nprintln((*pa).GetName()) println((\u0026amp;a).SetName()) 方法表达式与方法变量 # type A struct { name string } func (a A) GetName() string { return a.name } func main() { a := A{name: \u0026#34;new world\u0026#34;} f1 := A.GetName // 方法表达式 f1(a) f2 := a.GetName // 方法变量 f2() } 方法表达式(Method Expression) 与方法变量(Method Value)本质上都是 Function Value 区别在于方法变量会捕获方法接收者形成闭包此方法变量的生命周期与方法接收者一样编译器会将其进行优化转换成对类型T的方法调用并传入接收者作为参数。 根据上面描述我们可以将上面代码中f2理解成如下代码\nfunc GetFunc() (func()) string { a := A{name: \u0026#34;new world\u0026#34;} return func() string { return A.GetName(a) } } f2 = GetFunc() "},{"id":33,"href":"/type/map/","title":"映射类型","section":"数据类型与数据结构","content":" 映射 # 映射也被称为哈希表(hash table)、字典。它是一种由key-value组成的抽象数据结构。大多数情况下它都能在O(1)的时间复杂度下实现增删改查功能。若在极端情况下出现所有key都发生哈希碰撞时则退回成链表形式此时复杂度为O(N)。\n映射底层一般都是由数组组成该数组每个元素称为桶它使用hash函数将key分配到不同桶中若出现碰撞冲突时候则采用链地址法也称为拉链法或者开放寻址法解决冲突。下图就是一个由姓名-号码构成的哈希表的结构图:\nGo语言中映射中key若出现冲突碰撞时候则采用链地址法解决Go语言中映射具有以下特点\n引用类型变量 读写并发不安全 遍历结果是随机的 数据结构 # Go语言映射的数据结构 Go语言中映射的数据结构是 runtime.hmap( runtime/map.go):\n// A header for a Go map. type hmap struct { count int // 元素个数用于len函数返回map元素数量 flags uint8 // 标志位标志当前map正在写等状态 B uint8 // buckets个数的对数即桶数量 = 2 ^ B noverflow uint16 // overflow桶数量的近似值overflow桶即溢出桶即链表法中存在链表上的桶的个数 hash0 uint32 // 随机数种子用于计算key的哈希值 buckets unsafe.Pointer // 指向buckets数组如果元素个数为0时该值为nil oldbuckets unsafe.Pointer // 扩容时指向旧的buckets nevacuate uintptr // 用于指示迁移进度,小于此值的桶已经迁移完成 extra *mapextra // 额外记录overflow桶信息 } 映射中每一个桶的结构是 runtime.bmap( runtime/map.go)\n// A bucket for a Go map. type bmap struct { tophash [bucketCnt]uint8 } 上面bmap结构是静态结构在编译过程中 runtime.bmap 会拓展成以下结构体:\ntype bmap struct{ tophash [8]uint8 keys [8]keytype // keytype 由编译器编译时候确定 values [8]elemtype // elemtype 由编译器编译时候确定 overflow uintptr // overflow指向下一个bmapoverflow是uintptr而不是*bmap类型是为了减少gc } bmap结构示意图\nbmap底层结构 每个桶bmap中可以装载8个key-value键值对。当一个key确定存储在哪个桶之后还需要确定具体存储在桶的哪个位置这个位置也称为桶单元一个bmap装载8个key-value键值对那么一个bmap共8个桶单元bmap中tophash就是用于实现快速定位key的位置。在实现过程中会使用key的hash值的高八位作为tophash值存放在bmap的tophash字段中。tophash计算公式如下\nfunc tophash(hash uintptr) uint8 { top := uint8(hash \u0026gt;\u0026gt; (sys.PtrSize*8 - 8)) if top \u0026lt; minTopHash { top += minTopHash } return top } 上面函数中hash是64位的sys.PtrSize值是8所以top := uint8(hash \u0026gt;\u0026gt; (sys.PtrSize*8 - 8))等效top = uint8(hash \u0026gt;\u0026gt; 56)最后top取出来的值就是hash的最高8位值。bmap的tophash字段不光存储key哈希值的高八位还会存储一些状态值用来表明当前桶单元状态这些状态值都是小于minTopHash的。\n为了避免key哈希值的高八位值出现这些状态值相等产生混淆情况所以当key哈希值高八位若小于minTopHash时候自动将其值加上minTopHash作为该key的tophash。桶单元的状态值如下\nemptyRest = 0 // 表明此桶单元为空,且更高索引的单元也是空 emptyOne = 1 // 表明此桶单元为空 evacuatedX = 2 // 用于表示扩容迁移到新桶前半段区间 evacuatedY = 3 // 用于表示扩容迁移到新桶后半段区间 evacuatedEmpty = 4 // 用于表示此单元已迁移 minTopHash = 5 // key的tophash值与桶状态值分割线值小于此值的一定代表着桶单元的状态大于此值的一定是key对应的tophash值 emptyRest和emptyOne状态都表示此桶单元为空都可以用来插入数据。但是emptyRest还代表着更高单元也为空那么遍历寻找key的时候当遇到当前单元值为emptyRest时候那么更高单元无需继续遍历。\n下图中桶单元1的tophash值是emptyOne桶单元3的tophash值是emptyRest那么我们一定可以推断出桶单元3以上都是emptyRest状态。\nbmap的tophash底层结构 bmap中可以装载8个key-value这8个key-value并不是按照key1/value1/key2/value2/key3/value3\u0026hellip;这样形式存储而采用key1/key2../key8/value1/../value8形式存储因为第二种形式可以减少padding源码中以map[int64]int8举例说明。\nhmap中extra字段是 runtime.mapextra 类型,用来记录额外信息:\n// mapextra holds fields that are not present on all maps. type mapextra struct { overflow *[]*bmap // 指向overflow桶指针组成的切片防止这些溢出桶被gc了 oldoverflow *[]*bmap // 扩容时候指向旧的溢出桶组成的切片防止这些溢出桶被gc了 //指向下一个可用的overflow 桶 nextOverflow *bmap } 当映射的key和value都不是指针类型时候bmap将完全不包含指针那么gc时候就不用扫描bmap。bmap指向溢出桶的字段overflow是uintptr类型为了防止这些overflow桶被gc掉所以需要mapextra.overflow将它保存起来。如果bmap的overflow是*bmap类型那么gc扫描的是一个个拉链表效率明显不如直接扫描一段内存(hmap.mapextra.overflow)\n映射的创建 # 当使用make函数创建映射时候若不指定map元素数量时候底层将使用是make_small函数创建hmap结构此时只产生哈希种子不初始化桶\nfunc makemap_small() *hmap { h := new(hmap) h.hash0 = fastrand() return h } 若指定map元素数量时候底层会使用 makemap 函数创建hmap结构\nfunc makemap(t *maptype, hint int, h *hmap) *hmap { mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size) if overflow || mem \u0026gt; maxAlloc { // 检查所有桶占用的内存是否大于内存限制 hint = 0 } // h不nil说明map结构已经创建在栈上了这个操作由编译器处理的 if h == nil { // h为nil则需要创建一个hmap类型 h = new(hmap) } h.hash0 = fastrand() // 设置map的随机数种子 B := uint8(0) for overLoadFactor(hint, B) { // 设置合适B的值 B++ } h.B = B // 如果B == 0那么map的buckets将会惰性分配allocated lazily使用时候再分配 // 如果B != 0时初始化桶 if h.B != 0 { var nextOverflow *bmap h.buckets, nextOverflow = makeBucketArray(t, h.B, nil) if nextOverflow != nil { h.extra = new(mapextra) h.extra.nextOverflow = nextOverflow // extra.nextOverflow指向下一个可用溢出桶位置 } } return h } makemap函数的第一个参数是maptype类指针它描述了创建的map中key和value元素的类型信息以及其他map信息第二个参数hint对应是make([Type]Type, len)中len参数第三个参数h如果不为nil说明当前map的结构已经有编译器在栈上创建了makemap只需要完成设置随机数种子等操作。\noverLoadFactor函数用来判断当前映射的加载因子是否超过加载因子阈值。makemap使用overLoadFactor函数来调整B值。\n加载因子描述了哈希表中元素填满程度加载因子越大表明哈希表中元素越多空间利用率高但是这也意味着冲突的机会就会加大。当哈希表中所有桶已写满情况下加载因子就是1此时再写入新key一定会产生冲突碰撞。为了提高哈希表写入效率就必须在加载因子超过一定值时这个值称为加载因子阈值进行rehash操作将桶容量进行扩容来尽量避免出现冲突情况。\nJava中hashmap的默认加载因子阈值是0.75Go语言中映射的加载因子阈值是6.5。为什么Go映射的加载因子阈值不是0.75而且超过了1这是因为Java中哈希表的桶存放的是一个key-value其满载因子是1Go映射中每个桶可以存8个key-value满载因子是8当加载因子阈值为6.5时候空间利用率和写入性能达到最佳平衡。\nfunc overLoadFactor(count int, B uint8) bool { // count \u0026gt; bucketCntbucketCnt值是8每一个桶可以存放8个key-value如果map中元素个数count小于8那么一定不会超过加载因子 // loadFactorNum和loadFactorDen的值分别是13和2bucketShift(B)等效于1\u0026lt;\u0026lt;B // 所以 uintptr(count) \u0026gt; loadFactorNum*(bucketShift(B)/loadFactorDen) 等于 uintptr(count) \u0026gt; 6.5 * 2^ B return count \u0026gt; bucketCnt \u0026amp;\u0026amp; uintptr(count) \u0026gt; loadFactorNum*(bucketShift(B)/loadFactorDen) } // bucketShift returns 1\u0026lt;\u0026lt;b, optimized for code generation. func bucketShift(b uint8) uintptr { return uintptr(1) \u0026lt;\u0026lt; (b \u0026amp; (sys.PtrSize*8 - 1)) } makeBucketArray函数是用来创建bmap array来用作为map的buckets。对于创建时指定元素大小超过(2^4) * 8时候除了创建map的buckets也会提前分配好一些桶作为溢出桶。buckets和溢出桶在内存上是连续的。为啥提前分配好溢出桶而不是在溢出时候再分配这是因为现在分配是直接申请一大片内存效率更高。\nhamp.extra.nextOverflow指向该溢出桶溢出桶的除了最后一个桶的overflow指向map的buckets其他桶的overflow指向nil这是用来判断溢出桶最后边界后面代码有涉及此处逻辑。\nfunc makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) { base := bucketShift(b) // 等效于 base := 1 \u0026lt;\u0026lt; b nbuckets := base if b \u0026gt;= 4 { // 对于小b不太可能出现溢出桶所以B超过4时候才考虑提前分配写溢出桶 nbuckets += bucketShift(b - 4) sz := t.bucket.size * nbuckets up := roundupsize(sz) if up != sz { nbuckets = up / t.bucket.size } } if dirtyalloc == nil { buckets = newarray(t.bucket, int(nbuckets)) } else { // 若dirtyalloc不为nil时 // dirtyalloc指向的之前已经使用完的map的buckets之前已使用完的map和当前map具有相同类型的t和b这样它buckets可以拿来复用 // 此时只需对dirtyalloc进行清除操作就可以作为当前map的buckets buckets = dirtyalloc size := t.bucket.size * nbuckets // 下面是清空dirtyalloc操作 if t.bucket.ptrdata != 0 { // map中key或value是指针类型 memclrHasPointers(buckets, size) } else { memclrNoHeapPointers(buckets, size) } } if base != nbuckets { // 多创建一些溢出桶 nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize))) // 溢出桶的最后一个的overflow字段指向buckets last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize))) last.setoverflow(t, (*bmap)(buckets)) } return buckets, nextOverflow } 我们画出桶初始化时候的分配示意图:\n映射中桶定位 通过上面分析整个映射创建过程可以看到使用make创建map时候返回都是hmap类型指针这也就说明Go语言中映射时引用类型的。\n访问映射操作 # 访问映射涉及到key定位的问题首先需要确定从哪个桶找确定桶之后还需要确定key-value具体存放在哪个单元里面每个桶里面有8个坑位。key定位详细流程如下\n首先需根据hash函数计算出key的hash值 该key的hash值的低hmap.B位的值是该key所在的桶 该key的hash值的高8位用来快速定位其在桶具体位置。一个桶中存放8个key遍历所有key找到等于该key的位置此位置对应的就是值所在位置 根据步骤3取到的值计算该值的hash再次比较若相等则定位成功。否则重复步骤3去bmap.overflow中继续查找。 若bmap.overflow链表都找个遍都没有找到则返回nil。 映射中桶定位 当m为2的x幂时候n对m取余数存在以下等式\nn % m = n \u0026amp; (m -1) 举个例子比如n为15m为8n%m等7, n\u0026amp;(m-1)也等于7取余应尽量使用第二种方式因为效率更高。\n那么对于映射中key定位计算就是\nkey对应value所在桶位置 = hash(key)%(hmap.B \u0026lt;\u0026lt; 1) = hash(key) \u0026amp; (hmap.B \u0026lt;\u0026lt;1 - 1) 那么为什么上面key定位流程步骤2中说的却是根据该key的hash值的低hmap.B位的值是该key所在的桶。两者是没有区别的只是一种意思不同说法。\n直接访问与逗号ok模式访问 # 访问映射操作方式有两种:\n第一种直接访问若key不存在则返回value类型的零值其底层实现mapaccess1函数\nv := a[\u0026#34;x\u0026#34;] 第二种是逗号ok模式如果key不存在除了返回value类型的零值ok变量也会设置为false其底层实现mapaccess2\nv, ok := a[\u0026#34;x\u0026#34;] 为了优化性能Go编译器会根据key类型采用不同底层函数比如对于key类型是int的底层实现是mapaccess1_fast64。具体文件可以查看runtime/map_fastxxx.go。优化版本函数有\nkey 类型 方法 uint64 func mapaccess1_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer uint64 func mapaccess2_fast64(t *maptype, h *hmap, key uint64) (unsafe.Pointer, bool) uint32 func mapaccess1_fast32(t *maptype, h *hmap, key uint32) unsafe.Pointer uint32 func mapaccess2_fast32(t *maptype, h *hmap, key uint32) (unsafe.Pointer, bool) string func mapaccess1_faststr(t *maptype, h *hmap, ky string) unsafe.Pointer string func mapaccess2_faststr(t *maptype, h *hmap, ky string) (unsafe.Pointer, bool) 这里面我们这分析通用的mapaccess1函数。\nfunc mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer { if h == nil || h.count == 0 { // map为nil或者map中元素个数为0则直接返回零值 if t.hashMightPanic() { t.hasher(key, 0) // see issue 23734 } return unsafe.Pointer(\u0026amp;zeroVal[0]) } if h.flags\u0026amp;hashWriting != 0 { // 有其他Goroutine正在写map则直接panic throw(\u0026#34;concurrent map read and map write\u0026#34;) } hash := t.hasher(key, uintptr(h.hash0)) // 计算出key的hash值 m := bucketMask(h.B) // m = 2^h.B - 1 b := (*bmap)(add(h.buckets, (hash\u0026amp;m)*uintptr(t.bucketsize))) // 根据上面介绍的取余操作转换成位与操作来获取key所在的桶 if c := h.oldbuckets; c != nil { // 如果oldbuckets不为0说明该map正在处于扩容过程中 if !h.sameSizeGrow() { // 如果不是等容量扩容此时buckets大小是oldbuckets的两倍那么m需减半然后用来定位key在旧桶中位置 m \u0026gt;\u0026gt;= 1 } oldb := (*bmap)(add(c, (hash\u0026amp;m)*uintptr(t.bucketsize))) // 获取key在旧桶的桶 if !evacuated(oldb) { // 如果旧桶数据没有迁移新桶里面,那就在旧桶里面找 b = oldb } } top := tophash(hash) // 计算出key的tophash bucketloop: for ; b != nil; b = b.overflow(t) { // for循环实现功能是先从当前桶找若未找到则当前桶的溢出桶b.overfolw(t)查找直到溢出桶为nil for i := uintptr(0); i \u0026lt; bucketCnt; i++ { // 每个桶有8个单元循环这8个单元一个个找 if b.tophash[i] != top { // 如果当前单元的tophash与key的tophash不一致 if b.tophash[i] == emptyRest { // 若单元tophash值是emptyRest则直接跳出整个大循环emptyRest表明当前单元和更高单元存储都为空所以无需在继续查找下去了 break bucketloop } continue // 继续查找桶其他的单元 } // 此时已找到tophash等于key的tophash的桶单元此时i记录这桶单元编号 k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) // dataOffset是bmap.keys相对于bmap的偏移k记录key存在bmap的位置 if t.indirectkey() { // 若key是指针类型 k = *((*unsafe.Pointer)(k)) } if t.key.equal(key, k) {// 如果key和存放bmap里面的key相等则获取对应value值返回 // value在bmap中的位置 = bmap.keys相对于bmap的偏移 + 8个key占用的空间(8 * keysize) + 该value在bmap.values中偏移(i * t.elemsize) e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize)) if t.indirectelem() { e = *((*unsafe.Pointer)(e)) } return e } } } return unsafe.Pointer(\u0026amp;zeroVal[0]) } 赋值映射操作 # 在map中增加和更新key-value时候都会调用runtime.mapassign方法同访问操作一样Go编译器针对不同类型的key会采用优化版本函数\nkey 类型 方法 uint64 func mapassign_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer unsafe.Pointer func mapassign_fast64ptr(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer uint32 func mapassign_fast32(t *maptype, h *hmap, key uint32) unsafe.Pointer unsafe.Pointer func mapassign_fast32ptr(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer string func mapassign_faststr(t *maptype, h *hmap, s string) unsafe.Pointer 这里面我们只分析通用的方法mapassign\nfunc mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer { if h == nil { // 对于nil map赋值操作直接panic。需要注意的是访问nil map返回的是value类型的零值 panic(plainError(\u0026#34;assignment to entry in nil map\u0026#34;)) } if h.flags\u0026amp;hashWriting != 0 { // 有其他Goroutine正在写操作则直接panic throw(\u0026#34;concurrent map writes\u0026#34;) } hash := t.hasher(key, uintptr(h.hash0)) // 计算出key的hash值 h.flags ^= hashWriting // 将写标志位置为1 if h.buckets == nil { // 惰性创建bucketsmake创建map时候并未初始buckets等到mapassign时候在创建初始化 h.buckets = newobject(t.bucket) // newarray(t.bucket, 1) } again: bucket := hash \u0026amp; bucketMask(h.B) // bucket := hash \u0026amp; (2^h.B - 1) if h.growing() { // 如果当前map处于扩容过程中则先进行扩容将key所对应的旧桶先迁移过来 growWork(t, h, bucket) } b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize))) // 获取key所在的桶 top := tophash(hash) // 计算出key的tophash var inserti *uint8 // 指向key的tophash应该存放的位置即bmap.tophash这个数组中某个位置 var insertk unsafe.Pointer // 指向key应该存放的位置即bmap.keys这个数组中某个位置 var elem unsafe.Pointer // 指向value应该存放的位置即bmap.values这个数组中某个位置 bucketloop: for { for i := uintptr(0); i \u0026lt; bucketCnt; i++ { if b.tophash[i] != top { if isEmpty(b.tophash[i]) \u0026amp;\u0026amp; inserti == nil { // 当i单元的tophash值为空那么说明该单元可以用来存放key-value。 // 再加上inserti == nil条件就是inserti只找到第一个空闲的单元即可 inserti = \u0026amp;b.tophash[i] insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize)) } if b.tophash[i] == emptyRest { // 如果i单元的tophash值为emptyRest那么剩下单元也不用继续找了剩下单元一定都是空的 break bucketloop } continue } // 上面代码是先找到第一个为空的桶单元然后把该桶单元相关的tophash、key、value等位置信息记录在insertiinsertkelem临时变量上。 // 这样当key没有在map中情况下可以拿insertiinsertkelem这变量将该key的信息写入到桶单元中这种情况下key是一个新key这种赋值操作属于新增操作。 // 下面代码部分就是处理中map已存在key的情况这时候我们只需要找到key所在桶单元中value的位置然后把value新值写入即可。 // 这种情况下key是一个旧key这种赋值操作属于更新操作。 k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) if t.indirectkey() { k = *((*unsafe.Pointer)(k)) } if !t.key.equal(key, k) { continue } if t.needkeyupdate() { typedmemmove(t.key, k, key) } elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize)) goto done } ovf := b.overflow(t) // 当前桶没有找到,继续在其溢出桶里面找, if ovf == nil { // 直到都没有找到,那么跳出循环,不在找了。 break } b = ovf } // 当map未扩容中那么就判断当前map是否需要扩容扩容条件是以下两个条件符合任意之一即可 // 1. 是否达到负载因子的阈值6.5 // 2. 溢出桶是否过多 if !h.growing() \u0026amp;\u0026amp; (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) { hashGrow(t, h) goto again // 跳到again标签处再来一遍 } // 如果上面两层for循环都没有找到空的桶单元那说明所有桶单元都写满了那么就得创建一个溢出桶了。 // 然后将数据存放到该溢出桶的第一个单元上 if inserti == nil { newb := h.newoverflow(t, b) // 创建一个溢出桶 inserti = \u0026amp;newb.tophash[0] insertk = add(unsafe.Pointer(newb), dataOffset) elem = add(insertk, bucketCnt*uintptr(t.keysize)) } // store new key/elem at insert position if t.indirectkey() { kmem := newobject(t.key) *(*unsafe.Pointer)(insertk) = kmem insertk = kmem } if t.indirectelem() { vmem := newobject(t.elem) *(*unsafe.Pointer)(elem) = vmem } typedmemmove(t.key, insertk, key) *inserti = top // 写入key的tophash值 h.count++ // 更新map的元素计数 done: if h.flags\u0026amp;hashWriting == 0 { throw(\u0026#34;concurrent map writes\u0026#34;) } h.flags \u0026amp;^= hashWriting // 将map的写标志置为0那么其他Gorountine可以进行写入操作了 if t.indirectelem() { elem = *((*unsafe.Pointer)(elem)) } return elem } 我们梳理总结下mapassign函数执行流程\n首先进行写标志检查和桶初始化检查。如果当前map写标志位已经置为1那么肯定有它Gorountine正在进行写操作那么直接panic。桶初始化检查是当map的桶未创建情况下则在桶初始化检查阶段创建一个桶。\n接下来判断桶是否处在扩容过程中如果处在扩容过程中那么先将当前key所在旧桶全部迁移到新桶中然后再接着迁移一个旧桶也就是说每次mapasssign最多只迁移两个旧桶。为什么一定要先迁移key所在的旧桶数据呢如果key是新key那么旧桶中一定没有这个key信息这种情况迁不迁移旧桶无关紧要但若key之前在旧桶已存在那么一定要先迁移如果不这样的话当key的新value写入新桶中之后再迁移那么旧桶中的旧数据就会覆盖掉新桶中key的value值为了应对这种情况所以一定要先迁移key所在旧桶数据。\n接下就是两层for循环。第一层for循环就是遍历当前key所在桶以及桶的溢出桶直到桶的所有溢出桶都遍历一遍后终止该层循环。第二层for循环遍历的是第一层for循环每次得到的桶中的8个桶单元。两层for循环是为了在map中找到key如果找到key那只需更新key对应value值就可。在循环过程中会记录下第一个为空的桶单元这样在未找到key的情况时候就把key-value信息写入这个桶单元中。如果map中未找到key且也未找到空的桶单元那么没有办法了只能创建一个溢出桶来存放该key-value。\n接下里判断当前map是否需要扩容如果需要扩容则调用hashGrow函数将旧的buckets挂到hmap.oldbuckets字段上再接着通过goto语法跳转标签形式跳到流程2继续执行下去\n最后就是将key的tophashkey值写入到找到的桶单元中并返回桶单元的value地址。value的写入是拿到mapassign返回的地址再写入的。\n接下来我们看下溢出桶创建操作\n首先会从预分配的溢出桶列表中取如果未取到则会现场创建一个溢出桶 若map的key和value都不是指针类型那么会将溢出桶记录到hmap.extra.overflow中 func (h *hmap) newoverflow(t *maptype, b *bmap) *bmap { var ovf *bmap if h.extra != nil \u0026amp;\u0026amp; h.extra.nextOverflow != nil { ovf = h.extra.nextOverflow // 从上面分析映射的创建过程代码中我们知道创建map的buckets时候有时候会顺便创建一些溢出桶 // h.extra.nextOverflow就是指向这些溢出桶 if ovf.overflow(t) == nil { // ovf不是最后一个溢出桶 h.extra.nextOverflow = (*bmap)(add(unsafe.Pointer(ovf), uintptr(t.bucketsize))) // extra.nextOverflow指向下一个溢出桶 } else { // ovf是最后一个溢出桶 ovf.setoverflow(t, nil) // 将ovf.overflow设置nil h.extra.nextOverflow = nil } } else { // 没有可用的预分配的溢出桶,则创建一个溢出桶 ovf = (*bmap)(newobject(t.bucket)) } h.incrnoverflow() // 更新溢出桶计数这个溢出桶计数可用来是否进行rehash的依据 if t.bucket.ptrdata == 0 { // 如果map中的key和value都不是指针类型那么将溢出桶指针添加到extra.overflow这个切片中 h.createOverflow() *h.extra.overflow = append(*h.extra.overflow, ovf) } b.setoverflow(t, ovf) return ovf } func (h *hmap) createOverflow() { if h.extra == nil { h.extra = new(mapextra) } if h.extra.overflow == nil { h.extra.overflow = new([]*bmap) } } func (b *bmap) overflow(t *maptype) *bmap { return *(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-sys.PtrSize)) } func (b *bmap) setoverflow(t *maptype, ovf *bmap) { *(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-sys.PtrSize)) = ovf } func (h *hmap) incrnoverflow() { if h.B \u0026lt; 16 { h.noverflow++ return } // 当h.B大于等于16时候有1/(1\u0026lt;\u0026lt;(h.B-15))的概率会更新h.noverflow // 比如h.B == 18时mask==7那么fastrand \u0026amp; 7 == 0的概率就是1/8 mask := uint32(1)\u0026lt;\u0026lt;(h.B-15) - 1 if fastrand()\u0026amp;mask == 0 { h.noverflow++ } } 映射的删除操作 # 在map中删除key-value时候都会调用runtime.mapdelete方法同访问操作一样Go编译器针对不同类型的key会采用优化版本函数\nkey 类型 方法 uint64 func mapdelete_fast64(t *maptype, h *hmap, key uint64) uint32 func mapdelete_fast32(t *maptype, h *hmap, key uint32) string func mapdelete_faststr(t *maptype, h *hmap, ky string) 这里面我们只大概分析通用删除操作mapdelete函数\n删除map中元素时候并不会释放内存。删除时候会清空映射中相应位置的key和value数据并将对应的tophash置为emptyOne。此外会检查当前单元旁边单元的状态是否也是空状态如果也是空状态那么会将当前单元和旁边空单元状态都改成emptyRest。\nfunc mapdelete(t *maptype, h *hmap, key unsafe.Pointer) { if h == nil || h.count == 0 { // 对nil map或者数量为0的map进行删除 if t.hashMightPanic() { t.hasher(key, 0) // see issue 23734 } return } if h.flags\u0026amp;hashWriting != 0 { // 有其他Goroutine正在写操作则直接panic throw(\u0026#34;concurrent map writes\u0026#34;) } hash := t.hasher(key, uintptr(h.hash0)) h.flags ^= hashWriting // 将写标志置为1删除操作也是一种写操作 bucket := hash \u0026amp; bucketMask(h.B) if h.growing() { growWork(t, h, bucket) } b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize))) bOrig := b top := tophash(hash) search: for ; b != nil; b = b.overflow(t) { for i := uintptr(0); i \u0026lt; bucketCnt; i++ { if b.tophash[i] != top { if b.tophash[i] == emptyRest { break search } continue } k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) k2 := k if t.indirectkey() { k2 = *((*unsafe.Pointer)(k2)) } if !t.key.equal(key, k2) { continue } // 清空key if t.indirectkey() { *(*unsafe.Pointer)(k) = nil } else if t.key.ptrdata != 0 { memclrHasPointers(k, t.key.size) } // 清空value e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize)) if t.indirectelem() { *(*unsafe.Pointer)(e) = nil } else if t.elem.ptrdata != 0 { memclrHasPointers(e, t.elem.size) } else { memclrNoHeapPointers(e, t.elem.size) } b.tophash[i] = emptyOne // 将tophash置为emptyOne // 下面代码是将当前单元附近的emptyOne状态的单元都改成emptyRest状态 if i == bucketCnt-1 { if b.overflow(t) != nil \u0026amp;\u0026amp; b.overflow(t).tophash[0] != emptyRest { goto notLast } } else { if b.tophash[i+1] != emptyRest { goto notLast } } for { b.tophash[i] = emptyRest if i == 0 { if b == bOrig { break } c := b for b = bOrig; b.overflow(t) != c; b = b.overflow(t) { } i = bucketCnt - 1 } else { i-- } if b.tophash[i] != emptyOne { break } } notLast: h.count-- break search } } if h.flags\u0026amp;hashWriting == 0 { throw(\u0026#34;concurrent map writes\u0026#34;) } h.flags \u0026amp;^= hashWriting } 扩容方式 # Go语言中映射扩容采用渐进式扩容避免一次性迁移数据过多造成性能问题。当对映射进行新增、更新时候会触发扩容操作然后进行扩容操作删除操作只会进行扩容操作不会进行触发扩容操作每次最多迁移2个bucket。扩容方式有两种类型\n等容量扩容 双倍容量扩容 等容量扩容 # 当对一个map不停进行新增和删除操作时候会创建了很多溢出桶而加载因子没有超过阈值不会发生双倍容量扩容这些桶利用率很低就会导致查询效率变慢。这时候就需要采用等容量扩容使用桶中数据更紧凑减少溢出桶数量从而提高查询效率。等容量扩容的条件是在未达到加载因子阈值情况下如果B小于15时溢出桶的数量大于2^BB大于等于15时候溢出桶数量大于2^15时候会进行等容量扩容操作\nfunc tooManyOverflowBuckets(noverflow uint16, B uint8) bool { if B \u0026gt; 15 { B = 15 } return noverflow \u0026gt;= uint16(1)\u0026lt;\u0026lt;(B\u0026amp;15) } 双倍容量扩容 # 双倍容量扩容指的是桶的数量变成旧桶数量的2倍。当映射的负载因子超过阈值时候会触发双倍容量扩容。\nfunc overLoadFactor(count int, B uint8) bool { return count \u0026gt; bucketCnt \u0026amp;\u0026amp; uintptr(count) \u0026gt; loadFactorNum*(bucketShift(B)/loadFactorDen) } 不论是等容量扩容还是双倍容量扩容都会新创建一个buckets然后将hmap.buckets指向这个新的bucketshmap.oldbuckets指向旧的buckets。\n进一步阅读 # 深度解密Go语言之map "},{"id":34,"href":"/concurrency/sync-cond/","title":"条件变量 - sync.Cond","section":"并发编程","content":" 条件变量 - sync.Cond # "},{"id":35,"href":"/type/empty_struct/","title":"空结构体","section":"数据类型与数据结构","content":" 空结构体 # 空结构体指的是没有任何字段的结构体。\n大小与内存地址 # 空结构体占用的内存空间大小为零字节,并且它们的地址可能相等也可能不等。当发生内存逃逸时候,它们的地址是相等的,都指向了 runtime.zerobase。\n// empty_struct.go type Empty struct{} //go:linkname zerobase runtime.zerobase var zerobase uintptr // 使用go:linkname编译指令将zerobase变量指向runtime.zerobase func main() { a := Empty{} b := struct{}{} fmt.Println(unsafe.Sizeof(a) == 0) // true fmt.Println(unsafe.Sizeof(b) == 0) // true fmt.Printf(\u0026#34;%p\\n\u0026#34;, \u0026amp;a) // 0x590d00 fmt.Printf(\u0026#34;%p\\n\u0026#34;, \u0026amp;b) // 0x590d00 fmt.Printf(\u0026#34;%p\\n\u0026#34;, \u0026amp;zerobase) // 0x590d00 c := new(Empty) d := new(Empty) fmt.Sprint(c, d) // 目的是让变量c和d发生逃逸 println(c) // 0x590d00 println(d) // 0x590d00 fmt.Println(c == d) // true e := new(Empty) f := new(Empty) println(e) // 0xc00008ef47 println(f) // 0xc00008ef47 fmt.Println(e == f) // flase } 从上面代码输出可以看到 a, b, zerobase 这三个变量的地址都是一样的最终指向的都是全局变量runtime.zerobase( runtime/malloc.go)。\n// base address for all 0-byte allocations var zerobase uintptr 我们可以通过下面方法再次来验证一下 runtime.zerobase 变量的地址是不是也是0x590d00\ngo build -o empty_struct empty_struct.go go tool nm ./empty_struct | grep 590d00 # 或者 objdump -t empty_struct | grep 590d00 执行上面命令输出以下的内容:\n590d00 D runtime.zerobase # 或者 0000000000590d00 g O .noptrbss\t0000000000000008 runtime.zerobase 从上面输出的内容可以看到 runtime.zerobase 的地址也是 0x590d00。\n接下来我们看看变量逃逸的情况\ngo run -gcflags=\u0026#34;-m -l\u0026#34; empty_struct.go # command-line-arguments ./empty_struct.go:15:2: moved to heap: a ./empty_struct.go:16:2: moved to heap: b ./empty_struct.go:18:13: ... argument does not escape ./empty_struct.go:18:31: unsafe.Sizeof(a) == 0 escapes to heap ./empty_struct.go:19:13: ... argument does not escape ./empty_struct.go:19:31: unsafe.Sizeof(b) == 0 escapes to heap ./empty_struct.go:20:12: ... argument does not escape ./empty_struct.go:21:12: ... argument does not escape ./empty_struct.go:22:12: ... argument does not escape ./empty_struct.go:24:10: new(Empty) escapes to heap ./empty_struct.go:25:10: new(Empty) escapes to heap ./empty_struct.go:26:12: ... argument does not escape ./empty_struct.go:29:13: ... argument does not escape ./empty_struct.go:29:16: c == d escapes to heap ./empty_struct.go:31:10: new(Empty) does not escape ./empty_struct.go:32:10: new(Empty) does not escape ./empty_struct.go:35:13: ... argument does not escape ./empty_struct.go:35:16: e == f escapes to heap 可以看到变量 c 和 d 逃逸到堆上,它们打印出来的都是 0x591d00且两者进行相等比较时候返回 true。而变量 e 和 f 打印出来的都是0xc00008ef47但两者进行相等比较时候却返回false。这因为Go有意为之的当空结构体变量未发生逃逸时候指向该变量的指针是不等的当空结构体变量发生逃逸之后指向该变量是相等的。这也就是 Go官方语法指南 所说的:\nPointers to distinct zero-size variables may or may not be equal\nGo语言比较操作符比较规则 注意:\n不论逃逸还是未逃逸我们都不应该对空结构体类型变量指向的内存地址是否一样做任何预期。 当一个结构体嵌入空结构体时,占用空间怎么计算? # 空结构体本身不占用空间,但是作为某结构体内嵌字段时候,有可能是占用空间的。具体计算规则如下:\n当空结构体是该结构体唯一的字段时该结构体是不占用空间的空结构体自然也不占用空间 当空结构体作为第一个字段或者中间字段时候,是不占用空间的 当空结构体作为最后一个字段时候,是占用空间的,大小跟其前一个字段保持一致 type s1 struct { a struct{} } type s2 struct { _ struct{} } type s3 struct { a struct{} b byte } type s4 struct { a struct{} b int64 } type s5 struct { a byte b struct{} c int64 } type s6 struct { a byte b struct{} } type s7 struct { a int64 b struct{} } type s8 struct { a struct{} b struct{} } func main() { fmt.Println(unsafe.Sizeof(s1{})) // 0 fmt.Println(unsafe.Sizeof(s2{})) // 0 fmt.Println(unsafe.Sizeof(s3{})) // 1 fmt.Println(unsafe.Sizeof(s4{})) // 8 fmt.Println(unsafe.Sizeof(s5{})) // 16 fmt.Println(unsafe.Sizeof(s6{})) // 2 fmt.Println(unsafe.Sizeof(s7{})) // 16 fmt.Println(unsafe.Sizeof(s8{})) // 0 } 当空结构体作为数组、切片的元素时候:\nvar a [10]int fmt.Println(unsafe.Sizeof(a)) // 80 var b [10]struct{} fmt.Println(unsafe.Sizeof(b)) // 0 var c = make([]struct{}, 10) fmt.Println(unsafe.Sizeof(c)) // 24即slice header的大小 用途 # 由于空结构体占用的空间大小为零,我们可以利用这个特性,完成一些功能,却不需要占用额外空间。\n阻止unkeyed方式初始化结构体 # type MustKeydStruct struct { Name string Age int _ struct{} } func main() { persion := MustKeydStruct{Name: \u0026#34;hello\u0026#34;, Age: 10} fmt.Println(persion) persion2 := MustKeydStruct{\u0026#34;hello\u0026#34;, 10} //编译失败,提示: too few values in MustKeydStruct{...} fmt.Println(persion2) } 实现集合数据结构 # 集合数据结构我们可以使用map来实现只关心key不必关心value我们就可以值设置为空结构体类型变量或者底层类型是空结构体的变量。\npackage main import ( \u0026#34;fmt\u0026#34; ) type Set struct { items map[interface{}]emptyItem } type emptyItem struct{} var itemExists = emptyItem{} func NewSet() *Set { set := \u0026amp;Set{items: make(map[interface{}]emptyItem)} return set } // 添加元素到集合 func (set *Set) Add(item interface{}) { set.items[item] = itemExists } // 从集合中删除元素 func (set *Set) Remove(item interface{}) { delete(set.items, item) } // 判断元素是否存在集合中 func (set *Set) Contains(item interface{}) bool { _, contains := set.items[item] return contains } // 返回集合大小 func (set *Set) Size() int { return len(set.items) } func main() { set := NewSet() set.Add(\u0026#34;hello\u0026#34;) set.Add(\u0026#34;world\u0026#34;) fmt.Println(set.Contains(\u0026#34;hello\u0026#34;)) fmt.Println(set.Contains(\u0026#34;Hello\u0026#34;)) fmt.Println(set.Size()) } 作为通道的信号传输 # 使用通道时候,有时候我们只关心是否有数据从通道内传输出来,而不关心数据内容,这时候通道数据相当于一个信号,比如我们实现退出时候。下面例子是基于通道实现的信号量。\n// empty struct var empty = struct{}{} // Semaphore is empty type chan type Semaphore chan struct{} // P used to acquire n resources func (s Semaphore) P(n int) { for i := 0; i \u0026lt; n; i++ { s \u0026lt;- empty } } // V used to release n resouces func (s Semaphore) V(n int) { for i := 0; i \u0026lt; n; i++ { \u0026lt;-s } } // Lock used to lock resource func (s Semaphore) Lock() { s.P(1) } // Unlock used to unlock resource func (s Semaphore) Unlock() { s.V(1) } // NewSemaphore return semaphore func NewSemaphore(N int) Semaphore { return make(Semaphore, N) } 进一步阅读 # The empty struct "},{"id":36,"href":"/concurrency/sync-waitgroup/","title":"等待组 - sync.WaitGroup","section":"并发编程","content":" 等待组 - sync.WaitGroup # 源码分析 # type WaitGroup struct { noCopy noCopy // waitgroup是不能够拷贝复制的是通过go vet来检测实现 /* waitgroup使用一个int64来计数高32位用来add计数低32位用来记录waiter数量。 若要原子性更新int64就必须保证该int64对齐系数是8即64位对齐。 对于64位系统直接使用一个int64类型字段就能保证原子性要求但对32位系统就不行了。 所以实现的时候并没有直接一个int64 而是使用[3]int32数组若[0]int32地址恰好是8对齐的那就waitgroup int64 = [0]int32 + [1]int32 否则一定是4对齐的 故[0]int32不用恰好错开了4字节此时[1]int32一定是8对齐的。此时waitgroup int64 = [1]int32 + [2]int32 通过这个技巧恰好满足32位和64位系统下int64都能原子性操作 */ state1 [3]uint32 // waitgroup对齐系数是4 } func (wg *WaitGroup) state() (statep *uint64, semap *uint32) { // 当state1是8对齐的则返回低8字节(statep)用来计数即state1[0]是add计数state1[1]是waiter计数 if uintptr(unsafe.Pointer(\u0026amp;wg.state1))%8 == 0 { return (*uint64)(unsafe.Pointer(\u0026amp;wg.state1)), \u0026amp;wg.state1[2] } else { // 反之则返回高8字节用来计数即state1[1]是add计数state1[2]是waiter计数 return (*uint64)(unsafe.Pointer(\u0026amp;wg.state1[1])), \u0026amp;wg.state1[0] } } // Add方法用来更新add计数器。即将原来计数值加上deltadelta可以为负值 // waitgroup的Done方法本质上就是Add(-1) // Add更新之后的计数器值不能小于0。当计数器值等于0时候会释放信号所有调用Wait方法而阻塞的Goroutine不再阻塞释放的信号量=waiter计数 func (wg *WaitGroup) Add(delta int) { statep, semap := wg.state() if race.Enabled { // 竞态检查,忽略不看 _ = *statep // trigger nil deref early if delta \u0026lt; 0 { // Synchronize decrements with Wait. race.ReleaseMerge(unsafe.Pointer(wg)) } race.Disable() defer race.Enable() } state := atomic.AddUint64(statep, uint64(delta)\u0026lt;\u0026lt;32) // delta左移32位然后原子性更新statep值并返回更新后的statep值 v := int32(state \u0026gt;\u0026gt; 32) // state高位的4字节是add计数赋值给v w := uint32(state) // state低位的4字节是waiter计数赋值给w if v \u0026lt; 0 { // add计数不能为负值。 panic(\u0026#34;sync: negative WaitGroup counter\u0026#34;) } // Add方法与Wait方法不能并发调用 if w != 0 \u0026amp;\u0026amp; delta \u0026gt; 0 \u0026amp;\u0026amp; v == int32(delta) { panic(\u0026#34;sync: WaitGroup misuse: Add called concurrently with Wait\u0026#34;) } if v \u0026gt; 0 || w == 0 { // add计数大于0或者waiter计数等于0直接返回不执行后面逻辑。 return } // statep指向state1字段其指向的值和state进行比较如果不一样说明存在并发调用了Add和Wait方法 // 此时v = 0, w \u0026gt; 0这个时候waitgroup的add计数和waiter计数不能再更改了。 // *statep != state情况举例假定当前groutine是g1执行到此处时, // 恰好另外一个groutine g2并发调用了Wait方法 // 那么waitgroup的state1字段会更新而g1中w的值还是g2调用Wait方法之前的waiter数 // 这会导致总有一个g永远得不到释放信号从而造成g泄漏。所以此处要进行panic判断 if *statep != state { panic(\u0026#34;sync: WaitGroup misuse: Add called concurrently with Wait\u0026#34;) } *statep = 0 // 重置计数器为0 for ; w != 0; w-- { // 有w个waiter则释放出w个信号 runtime_Semrelease(semap, false, 0) } } // Done() == Add(-1) func (wg *WaitGroup) Done() { wg.Add(-1) } // Wait会阻塞当前goroutine直到add计数器值为0 func (wg *WaitGroup) Wait() { statep, semap := wg.state() for { state := atomic.LoadUint64(statep) v := int32(state \u0026gt;\u0026gt; 32) w := uint32(state) // 使用for + cas进制原子性更新waiter计数 if atomic.CompareAndSwapUint64(statep, state, state+1) { // 更新成功后开始获取信号未获取到信号的话则当前g一直阻塞 runtime_Semacquire(semap) if *statep != 0 { panic(\u0026#34;sync: WaitGroup is reused before previous Wait has returned\u0026#34;) } return } } } 总结 # waitgroup是不能值传递的 Add方法的传值可以是负数但加上该传值之后的waitgroup计数器值不能是负值 Done方法实际上调用的是Add(-1) Add方法和Wait方法不能并发调用 Wait方法可以多次调用调用此方法的goroutine会阻塞一直阻塞到waitgroup计数器值变为0。 "},{"id":37,"href":"/type-system/","title":"类型系统","section":"简介","content":" 类型系统 # 知者不惑,仁者不忧,勇者不惧。\n类型系统 接口 反射 "},{"id":38,"href":"/type-system/type/","title":"类型系统","section":"类型系统","content":" 类型系统 # "},{"id":39,"href":"/concurrency/sync-pool/","title":"缓冲池 - sync.Pool","section":"并发编程","content":" 缓冲池 - sync.Pool # A Pool is a set of temporary objects that may be individually saved and retrieved.\nAny item stored in the Pool may be removed automatically at any time without notification. If the Pool holds the only reference when this happens, the item might be deallocated.\nA Pool is safe for use by multiple goroutines simultaneously.\nPool\u0026rsquo;s purpose is to cache allocated but unused items for later reuse, relieving pressure on the garbage collector. That is, it makes it easy to build efficient, thread-safe free lists. However, it is not suitable for all free lists\nsync.Pool提供了临时对象缓存池存在池子的对象可能在任何时刻被自动移除我们对此不能做任何预期。sync.Pool可以并发使用它通过复用对象来减少对象内存分配和GC的压力。当负载大的时候临时对象缓存池会扩大缓存池中的对象会在每2个GC循环中清除。\nsync.Pool拥有两个对象存储容器local pool和victim cache。local pool与victim cache相似相当于primary cache。当获取对象时优先从local pool中查找若未找到则再从victim cache中查找若也未获取到则调用New方法创建一个对象返回。当对象放回sync.Pool时候会放在local pool中。当GC开始时候首先将victim cache中所有对象清除然后将local pool容器中所有对象都会移动到victim cache中所以说缓存池中的对象会在每2个GC循环中清除。\nvictim cache是从CPU缓存中借鉴的概念。下面是维基百科中关于victim cache的定义\n所谓受害者缓存Victim Cache是一个与直接匹配或低相联缓存并用的、容量很小的全相联缓存。当一个数据块被逐出缓存时并不直接丢弃而是暂先进入受害者缓存。如果受害者缓存已满就替换掉其中一项。当进行缓存标签匹配时在与索引指向标签匹配的同时并行查看受害者缓存如果在受害者缓存发现匹配就将其此数据块与缓存中的不匹配数据块做交换同时返回给处理器。\n受害者缓存的意图是弥补因为低相联度造成的频繁替换所损失的时间局部性。\n用法 # sync.Pool提供两个接口Get和Put分别用于从缓存池中获取临时对象和将临时对象放回到缓存池中\nfunc (p *Pool) Get() interface{} func (p *Pool) Put(x interface{}) 示例1 # type A struct { Name string } func (a *A) Reset() { a.Name = \u0026#34;\u0026#34; } var pool = sync.Pool{ New: func() interface{} { return new(A) }, } func main() { objA := pool.Get().(*A) objA.Reset() // 重置一下对象数据,防止脏数据 defer pool.Put(objA) objA.Name = \u0026#34;test123\u0026#34; fmt.Println(objA) } 接下来我们进行基准测试下未使用和使用sync.Pool情况\ntype A struct { Name string } func (a *A) Reset() { a.Name = \u0026#34;\u0026#34; } var pool = sync.Pool{ New: func() interface{} { return new(A) }, } func BenchmarkWithoutPool(b *testing.B) { var a *A b.ReportAllocs() b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { for j := 0; j \u0026lt; 10000; j++ { a = new(A) a.Name = \u0026#34;tink\u0026#34; } } } func BenchmarkWithPool(b *testing.B) { var a *A b.ReportAllocs() b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { for j := 0; j \u0026lt; 10000; j++ { a = pool.Get().(*A) a.Reset() a.Name = \u0026#34;tink\u0026#34; pool.Put(a) // 一定要记得放回操作否则退化到每次都需要New操作 } } } 基准测试结果如下:\n# go test -benchmem -run=^$ -bench . goos: darwin goarch: amd64 BenchmarkWithoutPool-8 3404 314232 ns/op 160001 B/op 10000 allocs/op BenchmarkWithPool-8 5870 220399 ns/op 0 B/op 0 allocs/op 从上面基准测试中我们可以看到使用sync.Pool之后每次执行的耗时由314232ns降到220399ns降低了29.8%每次执行的内存分配降到0注意这是平均值并不是没进行过内存分配只不过是绝大数操作没有进行过内存分配最终平均下来四舍五入之后为0。\n示例2 # go-redis/redis项目中实现连接池时候使用到sync.Pool来创建定时器\n// 创建timer Pool var timers = sync.Pool{ New: func() interface{} { // 定义创建临时对象创建方法 t := time.NewTimer(time.Hour) t.Stop() return t }, } func (p *ConnPool) waitTurn(ctx context.Context) error { select { case \u0026lt;-ctx.Done(): return ctx.Err() default: } ... timer := timers.Get().(*time.Timer) // 从缓存池中取出对象 timer.Reset(p.opt.PoolTimeout) select { ... case \u0026lt;-timer.C: timers.Put(timer) // 将对象放回到缓存池中,以便下次使用 atomic.AddUint32(\u0026amp;p.stats.Timeouts, 1) return ErrPoolTimeout } 数据结构 # sync.Pool底层数据结构体是Pool结构体( sync/pool.go)\ntype Pool struct { noCopy noCopy // nocopy机制用于go vet命令检查是否复制后使用 local unsafe.Pointer // 指向[P]poolLocal数组P等于runtime.GOMAXPROCS(0) localSize uintptr // local数组大小即[P]poolLocal大小 victim unsafe.Pointer // 指向上一个gc循环前的local victimSize uintptr // victim数组大小 New func() interface{} // 创建临时对象的方法当从local数组和victim数组中都没有找到临时对象缓存那么会调用此方法现场创建一个 } Pool.local指向大小为runtime.GOMAXPROCS(0)的poolLocal数组相当于大小为runtime.GOMAXPROCS(0)的缓存槽(solt)。每一个P都会通过其ID关联一个槽位上的poolLocal比如对于ID=1的P关联的poolLocal就是[1]poolLocal这个poolLocal属于per-P级别的poolLocal与P关联的M和G可以无锁的操作此poolLocal。\npoolLocal结构如下\ntype poolLocal struct { poolLocalInternal // 内嵌poolLocalInternal结构体 // 进行一些padding阻止false share pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte } type poolLocalInternal struct { private interface{} // 私有属性,快速存取临时对象 shared poolChain // shared是一个双端链表 } 为啥不直接把所有poolLocalInternal字段都写到poolLocal里面而是采用内嵌形式这是为了好计算出poolLocal的padding大小。\npoolChain结构如下\ntype poolChain struct { // 指向双向链表头 head *poolChainElt // 指向双向链表尾 tail *poolChainElt } type poolChainElt struct { poolDequeue next, prev *poolChainElt } type poolDequeue struct { // headTail高32位是环形队列的head // headTail低32位是环形队列的tail // [tail, head)范围是队列所有元素 headTail uint64 vals []eface // 用于存放临时对象大小是2的倍数最小尺寸是8最大尺寸是dequeueLimit } type eface struct { typ, val unsafe.Pointer } poolLocalInternal的shared字段指向是一个双向链表(doubly-linked list)链表每一个元素都是poolChainElt类型poolChainElt是一个双端队列Double-ended Queue简写deque并且链表中每一个元素的队列大小是2的倍数且是前一个元素队列大小的2倍。poolChainElt是基于环形队列(Circular Queue)实现的双端队列。\n若poolLocal属于当前P那么可以对shared进行pushHead和popHead操作而其他P只能进行popTail操作。当前其他P进行popTail操作时候会检查链表中节点的poolChainElt是否为空若是空则会drop掉该节点这样当popHead操作时候避免去查一个空的poolChainElt。\npoolDequeue中的headTail字段的高32位记录的是环形队列的head其低32位是环形队列的tail。vals是环形队列的底层数组。\nGet操作 # 我们来看下如何从sync.Pool中取出临时对象。下面代码已去掉竞态检测相关代码。\nfunc (p *Pool) Get() interface{} { l, pid := p.pin() // 返回当前per-P级poolLocal和P的id x := l.private l.private = nil if x == nil { x, _ = l.shared.popHead() if x == nil { x = p.getSlow(pid) } } runtime_procUnpin() if x == nil \u0026amp;\u0026amp; p.New != nil { x = p.New() } return x } 上面代码执行流程如下:\n首先通过调用pin方法获取当前G关联的P对应的poolLocal和该P的id 接着查看poolLocal的private字段是否存放了对象如果有的话那么该字段存放的对象可直接返回这属于最快路径。 若poolLocal的private字段未存放对象那么就尝试从poolLocal的双端队列中取出对象这个操作是lock-free的。 若G关联的per-P级poolLocal的双端队列中没有取出来对象那么就尝试从其他P关联的poolLocal中偷一个。若从其他P关联的poolLocal没有偷到一个那么就尝试从victim cache中取。 若步骤4中也没没有取到缓存对象那么只能调用pool.New方法新创建一个对象。 我们来看下pin方法\nfunc (p *Pool) pin() (*poolLocal, int) { pid := runtime_procPin() // 禁止M被抢占 s := atomic.LoadUintptr(\u0026amp;p.localSize) // 原子性加载local pool的大小 l := p.local if uintptr(pid) \u0026lt; s { // 如果local pool大小大于P的id那么从local pool取出来P关联的poolLocal return indexLocal(l, pid), pid } /* * 当p.local指向[P]poolLocal数组还没有创建 * 或者通过runtime.GOMAXPROCS()调大P数量时候都可能会走到此处逻辑 */ return p.pinSlow() } func (p *Pool) pinSlow() (*poolLocal, int) { runtime_procUnpin() allPoolsMu.Lock() // 加锁 defer allPoolsMu.Unlock() pid := runtime_procPin() s := p.localSize l := p.local if uintptr(pid) \u0026lt; s { // 加锁后再次判断一下P关联的poolLocal是否存在 return indexLocal(l, pid), pid } if p.local == nil { // 将p记录到全局变量allPools中执行GC钩子时候会使用到 allPools = append(allPools, p) } size := runtime.GOMAXPROCS(0) // 根据P数量创建p.local local := make([]poolLocal, size) atomic.StorePointer(\u0026amp;p.local, unsafe.Pointer(\u0026amp;local[0])) atomic.StoreUintptr(\u0026amp;p.localSize, uintptr(size)) return \u0026amp;local[pid], pid } func indexLocal(l unsafe.Pointer, i int) *poolLocal { // 通过uintptr和unsafe.Pointer取出[P]poolLocal数组中索引i对应的poolLocal lp := unsafe.Pointer(uintptr(l) + uintptr(i)*unsafe.Sizeof(poolLocal{})) return (*poolLocal)(lp) } pin方法中会首先调用runtime_procPin来设置M禁止被抢占。GMP调度模型中M必须绑定到P之后才能执行G禁止M被抢占就是禁止M绑定的P被剥夺走相当于pin processor。\npin方法中为啥要首先禁止M被抢占这是因为我们需要找到per-P级的poolLocal如果在此过程中发生M绑定的P被剥夺那么我们找到的就可能是其他M的per-P级poolLocal没有局部性可言了。\nruntime_procPin方法是通过给M加锁实现禁止被抢占的即m.locks++。当m.locks==0时候m是可以被抢占的:\n//go:linkname sync_runtime_procPin sync.runtime_procPin //go:nosplit func sync_runtime_procPin() int { return procPin() } //go:linkname sync_runtime_procUnpin sync.runtime_procUnpin //go:nosplit func sync_runtime_procUnpin() { procUnpin() } //go:nosplit func procPin() int { _g_ := getg() mp := _g_.m mp.locks++ // 给m加锁 return int(mp.p.ptr().id) } //go:nosplit func procUnpin() { _g_ := getg() _g_.m.locks-- } go:linkname是编译指令用于将私有函数或者变量在编译阶段链接到指定位置。从上面代码中我们可以看到sync.runtime_procPin和sync.runtime_procUnpin最终实现方法是sync_runtime_procPin和sync_runtime_procUnpin。\npinSlow方法用到的allPoolsMu和allPools是全局变量\nvar ( allPoolsMu Mutex // allPools is the set of pools that have non-empty primary // caches. Protected by either 1) allPoolsMu and pinning or 2) // STW. allPools []*Pool // oldPools is the set of pools that may have non-empty victim // caches. Protected by STW. oldPools []*Pool ) 接下我们来看Get流程中步骤3的实现\nfunc (c *poolChain) popHead() (interface{}, bool) { d := c.head // 从双向链表的头部开始 for d != nil { if val, ok := d.popHead(); ok { // 从双端队列头部取对象缓存,若取到则返回 return val, ok } // 若未取到,则尝试从上一个节点开始取 d = loadPoolChainElt(\u0026amp;d.prev) } return nil, false } func loadPoolChainElt(pp **poolChainElt) *poolChainElt { return (*poolChainElt)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(pp)))) } 最后我们看下Get流程中步骤4的实现\nfunc (p *Pool) getSlow(pid int) interface{} { size := atomic.LoadUintptr(\u0026amp;p.localSize) locals := p.local for i := 0; i \u0026lt; int(size); i++ { // 尝试从其他P关联的poolLocal取一个 // 类似GMP调度模型从其他P的runable G队列中偷一个 // 偷的时候是双向链表尾部开始偷这个和从本地P的poolLocal取恰好是反向的 l := indexLocal(locals, (pid+i+1)%int(size)) if x, _ := l.shared.popTail(); x != nil { return x } } // 若从其他P的poolLocal没有偷到则尝试从victim cache取 size = atomic.LoadUintptr(\u0026amp;p.victimSize) if uintptr(pid) \u0026gt;= size { return nil } locals = p.victim l := indexLocal(locals, pid) if x := l.private; x != nil { l.private = nil return x } for i := 0; i \u0026lt; int(size); i++ { l := indexLocal(locals, (pid+i)%int(size)) if x, _ := l.shared.popTail(); x != nil { return x } } atomic.StoreUintptr(\u0026amp;p.victimSize, 0) return nil } func (c *poolChain) popTail() (interface{}, bool) { d := loadPoolChainElt(\u0026amp;c.tail) if d == nil { return nil, false } for { d2 := loadPoolChainElt(\u0026amp;d.next) if val, ok := d.popTail(); ok { // 从双端队列的尾部出队 return val, ok } if d2 == nil { // 若下一个节点为空,则返回。说明链表已经遍历完了 return nil, false } // 下面代码会将当前节点从链表中删除掉。 // 为什么要删掉它,因为该节点的队列里面有没有对象缓存了, // 删掉之后下次本地P取的时候不必遍历此空节点了 if atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(\u0026amp;c.tail)), unsafe.Pointer(d), unsafe.Pointer(d2)) { storePoolChainElt(\u0026amp;d2.prev, nil) } d = d2 } } func storePoolChainElt(pp **poolChainElt, v *poolChainElt) { atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(pp)), unsafe.Pointer(v)) } 我们画出Get流程中步骤3和4的中从local pool取对象示意图\n总结下从local pool流程是\n首先从当前P的localPool的私有属性private上取 若未取到则从localPool中由队列组成的双向链表上取方向是从头部节点队列开始依次往上查找 如果当前P的localPool中没有取到则尝试从其他P的localPool偷一个方向是从尾部节点队列开始依次向下查找若当前节点为空会把当前节点从链表中删掉。 Put操作 # 接下来我们还看下对象归还操作:\nfunc (p *Pool) Put(x interface{}) { if x == nil { return } l, _ := p.pin() // 返回当前P的localPool if l.private == nil { // 若localPool的private没有存放对象那就存放在private上这是最快路径。取的时候优先从private上面取 l.private = x x = nil } if x != nil { // 入队 l.shared.pushHead(x) } runtime_procUnpin() } 流程步骤如下:\n调用pin方法返回当前P的localPool 若当前P的localPool的private属性没有存放对象那就存放其上面这是最快路径取的时候优先从private上面取 若当前P的localPool的private属性已经存放了归还的对象那么就将对象入队存储。 我们接着看步骤3中代码\nfunc (c *poolChain) pushHead(val interface{}) { d := c.head if d == nil { // 双向链表头部节点为空,则创建 // 头部节点的队列长度为8 const initSize = 8 d = new(poolChainElt) d.vals = make([]eface, initSize) c.head = d storePoolChainElt(\u0026amp;c.tail, d) } // 将归还对象入队 if d.pushHead(val) { return } // 若归还对象入队失败,说明当前头部节点的队列已满,会走后面的逻辑: // 创建新的队列节点新的队列长度是当前节点队列的2倍最大不超过dequeueLimit // 然后将新的队列节点设置为双向链表的头部 newSize := len(d.vals) * 2 if newSize \u0026gt;= dequeueLimit { newSize = dequeueLimit } d2 := \u0026amp;poolChainElt{prev: d} // 新节点的prev指针指向旧的头部节点 d2.vals = make([]eface, newSize) c.head = d2 // 新节点成为双向链表的头部节点 storePoolChainElt(\u0026amp;d.next, d2) // 旧的头部节点next指针指向新节点 d2.pushHead(val) // 归还的临时对象入队新节点的队列中 } 从上面代码可以看到创建的双向链表第一个节点队列的大小为8第二个节点队列大小为16第三个节点队列大小为32依次类推最大为dequeueLimit。每个节点队列的大小都是2的n次幂这是因为队列使用环形队列结构实现的底层是数组同前面介绍的映射一样定位位置时候取余运算可以改成与运算更高效。\n我们画出双向链表中头部节点队列未满和已满两种情况下示意图\n双端队列 - poolDequeue # 从上面Get操作和Put操作中我们可以看到都是对poolChain操作poolChain操作最终都是对双端队列poolDequeue的操作Get操作对应poolDequeue的popHead和popTail, Put操作对应poolDequeue的pushHead。\n再看一下poolDequeue结构体定义\ntype poolDequeue struct { headTail uint64 vals []eface } type eface struct { typ, val unsafe.Pointer } type dequeueNil *struct{} poolDequeue是一个无锁的lock-free)、固定大小的fixed-size) 单一生产者(single-producer),多消费者multi-consumer队列。单一生产者可以从队列头部push和pop元素消费者可以从队列尾部pop元素。poolDequeue是基于环形队列实现的双端队列。所谓双端队列double-ended queue双端队列简写deque是一种具有队列和栈的性质的数据结构。双端队列中的元素可以从两端弹出其限定插入和删除操作在表的两端进行。poolDequeue支持在两端删除操作只支持在head端插入。\npoolDequeue的headTail字段是由环形队列的head索引即rear索引和tail索引即front索引打包而来headTail是64位无符号整形其高32位是head索引低32位是tail索引\nconst dequeueBits = 32 func (d *poolDequeue) unpack(ptrs uint64) (head, tail uint32) { const mask = 1\u0026lt;\u0026lt;dequeueBits - 1 head = uint32((ptrs \u0026gt;\u0026gt; dequeueBits) \u0026amp; mask) tail = uint32(ptrs \u0026amp; mask) return } func (d *poolDequeue) pack(head, tail uint32) uint64 { const mask = 1\u0026lt;\u0026lt;dequeueBits - 1 return (uint64(head) \u0026lt;\u0026lt; dequeueBits) | uint64(tail\u0026amp;mask) } head索引指向的是环形队列中下一个需要填充的槽位即新入队元素将会写入的位置tail索引指向的是环形队列中最早入队元素位置。环形队列中元素位置范围是[tail, head)。\n我们知道环形队列中为了解决head == tail即可能是队列为空也可能是队列空间全部占满的二义性有两种解决办法1. 空余单元法, 2. 记录队列元素个数法。\n采用空余单元法时队列中永远有一个元素空间不使用即队列中元素个数最多有QueueSize -1个。此时队列为空和占满的判断条件如下\nhead == tail // 队列为空 (head + 1)%QueueSize == tail // 队列已满 而poolDequeue采用的是记录队列中元素个数法相比空余单元法好处就是不会浪费一个队列元素空间。后面章节讲到的有缓存通道使用到的环形队列也是采用的这种方案。这种方案队列为空和占满的判断条件如下\nhead == tail // 队列为空 tail + nums_of_elment_in_queue == head 删除操作 # 删除操作即出队操作。\nfunc (d *poolDequeue) popHead() (interface{}, bool) { var slot *eface for { ptrs := atomic.LoadUint64(\u0026amp;d.headTail) head, tail := d.unpack(ptrs) if tail == head { // 队列为空情况 return nil, false } head-- ptrs2 := d.pack(head, tail) // 先原子性更新head索引信息更新成功则取出队列最新的元素所在槽位地址 if atomic.CompareAndSwapUint64(\u0026amp;d.headTail, ptrs, ptrs2) { slot = \u0026amp;d.vals[head\u0026amp;uint32(len(d.vals)-1)] break } } val := *(*interface{})(unsafe.Pointer(slot)) // 取出槽位对应存储的值 if val == dequeueNil(nil) { val = nil } // 不同与popTailpopHead是没有竞态问题所以可以直接将其复制为eface{} *slot = eface{} return val, true } func (d *poolDequeue) popTail() (interface{}, bool) { var slot *eface for { ptrs := atomic.LoadUint64(\u0026amp;d.headTail) head, tail := d.unpack(ptrs) if tail == head { // 队列为空情况 return nil, false } ptrs2 := d.pack(head, tail+1) // 先原子性更新tail索引信息更新成功则取出队列最后一个元素所在槽位地址 if atomic.CompareAndSwapUint64(\u0026amp;d.headTail, ptrs, ptrs2) { slot = \u0026amp;d.vals[tail\u0026amp;uint32(len(d.vals)-1)] break } } val := *(*interface{})(unsafe.Pointer(slot)) if val == dequeueNil(nil) { val = nil } /** 理解后面代码,我们需意识到*slot = eface{}或slot = *eface(nil)不是一个原子操作。 这是因为每个槽位存放2个8字节的unsafe.Pointer。而Go atomic包是不支持16字节原子操作只能原子性操作solt中的其中一个字段。 后面代码中先将solt.val置为nil然后原子操作solt.typ那么pushHead操作时候只需要判断solt.typ是否nil既可以判断这个槽位完全被清空了当solt.typ==nil时候solt.val一定是nil。 */ slot.val = nil atomic.StorePointer(\u0026amp;slot.typ, nil) return val, true } 插入操作 # 插入操作即入队操作。\nfunc (d *poolDequeue) pushHead(val interface{}) bool { ptrs := atomic.LoadUint64(\u0026amp;d.headTail) head, tail := d.unpack(ptrs) if (tail+uint32(len(d.vals)))\u0026amp;(1\u0026lt;\u0026lt;dequeueBits-1) == head { // 队列已写满情况 return false } slot := \u0026amp;d.vals[head\u0026amp;uint32(len(d.vals)-1)] typ := atomic.LoadPointer(\u0026amp;slot.typ) if typ != nil { // 说明有其他Goroutine正在pop此槽位当pop完成之后会drop掉此槽位队列还是保持写满状态 return false } if val == nil { val = dequeueNil(nil) } *(*interface{})(unsafe.Pointer(slot)) = val atomic.AddUint64(\u0026amp;d.headTail, 1\u0026lt;\u0026lt;dequeueBits) return true } pool回收 # 文章开头介绍sync.Pool时候我们提到缓存池中的对象会在每2个GC循环中清除。我们现在看看这块逻辑\nfunc poolCleanup() { for _, p := range oldPools { // 清空victim cache p.victim = nil p.victimSize = 0 } // 将primary cache(local pool)移动到victim cache for _, p := range allPools { p.victim = p.local p.victimSize = p.localSize p.local = nil p.localSize = 0 } oldPools, allPools = allPools, nil } func init() { runtime_registerPoolCleanup(poolCleanup) } sync.Pool通过在包初始化时候使用runtime_registerPoolCleanup注册GC的钩子poolCleanup来进行pool回收处理。runtime_registerPoolCleanup函数通过编译指令go:linkname链接到 runtime/mgc.go 文件中 sync_runtime_registerPoolCleanup 函数:\nvar poolcleanup func() //go:linkname sync_runtime_registerPoolCleanup sync.runtime_registerPoolCleanup func sync_runtime_registerPoolCleanup(f func()) { poolcleanup = f } func clearpools() { // clear sync.Pools if poolcleanup != nil { poolcleanup() } ... } // gc入口 func gcStart(trigger gcTrigger) { ... clearpools() ... } poolCleanup函数会在一次GC时候会将local pool中缓存对象移动到victim cache中然后在下一次GC时候清空victim cache对象。\n进一步阅读 # Go: Understand the Design of Sync.Pool CPU缓存-受害者缓存 "},{"id":40,"href":"/feature/","title":"语言特性","section":"简介","content":" 语言特性 # 仰之弥高,钻之弥坚。\n逗号ok模式 遍历 - for-range语法 延迟执行 - defer语法 通道选择器 - select语法 恐慌与恢复 - panic/recover "},{"id":41,"href":"/concurrency/sync-rwmutex/","title":"读写锁 - sync.RWMutex","section":"并发编程","content":" 读写锁 - sync.RWMutex # RWMutex是Go语言中内置的一个reader/writer锁用来解决读者-写者问题Readerswriters problem。在任意一时刻一个RWMutex只能由任意数量的reader持有或者只能由一个writer持有。\n读者-写者问题 # 读者-写者问题Readerswriters problem描述了计算机并发处理读写数据遇到的问题如何保证数据完整性、一致性。解决读者-写者问题需保证对于一份资源操作满足以下下条件:\n读写互斥 写写互斥 允许多个读者同时读取 解决读者-写者问题可以采用读者优先readers-preference方案或者写者优先writers-preference方案。\n读者优先readers-preference读者优先是读操作优先于写操作即使写操作提出申请资源但只要还有读者在读取操作就还允许其他读者继续读取操作直到所有读者结束读取才开始写。读优先可以提供很高的并发处理性能但是在频繁读取的系统中会长时间写阻塞导致写饥饿。\n写者优先writers-preference写者优先是写操作优先于读操作如果有写者提出申请资源在申请之前已经开始读取操作的可以继续执行读取但是如果再有读者申请读取操作则不能够读取只有在所有的写者写完之后才可以读取。写者优先解决了读者优先造成写饥饿的问题。但是若在频繁写入的系统中会长时间读阻塞导致读饥饿。\nRWMutex设计采用写者优先方法保证写操作优先处理。\n源码分析 # 下面分析的源码进行精简处理去掉了race检查功能的代码。\nRWMutex的定义 # type RWMutex struct { w Mutex // 互斥锁 writerSem uint32 // writers信号量 readerSem uint32 // readers信号量 readerCount int32 // reader数量 readerWait int32 // writer申请锁时候已经申请到锁的reader的数量 } const rwmutexMaxReaders = 1 \u0026lt;\u0026lt; 30 // 最大reader数用于反转readerCount RLock/RUnlock的实现 # func (rw *RWMutex) RLock() { if atomic.AddInt32(\u0026amp;rw.readerCount, 1) \u0026lt; 0 { // 如果rw.readerCount为负数说明此时已有一个writer持有锁或者正在申请锁。 runtime_SemacquireMutex(\u0026amp;rw.readerSem, false, 0) // 此时reader休眠阻塞在readerSem信号上等待唤醒 } } func (rw *RWMutex) RUnlock() { if r := atomic.AddInt32(\u0026amp;rw.readerCount, -1); r \u0026lt; 0 { // r小于0说明此时有等待请求锁的writer rw.rUnlockSlow(r) } } func (rw *RWMutex) rUnlockSlow(r int32) { if r+1 == 0 || r+1 == -rwmutexMaxReaders { // RLock之前已经进行了RUnlock操作 throw(\u0026#34;sync: RUnlock of unlocked RWMutex\u0026#34;) } if atomic.AddInt32(\u0026amp;rw.readerWait, -1) == 0 { // 此时是最后一个获取到锁的reader进行RUnlock操作那么释放writerSem信号唤醒等待的writer来获取锁。 runtime_Semrelease(\u0026amp;rw.writerSem, false, 1) } } Lock/Unlock的实现 # func (rw *RWMutex) Lock() { rw.w.Lock() // 加互斥锁阻塞其他writer进行Lock操作保证写-写互斥。 // 将rw.readerCount 更改为rw.readerCount - rwmutexMaxReaders // 此时rw.readerCount由一个正数转变成一个负数这种方式既能保持记录reader数量又能表明有writer正在请求锁 r := atomic.AddInt32(\u0026amp;rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders if r != 0 \u0026amp;\u0026amp; atomic.AddInt32(\u0026amp;rw.readerWait, r) != 0 { // r!=0表明此时有reader持有锁则当前writer只能阻塞等待但为了保证写优先需要readerWait记录当前已获取到锁的读者数量 runtime_SemacquireMutex(\u0026amp;rw.writerSem, false, 0) } } func (rw *RWMutex) Unlock() { r := atomic.AddInt32(\u0026amp;rw.readerCount, rwmutexMaxReaders) if r \u0026gt;= rwmutexMaxReaders { // Lock之前先进行了Unlock操作 throw(\u0026#34;sync: Unlock of unlocked RWMutex\u0026#34;) } for i := 0; i \u0026lt; int(r); i++ { // 释放信号唤醒阻塞的reader们 runtime_Semrelease(\u0026amp;rw.readerSem, false, 0) } rw.w.Unlock() // 是否互锁锁允许其他writer进行获取锁操作了 } 对于读者优先readers-preference的读写锁只需要一个readerCount记录所有读者就可以轻易实现。Go中的RWMutex实现的是写者优先writers-preference的读写锁那就需要用到readerWait来记录写者申请锁时候已经获取到锁的读者数量。\n这样当后续有其他读者继续申请锁时候可以读取readerWait是否大于0大于0则说明有写者已经申请锁了按照写者优先writers-preference原则该读者需要排到写者之后但是我们还需要记录这些排在写者后面读者的数量呀毕竟写着将来释放锁的时候还得唤醒一个个这些读者。这种情况下既要读取readerWait又要更新排队的读者数量这是两个操作无法原子化。RWMutex在实现时候通过将readerCount转换成负数一方面表明有写者申请了锁另一方面readerCount还可以继续记录排队的读者数量解决刚描述的无法原子化的问题真是巧妙\n对于读者优先readers-preference的读写锁我们可以借助Mutex实现。示例代码如下\ntype rwlock struct { reader_cnt int reader_lock sync.Mutex writer_lock sync.Mutex } func NewRWLock() *rwlock { return \u0026amp;rwlock{} } func (l *rwlock) RLock() { l.reader_lock.Lock() defer l.reader_lock.Unlock() l.reader_cnt++ if l.reader_cnt == 1 { // first reader l.writer_lock.Lock() } } func (l *rwlock) RUnlock() { l.reader_lock.Lock() defer l.reader_lock.Unlock() l.reader_cnt-- if l.reader_cnt == 0 { // latest reader l.writer_lock.Unlock() } } func (l *rwlock) Lock() { l.writer_lock.Lock() } func (l *rwlock) Unlock() { l.writer_lock.Unlock() } 上面示例代码中,尽管读者操作的实现上用到互斥锁,但由于它是用完立马就是释放掉,性能不会差太多。\n三大错误使用场景 # RLock/RUnlock、Lock/Unlock未成对出现 # 同互斥锁一样sync.RWMutex的RLock/RUnlock以及Lock/Unlock总是成对出现的。Lock或RLock多余调用会导致锁没有释放可能出现死锁Unlock或RUnlock多余的调用会大导致panic.\nfunc main() { var l sync.RWMutex l.Lock() l.Unlock() l.Unlock() // fatal error: sync: Unlock of unlocked RWMutex } 对于Lock/Unlock未成对出现所有可能情况如下\n如果只有Lock情况\n如果有一个 goroutine 只执行 Lock 操作而不执行 Unlock 操作,那么其他的 goroutine 就会一直被阻塞(拿不到锁),随着越来越多的阻塞的 goroutine 越来越多,整个系统最终会崩溃。\n如果只有Unlock情况\n如果其他 goroutine 持有锁,锁将被释放。 如果锁处于空闲状态unoccupied state它会panic。 复制sync.RWMutex作为函数值传递 # 同Mutex一样RWMutex也是不能复制使用的考虑下面场景代码\nfunc main() { var l sync.RWMutex l.Lock() foo(l) l.Lock() l.Unlock() } func foo(l sync.RWMutex) { l.Unlock() } 上面场景代码中本意先使用l.Lock()进行上锁操作然后调用foo(l)释放该锁最后再次上锁和释放锁。但这种操作是错误的会导致死锁。foo()函数接收的参数是变量l的一个副本该副本把之前l变量的锁状态锁状态指的是writerSemreaderCount等字段信息也复制了一遍此时副本的锁状态是上锁状态的所以foo函数中是可以进行释放锁操作的但释放的并不是最开始的那个锁。\n我们可以使用go vet命令检测复制锁情况\nvagrant@vagrant:~$ go vet main.go # command-line-arguments ./main.go:8:6: call of foo copies lock value: sync.RWMutex ./main.go:13:12: foo passes lock by value: sync.RWMutex 解决上面问题可以使用指针传递:\nfunc foo(l *sync.RWMutex) { l.Unlock() } 不可重入导致死锁 # 可重入锁(ReentrantLock)指的一个线程中可以多次获取同一把锁换到Go语言场景就是一个Goroutine中Mutex和RWMutex可以连续Lock操作而不会导致死锁。同互斥体Mutex一样RWMutex也是不可重入锁不支持重入。\nfunc main() { var l sync.RWMutex l.Lock() foo(\u0026amp;l) // foo中尝试重入锁会导致死锁 l.Unlock() } func foo(l *sync.RWMutex) { l.Lock() l.Unlock() } 下面是读锁和写锁重入时候导致的死锁:\nfunc main() { var l sync.RWMutex l.RLock() foo(\u0026amp;l) l.RUnlock() } func foo(l *sync.RWMutex) { l.Lock() l.Unlock() } 上面代码中写锁重入时候,需要读锁先释放,而读锁释放又依赖写锁,这样就形成了死循环,导致死锁。\n进一步阅读 # Readerswriters problem sync.RWMutex: Solving readers-writers problems Use sync.Mutex, sync.RWMutex to lock share data for race condition "},{"id":42,"href":"/gmp/scheduler/","title":"调度器","section":"G-M-P调度机制","content":" 调度器 # "},{"id":43,"href":"/gmp/gmp-model/","title":"调度机制概述","section":"G-M-P调度机制","content":" GMP模型 # Golang的一大特色就是Goroutine。Goroutine是Golang支持高并发的重要保障。Golang可以创建成千上万个Goroutine来处理任务将这些Goroutine分配、负载、调度到处理器上采用的是G-M-P模型。\n什么是Goroutine # Goroutine = Golang + Coroutine。Goroutine是golang实现的协程是用户级线程。Goroutine具有以下特点\n相比线程其启动的代价很小以很小栈空间启动2Kb左右 能够动态地伸缩栈的大小最大可以支持到Gb级别 工作在用户态,切换成很小 与线程关系是n:m即可以在n个系统线程上多工调度m个Goroutine 进程、线程、Goroutine # 在仅支持进程的操作系统中,进程是拥有资源和独立调度的基本单位。在引入线程的操作系统中,线程是独立调度的基本单位,进程是资源拥有的基本单位。在同一进程中,线程的切换不会引起进程切换。在不同进程中进行线程切换,如从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换\n线程创建、管理、调度等采用的方式称为线程模型。线程模型一般分为以下三种\n内核级线程(Kernel Level Thread)模型 用户级线程(User Level Thread)模型 两级线程模型,也称混合型线程模型 三大线程模型最大差异就在于用户级线程与内核调度实体KSEKSEKernel Scheduling Entity之间的对应关系。KSE是Kernel Scheduling Entity的缩写其是可被操作系统内核调度器调度的对象实体是操作系统内核的最小调度单元可以简单理解为内核级线程。\n用户级线程即协程由应用程序创建与管理协程必须与内核级线程绑定之后才能执行。线程由 CPU 调度是抢占式的,协程由用户态调度是协作式的,一个协程让出 CPU 后,才执行下一个协程。\n用户级线程(ULT)与内核级线程(KLT)比较:\n特性 用户级线程 内核级线程 创建者 应用程序 内核 操作系统是否感知存在 否 是 开销成本 创建成本低,上下文切换成本低,上下文切换不需要硬件支持 创建成本高,上下文切换成本高,上下文切换需要硬件支持 如果线程阻塞 整个进程将被阻塞。即不能利用多处理来发挥并发优势 其他线程可以继续执行,进程不会阻塞 案例 Java thread, POSIX threads Window Solaris 内核级线程模型 # 内核级线程模型中用户线程与内核线程是一对一关系1 : 1。线程的创建、销毁、切换工作都是有内核完成的。应用程序不参与线程的管理工作只能调用内核级线程编程接口(应用程序创建一个新线程或撤销一个已有线程时,都会进行一个系统调用)。每个用户线程都会被绑定到一个内核线程。用户线程在其生命期内都会绑定到该内核线程。一旦用户线程终止,两个线程都将离开系统。\n操作系统调度器管理、调度并分派这些线程。运行时库为每个用户级线程请求一个内核级线程。操作系统的内存管理和调度子系统必须要考虑到数量巨大的用户级线程。操作系统为每个线程创建上下文。进程的每个线程在资源可用时都可以被指派到处理器内核。\n内核级线程模型有如下优点\n在多处理器系统中内核能够并行执行同一进程内的多个线程 如果进程中的一个线程被阻塞,不会阻塞其他线程,是能够切换同一进程内的其他线程继续执行 当一个线程阻塞时,内核根据选择可以运行另一个进程的线程,而用户空间实现的线程中,运行时系统始终运行自己进程中的线程 缺点:\n线程的创建与删除都需要CPU参与成本大 用户级线程模型 # 用户线程模型中的用户线程与内核线程KSE是多对一关系N : 1。线程的创建、销毁以及线程之间的协调、同步等工作都是在用户态完成具体来说就是由应用程序的线程库来完成。内核对这些是无感知的内核此时的调度都是基于进程的。线程的并发处理从宏观来看任意时刻每个进程只能够有一个线程在运行且只有一个处理器内核会被分配给该进程。\n从上图中可以看出来库调度器从进程的多个线程中选择一个线程然后该线程和该进程允许的一个内核线程关联起来。内核线程将被操作系统调度器指派到处理器内核。用户级线程是一种”多对一”的线程映射\n用户级线程有如下优点\n创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多, 因为保存线程状态的过程和调用程序都只是本地过程 线程能够利用的表空间和堆栈空间比内核级线程多 缺点:\n线程发生I/O或页面故障引起的阻塞时如果调用阻塞系统调用则内核由于不知道有多线程的存在而会阻塞整个进程从而阻塞所有线程, 因此同一进程中只能同时有一个线程在运行 资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用 两级线程模型 # 两级线程模型中用户线程与内核线程是一对一关系N : M。两级线程模型充分吸收上面两种模型的优点尽量规避缺点。其线程创建在用户空间中完成线程的调度和同步也在应用程序中进行。一个应用程序中的多个用户级线程被绑定到一些小于或等于用户级线程的数目内核级线程上。\nGolang的线程模型 # Golang在底层实现了混合型线程模型。M即系统线程由系统调用产生一个M关联一个KSE即两级线程模型中的系统线程。G为Groutine即两级线程模型的的应用及线程。M与G的关系是N:M。\nG-M-P模型概览 # G-M-P分别代表\nG - GoroutineGo协程是参与调度与执行的最小单位 M - Machine指的是系统级线程 P - Processor指的是逻辑处理器P关联了的本地可运行G的队列(也称为LRQ)最多可存放256个G。 GMP调度流程大致如下\n线程M想运行任务就需得获取 P即与P关联。 然从 P 的本地队列(LRQ)获取 G 若LRQ中没有可运行的GM 会尝试从全局队列(GRQ)拿一批G放到P的本地队列 若全局队列也未找到可运行的G时候M会随机从其他 P 的本地队列偷一半放到自己 P 的本地队列。 拿到可运行的G之后M 运行 GG 执行之后M 会从 P 获取下一个 G不断重复下去。 调度的生命周期 # M0 是启动程序后的编号为 0 的主线程,这个 M 对应的实例会在全局变量 runtime.m0 中,不需要在 heap 上分配M0 负责执行初始化操作和启动第一个 G 在之后 M0 就和其他的 M 一样了 G0 是每次启动一个 M 都会第一个创建的 gourtineG0 仅用于负责调度的 GG0 不指向任何可执行的函数,每个 M 都会有一个自己的 G0。在调度或系统调用时会使用 G0 的栈空间,全局变量的 G0 是 M0 的 G0 上面生命周期流程说明:\nruntime 创建最初的线程 m0 和 goroutine g0并把两者进行关联g0.m = m0) 调度器初始化设置M最大数量P个数栈和内存出事以及创建 GOMAXPROCS个P 示例代码中的 main 函数是 main.mainruntime 中也有 1 个 main 函数 ——runtime.main代码经过编译后runtime.main 会调用 main.main程序启动时会为 runtime.main 创建 goroutine称它为 main goroutine 吧,然后把 main goroutine 加入到 P 的本地队列。 启动 m0m0 已经绑定了 P会从 P 的本地队列获取 G获取到 main goroutine。 G 拥有栈M 根据 G 中的栈信息和调度信息设置运行环境 M 运行 G G 退出,再次回到 M 获取可运行的 G这样重复下去直到 main.main 退出runtime.main 执行 Defer 和 Panic 处理,或调用 runtime.exit 退出程序。 G-M-P的数量 # G 的数量:\n理论上没有数量上限限制的。查看当前G的数量可以使用runtime. NumGoroutine()\nP 的数量:\n由启动时环境变量 $GOMAXPROCS 或者是由runtime.GOMAXPROCS() 决定。这意味着在程序执行的任意时刻都只有 $GOMAXPROCS 个 goroutine 在同时运行。\nM 的数量:\ngo 语言本身的限制go 程序启动时,会设置 M 的最大数量,默认 10000. 但是内核很难支持这么多的线程数,所以这个限制可以忽略。 runtime/debug 中的 SetMaxThreads 函数,设置 M 的最大数量 一个 M 阻塞了,会创建新的 M。M 与 P 的数量没有绝对关系,一个 M 阻塞P 就会去创建或者切换另一个 M所以即使 P 的默认数量是 1也有可能会创建很多个 M 出来。\n调度的流程状态 # 从上图我们可以看出来:\n每个P有个局部队列局部队列保存待执行的goroutine(流程2)当M绑定的P的的局部队列已经满了之后就会把goroutine放到全局队列(流程2-1) 每个P和一个M绑定M是真正的执行P中goroutine的实体(流程3)M从绑定的P中的局部队列获取G来执行 当M绑定的P的局部队列为空时M会从全局队列获取到本地队列来执行G(流程3.1)当从全局队列中没有获取到可执行的G时候M会从其他P的局部队列中偷取G来执行(流程3.2)这种从其他P偷的方式称为work stealing 当G因系统调用(syscall)阻塞时会阻塞M此时P会和M解绑即hand off并寻找新的idle的M若没有idle的M就会新建一个M(流程5.1)。 当G因channel或者network I/O阻塞时不会阻塞MM会寻找其他runnable的G当阻塞的G恢复后会重新进入runnable进入P队列等待执行(流程5.3) 调度过程中阻塞 # GMP模型的阻塞可能发生在下面几种情况\nI/Oselect block on syscall channel 等待锁 runtime.Gosched() 用户态阻塞 # 当goroutine因为channel操作或者network I/O而阻塞时实际上golang已经用netpoller实现了goroutine网络I/O阻塞不会导致M被阻塞仅阻塞G对应的G会被放置到某个wait队列(如channel的waitq)该G的状态由_Gruning变为_Gwaitting而M会跳过该G尝试获取并执行下一个G如果此时没有runnable的G供M运行那么M将解绑P并进入sleep状态当阻塞的G被另一端的G2唤醒时比如channel的可读/写通知G被标记为runnable尝试加入G2所在P的runnext然后再是P的Local队列和Global队列。\n系统调用阻塞 # 当G被阻塞在某个系统调用上时此时G会阻塞在_Gsyscall状态M也处于 block on syscall 状态此时的M可被抢占调度执行该G的M会与P解绑而P则尝试与其它idle的M绑定继续执行其它G。如果没有其它idle的M但P的Local队列中仍然有G需要执行则创建一个新的M当系统调用完成后G会重新尝试获取一个idle的P进入它的Local队列恢复执行如果没有idle的PG会被标记为runnable加入到Global队列。\nG-M-P内部结构 # G的内部结构 # G的内部结构中重要字段如下完全结构参见 源码\ntype g struct { stack stack // g自己的栈 m *m // 隶属于哪个M sched gobuf // 保存了g的现场goroutine切换时通过它来恢复 atomicstatus uint32 // G的运行状态 goid int64 schedlink guintptr // 下一个g, g链表 preempt bool //抢占标记 lockedm muintptr // 锁定的M,g中断恢复指定M执行 gopc uintptr // 创建该goroutine的指令地址 startpc uintptr // goroutine 函数的指令地址 } G的状态有以下9种可以参见 代码:\n状态 值 含义 _Gidle 0 刚刚被分配,还没有进行初始化。 _Grunnable 1 已经在运行队列中,还没有执行用户代码。 _Grunning 2 不在运行队列里中,已经可以执行用户代码,此时已经分配了 M 和 P。 _Gsyscall 3 正在执行系统调用,此时分配了 M。 _Gwaiting 4 在运行时被阻止没有执行用户代码也不在运行队列中此时它正在某处阻塞等待中。Groutine wait的原因有哪些参加 代码 _Gmoribund_unused 5 尚未使用,但是在 gdb 中进行了硬编码。 _Gdead 6 尚未使用,这个状态可能是刚退出或是刚被初始化,此时它并没有执行用户代码,有可能有也有可能没有分配堆栈。 _Genqueue_unused 7 尚未使用。 _Gcopystack 8 正在复制堆栈,并没有执行用户代码,也不在运行队列中。 M的结构 # M的内部结构完整结构参见 源码\ntype m struct { g0 *g // g0, 每个M都有自己独有的g0 curg *g // 当前正在运行的g p puintptr // 隶属于哪个P nextp puintptr // 当m被唤醒时首先拥有这个p id int64 spinning bool // 是否处于自旋 park note alllink *m // on allm schedlink muintptr // 下一个m, m链表 mcache *mcache // 内存分配 lockedg guintptr // 和 G 的lockedm对应 freelink *m // on sched.freem } P的内部结构 # P的内部结构完全结构参见 源码\ntype p struct { id int32 status uint32 // P的状态 link puintptr // 下一个P, P链表 m muintptr // 拥有这个P的M mcache *mcache // P本地runnable状态的G队列无锁访问 runqhead uint32 runqtail uint32 runq [256]guintptr runnext guintptr // 一个比runq优先级更高的runnable G // 状态为dead的G链表在获取G时会从这里面获取 gFree struct { gList n int32 } gcBgMarkWorker guintptr // (atomic) gcw gcWork } P有以下几种状态参加 源码\n状态 值 含义 _Pidle 0 刚刚被分配,还没有进行进行初始化。 _Prunning 1 当 M 与 P 绑定调用 acquirep 时P 的状态会改变为 _Prunning。 _Psyscall 2 正在执行系统调用。 _Pgcstop 3 暂停运行,此时系统正在进行 GC直至 GC 结束后才会转变到下一个状态阶段。 _Pdead 4 废弃,不再使用。 调度器的内部结构 # 调度器内部结构,完全结构参见 源码\ntype schedt struct { lock mutex midle muintptr // 空闲M链表 nmidle int32 // 空闲M数量 nmidlelocked int32 // 被锁住的M的数量 mnext int64 // 已创建M的数量以及下一个M ID maxmcount int32 // 允许创建最大的M数量 nmsys int32 // 不计入死锁的M数量 nmfreed int64 // 累计释放M的数量 pidle puintptr // 空闲的P链表 npidle uint32 // 空闲的P数量 runq gQueue // 全局runnable的G队列 runqsize int32 // 全局runnable的G数量 // Global cache of dead G\u0026#39;s. gFree struct { lock mutex stack gList // Gs with stacks noStack gList // Gs without stacks n int32 } // freem is the list of m\u0026#39;s waiting to be freed when their // m.exited is set. Linked through m.freelink. freem *m } 观察调度流程 # GODEBUG trace方式 # GODEBUG 变量可以控制运行时内的调试变量参数以逗号分隔格式为name=val。观察GMP可以使用下面两个参数\nschedtrace设置 schedtrace=X 参数可以使运行时在每 X 毫秒输出一行调度器的摘要信息到标准 err 输出中。\nscheddetail设置 schedtrace=X 和 scheddetail=1 可以使运行时在每 X 毫秒输出一次详细的多行信息信息内容主要包括调度程序、处理器、OS 线程 和 Goroutine 的状态。\npackage main import ( \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) func main() { var wg sync.WaitGroup for i := 0; i \u0026lt; 2000; i++ { wg.Add(1) go func() { a := 0 for i := 0; i \u0026lt; 1e6; i++ { a += 1 } wg.Done() }() time.Sleep(100 * time.Millisecond) } wg.Wait() } 执行一下命令:\nGODEBUG=schedtrace=1000 go run ./test.go\n输出内容如下\nSCHED 0ms: gomaxprocs=1 idleprocs=1 threads=4 spinningthreads=0 idlethreads=1 runqueue=0 [0] SCHED 1001ms: gomaxprocs=1 idleprocs=1 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0] SCHED 2002ms: gomaxprocs=1 idleprocs=1 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0] SCHED 3002ms: gomaxprocs=1 idleprocs=1 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0] SCHED 4003ms: gomaxprocs=1 idleprocs=1 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0] 输出内容解释说明:\nSCHED XXms: SCHED是调度日志输出标志符。XXms是自程序启动之后到输出当前行时间 gomaxprocs P的数量等于当前的 CPU 核心数或者GOMAXPROCS环境变量的值 idleprocs 空闲P的数量与gomaxprocs的差值即运行中P的数量 threads 线程数量即M的数量 spinningthreads自旋状态线程的数量。当M没有找到可供其调度执行的 Goroutine 时,该线程并不会销毁,而是出于自旋状态 idlethreads空闲线程的数量 runqueue全局队列中G的数量 [0]表示P本地队列下G的数量有几个P中括号里面就会有几个数字 Go tool trace方式 # func main() { // 创建trace文件 f, err := os.Create(\u0026#34;trace.out\u0026#34;) if err != nil { panic(err) } defer f.Close() // 启动trace goroutine err = trace.Start(f) if err != nil { panic(err) } defer trace.Stop() // main fmt.Println(\u0026#34;Hello trace\u0026#34;) } 执行下面命令产生trace文件trace.out\ngo run test.go\n执行下面命令打开浏览器打开控制台查看。\ngo tool trace trace.out\n总结 # Golang的线程模型采用的是混合型线程模型线程与协程关系是N:M。 Golang混合型线程模型实现采用GMP模型进行调度G是goroutine是golang实现的协程M是OS线程P是逻辑处理器。 每一个M都需要与一个P绑定P拥有本地可运行G队列M是执行G的单元M获取可运行G流程是先从P的本地队列获取若未获取到则从其他P偷取过来即work steal)若其他的P也没有则从全局G队列获取若都未获取到则M将处于自旋状态并不会销毁。 当执行G时候发生通道阻塞等用户级别阻塞时候此时M不会阻塞M会继续寻找其他可运行的G当阻塞的G恢复之后重新进入P的队列等待执行若G进行系统调用时候会阻塞M此时P会和M解绑(即hand off)并寻找新的空闲的M。若没有空闲的就会创建一个新的M。 Work Steal和Hand Off保证了线程的高效利用。 G-M-P高效的保证策略有\nM是可以复用的不需要反复创建与销毁当没有可执行的Goroutine时候就处于自旋状态等待唤醒 Work Stealing和Hand Off策略保证了M的高效利用 内存分配状态(mcache)位于PG可以跨M调度不再存在跨M调度局部性差的问题 M从关联的P中获取G不需要使用锁是lock free的 参考资料 # Golang 调度器 GMP 原理与调度全分析 Go: Work-Stealing in Go Scheduler 线程的3种实现方式\u0026ndash;内核级线程, 用户级线程和混合型线程 "},{"id":44,"href":"/feature/comma-ok/","title":"逗号ok模式","section":"语言特性","content":" 逗号ok模式 # 通过逗号ok模式(comma ok idiom)我们可以进行类型断言判断映射中是否存在某个key以及通道是否关闭。\n类型断言 # // 方式1 var ( v T ok bool ) v, ok = x.(T) // 方式2 v, ok := x.(T) // x是接口类型的变量T是要断言的类型 // 方式3 var v, ok = x.(T) // 方式4 v := x.(T) // 当心此种方式断言,若断言失败会发生恐慌 判断key是否存在映射中 # // 方式1 v, ok := a[x] // 方式2 var v, ok = a[x] 判断通道是否关闭 # // 方式1 var ( x T ok bool ) x, ok = \u0026lt;-ch // 方式2 x, ok := \u0026lt;-ch // 方式3 var x, ok = \u0026lt;-ch "},{"id":45,"href":"/concurrency/channel/","title":"通道 - channel","section":"并发编程","content":" 通道 - channel # Golang中Channel是goroutine间重要通信的方式是并发安全的通道内的数据First In First Out我们可以把通道想象成队列。\nchannel数据结构 # Channel底层数据结构是一个结构体。\ntype hchan struct { qcount uint // 队列中元素个数 dataqsiz uint // 循环队列的大小 buf unsafe.Pointer // 指向循环队列 elemsize uint16 // 通道里面的元素大小 closed uint32 // 通道关闭的标志 elemtype *_type // 通道元素的类型 sendx uint // 待发送的索引即循环队列中的队尾指针rear recvx uint // 待读取的索引即循环队列中的队头指针front recvq waitq // 接收等待队列 sendq waitq // 发送等待队列 lock mutex // 互斥锁 } hchan结构体中的buf指向一个数组用来实现循环队列sendx是循环队列的队尾指针recvx是循环队列的队头指针。dataqsize是缓存型通道的大小qcount记录着通道内数据个数。\n循环队列一般使用空余单元法来解决队空和队满时候都存在font=rear带来的二义性问题但这样会浪费一个单元。golang的channel中是通过增加qcount字段记录队列长度来解决二义性一方面不会浪费一个存储单元另一方面当使用len函数查看通道长度时候可以直接返回qcount字段一举两得。\nhchan结构体中另一重要部分是recvq,sendq分别存储了等待从通道中接收数据的goroutine和等待发送数据到通道的goroutine。两者都是waitq类型。\nwaitq是一个结构体类型waitq和sudog构成双向链表其中sudog是链表元素的类型waitq中first和last字段分别指向链表头部的sudog链表尾部的sudog。\ntype waitq struct { first *sudog last *sudog } type sudog struct { ... g *g // 当前阻塞的G ... next *sudog prev *sudog elem unsafe.Pointer ... } hchan结构图如下\nchannel的创建 # 在分析channel的创建代码之前我们看下源码文件中最开始定义的两个常量\nconst ( maxAlign = 8 hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))\u0026amp;(maxAlign-1)) ... ) maxAlgin用来设置内存最大对齐值对应就是64位系统下cache line的大小。当结构体是8字节对齐时候能够避免false share提高读写速度 hchanSize用来设置chan大小unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))\u0026amp;(maxAlign-1))这个复杂公式用来计算离unsafe.Sizeof(hchan{})最近的8的倍数。假设hchan{}大小是13hchanSize是16。 假设n代表unsafe.Sizeof(hchan{})a代表maxAlignc代表hchanSize则上面hchanSize的计算公式可以抽象为\nc = n + ((-n) \u0026amp; (a - 1))\n计算离8最近的倍数只需将n补足与到8倍数的差值就可c也可以用下面公式计算\nc = n + (a - n%a)\n感兴趣的可以证明在a为2的n的次幂时候上面两个公式是相等的。\nfunc makechan(t *chantype, size int) *hchan { elem := t.elem // 通道元素的大小不能超过64K if elem.size \u0026gt;= 1\u0026lt;\u0026lt;16 { throw(\u0026#34;makechan: invalid channel element type\u0026#34;) } // hchanSize大小不是maxAlign倍数或者通道数据元素的对齐保证大于maxAlign if hchanSize%maxAlign != 0 || elem.align \u0026gt; maxAlign { throw(\u0026#34;makechan: bad alignment\u0026#34;) } // 判断通道数据是否超过内存限制 mem, overflow := math.MulUintptr(elem.size, uintptr(size)) if overflow || mem \u0026gt; maxAlloc-hchanSize || size \u0026lt; 0 { panic(plainError(\u0026#34;makechan: size out of range\u0026#34;)) } var c *hchan switch { case mem == 0: // 无缓冲通道 c = (*hchan)(mallocgc(hchanSize, nil, true)) c.buf = c.raceaddr() case elem.ptrdata == 0: // 当通道数据元素不含指针hchan和buf内存空间调用mallocgc一次性分配完成 c = (*hchan)(mallocgc(hchanSize+mem, nil, true)) // hchan和buf内存上布局是紧挨着的 c.buf = add(unsafe.Pointer(c), hchanSize) default: // 当通道数据元素含指针时候先创建hchan然后给buf分配内存空间 c = new(hchan) c.buf = mallocgc(mem, elem, true) } c.elemsize = uint16(elem.size) c.elemtype = elem c.dataqsiz = uint(size) ... return c } 发送数据到channel # func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { // 当通道为nil时候 if c == nil { // 非阻塞模式下直接返回false if !block { return false } // 调用gopark将当前Goroutine休眠调用gopark时候将传入unlockf设置为nil当前Goroutine会一直休眠 gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2) throw(\u0026#34;unreachable\u0026#34;) } // 调试,不必关注 if debugChan { print(\u0026#34;chansend: chan=\u0026#34;, c, \u0026#34;\\n\u0026#34;) } // 竞态检测,不必关注 if raceenabled { racereadpc(c.raceaddr(), callerpc, funcPC(chansend)) } // 非阻塞模式下不使用锁快速检查send操作 if !block \u0026amp;\u0026amp; c.closed == 0 \u0026amp;\u0026amp; ((c.dataqsiz == 0 \u0026amp;\u0026amp; c.recvq.first == nil) || (c.dataqsiz \u0026gt; 0 \u0026amp;\u0026amp; c.qcount == c.dataqsiz)) { return false } var t0 int64 if blockprofilerate \u0026gt; 0 { t0 = cputicks() } // 加锁 lock(\u0026amp;c.lock) // 如果通道已关闭,再发送数据,发生恐慌 if c.closed != 0 { unlock(\u0026amp;c.lock) panic(plainError(\u0026#34;send on closed channel\u0026#34;)) } // 从接收者队列recvq中取出一个接收者接收者不为空情况下直接将数据传递给该接收者 if sg := c.recvq.dequeue(); sg != nil { send(c, sg, ep, func() { unlock(\u0026amp;c.lock) }, 3) return true } // 缓冲队列中的元素个数小于队列的大小 // 说明缓冲队列还有空间 if c.qcount \u0026lt; c.dataqsiz { qp := chanbuf(c, c.sendx) // qp指向循环数组中未使用的位置 if raceenabled { raceacquire(qp) racerelease(qp) } // 将发送的数据写入到qp指向的循环数组中的位置 typedmemmove(c.elemtype, qp, ep) c.sendx++ // 将send加一相当于循环队列的front指针向前进1 if c.sendx == c.dataqsiz { //当循环队列最后一个元素已使用此时循环队列将再次从0开始 c.sendx = 0 } c.qcount++ // 队列中元素计数加1 unlock(\u0026amp;c.lock) // 释放锁 return true } if !block { unlock(\u0026amp;c.lock) return false } gp := getg() // 获取当前的G mysg := acquireSudog() // 返回一个sudog mysg.releasetime = 0 if t0 != 0 { mysg.releasetime = -1 } mysg.elem = ep // 发送的数据 mysg.waitlink = nil mysg.g = gp // 当前G即发送者 mysg.isSelect = false mysg.c = c gp.waiting = mysg gp.param = nil c.sendq.enqueue(mysg) // 将当前发送者入队sendq中 goparkunlock(\u0026amp;c.lock, waitReasonChanSend, traceEvGoBlockSend, 3) // 将当前goroutine放入waiting状态并释放c.lock锁 // Ensure the value being sent is kept alive until the // receiver copies it out. The sudog has a pointer to the // stack object, but sudogs aren\u0026#39;t considered as roots of the // stack tracer KeepAlive(ep) // someone woke us up. if mysg != gp.waiting { throw(\u0026#34;G waiting list is corrupted\u0026#34;) } gp.waiting = nil if gp.param == nil { if c.closed == 0 { throw(\u0026#34;chansend: spurious wakeup\u0026#34;) } panic(plainError(\u0026#34;send on closed channel\u0026#34;)) } gp.param = nil if mysg.releasetime \u0026gt; 0 { blockevent(mysg.releasetime-t0, 2) } mysg.c = nil releaseSudog(mysg) return true } func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { if raceenabled { if c.dataqsiz == 0 { // 无缓冲通道 racesync(c, sg) } else { qp := chanbuf(c, c.recvx) raceacquire(qp) racerelease(qp) raceacquireg(sg.g, qp) racereleaseg(sg.g, qp) c.recvx++ // 相当于循环队列的rear指针向前进1 if c.recvx == c.dataqsiz { // 队列数组中最后一个元素已读取,则再次从头开始读取 c.recvx = 0 } c.sendx = c.recvx } } if sg.elem != nil { // 复制数据到sg中 sendDirect(c.elemtype, sg, ep) sg.elem = nil } gp := sg.g unlockf() gp.param = unsafe.Pointer(sg) if sg.releasetime != 0 { sg.releasetime = cputicks() } goready(gp, skip+1) // 使goroutine变成runnable状态唤醒goroutine } func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) { dst := sg.elem typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size) memmove(dst, src, t.size) } // 返回缓存槽i位置的对应的指针 func chanbuf(c *hchan, i uint) unsafe.Pointer { return add(c.buf, uintptr(i)*uintptr(c.elemsize)) } // 将src值复制到dst // 源码https://github.com/golang/go/blob/2bc8d90fa21e9547aeb0f0ae775107dc8e05dc0a/src/runtime/mbarrier.go#L156 func typedmemmove(typ *_type, dst, src unsafe.Pointer) { if dst == src { return } ... memmove(dst, src, typ.size) ... } 从channel中读取数据 # func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) { // 当通道为nil时候 if c == nil { if !block { // 当非阻塞模式直接返回 return } // 一直阻塞 gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2) throw(\u0026#34;unreachable\u0026#34;) } ... // 加锁锁 lock(\u0026amp;c.lock) // 当通道已关闭,且通道缓冲没有元素时候,直接返回 if c.closed != 0 \u0026amp;\u0026amp; c.qcount == 0 { if raceenabled { raceacquire(c.raceaddr()) } unlock(\u0026amp;c.lock) // 释放锁 if ep != nil { typedmemclr(c.elemtype, ep) // 清空ep指向的内存 } return true, false } // 从发送者队列中取出一个发送者,发送者不为空时候,将发送者数据传递给接收者 if sg := c.sendq.dequeue(); sg != nil { recv(c, sg, ep, func() { unlock(\u0026amp;c.lock) }, 3) return true, true } // 缓冲队列中有数据情况下,从缓存队列取出数据,传递给接收者 if c.qcount \u0026gt; 0 { // qp指向循环队列数组中元素 qp := chanbuf(c, c.recvx) if raceenabled { raceacquire(qp) racerelease(qp) } if ep != nil { // 直接qp指向的数据复制到ep指向的地址 typedmemmove(c.elemtype, ep, qp) } // 清空qp指向内存的数据 typedmemclr(c.elemtype, qp) c.recvx++ // 相当于循环队列中的rear加1 if c.recvx == c.dataqsiz { // 队列最后一个元素已读取出来recvx指向0 c.recvx = 0 } c.qcount-- // 队列中元素个数减1 unlock(\u0026amp;c.lock) // 释放锁 return true, true } if !block { unlock(\u0026amp;c.lock) return false, false } gp := getg() mysg := acquireSudog() mysg.releasetime = 0 if t0 != 0 { mysg.releasetime = -1 } mysg.elem = ep mysg.waitlink = nil gp.waiting = mysg mysg.g = gp mysg.isSelect = false mysg.c = c gp.param = nil c.recvq.enqueue(mysg) goparkunlock(\u0026amp;c.lock, waitReasonChanReceive, traceEvGoBlockRecv, 3) if mysg != gp.waiting { throw(\u0026#34;G waiting list is corrupted\u0026#34;) } gp.waiting = nil if mysg.releasetime \u0026gt; 0 { blockevent(mysg.releasetime-t0, 2) } closed := gp.param == nil gp.param = nil mysg.c = nil releaseSudog(mysg) return true, !closed } func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { if c.dataqsiz == 0 { if raceenabled { racesync(c, sg) } if ep != nil { recvDirect(c.elemtype, sg, ep) } } else { qp := chanbuf(c, c.recvx) if raceenabled { raceacquire(qp) racerelease(qp) raceacquireg(sg.g, qp) racereleaseg(sg.g, qp) } // 复制队列中数据到接收者 if ep != nil { typedmemmove(c.elemtype, ep, qp) } typedmemmove(c.elemtype, qp, sg.elem) c.recvx++ if c.recvx == c.dataqsiz { c.recvx = 0 } c.sendx = c.recvx } sg.elem = nil gp := sg.g unlockf() gp.param = unsafe.Pointer(sg) if sg.releasetime != 0 { sg.releasetime = cputicks() } goready(gp, skip+1) // 唤醒G } 关闭channel # func closechan(c *hchan) { // 当关闭的通道是nil时候直接恐慌 if c == nil { panic(plainError(\u0026#34;close of nil channel\u0026#34;)) } // 加锁 lock(\u0026amp;c.lock) // 通道已关闭,再次关闭直接恐慌 if c.closed != 0 { unlock(\u0026amp;c.lock) panic(plainError(\u0026#34;close of closed channel\u0026#34;)) } ... c.closed = 1 // 关闭标志closed置为1 var glist gList // 将接收者添加到glist中 for { sg := c.recvq.dequeue() if sg == nil { break } if sg.elem != nil { typedmemclr(c.elemtype, sg.elem) sg.elem = nil } if sg.releasetime != 0 { sg.releasetime = cputicks() } gp := sg.g gp.param = nil if raceenabled { raceacquireg(gp, c.raceaddr()) } glist.push(gp) } // 将发送者添加到glist中 for { sg := c.sendq.dequeue() if sg == nil { break } sg.elem = nil if sg.releasetime != 0 { sg.releasetime = cputicks() } gp := sg.g gp.param = nil if raceenabled { raceacquireg(gp, c.raceaddr()) } glist.push(gp) // } unlock(\u0026amp;c.lock) // 循环glist调用goready唤醒所有接收者和发送者 for !glist.empty() { gp := glist.pop() gp.schedlink = 0 goready(gp, 3) } } 总结 # channel规则 操作 空Channel 已关闭Channel 活跃Channel close(ch) panic panic 成功关闭 ch \u0026lt;-v 永远阻塞 panic 成功发送或阻塞 v,ok = \u0026lt;-ch 永远阻塞 不阻塞 成功接收或阻塞 注意: 从空通道中写入或读取数据会永远阻塞这会造成goroutine泄漏。\n发送、接收数据以及关闭通道流程图 "},{"id":46,"href":"/feature/select/","title":"通道选择器-select","section":"语言特性","content":" 通道选择器-select # Go 语言中select关键字结构跟switch结构类似但是select结构的case语句都是跟通道操作相关的。Go 语言会从select结构中已经可读取或可以写入通道对应的case语句中随机选择一个执行如果所有case语句中的通道都不能可读取或可写入且存在default语句的话那么会执行default语句。\n根据Go 官方语法指南指出select语句执行分为以下几个步骤\nFor all the cases in the statement, the channel operands of receive operations and the channel and right-hand-side expressions of send statements are evaluated exactly once, in source order, upon entering the \u0026ldquo;select\u0026rdquo; statement. The result is a set of channels to receive from or send to, and the corresponding values to send. Any side effects in that evaluation will occur irrespective of which (if any) communication operation is selected to proceed. Expressions on the left-hand side of a RecvStmt with a short variable declaration or assignment are not yet evaluated.\n对于case分支语句中写入通道的右侧表达式都会先执行执行顺序是按照代码中case分支顺序由上到下执行。case分支语句中读取通道的左右表达式不会先执行的。\nIf one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection. Otherwise, if there is a default case, that case is chosen. If there is no default case, the \u0026ldquo;select\u0026rdquo; statement blocks until at least one of the communications can proceed.\n如果有一个或者多个case分支的通道可以通信读取或写入那么会随机选择一个case分支执行。否则如果存在default分支那么执行default分支若没有default分支那么select语句会阻塞直到某一个case分支的通道可以通信。\nUnless the selected case is the default case, the respective communication operation is executed.\n除非选择的case分支是default分支否则将执行相应case分支的通道读写操作。\nIf the selected case is a RecvStmt with a short variable declaration or an assignment, the left-hand side expressions are evaluated and the received value (or values) are assigned.\nThe statement list of the selected case is executed.\n执行所选case中的语句。\n上面介绍的执行顺序第一步骤我们可以从下面代码输出结果可以看出来\nfunc main() { ch := make(chan int, 1) select { case ch \u0026lt;- getVal(1): println(\u0026#34;recv: \u0026#34;, \u0026lt;-ch) case ch \u0026lt;- getVal(2): println(\u0026#34;recv: \u0026#34;, \u0026lt;-ch) } } func getVal(n int) int { println(\u0026#34;getVal: \u0026#34;, n) return n } 上面代码输出结果可能如下:\ngetVal: 1 getVal: 2 recv: 2 可以看到通道写入的右侧表达式getVal(1)和getVal(2)都会立马执行执行顺序跟case语句顺序一样。\n接下来我们来看看第二步骤\nfunc main() { ch := make(chan int, 1) ch \u0026lt;- 100 select { case i := \u0026lt;-ch: println(\u0026#34;case1 recv: \u0026#34;, i) case i := \u0026lt;-ch: println(\u0026#34;case2 recv: \u0026#34;, i) } } 上面代码中case1 和case2分支的通道都是可以通信状态那么Go会随机选择一个分支执行我们执行代码后打印出来的结果可以证明这一点。\n我们接下来再看看下面的代码\nfunc main() { ch := make(chan int, 1) go func() { time.Sleep(time.Second) ch \u0026lt;- 100 }() select { case i := \u0026lt;-ch: println(\u0026#34;case1 recv: \u0026#34;, i) case i := \u0026lt;-ch: println(\u0026#34;case2 recv: \u0026#34;, i) default: println(\u0026#34;default case\u0026#34;) } } 上面代码中case1 和case2语句中的ch是未可以通信状态由于存在default分支那么Go会执行default分支进而打印出default case。\n如果我们注释掉default分支我们可以发现select会阻塞直到1秒之后ch通道是可以通信状态此时case1或case2中某个分支会执行。\n"},{"id":47,"href":"/function/closure/","title":"闭包","section":"函数","content":" 闭包 # C语言中函数名称就是函数的首地址。Go语言中函数名称跟C语言一样函数名指向函数的首地址即函数的入口地址。从前面《 基础篇-函数-一等公民》那一章节我们知道Go 语言中函数是一等公民,它可以绑定变量,作函数参数,做函数返回值,那么它底层是怎么实现的呢?\n我们先来了解下 Function Value 这个概念。\nFunction Value # Go 语言中函数是一等公民函数可以绑定到变量也可以做参数传递以及做函数返回值。Golang把这样的参数、返回值、变量称为Function value。\nGo 语言中Function value本质上是一个指针但是其并不直接指向函数的入口地址而是指向的runtime.funcval( runtime/runtime2.go)这个结构体。该结构体中的fn字段存储的是函数的入口地址\ntype funcval struct { fn uintptr // variable-size, fn-specific data here } 我们以下面这段代码为例来看下Function value是如何使用的:\nfunc A(i int) { i++ fmt.Println(i) } func B() { f1 := A f1(1) } func C() { f2 := A f2(2) } 上面代码中函数A被赋值给变量f1和f2这种情况下编译器会做出优化让f1和f2共用一个funcval结构体该结构体是在编译阶段分配到数据段的只读区域(.rodata)。如下图所示那样f1和f2都指向了该结构体的地址addr2该结构体的fn字段存储了函数A的入口地址addr1\n为什么f1和f2需要通过了一个二级指针来获取到真正的函数入口地址而不是直接将f1f2指向函数入口地址addr1。关于这个原因就涉及到Golang中闭包设计与实现了。\n闭包 # 闭包(Closure) 通俗点讲就是能够访问外部函数内部变量的函数。像这样能被访问的变量通常被称为捕获变量。\n闭包函数指令在编译阶段生成但因为每个闭包对象都要保存自己捕获的变量所以要等到执行阶段才创建对应的闭包对象。我们来看下下面闭包的例子\npackage main func A() func() int { i := 3 return func() int { return i } } func main() { f1 := A() f2 := A() print(f1()) pirnt(f2()) } 上面代码中当执行main函数时会在其栈帧区间内为局部变量f1和f2分配栈空间当执行第一个A函数时候会在其栈帧空间分配栈空间来存放局部变量i然后在堆上分配一个funcval结构体其地址假定addr2)该结构体的fn字段存储的是A函数内那个闭包函数的入口地址其地址假定为addr1。A函数除了分配一个funcval结构体外还会挨着该结构体分配闭包函数的变量捕获列表该捕获列表里面只有一个变量i。由于捕获列表的存在所以说闭包函数是一个有状态函数。\n当A函数执行完毕后其返回值赋值给f1此时f1指向的就是地址addr2。同理下来f2指向地址addr3。f1和f2都能通过funcval取到了闭包函数入口地址但拥有不同的捕获列表。\n当执行f1()时候Go 语言会将其对应funcval地址存储到特定寄存器比如amd64平台中使用rax寄存器这样在闭包函数中就可以通过该寄存器取出funcval地址然后通过偏移找到每一个捕获的变量。由此可以看出来Golang中闭包就是有捕获列表的Function value。\n根据上面描述我们画出内存布局图\n若闭包捕获的变量会发生改变编译器会智能的将该变量逃逸到堆上这样外部函数和闭包引用的是同一个变量此时不再是变量值的拷贝。这也是为什么下面代码总是打印循环的最后面一个值。\npackage main func main() { fns := make([]func(), 0, 5) for i := 0; i \u0026lt; 5; i++ { fns = append(fns, func() { println(i) }) } for _, fn := range fns { // 最后输出5个5而不是01234 fn() } } 感兴趣的可以仿造上图,画出上面代码的内存布局图。重点关注闭包函数捕获的不是值拷贝,而是引用一个堆变量。\n"}]