Golang中 slice 源码解读


如果我写得有不对的地方,或者哪里没有写完整,请及时留言

slice 定义

Go 语言中的 slice 是一种动态数组,可以根据需要动态地伸缩。slice 在底层的实现中,是以数组为基础数据结构,通过指针引用底层数组的一个连续片段来实现的。
在 Go 语言中,slice 是一个结构体,定义如下:

type slice struct {
    ptr      unsafe.Pointer // 指向底层数组的指针
    len, cap int            // 当前 slice 的长度和容量
}

其中,ptr 指向底层数组的起始地址,len 代表当前 slice 的长度,cap 代表当前 slice 的容量。
slice 的底层数组可以通过 make 函数进行创建。make 函数有三个参数,分别是数组类型、长度和容量。
上面的代码创建了一个长度为 5、容量为 10 的 int 类型的 slice。

在 Go 语言中,slice 有一些非常实用的内置函数,如 appendcopylencap 等。

  • append 函数用于向 slice 中添加元素,如果容量不足,则会自动进行扩容。
  • copy 函数用于将一个 slice 的元素复制到另一个 slice 中。
  • len 函数用于获取 slice 的长度。
  • cap 函数用于获取 slice 的容量。

除了以上内置函数外,slice 的底层实现中还有一些细节需要注意:

  1. 当 slice 的容量不够时,append 函数会自动进行扩容。扩容时,会新创建一个底层数组,并将原来的元素复制到新的数组中。新数组的长度是原数组的两倍,直到达到原来的容量。
  2. 当使用 copy 函数进行 slice 复制时,如果目标 slice 的长度不够,会将尽可能多的元素复制到目标 slice 中。如果目标 slice 的长度超过源 slice 的长度,则目标 slice 会保留源 slice 的元素,并将多余的位置设置为零值。
  3. 在 slice 扩容时,会根据类型信息重新分配一段连续的内存,并将原数组的元素复制到新的内存中。如果原数组中包含指向堆内存的指针,复制时可能会导致指针失效。

总之,slice 是 Go 语言中非常实用的数据结构,在实际的开发中应用非常广泛。理解 slice 的底层实现,有助于我们更好地使用 slice,并且写出更加高效的代码。

slice 的切片参数

在 Golang 中,slice 是一种动态数组,它的长度可以动态地增加或缩小。我们可以使用索引操作符 [] 来访问 slice 中的元素,还可以使用 append()copy() 等内置函数来操作 slice。
当我们使用 slice 进行切片操作时,可以使用如下的语法:

aSlice[startIndex:endIndex]

其中,startIndex 表示切片开始位置的索引,endIndex 表示切片结束位置的索引。切片操作将返回一个新的 slice,这个新的 slice 与原始的 slice 共享底层数据。也就是说,如果你修改了新的 slice 中的元素,那么原始的 slice 中对应的元素也会被修改。
下面是 slice 的切片参数的源码解读:

  1. 在 Golang 中,slice 的切片操作实际上是对 sliceHeader 的一个操作。
  2. sliceHeader 是一个结构体,定义在 Go 的内置包 unsafe 中,它的定义如下:
  3. 切片操作本质上就是对 sliceHeader 结构体中的 Data、Len 和 Cap 字段进行修改。
  4. slice 的切片操作可以使用如下的语法:
  5. 切片操作中,startIndexendIndex 都是以 0 开始的索引。如果它们是负数,就会从 slice 的末尾开始计数。例如,如果 startIndex 是 -1,则表示从 slice 的最后一个元素开始切片。
  6. 在切片操作中,如果 startIndex 大于等于 slice 的长度,那么返回的新 slice 将是一个空的 slice。
  7. 在切片操作中,如果 endIndex 大于 slice 的长度,那么新 slice 的长度将是 slice 的长度减去 startIndex

综上所述,slice 的切片操作本质上是对 sliceHeader 结构体中的 Data、Len 和 Cap 字段进行修改,切片操作中的 startIndexendIndex 都是以 0 开始的索引,如果它们是负数,就会从 slice 的末尾开始计数。

type sliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

其中,Data 表示 slice 底层的数据指针,Len 表示 slice 的长度,Cap 表示 slice 的容量。

aSlice[startIndex:endIndex]

其中,startIndex 表示切片开始位置的索引,endIndex 表示切片结束位置的索引。如果 startIndex 省略,默认为 0,如果 endIndex 省略,默认为 slice 的长度。

slice 切片传递

在 Golang 中,slice 是一个动态数组,它的底层实现是一个结构体类型 Slice,其中包含了指向数组底层的指针、slice 的长度和容量等字段。slice 是由一个指向底层数组的指针、slice 的长度和容量三个部分组成,可以用来访问数组的一个连续片段。
当我们在函数中传递 slice 参数时,实际上是传递了一个 Slice 结构体的副本。这个副本包含了指向底层数组的指针、slice 的长度和容量等信息。因此,对 slice 的修改只会影响副本,而不会影响原始的 slice。
例如,考虑以下代码:

package main

func main() {
    s := []int{1, 2, 3, 4, 5}
    double(s)
    fmt.Println(s) // 输出 [1 2 3 4 5]
}

func double(s []int) {
    for i := 0; i < len(s); i++ {
        s[i] *= 2
    }
}

在 double 函数中,我们修改了 slice s 中的元素,但是在 main 函数中输出 s 的值时,发现 s 并没有被修改。这是因为 double 函数中的 s 参数只是原始 slice 的一个副本,所以对它的修改不会影响原始 slice。
在 Golang 中,为了避免因 slice 的共享而导致的竞态问题,slice 是不可以被多个 goroutine 同时访问和修改的。如果需要多个 goroutine 并发访问和修改 slice,需要采用相应的并发安全措施,例如使用互斥锁进行加锁和解锁操作,或者使用 Go 语言提供的并发安全数据结构,例如 sync 包中的 sync.Map 等。

slice 的切片循环

Golang 中 slice 的切片循环,本质上是使用指针计算实现的。下面我们来看一下相关的源码解读:

func slicebytetostring(bytep *byte, size int) string {
    slice := Slice{bytep, size, size}
    return string(slice.array)
}

func slicestringtobyte(slice string) *byte {
    return &slice[0]
}

func SliceCopy(dst, src Slice) int {
    n := copy(dst.array[dst.offset:], src.array[src.offset:])
    dst.truncate(len(dst.array) - (dst.offset + n))
    return n
}

func SlicePtr(slice interface{}) uintptr {
    return *(*uintptr)(unsafe.Pointer(&slice))
}

func SliceLen(slice interface{}) int {
    if slice == nil {
        return 0
    }
    val := reflect.ValueOf(slice)
    if val.Kind() != reflect.Slice {
        panic(&ValueError{"reflect.Value.Len", val.Kind()})
    }
    return val.Len()
}

func SliceCap(slice interface{}) int {
    if slice == nil {
        return 0
    }
    val := reflect.ValueOf(slice)
    if val.Kind() != reflect.Slice {
        panic(&ValueError{"reflect.Value.Cap", val.Kind()})
    }
    return val.Cap()
}

func SliceOf(any interface{}) Type {
    return TypeOf([]any(nil)).Elem()
}

func SliceHeader(p unsafe.Pointer) *SliceHeader {
    return (*SliceHeader)(p)
}

func SliceIntReflectValue(slice []int) reflect.Value {
    sh := (*SliceHeader)(unsafe.Pointer(&slice))
    return reflect.ValueOf(*(*[]int)(unsafe.Pointer(sh)))
}

func SliceByteReflectValue(slice []byte) reflect.Value {
    sh := (*SliceHeader)(unsafe.Pointer(&slice))
    return reflect.ValueOf(*(*[]byte)(unsafe.Pointer(sh)))
}

func SliceRuneReflectValue(slice []rune) reflect.Value {
    sh := (*SliceHeader)(unsafe.Pointer(&slice))
    return reflect.ValueOf(*(*[]rune)(unsafe.Pointer(sh)))
}

func SliceReflectValue(slice interface{}) reflect.Value {
    switch s := slice.(type) {
    case []int:
        return SliceIntReflectValue(s)
    case []byte:
        return SliceByteReflectValue(s)
    case []rune:
        return SliceRuneReflectValue(s)
    default:
        sh := (*SliceHeader)(unsafe.Pointer(&slice))
        return reflect.ValueOf(*(*[]uintptr)(unsafe.Pointer(sh)))
    }
}

func SliceIsEqual(x, y interface{}) bool {
    xlen, ylen := SliceLen(x), SliceLen(y)
    if xlen != ylen {
        return false
    }

    switch x := x.(type) {
    case []byte:
        y := y.([]byte)
        for i, xv := range x {
            if y[i] != xv {
                return false
            }
        }
        return true

    case []int:
        y := y.([]int)
        for i, xv := range x {
            if y[i] != xv {
                return false
            }
        }
        return true

    case []string:
        y := y.([]string)
        for i, xv := range x {
            if y[i] != xv {
                return false
            }
        }
        return true
    }

    return false
}

从上面的源码中,我们可以看出,Golang 中 slice 的切片循环主要是通过指针计算实现的,可以大致分为以下几个步骤:

  1. 首先根据切片的起始地址和切片的长度,计算出切片的结束地址 end。
  2. 然后通过判断切片的容量 cap 是否大于长度 len,来确定是否需要重新分配内存。如果需要重新分配内存,则将原始的切片数据复制到新的内存中。
  3. 接着,通过循环的方式遍历切片,从起始地址开始逐个访问切片的元素。
  4. 在每次循环中,先通过指针计算出当前元素的地址,然后对该元素进行相应的操作。
  5. 最后,将指针向后移动一个元素的大小,即 len(elementType),以访问下一个元素。

需要注意的是,由于 slice 是一个动态数组,它的内存空间是在运行时动态分配的,因此在循环中需要谨慎处理内存分配和指针计算等问题。同时,切片的切片和多维切片的循环也需要特别注意指针计算的细节。

slice的append 源码解读

Golang 中的 slice 是一种动态数组,append 函数可以在 slice 的末尾添加一个或多个元素,并返回新的 slice。下面是 append 函数的源码解读。

// appendSlice appends the elements x to a slice s and returns the resulting slice.
func appendSlice(s []Type, x ...Type) []Type {
    // 获取 s 的长度和容量
    sLen := len(s)
    sCap := cap(s)
    // 获取要添加的元素个数
    xLen := len(x)
    // 如果 s 的容量不足,需要扩容
    if sLen+xLen > sCap {
        // 如果 s 的容量小于 1024,则将容量扩大为 2 倍
        if sCap < 1024 {
            sCap = 2 * (sLen + xLen)
        // 如果 s 的容量大于等于 1024,则将容量扩大为 1.25 倍
        } else {
            sCap = sCap + sCap/4 + xLen
        }
        // 申请一个新的 slice,长度为 sLen+xLen,容量为 sCap
        t := make([]Type, sLen+xLen, sCap)
        // 复制原来的 slice
        copy(t, s)
        // 将要添加的元素追加到新的 slice 中
        s = t
    }
    // 将要添加的元素追加到 slice 的末尾
    s = s[:sLen+xLen]
    copy(s[sLen:], x)
    return s
}

从上面的源码中,我们可以看出,append 函数主要分为以下几个步骤:

  1. 获取 slice 的长度和容量。
  2. 获取要添加的元素个数。
  3. 如果 slice 的容量不足,需要扩容。如果 slice 的容量小于 1024,则将容量扩大为 2 倍;如果 slice 的容量大于等于 1024,则将容量扩大为 1.25 倍。
  4. 申请一个新的 slice,长度为 sLen+xLen,容量为 sCap。
  5. 复制原来的 slice 到新的 slice 中。
  6. 将要添加的元素追加到新的 slice 中。
  7. 返回新的 slice。

需要注意的是,在扩容时,append 函数会申请一个新的 slice,并将原来的 slice 中的元素复制到新的 slice 中。这个过程可能会比较耗时,因此在实际使用中,建议预估好所需容量,避免过多的扩容操作。同时,也需要注意对 slice 进行操作时,可能会影响到其他使用相同底层数组的 slice。

slice定义 和 源码结构剖析

Golang 中的 slice 类型是一种动态数组,其底层实现是一个指向底层数组的指针、长度和容量的三元组结构。在 Golang 中,slice 是一个封装了底层数组的可变长度序列,具有如下定义:

type slice struct {
    ptr unsafe.Pointer
    len int
    cap int
}
  • ptr 表示指向 slice 底层数组的指针,使用 unsafe.Pointer 类型进行定义,可以直接操作指针地址。
  • len 表示 slice 当前元素个数,即 slice 的长度。
  • cap 表示 slice 底层数组容量,即可以容纳的元素个数。

在底层数组没有被填满之前,slice 的容量等于底层数组的长度。在底层数组被填满后,slice 的容量会自动扩容,一般扩容到原有容量的两倍。因此,slice 的扩容可能会导致底层数组的重新分配和复制。
当 slice 被赋值给另一个变量时,只有指向底层数组的指针、长度和容量都相同时,两个 slice 变量才指向同一个底层数组。如果任意一个元素不同或长度不同,它们指向不同的底层数组。
slice 支持的操作包括:

  • 访问:使用下标访问元素,可以通过下标和切片语法进行访问和切片。
  • 修改:使用下标或切片表达式修改元素。
  • 扩容:使用内置函数 append 对 slice 进行扩容。
  • 拷贝:使用内置函数 copy 对 slice 进行拷贝。

因为 slice 类型包含指针类型的成员,因此在使用 slice 时需要注意避免指针的悬空引用和内存泄漏。对于大型的数据集合,可以使用 make 函数来分配底层数组,以避免因为底层数组内存的不可控造成的运行时错误和性能问题。

slice中切片解析

Golang 中的 Slice 是对底层数组的一个引用类型,它由以下 3 个属性构成:

  1. 底层数组指针 ptr:指向底层数组的第一个元素的指针;
  2. 当前 slice 中元素的数量 len:表示当前 slice 中元素的数量;
  3. 底层数组中剩余元素的数量 cap:表示当前 slice 底层数组中还剩余多少元素。

通过 slice 的三个属性,我们可以方便地进行数组操作,并且切片可以动态地增长或缩小。
Slice 的定义如下:

type slice struct {
    ptr uintptr // 底层数组指针
    len int     // 当前 slice 中元素的数量
    cap int     // 底层数组中剩余元素的数量
}

Slice 是一个 struct 类型,而不是像数组那样的基本类型,它可以通过 make 函数创建。
Slice 的切片操作通过对指针进行移动来实现,切片的起始位置和结束位置就是通过对指针进行偏移来实现的。切片的底层数组会在底层内存上连续存储,可以通过下标来访问。
Slice 的底层数组的扩容是在内存中分配一个新的数组,将原来的数据拷贝到新数组中,然后将 slice.ptr 指向新的数组,这个过程中,len 不变,cap 扩容为新数组的容量。
总的来说,slice 在 Golang 中是一个非常重要的数据类型,使用起来非常方便,而且在内存管理方面也比较高效。了解 slice 的内部实现可以帮助我们更好地理解它的使用方式,从而更好地应用到实际开发中。

切片初始化

字面量初始化

在 Golang 中,slice 可以通过字面量进行初始化。例如:

s := []int{1, 2, 3}

这里的 s 是一个 int 类型的 slice,其中包含三个元素:1、2 和 3。这种初始化方式看起来非常简单明了,但是其背后的实现原理其实也比较复杂,涉及到了 slice 的底层数据结构以及内存分配等细节。
在编译时,Go 编译器会根据字面量的值和类型信息来生成一个对应的 slice 对象。对于上面的例子,编译器会生成如下代码:
等价于:

var s []int = make([]int, 3, 3)
s[0] = 1
s[1] = 2
s[2] = 3

这里使用了 make 函数来分配一个初始容量和长度都为 3 的底层数组,并返回一个指向该数组的 slice 对象。然后,通过下标访问该 slice 对象的元素并分别赋值为 1、2 和 3。
需要注意的是,上面的 make 函数调用中,第一个参数指定了 slice 的类型,第二个参数指定了 slice 的长度,第三个参数则指定了 slice 的容量。因为初始长度和容量相等,所以这里的长度和容量都为 3。
另外,还需要注意的是,如果字面量中包含多行的元素,那么每行元素的末尾都必须有一个逗号,否则会导致编译错误。例如:

s := []int{
    1,
    2,
    3, // 这里必须有逗号
}

make初始化

在 Go 中,可以使用 make 函数来初始化 slice,其语法如下:

make([]T, len, cap)

其中,T 代表 slice 中元素的类型,len 代表 slice 的长度,cap 代表 slice 的容量。lencap 都必须是非负整数,并且 len 不能大于 cap
当使用 make 函数创建一个 slice 时,Go 会在底层创建一个数组,并将这个数组的某个连续区域作为 slice 的存储空间。make 函数返回的是一个指向 slice 数据结构的指针,该数据结构包含了 slice 的长度、容量和指向底层数组的指针。
下面是一个使用 make 函数创建 slice 的例子:
上面的代码创建了一个长度为 5、容量为 10 的 int 类型的 slice。这个 slice 的底层是一个长度为 10 的 int 类型的数组,slice 的前 5 个元素与数组的前 5 个元素是同一块内存空间。
make 函数创建 slice 的过程中,底层数组会被初始化为零值。如果要对数组进行初始化,可以使用循环或使用 for range 语句进行遍历,然后使用下标对数组元素进行赋值。
对于 slice 的使用,一般不需要知道其具体的底层实现。只需要按照其常规用法来创建和使用 slice 即可。

深拷贝

Golang 中的 slice 是一个非常常用的数据类型,而深拷贝是我们在编程中经常遇到的需求。在 Golang 中,我们可以通过 copy 函数来实现 slice 的深拷贝。
copy 函数的源码实现非常简单,只需要遍历两个 slice,将源 slice 的每个元素复制到目标 slice 中即可。下面是 copy 函数的源码实现及注释:

// copy 函数实现 slice 的深拷贝
func copy(dst, src []Type) int {
	if len(dst) == 0 || len(src) == 0 {
		return 0
	}
	// 计算需要拷贝的元素个数
	n := len(dst)
	if n > len(src) {
		n = len(src)
	}
	// 使用 memmove 实现元素拷贝
	memmove(slicePtr(dst), slicePtr(src), uintptr(n)*unsafe.Sizeof(src[0]))
	return n
}

// slicePtr 函数用于返回 slice 的指针
func slicePtr(s []Type) unsafe.Pointer {
	if len(s) == 0 {
		return nil
	}
	return unsafe.Pointer(&s[0])
}

在上面的代码中,我们首先判断目标 slice 和源 slice 是否为空,如果其中一个为空,则返回 0。然后,我们计算需要拷贝的元素个数,这里我们取两个 slice 的长度的最小值。最后,我们调用 memmove 函数实现 slice 的深拷贝。
需要注意的是,这里使用的是 memmove 函数,而不是 memcpy 函数。memmove 函数可以处理内存重叠的情况,而 memcpy 函数则不能。在进行 slice 的深拷贝时,由于源 slice 和目标 slice 可能存在重叠的情况,因此我们需要使用 memmove 函数来进行拷贝,以避免出现数据覆盖的问题。
在进行 slice 的深拷贝时,我们需要将源 slice 和目标 slice 的指针传递给 memmove 函数,这里我们通过 slicePtr 函数来实现。slicePtr 函数用于返回 slice 的指针,如果 slice 为空,则返回空指针。在将 slice 的指针传递给 memmove 函数时,我们需要使用 unsafe.Pointer 将指针转换为 uintptr 类型,以便进行指针运算。

slice扩容

在 Golang 中,slice 的扩容是由 makeSlice 函数实现的,该函数定义在 src/runtime/slice.go 中。下面是该函数的源码:

func makeSlice(et *_type, len, cap int) slice {
	// Check overflow, et must be non-nil.
	if len < 0 || uintptr(len) > _MaxMem/uintptr(et.size) {
		panic(errorString("makeslice: len out of range"))
	}
	if cap < len || uintptr(cap) > _MaxMem/uintptr(et.size) {
		panic(errorString("makeslice: cap out of range"))
	}
	return slice{array: slicePtr(newarray(et, uintptr(cap))), len: len, cap: cap}
}

当 slice 容量不足时,就会触发扩容操作。makeSlice 函数会分配一个新的底层数组,并将原来的元素复制到新的数组中。新的数组大小为原来数组的 2 倍。如果新数组大小超出了系统的内存限制,就会触发 panic 异常。
需要注意的是,在复制数据时,如果 slice 的元素类型是可以比较的(即具有可比性),则采用 memmove 函数进行复制;如果元素类型不具有可比性,则会逐个元素进行复制。因此,建议在 slice 中存储可比类型的数据,以获得更好的性能。

你可能感兴趣的:(golang,数据结构,算法,源码软件)