用 go/types 分析 Go 源码时,表达式的类型和值都在 Info.Types 里,但“值”不是一个已经转换好的 int64 或 float64。它对应的是 types.TypeAndValue.Value,底层类型是 go/constant.Value。要保留大整数、分数、复数和字符串的精确表示,应该先拿到这个对象,再按照 Kind 选择读取方式。
官方地址:https://pkg.go.dev/go/types#TypeAndValue
核心流程可以压缩成一句话:用同一个ast.Expr作为键查询Info.Types,先判断Value是否存在,再用ExactString或带有精度标记的转换函数读取结果。不要把常量直接格式化成普通浮点数后再做判断。
TypeAndValue 到底保存什么
TypeAndValue 只有两个对调用方重要的字段:Type 表示表达式的静态类型,Value 表示它在编译期可求值时的常量值。官方文档的限定很关键:Value 是“for constants”,所以变量读取、函数调用或运行时计算得到的表达式,不应该被当成缺省的数字零值。
同一个表达式可能同时拥有类型和值。例如 1 << 70 先是一个可以精确表示的整数常量,Type 可能是未命名的整数类型,Value 则保留完整结果。到了需要赋给某个具体类型的语境,类型检查器才会继续判断它能否表示为目标类型。
这里有一个实用的接口设计取舍:报告层最好同时保留 Type 的字符串和 Value.ExactString() 的原始文本。前者帮助读者理解类型,后者避免展示层先做有损转换。
让 Config.Check 填充 Info.Types
Info.Types 的键是具体的 ast.Expr 指针,而不是源码字符串。因此解析源码后要保留目标节点,再把同一个节点交给 info.Types 查询。下面的示例选取常量声明右侧的二元表达式,并把类型、精确值和整数转换结果分别打印出来。
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"go/types"
)
func main() {
const src = `package demo
const exact = 1 << 70
var runtimeValue = load()
func load() int { return 1 }
`
// FileSet 保存源码位置,Info.Types 会用 AST 节点作为查询键。
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "demo.go", src, 0)
if err != nil {
panic(err)
}
info := &types.Info{}
conf := types.Config{}
// Check 做静态类型检查并填充 Types,不会执行 load 或其他运行时函数。
_, err = conf.Check("demo", fset, []*ast.File{file}, info)
if err != nil {
fmt.Println("type check error:", err)
}
var target ast.Expr
ast.Inspect(file, func(node ast.Node) bool {
expr, ok := node.(*ast.BinaryExpr)
if ok && expr.Op == token.SHL {
// 保留这个节点本身,后面必须用同一个指针查询 Info.Types。
target = expr
return false
}
return true
})
tv, ok := info.Types[target]
if !ok || tv.Value == nil {
fmt.Println("expression is not a constant")
return
}
fmt.Println("type:", tv.Type)
fmt.Println("exact:", tv.Value.ExactString())
}
这段代码的关键不是打印语句,而是查询链:parser.ParseFile 产生节点,Config.Check 使用这些节点填充 Info.Types,之后用同一个节点作为 map 键。若重新解析一份内容相同的源码,得到的是另一组节点,不能拿新节点去查询旧的 Info。
从 Value 读取精确常量
go/constant 把常量分成 Bool、String、Int、Float 和 Complex。最稳妥的通用展示方式是 ExactString():它返回带引号的人类可读精确形式;字符串如果需要去掉引号,再用 constant.StringVal。
如果调用方确实要得到机器数值,应该接收第二个布尔返回值。constant.Int64Val 和 constant.Float64Val 都会告诉你转换是否精确;当结果不精确时,返回的数值不能继续当作原始常量参与关键判断。
package inspect
import (
"fmt"
"go/constant"
"go/types"
)
// DescribeConstant 把精确文本与可选的机器值分开,避免调用方误把近似值当原值。
func DescribeConstant(tv types.TypeAndValue) (string, string) {
if tv.Value == nil {
// 非常量表达式没有可导出的编译期值。
return "非常量", ""
}
exact := tv.Value.ExactString()
switch tv.Value.Kind() {
case constant.Int:
n, ok := constant.Int64Val(tv.Value)
if ok {
return exact, fmt.Sprintf("int64=%d", n)
}
// 大整数仍然可以用 exact 文本保存,不能把失败转换后的零值返回。
return exact, "int64=不精确"
case constant.Float:
n, ok := constant.Float64Val(tv.Value)
if ok {
return exact, fmt.Sprintf("float64=%g", n)
}
return exact, "float64=不精确"
case constant.String:
// StringVal 去掉 ExactString 中用于展示的引号。
return exact, "string=" + constant.StringVal(tv.Value)
case constant.Bool, constant.Complex:
// 布尔和复数保留精确文本,按业务需要再拆分实部与虚部。
return exact, "按精确文本处理"
default:
return exact, "unknown"
}
}
这里把两个结果分开,是为了让 API 使用者明确知道:exact 可以用于展示、缓存键或进一步的符号分析;第二列只是可选的便捷表示。尤其是浮点常量,转成 float64 后可能只得到最近似的二进制浮点值。
非常量、Unknown 和转换失败怎么区分
实际工具中至少要区分下面几类状态:
| 状态 | 含义 | 建议处理 |
|---|---|---|
info.Types 没有键 | 节点不是可记录的表达式,或当前信息容器没有被正确填充 | 先确认使用了同一 AST 和正确的 Info |
TypeAndValue.Value == nil | 表达式有类型,但不是编译期常量 | 返回“非常量”,不要返回数字零值 |
Value.Kind() == constant.Unknown | 常量求值因为错误等原因未知 | 保留错误上下文,不把 Unknown 当成合法结果 |
Int64Val/Float64Val 的布尔值为 false | 可以保留精确值,但目标机器类型无法无损表示 | 继续使用 ExactString,或改用大数/有理数模型 |
类型检查返回错误时,Info 中的结果也可能不完整。因而工具的错误模型最好同时携带“检查错误”和“当前已收集的表达式结果”,并明确标记结果不是完整快照。这样调用方不会因为拿到一个条目,就误以为整个文件都已经成功分析。
怎样给调用方设计稳定的返回模型
如果这个分析器要被命令行、编辑器或索引服务共同使用,我会把输出拆成三层:
- 语义层:记录表达式位置、
Type.String()、Value.Kind()和Value.ExactString()。 - 便捷层:在能够证明精确时,附加
int64、uint64或float64字段。 - 状态层:记录非恒定表达式、Unknown、转换不精确和类型检查错误,避免这些情况被压扁成空字符串。
这种设计比直接把 constant.Value 序列化成一个字符串更耐用。字符串可以作为兼容的展示字段,但不能替代 Kind 与精度状态;否则后续想判断“这是一个大整数还是一个近似浮点数”时,只能重新解析展示文本。
type ConstantRecord struct {
// Exact 保留 go/constant 的精确人类可读表示。
Exact string `json:"exact"`
// Kind 让调用方无需猜测 Exact 的语义。
Kind string `json:"kind"`
// MachineValue 只在转换精确时填充,不能用空值代表零值。
MachineValue string `json:"machine_value,omitempty"`
ExactMachineValue bool `json:"exact_machine_value"`
}
上面的结构没有把任意的 Go 接口直接暴露给 JSON,而是把精度承诺显式写进字段。对于静态分析工具,这比追求“所有值都能转成一个通用数字”更容易保持长期兼容。
常见追问
为什么直接调用 tv.Value.String() 还不够?
String() 适合短展示,文档允许数字结果是近似形式、字符串可能被缩短。需要完整可读表示时使用 ExactString();需要原始字符串内容时使用 constant.StringVal。
所有出现在 Info.Types 的表达式都有 Value 吗?
不是。Types 记录表达式类型,只有编译期常量表达式才同时提供常量值。函数调用、变量读取等表达式仍可能有 Type,但没有可直接导出的 Value。
大整数应该转成 float64 方便存储吗?
不建议。大整数和分数一旦转成浮点,可能损失精度。至少保留 ExactString,若业务需要计算,再使用合适的大数或有理数表示。
小结
提取 Go 常量表达式的精确值,关键是保留 ast.Expr 节点,用它查询 Info.Types 得到 TypeAndValue,再通过 go/constant.Value 的 Kind、ExactString 和带精度标记的转换函数读取结果。Type 解决“它是什么类型”,Value 解决“它在编译期具体是多少”;两者都保留下来,调用方才能在展示、索引和计算之间做出清晰取舍。