剪否贼啊八匹马 發表於 2020-2-21 15:29:00

GO语言slice详解(结合源码)

<h2 id="一go语言中slice的定义">一、GO语言中slice的定义</h2>
<p>slice 是一种结构体类型,在源码中的定义为:</p>
<blockquote>
<p>src/runtime/slice.go</p>
</blockquote>
<pre><code>type slice struct {
        array unsafe.Pointer
        len   int
        cap   int
}

</code></pre>
<p>从定义中可以看到,slice是一种值类型,里面有3个元素。array是数组指针,它指向底层分配的数组;len是底层数组的元素个数;cap是底层数组的容量,超过容量会扩容。</p>
<h2 id="二初始化操作">二、初始化操作</h2>
<p>slice有三种初始化操作,请看下面代码:</p>
<pre><code>package main
import "fmt"
func main() {
        //1、make
        a := make([]int32, 0, 5)
        //2、[]int32{}
        b := []int32{1, 2, 3}
        //3、new([]int32)
        c := *new([]int32)
        fmt.Println(a, b, c)
}
</code></pre>
<p>这几种初始化方式,在底层实现是不一样的。有一种了解底层实现好的方法,就是看反汇编的调用函数。运行下面命令即可看到代码某一行的反汇编:</p>
<p><code>go tool compile -S plan9Test.go | grep plan9Test.go:行号</code></p>
<h3 id="1make初始化">1、make初始化</h3>
<p>make函数初始化有三个参数,第一个是类型,第二个长度,第三个容量,容量要大于等于长度。slice的make初始化调用的是底层的<code>runtime.makeslice</code>函数。</p>
<pre><code>func makeslice(et *_type, len, cap int) slice {
        // NOTE: The len &gt; maxElements check here is not strictly necessary,
        // but it produces a 'len out of range' error instead of a 'cap out of range' error
        // when someone does make([]T, bignumber). 'cap out of range' is true too,
        // but since the cap is only being supplied implicitly, saying len is clearer.
        // See issue 4085.
        maxElements := maxSliceCap(et.size)
        if len &lt; 0 || uintptr(len) &gt; maxElements {
                panic(errorString("makeslice: len out of range"))
        }

        if cap &lt; len || uintptr(cap) &gt; maxElements {
                panic(errorString("makeslice: cap out of range"))
        }

        p := mallocgc(et.size*uintptr(cap), et, true)
        return slice{p, len, cap}
}
</code></pre>
<p>主要就是调用<code>mallocgc</code>分配一块 个数cap*类型大小 的内存给底层数组,然后返回一个slice,slice的array指针指向分配的底层数组。</p>
<h3 id="2int32-初始化">2、[]int32{} 初始化</h3>
<p>这种初始化底层是调用 <code>runtime.newobject</code> 函数直接分配相应个数的底层数组。</p>
<pre><code>// implementation of new builtin
// compiler (both frontend and SSA backend) knows the signature
// of this function
func newobject(typ *_type) unsafe.Pointer {
        return mallocgc(typ.size, typ, true)
}
</code></pre>
<h3 id="3newint32-初始化">3、new([]int32) 初始化</h3>
<p>这种初始化底层也是调用 <code>runtime.newobject</code> ,new是返回slice 的地址,所以要取地址里面内容才是真正的slice。</p>
<h2 id="三reslice切片操作">三、reSlice(切片操作)</h2>
<p>所谓reSlice,是基于已有 slice 创建新的 slice 对象,以便在<strong>容量cap允许范围内</strong>调整属性。</p>
<pre><code>data := []int32{0,1,2,3,4,5,6}
slice := data//
</code></pre>
<p>切片操作有三个参数,low、high、max,新生成的 slice 结构体三个参数,指针array指向原slice 底层数组元素下标为low的位置, len = high - low, cap = max - low。如下图所示:</p>
<p><img src="https://img2018.cnblogs.com/blog/682961/202002/682961-20200220172431256-1509936359.png"></p>
<p><strong>切片操作主要要注意的就是在原slice 容量允许范围,超出容量范围会报panic</strong>。</p>
<h2 id="四append-操作">四、append 操作</h2>
<p>slice 的 append 操作是向底层数组尾部添加数据,返回 <strong>新的slice对象</strong>。</p>
<p>请看下面一段代码:</p>
<pre><code>package main
import (
        "fmt"
)
func main() {
        a := make([]int32, 1, 2)
        b := append(a, 1)
        c := append(a, 1, 2)
        fmt.Printf("a的地址:%p, 第一个元素地址:%p,容量:%v\n", &amp;a, &amp;a, cap(a))
        //a的地址:0xc42000a060, 第一个元素地址:0xc42001a090,容量:2
        fmt.Printf("b的地址:%p, 第一个元素地址:%p,容量:%v\n", &amp;b, &amp;b, cap(b))
        //b的地址:0xc42000a080, 第一个元素地址:0xc42001a090,容量:2
        fmt.Printf("c的地址:%p, 第一个元素地址:%p,容量:%v\n", &amp;c, &amp;c, cap(c))
        //c的地址:0xc42000a0a0, 第一个元素地址:0xc42001a0a0,容量:4
}
</code></pre>
<p>从上面代码的打印结果中可以看出:a 是一个底层数组有一个元素,容量为2的slice;append 1个元素后,没有超出容量,产生了一个新的slice b,a 和 b 底层数组首元素相同地址,说明a,b共用底层数组;append 2个元素,超出了容量,产生一个新的slice c,c的底层数组地址变了,容量也翻倍了。<br>
那么得出结论,append操作的运行过程:<br>
1、如果添加数据后没有超过原始容量,新的slice对象 和原始slice共用底层数组,len 数据会变化,cap数据不变;<br>
2、添加数据后超过了容量那就会扩容,重新分配一个新的底层数组,然后拷贝底层数组数据过去,那么append后产生的新slice对象和原始的slice就没有任何关系了。</p>
<h3 id="扩容机制">扩容机制</h3>
<p>看汇编代码可以知道,扩容调用的是底层函数 <code>runtime.growslice</code>。<br>
这个函数是这样定义的:<br>
<code>func growslice(et *_type, old slice, cap int) slice {}</code><br>
这个函数传入三个参数:slice的原始类型,原始slice,期望的最小容量;返回一个新的slice,新slice 至少是拥有期望的最小容量,元素从原slice copy过来。</p>
<p>扩容规则主要是下面这段代码:</p>
<pre><code>newcap := old.cap
        doublecap := newcap + newcap
        if cap &gt; doublecap {
                newcap = cap
        } else {
                if old.len &lt; 1024 {
                        newcap = doublecap
                } else {
                        for newcap &lt; cap {
                                newcap += newcap / 4
                        }
                }
        }
</code></pre>
<p>扩容规则就是两点:</p>
<ol>
<li>如果期望的最小容量大于原始的两倍容量时,那么新的容量就是等于期望的最小容量;</li>
<li>不满足第一种情况,那么判断原slice的底层数组元素长度是不是小于1024。小于1024,新容量是原来的两倍;大于等于1024 ,新容量是原来的1.25倍。</li>
</ol>
<p>上面是扩容的基本规则判断,实际上扩容还要考虑到内存对齐情况:</p>
<pre><code>var lenmem, newlenmem, capmem uintptr
        const ptrSize = unsafe.Sizeof((*byte)(nil))
        switch et.size {
        case 1:
                lenmem = uintptr(old.len)
                newlenmem = uintptr(cap)
                capmem = roundupsize(uintptr(newcap))
                newcap = int(capmem)
        case ptrSize:
                lenmem = uintptr(old.len) * ptrSize
                newlenmem = uintptr(cap) * ptrSize
                capmem = roundupsize(uintptr(newcap) * ptrSize)
                newcap = int(capmem / ptrSize)
        default:
                lenmem = uintptr(old.len) * et.size
                newlenmem = uintptr(cap) * et.size
                capmem = roundupsize(uintptr(newcap) * et.size)
                newcap = int(capmem / et.size)
        }
</code></pre>
<pre><code>内存对齐之后,扩容的倍数就会 &gt;= 2 或则 1.25 了。
</code></pre>
<h3 id="为什么要内存对齐">为什么要内存对齐?</h3>
<pre><code>1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
</code></pre>
<h2 id="五函数调用中实参和形参的相互影响">五、函数调用中实参和形参的相互影响</h2>
<h3 id="1slice值传递在调用函数中直接操作底层数组">1、slice值传递,在调用函数中直接操作底层数组</h3>
<p>来看下面一段代码:</p>
<pre><code>package main
import (
        "fmt"
)

func OpSlice(b []int32) {
        fmt.Printf("len: %d, cap: %d, data:%+v \n", len(b), cap(b), b)
        //len: 5, cap: 5, data:
        fmt.Printf("b第一个元素地址:%p\n", &amp;b)
        //b第一个元素地址:0xc420016120

        b = 100
        fmt.Printf("len: %d, cap: %d, data:%+v \n", len(b), cap(b), b)
        //len: 5, cap: 5, data:
        fmt.Printf("b第一个元素地址:%p\n", &amp;b)
        //b第一个元素地址:0xc420016120
}

func main() {
        a := []int32{1, 2, 3, 4, 5}
        fmt.Printf("len: %d, cap: %d, data:%+v \n", len(a), cap(a), a)
        //len: 5, cap: 5, data:
        fmt.Printf("a第一个元素地址:%p\n", &amp;a)
        //a第一个元素地址:0xc420016120
        OpSlice(a)

        fmt.Printf("len: %d, cap: %d, data:%+v \n", len(a), cap(a), a)
        //len: 5, cap: 5, data:
        fmt.Printf("a第一个元素地址:%p\n", &amp;a)
        //a第一个元素地址:0xc420016120
}
</code></pre>
<p>从这段代码的打印中可以看到:<br>
main函数中的slice a 是实参,值传递给调用函数时,要临时拷贝一份给b,所以a,b 的地址是不一样的,slice b 结构体中的三个元素都是a中的拷贝,但是元素array是指针,指针的拷贝还是指针,他们指向同一块底层数组,所以a,b底层数组的第一个元素地址是一样的。a,b共用同一块底层数组,在调用函数中,直接改变b的第一个元素内容,函数返回后a的第一个元素也变了,相当于改变了实参。</p>
<h3 id="2slice-指针传递">2、slice 指针传递</h3>
<p>slice 指针传递就没什么说的了,在被调用函数中相当于操作的是实参中同一个slice,所有修改都会反映到实参。</p>
<h3 id="3slice-切片传递">3、slice 切片传递</h3>
<h4 id="不扩容的情况来看下面一段代码">不扩容的情况,来看下面一段代码:</h4>
<pre><code>package main
import (
        "fmt"
)
func OpSlice(b []int32) {
        fmt.Printf("len: %d, cap: %d, data:%+v \n", len(b), cap(b), b)
        //len: 3, cap: 9, data:
        fmt.Printf("b第一个元素地址:%p\n", &amp;b)
        //b第一个元素地址:0xc42007a064

        b = append(b, 100)
        fmt.Printf("len: %d, cap: %d, data:%+v \n", len(b), cap(b), b)
        //len: 4, cap: 9, data:
        fmt.Printf("b第一个元素地址:%p\n", &amp;b)
        //b第一个元素地址:0xc42007a064

}

func main() {
        a := []int32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
        fmt.Printf("a第2个元素地址:%p\n", &amp;a)
        //a第2个元素地址:0xc42007a064

        fmt.Printf("len: %d, cap: %d, data:%+v \n", len(a), cap(a), a)
        //len: 10, cap: 10, data:
        fmt.Printf("a第一个元素地址:%p\n", &amp;a)
        //a第一个元素地址:0xc42007a060
        OpSlice(a)

        fmt.Printf("len: %d, cap: %d, data:%+v \n", len(a), cap(a), a)
        //len: 10, cap: 10, data:
        fmt.Printf("a第一个元素地址:%p\n", &amp;a)
        //a第一个元素地址:0xc42007a060

}
</code></pre>
<p>前面已经讲过,切片和原slice是共用底层数组的。不扩容情况下,对切片产生的新的slice append 操作,新增加的元素会添加到底层数组尾部,会覆盖原有的值,反映到原slice中去;</p>
<h3 id="总结">总结</h3>
<p>无论是slice的什么操作:拷贝,append,reSlice 等等都会产生新的slice,但是他们是共用底层数组的,不扩容情况,他们增删改元素都会影响到原来的slice底层数组;扩容情况下,产生的是一个“真正的”新的slice对象,和原来的完全独立开了,底层数组完全不会影响。</p>
<h2 id="参考资料">参考资料</h2>
<ol>
<li>深度解密Go语言之Slice.</li>
<li>Go语言学习笔记.</li>
</ol>


</div>
<div id="MySignature" role="contentinfo">
    悟以往之不谏,知来者之可追!<br><br>
来源:https://www.cnblogs.com/awesomeHai/p/liuhai-0212.html
頁: [1]
查看完整版本: GO语言slice详解(结合源码)