golang には in がありません。 Golang は同様の Python 演算子を提供していません。また、PHP の in_array など、他の言語のような標準ライブラリ関数も提供しません。理由: 1. in 関数の実装は非常にシンプルで不要です; 2. さまざまなシナリオで、固定されたメソッドではなく、実際の状況に基づいてどのメソッドを実装するかを分析する必要もあります。
このチュートリアルの動作環境: Windows 7 システム、GO バージョン 1.18、Dell G3 コンピューター。
以前に Zhihu で質問を見ました: なぜ Golang には Python と同じ機能がないのですか?そこで、この質問を検索してみたところ、依然として多くの人がそのような質問を抱えていることがわかりました。
今日はこのトピックについて話しましょう。
in は非常によく使われる関数です。言語によっては contains と呼ばれることもあります。言語によって表現は異なりますが、基本的には同じです。しかし、残念ながら、Go には同様の Python 演算子が用意されておらず、他の言語のような標準ライブラリ関数 (PHP の in_array など) も提供されていません。
Go の哲学は、少ないほど豊かであるというものです。おそらく Go チームは、これは実装が現実的ではない機能だと感じていると思います。
なぜ重要ではないと言えるのですか?自分で実装したい場合はどうすればよいでしょうか?
実装方法は3つ考えられます。1つはトラバーサル、もう1つはsortの二分探索、3つ目はmapのキーインデックスです。
Traversal
Traversal は、簡単に思いつく最も単純な実装方法です。
例は次のとおりです:
func InIntSlice(haystack []int, needle int) bool { for _, e := range haystack { if e == needle { return true } } return false }
上記は、[]int 型変数に指定された int が存在するかどうかを調べる例を示しています。 、実装するのは簡単だと私が言う理由もわかります。
この例には欠陥があり、単一のタイプしかサポートしていません。インタープリタ型言語などの一般的な機能をサポートしたい場合は、リフレクションを使用する必要があります。
コードは次のとおりです。
func In(haystack interface{}, needle interface{}) (bool, error) { sVal := reflect.ValueOf(haystack) kind := sVal.Kind() if kind == reflect.Slice || kind == reflect.Array { for i := 0; i < sVal.Len(); i++ { if sVal.Index(i).Interface() == needle { return true, nil } } return false, nil } return false, ErrUnSupportHaystack }
より汎用性を高めるために、In 関数の入力パラメータ haystack と neede は両方ともインターフェース タイプです。{}
すべての入力パラメータがインターフェースであることの利点について簡単に説明します。{}。主なポイントは次の 2 つです:
まず、haystack はインターフェースです。{} in でサポートされる型はスライスに限らず、配列も含まれます。この関数は内部でリフレクションを通じて干し草の山の型チェックを実行し、スライスと配列をサポートしていることがわかります。他のタイプの場合は、エラーが表示されますが、マップなどの新しいタイプのサポートを追加するのは、実際には非常に簡単です。ただし、この方法は推奨されません。in の効果は _, ok := m[k] の構文を通じて実現できるからです。
2 番目に、haystack はインターフェース{}であり、[]interface{} も要件を満たしており、needle はインターフェース{}です。このようにして、インタープリター言語と同じ効果を達成できます。
どうやって理解すればいいでしょうか?直接的な例は次のとおりです。
gotin.In([]interface{}{1, "two", 3}, "two")
haystack は []interface{}{1, "two", 3}、needle はインターフェイス{}で、このときの値は「two」です。インタプリタ型言語では、要素はどのような型であってもよく、同じ効果を持つ必要はないようです。このように、私たちはそれを好きなように使うことができます。
ただし、In 関数の実装に次のようなコードがあることに注意してください:
if sVal.Index(i).Interface() == needle { ... }
Go のすべての型を == を使用して比較できるわけではありません。スライスまたはマップが含まれています。エラーが報告される場合があります。
二分探索
要素が存在するかどうか、つまり配列またはスライスに大きな要素が含まれているかどうかを確認するためのトラバースには欠点があります。データ量 (例: 1,000,000 データ、つまり 100 万) 最悪のシナリオは、確認するために 1,000,000 回走査する必要があり、時間計算量が On であることです。
走査回数を減らす方法はありますか?
自然に思い浮かぶ方法は二分探索であり、その時間計算量は log2(n) です。ただし、このアルゴリズムには前提条件があり、順序付けられたシーケンスに依存しています。
したがって、最初に解決する必要がある問題は、シーケンスを順序付けすることです。Go の標準ライブラリでは、sort パッケージでこの関数がすでに提供されています。
サンプル コードは次のとおりです:
fmt.Println(sort.SortInts([]int{4, 2, 5, 1, 6}))
[]int の場合、使用する関数は SortInts です。他のタイプのスライスの場合、sort は [] などの関連関数も提供します。文字列から SortStrings までの並べ替え。
ソート後、バイナリ検索を実行できます。幸いなことに、Go にもこの機能が用意されています。[]int 型に対応する関数は SearchInts です。
この関数を簡単に紹介します。まず定義を見てみましょう:
func SearchInts(a []int, x int) int
入力パラメータは理解しやすく、スライス a から x を検索します。重要な点は戻り値について説明することです。これは、後で要素が存在するかどうかを確認するために重要です。戻り値の意味はスライス内の要素の位置を返すことであり、要素が存在しない場合にはスライスの順序を保ったまま要素を挿入すべき位置を返す。
たとえば、シーケンスは次のとおりです:
1 2 6 8 9 11
x が 6 であると仮定し、検索後、その位置がインデックス 2 にあることがわかります。x が 7 の場合は、要素が存在しないことがわかり、シーケンスに挿入すると、 は 6 と 8 の間に配置され、インデックス位置は 3 であるため、戻り値は 3 になります。
コードテスト:
fmt.Println(sort.SearchInts([]int{1, 2, 6, 8, 9, 11}, 6)) // 2 fmt.Println(sort.SearchInts([]int{1, 2, 6, 8, 9, 11}, 7)) // 3
如果判断元素是否在序列中,只要判断返回位置上的值是否和查找的值相同即可。
但还有另外一种情况,如果插入元素位于序列最后,例如元素值为 12,插入位置即为序列的长度 6。如果直接查找 6 位置上的元素就可能发生越界的情况。那怎么办呢?其实判断返回是否小于切片长度即可,小于则说明元素不在切片序列中。
完整的实现代码如下:
func SortInIntSlice(haystack []int, needle int) bool { sort.Ints(haystack) index := sort.SearchInts(haystack, needle) return index < len(haystack) && haystack[index] == needle }
但这还有个问题,对于无序的场景,如果每次查询都要经过一次排序并不划算。最好能实现一次排序,稍微修改下代码。
func InIntSliceSortedFunc(haystack []int) func(int) bool { sort.Ints(haystack) return func(needle int) bool { index := sort.SearchInts(haystack, needle) return index < len(haystack) && haystack[index] == needle } }
上面的实现,我们通过调用 InIntSliceSortedFunc 对 haystack 切片排序,并返回一个可多次使用的函数。
使用案例如下:
in := gotin.InIntSliceSortedFunc(haystack) for i := 0; i<maxNeedle; i++ { if in(i) { fmt.Printf("%d is in %v", i, haystack) } }
二分查找的方式有什么不足呢?
我想到的重要一点,要实现二分查找,元素必须是可排序的,如 int,string,float 类型。而对于结构体、切片、数组、映射等类型,使用起来就不是那么方便,当然,如果要用,也是可以的,不过需要我们进行一些适当扩展,按指定标准排序,比如结构的某个成员。
到此,二分查找的 in 实现就介绍完毕了。
map key
本节介绍 map key 方式。它的算法复杂度是 O1,无论数据量多大,查询性能始终不变。它主要依赖的是 Go 中的 map 数据类型,通过 hash map 直接检查 key 是否存在,算法大家应该都比较熟悉,通过 key 可直接映射到索引位置。
我们常会用到这个方法。
_, ok := m[k] if ok { fmt.Println("Found") }
那么它和 in 如何结合呢?一个案例就说明白了这个问题。
假设,我们有一个 []int 类型变量,如下:
s := []int{1, 2, 3}
为了使用 map 的能力检查某个元素是否存在,可以将 s 转化 map[int]struct{}。
m := map[interface{}]struct{}{ 1: struct{}{}, 2: struct{}{}, 3: struct{}{}, 4: struct{}{}, }
如果检查某个元素是否存在,只需要通过如下写法即可确定:
k := 4 if _, ok := m[k]; ok { fmt.Printf("%d is found\n", k) }
是不是非常简单?
补充一点,关于这里为什么使用 struct{},可以阅读我之前写的一篇关于 Go 中如何使用 set 的文章。
按照这个思路,实现函数如下:
func MapKeyInIntSlice(haystack []int, needle int) bool { set := make(map[int]struct{}) for _ , e := range haystack { set[e] = struct{}{} } _, ok := set[needle] return ok }
实现起来不难,但和二分查找有着同样的问题,开始要做数据处理,将 slice 转化为 map。如果是每次数据相同,稍微修改下它的实现。
func InIntSliceMapKeyFunc(haystack []int) func(int) bool { set := make(map[int]struct{}) for _ , e := range haystack { set[e] = struct{}{} } return func(needle int) bool { _, ok := set[needle] return ok } }
对于相同的数据,它会返回一个可多次使用的 in 函数,一个使用案例如下:
in := gotin.InIntSliceMapKeyFunc(haystack) for i := 0; i<maxNeedle; i++ { if in(i) { fmt.Printf("%d is in %v", i, haystack) } }
对比前两种算法,这种方式的处理效率最高,非常适合于大数据的处理。接下来的性能测试,我们将会看到效果。
性能
介绍完所有方式,我们来实际对比下每种算法的性能。测试源码位于 gotin_test.go 文件中。
基准测试主要是从数据量大小考察不同算法的性能,本文中选择了三个量级的测试样本数据,分别是 10、1000、1000000。
为便于测试,首先定义了一个用于生成 haystack 和 needle 样本数据的函数。
代码如下:
func randomHaystackAndNeedle(size int) ([]int, int){ haystack := make([]int, size) for i := 0; i<size ; i++{ haystack[i] = rand.Int() } return haystack, rand.Int() }
输入参数是 size,通过 http://rand.Int() 随机生成切片大小为 size 的 haystack 和 1 个 needle。在基准测试用例中,引入这个随机函数生成数据即可。
举个例子,如下:
func BenchmarkIn_10(b *testing.B) { haystack, needle := randomHaystackAndNeedle(10) b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = gotin.In(haystack, needle) } }
首先,通过 randomHaystackAndNeedle 随机生成了一个含有 10 个元素的切片。因为生成样本数据的时间不应该计入到基准测试中,我们使用 b.ResetTimer() 重置了时间。
其次,压测函数是按照 Test+函数名+样本数据量 规则编写,如案例中 BenchmarkIn_10,表示测试 In 函数,样本数据量为 10。如果我们要用 1000 数据量测试 InIntSlice,压测函数名为 BenchmarkInIntSlice_1000。
测试开始吧!简单说下我的笔记本配置,Mac Pro 15 版,16G 内存,512 SSD,4 核 8 线程的 CPU。
测试所有函数在数据量在 10 的情况下的表现。
$ go test -run=none -bench=10$ -benchmem
匹配所有以 10 结尾的压测函数。
测试结果:
goos: darwin goarch: amd64 pkg: github.com/poloxue/gotin BenchmarkIn_10-8 3000000 501 ns/op 112 B/op 11 allocs/op BenchmarkInIntSlice_10-8 200000000 7.47 ns/op 0 B/op 0 allocs/op BenchmarkInIntSliceSortedFunc_10-8 100000000 22.3 ns/op 0 B/op 0 allocs/op BenchmarkSortInIntSlice_10-8 10000000 162 ns/op 32 B/op 1 allocs/op BenchmarkInIntSliceMapKeyFunc_10-8 100000000 17.7 ns/op 0 B/op 0 allocs/op BenchmarkMapKeyInIntSlice_10-8 3000000 513 ns/op 163 B/op 1 allocs/op PASS ok github.com/poloxue/gotin 13.162s
表现最好的并非 SortedFunc 和 MapKeyFunc,而是最简单的针对单类型的遍历查询,平均耗时 7.47ns/op,当然,另外两种方式表现也不错,分别是 22.3ns/op 和 17.7ns/op。
表现最差的是 In、SortIn(每次重复排序) 和 MapKeyIn(每次重复创建 map)两种方式,平均耗时分别为 501ns/op 和 513ns/op。
测试所有函数在数据量在 1000 的情况下的表现。
$ go test -run=none -bench=1000$ -benchmem
测试结果:
goos: darwin goarch: amd64 pkg: github.com/poloxue/gotin BenchmarkIn_1000-8 30000 45074 ns/op 8032 B/op 1001 allocs/op BenchmarkInIntSlice_1000-8 5000000 313 ns/op 0 B/op 0 allocs/op BenchmarkInIntSliceSortedFunc_1000-8 30000000 44.0 ns/op 0 B/op 0 allocs/op BenchmarkSortInIntSlice_1000-8 20000 65401 ns/op 32 B/op 1 allocs/op BenchmarkInIntSliceMapKeyFunc_1000-8 100000000 17.6 ns/op 0 B/op 0 allocs/op BenchmarkMapKeyInIntSlice_1000-8 20000 82761 ns/op 47798 B/op 65 allocs/op PASS ok github.com/poloxue/gotin 11.312s
表现前三依然是 InIntSlice、InIntSliceSortedFunc 和 InIntSliceMapKeyFunc,但这次顺序发生了变化,MapKeyFunc 表现最好,17.6 ns/op,与数据量 10 的时候相比基本无变化。再次验证了前文的说法。
同样的,数据量 1000000 的时候。
$ go test -run=none -bench=1000000$ -benchmem
测试结果如下:
goos: darwin goarch: amd64 pkg: github.com/poloxue/gotin BenchmarkIn_1000000-8 30 46099678 ns/op 8000098 B/op 1000001 allocs/op BenchmarkInIntSlice_1000000-8 3000 424623 ns/op 0 B/op 0 allocs/op BenchmarkInIntSliceSortedFunc_1000000-8 20000000 72.8 ns/op 0 B/op 0 allocs/op BenchmarkSortInIntSlice_1000000-8 10 138873420 ns/op 32 B/op 1 allocs/op BenchmarkInIntSliceMapKeyFunc_1000000-8 100000000 16.5 ns/op 0 B/op 0 allocs/op BenchmarkMapKeyInIntSlice_1000000-8 10 156215889 ns/op 49824225 B/op 38313 allocs/op PASS ok github.com/poloxue/gotin 15.178s
MapKeyFunc 依然表现最好,每次操作用时 17.2 ns,Sort 次之,而 InIntSlice 呈现线性增加的趋势。一般情况下,如果不是对性能要特殊要求,数据量特别大的场景,针对单类型的遍历已经有非常好的性能了。
从测试结果可以看出,反射实现的通用 In 函数每次执行需要进行大量的内存分配,方便的同时,也是以牺牲性能为代价的。
总结
本文通过一个问题引出主题,为什么 Go 中没有类似 Python 的 In 方法。我认为,一方面是实现非常简单,没有必要。除此以外,另一方面,在不同场景下,我们还需要根据实际情况分析用哪种方式实现,而不是一种固定的方式。
接着,我们介绍了 In 实现的三种方式,并分析了各自的优劣。通过性能分析测试,我们能得出大致的结论,什么方式适合什么场景,但总体还是不能说足够细致,有兴趣的朋友可以继续研究下。
以上がgolangにはありますかの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。