编译器眼中,type t = u 是类型别名,与u完全等价,不生成新类型;type t u 是类型定义,创建独立新类型,需显式转换、不共享方法集、反射显示不同名称。

编译器眼里,type T = U 和 type T U 是两种东西
Go 编译器在类型检查阶段就严格区分二者:type T = U 是类型别名(alias),它不生成新类型,只是给 U 换个名字;而 type T U 是类型定义(definition),它创建一个独立的、不可隐式转换的新类型。这个差异不是运行时才体现的,而是从 AST 构建、类型推导、接口实现检查到方法集合并,全程参与编译流程。
type T = U 赋值无需转换,type T U 必须显式转换
这是最直接暴露编译期差异的行为:
-
type MyIntAlias = int:变量a MyIntAlias可直接赋值给var b int,反之亦然,无警告无错误 -
type MyIntDef int:a MyIntDef不能直接赋值给int变量,否则报错cannot use a (type MyIntDef) as type int in assignment - 函数参数传入时同理:接收
int的函数不能直接传MyIntDef,但能收MyIntAlias
接口实现检查对二者处理完全不同
接口是否被满足,取决于方法集;而方法集的归属由类型定义方式决定:
-
type ReadCloser = io.ReadCloser:它和io.ReadCloser方法集完全一致,任何实现io.ReadCloser的值都能赋给ReadCloser变量 -
type ReadCloser io.ReadCloser:这是非法的——因为io.ReadCloser是接口,Go 不允许基于接口做类型定义(除非右侧是具体实现类型,如*http.response) - 更典型的是结构体场景:
type User struct{ Name string }和type Admin = User共享方法集;但type Admin User则不会继承User上定义的任何方法,必须重写
反射与 go/types 工具链能清晰看到编译期痕迹
反射不是运行时魔法,它的结果由编译期类型信息决定:
-
reflect.TypeOf(MyIntAlias(0)).Name()返回空字符串(基础类型),.PkgPath()为空,.Kind()是Int -
reflect.TypeOf(MyIntDef(0)).Name()返回"MyIntDef",.PkgPath()是当前包路径,.Kind()仍是Int,但类型身份已不同 - 用
go/types分析 AST 时,type T = U的TypeName对象的Underlying()直接指向U的类型节点;而type T U的Underlying()是U,但其自身是一个独立的NamedType节点
真正容易被忽略的是:类型别名在跨包使用时,如果原类型在另一个模块中被重构(比如移动到新包),别名若未同步更新,编译会直接失败——它不是“松耦合”,而是“硬绑定”。而类型定义只要底层结构没变,哪怕包路径变了,也能靠转换继续用。











