从 Golang 中的数组中选择元素的最惯用方法?

Posted

技术标签:

【中文标题】从 Golang 中的数组中选择元素的最惯用方法?【英文标题】:Most idiomatic way to select elements from an array in Golang? 【发布时间】:2016-09-30 11:31:00 【问题描述】:

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

我可以遍历每个元素,运行if 语句,并将其添加到沿途的切片中。但我很好奇是否有一种惯用的或更类似于 golang 的方式来实现这一点。

举个例子,在 Ruby 中可以做同样的事情

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

【问题讨论】:

【参考方案1】:

您可以像以前一样使用循环并将其包装到 utils 函数中以供重复使用。

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

如果你想使用 lib,最后一个选项,你可以看看我创建的 https://github.com/ledongthuc/goterators#filter 以重用聚合和转换函数。

它需要 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。 希望对你有用!

【讨论】:

【参考方案2】:

这是一个使用递归来完成过滤的折叠和过滤器的优雅示例。 FoldRight 通常也很有用。它不是堆叠安全的,但可以通过蹦床来实现。一旦 Golang 有了泛型,它就可以完全泛化为任何 2 种类型:

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)

这是一个过滤掉所有长度

    var p = func(s string) bool 
                if len(s) < 8 
                    return true
                 else 
                    return false
                
            

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

【讨论】:

【参考方案3】:

有几种不错的方法可以过滤切片而不需要分配或新的依赖项。发现于the Go wiki on Github:

过滤器(就地)

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)索引a 中索引n 之后的项目,就像在第二种方法中一样。

编辑:第二种方式基本上是 MicahStetson 在his answer 中描述的。在我的代码中,我使用了类似于以下的函数,它在性能和可读性方面可能与它一样好:

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

【讨论】:

【参考方案4】:

我正在开发这个库: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 section of the project。您可以在此link 中尝试。我希望你喜欢它...

问候...

【讨论】:

【参考方案5】:

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

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

【讨论】:

【参考方案6】:

在 Ruby 中没有单行代码,但使用辅助函数可以让它几乎一样短。

这是我们的辅助函数,它在切片上循环,只选择并返回满足函数值捕获的条件的元素:

func filter(ss []string, test func(string) bool) (ret []string) 
    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 上试试):

[asdf nfoo_1]

注意:

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

注意 #2:

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

【讨论】:

【参考方案7】:

今天,我偶然发现了一个令我惊讶的漂亮成语。如果您想在不分配的情况下就地过滤切片,请使用具有相同后备数组的两个切片:

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

【讨论】:

这是一个经典的“技巧”。 s = s[:0] 保留底层数组和切片容量,仅将切片长度归零。【参考方案8】:

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

【讨论】:

【参考方案9】:

看看robpike's filter library。这将允许您这样做:

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 系列语言所采用的方法非常相似,即使不相同。

【讨论】:

我认为 for 循环会更像这样:for _, elt := range a if(!strings.HasPrefix(elt, "foo_") &amp;&amp; len(elt) &lt;= 7) nofoos = append(nofoos, elt) 【参考方案10】:

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

运行示例:Playground

【讨论】:

以上是关于从 Golang 中的数组中选择元素的最惯用方法?的主要内容,如果未能解决你的问题,请参考以下文章

在不转换为数组的情况下迭代 NodeList 并移动其元素的惯用方法是啥?

在golang中处理逻辑错误与编程错误的惯用方法

是否有一种惯用的方式来操作 Ruby 中的 2 个数组?

从数组列表中删除元素的最有效方法? [关闭]

选择排序

在 Go 中访问二维数组中的相邻元素的最有效方法是啥?