Go Code Style and Architectural Guide
This document outlines the Go-specific code style, conventions, and architectural patterns for the backlog
repository. Adhering to these guidelines is crucial for maintaining a clean, idiomatic, and maintainable codebase.
1. Project Layout
The project follows the standard Go project layout recommendations.
internal/
: All core application code is located here. This code is not meant to be imported by other projects.internal/cmd
: Defines the CLI commands using the Cobra library. Each command and its related flags are in a separate file (e.g.,task_create.go
).internal/core
: Contains the core business logic and data structures (e.g.,Task
,TaskStore
). This package is the heart of the application and is designed to be decoupled from the CLI.internal/commit
: Handles Git-related operations, such as automatic commits.internal/mcp
: Implements the Model-Context-Protocol (MCP) server for AI agent integration.internal/logging
: Provides a structured logging setup for the application.internal/paths
: Provides utilities for resolving repository paths.
pkg/
: (Not currently used) Would contain library code intended for external use.main.go
: The main application entry point. It is kept minimal, with its primary role being to initialize and execute the root Cobra command.
2. Go Language & Idioms
- Formatting: All code must be formatted with
gofmt
. The CI pipeline will fail if code is not formatted correctly. - Linting: We use
golangci-lint
with a strict configuration to enforce idiomatic Go. Runmake lint
locally before pushing changes. - Naming Conventions:
- Package names are short, concise, and all lowercase (e.g.,
core
,commit
). - Public symbols (variables, functions, types) are
PascalCase
. - Private symbols are
camelCase
. - Acronyms like
ID
andAPI
should be consistently cased (e.g.,taskID
, nottaskId
).
- Package names are short, concise, and all lowercase (e.g.,
- Interfaces:
- Interfaces are defined by the consumer. For example, if a function needs a
Reader
, it should acceptio.Reader
, not a concrete type. - Keep interfaces small and focused on a single behavior (e.g.,
io.Reader
,fmt.Stringer
). TheTaskStore
interface ininternal/core/store.go
is a good example.
- Interfaces are defined by the consumer. For example, if a function needs a
- Structs:
- Structs should be initialized with explicit field names where possible (e.g.,
core.Task{Title: "New Task"}
). - Group related fields together. Add comments to explain complex or non-obvious fields.
- Structs should be initialized with explicit field names where possible (e.g.,
- Pointers vs. Values:
- Use pointers for large structs or when a method needs to modify the receiver.
- Use values for small, immutable structs or built-in types.
- In this project,
*Task
is used frequently because tasks are often modified and passed around.
3. Error Handling
- Always Check Errors: Never ignore an error with the blank identifier (
_
). The only exception is for aClose()
call on a read-only resource where failure has no consequence. - Error Wrapping: Errors that cross package boundaries must be wrapped to provide context. Use
fmt.Errorf("operation failed: %w", err)
. This creates a chain of errors that can be inspected for debugging. - Custom Error Types: For specific, expected errors (e.g., "task not found"), use custom error variables like
core.ErrNotFound
. This allows callers to check for specific error conditions usingerrors.Is()
. - No Panics: The application must not use
panic
for recoverable errors. Panics are reserved for unrecoverable, programmer-level mistakes that indicate a bug.
4. Logging
- Structured Logging: We use a structured logger (e.g.,
zerolog
) for all application output. This allows for easier parsing, filtering, and analysis of logs. - Log Levels: Use appropriate log levels:
logging.Error
: For failures that prevent a feature from working.logging.Warn
: For potential issues that do not cause a failure.logging.Info
: For general, informative messages (e.g., "task created successfully").logging.Debug
: For verbose, development-only messages.
5. Concurrency
- (Not heavily used yet) When introducing concurrency, prefer channels for communication and synchronization over explicit locks where possible.
- Ensure all goroutines are properly managed and have a clear exit path to prevent leaks.
6. Testing
- Test Location: Tests for
foo.go
are located infoo_test.go
within the same package. - Test Tables: For testing multiple inputs and outputs, use table-driven tests.
- Filesystem Abstraction: We use the
afero
library to abstract the filesystem.- In production,
afero.NewOsFs()
is used. - In tests,
afero.NewMemMapFs()
provides a fast, in-memory filesystem, making tests hermetic and reliable.
- In production,
- Dependency Injection: Core components like
FileTaskStore
are designed for dependency injection. They accept dependencies (likeafero.Fs
) via their constructor (NewFileTaskStore
). This is critical for decoupling and testability.
7. CLI Implementation
- Library: The CLI is built using the
cobra
library. - Command Documentation: All user-facing commands must include a
Short
description, aLong
description, and comprehensiveExample
usage strings.
8. Git Integration
- Automatic Commits: Task operations (create, edit, archive) trigger automatic Git commits to maintain a history of changes.
- Clean Worktree: The auto-commit feature will not run if the Git worktree is dirty. This prevents accidental inclusion of unrelated changes.
9. Code Generation
go:generate
: We use//go:generate
to automate tasks like generating CLI documentation. Runmake docs
to update generated content. This ensures documentation stays in sync with the code.