>백엔드 개발 >Golang >golang이 있습니까?

golang이 있습니까?

青灯夜游
青灯夜游원래의
2023-01-18 15:01:161603검색

golang에는 없습니다. Golang은 유사한 Python 연산자를 제공하지 않으며 PHP의 in_array와 같은 다른 언어와 같은 표준 라이브러리 함수도 제공하지 않습니다. 이유: 1. in 함수의 구현은 매우 간단하고 불필요합니다. 2. 다양한 시나리오에서는 고정된 방법이 아닌 실제 상황에 따라 어떤 방법을 구현할지 분석해야 합니다.

golang이 있습니까?

이 튜토리얼의 운영 환경: 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 实现的三种方式,并分析了各自的优劣。通过性能分析测试,我们能得出大致的结论,什么方式适合什么场景,但总体还是不能说足够细致,有兴趣的朋友可以继续研究下。

【相关推荐:Go视频教程编程教学

위 내용은 golang이 있습니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.