About GoGreement
Welcome to the GoGreement project documentation!
GoGreement is a static analysis linter that extends Go's capabilities by adding compile-time enforcements for architectural agreements. It helps teams maintain code quality through annotations like immutability, interface implementation contracts, constructor restrictions, and test-only boundaries.
Why GoGreement?
Programming is about agreements between developers. The goal of this project is to help enforce these agreements through static analysis.
Many modern programming languages provide built-in mechanisms for:
- Ensuring immutability (
finalin Java,readonlyin C#) - Explicitly declaring interface implementations (
implementskeyword) - Restricting object instantiation (private constructors, factory patterns)
- Marking code as test-only (
@VisibleForTestingin various languages)
Go doesn't have these built-in features. GoGreement fills this gap.
How Does It Work?
We add annotations as comments in your Go code:
// @implements io.Writer
// @immutable
// @testonly
// @constructor New
Then run the GoGreement linter as part of your static analysis pipeline. It will report errors if the agreements are violated.
Quick Example
✅ Valid Code - Agreement Enforced
package mypackage
import "io"
// @implements &io.Reader
// The linter will verify this annotation
type MyReader struct {
data []byte
pos int
}
// Correctly implements Read method
func (r *MyReader) Read(p []byte) (n int, err error) {
if r.pos >= len(r.data) {
return 0, io.EOF
}
n = copy(p, r.data[r.pos:])
r.pos += n
return n, nil
}
❌ Invalid Code - Linter Catches the Error
package mypackage
import "io"
// @implements &io.Reader
type BrokenReader struct {
data []byte
}
// ERROR: Missing Read method!
// Linter will report: IMPL03 - Missing required methods: Read
More Powerful Examples
Immutability Enforcement
// @immutable
// @constructor NewPoint
type Point struct {
X, Y int
}
func NewPoint(x, y int) Point {
return Point{X: x, Y: y} // ✅ Allowed in constructor
}
func MovePoint(p Point) {
p.X += 10 // ❌ ERROR: IMM01 - Field of immutable type is being assigned
}
func ValidUsage(p Point) Point {
// ✅ Correct: Create new instances instead of mutating
return Point{X: p.X + 10, Y: p.Y}
}
Constructor Restrictions
// @constructor NewDB, MustConnect
type Database struct {
conn *sql.DB
}
func NewDB(dsn string) (*Database, error) {
// ✅ Allowed: Named constructor
return &Database{conn: nil}, nil
}
func createBroken() {
db := Database{} // ❌ ERROR: CTOR01 - Use constructor functions
}
Test-Only Boundaries
// @testonly
type MockService struct {
calls int
}
// @testonly
func CreateMock() *MockService {
return &MockService{}
}
// in production code (not *_test.go)
func productionFunc() {
mock := CreateMock() // ❌ ERROR: TONL02 - TestOnly function in non-test
}
// in test file (*_test.go)
func TestMyCode(t *testing.T) {
mock := CreateMock() // ✅ Allowed in tests
}
What's Next?
- Getting Started - Install and configure GoGreement
- Annotations - Learn about all available annotations
- Error Codes - Reference for all error codes
Getting Started
This guide will help you install and start using GoGreement in your Go projects.
Installation
Recommended: Binary Installation
The easiest way to install GoGreement is using go install:
For stable version (recommended):
go install github.com/a14e/gogreement/cmd/gogreement@v0.0.1
For latest version:
go install github.com/a14e/gogreement/cmd/gogreement@latest
This will download and install GoGreement to your $GOPATH/bin (or $HOME/go/bin if you're using Go modules).
Important: Make sure your $GOPATH/bin (or $HOME/go/bin) is in your PATH to run gogreement from anywhere.
From Source (for developers)
If you prefer to build from source or want to contribute:
git clone https://github.com/a14e/gogreement
cd gogreement
make build
This will create a gogreement (or gogreement.exe on Windows) binary in the project directory.
Usage
GoGreement is built on top of Go's golang.org/x/tools/go/analysis framework and uses the multichecker pattern. This means you can run it like any other Go analysis tool.
Basic Usage
Run GoGreement on your project:
gogreement ./...
This will analyze all packages in the current module.
Alternative: Using go vet
You can also run GoGreement using go vet with the -vettool flag:
go vet -vettool=gogreement ./...
This approach has the advantage of persistent package facts between analyzers, which can improve cross-package analysis performance and accuracy in large multi-module projects.
Analyzing Specific Packages
gogreement ./pkg/mypackage
gogreement ./...
With Standard Multichecker Flags
Since GoGreement uses the standard analysis framework, it supports all standard multichecker flags:
# Run with JSON output
gogreement -json ./...
# Show analyzer documentation
gogreement -help
# Run specific analyzers only (if you extend GoGreement)
gogreement -analyzers=ImmutableChecker ./...
Configuration
GoGreement can be configured using environment variables or command-line flags. Command-line flags take priority over environment variables.
Configuration Options
| Option | Environment Variable | Command-Line Flag | Default | Description |
|---|---|---|---|---|
| Scan Tests | GOGREEMENT_SCAN_TESTS | --config.scan-tests | false | Whether to analyze test files (*_test.go). By default, test files are excluded. |
| Exclude Paths | GOGREEMENT_EXCLUDE_PATHS | --config.exclude-paths | testdata | Comma-separated list of path patterns to exclude. Paths are matched as substrings. |
| Exclude Checks | GOGREEMENT_EXCLUDE_CHECKS | --config.exclude-checks | (empty) | Comma-separated list of check codes to exclude globally. Supports individual codes (IMM01), categories (IMM), or ALL. |
Configuration Examples
Environment Variables
# Include test files in analysis
export GOGREEMENT_SCAN_TESTS=true
# Exclude multiple paths
export GOGREEMENT_EXCLUDE_PATHS=testdata,vendor,third_party
# Exclude specific checks globally
export GOGREEMENT_EXCLUDE_CHECKS=IMM01,CTOR
# Exclude entire category
export GOGREEMENT_EXCLUDE_CHECKS=TONL
# Exclude all checks (useful for debugging)
export GOGREEMENT_EXCLUDE_CHECKS=ALL
# Run analysis
gogreement ./...
Command-Line Flags
# Enable test file scanning
gogreement --config.scan-tests=true ./...
# Exclude paths
gogreement --config.exclude-paths=testdata,vendor ./...
# Exclude specific error codes
gogreement --config.exclude-checks=IMM01,CTOR02 ./...
# Exclude entire category of checks
gogreement --config.exclude-checks=IMM ./...
# Combined flags
gogreement --config.scan-tests=true --config.exclude-paths=vendor --config.exclude-checks=TONL ./...
Boolean Value Formats
For boolean options like GOGREEMENT_SCAN_TESTS, the following values are accepted (case-insensitive):
- True:
true,1,yes,on - False:
false,0,no,off, or any other value
Multiple Values
For options that accept multiple values (GOGREEMENT_EXCLUDE_PATHS, GOGREEMENT_EXCLUDE_CHECKS):
# Comma-separated with or without spaces
export GOGREEMENT_EXCLUDE_PATHS=testdata,vendor,generated
export GOGREEMENT_EXCLUDE_PATHS="testdata, vendor, generated"
# Error codes are automatically converted to uppercase
export GOGREEMENT_EXCLUDE_CHECKS=imm01,ctor # Becomes IMM01,CTOR
Excluding Checks
Module-Level Exclusion
Use --config.exclude-checks or GOGREEMENT_EXCLUDE_CHECKS to exclude checks across your entire project:
# Exclude all immutability checks
gogreement --config.exclude-checks=IMM ./...
# Exclude specific codes
gogreement --config.exclude-checks=IMM01,CTOR02,TONL03 ./...
File and Code-Level Exclusion
Use // @ignore comments in your code for fine-grained control. See the @ignore annotation documentation for details.
Integration with CI/CD
GitHub Actions Example
name: GoGreement Analysis
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.22'
- name: Install GoGreement
run: go install github.com/a14e/gogreement/cmd/gogreement@v0.0.1
- name: Run GoGreement
run: gogreement ./...
env:
GOGREEMENT_SCAN_TESTS: false
GOGREEMENT_EXCLUDE_PATHS: testdata,vendor
Makefile Integration
.PHONY: lint
lint:
gogreement ./...
.PHONY: lint-all
lint-all:
GOGREEMENT_SCAN_TESTS=true gogreement ./...
Next Steps
Now that you have GoGreement installed and configured:
- Learn about annotations: Read the Annotations section
- Understand error codes: Check the Error Codes reference
- Review limitations: See Limitations for known constraints
Limitations
GoGreement is a powerful static analysis tool, but it has some limitations you should be aware of:
1. No Generics Support
Generics (type parameters) are not currently supported in any annotations. This means you cannot use GoGreement annotations on generic types or functions.
// Not supported yet
// @immutable
type Container[T any] struct {
value T
}
2. Import-Based Analysis Only
Due to how the analysis framework works, GoGreement only analyzes types and functions that are imported by the packages being analyzed.
This means:
- If you annotate a type in package A
- But package B never imports package A
- Package B won't see the annotations from package A
Impact: Cross-package enforcement only works for types that are actually imported.
3. Lenient Annotation Parsing
Many annotations do not fail with errors if they cannot be fully parsed. This is an intentional design decision to support:
- Comments that mention annotation keywords in the middle of text
- Additional comments after annotations
- Gradual adoption without breaking existing codebases
Example: These won't be recognized as valid annotations:
// This is a note about @immutable types in general
// (not at the start - won't be recognized)
type MyType struct {}
// TODO: add @constructor later
// (not at the start - won't be recognized)
type Other struct {}
// @constructor
// (no function names specified - returns nil, won't be recognized)
type NeedsFunctions struct {}
4. In-Memory Fact Caching
Package facts are cached in memory during analysis, which can increase memory usage for large projects.
Optimization: Run as vet tool for disk-based caching:
go vet -vettool=gogreement ./...
Note: This is a property of the underlying analysis framework.
5. No golangci-lint Integration (Yet)
GoGreement is not yet integrated with golangci-lint. You need to run it as a standalone tool.
Current status: We are working on adding golangci-lint support in future releases.
6. Analysis Framework Limitations
GoGreement must be run after all code generation is complete. The analysis framework fails when encountering generated code that references undefined types from other packages.
# Required order:
go generate ./... # Generate any code (required)
go mod tidy # Update dependencies
gogreement ./... # Then run GoGreement
This can cause errors like:
pkg/somefile.go:119:29: undefined: somepkg.SomeType
pkg/somefile.go:123:23: undefined: otherpkg.SomeConstant
pkg/anotherfile.go:150:33: undefined: generatedpb.SomeEnum_Value
In such cases, GoGreement won't start analyzing - the underlying Go compiler that powers the analysis framework will fail to parse the codebase.
7. Pointer vs Value Receiver Distinction Required
For @implements annotations, you must be explicit about pointer vs value receivers:
// These are DIFFERENT:
// @implements io.Reader // Value receiver methods
// @implements &io.Reader // Pointer receiver methods
type MyReader struct {}
Workarounds
Most limitations can be worked around:
- No generics: Use concrete types or wrapper types
- Import-based analysis: Ensure annotated types are imported where needed
- golangci-lint: Run GoGreement as a separate step in CI/CD
Annotations
All enforcement mechanisms in GoGreement are based on annotations. Annotations are special comments that start with @ and follow a specific format:
// @annotation param1 param2
How Annotations Work
- Write annotations as comments above types, functions, or methods
- Run GoGreement analyzer on your code
- Get violations reported if agreements are broken
Available Annotations
GoGreement supports five core annotations:
| Annotation | Purpose | Applied To |
|---|---|---|
| @implements | Enforce interface implementation contracts | Types |
| @immutable | Prevent field mutations after creation | Types |
| @constructor | Restrict object creation to specific functions | Types |
| @testonly | Limit usage to test files only | Types, Functions, Methods |
| @ignore | Suppress specific violations | Files, Blocks, Lines |
Annotation Syntax Rules
1. Must Start Comment Line
Annotations must be at the beginning of a comment line (after // and whitespace):
// @immutable ✅ Valid
// @immutable ✅ Valid (whitespace OK)
// TODO: @immutable ❌ Invalid (not at start)
2. Case-Sensitive Keywords
Annotation keywords are case-sensitive and must be lowercase:
// @immutable ✅ Valid
// @Immutable ❌ Invalid
// @IMMUTABLE ❌ Invalid
3. Additional Comments Allowed
You can add comments after annotation parameters:
// @constructor New, Create // These are the factory functions
// @implements &io.Reader // Pointer receiver required
4. Multiple Annotations
You can use multiple annotations on the same declaration:
// @immutable
// @constructor NewPoint
type Point struct {
X, Y int
}
Annotation Scope
Annotations are only recognized on top-level declarations:
// ✅ Top-level declaration - annotation works
// @immutable
type Config struct {
Host string
}
func Example() {
// ❌ Not a top-level declaration - annotation ignored
// @immutable
type LocalConfig struct {
Host string
}
}
Annotations on nested types, local functions, or any declarations inside functions are ignored by GoGreement.
Annotation Processing
GoGreement uses a two-phase approach:
-
Reading Phase (
AnnotationReaderanalyzer)- Scans all files for annotations on top-level declarations
- Parses and validates syntax
- Exports annotations as package facts
-
Checking Phase (Individual checkers)
- Import annotations from dependencies
- Build cross-package indices
- Detect violations
- Report errors with specific codes
This design enables cross-package enforcement - annotations in one package affect analysis in packages that import it.
Next Steps
Learn about each annotation in detail:
- @implements - Ensure types implement interfaces
- @immutable - Enforce immutability
- @constructor - Control object creation
- @testonly - Restrict to tests
- @ignore - Suppress violations
@implements Annotation
The @implements annotation enforces that a type implements a specific interface. When you add this annotation, GoGreement verifies at analysis time that all required methods are present with correct signatures.
Motivation
Historically, Go had no direct way to explicitly declare that a struct implements an interface. You could write:
var _ MyInterface = (*MyStruct)(nil)
But this is not ideal - it's cryptic, error-prone, and doesn't clearly express intent.
The @implements annotation fills this gap by providing a clear, explicit declaration that is verified by the linter.
Syntax
// @implements InterfaceName
// @implements PackageName.InterfaceName
// @implements &InterfaceName
// @implements &PackageName.InterfaceName
Parameters
- Interface Name (required): Name of the interface to implement
- Package Prefix (optional): Package name for external interfaces
- Pointer Marker
&(optional): Indicates pointer receiver methods
How It Works
- Annotation is parsed when GoGreement scans the file
- Interface definition is loaded from the specified package
- Method signatures are compared between the type and interface
- Violations are reported if methods are missing or signatures don't match
Key Behaviors
- No generics support: Cannot be used with generic types or interfaces
- No comparable constraint support: Cannot verify
comparableconstraint - only explicit method signatures are checked - Imports required: External interfaces must be imported (even with
import _ "package"if not used) - Pointer vs value:
@implements Interfaceand@implements &Interfaceare different contracts - Signature matching: Validation is based on method signature comparison
- No multi-interface syntax: Use separate lines for multiple interfaces
- Strict parsing: Extra characters before the annotation will cause it to be ignored
- Receiver compatibility: Pointer receiver methods can satisfy value receiver requirements (following Go's standard method set rules), but value receiver methods cannot satisfy pointer receiver requirements
Can Be Declared On
Struct Types
// @implements &io.Reader
type MyReader struct {
data []byte
pos int
}
func (r *MyReader) Read(p []byte) (n int, err error) {
return 0, nil
}
Named Types
// @implements fmt.Stringer
type Status int
func (s Status) String() string {
return "status"
}
Error Codes
| Code | Description | Example |
|---|---|---|
| IMPL01 | Package not found in imports | Using @implements pkg.Interface without importing pkg |
| IMPL02 | Interface not found in package | Interface name doesn't exist or is misspelled |
| IMPL03 | Missing or incorrect methods | Type doesn't implement all required methods with correct signatures |
Examples
✅ Basic Interface Implementation
package main
import "io"
// @implements &io.Reader
type ByteReader struct {
data []byte
pos int
}
func (r *ByteReader) Read(p []byte) (n int, err error) {
if r.pos >= len(r.data) {
return 0, io.EOF
}
n = copy(p, r.data[r.pos:])
r.pos += n
return n, nil
}
✅ Multiple Interfaces
// @implements &io.Reader
// @implements &io.Closer
type ReaderCloser struct {
file *os.File
}
func (rc *ReaderCloser) Read(p []byte) (n int, err error) {
return rc.file.Read(p)
}
func (rc *ReaderCloser) Close() error {
return rc.file.Close()
}
✅ Current Package Interface
type Validator interface {
Validate() error
}
// @implements Validator
type User struct {
Name string
}
func (u User) Validate() error {
if u.Name == "" {
return errors.New("name required")
}
return nil
}
✅ Value vs Pointer Receivers
// @implements fmt.Stringer
type Status int
func (s Status) String() string {
return fmt.Sprintf("Status(%d)", s)
}
// @implements &io.Writer
type Buffer struct {
data []byte
}
func (b *Buffer) Write(p []byte) (n int, err error) {
b.data = append(b.data, p...)
return len(p), nil
}
❌ Missing Method
// @implements &io.ReadWriter
type BrokenRW struct {}
func (rw *BrokenRW) Read(p []byte) (n int, err error) {
return 0, nil
}
// [IMPL03] type "BrokenRW" does not implement interface "&io.ReadWriter"
// missing methods:
// Write([]byte) (int, error)
❌ Wrong Signature
// @implements &io.Reader
type BadReader struct {}
// [IMPL03] type "BadReader" does not implement interface "&io.Reader"
// missing methods:
// Read([]byte) (int, error)
func (r *BadReader) Read(p []byte) int {
return 0
}
❌ Package Not Imported
// @implements http.Handler
// [IMPL01] package "http" referenced in @implements annotation on type "MyHandler" is not imported
type MyHandler struct {}
Fix: Add import
import "net/http"
// @implements http.Handler
type MyHandler struct {}
func (h MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Implementation
}
❌ Pointer vs Value Mismatch
// @implements io.Writer
type Writer struct {}
func (w *Writer) Write(p []byte) (n int, err error) {
return len(p), nil
}
// [IMPL03] type "Writer" does not implement interface "io.Writer"
// missing methods:
// Write([]byte) (int, error)
Fix: Use & in annotation
// @implements &io.Writer
type Writer struct {}
func (w *Writer) Write(p []byte) (n int, err error) {
return len(p), nil
}
Best Practices
1. Always Import Interfaces
Even if the interface isn't used directly, add an import:
import _ "io"
// @implements &io.Reader
type MyReader struct {}
2. Use Pointer Marker Correctly
Match the receiver type in your implementation:
- Value receivers →
@implements Interface - Pointer receivers →
@implements &Interface
3. One Interface Per Line
// ✅ Good
// @implements &io.Reader
// @implements &io.Closer
type RC struct {}
// ❌ Bad - Not supported
// @implements &io.Reader, &io.Closer
type RC struct {}
4. Document Why
Add comments explaining the purpose:
// @implements &http.Handler handles API requests for user management
type UserHandler struct {
db *sql.DB
}
Related Annotations
- @immutable: Often combined with
@implementsfor immutable data structures - @testonly: Use for mock implementations in tests
See Also
@immutable Annotation
The @immutable annotation enforces immutability constraints on types. Once an object is created, its fields cannot be modified.
Motivation
Go doesn't provide a built-in way to declare types or fields as immutable. This contrasts with many functional programming techniques that rely on immutability to provide additional stability guarantees and reduce risks in concurrent code.
Use cases:
- Messages sent through channels
- HTTP request/response objects
- Configuration objects
- Value objects in domain models
The @immutable annotation fills this gap by providing compile-time enforcement of immutability.
Syntax
// @immutable
type TypeName struct {
// fields
}
No parameters required - simply add // @immutable above the type declaration.
@mutable Field Exceptions
Specific fields in immutable types can be marked as mutable to allow runtime modifications like cache updates or state changes.
// @immutable
type CachedUser struct {
ID int64
Name string
// @mutable
Cache map[string]interface{}
}
How It Works
GoGreement detects the following violations on immutable types:
- Field assignments:
obj.field = value - Compound assignments:
obj.field += value,obj.field -= value, etc. - Increment/decrement:
obj.field++,obj.field-- - Index assignments:
obj.items[0] = value,obj.dict["key"] = value - Receiver operations in methods: For methods on immutable types:
*receiver = value(receiver reassignment)*receiver++,*receiver--(receiver increment/decrement)
Key Behaviors
- No generics support: Cannot be used with generic types
- Weak immutability guarantees:
- Prevents field assignments and compound operations
- Prevents assignments through methods
- Does NOT prevent mutations through pointers or reflection
- Constructor exception: Checks are ignored inside functions marked with
@constructor - @mutable field exceptions: Fields marked with
@mutablecan be modified even in immutable types - Can be suppressed: Use
@ignoreto disable checks in specific scopes - Cross-package enforcement: Works even if
@immutablewas declared in external modules
Can Be Declared On
Struct Types
// @immutable
type Point struct {
X, Y int
}
Interface Types
// @immutable
type ReadOnlyConfig interface {
GetValue(key string) string
}
Named Types
// @immutable
type UserID string
// @immutable
type StatusCode int
Error Codes
| Code | Description | Example |
|---|---|---|
| IMM01 | Field assignment | point.X = 10 |
| IMM02 | Compound assignment | point.X += 5, point.Y *= 2 |
| IMM03 | Increment/decrement | point.X++, count-- |
| IMM04 | Index assignment | obj.items[0] = value, obj.dict["key"] = value |
Examples
✅ Basic Immutable Type
// @immutable
// @constructor NewPoint
type Point struct {
X, Y int
}
func NewPoint(x, y int) Point {
return Point{X: x, Y: y} // ✅ Allowed in constructor
}
func DoublePoint(p Point) Point {
// ✅ Correct: Create new instance instead of mutating
return Point{X: p.X * 2, Y: p.Y * 2}
}
✅ Immutable Configuration
// @immutable
// @constructor LoadConfig
type Config struct {
Host string
Port int
TLS bool
}
func LoadConfig(path string) (*Config, error) {
cfg := &Config{ // ✅ Allowed in constructor
Host: "localhost",
Port: 8080,
TLS: false,
}
return cfg, nil
}
❌ Field Assignment
// @immutable
type Point struct {
X, Y int
}
func MovePoint(p *Point) {
p.X += 10 // ❌ error: [IMM02] immutability violation in type "Point": cannot use += on field "X" of immutable type (outside constructor)
p.Y = 20 // ❌ error: [IMM01] immutability violation in type "Point": cannot assign to field "Y" of immutable type
}
❌ Increment/Decrement
// @immutable
type Counter struct {
value int
}
func Increment(c *Counter) {
c.value++ // ❌ error: [IMM03] immutability violation in type "Counter": cannot use ++ on field "value" of immutable type (outside constructor)
}
func Decrement(c *Counter) {
c.value-- // ❌ error: [IMM03] immutability violation in type "Counter": cannot use -- on field "value" of immutable type (outside constructor)
}
❌ Index Assignment
// @immutable
type Data struct {
items []int
dict map[string]int
}
func Modify(d *Data) {
d.items[0] = 42 // ❌ error: [IMM04] immutability violation in type "Data": cannot modify element of field "items" of immutable type
d.dict["key"] = 100 // ❌ error: [IMM04] immutability violation in type "Data": cannot modify element of field "dict" of immutable type
}
✅ Using @ignore to Suppress
// @immutable
type Cache struct {
data map[string]string
}
func (c *Cache) Update(key, value string) {
// @ignore IMM04
c.data[key] = value // ✅ Suppressed via @ignore
}
✅ Cross-Package Immutability
Package models:
package models
// @immutable
type User struct {
ID int
Name string
}
Package main:
package main
import "myapp/models"
func updateUser(u *models.User) {
u.Name = "New Name" // ❌ ERROR: IMM01 - User is immutable (from external package)
}
✅ Using @mutable Fields
// @immutable
// @constructor NewCachedUser
type CachedUser struct {
ID int64 // ❌ Cannot be modified
Name string // ❌ Cannot be modified
// @mutable
Cache map[string]interface{} // ✅ Can be modified
}
func UpdateCache(user *CachedUser, key string, value interface{}) {
user.Cache[key] = value // ✅ Allowed - Cache is @mutable
}
func UpdateName(user *CachedUser, name string) {
user.Name = name // ❌ ERROR: IMM01 - Name is not @mutable
}
✅ Correct Pattern: Return New Instances
// @immutable
// @constructor NewPerson
type Person struct {
Name string
Age int
}
func NewPerson(name string, age int) Person {
return Person{Name: name, Age: age}
}
// ✅ Correct: Return modified copy
func WithAge(p Person, newAge int) Person {
return Person{
Name: p.Name,
Age: newAge,
}
}
// ✅ Correct: Builder pattern for construction
func (p Person) WithName(name string) Person {
return Person{Name: name, Age: p.Age}
}
❌ Receiver Operations in Methods
// @immutable
// @constructor NewCounter
type Counter struct {
value int
}
func NewCounter() Counter {
return Counter{value: 0}
}
func (c *Counter) Increment() {
c.value++ // ❌ [IMM03] cannot use ++ on field "value" of immutable type (outside constructor)
}
func (c *Counter) Reset(newVal int) {
*c = Counter{value: newVal} // ❌ [IMM01] cannot reassign immutable receiver (outside constructor)
}
func (c *Counter) Decrement() {
(*c)-- // ❌ [IMM03] cannot use -- on immutable receiver (outside constructor)
}
Best Practices
1. Combine with @constructor
Always use @constructor with @immutable to control object creation:
// @immutable
// @constructor NewConfig
type Config struct {
value string
}
func NewConfig(v string) Config {
return Config{value: v}
}
2. Return New Instances
Instead of mutating, return new instances:
// ❌ Bad: Mutation
func UpdateName(u *User) {
u.Name = "newname" // ERROR
}
// ✅ Good: Return new instance
func WithName(u User, name string) User {
return User{ID: u.ID, Name: name}
}
3. Use for Value Objects
Immutable types work well for value objects:
// @immutable
// @constructor NewMoney
type Money struct {
Amount int
Currency string
}
func (m Money) Add(other Money) (Money, error) {
if m.Currency != other.Currency {
return Money{}, errors.New("currency mismatch")
}
return Money{
Amount: m.Amount + other.Amount,
Currency: m.Currency,
}, nil
}
5. Document Immutability Intent
// @immutable
// Point represents an immutable 2D coordinate.
// Use NewPoint to create instances and helper functions to derive new points.
type Point struct {
X, Y int
}
Limitations
Weak Guarantees
@immutable provides weak immutability - it prevents direct field assignments but doesn't prevent:
- Pointer manipulation: Modifying through
unsafepointers - Reflection: Mutations via
reflectpackage - Slice/map element mutations: Modifying elements inside slices or maps (only index assignment is caught)
// @immutable
type Data struct {
items []Item // The slice itself can't be reassigned, but elements can be modified
}
func modify(d Data) {
d.items = nil // ❌ ERROR: IMM01 - Assignment
d.items[0] = newItem // ❌ ERROR: IMM04 - Index assignment
d.items[0].field = 123 // ✅ No error - element field modification not caught
}
Workaround
For stronger guarantees, use unexported fields with exported getter methods:
// @immutable
type SafeData struct {
items []Item // unexported - can't be accessed directly
}
func (d SafeData) Items() []Item {
// Return copy to prevent external modifications
result := make([]Item, len(d.items))
copy(result, d.items)
return result
}
Related Annotations
- @constructor: Control object creation
- @ignore: Suppress violations when needed
- @implements: Often combined for immutable interfaces
See Also
@constructor Annotation
The @constructor annotation restricts object instantiation to specific functions. Objects can only be created within the designated constructor functions.
Motivation
Go doesn't have built-in mechanisms to restrict how objects are created. This is problematic when:
- Objects require specific initialization (database connections, sockets)
- Invariants must be established at creation time
- Factory patterns are required for proper setup
- You want to ensure validation happens during construction
The @constructor annotation fills this gap by enforcing that objects are only created through designated functions.
Syntax
// @constructor FunctionName
// @constructor Func1, Func2, Func3
type TypeName struct {
// fields
}
Parameters
- Function Names (required): Comma-separated list of constructor function names
- Functions must be in the same package as the type
- If a specified function doesn't exist, no error is raised
How It Works
GoGreement detects the following violations outside constructor functions:
- Composite literals:
TypeName{} - new() calls:
new(TypeName) - Var declarations:
var x TypeName
Key Behaviors
- No generics support: Cannot be used with generic types
- Same package only: Constructor functions must be in the same package as the type
- Non-existent constructors OK: No error if a named constructor doesn't exist
- Can be suppressed: Use
@ignoreto allow creation in specific places - Cross-package enforcement: Works even if
@constructorwas declared in an external module
Can Be Declared On
Struct Types
// @constructor NewDatabase
type Database struct {
conn *sql.DB
}
Interface Types
// @constructor NewReader
type Reader interface {
Read(p []byte) (n int, err error)
}
Named Types
// @constructor NewStatus
type Status int
Error Codes
| Code | Description | Example |
|---|---|---|
| CTOR01 | Composite literal outside constructor | db := Database{} |
| CTOR02 | new() call outside constructor | db := new(Database) |
| CTOR03 | Var declaration creates zero-initialized instance | var db Database |
Examples
✅ Basic Constructor Pattern
// @constructor NewDatabase
type Database struct {
conn *sql.DB
}
func NewDatabase(dsn string) (*Database, error) {
conn, err := sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
return &Database{conn: conn}, nil // ✅ Allowed in constructor
}
✅ Multiple Constructors
// @constructor New, NewWithDefaults, MustNew
type Config struct {
Host string
Port int
}
func New(host string, port int) *Config {
return &Config{Host: host, Port: port} // ✅ Allowed
}
func NewWithDefaults() *Config {
return &Config{Host: "localhost", Port: 8080} // ✅ Allowed
}
func MustNew(host string, port int) *Config {
if port == 0 {
panic("port required")
}
return &Config{Host: host, Port: port} // ✅ Allowed
}
❌ Composite Literal Outside Constructor
// @constructor NewUser
type User struct {
ID int
Name string
}
func NewUser(id int, name string) *User {
return &User{ID: id, Name: name} // ✅ Allowed
}
func createBroken() {
u := User{ID: 1, Name: "Alice"} // ❌ [CTOR01] type instantiation must be in constructor (allowed: [NewUser])
}
❌ Using new() Outside Constructor
// @constructor NewBuffer
type Buffer struct {
data []byte
}
func NewBuffer(size int) *Buffer {
return &Buffer{data: make([]byte, size)} // ✅ Allowed
}
func allocateBroken() {
buf := new(Buffer) // ❌ [CTOR02] type instantiation with new() must be in constructor (allowed: [NewBuffer])
}
❌ Var Declaration Outside Constructor
// @constructor NewPoint
type Point struct {
X, Y int
}
func NewPoint(x, y int) Point {
return Point{X: x, Y: y} // ✅ Allowed
}
func useBroken() {
var p Point // ❌ [CTOR03] zero-initialized variable declaration must be in constructor (allowed: [NewPoint])
p = NewPoint(1, 2) // Too late - already zero-initialized
}
✅ Using @ignore to Suppress
// @constructor NewCache
type Cache struct {
items map[string]string
}
func NewCache() *Cache {
return &Cache{items: make(map[string]string)}
}
func resetCache(c *Cache) {
// @ignore CTOR01
*c = Cache{items: make(map[string]string)} // ✅ Suppressed
}
✅ Cross-Package Enforcement
Package db:
package db
// @constructor Open
type Connection struct {
dsn string
}
func Open(dsn string) (*Connection, error) {
return &Connection{dsn: dsn}, nil
}
Package main:
package main
import "myapp/db"
func main() {
// ❌ ERROR: CTOR01 - Connection requires constructor
conn := db.Connection{dsn: "localhost"}
// ✅ Correct: Use constructor
conn, err := db.Open("localhost")
}
✅ With Validation
// @constructor NewEmail
type Email string
func NewEmail(s string) (Email, error) {
if !strings.Contains(s, "@") {
return "", errors.New("invalid email")
}
return Email(s), nil // ✅ Allowed
}
func validate(input string) {
// ❌ ERROR: CTOR01
email := Email(input) // Bypass validation!
// ✅ Correct: Use constructor
email, err := NewEmail(input)
}
✅ Builder Pattern
// @constructor NewRequestBuilder
type RequestBuilder struct {
method string
url string
headers map[string]string
}
func NewRequestBuilder() *RequestBuilder {
return &RequestBuilder{ // ✅ Allowed
headers: make(map[string]string),
}
}
func (rb *RequestBuilder) Method(m string) *RequestBuilder {
rb.method = m
return rb
}
func (rb *RequestBuilder) URL(u string) *RequestBuilder {
rb.url = u
return rb
}
func (rb *RequestBuilder) Build() *http.Request {
// Build actual request
return nil
}
Best Practices
1. Validate in Constructors
Use constructors to enforce invariants:
// @constructor NewPositiveInt
type PositiveInt int
func NewPositiveInt(n int) (PositiveInt, error) {
if n <= 0 {
return 0, errors.New("must be positive")
}
return PositiveInt(n), nil
}
2. Initialize Resources
Use constructors for resource acquisition:
// @constructor OpenFile
type File struct {
handle *os.File
}
func OpenFile(path string) (*File, error) {
handle, err := os.Open(path)
if err != nil {
return nil, err
}
return &File{handle: handle}, nil
}
3. Provide Multiple Constructors
Offer convenience constructors:
// @constructor New, NewDefault, NewFromConfig
type Server struct {
host string
port int
}
func New(host string, port int) *Server {
return &Server{host: host, port: port}
}
func NewDefault() *Server {
return New("localhost", 8080)
}
func NewFromConfig(cfg *Config) *Server {
return New(cfg.Host, cfg.Port)
}
4. Document Constructor Requirements
// @constructor NewPool
// Pool manages a pool of database connections.
// Must be created with NewPool to ensure proper initialization.
type Pool struct {
conns []*sql.DB
}
func NewPool(size int, dsn string) (*Pool, error) {
// Initialize pool
return &Pool{}, nil
}
5. Combine with @immutable
// @immutable
// @constructor NewConfig
type Config struct {
timeout time.Duration
}
func NewConfig(timeout time.Duration) *Config {
return &Config{timeout: timeout}
}
Common Patterns
Singleton Pattern
// @constructor GetInstance
type Singleton struct {
data string
}
var instance *Singleton
var once sync.Once
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{data: "initialized"}
})
return instance
}
Factory Pattern
// @constructor NewLogger
type Logger interface {
Log(msg string)
}
type logger struct {
level string
}
func NewLogger(level string) Logger {
return &logger{level: level}
}
Limitations
1. Non-Existent Constructors
If you misspell a constructor name, no error is raised:
// @constructor NewUzer // Typo! Should be NewUser
type User struct {
name string
}
// NewUser is defined, but annotation says NewUzer
func NewUser(name string) *User {
return &User{name: name}
}
func main() {
u := User{} // ❌ Will report CTOR01, even though constructor exists
}
Solution: Be careful with constructor names.
2. Same Package Only
Constructors must be in the same package:
package models
// @constructor factory.CreateUser
// ❌ Won't work - factory is different package
type User struct {}
Related Annotations
- @immutable: Often combined to ensure objects can't be mutated after construction
- @ignore: Suppress violations when needed
See Also
@testonly Annotation
The @testonly annotation restricts usage of types, functions, or methods to test files only. Any usage outside *_test.go files will be flagged as a violation.
Motivation
Go doesn't provide built-in mechanisms to mark code as test-only. Many other languages have features like:
@VisibleForTesting(Java/Android)internaltest helpers- Test-only APIs
The @testonly annotation fills this gap, allowing you to clearly mark test utilities and prevent their accidental use in production code.
Syntax
// @testonly
type TestHelper struct {}
// @testonly
func CreateMock() *MockService {}
// @testonly
func (m *MockService) Reset() {}
No parameters required - simply add // @testonly above the declaration.
How It Works
GoGreement detects usage of @testonly items outside test files:
- Type usage: Variable declarations, composite literals, type assertions
- Function calls: Direct calls to
@testonlyfunctions - Method calls: Calls to
@testonlymethods
Key Behaviors
- Test files only: Only
*_test.gofiles can use@testonlyitems - No generics support: Cannot be used with generic declarations
- Nested @testonly allowed:
@testonlycode can call other@testonlycode - Per-file deduplication: Only one error per type per file (avoids spam)
- Catches all usage: Type assertions, composite literals, variable declarations
- Can be suppressed: Use
@ignoreto allow usage in specific places
Can Be Declared On
Types
// @testonly
type MockDatabase struct {
calls int
}
// @testonly
type TestConfig struct {
Host string
}
Functions
// @testonly
func CreateTestData() []User {
return []User{{ID: 1, Name: "Test"}}
}
Methods
type Service struct {
db *sql.DB
}
// @testonly
func (s *Service) ResetForTesting() {
// Clear state for tests
}
Error Codes
| Code | Description | Example |
|---|---|---|
| TONL01 | TestOnly type used in non-test context | var m MockService in production code |
| TONL02 | TestOnly function called in non-test context | CreateMock() in production code |
| TONL03 | TestOnly method called in non-test context | obj.ResetForTesting() in production code |
Examples
✅ Basic Test Helper
helper.go:
package myapp
// @testonly
type TestHelper struct {
state map[string]interface{}
}
// @testonly
func NewTestHelper() *TestHelper {
return &TestHelper{state: make(map[string]interface{})}
}
// @testonly
func (h *TestHelper) Set(key string, value interface{}) {
h.state[key] = value
}
helper_test.go:
package myapp
func TestSomething(t *testing.T) {
helper := NewTestHelper() // ✅ Allowed in test file
helper.Set("key", "value") // ✅ Allowed in test file
}
production.go:
package myapp
func ProductionCode() {
helper := NewTestHelper() // ❌ [TONL02] function NewTestHelper is marked @testonly and can only be called in test files
}
✅ Mock Implementation
// @testonly
type MockUserService struct {
users []User
}
// @testonly
func NewMockUserService() *MockUserService {
return &MockUserService{users: []User{}}
}
// @testonly
func (m *MockUserService) AddUser(u User) {
m.users = append(m.users, u)
}
func (m *MockUserService) GetUser(id int) (*User, error) {
// Real interface method - not @testonly
for _, u := range m.users {
if u.ID == id {
return &u, nil
}
}
return nil, errors.New("not found")
}
✅ Nested @testonly Calls
// @testonly
func setupDatabase() *sql.DB {
return nil
}
// @testonly
func createTestEnvironment() *TestEnv {
db := setupDatabase() // ✅ Allowed - @testonly calling @testonly
return &TestEnv{DB: db}
}
// In test file
func TestIntegration(t *testing.T) {
env := createTestEnvironment() // ✅ Allowed in test
// ...
}
❌ Type Usage in Production
// @testonly
type MockCache struct {
data map[string]string
}
func ProductionCode() {
// ❌ [TONL01] type MockCache is marked @testonly and can only be used in test files
var cache MockCache
// ❌ [TONL01] type MockCache is marked @testonly and can only be used in test files
cache = MockCache{data: make(map[string]string)}
// ❌ [TONL01] type MockCache is marked @testonly and can only be used in test files
if c, ok := something.(*MockCache); ok {
_ = c
}
}
❌ Function Call in Production
// @testonly
func GenerateTestID() string {
return "test-" + uuid.New().String()
}
func CreateUser(name string) *User {
return &User{
ID: GenerateTestID(), // ❌ [TONL02] function GenerateTestID is marked @testonly and can only be called in test files
Name: name,
}
}
❌ Method Call in Production
type UserRepository struct {
db *sql.DB
}
// @testonly
func (r *UserRepository) ClearAll() error {
_, err := r.db.Exec("DELETE FROM users")
return err
}
func ResetProduction(repo *UserRepository) {
repo.ClearAll() // ❌ [TONL03] method ClearAll on UserRepository is marked @testonly and can only be called in test files
}
✅ Using @ignore to Suppress
// @testonly
type DebugHelper struct {
verbose bool
}
func debugFunction() {
// @ignore TONL01
helper := DebugHelper{verbose: true} // ✅ Suppressed for debugging
_ = helper
}
✅ Test Fixtures
// @testonly
type UserFixture struct {
Admin User
Regular User
Suspended User
}
// @testonly
func LoadUserFixtures() *UserFixture {
return &UserFixture{
Admin: User{ID: 1, Name: "Admin", Role: "admin"},
Regular: User{ID: 2, Name: "User", Role: "user"},
Suspended: User{ID: 3, Name: "Banned", Role: "suspended"},
}
}
// In test file
func TestUserPermissions(t *testing.T) {
fixtures := LoadUserFixtures() // ✅ Allowed
// Test with fixtures.Admin, fixtures.Regular, etc.
}
✅ Spy/Stub Pattern
type Logger interface {
Log(msg string)
}
// @testonly
type SpyLogger struct {
messages []string
}
// @testonly
func NewSpyLogger() *SpyLogger {
return &SpyLogger{}
}
func (s *SpyLogger) Log(msg string) {
s.messages = append(s.messages, msg) // Not @testonly - interface method
}
// @testonly
func (s *SpyLogger) Messages() []string {
return s.messages
}
// In test
func TestLogging(t *testing.T) {
spy := NewSpyLogger() // ✅ Allowed
service := NewService(spy)
service.DoSomething()
messages := spy.Messages() // ✅ Allowed
assert.Equal(t, 1, len(messages))
}
Best Practices
1. Use for Test Utilities
Mark test helpers and utilities:
// @testonly
func AssertNoError(t *testing.T, err error) {
if err != nil {
t.Fatal(err)
}
}
// @testonly
func CreateTempDir(t *testing.T) string {
dir, err := os.MkdirTemp("", "test")
AssertNoError(t, err)
t.Cleanup(func() { os.RemoveAll(dir) })
return dir
}
2. Mark Mock Implementations
type UserService interface {
GetUser(id int) (*User, error)
CreateUser(u *User) error
}
// @testonly
type MockUserService struct {
users map[int]*User
}
// @testonly
func NewMockUserService() *MockUserService {
return &MockUserService{users: make(map[int]*User)}
}
3. Test Fixtures and Factories
// @testonly
func MustCreateUser(t *testing.T, name string) *User {
user, err := CreateUser(name)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
return user
}
4. Document Test-Only Purpose
// @testonly
// ResetDatabase drops all tables and recreates schema.
// WARNING: This is for testing only and will destroy all data.
func ResetDatabase(db *sql.DB) error {
// Dangerous operation
return nil
}
Deduplication Behavior
GoGreement deduplicates type usage violations per file to avoid spam:
// @testonly
type MockCache struct {}
func ProductionCode() {
var c1 MockCache // ❌ ERROR: TONL01
var c2 MockCache // ✅ No error - deduplicated
var c3 MockCache // ✅ No error - deduplicated
// Only ONE error per file for MockCache type usage
}
Function and method calls are NOT deduplicated - each call reports a separate error.
Limitations
1. Per-File Deduplication
Type errors are shown once per file, even if the type is used many times:
// Only first usage reports error
var m1 MockService // ❌ ERROR
var m2 MockService // No error (deduplicated)
var m3 MockService // No error (deduplicated)
2. Indirect Usage Not Caught
If production code uses a non-test-only function that internally uses @testonly code:
// @testonly
func helper() int {
return 42
}
func indirect() int {
return helper() // ❌ ERROR caught here
}
func production() {
x := indirect() // ✅ No error - indirect usage
}
Related Annotations
- @ignore: Suppress violations when needed
- @implements: Often used with
@testonlyfor mock interfaces
See Also
@packageonly Annotation
The @packageonly annotation restricts usage of types, functions, or methods to specific packages.
Motivation
Go doesn't provide built-in mechanisms to restrict which packages can use specific code. This is needed for maintaining clean architecture:
- Controllers should only be used in routing layer
- Database models should only be accessed by repository layer
- Internal utilities should stay within specific modules
The @packageonly annotation fills this gap.
Syntax
// @packageonly
type Helper struct {}
// @packageonly pkg1, pkg2
func SpecialFunction() {}
// @packageonly myapp/internal/auth
func (s *Service) AdminMethod() {}
Parameters
- Package list (optional): Comma-separated list of allowed packages
- Can specify package names (e.g.,
testing) or full paths (e.g.,myapp/internal/auth) - If no packages specified, only the current package is allowed
- Current package is always included automatically
How It Works
GoGreement detects usage of @packageonly items outside allowed packages:
- Type usage: Variable declarations, composite literals, type assertions
- Function calls: Direct calls to
@packageonlyfunctions - Method calls: Calls to
@packageonlymethods
Key Behaviors
- Current package always allowed: The declaring package is automatically included
- No generics support: Cannot be used with generic declarations
- Per-file deduplication: Only one error per type per file (avoids spam)
- Catches all usage: Type assertions, composite literals, variable declarations
- Can be suppressed: Use
@ignoreto allow usage in specific places - Cross-package enforcement: Works even if
@packageonlywas declared in an external module - Multiple annotations supported: Multiple
@packageonlyannotations on the same declaration combine their package lists - all specified packages are allowed
Can Be Declared On
Types
// @packageonly routes
type Controller struct {
service *Service
}
Functions
// @packageonly myapp/internal/admin
func ExecuteAdminCommand(cmd string) error {
return nil
}
Methods
type Repository struct {
db *sql.DB
}
// @packageonly testing
func (r *Repository) ClearAll() error {
return nil
}
Error Codes
| Code | Description | Example |
|---|---|---|
| PKGO01 | PackageOnly type used outside allowed packages | var h Helper in unauthorized package |
| PKGO02 | PackageOnly function called outside allowed packages | ExecuteAdminCommand() in unauthorized package |
| PKGO03 | PackageOnly method called outside allowed packages | repo.ClearAll() in unauthorized package |
Examples
✅ Basic Usage
helpers/helper.go:
package helpers
// @packageonly services, handlers
type InternalHelper struct {
state map[string]interface{}
}
// @packageonly services
func ProcessInternal(data string) error {
return nil
}
services/service.go:
package services
import "myapp/helpers"
func BusinessLogic() {
h := helpers.InternalHelper{} // ✅ Allowed - services in list
helpers.ProcessInternal("data") // ✅ Allowed
}
main.go:
package main
import "myapp/helpers"
func main() {
// ❌ [PKGO01] type InternalHelper is marked @packageonly and can only be used in packages: [services, handlers, helpers]
h := helpers.InternalHelper{}
// ❌ [PKGO02] function ProcessInternal is marked @packageonly and can only be called in packages: [services, helpers]
helpers.ProcessInternal("data")
}
✅ Current Package Always Allowed
package auth
// @packageonly
type SessionManager struct {
sessions map[string]*Session
}
func NewSessionManager() *SessionManager {
return &SessionManager{ // ✅ Allowed - same package
sessions: make(map[string]*Session),
}
}
✅ Method-Level Restrictions
package repository
type UserRepository struct {
db *sql.DB
}
// Public method - no restrictions
func (r *UserRepository) GetUser(id int) (*User, error) {
return nil, nil
}
// @packageonly testing
func (r *UserRepository) InsertTestData(users []User) error {
return nil
}
Usage:
package handlers
import "myapp/repository"
func HandleRequest(repo *repository.UserRepository) {
user, _ := repo.GetUser(1) // ✅ Allowed - public method
// ❌ [PKGO03] method InsertTestData on UserRepository is marked @packageonly and can only be called in packages: [testing, repository]
repo.InsertTestData(nil)
}
✅ Using @ignore to Suppress
// @packageonly services
type InternalCache struct {
data map[string]string
}
func debugFunction() {
// @ignore PKGO01
cache := InternalCache{} // ✅ Suppressed
}
✅ Multiple Annotations (Merged)
Multiple @packageonly annotations on the same declaration are merged together:
package helpers
// @packageonly services
// @packageonly handlers, controllers
type SharedHelper struct {
data string
}
This is equivalent to:
// @packageonly services, handlers, controllers
type SharedHelper struct {
data string
}
All packages from all @packageonly annotations are combined into a single allowed list. This is useful when annotations are added incrementally or by different developers.
✅ Cross-Package Enforcement
Package external/lib:
package lib
// @packageonly myapp/internal/core
type InternalAPI struct {
token string
}
Package myapp/main:
package main
import "external/lib"
func main() {
// ❌ [PKGO01] type InternalAPI is marked @packageonly and can only be used in packages: [myapp/internal/core, lib]
api := lib.InternalAPI{}
}
Package myapp/internal/core:
package core
import "external/lib"
func CoreLogic() {
api := lib.InternalAPI{} // ✅ Allowed
}
Best Practices
Architectural Boundaries
// @packageonly routes
type HTTPController struct {
service *BusinessService
}
// @packageonly repository
type DatabaseConnection struct {
db *sql.DB
}
Administrative Functions
// @packageonly admin, internal/admin
func ResetAllData() error {
return nil
}
Deduplication Behavior
Type usage violations are deduplicated per file:
// @packageonly admin
type AdminHelper struct {}
func UnauthorizedCode() {
var h1 AdminHelper // ❌ ERROR: PKGO01
var h2 AdminHelper // ✅ No error - deduplicated
var h3 AdminHelper // ✅ No error - deduplicated
}
Function and method calls are NOT deduplicated - each call reports a separate error.
Limitations
Package Name Ambiguity
If multiple packages have the same name, use full paths:
// ❌ Ambiguous
// @packageonly util
// ✅ Clear
// @packageonly myapp/internal/util
Related Annotations
- @ignore: Suppress violations when needed
- @testonly: Simpler version that restricts to test files only
See Also
@ignore Annotation
The @ignore annotation suppresses specific violations in your code. Use it when you need to bypass checks in specific scopes or when a check produces false positives.
Motivation
Sometimes you need to violate architectural agreements for valid reasons:
- Debugging or temporary code
- Performance-critical sections
- Gradual migration to new patterns
- Working around third-party library constraints
The @ignore annotation provides fine-grained control over which violations to suppress and where.
Syntax
// @ignore CODE1, CODE2
// @ignore CATEGORY
// @ignore ALL
Parameters
- Error Codes (required): Comma-separated list of codes to ignore
- Specific codes:
IMM01,CTOR02,TONL03,PKGO01,IMPL01 - Categories:
IMM,CTOR,TONL,PKGO,IMPL(ignores all codes in category) - All violations:
ALL
- Specific codes:
- Case-insensitive:
imm01,IMM01,Imm01all work (normalized to uppercase)
Scope Types
1. File-Level Scope
Place @ignore before the package declaration to affect the entire file:
// @ignore IMM
package mypackage
// All immutability checks are suppressed in this file
2. Block-Level Scope
Place @ignore before a declaration to affect that declaration:
// @ignore CTOR01
type User struct {
ID int
}
func createUser() {
u := User{ID: 1} // ✅ Suppressed - CTOR01 ignored for User
}
3. Inline Scope
Place @ignore on the same line as code to affect just that line:
func modify(p *Point) {
p.X = 10 // @ignore IMM01
}
Key Behaviors
- Hierarchical matching:
ALL> Category (IMM) > Specific code (IMM01) - Case-insensitive: Codes are normalized to uppercase automatically
- Only affects checking: Doesn't affect annotation scanning phase
- Module-level option: Use
--config.exclude-checksflag for project-wide exclusions
Supported Annotations
| Annotation | Supported | Codes |
|---|---|---|
| @immutable | ✅ Yes | IMM01, IMM02, IMM03, IMM04 |
| @constructor | ✅ Yes | CTOR01, CTOR02, CTOR03 |
| @testonly | ✅ Yes | TONL01, TONL02, TONL03 |
| @packageonly | ✅ Yes | PKGO01, PKGO02, PKGO03 |
| @implements | ✅ Yes | IMPL01, IMPL02, IMPL03 |
Examples
✅ File-Level Ignore
// @ignore IMM, CTOR
package legacy
// All immutability and constructor checks suppressed in this file
type Config struct {
value string
}
func mutate(c *Config) {
c.value = "new" // ✅ No error - IMM ignored
}
func create() {
cfg := Config{} // ✅ No error - CTOR ignored
}
✅ Block-Level Ignore
package myapp
// @immutable
type Point struct {
X, Y int
}
// @ignore IMM01, IMM02
func unsafeMutate(p *Point) {
p.X = 10 // ✅ Suppressed - IMM01 ignored
p.Y += 5 // ✅ Suppressed - IMM02 ignored
}
✅ Inline Ignore
// @immutable
type Counter struct {
value int
}
func increment(c *Counter) {
c.value++ // @ignore IMM03
}
✅ Category-Level Ignore
// @ignore IMM
func batchUpdate(points []*Point) {
for _, p := range points {
p.X = 0 // ✅ All IMM* codes suppressed
p.Y = 0 // ✅ All IMM* codes suppressed
p.X++ // ✅ All IMM* codes suppressed
}
}
✅ Ignore All
// @ignore ALL
func debugFunction() {
// All violations suppressed here
var db Database{} // CTOR violations ignored
db.conn = nil // IMM violations ignored
}
✅ Multiple Codes
func complexOperation(p *Point) {
// @ignore IMM01, IMM02, IMM03
p.X = 10
p.Y += 5
p.X++
}
✅ Case-Insensitive
// All of these work the same:
// @ignore IMM01
// @ignore imm01
// @ignore Imm01
func modify(p *Point) {
p.X = 10 // @ignore imm01 (normalized to IMM01)
}
✅ Gradual Migration
When migrating to immutable types:
// @immutable
type Config struct {
timeout int
}
// Old code - suppress temporarily during migration
// @ignore IMM
func legacyUpdate(c *Config) {
c.timeout = 5000 // ✅ Suppressed during migration
}
// New code - follows immutability
func newUpdate(c Config) Config {
return Config{timeout: 5000}
}
✅ Performance-Critical Section
// @immutable
type Stats struct {
counts []int
}
func (s *Stats) incrementUnsafe(index int) {
// Performance-critical: avoid allocation
// @ignore IMM04
s.counts[index]++
}
Best Practices
1. Document Why
Always explain why you're ignoring a check:
// @ignore IMM01
// JUSTIFICATION: Need to modify cache for performance
// TODO(user): Refactor to use copy-on-write
func updateCache(c *Cache, key string, value interface{}) {
c.data[key] = value
}
2. Be Specific
Prefer specific codes over categories:
// ✅ Good: Specific code
// @ignore IMM01
p.X = 10
// ❌ Bad: Too broad
// @ignore IMM
p.X = 10
p.Y += 5
3. Minimize Scope
Use inline scope when possible:
// ✅ Good: Minimal scope
func process(p *Point) {
validate(p)
p.X = normalize(p.X) // @ignore IMM01
process(p)
}
// ❌ Bad: Too broad
// @ignore IMM01
func process(p *Point) {
validate(p)
p.X = normalize(p.X)
process(p) // IMM01 suppressed here too (unintended)
}
4. Temporary Ignores
Mark temporary ignores with TODOs:
// @ignore CTOR01
// TODO(alice): Add proper constructor after refactoring
type TempConfig struct {
value string
}
5. Review Regularly
Add comments to track ignores:
// @ignore IMM01
// Added: 2024-01-15
// Reason: Performance optimization
// Review: 2024-06-01
func hotPath(data *Data) {
data.value = compute()
}
Ignore Hierarchy
GoGreement checks codes in this order:
- ALL - Suppresses everything
- Category (e.g.,
IMM) - Suppresses all codes in category - Specific Code (e.g.,
IMM01) - Suppresses only that code
Example:
// @ignore ALL
// Suppresses: IMM01, IMM02, IMM03, IMM04, CTOR01, CTOR02, CTOR03, TONL01, TONL02, TONL03, PKGO01, PKGO02, PKGO03, IMPL01, IMPL02, IMPL03
// @ignore IMM
// Suppresses: IMM01, IMM02, IMM03, IMM04
// @ignore PKGO
// Suppresses: PKGO01, PKGO02, PKGO03
// @ignore IMPL
// Suppresses: IMPL01, IMPL02, IMPL03
// @ignore IMM01
// Suppresses: IMM01 only
Module-Level Exclusion
For project-wide exclusions, use command-line flags instead of @ignore:
# Exclude all immutability checks project-wide
gogreement --config.exclude-checks=IMM ./...
# Exclude specific codes
gogreement --config.exclude-checks=IMM01,CTOR02 ./...
Or set environment variable:
export GOGREEMENT_EXCLUDE_CHECKS=IMM,TONL
gogreement ./...
See Getting Started - Configuration for more details.
Common Patterns
Debugging Code
// @ignore ALL
func debugDump(state *State) {
state.value = readFromDebugger() // Temporary debug code
}
Third-Party Integration
// @ignore CTOR01
// Reason: Required by external framework
type PluginConfig struct {
Name string
}
Test Utilities in Production Code
// @ignore TONL02
// Reason: Needed for integration test setup in main package
func resetForIntegrationTests() {
helper := testHelper() // Normally not allowed
}
Related
- Error Codes Reference: List of all error codes
- Getting Started - Configuration: Module-level exclusions
See Also
Error Codes Reference
GoGreement reports violations using structured error codes. Each code identifies a specific type of violation and can be used with the @ignore annotation to suppress false positives.
Code Structure
Error codes follow the format: [CATEGORY][NUMBER]
- Category: 2-4 letter prefix identifying the annotation (e.g.,
IMM,CTOR,TONL,PKGO,IMPL) - Number: Two-digit sequential number within the category (e.g.,
01,02)
Example: IMM01 = Immutable category, violation type 01
All Error Codes
IMM - Immutable Violations
Violations of @immutable annotations. These can be suppressed with @ignore.
| Code | Description | Example |
|---|---|---|
| IMM01 | Field of immutable type is being assigned | point.X = 10 |
| IMM02 | Compound assignment to immutable field | point.X += 5, count *= 2 |
| IMM03 | Increment/decrement of immutable field | point.X++, count-- |
| IMM04 | Index assignment to immutable collection | obj.items[0] = value, obj.dict["key"] = val |
Suppress with:
// @ignore IMM- All immutability checks// @ignore IMM01- Specific check only
Documentation: @immutable
CTOR - Constructor Violations
Violations of @constructor annotations. These can be suppressed with @ignore.
| Code | Description | Example |
|---|---|---|
| CTOR01 | Composite literal used outside allowed constructor functions | db := Database{} |
| CTOR02 | new() call used outside allowed constructor functions | db := new(Database) |
| CTOR03 | Variable declaration creates zero-initialized instance outside allowed constructor functions | var db Database |
Suppress with:
// @ignore CTOR- All constructor checks// @ignore CTOR01- Specific check only
Documentation: @constructor
TONL - TestOnly Violations
Violations of @testonly annotations. These can be suppressed with @ignore.
| Code | Description | Example |
|---|---|---|
| TONL01 | TestOnly type used outside test context | var mock MockService in production code |
| TONL02 | TestOnly function called outside test context | CreateMock() in production code |
| TONL03 | TestOnly method called outside test context | service.ResetForTesting() in production code |
Suppress with:
// @ignore TONL- All testonly checks// @ignore TONL01- Specific check only
Documentation: @testonly
PKGO - PackageOnly Violations
Violations of @packageonly annotations. These can be suppressed with @ignore.
| Code | Description | Example |
|---|---|---|
| PKGO01 | PackageOnly type used outside allowed packages | var helper InternalHelper in unauthorized package |
| PKGO02 | PackageOnly function called outside allowed packages | ExecuteAdminCommand() in unauthorized package |
| PKGO03 | PackageOnly method called outside allowed packages | repo.InsertTestData() in unauthorized package |
Suppress with:
// @ignore PKGO- All packageonly checks// @ignore PKGO01- Specific check only
Documentation: @packageonly
IMPL - Implements Violations
Violations of @implements annotations. These can be suppressed with @ignore.
| Code | Description | Example |
|---|---|---|
| IMPL01 | Package not found in imports | Using @implements pkg.Interface without importing pkg |
| IMPL02 | Interface not found in package | Interface name doesn't exist or is misspelled |
| IMPL03 | Missing or incorrect methods | Type doesn't implement all required methods with correct signatures |
Suppress with:
// @ignore IMPL- All implements checks// @ignore IMPL01- Specific check only
Documentation: @implements
Using Error Codes
With @ignore Annotation
Suppress specific violations in your code:
// Suppress specific code
// @ignore IMM01
point.X = 10
// Suppress multiple codes
// @ignore IMM01, CTOR02
func unsafeOperation() {
point.X = 10
db := new(Database)
}
// Suppress entire category
// @ignore IMM
func batchUpdate() {
// All IMM* codes suppressed
}
// Suppress all violations
// @ignore ALL
func debugFunction() {
// Everything suppressed
}
With Command-Line Flags
Exclude checks globally across your project:
# Exclude all immutability checks
gogreement --config.exclude-checks=IMM ./...
# Exclude specific codes
gogreement --config.exclude-checks=IMM01,CTOR02,TONL03 ./...
# Exclude multiple categories
gogreement --config.exclude-checks=IMM,TONL ./...
With Environment Variables
Set project-wide defaults:
export GOGREEMENT_EXCLUDE_CHECKS=IMM01,CTOR
gogreement ./...
Code Hierarchy
Codes follow a hierarchical structure for suppression:
ALL
├── IMM (Immutable)
│ ├── IMM01 (Field assignment)
│ ├── IMM02 (Compound assignment)
│ ├── IMM03 (Increment/decrement)
│ └── IMM04 (Index assignment)
├── CTOR (Constructor)
│ ├── CTOR01 (Composite literal)
│ ├── CTOR02 (new() call)
│ └── CTOR03 (Var declaration)
├── TONL (TestOnly)
│ ├── TONL01 (Type usage)
│ ├── TONL02 (Function call)
│ └── TONL03 (Method call)
├── PKGO (PackageOnly)
│ ├── PKGO01 (Type usage)
│ ├── PKGO02 (Function call)
│ └── PKGO03 (Method call)
└── IMPL (Implements)
├── IMPL01 (Package not found)
├── IMPL02 (Interface not found)
└── IMPL03 (Missing methods)
When you suppress a code at any level, all codes below it are also suppressed:
@ignore ALL→ Suppresses everything@ignore IMM→ Suppresses IMM01, IMM02, IMM03, IMM04@ignore IMM01→ Suppresses only IMM01
Quick Reference by Annotation
| Annotation | Description | Codes |
|---|---|---|
| @immutable | Prevents field mutations | IMM01, IMM02, IMM03, IMM04 |
| @constructor | Restricts object creation | CTOR01, CTOR02, CTOR03 |
| @testonly | Limits to test files | TONL01, TONL02, TONL03 |
| @packageonly | Limits to specific packages | PKGO01, PKGO02, PKGO03 |
| @implements | Verifies interface implementation | IMPL01, IMPL02, IMPL03 |
Error Message Format
GoGreement error messages include the error code for easy reference:
path/to/file.go:15:2: IMM01 - Field of immutable type is being assigned
path/to/file.go:23:5: CTOR01 - Composite literal used outside allowed constructor functions
path/to/file.go:45:10: TONL02 - TestOnly function called outside test context
Best Practices
1. Be Specific
Use the most specific code possible when suppressing:
// ✅ Good
// @ignore IMM01
point.X = 10
// ❌ Too broad
// @ignore ALL
point.X = 10
2. Document Suppressions
Always explain why you're suppressing a check:
// @ignore IMM01
// REASON: Performance-critical path, avoiding allocations
// TODO: Refactor to use copy-on-write
cache.data[key] = value
3. Prefer Fixing Over Suppressing
Suppression should be the exception, not the rule:
// ❌ Bad: Suppressing instead of fixing
// @ignore IMM
type Point struct { X, Y int }
// ✅ Good: Fix the architecture
// Remove @immutable if mutation is required
type Point struct { X, Y int }
4. Review Suppressions Regularly
Periodically search for @ignore in your codebase and review whether suppressions are still needed.
See Also
- @ignore Annotation: Detailed guide on suppressing violations
- Getting Started - Configuration: Module-level exclusions
- Annotations: Learn about all annotations
Contributing
Contributions to GoGreement are welcome! Whether it's bug fixes, new features, documentation improvements, or test coverage, all contributions help make the project better.
Requirements
When contributing code, please ensure:
- Test Coverage: Every change must be covered by tests (both unit and integration tests)
- Test Data Location: Test data files are located in the
testdatadirectory - Code Reuse: Reuse collection types and utilities from the
utilmodule instead of duplicating code - Self-Documenting Code: Use the project's own annotations on the codebase itself
- Documentation Updates: Update documentation in the
book/directory when making user-facing changes
Project Structure
Source Code
cmd/gogreement/main.go- Entry pointsrc/analyzer/- Analyzer registry and orchestrationsrc/annotations/- Annotation parsing and fact typessrc/codes/- Error code definitionssrc/config/- Configuration managementsrc/constructor/- Constructor checkersrc/immutable/- Immutability checkersrc/implements/- Interface implementation checkersrc/testonly/- Test-only checkersrc/ignore/- Ignore directive parsingsrc/indexing/- Cross-package fact indexingsrc/util/- Shared utilitiessrc/testutil/- Testing helpers
Tests
testdata/unit/- Unit test fixtures for individual checkerstestdata/integration/src/- Integration test fixtures for cross-package analysis
Documentation
book/gogreement-docs/- User documentation built with mdBook
Development Workflow
1. Setup
# Clone the repository
git clone https://github.com/a14e/gogreement
cd gogreement
# Install development tools
make install
2. Make Changes
# Run tests frequently
make test
# Run linters
make lint
# Format code
make fmt
3. Testing
# Run all tests
make test
# Run specific test
go test ./src/immutable/...
# Run integration tests
go test ./src/analyzer/...
Note: Integration tests use a multi-module structure in testdata/integration/src. Each test scenario (e.g., multimodule_implements/) is a separate Go module in that directory. The analysistest.Run() function automatically adds /src to the testdata path and handles module loading.
4. Pre-Commit Checks
Before committing, ensure all checks pass:
# Run full pre-build check
make pre-build
This runs:
go mod tidymake fmtmake lint(golangci-lint and nilaway)make test
5. Build
make build
Adding New Features
Adding a New Checker
To add a new annotation checker:
-
Create checker package in
src/newchecker/src/newchecker/ ├── checker.go ├── checker_test.go └── reporting.go -
Add annotation type to
PackageAnnotationsstruct insrc/annotations/annotation.go(if your checker needs a new annotation type):type PackageAnnotations struct { ImplementsAnnotations []ImplementsAnnotation ConstructorAnnotations []ConstructorAnnotation ImmutableAnnotations []ImmutableAnnotation TestonlyAnnotations []TestOnlyAnnotation NewCheckerAnnotations []NewCheckerAnnotation // Add your annotation slice here } -
Define fact type in
src/annotations/annotation.goImportant: Each checker must have its own fact type. This is a workaround for a limitation in the Go analysis framework—facts are not shared between different analyzers. By creating separate types (even though they wrap the same
PackageAnnotations), we allow each checker to export and import facts independently.type NewCheckerFact PackageAnnotations func (*NewCheckerFact) AFact() {} func (f *NewCheckerFact) GetAnnotations() *PackageAnnotations { return (*PackageAnnotations)(f) } func (*NewCheckerFact) Empty() AnnotationWrapper { return &NewCheckerFact{} } -
Add error codes in
src/codes/codes.goconst ( NewCheckerViolation01 = "NEWC01" NewCheckerViolation02 = "NEWC02" ) var CodesByCategory = map[string][]Code{ // ... "NEWC": { {NewCheckerViolation01, "Description of NEWC01"}, {NewCheckerViolation02, "Description of NEWC02"}, }, } -
Register analyzer in
src/analyzer/analyzer.gofunc AllAnalyzers() []*analysis.Analyzer { return []*analysis.Analyzer{ AnnotationReader, IgnoreReader, NewChecker, // Add here // ... } } -
Create unit tests in
testdata/unit/newcheckertests/ -
Create integration tests in
testdata/integration/src/multimodule_newchecker/ -
Update documentation in
book/gogreement-docs/src/02_0X_newchecker.md
Adding a New Error Code
Follow these steps when adding a new violation type:
-
Update
src/codes/codes.go:const ( ExistingCode01 = "EXIST01" ExistingCode02 = "EXIST02" NewCode03 = "EXIST03" // Add new code ) var CodesByCategory = map[string][]Code{ "EXIST": { {ExistingCode01, "Description"}, {ExistingCode02, "Description"}, {NewCode03, "Description of new code"}, // Add entry }, } -
Update checker to use the new code:
import "github.com/a14e/github.com/a14e/gogreement/src/codes" violation := Violation{ Code: codes.NewCode03, // ... } -
Tests automatically verify:
- Code uniqueness
- Category prefix correctness
- Reverse mapping
Best Practices for Error Codes
- Use different codes for different violation types: Each distinct violation should have its own code
- Enable selective suppression: Users can then use
@ignore EXIST03to suppress only that specific violation - Examples:
IMM01for field assignments,IMM02for compound assignmentsTONL01for type usage,TONL02for function calls
Testing Guidelines
Unit Tests
- Located in same package as the code being tested
- Use
testutil.CreateTestPass()for creating test passes - Use
testutil.GetUnitTestdataPath()for test fixtures
func TestMyChecker(t *testing.T) {
pass := testutil.CreateTestPass(t, "packagename")
violations := checker.CheckSomething(cfg, pass, annotations)
require.Equal(t, expectedViolations, violations)
}
Integration Tests
- Located in
src/analyzer/analyzer_integration_test.go - Use
testutil.GetIntegrationTestdataPath()for fixtures - Test cross-package analysis and package facts
func TestCrossPackage(t *testing.T) {
testdata := testutil.GetRootTestdataPath() + "/integration"
analysistest.Run(t, testdata, Checker, "multimodule_x/modA", "multimodule_x/modB")
}
Code Style
- Comments: All code comments must be in English
- Formatting: Use
make fmtto format code - Linting: Code must pass
golangci-lintandnilaway - No Redundant Comments: Comments should explain why, not what. If the code is self-explanatory, comments are unnecessary.
Third-Party Dependencies
When adding external libraries:
- Ensure the library has an MIT-compatible license
- Update
THIRD_PARTY_LICENSESfile - Only include direct dependencies (not transitive or test dependencies)
# Add dependency
go get github.com/example/library
# Update third-party licenses
# Add entry to THIRD_PARTY_LICENSES with:
# - Library name
# - Version
# - License type
# - License text
Documentation
Documentation is built using mdBook.
Building Docs Locally
cd book/gogreement-docs
# Install mdBook
cargo install mdbook
# Serve docs locally
mdbook serve
# Build docs
mdbook build
Documentation Structure
book/gogreement-docs/src/SUMMARY.md- Table of contentsbook/gogreement-docs/src/01_*.md- Getting started guidesbook/gogreement-docs/src/02_*.md- Annotation referencesbook/gogreement-docs/src/03_codes.md- Error codes referencebook/gogreement-docs/src/04_contributing.md- This file
Pull Request Guidelines
Before submitting a pull request:
-
Run all checks:
make pre-build -
Write descriptive commit messages:
Add support for generic types in @immutable - Implement generic type parsing - Add tests for generic immutable types - Update documentation -
Update documentation if your change affects user-facing behavior
-
Add tests for all new functionality
-
Keep changes focused: One PR should address one issue or feature
Getting Help
- Issues: Report bugs or request features at github.com/a14e/gogreement/issues
- Discussions: Ask questions or discuss ideas in GitHub Discussions
License
By contributing to GoGreement, you agree that your contributions will be licensed under the same license as the project.