写点什么

又一道比较运算符相关的面试题让我明白基础很重要

用户头像
Gopher指北
关注
发布于: 2020 年 11 月 09 日
又一道比较运算符相关的面试题让我明白基础很重要

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

比较运算不简单啊


我们先看一下上一期的投票结果:



首先,笔者自己选择了true,所以实际结果是41%的读者都选择了错误的答案。看到这个结果,笔者相信上一篇文章还是能够帮助到大家。


经过千辛万苦终于明白了上一道面试题是咋回事儿,这个时候却见面试官微微一笑道:“下面的输出结果是什么”。

type blankSt struct {	a int	_ string}bst1 := blankSt{1, "333"}bst2 := struct {	a int	_ string}{1, "555"}fmt.Println(bst1 == bst2)
复制代码

这里笔者先留个悬念,结果见后文。

类型声明

注意:本节不介绍语法等基础内容,主要描述一些名词以便于后文的理解。


类型声明将标识符(类型名称)绑定到类型,其两种形式为类型定义和类型别名。


下面我们通过一个例子对标识符类型defined type(后文会使用这个名词)进行解释:


// 类型别名type t1 = struct{ x, y float64 }// 类型定义type t2 struct{ x, y float64 }
复制代码


标识符(类型名称):在上面的例子中,t1,t2 为标识符。


类型struct{ x, y float64 }为类型。


类型别名不会创建新的类型。在上述例子中t1struct{ x, y float64 }是相同的类型。


类型定义会创建新的类型,且这个新类型又被叫做defined type。在上述例子中,新类型t2struct{ x, y float64 }是不同的类型。

underlying type


定理一

每一个类型 T 都有一个underlying type(笔者称之为原始类型,在后面文章中的原始类型均代表 underlying type)。


定理二:如果 T 是预定义的booleannumericstring类型之一,或者是类型字面量则 T 的原始类型是其本身,否则 T 的原始类型为 T 在其类型声明中引用的类型的原始类型。


Go 中的数组、结构体、指针、函数、interface{}、slice、map 和 channel 类型均由类型字面量构成。下面以 map 为例:


type T map[int]stringvar a map[int]string
复制代码


在上面的例子中,T 为 map 类型,a 为 map 类型的变量,类型字面量均为map[int]string且根据定理二可知 T 的原始类型为map[int]string


下面再看一个例子加深对原始类型的理解:


type (	A1 = string	A2 = A1)
type ( B1 string B2 B1 B3 []B1 B4 B3)
复制代码

上述例子中,stringA1A2B1B2的原始类型为string[]B1B3B4的原始类型为[]B1

类型相同

在 Go 中一个defined type类型总是和其他类型不同。类型相同情况如下:


1、两个数组长度相同且元素类型相同则这两个数组类型相同。


2、两个切片元素类型相同则这两个切片类型相同。


3、两个函数有相同数量的参数和相同数量的返回值,且对应位置的参数类型和返回值类型均相同则这两个函数类型相同。


4、如果两个指针具有相同的基本类型则这两个指针类型相同。


5、如果两个 map 具有相同类型的 key 和相同类型的元素则这两个 map 类型相同。


6、如果两个 channel 具有相同的元素类型且方向相同则这两个 channel 类型相同。


7、如果两个结构体具有相同数量的字段,且对应字段名称相同,类型相同并且标签相同则这两个结构体类型相同。对于不同包下面的结构体,只要包含未导出字段则这两个结构体类型不相同。


8、如果两个接口的方法数量和名称均相等,且相同名称的方法具有相同的函数类型则这两个接口类型相同。

类型可赋值


满足下列任意条件时,变量 x 能够赋值给类型为 T 的变量。


1、x 的类型和 T 类型相同。


2、x 的类型 V 和 T 具有相同的原始类型,并且 V 和 T 至少有一个不是defined type


type (	m1 map[int]string	m2 m1)var map1 map[int]string = make(map[int]string)var map2 m1 = map1fmt.Println(map2)
复制代码

map1 和 map2 变量的原始类型为map[int]string,且满足只有 map2 是defined type,所以能够正常赋值。


var map3 m2 = map1fmt.Println(map3)var map4 m2 = map2
复制代码

map3 和 map1 同样满足条件,所以能够正常赋值。但是 map4 和 map2 不满足至少有一个不是defined type这一条件,故会编译报错。


3、T 是 interface{} 并且 x 的类型实现了 T 的所有方法。


4、x 是双向通道,T 是通道类型,x 的类型 V 和 T 具有相同的元素类型,并且 V 和 T 中至少有一个不是defined type


根据上面我们可以知道一个隐藏逻辑是,双向通道能够赋值给单向通道,但是单向通道不能赋值给双向通道。



var c1 chan int = make(chan int)var c2 chan<- int = c1fmt.Println(c2 == c1) // truec1 = c2 // 编译错误:cannot use c2 (variable of type chan<- int) as chan int value in assignment
复制代码

因为 c1 能够正常赋值给 c2,所以根据前一篇文章的定理“在任何比较中,至少满足一个操作数能赋值给另一个操作数类型的变量”知 c1 和 c2 可比较。


5、x 是预声明标识符nil,T 是指针、函数、切片、map、channel 或 interface{}类型。


6、x 是可由类型 T 的值表示的无类型常量。


type (	str1 string	str2 str1)const s1 = "1111"var s3 str1 = s1var s4 str2 = s1fmt.Println(s3, s4) // 1111 1111
复制代码

上述例子中,s1 是无类型字符串常量故 s1 可以赋值给类型为 str1 和 str2 的变量。


下图是在 vscode 中当鼠标悬浮在变量 s1 上时给的提示。



注意:笔者在实际的验证过程中发现部分有类型的常量和变量在赋值时会编译报错。


const s2 string = "1111"var s5 str1 = s2
复制代码

上述代码在 vscode 中的错误为cannot use s2 (constant "1111" of type string) as str1 value in variable declaration


看到上述编译报错,笔者顿时惊了,就算不满足第 6 点也应该满足第 2 点呀。抱着满是疑惑的心情笔者利用代码跳转,最后在builtin.go发现了type string string这样一条语句。


结合上述代码我们知道str1string是由类型定义创建的新类型即defined type,所以var s5 str1 = s2也不满足第 2 点。


builtin.go文件对booleannumericstring的类型均做了类型定义,下面以int做近一步验证:


type int1 intvar i1 int = 1const i2 int = 1var i3 int1 = i1 // cannot use i1 (variable of type int) as int1 value in variable declarationvar i4 int1 = i2 // cannot use i2 (constant 1 of type int) as int1 value in variable declaration
复制代码


上述结果符合预期,因此我们在平时的开发中对于变量赋值的细节还需牢记于心。

分析总结


有了前面类型相同和*类型可赋值*两小节的基础知识我们按照下面步骤对本篇的面试题进行分析总结。


1、类型是否相同?


我们先列出面试题中需要比较的两个结构体:


type blankSt struct {	a int	_ string}struct {	a int	_ string}
复制代码


根据类型相同小节的第 7 点知,这两个结构体具有相同数量的字段,且对应字段名称相同、类型相同并且标签也相同,因此这两个结构体类型相同。


2、是否满足可赋值条件?


根据类型可赋值小节的第 1 点知,这两个结构体类型相同因此满足可赋值条件。


面试题中的两个结构体比较简单,下面笔者对结构体的不同场景进行补充。


  • 结构体 tag 不同


type blankSt1 struct {	a int `json:"a"`	_ string}bst11 := struct {	a int	_ string}{1, "555"}var bst12 blankSt1 = bst11
复制代码

上述代码在 vscode 中的报错为cannot use bst11 (variable of type struct{a int; _ string}) as blankSt1 value in variable declaration。两个结构体只要 tag 不同则这两个结构体类型不同,此时这两个结构体不满足任意可赋值条件。


  • 结构体在不同包,且所有字段均导出

package ttt
type ST1 = struct { F string}var A = ST1{ F: "1111",}
package main
type st1 struct { F string}
var st11 st1 = ttt.Afmt.Println(st11) // output: {1111}
复制代码

根据类型相同小节的第 7 点和类型可赋值小节的第 1 点知,ST1 和 st1 类型相同且可赋值,因此上述代码能够正常运行


  • 结构体在不同包,且包含未导出字段


package ttttype ST2 = struct {	F string	a string}var B = ST2{	F: "1111",}package maintype st2 struct {	F string	a string}var st21 st2 = ttt.Bfmt.Println(st21)
复制代码


运行上述代码时出现cannot use ttt.B (type struct { F string; ttt.a string }) as type st2 in assignment错误。


由于 st2 和 ST2 类型不同且他们的原始类型分别为struct { F string a string } struct { F string; ttt.a string },所以 ttt.b 无法赋值给 st21。


3、总结


blankStstruct { a int _ string }类型相同且满足可赋值条件,因此根据“在任何比较中,至少满足一个操作数能赋值给另一个操作数类型的变量”这一定理知面试题中的bst1bst2可比较。


接下来根据上一篇文章提到的结构体比较规则知bst1bst2相等,所以面试题最终输出结果为true


如果不是再去研读一篇 Go 的基础语法,笔者还不知道曾经遗漏了这么多细节。“读书百遍其义自见”,古人诚不欺我!


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

注:

1. 写本文时, 笔者所用 go 版本为: go1.14.2

2. 文章中所用完整例子:https://github.com/Isites/go-coder/blob/master/types/main.go


发布于: 2020 年 11 月 09 日阅读数: 31
用户头像

Gopher指北

关注

还未添加个人签名 2020.09.15 加入

欢迎关注公众号:Gopher指北

评论

发布
暂无评论
又一道比较运算符相关的面试题让我明白基础很重要