« Back to Index

Basic Go Project Structure

View original Gist on GitHub

Tags: #go #project

1. README.md

.
├── Makefile
├── cmd
│   └── api
│       └── main.go
├── go.mod
├── go.sum
├── internal
│   ├── api
│   │   └── api.go
│   └── log
│       ├── log.go
│       └── log_test.go
├── revive.toml
├── tools.mod
└── tools.sum

Makefile

.DEFAULT_GOAL := run  ## Default make target
TOOLS = ""            ## List of dev tools
TOOLS = \
	github.com/mgechev/revive \
	golang.org/x/lint/golint \
	golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness \
	golang.org/x/vuln/cmd/govulncheck \
	honnef.co/go/tools/cmd/staticcheck \
	mvdan.cc/gofumpt

.PHONY: help
help: ## Displays list of Makefile targets and documented variables
	@echo "Targets:"
	@grep -h -E '^[0-9a-zA-Z_.-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "  \033[36m%-20s\033[0m %s\n", $$1, $$2}'
	@echo ""
	@echo "Variables:"
	@grep -h -E '^[0-9a-zA-Z_.-]+\s[?:]?=.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = "[?:]?=.*?## "}; {printf "  \033[36m%-20s\033[0m %s\n", $$1, $$2}'
	@echo ""
	@echo "Default target:"
	@printf "  \033[36m%s\033[0m\n" $(.DEFAULT_GOAL)

.PHONY: api-update
api-update: ## Update all API application dependencies
	go get -u -t ./...
	go mod tidy
	if [ -d "vendor" ]; then go mod vendor; fi

.PHONY: fmt
fmt: ## Format all Go files using gofumpt
	go tool -modfile=tools.mod gofumpt -w .

.PHONY: lint-all
lint-all: lint-golint lint-govet lint-govul lint-nilness lint-revive lint-staticcheck ## Lint project using all linters

.PHONY: lint-golint
lint-golint: ## Lint project using golint
	go tool -modfile=tools.mod golint -set_exit_status $(shell go list -f '{{.Dir}}' ./... )

.PHONY: lint-govet
lint-govet: ## Lint project using go vet
	go vet ./...

.PHONY: lint-govul
lint-govul: ## Lint project using govulncheck
	go tool -modfile=tools.mod govulncheck ./...

.PHONY: lint-nilness
lint-nilness: ## Lint project using nilness
	go tool -modfile=tools.mod nilness ./...

.PHONY: lint-revive
lint-revive: ## Lint project using revive
	go tool -modfile=tools.mod revive -config revive.toml ./...

.PHONY: lint-staticcheck
lint-staticcheck: ## Lint project using staticcheck
	go tool -modfile=tools.mod staticcheck ./...

.PHONY: run
run: ## Run the API server (opts: HUMANLOG=true)
	@# humanlog doesn't sort keys lexicographically.
	@# to do that we must set `--sort-longest=false`
	@# this causes the key sorting we want (as a side effect)
	@# while at the same time avoiding humanlog from trying to sort by key length
	go run ./cmd/api/main.go $(if \
		$(filter true,$(HUMANLOG)),| \
			humanlog \
				--message-fields=event \
				--sort-longest=false \
				--truncate=false, \
	)

.PHONY: test
test: ## Run the Go test suite
	go test ./...

.PHONY: tools-install
tools-install: ## Install dev tools
	@if [ ! -f tools.mod ]; then \
		echo "Initializing tools.mod"; \
		go mod init -modfile=tools.mod github.com/fastly/ascerta/tools; \
	fi
	@$(foreach tool,$(TOOLS), \
		if ! go tool -modfile=tools.mod | grep "$(tool)" >/dev/null; then \
			echo "installing $(tool)"; \
			go get -modfile=tools.mod -tool "$(tool)"@latest; \
		fi; \
	)
	@[ -x "$(shell which humanlog)" ] || curl -sSL "https://humanlog.io/install.sh" | NONINTERACTIVE=true bash

.PHONY: tools-update
tools-update: ## Update dev tools
	go get -u -modfile=tools.mod tool
	go mod tidy

# checkmake (https://github.com/mrtazz/checkmake) requires these targets be set
.PHONY: all clean test

cmd - api - main.go

// Package main is the starting point for the Ascerta API.
package main

import (
	"context"
	"log/slog"
	"os"

	"github.com/fastly/ascerta/internal/api"
	"github.com/fastly/ascerta/internal/log"
)

func main() {
	l := log.New()
	if err := api.Run(l); err != nil {
		ctx := context.Background()
		l.LogAttrs(ctx, slog.LevelError, "api_run", slog.Any("err", err))
		os.Exit(1)
	}
}

go.mod

module github.com/fastly/ascerta

go 1.24

require github.com/fastly/fst-go v1.13.0

require (
	github.com/beorn7/perks v1.0.1 // indirect
	github.com/cespare/xxhash/v2 v2.3.0 // indirect
	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
	github.com/prometheus/client_golang v1.21.1 // indirect
	github.com/prometheus/client_model v0.6.1 // indirect
	github.com/prometheus/common v0.63.0 // indirect
	github.com/prometheus/procfs v0.15.1 // indirect
	golang.org/x/sys v0.31.0 // indirect
	google.golang.org/protobuf v1.36.5 // indirect
)

go.sum

github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fastly/fst-go v1.13.0 h1:rtRF6RZUjOBMGzaNWD9M87b6yEJn8yPWHZ2dcQjR1TA=
github.com/fastly/fst-go v1.13.0/go.mod h1:95Gbykg+NKlc2JPo9SfZp5W6wWILlLqdc6HUIWEySsc=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal - api - api.go

// Package api contains code for running an API server.
package api

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

const (
	apiPort         = ":8080"
	apiReadTimeout  = time.Duration(5) * time.Second
	apiWriteTimeout = time.Duration(5) * time.Second
)

// Run starts the API.
func Run(l *slog.Logger) error {
	ctx := context.Background()

	s := http.Server{
		Addr:         apiPort,
		ReadTimeout:  apiReadTimeout,
		WriteTimeout: apiWriteTimeout,
	}

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

	go func() {
		signalType := <-quit
		l.LogAttrs(ctx, slog.LevelWarn, "api_shutdown", slog.String("signal", signalType.String()))

		if err := s.Shutdown(context.Background()); err != nil { // nolint:contextcheck // we use a separate context to avoid cancellation
			err := fmt.Errorf("unable to gracefully stop API server: %w", err)
			l.LogAttrs(ctx, slog.LevelError, "api_shutdown", slog.Any("error", err))
		}
	}()

	if err := s.ListenAndServe(); err != nil {
		err := fmt.Errorf("api server failed to listen and serve requests: %w", err)
		l.LogAttrs(ctx, slog.LevelError, "api_serve", slog.Any("error", err))
		return err
	}

	return nil
}

internal - log - log.go

// Package log contains code for creating a structured logger.
package log

import (
	"context"
	"io"
	"log"
	"log/slog"
	"os"
	"path/filepath"
	"strings"

	"github.com/fastly/fst-go/build"
)

// contextKey is used to store a logger in a context.
type contextKey struct{}

var (
	// ContextKey is used to store a logger in a context.
	// Only to be used in places where a logger can't be passed in as an argument.
	ContextKey = contextKey{}

	// Level allows dynamically changing the output level via .Set() method.
	// Defaults to [slog.LevelInfo].
	// EXAMPLE: log.Level.Set(slog.LevelDebug)
	Level = new(slog.LevelVar)
)

// New returns a log.Logger configured for stdout.
func New() *slog.Logger {
	return NewWithOutputLevel(os.Stdout, Level)
}

// NewWithOutput returns a [*slog.Logger] configured with an output writer.
func NewWithOutput(w io.Writer) *slog.Logger {
	return slog.New(slog.NewJSONHandler(w, defaultOptions()).WithAttrs(defaultAttrs()))
}

// NewWithOutputLevel returns a [*slog.Logger] configured with an output writer and Level.
func NewWithOutputLevel(w io.Writer, l slog.Leveler) *slog.Logger {
	opts := defaultOptions()
	opts.Level = l
	return slog.New(slog.NewJSONHandler(w, opts).WithAttrs(defaultAttrs()))
}

// Adapt returns a [log.Logger] for use with packages that are not yet compatible with
// [log/slog].
func Adapt(l *slog.Logger, level slog.Level) *log.Logger {
	return slog.NewLogLogger(l.Handler(), level)
}

// FromContext returns the logger attached to a context.
func FromContext(ctx context.Context) *slog.Logger {
	logger, ok := ctx.Value(ContextKey).(*slog.Logger)
	if !ok {
		logger = New()
	}
	return logger
}

// defaultOptions defines default logger options.
func defaultOptions() *slog.HandlerOptions {
	return &slog.HandlerOptions{
		AddSource:   true,
		ReplaceAttr: slogReplaceAttr,
		Level:       Level,
	}
}

// defaultAttrs defines default logger attributes.
func defaultAttrs() []slog.Attr {
	return []slog.Attr{
		slog.Group("app",
			slog.String("name", build.Info.Project),
			slog.String("repo", build.Info.Repository),
			slog.String("version", build.Info.Version),
		),
	}
}

// slogReplaceAttr adjusts the log output.
//
// - Restricts these changes to top-level keys (not keys within groups)
//   - Changes default time field value to UTC time zone
//   - Replaces msg key with event
//   - Omits event field if empty
//   - Omits error field if when nil
//   - Truncates source's filename to project directory
//
// See https://pkg.go.dev/log/slog#HandlerOptions.ReplaceAttr
// N.B: TextHandler manages quoting attribute values as necessary.
func slogReplaceAttr(groups []string, a slog.Attr) slog.Attr {
	// Limit application of these rules only to top-level keys
	if len(groups) == 0 {
		// Set time zone to UTC
		if a.Key == slog.TimeKey {
			a.Value = slog.TimeValue(a.Value.Time().UTC())
			return a
		}
		// Use event as the default MessageKey, remove if empty
		if a.Key == slog.MessageKey {
			a.Key = "event"
			if a.Value.String() == "" {
				return slog.Attr{}
			}
			return a
		}
		// Display a 'partial' path.
		// Avoids ambiguity when multiple files have the same name across packages.
		if a.Key == slog.SourceKey {
			if source, ok := a.Value.Any().(*slog.Source); ok {
				a.Key = "caller"
				if _, after, ok := strings.Cut(source.File, "ascerta"+string(filepath.Separator)); ok {
					source.File = after
				}
			}
		}
	}

	// Remove error key=value when error is nil
	if a.Equal(slog.Any("error", error(nil))) {
		return slog.Attr{}
	}

	// Present durations and delays etc as milliseconds
	switch a.Key {
	case "dur", "delay", "p95", "previous_p95", "remaining", "max_wait":
		a.Value = slog.Float64Value(a.Value.Duration().Seconds() * 1000) //nolint:mnd
	}

	return a
}

internal - log - log_test.go

package log

import (
	"bytes"
	"context"
	"encoding/json"
	"log/slog"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

func TestLogger(t *testing.T) {
	buf := new(bytes.Buffer)
	l := NewWithOutput(buf)

	type App struct {
		Name    string `json:"name"`
		Version string `json:"version"`
	}

	type Caller struct {
		Function string `json:"function"`
		File     string `json:"file"`
		Line     int    `json:"line"`
	}

	type log struct {
		Time      string  `json:"time"`
		Level     string  `json:"level"`
		Caller    Caller  `json:"caller"`
		Event     string  `json:"event"`
		App       App     `json:"app"`
		Error     string  `json:"error"`
		Dur       float64 `json:"dur"`
		Delay     float64 `json:"delay"`
		P95       float64 `json:"p95"`
		PrevP95   float64 `json:"previous_p95"`
		Remaining float64 `json:"remaining"`
		MaxWait   float64 `json:"max_wait"`
	}

	t.Run("has expected fields", func(t *testing.T) {
		dumpLogs(t, buf)
		l.LogAttrs(context.Background(), slog.LevelInfo, "testing")
		var data log
		_ = json.Unmarshal(buf.Bytes(), &data)
		if data.Time == "" {
			t.Error("missing field: time")
		}
	})

	t.Run("outputs debug at LevelDebug", func(t *testing.T) {
		Level.Set(slog.LevelDebug)                                   // change for test
		defer func(lvl slog.Level) { Level.Set(lvl) }(Level.Level()) // ensure reset to default

		dumpLogs(t, buf)
		ctx := context.Background()
		l.LogAttrs(ctx, slog.LevelDebug, "")

		var data log
		_ = json.Unmarshal(buf.Bytes(), &data)
		if data.Level != "DEBUG" {
			t.Error("missing level=DEBUG")
		}
	})

	t.Run("elides empty event message", func(t *testing.T) {
		dumpLogs(t, buf)
		l.LogAttrs(context.Background(), slog.LevelInfo, "")
		var data log
		_ = json.Unmarshal(buf.Bytes(), &data)
		if data.Event != "" {
			t.Error("log should not contain an event")
		}
	})

	t.Run("elides empty error", func(t *testing.T) {
		dumpLogs(t, buf)
		l.LogAttrs(context.Background(), slog.LevelInfo, "test message", slog.Any("error", nil))
		var data log
		_ = json.Unmarshal(buf.Bytes(), &data)
		if data.Error != "" {
			t.Error("log should not contain an empty error")
		}
	})

	t.Run("uses UTC time zone", func(t *testing.T) {
		dumpLogs(t, buf)
		l.LogAttrs(context.Background(), slog.LevelInfo, "UTC")

		var data log
		_ = json.Unmarshal(buf.Bytes(), &data)

		p, err := time.Parse(time.RFC3339, data.Time)
		if err != nil {
			t.Fatalf("time field failed to parse: %s", err)
		}
		if z, _ := p.Zone(); z != time.UTC.String() {
			t.Errorf("expected time in UTC zone, got %s", z)
		}
	})

	t.Run("uses shorter source location", func(t *testing.T) {
		dumpLogs(t, buf)
		l.LogAttrs(context.Background(), slog.LevelInfo, "source -> caller test")

		var data log
		_ = json.Unmarshal(buf.Bytes(), &data)

		// slogReplaceAttr replaces the top-level key "source" with "caller"
		// IMPORTANT: the name of the repo is hard-coded into the slogReplaceAttr logic
		if data.Caller.File == "" {
			t.Error("field not found")
		}
		if strings.HasPrefix(data.Caller.File, string(filepath.Separator)) {
			t.Errorf("caller includes full path: %s", data.Caller.File)
		}
	})

	t.Run("displays milliseconds", func(t *testing.T) {
		dumpLogs(t, buf)
		const ts = 1234567890
		l.LogAttrs(context.Background(), slog.LevelInfo, "",
			slog.Duration("dur", ts),
			slog.Duration("delay", ts),
			slog.Duration("p95", ts),
			slog.Duration("previous_p95", ts),
			slog.Duration("remaining", ts),
			slog.Duration("max_wait", ts),
		)

		var data log
		_ = json.Unmarshal(buf.Bytes(), &data)

		want := 1234.56789
		if data.Dur != want || data.Delay != want || data.P95 != want || data.PrevP95 != want || data.Remaining != want || data.MaxWait != want {
			t.Errorf("log should contain: %f", want)
		}
	})
}

func dumpLogs(t *testing.T, buf *bytes.Buffer) {
	t.Helper()
	t.Cleanup(func() {
		t.Helper()
		if t.Failed() || testing.Verbose() {
			t.Log("Logs:\n", buf.String())
		}
		buf.Reset()
	})
}

revive.toml

enableAllRules = true

# DISABLED RULES

[rule.add-constant]
disabled = true

[rule.cognitive-complexity]
disabled = true

[rule.cyclomatic]
disabled = true

[rule.line-length-limit]
disabled = true

[rule.max-public-structs]
disabled = true

[rule.unused-receiver]
disabled = true

# ACTIVE RULES

[rule.argument-limit]
severity = "warning"
arguments = [6]

[rule.atomic]
severity = "warning"

[rule.bare-return]
severity = "warning"

[rule.bool-literal-in-expr]
severity = "warning"

[rule.comment-spacings]
arguments = ["nolint:", "lint:ignore", "exhaustive:ignore", "codespell:ignore"]

[rule.confusing-naming]
severity = "warning"

[rule.confusing-results]
severity = "warning"

[rule.constant-logical-expr]
severity = "error"

[rule.context-as-argument]
severity = "error"

[rule.context-keys-type]
severity = "error"

[rule.deep-exit]
severity = "warning"

[rule.defer]
severity = "warning"

[rule.early-return]
severity = "warning"

[rule.empty-block]
severity = "error"

[rule.empty-lines]
severity = "warning"

[rule.error-naming]
severity = "error"

[rule.error-return]
severity = "error"

[rule.error-strings]
severity = "error"

[rule.errorf]
severity = "warning"

[rule.exported]
severity = "error"

[rule.flag-parameter]
severity = "warning"

[rule.function-result-limit]
severity = "warning"
arguments = [4]

[rule.function-length]
severity = "warning"
arguments = [50, 0]

[rule.get-return]
severity = "error"

[rule.identical-branches]
severity = "error"

[rule.if-return]
severity = "warning"

[rule.increment-decrement]
severity = "error"

[rule.indent-error-flow]
severity = "warning"

[rule.import-shadowing]
severity = "warning"

[rule.modifies-parameter]
severity = "warning"

[rule.modifies-value-receiver]
severity = "warning"

[rule.nested-structs]
severity = "warning"

[rule.optimize-operands-order]
severity = "warning"

[rule.package-comments]
severity = "warning"

[rule.range]
severity = "warning"

[rule.range-val-in-closure]
severity = "warning"

[rule.range-val-address]
severity = "warning"

[rule.receiver-naming]
severity = "warning"

[rule.redefines-builtin-id]
severity = "error"

[rule.string-of-int]
severity = "warning"

[rule.struct-tag]
severity = "warning"

[rule.superfluous-else]
severity = "warning"

[rule.time-equal]
severity = "warning"

[rule.time-naming]
severity = "warning"

[rule.var-declaration]
severity = "warning"

[rule.var-naming]
severity = "warning"

[rule.unconditional-recursion]
severity = "error"

[rule.unexported-naming]
severity = "warning"

[rule.unexported-return]
severity = "error"

[rule.unhandled-error]
severity = "warning"
arguments = [
  "fmt.Print",
  "fmt.Printf",
  "fmt.Println",
  "fmt.Fprint",
  "fmt.Fprintf",
  "fmt.Fprintln",
]

[rule.unnecessary-stmt]
severity = "warning"

[rule.unreachable-code]
severity = "warning"

[rule.unused-parameter]
severity = "warning"

[rule.use-any]
severity = "warning"

[rule.useless-break]
severity = "warning"

[rule.waitgroup-by-value]
severity = "warning"

tools.mod

module github.com/fastly/ascerta/tools

go 1.24.1

tool (
	github.com/mgechev/revive
	golang.org/x/lint/golint
	golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness
	golang.org/x/vuln/cmd/govulncheck
	honnef.co/go/tools/cmd/staticcheck
	mvdan.cc/gofumpt
)

require (
	github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
	github.com/chavacava/garif v0.1.0 // indirect
	github.com/fatih/color v1.18.0 // indirect
	github.com/fatih/structtag v1.2.0 // indirect
	github.com/google/go-cmp v0.7.0 // indirect
	github.com/hashicorp/go-version v1.7.0 // indirect
	github.com/mattn/go-colorable v0.1.14 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/mattn/go-runewidth v0.0.16 // indirect
	github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 // indirect
	github.com/mgechev/revive v1.7.0 // indirect
	github.com/olekukonko/tablewriter v0.0.5 // indirect
	github.com/rivo/uniseg v0.4.7 // indirect
	github.com/spf13/afero v1.14.0 // indirect
	golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394 // indirect
	golang.org/x/lint v0.0.0-20241112194109-818c5a804067 // indirect
	golang.org/x/mod v0.24.0 // indirect
	golang.org/x/sync v0.12.0 // indirect
	golang.org/x/sys v0.31.0 // indirect
	golang.org/x/telemetry v0.0.0-20250310203348-fdfaad844314 // indirect
	golang.org/x/text v0.23.0 // indirect
	golang.org/x/tools v0.31.0 // indirect
	golang.org/x/vuln v1.1.4 // indirect
	honnef.co/go/tools v0.6.1 // indirect
	mvdan.cc/gofumpt v0.7.0 // indirect
)

tools.sum

github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc=
github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 h1:zpIH83+oKzcpryru8ceC6BxnoG8TBrhgAvRg8obzup0=
github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg=
github.com/mgechev/revive v1.7.0 h1:JyeQ4yO5K8aZhIKf5rec56u0376h8AlKNQEmjfkjKlY=
github.com/mgechev/revive v1.7.0/go.mod h1:qZnwcNhoguE58dfi96IJeSTPeZQejNeoMQLUZGi4SW4=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ=
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394 h1:VI4qDpTkfFaCXEPrbojidLgVQhj2x4nzTccG0hjaLlU=
golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
golang.org/x/lint v0.0.0-20241112194109-818c5a804067 h1:adDmSQyFTCiv19j015EGKJBoaa7ElV0Q1Wovb/4G7NA=
golang.org/x/lint v0.0.0-20241112194109-818c5a804067/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 h1:FemxDzfMUcK2f3YY4H+05K9CDzbSVr2+q/JKN45pey0=
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
golang.org/x/telemetry v0.0.0-20250310203348-fdfaad844314 h1:UY+gQAskx5vohcvUlJDKkJPt9lALCgtZs3rs8msRatU=
golang.org/x/telemetry v0.0.0-20250310203348-fdfaad844314/go.mod h1:16eI1RtbPZAEm3u7hpIh7JM/w5AbmlDtnrdKYaREic8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU=
mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo=