目录

go如何正确删除切片内元素

go 的切片删除逻辑有隐藏bug——遍历删除重复元素时存在漏删,排查后才发现是遍历顺序没处理好。

其实Go的切片因为是动态数组,删除元素不像其他语言的集合那样有现成方法,稍不注意就会踩坑。

切片删除的本质是通过append函数重新组合元素,因为切片的底层是数组,删除元素后需要调整底层数组的引用和长度。

不同场景对应不同方法,先看最常用的三种。

方案一、重新分配新切片

已知要删除元素的索引时,用这种方法最高效,核心是把索引前后的元素通过append拼接。

比如要删除切片slice中索引为index的元素:

// 删除索引i处的元素(保持剩余元素顺序)
func removeOrdered(s []int, i int) []int {
     arr:=make([]int,0,len(s)-1)
     arr=apppen(arr,s[:i]...)
     arr=apppen(arr,s[i+1:]...)
    return arr
}

func main() {
    s := []int{1, 2, 3, 4, 5}
    s = removeOrdered(s, 2) // 删除索引2的元素(3)
    fmt.Println(s) // [1 2 4 5]
}

常用、保持元素顺序、数据量大性能差

方案二、快速删除

快速删除、但是无法保持原顺序

// 快速删除(不保持顺序,但时间复杂度O(1))
func removeUnordered(s []int, i int) []int {
    s[i] = s[len(s)-1] // 用最后一个元素覆盖要删除的元素
    return s[:len(s)-1] // 截掉最后一个元素
}

func main() {
    s := []int{1, 2, 3, 4, 5}
    s = removeUnordered(s, 2) // 删除索引2的元素(3)
    fmt.Println(s) // [1 2 5 4]
}

方案三、使用copy函数删除元素

func removeWithCopy(s []int, i int) []int {
    copy(s[i:], s[i+1:])
    return s[:len(s)-1]
}

func main() {
    s := []int{1, 2, 3, 4, 5}
    s = removeWithCopy(s, 2) // 删除索引2的元素(3)
    fmt.Println(s) // [1 2 4 5]
}

方案四、 按指定值删除

如果要删除切片中所有等于某个指定值的元素(比如删除所有"error"字符串),可以结合遍历和索引判断实现。

比如删除切片中所有值为3的元素:

package main

import "fmt"

func main() {
    nums := []int{1, 3, 2, 3, 4, 3, 5}
    var newNums []int
    
    target := 3 // 要删除的指定值
    for _, num := range nums {
        if num != target { // 保留不等于目标值的元素
            newNums = append(newNums, num)
        }
    }
    
    nums = newNums
    fmt.Println(nums) // 输出:[1 2 4 5]
}

泛型实现

func remove[T any](s []T, i int) []T {
    arr:=make([]T,0,len(s)-1)
     arr=apppen(arr,s[:i]...)
     arr=apppen(arr,s[i+1:]...)
    return arr
}

常见避坑

在删除切片时会遇到漏删、panic、内存泄漏等问题,其实都是没理解切片的底层原理。

Q1:正序遍历删除时漏删元素

这是最常见的坑!比如正序遍历删除所有偶数时,会发现漏删了相邻的偶数:

// 错误示例:正序遍历删除漏删
nums := []int{2, 4, 6, 8}
for i := 0; i < len(nums); i++ {
    if nums[i]%2 == 0 {
        nums = append(nums[:i], nums[i+1:]...)
        // 问题:删除后切片长度减1,i递增后会跳过下一个元素
    }
}
fmt.Println(nums) // 输出:[4 8](漏删了4和8)

原因:正序删除时,删除元素后切片长度减少1,下一个元素会移动到当前索引位置,但i会继续递增,导致跳过这个元素。

解决方法:删除后让i减1,或者直接用倒序遍历:

// 正确示例:倒序遍历删除
nums := []int{2, 4, 6, 8}
for i := len(nums) - 1; i >= 0; i-- {
    if nums[i]%2 == 0 {
        nums = append(nums[:i], nums[i+1:]...)
    }
}
fmt.Println(nums) // 输出:[](全部删除成功)

Q2:删除后切片内存未释放导致泄漏

切片删除元素后,底层数组的容量(cap)不会自动缩小,原来的元素还占用内存,大量操作时会导致内存泄漏。

比如一个长度10000的切片,删除后只剩10个元素,但cap还是10000:

nums := make([]int, 10000, 10000)
nums = nums[:10] // 只保留前10个元素
fmt.Println(len(nums), cap(nums)) // 输出:10 10000(cap未变)

解决方法:创建新切片并指定容量,让底层数组重新分配内存:

nums := make([]int, 10000, 10000)
// 重新创建切片,容量设为长度,释放多余内存
newNums := make([]int, 0, 10)
newNums = append(newNums, nums[:10]...)
nums = newNums
fmt.Println(len(nums), cap(nums)) // 输出:10 10(cap正常)

Q3:删除nil切片或空切片报错

如果切片是nil或者长度为0,直接删除会panic吗?先看测试代码:

var nilSlice []int // nil切片
emptySlice := []int{} // 空切片(len=0, cap=0)

// 测试删除nil切片
nilSlice = append(nilSlice[:0], nilSlice[1:]...) 
fmt.Println(nilSlice) // 输出:[](不报错)

// 测试删除空切片
emptySlice = append(emptySlice[:0], emptySlice[1:]...)
fmt.Println(emptySlice) // 输出:[](不报错)

结论:nil切片和空切片删除时不会panic,但索引不能超出范围。

不过实际开发中还是建议先判断切片是否为空,避免无效操作,比如在工具函数中加前置判断:

if len(slice) == 0 {
    fmt.Println("切片为空,无需删除")
    return slice
}

Q4:删除后原切片被意外修改

因为切片是引用类型,指向底层数组,所以如果多个切片引用同一个底层数组,删除其中一个切片的元素会影响其他切片:

// 错误示例:多个切片共享底层数组
slice1 := []int{1, 2, 3, 4}
slice2 := slice1[:3] // slice2引用slice1的底层数组

// 删除slice2的元素
slice2 = append(slice2[:1], slice2[2:]...)
fmt.Println(slice2) // 输出:[1 3]
fmt.Println(slice1) // 输出:[1 3 3 4](slice1被意外修改)

解决方法:删除前先复制切片,创建新的底层数组,避免关联影响:

// 正确示例:复制切片后再删除
slice1 := []int{1, 2, 3, 4}
// 复制slice1的前3个元素到新切片,创建独立底层数组
slice2 := make([]int, 3)
copy(slice2, slice1[:3])

// 再删除slice2的元素
slice2 = append(slice2[:1], slice2[2:]...)
fmt.Println(slice2) // 输出:[1 3]
fmt.Println(slice1) // 输出:[1 2 3 4](slice1不受影响)

总结

不同场景选对方法能少走很多弯路

其实Go切片删除的核心就是理解“引用类型+append重组”的本质,避开遍历顺序、内存泄漏这些坑,基本就能熟练运用了。

如果大家还有其他踩坑经历,欢迎在评论区分享交流~

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-slice-del-el/

备用原文链接: https://blog.fiveyoboy.com/articles/go-slice-del-el/