登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go types.Named Origin 怎样获取泛型类型的原始定义

来源:17golang原创

时间:2026-11-02 01:14:04 297浏览 收藏

我在写 Go 静态分析规则时,第一次遇到 Box[int] 这类类型,最容易把“当前实例”“泛型声明”和“底层结构”混成一个对象。go/types.Named.Origin 解决的正是其中一个很具体的问题:从实例化后的 *types.Named 找回产生它的泛型原始定义。

如果当前类型是由泛型 Named 实例化得到的,Origin() 返回原始泛型 *Named;如果当前类型没有实例化关系,它就返回当前对象自身。需要类型实参看 TypeArgs(),需要类型形参看 TypeParams(),不要用 Underlying() 代替它们。

我为什么会需要 Origin

静态分析器拿到的类型通常来自 go/types.Info。当源码里写的是 Box[int],类型检查结果仍然是一个 *types.Named,但它已经带有具体类型实参。此时如果规则的目标是“所有名为 Box 的泛型声明”,直接把当前类型的字符串交给规则并不稳妥:字符串适合展示,不适合表达定义与实例之间的关系。

Origin() 给出一个稳定的回到原始定义的入口。它不负责展开字段,也不负责返回 int 这样的实参;它只回答“这个 Named 是从哪个泛型 Named 来的”。下面的结构图把检查结果、实例对象和泛型关系放在同一张图里,图中关系是静态说明,不是运行截图。

go/types 检查结果、实例化 Named 与 Origin 原始泛型定义之间的静态关系图
图1:types.Info 中的实例化 Named 与 Origin、TypeArgs、TypeParams 的静态关系说明图。

Origin 返回的到底是什么

官方 API 对它的定义可以压缩成两句话:

  • 当前 Named 是实例化类型时,返回产生它的泛型原始 *Named。
  • 当前 Named 不是实例化类型时,返回当前 *Named。

假设源码里有 type Box[T any] struct { Value T },再使用 Box[int]。当前变量的类型可以理解为实例 Box[int],而 Origin() 指向泛型定义 Box[T]。这两个对象在分析时用途不同:前者告诉你当前代码用了什么,后者告诉你代码使用的是哪个泛型声明。

这里的“原始”不是底层匿名结构体。Box[T] 仍是命名类型,只有调用 Underlying() 才会进入结构体等底层表示。

Origin、Obj、TypeArgs 和 Underlying 怎么选

我实际使用时会先问规则要回答哪一个问题,再选方法。几个 API 名字都挂在 *types.Named 上,但它们的语义并不重叠。

想知道什么优先使用得到的内容
实例来自哪个泛型声明Origin()原始泛型 *types.Named
声明对象叫什么、位于哪里Obj()对应的 *types.TypeName
实例使用了哪些具体类型TypeArgs()类型实参列表;未实例化时通常为 nil
泛型声明定义了哪些形参TypeParams()类型形参列表
底层结构是什么Underlying()底层 types.Type,不是原始 Named

Obj() 在实例化类型上仍然对应原始声明的类型名,所以它适合取名字和源码对象;但如果规则需要明确区分“泛型声明”和“当前实例”,我会同时保留 named 与 named.Origin(),再分别读取参数。

types.Named 上 Origin、Obj、TypeArgs、TypeParams 和 Underlying 的查询职责对比图
图2:同一个 *types.Named 上五种查询方法的职责边界说明图。

从 go/types.Info 里拿到原始泛型定义

下面的示例只展示静态分析的取值路径:解析一段含有泛型声明和实例变量的源码,从 Info.Defs 找到变量,再把变量类型断言成 *types.Named。示例中的输出用于说明字段关系,不代表本文在本机执行过。

package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"go/types"
)

func main() {
	const source = `package demo

type Box[T any] struct {
	Value T
}

var current Box[int]
`

	// 解析源码并让 go/types 生成声明对象与类型信息。
	fset := token.NewFileSet()
	file, err := parser.ParseFile(fset, "demo.go", source, 0)
	if err != nil {
		panic(err)
	}
	info := &types.Info{
		Defs: make(map[*ast.Ident]types.Object),
	}
	conf := types.Config{}
	if _, err := conf.Check("demo", fset, []*ast.File{file}, info); err != nil {
		panic(err)
	}

	for ident, object := range info.Defs {
		if ident.Name != "current" {
			continue
		}
		variable, ok := object.(*types.Var)
		if !ok {
			continue
		}

		// 当前类型保留实例实参,Origin 则回到泛型原始定义。
		named, ok := variable.Type().(*types.Named)
		if !ok {
			continue
		}
		origin := named.Origin()
		fmt.Println("当前类型:", named)
		fmt.Println("原始定义:", origin)
		fmt.Println("类型实参:", named.TypeArgs())
		fmt.Println("类型形参:", origin.TypeParams())
	}
}

这段逻辑里最重要的是保留两个变量:

  • named 表示当前代码位置看到的 Named,适合读取 TypeArgs()。
  • origin 表示泛型声明,适合读取 TypeParams()、Obj() 或声明级规则。

如果分析对象来自 Info.Types 而不是 Info.Defs,取法相同:先拿到 types.Type,再做 *types.Named 断言。不要假设每个类型都是 Named,基础类型、指针、切片和接口都可能需要先按实际类型处理。

几个容易误判的边界

非泛型 Named 不会凭空产生另一个对象

对普通的 type UserID int,当前对象没有泛型实例化来源,UserID.Origin() 的语义就是返回它自己。代码因此可以无条件调用 Origin(),但不能无条件假设 TypeArgs() 非空。

TypeParams 不等于当前使用的参数

TypeParams() 描述声明端的形参,例如 T;TypeArgs() 描述使用端的实参,例如 int。在实例化类型上,原始泛型声明的形参信息仍然有价值,所以需要比较“参数个数”时应明确比较哪一边。

Underlying 不适合判断泛型来源

Underlying() 会把命名类型的视角转到底层结构。两个不同的命名类型可能拥有相同或相似的底层类型,因此用它来追溯泛型声明会丢掉命名身份。只有当规则确实关心字段、方法集或底层接口时,才继续向下查询。

String 只适合展示和日志

String() 对排查很方便,可以让日志显示 Box[int] 与 Box[T] 的差异。但字符串格式不是声明身份的替代品;规则匹配应优先使用 Obj()、Origin() 和参数列表。

我的选择结论

如果任务是“这个类型实例来自哪个泛型声明”,直接用 named.Origin();如果还要知道它是用什么类型实例化的,再读取 named.TypeArgs()。声明端的形参、名称和底层表示分别交给 TypeParams()、Obj() 与 Underlying()。

分析任务推荐组合不建议的替代
匹配所有 Box 的实例Origin() + Obj()直接比较字符串
记录 Box[int] 的具体化信息Origin() + TypeArgs()只读 Underlying()
检查泛型声明是否有形参Origin().TypeParams()把 TypeArgs() 当成声明信息
检查字段或底层接口Underlying()用 Origin() 代替结构查询

记住这条最短路径就够了:Named 是当前类型视角,Origin 是泛型来源,TypeArgs 是实例输入,TypeParams 是声明输入,Underlying 是底层表示。把五个问题分开,静态分析规则就不容易在泛型实例和原始定义之间走错。

相关问题

Origin 会返回 nil 吗?

对有效的 *types.Named,它返回当前 Named 或其原始 Named,不是用来表示“没有泛型”的 nil 哨兵。

怎么拿到 Box[int] 里的 int?

从当前实例读取 TypeArgs(),再按索引取得对应的 types.Type;不要从 Origin() 读取,因为 Origin 代表声明端。

Origin 能把底层类型恢复成 Named 吗?

不能。Origin 只处理 Named 的泛型来源关系;底层结构应通过 Underlying() 查询,二者解决的是不同层次的问题。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>