slice(切片)Go1.15
slice创建 在Go语言中,创建切片的方式有一下几种 :
var slice []int*new([]int)[]int{1, 2, 3}make([]int, 1, 2)array[:] / sourceSlice[:]
不同的创建方式对应的底层实现也有一定的区别,在后文笔者会从汇编层面上对这几种方式的汇编实现进行剖析,如有偏颇之处,敬请指正。
前置知识内置类型常量
unsafe.Pointer
src/unsafe/unsafe.go
_type
size
uintptr
该类型是GoLang的内置类型,在64位机上可以理解为一个64位无符号整数,能够表示64位机上所有的地址
PtrSize
src/runtime/internal/sys
const PtrSize = 4 << (^uintptr(0) >> 63) // 在64位机上值为 8
MaxUintptr
runtime/internal/math/math.go
const MaxUintptr = ^uintptr(0) // 在64位机上值为0xffff ffff ffff ffff ffff ffff ffff ffff
maxAlloc
runtime/malloc.go2^32 - 1
receenabled
是否开启race竞态检测,默认为false,当使用build -race时为true
msanenabled
使用build -msan时为true,windows amd64位机不支持该命令
到此,笔者对后面所有需要用到的类型,常量都做了简单了介绍,如果读者对某些地方有疑问或者不理解,不妨直接查看源码或写一写demo测试验证一下自己的疑惑;另外,笔者也会在后文用到以上类型/常量时再次进行介绍,希望能够解决读者的问题。
slice源码/src/runtime/slice.go
slice底层结构
runtime/slice.goslice
type slice struct { array unsafe.Pointer // 指向一块连续内存,该内存中存放的是slice中的数据 len int // 记录slice当前长度 cap int // 记录slice当前容量 }
至此我们可以用下图来表示一个slice示例
构造切片-makeslice()
该函数用来构造一个slice对象,源码如下
// input: et: slice类型元信息,slice长度,slice容量 func makeslice(et *_type, len, cap int) unsafe.Pointer { // 调用MulUintptr函数:获取创建该切片需要的内存,是否溢出(超过2^64) // 2^64是64位机能够表示的最大内存地址 mem, overflow := math.MulUintptr(et.size, uintptr(cap)) // 如果溢出 | 超过能够分配的最大内存(2^32 - 1) | 非法输入, 报错并返回 if overflow || mem > maxAlloc || len < 0 || len > cap { mem, overflow := math.MulUintptr(et.size, uintptr(len)) if overflow || mem > maxAlloc || len < 0 { panicmakeslicelen() } panicmakeslicecap() } // 调用mallocgc函数分配一块连续内存并返回该内存的首地址 // 该函数实现涉及到了go语言内存管理,比较复杂,不是本文的主题 // 后面会单独介绍 return mallocgc(mem, et, true) }
根据slice类型,长度以及容量计算出需要的内存大小,如果合法则调用mallocgc申请相应的连续内存并返回首地址。MulUintptr()
func MulUintptr(a, b uintptr) (uintptr, bool) { // 如果slice类型大小为0(如struct{}类型) 或 (a | b) < 2 ^ 32,肯定不会发生溢出 if a|b < 1<<(4*sys.PtrSize) || a == 0 { return a * b, false } // 如果a * b > 2 ^64,说明发生了溢出 overflow := b > MaxUintptr/a return a * b, overflow }
MulUintptr函数的实现也比较简单,利用一些已有常量计算所需内存大小并判断是否溢出。
切片扩容-growslice()
growslice()
func growslice(et *_type, old slice, cap int) slice { // 默认为false, 如果go build 时添加了 -race参数, raceenabled = true if raceenabled { callerpc := getcallerpc() racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice)) } // 默认为false, 如果go build 时添加了 -msan参数, msanenabled = true if msanenabled { msanread(old.array, uintptr(old.len*int(et.size))) } // 如果cap < old.cap,说明cap溢出 if cap < old.cap { panic(errorString("growslice: cap out of range")) } // 如果et.size, 说明类型大小为0, 直接返回 zerobase 类型的新切片 if et.size == 0 { return slice{unsafe.Pointer(&zerobase), old.len, cap} } newcap := old.cap doublecap := newcap + newcap // 如果需要的容量大于老容量的2倍, 直接将扩容后新容量设置为需要的容量大小 // 也就是不需要进行2倍或者1.25倍的增长 if cap > doublecap { newcap = cap } else { // 如果老容量小于1024, 2倍扩容 if old.len < 1024 { newcap = doublecap } else { // 以1.25倍增大新容量, 直到溢出或者新容量大于需要的容量为止 for 0 < newcap && newcap < cap { newcap += newcap / 4 } // 如果溢出,那么将新容量设置为需要的容量 if newcap <= 0 { newcap = cap } } } var overflow bool var lenmem, newlenmem, capmem uintptr switch { // 当类型大小为1时,不需要乘除计算就能够得到所需要的值 case et.size == 1: lenmem = uintptr(old.len) newlenmem = uintptr(cap) // 内存对齐 capmem = roundupsize(uintptr(newcap)) // 判断是否溢出 overflow = uintptr(newcap) > maxAlloc newcap = int(capmem) // 当类型大小是8个字节时 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) > maxAlloc/sys.PtrSize newcap = int(capmem / sys.PtrSize) // 当类型大小是2的幂次方时 case isPowerOfTwo(et.size): var shift uintptr if sys.PtrSize == 8 { // Mask shift for better code generation. shift = uintptr(sys.Ctz64(uint64(et.size))) & 63 } else { shift = uintptr(sys.Ctz32(uint32(et.size))) & 31 } lenmem = uintptr(old.len) << shift newlenmem = uintptr(cap) << shift capmem = roundupsize(uintptr(newcap) << shift) overflow = uintptr(newcap) > (maxAlloc >> shift) newcap = int(capmem >> 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 > maxAlloc { panic(errorString("growslice: cap out of range")) } var p unsafe.Pointer // 如果是切片类型是指针类型,那么会调用memclrNoHeapPointers将 // newlenmem以后的capmem-newlenmem全部置0 if et.ptrdata == 0 { // 申请一块capmem大小的连续内存并返回首地址 p = mallocgc(capmem, nil, false) memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem) } else { p = mallocgc(capmem, et, true) if lenmem > 0 && writeBarrier.enabled { bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata) } } // 将老切片上的元素移动到新的内存中 memmove(p, old.array, lenmem) // 返回新的切片 return slice{p, old.len, newcap} }
needCapoldCapnewCap
needCap > oldCap * 2newCap = needCapoldCap < 1024newCap = oldCap * 2oldCap = oldCap * 1.25oldCapneedCapoldCap < 0oldCapnewCap
newCap内存对齐roundupsize
func roundupsize(size uintptr) uintptr { // 如果size < 32768(2的15次方) if size < _MaxSmallSize { // 如果size < 1024 - 8(2的10次方 - 8) if size <= smallSizeMax-8 { return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]]) } else { return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]]) } } if size+_PageSize < size { return size } return alignUp(size, _PageSize) }
该函数实现比较简单,根据当前大小,利用代码中硬编的一些参数来进行内存扩容
切片拷贝-copy()
copy(src, dst)src/cmd/compile/internal/walk.gocopyany()
memmovesrc/runtime/memmove_*.ssrc/runtime/slice.goslicecopy()
func slicecopy(toPtr unsafe.Pointer, toLen int, fmPtr unsafe.Pointer, fmLen int, width uintptr) int { if fmLen == 0 || toLen == 0 { return 0 } n := fmLen if toLen < n { n = toLen } if width == 0 { return n } if raceenabled { callerpc := getcallerpc() pc := funcPC(slicecopy) racereadrangepc(fmPtr, uintptr(n*int(width)), callerpc, pc) racewriterangepc(toPtr, uintptr(n*int(width)), callerpc, pc) } if msanenabled { msanread(fmPtr, uintptr(n*int(width))) msanwrite(toPtr, uintptr(n*int(width))) } size := uintptr(n) * width if size == 1 { // common case worth about 2x to do here // TODO: is this still worth it with new memmove impl? *(*byte)(toPtr) = *(*byte)(fmPtr) // known to be a byte pointer } else { memmove(toPtr, fmPtr, size) } return n }
memmove
结合Plan9汇编 src/runtime/slice.goPlan9
获取汇编代码
再开始之前,读者需要了解一下两个获取汇编代码的命令
go tool compile -S -N -l xxx.go.go源码文件go tool objdump xxx.exe可执行go build -gcflags="-N -l" xxx.go-gcflags="-N -l"
深入汇编实现
笔者将通过汇编代码,探究以不同方式创建slice时汇编实现的不同以及slice在汇编代码中的表示。
声明方式
当我们以如下方式创建slice时
func main() { var slice []int _ = slice }
查看汇编代码如下
"".main STEXT nosplit size=40 args=0x0 locals=0x20 0x0000 00000 (main.go:3) TEXT "".main(SB), NOSPLIT|ABIInternal, $32-0 // 局部变量+可能需要的额外调用函数的参数空间的总大小为32(不包括调用其它函数时的 ret address 的大小), 0表示函数的参数 + 返回值大小为0 0x0000 00000 (main.go:3) SUBQ $32, SP // 当前函数栈为32字节 0x0004 00004 (main.go:3) MOVQ BP, 24(SP) 0x0009 00009 (main.go:3) LEAQ 24(SP), BP // 存放调用者BP ... // 省略部分GC相关汇编 0x000e 00014 (main.go:6) MOVQ $0, "".slice(SP) // 存放sliceHeader的array字段 0x0016 00022 (main.go:6) XORPS X0, X0 // X0是一个128位寄存器,将X0寄存器清0 0x0019 00025 (main.go:6) MOVUPS X0, "".slice+8(SP) // 将SP + 8开始的16个字节赋值为0,即len = 0, cap = 0 0x001e 00030 (main.go:15) MOVQ 24(SP), BP // 恢复调用者BP 0x0023 00035 (main.go:15) ADDQ $32, SP // 清除当前函数栈帧 0x0027 00039 (main.go:15) RET // 返回
makeslice
(SP, SP + 8, SP + 16)来表示一个slice的array, len和cap。如下图:
new关键字
当我们以如下方式创建slice时
func main() { slice := *new([]int) _ = slice }
*
"".main STEXT nosplit size=82 args=0x0 locals=0x40 0x0000 00000 (main.go:3) TEXT "".main(SB), NOSPLIT|ABIInternal, $64-0 // 局部变量+可能需要的额外调用函数的参数空间的总大小为64(不包括调用其它函数时的 ret address 的大小), 0表示函数的参数 + 返回值大小为0 0x0000 00000 (main.go:3) SUBQ $64, SP // main函数栈大小为64字节 0x0004 00004 (main.go:3) MOVQ BP, 56(SP) 0x0009 00009 (main.go:3) LEAQ 56(SP), BP // 存储调用者BP,并修改BP为当前main函数BP ... // 省略部分GC相关汇编代码 0x000e 00014 (main.go:4) MOVQ $0, ""..autotmp_2+32(SP) // 将SP + 32位置的8个字节置0 0x0017 00023 (main.go:4) XORPS X0, X0 // 将128位X0寄存器清0 0x001a 00026 (main.go:4) MOVUPS X0, ""..autotmp_2+40(SP) // 将SP + 40位置的16个字节置0,用于表示sliceHeader的len和cap ... // 省略GC相关汇编,下面GC相关汇编全被省略 0x001f 00031 (main.go:4) LEAQ ""..autotmp_2+32(SP), AX 0x0024 00036 (main.go:4) MOVQ AX, ""..autotmp_1(SP) // 这两条汇编将sliceHeader开始的地址值赋值给SP开始的8个字节 0x0028 00040 (main.go:4) TESTB AL, (AX) 0x002a 00042 (main.go:4) MOVQ ""..autotmp_2+32(SP), AX 0x002f 00047 (main.go:4) MOVQ ""..autotmp_2+40(SP), CX 0x0034 00052 (main.go:4) MOVQ ""..autotmp_2+48(SP), DX 0x0039 00057 (main.go:4) MOVQ AX, "".slice+8(SP) // sliceHeader的array字段 0x003e 00062 (main.go:4) MOVQ CX, "".slice+16(SP) // sliceHeader的len字段 0x0043 00067 (main.go:4) MOVQ DX, "".slice+24(SP) // sliceHeader的cap字段 0x0048 00072 (main.go:5) MOVQ 56(SP), BP 0x004d 00077 (main.go:5) ADDQ $64, SP // 恢复调用者BP,清除当前栈帧 0x0051 00081 (main.go:5) RET // 返回
字面量
当我们以如下方式创建slice时
func main() { slice := []int{1, 2, 3} _ = slice }
查看汇编代码如下
"".main STEXT nosplit size=108 args=0x0 locals=0x40 0x0000 00000 (main.go:3) TEXT "".main(SB), NOSPLIT|ABIInternal, $64-0 0x0000 00000 (main.go:3) SUBQ $64, SP 0x0004 00004 (main.go:3) MOVQ BP, 56(SP) 0x0009 00009 (main.go:3) LEAQ 56(SP), BP // 构造main函数栈帧并存储调用者BP ... // 省略GC相关汇编,下面直接省略不做提示 0x000e 00014 (main.go:4) MOVQ $0, ""..autotmp_3(SP) 0x0016 00022 (main.go:4) XORPS X0, X0 0x0019 00025 (main.go:4) MOVUPS X0, ""..autotmp_3+8(SP) // 将SP开始的24字节置0, 该内存用于后面存储切片元素 0x001e 00030 (main.go:4) LEAQ ""..autotmp_3(SP), AX 0x0022 00034 (main.go:4) MOVQ AX, ""..autotmp_1+24(SP) // 将SP + 24开始的8个字节置为sliceHeader中array的首地址 0x0027 00039 (main.go:4) TESTB AL, (AX) 0x0029 00041 (main.go:4) MOVQ $1, ""..autotmp_3(SP) // 存储切片元素1 0x0031 00049 (main.go:4) TESTB AL, (AX) 0x0033 00051 (main.go:4) MOVQ $2, ""..autotmp_3+8(SP) // 存储切片元素2 0x003c 00060 (main.go:4) TESTB AL, (AX) 0x003e 00062 (main.go:4) MOVQ $3, ""..autotmp_3+16(SP) // 存储切片元素3 0x0047 00071 (main.go:4) TESTB AL, (AX) 0x0049 00073 (main.go:4) JMP 75 0x004b 00075 (main.go:4) MOVQ AX, "".slice+32(SP) // 存储sliceHeader的array字段 0x0050 00080 (main.go:4) MOVQ $3, "".slice+40(SP) // 存储sliceHeader的len字段 0x0059 00089 (main.go:4) MOVQ $3, "".slice+48(SP) // 存储sliceHeader的cap字段 0x0062 00098 (main.go:5) MOVQ 56(SP), BP 0x0067 00103 (main.go:5) ADDQ $64, SP // 恢复调用者BP并清除当前函数栈帧 0x006b 00107 (main.go:5) RET // 返回
由于进行了逃逸分析后发现当前slice切片可以在栈上分配,所以编译器再将slice的内容{1, 2, 3}都分配在了栈上。
make方式
当我们以如下方式创建slice时
func main() { slice := make([]int, 1, 2) _ = slice }
查看汇编代码如下
"".main STEXT nosplit size=64 args=0x0 locals=0x30 0x0000 00000 (main.go:3) TEXT "".main(SB), NOSPLIT|ABIInternal, $48-0 0x0000 00000 (main.go:3) SUBQ $48, SP 0x0004 00004 (main.go:3) MOVQ BP, 40(SP) 0x0009 00009 (main.go:3) LEAQ 40(SP), BP // 创建main函数栈帧并保存调用者BP ... // 省略GC相关汇编 0x000e 00014 (main.go:4) XORPS X0, X0 0x0011 00017 (main.go:4) MOVUPS X0, ""..autotmp_1(SP) 0x0015 00021 (main.go:4) LEAQ ""..autotmp_1(SP), AX 0x0019 00025 (main.go:4) TESTB AL, (AX) 0x001f 00031 (main.go:4) MOVQ AX, "".slice+16(SP) // sliceHeader的array字段 0x0024 00036 (main.go:4) MOVQ $1, "".slice+24(SP) // len字段 0x002d 00045 (main.go:4) MOVQ $2, "".slice+32(SP) // cap字段 0x0036 00054 (main.go:5) MOVQ 40(SP), BP 0x003b 00059 (main.go:5) ADDQ $48, SP // 恢复调用者BP,清除当前栈帧 0x003f 00063 (main.go:5) RET // 返回
如果切片足够小并且没有发生逃逸
我们创建一个大切片,从而达到堆上分配的目的,代码如下
func main() { slice := make([]int, 1, 10000) _ = slice }
汇编代码如下所示
"".main STEXT size=115 args=0x0 locals=0x40 0x0000 00000 (main.go:3) TEXT "".main(SB), ABIInternal, $64-0 ... // 省略栈分裂,GC相关汇编 0x0016 00022 (main.go:3) SUBQ $64, SP 0x001a 00026 (main.go:3) MOVQ BP, 56(SP) 0x001f 00031 (main.go:3) LEAQ 56(SP), BP // 开辟当前main函数栈帧并保存调用者BP ... // 省略GC相关代码 0x0024 00036 (main.go:4) LEAQ type.int(SB), AX // 获取int类型大小 0x002b 00043 (main.go:4) MOVQ AX, (SP) 0x002f 00047 (main.go:4) MOVQ $1, 8(SP) 0x0038 00056 (main.go:4) MOVQ $10000, 16(SP) // 分别将size, len, cap作为调用makeslice参数压栈 0x0041 00065 (main.go:4) CALL runtime.makeslice(SB) // 调用makeslice创建slice 0x0046 00070 (main.go:4) MOVQ 24(SP), AX // 将makeslice函数返回值保存到AX中 0x004b 00075 (main.go:4) PCDATA $0, $0 0x004b 00075 (main.go:4) MOVQ AX, "".slice+32(SP) 0x0050 00080 (main.go:4) MOVQ $1, "".slice+40(SP) 0x0059 00089 (main.go:4) MOVQ $10000, "".slice+48(SP) // 将sliceHeader的array, len, cap字段保存在SP + 32开始的24个字节中 0x0062 00098 (main.go:15) MOVQ 56(SP), BP 0x0067 00103 (main.go:15) ADDQ $64, SP // 恢复调用者BP,清除当前栈帧 0x006b 00107 (main.go:15) RET // 返回
可以看到,这里笔者特意将slice的cap设为10000,这样编译器在处理时就会选择在堆上创建slice而不是在栈上。
从数组中获取
当我们以如下方式创建slice时
func main() { array := [3]int{1, 2, 3} slice := array[:1] _ = slice }
查看汇编代码如下
"".main STEXT nosplit size=99 args=0x0 locals=0x38 0x0000 00000 (main.go:3) TEXT "".main(SB), NOSPLIT|ABIInternal, $56-0 0x0000 00000 (main.go:3) SUBQ $56, SP 0x0004 00004 (main.go:3) MOVQ BP, 48(SP) 0x0009 00009 (main.go:3) LEAQ 48(SP), BP // 开辟main函数栈帧并保存调用者BP 0x000e 00014 (main.go:4) MOVQ $0, "".array(SP) 0x0016 00022 (main.go:4) XORPS X0, X0 0x0019 00025 (main.go:4) MOVUPS X0, "".array+8(SP) // 将SP开始的24个字节置0 0x001e 00030 (main.go:4) MOVQ $1, "".array(SP) 0x0026 00038 (main.go:4) MOVQ $2, "".array+8(SP) 0x002f 00047 (main.go:4) MOVQ $3, "".array+16(SP) // 为数组元素赋值 0x0038 00056 (main.go:5) LEAQ "".array(SP), AX // 将数组首地址赋值给AX 0x003c 00060 (main.go:5) TESTB AL, (AX) 0x0042 00066 (main.go:5) MOVQ AX, "".slice+24(SP) 0x0047 00071 (main.go:5) MOVQ $1, "".slice+32(SP) 0x0050 00080 (main.go:5) MOVQ $3, "".slice+40(SP) // 分别将array, len, cap填充到栈 0x0059 00089 (main.go:17) MOVQ 48(SP), BP 0x005e 00094 (main.go:17) ADDQ $56, SP // 恢复调用者BP并清楚栈帧 0x0062 00098 (main.go:17) RET // 返回
数组和slice底层实际上共用了一块连续内存来存储数据
func main() { getSlice() } func getSlice() []int { array := [3]int{1, 2, 3} slice := array[:1] return slice }
getSlice()
"".getSlice STEXT size=181 args=0x18 locals=0x38 0x0000 00000 (main.go:16) TEXT "".getSlice(SB), ABIInternal, $56-24 // 局部变量+可能需要的额外调用函数的参数空间的总大小为56(不包括调用其它函数时的 ret address 的大小), 24表示函数的参数 + 返回值大小为24,由于getSlice函数没有参数,24就是返回值,即sliceHeader三个字段的大小 // 省略GC和栈分裂相关汇编 0x001a 00026 (main.go:16) SUBQ $56, SP 0x001e 00030 (main.go:16) MOVQ BP, 48(SP) 0x0023 00035 (main.go:16) LEAQ 48(SP), BP // 开辟getSlice函数栈帧,保存调用者(main函数)BP ... // 省略GC相关汇编 0x0028 00040 (main.go:16) MOVQ $0, "".~r0+64(SP) 0x0031 00049 (main.go:16) XORPS X0, X0 0x0034 00052 (main.go:16) MOVUPS X0, "".~r0+72(SP) // 将main函数中局部变量slice的三个字段置0 0x0039 00057 (main.go:17) LEAQ type.[3]int(SB), AX // 获取[3]int类型数组大小 0x0040 00064 (main.go:17) MOVQ AX, (SP) 0x0044 00068 (main.go:17) CALL runtime.newobject(SB) // 调用newobject在堆上创建数组 0x0049 00073 (main.go:17) MOVQ 8(SP), AX 0x004e 00078 (main.go:17) MOVQ AX, "".&array+16(SP) // 将数组首地址保存在SP + 16开始的8个字节中 0x0053 00083 (main.go:17) MOVQ ""..stmp_0(SB), CX 0x005a 00090 (main.go:17) MOVQ CX, (AX) 0x005d 00093 (main.go:17) MOVUPS ""..stmp_0+8(SB), X0 0x0064 00100 (main.go:17) MOVUPS X0, 8(AX) 0x0068 00104 (main.go:18) MOVQ "".&array+16(SP), AX // 为数组赋值{1, 2, 3} 0x0073 00115 (main.go:18) MOVQ AX, "".slice+24(SP) 0x0078 00120 (main.go:18) MOVQ $1, "".slice+32(SP) 0x0081 00129 (main.go:18) MOVQ $3, "".slice+40(SP) // 从数组创建slice切片 0x008a 00138 (main.go:19) MOVQ AX, "".~r0+64(SP) 0x008f 00143 (main.go:19) MOVQ $1, "".~r0+72(SP) 0x0098 00152 (main.go:19) MOVQ $3, "".~r0+80(SP) // 设置main函数局部变量slice的三个字段 0x00a1 00161 (main.go:19) MOVQ 48(SP), BP 0x00a6 00166 (main.go:19) ADDQ $56, SP // 恢复main函数BP,清除getSlice函数栈帧 0x00aa 00170 (main.go:19) RET // 返回 ""..stmp_0 SRODATA size=24 0x0000 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 ................ 0x0010 03 00 00 00 00 00 00 00
可以看到当发生逃逸时,就会调用newobject在堆上创建一个数组,然后slice和该数组共享底层的连续内存
总结
在本节中通过汇编代码可以很清楚的看到,slice在底层实现是通过三个变量::
- array : 保存数据的连续内存首地址
- len: 数据长度
- cap: 连续内存大小
并且当slice较小且没有发生逃逸时,编译器会选择在栈上分配而不是在堆上来保证效率。
#Go##学习路径#