当前位置:首页 > 文章列表 > Golang > Go问答 > Go 不同 FileSet 的 Pos 为什么不能直接比较大小

Go 不同 FileSet 的 Pos 为什么不能直接比较大小

来源:17golang原创 2026-11-10 17:37:53 0浏览 收藏

结论先说:token.Pos 虽然底层是整数,但它不是整个项目共享的全局字节偏移。它属于创建它的 token.FileSet;两个独立的 FileSet 都可能从相同的基准开始分配位置,所以跨 FileSet 直接写 p1 ,比较到的只是两个局部坐标域里的数字,不能推出源码顺序。

如果位置来自同一个 FileSet,可以用 Pos 比较同一文件内的偏移,也可以比较该 FileSet 中文件的加入顺序。跨 FileSet 则先转换为带文件身份的 token.Position,或者构造自己的显式排序键。

先把 Pos 放回所属 FileSet

Go 的 token.File 记录文件名、大小和行偏移表;FileSet 在添加文件时为它分配一个不重叠的位置区间。File.Pos(offset) 只是把文件内的字节偏移映射到这个 FileSet 的位置域,FileSet.Position(pos) 再把它还原成文件名、行号和列号。

因此,同一个整数必须连同“由哪个 FileSet 解释”一起看。下面的例子故意创建两个独立 FileSet,并让两个文件都取各自的第一个字节位置:

package main

import (
    "fmt"
    "go/token"
)

func main() {
    setA := token.NewFileSet()
    setB := token.NewFileSet()
    fileA := setA.AddFile("a.go", -1, 20)
    fileB := setB.AddFile("b.go", -1, 20)

    posA := fileA.Pos(0)
    posB := fileB.Pos(0)

    // 两个 FileSet 的基准都从 1 开始,因此数值可能相等。
    fmt.Println(posA == posB, int(posA), int(posB))
    fmt.Println(setA.Position(posA), setB.Position(posB))
}

这里的两个 Pos(0) 都是各自文件的起点,但它们并不指向同一份源码。即使某次添加文件后数值恰好不同,也不能把数值差当作跨文件集的距离;解释这些数值所需的文件表并不相同。

两个独立 FileSet 各自拥有局部 Pos 坐标域的静态说明图
图1:两个 FileSet 各自拥有位置坐标域,数值相同也不代表源码位置相同。

同一 FileSet 内,Pos 比较什么时候成立

官方定义给出的边界很重要:如果两个 Pos 属于同一个文件,比较它们等价于比较该文件中的源偏移;如果属于同一个 FileSet 但来自不同文件,比较结果反映的是文件加入 FileSet 的先后顺序。这个顺序是位置分配规则,不是文件名的字典序。

解析一组文件时,最稳妥的流程是让 parser、AST 和类型检查共用一个 FileSet,然后再比较位置:

package positionorder

import (
    "fmt"
    "go/token"
)

// Before 判断两个位置是否在同一个 FileSet 中且顺序可按 Pos 解释。
func Before(set *token.FileSet, left, right token.Pos) (bool, error) {
    leftFile := set.File(left)
    rightFile := set.File(right)
    if leftFile == nil || rightFile == nil {
        return false, fmt.Errorf("位置不属于当前 FileSet")
    }
    // 同一 FileSet 内,Pos 比较可表达文件加入顺序和同文件偏移。
    return left 

这个辅助函数只接受同一个 set 解释的两个位置。FileSet.File 能先把位置映射回所属文件,调用方就能在比较前发现把别的坐标域混入的错误。它仍然不等价于“按文件名排序”:如果业务需要字母序,应该显式取出文件名再排序。

跨 FileSet 先转换,再建立排序键

两个独立 FileSet 的位置没有天然的全局先后。跨域排序前,应先定义业务真正关心的顺序。例如代码索引器可以按文件名、行号、列号排序;需要保留字节级精度时,再把文件内偏移加入键。

package positionorder

import (
    "fmt"
    "go/token"
)

type Key struct {
    Filename string
    Line     int
    Column   int
    Offset   int
}

// MakeKey 把 FileSet 局部位置转换为带文件身份的业务键。
func MakeKey(set *token.FileSet, pos token.Pos) (Key, error) {
    file := set.File(pos)
    if file == nil {
        return Key{}, fmt.Errorf("位置不属于当前 FileSet")
    }
    position := set.Position(pos)
    // Offset 取文件内字节偏移,不把 Pos 当成跨域全局偏移。
    return Key{
        Filename: position.Filename,
        Line:     position.Line,
        Column:   position.Column,
        Offset:   file.Offset(pos),
    }, nil
}

拿到 Key 后,排序规则就可以由业务明确决定:先按 Filename,再按 Line、Column,最后按 Offset。如果文件名可能重复,还应加入模块路径、文件来源或调用方定义的文档 ID。关键不是选择哪一组字段,而是把跨域身份写进键里。

从 Pos 转换为带文件身份的显式排序键静态说明图
图2:跨 FileSet 排序先转换为带文件身份的 Position,再使用显式键。

从目标到落地的完整工作流

遇到位置比较问题时,可以按下面的顺序处理,先判断坐标域,再选择比较方式:

  1. 确认来源:给每个 Pos 标记创建它的 FileSet,检查 parser 或检查器是否复用了同一个 FileSet。
  2. 判断范围:同一文件可比较源偏移;同一 FileSet 的不同文件可表达加入顺序;不同 FileSet 不直接比较。
  3. 转换语义:用所属 FileSet 的 Position 取得文件名、行列;用所属 File 的 Offset 取得文件内字节偏移。
  4. 选择业务键:索引、诊断和展示通常按文件名与行列排序;编辑器内部若要稳定定位,可额外保留文档 ID。
  5. 保留生命周期:让产生 Pos 的 FileSet 与相关 AST、类型信息一起传递,不要把裸 Pos 脱离上下文长期存储。

一个简单的决策表如下:

场景建议做法不能做的事
同一 FileSet、同一文件直接比较 Pos,或比较 File.Offset把比较结果解释成跨项目全局序号
同一 FileSet、不同文件按 Pos 得到 FileSet 中的加入顺序误认为这是文件名字典序
不同 FileSet分别转换 Position,再按显式键排序直接比较 p1、p2 或相减
需要持久化位置保存文档身份、文件内偏移或行列只保存一个裸 token.Pos 整数

常见误区与边界

Pos 是不是永远不能比较?不是。同一 FileSet 的位置比较是有定义的;问题在于把不同 FileSet 的局部整数当成同一坐标系。

为什么两个 FileSet 会出现相同的 Pos?每个新 FileSet 都从自己的基准开始分配位置,独立创建的第一个文件通常会得到相同的起点范围。相等只说明数值相等,不说明文件身份相等。

Position 能不能直接作为排序依据?token.Position 是结构化结果,适合展示和构造键,但最终排序仍要由业务决定,尤其要处理文件名重复、未加载文件和列号语义。

最小原则是什么?让 Pos 与所属 FileSet 成对传递;跨 FileSet 时先恢复文件身份和文件内偏移,再进行比较。这样排序依据就从“碰巧相等的整数”变成了可解释的业务数据。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go token.FileSet 怎样通过 Read 与 Write 保存位置映射Go token.FileSet 怎样通过 Read 与 Write 保存位置映射
上一篇
Go token.FileSet 怎样通过 Read 与 Write 保存位置映射
Go constant.Unknown 怎样在常量运算中传播
下一篇
Go constant.Unknown 怎样在常量运算中传播
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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模型性能。
    967次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    974次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1084次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1060次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    899次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码