Skip to main content
Kosh follows Go best practices and specific conventions for consistency and maintainability.

General Go Conventions

Formatting

Always use gofmt for code formatting (handled automatically by most editors).
# Format all Go files
go fmt ./...

# Check formatting
gofmt -l .

Naming Conventions

Packages:
  • Short, lowercase, singular
  • No underscores or mixedCaps
package parser   // Good
package config   // Good
package md_parser // Bad
package Parser    // Bad
Interfaces:
  • Use er suffix for capability-based names
builder/services/interfaces.go
type PostService interface {
    Process(ctx context.Context, shouldForce bool) (*PostResult, error)
}

type CacheService interface {
    GetPost(id string) (*cache.PostMeta, error)
}

type RenderService interface {
    RenderPage(path string, data models.PageData)
}
Variables:
  • camelCase for unexported
  • PascalCase for exported
var configPath string        // unexported
var SharedBufferPool *Pool   // exported

type Config struct {
    BaseURL    string  // exported field
    outputDir  string  // unexported field
}
Constants:
  • Use const blocks with clear names
  • Avoid magic numbers
builder/utils/worker_pool.go
const (
    // MaxWorkers is the maximum number of workers in a pool
    MaxWorkers = 32
    // WorkerBufferSize is the channel buffer multiplier
    WorkerBufferSize = 4
)

Error Handling

Never ignore errors - always handle or log them appropriately.

Check Errors Immediately

Always check errors right after function calls:
// Good
data, err := os.ReadFile(path)
if err != nil {
    return fmt.Errorf("failed to read file %s: %w", path, err)
}

// Bad - deferred error check
data, err := os.ReadFile(path)
processData(data)
if err != nil { // Too late!
    return err
}

Wrap Errors with Context

Use %w verb to wrap errors and preserve the error chain:
builder/services/post_service.go
func (s *postServiceImpl) Process(ctx context.Context, ...) (*PostResult, error) {
    files, err := s.getMarkdownFiles()
    if err != nil {
        return nil, fmt.Errorf("failed to list markdown files: %w", err)
    }
    
    // Process files...
}

Avoid Panic

Only use panic for critical startup failures:
// Good - return error
func Open(path string) (*Manager, error) {
    if err := os.MkdirAll(path, 0755); err != nil {
        return nil, fmt.Errorf("failed to create directory: %w", err)
    }
    return &Manager{path: path}, nil
}

// Bad - panic for recoverable error
func Open(path string) *Manager {
    if err := os.MkdirAll(path, 0755); err != nil {
        panic(err) // Don't do this!
    }
    return &Manager{path: path}
}

Log Errors Appropriately

Use structured logging for all error reporting:
s.logger.Error("Error walking content directory", 
    "path", path, 
    "error", err)

s.logger.Warn("Cache miss for post", 
    "postID", id,
    "reason", "not found in BoltDB")

Context & Cancellation

Context Propagation

All long-running operations must accept and respect context.Context:
builder/services/interfaces.go
type PostService interface {
    Process(ctx context.Context, shouldForce, forceSocialRebuild, outputMissing bool) (*PostResult, error)
    ProcessSingle(ctx context.Context, path string) error
}

type AssetService interface {
    Build(ctx context.Context) error
}

Graceful Shutdown

Respect context cancellation for clean shutdown:
builder/utils/worker_pool.go
func (p *WorkerPool[T]) worker() {
    defer p.wg.Done()
    for {
        select {
        case <-p.ctx.Done():
            return  // Graceful shutdown
        case task, ok := <-p.taskQueue:
            if !ok {
                return
            }
            p.handler(task)
        }
    }
}
Signal Handling:
  • SIGINT (Ctrl+C) triggers graceful shutdown
  • SIGTERM triggers graceful shutdown
  • 5-second timeout for cleanup

Security Best Practices

Path Validation

All file paths must be validated to prevent path traversal attacks.
Use validatePath() for all user-provided paths:
func validatePath(base, target string) (string, error) {
    // Normalize and validate path is within base directory
    absTarget := filepath.Join(base, filepath.Clean(target))
    if !strings.HasPrefix(absTarget, base) {
        return "", fmt.Errorf("invalid path: %s", target)
    }
    return absTarget, nil
}

Cryptographic Hashing

Always use BLAKE3 for hashing (MD5 is deprecated):
import "github.com/zeebo/blake3"

func hashContent(data []byte) string {
    hasher := blake3.New()
    hasher.Write(data)
    return hex.EncodeToString(hasher.Sum(nil))
}

Input Sanitization

Normalize and validate user-provided paths:
func NormalizePath(path string) string {
    // Clean path but preserve case for case-sensitive filesystems
    return filepath.Clean(path)
}

Safe Defaults

builder/cache/cache.go
opts := &bolt.Options{
    Timeout:         timeout,
    FreelistType:    bolt.FreelistArrayType,
    PageSize:        16384,
}

if isDev {
    opts.NoGrowSync = true   // Faster, less durable
} else {
    opts.NoGrowSync = false  // Slower, more durable
}

Structured Logging

Kosh uses log/slog for structured logging throughout the codebase.

Logger Access

// Via Builder
b.logger.Info("Building site", "posts", count)

// Direct import for utilities
import "log/slog"

slog.Info("Cache initialized", "path", cachePath)

Log Levels

// Info - Normal operations
logger.Info("Post processed", 
    "path", path,
    "duration", elapsed)

// Warn - Recoverable issues
logger.Warn("Cache miss", 
    "postID", id,
    "reason", "not found")

// Error - Failures requiring attention  
logger.Error("Failed to render post",
    "path", path,
    "error", err)
Avoid legacy log.Printf and fmt.Printf in build pipelines. Use structured logging instead.

Structured Fields

// Good - structured
logger.Info("Build complete",
    "posts", postCount,
    "duration", elapsed,
    "cacheHits", hits)

// Bad - string formatting
logger.Info(fmt.Sprintf("Built %d posts in %v (cache: %d hits)", 
    postCount, elapsed, hits))

Generics Usage

Kosh uses Go 1.23 generics for type-safe operations.

Generic Cache Retrieval

builder/cache/cache.go
// Generic function for type-safe cache operations
func getCachedItem[T any](db *bolt.DB, bucketName string, key []byte) (*T, error) {
    var item T
    err := db.View(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(bucketName))
        if b == nil {
            return fmt.Errorf("bucket not found: %s", bucketName)
        }
        
        data := b.Get(key)
        if data == nil {
            return fmt.Errorf("key not found")
        }
        
        return json.Unmarshal(data, &item)
    })
    if err != nil {
        return nil, err
    }
    return &item, nil
}

// Usage
post, err := getCachedItem[PostMeta](m.db, BucketPosts, []byte(postID))
Benefits:
  • Type Safety - Compile-time type checking
  • Code Reduction - Single implementation for all types
  • Performance - No runtime type assertions

Generic Worker Pool

builder/utils/worker_pool.go
type WorkerPool[T any] struct {
    workers   int
    ctx       context.Context
    wg        sync.WaitGroup
    taskQueue chan T
    handler   func(T)
}

func NewWorkerPool[T any](ctx context.Context, workers int, handler func(T)) *WorkerPool[T] {
    if workers <= 0 {
        workers = runtime.NumCPU()
    }
    if workers > MaxWorkers {
        workers = MaxWorkers
    }
    return &WorkerPool[T]{
        workers:   workers,
        ctx:       ctx,
        taskQueue: make(chan T, workers*WorkerBufferSize),
        handler:   handler,
    }
}

Linting

We use golangci-lint for static analysis.
# Run linter
golangci-lint run

# Auto-fix issues
golangci-lint run --fix

# Run on specific package
golangci-lint run ./builder/services/

Common Issues

Unused variables:
// Bad
result, err := doSomething()
if err != nil {
    return err
}
// result never used

// Good
_, err := doSomething()  // Explicitly ignore
if err != nil {
    return err
}
Error shadowing:
// Bad
err := firstOp()
if err != nil {
    err := logError(err)  // Shadows outer err
    return err
}

// Good  
if err := firstOp(); err != nil {
    if logErr := logError(err); logErr != nil {
        return logErr
    }
    return err
}

Comments and Documentation

Package Documentation

// Package search implements advanced full-text search with BM25 scoring,
// fuzzy matching, stemming, and phrase support.
package search

Function Documentation

// NewWorkerPool creates a worker pool with the specified number of workers.
// If workers <= 0, defaults to runtime.NumCPU().
// If workers > MaxWorkers, capped at MaxWorkers.
func NewWorkerPool[T any](ctx context.Context, workers int, handler func(T)) *WorkerPool[T] {
    // ...
}

Inline Comments

// Calculate initial mmap size based on existing database
initialSize := 10 * 1024 * 1024 // Default 10MB
if info, err := os.Stat(dbPath); err == nil {
    // Use 2x current size, minimum 10MB, maximum 100MB
    calculatedSize := int(info.Size()) * 2
    if calculatedSize > 100*1024*1024 {
        initialSize = 100 * 1024 * 1024
    }
}

TODO Comments

// TODO(username): Add support for custom analyzers
// TODO: Optimize memory usage for large indexes

Build docs developers (and LLMs) love