Skip to main content

Code Style Guide

Station follows standard Go conventions with some project-specific guidelines.

Go Style

Official Guidelines

Station follows the official Go style guides:

Formatting

# Format code (required before commit)
make fmt

# Or directly
gofmt -s -w .
goimports -w .

Linting

# Run linter
make lint

# Or directly
golangci-lint run
The project uses golangci-lint with these enabled linters:
  • gofmt - Format checking
  • goimports - Import organization
  • govet - Go vet checks
  • errcheck - Error handling
  • staticcheck - Static analysis
  • gosimple - Simplifications
  • ineffassign - Unused assignments

Naming Conventions

Packages

// Good - short, lowercase, no underscores
package services
package repositories
package config

// Bad
package agent_services
package AgentRepository

Functions and Methods

// Good - verb-noun, exported if needed
func (s *AgentService) Execute(ctx context.Context, id int64) error
func (r *AgentRepository) GetByID(ctx context.Context, id int64) (*Agent, error)
func parseConfig(path string) (*Config, error)

// Bad
func (s *AgentService) DoExecute(ctx context.Context, id int64) error  // redundant "Do"
func (r *AgentRepository) agent_by_id(id int64) *Agent                  // snake_case

Interfaces

// Good - suffix with -er for single method, or describe behavior
type AgentExecutor interface {
    Execute(ctx context.Context, task string) (string, error)
}

type AgentService interface {
    Execute(ctx context.Context, id int64, task string) (string, error)
    Create(ctx context.Context, agent *Agent) (int64, error)
    Update(ctx context.Context, agent *Agent) error
    Delete(ctx context.Context, id int64) error
}

// Bad
type IAgentService interface { }  // Don't prefix with I

Variables

// Good
var defaultTimeout = 30 * time.Second
agentID := params.Get("id")
mcpServer := config.MCPServers[0]

// Bad
var DefaultTimeout = 30 * time.Second  // unexported should be lowercase
agent_id := params.Get("id")           // snake_case

Constants

// Good
const (
    DefaultMaxSteps    = 5
    DefaultAPIPort     = 8585
    StatusPending      = "pending"
    StatusRunning      = "running"
    StatusCompleted    = "completed"
)

// Bad
const DEFAULT_MAX_STEPS = 5  // SCREAMING_SNAKE_CASE

Code Organization

File Structure

// Order of declarations in a file:
// 1. Package comment (if main file)
// 2. Package declaration
// 3. Imports (grouped: stdlib, external, internal)
// 4. Constants
// 5. Variables
// 6. Types (interfaces, then structs)
// 7. Functions (constructors first, then methods)

package services

import (
    "context"
    "database/sql"
    "fmt"
    "time"

    "github.com/google/uuid"
    "go.opentelemetry.io/otel"

    "github.com/cloudshipai/station/internal/config"
    "github.com/cloudshipai/station/internal/db/repositories"
)

const (
    defaultTimeout = 30 * time.Second
)

var (
    ErrNotFound = errors.New("not found")
)

type AgentService interface {
    Execute(ctx context.Context, id int64, task string) error
}

type agentServiceImpl struct {
    repo   repositories.AgentRepository
    config *config.Config
}

func NewAgentService(repo repositories.AgentRepository, cfg *config.Config) AgentService {
    return &agentServiceImpl{repo: repo, config: cfg}
}

func (s *agentServiceImpl) Execute(ctx context.Context, id int64, task string) error {
    // ...
}

Import Grouping

import (
    // Standard library
    "context"
    "fmt"
    "time"

    // External dependencies
    "github.com/labstack/echo/v4"
    "go.opentelemetry.io/otel"

    // Internal packages
    "github.com/cloudshipai/station/internal/config"
    "github.com/cloudshipai/station/internal/services"
)

Error Handling

Error Wrapping

// Good - wrap errors with context
func (s *AgentService) Execute(ctx context.Context, id int64, task string) error {
    agent, err := s.repo.GetByID(ctx, id)
    if err != nil {
        return fmt.Errorf("get agent %d: %w", id, err)
    }

    result, err := s.engine.Execute(ctx, agent, task)
    if err != nil {
        return fmt.Errorf("execute agent %s: %w", agent.Name, err)
    }

    return nil
}

// Bad - losing error context
func (s *AgentService) Execute(ctx context.Context, id int64, task string) error {
    agent, err := s.repo.GetByID(ctx, id)
    if err != nil {
        return err  // No context about where/why
    }
    return nil
}

Custom Errors

// Define sentinel errors
var (
    ErrAgentNotFound    = errors.New("agent not found")
    ErrInvalidInput     = errors.New("invalid input")
    ErrExecutionTimeout = errors.New("execution timeout")
)

// Use errors.Is for checking
if errors.Is(err, ErrAgentNotFound) {
    return http.StatusNotFound, nil
}

Don’t Ignore Errors

// Good
result, err := s.Execute(ctx, task)
if err != nil {
    log.Error("execution failed", "error", err)
    return err
}

// Bad
result, _ := s.Execute(ctx, task)  // Ignoring error!

Context Usage

// Always pass context as first parameter
func (s *AgentService) Execute(ctx context.Context, id int64, task string) error {
    // Use context for cancellation
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
    }

    // Pass context to downstream calls
    agent, err := s.repo.GetByID(ctx, id)
    if err != nil {
        return err
    }

    return nil
}

Concurrency

Channel Patterns

// Use buffered channels when appropriate
results := make(chan Result, len(tasks))

// Always close channels from producer
go func() {
    defer close(results)
    for _, task := range tasks {
        results <- process(task)
    }
}()

// Consumer reads until closed
for result := range results {
    handleResult(result)
}

Mutex Usage

type SafeCounter struct {
    mu    sync.RWMutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.count
}

Documentation

Package Comments

// Package services provides the core business logic for Station.
// It includes agent execution, MCP management, and environment configuration.
package services

Function Comments

// Execute runs the specified agent with the given task.
// It returns the agent's response or an error if execution fails.
//
// The execution respects the context deadline and will return
// context.DeadlineExceeded if the timeout is reached.
func (s *AgentService) Execute(ctx context.Context, id int64, task string) (string, error) {
    // ...
}

Inline Comments

func (s *AgentService) Execute(ctx context.Context, id int64, task string) error {
    // Validate agent exists before execution
    agent, err := s.repo.GetByID(ctx, id)
    if err != nil {
        return err
    }

    // Initialize MCP connections for tool access
    // This may take a few seconds for large environments
    mcpManager, err := s.initMCP(ctx, agent.Environment)
    if err != nil {
        return fmt.Errorf("init mcp: %w", err)
    }
    defer mcpManager.Close()

    // Execute with timeout from agent config
    timeout := time.Duration(agent.TimeoutSeconds) * time.Second
    ctx, cancel := context.WithTimeout(ctx, timeout)
    defer cancel()

    return s.engine.Execute(ctx, agent, task)
}

Testing Style

See the Testing Guide for detailed testing conventions.
// Use descriptive test names
func TestAgentService_Execute_ReturnsErrorWhenAgentNotFound(t *testing.T) { }
func TestAgentService_Execute_SuccessfulExecution(t *testing.T) { }
func TestAgentService_Execute_TimeoutExceeded(t *testing.T) { }

// Use table-driven tests
func TestValidateInput(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        wantErr bool
    }{
        {"valid input", "hello", false},
        {"empty input", "", true},
    }
    // ...
}

Pre-commit Checklist

Before committing:
  1. Format code: make fmt
  2. Run linter: make lint
  3. Run tests: make test
  4. Check for issues: make check (runs fmt, lint, test)

Next Steps