如何比较两个结构体、切片或映射是否相等?

210
我想检查两个结构体、切片和映射是否相等。
但是,我在以下代码中遇到了问题。请看我在相关行的注释。
package main

import (
    "fmt"
    "reflect"
)

type T struct {
    X int
    Y string
    Z []int
    M map[string]int
}

func main() {
    t1 := T{
        X: 1,
        Y: "lei",
        Z: []int{1, 2, 3},
        M: map[string]int{
            "a": 1,
            "b": 2,
        },
    }

    t2 := T{
        X: 1,
        Y: "lei",
        Z: []int{1, 2, 3},
        M: map[string]int{
            "a": 1,
            "b": 2,
        },
    }

    fmt.Println(t2 == t1)
    //error - invalid operation: t2 == t1 (struct containing []int cannot be compared)

    fmt.Println(reflect.ValueOf(t2) == reflect.ValueOf(t1))
    //false
    fmt.Println(reflect.TypeOf(t2) == reflect.TypeOf(t1))
    //true

    //Update: slice or map
    a1 := []int{1, 2, 3, 4}
    a2 := []int{1, 2, 3, 4}

    fmt.Println(a1 == a2)
    //invalid operation: a1 == a2 (slice can only be compared to nil)

    m1 := map[string]int{
        "a": 1,
        "b": 2,
    }
    m2 := map[string]int{
        "a": 1,
        "b": 2,
    }
    fmt.Println(m1 == m2)
    // m1 == m2 (map can only be compared to nil)
}

http://play.golang.org/p/AZIzW2WunI


请注意“无效操作:t2 == t1(包含map [string] int的结构体无法进行比较)”,如果结构体在其定义中没有int [],则会发生这种情况。 - Victor
8个回答

219
你可以使用reflect.DeepEqual,或者你可以实现自己的函数(从性能方面来说,这比使用反射更好):

http://play.golang.org/p/CPdfsYGNy_

m1 := map[string]int{   
    "a":1,
    "b":2,
}
m2 := map[string]int{   
    "a":1,
    "b":2,
}
fmt.Println(reflect.DeepEqual(m1, m2))

3
是否可以忽略一些字段,例如使用 cmpopts.IgnoreFields(person{},"ID") 来比较 p1p2 是否相等? - tuxErrante
2
不要忽略字段,将ID与要比较的数据分开(通过嵌入结构);这样你就可以在忽略ID的情况下进行比较。示例:https://go.dev/play/p/HvWunmktpkm - cyrix

133

reflect.DeepEqual 往往被错误地用于比较两个类似的结构体,例如你的问题。

cmp.Equal 是一个更好的比较结构体的工具。

为了解释为什么反射是不明智的,让我们看一下文档

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

...

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

如果我们比较两个相同UTC时间的time.Time值,如果它们的元数据时区不同,则t1 == t2将返回false。

go-cmp会查找Equal()方法,以正确比较时间。

示例:

m1 := map[string]int{
    "a": 1,
    "b": 2,
}
m2 := map[string]int{
    "a": 1,
    "b": 2,
}
fmt.Println(cmp.Equal(m1, m2)) // will result in true

重要提示:

使用cmp.Equal时要小心,因为它可能导致恐慌情况

它旨在仅在测试中使用,因为性能不是目标, 并且如果无法比较值,则可能会引发恐慌。其易于引发恐慌的特性意味着它不适用于生产环境, 在那里虚假的恐慌可能是致命的。


21
没错!编写测试时,使用go-cmp而不是reflect非常重要。 - Kevin Minehart
8
根据 cmp 的文档,建议仅在编写测试时使用 cmp,因为如果对象不可比较,它可能会导致程序崩溃。 - martin
github.com/stretchr/testify/require在比较两个结构体时使用了reflect.DeepEqual,因此当两个私有字段不相等时会失败。 - Dennis
只是一点说明,time包的文档中提到不要使用t1 == t2。"一般情况下,优先使用t.Equal(u)而不是t == u" 这就是t.Equal的作用。请参阅https://pkg.go.dev/time#Time和https://pkg.go.dev/time#Time.Equal。 - undefined

29

以下是如何编写自己的功能函数http://play.golang.org/p/Qgw7XuLNhb

func compare(a, b *T) bool {
  if a == b {
    return true
  }
  if a.X != b.X || a.Y != b.Y {
    return false
  }
  if len(a.Z) != len(b.Z) || len(a.M) != len(b.M) {
    return false
  }
  for i, v := range a.Z {
    if b.Z[i] != v {
      return false
    }
  }
  for k, v := range a.M {
    if b.M[k] != v {
      return false
    }
  }
  return true
}

更新:Go 1.18

import (
    "golang.org/x/exp/maps"
    "golang.org/x/exp/slices"
)

func compare(a, b *T) bool {
    if a == b {
        return true
    }
    if a.X != b.X {
        return false
    }
    if a.Y != b.Y {
        return false
    }
    if !slices.Equal(a.Z, b.Z) {
        return false
    }
    return maps.Equal(a.M, b.M)
}

5
我建议添加 if len(a.Z) != len(b.Z) || len(a.M) != len(b.M) { return false },因为其中一个可能有额外的字段。 - OneOfOne
1
所有的结构信息在编译时都已知。遗憾的是,编译器无法以某种方式完成这项繁重的工作。 - Rick-777
3
@Rick-777,对于切片来说,并没有定义比较方法。这是语言设计者所希望的。与比较简单的整数相比,定义切片的比较并不简单。如果两个切片包含相同顺序的元素,它们是否相等?但是如果它们的容量不同呢?等等。 - justinas
2
如果&a == &b { 返回真 }如果要比较的参数是按值传递的,则永远不会评估为真。 - Sean
1
看起来你把第一个语句改成了 if a == b ...,这是不可编译的(因此 OP 提出了问题)。我想你想要跟进 Sean 的注释。你需要将 T 改为 *T,然后第一个 if() 就可以编译(并按预期工作)。 - Alexis Wilke

18

如果你想在测试中使用它,自2017年7月以来,你可以使用cmp.Equalcmpopts.IgnoreFields选项。

func TestPerson(t *testing.T) {
    type person struct {
        ID   int
        Name string
    }

    p1 := person{ID: 1, Name: "john doe"}
    p2 := person{ID: 2, Name: "john doe"}
    println(cmp.Equal(p1, p2))
    println(cmp.Equal(p1, p2, cmpopts.IgnoreFields(person{}, "ID")))

    // Prints:
    // false
    // true
}

请注意,cmp包不适用于生产代码,因为如果无法比较值,它会导致程序崩溃。它仅用于测试。请参阅此答案中的主持人注释和评论:https://dev59.com/XWAf5IYBdhLWcg3wizME#45222521。 - nishanthshanmugham
1
不错的发现,让我修改答案。 - wst

11
如果你正在进行单元测试比较,一种方便的替代方法是在 testify 中使用 EqualValues 函数。请参考:testifyEqualValues

4

如果你想比较简单的一级结构体,最好和简单的方法是使用 if 语句。

像这样 if s1 == s2

这里有一个简单的例子:

type User struct { 
    name      string 
    email           string
} 

func main() {
    u1 := User{
            name: "Iron Man", 
            email: "ironman@avengers.com",
    }
    u2 := User{
            name: "Iron Man", 
            email: "ironman@avengers.com",
    }
        // Comparing 2 structs
        if u1 == u2 {
            fmt.Println("u1 is equal to u2")
        } else {
            fmt.Println("u1 is not equal to u2")
        }
}

结果: u1等于u2

您可以在这里进行尝试。


3

比较地图的新方法

这个提案 (https://github.com/golang/go/issues/47649) 是 Go 通用性未来实现的一部分,引入了一个新的函数来比较两个地图,maps.Equal:

// Equal reports whether two maps contain the same key/value pairs.
// Values are compared using ==.
func Equal[M1, M2 constraints.Map[K, V], K, V comparable](m1 M1, m2 M2) bool

示例用法

strMapX := map[string]int{
    "one": 1,
    "two": 2,
}
strMapY := map[string]int{
    "one": 1,
    "two": 2,
}

equal := maps.Equal(strMapX, strMapY)
// equal is true
maps软件包位于golang.org/x/exp/maps中。这是实验性的,不在Go兼容性保证之内。他们的目标是将其移动到Go 1.19的std lib中。
您可以在gotip playground中看到它的工作原理:https://gotipplay.golang.org/p/M0T6bCm1_3m

0
我们还可以使用哈希算法来比较嵌套数据。 对于比较每个值的基本思想是使用fmt包可以处理的字符串。如果你查看fmt包,特别是负责打印数据的功能,你会发现fmt已经在内部使用了反射。因此,我们可以使用以下表达式:
fmt.Println(t1)
// prints: {1 lei [1 2 3] map[a:1 b:2]}

val := fmt.Sprint(t1)
// val == "{1 lei [1 2 3] map[a:1 b:2]}"

所以,了解这一点,我们可以使用这个表达式进行比较:
fmt.Sprint(t1) == fmt.Sprint(t2) // true

但是这里出现了一个问题。这个表达式在遇到变量中的指针时将无法正常工作。
fmt.Sprint(t1) == fmt.Sprint(&t2) // false
fmt.Sprint(&t1) == fmt.Sprint(t2) // false

如果我们看一下这两个值之间的差异,我们可以看到一个小细节:
fmt.Pprint(t1)  //  {1 lei [1 2 3] map[a:1 b:2]}
fmt.Sprint(&t1) // &{1 lei [1 2 3] map[a:1 b:2]}

在这种情况下,我们应该使用reflect.Indirect()
ref1 := reflect.Indirect(reflect.ValueOf(t1))
ref2 := reflect.Indirect(reflect.ValueOf(&t2))

fmt.Sprint(ref1) == fmt.Sprint(ref2) // true

最终版本:
func HashNested(nested interface{}) string {
    nested = fmt.Sprint(reflect.Indirect(reflect.ValueOf(nested)))
    return fmt.Sprintf("%x", sha256.Sum256([]byte(nested.(string))))
}

func CompareNested(nestedA, nestedB interface{}) bool {
    return HashNested(nestedA) == HashNested(nestedB)
}

或者不使用哈希算法:
func CompareNested(nestedA, nestedB interface{}) bool {
    nestedA = fmt.Sprint(reflect.Indirect(reflect.ValueOf(nestedA)))
    nestedB = fmt.Sprint(reflect.Indirect(reflect.ValueOf(nestedB)))
    return nestedA == nestedB
}

结果:
fmt.Println(CompareNested(t1, t2)) // true
fmt.Println(CompareNested(a1, a2)) // true
fmt.Println(CompareNested(m1, m2)) // true

请查看Go Playground


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