本文主要介绍slice相关的操作:元素赋值(修改)makecopymakeandcopyappend环境OS:Ubuntu20.04.2LTS;x86_64Go:goversiongo1.16.2linux/amd64操作系统声明,处理不同的设备架构和Golang版本可能会导致相同源代码编译后在运行时内存地址和数据结构不同。本文仅保证当前环境下学习过程中分析数据的准确性和有效性。代码清单packagemainimport"fmt"funcmain(){varsrc=[]int{1,2,3,4,5,6,7,8,9,10}src[3]=100//src[13]=200dst:=makeSlice()makeSliceCopy(src)growSlice(src)copySlice(dst,src)sliceStringCopy([]byte("helloworld"),"helloslice")}//go:noinlinefuncsliceStringCopy(slice[]byte,sstring){复制(slice,s)PrintInterface(string(slice))}//go:noinlinefunccopySlice(dst[]int,src[]int){copy(dst,src)PrintInterface(dst)}//go:noinlinefuncgrowSlice(slice[]int){slice=append(slice,11)PrintInterface(slice)}//go:noinlinefuncmakeSliceCopy(array[]int){slice:=make([]int,5)copy(slice,array)PrintInterface(slice)}//go:noinlinefuncmakeSlice()[]int{slice:=make([]int,5)//slice:=make([]int,5,10)//slice:=make([]int,10,5)//"lenlargerthancapinmake(%v)"returnsslice}//go:noinlinefuncPrintInterface(vinterface{}){fmt.Println("it=",v)}深内存1、元素赋值的操作非常简单。位移定位元素内存并赋值,对应一条机器指令:如果后面元素的index超过runtime.slice.cap,就会panic。src[13]=200查看可执行程序。Golang编译器发现代码异常后,直接使用runtime.panicIndex函数调用来代替元素赋值和后续所有操作,并退出程序。这就很奇怪了:明明在编译的时候发现了代码逻辑错误,却并没有终止编译过程,而是变成了运行时异常。运行时异常更好吗?这个问题没有找到合理的答案。我们只能猜测这是编译器处理各种代码场景的通用编译处理逻辑,而不仅仅是处理本例中的情况。2.make使用make关键字动态创建切片。编译后make会变成什么命令看情况。代码清单中第42行的makeSlice函数编译后,对应的机器指令如下:可以看到,编译make关键字后,变成runtime.makeslice函数调用,其实现如下:funcmakeslice(et*_type,len,capint)unsafe.Pointer{//计算要分配的内存字节数mem,overflow:=math.MulUintptr(et.size,uintptr(cap))ifoverflow||mem>maxAlloc||len<0||len>cap{mem,overflow:=math.MulUintptr(et.size,uintptr(len))ifoverflow||mem>maxAlloc||len<0{panicmakeslicelen()}panicmakeslicecap()}//直接分配memoryreturnmallocgc(mem,et,true)}上面的代码很简单,有几个判断条件来说明:(1)overflow表示元素大小和元素个数的乘积是否溢出,即是否是大于64位无符号整数的最大值,且不得大于;(2)maxAlloc的值为0x1000000000000。事实上,大多数64位处理器和操作系统的内存可寻址范围都不是64位,而不会超过48位。这是一个Golang的内存分配和校验逻辑;(3)len>cap,Golang编译器会检查,编译失败。另外,在Golang源码中,有一个runtime.makeslice64函数,在编译后的可执行程序中并没有出现。在Go编译代码中看到的应该与32位程序编译有关。我们更关心64位程序,所以我们不会深入研究它。3、复制代码表中第23行的copySlice函数编译后,对应的机器指令如下:翻译成Golang伪代码,大致思路如下:funccopySlice(dst[]int,src[]int){n:=len(dst)ifn>len(src){n=len(src)}if&dst[0]!=&src[0]{runtime.memmove(&dst[0],&src[0],len(dst)*8)}PrintInterface(dst)}仔细阅读上面的指令代码,确保其逻辑与runtime.slicecopy函数相匹配,也就是说copy关键字编译后变成runtime.slicecopy函数调用。但是编译器对runtime.slicecopy函数进行了内联优化,所以直接调用runtime.slicecopy函数最终是看不到的。在Golang中,copy关键字可以用来将一个字符串对象复制到一个[]byte对象中;由于string类型还没有学习,这个特例暂时搁置。4.make和copy当make和copy这两个关键字一起使用时,又发生了新的变化。编译代码清单中第35行的makeSliceCopy函数后,对应的机器指令如下:戈兰编译器。该函数的源码逻辑非常清晰,这里不再赘述。5.append代码列表中第29行的growSlice函数对已经满的slice进行append操作。编译后对应的机器指令如下:上面代码的逻辑是:先比较len(slice)+1和cap(slice),当对一个fullslice进行append操作时,会触发length扩展底层数组(分配一个新的Array),翻译成Golang伪代码,大致思路如下:funcgrowSlice(slice[]int){iflen(slice)+1>cap(slice){slice=runtime.growslice(element_type_pointer,slice,11)}//cap(slice)==20slice[len(slice)]=17PrintInterface(slice)}runtime.growslice函数的作用是:当slice进行append操作时,如果slice已经满了,调用这个函数重新分配底层数组进行扩容。本例中,原切片的容量为10,调用runtime.growslice函数后,容量变为20。切片元素为int类型(element_type_pointer),解析该类型可以读取内存中的整数。通过以上的学习和研究,我对slice的各种操作有了一个本质的了解,相信使用起来会更加得心应手。本文转载自微信公众号「记忆中的Golang」
