Golang为什么要用命名和非命名结构体进行比较,结果相等?

3

《Go编程语言规范Specification》中提到:

如果结构体的所有字段都是可比较的,则结构体值也是可比较的。如果两个结构体值对应的非空字段相等,则这两个结构体值相等。

但是根据下面的代码片段,变量v1和v3看起来类型不同,为什么它们可以得到true输出:

package main

import "fmt"
import "reflect"

type T1 struct { name string }
type T2 struct { name string }

func main() {
    v1 := T1 { "foo" }
    v2 := T2 { "foo" }
    v3 := struct{ name string } {"foo"}
    v4 := struct{ name string } {"foo"}

    fmt.Println("v1: type=", reflect.TypeOf(v1), "value=", reflect.ValueOf(v1)) // v1: type= main.T1 value= {foo}
    fmt.Println("v2: type=", reflect.TypeOf(v2), "value=", reflect.ValueOf(v2)) // v2: type= main.T2 value= {foo}
    fmt.Println("v3: type=", reflect.TypeOf(v3), "value=", reflect.ValueOf(v3)) // v3: type= struct { name string } value= {foo}
    fmt.Println("v4: type=", reflect.TypeOf(v4), "value=", reflect.ValueOf(v4)) // v4: type= struct { name string } value= {foo}

    //fmt.Println(v1 == v2) // compiler error: invalid operation: v1 == v2 (mismatched types T1 and T2)
    fmt.Println(v1 == v3)   // true, why? their type is different
    fmt.Println(v2 == v3)   // true, why?
    fmt.Println(v3 == v4)   // true
}

合理的是,v1 == v2 会因为它们是不同类型而导致编译错误,然而如何解释 v1 == v3 得到了一个 true 的结果呢?因为它们也有不同的类型,一个是带有命名结构体类型 T1,另一个是匿名结构体。
感谢 @icza、@John Weldon 的解释,我认为这个问题已经解决了,现在我正在更新问题。
总之,如果一个结构体满足以下两个规范,则可以进行比较:
  1. 如果所有字段都是可比较的,则结构体值是可比较的。如果它们对应的非空字段相等,则两个结构体值相等。

  2. 在任何比较中,第一个操作数必须可分配给第二个操作数的类型,反之亦然。

第一个规范是针对结构体类型变量的具体定义;第二个规范是针对所有类型变量的比较,当然也包括结构体类型变量。
在我的示例中,比较变量 v1 和 v3 符合这两个规范的定义。
  1. 所有字段都是可比较的;事实上,第一个规范定义了结构体规则,它关注的是字段,而不是结构体本身,因此无论是命名结构体还是匿名结构体,它们都遵循同样的规则。
  2. 变量v1和v3是可赋值的。(根据规则:x的类型V和T具有相同的基础类型,并且V或T中至少有一个不是已定义的类型)

这就是为什么"v1 == v3"能得到真结果的原因。


我不是go专家(因此只是留言),但我猜结构体的行为与字符串类似,即它们在需要时才会被赋予类型。也就是说,将某个字符串常量与命名的字符串类型进行比较是可以的,但将两个命名的字符串类型进行比较将引发编译器错误。这是由于go非常类型严格,因此认为这是2个“不同”的命名类型,从而引发了错误。再次说明,我自己也正在学习go,可能完全错了。 - mattjegan
2个回答

5
如果您仔细阅读规范,您会发现如果相应的非空字段相等,则两个结构体相等。如果类型name不同,则编译器将失败,但如果一个或两个类型是匿名的,则它们将是可比较的。类型T1T2和匿名结构体实际上是相同的类型,因为它们具有相同的字段。当字段值相同时,它们将被视为相同。
查看规范中的type identity可能会使其更加清晰(也可能不会,因人而异)。

如果两个结构体类型具有相同的字段序列,并且相应的字段具有相同的名称、相同的类型和相同的标签,则它们是相同的。来自不同包的非导出字段名称始终不同。

因此,如果您尝试通过添加字段标记或将类型放在不同的包中来更改类型,则可能会得到您期望的差异。

1
你说过结构体可以比较,即使类型名称不同也可以比较。那么能否解释一下为什么 OP 在 v1 == v2 时会得到编译器错误? - mattjegan
@mattjegan 很好的发现 - 是我的错误。已更新措辞。 - John Weldon

2
你遗漏了一个事实,即(相等)比较运算符并不要求其操作数具有相同的类型。该要求根据规范:比较运算符:确定:

在任何比较中,第一个操作数必须可以赋值给第二个操作数的类型,反之亦然。

是的,类型相等会自动赋予可赋值属性,但还有其他情况:
一个值 x 可以被分配给类型为 T 的变量("x 可以被分配给 T")在以下任一情况下:
  • x 的类型与 T 相同。
  • x 的类型 VT 有相同的 底层类型,并且 VT 中至少有一个不是 定义的 类型。
  • T 是一个接口类型,x 实现T
  • x 是一个双向通道值,T 是一个通道类型,x 的类型 VT 有相同的元素类型,并且 VT 中至少有一个不是定义的类型。
  • x 是预声明的标识符 nil,并且 T 是指针、函数、切片、映射、通道或接口类型。
  • x 是一个未命名的常量,其可以用类型 T 的值表示。
这里适用于突出显示的规则。 v1v2v3都具有相同的基础类型(即struct { name string }),在v1 == v3v2 == v3比较中只有v1的类型是定义的。因此它们是可赋值的,并且从那里开始,您引用的适用于struct值的比较规则清楚地解释了为什么结果是true

如果所有字段都是可比较的,则结构值是可比较的。如果它们对应的非空字段相等,则两个结构值相等。

如果您考虑一下,这并不令人“震惊”。您正在比较值而不是类型。当可能使用比较时可能不直观的是什么,但可赋值性的规则清晰且干净地列出了规则。
另一个类似的示例(在Go Playground上尝试):
var i interface{} = 3
var j int = 3
fmt.Println(i == j) // Comparing interface{} with int: true

我们正在比较 interface{}int 类型的值(甚至比您的结构体类型更“远”),但这是有效的,结果为true,这也不令人震惊,相反,我们会期望这样。 int 可以赋值给类型为 interface{} 的变量(一切都可以赋值给 interface{})。在比较它们时,将创建一个隐式的 interface{} 值来包装 j,并且根据以下规则比较这两个接口值:

接口值是可比较的。如果它们具有相同的动态类型和相等的动态值,或者两者都具有值 nil,则两个接口值相等。

注:

可赋性是可比性的要求,但不是充分条件。还必须满足 规范:比较运算符 中概述的进一步要求,这些要求基于操作数的类型。

等式运算符 ==!= 适用于可比较的操作数。排序运算符 <<=>>= 适用于有序的操作数。这些术语和比较结果的定义如下:
  • 布尔值是可比较的。如果两个布尔值都是 true 或都是 false,则它们相等。
  • 整数值按通常方式进行比较和排序。
  • 浮点数值按 IEEE-754 标准定义进行比较和排序。
  • 复数值是可比较的。如果两个复数值 u 和 v 满足 real(u) == real(v)imag(u) == imag(v),则它们相等。
  • 字符串值按字节的词法顺序进行比较和排序。
  • 指针值是可比较的。如果两个指针值指向相同的变量或者都具有值 nil,则它们相等。指向不同零大小变量的指针可能相等也可能不相等。
  • 通道值是可比较的。如果两个通道值是由同一个 make 调用创建的或者都具有值 nil,则它们相等。
  • 接口值是可比较的。如果它们具有 相同 的动态类型和相等的动态值,或者都具有值 nil,则它们相等。
  • 非接口类型 X 的值 x 和接口类型 T 的值 t 是可比较的,当且仅当类型 X 可比较且 X 实现了 T。如果 t 的动态类型与 X 相同且 t 的动态值等于 x,则它们相等。
  • 结构体值是可比较的,如果它们的所有字段都是可比较的。如果两个结构体值的相应非空白字段相等,则它们相等。
  • 数组值是可比较的,如果数组元素类型的值是可比较的。如果两个数组值的相应元素相等,则它们相等。
如果两个具有相同动态类型的接口值无法进行比较,则会导致运行时出错。此行为不仅适用于直接接口值比较,还适用于比较接口值数组或具有接口类型字段的结构体。
切片、映射和函数值是不可比较的。但是,作为一个特殊情况,切片、映射或函数值可以与预声明的标识符 nil 进行比较。将指针、通道和接口值与 nil 进行比较也是允许的,并遵循上述通用规则。
作为一个微不足道的例子,规范明确指出切片值是不可比较的。
i1, i2 := []int{1, 2}, []int{1, 2}
fmt.Println(i1 == i2)
// invalid operation: i1 == i2 (slice can only be compared to nil)

即使i1i2具有相同的类型,因此i1可以分配给i2(反之亦然),但仍不允许比较,因此它们不可比较。
另一个不太平凡的例子是,如果结构体的所有字段都是可比较的,则可以比较结构体。因此,例如这个结构体的值不可比较(因为它具有切片类型的字段,而切片不可比较):
type Foo struct { Names []string }

我仍然对规范“在任何比较中,第一个操作数必须可分配给第二个操作数的类型,反之亦然。”感到困惑;根据我的理解,如果变量v1和v2是可比较的,这意味着v1可以分配给v2或v2可以分配给v1,但这并不意味着“如果v1可以分配给v2,反之亦然,则v1和v2是可比较的”。因此,可分配是可比较的必要条件,而不是充分条件。这是真的吗?谢谢 - Hui
@Hui,你的理解是正确的。请查看带有示例的编辑答案。 - icza
通过您的详细解释,我已经理解了问题,我会更新我的问题。 - Hui

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