从 Golang 中的数组 select 元素的最惯用方式?
Most idiomatic way to select elements from an array in Golang?
我有一个字符串数组,我想排除以 foo_
开头或超过 7 个字符的值。
我可以遍历每个元素,运行 if
语句,并沿途将其添加到切片中。但我很好奇是否有一种惯用的或更像 golang 的方式来实现它。
例如,在 Ruby 中可能会像
那样做同样的事情
my_array.select! { |val| val !~ /^foo_/ && val.length <= 7 }
没有像 Ruby 中那样的单行代码,但是使用辅助函数可以使它几乎一样短。
这是我们的辅助函数,它遍历一个切片,选择并 returns 仅满足函数值捕获的条件的元素:
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]
注:
如果预期会选择很多元素,预先分配一个 "big" ret
切片并使用简单赋值而不是 append()
可能会有利可图。在返回之前,将 ret
切片,使其长度等于所选元素的数量。
注#2:
在我的示例中,我选择了一个 test()
函数来判断是否要返回一个元素。所以我不得不反转你的 "exclusion" 条件。显然,您可以编写辅助函数来期望一个测试函数告诉排除什么(而不是包括什么)。
在 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
看看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]
}
有趣的是 README.md 作者 Rob:
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 的说法,最惯用的方式是这样的:
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 系列语言采用的方法即使不完全相同也非常相似。
"Select Elements from Array" 通常也称为 过滤函数 。里面没有这样的东西。也没有其他 "Collection Functions",例如 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.
他们提供了字符串过滤函数的实现示例:
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 试图只引入正交概念,这意味着当你可以用一种方式解决问题时,不应该有太多的方法来解决它。这通过仅具有几个核心概念来增加语言的简单性,这样并不是每个开发人员都使用该语言的不同子集。
今天偶然发现一个漂亮的成语,让我很吃惊。如果您想在不分配的情况下就地过滤一个切片,请使用两个具有相同后备数组的切片:
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 = nil
或 s = make([]T, 0, len(s))
,具体取决于您是否希望 append()
为您分配。
看看这个图书馆:github.com/thoas/go-funk
它提供了 Go 中许多救生习语的实现(例如,包括过滤数组中的元素)。
r := funk.Filter([]int{1, 2, 3, 4}, func(x int) bool {
return x%2 == 0
}
有几种很好的方法可以在没有分配或新依赖项的情况下过滤切片。在 the Go wiki on Github 中找到:
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 循环。
我正在开发这个库: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. You can try it in this link。希望大家喜欢...
问候...
这里有一个优雅的Fold和Filter的例子,使用递归来完成过滤。 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)
}
下面是它过滤掉所有长度<8的字符串的用法示例
var p = func(s string) bool {
if len(s) < 8 {
return true
} else {
return false
}
}
FilterStrings([]string{"asd","asdfas","asdfasfsa","asdfasdfsadfsadfad"}, p)
您可以像以前一样使用循环并将其包装到实用程序函数中以供重用。
对于多数据类型支持,复制粘贴将是一种选择。另一种选择是编写一个生成工具。
最后一个选项,如果你想使用 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 select。
希望对你有用!
我有一个字符串数组,我想排除以 foo_
开头或超过 7 个字符的值。
我可以遍历每个元素,运行 if
语句,并沿途将其添加到切片中。但我很好奇是否有一种惯用的或更像 golang 的方式来实现它。
例如,在 Ruby 中可能会像
那样做同样的事情my_array.select! { |val| val !~ /^foo_/ && val.length <= 7 }
没有像 Ruby 中那样的单行代码,但是使用辅助函数可以使它几乎一样短。
这是我们的辅助函数,它遍历一个切片,选择并 returns 仅满足函数值捕获的条件的元素:
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]
注:
如果预期会选择很多元素,预先分配一个 "big" ret
切片并使用简单赋值而不是 append()
可能会有利可图。在返回之前,将 ret
切片,使其长度等于所选元素的数量。
注#2:
在我的示例中,我选择了一个 test()
函数来判断是否要返回一个元素。所以我不得不反转你的 "exclusion" 条件。显然,您可以编写辅助函数来期望一个测试函数告诉排除什么(而不是包括什么)。
在 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
看看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]
}
有趣的是 README.md 作者 Rob:
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 的说法,最惯用的方式是这样的:
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 系列语言采用的方法即使不完全相同也非常相似。
"Select Elements from Array" 通常也称为 过滤函数 。里面没有这样的东西。也没有其他 "Collection Functions",例如 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.
他们提供了字符串过滤函数的实现示例:
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 试图只引入正交概念,这意味着当你可以用一种方式解决问题时,不应该有太多的方法来解决它。这通过仅具有几个核心概念来增加语言的简单性,这样并不是每个开发人员都使用该语言的不同子集。
今天偶然发现一个漂亮的成语,让我很吃惊。如果您想在不分配的情况下就地过滤一个切片,请使用两个具有相同后备数组的切片:
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 = nil
或 s = make([]T, 0, len(s))
,具体取决于您是否希望 append()
为您分配。
看看这个图书馆:github.com/thoas/go-funk 它提供了 Go 中许多救生习语的实现(例如,包括过滤数组中的元素)。
r := funk.Filter([]int{1, 2, 3, 4}, func(x int) bool {
return x%2 == 0
}
有几种很好的方法可以在没有分配或新依赖项的情况下过滤切片。在 the Go wiki on Github 中找到:
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 循环。
我正在开发这个库: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. You can try it in this link。希望大家喜欢...
问候...
这里有一个优雅的Fold和Filter的例子,使用递归来完成过滤。 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)
}
下面是它过滤掉所有长度<8的字符串的用法示例
var p = func(s string) bool {
if len(s) < 8 {
return true
} else {
return false
}
}
FilterStrings([]string{"asd","asdfas","asdfasfsa","asdfasdfsadfsadfad"}, p)
您可以像以前一样使用循环并将其包装到实用程序函数中以供重用。
对于多数据类型支持,复制粘贴将是一种选择。另一种选择是编写一个生成工具。
最后一个选项,如果你想使用 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 select。 希望对你有用!