主页 > 软件开发  > 

【十】Golang切片

【十】Golang切片

💢欢迎来到张胤尘的开源技术站 💥开源如江河,汇聚众志成。代码似星辰,照亮行征程。开源精神长,传承永不忘。携手共前行,未来更辉煌💥

文章目录 切片切片的定义内存模型切片初始化使用 `make` 函数只指定长度,不指定容量同时指定长度和容量 使用切片字面量从数组或切片创建切片 切片扩容扩容流程优化建议 常用操作添加元素复制切片删除元素通过切片操作删除通过 `copy` 删除 遍历切片`for` 循环`for range` 循环 切片中元素的比较切片排序查找某个元素 常见问题切片底层数组共享问题切片扩容机制导致的性能问题切片的零值和 `nil` 判断切片作为函数参数的陷阱

切片

在 golang 语言中,切片是一种非常灵活且强大的数据结构,它提供了对数组的动态封装,允许动态调整大小。

切片的定义

切片的类型定义为 []T,其中 T 是切片中存储的元素类型。如下所示:

var slice []int

这表示 slice 是一个存储整数类型的切片。

内存模型

在 golang 语言中,切片的内存模型是基于底层数组的引用机制实现的。也就是说切片并不直接存储数据,而是底层数组来存储数据。

在每个切片中都包含了一个切片头,它是一个包含三个字段的结构体,如下所示:

源码文件:go/src/runtime/slice.go

type slice struct { array unsafe.Pointer len int cap int } array:指向底层数组的某个位置,表示切片的起始元素。len:切片的长度,表示切片当前包含的元素数量。cap:切片的容量,表示从切片的起始位置到底层数组末尾的元素数量。

切片通过其切片头中的 指针 来确定在底层数组中的起始位置。这个指针直接指向底层数组的某个元素,而切片的操作(如访问、修改等)都是基于这个指针的偏移量进行的。下面给出一个例子,可以更好的理解切片的内存模型,如下所示:

package main import "fmt" func main() { array := [5]int{1, 2, 3, 4, 5} fmt.Println(array) // [1 2 3 4 5] a := array[1:4] // 创建一个切片a,引用数组的索引 1 到索引 3 的部分 b := array[2:4] // 创建一个切片b,引用数组的索引 2 到索引 3 的部分 fmt.Println(a) // [2 3 4] fmt.Println(len(a)) // 3 fmt.Println(cap(a)) // 4 fmt.Println(b) // [3 4] fmt.Println(len(b)) // 2 fmt.Println(cap(b)) // 3 }

从以上代码示例中,可知如下信息:

a 切片指向的底层数组 array 的起始位置索引为1;b 切片指向底层数组 array 的起始位置索引为2。a 切片中包含的元素数量(长度)是3;b 切片中包含的元素数量(长度)是2。a 切片中起始位置到数组 array 末尾的元素数量是4(索引1到索引4);b 切片中起始位置到数组 array 末尾的元素数量是3(索引2到索引4)

如下图所示:

切片初始化

golang 中提供了几种针对切片初始化的方式:使用 make 函数、使用切片字面量、从数组或切片创建切片。

使用 make 函数

在 golang 中使用 make 函数是创建并初始化切片的常用方法,它会分配一个底层数组,并返回一个指向该数组的切片,如下所示:

make([]T, len, cap) T 是切片的元素类型。len 是切片的长度(必须指定)。cap 是切片的容量(可选,如果省略,则默认等于 len 长度)。 只指定长度,不指定容量 package main import "fmt" func main() { // 创建一个长度为 5 的切片,容量默认等于长度 slice := make([]int, 5) fmt.Println("Slice:", slice) // Slice: [0 0 0 0 0] fmt.Println("Length:", len(slice)) // Length: 5 fmt.Println("Capacity:", cap(slice)) // Capacity: 5 }

在这个例子中:

切片的长度为 5,表示切片中初始有 5 个元素。未指定切片的容量大小,故容量默认等于长度,表示底层数组的大小是5,一旦超过5个元素需要重新分配底层数组(切片的扩容)。 同时指定长度和容量 package main import "fmt" func main() { // 创建一个长度为 3,容量为 10 的切片 slice := make([]int, 3, 10) fmt.Println("Slice:", slice) // Slice: [0 0 0] fmt.Println("Length:", len(slice)) // Length: 3 fmt.Println("Capacity:", cap(slice)) // Capacity: 10 }

在这个例子中:

切片的长度为 3,表示切片中初始有 3 个元素。切片的容量为 10,表示底层数组的大小为 10,切片可以扩展到最多 10 个元素而不需要重新分配底层数组。 使用切片字面量

golang 中可以直接通过切片字面量初始化,如下所示:

package main import "fmt" func main() { slice := []int{1, 2, 3, 4, 5} fmt.Println(slice) // [1 2 3 4 5] } 从数组或切片创建切片

在 golang 中数组是固定长度的序列,而切片是动态的。可以通过切片表达式从数组或另一个切片创建新的切片,如下所示:

package main import "fmt" func main() { arr := [5]int{1, 2, 3, 4, 5} // 从数组中创建切片 // slice[start:end] 表示从 start 索引开始到 end 索引结束(不包括 end) slice := arr[1:3] fmt.Println(slice) // [2 3] }

在上面的代码实例中,slice 的 start 和 end 是可选的:

如果省略 start,则默认为 0。如果省略 end,则默认为数组的长度。如果同时省略 start 和 end,则表示整个数组。

例如:

package main import "fmt" func main() { array := [5]int{1, 2, 3, 4, 5} slice1 := array[:3] slice2 := array[2:] slice3 := array[:] fmt.Println(slice1) // [1, 2, 3] fmt.Println(slice2) // [3, 4, 5] fmt.Println(slice3) // [1, 2, 3, 4, 5] }

另外 golang 中切片本身也可以被切片,生成新的切片。这与从数组中创建切片的语法类似。

package main import "fmt" func main() { // 定义一个切片 slice := []int{1, 2, 3, 4, 5} // 从切片中创建新的切片 newSlice := slice[1:4] fmt.Println("Original Slice:", slice) // Original Slice: [1 2 3 4 5] fmt.Println("New Slice:", newSlice) // New Slice: [2 3 4] } 切片扩容

参考文章:Golang 切片(slice)源码分析(二、append实现)

这个博主还有很多其他不错的文章,也推荐大家去观看。向大佬致敬👏👏👏

在 golang 语言中,切片的扩容机制是其动态特性的重要体现。当向切片中添加元素时,如果切片的容量不足以容纳新的元素,golang 会自动分配一个新的更大的底层数组,并将旧数组的内容复制到新数组中,这个过程称为切片的扩容。

扩容流程

当调用 append() 函数向切片中添加元素时,如果切片的长度(len(slice))等于其容量(cap(slice)),则触发扩容。扩容的目的是确保切片有足够的空间来容纳新的元素。

首先观察 append 函数的定义,如下所示:

源码文件:src/builtin.go

// The append built-in function appends elements to the end of a slice. If // it has sufficient capacity, the destination is resliced to accommodate the // new elements. If it does not, a new underlying array will be allocated. // Append returns the updated slice. It is therefore necessary to store the // result of append, often in the variable holding the slice itself: // // slice = append(slice, elem1, elem2) // slice = append(slice, anotherSlice...) // // As a special case, it is legal to append a string to a byte slice, like this: // // slice = append([]byte("hello "), "world"...) func append(slice []Type, elems ...Type) []Type

大致解释这个函数定义:

如果切片引用的底层数组中有足够的容量,将元素追加到切片的末尾,不会触发扩容操作。如果切片引用的底层数组中没有足够的容量,将分配一个新的底层数组,并将旧数组的内容拷贝到新数组中。新数组分配完成后,旧数组会被丢弃,切片的指针会被更新为指向新的底层数组,并进行返回。

在 golang 中 append 函数的处理逻辑是直接嵌入到编译器的代码生成阶段的。编译器会根据切片的当前状态(长度和容量)动态生成代码。当切片的容量不足以容纳新元素时,编译器会插入对 runtime.growslice 的调用,如下所示:

源码文件:src/cmd/internal/ssagen/ssa.go

// append converts an OAPPEND node to SSA. // If inplace is false, it converts the OAPPEND expression n to an ssa.Value, // adds it to s, and returns the Value. // If inplace is true, it writes the result of the OAPPEND expression n // back to the slice being appended to, and returns nil. // inplace MUST be set to false if the slice can be SSA'd. // Note: this code only handles fixed-count appends. Dotdotdot appends // have already been rewritten at this point (by walk). func (s *state) append(n *ir.CallExpr, inplace bool) *ssa.Value { // If inplace is false, process as expression "append(s, e1, e2, e3)": // // ptr, len, cap := s // len += 3 // if uint(len) > uint(cap) { // ptr, len, cap = growslice(ptr, len, cap, 3, typ) // Note that len is unmodified by growslice. // } // // with write barriers, if needed: // *(ptr+(len-3)) = e1 // *(ptr+(len-2)) = e2 // *(ptr+(len-1)) = e3 // return makeslice(ptr, len, cap) // // // If inplace is true, process as statement "s = append(s, e1, e2, e3)": // // a := &s // ptr, len, cap := s // len += 3 // if uint(len) > uint(cap) { // ptr, len, cap = growslice(ptr, len, cap, 3, typ) // vardef(a) // if necessary, advise liveness we are writing a new a // *a.cap = cap // write before ptr to avoid a spill // *a.ptr = ptr // with write barrier // } // *a.len = len // // with write barriers, if needed: // *(ptr+(len-3)) = e1 // *(ptr+(len-2)) = e2 // *(ptr+(len-1)) = e3 // ... }

以上这段代码位于负责将 append 的高级表示转换为底层的 SSA 表示。

SSA 静态单赋值是一种编译器中间表示形式,广泛用于编译器优化和分析中。它的核心特征是每个变量在其生命周期内只能被赋值一次。如果一个变量在原始代码中被多次赋值,SSA 会通过引入多个版本的变量来解决这个问题。

例如:

x = 1 x = x + 1

转换为 SSA 形式:

x1 = 1 x2 = x1 + 1

在编译过程中,golang 的源代码首先被转换为抽象语法树(AST),然后进一步转换为 SSA 形式。在 SSA 形式下,编译器可以进行各种优化,如常量折叠、死代码消除等,最终将优化后的 SSA 代码转换为目标机器的机器码。

上面的注释中指出,append 的处理方式分为两种情况,取决于 inplace 参数的值:

inplace = false:将 append 视为一个表达式,返回一个新的切片值。 newSlice := append(s, e1, e2, e3) inplace = true:将 append 的结果写回到原切片变量中,不返回值。 s = append(s, e1, e2, e3)

append 的核心逻辑都是判断是否需要扩容:如果新长度超过当前容量,则调用 runtime.growslice 进行扩容。

// if uint(len) > uint(cap) { // ptr, len, cap = growslice(ptr, len, cap, 3, typ) // Note that len is unmodified by growslice. // }

下面给出 runtime 包下切片实现扩容 growslice 的源码:

源码文件:src/runtime/slice.go

// growslice allocates new backing store for a slice. // // arguments: // // oldPtr = pointer to the slice's backing array // newLen = new length (= oldLen + num) // oldCap = original slice's capacity. // num = number of elements being added // et = element type // // return values: // // newPtr = pointer to the new backing store // newLen = same value as the argument // newCap = capacity of the new backing store // // Requires that uint(newLen) > uint(oldCap). // Assumes the original slice length is newLen - num // ... // oldPtr:旧切片的底层数组指针 // newLen:扩容后切片的期望长度(旧长度 + 追加元素的数量)。 // oldCap:旧切片的容量。 // num:需要追加的元素数量。 // et:元素类型(`_type` 是 `golang` 运行时中描述类型的结构体)。 // slice:分配一个新的底层数组,并将旧数组的内容拷贝到新数组中 func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice { // oldLen 是旧切片的长度,通过 newLen - num 计算得到 oldLen := newLen - num // ... 检测潜在的竞争条件 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 oldPtr in this case. // append 操作不应该创建一个指针为 nil 但长度非零的切片。 // 换句话说,即使切片的底层数组为空,append 也应该返回一个有效的切片,而不是一个指针为 nil 的切片。 // 在这种情况下(即元素大小为零),append 不需要保留旧的指针(oldPtr)。 // 这可能是因为零大小的元素不会占用实际的内存空间,因此不需要保留旧的内存地址。 return slice{unsafe.Pointer(&zerobase), newLen, newLen} } // 调用 nextslicecap 函数计算新的容量 newcap := nextslicecap(newLen, oldCap) // 内存对齐相关代码,返回实际分配内存的大小(需要根据如下两个切片内存字节进行对对齐) // ... roundupsize() // var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32} // var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 24, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768} // The check of overflow in addition to capmem > maxAlloc is needed // to prevent an overflow which can be used to trigger a segfault // on 32bit architectures with this example program: // // type T [1<<27 + 1]int64 // // var d T // var s []T // // func main() { // s = append(s, d, d, d, d) // print(len(s), "\n") // } // 内存溢出检查,如果 capmem 超过了 maxAlloc,或者在计算过程中发生了溢出,程序会抛出一个运行时错误(panic) if overflow || capmem > maxAlloc { panic(errorString("growslice: len out of range")) } // 根据计算出的新容量分配新的底层数组 // 查元素类型是否包含指针。如果元素类型不包含指针,则可以使用更高效的内存分配策略 var p unsafe.Pointer if !et.Pointers() { // mallocgc 是 Go 运行时中的内存分配函数,负责分配内存并处理垃圾回收相关的逻辑 p = mallocgc(capmem, nil, false) // 只清零新分配的内存中不会被 append 操作覆盖的部分。避免不必要的内存清零 memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem) } else { // mallocgc 是 Go 运行时中的内存分配函数,负责分配内存并处理垃圾回收相关的逻辑 p = mallocgc(capmem, et, true) // 如果旧切片的内存大小(以字节为单位)大于零,即旧切片中有数据需要复制,并且开启了启用了写屏障 if lenmem > 0 && writeBarrier.enabled { // 复制数据之前标记旧内存中的指针,通知 GC,以便 GC 能够正确处理这些指针 bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.Size_+et.PtrBytes, et) } } // 将旧数组的内容拷贝到新的底层数组中 memmove(p, oldPtr, lenmem) // 返回一个新的切片结构体,包含新的指针、长度和容量 return slice{p, newLen, newcap} } // nextslicecap computes the next appropriate slice length. // 用于计算切片扩容时新容量的函数 nextslicecap。 // 它的作用是根据当前切片的长度和旧容量,计算出一个合适的新容量。 func nextslicecap(newLen, oldCap int) int { // 初始化新容量为旧容量 newcap := oldCap // 计算旧容量的两倍 doublecap := newcap + newcap // 如果新长度超过旧容量的两倍,直接返回新长度 if newLen > doublecap { return newLen } // 定义阈值,用于区分小切片和大切片 const threshold = 256 if oldCap < threshold { return doublecap // 如果旧容量小于阈值,直接扩容为两倍 } for { // 大于阈值的切片,逐步增加容量,每次大约增加切片容量的1.25 newcap += (newcap + 3*threshold) >> 2 // 检查新容量是否满足需求,同时防止溢出 if uint(newcap) >= uint(newLen) { // 如果新容量大于等于新长度,退出循环 break } } // 如果新容量计算溢出,返回新长度 if newcap <= 0 { return newLen } // 返回计算后的新容量 return newcap }

总结这个函数的主要计算容量的逻辑:

如果新长度大于旧容量的两倍,则直接使用新长度作为新容量。如果旧容量小于阈值(256),则新容量为旧容量的两倍。如果旧容量大于阈值(256),则新容量会按照约 1.25 倍(趋近于1.25)增长。 优化建议

从切片的扩容流程的分析中看出,当底层引用数组触发扩容时会重新分配数组并进行数据的拷贝动作,频繁的扩容是十分耗费性能,所以在实际使用时尽可能保证以下优化建议:

预分配容量:如果已知切片的最终大小,建议在初始化时分配足够的容量,以减少扩容次数。 package main import "fmt" func main() { s := make([]int, 0, 10) // 预分配容量为 10 s = append(s, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}...) fmt.Println(s) // [0 1 2 3 4 5 6 7 8 9] fmt.Println(len(s)) // 10 fmt.Println(cap(s)) // 10 } 批量追加:如果需要追加多个元素,尽量使用一次 append 调用,而不是多次调用,如果一次性追加多个元素,编译器可以更高效地管理切片的扩容逻辑,减少不必要的内存分配。 package main import "fmt" func main() { // 追加单个元素 s := []int{1, 2, 3} s = append(s, 4) fmt.Println(s) // [1 2 3 4] // 批量追加元素 s = append(s, 5, 6, 7, 8) fmt.Println(s) // [1 2 3 4 5 6 7 8] } 常用操作 添加元素 package main import "fmt" func main() { s := []int{1, 2, 3} fmt.Println(s) // [1 2 3] s = append(s, 4) fmt.Println(s) // [1 2 3 4] s = append(s, 5, 6) fmt.Println(s) // [1, 2, 3, 4, 5, 6] s = append(s, []int{7, 8}...) fmt.Println(s) // [1, 2, 3, 4, 5, 6, 7, 8] } 复制切片 package main import "fmt" func main() { s1 := []int{1, 2, 3} s2 := make([]int, len(s1)) copy(s2, s1) fmt.Println(s2) // [1 2 3] } 删除元素 通过切片操作删除 package main import "fmt" func main() { s := []int{1, 2, 3, 4, 5} index := 2 // 删除索引为2的元素 s = append(s[:index], s[index+1:]...) fmt.Println(s) // [1 2 4 5] } 通过 copy 删除 package main import "fmt" func main() { s := []int{1, 2, 3, 4, 5} index := 2 copy(s[index:], s[index+1:]) s = s[:len(s)-1] fmt.Println(s) // [1, 2, 4, 5] } 遍历切片 for 循环 package main import "fmt" func main() { s := []int{1, 2, 3, 4, 5} for i := 0; i < len(s); i++ { fmt.Println(s[i]) } } for range 循环 package main import "fmt" func main() { // Index: 0, Value: 1 // Index: 1, Value: 2 // Index: 2, Value: 3 // Index: 3, Value: 4 // Index: 4, Value: 5 s := []int{1, 2, 3, 4, 5} for index, value := range s { fmt.Printf("Index: %d, Value: %d\n", index, value) } } 切片中元素的比较

切片不能直接比较(不能用 == 或 !=)。如果需要比较两个切片是否相等,可以逐个元素比较:

package main import "fmt" func main() { s1 := []int{1, 2, 3} s2 := []int{1, 2, 3} equal := true if len(s1) != len(s2) { equal = false } else { for i := range s1 { if s1[i] != s2[i] { equal = false break } } } fmt.Println(equal) // true } 切片排序

使用 sort 包对切片进行排序:

package main import ( "fmt" "sort" ) func main() { s := []int{3, 1, 4, 1, 5, 9, 2, 6} sort.Ints(s) // 升序排序 fmt.Println(s) // [1 1 2 3 4 5 6 9] sort.Sort(sort.Reverse(sort.IntSlice(s))) // 降序排序 fmt.Println(s) // [9 6 5 4 3 2 1 1] } 查找某个元素

切片没有直接的方法来检查是否包含某个元素,但可以通过遍历切片来实现:

package main import "fmt" func main() { slice := []int{1, 2, 3, 4, 5} // 检查元素是否存在 element := 3 contains := false for _, value := range slice { if value == element { contains = true break } } if contains { fmt.Printf("Yes") // Yes } else { fmt.Printf("No") } }

这种检查方式的时间复杂度是 O(n),因为需要遍历整个切片。

如果需要频繁检查元素是否存在,可以考虑使用 map 来优化性能将查找时间复杂度降低到 O(1):

package main import "fmt" func main() { slice := []int{1, 2, 3, 4, 5} emap := make(map[int]bool) for _, value := range slice { emap[value] = true } fmt.Println(emap[3]) // true fmt.Println(emap[6]) // false } 常见问题 切片底层数组共享问题

切片是对底层数组的引用,多个切片可能共享同一个底层数组。对一个切片的修改可能会影响其他切片。

package main import "fmt" func main() { s1 := []int{1, 2, 3, 4, 5} s2 := s1[1:3] // s2 = [2, 3],共享底层数组 s2[0] = 99 // 修改 s2 的第一个元素 fmt.Println(s1) // [1 99 3 4 5] }

如果需要独立修改切片,可以使用 copy 创建一个独立的副本:

package main import "fmt" func main() { s1 := []int{1, 2, 3, 4, 5} s2 := make([]int, len(s1[1:3])) copy(s2, s1[1:3]) // s2 现在是一个独立的副本 s2[0] = 99 // 修改 s2 的第一个元素 fmt.Println(s1) // [1 2 3 4 5] } 切片扩容机制导致的性能问题

切片的容量会根据需要自动扩容,但扩容操作会分配新的底层数组并复制数据,这可能导致性能问题,尤其是在频繁修改切片时。

package main import "fmt" func main() { s := make([]int, 0, 2) // 初始容量为 2 // Length: 1, Capacity: 2 // Length: 2, Capacity: 2 // Length: 3, Capacity: 4 // Length: 4, Capacity: 4 // Length: 5, Capacity: 8 // Length: 6, Capacity: 8 // Length: 7, Capacity: 8 // Length: 8, Capacity: 8 // Length: 9, Capacity: 16 // Length: 10, Capacity: 16 for i := 0; i < 10; i++ { s = append(s, i) fmt.Printf("Length: %d, Capacity: %d\n", len(s), cap(s)) } }

如果已知切片的最终大小,可以在创建时分配足够的容量,避免多次扩容。

package main import "fmt" func main() { s := make([]int, 0, 10) // 预分配容量为 10 // Length: 1, Capacity: 10 // Length: 2, Capacity: 10 // Length: 3, Capacity: 10 // Length: 4, Capacity: 10 // Length: 5, Capacity: 10 // Length: 6, Capacity: 10 // Length: 7, Capacity: 10 // Length: 8, Capacity: 10 // Length: 9, Capacity: 10 // Length: 10, Capacity: 10 for i := 0; i < 10; i++ { s = append(s, i) fmt.Printf("Length: %d, Capacity: %d\n", len(s), cap(s)) } }

或者尽量减少 append 的调用次数,例如通过批量处理数据。

package main func main() { s := make([]int, 0, 2) // 预分配容量为 2 s = append(s, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}...) } 切片的零值和 nil 判断

切片的零值是 nil,但 nil 切片和空切片(长度为 0 的切片)在某些情况下可能需要区分。

package main import "fmt" func main() { var s1 []int // nil 切片 s2 := []int{} // 空切片 fmt.Println(s1 == nil) // true fmt.Println(s2 == nil) // false fmt.Println(len(s1)) // 0 fmt.Println(cap(s1)) // 0 fmt.Println(len(s2)) // 0 fmt.Println(cap(s2)) // 0 }

如果需要处理用户输入的切片,建议在函数中明确检查 nil 和长度:

package main import "fmt" func main() { var s []int // nil 切片 if s == nil || len(s) == 0 { fmt.Println("切片为空或 nil") } } 切片作为函数参数的陷阱

切片作为函数参数时,由于是切片本身是引用类型(对底层数组的引用),函数内部对切片的修改会影响原始切片。如果需要避免这种情况,需要在函数内部创建副本。

package main import "fmt" func modifySlice(s []int) { s[0] = 99 } func main() { s := []int{1, 2, 3} modifySlice(s) fmt.Println(s) // [99 2 3] }

但是需要注意的是,在函数传递时只要不是指针传递都是值拷贝,也就是说 s 本身是 modifySlice 在 main 的副本,如果修改 s 本身(注意不是修改 s 底层引用的数组中的元素)对于 main 函数来说是没有任何影响的。

package main import "fmt" func modifySlice1(s []int) { s = []int{4, 5, 6, 7} } func modifySlice2(s *[]int) { *s = []int{4, 5, 6, 7} } func main() { s := []int{1, 2, 3} modifySlice1(s) fmt.Println(s) // [1 2 3] modifySlice2(&s) fmt.Println(s) // [4 5 6 7] }

🌺🌺🌺撒花!

如果本文对你有帮助,就点关注或者留个👍 如果您有任何技术问题或者需要更多其他的内容,请随时向我提问。

标签:

【十】Golang切片由讯客互联软件开发栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“【十】Golang切片