Documents
Go Modernization Gotchas
Go Modernization Gotchas
Type
Topic
Status
Published
Created
Feb 27, 2026
Updated
Feb 27, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

Go Modernization Gotchas#

The opnDossier project is a Go CLI tool for converting OPNsense firewall configurations to documentation. Built with Go 1.26+, it adopts modern Go features including iterator-based APIs, enhanced pointer literal syntax, and automated modernization tooling. This article documents the specific gotchas, patterns, and best practices discovered during the project's modernization journey.

Go 1.26 introduces several language improvements that can improve code quality and performance, but their adoption comes with subtleties that are not immediately obvious. The opnDossier codebase serves as a practical example of navigating these modernization challenges, from choosing when to use strings.SplitSeq over strings.Split, to handling conflicts between modernization tools and linters, to establishing shared constant patterns for validation.

This knowledge base article captures project-specific lessons learned while modernizing to Go 1.26, providing software engineers with concrete examples and actionable guidance for their own modernization efforts.

Go 1.26 Version Requirements#

The project requires Go 1.26+ with toolchain go1.26.0. This requirement is enforced through:

All code, documentation, and examples must be compatible with Go 1.26 standards.

Iterator APIs: strings.SplitSeq#

Overview#

strings.SplitSeq is a Go 1.24+ iterator-based string splitting function included in Go 1.26. It returns an iterator instead of allocating a slice, offering memory efficiency benefits when processing large strings or when early termination is possible.

When to Use SplitSeq vs Split#

Use strings.SplitSeq when:

Use strings.Split when:

  • You need the full slice of results
  • Slice semantics are clearer for the use case
  • Random access to split results is required

Code Examples#

Hostname Validation#

From internal/processor/validate_helpers.go:

func isValidHostname(hostname string) bool {
    if hostname == "" || len(hostname) > constants.MaxHostnameLength {
        return false
    }

    for part := range strings.SplitSeq(hostname, ".") {
        if part == "" || !hostnamePattern.MatchString(part) {
            return false
        }
    }

    return true
}

This validates each dot-separated label of a hostname without allocating a slice, and can exit early on the first invalid part.

Plugin String Parsing#

From internal/plugins/firewall/firewall.go:

for plugin := range strings.SplitSeq(device.System.Firmware.Plugins, ",") {
    if strings.EqualFold(strings.TrimSpace(plugin), autoConfigBackupPackage) {
        return checkResult{Result: true, Known: true}
    }
}

This parses a comma-separated plugin list and returns immediately when the target package is found.

JSON Log Line Processing#

From internal/logging/logger_test.go:

if tt.format == testFormatJSON {
    lines := strings.SplitSeq(strings.TrimSpace(output), "\n")
    for line := range lines {
        if line == "" {
            continue
        }
        // ... validate JSON
    }
}

This processes log output line-by-line for JSON validation in tests without allocating a full slice of lines.

Adoption Status#

The opnDossier codebase uses SplitSeq in 4 locations, showing pragmatic mixed usage: SplitSeq where iterator benefits apply, Split where slice semantics are necessary.

new() Syntax for Pointer Values#

Overview#

Go 1.26+ supports the new(expr) syntax for creating pointers to literal values. The opnDossier project uses this extensively (46 occurrences across test files) to replace helper functions like StringPtr().

Why *string Is Required#

The schema.Source and schema.Destination types use *string for the Any field to distinguish XML element presence from absence:

  • <any/> (self-closing) → *string pointing to "" (non-nil)
  • <any>1</any>*string pointing to "1" (non-nil)
  • absent element → nil

This is necessary because Go's encoding/xml produces "" for both self-closing tags and absent elements when using plain string fields.

Code Examples#

Creating *string Pointers for Source/Destination#

From internal/schema/security_test.go:

{name: "empty string any", src: Source{Any: new("")}, want: true},
{name: "non-empty any", src: Source{Any: new("1")}, want: true},

Validation Test Cases#

From internal/validator/opnsense_test.go:

{
    Type: "pass",
    Interface: schema.InterfaceList{"lan"},
    Source: schema.Source{Any: new("")},
},

Official Guidance#

From AGENTS.md:

// Good — Go 1.26+ new(expr) syntax
src := Source{Any: new(""), Network: new("lan")}

// Legacy — StringPtr helper (still available in model package)
src := Source{Any: model.StringPtr(""), Network: model.StringPtr("lan")}

Legacy Helper Function#

The StringPtr() helper function in internal/model/security.go remains for backward compatibility:

// StringPtr returns a pointer to the given string value.
// Convenience helper for constructing Source/Destination literals
// with the *string Any field. Prefer new(expr) in Go 1.26+.
func StringPtr(s string) *string {
	return new(s)
}

Note that the helper itself now internally uses new(s) rather than &s, showing the migration path.

Modernize Tool Integration#

Overview#

The opnDossier project integrates golang.org/x/tools modernize tool for automated Go 1.26+ compatibility checks and fixes.

Available Commands#

From Justfile:

# Apply modernization fixes
just modernize

# Check for modernization opportunities (dry-run)
just modernize-check

These commands run:

go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test ./...

Integration Points#

Primary Modernize Check: omitempty on Struct Fields#

From AGENTS.md linter troubleshooting table:

LinterIssueFix
modernizeomitempty on struct fieldsRemove omitempty from JSON tags on struct-typed fields (no effect in encoding/json); YAML omitempty is fine

Documentation specifies:

JSON struct tags on nested struct fields must NOT use omitempty (Go 1.26+ modernize check)

This applies specifically to the internal/model/common/ directory.

Additional Modernization Patterns#

The modernize tool has been used for:

  • Modernizing benchmark loops using b.Loop() for Go 1.24+ compatibility

Critical Gotcha: //go Directive Conflict#

The Problem#

The modernize tool with -fix flag auto-applies Go idiom upgrades but adds //go:fix inline directives that must be removed afterward.

From AGENTS.md:

# Modernization (Go 1.26+)
go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest -test -fix ./...
# Note: remove //go:fix inline directives afterward (conflicts with gocheckcompilerdirectives)

Why It Conflicts#

The gocheckcompilerdirectives linter is enabled in the project's .golangci.yml configuration. This linter validates that compiler directives are well-formed and recognized by the Go compiler.

The //go:fix directive is not a valid Go compiler directive, so gocheckcompilerdirectives flags it as an error.

Solution#

Manual cleanup required: After running modernize -fix, you must:

  1. Review all changes made by the tool
  2. Search for and remove all //go:fix directives
  3. Run linters to verify no directive conflicts remain

Current Status#

No //go:fix directives exist in the current codebase, indicating the project has successfully adopted this workflow.

Shared Validation Constants Pattern#

Overview#

The internal/constants/constants.go file contains shared constants used across the opnDossier application, including validation whitelists that serve as a single source of truth for allowed configuration values.

Validation Whitelists#

ValidOptimizationModes#

Defined at lines 106-113:

// ValidOptimizationModes defines the allowed system optimization modes.
// Shared by processor and validator packages — single source of truth.
var ValidOptimizationModes = map[string]struct{}{
    "normal": {},
    "high-latency": {},
    "aggressive": {},
    "conservative": {},
}

ValidPowerdModes#

Defined at lines 115-123:

// ValidPowerdModes defines the allowed powerd power modes.
// Shared by processor and validator packages — single source of truth.
var ValidPowerdModes = map[string]struct{}{
    "hadp": {},
    "hiadp": {},
    "adaptive": {},
    "minimum": {},
    "maximum": {},
}

Both use map[string]struct{} for efficient O(1) membership checking.

Usage in Processor Package#

From internal/processor/validate.go:

ValidOptimizationModes (line 80):

if _, ok := constants.ValidOptimizationModes[s.Optimization]; !ok {
    errors = append(errors, ValidationError{Field: "system.optimization", Message: "invalid optimization value"})
}

ValidPowerdModes checks for three power mode fields:

Usage in Validator Package#

From internal/validator/opnsense.go:

ValidOptimizationModes with detailed error messages:

if _, ok := constants.ValidOptimizationModes[system.Optimization]; !ok {
    errors = append(errors, ValidationError{
        Field: "system.optimization",
        Message: fmt.Sprintf("optimization '%s' must be one of: %v",
            system.Optimization,
            slices.Sorted(maps.Keys(constants.ValidOptimizationModes))),
    })
}

ValidPowerdModes with sorted list for user-friendly errors:

Benefits of This Pattern#

  1. Single Source of Truth: Explicitly documented in comments
  2. Prevents Duplication: No risk of validation logic diverging between packages
  3. Efficient Membership Checking: map[string]struct{} provides O(1) lookups
  4. Easy Maintenance: Single location to update when valid values change
  5. Consistency: Both packages use the same validation rules
  • Go 1.26 Language Features: Iterator-based APIs, enhanced literal syntax
  • Code Modernization: Automated tooling for Go version upgrades
  • Linter Configuration: Managing conflicts between tools and linters
  • Validation Patterns: Centralized constant management for consistency
  • XML Parsing: Pointer semantics for distinguishing element presence
  • Memory Efficiency: Iterator patterns for large data processing

Relevant Code Files#

File PathPurposeKey Content
go.modGo version declarationgo 1.26, toolchain go1.26.0
internal/constants/constants.goShared validation constantsValidOptimizationModes, ValidPowerdModes
internal/processor/validate.goProcessor validationUses shared constants for validation
internal/validator/opnsense.goValidator validationUses shared constants with detailed errors
internal/processor/validate_helpers.goValidation helpersstrings.SplitSeq for hostname validation
internal/plugins/firewall/firewall.goPlugin parsingstrings.SplitSeq for plugin list parsing
internal/schema/security.goXML schema types*string for distinguishing XML presence
internal/schema/security_test.goSchema testsnew("") syntax examples (46 occurrences)
internal/model/security.goLegacy helpersStringPtr() helper for backward compatibility
justfileDevelopment automationmodernize and modernize-check commands
.golangci.ymlLinter configurationgocheckcompilerdirectives linter enabled
AGENTS.mdDevelopment standardsModernization patterns and guidance
CHANGELOG.mdProject historyModernize tool integration history