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

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

我可以循环遍历每个元素,运行 if语句,并沿途将其添加到一个片中。但是我很好奇,是否有一种习惯用法或者更加类似于 Golang 的方法来达到这个目的。

例如,在 Ruby 中可以像

my_array.select! { |val| val !~ /^foo_/ && val.length <= 7 }
89866 次浏览

Ruby 中没有一行程序,但是使用 helper 函数可以使它几乎同样短。

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

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)

输出(在 去游乐场或通用版本 去游乐场上尝试) :

[asdf nfoo_1]

注:

如果预期将选择许多元素,那么预先分配一个“大”的 ret片,并使用简单的分配代替 append()可能是有利可图的。在返回之前,将 ret切片,使其长度等于所选元素的数目。

注二:

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

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

可以将这个 helper 函数调用为:

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

正在运行的例子是: 游乐场

看一下 罗比派克滤波器库,你可以这样做:

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

有趣的是,罗伯特的 README.md:

我想看看在 Go 中实现这种类型的东西有多难,尽可能使用我能够管理的最好的 API。这并不难。 几年前写的,我一次也没用过。相反,我只使用“ for”循环。 你也不该用它。

所以根据罗伯的说法,最惯用的方法是这样的:

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 家族语言采用的方法非常相似,如果不是完全相同的话。

“从数组中选择元素”通常也称为 过滤器函数。没有这种东西。也没有其他“收集函数”,如 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 试图只引入正交的概念,这意味着当你可以用一种方法来解决问题时,不应该有太多的方法来解决它。这通过只有几个核心概念为语言增加了简单性,因此并不是每个开发人员都使用语言的不同子集。

今天,我偶然发现了一个让我吃惊的成语。如果希望在不进行分配的情况下就地过滤一个切片,请使用具有相同后台数组的两个切片:

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

看看这个图书馆: < a href = “ https://github.com/thoas/go-funk”rel = “ nofollow norefrer”> github.com/thoas/go-funk 它提供了 Go 中许多可以挽救生命的习惯用法的实现(例如,包括对数组中的元素进行过滤)。

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

有几种不需要分配或新依赖项就可以过滤一个片的好方法:

过滤器(就位)

n := 0


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


}
a = a[:n]

还有一种更易读的方法:

未分配的过滤

这个技巧使用了一个切片共享相同的后台数组这一事实 和容量作为原始,因此存储被重用于 当然,原始的内容是经过修改的。

b := a[:0]


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

对于必须进行垃圾回收的元素,下面的代码可以 其后包括:

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

我正在开发这个库: 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

DOC 目前位于 项目的 wiki 部分。你可以在这个 链接试试。我希望你喜欢它..。

问候..。

下面是 Fold 和 Filter 的一个优雅示例,它使用递归来完成过滤。FoldRight 通常也很有用。这不是堆栈安全,但可以这样做与蹦床。一旦 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)

您可以像以前那样使用循环,并将其封装到 utils 函数中以便重用。

对于多数据类型支持,可以选择复制粘贴。另一种选择是编写生成工具。

最后一个选项,如果你想使用 lib,你可以看看我创建的重用聚合和转换函数的 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。 希望对你有用!