mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 16:46:37 +08:00
wavelet init
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
---
|
||||
name: go-documentation
|
||||
description: 在编写或审查 Go 包、类型、函数或方法的文档时使用。在创建新的导出类型、函数或包时也应主动使用,即使用户没有明确询问文档问题。不涵盖未导出符号的代码注释(参见 go-style-core)。
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
sources: "Google 风格指南"
|
||||
allowed-tools: Bash(bash:*)
|
||||
---
|
||||
|
||||
# Go 文档
|
||||
|
||||
## 可用脚本
|
||||
|
||||
- **`scripts/check-docs.sh`** — 报告缺少文档注释的导出函数、类型、方法、常量和包。运行 `bash scripts/check-docs.sh --help` 查看选项。
|
||||
|
||||
> 在为新包或导出类型编写文档注释并需要所有文档约定的完整参考时,请参阅 `assets/doc-template.go`。
|
||||
|
||||
---
|
||||
|
||||
## 文档注释
|
||||
|
||||
> **规范**:所有顶层导出名称必须有文档注释。
|
||||
|
||||
### 基本规则
|
||||
|
||||
1. 以被描述对象的名称开头
|
||||
2. 冠词("a"、"an"、"the")可以放在名称前面
|
||||
3. 使用完整句子(首字母大写,带标点符号)
|
||||
|
||||
```go
|
||||
// A Request represents a request to run a command.
|
||||
type Request struct { ...
|
||||
|
||||
// Encode writes the JSON encoding of req to w.
|
||||
func Encode(w io.Writer, req *Request) { ...
|
||||
```
|
||||
|
||||
行为不明显的未导出类型/函数也应有文档注释。
|
||||
|
||||
> **验证**:添加文档注释后,运行 `bash scripts/check-docs.sh` 验证是否有导出符号缺少文档。修复所有缺失后再继续。
|
||||
|
||||
---
|
||||
|
||||
## 注释语句
|
||||
|
||||
> **规范**:文档注释必须是完整的句子。
|
||||
|
||||
- 首字母大写,以标点符号结尾
|
||||
- 例外:如果含义清晰,可以以小写标识符开头
|
||||
- 结构体字段的行尾注释可以是短语
|
||||
|
||||
---
|
||||
|
||||
## 注释行长度
|
||||
|
||||
> **建议**:目标约 80 列,但不设硬性限制。
|
||||
|
||||
根据标点符号换行。不要拆分长 URL。
|
||||
|
||||
---
|
||||
|
||||
## 结构体文档
|
||||
|
||||
使用段落注释对字段分组。标记可选字段及默认值:
|
||||
|
||||
```go
|
||||
type Options struct {
|
||||
// 通用设置:
|
||||
Name string
|
||||
Group *FooGroup
|
||||
|
||||
// 自定义设置:
|
||||
LargeGroupThreshold int // 可选;默认值:10
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 包注释
|
||||
|
||||
> **规范**:每个包必须有且仅有一个包注释。
|
||||
|
||||
```go
|
||||
// Package math provides basic constants and mathematical functions.
|
||||
package math
|
||||
```
|
||||
|
||||
- 对于 `main` 包,使用二进制名称:`// The seed_generator command ...`
|
||||
- 对于较长的包注释,使用 `doc.go` 文件
|
||||
|
||||
> 在编写包级文档、main 包注释、doc.go 文件或可运行示例时,请阅读 [references/EXAMPLES.md](references/EXAMPLES.md)。
|
||||
|
||||
---
|
||||
|
||||
## 文档编写要点
|
||||
|
||||
> **建议**:记录非显而易见的行为,显而易见的行为无需记录。
|
||||
|
||||
| 主题 | 何时记录... | 何时跳过... |
|
||||
|------|------------|------------|
|
||||
| 参数 | 非显而易见的行为、边界情况 | 只是重复类型签名 |
|
||||
| 上下文 | 行为与标准取消不同 | 标准 `ctx.Err()` 返回 |
|
||||
| 并发 | 线程安全性不明确(例如,看似读取但内部修改) | 只读安全、修改不安全 |
|
||||
| 清理 | 始终记录资源释放要求 | — |
|
||||
| 错误 | 哨兵值、错误类型(使用 `*PathError`) | — |
|
||||
| 命名返回值 | 多个同类型参数、面向操作命名 | 类型本身已足够清晰 |
|
||||
|
||||
关键原则:
|
||||
|
||||
- 上下文取消返回 `ctx.Err()` 是隐含的 — 不要重复说明
|
||||
- 只读操作默认线程安全;修改操作默认不安全 — 不要重复说明
|
||||
- 始终记录清理要求(例如,`Call Stop to release resources`)
|
||||
- 在错误类型文档中使用指针(`*PathError`),以确保 `errors.Is`/`errors.As` 正确使用
|
||||
- 不要仅为启用裸返回而命名返回值 — 清晰性 > 简洁性
|
||||
|
||||
> 在记录参数行为、上下文取消、并发安全性、清理要求、错误返回或函数文档注释中的命名返回参数时,请阅读 [references/CONVENTIONS.md](references/CONVENTIONS.md)。
|
||||
|
||||
---
|
||||
|
||||
## 可运行示例
|
||||
|
||||
> **建议**:在测试文件(`*_test.go`)中提供可运行示例。
|
||||
|
||||
```go
|
||||
func ExampleConfig_WriteTo() {
|
||||
cfg := &Config{Name: "example"}
|
||||
cfg.WriteTo(os.Stdout)
|
||||
// Output:
|
||||
// {"name": "example"}
|
||||
}
|
||||
```
|
||||
|
||||
示例会出现在 Godoc 中,附加到对应的文档元素上。
|
||||
|
||||
> 在编写可运行 Example 函数、选择示例命名约定(Example vs ExampleType_Method)或添加包级 doc.go 文件时,请阅读 [references/EXAMPLES.md](references/EXAMPLES.md)。
|
||||
|
||||
---
|
||||
|
||||
## Godoc 格式化
|
||||
|
||||
> 在格式化 godoc 标题、链接、列表或代码块,使用信号增强来标记弃用通知,或在本地预览文档输出时,请阅读 [references/FORMATTING.md](references/FORMATTING.md)。
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 主题 | 关键规则 |
|
||||
|------|---------|
|
||||
| 文档注释 | 以名称开头,使用完整句子 |
|
||||
| 行长度 | 约 80 字符,优先考虑可读性 |
|
||||
| 包注释 | 每个包一个,放在 `package` 声明之前 |
|
||||
| 参数 | 仅记录非显而易见的行为 |
|
||||
| 上下文 | 记录与隐含行为不同的例外情况 |
|
||||
| 并发 | 记录线程安全性不明确的情况 |
|
||||
| 清理 | 始终记录资源释放要求 |
|
||||
| 错误 | 记录哨兵值和类型(注意指针) |
|
||||
| 示例 | 在测试文件中使用可运行示例 |
|
||||
| 格式化 | 空行分隔段落,缩进表示代码 |
|
||||
|
||||
---
|
||||
|
||||
## 相关技能
|
||||
|
||||
- **命名约定**:在为文档注释描述的标识符选择名称时,参见 [go-naming](../go-naming/SKILL.md)
|
||||
- **测试示例**:在编写出现在 godoc 中的可运行 `Example` 测试函数时,参见 [go-testing](../go-testing/SKILL.md)
|
||||
- **Lint 强制执行**:在使用 revive 或其他 linter 强制执行文档注释存在性时,参见 [go-linting](../go-linting/SKILL.md)
|
||||
- **风格原则**:在平衡文档详细程度与清晰简洁时,参见 [go-style-core](../go-style-core/SKILL.md)
|
||||
@@ -0,0 +1,61 @@
|
||||
// Package example demonstrates proper Go documentation conventions.
|
||||
//
|
||||
// This package shows how to write doc comments for packages, types,
|
||||
// functions, methods, and constants following Google Go Style Guide
|
||||
// conventions.
|
||||
//
|
||||
// # Getting Started
|
||||
//
|
||||
// Create a new Widget with [NewWidget]:
|
||||
//
|
||||
// w := example.NewWidget("name")
|
||||
// defer w.Close()
|
||||
package example
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrNotFound is returned when a requested item does not exist.
|
||||
var ErrNotFound = errors.New("example: not found")
|
||||
|
||||
// MaxRetries is the default number of retry attempts.
|
||||
const MaxRetries = 3
|
||||
|
||||
// Widget processes items with configurable options.
|
||||
//
|
||||
// A zero-value Widget is not valid; use [NewWidget] to create one.
|
||||
// Widget is safe for concurrent use.
|
||||
//
|
||||
// # Cleanup
|
||||
//
|
||||
// Call [Widget.Close] when done to release resources.
|
||||
type Widget struct {
|
||||
name string
|
||||
}
|
||||
|
||||
// NewWidget creates a Widget with the given name.
|
||||
//
|
||||
// Name must be non-empty; NewWidget panics otherwise.
|
||||
func NewWidget(name string) *Widget {
|
||||
if name == "" {
|
||||
panic("example: name must be non-empty")
|
||||
}
|
||||
return &Widget{name: name}
|
||||
}
|
||||
|
||||
// Process handles the given input and returns the result.
|
||||
//
|
||||
// Process returns [ErrNotFound] if the input references
|
||||
// a missing item.
|
||||
func (w *Widget) Process(input string) (string, error) {
|
||||
return input, nil
|
||||
}
|
||||
|
||||
// Close releases resources held by the Widget.
|
||||
func (w *Widget) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: Use [NewWidget] with functional options instead.
|
||||
func NewWidgetLegacy(name string) *Widget {
|
||||
return NewWidget(name)
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
# 文档约定参考
|
||||
|
||||
## 参数和配置
|
||||
|
||||
> **建议**:记录容易出错或非显而易见的参数,而非所有参数。
|
||||
|
||||
```go
|
||||
// 不好:重复了显而易见的信息
|
||||
// Sprintf formats according to a format specifier and returns the resulting string.
|
||||
//
|
||||
// format is the format, and data is the interpolation data.
|
||||
func Sprintf(format string, data ...any) string
|
||||
|
||||
// 好:记录了非显而易见的行为
|
||||
// Sprintf formats according to a format specifier and returns the resulting string.
|
||||
//
|
||||
// The provided data is used to interpolate the format string. If the data does
|
||||
// not match the expected format verbs or the amount of data does not satisfy
|
||||
// the format specification, the function will inline warnings about formatting
|
||||
// errors into the output string.
|
||||
func Sprintf(format string, data ...any) string
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 上下文
|
||||
|
||||
> **建议**:不要重复隐含的上下文行为;记录例外情况。
|
||||
|
||||
上下文取消被隐含地认为会中断函数并返回 `ctx.Err()`。不要记录这一点。
|
||||
|
||||
```go
|
||||
// 不好:重复了隐含的行为
|
||||
// Run executes the worker's run loop.
|
||||
//
|
||||
// The method will process work until the context is cancelled.
|
||||
func (Worker) Run(ctx context.Context) error
|
||||
|
||||
// 好:只记录关键信息
|
||||
// Run executes the worker's run loop.
|
||||
func (Worker) Run(ctx context.Context) error
|
||||
```
|
||||
|
||||
**当行为不同时记录:**
|
||||
|
||||
```go
|
||||
// 好:非标准的取消行为
|
||||
// Run executes the worker's run loop.
|
||||
//
|
||||
// If the context is cancelled, Run returns a nil error.
|
||||
func (Worker) Run(ctx context.Context) error
|
||||
|
||||
// 好:特殊的上下文要求
|
||||
// NewReceiver starts receiving messages sent to the specified queue.
|
||||
// The context should not have a deadline.
|
||||
func NewReceiver(ctx context.Context) *Receiver
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 并发
|
||||
|
||||
> **建议**:记录非显而易见的线程安全特性。
|
||||
|
||||
只读操作被认为是安全的;修改操作被认为是不安全的。不要重复说明这一点。
|
||||
|
||||
**何时记录:**
|
||||
|
||||
```go
|
||||
// 不明确的操作(看似只读但内部有修改)
|
||||
// Lookup returns the data associated with the key from the cache.
|
||||
//
|
||||
// This operation is not safe for concurrent use.
|
||||
func (*Cache) Lookup(key string) (data []byte, ok bool)
|
||||
|
||||
// API 提供同步机制
|
||||
// NewFortuneTellerClient returns an *rpc.Client for the FortuneTeller service.
|
||||
// It is safe for simultaneous use by multiple goroutines.
|
||||
func NewFortuneTellerClient(cc *rpc.ClientConn) *FortuneTellerClient
|
||||
|
||||
// 接口有并发要求
|
||||
// A Watcher reports the health of some entity (usually a backend service).
|
||||
//
|
||||
// Watcher methods are safe for simultaneous use by multiple goroutines.
|
||||
type Watcher interface {
|
||||
Watch(changed chan<- bool) (unwatch func())
|
||||
Health() error
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 清理
|
||||
|
||||
> **建议**:始终记录显式清理要求。
|
||||
|
||||
```go
|
||||
// 好:
|
||||
// NewTicker returns a new Ticker containing a channel that will send the
|
||||
// current time on the channel after each tick.
|
||||
//
|
||||
// Call Stop to release the Ticker's associated resources when done.
|
||||
func NewTicker(d Duration) *Ticker
|
||||
|
||||
// 好:展示如何清理
|
||||
// Get issues a GET to the specified URL.
|
||||
//
|
||||
// When err is nil, resp always contains a non-nil resp.Body.
|
||||
// Caller should close resp.Body when done reading from it.
|
||||
//
|
||||
// resp, err := http.Get("http://example.com/")
|
||||
// if err != nil {
|
||||
// // handle error
|
||||
// }
|
||||
// defer resp.Body.Close()
|
||||
// body, err := io.ReadAll(resp.Body)
|
||||
func (c *Client) Get(url string) (resp *Response, err error)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误
|
||||
|
||||
> **建议**:记录重要的错误哨兵值和类型。
|
||||
|
||||
```go
|
||||
// 好:记录哨兵值
|
||||
// Read reads up to len(b) bytes from the File and stores them in b.
|
||||
//
|
||||
// At end of file, Read returns 0, io.EOF.
|
||||
func (*File) Read(b []byte) (n int, err error)
|
||||
|
||||
// 好:记录错误类型(包含指针接收者)
|
||||
// Chdir changes the current working directory to the named directory.
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func Chdir(dir string) error
|
||||
```
|
||||
|
||||
注意使用 `*PathError`(而非 `PathError`)可以确保 `errors.Is` 和 `errors.As` 的正确使用。
|
||||
|
||||
对于包级别的错误约定,在包注释中记录。
|
||||
|
||||
---
|
||||
|
||||
## 命名返回参数
|
||||
|
||||
> **建议**:在类型本身不够清晰时用于文档说明。
|
||||
|
||||
```go
|
||||
// 好:多个同类型参数
|
||||
func (n *Node) Children() (left, right *Node, err error)
|
||||
|
||||
// 好:面向操作的名称阐明了用法
|
||||
// The caller must arrange for the returned cancel function to be called.
|
||||
func WithTimeout(parent Context, d time.Duration) (ctx Context, cancel func())
|
||||
|
||||
// 不好:类型已经很清晰,命名没有增加信息
|
||||
func (n *Node) Parent1() (node *Node)
|
||||
func (n *Node) Parent2() (node *Node, err error)
|
||||
|
||||
// 好:类型已足够
|
||||
func (n *Node) Parent1() *Node
|
||||
func (n *Node) Parent2() (*Node, error)
|
||||
```
|
||||
|
||||
不要仅为启用裸返回而命名返回值。清晰性 > 简洁性。
|
||||
|
||||
---
|
||||
|
||||
## 弃用通知
|
||||
|
||||
> **建议**:使用 `// Deprecated:` 注释标记符号为已弃用。
|
||||
|
||||
`Deprecated:` 段落必须出现在文档注释中紧接在符号之前。应说明使用什么替代。
|
||||
|
||||
**标准格式:**
|
||||
|
||||
```
|
||||
// Deprecated: Use NewThing instead.
|
||||
```
|
||||
|
||||
Godoc 会以特殊的视觉样式渲染 `Deprecated:` 注释,使其容易被发现。
|
||||
|
||||
**函数弃用:**
|
||||
|
||||
```go
|
||||
// EstimateSize returns an approximate byte count.
|
||||
//
|
||||
// Deprecated: Use [Size] instead, which returns an exact count.
|
||||
func EstimateSize(r io.Reader) (int64, error)
|
||||
```
|
||||
|
||||
**类型弃用:**
|
||||
|
||||
```go
|
||||
// LegacyClient talks to the v1 API.
|
||||
//
|
||||
// Deprecated: Use [Client] instead, which supports v2.
|
||||
type LegacyClient struct{ /* ... */ }
|
||||
```
|
||||
|
||||
**包弃用** — 在包文档注释中添加 `Deprecated:`:
|
||||
|
||||
```go
|
||||
// Package old provides the original implementation.
|
||||
//
|
||||
// Deprecated: Use package example/new instead.
|
||||
package old
|
||||
```
|
||||
|
||||
始终建议具体的替代方案,让调用者知道迁移目标。
|
||||
|
||||
---
|
||||
|
||||
## 注释语句 — 详细说明
|
||||
|
||||
> **规范**:文档注释必须是完整的句子。
|
||||
|
||||
- 首字母大写,以标点符号结尾
|
||||
- 例外:如果含义清晰,可以以小写标识符开头
|
||||
- 结构体字段的行尾注释可以是短语:
|
||||
|
||||
```go
|
||||
// 好:
|
||||
// A Server handles serving quotes from Shakespeare.
|
||||
type Server struct {
|
||||
// BaseDir points to the base directory for Shakespeare's works.
|
||||
//
|
||||
// Expected structure:
|
||||
// {BaseDir}/manifest.json
|
||||
// {BaseDir}/{name}/{name}-part{number}.txt
|
||||
BaseDir string
|
||||
|
||||
WelcomeMessage string // 用户登录时显示
|
||||
ProtocolVersion string // 与传入请求进行校验
|
||||
PageLength int // 每页行数(可选;默认值:20)
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,107 @@
|
||||
# 包注释和示例参考
|
||||
|
||||
## 包注释
|
||||
|
||||
> **规范**:每个包必须有且仅有一个包注释。
|
||||
|
||||
```go
|
||||
// 好:
|
||||
// Package math provides basic constants and mathematical functions.
|
||||
//
|
||||
// This package does not guarantee bit-identical results across architectures.
|
||||
package math
|
||||
```
|
||||
|
||||
### Main 包
|
||||
|
||||
使用二进制名称(与 BUILD 文件匹配):
|
||||
|
||||
```go
|
||||
// 好:
|
||||
// The seed_generator command is a utility that generates a Finch seed file
|
||||
// from a set of JSON study configs.
|
||||
package main
|
||||
```
|
||||
|
||||
有效格式:`Binary seed_generator`、`Command seed_generator`、`The seed_generator command`、`Seed_generator ...`
|
||||
|
||||
### doc.go
|
||||
|
||||
- 对于较长的包注释,使用仅包含包注释和 `package` 声明的 `doc.go` 文件
|
||||
- 放在 import 之后的维护者注释不会出现在 Godoc 中
|
||||
- 保持 doc.go 文件专注于面向用户的文档
|
||||
|
||||
```go
|
||||
// Package complex provides advanced mathematical operations for
|
||||
// complex number arithmetic, including polar form conversion,
|
||||
// matrix operations, and numerical integration.
|
||||
//
|
||||
// Basic usage
|
||||
//
|
||||
// Create a complex number and perform operations:
|
||||
//
|
||||
// z := complex.New(3, 4)
|
||||
// magnitude := z.Abs() // 5.0
|
||||
// conjugate := z.Conj() // (3, -4)
|
||||
//
|
||||
// Matrix operations
|
||||
//
|
||||
// The package supports complex-valued matrices:
|
||||
//
|
||||
// m := complex.NewMatrix(2, 2)
|
||||
// m.Set(0, 0, complex.New(1, 0))
|
||||
// det := m.Det()
|
||||
package complex
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 可运行示例
|
||||
|
||||
> **建议**:提供可运行示例来展示包的用法。
|
||||
|
||||
将示例放在测试文件(`*_test.go`)中:
|
||||
|
||||
```go
|
||||
// 好:
|
||||
func ExampleConfig_WriteTo() {
|
||||
cfg := &Config{
|
||||
Name: "example",
|
||||
}
|
||||
if err := cfg.WriteTo(os.Stdout); err != nil {
|
||||
log.Exitf("Failed to write config: %s", err)
|
||||
}
|
||||
// Output:
|
||||
// {
|
||||
// "name": "example"
|
||||
// }
|
||||
}
|
||||
```
|
||||
|
||||
示例会出现在 Godoc 中,附加到对应的文档元素上。
|
||||
|
||||
### 命名约定
|
||||
|
||||
| 函数名称 | 文档对象 |
|
||||
|----------|---------|
|
||||
| `Example()` | 包级别示例 |
|
||||
| `ExampleFoo()` | 函数 `Foo` |
|
||||
| `ExampleBar_Baz()` | 方法 `Bar.Baz` |
|
||||
| `ExampleFoo_suffix()` | `Foo` 示例的命名变体 |
|
||||
|
||||
### 技巧
|
||||
|
||||
- 使用 `// Output:` 注释使示例可通过 `go test` 进行测试和验证
|
||||
- 保持示例专注于展示一个概念
|
||||
- 使用真实但精简的数据
|
||||
- 对于复杂的设置,使用 `testMain` 或辅助函数保持示例主体简洁
|
||||
- 同一符号的多个示例使用小写 `_suffix`:
|
||||
|
||||
```go
|
||||
func ExampleNewClient_withTimeout() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
client := NewClient(ctx)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,85 @@
|
||||
# Godoc 格式化参考
|
||||
|
||||
## Godoc 格式化
|
||||
|
||||
> **建议**:使用 godoc 语法编写格式良好的文档。
|
||||
|
||||
**段落** - 用空行分隔:
|
||||
|
||||
```go
|
||||
// 好:
|
||||
// LoadConfig reads a configuration out of the named file.
|
||||
//
|
||||
// See some/shortlink for config file format details.
|
||||
```
|
||||
|
||||
**逐字/代码块** - 额外缩进两个空格:
|
||||
|
||||
```go
|
||||
// 好:
|
||||
// Update runs the function in an atomic transaction.
|
||||
//
|
||||
// This is typically used with an anonymous TransactionFunc:
|
||||
//
|
||||
// if err := db.Update(func(state *State) { state.Foo = bar }); err != nil {
|
||||
// //...
|
||||
// }
|
||||
```
|
||||
|
||||
**列表和表格** - 使用逐字格式:
|
||||
|
||||
```go
|
||||
// 好:
|
||||
// LoadConfig treats the following keys in special ways:
|
||||
// "import" will make this configuration inherit from the named file.
|
||||
// "env" if present will be populated with the system environment.
|
||||
```
|
||||
|
||||
**标题** - 单行,首字母大写,无标点(括号/逗号除外),后跟段落:
|
||||
|
||||
```go
|
||||
// 好:
|
||||
// Using headings
|
||||
//
|
||||
// Headings come with autogenerated anchor tags for easy linking.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 信号增强
|
||||
|
||||
> **建议**:添加注释以突出不寻常或容易被忽略的模式。
|
||||
|
||||
以下两种情况很难区分:
|
||||
|
||||
```go
|
||||
if err := doSomething(); err != nil { // 常见
|
||||
// ...
|
||||
}
|
||||
|
||||
if err := doSomething(); err == nil { // 不寻常!
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
添加注释来增强信号:
|
||||
|
||||
```go
|
||||
// 好:
|
||||
if err := doSomething(); err == nil { // 如果没有错误
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文档预览
|
||||
|
||||
> **建议**:在代码审查之前和期间预览文档。
|
||||
|
||||
```bash
|
||||
go install golang.org/x/pkgsite/cmd/pkgsite@latest
|
||||
pkgsite
|
||||
```
|
||||
|
||||
这可以验证 godoc 格式化是否正确渲染。
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.0.0"
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
$SCRIPT_NAME v$VERSION — Check for missing doc comments on exported Go symbols
|
||||
|
||||
USAGE
|
||||
bash $SCRIPT_NAME [options] [path]
|
||||
|
||||
DESCRIPTION
|
||||
Scans Go source files for exported functions, types, methods, constants,
|
||||
and variables that lack doc comments. Go convention requires all exported
|
||||
symbols to have a doc comment starting with the symbol name.
|
||||
|
||||
Exits 0 if all exports are documented, 1 if undocumented exports found,
|
||||
2 on error.
|
||||
|
||||
OPTIONS
|
||||
-h, --help Show this help message
|
||||
-v, --version Show version
|
||||
--json Output results as JSON
|
||||
--strict Also check unexported types/functions with 5+ lines
|
||||
--limit N Show at most N results (default: all)
|
||||
|
||||
ARGUMENTS
|
||||
path Directory or file to check (default: ./...)
|
||||
|
||||
EXAMPLES
|
||||
bash $SCRIPT_NAME
|
||||
bash $SCRIPT_NAME ./pkg/api
|
||||
bash $SCRIPT_NAME --json .
|
||||
bash $SCRIPT_NAME --strict ./internal/server
|
||||
EOF
|
||||
}
|
||||
|
||||
JSON_OUTPUT=false
|
||||
STRICT=false
|
||||
LIMIT=0
|
||||
TARGET=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-v|--version) echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
|
||||
--json) JSON_OUTPUT=true; shift ;;
|
||||
--strict) STRICT=true; shift ;;
|
||||
--limit) LIMIT="${2:?error: --limit requires a number}"; shift 2 ;;
|
||||
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;;
|
||||
*) TARGET="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
TARGET="${TARGET:-./...}"
|
||||
|
||||
json_escape() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
s="${s//$'\r'/}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
find_go_files() {
|
||||
local t="$1"
|
||||
if [[ -f "$t" ]]; then
|
||||
echo "$t"
|
||||
elif [[ -d "$t" ]]; then
|
||||
find "$t" -name '*.go' ! -name '*_test.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
|
||||
else
|
||||
local dir="${t%%/...}"
|
||||
dir="${dir:-.}"
|
||||
if [[ -d "$dir" ]]; then
|
||||
find "$dir" -name '*.go' ! -name '*_test.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
|
||||
else
|
||||
echo "error: path not found: $t" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
MISSING=()
|
||||
|
||||
add_missing() {
|
||||
local file="$1" line="$2" kind="$3" name="$4"
|
||||
MISSING+=("${file}:${line}|${kind}|${name}")
|
||||
}
|
||||
|
||||
check_file() {
|
||||
local file="$1"
|
||||
local prev_line=""
|
||||
local prev_prev_line=""
|
||||
local line_num=0
|
||||
|
||||
local in_grouped_block=false
|
||||
local grouped_kind=""
|
||||
|
||||
local re_method='^func[[:space:]]+\([^)]+\)[[:space:]]+([A-Z][a-zA-Z0-9]*)\('
|
||||
local re_func='^func[[:space:]]+([A-Z][a-zA-Z0-9]*)\('
|
||||
local re_unexported_func='^func[[:space:]]+([a-z][a-zA-Z0-9]*)\('
|
||||
local re_grouped_open='^(const|var|type)[[:space:]]*\($'
|
||||
local re_exported_type='^type[[:space:]]+([A-Z][a-zA-Z0-9]*)[[:space:]]'
|
||||
local re_unexported_type='^type[[:space:]]+([a-z][a-zA-Z0-9]*)[[:space:]]'
|
||||
local re_exported_const='^const[[:space:]]+([A-Z][a-zA-Z0-9]*)[[:space:]]'
|
||||
local re_exported_var='^var[[:space:]]+([A-Z][a-zA-Z0-9]*)[[:space:]]'
|
||||
local re_grouped_exported='^[[:space:]]+([A-Z][a-zA-Z0-9]*)'
|
||||
local re_grouped_unexported='^[[:space:]]+([a-z][a-zA-Z0-9]*)'
|
||||
|
||||
while IFS= read -r line; do
|
||||
line_num=$((line_num + 1))
|
||||
|
||||
# Check exported function/method declarations
|
||||
if [[ "$line" =~ ^func[[:space:]] ]]; then
|
||||
local name=""
|
||||
local kind=""
|
||||
# Method: func (r *Type) Name(
|
||||
if [[ "$line" =~ $re_method ]]; then
|
||||
name="${BASH_REMATCH[1]}"
|
||||
kind="method"
|
||||
# Function: func Name(
|
||||
elif [[ "$line" =~ $re_func ]]; then
|
||||
name="${BASH_REMATCH[1]}"
|
||||
kind="function"
|
||||
fi
|
||||
|
||||
if [[ -n "$name" ]]; then
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
add_missing "$file" "$line_num" "$kind" "$name"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Strict mode: also check unexported functions
|
||||
if $STRICT && [[ -z "$name" ]] && [[ "$line" =~ $re_unexported_func ]]; then
|
||||
name="${BASH_REMATCH[1]}"
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
add_missing "$file" "$line_num" "function" "$name"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check exported type declarations
|
||||
if [[ "$line" =~ $re_exported_type ]]; then
|
||||
local name="${BASH_REMATCH[1]}"
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
add_missing "$file" "$line_num" "type" "$name"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Strict mode: also check unexported type declarations
|
||||
if $STRICT && [[ "$line" =~ $re_unexported_type ]]; then
|
||||
local name="${BASH_REMATCH[1]}"
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
add_missing "$file" "$line_num" "type" "$name"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check exported const (single-line, not in block)
|
||||
if [[ "$line" =~ $re_exported_const ]]; then
|
||||
local name="${BASH_REMATCH[1]}"
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
add_missing "$file" "$line_num" "const" "$name"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check exported var (single-line, not blank identifier)
|
||||
if [[ "$line" =~ $re_exported_var ]]; then
|
||||
local name="${BASH_REMATCH[1]}"
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
add_missing "$file" "$line_num" "var" "$name"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check package comment
|
||||
if [[ "$line" =~ ^package[[:space:]]+ ]]; then
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
local pkg_name
|
||||
pkg_name=$(echo "$line" | sed 's/^package[[:space:]]*//;s/[[:space:]]*$//')
|
||||
add_missing "$file" "$line_num" "package" "$pkg_name"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Track grouped declaration blocks: const ( ... ), var ( ... ), type ( ... )
|
||||
if [[ "$line" =~ $re_grouped_open ]]; then
|
||||
in_grouped_block=true
|
||||
grouped_kind="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
if $in_grouped_block && [[ "$line" =~ ^\)[[:space:]]*$ ]]; then
|
||||
in_grouped_block=false
|
||||
grouped_kind=""
|
||||
fi
|
||||
if $in_grouped_block && [[ -n "$grouped_kind" ]]; then
|
||||
# Check for exported names inside grouped block
|
||||
if [[ "$line" =~ $re_grouped_exported ]]; then
|
||||
local gname="${BASH_REMATCH[1]}"
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
add_missing "$file" "$line_num" "$grouped_kind" "$gname"
|
||||
fi
|
||||
fi
|
||||
# Strict: also check unexported names in grouped blocks
|
||||
if $STRICT && [[ "$line" =~ $re_grouped_unexported ]]; then
|
||||
local gname="${BASH_REMATCH[1]}"
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
add_missing "$file" "$line_num" "$grouped_kind" "$gname"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
prev_prev_line="$prev_line"
|
||||
prev_line="$line"
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
is_documented() {
|
||||
local prev="$1"
|
||||
local prev_prev="$2"
|
||||
# Previous line is a comment (// or end of block comment */)
|
||||
if [[ "$prev" =~ ^[[:space:]]*//.* ]] || [[ "$prev" =~ \*/[[:space:]]*$ ]]; then
|
||||
return 0
|
||||
fi
|
||||
# Previous line might be empty but line before is comment (allow one blank line)
|
||||
if [[ -z "${prev// /}" ]] && [[ "$prev_prev" =~ ^[[:space:]]*//.* ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
FILES=()
|
||||
while IFS= read -r f; do
|
||||
[[ -n "$f" ]] && FILES+=("$f")
|
||||
done < <(find_go_files "$TARGET")
|
||||
|
||||
if [[ ${#FILES[@]} -eq 0 ]]; then
|
||||
if $JSON_OUTPUT; then
|
||||
echo '{"missing":[],"count":0,"status":"no_go_files"}'
|
||||
else
|
||||
echo "No Go files found in: $TARGET"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for file in "${FILES[@]}"; do
|
||||
check_file "$file"
|
||||
done
|
||||
|
||||
# Truncation
|
||||
TOTAL=${#MISSING[@]}
|
||||
TRUNCATED=false
|
||||
if [[ $LIMIT -gt 0 && $TOTAL -gt $LIMIT ]]; then
|
||||
MISSING=("${MISSING[@]:0:$LIMIT}")
|
||||
TRUNCATED=true
|
||||
fi
|
||||
|
||||
if $JSON_OUTPUT; then
|
||||
echo "{"
|
||||
echo ' "missing": ['
|
||||
first=true
|
||||
for entry in "${MISSING[@]+"${MISSING[@]}"}"; do
|
||||
IFS='|' read -r location kind name <<< "$entry"
|
||||
file="${location%%:*}"
|
||||
line="${location#*:}"
|
||||
$first || echo ","
|
||||
first=false
|
||||
printf ' {"file":"%s","line":%s,"kind":"%s","name":"%s"}' \
|
||||
"$(json_escape "$file")" "$line" "$(json_escape "$kind")" "$(json_escape "$name")"
|
||||
done
|
||||
echo ""
|
||||
echo " ],"
|
||||
printf ' "total": %d,\n' "$TOTAL"
|
||||
printf ' "truncated": %s\n' "$TRUNCATED"
|
||||
echo "}"
|
||||
else
|
||||
if [[ $TOTAL -eq 0 ]]; then
|
||||
echo "All exported symbols are documented."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Undocumented exported symbols:"
|
||||
echo ""
|
||||
for entry in "${MISSING[@]}"; do
|
||||
IFS='|' read -r location kind name <<< "$entry"
|
||||
printf " %s [%s] %s\n" "$location" "$kind" "$name"
|
||||
done
|
||||
if $TRUNCATED; then
|
||||
echo " ... and $((TOTAL - LIMIT)) more (use --limit to adjust)"
|
||||
fi
|
||||
echo ""
|
||||
echo "Total: $TOTAL undocumented symbol(s)"
|
||||
fi
|
||||
|
||||
if [[ $TOTAL -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
Reference in New Issue
Block a user