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

go-error-handling

Use when writing Go code that returns, wraps, or handles errors — choosing between sentinel errors, custom types, and fmt.Errorf (%w vs %v), structuring error flow, or deciding whether to log or return. Also use when propagating errors across package boundaries or using errors.Is/As, even if the user doesn't ask about error strategy. Does not cover panic/recover patterns (see go-defensive).

安装

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


name: go-error-handling description: Use when writing Go code that returns, wraps, or handles errors — choosing between sentinel errors, custom types, and fmt.Errorf (%w vs %v), structuring error flow, or deciding whether to log or return. Also use when propagating errors across package boundaries or using errors.Is/As, even if the user doesn't ask about error strategy. Does not cover panic/recover patterns (see go-defensive). allowed-tools: Bash(bash:*)

Go Error Handling

Compatibility: errors.Is, errors.As, and %w wrapping require Go 1.13+; structured logging examples may use log/slog from Go 1.21+.

Resource Routing

  • scripts/check-errors.sh - Run when checking string-based error matching, bare error propagation, and log-and-return patterns.
  • scripts/check-errors-ast.go - Implementation helper invoked by check-errors.sh; patch this when changing error-flow analysis behavior.
  • references/ERROR-FLOW.md - Read when deciding where to handle, wrap, log, or return errors.
  • references/ERROR-TYPES.md - Read when choosing sentinel errors, typed errors, or opaque errors.
  • references/WRAPPING.md - Read when choosing %w versus %v or crossing package boundaries.

In Go, errors are values — they are created by code and consumed by code.

Choosing an Error Strategy

  1. System boundary (RPC, IPC, storage)? → Wrap with %v to avoid leaking internals
  2. Caller needs to match specific conditions? → Sentinel or typed error, wrap with %w
  3. Caller just needs debugging context? → fmt.Errorf("...: %w", err)
  4. Leaf function, no wrapping needed? → Return the error directly

Default: wrap with %w and place it at the end of the format string.


Core Rules

Never Return Concrete Error Types

Never return concrete error types from exported functions — a concrete nil pointer can become a non-nil interface:

// Bad: Concrete type can cause subtle bugs
func Bad() *os.PathError { /*...*/ }

// Good: Always return the error interface
func Good() error { /*...*/ }

Error Strings

Error strings should not be capitalized and should not end with punctuation. Exception: exported names, proper nouns, or acronyms.

// Bad
err := fmt.Errorf("Something bad happened.")

// Good
err := fmt.Errorf("something bad happened")

For displayed messages (logs, test failures, API responses), capitalization is appropriate.

Return Values on Error

When a function returns an error, callers must treat all non-error return values as unspecified unless explicitly documented.

Tip: Functions taking a context.Context should usually return an error so callers can determine if the context was cancelled.


Handling Errors

When encountering an error, make a deliberate choice — do not discard with _:

  1. Handle immediately — address the error and continue
  2. Return to caller — optionally wrapped with context
  3. In exceptional caseslog.Fatal or panic

To intentionally ignore: add a comment explaining why.

n, _ := b.Write(p) // never returns a non-nil error

For related concurrent operations, use errgroup:

g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return task1(ctx) })
g.Go(func() error { return task2(ctx) })
if err := g.Wait(); err != nil { return err }

Avoid In-Band Errors

Don't return -1, nil, or empty string to signal errors. Use multiple returns:

// Bad: In-band error value
func Lookup(key string) int  // returns -1 for missing

// Good: Explicit error or ok value
func Lookup(key string) (string, bool)

This prevents callers from writing Parse(Lookup(key)) — it causes a compile-time error since Lookup(key) has 2 outputs.


Error Flow

Handle errors before normal code. Early returns keep the happy path unindented:

// Good: Error first, normal code unindented
if err != nil {
    return err
}
// normal code

Handle errors once — either log or return, never both:

Error encountered?
├─ Caller can act on it? → Return (with context via %w)
├─ Top of call chain? → Log and handle
└─ Neither? → Log at appropriate level, continue

Error Types

Advisory: Recommended best practice.

Caller needs to match?Message typeUse
Nostaticerrors.New("message")
Nodynamicfmt.Errorf("msg: %v", val)
Yesstaticvar ErrFoo = errors.New("...")
Yesdynamiccustom error type

Default: Wrap with fmt.Errorf("...: %w", err). Escalate to sentinels for errors.Is(), to custom types for errors.As().


Error Wrapping

Advisory: Recommended best practice.

  • Use %v: At system boundaries, for logging, to hide internal details
  • Use %w: To preserve error chain for errors.Is/errors.As

Key rules: Place %w at the end. Add context callers don't have. If annotation adds nothing, return err directly.

Validation: After implementing error handling, run bash scripts/check-errors.sh to detect common anti-patterns. Then run go vet ./... to catch additional issues.


Related Skills

  • Error naming: See go-naming when naming sentinel errors (ErrFoo) or custom error types
  • Testing errors: See go-testing when testing error semantics with errors.Is/errors.As or writing error-checking helpers
  • Panic handling: See go-defensive when deciding between panic and error returns, or writing recover guards
  • Guard clauses: See go-control-flow when structuring early-return error flow or reducing nesting
  • Logging decisions: See go-logging when choosing log levels, configuring structured logging, or deciding what context to include in log messages

附带文件

references/ERROR-FLOW.md
# Error Flow Patterns

Detailed patterns for error flow, the handle-once principle, and logging
decisions.

## Indent Error Flow

Handle errors before proceeding with normal code. This improves readability by
enabling the reader to find the normal path quickly.

```go
// Good: Error handling first, normal code unindented
if err != nil {
    // error handling
    return // or continue, etc.
}
// normal code
```

```go
// Bad: Normal code hidden in else clause
if err != nil {
    // error handling
} else {
    // normal code that looks abnormal due to indentation
}
```

### Avoid If-with-Initializer for Long-Lived Variables

If you use a variable for more than a few lines, move the declaration out:

```go
// Good: Declaration separate from error check
x, err := f()
if err != nil {
    return err
}
// lots of code that uses x
// across multiple lines
```

```go
// Bad: Variable scoped to else block, hard to read
if x, err := f(); err != nil {
    return err
} else {
    // lots of code that uses x
    // across multiple lines
}
```

---

## Handle Errors Once

When a caller receives an error, it should handle each error **only once**.
Choose ONE response:

1. **Return the error** (wrapped or verbatim) for the caller to handle
2. **Log and degrade gracefully** (don't return the error)
3. **Match and handle** specific error cases, return others

**If you return an error, don't log it yourself** — let the caller handle it.
Logging and returning the same error is the most common "handle errors once"
violation, causing duplicate noise as callers up the stack also handle the error.

```go
// Bad: Logs AND returns - causes noise in logs
u, err := getUser(id)
if err != nil {
    log.Printf("Could not get user %q: %v", id, err)
    return err  // Callers will also log this!
}

// Good: Wrap and return - let caller decide how to handle
u, err := getUser(id)
if err != nil {
    return fmt.Errorf("get user %q: %w", id, err)
}

// Good: Log and degrade gracefully (don't return error)
if err := emitMetrics(); err != nil {
    // Failure to write metrics should not break the application
    log.Printf("Could not emit metrics: %v", err)
}
// Continue execution...

// Good: Match specific errors, return others
tz, err := getUserTimeZone(id)
if err != nil {
    if errors.Is(err, ErrUserNotFound) {
        // User doesn't exist. Use UTC.
        tz = time.UTC
    } else {
        return fmt.Errorf("get user %q: %w", id, err)
    }
}
```

---

## Logging vs Returning Errors

> Handle an error exactly once — either log it or return it, never both.

### Decision Flow

```
Error encountered?
├─ Can the caller act on it? → Return the error (with context via %w)
├─ Is this the top of the call chain? → Log and handle (return HTTP status, exit, etc.)
└─ Neither? → Log at appropriate level and continue
```

### Don't Log and Return

```go
// Bad: error is logged AND returned — appears twice in logs
func process(ctx context.Context, id string) error {
    result, err := fetch(ctx, id)
    if err != nil {
        log.Printf("failed to fetch %s: %v", id, err)
        return fmt.Errorf("fetching %s: %w", id, err)
    }
    return handle(result)
}

// Good: return with context — let the caller decide whether to log
func process(ctx context.Context, id string) error {
    result, err := fetch(ctx, id)
    if err != nil {
        return fmt.Errorf("fetching %s: %w", id, err)
    }
    return handle(result)
}
```

### Structured Logging

Prefer structured logging (`slog` in Go 1.21+, or `log/slog`-compatible
libraries) over `log.Printf` for production code:

```go
// Good: structured fields are machine-parseable
slog.Error("fetch failed", "id", id, "err", err)

// Avoid: unstructured string interpolation
log.Printf("fetch failed for %s: %v", id, err)
```

### Verbosity Levels

| Level | Use for |
|-------|---------|
| Error | Actionable failures that need attention |
| Warn  | Degraded behavior that doesn't require immediate action |
| Info  | Key lifecycle events (startup, shutdown, config loaded) |
| Debug | Diagnostic detail useful during development |
references/ERROR-TYPES.md
# Error Types Reference

This reference covers structured error types, sentinel errors, and how to choose
the right error type for your use case.

---

## Error Structure

> The error-type decision table is in the parent skill (SKILL.md § Error Types).
> This reference covers: expanded code examples, sentinel errors, error checking
> with `errors.Is`/`errors.As`, and structured error types.

**Key considerations**:

- Does the caller need to match the error with `errors.Is` or `errors.As`?
- Is the error message static or does it require runtime values?
- Exported error variables/types become part of your public API

```go
// No matching needed, static message
func Open() error {
    return errors.New("could not open")
}

// Matching needed, static message - export a sentinel
var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
    return ErrCouldNotOpen
}

// Matching needed, dynamic message - use custom type
type NotFoundError struct {
    File string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("file %q not found", e.File)
}

func Open(file string) error {
    return &NotFoundError{File: file}
}
```

---

## Sentinel Errors

The simplest structured errors are unparameterized global values:

```go
// Good: Sentinel errors for programmatic checking
var (
    // ErrDuplicate occurs if this animal has already been seen.
    ErrDuplicate = errors.New("duplicate")

    // ErrMarsupial occurs because we're allergic to marsupials.
    ErrMarsupial = errors.New("marsupials are not supported")
)

func process(animal Animal) error {
    switch {
    case seen[animal]:
        return ErrDuplicate
    case marsupial(animal):
        return ErrMarsupial
    }
    seen[animal] = true
    return nil
}
```

---

## Checking Errors

For direct comparison (when errors are not wrapped):

```go
// Good: Direct comparison with sentinel
switch err := process(an); err {
case ErrDuplicate:
    return fmt.Errorf("feed %q: %v", an, err)
case ErrMarsupial:
    alternate := an.BackupAnimal()
    return handlePet(alternate)
}
```

When errors may be wrapped, use `errors.Is`:

```go
// Good: Works with wrapped errors
switch err := process(an); {
case errors.Is(err, ErrDuplicate):
    return fmt.Errorf("feed %q: %v", an, err)
case errors.Is(err, ErrMarsupial):
    // Try to recover...
}
```

**Never** match errors based on string content:

```go
// Bad: Fragile string matching
if regexp.MatchString(`duplicate`, err.Error()) {...}
if regexp.MatchString(`marsupial`, err.Error()) {...}
```

---

## Structured Error Types

For errors needing additional programmatic information, use struct types:

```go
// Good: Structured error with accessible fields
type PathError struct {
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

func (e *PathError) Unwrap() error { return e.Err }
```

Callers can use `errors.As` to extract the structured error:

```go
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println("Failed path:", pathErr.Path)
}
```

---

## Quick Reference

| Scenario | Error Type |
|----------|------------|
| No matching needed, static message | `errors.New("message")` |
| No matching needed, dynamic message | `fmt.Errorf("msg: %v", val)` |
| Matching needed, static message | `var ErrFoo = errors.New(...)` |
| Matching needed, dynamic message | custom struct type |
| Checking sentinel errors | `errors.Is(err, ErrFoo)` |
| Extracting structured errors | `errors.As(err, &target)` |
references/WRAPPING.md
# Error Wrapping Reference

This reference covers error wrapping with `%v` vs `%w`, placement conventions,
adding context to errors, and logging best practices.

---

## Wrapping Errors: %v vs %w

> **Advisory**: Recommended best practice.

The choice between `%v` and `%w` significantly impacts how errors are propagated
and inspected.

### Use %v for Simple Annotation

Use `%v` when you want to:

- Add context without preserving the error chain for programmatic inspection
- Create fresh, independent errors (especially at system boundaries like
  RPC/IPC)
- Log or display errors to humans

```go
// Good: %v at system boundary - hide internal details
func (s *Server) SuggestFortune(ctx context.Context, req *pb.Request) (*pb.Response, error) {
    if err != nil {
        return nil, fmt.Errorf("couldn't find fortune database: %v", err)
    }
}
```

### Use %w for Error Chain Preservation

Use `%w` when you want callers to programmatically inspect the underlying error:

```go
// Good: %w preserves error chain for errors.Is/errors.As
func (s *Server) internalFunction(ctx context.Context) error {
    if err != nil {
        return fmt.Errorf("couldn't find remote file: %w", err)
    }
}

// Caller can now check:
if errors.Is(err, fs.ErrNotExist) {
    // Handle not found case
}
```

### When to Use Each

**Use %w when**:
- Adding context while preserving the original error for programmatic inspection
- You explicitly document and test the underlying errors you expose

**Use %v when**:
- At system boundaries (RPC, IPC, storage) to translate to canonical error space
- Logging or displaying to humans
- Creating independent errors that hide implementation details

---

## Placement of %w

> **Advisory**: Recommended best practice.

Place `%w` at the **end** of the error string so error text mirrors error chain
structure:

```go
// Good: %w at end - prints newest to oldest
err1 := fmt.Errorf("err1")
err2 := fmt.Errorf("err2: %w", err1)
err3 := fmt.Errorf("err3: %w", err2)
fmt.Println(err3) // err3: err2: err1
```

```go
// Bad: %w at start - prints oldest to newest (confusing)
err1 := fmt.Errorf("err1")
err2 := fmt.Errorf("%w: err2", err1)
err3 := fmt.Errorf("%w: err3", err2)
fmt.Println(err3) // err1: err2: err3
```

```go
// Bad: %w in middle - incoherent order
err1 := fmt.Errorf("err1")
err2 := fmt.Errorf("err2-1 %w err2-2", err1)
err3 := fmt.Errorf("err3-1 %w err3-2", err2)
fmt.Println(err3) // err3-1 err2-1 err1 err2-2 err3-2
```

**Pattern**: Use the form `context message: %w`

---

## Adding Information to Errors

> **Advisory**: Recommended best practice.

### Add Context, Not Redundancy

Add information that you have but the caller/callee might not. Avoid duplicating
information the underlying error already provides:

```go
// Good: Adds meaningful context
if err := os.Open("settings.txt"); err != nil {
    return fmt.Errorf("launch codes unavailable: %v", err)
}
// Output: launch codes unavailable: open settings.txt: no such file or directory
```

```go
// Bad: Duplicates the filename
if err := os.Open("settings.txt"); err != nil {
    return fmt.Errorf("could not open settings.txt: %v", err)
}
// Output: could not open settings.txt: open settings.txt: no such file or directory
```

### Don't Annotate Without Purpose

If the annotation only indicates failure without adding information, just return
the error:

```go
// Bad: Annotation adds nothing
return fmt.Errorf("failed: %v", err)

// Good: Just return the error
return err
```

---

## Logging Errors

> **Advisory**: Recommended best practice.

When you do log errors, use `log/slog` (Go 1.21+) with structured key-value
pairs and the appropriate level:

- **`slog.Error`**: Reserve for actionable issues that need investigation.
- **`slog.Warn`**: For issues that may need attention but aren't immediately
  actionable.
- **`slog.Debug`**: For development tracing — only emitted when the handler's
  level is set to `LevelDebug`.

```go
// Good: Structured logging with appropriate levels
for _, q := range queries {
    slog.Debug("handling query", "query", q)
    q.Run()
}

// Good: Guard expensive formatting behind a level check
if slog.Default().Enabled(context.Background(), slog.LevelDebug) {
    slog.Debug("query plan", "explain", q.Explain())
}

// Bad: Expensive call evaluated even when debug logging is disabled
slog.Debug("query plan", "explain", q.Explain())
```

### Protect Sensitive Information

Be careful with PII (Personally Identifiable Information) in log messages. Many
log sinks are not appropriate for sensitive user data.

---

## Quick Reference

| Pattern | Guidance |
|---------|----------|
| `%v` | Use at system boundaries, for logging, to hide details |
| `%w` | Use to preserve error chain for programmatic inspection |
| `%w` placement | Always at the end: `"context: %w"` |
| Adding context | Add new info, don't duplicate existing info |
| Empty annotation | Just return `err` instead of `fmt.Errorf("failed: %v", err)` |
| Logging | Don't log and return; use appropriate log levels |
scripts/check-errors-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 finding struct {
	File    string `json:"file"`
	Line    int    `json:"line"`
	Rule    string `json:"rule"`
	Message string `json:"message"`
}

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

func usage() {
	fmt.Fprintf(os.Stdout, `check-errors.sh v%s - Check Go code for common error handling anti-patterns

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

OPTIONS
    -h, --help       Show this help message
    -v, --version    Show version
    --json           Output results as JSON
    --no-bare-return Skip the bare 'return err' check (high false-positive rate)
    --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-errors.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(`{"findings":[],"total":0,"truncated":false,"status":"no_go_files"}`)
		} else {
			fmt.Printf("No Go files found in: %s\n", opts.target)
		}
		return
	}

	findings := []finding{}
	for _, file := range files {
		fileFindings, err := analyzeFile(file, opts.checkBareReturn)
		if err != nil {
			fmt.Fprintf(os.Stderr, "error: parse %s: %v\n", file, err)
			os.Exit(2)
		}
		findings = append(findings, fileFindings...)
	}

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

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

	if opts.jsonOutput {
		out := struct {
			Findings  []finding `json:"findings"`
			Total     int       `json:"total"`
			Truncated bool      `json:"truncated"`
		}{Findings: findings, Total: total, Truncated: truncated}
		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 total == 0 {
			fmt.Println("No error handling anti-patterns found.")
			return
		}
		fmt.Println("Error handling anti-patterns found:")
		fmt.Println()
		for _, item := range findings {
			fmt.Printf("  %s:%d  [%s] %s\n", item.File, item.Line, item.Rule, item.Message)
		}
		if truncated {
			fmt.Printf("  ... and %d more (use --limit to adjust)\n", total-opts.limit)
		}
		fmt.Println()
		fmt.Printf("Total: %d finding(s)\n", total)
	}

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

func analyzeFile(path string, checkBareReturn bool) ([]finding, error) {
	fset := token.NewFileSet()
	file, err := parser.ParseFile(fset, path, nil, 0)
	if err != nil {
		return nil, err
	}

	var findings []finding
	var logLines []int
	var errReturnLines []int

	ast.Inspect(file, func(n ast.Node) bool {
		switch node := n.(type) {
		case *ast.BinaryExpr:
			if node.Op != token.EQL && node.Op != token.NEQ {
				return true
			}
			line := fset.Position(node.Pos()).Line
			switch {
			case isErrorCall(node.X) && isStringLiteral(node.Y):
				findings = append(findings, finding{
					File:    path,
					Line:    line,
					Rule:    "string-error-compare",
					Message: "comparing err.Error() to string; use errors.Is() or errors.As() instead",
				})
			case isStringLiteral(node.X) && isErrorCall(node.Y):
				findings = append(findings, finding{
					File:    path,
					Line:    line,
					Rule:    "string-error-compare",
					Message: "comparing string to err.Error(); use errors.Is() or errors.As() instead",
				})
			}
		case *ast.CallExpr:
			line := fset.Position(node.Pos()).Line
			if isStringsContainsErrorCall(node) {
				findings = append(findings, finding{
					File:    path,
					Line:    line,
					Rule:    "string-error-compare",
					Message: "using strings.Contains on err.Error(); use errors.Is() or errors.As() instead",
				})
			}
			if isLogCallWithErr(node) {
				logLines = append(logLines, line)
			}
		case *ast.ReturnStmt:
			if returnsErr(node) {
				line := fset.Position(node.Return).Line
				errReturnLines = append(errReturnLines, line)
				if checkBareReturn {
					findings = append(findings, finding{
						File:    path,
						Line:    line,
						Rule:    "bare-return-err",
						Message: "returning err without wrapping context; consider fmt.Errorf('...: %w', err)",
					})
				}
			}
		}
		return true
	})

	for _, retLine := range errReturnLines {
		for i := len(logLines) - 1; i >= 0; i-- {
			logLine := logLines[i]
			if logLine >= retLine {
				continue
			}
			if retLine-logLine > 5 {
				break
			}
			findings = append(findings, finding{
				File:    path,
				Line:    logLine,
				Rule:    "log-and-return",
				Message: fmt.Sprintf("error is both logged (line %d) and returned (line %d); handle errors once", logLine, retLine),
			})
			break
		}
	}

	return findings, nil
}

func isErrorCall(expr ast.Expr) bool {
	call, ok := expr.(*ast.CallExpr)
	if !ok || len(call.Args) != 0 {
		return false
	}
	sel, ok := call.Fun.(*ast.SelectorExpr)
	return ok && sel.Sel.Name == "Error"
}

func isStringLiteral(expr ast.Expr) bool {
	lit, ok := expr.(*ast.BasicLit)
	return ok && lit.Kind == token.STRING
}

func isStringsContainsErrorCall(call *ast.CallExpr) bool {
	sel, ok := call.Fun.(*ast.SelectorExpr)
	if !ok || sel.Sel.Name != "Contains" {
		return false
	}
	pkg, ok := sel.X.(*ast.Ident)
	if !ok || pkg.Name != "strings" || len(call.Args) == 0 {
		return false
	}
	return containsErrorCall(call.Args[0])
}

func containsErrorCall(expr ast.Expr) bool {
	found := false
	ast.Inspect(expr, func(n ast.Node) bool {
		if found {
			return false
		}
		if e, ok := n.(ast.Expr); ok && isErrorCall(e) {
			found = true
			return false
		}
		return true
	})
	return found
}

func isLogCallWithErr(call *ast.CallExpr) bool {
	sel, ok := call.Fun.(*ast.SelectorExpr)
	if !ok {
		return false
	}
	x, ok := sel.X.(*ast.Ident)
	if !ok {
		return false
	}
	switch x.Name {
	case "log", "logger", "slog":
	default:
		return false
	}
	for _, arg := range call.Args {
		if containsIdent(arg, "err") {
			return true
		}
	}
	return false
}

func containsIdent(expr ast.Expr, name string) bool {
	found := false
	ast.Inspect(expr, func(n ast.Node) bool {
		if found {
			return false
		}
		ident, ok := n.(*ast.Ident)
		if ok && ident.Name == name {
			found = true
			return false
		}
		return true
	})
	return found
}

func returnsErr(stmt *ast.ReturnStmt) bool {
	if len(stmt.Results) == 0 {
		return false
	}
	ident, ok := stmt.Results[len(stmt.Results)-1].(*ast.Ident)
	return ok && ident.Name == "err"
}

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 parseArgs(args []string) (options, error) {
	opts := options{target: ".", checkBareReturn: true}
	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 == "--no-bare-return":
			opts.checkBareReturn = false
		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
}
scripts/check-errors.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-errors.sh v$VERSION - Check Go code for common error handling anti-patterns

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

OPTIONS
    -h, --help       Show this help message
    -v, --version    Show version
    --json           Output results as JSON
    --no-bare-return Skip the bare 'return err' check (high false-positive rate)
    --limit N        Show at most N results (default: all)
EOF
            exit 0
            ;;
        -v|--version)
            echo "check-errors.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-errors-ast.go"
STAMP="$(cksum "$SRC" | awk '{print $1 "-" $2}')"
BIN="$CACHE_ROOT/check-errors-ast-$STAMP"

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

exec "$BIN" "$@"