类型

寒江蓑笠翁大约 13 分钟

类型

在之前的数据类型的小节中已经简单了介绍过了Go中的所有内置的数据类型,这些内置的基础类型,是后续自定义类型的基础。Go是一个典型的静态类型语言,所有变量的类型都会在编译期确定好,并且在整个程序的生命周期都不会再改变,这一小节会简单的介绍下Go的类型系统和基本使用。


静态强类型

Go是一个静态强类型语言,静态指的是Go所有变量的类型早在编译期间就已经确定了,在程序的生命周期都不会再发生改变,尽管Go中的短变量声明有点类似动态语言的写法,但其变量类型是由编译器自行推断的,最根本的区别在于它的类型一旦推断出来后不会再发生变化,动态语言则完全相反。所以下面的代码完全无法通过编译,因为a是int类型的变量,不能赋值字符串。

func main() {
	var a int = 64
	a = "64"
	fmt.Println(a) // cannot use "64" (untyped string constant) as int value in assignment
}

强类型则指的是在程序中执行严格的类型检查,如果出现类型不匹配的情况时,会立即告诉程序员不应该这么做,而不是像动态语言一样去尝试推断可能的结果。所以下面的代码无法通过编译,因为两者类型不同,无法进行运算。

func main() {
	fmt.Println(1 + "1") // invalid operation: 1 + "1" (mismatched types untyped int and untyped string)
}

类型后置

Go为什么要把类型声明放在后面而不是前面,很大程度上是从C语言吸取了教训,拿官方的一个例子展示效果,这是一个函数指针

int (*(*fp)(int (*)(int, int), int))(int, int)

说实话不认真看很难知道这是一个什么类型,在Go中类似的写法如下

f func(func(int,int) int, int) func(int, int) int

Go的声明方式始终遵循名字在前面,类型在后面的原则,从左往右读,大概第一眼就可以知道这是一个函数,且返回值为func(int,int) int。当类型变得越来越复杂时,类型后置在可读性上要好得多,Go在许多层面的设计都是为了可读性而服务的。


类型声明

在Go中通过类型声明,可以声明一个自定义名称的新类型,声明一个新类型通常需要一个类型名称以及一个基础类型,简单的例子如下:

type MyInt int64

在上述类型声明中,通过type关键字声明了一个基础类型为int64名为MyInt的类型。在Go中,每一个新声明的类型都必须有一个与之对应的基础类型,且类型名称不建议与已有的内置标识符重复。

type MyInt int64

type MyFloat64 float64

type MyMap map[string]int

// 可以通过编译,但是不建议使用,这会覆盖原有的类型
type int int64

通过类型声明的类型都是新类型,不同的类型无法进行运算,即便基础类型是相同的。

type MyFloat64 float64

var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(f1 + f)
invalid operation: f1 + f (mismatched types MyFloat64 and float64)

类型别名

类型别名与类型声明则不同,类型别名仅仅只是一个别名,并不是创建了一个新的类型,简单的例子如下:

type Int = int

两者是都是同一个类型,仅仅叫的名字不同,所以也就可以进行运算,所以下例自然也就可以通过编译。

type Int = int
var a Int = 1
var b int = 2
fmt.Println(a + b)
3

类型别名对于一些特别复杂的类型有很大的用处,例如现在有一个类型map[string]map[string]int,这是一个二维map,现有一个函数参数是map[string]map[string]int类型,如下

func PrintMyMap(mymap map[string]map[string]int) {
   fmt.Println(mymap)
}

这种情况下,就没有必要使用类型声明了,因为前者是声明了一个新的类型,无法作为该函数的参数,使用类型别名后的例子如下

type TwoDMap = map[string]map[string]int

func PrintMyMap(mymap TwoDMap) {
   fmt.Println(mymap)
}

使用类型别名后看起来会简洁一些。

提示

内置类型any就是interface{}的类型别名,两者完全等价,仅仅叫法不一样。


类型转换

在Go中,只存在显式的类型转换,不存在隐式类型转换,因此不同类型的变量无法进行运算,无法作为参数传递。类型转换适用的前提是知晓被转换变量的类型和要转换成的目标类型,例子如下:

type MyFloat64 float64

var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(float64(f1) + f)
0.30000000000000004

通过显式的将MyFloat64 转换为float64类型,才能进行加法运算。类型转换的另一个前提是:被转换类型必须是可以被目标类型代表的(Representability),例如int可以被int64类型所代表,也可以被float64类型代表,所以它们之间可以进行显式的类型转换,但是int类型无法被stringbool类型代表,因为也就无法进行类型转换。

提示

关于代表(Representabilitsy)的定义可以前往参考手册 - Representabilityopen in new window以了解更多细节。

即便两个类型可以相互代表,类型转换的结果也不总是正确的,看下面的一个例子:

var num1 int8 = 1
var num2 int32 = 512
fmt.Println(int32(num1), int8(num2))
1 0

num1被正确的转换为了int32类型,但是num2并没有。这是一个典型的数值溢出问题,int32能够表示31位整数,int8只能表示7位整数,高精度整数在向低精度整数转换时会抛弃高位保留低位,因此num1的转换结果就是0。在数字的类型转换中,通常建议小转大,而不建议大转小。


在使用类型转换时,对于一些类型需要避免歧义,例子如下:

*Point(p) // 等价于 *(Point(p))
(*Point)(p)  // 将p转换为类型 *Point
<-chan int(c)    // 等价于 <-(chan int(c))
(<-chan int)(c)  // 将c转换为类型  <-chan int
(func())(x)      // 将x转换为类型 func()
(func() int)(x)  // 将x转换为类型 func() int

类型断言

类型断言通常用于判断某一接口类型的变量是否属于某一个类型,示例如下

var b int = 1
var a interface{} = b
if intVal, ok := a.(int); ok {
   fmt.Println(intVal)
} else {
   fmt.Println("error type")
}
1

由于interface{}是空接口类型,空接口类型可以代表所有的类型,但是int类型无法代表interface{}类型,所以无法使用类型转换。而类型断言就可以判断其底层类型是否为想要的类型,类型断言语句有两个返回值,一个是类型转换过后的值,另一个是转换结果的布尔值。

类型判断

在Go中,switch语句还支持一种特殊的写法,通过这种写法可以根据不同的case做出不同的逻辑处理,使用的前提是入参必须是接口类型,示例如下:

var a interface{} = 2
switch a.(type) {
    case int: fmt.Println("int")
    case float64: fmt.Println("float")
    case string: fmt.Println("string")
}
int

提示

通过unsafe包下提供的操作,可以绕过Go的类型系统,就可以做到原本无法通过编译的类型转换操作。