返回 Skills
cxuu/golang-skills· Apache-2.0 内容可用

go-documentation

Use when writing or reviewing documentation for Go packages, types, functions, or methods. Also use proactively when creating new exported types, functions, or packages, even if the user doesn't explicitly ask about documentation. Does not cover code comments for non-exported symbols (see go-style-core).

安装

与 skills.sh 相同的 Command / Prompt 安装方式


name: go-documentation description: Use when writing or reviewing documentation for Go packages, types, functions, or methods. Also use proactively when creating new exported types, functions, or packages, even if the user doesn't explicitly ask about documentation. Does not cover code comments for non-exported symbols (see go-style-core). allowed-tools: Bash(bash:*)

Go Documentation

Resource Routing

  • scripts/check-docs.sh - Run when checking exported functions, types, methods, constants, and packages for missing doc comments.
  • scripts/check-docs-ast.go - Implementation helper invoked by check-docs.sh; patch this when changing documentation analysis behavior.
  • assets/doc-template.go - Use when starting a documented package or exported API.
  • references/CONVENTIONS.md - Read when documenting parameters, context behavior, concurrency safety, cleanup, errors, or named results.
  • references/EXAMPLES.md - Read when adding runnable examples or package examples.
  • references/FORMATTING.md - Read when formatting Godoc lists, paragraphs, links, and code blocks.

Doc Comments

Normative: All top-level exported names must have doc comments.

Basic Rules

  1. Begin with the name of the object being described
  2. An article ("a", "an", "the") may precede the name
  3. Use full sentences (capitalized, punctuated)
// 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) { ...

Unexported types/functions with unobvious behavior should also have doc comments.

Validation: After adding doc comments, run bash scripts/check-docs.sh to verify no exported symbols are missing documentation. Fix any gaps before proceeding.


Comment Sentences

Normative: Documentation comments must be complete sentences.

  • Capitalize the first word, end with punctuation
  • Exception: may begin with uncapitalized identifier if clear
  • End-of-line comments for struct fields can be phrases

Comment Line Length

Advisory: Aim for ~80 columns, but no hard limit.

Break based on punctuation. Don't split long URLs.


Struct Documentation

Group fields with section comments. Mark optional fields with defaults:

type Options struct {
    // General setup:
    Name  string
    Group *FooGroup

    // Customization:
    LargeGroupThreshold int // optional; default: 10
}

Package Comments

Normative: Every package must have exactly one package comment.

// Package math provides basic constants and mathematical functions.
package math
  • For main packages, use the binary name: // The seed_generator command ...
  • For long package comments, use a doc.go file

What to Document

Advisory: Document non-obvious behavior, not obvious behavior.

TopicDocument when...Skip when...
ParametersNon-obvious behavior, edge casesRestates the type signature
ContextsBehavior differs from standard cancellationStandard ctx.Err() return
ConcurrencyAmbiguous thread safety (e.g., read that mutates)Read-only is safe, mutation is unsafe
CleanupAlways document resource release
ErrorsSentinel values, error types (use *PathError)
Named resultsMultiple params of same type, action-oriented namesType alone is clear enough

Key principles:

  • Context cancellation returning ctx.Err() is implied — don't restate it
  • Read-only ops are assumed thread-safe; mutations assumed unsafe — don't restate
  • Always document cleanup requirements (e.g., Call Stop to release resources)
  • Use pointer in error type docs (*PathError) for correct errors.Is/errors.As
  • Don't name results just to enable naked returns — clarity > brevity

Runnable Examples

Advisory: Provide runnable examples in test files (*_test.go).

func ExampleConfig_WriteTo() {
    cfg := &Config{Name: "example"}
    cfg.WriteTo(os.Stdout)
    // Output:
    // {"name": "example"}
}

Examples appear in Godoc attached to the documented element.


Quick Reference

TopicKey Rule
Doc commentsStart with name, use full sentences
Line length~80 chars, prioritize readability
Package commentsOne per package, above package clause
ParametersDocument non-obvious behavior only
ContextsDocument exceptions to implied behavior
ConcurrencyDocument ambiguous thread safety
CleanupAlways document resource release
ErrorsDocument sentinels and types (note pointer)
ExamplesUse runnable examples in test files
FormattingBlank lines for paragraphs, indent for code

Related Skills

  • Naming conventions: See go-naming when choosing names for the identifiers your doc comments describe
  • Testing examples: See go-testing when writing runnable Example test functions that appear in godoc
  • Linting enforcement: See go-linting when using revive or other linters to enforce doc comment presence
  • Style principles: See go-style-core when balancing documentation verbosity against clarity and concision

附带文件

assets/doc-template.go
// 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)
}
references/CONVENTIONS.md
# Documentation Conventions Reference

> Sources: source/google-go-styleguide/decisions.md; source/google-go-styleguide/best-practices.md; source/golang-wiki/CodeReviewComments.md
> Authority: normative
> Minimum Go: any supported Go version
> Last verified: 2026-06-19

## Contents

- [Parameters and Configuration](#parameters-and-configuration)
- [Contexts](#contexts)
- [Concurrency](#concurrency)
- [Cleanup](#cleanup)
- [Errors](#errors)
- [Named Result Parameters](#named-result-parameters)
- [Deprecation Notices](#deprecation-notices)
- [Comment Sentences - Detailed](#comment-sentences--detailed)

## Parameters and Configuration

> **Advisory**: Document error-prone or non-obvious parameters, not everything.

```go
// Bad: Restates the obvious
// 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

// Good: Documents non-obvious behavior
// 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
```

---

## Contexts

> **Advisory**: Don't restate implied context behavior; document exceptions.

Context cancellation is implied to interrupt the function and return
`ctx.Err()`. Don't document this.

```go
// Bad: Restates implied behavior
// Run executes the worker's run loop.
//
// The method will process work until the context is cancelled.
func (Worker) Run(ctx context.Context) error

// Good: Just the essential
// Run executes the worker's run loop.
func (Worker) Run(ctx context.Context) error
```

**Document when behavior differs:**

```go
// Good: Non-standard cancellation behavior
// Run executes the worker's run loop.
//
// If the context is cancelled, Run returns a nil error.
func (Worker) Run(ctx context.Context) error

// Good: Special context requirements
// NewReceiver starts receiving messages sent to the specified queue.
// The context should not have a deadline.
func NewReceiver(ctx context.Context) *Receiver
```

---

## Concurrency

> **Advisory**: Document non-obvious thread safety characteristics.

Read-only operations are assumed safe; mutating operations are assumed unsafe.
Don't restate this.

**Document when:**

```go
// Ambiguous operation (looks read-only but mutates internally)
// 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 provides synchronization
// NewFortuneTellerClient returns an *rpc.Client for the FortuneTeller service.
// It is safe for simultaneous use by multiple goroutines.
func NewFortuneTellerClient(cc *rpc.ClientConn) *FortuneTellerClient

// Interface has concurrency requirements
// 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
}
```

---

## Cleanup

> **Advisory**: Always document explicit cleanup requirements.

```go
// Good:
// 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

// Good: Show how to clean up
// 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)
```

---

## Errors

> **Advisory**: Document significant error sentinel values and types.

```go
// Good: Document sentinel values
// 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)

// Good: Document error types (include pointer receiver)
// 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
```

Noting `*PathError` (not `PathError`) enables correct use of `errors.Is` and
`errors.As`.

For package-wide error conventions, document in the package comment.

---

## Named Result Parameters

> **Advisory**: Use for documentation when types alone aren't clear enough.

```go
// Good: Multiple params of same type
func (n *Node) Children() (left, right *Node, err error)

// Good: Action-oriented name clarifies usage
// The caller must arrange for the returned cancel function to be called.
func WithTimeout(parent Context, d time.Duration) (ctx Context, cancel func())

// Bad: Type already clear, name adds nothing
func (n *Node) Parent1() (node *Node)
func (n *Node) Parent2() (node *Node, err error)

// Good: Type is sufficient
func (n *Node) Parent1() *Node
func (n *Node) Parent2() (*Node, error)
```

Don't name results just to enable naked returns. Clarity > brevity.

---

## Deprecation Notices

> **Advisory**: Use the `// Deprecated:` comment to mark symbols as deprecated.

The `Deprecated:` paragraph must appear in the doc comment immediately before
the symbol. It should state what to use instead.

**Standard format:**

```
// Deprecated: Use NewThing instead.
```

Godoc renders `Deprecated:` comments with special visual styling, making them
easy to spot.

**Function deprecation:**

```go
// EstimateSize returns an approximate byte count.
//
// Deprecated: Use [Size] instead, which returns an exact count.
func EstimateSize(r io.Reader) (int64, error)
```

**Type deprecation:**

```go
// LegacyClient talks to the v1 API.
//
// Deprecated: Use [Client] instead, which supports v2.
type LegacyClient struct{ /* ... */ }
```

**Package deprecation** — add `Deprecated:` to the package doc comment:

```go
// Package old provides the original implementation.
//
// Deprecated: Use package example/new instead.
package old
```

Always suggest a concrete alternative so callers know what to migrate to.

---

## Comment Sentences — Detailed

> **Normative**: Documentation comments must be complete sentences.

- Capitalize the first word, end with punctuation
- Exception: may begin with uncapitalized identifier if clear
- End-of-line comments for struct fields can be phrases:

```go
// Good:
// 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 // displayed when user logs in
    ProtocolVersion string // checked against incoming requests
    PageLength      int    // lines per page (optional; default: 20)
}
```
references/EXAMPLES.md
# Package Comments and Examples Reference

## Package Comments

> **Normative**: Every package must have exactly one package comment.

```go
// Good:
// Package math provides basic constants and mathematical functions.
//
// This package does not guarantee bit-identical results across architectures.
package math
```

### Main Packages

Use the binary name (matching the BUILD file):

```go
// Good:
// The seed_generator command is a utility that generates a Finch seed file
// from a set of JSON study configs.
package main
```

Valid styles: `Binary seed_generator`, `Command seed_generator`, `The
seed_generator command`, `Seed_generator ...`

### doc.go

- For long package comments, use a `doc.go` file containing only the package
  comment and the `package` clause
- Maintainer comments placed after imports don't appear in Godoc
- Keep the doc.go file focused on user-facing documentation

```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
```

---

## Runnable Examples

> **Advisory**: Provide runnable examples to demonstrate package usage.

Place examples in test files (`*_test.go`):

```go
// Good:
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"
    // }
}
```

Examples appear in Godoc attached to the documented element.

### Naming Conventions

| Function Name | Documents |
|---------------|-----------|
| `Example()` | Package-level example |
| `ExampleFoo()` | Function `Foo` |
| `ExampleBar_Baz()` | Method `Bar.Baz` |
| `ExampleFoo_suffix()` | Named variant of `Foo` example |

### Tips

- Use `// Output:` comments to make examples testable and verifiable by `go test`
- Keep examples focused on demonstrating one concept
- Use realistic but minimal data
- For complex setup, use a `testMain` or helper to keep the example body clean
- Multiple examples for the same symbol use a lowercase `_suffix`:

```go
func ExampleNewClient_withTimeout() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    client := NewClient(ctx)
    // ...
}
```
references/FORMATTING.md
# Godoc Formatting Reference

## Godoc Formatting

> **Advisory**: Use godoc syntax for well-formatted documentation.

**Paragraphs** - Separate with blank lines:

```go
// Good:
// LoadConfig reads a configuration out of the named file.
//
// See some/shortlink for config file format details.
```

**Verbatim/Code blocks** - Indent by two additional spaces:

```go
// Good:
// 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 {
//     //...
//   }
```

**Lists and tables** - Use verbatim formatting:

```go
// Good:
// 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.
```

**Headings** - Single line, capital letter, no punctuation (except
parens/commas), followed by paragraph:

```go
// Good:
// Using headings
//
// Headings come with autogenerated anchor tags for easy linking.
```

---

## Signal Boosting

> **Advisory**: Add comments to highlight unusual or easily-missed patterns.

These two are hard to distinguish:

```go
if err := doSomething(); err != nil {  // common
    // ...
}

if err := doSomething(); err == nil {  // unusual!
    // ...
}
```

Add a comment to boost the signal:

```go
// Good:
if err := doSomething(); err == nil { // if NO error
    // ...
}
```

---

## Documentation Preview

> **Advisory**: Preview documentation before and during code review.

```bash
go install golang.org/x/pkgsite/cmd/pkgsite@latest
pkgsite
```

This validates godoc formatting renders correctly.
scripts/check-docs-ast.go
package main

import (
	"encoding/json"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"io/fs"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

const version = "1.1.0"

type missingDoc struct {
	File string `json:"file"`
	Line int    `json:"line"`
	Kind string `json:"kind"`
	Name string `json:"name"`
}

type parseError struct {
	File    string `json:"file"`
	Message string `json:"message"`
}

type parsedFile struct {
	path string
	file *ast.File
}

type packageInfo struct {
	name      string
	firstFile string
	firstLine int
	hasDoc    bool
}

func usage() {
	fmt.Fprintf(os.Stdout, `check-docs.sh v%s - Check for missing doc comments on exported Go symbols

USAGE
    bash check-docs.sh [options] [path]

OPTIONS
    -h, --help       Show this help message
    -v, --version    Show version
    --json           Output results as JSON
    --strict         Also check unexported types/functions
    --limit N        Show at most N results (default: all)
`, version)
}

func main() {
	opts, err := parseArgs(os.Args[1:])
	if err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(2)
	}
	if opts.help {
		usage()
		return
	}
	if opts.version {
		fmt.Printf("check-docs.sh v%s\n", version)
		return
	}

	files, err := findGoFiles(opts.target)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(2)
	}
	if len(files) == 0 {
		if opts.jsonOutput {
			fmt.Println(`{"missing":[],"total":0,"truncated":false,"status":"no_go_files"}`)
		} else {
			fmt.Printf("No Go files found in: %s\n", opts.target)
		}
		return
	}

	fset := token.NewFileSet()
	parsed := make([]parsedFile, 0, len(files))
	parseErrors := []parseError{}
	for _, path := range files {
		file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
		if err != nil {
			parseErrors = append(parseErrors, parseError{File: path, Message: err.Error()})
			continue
		}
		parsed = append(parsed, parsedFile{path: path, file: file})
	}

	packages := map[string]*packageInfo{}
	for _, pf := range parsed {
		key := packageKey(pf.path, pf.file.Name.Name)
		line := fset.Position(pf.file.Package).Line
		info, ok := packages[key]
		if !ok {
			info = &packageInfo{name: pf.file.Name.Name, firstFile: pf.path, firstLine: line}
			packages[key] = info
		}
		if pf.path < info.firstFile {
			info.firstFile = pf.path
			info.firstLine = line
		}
		if pf.file.Doc != nil {
			info.hasDoc = true
		}
	}

	missing := []missingDoc{}
	reportedPackage := map[string]bool{}
	for _, pf := range parsed {
		key := packageKey(pf.path, pf.file.Name.Name)
		info := packages[key]
		if !info.hasDoc && !reportedPackage[key] {
			missing = append(missing, missingDoc{
				File: info.firstFile,
				Line: info.firstLine,
				Kind: "package",
				Name: info.name,
			})
			reportedPackage[key] = true
		}
		missing = append(missing, findMissingDeclDocs(fset, pf, opts.strict)...)
	}

	sort.Slice(missing, func(i, j int) bool {
		if missing[i].File == missing[j].File {
			return missing[i].Line < missing[j].Line
		}
		return missing[i].File < missing[j].File
	})

	total := len(missing)
	truncated := false
	if opts.limit > 0 && total > opts.limit {
		missing = missing[:opts.limit]
		truncated = true
	}

	if opts.jsonOutput {
		out := struct {
			Missing     []missingDoc `json:"missing"`
			Total       int          `json:"total"`
			Truncated   bool         `json:"truncated"`
			Status      string       `json:"status,omitempty"`
			ParseErrors []parseError `json:"parse_errors,omitempty"`
		}{Missing: missing, Total: total, Truncated: truncated}
		if len(parseErrors) > 0 {
			out.Status = "parse_error"
			out.ParseErrors = parseErrors
		}
		data, err := json.Marshal(out)
		if err != nil {
			fmt.Fprintf(os.Stderr, "error: marshal JSON: %v\n", err)
			os.Exit(2)
		}
		fmt.Println(string(data))
	} else {
		if len(parseErrors) > 0 {
			fmt.Println("Malformed Go files:")
			fmt.Println()
			for _, item := range parseErrors {
				fmt.Printf("  %s  %s\n", item.File, item.Message)
			}
			fmt.Println()
		}
		if total == 0 {
			if len(parseErrors) == 0 {
				fmt.Println("All exported symbols are documented.")
				return
			}
		} else {
			fmt.Println("Undocumented exported symbols:")
			fmt.Println()
			for _, item := range missing {
				fmt.Printf("  %s:%d  [%s] %s\n", item.File, item.Line, item.Kind, item.Name)
			}
			if truncated {
				fmt.Printf("  ... and %d more (use --limit to adjust)\n", total-opts.limit)
			}
			fmt.Println()
			fmt.Printf("Total: %d undocumented symbol(s)\n", total)
		}
	}

	if len(parseErrors) > 0 {
		os.Exit(2)
	}
	if total > 0 {
		os.Exit(1)
	}
}

type options struct {
	jsonOutput bool
	strict     bool
	limit      int
	target     string
	help       bool
	version    bool
}

func parseArgs(args []string) (options, error) {
	opts := options{target: "./..."}
	var positionals []string
	for i := 0; i < len(args); i++ {
		arg := args[i]
		switch {
		case arg == "-h" || arg == "--help":
			opts.help = true
		case arg == "-v" || arg == "--version":
			opts.version = true
		case arg == "--json":
			opts.jsonOutput = true
		case arg == "--strict":
			opts.strict = true
		case arg == "--limit":
			if i+1 >= len(args) {
				return opts, fmt.Errorf("--limit requires a number")
			}
			i++
			limit, err := parseNonNegativeInt(args[i])
			if err != nil {
				return opts, fmt.Errorf("--limit must be a non-negative integer, got: %s", args[i])
			}
			opts.limit = limit
		case strings.HasPrefix(arg, "--limit="):
			value := strings.TrimPrefix(arg, "--limit=")
			limit, err := parseNonNegativeInt(value)
			if err != nil {
				return opts, fmt.Errorf("--limit must be a non-negative integer, got: %s", value)
			}
			opts.limit = limit
		case strings.HasPrefix(arg, "-"):
			return opts, fmt.Errorf("unknown option: %s", arg)
		default:
			positionals = append(positionals, arg)
		}
	}
	if len(positionals) > 1 {
		return opts, fmt.Errorf("expected at most one path")
	}
	if len(positionals) == 1 {
		opts.target = positionals[0]
	}
	return opts, nil
}

func parseNonNegativeInt(s string) (int, error) {
	if s == "" {
		return 0, fmt.Errorf("empty")
	}
	n := 0
	for _, r := range s {
		if r < '0' || r > '9' {
			return 0, fmt.Errorf("invalid")
		}
		n = n*10 + int(r-'0')
	}
	return n, nil
}

func findGoFiles(target string) ([]string, error) {
	info, err := os.Stat(target)
	if err == nil {
		if !info.IsDir() {
			if strings.HasSuffix(target, ".go") && !strings.HasSuffix(target, "_test.go") {
				return []string{target}, nil
			}
			return nil, nil
		}
		return walkGoFiles(target)
	}

	if strings.HasSuffix(target, "/...") {
		dir := strings.TrimSuffix(target, "/...")
		if dir == "" {
			dir = "."
		}
		if info, statErr := os.Stat(dir); statErr == nil && info.IsDir() {
			return walkGoFiles(dir)
		}
	}

	return nil, fmt.Errorf("path not found: %s", target)
}

func walkGoFiles(root string) ([]string, error) {
	var files []string
	err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			switch d.Name() {
			case ".git", "vendor":
				return filepath.SkipDir
			}
			return nil
		}
		if strings.HasSuffix(path, ".go") && !strings.HasSuffix(path, "_test.go") {
			files = append(files, path)
		}
		return nil
	})
	sort.Strings(files)
	return files, err
}

func packageKey(path, name string) string {
	return filepath.Dir(path) + "|" + name
}

func findMissingDeclDocs(fset *token.FileSet, pf parsedFile, strict bool) []missingDoc {
	var missing []missingDoc
	for _, decl := range pf.file.Decls {
		switch d := decl.(type) {
		case *ast.FuncDecl:
			if ast.IsExported(d.Name.Name) || strict {
				if d.Doc == nil {
					kind := "function"
					if d.Recv != nil {
						kind = "method"
					}
					missing = append(missing, missingDoc{
						File: pf.path,
						Line: fset.Position(d.Pos()).Line,
						Kind: kind,
						Name: d.Name.Name,
					})
				}
			}
		case *ast.GenDecl:
			for _, spec := range d.Specs {
				switch s := spec.(type) {
				case *ast.TypeSpec:
					if ast.IsExported(s.Name.Name) || strict {
						if s.Doc == nil && d.Doc == nil {
							missing = append(missing, missingDoc{
								File: pf.path,
								Line: fset.Position(s.Pos()).Line,
								Kind: "type",
								Name: s.Name.Name,
							})
						}
					}
				case *ast.ValueSpec:
					for _, name := range s.Names {
						if !ast.IsExported(name.Name) && !strict {
							continue
						}
						if s.Doc == nil && d.Doc == nil {
							missing = append(missing, missingDoc{
								File: pf.path,
								Line: fset.Position(name.Pos()).Line,
								Kind: strings.ToLower(d.Tok.String()),
								Name: name.Name,
							})
						}
					}
				}
			}
		}
	}
	return missing
}
scripts/check-docs.sh
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VERSION="1.1.0"

for arg in "$@"; do
    case "$arg" in
        -h|--help)
            cat <<EOF
check-docs.sh v$VERSION - Check for missing doc comments on exported Go symbols

USAGE
    bash check-docs.sh [options] [path]

OPTIONS
    -h, --help       Show this help message
    -v, --version    Show version
    --json           Output results as JSON
    --strict         Also check unexported types/functions
    --limit N        Show at most N results (default: all)
EOF
            exit 0
            ;;
        -v|--version)
            echo "check-docs.sh v$VERSION"
            exit 0
            ;;
    esac
done

if ! command -v go >/dev/null 2>&1; then
    echo "error: go is not installed or not in PATH" >&2
    exit 2
fi

CACHE_ROOT="${XDG_CACHE_HOME:-${HOME:-${TMPDIR:-/tmp}}/.cache}/golang-skills"
if ! mkdir -p "$CACHE_ROOT"; then
    CACHE_ROOT="${TMPDIR:-/tmp}/golang-skills-cache"
    mkdir -p "$CACHE_ROOT"
fi

SRC="$SCRIPT_DIR/check-docs-ast.go"
STAMP="$(cksum "$SRC" | awk '{print $1 "-" $2}')"
BIN="$CACHE_ROOT/check-docs-ast-$STAMP"

if [[ ! -x "$BIN" ]]; then
    GOCACHE="${GOCACHE:-$CACHE_ROOT/go-build}" go build -o "$BIN" "$SRC"
fi

exec "$BIN" "$@"
    go-documentation | Prompt Minder