从一道面试题来谈谈Golang中的 ==

写这篇文章的时候,已经离我找工作有一段时间了,但是觉得这道题不管是面试还是日常的工作中,都会经常遇到,所以还是特意写一篇文章,记录下自己对Golang中==的理解。如文章中出现不对的地方,请不吝赐教,谢谢。

注意,以下文章内容是基于 go1.16.4 进行演示的,如果和你验证时,结果不一致,可能 Go 的判断规则有所改变。

1、面试题

大家可以先不看结果,想想答案,再看后面的结果以及相关的分析。

type T interface {

}

func main()  {

	var (
		t T
		p *T

		i1 interface{} = t
		i2 interface{} = p
	)

	fmt.Println(i1 ==t, i1 == nil)
	fmt.Println(i2 ==p, i2 == nil)
	fmt.Println(t == nil)
	fmt.Println(p == nil)

}

执行结果:

true true
true false
true
true

分析:

1、interface 值由动态类型动态值组成。只有在类型都相同时才相等。接口变量i1是接口类型的零值,也就是它的类型和值部分都是nil,接口变量i2的动态值虽然是零值,但是动态类型为 *T
2、变量 t、p 都没有初始化,未分配内存,所以 变量t、p 都等于 nil。

对于上面的描述不太清楚的同学,不用着急,我们一起来学习 Golang 中的==,有较为详细的介绍。

2、Golang中的数据类型

Golang中的数据类型分为4大类,他们分别是:

  1. 基本类型 (Primary types): 整型(int/uint/int8/uint8/int16/uint16/int32/uint32/int64/uint64/byte/rune等)、浮点数(float32/float64)、复数类型(complex64/complex128)、字符串(string)、布尔(true/false)。这些是Go语言内置的基本数据类型,它们是Go语言的原始数据类型,不能再细分。
  2. 复合类型 (Composite types):又叫聚合类型。包括数组、结构体。复合类型允许将多个值组合成一个新的数据结构。
  3. 引用类型 (Reference types):这些类型在内存中存储的是数据的地址,包括 指针、切片(slice)、映射(map)、通道(channel)、函数类型(func)。引用类型允许在函数间共享和修改数据。
  4. 接口类型 (Interface types):接口类型是一种抽象类型,它定义了对象的行为,而不关心对象的具体类型。通过实现接口,可以实现多态性和代码复用。比如 error

其实接口类型可以看作是引用类型,在 Go 中,接口类型是一种特殊的引用类型,它包含一个指向实际数据的指针以及类型信息。当你将一个具体类型的值赋给接口变量时,接口会存储一个指向实际数据的指针或实际数据的拷贝。因此,接口可以看作是对其他类型的引用,而不是直接包含实际数据。

在Go语言中,自定义类型属于基本类型的概念中。

自定义类型属于基本类型的一种,它通过使用 type 关键字来创建新的类型,底层使用基本数据类型。通过自定义类型,我们可以为基本类型赋予更多语义,并且可以为它们定义自己的方法。自定义类型和其他基本类型具有相同的操作和运算规则,但在类型系统中它们是不同的类型。

例如使用 type number int64 时,我们自定义了一种数据类型,叫做number。虽然它底层使用了int64,但在类型系统中,numberfloat64是不同的类型。

在Go语言中,还有一种类型别名的叫法,是 Go1.9 引用的新功能。

类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。例如:

type byte = uint8
type rune = int32

==操作最重要的一个前提是:两个操作数类型必须相同!!!

golang 的类型系统非常严格,没有C/C++/python中的隐式类型转换。这个需要注意。

3、四大类型如何使用 ==

3.1、基本类型

基本类型的比较,就比较简单直观,直接使用==判断就好了,注意的是Go中并没有隐式转换,而且类型一致才可以

package main

import "fmt"

func main() {
	var a int64
	var b int64
	var c int32

	fmt.Println(a == b)
	fmt.Println(c)

	// Invalid operation: a == c (mismatched types int64 and int32)
	//fmt.Println(a == c)
}

接下来我们看看浮点数的比较:

package main

import "fmt"

func main() {
	var a float64 = 0.1
	var b float64 = 0.2
	var c float64 = 0.3

	fmt.Println(a + b)  // 0.30000000000000004
	fmt.Println(a+b == c)  // false

}

是不是有点小惊讶,这个是因为Go 中的 浮点数遵循 IEEE 754 标准,所以会有有些浮点数不能精确表示,浮点运算结果会有误差。

想大概了解计算机是如何表示浮点数的可以看看下面的文章,有一个基础的了解。

数字编码

注意:

浮点数做 判等 操作一般是使用 计算两个浮点数的差的绝对值,如果小于一定的值就认为它们相等,比如1e-9

package main

import (
	"fmt"
	"math"
)

func main() {
	var a = 0.1
	var b = 0.2
	var c = 0.3

	fmt.Println(a + b)  // 0.30000000000000004
	fmt.Println(math.Abs((a+b)-c) < 1e-9) // true
	fmt.Printf("%T", a) // float64
}

3.2、复合类型

合类型也叫做聚合类型。golang 中的复合类型只有两种:数组和结构体。它们是逐元素/字段比较的。

注意:数组的长度视为类型的一部分,长度不同的两个数组是不同的类型,不能直接比较

  • 对于数组来说,依次比较各个元素的值。根据元素类型的不同,再依据是基本类型、复合类型、引用类型或接口类型,按照特定类型的规则进行比较。所有元素全都相等,数组才是相等的。
  • 对于结构体来说,依次比较各个字段的值。根据字段类型的不同,再依据是 4 中类型中的哪一种,按照特定类型的规则进行比较。所有字段全都相等,结构体才是相等的。

注意:如包含了不支持直接使用 == 符号的类型,在编译阶段会报错。

例如:

package main

import "fmt"

type Student struct {
	Name string
	Age  int
	Sex  bool
}

type S1 struct {
	Name   string
	Scores []int8  // 注意这里定义的是 slice 类型
}

type ITest interface{}

func main() {
	arrayA := [...]int64{2, 3, 4}
	arrayB := [...]int64{2, 3, 4}
	arrayC := [...]int64{1, 3, 4}
	fmt.Println(arrayA == arrayB) // true
	fmt.Println(arrayB == arrayC) // false
	fmt.Println("-------")

	s1 := Student{"xiaoming", 18, false}
	s2 := Student{"xiaoming", 18, false}
	s3 := Student{"xiaowang", 18, false}
	fmt.Println(s1 == s2) // true
	fmt.Println(s1 == s3) // false

	fmt.Println("-------")
	a1 := [...]Student{s1, s2}
	// 注意这两个元素!
	a2 := [2]Student{s2, s2}
	a3 := [2]Student{s2, s3}
	fmt.Println(a1 == a2) // true
	fmt.Println(a3 == a2) // false
	fmt.Println("-------")

	var i1 ITest = 23
	var i2 ITest = 23
	var i3 ITest = "tt"
	var i4 ITest = 23

	fmt.Println(i1 == i2) // true
	fmt.Println(i3 == i4) // false
	is1 := [...]ITest{i1, i2}
	is2 := [...]ITest{i1, i4}
	is3 := [...]ITest{i1, i3}

	fmt.Println(is1 == is2) // true
	fmt.Println(is1 == is3) // false

	fmt.Println("-------")

	t1 := S1{"xw", []int8{66, 88}}
	t2 := S1{"xw", []int8{66, 88}}
	t3 := S1{"xw", []int8{66, 99}}
	
  // 为什么这里会报错呢,因为我们定义的结构体中的 Score 字段是 slice, slice 是不支持使用 == 符号的
	// Invalid operation: t1 == t2 (the operator == is not defined on S1)
	//fmt.Println(t1 == t2)
	// Invalid operation: t1 == t2 (the operator == is not defined on S1)
	//fmt.Println(t1 == t3)

	// go 中 slice 使用 reflect.DeepEqual 判断是否相等
	fmt.Println(reflect.DeepEqual(t1, t2)) // true
	fmt.Println(reflect.DeepEqual(t1, t3)) // false
}

3.3、引用类型

引用类型是指那些底层数据结构的值是引用地址(指针)的类型。它们在内存中存储的是指向实际数据的指针,而不是实际数据本身。切片、映射、通道和函数都是引用类型,因为它们在底层都使用了指针来引用实际的数据。

引用类型的比较实际判断的是两个变量是不是指向同一份数据,它不会去比较实际指向的数据。

关于引用类型,有几个比较特殊的规定:

  • 切片之间不允许比较。切片只能与nil值比较。
  • map之间不允许比较。map只能与nil值比较。
  • 函数之间不允许比较。函数只能与nil值比较。

接下来我们在仔细看看各个类型的具体介绍。

3.3.1、指针
package main

import (
	"fmt"
)

type Student struct {
	Name string
	Age  int
	Sex  bool
}

func main() {

	s1 := &Student{"xiaoming", 18, false}
	s2 := &Student{"xiaoming", 18, false}
	s3 := s1
	fmt.Println(s1 == s2) // false
	fmt.Println(s1 == s3) // true
}

s1 和 s2 虽然数据一样,但是他们在内存中的地址并不相等,所以他们是不相等的,s1 和 s3 指向的是同一份内存地址,所以是相等的。

3.3.2、channel 和 函数类型

接下来我们再看看 channel 和 函数类型:

package main

import "fmt"

type Student struct {
	Name string
	Age  int
	Sex  bool
}

func main() {

	ch1 := make(chan bool, 1)
	ch2 := make(chan bool, 1)
	ch3 := ch1
	fmt.Println(ch1 == ch2) // false
	fmt.Println(ch1 == ch3) // true
	fmt.Println("-----")

	a := TestFunc
	b := TestFunc
	c := a

	// invalid operation: a == b (func can only be compared to nil)
	//fmt.Println(a == b)
	// invalid operation: a == c (func can only be compared to nil)
	//fmt.Println(a == c)

	fmt.Println(a)  // 0x10a3400
	fmt.Println(b)  // 0x10a3400
	fmt.Println(c)  // 0x10a3400
}

func TestFunc() {

}

从上面可以看出来,函数类型不支持直接判等操作。原因是:函数类型不支持直接的判等操作是因为函数类型是一种复杂的类型,它包含了函数的签名和实现代码等信息。由于函数可以是闭包,可能捕获了外部变量,因此函数的判等操作会涉及到比较函数的底层实现和捕获的变量等细节,这会导致判等操作的复杂性和不确定性。

所以从中也可以看出来 Go 中判断引用类型是否相等,不是简单的判断变量所在的内存地址是否一致,而是根据相应的类型,有不同的判断规则,这里大家需要注意。

3.3.3、slice

再看看切片。因为切片是引用类型,它可以间接的指向自己。例如:

a := []interface{}{ 1, 2.0 }
a[1] = a
fmt.Println(a)

// !!!
// runtime: goroutine stack exceeds 1000000000-byte limit
// fatal error: stack overflow

上面代码将a赋值给a[1]导致递归引用,fmt.Println(a)语句直接爆栈。

  • 切片如果直接比较引用地址,是不合适的。首先,切片与数组是比较相近的类型,比较方式的差异会造成使用者的混淆。另外,长度和容量是切片类型的一部分,不同长度和容量的切片如何比较?
  • 切片如果像数组那样比较里面的元素,又会出现上来提到的循环引用的问题。虽然可以在语言层面解决这个问题,但是 golang 团队认为不值得为此耗费精力。

基于上面两点原因,golang 直接规定切片类型不可比较。使用==比较切片直接编译报错。

例如:

var a []int
var b []int

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

如果实在是需要判断 slice 中元素是否相等,我们一般是自定义一个 判断函数或者使用reflect.DeepEqual函数。

package main

import (
	"fmt"
	"reflect"
)

func slicesAreEqual(slice1, slice2 []int) bool {
	if len(slice1) != len(slice2) {
		return false
	}

	for i := 0; i < len(slice1); i++ {
		if slice1[i] != slice2[i] {
			return false
		}
	}

	return true
}

func main() {
	slice1 := []int{1, 2, 3}
	slice2 := []int{1, 2, 3}
	slice3 := []int{4, 5, 6}

	fmt.Println(reflect.DeepEqual(slice1, slice2)) // 输出: false (reflect.DeepEqual 可以进行值相等判断)
	fmt.Println(slicesAreEqual(slice1, slice2))   // 输出: true

	fmt.Println(slicesAreEqual(slice1, slice3)) // 输出: false
}

注意,在上面的示例中,我们自定义了slicesAreEqual函数来判断两个切片是否拥有相同的元素。这个示例中我们使用了reflect.DeepEqual来进行值相等的判断,但是不推荐在切片的值相等判断中使用reflect.DeepEqual,因为它会将切片的元素逐个进行深度比较,效率较低,尤其在切片较大时。通常最好手动遍历比较切片的元素。

3.3.4、map

在 Go 中,map 类型不支持直接的判等操作,因为 map 是一个引用类型,并不存储在变量中的实际数据,而是一个指向底层数据结构的指针。map 是一种哈希表的实现,它包含了键值对的集合。

当你将一个 map 赋值给另一个变量时,它们引用同一个底层的 map 数据。因此,两个 map 可能引用相同的底层数据,但它们仍然是不同的 map 对象。直接比较两个 map 是否相等,并不能确定它们是否引用同一个底层数据。

如果你需要判断两个 map 是否包含相同的键值对,你可以通过手动遍历比较 map 的键值对来实现。这涉及到比较每个键值对的键和值是否相等。

package main

import (
	"fmt"
	"reflect"
)

func mapsAreEqual(map1, map2 map[string]int) bool {
	if len(map1) != len(map2) {
		return false
	}

	for key, value := range map1 {
		if map2Value, ok := map2[key]; !ok || map2Value != value {
			return false
		}
	}

	return true
}

func main() {
	map1 := map[string]int{"a": 1, "b": 2, "c": 3}
	map2 := map[string]int{"a": 1, "b": 2, "c": 3}
	map3 := map[string]int{"a": 1, "b": 2, "c": 4}
  
  // invalid operation: map1 == map2 (map can only be compared to nil)
	//fmt.Println(map1 == map2)

	fmt.Println(reflect.DeepEqual(map1, map2)) // 输出: true (reflect.DeepEqual 可以进行值相等判断)
	fmt.Println(mapsAreEqual(map1, map2))     // 输出: true

	fmt.Println(mapsAreEqual(map1, map3)) // 输出: false
}

在上面的示例中,我们自定义了mapsAreEqual函数来判断两个 map 是否包含相同的键值对。请注意,与前面提到的reflect.DeepEqual一样,我们也不推荐在 map 的值相等判断中使用reflect.DeepEqual,因为它会将 map 的键值对逐个进行深度比较,效率较低,尤其在 map 较大时。通常最好手动遍历比较 map 的键值对。

注意:

由于map的底层原理是使用到了 hash 表,所以所有不可比较的类型都不能作为mapkey。例如:

// invalid map key type []int
m1 := make(map[[]int]int)

type A struct {
    a []int
    b string
}
// invalid map key type A
m2 := make(map[A]int)

由于切片类型不可比较,不能作为mapkey,编译时m1 := make(map[[]int]int)报错。 由于结构体A含有切片字段,不可比较,不能作为mapkey,编译报错。

3.4、接口类型

以下内容来自后面的参考链接 深入理解Go之== ,十分感谢原博文作者。

接口类型的值可以是任意一个实现了该接口的类型值,所以接口值除了需要记录具体之外,还需要记录这个值属于的类型。也就是说接口值由“类型”和“值”组成,鉴于这两部分会根据存入值的不同而发生变化,我们称之为接口的动态类型动态值

接口值的比较涉及这两部分的比较,只有当动态类型完全相同且动态值相等(动态值使用==比较),两个接口值才是相等的。

package main

import "fmt"

func main() {
	var a interface{} = 1
	var b interface{} = 2
	var c interface{} = 1
	var d interface{} = 1.0
	fmt.Println(a == b) // false
	fmt.Println(a == c) // true
	fmt.Println(a == d) // false
}

ab动态类型相同(都是int),动态值也相同(都是1,基本类型比较),故两者相等。 ac动态类型相同,动态值不等(分别为12,基本类型比较),故两者不等。 ad动态类型不同,aintdfloat64,故两者不等。

package main

import "fmt"

func main() {
	type A struct {
		a int
		b string
	}

	var aa interface{} = A{a: 1, b: "test"}
	var bb interface{} = A{a: 1, b: "test"}
	var cc interface{} = A{a: 2, b: "test"}

	fmt.Println(aa == bb) // true
	fmt.Println(aa == cc) // false

	var dd interface{} = &A{a: 1, b: "test"}
	var ee interface{} = &A{a: 1, b: "test"}
	fmt.Println(dd == ee) // false
}

aabb动态类型相同(都是A),动态值也相同(结构体A,见上面复合类型的比较规则),故两者相等。 aacc动态类型相同,动态值不同,故两者不等。 ddee动态类型相同(都是*A),动态值使用指针(引用)类型的比较,由于不是指向同一个地址,故不等。

注意:

如果接口的动态值不可比较,强行比较会panic!!!

var a interface{} = []int{1, 2, 3, 4}
var b interface{} = []int{1, 2, 3, 4}
// panic: runtime error: comparing uncomparable type []int
fmt.Println(a == b)

ab的动态值是切片类型,而切片类型不可比较,所以a == bpanic

接口值的比较不要求接口类型(注意不是动态类型)完全相同,只要一个接口可以转化为另一个就可以比较。例如:

package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	var f *os.File

	var r io.Reader = f
	var rc io.ReadCloser = f
	fmt.Println(r == rc) // true

	var w io.Writer = f
	// invalid operation: r == w (mismatched types io.Reader and io.Writer)
	fmt.Println(r == w)
}

type ReadCloser interface {
	Reader
	Closer
}

r的类型为io.Reader接口,rc的类型为io.ReadCloser接口。查看源码,io.ReadCloser的定义如下:

io.ReadCloser可转化为io.Reader,故两者可比较。

io.Writer不可转化为io.Reader,编译报错。

4、注意事项

不可比较性:

前面说过,golang 中的切片类型、map类型、函数类型(func)是不可比较的。所有含有切片的类型都是不可比较的。例如:

  • 数组元素是切片类型、map类型、函数类型(func)。
  • 结构体有切片类型、map类型、函数类型(func)的字段。
  • 指针指向的是切片类型、map类型、函数类型(func)。

不可比较性会传递,如果一个结构体由于含有切片字段不可比较,那么将它作为元素的数组不可比较,将它作为字段类型的结构体不可比较

package main

import "fmt"

func main() {

	type T struct {
		a map[string]bool
	}
	t1 := T{
		a: map[string]bool{"ni": true},
	}

	t2 := T{
		a: map[string]bool{"ni": true},
	}

	// invalid operation: t1 == t2 (struct containing map[string]bool cannot be compared)
	fmt.Println(t1 == t2)

	type T1 struct {
		a func()
	}

	t3 := T1{
		a: func() {},
	}

	t4 := T1{
		a: func() {},
	}

	// invalid operation: t1 == t2 (struct containing func() cannot be compared)
	fmt.Println(t3 == t4)
}

关于引用类型,有几个比较特殊的规定:

  • 切片之间不允许比较。切片只能与nil值比较。
  • map之间不允许比较。map只能与nil值比较。
  • 函数之间不允许比较。函数只能与nil值比较。

参考链接:

Go语言基础之接口

4、interface

深入理解Go之==

posted @ 2023-08-05 13:29  画个一样的我  阅读(327)  评论(0编辑  收藏  举报