我有一个字符串数组,我想排除以foo_开头或超过7个字符的值。

我可以遍历每个元素,运行if语句,然后将其添加到切片中。 但是我很好奇是否有一种惯用的或更像golang的方式来实现这一目标。

例如,在Ruby中可能会完成与

1
my_array.select! { |val| val !~ /^foo_/ && val.length <= 7 }

没有像Ruby中那样的单行代码,但是借助辅助函数,您可以使其变得几乎一样短。

这是我们的辅助函数,该函数遍历一个切片,并仅选择并返回满足由函数值捕获的条件的元素:

1
2
3
4
5
6
7
8
func filter(ss []string, test func(string) bool) (ret []string) {
    for _, s := range ss {
        if test(s) {
            ret = append(ret, s)
        }
    }
    return
}

使用此帮助器功能,您的任务:

1
2
3
4
5
6
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)

输出(在Go Playground上尝试):

1
[asdf nfoo_1]

注意:

如果预期将选择许多元素,则预先分配一个"大" ret切片,并使用简单分配而不是append()可能是有利的。在返回之前,对ret进行切片,使其长度等于所选元素的数量。

笔记2:

在我的示例中,我选择了test()函数,该函数告知是否要返回元素。因此,我必须反转您的"排除"条件。显然,您可能编写了helper函数来期望一个tester函数,该函数会告诉您要排除的内容(而不是要包括的内容)。


看看robpike的过滤器库。这将允许您执行以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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]
}

有趣的是Rob的README.md:

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.

因此,Rob所说的最惯用的方式是:

1
2
3
4
5
6
7
8
9
10
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]
}

这种风格与任何C族语言所采用的方法都非常相似(甚至不同)。

  • 我认为for循环更像这样:for _, elt := range a { if(!strings.HasPrefix(elt,"foo_") && len(elt) <= 7) { nofoos = append(nofoos, elt) } }

今天,我偶然发现了一个使我惊讶的漂亮成语。如果要在不分配的情况下就位过滤片,请使用具有相同支持数组的两个片:

1
2
3
4
5
6
7
8
9
10
s := []T{
    // the input
}
s2 := s
s = s[:0]
for _, v := range s2 {
    if shouldKeep(v) {
        s = append(s, v)
    }
}

这是删除重复字符串的特定示例:

1
2
3
4
5
6
7
8
9
10
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)
    }
}

如果需要保留两个切片,只需将s = s[:0]替换为s = nils = make([]T, 0, len(s)),这取决于您是否要为append()进行分配。

  • 这是一个经典的把戏。 s = s [:0]保留基础的数组和切片容量,仅将切片长度清零。

在Go中,没有一种惯用的方法可以在一行中获得与Ruby中相同的预期结果,但是通过一个辅助函数,您可以获得与Ruby中相同的表现力。

您可以将该助手功能称为:

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

这是完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
    }))
}

运行示例:操场


有两种不错的方法可以在没有分配或没有新依赖项的情况下过滤片。在Github的Go Wiki中找到:

Filter (in place)

1
2
3
4
5
6
7
8
9
10
n := 0

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

}
a = a[:n]

另一种更易读的方式:

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.

1
2
3
4
5
6
7
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:

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

我不确定的一件事是,第一种方法是否需要清除索引n之后的切片a中的项目(设置为nil),就像第二种方法一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
}

请注意,如果切片中的项目不是指针并且不包含指针,则可以跳过第二个for循环。


看看这个库:github.com/thoas/go-funk
它在Go中提供了许多救生习惯的实现(例如,包括过滤数组中的元素)。

1
2
3
r := funk.Filter([]int{1, 2, 3, 4}, func(x int) bool {
    return x%2 == 0
}

"从数组中选择元素"通常也称为过滤器功能。没有这样的事情了。也没有其他"集合函数",例如map或reduce。对于获得期望结果的最惯用方式,我发现https://gobyexample.com/collection-functions是一个很好的参考:

[...] in Go it’s common to provide collection functions if and when they are specifically needed for your program and data types.

它们提供了字符串过滤器功能的实现示例:

1
2
3
4
5
6
7
8
9
func Filter(vs []string, f func(string) bool) []string {
    vsf := make([]string, 0)
    for _, v := range vs {
        if f(v) {
            vsf = append(vsf, v)
        }
    }
    return vsf
}

但是,他们还说,仅内联函数通常是可以的:

Note that in some cases it may be clearest to just inline the
collection-manipulating code directly, instead of creating and calling
a helper function.

通常,golang尝试仅引入正交概念,这意味着当您可以以一种方式解决问题时,应该没有太多其他方式可以解决问题。通过仅具有一些核心概念,这增加了语言的简化性,因此并非每个开发人员都使用该语言的不同子集。