在Golang中选择数组元素的最惯用方法是什么?

86

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

我可以循环遍历每个元素,运行 if 语句,并在此过程中将其添加到一个切片中。但我想知道是否有一种成语化或更像 Golang 的方式来实现这一点。

举个例子,在 Ruby 中可以使用以下代码实现同样的功能:

my_array.select! { |val| val !~ /^foo_/ && val.length <= 7 }
10个回答

126

虽然在Ruby中有一行代码的方式,但是在Go语言中没有这样的方式,但是通过使用一个帮助函数,你可以使代码几乎达到同样的简洁。

下面是我们的帮助函数,它循环遍历一个切片,并选择并返回只满足由函数值捕获的条件的元素:

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

从Go 1.18开始,我们可以将其编写为通用代码,以使其适用于所有类型,而不仅仅是string

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

使用这个辅助函数,您可以完成以下任务:
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或通用版本的 Go Playground上尝试):

[asdf nfoo_1]

注意:

如果预计选择的元素很多,事先分配一个“大”ret切片可能会更有利,并使用简单的赋值而不是append()。在返回之前,将ret切片为与所选元素数量相等的长度。

注意 #2:

在我的示例中,我选择了一个test()函数,该函数告诉我们是否要返回一个元素。因此,我不得不反转你的“排除”条件。显然,您也可以编写帮助程序函数来期望测试器函数,该函数告诉我们要排除什么(而不是要包含什么)。


这意味着每种类型都需要不同的过滤函数? - Corey Alix
3
@CoreyAlix 不是的,从Go 1.18开始,你可以使用泛型,因此我们可以编写一个适用于所有类型的单个函数。请参见编辑后的答案。 - icza

25

看看Rob Pike的过滤器库,这将允许您执行:

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中写道:

我想看看用Go实现这种东西有多困难,同时尽可能使用简洁的API。实际上并不难。 几年前我写过它,但从未使用过。相反,我只使用“for”循环。 你也不应该使用它。

因此,根据Rob的说法,最符合惯用语的方式是类似于:

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系语言采取的方法非常相似。


3
我认为for循环应该像这样更符合惯用法:`for _, elt := range a { if(!strings.HasPrefix(elt, "foo_") && len(elt) <= 7) { nofoos = append(nofoos, elt) } }` - bersling

9

有几种不需要分配内存或引入新依赖的方法可以过滤切片。在Github上的Go Wiki中找到:

Filter (in place)

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.

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
}

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

编辑:第二种方法基本上就是MicahStetson在他的答案中描述的方法。在我的代码中,我使用类似于以下函数的函数,这可能是性能和可读性方面最好的选择:

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循环。

8

今天我偶然发现一个很有意思的习语,让我感到惊讶。如果你想在不分配内存的情况下就地过滤一个切片,请使用两个具有相同支撑数组的切片:

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

以下是移除重复字符串的一个具体示例:

这里是移除重复字符串的一个具体示例:

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() 为您分配内存。

1
这是一个经典的“技巧”。s = s [:0] 保留底层数组和切片容量,仅将切片长度清零。 - Theodore Zographos

4

在Go语言中,没有像Ruby一样能够以单行代码实现相同预期结果的惯用方式,但是通过使用一个辅助函数,您可以获得与Ruby一样的表达能力。

您可以调用这个辅助函数:

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

以下是完整的代码:

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

以下是相关的it技术翻译内容,示例程序可参考Playground


2
您可以像之前一样使用循环,并将其包装到一个工具函数中以便重用。
对于多数据类型支持,复制粘贴是一种选择。另一种选择是编写生成工具。
最后一种选择是如果您想要使用库,您可以看一下我创建的https://github.com/ledongthuc/goterators#filter,以重用聚合和转换函数。

enter image description here

需要使用 Go 1.18 版本才能使用支持泛型和动态类型的功能。
filteredItems, err := Filter(list, func(item int) bool {
  return item % 2 == 0
})

filteredItems, err := Filter(list, func(item string) bool {
  return item.Contains("ValidWord")
})

filteredItems, err := Filter(list, func(item MyStruct) bool {
  return item.Valid()
})

它还支持Reduce功能,以便您优化选择方式。希望对您有所帮助!

1

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

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

0

这是一个优雅的例子,同时使用了Fold和Filter,利用递归实现过滤。FoldRight也通常很有用。虽然它不是堆栈安全的,但通过trampolining可以实现堆栈安全。一旦Golang拥有泛型,它就可以完全泛化为任何两种类型。

func FoldRightStrings(as, z []string, f func(string, []string) []string) []string {
    if len(as) > 1 {//Slice has a head and a tail.
        h, t := as[0], as[1:len(as)]
        return f(h, FoldRightStrings(t, z, f))
    } else if len(as) == 1 {//Slice has a head and an empty tail.
        h := as[0]
        return f(h, FoldRightStrings([]string{}, z, f))
    }
    return z
}

func FilterStrings(as []string, p func(string) bool) []string {
    var g = func(h string, accum []string) []string {
        if p(h) {
            return append(accum, h)
        } else {
            return accum
        }
    }
    return FoldRightStrings(as, []string{}, g)
}

这是一个关于编程的例子,用于过滤所有长度小于8的字符串。
    var p = func(s string) bool {
                if len(s) < 8 {
                    return true
                } else {
                    return false
                }
            }

 FilterStrings([]string{"asd","asdfas","asdfasfsa","asdfasdfsadfsadfad"}, p)

0
“从数组中选择元素”通常被称为过滤函数。在Go语言中不存在这样的函数。也没有其他的“集合函数”,如map或reduce。为了得到最符合惯用法的结果,我认为可以参考https://gobyexample.com/collection-functions

[...]在Go中,如果有必要使用特定的程序和数据类型,通常提供集合函数。

他们还提供了一个字符串过滤函数的实现示例:

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
}

然而,他们也说,直接内联函数通常是可以的:

请注意,在某些情况下,直接内联集合操作代码可能更清晰,而不是创建和调用帮助函数。

总的来说,Golang 尝试只引入正交概念,这意味着当您可以用一种方式解决问题时,不应该有太多其他解决方法。通过仅具有少数核心概念,使语言变得简单,以便不是每个开发人员都使用语言的不同子集。


0

我正在开发这个库:https://github.com/jose78/go-collection。请尝试使用以下示例来过滤元素:

package main
    
import (
    "fmt"

    col "github.com/jose78/go-collection/collections"
)

type user struct {
    name string
    age  int
    id   int
}

func main() {
    newMap := generateMapTest()
    if resultMap, err := newMap.FilterAll(filterEmptyName); err != nil {
        fmt.Printf("error")
    } else {
        fmt.Printf("Result: %v\n", resultMap)

        result := resultMap.ListValues()
        fmt.Printf("Result: %v\n", result)
        fmt.Printf("Result: %v\n", result.Reverse())
        fmt.Printf("Result: %v\n", result.JoinAsString(" <---> "))
        fmt.Printf("Result: %v\n", result.Reverse().JoinAsString(" <---> "))

        result.Foreach(simpleLoop)
        err := result.Foreach(simpleLoopWithError)
        if err != nil {
            fmt.Println(err)
        }
    }
}

func filterEmptyName(key interface{}, value interface{}) bool {
    user := value.(user)
    return user.name != "empty"
}

func generateMapTest() (container col.MapType) {
    container = col.MapType{}
    container[1] = user{"Alvaro", 6, 1}
    container[2] = user{"Sofia", 3, 2}
    container[3] = user{"empty", 0, -1}
    return container
}

var simpleLoop col.FnForeachList = func(mapper interface{}, index int) {
    fmt.Printf("%d.- item:%v\n", index, mapper)
}

var simpleLoopWithError col.FnForeachList = func(mapper interface{}, index int) {
    if index > 0 {
        panic(fmt.Sprintf("Error produced with index == %d\n", index))
    }
    fmt.Printf("%d.- item:%v\n", index, mapper)
}

执行结果:

Result: map[1:{Alvaro 6 1} 2:{Sofia 3 2}]
Result: [{Sofia 3 2} {Alvaro 6 1}]
Result: [{Alvaro 6 1} {Sofia 3 2}]
Result: {Sofia 3 2} <---> {Alvaro 6 1}
Result: {Alvaro 6 1} <---> {Sofia 3 2}
0.- item:{Sofia 3 2}
1.- item:{Alvaro 6 1}
0.- item:{Sofia 3 2}
Recovered in f Error produced with index == 1

ERROR: Error produced with index == 1
Error produced with index == 1

目前的文档位于项目的维基部分。您可以在此链接中尝试。希望您喜欢它...

敬礼...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接