Every component in go-swarm-icons is safe for concurrent use. This page documents the specific synchronization mechanism protecting each type.
Concurrency guarantee
All public types in the library are safe for concurrent use from multiple goroutines. No external synchronization is needed. This guarantee is achieved through a combination of sync.RWMutex, sync.Once, and structural immutability.
Concurrency model
| Component | Mechanism | Protected state |
|---|---|---|
IconManager |
sync.RWMutex |
providers map, aliases map, default prefix, fallback config, renderer reference |
DirectoryProvider |
iconCache (sync.RWMutex) |
resolved icon cache |
JsonCollectionProvider |
sync.Once + iconCache (sync.RWMutex) |
lazy JSON parse + resolved icon cache |
IconifyProvider |
iconCache (sync.RWMutex) |
HTTP response cache |
SpriteCollector |
sync.RWMutex |
symbols map |
Icon |
Immutability | No mutable state |
The iconCache type
Three of the four providers (DirectoryProvider, JsonCollectionProvider, IconifyProvider) share the same caching primitive: iconCache (defined in cache.go). It wraps a sync.RWMutex and a map[string]*Icon.
The cache uses a double-checked locking pattern in GetOrLoad:
1. RLock2. Check map for name3. If found: RUnlock, return cached icon4. RUnlock5. Call the loader function (no lock held)6. If loader returns (nil, false): return not found7. Lock (write)8. Check map again (another goroutine may have stored it)9. If found: Unlock, return the other goroutine's result10. Store the new icon11. Unlock, return the new iconThis pattern has two important properties:
The read path is fast: Under normal load (cache populated), every Get call acquires only a read lock, checks the map, and returns. Read locks do not contend with each other, so concurrent reads scale linearly with goroutine count.
The loader runs without holding a lock: Step 5 calls the provider-specific loading function (disk I/O, JSON resolution, or HTTP request) without any lock held. This prevents slow I/O from blocking other goroutines that are reading cached icons. The trade-off is that two goroutines racing on the same uncached name both run the loader, and the first to acquire the write lock wins. The second goroutine's result is discarded. This is acceptable because loader results are deterministic and the race is rare.
IconManager locking
The manager's Get method minimizes lock hold time by copying all configuration values into local variables under a single read lock, then releasing the lock before proceeding with resolution:
1. RLock2. Copy: defaultPrefix, providers, fallbackIcon, prefixFallbacks, ignoreNotFound, renderer3. RUnlock4. Proceed with resolution using local copies (no lock held)This means that configuration changes (via SetDefaultPrefix, SetAlias, Register, etc.) made by another goroutine during resolution do not affect the in-progress Get call. The call sees a consistent snapshot of the configuration as of step 2.
Mutation methods (Register, SetAlias, SetDefaultPrefix, SetFallbackIcon, SetFallbackIconForPrefix, SetIgnoreNotFound, SetRenderer) acquire a write lock for the duration of the map or field write. These operations are fast (map insertion or field assignment) and do not perform I/O.
JsonCollectionProvider lazy parse
The JsonCollectionProvider uses sync.Once to ensure the JSON collection is parsed exactly once, regardless of how many goroutines call Get, Has, or All concurrently. The first goroutine to call load() runs json.Unmarshal; all other goroutines block on the Once until parsing completes, then proceed with the parsed data.
After the Once completes, the parsed data (*jsonCollection) is read-only. Alias resolution and icon construction happen per-call and store results in the iconCache, which has its own RWMutex.
Icon immutability
The Icon type has no exported fields and no mutating methods. Every fluent method (Size, Class, Fill, Rotate, etc.) calls clone(), which creates a new *Icon with a deep-copied attribute map. The original icon is never modified.
This means:
- Provider caches store
*Iconvalues that are never mutated by consumer code. - Multiple goroutines can call
Getfor the same icon name, apply different transformations, and produce independent results without coordination. - No defensive copying is needed when passing
*Iconvalues between functions or goroutines.
The Attributes() method returns a deep copy of the attribute map, so callers cannot mutate the icon's state through the returned map.
SpriteCollector locking
Register acquires a write lock to insert into the symbols map. SpriteSheet acquires a read lock to iterate the map. Reset acquires a write lock to replace the map. All three operations are safe to call from multiple goroutines.
See also
- Performance & Caching: practical caching guidance and provider selection for throughput
- Architecture: how these synchronization mechanisms compose in the resolution flow