Help us shape the future of Go! Take the 2026 Go Developer Survey

goseal

package module
v0.0.6 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: MIT Imports: 10 Imported by: 0

README

goseal

goseal is a linter that checks direct initialization of Go structs and enforces creation through factory functions. It "seals" your structs, ensuring they can only be instantiated through approved factory functions. Use it to prevent invalid values from leaking into your objects, such as when applying the Value Object pattern.

Development

Install mise and set up the project tools:

mise trust
mise install

This installs the locked versions of Go and golangci-lint defined in mise.toml and mise.lock. Once mise is activated in your shell, use the existing Make targets as usual:

make test
make lint

Installation

go install github.com/jimmysharp/goseal/cmd/goseal@latest

Configuration

Create .goseal.yml in your project root:

Note: While target-packages is optional (omitting it will target all packages), it is strongly recommended to explicitly specify the packages containing your domain structs. This prevents false positives with structs in third-party or standard library code.

# List of regexps for packages containing target structs to protect
# If not specified or empty, all packages are targeted
# Default: []
target-packages:
  - "github\\.com/yourorg/domain/.*"
  - "github\\.com/yourorg/model/.*"

# List of regexps for struct names to exclude from protection
# Even if in target-packages, these structs won't be protected
# Default: []
exclude-structs:
  - "^Config$"
  - "DTO$"

# List of regexps for functions considered as factory functions
# If empty, struct initialization is allowed in any context
# Default: []
factory-names:
  - "^New.*"

# Scope for struct initialization
# - any: Allow initialization from all packages
# - in-target-packages: Allow initialization from packages in target-packages
# - same-package: Allow initialization only within the same package
# Default: same-package
init-scope: same-package

# Scope for field mutation
# - any: Allow field mutation everywhere
# - in-target-packages: Allow field mutation from packages in target-packages
# - receiver: Allow field mutation only in receiver methods
# - same-package: Allow field mutation within the same package
# - never: Never allow field mutation
# Default: receiver
mutation-scope: receiver

# List of regexps for files to ignore
# Default: []
ignore-files:
  - "_test\\.go$"
  - "mock_.*\\.go$"

Note: Auto-generated files are automatically skipped.

Usage

Standalone
goseal ./...
golangci-lint (custom plugin)

goseal can also be used as a golangci-lint custom plugin. When used as a plugin, .goseal.yml is not used. Instead, configure settings directly in .golangci.yml.

Create .custom-gcl.yml to build a custom golangci-lint binary with goseal:

version: v2.9.0
plugins:
  - module: 'github.com/jimmysharp/goseal'
    import: 'github.com/jimmysharp/goseal/plugin'

Then configure goseal in .golangci.yml (the same settings as .goseal.yml are available under settings):

version: "2"
linters:
  enable:
    - goseal
  settings:
    custom:
      goseal:
        type: module
        settings:
          target-packages:
            - "github\\.com/yourorg/domain/.*"
          factory-names:
            - "^New.*"
          init-scope: same-package
          mutation-scope: receiver

Build and run:

golangci-lint custom
./custom-gcl run ./...

Examples

Example domain object
package domain

type User struct {
    ID   string
    Name string
}

func NewUser(id, name string) *User {
    // ✅ Allowed inside a factory function
    return &User{
        ID:   id,
        Name: name,
    }
}

// ✅ Allowed: mutation in receiver method (when mutation-scope: receiver)
func (u *User) UpdateName(name string) {
    u.Name = name
}
Example that gets flagged (NG)
package app

import "github.com/yourorg/domain"

func CreateUser() {
    // ❌ Direct use of a struct literal (when init-scope: same-package)
    user := domain.User{
        ID:   "123",
        Name: "Alice",
    }
    
    // ❌ Direct field assignment (when mutation-scope: receiver)
    user.Name = "Bob"
}
package app

import "github.com/yourorg/domain"

func CreateUser() {
    // ✅ Use the factory function
    user := domain.NewUser("123", "Alice")
    
    // ✅ Use the method for mutation
    user.UpdateName("Bob")
}

Usecases

Strict constructor usage with immutability

Force constructor usage in domain packages and prohibit all mutations:

target-packages:
  - "github\\.com/yourorg/domain/.*"
factory-names:
  - "^New.*"
init-scope: same-package
mutation-scope: never
Entity Pattern

Allow mutation through methods while preventing direct field access:

target-packages:
  - "github\\.com/yourorg/domain/entity/.*"
factory-names:
  - "^New.*"
init-scope: same-package
mutation-scope: receiver
Domain packages with subpackages

Allow cross-package initialization within domain packages (useful when domain is split into subpackages like domain/user, domain/order, etc.):

target-packages:
  - "github\\.com/yourorg/domain/.*"
factory-names:
  - "^New.*"
init-scope: in-target-packages
mutation-scope: never
Trust package boundaries

Allow free operations within domain packages:

target-packages:
  - "github\\.com/yourorg/domain/.*"
init-scope: same-package
mutation-scope: same-package
Exclude specific structs

Exclude specific structs (e.g., configuration structs, DTOs) from protection:

target-packages:
  - "github\\.com/yourorg/domain/.*"
exclude-structs:
  - "^Config$"
  - "DTO$"
factory-names:
  - "^New.*"
init-scope: same-package
mutation-scope: receiver

License

MIT License – see LICENSE for details.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewAnalyzer

func NewAnalyzer(config *Config) *analysis.Analyzer

Types

type Config

type Config struct {
	TargetPackages []*regexp.Regexp // Regex patterns for packages containing target structs (if empty, all packages are targeted)
	ExcludeStructs []*regexp.Regexp // Regex patterns for struct names to exclude from protection
	FactoryNames   []*regexp.Regexp // Regex patterns for factory function names (if empty, all function names are allowed)
	InitScope      InitScope        // Scope for struct initialization
	MutationScope  MutationScope    // Scope for field mutation
	IgnoreFiles    []*regexp.Regexp // Regex patterns for files to ignore
}

func NewConfig

func NewConfig(
	targetPackages []*regexp.Regexp,
	excludeStructs []*regexp.Regexp,
	factoryNames []*regexp.Regexp,
	initScope InitScope,
	mutationScope MutationScope,
	ignoreFiles []*regexp.Regexp,
) (*Config, error)

func ParseConfig

func ParseConfig(path string) (*Config, error)

func ParseFromYAML added in v0.0.3

func ParseFromYAML(data []byte) (*Config, error)

func (*Config) UnmarshalJSON added in v0.0.3

func (c *Config) UnmarshalJSON(data []byte) error

type InitScope

type InitScope string
const (
	InitScopeAny              InitScope = "any"
	InitScopeInTargetPackages InitScope = "in-target-packages"
	InitScopeSamePackage      InitScope = "same-package"
)

type MutationScope

type MutationScope string
const (
	MutationScopeAny              MutationScope = "any"
	MutationScopeInTargetPackages MutationScope = "in-target-packages"
	MutationScopeReceiver         MutationScope = "receiver"
	MutationScopeSamePackage      MutationScope = "same-package"
	MutationScopeNever            MutationScope = "never"
)

Directories

Path Synopsis
cmd
goseal command

Jump to

Keyboard shortcuts

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