当前位置:首页 > 文章列表 > Golang > Go问答 > Go types.Named Origin 怎样获取泛型类型的原始定义

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

来源:17golang原创 2026-11-02 01:14:04 0浏览 收藏

我在写 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删除
Go HTTP_PROXY 环境变量为什么改变了客户端请求路径Go HTTP_PROXY 环境变量为什么改变了客户端请求路径
上一篇
Go HTTP_PROXY 环境变量为什么改变了客户端请求路径
Go TLS 最低版本设置后旧客户端为什么握手失败
下一篇
Go TLS 最低版本设置后旧客户端为什么握手失败
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    835次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    834次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    917次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    893次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    715次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码