Skip to content

Go – Code Formatting & Structure

Go is the only major language where formatting is enforced by an official toolgofmt. There is no room for debate about brace placement, indentation style, or spacing. Every Go developer reads and writes the same format.

Non-negotiable rule

All Go code at Cygnus Dynamics must be formatted with gofmt before committing. Code that has not been run through gofmt will be rejected in CI. Configure your editor to run gofmt on save — there is no reason not to.

gofmt — The Formatter

# Format all files in the current module
gofmt -w .

# Use goimports — formats AND manages import groups (preferred)
goimports -w .

# Check formatting without changing files (for CI)
gofmt -l .

goimports is the recommended tool — it does everything gofmt does, plus it automatically adds missing imports and removes unused ones.

Editor integration

Configure your editor to run goimports on every save. In VS Code install the Go extension and enable editor.formatOnSave. In GoLand / IntelliJ, enable goimports as the formatter in Settings → Go → On Save.

Indentation

Go uses tabs for indentation — not spaces. gofmt enforces this. Never argue about it; let the tool handle it.

// ✅ Tabs — enforced by gofmt
func processOrder(ctx context.Context, order *Order) error {
    if order == nil {
        return ErrNilOrder
    }

    for _, item := range order.Items {
        if err := validateItem(item); err != nil {
            return fmt.Errorf("validating item %d: %w", item.ID, err)
        }
    }

    return nil
}

Line Length

Go has no enforced line length limit, but the community standard is to keep lines under 120 characters. Break long lines at natural points:

// ✅ Break long function signatures
func (s *OrderService) CreateOrder(
    ctx             context.Context,
    userID          int64,
    items           []*OrderItem,
    shippingAddress *Address,
) (*Order, error) {
    // ...
}

// ✅ Break long function calls
order, err := s.orderRepo.FindByIDWithItems(
    ctx,
    orderID,
    WithStatus(StatusConfirmed),
    WithEagerItems(),
)

// ✅ Break long conditions — operator at start of next line
if order.Status == StatusPending &&
    order.Total.Amount > MinOrderThreshold &&
    user.IsVerified {
    processOrder(ctx, order)
}

Brace Style

Go uses a mandatory K&R brace style — the opening brace is always on the same line. A newline before { is a syntax error in most contexts:

// ✅ Correct — K&R opening brace on same line
func processOrder(order *Order) error {
    if order.IsEmpty() {
        return ErrEmptyOrder
    }
    return nil
}

// ❌ Syntax error — opening brace on new line
func processOrder(order *Order) error
{                                        // SYNTAX ERROR
    return nil
}

Semicolons

Go does not use semicolons in source code. As a consequence: - else must be on the same line as the closing } of the if block - The opening { of any block must be on the same line as the keyword

// ✅ Correct — else on same line as closing }
if condition {
    doSomething()
} else {
    doSomethingElse()
}

// ❌ Syntax error — else on new line
if condition {
    doSomething()
}
else {                    // SYNTAX ERROR
    doSomethingElse()
}

Blank Lines

package orderservice

import (
    "context"
    "fmt"
    "log/slog"
)
                                // ← one blank line after imports

const defaultTimeout = 30 * time.Second

// OrderService manages order lifecycle operations.
type OrderService struct {
    repo    OrderRepository
    payment PaymentGateway
    logger  *slog.Logger
}
                                // ← one blank line between type and first method

// NewOrderService creates a new OrderService with the given dependencies.
func NewOrderService(repo OrderRepository, payment PaymentGateway, logger *slog.Logger) *OrderService {
    return &OrderService{repo: repo, payment: payment, logger: logger}
}
                                // ← one blank line between functions

// GetOrder retrieves an order by its ID.
func (s *OrderService) GetOrder(ctx context.Context, id int64) (*Order, error) {
    if id <= 0 {
        return nil, fmt.Errorf("invalid order id: %d", id)
    }
                                // ← blank line between logical sections
    order, err := s.repo.FindByID(ctx, id)
    if err != nil {
        return nil, fmt.Errorf("fetching order %d: %w", id, err)
    }

    return order, nil
}

Imports

Three-Group Convention

Organise imports in three groups, each separated by a blank line. goimports does this automatically:

import (
    // Group 1: Standard library
    "context"
    "errors"
    "fmt"
    "time"

    // Group 2: Third-party packages
    "github.com/google/uuid"
    "golang.org/x/sync/errgroup"

    // Group 3: Internal packages
    "github.com/cygnusdynamics/platform/internal/order"
    "github.com/cygnusdynamics/platform/pkg/payment"
)

Rules

  • No unused imports — the Go compiler rejects them
  • No wildcard imports (import . "package") — they pollute the namespace
  • Rename an import only when there is a genuine name collision
// ✅ Rename only when necessary
import (
    googlegrpc   "google.golang.org/grpc"
    internalgrpc "github.com/cygnusdynamics/platform/pkg/grpc"
)

// ❌ Rename without reason
import myFmt "fmt"

Blank Import (Side Effects)

import (
    "database/sql"

    _ "github.com/lib/pq" // registers PostgreSQL driver via init()
)

Source File Structure

1. Package clause
2. Package-level doc comment (if this is doc.go or the main file)
3. Import block
4. Constants
5. Variables
6. Types (structs, interfaces)
7. Functions and methods (exported first, then unexported)
// Package orderservice provides order lifecycle management.
package orderservice

import (
    "context"
    "fmt"
    "time"

    "log/slog"

    "github.com/cygnusdynamics/platform/internal/domain"
)

const (
    DefaultTimeout = 30 * time.Second
    MaxOrderItems  = 100
)

var ErrOrderNotFound = errors.New("order not found")

type OrderService struct {
    repo   domain.OrderRepository
    logger *slog.Logger
}

func NewOrderService(repo domain.OrderRepository, logger *slog.Logger) *OrderService {
    return &OrderService{repo: repo, logger: logger}
}

// Exported methods first
func (s *OrderService) CreateOrder(ctx context.Context, req *domain.CreateOrderRequest) (*domain.Order, error) {
    // ...
}

// Unexported methods after
func (s *OrderService) validateRequest(req *domain.CreateOrderRequest) error {
    // ...
}

Project Structure

Every Cygnus Dynamics Go service follows this layout, based on the widely-adopted Go project layout conventions:

myservice/
├── cmd/
│   └── myservice/
│       └── main.go             # entry point — wires dependencies, starts server
├── internal/                   # private — cannot be imported by other modules
│   ├── order/
│   │   ├── order.go            # domain types: Order, OrderStatus, Money
│   │   ├── service.go          # business logic
│   │   ├── service_test.go     # unit tests
│   │   └── repository.go       # interface definition (owned by consumer)
│   ├── payment/
│   │   ├── payment.go
│   │   └── gateway.go          # interface definition
│   └── config/
│       └── config.go           # validated config from environment
├── postgres/                   # infrastructure: PostgreSQL implementation
│   ├── order_repo.go
│   ├── order_repo_test.go      # integration tests
│   └── migrations/
│       └── 001_create_orders.sql
├── stripe/                     # infrastructure: Stripe payment gateway
│   └── gateway.go
├── api/                        # infrastructure: HTTP handlers and routing
│   ├── order_handler.go
│   ├── order_handler_test.go
│   ├── middleware.go
│   └── routes.go
├── pkg/                        # public utilities — safe to import by other modules
│   └── money/
│       └── money.go
├── go.mod
├── go.sum
├── Makefile
└── README.md

Key rules: - cmd/ — entry points only, thin main() that wires dependencies - internal/ — domain logic and interfaces; import-protected - Infrastructure packages (postgres/, stripe/, api/) — at top level or in infra/ - pkg/ — genuinely reusable utilities that external modules may import; keep it small - Never put business logic in main.go - Test files live alongside the code they test, not in a separate test/ directory

Package Comments

Every package must have a doc comment:

// Package orderservice provides order lifecycle management for
// the Cygnus Dynamics platform.
package orderservice

For packages with extensive documentation, use a doc.go file:

// Package payment provides a unified interface for payment gateway integration.
//
// Supported gateways:
//   - Stripe
//   - PayPal
//   - Adyen
package payment
Tool Purpose Command
gofmt Canonical formatting gofmt -l .
goimports Formatting + import management goimports -w .
go vet Static analysis — catches real bugs go vet ./...
staticcheck Extended static analysis staticcheck ./...
golangci-lint Multi-linter runner golangci-lint run ./...
go test Run all tests go test ./...
go test -race Race condition detector go test -race ./...

Standard .golangci.yml:

linters:
  enable:
    - errcheck
    - gosimple
    - govet
    - ineffassign
    - staticcheck
    - unused
    - goimports
    - misspell
    - revive

linters-settings:
  goimports:
    local-prefixes: github.com/cygnusdynamics

issues:
  exclude-use-default: false

Makefile targets:

.PHONY: fmt lint test

fmt:
    goimports -w .

lint:
    golangci-lint run ./...

test:
    go test -race -cover ./...

check: fmt lint test