How go-modern-guidelines Validates sync.OnceFunc and sync.OnceValue Concurrent Patterns
The go-modern-guidelines linter enforces idiomatic usage of sync.Once-based initialization patterns, ensuring that once-func side effects and once-value singletons are implemented without data races, discarded results, or redundant synchronization.
The JetBrains/go-modern-guidelines repository provides a comprehensive linting framework designed to enforce modern Go concurrency idioms. Among its critical checks are validations for concurrent patterns analogous to sync.OnceFunc and sync.OnceValue—techniques that simplify lazy initialization and singleton management in Go 1.21+. Understanding how this tool analyzes these patterns helps developers avoid subtle races and incorrect usage of the sync.Once API.
Pattern Validation for sync.OnceFunc and sync.OnceValue
The tool distinguishes between two fundamental initialization patterns that mirror the behavior of sync.OnceFunc and sync.OnceValue. These checks are implemented in internal/guidelines/guidelines.go, where the analyzer inspects variable declarations and method calls to enforce correctness.
Once-Func Side Effect Patterns
For side-effect-heavy initialization (the functional equivalent of sync.OnceFunc), the linter verifies that the Do method receives a function literal or named function containing the initialization logic. The tool specifically flags instances where a sync.Once variable is copied or reassigned after creation, which would break the exactly-once guarantee. It also ensures that no additional synchronization primitives (such as extra mutexes) wrap the once.Do call, preventing redundant locking that indicates a design flaw.
Once-Value Singleton Patterns
When implementing lazy singletons (matching sync.OnceValue semantics), the linter confirms that the computed result is stored in a package-level variable within the Do closure rather than returned directly. Since sync.Once.Do discards any return value from the function parameter, the tool flags attempts to return values from inside the closure. It further validates that the stored value is read only after once.Do completes, preventing race conditions where goroutines might access uninitialized zero values.
Implementation in the Guidelines Package
The core logic resides in internal/guidelines/guidelines.go, where the static analyzer traverses AST nodes to identify sync.Once declarations and Do method invocations. The test suite validates these rules in internal/guidelines/guidelines_test.go, which includes related concurrency checks such as the sync_waitgroup_go rule referenced at line 15. This framework ensures that patterns involving WaitGroup and Once primitives are applied consistently across inspected codebases.
Common Anti-Patterns Detected
The linter identifies specific incorrect implementations that violate Go 1.21+ concurrency guidelines:
- Discarding return values: Attempting to return a value from the function passed to
once.Do, which the method silently ignores. - Pre-initialization access: Reading a package-level variable before
once.Docompletes, creating a potential race condition. - Value copying: Reassigning or passing
sync.Oncestructs by value, which breaks the internal synchronization state.
Correct Once-Func Implementation
package config
import (
"sync"
)
var (
once sync.Once
config Config
)
func Load() Config {
once.Do(func() {
// Expensive initialization performed only once.
config = readConfigFromDisk()
})
return config
}
Correct Once-Value Implementation
package logger
import (
"log"
"os"
"sync"
)
var (
once sync.Once
logger *log.Logger
)
func Logger() *log.Logger {
once.Do(func() {
// Initialise the singleton logger.
logger = log.New(os.Stdout, "APP: ", log.LstdFlags)
})
return logger
}
Incorrect Patterns
Returning values from Do (discarded result):
// This pattern fails because sync.Once.Do discards the returned value.
once.Do(func() *Config {
return readConfigFromDisk() // Result is ignored; config remains nil
})
Reading before initialization:
// Race condition: use() may execute before once.Do assigns cachedValue.
var cachedValue string
once.Do(func() {
cachedValue = computeExpensiveValue()
})
use(cachedValue) // May see zero value if called concurrently
Summary
- The go-modern-guidelines tool validates
sync.Onceusage patterns ininternal/guidelines/guidelines.go, distinguishing between once-func side effects and once-value singletons. - Once-func patterns must assign results to outer variables rather than returning them from the
Doclosure, as return values are discarded. - Once-value implementations require reading the cached result strictly after
once.Docompletes to prevent data races. - The linter detects copied
sync.Oncevariables and redundant mutexes, ensuring exactly-once semantics remain intact.
Frequently Asked Questions
What is the difference between the once-func and once-value patterns checked by the tool?
The once-func pattern wraps side-effect initialization code (like setting up connections or loading files), while the once-value pattern captures and reuses a computed result (like a singleton logger or configuration object). According to the JetBrains/go-modern-guidelines source code, the linter applies different validation rules to each: it checks for discarded return values in once-func patterns and pre-initialization reads in once-value patterns.
Why does the linter flag function return values inside once.Do?
The sync.Once.Do method signature accepts func() and explicitly discards any return values from the invoked function. The linter flags attempts to return values directly because these results are silently dropped, causing the initialization to fail. The correct approach assigns the value to a variable declared outside the closure, as shown in the internal/guidelines implementation examples.
Where are these concurrency checks implemented in the repository?
The validation logic resides in internal/guidelines/guidelines.go, with comprehensive test coverage in internal/guidelines/guidelines_test.go. Line 15 of the test file references the sync_waitgroup_go rule, demonstrating the framework's unified approach to validating sync package primitives including Once patterns.
Does the tool detect if a sync.Once variable is copied?
Yes, the linter verifies that sync.Once variables are not reassigned or passed by value after creation. Copying a sync.Once struct breaks its internal synchronization state, potentially allowing the wrapped function to execute multiple times. The tool flags these copies to maintain the exactly-once guarantee required by sync.OnceFunc and sync.OnceValue semantics.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →