Golang 中 Slice的分析与使用(含源码)

news2024/10/2 3:22:11

文章目录

    • 1、slice结构体
    • 2、slice初始化
    • 3、append操作
    • 4、slice截取
    • 5、slice深拷贝
    • 6、值传递还是引用传递
    • 参考文献

众所周知,在golang中,slice(切片)是我们最常使用到的一种数据结构,是一种可变长度的数组,本篇文章我们主要结合源码来介绍一下slice的底层实现,以及在使用slice时的一些注意事项。

1、slice结构体

首先我们来看一段代码

package main
import (
    "fmt"
    "unsafe"
)
func main() {
    var a int
    var b int8
    var c int16
    var d int32
    var e int64
    slice := make([]int, 0)
    slice = append(slice, 1)
    fmt.Printf("int:%d\nint8:%d\nint16:%d\nint32:%d\nint64:%d\n", unsafe.Sizeof(a), unsafe.Sizeof(b), unsafe.Sizeof(c), unsafe.Sizeof(d), unsafe.Sizeof(e))
    fmt.Printf("slice:%d", unsafe.Sizeof(slice))
}

该程序输出golang中常用数据类型占多少byte,输出结果是

int:8 
int8:1 
int16:2 
int32:4 
int64:8 
slice:24

我们可以看到slice占24byte,为什么会占24byte,这就跟slice底层定义的结构有关,我们在golang的runtime/slice.go中可以找到slice的结构定义,如下:

type slice struct {
    array unsafe.Pointer//指向底层数组的指针
    len   int//切片的长度
    cap   int//切片的容量
}

我们可以看到slice中定义了三个变量,一个是指向底层数字的指针array,另外两个是切片的长度len和切片的容量cap。

2、slice初始化

简单了解了slice的底层结构后,我们来看下slice的初始化,在golang中slice有多重初始化方式,在这里我们就不一一介绍了,感兴趣的朋友可以自行百度,我们主要关注slice在底层是如何初始化的,首先我们来看一段代码

package main
import "fmt"
func main() {
    slice := make([]int, 0)
    slice = append(slice, 1)
    fmt.Println(slice, len(slice), cap(slice))
}

很简单的一段代码,make一个slice,往slice中append一个一个1,打印slice内容,长度和容量,接下来我们利用gotool提供的工具将以上代码反汇编

go tool compile -S slice.go 	

得到汇编代码如下(截取部分):

0x0000 00000 (slice.go:8)   TEXT    "".main(SB), ABIInternal, $152-0
0x0000 00000 (slice.go:8)   MOVQ    (TLS), CX
0x0009 00009 (slice.go:8)   LEAQ    -24(SP), AX
0x000e 00014 (slice.go:8)   CMPQ    AX, 16(CX)
0x0012 00018 (slice.go:8)   JLS 375
0x0018 00024 (slice.go:8)   SUBQ    $152, SP
0x001f 00031 (slice.go:8)   MOVQ    BP, 144(SP)
0x0027 00039 (slice.go:8)   LEAQ    144(SP), BP
0x002f 00047 (slice.go:8)   FUNCDATA    $0, gclocals- f14a5bc6d08bc46424827f54d2e3f8ed(SB)//编译器产生,用于保存一些垃圾收集相关的信息
0x002f 00047 (slice.go:8)   FUNCDATA    $1, gclocals- 3e7bd269c75edba02eda3b9069a96409(SB)
0x002f 00047 (slice.go:8)   FUNCDATA    $2, gclocals- f6aec3988379d2bd21c69c093370a150(SB)
0x002f 00047 (slice.go:8)   FUNCDATA    $3, "".main.stkobj(SB)
0x002f 00047 (slice.go:9)   PCDATA  $0, $1
0x002f 00047 (slice.go:9)   PCDATA  $1, $0
0x002f 00047 (slice.go:9)   LEAQ    type.int(SB), AX
0x0036 00054 (slice.go:9)   PCDATA  $0, $0
0x0036 00054 (slice.go:9)   MOVQ    AX, (SP)
0x003a 00058 (slice.go:9)   XORPS   X0, X0
0x003d 00061 (slice.go:9)   MOVUPS  X0, 8(SP)
0x0042 00066 (slice.go:9)   CALL    runtime.makeslice(SB)//初始化slice
0x0047 00071 (slice.go:9)   PCDATA  $0, $1
0x0047 00071 (slice.go:9)   MOVQ    24(SP), AX
0x004c 00076 (slice.go:10)  PCDATA  $0, $2
0x004c 00076 (slice.go:10)  LEAQ    type.int(SB), CX
0x0053 00083 (slice.go:10)  PCDATA  $0, $1
0x0053 00083 (slice.go:10)  MOVQ    CX, (SP)
0x0057 00087 (slice.go:10)  PCDATA  $0, $0
0x0057 00087 (slice.go:10)  MOVQ    AX, 8(SP)
0x005c 00092 (slice.go:10)  XORPS   X0, X0
0x005f 00095 (slice.go:10)  MOVUPS  X0, 16(SP)
0x0064 00100 (slice.go:10)  MOVQ    $1, 32(SP)
0x006d 00109 (slice.go:10)  CALL    runtime.growslice(SB)//append操作
0x0072 00114 (slice.go:10)  PCDATA  $0, $1
0x0072 00114 (slice.go:10)  MOVQ    40(SP), AX
0x0077 00119 (slice.go:10)  MOVQ    48(SP), CX
0x007c 00124 (slice.go:10)  MOVQ    56(SP), DX
0x0081 00129 (slice.go:10)  MOVQ    DX, "".slice.cap+72(SP)
0x0086 00134 (slice.go:10)  MOVQ    $1, (AX)
0x008d 00141 (slice.go:11)  PCDATA  $0, $0
0x008d 00141 (slice.go:11)  MOVQ    AX, (SP)
0x0091 00145 (slice.go:10)  LEAQ    1(CX), AX
0x0095 00149 (slice.go:10)  MOVQ    AX, "".slice.len+64(SP)
0x009a 00154 (slice.go:11)  MOVQ    AX, 8(SP)
0x009f 00159 (slice.go:11)  MOVQ    DX, 16(SP)
0x00a4 00164 (slice.go:11)  CALL    runtime.convTslice(SB)//类型转换
0x00a9 00169 (slice.go:11)  PCDATA  $0, $1
0x00a9 00169 (slice.go:11)  MOVQ    24(SP), AX
0x00ae 00174 (slice.go:11)  PCDATA  $0, $0
0x00ae 00174 (slice.go:11)  PCDATA  $1, $1
0x00ae 00174 (slice.go:11)  MOVQ    AX, ""..autotmp_33+88(SP)
0x00b3 00179 (slice.go:11)  MOVQ    "".slice.len+64(SP), CX
0x00b8 00184 (slice.go:11)  MOVQ    CX, (SP)
0x00bc 00188 (slice.go:11)  CALL    runtime.convT64(SB)
0x00c1 00193 (slice.go:11)  PCDATA  $0, $1
0x00c1 00193 (slice.go:11)  MOVQ    8(SP), AX
0x00c6 00198 (slice.go:11)  PCDATA  $0, $0
0x00c6 00198 (slice.go:11)  PCDATA  $1, $2
0x00c6 00198 (slice.go:11)  MOVQ    AX, ""..autotmp_34+80(SP)
0x00cb 00203 (slice.go:11)  MOVQ    "".slice.cap+72(SP), CX
0x00d0 00208 (slice.go:11)  MOVQ    CX, (SP)
0x00d4 00212 (slice.go:11)  CALL    runtime.convT64(SB)
0x00d9 00217 (slice.go:11)  PCDATA  $0, $1
0x00d9 00217 (slice.go:11)  MOVQ    8(SP), AX
0x00de 00222 (slice.go:11)  PCDATA  $1, $3
0x00de 00222 (slice.go:11)  XORPS   X0, X0

大家可能看到这里有点蒙,这是在干啥,其实我们只需要关注一些关键的信息就好了,主要是这几行

0x0042 00066 (slice.go:9)   CALL    runtime.makeslice(SB)//初始化slice
0x006d 00109 (slice.go:10)  CALL    runtime.growslice(SB)//append操作
0x00a4 00164 (slice.go:11)  CALL    runtime.convTslice(SB)//类型转换
0x00bc 00188 (slice.go:11)  CALL    runtime.convT64(SB)
0x00d4 00212 (slice.go:11)  CALL    runtime.convT64(SB)

我们能观察出,底层是调用runtime中的makeslice方法来创建slice的,我们来看一下makeslice函数到底做了什么

func makeslice(et *_type, len, cap int) unsafe.Pointer {
    mem, overflow := math.MulUintptr(et.size, uintptr(cap))
    if overflow || mem > maxAlloc || len < 0 || len > cap {
        // NOTE: Produce 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 golang.org/issue/4085.
        mem, overflow := math.MulUintptr(et.size, uintptr(len))
        if overflow || mem > maxAlloc || len < 0 {
            panicmakeslicelen()
        }
        panicmakeslicecap()
    }
     
  // Allocate an object of size bytes.
    // Small objects are allocated from the per-P cache's free lists.
    // Large objects (> 32 kB) are allocated straight from the heap.
    return mallocgc(mem, et, true)
}
func panicmakeslicelen() {
    panic(errorString("makeslice: len out of range"))
}
func panicmakeslicecap() {
    panic(errorString("makeslice: cap out of range"))
}

MulUintptr函数源码

package math
import "runtime/internal/sys"
const MaxUintptr = ^uintptr(0)
// MulUintptr returns a * b and whether the multiplication overflowed.
// On supported platforms this is an intrinsic lowered by the compiler.
func MulUintptr(a, b uintptr) (uintptr, bool) {
  if a|b < 1<<(4*sys.PtrSize) || a == 0 {//a|b < 1<<(4*8)
        return a * b, false
    }
    overflow := b > MaxUintptr/a
    return a * b, overflow
}

简单来说,makeslice函数的工作主要就是计算slice所需内存大小,然后调用mallocgc进行内存的分配。计算slice所需内存又是通过MulUintptr来实现的,MulUintptr的源码我们也已经贴出,主要就是用切片中元素大小和切片的容量相乘计算出所需占用的内存空间,如果内存溢出,或者计算出的内存大小大于最大可分配内存,MulUintptr的overflow会返回true,makeslice就会报错。另外如果传入长度小于0或者长度小于容量,makeslice也会报错。

3、append操作

首先我们来看一段程序

package main
 
import (
   "fmt"
   "unsafe"
)
 
func main() {
   slice := make([]int, 0, 10)
   slice = append(slice, 1)
   fmt.Println(unsafe.Pointer(&slice[0]), len(slice), cap(slice))
   slice = append(slice, 2)
   fmt.Println(unsafe.Pointer(&slice[0]), len(slice), cap(slice))
}

我们直接给出结果

0xc00009e000 1 10
0xc00009e000 2 10

我们可以看到,当slice容量足够时,我们往slice中append一个2,slice底层数组指向的内存地址没有发生改变;再看一段程序

func main() {
   slice := make([]int, 0)
   slice = append(slice, 1)
   fmt.Printf("%p %d %d\n", unsafe.Pointer(&slice[0]), len(slice), cap(slice))
   slice = append(slice, 2)
   fmt.Printf("%p %d %d\n", unsafe.Pointer(&slice[0]), len(slice), cap(slice))
}

输出结果是

0xc00009a008 1 1
0xc00009a030 2 2

我们可以看到当往slice中append一个1后,slice底层数组的指针指向地址0xc00009a008,长度为1,容量为1。这时再往slice中append一个2,那么slice的容量不够了,此时底层数组会发生copy,会重新分配一块新的内存地址,容量也变成了2,所以我们会看到底层数组的指针指向地址发生了改变。根据之前汇编的结果我们知晓了,append操作其实是调用了runtime/slice.go中的growslice函数,我们来看下源码:

func growslice(et *_type, old slice, cap int) slice {
    ...
    ...
    if cap < old.cap {
        panic(errorString("growslice: cap out of range"))
    }
    if et.size == 0 {
        // append should not create a slice with nil pointer but non-zero len.
        // We assume that append doesn't need to preserve old.array in this case.
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }
    newcap := old.cap//1280
    doublecap := newcap + newcap//1280+1280=2560
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4//1280*1.25=1600
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
  ...
}

我们主要关注下cap的扩容规则,从源码中我们可以简单的总结出slice容量的扩容规则:当原slice的cap小于1024时,新slice的cap变为原来的2倍;原slice的cap大于1024时,新slice变为原来的1.25倍,我们写个程序来验证下:

package main
import "fmt"
func main() {
    slice := make([]int, 0)
    oldCap := cap(slice)
    for i := 0; i < 4096; i++ {
        slice = append(slice, i)
        newCap := cap(slice)
        if newCap != oldCap {
            fmt.Printf("oldCap = %-4d  after append %-4d  newCap = %-4d\n", oldCap, i, newCap)
            oldCap = newCap
        }
    }
}

这段程序实现的功能是:当cap发生改变时,打印出cap改变前后的值。我们来看程序的输出结果:

oldCap = 0     after append 0     newCap = 1  
oldCap = 1     after append 1     newCap = 2  
oldCap = 2     after append 2     newCap = 4  
oldCap = 4     after append 4     newCap = 8  
oldCap = 8     after append 8     newCap = 16 
oldCap = 16    after append 16    newCap = 32 
oldCap = 32    after append 32    newCap = 64 
oldCap = 64    after append 64    newCap = 128
oldCap = 128   after append 128   newCap = 256
oldCap = 256   after append 256   newCap = 512
oldCap = 512   after append 512   newCap = 1024
oldCap = 1024  after append 1024  newCap = 1280
oldCap = 1280  after append 1280  newCap = 1696
oldCap = 1696  after append 1696  newCap = 2304
oldCap = 2304  after append 2304  newCap = 3072
oldCap = 3072  after append 3072  newCap = 4096

一开始的时候看起来跟我说的扩容规则是一样的,从1->2->4->8->16…->1024,都是成倍增长,当cap大于1024后,再append元素,cap变为1280,变成了1024的1.25倍,也符合我们的规则;但是继续append,1280->1696,似乎不是1.25倍,而是1.325倍,可见扩容规则并不是我们以上所说的那么简单,我们再继续往下看源码:

var overflow bool
    var lenmem, newlenmem, capmem uintptr
    // Specialize for common values of et.size.
    // For 1 we don't need any division/multiplication.
    // For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
    // For powers of 2, use a variable shift.
    switch {
    case et.size == 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        overflow = uintptr(newcap) > maxAlloc
        newcap = int(capmem)
    case et.size == sys.PtrSize:
        lenmem = uintptr(old.len) * sys.PtrSize
        newlenmem = uintptr(cap) * sys.PtrSize
        capmem = roundupsize(uintptr(newcap) * sys.PtrSize)//13568
        overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
        newcap = int(capmem / sys.PtrSize)//13568/8=1696
    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)
    }

我们看到每个case中都执行了roundupsize,我们再看下roundupsize的源码,如下:

package runtime
// Returns size of the memory block that mallocgc will allocate if you ask for the size.
func roundupsize(size uintptr) uintptr {
    if size < _MaxSmallSize {//size=1600*8=12800<32768
        if size <= smallSizeMax-8 {//12800<=0
            return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])
        } else {
            return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]])//size_to_class128[92]= 56
      //class_to_size[56]=13568
      //13568/8=1696
        }
    }
    if size+_PageSize < size {
        return size
    }
    return round(size, _PageSize)
}
const _MaxSmallSize   = 32768
const   smallSizeDiv    = 8
const   smallSizeMax    = 1024
const largeSizeDiv    = 128

其实roundupsize是内存对齐的过程,我们知道golang中内存分配是根据对象大小来配不同的mspan,为了避免造成过多的内存碎片,slice在扩容中需要对扩容后的cap容量进行内存对齐的操作,接下来我们对照源码来实际计算下cap容量是否由1280变成了1696

11
从以上流程图可以看出,cap在变成1600后又进入了内存对齐的过程,最终cap变为了1696。

4、slice截取

go中的slice是支持截取操作的,虽然使用起来非常的方便,但是有很多坑,稍有不慎就会出现bug且不易排查。让我们来看一段程序

package main
 
import "fmt"
 
func main() {
    slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    s1 := slice[2:5]
    s2 := s1[2:7]
    fmt.Printf("len=%-4d cap=%-4d slice=%-1v \n", len(slice), cap(slice), slice)
    fmt.Printf("len=%-4d cap=%-4d s1=%-1v \n", len(s1), cap(s1), s1)
    fmt.Printf("len=%-4d cap=%-4d s2=%-1v \n", len(s2), cap(s2), s2)
}

程序输出

len=10   cap=10   slice=[0 1 2 3 4 5 6 7 8 9]
len=3    cap=8    s1=[2 3 4]
len=5    cap=6    s2=[4 5 6 7 8]

s1的长度变成3,cap变为8(默认截取到最大容量), 但是s2截取s1的第2到第7个元素,左闭右开,很多人想问,s1根本没有那么元素啊,但是实际情况是s2截取到了,并且没有发生数组越界,原因就是s2实际截取的是底层数组,目前slice、s1、s2都是共用的同一个底层数组。我们继续操作

fmt.Println("--------append 100----------------")
s2 = append(s2, 100)

输出结果是:

--------append 100----------------
len=10   cap=10   slice=[0 1 2 3 4 5 6 7 8 100]
len=3    cap=8    s1=[2 3 4]
len=6    cap=6    s2=[4 5 6 7 8 100]

我们看到往s2里append数据影响到了slice,正是因为两者底层数组是一样的;但是既然都是共用的同一底层数组,s1为什么没有100,这个问题再下一节会讲到,大家稍安勿躁。我们继续进行操作:

fmt.Println("--------append 200----------------")
s2 = append(s2, 200)

输出结果是:

--------append 200----------------
len=10   cap=10   slice=[0 1 2 3 4 5 6 7 8 100]
len=3    cap=8    s1=[2 3 4]
len=7    cap=12   s2=[4 5 6 7 8 100 200]

我们看到继续往s2中append一个200,但是只有s2发生了变化,slice并未改变,为什么呢?对,是因为在append完100后,s2的容量已满,再往s2中append,底层数组发生复制,系统分配了一块新的内存地址给s2,s2的容量也翻倍了。我们继续操作:

fmt.Println("--------modify s1----------------")
s1[2] = 20

输出会是什么样呢?

--------modify s1----------------
len=10   cap=10   slice=[0 1 2 3 20 5 6 7 8 100]
len=3    cap=8    s1=[2 3 20]
len=7    cap=12   s2=[4 5 6 7 8 100 200]

这就很容易理解了,我们对s1进行更新,影响了slice,因为两者共用的还是同一底层数组,s2未发生改变是因为在上一步时底层数组已经发生了变化;

以此来看,slice截取的坑确实很多,极容易出现bug,并且难以排查,大家在使用的时候一定注意。

5、slice深拷贝

上一节中对slice进行的截取,新的slice和原始slice共用同一个底层数组,因此可以看做是对slice的浅拷贝,那么在go中如何实现对slice的深拷贝呢?那么就要依赖golang提供的copy函数了,我们用一段程序来简单看下如何实现深拷贝:

func main() {
 
    // Creating slices
    slice1 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    var slice2 []int
    slice3 := make([]int, 5)
 
    // Before copying
    fmt.Println("------------before copy-------------")
    fmt.Printf("len=%-4d cap=%-4d slice1=%v\n", len(slice1), cap(slice1), slice1)
    fmt.Printf("len=%-4d cap=%-4d slice2=%v\n", len(slice2), cap(slice2), slice2)
    fmt.Printf("len=%-4d cap=%-4d slice3=%v\n", len(slice3), cap(slice3), slice3)
 
 
    // Copying the slices
    copy_1 := copy(slice2, slice1)
    fmt.Println()
    fmt.Printf("len=%-4d cap=%-4d slice1=%v\n", len(slice1), cap(slice1), slice1)
    fmt.Printf("len=%-4d cap=%-4d slice2=%v\n", len(slice2), cap(slice2), slice2)
    fmt.Println("Total number of elements copied:", copy_1)
}

首先定义了三个slice,然后将slice1 copy到slice2,我们来看下输出结果:

------------before copy-------------
len=10   cap=10   slice1=[0 1 2 3 4 5 6 7 8 9]
len=0    cap=0    slice2=[]
len=5    cap=5    slice3=[0 0 0 0 0]
 
len=10   cap=10   slice1=[0 1 2 3 4 5 6 7 8 9]
len=0    cap=0    slice2=[]
Total number of elements copied: 0

我们发现slice1的内容并未copy到slice2,为什么呢?我们再试下将slice1 copy到slice3,如下:

copy_2 := copy(slice3, slice1)

输出结果:

len=10   cap=10   slice1=[0 1 2 3 4 5 6 7 8 9]
len=5    cap=5    slice3=[0 1 2 3 4]
Total number of elements copied: 5

我们看到copy成功,slice3和slice2唯一的区别就是slice3的容量为5,而slice2容量为0,那么是否是深拷贝呢,我们修改slice3的内容看下:

slice3[0] = 100

我们再看下输出结果:

len=10   cap=10   slice1=[0 1 2 3 4 5 6 7 8 9]
len=5    cap=5    slice3=[100 1 2 3 4]

我们可以看到修改slice3后,slice1的值并未改变,可见copy实现的是深拷贝。由此可见,copy函数为slice提供了深拷贝能力,但是需要在拷贝前申请内存空间。参照makeslice和growslice我们对本节一开始的程序进行反汇编,得到汇编代码(部分)如下:

0x0080 00128 (slice.go:10)  CALL    runtime.makeslice(SB)
0x0085 00133 (slice.go:10)  PCDATA  $0, $1
0x0085 00133 (slice.go:10)  MOVQ    24(SP), AX
0x008a 00138 (slice.go:10)  PCDATA  $1, $2
0x008a 00138 (slice.go:10)  MOVQ    AX, ""..autotmp_75+96(SP)
0x008f 00143 (slice.go:11)  PCDATA  $0, $4
0x008f 00143 (slice.go:11)  MOVQ    ""..autotmp_74+104(SP), CX
0x0094 00148 (slice.go:11)  CMPQ    AX, CX
0x0097 00151 (slice.go:11)  JEQ 176
0x0099 00153 (slice.go:11)  PCDATA  $0, $5
0x0099 00153 (slice.go:11)  MOVQ    AX, (SP)
0x009d 00157 (slice.go:11)  PCDATA  $0, $0
0x009d 00157 (slice.go:11)  MOVQ    CX, 8(SP)
0x00a2 00162 (slice.go:11)  MOVQ    $40, 16(SP)
0x00ab 00171 (slice.go:11)  CALL    runtime.memmove(SB)
0x00b0 00176 (slice.go:12)  MOVQ    $10, (SP)
0x00b8 00184 (slice.go:12)  CALL    runtime.convT64(SB)

我们发现copy函数其实是调用runtime.memmove,其实我们在研究runtime/slice.go文件中的源码的时候,会发现有一个slicecopy函数,这个函数最终就是调用runtime.memmove来实现slice的copy的,我们看下源码:

func slicecopy(to, fm slice, width uintptr) int {
    // 如果源切片或者目标切片有一个长度为0,那么就不需要拷贝,直接 return
    if fm.len == 0 || to.len == 0 {
        return 0
    }
     
    // n 记录下源切片或者目标切片较短的那一个的长度
    n := fm.len
    if to.len < n {
        n = to.len
    }
 
    // 如果入参 width = 0,也不需要拷贝了,返回较短的切片的长度
    if width == 0 {
        return n
    }
 
    //如果开启竞争检测
    if raceenabled {
        callerpc := getcallerpc()
        pc := funcPC(slicecopy)
        racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
        racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
    }
    if msanenabled {
        msanwrite(to.array, uintptr(n*int(width)))
        msanread(fm.array, 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)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
    } else {
        //如果不止一个元素,那么就从 fm.array 地址开始,拷贝到 to.array 地址之后,拷贝个数为size
        memmove(to.array, fm.array, size)
    }
    return n
}

源码解读见中文注释。

6、值传递还是引用传递

slice在作为函数参数进行传递的时候,是值传递还是引用传递,我们来看一段程序:

package main
 
import "fmt"
 
func main() {
    slice := make([]int, 0, 10)
    slice = append(slice, 1)
    fmt.Println(slice, len(slice), cap(slice))
    fn(slice)
    fmt.Println(slice, len(slice), cap(slice))
}
func fn(in []int) {
    in = append(in, 5)
}

很简单的一段程序,我们直接来看输出结果

[1] 1 10
[1] 1 10

可见fn内的append操作并未对slice产生影响,那我们再看一段代码:

package main
 
import "fmt"
 
func main() {
    slice := make([]int, 0, 10)
    slice = append(slice, 1)
    fmt.Println(slice, len(slice), cap(slice))
    fn(slice)
    fmt.Println(slice, len(slice), cap(slice))
}
func fn(in []int) {
    in[0] = 100
}

输出是什么?我们来看下

[1] 1 10
[100] 1 10

slice居然改变了,是不是有点混乱?前面我们说到slice底层其实是一个结构体,len、cap、array分别表示长度、容量、底层数组的地址,当slice作为函数的参数传递的时候,跟普通结构体的传递是没有区别的;如果直接传slice,实参slice是不会被函数中的操作改变的,但是如果传递的是slice的指针,是会改变原来的slice的;另外,无论是传递slice还是slice的指针,如果改变了slice的底层数组,那么都是会影响slice的,这种通过数组下标的方式更新slice数据,是会对底层数组进行改变的,所以就会影响slice。

那么,讲到这里,在第一段程序中在fn函数内append的5到哪里去了,不可能凭空消失啊,我们再来看一段程序

package main
 
import "fmt"
 
func main() {
    slice := make([]int, 0, 10)
    slice = append(slice, 1)
    fmt.Println(slice, len(slice), cap(slice))
    fn(slice)
    fmt.Println(slice, len(slice), cap(slice))
    s1 := slice[0:9]//数组截取
    fmt.Println(s1, len(s1), cap(s1))
}
func fn(in []int) {
    in = append(in, 5)
}

我们来看输出结果

[1] 1 10
[1] 1 10
[1 5 0 0 0 0 0 0 0] 9 10

显然,虽然在append后,slice中并未展示出5,也无法通过slice[1]取到(会数组越界),但是实际上底层数组已经有了5这个元素,但是由于slice的len未发生改变,所以我们在上层是无法获取到5这个元素的。那么,再问一个问题,我们是不是可以手动强制改变slice的len长度,让我们可以获取到5这个元素呢?是可以的,我们来看一段程序

package main
 
import (
    "fmt"
    "reflect"
    "unsafe"
)
 
func main() {
    slice := make([]int, 0, 10)
    slice = append(slice, 1)
    fmt.Println(slice, len(slice), cap(slice))
    fn(slice)
    fmt.Println(slice, len(slice), cap(slice))
    (*reflect.SliceHeader)(unsafe.Pointer(&slice)).Len = 2 //强制修改slice长度
    fmt.Println(slice, len(slice), cap(slice))
}
 
func fn(in []int) {
    in = append(in, 5)
}

我们来看输出结果

[1] 1 10
[1] 1 10
[1 5] 2 10

可以看出,通过强制修改slice的len,我们可以获取到了5这个元素。

所以再次回答一开始我们提出的问题,slice是值传递还是引用传递?答案是值传递!

以上,在使用golang中的slice的时候大家一定注意,否则稍有不慎就会出现bug。

参考文献

【1】《深入解析Go中Slice底层实现》:https://halfrost.com/go_slice/

【2】《理解Go中的Slice》:https://sanyuesha.com/2018/07/31/go-slice/

【3】《深度解密Go语言之Slice》:https://segmentfault.com/a/1190000019378931

【4】《The Go Programming Language Specification》:https://golang.org/ref/spec

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/397816.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

三维人脸实践:基于Face3D的渲染、生成与重构 <二>

face3d: Python tools for processing 3D face git code: https://github.com/yfeng95/face3d paper list: PaperWithCode 3DMM方法&#xff0c;基于平均人脸模型&#xff0c;可广泛用于基于关键点的人脸生成、位姿检测以及渲染等&#xff0c;能够快速实现人脸建模与渲染。推…

MySQL基础篇3

第一章 多表关系实战 1.1 实战1&#xff1a;省和市 方案1&#xff1a;多张表&#xff0c;一对多 方案2&#xff1a;一张表&#xff0c;自关联一对多 id1 name‘北京’ p_id null; id2 name‘昌平’ p_id1 id3 name‘大兴’ p_id1 id3 name‘上海’ p_idnull id4 name‘浦东’…

中国人工智能企业中集飞瞳全球港航人工智能领军者,箱况残缺检测视频流动态感知智能闸口,自动化港口码头数字化智慧港航中国人工智能企业

中国人工智能企业中集飞瞳全球港航人工智能领军者&#xff0c;箱况残缺检测视频流动态感知超级智能闸口&#xff0c;自动化港口码头数字化智慧港航。CIMCAI已完成全球250万人工智能集装箱箱况检验&#xff0c;完成全球上亿集装箱信息&#xff0c;先进产品在全球各港区及集装箱枢…

CNStack 多集群服务:基于 OCM 打造完善的集群管理能力

作者&#xff1a;学靖 概述 随着 Kubernetes 在企业业务中的应用和发展&#xff0c;单集群内的管理能力已经趋于完善&#xff0c;越来越多的客户期望在多云、多集群场景部署其业务&#xff0c;因此需要提供相应的多云、多集群管理能力。 CNStack 多集群服务是 CNStack 面向多…

【实现“大图”功能 Objective-C语言】

一、这时候,我们来实现另外一个功能,就是点击,实现这个“大图”, 1.点击“大图”按钮,实现这个“大图”, 那么我先给大家说一下,这个点击“按钮”,实现“大图”,这个思路是怎样的,大家看一下,这个示例程序,当你点击“大图”的时候,首先,这个图片变大,同时,后…

Nvidia jetson nano 部署yolov5_技术文档

Nvidia jetson nano 部署yolov5_技术文档 每天一句小姜格言&#xff1a;我行&#xff0c;我不是一般人儿 部署开始&#xff1a; 1、通过FileZilla&#xff0c;将window文件传输至jetson nano 上的nano文件夹下。 2、查看cuda 我买的jetson nano是带有配置好的镜像。系统配置…

[数据结构]:16-归并排序(顺序表指针实现形式)(C语言实现)

目录 前言 已完成内容 归并排序实现 01-开发环境 02-文件布局 03-代码 01-主函数 02-头文件 03-PSeqListFunction.cpp 04-SortFunction.cpp 结语 前言 此专栏包含408考研数据结构全部内容&#xff0c;除其中使用到C引用外&#xff0c;全为C语言代码。使用C引用主要是…

嵌入式开发:CIA保护跨连接设备的嵌入式数据

在嵌入式开发中&#xff0c;ITTIA SDL保护数据并确保嵌入式系统的开发安全。嵌入式系统中的数据管理安全威胁是什么?ITTIA如何解决这个问题?嵌入式系统和企业系统的数据管理理想情况下遵循相同的安全威胁。有三个主要的基本原则或目标被称为CIA&#xff1a;保密性&#xff1a…

【FATE联邦学习】standalone版Fateboard修改配置

背景&做法 很多其他程序&#xff08;比如vscode的code server&#xff09;也会使用这个 127 0 0 1:8080 socket进行通信&#xff0c;这样就没办法远程用vscode去开发了&#xff0c;所以需要修改下Fateboard的socket配置。官方文档中也给出了如何修改配置 The default data…

代码随想录--数组--滑动窗口解决最长/短子数组题型

注意题目是说找连续数组的和>s的最小长度&#xff0c;是“和”&#xff0c;不然都不知道题目在说什么。 http://【拿下滑动窗口&#xff01; | LeetCode 209 长度最小的子数组】 https://www.bilibili.com/video/BV1tZ4y1q7XE/?share_sourcecopy_web 看一下暴力算法&…

android 卡顿、ANR优化(1)屏幕刷新机制

前言&#xff1a; 本文通过阅读各种文章和源码总结出来的&#xff0c;如有不对&#xff0c;还望指出 目录 正文 基础概念 视觉暂留 逐行扫描 帧 CPU/GPU/Surface&#xff1a; 帧率、刷新率、画面撕裂 画面撕裂 Android屏幕刷新机制的演变 单缓存&#xff08;And…

限流算法详解

限流是我们经常会碰到的东西&#xff0c;顾名思义就是限制流量。它能保证我们的系统不会被突然的流量打爆&#xff0c;保证系统的稳定运行。像我们生活中&#xff0c;地铁就会有很多护栏&#xff0c;弯弯绕绕的&#xff0c;这个就是一种限流。像我们抢茅台&#xff0c;肯定大部…

案例17-环境混用带来的影响

目录一、背景介绍背景事故二、思路&方案三、过程四、总结nginx做转发fastdfs&#xff08;文件上传下载&#xff09;五、升华一、背景介绍 本篇博客主要介绍开发中项目使用依赖项环境闭一只带来的恶劣影响&#xff0c;在错误中成长进步。 背景 本公司另外一个产品开发God…

爱因斯坦求和约定 含代码einsum

目录 一、简介 1.哑标 2.自由标 二、torch实现 1.计算迹 2.取矩阵对角线 3.计算外积 4.batch矩阵乘法 5.带有子列表和省略号 一、简介 爱因斯坦求和约定(Einstein summation convention)是一种标记的约定, 又称为爱因斯坦标记法(Einstein notation), 可以基于一些约定…

position:absolute详解

position:absolute详解 日常开发中&#xff0c;经常会涉及元素的定位&#xff0c;我们都知道&#xff0c;绝对定位相对于最近position不为static的父级元素来定位&#xff0c;但其中定位的位置还是有细微的差别的。 绝对定位根据left和top属性来规定绝对定位元素的位置。 基…

2023年前端面试题集锦

2023年又是行情惨淡的一年&#xff0c;为此我从 「枇杷村IT面试宝典」小程序里收集了一些题目&#xff0c;更多题目可以扫下方二维码查看 现做个总结如下&#xff1a; 1. 在JavaScript中, 0 -0的结果是什么&#xff1f; 结果为true&#xff01; 严格等于比较的是值和类型&…

tcpdump写文件-w文件大小为0字节问题处理

一同事找来&#xff0c;说用tcpdump在一台linux服务器上抓包写文件&#xff0c;文件大小为0&#xff0c;不知道是什么原因造成&#xff0c;让协助解决。 自己登陆服务器试了一下&#xff0c;发现问题确实如此 不用-w&#xff0c;让打印在平面上&#xff0c;发现正常 以为权限不…

虹科案例 | 如何可持续的对变压器进行温度监控?

为了延长变压器的使用寿命&#xff0c;需要一个测量系统来监测内部整个绕组区域的温度。它必须明确温度升高发生的位置及其强度。您可以在此处了解为什么会这样以及如何在实践中实施? PART 1 变压器多点测温问题 变压器的工作温度越高&#xff0c;使用寿命越短。这里主要存在…

C++核心知识(五)—— 继承和派生

1. 继承概述1.1 为什么需要继承网页类 class IndexPage{ public://网页头部void Header(){cout << "网页头部!" << endl;}//网页左侧菜单void LeftNavigation(){cout << "左侧导航菜单!" << endl;}//网页主体部分void MainBody(){…

[SQL Statements] 基本的SQL知识 之DDL针对表结构和表空间的基本操作

[SQL Statements] 基本的SQL知识 之DDL针对表结构和表空间的基本操作 什么是数据库的表以及表空间 在MySQL中&#xff0c;一个数据库可以包含多个表&#xff0c;每个表是由若干个列&#xff08;column&#xff09;和行&#xff08;row&#xff09;组成的。表是存储数据的基本…