Go go/types.Info.Selections 怎样找到嵌入字段的真实对象
来源:17golang原创
时间:2026-10-25 16:49:52 452浏览 收藏
做 Go 静态分析时,很多人会先从 types.Info.Types 查选择器的类型,结果却拿不到嵌入字段真正对应的对象。关键在于:x.f 这类非限定选择器由 types.Info.Selections 记录,值是 *types.Selection;再结合 Obj、Type 和 Index,才能把提升字段还原出来。
官方地址:https://pkg.go.dev/go/types
需要找到选择器最终指向的字段或方法时,初始化Info.Selections,完成类型检查后按*ast.SelectorExpr查询;嵌入层级看Selection.Index(),最终声明对象看Selection.Obj(),表达式自身的类型看Selection.Type()。
为什么只查 Info.Types 会漏掉真实对象
go/types.Info 把不同种类的结果放在不同映射里。Types 面向表达式类型和常量值,Selections 专门记录不属于限定标识符的选择器表达式,例如 p.Name 或 p.Method()。当 Name 来自嵌入字段时,表达式类型和声明对象并不是同一个观察角度。
尤其要注意,pkg.Name 这样的限定标识符不进入 Selections。它属于包名加标识符的解析场景,应该从 Uses 等信息查看;不要看到所有带点的语法都去查 Selections。

先把 Selections 映射交给类型检查器
下面的代码只做三件事:准备一段带嵌入字段的源码,创建 types.Info,再遍历语法树查询选择器。示例把 Selections、Types 和 Uses 都初始化出来,便于同时观察表达式类型与对象解析结果。
package main
import (
"fmt"
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
)
const source = `
package demo
type Identity struct { ID int }
type User struct { Identity }
func use(u User) int {
return u.ID
}
`
func main() {
// FileSet 统一保存源码位置,parser 负责把文本变成 AST。
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "demo.go", source, 0)
if err != nil {
panic(err)
}
info := &types.Info{
// Selections 保存 u.ID 这类非限定选择器的解析结果。
Selections: make(map[&ast.SelectorExpr]*types.Selection),
// Types 让后面能观察选择器表达式自身的类型。
Types: make(map[ast.Expr]types.TypeAndValue),
// Uses 用于观察普通标识符引用到的声明对象。
Uses: make(map[*ast.Ident]types.Object),
}
conf := &types.Config{Importer: importer.Default()}
if _, err := conf.Check("demo", fset, []*ast.File{file}, info); err != nil {
// 类型检查失败时,Info 可能只包含不完整的已收集信息。
panic(err)
}
ast.Inspect(file, func(node ast.Node) bool {
selector, ok := node.(*ast.SelectorExpr)
if !ok {
return true
}
selection := info.Selections[selector]
if selection == nil {
// 限定标识符或未完成的检查结果不一定有 Selection。
return true
}
// Obj 是最终声明对象,Type 是 selector 表达式的类型。
fmt.Printf("%s -> %s / %s\\n", selector.Sel.Name,
selection.Obj().Name(), selection.Type().String())
return true
})
}
这里最容易漏掉的是 Selections: make(...)。Info 只会收集调用方提供了映射的结果;映射为 nil 时,即使类型检查成功,也不要期待 Selections 自动填充。
用 Selection 的几个方法拆开“真实对象”
拿到 *types.Selection 后,不要把所有信息都当成字段声明本身。Obj() 返回选择器最终指向的 *types.Var 或 *types.Func;Type() 返回 x.f 这个表达式的类型;Recv() 描述选择器左侧接收者的类型。
Kind() 可以进一步区分字段值、方法值和方法表达式。对嵌入字段问题来说,最重要的是 Index():最后一项是声明字段或方法在声明类型中的索引,前面的项描述为到达它而经过的嵌入字段索引。
func describeSelection(sel *types.Selection) {
// Index 的前置项描述嵌入字段链,最后一项定位声明类型中的成员。
indexes := sel.Index()
fmt.Printf("kind=%s recv=%s type=%s indirect=%t\\n",
sel.Kind(), sel.Recv(), sel.Type(), sel.Indirect())
// Obj 指向最终字段或方法;空值保护避免错误输入导致解引用失败。
if obj := sel.Obj(); obj != nil {
fmt.Printf("object=%s index=%v\\n", obj.Name(), indexes)
}
}
例如 User 嵌入 Identity 后使用 u.ID,Obj() 指向 Identity 中声明的 ID,而不是凭空生成一个名为 ID 的新字段。Index() 让静态分析器知道中间经过了哪一层嵌入。

空映射、限定标识符和 Indirect 怎么排查
如果查询不到选择结果,先按下面的顺序缩小范围:
- 确认节点类型。只有
*ast.SelectorExpr才能作为Selections的键,pkg.Name这类限定标识符不在这个映射里。 - 确认映射已初始化。
Selections、Types等字段是按需收集的,未提供 map 不会自动补齐。 - 确认检查错误。类型错误会让收集结果不完整;调试工具可以展示错误,同时把已收集对象标记为可能不完整。
- 确认读取的是同一个 AST 节点。重新解析或复制节点后,指针键已经不同,不能用另一个 AST 的
SelectorExpr查询。 - 单独解释 Indirect。
Indirect()只说明从接收者到成员的路径是否发生指针间接,不等于字段一定来自嵌入,也不替代Index()。
这几项可以放进静态分析器的诊断信息中:先打印选择器文本,再打印 Kind、Obj、Type、Index 和 Indirect。这样比只输出一个对象名更容易定位“字段存在但路径判断错了”的问题。
写静态分析规则时该取哪一项
最终可以按任务选择观察面:
| 要回答的问题 | 优先读取 | 原因 |
|---|---|---|
| 选择器最终指向哪个声明 | Selection.Obj() | 返回字段或方法对象 |
x.f 表达式的类型是什么 | Selection.Type() | 反映选择器表达式自身的类型 |
| 中间经过哪些嵌入字段 | Selection.Index() | 前置索引描述嵌入路径,末项定位成员 |
| 接收者从什么类型开始 | Selection.Recv() | 描述选择器的接收者类型 |
| 是否发生指针间接 | Selection.Indirect() | 补充访问路径属性,不代替对象定位 |
如果只需要判断某个选择器是否来自嵌入层,读取 Index() 的长度和前置项即可;如果还要生成可跳转的声明位置,再把 Obj() 转成具体的 *types.Var 或 *types.Func 并关联其位置。
常见问题
为什么 Info.Types 里能看到类型,却没有 Selection?
因为这两个映射职责不同,而且 Info 只收集调用方提供的字段。检查 Selections 是否初始化,并确认当前 AST 节点确实是非限定选择器。
Selection.Obj() 和 Selection.Type() 为什么可能不同?
Obj() 是声明处的字段或方法对象,Type() 是当前选择表达式的类型;经过嵌入、方法值绑定或指针接收者处理后,两者承担的语义自然不同。
小结
查找 Go 选择器的真实对象,核心不是把所有带点语法都塞进一个映射,而是先区分限定标识符与非限定选择器,再从 Info.Selections 取得 Selection。对象看 Obj,表达式类型看 Type,嵌入路径看 Index,接收者和指针边界分别看 Recv 与 Indirect。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
276 收藏
-
215 收藏
-
119 收藏
-
238 收藏
-
426 收藏
-
408 收藏
-
282 收藏
-
162 收藏
-
359 收藏
-
397 收藏
-
276 收藏
-
108 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习