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

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

比较运算不简单啊

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

image

首先,笔者自己选择了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]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
)

上述例子中,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 = 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上时给的提示。

image

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

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 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、总结

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
禁止转载,如需转载请通过简信或评论联系作者。

推荐阅读更多精彩内容

  • 第五十章 未知 黑暗中,一道光束闪过,广袤无垠的黑暗迅速将余光吞噬,光束消失在尽头,只有瞬间的光明,这里...
    余间上古阅读 108评论 0 10
  • 我国诗歌史上有一种特异的诗歌形式集句诗,就如同天上七种色彩构成的彩虹;如地上由多种丝线织就的锦缎。集句诗就是选取前...
    听雨到清晨阅读 426评论 6 25
  • 一、变量: 变量类型 变量名 = 变量值; 开头:字母,$,_。 首字母之后字母,$ ,_ ,数字组合。 不能使用...
    雪国_3472阅读 34评论 0 2
  • 职场中的人员大体可以分为三类,靠能力赚钱、靠忠诚赚钱、靠服务赚钱,企业领导对三类人员的核心诉求不一样,因此他们的前...
    职业规划高老师阅读 242评论 2 15
  • 霜降的日子,秋有些灿烂了。清晨到晌午天很蓝,和绚的秋风懒洋洋的召唤着秋叶,这个季节天空里每每洒落的是秋叶,不...
    冉彩漫画诗阅读 348评论 4 24