数据类型是语言的基础,对于强类型的语言来说,正确使用类型也是一个基本功,不可轻视。
一、Go 的类型有哪些?
Go语言内置以下这些基础类型:
- 布尔类型: bool。
- 整型: int8、 byte、 int16、 int、 uint、 uintptr等。
- 浮点类型: float32 、 float64。
- 复数类型: complex64、 complex128。
- 字符串: string。
- 字符类型: rune。
- 错误类型: error。
同时, Go语言也支持以下这些复合类型:
- 指针( pointer)
- 数组( array)
- 切片( slice)
- 字典( map)
- 通道( chan)
- 结构体( struct)
- 接口( interface)
二、怎么判断一个变量的类型(断言)?
很多语言都有自己判断变量类型的方式,大多数高级语言都是提供了对应的函数,例如 PHP 的 gettype() ,js 的 typeof, 或者java包装类型变量的 getClass().getTypeName(),然而,在 go 语言中,使用的是类型断言的方式。
语法形式是x.(T)。其中的x代表要被判断类型的值。这个值当下的类型必须是接口类型的。
我们举个例子:
package main
import "fmt"
func main() {
name := "mclink"
//s, ok := block.(string) 这个会报错,必须是接口类型才能使用断言
s1, ok1 := interface{}(name).(struct{})
s2, ok2 := interface{}(name).(string)
//s3 := interface{}(name).(int) 类型判断不对,会有panic
fmt.Println(s1, s2, ok1, ok2) // {} mclink false true
}
首先类型断言的 x 必须是一个接口类型,所以我们需要对非接口类型的变量进行强制类型转换,类型转换的语法就是 type(xxx), 对应这里的 interface{}(), 空接口类型可以容纳世间万物。转换成接口类型,我们再按照 x.(T) 的语法对其进行套入。其中,会有两个返回值。我们可以选择性的选择接收一个或者两个返回值,其中两个返回的结果在不同场景下均不同,有这样的规律:
我们分别把两个返回值标记为 r1, r2
- 当类型断言正确时,r1 为断言的原值,r2 为布尔值 true
- 当类型断言不正确时,r1 为断言类型(T)的零值,r2 为 false
- 当不接收 r2 时,类型断言不正确会有 panic 抛出
三、怎么转换类型?
go 语言是强类型语言,虽然有自动类型推断的功能,但是并不像PHP这种弱类型一样可以无视变量类型随便玩,请记住它是强类型语言。因此,在一些情况下,我们需要进行类型的转换,一般的转换方式是 T(x), 其中 x 可以是一个变量,也可以是一个代表值的字面量(比如1.23和struct{}{})
并且go 只支持强制类型转换,不支持隐式类型转换,例如
package main
import "fmt"
func main() {
var a int64 = 3
var b int32
//b = a //这样会报错
b = int32(a) // 这样可以
fmt.Printf("b 为 : %d", b)
}
如果从源类型到目标类型的转换是不合法的,那么就会引发一个编译错误,IDE 会自动识别这种错误,因此我们在此不关心这种问题,需要我们关心的是,语言层次允许的不规范转换问题。
- 对于整数类型值、整数常量之间的类型转换
不同整数类型能存储的数值范围是不同的,当从大范围整数转为小整数范围会有一些神奇的现象发生,例如
var srcNum int16 = -255
dstNum := int8(srcNum)
首先我们分析一下, 两者的存储范围分别是:
int8: -128 ~ 127
int16: -32768 ~ 32767
-255 一看就知道放不进 int8中,那 go 会怎么做呢?
要明白这个东西,首先你要知道,整数在 Go 语言以及计算机中都是以补码的形式存储的。这主要是为了简化计算机对整数的运算过程。正数的补码是它本身,负数的补码其实就是符号位不变,原码各位求反再加 1。
然后 -255 的原码是 1000000011111111, 补码为11111111 00000001(首先是算出反码为 11111111 00000000,然后加1得到 11111111 00000001),当 -255 要强制性存在不够存的 int8 里时,go 语言会把前面的八个多余的二进制数直接截掉,那么就剩下了 00000001 ,因为它的符号位(最高位)是0,说明是个正数,由于正数的补码是自己,所以算出它的原码也是自己,即 十进制的 1。
所以,当整数值的类型的有效范围由宽变窄时,只需在补码形式下截掉一定数量的高位二进制数即可。同理,当把一个浮点数类型的值转换为整数类型值时,前者的小数部分会被全部截掉。
四、别名类型和潜在类型
举个例子
type MyString = string // 1
type MyString2 string // 2
上面两个长得很像,却有着完全不同的含义。第一条声明语句表示 MyString 是 string 的别名类型,顾名思义,别名类型和源类型实际上是完全相同的,唯一的不同只有名称,就像你的名字和绰号,朋友叫的实际都是你。
在 go 语言中,内建的基本类型中就存在两个别名类型, 例如 byte 是 unit8 的别名类型, rune 是 int32 的别名类型。
而第二条声明语句却完全不同,它表示对原有类型的类型在定义,在 go 语言中 ,MyString2 和 string 属于不同的类型。对于这里的类型在定义来说, string 类型可以说是 MyString2 的潜在类型。潜在类型的意识就是说某个类型在本质上是哪个类型。潜在类型相同的不同类型的值之间可以进行类型转换。
例如这样:
package main
import "fmt"
type MyString string
func main() {
var test MyString = "test"
fmt.Println(string(test)) // 这样是允许的
}
但对于集合类的类型[]MyString2与[]string来说这样做却是不合法的,因为[]MyString2与[]string的潜在类型不同,分别是[]MyString2和[]string。另外,即使两个不同类型的潜在类型相同,它们的值之间也不能进行判等或比较,它们的变量之间也不能赋值。
例如:
package main
import "fmt"
type MyString string
func main() {
var testArr []MyString= make([]MyString,3)
//fmt.Println([]string(testArr)) // 这个会报错
}
对于上面的强制类型转换,IDE会报这样的错误:
Cannot convert expression of type ‘[]MyString’ to type ‘[]string’