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

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。

SelectorExpr、Info.Types、Info.Selections 与 Selection 对象之间的静态关系说明图
图1:go/types 映射关系说明图,区分表达式类型与选择器解析结果。

先把 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() 让静态分析器知道中间经过了哪一层嵌入。

SelectorExpr p.Name、Selection.Index、嵌入字段链与最终对象属性的静态关系说明图
图2:Selection.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。

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