Remove from slice inplace in Golang

时间:2019-05-31 11:54:30

标签: loops go iterator slice

I have the following test that is printing the original input slice (after the filtering) without the element that has been removed, but with an extra element at the end making the input slice of the same length, even if after the filtering it should be shorter.

I've gone through this doc https://github.com/golang/go/wiki/SliceTricks#delete However I think I am missing some gotchas about Go, because it seems I am using slices with the wrong approach.

  • how can I avoid to have an "output slice"? (which is printed in the correct way, containing the right elements, having the expected length and capacity)
  • why my attempt at "removing inplace" leads to having the "input slice" with the same length as it had before the filtering process?
  • why the "input slice" has the same length as before I was applying the filtering process? How can I make the remove operation to change the length of the "input slice"?

This is the code:

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))
}

This is the output of the test execution:

$ 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

Update on the "input slice as pointer"

OK, so assuming I would like to deal with the original input slice, i.e. no copy or output slice.

  • Why the following code throws a runtime panic in the commented line of code? (pointedInSl[inLen-1] = FooItem{})
  • Why the printed slice (after applying the function) contains 2 identical itmes at the end of it? How con I remove the last redundant element?
  • Why the length of the slice after applying the function is still the same as the one of the slice before applying the function?
  • How can I make the original slice shrink of 1 (i.e. being of output length = original length - 1)?

This is the code:

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)
}

This is the weird output:

$ 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

2 个答案:

答案 0 :(得分:4)

当您拥有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]),新切片将与原始切片共享后备阵列。这意味着,如果您修改新切片的 elements ,则原始切片也会观察到这些变化。因此,如果您从新切片中删除一个元素,然后将这些元素复制到已删除元素的位置,则原始切片的最后一个元素仍将存在,该位置将被原始切片“覆盖”。通常的做法是将最后一个元素清零,以便垃圾收集器可以回收其内存(如果它是指针类型)(或“类似”的切片,映射或通道)。有关详细信息,请参见Memory leak in golang sliceDoes go garbage collect parts of slices?

直接回答您的问题:

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

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

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

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

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

这是由前两个问题回答的。

查看相关问题:

Are golang slices pass by value?

答案 1 :(得分:0)

我建议对icza答案进行编辑,以在其底部提供一个最小的工作代码示例,以说明他提供的有用信息。被拒绝,指出它作为编辑是没有意义的,它应该被写为评论或答案,所以就在这里(功劳主要来自icza):

最小工作代码示例(带注释以提供一些上下文):

// 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
}