检查两个切片是否相等

444

如果==!=不可用,我该如何检查两个切片是否相等?

package main

import "fmt"

func main() {
    s1 := []int{1, 2}
    s2 := []int{1, 2}
    fmt.Println(s1 == s2)
}

这无法编译,显示错误信息:

无效操作:s1 == s2(切片只能与nil进行比较)


https://pkg.go.dev/golang.org/x/exp/slices#Equal - guettli
13个回答

340

你应该使用reflect.DeepEqual()

DeepEqual是Go语言==运算符的递归放松版本。

DeepEqual报告x和y是否“深度相等”,定义如下。 如果以下情况之一适用,则具有相同类型的两个值深度相等。 不同类型的值永远不会深度相等。

数组值在其对应元素深度相等时深度相等。

结构体值如果其对应的字段(包括公开和未公开的字段)深度相等,则深度相等。

函数值如果都为nil则它们深度相等;否则它们不深度相等。

接口值如果它们持有深度相等的具体值,则它们深度相等。

Map值如果它们是相同的映射对象或者如果它们具有相同的长度并且其相应键(使用Go相等性匹配)映射到深度相等的值,则它们深度相等。

指针值如果使用Go的==运算符相等或者它们指向深度相等的值,则它们深度相等。

切片值在以下所有条件均为真时深度相等:它们都是nil或都是非nil,它们具有相同的长度,并且它们指向相同底层数组的相同初始条目(即&x[0] == &y[0]),或者它们的相应元素(长度最多)是深度相等的。请注意,一个非nil的空切片和一个nil切片(例如[]byte{}和[]byte(nil))不是深度相等的。

其他值-数字、布尔值、字符串和通道-如果使用Go的==运算符相等,则它们深度相等。


24
非常有用的答案。无论一般反映包的性能如何,拥有一个预打包的深度相等函数,供在测试用例中使用时简单易行和正确性至上,是非常好的。 - WeakPointer
59
我刚刚进行了一项基准测试,发现 reflect.DeepEqual 比循环慢了150倍。如果有人想在生产中使用这种方法,只是提供一下信息。 - nikdeapen
5
抱歉,为了确保翻译准确无误,我需要更多的上下文信息。这句话是在描述什么情境或者讨论什么话题呢? - Hemant_Negi
9
如果两个切片的顺序不同,它们就不相等。如果您想比较两个切片的相等性而忽略顺序,则可以对它们进行排序,然后检查,或将一个切片中的项目移动到映射中,然后检查另一个切片中的每个元素是否在映射中(此外,请确保它们具有相同的长度)。 - robbert229
11
Rob Pike(2011年)在官方Go博客上谈到反射:“它是一种强大的工具,应该小心使用并避免除非绝对必要。”我不会在生产代码中仅仅为了比较切片而使用反射。这很容易写成一个函数。但请注意,对于这个问题所选择的答案也存在潜在缺陷,具体取决于您期望它表现出的行为:它将发现已初始化但仍处于len 0和cap 0状态的切片与已声明但未初始化的切片不匹配。 - jrefior
显示剩余2条评论

220

您需要循环遍历切片中的每个元素并进行测试。对于切片,等式是没有定义的。然而,如果您正在比较[]byte类型的值,则有一个bytes.Equal函数可用。

func testEq(a, b []Type) bool {
    if len(a) != len(b) {
        return false
    }
    for i := range a {
        if a[i] != b[i] {
            return false
        }
    }
    return true
}

21
建议:for i, v := range a { if v != b[i] { return false } }。意思是对于两个切片a和b,通过循环比较它们的每个元素是否相等来检查它们是否相等。如果找到不相等的元素,则返回false表示它们不相等。 - zzzz
41
注意,这种方法在长度不同的情况下会失败。 - Filippo Valsorda
8
Go语言通常建议尽量避免使用反射,除非绝对必要。是的,需要为每种类型进行操作,但通常不会经常这样做。此外,reflect.DeepEqual可能会产生意想不到的结果,例如会将指向相同值的两个不同指针判定为相等。 - Stephen Weinberg
3
@FiloSottile 长度在之前已经被检查过了,只有在长度不同时才会进入循环。 - icza
2
@VladDidenko现在我知道你的意思了。是的,你是对的。虽然在大多数情况下,我会认为nil切片等于长度为0的切片,因为我的切片是如何创建/初始化的可能会导致不同的结果。例如,使用短变量声明通常会写成s:= []string{},它将不是nil,但使用var它将是var s []string,这将将其初始化为nil。请注意,两者都打印为空切片[]:[Go Playground] (http://play.golang.org/p/MJYiSq1q39) - icza
显示剩余12条评论

99

这只是一个例子,使用了reflect.DeepEqual(),该函数在@VictorDeryagin的回答中提到。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    a := []int {4,5,6}
    b := []int {4,5,6}
    c := []int {4,5,6,7}

    fmt.Println(reflect.DeepEqual(a, b))
    fmt.Println(reflect.DeepEqual(a, c))

}

结果:

true
false

Go Playground 中尝试一下


2
就性能而言,这与被接受的答案相比如何? - Manu Manjunath

58

如果你有两个[]byte,可以使用bytes.Equal进行比较。Golang文档表示:

Equal返回一个布尔值,指示a和b是否具有相同的长度并包含相同的字节。 nil参数等效于空切片。

用法:

package main

import (
    "fmt"
    "bytes"
)

func main() {
    a := []byte {1,2,3}
    b := []byte {1,2,3}
    c := []byte {1,2,2}

    fmt.Println(bytes.Equal(a, b))
    fmt.Println(bytes.Equal(a, c))
}

这将被打印

true
false

17

目前,这里有一个https://github.com/google/go-cmp,它旨在成为比reflect.DeepEqual更强大、更安全的替代品,用于比较两个值是否语义相等。

package main

import (
    "fmt"

    "github.com/google/go-cmp/cmp"
)

func main() {
    a := []byte{1, 2, 3}
    b := []byte{1, 2, 3}

    fmt.Println(cmp.Equal(a, b)) // true
}

14

当使用切片时,不能使用==!=进行比较,但如果可以对元素进行比较,则Go 1.18提供了一个新函数来轻松比较两个切片:slices.Equal

Equal用于报告两个切片是否相等:长度相同且所有元素都相等。如果长度不同,则返回false。否则,按递增索引顺序比较元素,并在第一组不相等的元素处停止比较。浮点NaN不被视为相等。

slices包的导入路径是golang.org/x/exp/slicesexp包中的代码是实验性的,尚未稳定。它最终将会被移动到标准库中 在Go 1.19中

然而,您可以在Go 1.18及之后的版本中立即使用它 (playground)。

    sliceA := []int{1, 2}
    sliceB := []int{1, 2}
    equal := slices.Equal(sliceA, sliceB)
    fmt.Println(equal) // true

    type data struct {
        num   float64
        label string
    }

    sliceC := []data{{10.99, "toy"}, {500.49, "phone"}}
    sliceD := []data{{10.99, "toy"}, {200.0, "phone"}}
    equal = slices.Equal(sliceC, sliceD)
    fmt.Println(equal) // true
如果切片的元素不支持==!=,您可以使用slices.EqualFunc,并为元素类型定义任何有意义的比较函数。

2
在go1.19中,它仍然位于"golang.org/x/exp/slices" - Eric

9
如果您希望编写测试,那么github.com/stretchr/testify/assert将是您的好朋友。 在文件开头导入该库:
import (
    "github.com/stretchr/testify/assert"
)

然后在测试内部执行:


func TestEquality_SomeSlice (t * testing.T) {
    a := []int{1, 2}
    b := []int{2, 1}
    assert.Equal(t, a, b)
}

错误提示将是:
                Diff:
                --- Expected
                +++ Actual
                @@ -1,4 +1,4 @@
                 ([]int) (len=2) {
                + (int) 1,
                  (int) 2,
                - (int) 2,
                  (int) 1,
Test:           TestEquality_SomeSlice

assert.Equal 内部使用 reflect.DeepEqual,这可能会使您的测试运行变慢,并最终影响您的流水线。 - Deepak Sah
2
@DeepakSah 你有性能差异的基准吗?根据我的经验,在测试中性能瓶颈不在于断言相等,而且你可以获得优质的消息,这提高了生产力。 - Gabriel Furstenheim
5
您可以使用assert.ElementsMatch(t, a, b)来忽略元素顺序。 - marcelosalloum

3

想到了一个巧妙的技巧,想和大家分享。

如果你想知道的是两个切片是否相同(即它们别名为数据的同一区域),而不仅仅是相等(一个切片中每个索引处的值等于另一个切片中相同索引处的值),那么你可以通过以下方式高效比较它们:

foo := []int{1,3,5,7,9,11,13,15,17,19}

// these two slices are exactly identical
subslice1 := foo[3:][:4]
subslice2 := foo[:7][3:]

slicesEqual := &subslice1[0]  == &subslice2[0]   && 
               len(subslice1) == len(subslice2)

在进行这种比较时需要注意一些限制,特别是不能以此方式比较空切片,并且不会比较切片的容量,因此这种“相同性”属性仅在从切片中读取或重新切割严格更窄的子切片时才真正有用,因为任何尝试增长切片的操作都会受到切片容量的影响。尽管如此,能够高效地声明“这两个巨大的内存块实际上是相同的块,是或否”仍然非常有用。


您的代码语法有误,可能是因为您有多余的括号。 - user4466350
有人可能想要运行fmt.Printf("%p %p\n", &subslice1[0], &subslice2[0])来查看两者是否共享相同的内存地址,并且只要将相同的索引与两个切片进行比较,它就可以正常工作。例如 fmt.Printf("%p %p\n", &subslice1[1], &subslice2[1]) 等。 - user4466350
2
它们会。重新切片不会重新分配内存,而是将原始切片正在使用的存储别名化,而且切片也总是连续的,因此不可能出现在某些索引处为真但在其他索引处不为真的切片。 - Wug

1
从Go 1.21版开始,您可以在标准库中使用通用函数。
package main

import (
    "fmt"
    "slices"
)

func main() {
    s1 := []int{1, 4, 1, 4, 2, 1, 3, 5, 6, 2}
    s2 := []int{1, 4, 1, 4, 2, 1, 3, 5, 6, 2}

    fmt.Println(slices.Equal(s1, s2))

    s3 := []string{"foo", "bar"}
    s4 := []string{"foo", "baz"}

    fmt.Println(slices.Equal(s3, s4))
}

游乐场:https://go.dev/play/p/_WwU0BSwN2P


1
Golang引入了一个名为Slices的包,其中包含对任意类型切片非常有用的各种函数。我们还可以使用Equal函数来判断两个切片是否相等。

https://cs.opensource.google/go/x/exp/+/06a737ee:slices/slices.go;l=22

// Equal reports whether two slices are equal: the same length and all
// elements equal. If the lengths are different, Equal returns false.
// Otherwise, the elements are compared in increasing index order, and the
// comparison stops at the first unequal pair.
// Floating point NaNs are not considered equal.
func Equal[E comparable](s1, s2 []E) bool {
    if len(s1) != len(s2) {
        return false
    }
    for i := range s1 {
        if s1[i] != s2[i] {
            return false
        }
    }
    return true
}

代码
package main

import (
    "fmt"

    "golang.org/x/exp/slices"
)

func main() {
    s1 := []int{1, 2}
    s2 := []int{1, 2}

    equal := slices.Equal(s1, s2)
    fmt.Println("Is Equal ? ", equal)
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接