Most idiomatic way to select elements from an array in Golang?

Solution 1:

There is no one-liner as you have it in Ruby, but with a helper function you can make it almost as short.

Here's our helper function that loops over a slice, and selects and returns only the elements that meet a criteria captured by a function value:

func filter(ss []string, test func(string) bool) (ret []string) {
    for _, s := range ss {
        if test(s) {
            ret = append(ret, s)
        }
    }
    return
}

Using this helper function your task:

ss := []string{"foo_1", "asdf", "loooooooong", "nfoo_1", "foo_2"}

mytest := func(s string) bool { return !strings.HasPrefix(s, "foo_") && len(s) <= 7 }
s2 := filter(ss, mytest)

fmt.Println(s2)

Output (try it on the Go Playground):

[asdf nfoo_1]

Note:

If it is expected that many elements will be selected, it might be profitable to allocate a "big" ret slice beforehand, and use simple assignment instead of the append(). And before returning, slice the ret to have a length equal to the number of selected elements.

Note #2:

In my example I chose a test() function which tells if an element is to be returned. So I had to invert your "exclusion" condition. Obviously you may write the helper function to expect a tester function which tells what to exclude (and not what to include).

Solution 2:

Have a look at robpike's filter library. This would allow you to do:

package main

import (
    "fmt"
    "strings"
    "filter"
)

func isNoFoo7(a string) bool {
    return ! strings.HasPrefix(a, "foo_") && len(a) <= 7
}

func main() {
    a := []string{"test", "some_other_test", "foo_etc"}
    result := Choose(a, isNoFoo7)
    fmt.Println(result) // [test]
}

Interestingly enough the README.md by Rob:

I wanted to see how hard it was to implement this sort of thing in Go, with as nice an API as I could manage. It wasn't hard. Having written it a couple of years ago, I haven't had occasion to use it once. Instead, I just use "for" loops. You shouldn't use it either.

So the most idiomatic way according to Rob would be something like:

func main() {
    a := []string{"test", "some_other_test", "foo_etc"}
    nofoos := []string{}
    for i := range a {
        if(!strings.HasPrefix(a[i], "foo_") && len(a[i]) <= 7) {
            nofoos = append(nofoos, a[i])
        }
    }
    fmt.Println(nofoos) // [test]
}

This style is very similar, if not identical, to the approach any C-family language takes.

Solution 3:

Today, I stumbled on a pretty idiom that surprised me. If you want to filter a slice in place without allocating, use two slices with the same backing array:

s := []T{
    // the input
} 
s2 := s
s = s[:0]
for _, v := range s2 {
    if shouldKeep(v) {
        s = append(s, v)
    }
}

Here's a specific example of removing duplicate strings:

s := []string{"a", "a", "b", "c", "c"}
s2 := s
s = s[:0]
var last string
for _, v := range s2 {
    if len(s) == 0 || v != last {
        last = v
        s = append(s, v)
    }
}

If you need to keep both slices, simply replace s = s[:0] with s = nil or s = make([]T, 0, len(s)), depending on whether you want append() to allocate for you.

Solution 4:

There are a couple of nice ways to filter a slice without allocations or new dependencies. Found in the Go wiki on Github:

Filter (in place)

n := 0

for _, x := range a {
  if keep(x) {
      a[n] = x
      n++
  }

}
a = a[:n]

And another, more readable, way:

Filtering without allocating

This trick uses the fact that a slice shares the same backing array and capacity as the original, so the storage is reused for the filtered slice. Of course, the original contents are modified.

b := a[:0]

for _, x := range a {
  if f(x) {
      b = append(b, x)
  }
}

For elements which must be garbage collected, the following code can be included afterwards:

for i := len(b); i < len(a); i++ {
  a[i] = nil // or the zero value of T
}

One thing I'm not sure about is whether the first method needs clearing (setting to nil) the items in slice a after index n, like they do in the second method.

EDIT: the second way is basically what MicahStetson described in his answer. In my code I use a function similar to the following, which is probably as good as it gets in terms on performance and readability:

func filterSlice(slice []*T, keep func(*T) bool) []*T {
    newSlice := slice[:0]

    for _, item := range slice {
        if keep(item) {
            newSlice = append(newSlice, item)
        }
    }
    // make sure discarded items can be garbage collected
    for i := len(newSlice); i < len(slice); i++ {
        slice[i] = nil
    }
    return newSlice
}

Note that if items in your slice are not pointers and don't contain pointers you can skip the second for loop.

Solution 5:

There isn't an idiomatic way you can achieve the same expected result in Go in one single line as in Ruby, but with a helper function you can obtain the same expressiveness as in Ruby.

You can call this helper function as:

Filter(strs, func(v string) bool {
    return strings.HasPrefix(v, "foo_") // return foo_testfor
}))

Here is the whole code:

package main

import "strings"
import "fmt"

// Returns a new slice containing all strings in the
// slice that satisfy the predicate `f`.
func Filter(vs []string, f func(string) bool) []string {
    vsf := make([]string, 0)
    for _, v := range vs {
        if f(v) && len(v) > 7 {
            vsf = append(vsf, v)
        }
    }
    return vsf
}

func main() {

    var strs = []string{"foo1", "foo2", "foo3", "foo3", "foo_testfor", "_foo"}

    fmt.Println(Filter(strs, func(v string) bool {
        return strings.HasPrefix(v, "foo_") // return foo_testfor
    }))
}

And the running example: Playground