Remove from slice inplace in Golang

偶尔善良 提交于 2020-03-23 12:08:08

问题


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

回答1:


When you have a variable of int type, and you want to write a function that increments its value, how do you do it? You either pass a pointer to the variable, or you return the incremented value which you have to assign to the original variable.

For example (try it on the Go Playground):

func inc(i int) int { i++; return i }

var i int = 2
inc(i)
fmt.Println(i) // This will be 2

In the above code you pass i to inc() which increments it and returns its value. The original i of course will not change, i inside inc() is just a copy, independent form the original i. For the original to change, you would have to assing the return value:

i = inc(i)

Or use pointers in the first place (try it on the Go Playground):

func inc(i *int) { *i++ }

var i int = 2
inc(&i)
fmt.Println(i) // This will be 3

The same thing goes with slices. If you want / have to modify the slice header (which is a data pointer, length and capacity, see reflect.SliceHeader), you either have to pass a pointer to that slice (not very common), or you have to return the modified, new slice header which you have to assign at the caller. This is the solution used more often, this is the approach the builtin append() follows too.

When you slice a slice, (e.g. someslice[min:max]), the new slice will share the backing array with the original one. Which means if you modify the elements of the new slice, the original will also observe those changes. So if you remove an element from the new slice and you copy the elements to the place of the removed element, the last element of the original slice will still be there, which is "covered" by the original slice. Usual practice is to zero the last element so the garbage collector can reclaim its memory should it be a pointer type (or "similar" like a slice, map or channel). For details, see Memory leak in golang slice and Does go garbage collect parts of slices?

To answer you questions directly:

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

As stated in this answer: you would have to pass a pointer to your slice, and modify the pointed value in FindAndRemoveFromFooSlice(), and so you don't have to return the new slice.

  • why my attempt at "removing inplace" leads to having the "input slice" with the same length as it had before the filtering process?

You never modified the original slice, you passed it so a copy is made, and inside FindAndRemoveFromFooSlice() you can only modify the copy (but you didn't even modify the copy). You return a new slice, but you didn't assign it, so the original slice (header) is intact.

  • 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 answered by the previous 2 questions.

See related questions:

Are golang slices pass by value?

slice vs map to be used in parameter




回答2:


I suggested an edit of icza answer to provide at its bottom a minimum working code example for the useful information he gave. It was rejected stating that it makes no sense as an edit and that it should have been written as a comment or an answer, so here it is (the credit goes mostly to icza):

Minimum Working Code Example (with comments to give some context):

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


来源:https://stackoverflow.com/questions/56394632/remove-from-slice-inplace-in-golang

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!