怎样判断一个变量的类型?
package main
import "fmt"
var container = []string{"zero", "one", "two"}
func main() {
container := map[int]string{0: "zero", 1: "one", 2: "two"}
fmt.Printf("The element is %q.\n", container[1])
}
那么,怎样在打印其中元素之前,正确判断变量container的类型?
典型回答
使用类型断言
value, ok := interface{}(container).([]string)
这里有一条赋值语句。在赋值符号的右边,是一个类型断言表达式。
它包括了用来把container
变量的值转换为空接口值的interface{}(container)
。
以及一个用于判断前者的类型是否为切片类型[]string
的.([]string)
。
这个表达式的结果可以被赋给两个变量,在这里由value和ok代表。变量ok是布尔(bool)类型的,它将代表类型判断的结果,true和false。
如果是true,那么被判断的值将会被自动转换为[]string
类型的值,并赋给变量value,否则value将被赋予nil(即“空”)。
顺便提一下,这里的ok也可以没有。也就是说,类型断言表达式的结果,可以只被赋给一个变量,在这里是value。
问题解析
类型断言表达式的语法形式是x.(T)
。其中的x代表要被判断类型的值。这个值当下的类型必须是接口类型的,不过具体是哪个接口类型其实是无所谓的。
所以,当这里container
变量类型不是任何的接口类型时,我们就需要先把它转成某个接口类型的值。
在Go语言中,interface{}
代表空接口,任何类型都是它的实现类型。
你可能会对这里的{}产生疑惑,为什么在关键字interface的右边还要加上这个东西?
请记住,一对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含任何内容的数据结构(或者说数据类型)。
我们向答案的最右边看。圆括号中[]string是一个类型字面量。所谓类型字面量,就是用来表示数据类型本身的若干个字符。
比如,string
是表示字符串类型的字面量,uint8
是表示 8 位无符号整数类型的字面量。再复杂一些的就是我们刚才提到的[]string
,用来表示元素类型为string
的切片类型,以及map[int]string
,用来表示键类型为int
、值类型为string
的字典类型。
类型转换规则中有哪些值得注意的地方?
类型转换表达式已经在前面展示过,它的语法形式是T(x)。
其中的x可以是一个变量,也可以是一个代表值的字面量(比如1.23和struct{}),还可以是一个表达式。
注意:如果是表达式,那么该表达式的结果只能是一个值,而不能是多个值。在这个上下文中,x可以被叫做源值,它的类型就是源类型,而那个T代表的类型就是目标类型。
类型转化有三个很常用并且非常值得注意的知识点:
一、对于整数类型值、整数常量之间的类型转换,原则上只要源值在目标类型的可表示范围内就是合法的。
比如,之所以uint8(255)
可以把无类型的常量255转换成uint8
类型的值,是因为255在[0,255]的范围内。
但需要特别注意的是,源整数类型的可表示范围较大,而目标类型的可表示范围较小的情况,比如把值的类型从int16
转换为int8
。请看下面这段代码:
var srcInt = int16(-255)
dstInt := int8(srcInt)
变量srcInt的值是int16类型的-255,而变量dstInt的值是由前者转换而来的,类型是int8。int16类型的可表示范围可比int8类型大了不少。问题是,dstInt的值是多少?
首先你要知道,整数在 Go 语言以及计算机中都是以补码的形式存储的
。这主要是为了简化计算机对整数的运算过程。补码其实就是原码各位求反再加 1。
比如,int16类型的值-255的补码是1111111100000001
。如果我们把该值转换为int8类型的值,那么 Go 语言会把在较高位置(或者说最左边位置)上的 8 位二进制数直接截掉,从而得到00000001
。
又由于其最左边一位是0,表示它是个正整数,以及正整数的补码就等于其原码,所以dstInt的值就是1。
一定要记住,当整数值的类型的有效范围由宽变窄时,只需在补码形式下截掉一定数量的高位二进制数即可
。
类似的快刀斩乱麻规则还有:当把一个浮点数类型的值转换为整数类型值时,前者的小数部分会被全部截掉。
二、虽然直接把一个整数值转换为一个string类型的值是可行的,但值得关注的是,被转换的整数值应该可以代表一个有效的Unicode代码点,否则转换的结果将会是"�"(仅由高亮的问号组成的字符串值)。
字符"�"的Unicode代码点是U+FFFD。它是Unicode标准
中定义的Replacement Character
,专用于替换那些未知的、不被认可的以及无法展示的字符。
面试肯定不会去问"哪个整数值转换后会得到哪个字符串",但是可能会写下:
string(-1)
并询问会得到什么?这可是完全不同的问题啊。由于-1肯定无法代表一个有效的Unicode代码点,所以得到的总会是"�"。在实际工作中,我们在排查问题时可能会遇到"�",我们要知道这可能是由于什么引起的。
三、关于string类型与各种切片类型之间的互转的。
一个值在从string类型向[]byte类型转换时代表着以 UTF-8 编码的字符串会被拆分成零散、独立的字节。
除了与ASCII编码兼容的那部分字符集,以 UTF-8 编码的某个单一字节是无法代表一个字符的。
string([]byte{'\xe4', '\xbd', '\xa0', '\xe5', '\xa5', '\xbd'}) // 你好
比如,UTF-8 编码的三个字节\xe4、\xbd和\xa0合在一起才能代表字符’你’,而\xe5、\xa5和\xbd合在一起才能代表字符’好’。
其次,一个值在从string类型向[]rune类型转换时代表着字符串会被拆分成一个个Unicode 字符。
什么是别名类型?什么是潜在类型?
我们可以用关键字type声明自定义的各种类型。当然了,这些类型必须在 Go 语言基本类型和高级类型的范畴之内。在它们当中,有一种被叫做“别名类型”的类型。我们可以像下面这样声明它:
type MyString = string
这条声明语句表示,MyString是string类型的别名类型。顾名思义,别名类型与其源类型的区别恐怕只是在名称上,它们是完全相同的。
源类型与别名类型是一对概念,是两个对立的称呼。别名类型主要是为了代码重构而存在的。
Go 语言内建的基本类型中就存在两个别名类型。byte是uint8的别名类型,而rune是int32的别名类型。
注意,如果我这样声明:
type Mystring2 string //注意,这里没有等号。
MyString2和string就是两个不同的类型了。这里的MyString2是一个新的类型,不同于其他任何类型。
这种方式也可以被叫做对类型的再定义。我们刚刚把string类型再定义成了另外一个类型MyString2。
对于这里的类型再定义来说,string可以被称为MyString2的潜在类型。潜在类型的含义是,某个类型在本质上是哪个类型。
潜在类型相同的不同类型的值之间是可以进行类型转换的
。因此,MyString2类型的值与string类型的值可以使用类型转换表达式进行互转。
但对于集合类的类型[]MyString2与[]string来说这样做却是不合法的,因为[]MyString2与[]string的潜在类型不同,分别是[]MyString2和[]string。另外,即使两个不同类型的潜在类型相同,它们的值之间也不能进行判等或比较,它们的变量之间也不能赋值。
文章来自郝林老师的《Go语言36讲》