写这篇文章的时候,已经离我找工作有一段时间了,但是觉得这道题不管是面试还是日常的工作中,都会经常遇到,所以还是特意写一篇文章,记录下自己对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 是一种哈希表的实现,它包含了键值对的集合。

在 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} fmt.Println(reflect.DeepEqual(map1, map2)) // 输出: false (reflect.DeepEqual 可以进行值相等判断)
fmt.Println(mapsAreEqual(map1, map2)) // 输出: true fmt.Println(mapsAreEqual(map1, map3)) // 输出: false
}

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

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之==

从一道面试题来谈谈Golang中的 ==的更多相关文章

  1. 一道笔试题来理顺Java中的值传递和引用传递

      题目如下: private static void change(StringBuffer str11, StringBuffer str12) { str12 = str11; str11 = ...

  2. 谈谈Golang中goroutine的调度问题

    goroutine的调度问题,同样也是我之前面试的问题,不过这个问题我当时并不是很清楚,回来以后立马查阅资料,现整理出来备忘. 有一些预备知识需要说明,就是操作系统中的线程.操作系统中的线程分为两种: ...

  3. 通过一道笔试题浅谈javascript中的promise对象

    因为前几天做了一个promise对象捕获错误的面试题目,所以这几天又重温了一下promise对象.现在借这道题来分享下一些很基础的知识点. 下面是一个面试题目,三个promise对象捕获错误的例子,返 ...

  4. Java中有关构造函数的一道笔试题解析

    Java中有关构造函数的一道笔试题解析 1.详细题目例如以下 下列说法正确的有() A. class中的constructor不可省略 B. constructor必须与class同名,但方法不能与c ...

  5. 一道笔试题和UML思想 ~

    一句软件工程界的名言,让我想起了一个和一道笔试题有关的故事.希望更多的人了解 UML 背后的思想比他的语法更重要,是笔者写作本文的一点小愿望. 一.从一句软件工程名言说起 对很多事情的处理上,东西方都 ...

  6. PHP递归创建多级目录(一道面试题的解题过程)

    今天看到一道面试题,要写出一个可以创建多级目录的函数: 我的第一个感觉就是用递归创建,具体思路如下: function Directory($dir){ if(is_dir($dir) || @mkd ...

  7. 关于Java类加载双亲委派机制的思考(附一道面试题)

    预定义类加载器和双亲委派机制 JVM预定义的三种类型类加载器: 启动(Bootstrap)类加载器:是用本地代码实现的类装入器,它负责将 <Java_Runtime_Home>/lib下面 ...

  8. 一道面试题比较synchronized和读写锁

    一.科普定义 这篇博文的两个主角“synchronized”和“读写锁” 1)synchronized 这个同步关键字相信大家都用得比较多,在上一篇“多个线程之间共享数据的方式”中也详细列举他的应用, ...

  9. <转>一道面试题比较synchronized和读写锁

    一.科普定义(原文:http://903497571.iteye.com/blog/1874752) 这篇博文的两个主角“synchronized”和“读写锁” 1)synchronized 这个同步 ...

  10. 一道面试题与Java位操作 和 BitSet 库的使用

    前一段时间在网上看到这样一道面试题: 有个老的手机短信程序,由于当时的手机CPU,内存都很烂.所以这个短信程序只能记住256条短信,多了就删了. 每个短信有个唯一的ID,在0到255之间.当然用户可能 ...

随机推荐

  1. Git&GitHub简介与入手(三)

    五.eclipse中的git使用 1.eclipse中本地库操作 (检测显示创建成功): (设置本地库范围签名): (一些git里的图标,保存着不同内容): https://github.com/gi ...

  2. 2022-06-13:golang中,[]byte和结构体如何相互转换?

    2022-06-13:golang中,[]byte和结构体如何相互转换? 答案2022-06-13: []byte和结构体的转换的应用场景是数据解析. 代码里有两种方法,一种是内存不共用,另一种是内存 ...

  3. IBM小型机 - 登录Web控制台

    前言: IBM 小型机没有VGA或者HDMI接口,只能通过web或者串口的方式,配置和查看设备的硬件信息: 我们可以通过两种方式获取小型机的IP,并通过浏览器访问. 操作步骤: 1.服务器接通电源,直 ...

  4. annotate()使用聚合计数、求和、平均数 raw()执行原生的SQL

    annotate()使用聚合计数.求和.平均数  raw()执行原生的SQL # 按老师分组,求课程的销量 Course.objects.values('Teacher').annotate(vol= ...

  5. HINT: Add or change a related_name argument to the definition for 'usersApp.

    错误原因是你的项目使用的不是Django自带的用户表,采用的自定义的用户表,这个时候需要在settings.py里面进行指定. AUTH_USER_MODEL = 'usersApp.UserProf ...

  6. postman接口关联1

    1.接口关联 在接口测试中,经常会遇到后一接口的请求数据需要用到前一接口响应数据 关联接口:简单描述就是一个接口的返回结果作为另一个接口入参,其中最典型的就是需要登录token验证的接口,需要先调用登 ...

  7. GPT虚拟直播Demo系列(二)|无人直播间实现虚拟人回复粉丝

    摘要 虚拟人和数字人是人工智能技术在现实生活中的具体应用,它们可以为人们的生活和工作带来便利和创新.在直播间场景里,虚拟人和数字人可用于直播主播.智能客服.营销推广等.接入GPT的虚拟人像是加了超强b ...

  8. 【python基础】循环语句-break关键字

    1.break关键字 break关键字,其作用是在循环中的代码块遇到此关键字,立刻跳出整个循环,执行循环外的下一条语句. 其在while和for循环中的作用示意图如下: 1.1break在while循 ...

  9. 让AI支持游戏AI模型:从经典AI算法到最新技术的应用

    目录 20. 让 AI 支持游戏AI模型:从经典 AI 算法到最新技术的应用 1. 引言 2. 技术原理及概念 2.1 基本概念解释 2.2 技术原理介绍 2.2.2 最新技术介绍 3. 实现步骤与流 ...

  10. 将onnx的静态batch改为动态batch及修改输入输出层的名称

    目录 背景 操作 修改输入输出层 修改输入输出层名称 完整代码 背景 在模型的部署中,为了高效利用硬件算力,常常会需要将多个输入组成一个batch同时输入网络进行推理,这个batch的大小根据系统的负 ...