【问题标题】:Remove from slice inplace in Golang在 Golang 中从切片中删除
【发布时间】:2019-10-17 01:50:31
【问题描述】:

我有以下测试正在打印原始输入切片(过滤后),但没有删除已删除的元素,但最后有一个额外元素使输入切片的长度相同,即使在过滤后也是如此它应该更短。

我已经浏览了这个文档https://github.com/golang/go/wiki/SliceTricks#delete 但是我认为我错过了一些关于 Go 的陷阱,因为我似乎使用了错误的方法来使用切片。

  • 如何避免出现“输出切片”? (以正确的方式打印,包含正确的元素,具有预期的长度和容量)
  • 为什么我尝试“移除就地”会导致“输入切片”的长度与过滤过程之前的长度相同?
  • 为什么“输入切片”的长度与我应用过滤过程之前的长度相同?如何进行删除操作以更改“输入切片”的长度?

这是代码:

package foo

import (
    "fmt"
    "log"
    "math/rand"
    "testing"
)

type FooItem struct {
    Id       int
    Category string
    Value    float64
}

const minRand = 0
const maxRand = 10

const maxSliceLen = 3

var inFooSlice []FooItem

func init() {
    for i := 1; i <= maxSliceLen; i++ {
        inFooSlice = append(inFooSlice, FooItem{
            Id:       i,
            Category: "FooCat",
            Value:    minRand + rand.Float64()*(maxRand-minRand),
        })
    }
}

// this is the function I am testing
func FindAndRemoveFromFooSlice(iFilter int, inSl []FooItem) (*FooItem, []FooItem) {

    inLen := len(inSl)
    outSl := make([]FooItem, inLen)

    for idx, elem := range inSl {
        if elem.Id == iFilter {
            log.Printf("Loop ID %v", idx)

            // check these docs: https://github.com/golang/go/wiki/SliceTricks#delete
            outSl = inSl[:idx+copy(inSl[idx:], inSl[idx+1:inLen])]
            outSl = outSl[:inLen-1]

            return &elem, outSl
        }
    }
    return nil, nil
}

func TestFoo(t *testing.T) {
    fmt.Printf("\nOriginal (PRE) slice\n")
    fmt.Println(inFooSlice)
    fmt.Println(len(inFooSlice))
    fmt.Println(cap(inFooSlice))

    idFilter := 1

    fePtr, outFooSlice := FindAndRemoveFromFooSlice(idFilter, inFooSlice)

    fmt.Printf("\nOriginal (POST) slice\n")
    fmt.Println(inFooSlice)
    fmt.Println(len(inFooSlice))
    fmt.Println(cap(inFooSlice))

    fmt.Printf("\nFiltered element\n")
    fmt.Println(*fePtr)

    fmt.Printf("\nOutput slice\n")
    fmt.Println(outFooSlice)
    fmt.Println(len(outFooSlice))
    fmt.Println(cap(outFooSlice))
}

这是测试执行的输出:

$ go test -v -run TestFoo
=== RUN   TestFoo

Original (PRE) slice
[{1 FooCat 6.046602879796196} {2 FooCat 9.405090880450125} {3 FooCat 6.645600532184904}]
3
4
2019/05/31 12:53:30 Loop ID 0

Original (POST) slice
[{2 FooCat 9.405090880450125} {3 FooCat 6.645600532184904} {3 FooCat 6.645600532184904}]
3
4

Filtered element
{1 FooCat 6.046602879796196}

Output slice
[{2 FooCat 9.405090880450125} {3 FooCat 6.645600532184904}]
2
4
--- PASS: TestFoo (0.00s)
PASS
ok      git.openenergi.net/scm/flex/service/common  0.008s

更新“输入切片作为指针”

好的,假设我想处理原始输入切片,即没有复制或输出切片。

  • 为什么下面的代码会在注释的代码行中引发运行时恐慌? (pointedInSl[inLen-1] = FooItem{})
  • 为什么打印的切片(应用该功能后)的末尾包含 2 个相同的项目?如何删除最后一个冗余元素?
  • 为什么应用函数后的切片长度和应用函数前的切片长度一样?
  • 如何使原始切片缩小 1(即输出长度 = 原始长度 - 1)?

这是代码:

func FindAndRemoveFromFooSliceInPlace(iFilter int, inSl *[]FooItem) *FooItem {
    pointedInSl := *inSl
    inLen := len(pointedInSl)
    for idx, elem := range pointedInSl {
        if elem.Id == iFilter {
            log.Printf("Loop ID %v", idx)

            // check these docs: https://github.com/golang/go/wiki/SliceTricks#delete
            pointedInSl = append(pointedInSl[:idx], pointedInSl[idx+1:inLen]...)
            // pointedInSl[inLen-1] = FooItem{} // why this throws a runtime "panic: runtime error: index out of range" ???
            pointedInSl = pointedInSl[:inLen-1]

            return &elem
        }
    }
    return nil
}

func TestFooInPlace(t *testing.T) {
    fmt.Printf("\nOriginal (PRE) slice\n")
    fmt.Println(inFooSlice)
    fmt.Println(len(inFooSlice))
    fmt.Println(cap(inFooSlice))

    idFilter := 1

    fePtr := FindAndRemoveFromFooSliceInPlace(idFilter, &inFooSlice)

    fmt.Printf("\nOriginal (POST) slice\n")
    fmt.Println(inFooSlice)
    fmt.Println(len(inFooSlice))
    fmt.Println(cap(inFooSlice))

    fmt.Printf("\nFiltered element\n")
    fmt.Println(*fePtr)
}

这是奇怪的输出:

$ go test -v -run TestFooInPlace
=== RUN   TestFooInPlace

Original (PRE) slice
[{1 FooCat 6.046602879796196} {2 FooCat 9.405090880450125} {3 FooCat 6.645600532184904}]
3
4
2019/05/31 16:32:38 Loop ID 0

Original (POST) slice
[{2 FooCat 9.405090880450125} {3 FooCat 6.645600532184904} {3 FooCat 6.645600532184904}]
3
4

Filtered element
{1 FooCat 6.046602879796196}
--- PASS: TestFooInPlace (0.00s)
PASS
ok      git.openenergi.net/scm/flex/service/common  0.007s

【问题讨论】:

    标签: loops go iterator slice


    【解决方案1】:

    当你有一个int 类型的变量,并且你想写一个增加它的值的函数,你怎么做呢?您要么将指针传递给变量,要么返回必须分配给原始变量的递增值。

    例如(在Go Playground 上试试):

    func inc(i int) int { i++; return i }
    
    var i int = 2
    inc(i)
    fmt.Println(i) // This will be 2
    

    在上面的代码中,你将i 传递给inc(),它会增加它并返回它的值。原来的i当然不会变,i里面的inc()只是一个拷贝,独立于原来的i。要更改原始值,您必须设置返回值:

    i = inc(i)
    

    或者首先使用指针(在Go Playground上试试):

    func inc(i *int) { *i++ }
    
    var i int = 2
    inc(&i)
    fmt.Println(i) // This will be 3
    

    切片也是如此。如果您想/必须修改切片头(这是一个数据指针、长度和容量,请参阅reflect.SliceHeader),您要么必须传递一个指向该切片的指针(不是很常见),要么您必须返回修改后的, 您必须在调用者处分配的新切片标头。这是更常用的解决方案,这也是内置的append() 遵循的方法。

    当您对切片进行切片时(例如someslice[min:max]),新切片将与原始切片共享支持数组。这意味着如果您修改新切片的 元素,原始切片也会观察到这些变化。因此,如果您从新切片中删除一个元素并将元素复制到已删除元素的位置,则原始切片的最后一个元素仍然存在,它被原始切片“覆盖”。通常的做法是将最后一个元素归零,以便垃圾收集器可以在它是指针类型(或“类似”,如切片、映射或通道)时回收其内存。详情见Memory leak in golang sliceDoes go garbage collect parts of slices?

    直接回答您的问题:

    • 如何避免出现“输出切片”? (以正确的方式打印,包含正确的元素,具有预期的长度和容量)

    如本答案所述:您必须将指针传递给切片,并修改 FindAndRemoveFromFooSlice() 中的指向值,因此您不必返回新切片。

    • 为什么我尝试“就地移除”会导致“输入切片”的长度与过滤过程之前的长度相同?

    您从未修改过原始切片,您将其传递给了副本,而在FindAndRemoveFromFooSlice() 中您只能修改副本(但您甚至没有修改副本)。您返回一个新切片,但您没有分配它,因此原始切片(标题)是完整的。

    • 为什么“输入切片”的长度与我应用过滤过程之前的长度相同?如何进行删除操作以更改“输入切片”的长度?

    前两个问题已经回答了这个问题。

    查看相关问题:

    Are golang slices pass by value?

    slice vs map to be used in parameter

    【讨论】:

    • 感谢概述和外部资源,我仍然不明白为什么在缩小作为指针提供的原始输入切片时无法“删除”最后一项(“重复”) .我已经用更多代码更新了原始问题。谢谢
    • @tppz 因为您取消引用了指针:pointedInSl := *inSl,它创建了一个副本,并且您只分配给这个副本。在返回之前,请务必将新切片分配给 pointed 值,如下所示:*inSl = pointedInSl.
    【解决方案2】:

    我建议对 icza 答案进行编辑,以便在其底部提供一个最低限度的工作代码示例,以获取他提供的有用信息。它被拒绝了,说它作为一个编辑没有意义,它应该被写成评论或答案,所以这里是(主要归功于 icza):

    最小工作代码示例(用 cmets 给出一些上下文):

    // use a pointer for the input slice so then it is changed in-place
    func FindAndRemoveFromFooSliceInPlace(iFilter int, inSl *[]FooItem) *FooItem {
        pointedInSl := *inSl // dereference the pointer so then we can use `append`
        inLen := len(pointedInSl)
        for idx, elem := range pointedInSl {
            if elem.Id == iFilter {
                log.Printf("Loop ID %v", idx)
    
                // check these docs: https://github.com/golang/go/wiki/SliceTricks#delete
                pointedInSl = append(pointedInSl[:idx], pointedInSl[idx+1:inLen]...)
                pointedInSl = pointedInSl[:inLen-1]
                *inSl = pointedInSl // assigning the new slice to the pointed value before returning
    
                return &elem
            }
        }
        return nil
    }
    

    【讨论】:

      猜你喜欢
      • 2018-06-15
      • 2016-03-10
      • 2016-09-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-07-08
      • 1970-01-01
      相关资源
      最近更新 更多