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

go-interfaces

Use when defining or implementing Go interfaces, designing abstractions, creating mockable boundaries for testing, or composing types through embedding. Also use when deciding whether to accept an interface or return a concrete type, or using type assertions or type switches, even if the user doesn't explicitly mention interfaces. Does not cover generics-based polymorphism (see go-generics).

安装

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


name: go-interfaces description: Use when defining or implementing Go interfaces, designing abstractions, creating mockable boundaries for testing, or composing types through embedding. Also use when deciding whether to accept an interface or return a concrete type, or using type assertions or type switches, even if the user doesn't explicitly mention interfaces. Does not cover generics-based polymorphism (see go-generics). allowed-tools: Bash(bash:*)

Go Interfaces and Composition

Resource Routing

  • scripts/check-interface-compliance.sh - Run as a heuristic to find exported interfaces that may need compile-time assertions.
  • scripts/check-interface-compliance.go - Implementation helper invoked by check-interface-compliance.sh; patch this when changing method-set analysis.
  • references/EMBEDDING.md - Read when embedding interfaces or structs in public APIs.
  • references/RECEIVER-TYPE.md - Read when pointer/value receivers affect interface satisfaction.

Accept Interfaces, Return Concrete Types

Interfaces belong in the package that consumes values, not the package that implements them. Return concrete (usually pointer or struct) types from constructors so new methods can be added without refactoring.

// Good: consumer defines the interface it needs
package consumer

type Thinger interface { Thing() bool }

func Foo(t Thinger) string { ... }
// Good: producer returns concrete type
package producer

type Thinger struct{ ... }
func (t Thinger) Thing() bool { ... }
func NewThinger() Thinger { return Thinger{ ... } }
// Bad: producer defines and returns its own interface
package producer

type Thinger interface { Thing() bool }
type defaultThinger struct{ ... }
func NewThinger() Thinger { return defaultThinger{ ... } }

Do not define interfaces before they are used. Without a realistic example of usage, it is too difficult to see whether an interface is even necessary.


Generality: Hide Implementation, Expose Interface

If a type exists only to implement an interface with no exported methods beyond that interface, return the interface from constructors to hide the implementation:

func NewHash() hash.Hash32 {
    return &myHash{}  // unexported type
}

Benefits: implementation can change without affecting callers, substituting algorithms requires only changing the constructor call.


Type Assertions: Comma-Ok Idiom

Without checking, a failed assertion causes a runtime panic. Always use the comma-ok idiom to test safely:

str, ok := value.(string)
if ok {
    fmt.Printf("string value is: %q\n", str)
}

To check if a value implements an interface:

if _, ok := val.(json.Marshaler); ok {
    fmt.Printf("value %v implements json.Marshaler\n", val)
}

Type Switch

It's idiomatic to reuse the variable name (t := t.(type)) — the variable has the correct type in each case branch. When a case lists multiple types (case int, int64:), the variable has the interface type.


Embedding

Avoid embedding types in public structs — the inner type's full method set becomes part of your public API. Use unexported fields instead.


Interface Satisfaction Checks

Use a blank identifier assignment to verify a type implements an interface at compile time:

var _ json.Marshaler = (*RawMessage)(nil)

This causes a compile error if *RawMessage doesn't implement json.Marshaler.

Use this pattern when:

  • There are no static conversions that would verify the interface automatically
  • The type must satisfy an interface for correct behavior (e.g., custom JSON marshaling)
  • Interface changes should break compilation, not silently degrade

Don't add these checks for every interface — only when no other static conversion would catch the error.

Validation: After defining interfaces or implementations, run bash scripts/check-interface-compliance.sh to verify all concrete types have compile-time var _ I = (*T)(nil) checks.


Receiver Type

If in doubt, use a pointer receiver. Don't mix receiver types on a single type — if any method needs a pointer, use pointers for all methods. Use value receivers only for small, immutable types (Point, time.Time) or basic types.


Quick Reference

ConceptPatternNotes
Consumer owns interfaceDefine interfaces where usedNot in the implementing package
Safe type assertionv, ok := x.(Type)Returns zero value + false
Type switchswitch v := x.(type)Variable has correct type per case
Interface embeddingtype RW interface { Reader; Writer }Union of methods
Struct embeddingtype S struct { *T }Promotes T's methods
Interface checkvar _ I = (*T)(nil)Compile-time verification
GeneralityReturn interface from constructorHide implementation

Related Skills

  • Interface naming: See go-naming when naming interfaces (the -er suffix convention) or choosing receiver names
  • Error types: See go-error-handling when implementing the error interface, custom error types, or errors.As matching
  • Generics vs interfaces: See go-generics when deciding whether generics are needed or an interface already suffices
  • Functional options: See go-functional-options when using an interface-based Option pattern for flexible constructors
  • Defensive boundaries: See go-defensive when interface assertions are one part of a broader API-boundary hardening pass

附带文件

references/EMBEDDING.md
# Embedding Patterns in Go

> **Sources**: Effective Go, Uber Style Guide

Go uses embedding for composition instead of inheritance. Embedding promotes
the inner type's methods to the outer type, satisfying interfaces automatically.

## Interface Embedding

Combine interfaces by embedding them:

```go
type ReadWriter interface {
    Reader
    Writer
}
```

A `ReadWriter` can do what a `Reader` does *and* what a `Writer` does. Only
interfaces can be embedded within interfaces.

## Struct Embedding

Embedding promotes methods from the inner type to the outer type without
explicit forwarding.

```go
type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}
```

With embedding, `bufio.ReadWriter` satisfies `io.Reader`, `io.Writer`, and
`io.ReadWriter` automatically.

Mix embedded and named fields:

```go
type Job struct {
    Command string
    *log.Logger
}

job.Println("starting now...")
job.Logger.SetPrefix("Job: ")
```

## Method Overriding

Define a method on the outer type to override the promoted method:

```go
func (job *Job) Printf(format string, args ...any) {
    job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
```

The outer method takes precedence — calls to `job.Printf(...)` invoke the
outer method, while the embedded method is still accessible via
`job.Logger.Printf(...)`.

## Embedding vs Subclassing

When an embedded method is invoked, the receiver is the **inner** type, not the
outer one. The embedded type has no knowledge that it is embedded — there is no
equivalent to `this` or `super` referencing the containing type.

```go
type Base struct{}
func (b *Base) Name() string { return "Base" }

type Derived struct{ Base }

d := Derived{}
d.Name() // returns "Base", not "Derived"
```

## Name Conflict Resolution

1. **Outer hides inner** — Fields or methods on the outer type shadow those
   promoted from an embedded type at the same name
2. **Same-level conflicts are errors** — If two embedded types at the same
   depth promote the same name, it is a compile error (unless the name is
   never accessed)

```go
type A struct{}
func (A) Hello() string { return "A" }

type B struct{}
func (B) Hello() string { return "B" }

type C struct {
    A
    B
}

// c.Hello()  // compile error: ambiguous selector
c.A.Hello()   // OK: explicit disambiguation
```

## Don't Embed in Public Structs

Embedding exposes the inner type's full method set as part of your public API.
This creates a maintenance burden: changes to the embedded type's methods
break your API's compatibility guarantees.

**Bad**
```go
type SMap struct {
    sync.Mutex  // Lock and Unlock are now part of SMap's API
    data map[string]string
}
```

**Good**
```go
type SMap struct {
    mu   sync.Mutex  // unexported field — implementation detail
    data map[string]string
}

func (m *SMap) Get(k string) string {
    m.mu.Lock()
    defer m.mu.Unlock()
    return m.data[k]
}
```

Exception: Embedding is acceptable in test types and internal structs where
API stability is not a concern.

## The HandlerFunc Adapter Pattern

Methods can be defined on any named type, not just structs. The
`http.HandlerFunc` pattern converts an ordinary function into an interface
implementation:

```go
type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
    f(w, req)
}
```

Any function with the right signature becomes an HTTP handler:

```go
http.Handle("/args", http.HandlerFunc(ArgServer))
```

This adapter pattern is useful whenever you need a single-method interface
satisfied by a standalone function.
references/RECEIVER-TYPE.md
# Receiver Type: Pointer vs Value

> **Advisory**: Go Wiki CodeReviewComments

Choosing whether to use a value or pointer receiver on methods can be difficult.
**If in doubt, use a pointer**, but there are times when a value receiver makes
sense.

## When to Use Pointer Receiver

- **Method mutates receiver**: The receiver must be a pointer
- **Receiver contains sync.Mutex or similar**: Must be a pointer to avoid copying
- **Large struct or array**: A pointer receiver is more efficient. If passing all
  elements as arguments feels too large, it's too large for a value receiver
- **Concurrent or called methods might mutate**: If changes must be visible in
  the original receiver, it must be a pointer
- **Elements are pointers to something mutating**: Prefer pointer receiver to
  make the intention clearer

## When to Use Value Receiver

- **Small unchanging structs or basic types**: Value receiver for efficiency
- **Map, func, or chan**: Don't use a pointer to them
- **Slice without reslicing/reallocating**: Don't use a pointer if the method
  doesn't reslice or reallocate the slice
- **Small value types with no mutable fields**: Types like `time.Time` with no
  mutable fields and no pointers work well as value receivers
- **Simple basic types**: `int`, `string`, etc.

```go
// Value receiver: small, immutable type
type Point struct {
    X, Y float64
}

func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

// Pointer receiver: method mutates receiver
func (p *Point) ScaleBy(factor float64) {
    p.X *= factor
    p.Y *= factor
}

// Pointer receiver: contains sync.Mutex
type Counter struct {
    mu    sync.Mutex
    count int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    c.count++
    c.mu.Unlock()
}
```

## Consistency Rule

**Don't mix receiver types**. Choose either pointers or struct types for all
available methods on a type. If any method needs a pointer receiver, use pointer
receivers for all methods.

```go
// Good: Consistent pointer receivers
type Buffer struct {
    data []byte
}

func (b *Buffer) Write(p []byte) (int, error) { /* ... */ }
func (b *Buffer) Read(p []byte) (int, error)  { /* ... */ }
func (b *Buffer) Len() int                     { return len(b.data) }

// Bad: Mixed receiver types
func (b Buffer) Len() int                      { return len(b.data) }  // inconsistent
```
scripts/check-interface-compliance.go
package main

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

const version = "1.1.0"

type ifaceInfo struct {
	Name string `json:"name"`
	File string `json:"file"`
	Line int    `json:"line"`

	key string
	obj *types.TypeName
}

type result struct {
	Interfaces     []ifaceInfo `json:"interfaces"`
	Missing        []ifaceInfo `json:"missing"`
	CountInterface int         `json:"count_interfaces"`
	CountMissing   int         `json:"count_missing"`
	Truncated      bool        `json:"truncated"`
	Status         string      `json:"status,omitempty"`
}

type sourceFile struct {
	path          string
	pkgName       string
	includeInScan bool
}

type packageGroup struct {
	key   string
	dir   string
	name  string
	files []sourceFile
}

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

func usage() {
	fmt.Fprintf(os.Stdout, `check-interface-compliance.sh v%s - Find likely missing compile-time interface compliance verifications

USAGE
    bash check-interface-compliance.sh [options] [path]

OPTIONS
    -h, --help       Show this help message
    -v, --version    Show version
    --json           Output results as JSON
    --include-test   Also scan _test.go files for interface definitions and implementations
    --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-interface-compliance.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)
	}

	sourceFiles := make([]sourceFile, 0, len(files))
	for _, path := range files {
		isTest := strings.HasSuffix(path, "_test.go")
		if isTest && !opts.includeTest {
			continue
		}
		pkgName, err := packageName(path)
		if err != nil {
			fmt.Fprintf(os.Stderr, "error: parse package %s: %v\n", path, err)
			os.Exit(2)
		}
		sourceFiles = append(sourceFiles, sourceFile{path: path, pkgName: pkgName, includeInScan: true})
	}

	if len(sourceFiles) == 0 {
		out := result{
			Interfaces: []ifaceInfo{},
			Missing:    []ifaceInfo{},
			Status:     "no_go_files",
		}
		emit(out, opts.jsonOutput)
		return
	}

	allFiles := make([]sourceFile, 0, len(files))
	for _, path := range files {
		pkgName, err := packageName(path)
		if err != nil {
			fmt.Fprintf(os.Stderr, "error: parse package %s: %v\n", path, err)
			os.Exit(2)
		}
		include := !strings.HasSuffix(path, "_test.go") || opts.includeTest
		allFiles = append(allFiles, sourceFile{path: path, pkgName: pkgName, includeInScan: include})
	}

	groups := groupByPackage(allFiles)
	assertions := map[string]map[string]bool{}
	var interfaces []ifaceInfo
	localImpl := map[string]map[string]bool{}

	for _, group := range groups {
		groupAssertions, groupInterfaces, groupImpls, err := analyzePackage(group)
		if err != nil {
			fmt.Fprintf(os.Stderr, "error: %v\n", err)
			os.Exit(2)
		}
		assertions[group.key] = groupAssertions
		interfaces = append(interfaces, groupInterfaces...)
		localImpl[group.key] = groupImpls
	}

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

	missing := []ifaceInfo{}
	for _, iface := range interfaces {
		if localImpl[iface.key][iface.Name] && !assertions[iface.key][iface.Name] {
			missing = append(missing, iface)
		}
	}
	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
	})

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

	out := result{
		Interfaces:     interfaces,
		Missing:        missing,
		CountInterface: len(interfaces),
		CountMissing:   totalMissing,
		Truncated:      truncated,
	}
	if len(interfaces) == 0 {
		out.Status = "no_exported_interfaces"
	}

	emit(out, opts.jsonOutput)
	if totalMissing > 0 {
		os.Exit(1)
	}
}

func emit(out result, jsonOutput bool) {
	if jsonOutput {
		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))
		return
	}

	if out.Status == "no_go_files" {
		fmt.Println("No Go files found.")
		return
	}
	if out.Status == "no_exported_interfaces" {
		fmt.Println("No exported interfaces found.")
		return
	}

	fmt.Printf("Exported interfaces found: %d\n\n", out.CountInterface)
	if out.CountMissing == 0 {
		fmt.Println("All interfaces have compile-time compliance checks.")
		return
	}

	fmt.Println("Missing compile-time compliance checks:")
	fmt.Println()
	for _, item := range out.Missing {
		fmt.Printf("  %s:%d  interface '%s' has no 'var _ %s = ...' assertion\n", item.File, item.Line, item.Name, item.Name)
	}
	if out.Truncated {
		fmt.Printf("  ... and %d more (use --limit to adjust)\n", out.CountMissing-len(out.Missing))
	}
	fmt.Println()
	fmt.Println("Add compile-time checks like:")
	fmt.Println("  var _ MyInterface = (*MyImpl)(nil)")
	fmt.Println()
	fmt.Printf("Total: %d interface(s) missing verification\n", out.CountMissing)
}

func analyzePackage(group packageGroup) (map[string]bool, []ifaceInfo, map[string]bool, error) {
	fset := token.NewFileSet()
	parsed := make([]*ast.File, 0, len(group.files))
	fileByAST := map[*ast.File]sourceFile{}
	for _, sf := range group.files {
		file, err := parser.ParseFile(fset, sf.path, nil, 0)
		if err != nil {
			return nil, nil, nil, fmt.Errorf("parse %s: %w", sf.path, err)
		}
		parsed = append(parsed, file)
		fileByAST[file] = sf
	}

	info := &types.Info{
		Defs: map[*ast.Ident]types.Object{},
	}
	conf := types.Config{
		Importer: importer.Default(),
		Error:    func(error) {},
	}
	_, _ = conf.Check(group.key, fset, parsed, info)

	assertions := map[string]bool{}
	interfaces := []ifaceInfo{}
	typeNames := []*types.TypeName{}

	for _, file := range parsed {
		sf := fileByAST[file]
		for _, decl := range file.Decls {
			gen, ok := decl.(*ast.GenDecl)
			if !ok {
				continue
			}
			switch gen.Tok {
			case token.VAR:
				for _, spec := range gen.Specs {
					vs, ok := spec.(*ast.ValueSpec)
					if !ok || vs.Type == nil {
						continue
					}
					for _, name := range vs.Names {
						if name.Name != "_" {
							continue
						}
						if asserted := assertedInterface(vs.Type); asserted != "" {
							assertions[asserted] = true
						}
					}
				}
			case token.TYPE:
				for _, spec := range gen.Specs {
					ts, ok := spec.(*ast.TypeSpec)
					if !ok {
						continue
					}
					obj, ok := info.Defs[ts.Name].(*types.TypeName)
					if !ok {
						continue
					}
					typeNames = append(typeNames, obj)
					if !sf.includeInScan || !ast.IsExported(ts.Name.Name) {
						continue
					}
					if iface, ok := obj.Type().Underlying().(*types.Interface); ok {
						iface.Complete()
						interfaces = append(interfaces, ifaceInfo{
							Name: ts.Name.Name,
							File: sf.path,
							Line: fset.Position(ts.Pos()).Line,
							key:  group.key,
							obj:  obj,
						})
					}
				}
			}
		}
	}

	impls := map[string]bool{}
	for _, iface := range interfaces {
		ifaceType, ok := iface.obj.Type().Underlying().(*types.Interface)
		if !ok {
			continue
		}
		ifaceType.Complete()
		if ifaceType.NumMethods() == 0 {
			continue
		}
		for _, typeName := range typeNames {
			if typeName == iface.obj {
				continue
			}
			if !typeBelongsToScannedFile(typeName, group.files, fset) {
				continue
			}
			if _, ok := typeName.Type().Underlying().(*types.Interface); ok {
				continue
			}
			if implements(typeName.Type(), ifaceType) {
				impls[iface.Name] = true
				break
			}
		}
	}

	return assertions, interfaces, impls, nil
}

func typeBelongsToScannedFile(typeName *types.TypeName, files []sourceFile, fset *token.FileSet) bool {
	pos := fset.Position(typeName.Pos())
	for _, sf := range files {
		if sf.includeInScan && filepath.Clean(sf.path) == filepath.Clean(pos.Filename) {
			return true
		}
	}
	return false
}

func implements(t types.Type, iface *types.Interface) bool {
	if types.Implements(t, iface) {
		return true
	}
	if _, ok := t.(*types.Pointer); ok {
		return false
	}
	return types.Implements(types.NewPointer(t), iface)
}

func assertedInterface(expr ast.Expr) string {
	switch e := expr.(type) {
	case *ast.Ident:
		if ast.IsExported(e.Name) {
			return e.Name
		}
	case *ast.IndexExpr:
		return assertedInterface(e.X)
	case *ast.IndexListExpr:
		return assertedInterface(e.X)
	}
	return ""
}

func groupByPackage(files []sourceFile) []packageGroup {
	byKey := map[string]*packageGroup{}
	for _, sf := range files {
		key := filepath.Dir(sf.path) + "|" + sf.pkgName
		group, ok := byKey[key]
		if !ok {
			group = &packageGroup{key: key, dir: filepath.Dir(sf.path), name: sf.pkgName}
			byKey[key] = group
		}
		group.files = append(group.files, sf)
	}

	groups := make([]packageGroup, 0, len(byKey))
	for _, group := range byKey {
		sort.Slice(group.files, func(i, j int) bool { return group.files[i].path < group.files[j].path })
		groups = append(groups, *group)
	}
	sort.Slice(groups, func(i, j int) bool { return groups[i].key < groups[j].key })
	return groups
}

func packageName(path string) (string, error) {
	fset := token.NewFileSet()
	file, err := parser.ParseFile(fset, path, nil, parser.PackageClauseOnly)
	if err != nil {
		return "", err
	}
	return file.Name.Name, nil
}

func findGoFiles(target string) ([]string, error) {
	info, err := os.Stat(target)
	if err == nil {
		if !info.IsDir() {
			if strings.HasSuffix(target, ".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") {
			files = append(files, path)
		}
		return nil
	})
	sort.Strings(files)
	return files, err
}

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 == "--include-test":
			opts.includeTest = 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
}
scripts/check-interface-compliance.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-interface-compliance.sh v$VERSION - Find likely missing compile-time interface compliance verifications

USAGE
    bash check-interface-compliance.sh [options] [path]

OPTIONS
    -h, --help       Show this help message
    -v, --version    Show version
    --json           Output results as JSON
    --include-test   Also scan _test.go files for interface definitions and implementations
    --limit N        Show at most N results (default: all)
EOF
            exit 0
            ;;
        -v|--version)
            echo "check-interface-compliance.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-interface-compliance.go"
STAMP="$(cksum "$SRC" | awk '{print $1 "-" $2}')"
BIN="$CACHE_ROOT/check-interface-compliance-$STAMP"

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

exec "$BIN" "$@"