goreach

module
v0.10.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 28, 2026 License: MIT

README

goreach

Go Reference Go Report Card

Find unreached code paths in running Go services.

goreach sits on top of Go's native coverage instrumentation (go build -cover / GOCOVERDIR) and turns raw coverage data into actionable JSON reports highlighting exactly which functions and code blocks your production traffic never touches.

Key Features

  • Production coverage analysis -- Collect coverage from long-running services without stopping them
  • Flush SDK -- Lightweight, zero-dependency library to emit coverage data on a schedule, via HTTP, signal, or manual trigger
  • Multi-build merge -- Automatically reconcile coverage across different binary versions
  • Web UI -- Interactive browser-based viewer with inline source preview
  • Cloud-agnostic storage -- Local disk, S3, GCS, Azure, or any custom backend via a simple interface

Install

go install github.com/yag13s/goreach/cmd/goreach@latest

Quick Start

# Build with coverage
go build -cover -covermode=set -o myserver ./cmd/myserver

# Run with GOCOVERDIR
mkdir -p /tmp/coverage
GOCOVERDIR=/tmp/coverage ./myserver

# Stop the process, then analyze
goreach analyze -coverdir /tmp/coverage -pretty
View the report in browser
goreach view -src . report.json
Try without building — pre-generated sample reports

The repository includes sample reports so you can explore the viewer and merge workflow immediately:

# View a single-build report
goreach view testdata/sample-reports/v1.json

# Merge two build versions and view
goreach merge -pretty -o testdata/sample-reports/merged.json \
    testdata/sample-reports/v1.json \
    testdata/sample-reports/v2.json
goreach view -src . testdata/sample-reports/merged.json

To generate fresh coverage data from the sample server:

bash testdata/sampleserver/run.sh

CLI Commands

Command Description
goreach analyze Analyze coverage data and output unreached code as JSON
goreach merge Merge multiple reports, taking max coverage per function
goreach view Launch interactive Web UI with optional source preview
goreach summary Print a text coverage summary
goreach version Show version info
analyze flags
Flag Description Default
-profile <file> Text coverage profile path --
-coverdir <dir> GOCOVERDIR path (exclusive with -profile) --
-r Recursively search coverdir false
-pkg <prefixes> Package filter (comma-separated) all
-threshold <float> Show functions with coverage <= X% 100
-min-statements <n> Show functions with >= N unreached statements 0
-o <file> Output file stdout
-pretty Pretty-print JSON false
merge flags
Flag Description Default
-o <file> Output file stdout
-pretty Pretty-print JSON false

Uses the newest report as the structural base. Takes the maximum coverage_percent per function across all inputs. Deleted functions (only in older reports) are excluded.

When an older build wins on coverage but lacks unreached block detail (e.g. covdata func origin), the latest build's blocks are preserved in latest_unreached_blocks. The viewer shows a toggle to switch between merged and latest-build block views.

view flags
Flag Description Default
-src <dir> Source root for inline code preview -- (disabled)
-port <n> HTTP port 0 (random)
-no-open Don't auto-open browser false

Multi-Build Workflow

When deploying new versions of your service, each build produces different coverage metadata. goreach handles this by analyzing each build version separately, then merging the results.

Per-build analyze → merge

Analyze each build version in its own directory, then merge. Merging takes the maximum coverage_percent per function across all inputs, so no coverage is lost when you redeploy.

# Directory structure after collecting coverage from multiple deploys:
# coverage-data/
# ├── abc1234/          ← build version (git short hash)
# │   └── pod-name/
# │       ├── covmeta.*
# │       └── covcounters.*
# └── def5678/
#     └── pod-name/
#         ├── covmeta.*
#         └── covcounters.*

# 1. Analyze each build version separately
for dir in coverage-data/*/; do
    version=$(basename "$dir")
    goreach analyze -coverdir "$dir" -r -pretty -o "reports/$version.json"
done

# 2. Merge all per-version reports
goreach merge -pretty -o merged-report.json reports/*.json

# 3. View in browser (with source preview)
goreach view -src . merged-report.json

Makefile example:

analyze-coverage:
	@mkdir -p coverage-reports
	@for dir in coverage-data/*/; do \
		version=$$(basename "$$dir"); \
		goreach analyze -coverdir "$$dir" -r -pretty \
			-o "coverage-reports/$$version.json"; \
	done

merge-coverage:
	goreach merge -pretty -o coverage-report.json coverage-reports/*.json

coverage: download-coverage analyze-coverage merge-coverage
JSON output vs Web UI
Use case Command Features
CI / scripts analyze -o report.json Filter with jq, diffable, store in Git
Human review view report.json -src . Inline source preview, package tree, block detail
Text overview summary -coverdir ... Terminal-friendly function list with coverage %

JSON report structure (shortened):

{
  "version": 1,
  "generated_at": "2025-02-28T...",
  "mode": "atomic",
  "total": {
    "total_statements": 120,
    "covered_statements": 95,
    "coverage_percent": 79.16
  },
  "packages": [{
    "import_path": "myapp/handler",
    "files": [{
      "file_name": "myapp/handler/handler.go",
      "functions": [{
        "name": "HandleRequest",
        "line": 28,
        "total_statements": 12,
        "covered_statements": 9,
        "coverage_percent": 75.0,
        "unreached_blocks": [
          {"start_line": 42, "end_line": 45, "num_statements": 2}
        ]
      }]
    }]
  }]
}

Flush SDK

A zero-dependency library for collecting coverage from running processes. Embed it in your service to flush coverage data without waiting for process exit.

import "github.com/yag13s/goreach/flush"

flush.Enable(flush.Config{
    Storage:      flush.LocalStorage{Dir: "/var/coverage"},
    ServiceName:  "myserver",
    BuildVersion: version,
    Interval:     5 * time.Minute,
    Clear:        true,
})
defer flush.Stop()

Note: When using the flush SDK, build with -covermode=atomic. The set mode is not supported for runtime counter reads.

Safe to call on binaries built without -cover -- all flush operations become no-ops.

Flush Triggers
Trigger Use Case How
Periodic Long-running servers Config{Interval: 5 * time.Minute}
Manual Lambda, request-scoped flush.Emit()
HTTP CronJob, external trigger flushhttp.Handler()
Signal Batch jobs, non-HTTP processes flush.HandleSignal(syscall.SIGUSR1)
Shutdown All processes defer flush.Stop()
Storage Interface
type Storage interface {
    Store(ctx context.Context, files []string, meta Metadata) error
}

Built-in: LocalStorage, WriterStorage, objstore.Storage (S3/GCS/Azure).

S3 example
import "github.com/yag13s/goreach/flush/objstore"

storage := &objstore.Storage{
    Upload: func(ctx context.Context, key string, body io.Reader) error {
        _, err := s3Client.PutObject(ctx, &s3.PutObjectInput{
            Bucket: &bucket, Key: &key, Body: body,
        })
        return err
    },
}

Default key format: <prefix>/<service>/<version>/<pod>/<filename>

HTTP Endpoints (opt-in)
import "github.com/yag13s/goreach/flush/flushhttp"

mux.Handle("/internal/coverage/", flushhttp.Handler())
Method Path Action
GET /internal/coverage Return current coverage data
POST /internal/coverage/flush Flush to storage
POST /internal/coverage/clear Reset counters

Architecture

flowchart TB
    subgraph App["Instrumented Application"]
        BIN["go build -cover binary"]
        SDK["flush SDK"]
        BIN --- SDK
    end

    subgraph Store["Storage"]
        LOCAL["Local Disk"]
        REMOTE["S3 / GCS / Azure"]
    end

    subgraph CLI["goreach CLI"]
        ANALYZE["analyze"]
        MERGE["merge"]
        VIEW["view"]
    end

    App -- "covmeta + covcounters" --> Store
    Store --> CLI

Deployment Examples

Kubernetes + S3
flush.Enable(flush.Config{
    Storage: &objstore.Storage{
        Upload: func(ctx context.Context, key string, body io.Reader) error {
            _, err := s3Client.PutObject(ctx, &s3.PutObjectInput{
                Bucket: &bucket, Key: &key, Body: body,
            })
            return err
        },
    },
    ServiceName:  "myserver",
    BuildVersion: version,
    Interval:     10 * time.Minute,
})
defer flush.Stop()
# Download and analyze
aws s3 sync s3://bucket/goreach/myserver/ /tmp/coverage/
goreach analyze -coverdir /tmp/coverage -r -pretty
AWS Lambda

Lambda environments freeze between invocations, so timer-based flush does not work. Call flush.Emit() at the end of each request instead.

// init — runs once per cold start
func init() {
    flush.Enable(flush.Config{
        Storage:      storage,  // e.g. objstore.Storage with S3
        ServiceName:  "my-lambda",
        BuildVersion: os.Getenv("BUILD_VERSION"),
        Clear:        true,
    })
}

// handler — flush per invocation
func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
    resp, err := process(ctx, req)
    flush.Emit()  // must be explicit, timers are frozen
    return resp, err
}

Build script — a shell wrapper creates GOCOVERDIR before the Go binary starts and embeds BUILD_VERSION so coverage data is organized by deploy:

VERSION=$(git rev-parse --short HEAD)
GOOS=linux GOARCH=arm64 go build -cover -covermode=atomic -o bootstrap.bin .
printf '#!/bin/sh\nmkdir -p /tmp/coverage-data\nexport BUILD_VERSION=%s\nexec /var/task/bootstrap.bin "$@"\n' "$VERSION" > bootstrap
chmod +x bootstrap

Collect and analyze:

# Download from S3
aws s3 sync s3://my-bucket/goreach/my-lambda/ coverage-data/

# Analyze per build version, then merge
for dir in coverage-data/*/; do
    version=$(basename "$dir")
    goreach analyze -coverdir "$dir" -r -pretty -o "reports/$version.json"
done
goreach merge -pretty -o report.json reports/*.json

# View
goreach view -src . report.json
CronJob-triggered flush
apiVersion: batch/v1
kind: CronJob
spec:
  schedule: "0 */6 * * *"
  jobTemplate:
    spec:
      containers:
      - name: coverage-trigger
        command: ["curl", "-X", "POST", "http://myserver:8080/internal/coverage/flush"]

Overhead

Real-world benchmark on a production AWS Lambda (ARM64, 256 MB, ap-northeast-1) serving a Go API with DynamoDB reads/writes. 50 requests × 14 endpoints (6 read, 8 write lifecycle).

Three configurations compared:

Configuration p50 (avg across endpoints) Δ from baseline
Baseline — normal build 58.4 ms
Instrumentation only-cover -covermode=atomic, no flush 62.7 ms +4.3 ms (+7%)
Full — instrumentation + flush.Emit() to S3 per request 123.0 ms +64.6 ms (+111%)

The -cover instrumentation itself adds ~4 ms per request and ~30 KB to the binary (+0.1%) — negligible for most services.

The remaining ~60 ms in the "full" configuration is the S3 PutObject in flush.Emit(). This cost is not inherent to goreach — it depends entirely on your flush strategy:

  • Lambda (flush.Emit() per request): ~60 ms/req overhead from S3 write
  • Long-running server (Interval: 5 * time.Minute): amortized to near-zero per request
  • HTTP/signal trigger: zero overhead on normal requests
Per-endpoint breakdown

14 REST endpoints (CRUD operations backed by DynamoDB) measured individually:

Endpoint          Op       | Baseline |  Instr.  |   Full   | Instr. Δ
──────────────────────────────────────────────────────────────────────
GET    /items     read     |  49.1 ms |  49.8 ms | 105.1 ms |  +0.7 ms
GET    /items/:id read     |  51.6 ms |  54.3 ms | 113.4 ms |  +2.8 ms
GET    /nested/a  read     |  55.0 ms |  58.3 ms | 117.8 ms |  +3.3 ms
GET    /nested/b  read     |  65.6 ms |  69.9 ms | 127.5 ms |  +4.3 ms
GET    /nested/c  read     |  55.2 ms |  61.8 ms | 115.8 ms |  +6.6 ms
GET    /global    read     |  56.3 ms |  62.8 ms | 117.3 ms |  +6.5 ms
POST   /items     create   |  54.0 ms |  60.3 ms | 120.0 ms |  +6.2 ms
PUT    /items/:id update   |  56.7 ms |  64.1 ms | 120.1 ms |  +7.5 ms
DELETE /items/:id delete   |  53.4 ms |  57.5 ms | 119.6 ms |  +4.1 ms
POST   /comments  create   |  61.0 ms |  64.7 ms | 130.8 ms |  +3.6 ms
DELETE /comments  delete   |  60.4 ms |  67.8 ms | 133.3 ms |  +7.4 ms
POST   /routes    create   |  69.3 ms |  69.3 ms | 140.9 ms |  +0.0 ms
PUT    /routes    update   |  69.8 ms |  71.3 ms | 131.2 ms |  +1.5 ms
DELETE /routes    delete   |  59.9 ms |  65.4 ms | 129.5 ms |  +5.5 ms

All values are p50 (median) over 50 iterations. Measured with curl from the same region.

Requirements

  • Go 1.26+
  • go tool covdata (included with Go)

License

MIT

Directories

Path Synopsis
cmd
goreach command
Command goreach analyzes Go coverage data to identify unreached code paths.
Command goreach analyzes Go coverage data to identify unreached code paths.
Package flush provides zero-dependency runtime coverage collection for Go services.
Package flush provides zero-dependency runtime coverage collection for Go services.
flushhttp
Package flushhttp provides an HTTP handler for triggering coverage flush.
Package flushhttp provides an HTTP handler for triggering coverage flush.
objstore
Package objstore provides a flush.Storage implementation that uploads coverage files to a remote object store (S3, GCS, Azure Blob, etc.).
Package objstore provides a flush.Storage implementation that uploads coverage files to a remote object store (S3, GCS, Azure Blob, etc.).
internal
analysis
Package analysis matches coverage profiles against source AST to identify unreached code.
Package analysis matches coverage profiles against source AST to identify unreached code.
astmap
Package astmap extracts function declarations and their source positions from Go files.
Package astmap extracts function declarations and their source positions from Go files.
covparse
Package covparse converts GOCOVERDIR binary coverage data into text profiles.
Package covparse converts GOCOVERDIR binary coverage data into text profiles.
merge
Package merge combines multiple goreach report.json files into a single report by taking the maximum coverage per function across all builds.
Package merge combines multiple goreach report.json files into a single report by taking the maximum coverage per function across all builds.
report
Package report defines the JSON report schema and generation for goreach.
Package report defines the JSON report schema and generation for goreach.
viewer
Package viewer serves a web UI for goreach report.json files.
Package viewer serves a web UI for goreach report.json files.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL