Golang for range 中 Slice 安全删除元素

为什么简单append不安全?

平时开发经常遇到删除Slice中的元素,但像下面这样是不行的

package main
import "fmt"
func main() {
        // 初始化包含重复数字的切片
        a := []int{1, 2, 3, 4, 5, 5, 6, 5, 7, 8, 9, 10}

        fmt.Println("初始切片:", a)

        // 使用 range 遍历切片,尝试删除所有的5
        for i, value := range a {
                if value == 5 {
                        // 删除当前元素
                        a = append(a[:i], a[i+1:]...)
                        fmt.Printf("删除元素后的切片(当前删除的元素5的索引: %d): %v\n", i, a)
                }
        }

        fmt.Println("最终切片:", a)
}

运行后输出:

初始切片: [1 2 3 4 5 5 6 5 7 8 9 10]
删除元素后的切片(当前删除的元素5的索引: 4): [1 2 3 4 5 6 5 7 8 9 10]
删除元素后的切片(当前删除的元素5的索引: 6): [1 2 3 4 5 6 7 8 9 10]
最终切片: [1 2 3 4 5 6 7 8 9 10]

漏了一个元素5没有删除
原因就在遍历中删除元素后,后续的元素都向前移动了一位,但 range 循环的索引 i 仍然递增,导致实际上跳过了一个本应该检查的元素(因为它现在位于前一个索引位置)。

解决方法

1. 使用索引操作

可以在遍历时使用额外的索引来控制修改操作,以确保不会因为切片变化而影响遍历结果。

package main

import "fmt"

func main() {
        a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
        n := len(a) // 获取初始长度
        for i := 0; i < n; i++ {
                if a[i] == 5 {
                        // 删除元素5,并调整切片长度
                        a = append(a[:i], a[i+1:]...)
                        // 减小n和i以适应新的切片长度
                        n--
                        i--
                }
        }

        for _, item := range a {
                fmt.Println(item)
        }
}

2. 逆序遍历

另一种方法是从后向前遍历和修改数组,这样即使数组大小变化,也不会影响到未遍历到的元素。

package main

import "fmt"

func main() {
        a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
        for i := len(a) - 1; i >= 0; i-- {
                if a[i] == 5 {
                        a = append(a[:i], a[i+1:]...)
                }
        }

        for _, item := range a {
                fmt.Println(item)
        }
}

3. 新建一个数组

这种方法缺点是要额外空间,但是更好理解和实现

package main

import "fmt"

func main() {
        a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
        // 创建一个新的切片用于存放删除特定元素后的结果
        var b []int

        for _, item := range a {
                if item != 5 {
                        b = append(b, item)
                }
        }

        // 输出新的切片,已经不包含元素5
        for _, item := range b {
                fmt.Println(item)
        }
}

我比较喜欢第二种,简单的改成倒序就可以,也无需额外空间
关于golang slice的更多细节和扩容介绍可以看我的博客
https://www.cztcode.com/2023/5094/

发表评论