Go语言实现对象相等断言的探索与思考

103 阅读4分钟

在Go语言的开发过程中,经常会遇到需要判断两个对象是否相等的场景。本次我们聚焦于实现一个Equal函数,用于断言两个对象是否相等,输入两个值,输出是否相等。接下来,我们将从我的思路、源码思路以及两者的对比等方面进行详细探讨。

问题描述

实现Equal函数,断言两个对象相等,输入两个值,输出是否相等。

  • 示例1
    • 输入:123, 123
    • 输出:相等
    • 原因:相同的基本类型值。
  • 示例2
    • 输入:&a, &b(其中a和b都是123)
    • 输出:相等
    • 原因:指针指向的值相同。
  • 示例3
    • 输入:func() {}, func() {}
    • 输出:不相等
    • 原因:函数无法比较,总是失败。
  • 源码地址: github.com/stretchr/te…

我的思路

首先这两个对象是接口类型的,我记得Go语言里面有判断接口类型方法,那么第一步是判断接口类型。

  1. 基础类型:比如123 | 123,我查找go.dev/tour/basics… 网站知道Go语言有基本bool、string、int、byte、float32、complex64,这些使用==就可以判断,这样解决了基础类型。
  2. 指针类型:比如 &a (a=5) | &b (b=5),指针类型我记得用*可以去到值,再使用==比较,然后我不清楚指针的地址值是怎么比较的,是使用==比较吗?
  3. 结构体类型:比如User{Name:"Alice"} | User{Name:"Alice"} ,判断结构体怎么相等,不相等,甚至多字段少字段的方式我很模糊,不清楚。
  4. 集合类型:比如[]int{1,2} | []int{1,2} ,这种比较先比较len的长度,然后将每一个取出来,看是否是上面的3个类型进行比较。
  5. 其余特殊类型:我先默认全部无法比较,返回不相等。

源码思路

  1. Equal函数:它接受TestingT接口、expectedactual参数,以及可选的msgAndArgs。首先检查t是否为tHelper类型,调用Helper方法,然后调用validateEqualArgs来验证参数是否有效,如果验证失败,则返回Fail。接着使用ObjectAreEqual来判断expectedactual是否相等,如果不相等,生成差异信息并返回Fail,否则返回true
  2. validateEqualArgs函数:检查两个函数是否都为nil,如果是则返回nil。然后检查是否有参数是函数类型,如果是则返回错误。这里参数在验证代码时就排除了函数类型,和我的思路一致。
  3. isFunction函数:通过反射检查函数类型是否是reflect.Func,以此判断是否是函数。这里的reflect包是Go语言SDK自带的包。
  4. ObjectAreEqual:是函数的核心比较逻辑,首先处理了nil的情况,如果其中一个为nil,则比较两者是否都是nil。然后处理[]byte类型的特殊情况,使用bytes.Equal进行比较。对于其他类型,直接使用reflect.DeepEqual进行深度比较。

对比两者思路

  1. 基础类型:我认为使用==即可,但代码中使用reflect.DeepEqual,实际上reflect.DeepEqual对于基础类型也会使用==比较,所以结果一致。
  2. 指针类型:我认为比较指针指向的值,而代码reflect.DeepEqual会递归解引用的值,比较指向的值,而不是地址。例如,&a&b如果指向相同的值,即使地址不同,也会被视为相等。
  3. 结构体类型:我认为需要逐个字段比较,而代码中reflect.DeepEqual会自动比较结构体的所有字段,包含导出和非导出字段。如果结构体所有字段相等,则结构体相等。这处理了我说的字段数量不相同的问题(如果结构体类型不相同,直接不相等)。
  4. 集合类型:我提到比较长度和每个元素,而reflect.DeepEqual对于切片会检查顺序和元素值,对于映射会检查键值对的匹配,但不考虑顺序。例如,[]int{1,2}[]int{2,1}视为不相等,而map[int]string{1:"a"}map[int]string{1:"a"}视为相等。
  5. 特殊类型:函数类型在validateEqualArgs被过滤,直接返回错误。通道类型在reflect.DeepEqual中比较通道的类型和值,但我认为通道无法比较。

感悟

如果按照我的思路,实现起来不仅繁琐且耗时,而且不容易维护。而这次探究源码的解决思路,我一下子发现解决方式可以如此高效。特别是我之前不知道Go语言中好用的reflect.DeepEqual可以帮我简化大量特殊类型的比较,也不知道原来将一个对比问题分而治之效果如此显著。

比如源码将Equal这个函数分为「检查参数」和「对比」,将「检查参数」分解为「判断是否为函数」。将「对比」分解为「如果一个是nil则比较两者是否都为nil」、「[]byte类型的特殊情况使用bytes.Equal比较」、「其他类型直接使用reflect.DeepEqual进行比较」。

这启发我要探究更多的源码解题思路,熟悉更多高质量的解题思路,提升自己的编程能力。