来自公众号:新世界杂货铺
比较运算不简单啊
我们先看一下上一期的投票结果:
首先,笔者自己选择了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 }
为类型。
类型别名不会创建新的类型。在上述例子中t1
和struct{ x, y float64 }
是相同的类型。
类型定义会创建新的类型,且这个新类型又被叫做defined type
。在上述例子中,新类型t2
和struct{ x, y float64 }
是不同的类型。
underlying type
定理一:
每一个类型T都有一个underlying type
(笔者称之为原始类型,在后面文章中的原始类型均代表underlying type)。
定理二:如果T是预定义的boolean
、numeric
和string
类型之一,或者是类型字面量则T的原始类型是其本身,否则T的原始类型为T在其类型声明中引用的类型的原始类型。
Go中的数组、结构体、指针、函数、interface{}、slice、map和channel类型均由类型字面量构成。下面以map为例:
type T map[int]string
var 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
)
上述例子中,string
、A1
、 A2
、B1
和B2
的原始类型为string
、[]B1
、B3
和B4
的原始类型为[]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 = map1
fmt.Println(map2)
map1和map2变量的原始类型为map[int]string
,且满足只有map2是defined type
,所以能够正常赋值。
var map3 m2 = map1
fmt.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 = c1
fmt.Println(c2 == c1) // true
c1 = 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 = s1
var s4 str2 = s1
fmt.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
这样一条语句。
结合上述代码我们知道str1
和string
是由类型定义创建的新类型即defined type
,所以var s5 str1 = s2
也不满足第2点。
builtin.go
文件对boolean
、numeric
和string
的类型均做了类型定义,下面以int
做近一步验证:
type int1 int
var i1 int = 1
const i2 int = 1
var i3 int1 = i1 // cannot use i1 (variable of type int) as int1 value in variable declaration
var 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.A
fmt.Println(st11) // output: {1111}
根据类型相同小节的第7点和类型可赋值小节的第1点知,ST1和st1类型相同且可赋值,因此上述代码能够正常运行
- 结构体在不同包,且包含未导出字段
package ttt
type ST2 = struct {
F string
a string
}
var B = ST2{
F: "1111",
}
package main
type st2 struct {
F string
a string
}
var st21 st2 = ttt.B
fmt.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、总结
blankSt
和struct { a int _ string }
类型相同且满足可赋值条件,因此根据“在任何比较中,至少满足一个操作数能赋值给另一个操作数类型的变量”这一定理知面试题中的bst1
和bst2
可比较。
接下来根据上一篇文章提到的结构体比较规则知bst1
和bst2
相等,所以面试题最终输出结果为true
。
如果不是再去研读一篇Go的基础语法,笔者还不知道曾经遗漏了这么多细节。“读书百遍其义自见”,古人诚不欺我!
最后,衷心希望本文能够对各位读者有一定的帮助。
注:
- 写本文时, 笔者所用go版本为: go1.14.2
- 文章中所用完整例子:https://github.com/Isites/go-...