golang에는 없습니다. Golang은 유사한 Python 연산자를 제공하지 않으며 PHP의 in_array와 같은 다른 언어와 같은 표준 라이브러리 함수도 제공하지 않습니다. 이유: 1. in 함수의 구현은 매우 간단하고 불필요합니다. 2. 다양한 시나리오에서는 고정된 방법이 아닌 실제 상황에 따라 어떤 방법을 구현할지 분석해야 합니다.
이 튜토리얼의 운영 환경: Windows 7 시스템, GO 버전 1.18, Dell G3 컴퓨터.
전에 Zhihu에서 질문을 봤습니다. 왜 Golang에는 Python과 동일한 기능이 없나요? 그래서 이 질문을 검색해 보니 아직도 이런 질문을 갖고 계신 분들이 많더군요.
오늘은 이 주제에 대해 이야기해 보겠습니다.
in은 매우 일반적으로 사용되는 함수입니다. 일부 언어에서는 포함이라고도 합니다. 언어마다 표현이 다르지만 기본적으로는 있습니다. 하지만 불행하게도 Go는 유사한 Python 연산자를 제공하지 않으며 PHP의 in_array와 같은 다른 언어와 같은 표준 라이브러리 함수도 제공하지 않습니다.
Go의 철학은 Less is more를 추구하는 것입니다. 아마도 Go 팀에서는 이것이 구현하기에 실용적이지 않은 기능이라고 생각할 것 같습니다.
왜 하찮다고 하시나요? 직접 구현하고 싶다면 어떻게 해야 할까요?
제가 생각할 수 있는 구현 방법은 세 가지가 있습니다. 하나는 순회, 다른 하나는 정렬의 이진 검색, 세 번째는 맵의 키 인덱스입니다.
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과 needle은 모두 인터페이스{} 유형입니다.
인터페이스{}인 모든 입력 매개변수의 이점에 대해 간략하게 이야기해 보겠습니다. 다음과 같은 두 가지 주요 사항이 있습니다.
첫째, haystack은 인터페이스{} 유형이므로 in에서 지원하는 유형은 단순히 슬라이스뿐만 아니라 배열도 가능합니다. 함수가 내부적으로 리플렉션을 통해 haystack에 대한 유형 검사를 수행하고 슬라이스와 배열을 지원한다는 것을 알 수 있습니다. 다른 유형인 경우 맵과 같은 새로운 유형 지원을 추가하는 것은 실제로 매우 간단합니다. 하지만 이 방법은 권장되지 않습니다. 왜냐하면 in의 효과는 _, ok := m[k] 구문을 통해 얻을 수 있기 때문입니다.
둘째, haystack은 인터페이스{}이고, []인터페이스{}도 요구 사항을 충족하며 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의 표준 라이브러리는 이미 정렬 패키지에 이 기능을 제공합니다.
샘플 코드는 다음과 같습니다.
fmt.Println(sort.SortInts([]int{4, 2, 5, 1, 6}))
[]int의 경우 사용하는 함수는 SortInts입니다. 다른 유형의 슬라이스인 경우 sort는 관련 함수도 제공합니다. 예를 들어 []string은 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!