47

一道比较运算符相关的面试题把我虐的体无完肤

 3 years ago
source link: https://studygolang.com/articles/30918
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

来自公#众#号:新世界杂货铺

杂(货铺)谈

今天这篇文章相对来说比较基础,大家花几分钟时间看看,有所收获自然是最好,没有收获也就消磨几分钟时间罢了,你不亏,笔者也不亏~

前几期还是有一定难度的HTTP系列文章,今天却是画风突变讲起了基础,这当然是因为基础重要呀。正所谓万丈高楼平地起,我们夯实基础,楼才能建的高,毕竟精美的小矮楼总是很容易被高楼遮挡。嗨,扯远了,总之笔者今天写这篇文章绝对不是下面这个原因:

累呀!真的累!工作嘛?当然不是!前几期分析HTTP系列文章确实耗费了太多精力,周末连续熬夜就算是铁打的人也扛不住,所以本周只有养精蓄锐这一个目的。

杂(货铺)言

Go中的比较操作符,你真的了解吗?假如面试官问你下面输出什么,你的答案是什么?

type blankSt struct {
  a int
  _ string
}
var (
  bst1 = blankSt{1, "333"}
  bst2 = blankSt{1, "44444"}
)
fmt.Println(bst1 == bst2)

今天,笔者总结了一份比较运算符的相关文档,助力读者夯实基础(上述答案请参考后文)。

基本定理

在Go中,比较运算符也是遵循定理的, 两条基本定理如下:

定理一:相等运算符 ==!= 适用于具有可比性的操作数,排序运算符 <<=>>= 适用于可排序的操作数。

定理二:在任何比较中,至少满足一个操作数能赋值给另一个操作数类型的变量。

常见类型的比较

常见类型的比较大家都懂,在这里笔者就不详细介绍了,仅枚举一下原则加深大家的印象:

  • 布尔值是可比较的,但不可排序,即仅适用于 ==!= 运算符。
  • 整数和浮点数是可比较的且可排序,适用于所有比较运算符。
  • 字符串的值是可比较的,且按字节排序,即比较时按字节比较大小(不理解字符串和字节切片关系的,请参考 深入理解go中字符串
    这篇文章)。

以上即为常见类型的比较原则,下面我们结合例子逐步理解各种类型之间的比较。

不可比较的类型

在Go中, 切片map ,和 func 是不可比较的,他们仅可以和预声明表示符 nil 进行比较。能和nil进行比较的还有 指针管道interface{}

复数之间的比较

复数可比较但不可排序,实部和虚部均相等两个复数才相等。复数仅适用 ==!= 这两个比较运算符:

var c1 complex128 = complex(1, 2) // 1+2i
var c2 complex128 = complex(3, 4) // 3+4i
fmt.Println(c1 == c2)

上述输出结果为 false ,证明复数之间可比较。

var c1 complex128 = complex(1, 2) // 1+2i
var c2 complex128 = complex(3, 4) // 3+4i
fmt.Println(c1 >= c2)

此时程序无法运行,在vscode中的错误提醒为 cannot compare c1 >= c2 (operator >= not defined for complex128)compiler 。由此确认复数不可排序。

结构体之间的比较

如果结构体的所有字段都是可比较的,则他们所有非匿名字段的值相等,结构体的值才相等。验证如下:

type canC struct {
    c int
}
var st1, st2 canC
fmt.Println(st1 == st2)
st1.c = 3
fmt.Println(st1 == st2)

上述输出分别为 truefalse 。由此验证非匿名字段的值相等,结构体的值才相等。

fmt.Println(st1 <= st2)

此行代码在vscode中的错误提醒为 cannot compare st1 <= st2 (operator <= not defined for canC)compiler 。由此可知,即使结构体满足比较条件也无法适用于 <<=>>= 运算符。

注:后文中提到不可排序均代表着无法适用于 <<=>>= 运算符,在后文中不再对不可排序给出例子。

下面看看包含匿名字段的结构体比较时有什么不同:

type blankSt struct {
  a int
  _ string
}
var (
  bst1 = blankSt{1, "333"}
  bst2 = blankSt{1, "44444"}
)
fmt.Println(bst1 == bst2)

上述输出为 true ,符合非匿名字段的值相等时结构体的值相等这一原则。注意, 如果匿名字段是不可比较的类型时,上述代码会编译报错

最后我们看看包含不可比较类型的结构体:

type canNotC struct {
    m func() int
}
fmt.Println(canNotC{} == canNotC{})

上述代码在vscode中的错误提醒为 cannot compare (canNotC literal) == (canNotC literal) (operator == not defined for canNotC)compiler ,由此可知,结构体如果要可比较,则其内部的所有字段必须全为可比较类型。

指针之间的比较

指针是可比较的,但不可排序。如果两个指针指向同一个变量,或者两个指针值均为nil,则这两个指针相等。相信读者对于这一点应该是没有异议的,但是有一个情况却是十分需要注意的。

zero-size variables:如果结构体没有任何字段或者数组没有任何元素,则其大小为0,即 unsafe.Sizeof 的计算结果为0。两个不同的 zero-size variables 在内存中可能具有相同的地址。

指向不同zero-size variables的两个指针可能相等也可能不相等。

var arr1, arr2 [0]int
parr1 := &arr1
parr2 := &arr2
fmt.Println(unsafe.Sizeof(arr1))
fmt.Println(parr1 == parr2)
fmt.Println(uintptr(unsafe.Pointer(parr1)), uintptr(unsafe.Pointer(parr2)))
// 输出如下:
0
false
824634830552 824634830552 // 每次运行输出的地址不一定相同

笔者多次运行, parr1 == parr2 始终输出为 false ,目前尚未发现输出为 true 的情况,在 https://github.com/golang/go/issues/23440 也有人遇到同笔者相同的情况,所以笔者就不再对此问题做更近一步的分析。

Channel之间的比较

在写这篇文章前,笔者从来都没有想过Channel之间是可以比较的。事实上,管道是可比较类型,golang原文如下:

Channel values are comparable. 
Two channel values are equal if they were created by the same call to make or if both have value nil

这里需要注意的是,只有相同调用的管道才是相等的:

var cint1, cint2 chan<- string
cint3 := make(chan string, 2)
cint4 := make(chan string, 2)
cint5 := make(chan string)
fmt.Println(cint1 == cint2, cint3 == cint4, cint5 == cint1) // true false false
cint1 = cint4
fmt.Println(cint1 == cint4) //true

上述中, cint1cint2 初始值均为nil,所以输出 true 。双向通道 cint4 赋值给单向通道 cint1 时,满足相同的 make 调用这一条件,所以输出也为 true

Interface{}之间比较

Interface{}是可比较的,但是不可排序。两个Interface{}变量的动态类型和动态value均一致它们才相等,两个变量均为nil也是相等的。针对这一原则笔者对其分以下几种情况讨论。

一、interface{}不为nil,且动态类型均为可比较类型时:

var (
  i1 interface{} = uint(1)
  i2 interface{} = uint(1)
  i3 interface{} = uint(3)
  i4 interface{} = int(3)
  i5 interface{} = []int{}
  i6 interface{} = map[int]string{}
  i7 interface{} = map[int]string{}
)
fmt.Println(i1 == i2, i1 == i3,i3 == i4)

上述输出结果为 true false false ,这符合 动态类型和动态value均一致时才相等 的原则。

二、如果比较双方动态类型一致且为不可比较类型时会panic:

这种情况可正常编译,但是会造成运行时崩溃,所以一定要注意!!!

fmt.Println(i5 == i6)
fmt.Println(i7 == i6)

上述比较 i5i6 时能够正常输出,但是 i6i7 比较时出现如下错误:

raAVzi.jpg!mobile

image

所以,笔者在这里再次强调,如果项目中有不小心直接使用了 interface{} 进行比较的,请一定要注意️。

三、动态value为nil的interface{}不一定等于nil:

func t() interface{} {
    var err *error
    return err
}

func t1() interface{} {
    return nil
}
fmt.Println(t() == nil, t1() == nil) // 输出false, true

由上述代码知,如果不是直接返回 nilinterface{}nil 进行比较时是不相等的。相信很多人在平时的开发中都有可能会忽略这个问题。下面我们对它为什么不相等进行简单的分析。

在Go中, interface{} 的实现为两个元素,类型 t 和值 v 。v是一个具体的值,如int、struct、或指针等。

如果,我们在接口中存储int值3,则接口中的值为(t=int,v=3)。

值v也称为接口的动态值,因为在程序执行期间,给定的接口变量可能包含不同的值v(以及相应的类型t)。

只有当v和t都未设置时,接口值才是nil(t=nil,未设置v)。

如果,我们在接口中存储一个类型为 int的nil指针, 那么不管指针的值是什么,内部类型都会是 int:(t=*int,v=nil)。因此,即使内部的指针v为nil,这样的接口值也是非nil的。

本部分内容翻译整理自 https://golang.org/doc/faq#nil_error

非接口类型X实现了接口T,则X的变量x能和T的变量t进行比较

原则:非接口类型X实现了接口T,则X的变量x能和T的变量t进行比较。只有当t的动态变量类型为X且t的动态value和x相等时,t才等于x。

推论:又因为go中任意类型都默认实现了 interface{} ,则意味着 interface{} 变量能和任意的非 interface{} 类型的可比较类型进行比较。

验证原则:

type it interface {
    f()
}
type ix1 int
func (x ix1) f() {}
type ix2 map[int]int
func (x ix2) f() {}

x1 := ix1(2)
var t1 it = ix2{}
// 类型不一致时
fmt.Println(x1 == t1) // fasle
// 类型一致时
t1 = ix1(2)
fmt.Println(t1 == x1) // true

上面的输出分别为 falsetrue ,符合原则。

验证推论:

var it1 interface{} = "111"
fmt.Println(it1 == 1)

上面能够正常比较,说明推论正确,且输出为 false ,符合原则。

注意:下面情况会panic

var t2 it = ix2{}
var t3 it = ix2{}
fmt.Println(t2 == t3)

上述代码发生panic,符和 Interface{}之间比较 的原则。

数组之间的比较

两个数组的元素类型相同且是可比较类型,并且数组的长度相同,则这两个数组可比较。当两个可比较的数组对应元素均相等时,则这两个数组相等。即使两个数组可比较,但依旧不可排序。

类型相同但元素不可比较时:

var array1 [3][]int
var array2 [3][]int
fmt.Println(array1 == array2)

上述代码在vscode中的错误为 cannot compare array1 == array2 (operator == not defined for [3][]int)compiler ,所以如果数组元素为不可比较类型,则数组也不可比较。

数组元素可比较但数组长度不一致时:

var array3 [3]int
var array4 [2]int
fmt.Println(array3 == array4)

上述代码在vscode中的错误为 cannot compare array3 == array4 (mismatched types [3]int and [2]int)compiler ,所以如果数组长度不一致时,则两个数组不可比较。

满足数组长度相等且元素类型可比较时:

var array5, array6 [3]int
fmt.Println(array5 == array6)
array5 = [...]int{3, 2, 1}
array6 = [...]int{1, 2, 3}
fmt.Println(array5 == array6)

上述输出分别为 truefalse ,符合可比较数组一一判断对应元素是否相等这一原则。所以,我们平时在开发中可以利用该原则快速比较数组是否相等。

最后,衷心希望本文能够对各位读者有一定的帮助。

注:

  1. 写本文时, 笔者所用go版本为: go1.14.2
  2. 文章中所用完整例子: https://github.com/Isites/go-coder/blob/master/comparison-operators/main.go

参考:

https://golang.org/ref/spec#Comparison_operators

有疑问加站长微信联系

iiUfA3j.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK