Skip to content

Code Style Guide

This document defines the coding standards and conventions for AgenticGoKit to ensure consistency, readability, and maintainability across the codebase.

🎯 Core Principles

  1. Clarity over Cleverness: Write code that is easy to understand
  2. Consistency: Follow established patterns throughout the codebase
  3. Simplicity: Prefer simple solutions over complex ones
  4. Performance: Be mindful of performance implications
  5. Documentation: Code should be self-documenting with helpful comments

🏗️ Go Language Standards

Follow Standard Go Conventions

AgenticGoKit adheres to all standard Go conventions:

Formatting and Tools

Use the standard Go toolchain for consistent formatting:

bash
# Format code
go fmt ./...

# Run linter
golangci-lint run

# Check for unused code
go mod tidy

# Vet for common mistakes
go vet ./...

Required Tools Configuration

.golangci.yml

yaml
run:
  timeout: 5m
  modules-download-mode: readonly

linters-settings:
  gocyclo:
    min-complexity: 15
  
  goconst:
    min-len: 3
    min-occurrences: 3
  
  goimports:
    local-prefixes: github.com/kunalkushwaha/agenticgokit
  
  govet:
    check-shadowing: true
  
  misspell:
    locale: US

linters:
  enable:
    - gofmt
    - goimports
    - govet
    - gocyclo
    - goconst
    - misspell
    - ineffassign
    - staticcheck
    - unused
    - errcheck
    - gosimple
    - deadcode
    - varcheck
    - typecheck

issues:
  exclude-rules:
    - path: _test\.go
      linters:
        - gocyclo
        - errcheck
        - dupl
        - gosec

📁 Package Organization

Directory Structure

agenticgokit/
├── cmd/                    # Main applications
│   └── agentcli/          # CLI application
├── core/                   # Public API
│   ├── agent.go           # Core interfaces
│   ├── runner.go          # Public runner interface
│   └── *.go               # Other public APIs
├── internal/               # Private implementation
│   ├── agents/            # Agent implementations
│   ├── mcp/               # MCP implementation
│   ├── llm/               # LLM provider implementations
│   └── */                 # Other internal packages
├── pkg/                    # Utility packages (if needed)
├── examples/               # Example applications
├── docs/                   # Documentation
└── scripts/                # Build and development scripts

Package Naming

  • Use lowercase, single-word package names
  • Avoid underscores or mixed case
  • Package names should be descriptive but concise
  • Avoid generic names like util, common, base
go
// Good
package mcp
package agents
package llm

// Bad
package mcpUtils
package agent_handlers
package LLMProviders

Import Organization

Group imports in this order with blank lines between groups:

go
package core

import (
    // Standard library
    "context"
    "fmt"
    "time"
    
    // Third-party dependencies
    "github.com/spf13/cobra"
    "go.opentelemetry.io/otel/trace"
    
    // Local imports
    "github.com/kunalkushwaha/agenticgokit/internal/mcp"
    "github.com/kunalkushwaha/agenticgokit/pkg/utils"
)

🏷️ Naming Conventions

Variables and Functions

Use camelCase for variables and functions:

go
// Good
var agentCount int
var lastExecutionTime time.Time
func executeAgent() error
func getAgentByName(name string) Agent

// Bad
var agent_count int
var LastExecutionTime time.Time
func execute_agent() error
func GetAgentByName(name string) Agent

Constants

Use camelCase for unexported constants, PascalCase for exported:

go
// Good
const defaultTimeout = 30 * time.Second
const MaxRetryAttempts = 3

// Bad
const DEFAULT_TIMEOUT = 30 * time.Second
const max_retry_attempts = 3

Types and Interfaces

Use PascalCase for exported types, camelCase for unexported:

go
// Good
type Agent interface {}
type AgentHandler interface {}
type llmProvider struct {}

// Bad
type agent interface {}
type agentHandler interface {}
type LLMProvider struct {}

Interface Naming

  • Use noun or adjective forms
  • Single-method interfaces often end with "-er"
  • Avoid "I" prefix
go
// Good
type Runner interface {}
type AgentHandler interface {}
type Executor interface {}

// Bad
type IRunner interface {}
type AgentHandlerInterface interface {}
type ExecutorImpl interface {}

📝 Documentation Standards

Package Documentation

Every package should have a package comment:

go
// Package core provides the public API for AgenticGoKit.
// Define interfaces in core and keep implementations in internal/ and plugins/.
//
// Example usage (config-driven):
//	runner, err := core.NewRunnerFromConfig("agentflow.toml")
//	if err != nil { log.Fatal(err) }
//	_ = runner.RegisterAgent("my-agent", handler)
package core

Function Documentation

Document all exported functions with their purpose, parameters, return values, and any side effects:

go
// NewRunner creates a new agent runner with the provided configuration.
// It initializes all configured LLM providers and MCP servers.
//
// The runner will not start processing events until Start() is called.
// Configuration errors will be returned immediately, while connection
// errors to external services may be retried automatically.
//
// Parameters:
//   - config: Configuration for the runner and its dependencies
//
// Returns:
//   - *Runner: Configured runner instance
//   - error: Configuration or initialization error
func NewRunner(config *Config) (*Runner, error) {
    // Implementation...
}

Type Documentation

Document types, especially interfaces:

go
// AgentHandler defines the interface for implementing agent logic.
//
// Implementations should be stateless and thread-safe, as the same
// handler may be called concurrently by multiple goroutines.
//
// The Run method should process the input event and state, perform
// any necessary operations (including calling tools or LLMs), and
// return the result with any state changes.
type AgentHandler interface {
    // Run processes an event and returns the result.
    //
    // The context may include deadlines, cancellation, and tracing
    // information. Implementations should respect context cancellation.
    //
    // The event contains the input data and metadata. The state
    // represents the current session state and may be modified.
    //
    // Returns AgentResult with response data and updated state,
    // or an error if processing fails.
    Run(ctx context.Context, event Event, state State) (AgentResult, error)
}

Comment Guidelines

  • Use complete sentences with proper capitalization and punctuation
  • Explain "why" not just "what"
  • Include examples for complex functionality
  • Document any limitations or gotchas
go
// validateConfig checks the configuration for common errors and
// provides helpful suggestions for fixes.
//
// This validation is performed at startup to catch configuration
// issues early, before attempting to connect to external services.
// Some validations (like network connectivity) are performed lazily.
func validateConfig(config *Config) error {
    // Check required fields first to provide clear error messages
    if config.LLM.Provider == "" {
        return fmt.Errorf("llm.provider is required")
    }
    
    // Validate provider-specific configuration
    switch config.LLM.Provider {
    case "azure":
        return validateAzureConfig(&config.LLM.Azure)
    case "openai":
        return validateOpenAIConfig(&config.LLM.OpenAI)
    default:
        return fmt.Errorf("unsupported llm provider: %s", config.LLM.Provider)
    }
}

🔧 Error Handling

Error Types

Define specific error types for different categories:

go
// ValidationError represents a configuration or input validation error
type ValidationError struct {
    Field   string
    Value   interface{}
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed for field %s: %s", e.Field, e.Message)
}

// TimeoutError represents an operation timeout
type TimeoutError struct {
    Operation string
    Duration  time.Duration
}

func (e *TimeoutError) Error() string {
    return fmt.Sprintf("operation %s timed out after %v", e.Operation, e.Duration)
}

Error Wrapping

Use error wrapping to provide context:

go
func executeAgent(ctx context.Context, agent AgentHandler, event Event, state State) (AgentResult, error) {
    result, err := agent.Run(ctx, event, state)
    if err != nil {
        return AgentResult{}, fmt.Errorf("failed to execute agent %s: %w", agent.Name(), err)
    }
    return result, nil
}

Error Messages

  • Start with lowercase letter (Go convention)
  • Be specific and actionable
  • Include relevant context
  • Avoid implementation details in user-facing errors
go
// Good
return fmt.Errorf("failed to connect to MCP server %s: %w", serverName, err)
return ValidationError{Field: "timeout", Message: "must be positive"}

// Bad
return fmt.Errorf("Connection failed")
return fmt.Errorf("Error in line 42 of mcp.go")

🧪 Testing Standards

Test File Organization

  • Test files should be in the same package as the code they test
  • Use _test.go suffix
  • Group related tests in the same file
go
// agent_test.go
package core

import (
    "context"
    "testing"
    "time"
    
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

Test Function Naming

Use descriptive test names that explain the scenario:

go
// Good
func TestAgent_Run_WithValidInput_ReturnsSuccess(t *testing.T) {}
func TestRunner_RegisterAgent_WithNilHandler_ReturnsError(t *testing.T) {}
func TestMCPManager_ExecuteTool_WhenServerUnavailable_RetriesAndFails(t *testing.T) {}

// Bad
func TestAgent(t *testing.T) {}
func TestRunner1(t *testing.T) {}
func TestError(t *testing.T) {}

Test Structure

Use the Arrange-Act-Assert pattern:

go
func TestAgent_Run_WithValidInput_ReturnsSuccess(t *testing.T) {
    // Arrange
    agent := &TestAgent{name: "test-agent"}
    event := NewEvent("test", map[string]interface{}{
        "query": "Hello world",
    })
    state := NewState()
    ctx := context.Background()
    
    // Act
    result, err := agent.Run(ctx, event, state)
    
    // Assert
    require.NoError(t, err)
    assert.True(t, result.Success)
    assert.Equal(t, "Hello world", result.Data["processed_query"])
}

Table-Driven Tests

Use table-driven tests for multiple scenarios:

go
func TestValidateConfig(t *testing.T) {
    tests := []struct {
        name    string
        config  Config
        wantErr bool
        errMsg  string
    }{
        {
            name: "valid azure config",
            config: Config{
                LLM: LLMConfig{
                    Provider: "azure",
                    Azure: AzureConfig{
                        Endpoint: "https://test.openai.azure.com",
                        APIKey:   "test-key",
                    },
                },
            },
            wantErr: false,
        },
        {
            name: "missing provider",
            config: Config{
                LLM: LLMConfig{},
            },
            wantErr: true,
            errMsg:  "llm.provider is required",
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := validateConfig(&tt.config)
            
            if tt.wantErr {
                require.Error(t, err)
                assert.Contains(t, err.Error(), tt.errMsg)
            } else {
                require.NoError(t, err)
            }
        })
    }
}

🚀 Performance Guidelines

Memory Allocation

Minimize allocations in hot paths:

go
// Good - reuse slice capacity
func processEvents(events []Event) []Result {
    results := make([]Result, 0, len(events))
    for _, event := range events {
        result := processEvent(event)
        results = append(results, result)
    }
    return results
}

// Bad - repeated allocations
func processEvents(events []Event) []Result {
    var results []Result
    for _, event := range events {
        result := processEvent(event)
        results = append(results, result)
    }
    return results
}

String Building

Use strings.Builder for efficient string concatenation:

go
// Good
func buildPrompt(parts []string) string {
    var builder strings.Builder
    builder.Grow(len(parts) * 50) // Pre-allocate if size is known
    
    for i, part := range parts {
        if i > 0 {
            builder.WriteString("\n")
        }
        builder.WriteString(part)
    }
    
    return builder.String()
}

// Bad
func buildPrompt(parts []string) string {
    result := ""
    for i, part := range parts {
        if i > 0 {
            result += "\n"
        }
        result += part
    }
    return result
}

Context Usage

Always pass context and respect cancellation:

go
func processWithTimeout(ctx context.Context, data []byte) error {
    // Create timeout context
    timeoutCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()
    
    // Check for cancellation in loops
    for i, item := range data {
        select {
        case <-timeoutCtx.Done():
            return timeoutCtx.Err()
        default:
        }
        
        if err := processItem(timeoutCtx, item); err != nil {
            return fmt.Errorf("failed to process item %d: %w", i, err)
        }
    }
    
    return nil
}

🔒 Security Guidelines

Input Validation

Validate all external inputs:

go
func processUserQuery(query string) error {
    // Validate input length
    if len(query) > maxQueryLength {
        return ValidationError{
            Field:   "query",
            Message: fmt.Sprintf("exceeds maximum length of %d characters", maxQueryLength),
        }
    }
    
    // Check for malicious content
    if containsSQLInjection(query) {
        return ValidationError{
            Field:   "query",
            Message: "contains potentially malicious content",
        }
    }
    
    return nil
}

Secrets Handling

Never log or expose secrets:

go
// Good
func logConfig(config *Config) {
    log.Printf("LLM Provider: %s", config.LLM.Provider)
    log.Printf("Endpoint: %s", config.LLM.Azure.Endpoint)
    // Don't log API key
}

// Bad
func logConfig(config *Config) {
    log.Printf("Config: %+v", config) // This might expose secrets
}

Resource Limits

Implement appropriate limits:

go
const (
    maxConcurrentRequests = 100
    maxRequestSize       = 10 * 1024 * 1024 // 10MB
    maxExecutionTime     = 5 * time.Minute
)

func processRequest(ctx context.Context, request Request) error {
    // Check size limits
    if len(request.Data) > maxRequestSize {
        return fmt.Errorf("request too large: %d bytes", len(request.Data))
    }
    
    // Set timeout
    timeoutCtx, cancel := context.WithTimeout(ctx, maxExecutionTime)
    defer cancel()
    
    return doProcessRequest(timeoutCtx, request)
}

📋 Code Review Checklist

Before Submitting

  • [ ] Code follows Go formatting standards (go fmt)
  • [ ] All linters pass (golangci-lint run)
  • [ ] Tests are written and passing
  • [ ] Documentation is updated
  • [ ] Error handling is appropriate
  • [ ] Performance implications considered
  • [ ] Security implications considered

During Review

  • [ ] Code is readable and well-structured
  • [ ] Variable and function names are clear
  • [ ] Comments explain complex logic
  • [ ] Error messages are helpful
  • [ ] Tests cover edge cases
  • [ ] No obvious performance issues
  • [ ] Follows established patterns

This code style guide ensures consistency and quality across the AgenticGoKit codebase, making it easier for contributors to understand, maintain, and extend the system.

Note: Prefer config-driven orchestration and runner creation in examples. Avoid direct use of internal builders or processEvent in public docs and comments.

Released under the Apache 2.0 License.