当前位置: 首页 > news >正文

【十】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]
}

在上面的代码实例中,slicestartend 是可选的:

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

例如:

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

大致解释这个函数定义:

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

golangappend 函数的处理逻辑是直接嵌入到编译器的代码生成阶段的。编译器会根据切片的当前状态(长度和容量)动态生成代码。当切片的容量不足以容纳新元素时,编译器会插入对 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 本身是 modifySlicemain 的副本,如果修改 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]
}

🌺🌺🌺撒花!

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

相关文章:

  • MySQL数据库入门到大蛇尚硅谷宋红康老师笔记 基础篇 part 14
  • 以太网详解(八)传输层协议:TCP/UDP 协议
  • 基于Springboot+Vue前后端分离的农场投入品运营线上管理系统设计与实现+万字文档+指导搭建视频
  • c/c++蓝桥杯经典编程题100道(19)汉诺塔问题
  • VSCode 实用快捷键
  • 人工智能基础之数学基础:01高等数学基础
  • ASCII 与 Unicode:两种字符编码的定义和不同
  • 15.3.10 窗体下使用多线程
  • C语言的区块链
  • git pull 与 git pull --rebase的区别与使用
  • Python Cookbook-1.13 访问子字符串
  • 用React实现一个登录界面
  • docker修改镜像默认存储路径(基于 WSL2 的迁移方法)
  • 【SpringBoot3】Spring Boot 3.0 集成 Mybatis Plus
  • 嵌入式学习第十六天--stdio(二)
  • 【PHP】php+mysql 活动信息管理系统(源码+论文+数据库+数据库文件)【独一无二】
  • Ubuntu 系统 cuda12.2 安装 MMDetection3D
  • LeetCode--23. 合并 K 个升序链表【堆和分治】
  • 从零开始部署DeepSeek:基于Ollama+Flask的本地化AI对话系统
  • Everything——你的文件搜索效率革命
  • 泽连斯基:俄代表团级别低,没人能做决定
  • 六省会共建交通枢纽集群,中部离经济“第五极”有多远?
  • 体坛联播|博洛尼亚时隔51年再夺意杯,皇马逆转马洛卡
  • 马上评|安排见义勇为学生补考,善意与善意的双向奔赴
  • 中国乒协坚决抵制恶意造谣,刘国梁21日将前往多哈参加国际乒联会议
  • 佩斯科夫:俄方代表团15日将在伊斯坦布尔等候乌克兰代表团