5.3 KiB
Internal Package Organization
This document explains how to organize code in the internal/ directory of Go CLI projects.
The internal/ Directory
The internal/ directory is a special Go convention. Packages inside internal/ can only be imported by code in the parent tree. This enforces encapsulation and prevents external projects from depending on internal implementation details.
Standard Package Structure
A typical Go CLI project has this structure:
project/
├── cmd/ # Command definitions (public API of the CLI)
├── internal/ # Private implementation
│ ├── config/ # Configuration structures
│ ├── database/ # Database access layer
│ └── [domain packages] # Business logic packages
├── main.go # Entry point
└── go.mod # Dependencies
Package Guidelines
cmd/ Package
Purpose: Define the CLI commands and their flags
Contents:
root.go: Root command and configuration initializationversion.go: Version commandconstants.go: CLI-level constants- One file per command (e.g.,
fetch.go,export.go)
Responsibilities:
- Parse and validate user input
- Set up configuration and logging
- Call into
internal/packages to do the work - Format and display output
Anti-patterns:
- Heavy business logic in command handlers
- Direct database access
- Complex algorithms
internal/config/ Package
Purpose: Define configuration structures
Contents:
config.go: Config struct definitions
Example:
package config
type Config struct {
Database string
Verbose bool
Fetch struct {
Concurrency int
Timeout time.Duration
}
}
internal/database/ Package
Purpose: Encapsulate all database operations
Contents:
database.go: Connection management, initializationmigrations.go: Migration systemschema.sql: Initial schema (embedded)- Optional:
queries.gofor complex queries
Responsibilities:
- Database connection lifecycle
- Schema initialization and migrations
- Data access methods
- Transaction management
Anti-patterns:
- Business logic in database layer
- Exposing
*sql.DBdirectly - SQL in command files
Domain-Specific Packages
Create additional packages in internal/ for each major domain or feature:
internal/
├── feeds/ # Feed parsing and processing
├── fetcher/ # HTTP fetching logic
├── renderer/ # Output rendering
└── exporter/ # Export functionality
Guidelines:
- One package per cohesive responsibility
- Packages should be importable by
cmd/and by each other - Keep packages focused and single-purpose
- Use clear, descriptive names
Layering and Dependencies
Follow these dependency rules:
main.go
└─> cmd/
└─> internal/config/
└─> internal/database/
└─> internal/[domain]/
└─> internal/[other domains]/
Rules:
cmd/can import anyinternal/packageinternal/packages can import each other as needed- Avoid circular dependencies between
internal/packages - Keep
cmd/thin - it orchestrates but doesn't implement
Example: Adding a New Feature
Let's say you want to add feed fetching functionality:
-
Create the package:
internal/fetcher/ ├── fetcher.go # Main fetching logic └── fetcher_test.go # Tests -
Define the API:
package fetcher type Fetcher struct { client *http.Client // ... } func New(opts ...Option) *Fetcher { ... } func (f *Fetcher) Fetch(url string) ([]byte, error) { ... } -
Use in command:
// cmd/fetch.go package cmd import "yourproject/internal/fetcher" var fetchCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { f := fetcher.New() data, err := f.Fetch(url) // ... }, }
Common Patterns
Option Pattern for Configuration
type Fetcher struct {
timeout time.Duration
}
type Option func(*Fetcher)
func WithTimeout(d time.Duration) Option {
return func(f *Fetcher) {
f.timeout = d
}
}
func New(opts ...Option) *Fetcher {
f := &Fetcher{timeout: 30 * time.Second}
for _, opt := range opts {
opt(f)
}
return f
}
Embedding Resources
For SQL, templates, or other resources:
import _ "embed"
//go:embed schema.sql
var schemaSQL string
Error Wrapping
Always wrap errors with context:
if err != nil {
return fmt.Errorf("failed to fetch feed %s: %w", url, err)
}
Testing
- Put tests in
_test.gofiles alongside the code - Use table-driven tests for multiple cases
- Consider using
internal/database/database_test.gowith in-memory SQLite for database tests
When to Create a New Package
Create a new internal/ package when:
- You have a cohesive set of related functionality
- The code would make commands cleaner and more focused
- You want to unit test logic separately from CLI interaction
- Multiple commands need to share the same functionality
Don't create a package when:
- It would only have one small function
- It's tightly coupled to a single command
- It would create circular dependencies