Kosh follows Go best practices and specific conventions for consistency and maintainability.
General Go Conventions
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.
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))
}
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
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
// 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
}
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] {
// ...
}
// 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(username): Add support for custom analyzers
// TODO: Optimize memory usage for large indexes