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

Go go/doc.Filter 怎样从文档树中移除未导出声明

来源:17golang原创

时间:2026-10-13 05:05:01 178浏览 收藏

做 Go 文档生成器时,我遇到过一个很具体的分叉:同一份 *doc.Package 既要支持内部 API 视图,也要支持对外发布的公开 API 视图。继续手写“哪些声明保留”的循环,很快会漏掉常量、变量、类型和关联方法中的某一类。

这时可以把过滤规则交给 go/doc:定义一个接收名称的 doc.Filter,再调用 Package.Filter。它会按名称裁剪包级文档节点,但有两个容易被忽略的边界:调用后包级 Doc 会被清空;如果类型名称本身命中过滤器,该类型的成员不会再自动逐个过滤。

如果目标是“只展示公开声明”,最小做法是用 ast.IsExported 作为 doc.Filter,在渲染公开文档前调用一次 Package.Filter;如果还要隐藏类型内部成员,就必须在自己的渲染层继续做字段和方法策略。

先看 Filter 改动了哪些节点

go/doc 面向的是已经从 Go AST 提取出来的文档结构。官方定义的过滤器本质上是 func(string) bool:返回 true 的名称通过,返回 false 的名称被排除。Package.Filter 会处理包级常量、变量、类型和函数集合,并把筛选结果写回传入的 Package。

doc.Package 经过 Filter 名称谓词后形成公开 API 视图的结构说明图
图1:结构说明图,go/doc.Filter 按名称保留包级文档节点,并把结果写回同一个 Package。

这意味着它更像“准备渲染数据”的一步,而不是独立的只读查询。调用前后使用的是同一个对象,过滤完成后不应再把它当成完整文档树使用。

定义一个真正可复用的导出名称过滤器

不要把规则写成 name[0] >= 'A' && name[0] 。Go 标识符可以使用 Unicode,直接取第一个字节会把非 ASCII 的导出判断做错。标准库已经提供了匹配 Go 标识符导出规则的辅助函数,适合直接传给 doc.Filter。

package main

import (
	"fmt"
	"go/ast"
	"go/doc"
)

// exportedOnly 返回 true 表示名称应出现在公开文档中。
func exportedOnly(name string) bool {
	// ast.IsExported 遵循 Go 的 Unicode 导出规则,不要手写 ASCII 判断。
	return ast.IsExported(name)
}

func filterPublicDocs(pkg *doc.Package) {
	// Filter 会原地裁剪 pkg;调用后不要再把它当作完整文档树。
	pkg.Filter(doc.Filter(exportedOnly))
	fmt.Printf("公开函数数量:%d\\n", len(pkg.Funcs))
}

这里没有创建新的过滤器类型,普通函数就足够满足 doc.Filter 的函数签名。若项目需要特殊公开策略,也可以在这个函数里加入命名白名单,但要明确那已经不是“所有导出声明”了。

把过滤放到文档生成流程的正确位置

通常先用 doc.NewFromFiles 或 doc.New 得到完整的 Package,再根据输出目标选择是否过滤。内部检索、交叉引用和诊断需要完整树;公开 API 文档、对外索引或精简导航则适合使用过滤后的视图。

func publicPackage(fset *token.FileSet, files []*ast.File, importPath string) (*doc.Package, error) {
	// 先构造完整文档,保留 _test.go 中可关联的示例信息。
	pkg, err := doc.NewFromFiles(fset, files, importPath)
	if err != nil {
		return nil, fmt.Errorf("生成文档包失败:%w", err)
	}

	// 公开输出只保留通过 ast.IsExported 的包级声明。
	pkg.Filter(doc.Filter(ast.IsExported))
	return pkg, nil
}

上面的片段需要同时导入 fmt、go/ast、go/doc 和 go/token。真正的工程里,最好在函数注释中写清返回对象已经是“公开视图”,避免调用方误以为它仍包含未导出声明。

最容易误判的边界:类型命中后成员不会自动裁剪

过滤器的名称匹配不是递归的“每个字段都检查一遍”。类型名称本身通过过滤器时,go/doc 会保留这个类型文档;这条路径不会继续把它的字段、方法和关联声明逐个过滤。

反过来,如果类型名称没有通过,但它关联的常量、变量、构造函数或方法中有名称通过,类型节点仍可能作为承载这些匹配声明的容器留下。也就是说,判断结果不能只看 len(pkg.Types),还要看类型名是否命中以及类型下的成员集合。

Go go/doc.Filter 类型命中与方法命中两条过滤路径的边界说明图
图2:边界说明图,类型名称一旦命中过滤器,go/doc.Filter 不会再对该类型成员做第二轮名称裁剪。

因此,“公开 API 文档”有两种常见目标:

目标推荐做法需要注意
公开声明导航Package.Filter(ast.IsExported)接受类型命中后的成员保留语义
严格隐藏内部成员先过滤包级节点,再在渲染类型时检查字段和方法这是渲染策略,不是 Filter 单独完成的工作
内部完整文档保留未过滤的原始 Package不要把过滤后的对象回写到共享缓存

调用后包级 Doc 为什么会变空

Package.Filter 的行为还包括把包级 Doc 设为空。这对公开索引有利:包简介不会在过滤后的文档树中继续作为完整包说明出现。但如果页面还需要包简介,就不能在过滤之后临时期待它仍然存在。

更稳妥的流程是先保存需要展示的摘要,或者保留一份完整 Package,再对用于公开渲染的对象做过滤:

type PublicDocs struct {
	Summary string
	Package *doc.Package
}

func preparePublicDocs(full *doc.Package) PublicDocs {
	// 先保存包简介,因为 Filter 会清空 full.Doc。
	summary := full.Doc

	// 当前示例只复制顶层指针;若后续还要保留完整树,应使用独立的深拷贝策略。
	full.Filter(doc.Filter(ast.IsExported))
	return PublicDocs{Summary: summary, Package: full}
}

这里的关键不是为了“修复” Filter,而是把副作用纳入数据生命周期设计:完整树和公开树是两个用途不同的视图。共享缓存、异步渲染或多次导出场景下,最好在架构层明确谁拥有并修改 *doc.Package。

把这个能力当作文档流水线的一个阶段

从工程角度看,go/doc.Filter 解决的是一个长期存在的切换问题:同一套解析和文档模型,可以按发布目标选择“全量”或“公开”。它的收益是减少重复遍历和手写漏项;代价是原地修改、包注释清空以及类型成员边界需要额外处理。

适合采用它的角色包括自动生成 API 目录的工具维护者、需要同时发布内部和外部文档的团队,以及希望用标准库保持过滤规则一致的代码生成器作者。若只是一次性检查源码访问控制,直接在 AST 层分析更合适,不应把 Package.Filter 当成通用权限分析器。

发布前的观察清单

  1. 过滤器是否只接收名称并返回布尔值,且导出判断没有被 ASCII 规则限制?
  2. 调用前是否已经决定要不要保留包级 Doc?
  3. 是否明确类型名称命中后成员不会再自动逐个隐藏?
  4. 公开渲染是否使用了过滤后的 Package,而内部渲染仍保留完整视图?
  5. 是否把原地修改写进了缓存或并发共享对象的生命周期说明?

把这五项放进文档生成器的设计记录,通常比再写一套“移除未导出声明”的遍历更可靠。go/doc.Filter 的价值就在于它把常见的包级筛选交给标准库,同时把真正需要产品决策的部分——类型成员展示、摘要保存和对象所有权——清楚地留给调用方。

相关问题

go/doc.Filter 会修改原来的 Package 吗?

会。它会原地重排并截短包级集合,并把包级 Doc 清空;需要保留完整视图时,应先保存或复制。

用 ast.IsExported 能处理中文导出名称吗?

它按 Go 的标识符导出规则判断,不应自行用第一个字节比较 ASCII 大写字母。

为什么隐藏类型没有被完全删掉?

类型名未命中时,只要其关联声明中还有名称命中,类型仍可能作为容器保留。要严格控制展示内容,需要在渲染层继续处理成员。

本文依据 Go 官方 go/doc 文档对 Filter 和 Package.Filter 的说明,示例聚焦文档树筛选,不把它扩展成 AST 访问控制分析。

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