深色模式
Go语言类型系统
概述
Go 的类型系统不算花哨,但特别讲究边界清楚。哪些类型本来就是同一个类型,哪些只是底层结构相同,哪些可以直接赋值,哪些必须显式转换,这些规则看起来零碎,实际却都绕着同一条主线在转。
这篇文章不把接口、方法集、rune 这些专题再重复展开,而是先把类型系统最基础的判断线讲清楚:Go 里有哪些类型,什么叫定义类型,什么叫类型别名,底层类型到底在解决什么问题。
先看 Go 里有哪些类型
Go 的类型可以先粗分成两大类:
- 基本类型
- 复合类型
这个分类不是为了背名词,而是为了先建立一个总览。
基本类型
Go 的基本类型主要包括这些:
- 布尔类型:
bool - 字符串类型:
string - 整数类型:
int8、int16、int32、int64、int - 无符号整数类型:
uint8、uint16、uint32、uint64、uint、uintptr - 浮点类型:
float32、float64 - 复数类型:
complex64、complex128
这里有两个经常单独看到的名字:
byte是uint8的别名rune是int32的别名
它们很常见,但不是新发明出来的独立底层类型。
复合类型
复合类型指的是在已有类型基础上组合出来的类型。常见的有:
- 数组
slicemapstruct- 指针
- 函数
channelinterface
例如:
go
[3]int
[]string
map[string]int
struct{ Name string }
*User
func(int) error
chan int这些类型不一定都有名字,但它们都是真实存在的类型。
Go 里最容易混的不是分类,而是“是不是同一个类型”
例如下面几个写法,看起来很像:
go
type UserID int
type Age = int
var a int
var b UserID
var c Age但它们的关系完全不同:
UserID是一个新定义出来的类型Age只是int的别名a和c的类型本质上是同一个类型b和a则不是同一个类型
Go 类型系统里,很多后续规则都从这里分叉。
类型声明有两种:定义类型和类型别名
类型定义
类型定义会创建一个新的类型:
go
type UserID int
type Celsius float64
type IntSlice []int这里的 UserID、Celsius、IntSlice 都是新类型。即使它们和原类型长得很像,编译器也不会把它们直接当成同一个类型。
例如:
go
type UserID int
var id UserID = 10
var n int
n = int(id)这里需要显式转换。因为 UserID 和 int 不是同一个类型。
类型别名
类型别名不会创建新类型,只是给已有类型起了另一个名字:
go
type Age = int
type Text = string这里的 Age 和 int 是同一个类型,Text 和 string 也是同一个类型。
例如:
go
type Age = int
var a Age = 18
var n int = a这里不需要转换,因为它们本来就是同一个类型。
定义类型和别名的区别,核心就一条
可以先只记这一句:
type T U会定义一个新类型type T = U不会定义新类型
这个区别看起来只是多了个等号,实际影响很大。它会直接影响:
- 能不能直接赋值
- 是否需要显式转换
- 方法能不能定义在这个类型上
- 这个名字到底是不是独立类型
命名类型和非命名类型
Go 资料里还经常会看到另一组词:
- 命名类型
- 非命名类型
例如:
go
type UserID int
var a UserID
var b []int
var c struct{ Name string }这里:
UserID是命名类型[]int是非命名类型struct{ Name string }也是非命名类型
命名类型不等于“基本类型”,非命名类型也不等于“不能用”。它只是说明这个类型有没有通过类型声明拿到一个名字。
底层类型到底是什么
底层类型这个概念,主要是为了描述类型之间更深一层的来源关系。
几个最常用的判断规则是:
- 预声明类型的底层类型是它自己。
- 类型别名的底层类型就是它所指向类型的底层类型。
- 新定义类型和它来源类型共享同一个底层类型。
- 非命名复合类型的底层类型通常就是它自己。
例如:
go
type UserID int
type Score UserID
type Age = int这几个类型的底层关系可以这样看:
int的底层类型是intUserID的底层类型是intScore的底层类型是intAge和int是同一个类型,所以底层类型也是int
为什么底层类型有用
因为 Go 在做赋值兼容性、类型转换、某些运算判断时,不只看“名字像不像”,还会看它们底层是不是同一路。
例如:
go
type MyInt int
type YourInt int
var a MyInt = 10
var b YourInt
b = YourInt(a)MyInt 和 YourInt 不是同一个类型,所以不能直接赋值;但它们底层类型相同,所以可以显式转换。
底层类型的意义,大多体现在这种地方。平时不一定天天把这个词挂嘴边,但编译器其实一直在按这套规则做判断。
赋值时先问:是不是同一个类型
Go 对赋值的第一层判断通常很直接:
- 同一个类型,通常可以直接赋值
- 不是同一个类型,就要看有没有额外规则允许
例如:
go
var a int = 10
var b int
b = a这当然没问题。
但换成:
go
type UserID int
var id UserID = 10
var n int这里 n = id 就不成立,因为它们不是同一个类型。
转换时先问:底层上能不能转
显式转换是另一回事。Go 对转换比对直接赋值宽一些,但也不是随便转。
例如:
go
type UserID int
var id UserID = 10
var n int = int(id)这里可以转换,因为 UserID 的底层类型是 int。
再比如:
go
type Name string
var s string = "gopher"
var n Name = Name(s)这也成立,因为底层关系兼容。
可以先记一个够用的经验:Go 允许很多“底层兼容”的显式转换,但不会因为它们看起来像就自动帮你做隐式赋值。
自定义类型的真正价值,不只是多一个名字
很多人第一次看 type UserID int 会觉得这只是换个名字。其实不是。
定义新类型的价值通常有两个:
- 让语义更清楚
- 把本来能随便混用的值隔开
例如用户 ID、订单 ID、商品 ID 底层都可以是 int64,但把它们都保留成裸 int64,代码里就很容易混。定义成不同类型后,编译器会帮忙拦下一部分本来很容易写错的代码。
这才是 Go 里自定义类型最常见的工程意义。
几个特别容易混的点
byte 和 rune 是别名,不是新定义类型
它们是为了表达语义更清楚,不是为了创造新的底层规则。
type T U 和 type T = U 不是写法偏好差异
一个会创建新类型,一个不会。区别非常实在。
非命名类型也是真类型
像 []int、map[string]int、struct{ Name string } 都是真实类型,只是没有自己的类型名。
底层类型相同,不等于可以直接赋值
很多时候只是说明“可以显式转换”,不是说明“编译器会自动帮你当成同一个类型”。
一条够用的判断线
看到两个类型时,按这个顺序想最稳:
- 它们是不是同一个类型。
- 如果不是,是不是类型别名关系。
- 如果还不是,底层类型是否一致。
- 当前是在做直接赋值,还是显式转换。
Go 类型系统看起来规矩很多,但一旦沿着这条线判断,很多问题都不会再显得绕。
