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

use*_*003 31 arrays go slice

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

我可以循环遍历每个元素,运行if语句,并将其添加到路径中的切片.但我很好奇是否有一种惯用的或更像golang的方式来完成它.

例如,同样的事情可能在Ruby中完成

my_array.select! { |val| val !~ /^foo_/ && val.length <= 7 }
Run Code Online (Sandbox Code Playgroud)

icz*_*cza 43

你没有在Ruby中使用过单行程序,但是使用帮助程序功能可以使它几乎一样短.

这是我们的辅助函数循环切片,并选择并仅返回符合函数值捕获的条件的元素:

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

使用这个辅助函数你的任务:

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)
Run Code Online (Sandbox Code Playgroud)

输出(在Go Playground上试试):

[asdf nfoo_1]
Run Code Online (Sandbox Code Playgroud)

注意:

如果预计会选择许多元素,那么ret预先分配"大" 切片并使用简单赋值而不是使用简单赋值可能是有利可图的append().在返回之前,将切片的ret长度等于所选元素的数量.

笔记2:

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

  • @CoreyAlix 不,从 Go 1.18 开始,您可以使用泛型,因此我们可以编写一个适用于所有类型的函数。查看编辑后的答案。 (3认同)

Elw*_*ens 13

看看robpike的过滤器库.这将允许您:

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]
}
Run Code Online (Sandbox Code Playgroud)

有趣的是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]
}
Run Code Online (Sandbox Code Playgroud)

这种风格与任何C系列语言采用的方法非常相似(如果不相同).

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

Mic*_*son 7

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

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

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

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 这是一个经典的“伎俩”。s = s[:0] 保留底层数组和切片容量,仅将切片长度归零。 (2认同)

Ant*_*vos 5

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

过滤器(就地)

n := 0

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

}
a = a[:n]
Run Code Online (Sandbox Code Playgroud)

另一种更具可读性的方式:

过滤而不分配

这个技巧利用了一个事实,即切片与原始切片共享相同的后备数组和容量,因此存储被重新用于过滤切片。当然,对原始内容进行了修改。

b := a[:0]

for _, x := range a {
  if f(x) {
      b = append(b, x)
  }
}
Run Code Online (Sandbox Code Playgroud)

对于必须被垃圾收集的元素,可以在之后包含以下代码:

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

我不确定的一件事是,第一种方法是否需要清除(设置为nilaindex 之后slice 中的项目n,就像它们在第二种方法中所做的那样。

编辑:第二种方式基本上是 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
}
Run Code Online (Sandbox Code Playgroud)

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