Attestant Go Style Linter - enforces attestantio organization coding standards as a golangci-lint module plugin.
attgo-linter is a golangci-lint module plugin. This means:
- You cannot use
golangci-lint rundirectly - it will fail with "plugin not found" - You must build a custom golangci-lint binary that includes this plugin
- The custom binary replaces
golangci-lintfor projects using attgo-linter
Add these two files to your project root:
.custom-gcl.yml (build configuration):
version: v2.0.0
name: custom-gcl
plugins:
- module: "github.com/attestantio/attgo-linter"
version: v0.1.0.golangci.yml (linter configuration):
version: "2"
linters:
enable:
- attgo
settings:
custom:
attgo:
type: "module"
description: "Attestant organization style linter"
settings:
enable_no_pkg_logger: true
enable_enum_iota: true
enable_current_year: true# Requires golangci-lint v2.0+ installed
golangci-lint customThis creates ./custom-gcl (name from .custom-gcl.yml).
# Use the custom binary instead of golangci-lint
./custom-gcl runImportant: Always use ./custom-gcl run, not golangci-lint run.
Since module plugins require a custom binary, your CI workflow must build it first.
Add this workflow to .github/workflows/golangci-lint.yml:
name: golangci-lint
on:
push:
branches: [master]
pull_request:
permissions:
contents: read
jobs:
golangci:
name: lint
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "^1.25"
cache: false
# Cache the custom binary to speed up subsequent runs
- name: Cache custom golangci-lint
id: cache-custom-gcl
uses: actions/cache@v4
with:
path: ./custom-gcl
key: custom-gcl-${{ hashFiles('.custom-gcl.yml') }}
# Build custom binary only if not cached
- name: Build custom golangci-lint
if: steps.cache-custom-gcl.outputs.cache-hit != 'true'
run: |
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v2.0.0
golangci-lint custom
# Run using the custom binary
- name: Run golangci-lint
run: ./custom-gcl run --timeout=5mAdd to your Makefile:
CUSTOM_GCL := ./custom-gcl
# Build custom golangci-lint if needed
$(CUSTOM_GCL): .custom-gcl.yml
golangci-lint custom
.PHONY: lint
lint: $(CUSTOM_GCL)
$(CUSTOM_GCL) run
.PHONY: lint-fix
lint-fix: $(CUSTOM_GCL)
$(CUSTOM_GCL) run --fixThen run:
make lintversion: "2"
linters:
enable:
- attgo
settings:
custom:
attgo:
type: "module"
description: "Attestant organization style linter"
settings:
# HIGH PRIORITY - enabled by default
enable_no_pkg_logger: true
enable_enum_iota: true
enable_current_year: true
# MEDIUM PRIORITY - disabled by default
enable_capital_comment: false
enable_func_opts: false
enable_raw_string: false
# LOW PRIORITY - disabled by default
enable_struct_field_order: false
enable_interface_check: false
# Custom logger patterns (optional)
logger_type_patterns:
- "zerolog.Logger"
- "*zerolog.Logger"
- "zap.Logger"
- "*zap.Logger"
# Custom enum suffixes (optional)
enum_type_suffixes:
- "Type"
- "Status"
- "State"
- "Kind"
- "Mode"Loggers must be struct fields, not package-level variables.
Rationale: Package-level loggers make testing difficult and prevent proper dependency injection. Struct field loggers enable:
- Injecting mock loggers for testing
- Clear ownership of logging configuration
- Better traceability in concurrent code
Bad:
var log zerolog.Logger
func main() {
log.Info().Msg("hello")
}Good:
type Service struct {
log zerolog.Logger
}
func (s *Service) Run() {
s.log.Info().Msg("hello")
}Configuration:
settings:
logger_type_patterns:
- "zerolog.Logger"
- "*zerolog.Logger"
- "zap.Logger"
- "*zap.Logger"Enum types should use uint64 + iota pattern, not string constants.
Rationale: Integer-based enums provide several advantages:
- Memory efficiency (integers vs strings)
- Faster comparison operations
- Type safety preventing mixing of different enum types
- Exhaustive switch checking by static analyzers
- Explicit string representation via
String()method
Bad:
type SANType string
const (
SANTypeDNS SANType = "dns"
SANTypeEmail SANType = "email"
)Good:
type SANType uint64
const (
SANTypeUnknown SANType = iota
SANTypeDNS
SANTypeEmail
)
var sanTypeStrings = [...]string{"unknown", "dns", "email"}
func (s SANType) String() string {
return sanTypeStrings[s]
}Configuration:
settings:
enum_type_suffixes:
- "Type"
- "Status"
- "State"
- "Kind"
- "Mode"New files must have the current year in their copyright header.
Rationale: Accurate copyright years are important for:
- Legal compliance
- Indicating when code was created/modified
- Consistency across the codebase
Bad (in 2026):
// Copyright © 2025 Attestant Limited.Good:
// Copyright © 2026 Attestant Limited.Also acceptable (year ranges):
// Copyright © 2023-2026 Attestant Limited.Comments should start with a capital letter.
Rationale: Consistency and readability. Well-formatted comments indicate attention to code quality.
Exceptions:
- Comments starting with identifiers (
someFunc is...) - nolint directives
- URLs, TODOs, build tags
- License boilerplate text
Bad:
// this is a commentGood:
// This is a comment
// someVariable contains the valueService constructors with many parameters should use the functional options pattern.
Rationale: The functional options pattern provides:
- Extensibility without breaking existing callers
- Self-documenting named options
- Natural handling of optional parameters
- Easy default values
- Validation within option functions
Bad:
func NewService(log Logger, db DB, cache Cache, timeout time.Duration) *ServiceGood:
type Option func(*Service)
func WithLogger(log Logger) Option {
return func(s *Service) { s.log = log }
}
func New(opts ...Option) *ServicePrefer raw strings (backticks) over heavily escaped double-quoted strings.
Rationale: Raw strings improve readability when the string contains multiple quotes or backslashes, such as in queries, paths, or JSON.
Bad:
query := "vouch_relay_execution_config_total{result=\"succeeded\"}"
path := "C:\\Users\\name\\Documents\\file.txt"Good:
query := `vouch_relay_execution_config_total{result="succeeded"}`
path := `C:\Users\name\Documents\file.txt`Struct fields should be ordered: logger → metrics → dependencies → data → sync.
Rationale: Consistent field ordering creates predictable structure:
- Know where to find fields without searching
- Easier code review
- Faster onboarding for new developers
- Logical grouping of related fields
Example:
type Service struct {
// Logger
log zerolog.Logger
// Metrics
metrics *prometheus.Registry
// Dependencies
client *http.Client
db Database
// Data
config Config
cache map[string]Value
// Synchronization
mu sync.Mutex
done chan struct{}
}Suggests adding var _ Interface = (*Struct)(nil) compile-time checks.
Rationale: This pattern provides:
- Compile-time verification that a struct implements an interface
- Clear documentation of which interfaces a type implements
- Immediate build failure if methods are removed or signatures change
- Better IDE support for code navigation
Pattern:
type Reader interface {
Read(p []byte) (n int, err error)
}
type MyReader struct{}
// Compile-time check
var _ Reader = (*MyReader)(nil)
func (r *MyReader) Read(p []byte) (int, error) {
return 0, nil
}Use standard golangci-lint nolint directives:
var log zerolog.Logger //nolint:attgo_no_pkg_logger // legacy code
query := "escaped\"string" //nolint:attgo_raw_string // intentionalYou're using golangci-lint run instead of the custom binary. Use:
./custom-gcl run # NOT golangci-lint runClear the cache and rebuild:
rm -rf ~/.cache/golangci-lint
rm ./custom-gcl
golangci-lint customEnsure the version in .custom-gcl.yml matches a published release:
plugins:
- module: "github.com/attestantio/attgo-linter"
version: v0.1.0 # Must be a valid git tagApache License 2.0. See LICENSE for details.