Go 的注释与文档是同一来源:godoc 与 pkg.go.dev 直接把源码中的文档注释(doc comment)渲染为 API 文档站,不存在 Javadoc 那样"文档一套、注释一套"的双轨维护。由此可得一个直接推论:评价一个库"文档差",基本等同于评价它的注释差——注释之外没有第二个文档来源。
但注释良好不等于文档完备。一个仓库的文档至少分三层:doc comment 面向调用 API 的开发者,README 面向第一次打开仓库的开发者,CHANGELOG 面向准备升级的开发者。三层的常见失效方式不同:注释易流于复述代码,README 易偏向项目宣传,CHANGELOG 易退化为逐项罗列。库与应用常被混为一谈,但库被导入、应用被运行,文档侧重点不同,本文最后单独讨论。
先说明一个前提:godoc 生态的惯例建立在英文注释上,本文示例均为英文。推荐中文项目同样使用英文注释,以保证 pkg.go.dev 的渲染效果符合社区惯例。
godoc 注释
**每个导出符号的文档注释都应以符号名开头,后接动词短语。**这是 godoc 渲染包索引的约定,grep 定位同样依赖它:
// ✅ 名字开头 + 动词短语
// CalculateDiscount computes the final price after applying tiered discounts.
func CalculateDiscount(basePrice float64, quantity int, tiers []DiscountTier) (float64, error)
// ❌ 未以符号名开头:包索引中无法识别该注释所属的符号
// Compute the discount for an order.
func CalculateDiscount(basePrice float64, quantity int, tiers []DiscountTier) (float64, error)
代码评审遇到不以符号名开头的注释,应要求修改:pkg.go.dev 的包索引中无法识别该注释描述的是哪个符号。
**首句应是摘要句,并能独立成立。**pkg.go.dev 的包索引与 go doc 的列表输出只显示第一句。一个常被混淆的细节:Go 的注释句子以句号结束,godoc 依据第一个句号切分摘要;# Heading 这类结构行才不带标点。从其他语言带来的"摘要行不加句号"的习惯,在 Go 中必须修正。
**注释应解释"为什么",而不是复述"是什么"。**行为代码本身已经说明:
// ❌ 复述签名:删掉这条注释,读者没有信息损失
// GetUser gets a user by ID.
func GetUser(id string) (*User, error) {
// ✅ 解释 why / when / 错误路径
// GetUser retrieves a user from the database by their unique identifier.
// Use this for authenticated endpoints where you need the full user profile.
// For listing or searching, use ListUsers instead — it returns lighter projections.
//
// Returns ErrNotFound if no user exists with the given ID.
// Returns ErrDatabaseUnavailable if the connection pool is exhausted.
func GetUser(id string) (*User, error) {
❌ 那条示例的问题不在于没有以符号名开头——它恰恰以符号名开头;问题在于说完符号名之后,没有提供任何签名之外的信息。
常见反模式可以归为五类:复述签名(// Returns a string and an error)、营销用语(seamlessly、robust、enterprise-grade)、缺乏依据的动机描述(designed to improve scalability)、缺乏依据的未来承诺(supports future extensibility)、空洞的过渡语(it's worth noting that)。检验标准只有一条:删掉这条注释,读者损失了什么?答案是"没有",就应删除。
错误路径应逐个写明,并且使用具体错误值:Returns ErrNotFound if...,而不是笼统的 returns an error。签名中本来就包含 error,注释的价值在于说明是哪种错误、什么条件下触发。能力边界也应一并交代,一段 Limitations 说明可以减少大量相关 issue。
文档注释模板应按需裁剪:没有的段落直接删除,不应为对齐格式填充内容:
// Parse parses a duration string such as "300ms", "1.5h", or "2h45m".
//
// Parameters:
// - s: A duration string. Valid units are "ns", "us", "ms", "s", "m", "h".
//
// Returns the parsed duration.
// Returns ErrInvalidDuration if the string is empty or has an invalid format.
// Returns ErrOverflow if the duration exceeds math.MaxInt64 nanoseconds.
//
// Limitations:
// - Does not support day, week, month, or year units.
func Parse(s string) (time.Duration, error) {
接口注释应写契约,方法注释应写语义。并发安全要求、context 语义、边界值行为,不应留给实现者自行推断:
// Store defines a persistent key-value storage backend.
// Implementations must be safe for concurrent use by multiple goroutines.
//
// All methods accept a context for cancellation and deadlines.
// Implementations should respect context cancellation and return
// ctx.Err() when the context is done.
type Store interface {
// Get retrieves the value associated with key.
// Returns ErrNotFound if the key does not exist.
Get(ctx context.Context, key string) ([]byte, error)
// Delete removes a key from the store.
// Returns nil (not an error) if the key does not exist.
Delete(ctx context.Context, key string) error
}
**并发安全必须显式声明。**应写明 "It is safe for concurrent use",或反向声明 "It is NOT safe to call Close concurrently from multiple goroutines"。这是调用方无法从签名推断、又最容易引起误用的信息。操作是否幂等同理,应一句话说清。
排版细节
注释用空行分段。注释中缩进的段落(tab 缩进)会被 godoc 渲染为代码块,这是在注释中放置示例的标准方式:
// Transform applies a function to each element of a slice and returns
// a new slice with the results.
//
// Example:
//
// names := []string{"alice", "bob"}
// upper := Transform(names, strings.ToUpper)
// // upper: ["ALICE", "BOB"]
func Transform[T any, U any](slice []T, fn func(T) U) []U {
Go 1.19 之后,注释中可以使用 # Heading 划分小节。包注释较长时,应用它组织出 # Architecture、# Token Lifecycle、# Thread Safety 一类的结构;标题行不带标点,前后以空行分隔。
废弃 API 应使用 Deprecated: 惯例:单独成段,写清替代 API 与移除时间,pkg.go.dev 会为这类符号加删除线:
// OldFunc does something.
//
// Deprecated: Use NewFunc instead. OldFunc will be removed in v3.0.0.
func OldFunc() {}
只声明废弃而不给出替代方案与移除计划,对该 API 的使用者没有实际价值。
包注释一个包一份,位置二选一:一两个文件的小包放在主文件顶部、紧贴 package 子句;文件较多或注释超过十余行,放在 doc.go。不应每个文件各写一段。
// Package httputil provides HTTP utility functions for request parsing,
// response writing, and middleware chaining.
//
// It is designed to work with the standard net/http package and does not
// depend on any specific HTTP framework.
package httputil
该写与不该写
所有导出符号都必须有注释:函数、方法、类型、接口、常量、变量。这项检查不必依赖人工,revive 和 staticcheck 都能静态检测。非导出的复杂函数(算法实现、控制流复杂的逻辑)也值得写;名称自解释的简单 helper、测试函数可以不写;无逻辑的 getter/setter 一行就够。注释质量首先取决于命名:名称足以说明的符号,不应再依赖注释,细则见命名与代码风格。
TODO 注释必须带跟踪入口。有关联 issue 就写 // TODO(#123): ...;非正式场景可用 // TODO(username): ... 或裸 // TODO:。缺少跟踪指向的 TODO 注释通常会在数月内失效,应避免。
文件级说明只给确实需要的文件:实现了某个算法、复杂状态机或协议,或者单个文件聚集了 200+ 行相关逻辑,才在 import 之后写一段宏观介绍——这个文件为什么存在、做了哪些取舍,可以配 ASCII 图描述调度流程,但应说明结构,不应逐行翻译代码。CRUD handler、数据模型、文件名已能说明用途的文件,不写。
README:读者是谁
README 可以采用一个九段骨架:Title → Badges → Summary → Demo → Getting Started → Features/Specification → Contributing → Contributors → License。顺序本身就是承诺:读者自上而下阅读三十秒,应能完成"装上、跑通"。
徽章应在一行内放完。Go 项目常用六枚:Go Version、License、Build Status、Coverage、Go Report Card、pkg.go.dev Reference。最简的三个:
[](https://go.dev/)
[](https://pkg.go.dev/github.com/{owner}/{repo})
[](./LICENSE)
Demo 段按项目类型选择素材:库放代码片段,CLI 放 GIF 或视频,Web UI 放截图。Summary 应以一两句话说清项目是什么,不应写成宣传文案。
**库的 README 和应用的 README 是两种文体。**库的第一屏应是 go get 加十行内能跑通的示例;应用的第一屏应是安装方式清单,预编译二进制、go install、Docker、Homebrew 至少覆盖两种。每种安装路径覆盖一类用户的使用习惯:依赖 npm 的工具会被 DevOps 跳过,没有 Homebrew 的工具会被 macOS 用户跳过。
应用配置必须写成表格,说明加载顺序(flags > 环境变量 > 配置文件 > 默认值)以及每项的默认值、是否必填,放在 README 或单独的 docs/configuration.md:
| Variable | Description | Default | Required |
| ------------------ | -------------------------- | ------- | -------- |
| `MYTOOL_DB_URL` | Database connection string | — | Yes |
| `MYTOOL_LOG_LEVEL` | Log verbosity | `info` | No |
| `MYTOOL_PORT` | HTTP server port | `8080` | No |
CLI 的第一文档是 --help。cobra 的 Long 字段应写清用途与环境变量,Example 字段应放真实命令。文档全部集中在 README、--help 输出却信息匮乏,属于优先级颠倒——多数人第一次接触该工具,是在终端里执行 --help。
CONTRIBUTING 的验收标准应定为十分钟:完成 clone、修改一行代码、跑通测试。达不到要求,应添加 Makefile、docker-compose、devcontainer,或用 build tag 把较慢的集成测试隔离,而不应在文档中致歉。
CHANGELOG:写给要升级的人
每次发版必须更新 CHANGELOG,格式遵循 Keep a Changelog:Added、Changed、Deprecated、Removed、Fixed、Security 六个固定的变更日志分类,与语义化版本一一对应——新功能进入 minor,bug 修复进入 patch,Removed 一类破坏性变更只能进入 major,Deprecated 是 Removed 的预警期:
## [1.2.0] - 2026-03-08
### Added
- New `WithTimeout` option for client configuration
### Changed
- Improved retry logic to use exponential backoff
### Fixed
- Race condition in connection pool under heavy load
### Deprecated
- `SetTimeout()` method — use `WithTimeout()` option instead
[1.2.0]: https://github.com/{owner}/{repo}/compare/v1.1.0...v1.2.0
条目应写"用户的世界发生了什么变化",而不是"开发团队做了什么"。内部重构没有用户可见影响,属于 commit history,不进入 CHANGELOG;反过来,"修复了一个竞态"也不应表述为"显著提升可靠性"——一个修复就是一个修复。Deprecated 条目必须指明去向:替代 API 是什么、预计哪个版本移除,与代码注释里的 Deprecated: 遵循同一纪律。
小项目可以用 GitHub Releases 代替 CHANGELOG 文件,GoReleaser 能从 commit 自动生成 release notes。但"这条变更对读者意味着什么",仍应有人工把关。
库和应用
库的主文档面是 pkg.go.dev,文档工作应全部投入注释。打上 tag 之后,doc comment 中的代码块、# 标题、[Link text] 超链接、[FuncName] 符号互链、Deprecated 样式都会自动渲染。私有模块无法上架 pkg.go.dev:本地使用 go doc,内网可以部署 pkgsite。私有库的注释同样不应放松要求——团队会轮换,记忆会失效,而且当前的读者经常是 AI:
go doc github.com/{owner}/{repo}.FuncName
go doc -all github.com/{owner}/{repo}
go get -tool golang.org/x/pkgsite/cmd/pkgsite@latest
go tool pkgsite -http=:6060
**Example 测试是库文档的必要组成部分,不是装饰。**ExampleXxx 写在 _test.go 里,既渲染进 godoc 页面,又被 go test 验证。文档与代码不脱节,依靠的正是这个机制:
func ExampleMap() {
result := mypackage.Map([]int{1, 2, 3}, func(x int) int { return x * 2 })
fmt.Println(result)
// Output: [2 4 6]
}
命名规则:ExampleFuncName、ExampleTypeName_MethodName、ExampleFuncName_suffix 区分同一函数的多个示例(suffix 小写),包级示例命名为 Example。// Output: 注释必须写——没有它,示例只编译、不校验输出。测试写法细节见单元测试规范。
公开库还应提供一行 // Play: https://go.dev/play/p/xxx,使用者不安装包就能在 Playground 试运行。demo 应自包含(带全部 import 和 main)、先展示最常见用法、打印结果。
应用的主文档面是应用自身。--help 与配置文档之外,对外 API 的文档应优先从代码生成:REST 用 swaggo/swag 从注解生成 OpenAPI,gRPC 直接以 proto 注释作为文档源,用 buf lint / buf breaking 校验契约兼容性,事件流用 AsyncAPI。这些工具的版本管理与升级节奏见工具链与持续更新。
llms.txt
仓库根目录应放置 llms.txt,给 AI agent 一份结构化的项目概览:项目是什么、文档在哪、如何安装。这是一项正在成形的惯例,公开库建议采用,私有库同样值得采用——"读者是 AI"对内、外部仓库同样成立。
对 AI 友好的手段同时也是对人友好的手段:一致的 godoc 注释、结构化的 API 规格(OpenAPI/AsyncAPI/protobuf)、清晰的 README,LLM 与人类读者都能高效阅读。大型库还可以把文档站按 Diataxis 分区(Getting Started / Tutorial / How-to Guides / Reference / Explanation),再注册进 Context7、DeepWiki 这类文档聚合器,提高可发现性。
速查清单
- 所有导出符号必须有 doc comment,以符号名开头;首句完整、以句号结束——pkg.go.dev 的摘要取自首句
- 注释应写 why / when / 约束 / 错误;避免复述签名、营销用语、无依据的理由与承诺
- 错误应写具体错误值;并发安全必须显式声明
- 代码块使用 tab 缩进;长包注释使用
#标题,置于 doc.go Deprecated: Use X instead.应单独成段,写明移除计划- TODO 必须带 issue 指向;文件级说明仅用于算法、状态机、200+ 行的文件
- README 采用九段骨架;库首屏为 go get + 最小示例,应用提供至少两种安装路径与配置表
- CLI 应将 --help 作为第一文档;CONTRIBUTING 应保证十分钟内跑通
- CHANGELOG 遵循 Keep a Changelog 的变更日志分类,发版必须更新,只写用户可见的变化
- Example 测试必须带
// Output:;公开库应提供 Playground 链接 - 仓库根目录应放置 llms.txt