Skip to content

Go Style Guide

Effective Go standards for Fulcrum backend services.


Overview

This guide summarizes key rules from Effective Go and Fulcrum-specific conventions for writing idiomatic Go code.


Formatting

Rule Standard
Formatter gofmt (mandatory)
Indentation Tabs
Line Length No strict limit

All Go code must be formatted with gofmt. This is non-negotiable.


Naming

General Rules

  • Use MixedCaps or mixedCaps for multi-word names
  • Do not use underscores in names
  • Exported names start with uppercase (public)
  • Unexported names start with lowercase (private)

Specific Conventions

Type Convention Example
Package Short, lowercase, single-word policyengine
Getter No Get prefix Owner() not GetOwner()
Interface Method name + -er suffix Evaluator, Publisher

Fulcrum Conventions

// Context first in function signatures
func CreateEnvelope(ctx context.Context, tenantID string, req *CreateRequest) (*Envelope, error)

// Error wrapping with context
if err := store.Save(ctx, envelope); err != nil {
    return nil, fmt.Errorf("save envelope: %w", err)
}

// Interface suffix: -er
type PolicyEvaluator interface {
    Evaluate(ctx context.Context, req *EvalRequest) (*EvalResult, error)
}

Control Structures

If Statements

// No parentheses, mandatory braces, initialization allowed
if err := file.Chmod(0664); err != nil {
    return fmt.Errorf("chmod: %w", err)
}

For Loops

// Go's only looping construct
for i := 0; i < 10; i++ { }     // Traditional
for condition { }                // While-style
for { }                          // Infinite
for key, value := range items { } // Range iteration

Switch Statements

// No fall-through by default (use fallthrough explicitly)
switch status {
case StatusPending:
    // Handle pending
case StatusActive:
    // Handle active
default:
    return fmt.Errorf("unknown status: %s", status)
}

// Expression-less switch for cleaner if-else chains
switch {
case cost > 100:
    return ErrBudgetExceeded
case cost > 50:
    return ErrBudgetWarning
default:
    return nil
}

Functions

Multiple Returns

// Standard pattern: value + error
func GetPolicy(ctx context.Context, id string) (*Policy, error) {
    policy, err := store.Get(ctx, id)
    if err != nil {
        return nil, fmt.Errorf("get policy %s: %w", id, err)
    }
    return policy, nil
}

Defer

// Use for cleanup (runs when function returns)
func ReadFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()

    return io.ReadAll(f)
}

Data Structures

new vs make

Function Purpose Returns
new(T) Allocates zeroed memory *T (pointer)
make(T, ...) Initializes slices, maps, channels T (value)
// new: allocates and zeros
p := new(Policy)  // *Policy pointing to zeroed Policy

// make: initializes
s := make([]Policy, 0, 10)    // Empty slice, capacity 10
m := make(map[string]*Policy) // Initialized map
ch := make(chan Event, 100)   // Buffered channel

Slices and Maps

// Prefer slices over arrays
policies := make([]*Policy, 0)

// Map existence check with comma-ok
value, ok := myMap[key]
if !ok {
    return nil, ErrNotFound
}

Interfaces

Implicit Implementation

// No "implements" keyword needed
type PolicyEvaluator interface {
    Evaluate(ctx context.Context, req *EvalRequest) (*EvalResult, error)
}

// InMemoryEvaluator implements PolicyEvaluator by having the method
type InMemoryEvaluator struct {
    policies []*Policy
}

func (e *InMemoryEvaluator) Evaluate(ctx context.Context, req *EvalRequest) (*EvalResult, error) {
    // Implementation
}

Prefer Small Interfaces

// Good: Single-method interface
type Evaluator interface {
    Evaluate(ctx context.Context, req *EvalRequest) (*EvalResult, error)
}

// Good: Compose small interfaces
type PolicyService interface {
    Evaluator
    PolicyReader
    PolicyWriter
}

Concurrency

Core Philosophy

Do not communicate by sharing memory; instead, share memory by communicating.

Goroutines

// Lightweight concurrent function
go processEnvelope(ctx, envelope)

// With error handling via channels
errCh := make(chan error, 1)
go func() {
    errCh <- processEnvelope(ctx, envelope)
}()

Channels

// Create typed channels
events := make(chan Event, 100)  // Buffered channel

// Send and receive
events <- event      // Send
event := <-events    // Receive

// Select for multiple channels
select {
case event := <-events:
    handle(event)
case <-ctx.Done():
    return ctx.Err()
}

Error Handling

Explicit Handling Required

// NEVER discard errors
result, _ := doThing()  // BAD

// ALWAYS handle or propagate
result, err := doThing()
if err != nil {
    return fmt.Errorf("do thing: %w", err)
}

Error Wrapping

// Wrap errors with context
if err := validate(req); err != nil {
    return fmt.Errorf("validate request: %w", err)
}

// Check wrapped errors
if errors.Is(err, ErrNotFound) {
    return nil, status.Error(codes.NotFound, "policy not found")
}

Panic

// Only for truly unrecoverable situations
// Libraries should NEVER panic
func mustParse(s string) Config {
    cfg, err := Parse(s)
    if err != nil {
        panic(fmt.Sprintf("parse config: %v", err))
    }
    return cfg
}

Fulcrum-Specific Patterns

Context Usage

// Always pass context first
func (s *Service) CreateEnvelope(ctx context.Context, req *CreateRequest) (*Envelope, error) {
    // Use context for cancellation and tenant info
    tenantID := tenant.FromContext(ctx)

    // Propagate context to all downstream calls
    return s.store.Create(ctx, tenantID, envelope)
}

Table-Driven Tests

func TestEvaluate(t *testing.T) {
    tests := []struct {
        name    string
        input   *EvalRequest
        want    Decision
        wantErr bool
    }{
        {
            name:  "allows safe request",
            input: &EvalRequest{Tool: "read_file"},
            want:  DecisionAllow,
        },
        {
            name:  "denies bash",
            input: &EvalRequest{Tool: "bash"},
            want:  DecisionDeny,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := evaluator.Evaluate(ctx, tt.input)
            if (err != nil) != tt.wantErr {
                t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if got.Decision != tt.want {
                t.Errorf("decision = %v, want %v", got.Decision, tt.want)
            }
        })
    }
}

See Also


Document Version: 1.0 Last Updated: January 20, 2026