Type(类型)
Go 的类型同时决定值的表示、允许的操作、零值和 API 的赋值边界。理解“定义新类型”和“给现有类型起别名”的区别,能在编译期阻止单位或标识符混用。
定义类型与别名
type T U 创建一个身份独立的新定义类型;type T = U 只创建别名,别名和原类型完全相同。
package main
import "fmt"
type UserID int
type OrderID int
type LegacyID = UserID
func loadUser(id UserID) string { return fmt.Sprintf("user-%d", id) }
func main() {
userID := UserID(7)
orderID := OrderID(7)
fmt.Println(loadUser(userID))
// loadUser(orderID) // 编译错误:OrderID 不是 UserID。
var legacy LegacyID = userID
fmt.Println(loadUser(legacy)) // 别名仍是 UserID。
}
定义类型适合表达单位、ID 和受约束的业务值;它可以拥有自己的方法。别名通常用于迁移或兼容旧名称,不会创建新的 API 类型。
可赋值性与底层类型
两个不同定义类型即使底层类型相同,也不能直接赋值。只有在语言规范允许的可赋值条件成立时,才可以省略转换;最稳妥的做法是在 API 边界明确转换。
type Meters float64
type Feet float64
func toFeet(value Meters) Feet {
return Feet(value * 3.28084)
}
底层类型用于判断表示和部分转换规则,但不会让两个定义类型自动兼容。转换整数时要考虑截断和溢出;浮点转换可能损失精度;字符串与 []byte/[]rune 转换还可能产生新的数据副本。分配与优化属于实现细节,不能作为正确性的前提。
零值与可比较性
未显式初始化的变量使用零值:数值为 0、布尔为 false、字符串为空,指针、切片、映射、函数、接口和 channel 为 nil。数组和结构体的零值由其元素或字段递归构成。
nil 不意味着所有操作都一样:可以读取、遍历和删除 nil map,但写入会 panic;nil slice 可以读取长度并 append,nil channel 会使发送和接收永久阻塞。API 必须说明 nil 是否代表缺失、禁用或空集合。
只有可比较类型才能使用 ==/!=。切片、映射和函数(除与 nil 比较)不可比较;数组和结构体只有在所有元素或字段可比较时才可比较。接口值比较时,动态值也必须可比较,否则会在运行时 panic。
常见错误
- 依赖隐式转换,混用
int、int64或不同业务 ID。 - 把别名当成定义类型,或为兼容性误用别名导致边界消失。
- 只检查
nil,却没有区分 nil map、nil slice 和 nil channel 的操作语义。 - 使用浮点数做金额或需要精确相等的计数。
- 认为底层类型相同就一定可以赋值,忽略定义类型的身份。
与 TypeScript 的关键差异
TypeScript 类型主要用于编译期检查,运行时不会保留同等的类型边界;Go 的类型会参与运行时表示、零值和转换。TypeScript 的结构类型也不能直接类比 Go 的定义类型或接口满足规则。
小结
type T U创建新类型,type T = U创建别名。- 底层类型不等于赋值兼容;显式转换前检查精度、溢出和编码。
- 设计 API 时明确零值和 nil 容器的语义。
- 比较前确认类型本身及其动态值都可比较。