ctxmap

package module
v1.0.2 Latest Latest
Warning

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

Go to latest
Published: Nov 17, 2025 License: MIT Imports: 5 Imported by: 1

README

GoDoc GoMod Size License Go Go Report Card codecov

ctxmap

A thread-safe Go key-value map for contextual data. Features an immutable-style API and is heavily optimized for repeated string serialization via caching. Ideal for structured logging, request contexts, and dynamic configuration.

It is perfect for use in:

  • Structured Logging: A base logger context is created at startup (service, version). Each HTTP request gets a child context with WithPairs (request_id, user_id). The final String() call is fast.
  • Configuration Management: A global configuration can be held in a CtxMap. Sub-components can create prefixed, immutable views (dbConfig := globalConfig.WithPrefix("database")). This prevents components from accidentally modifying shared configuration.
  • Distributed Tracing Metadata: Storing and propagating span tags or baggage items. As a request moves through services, new tags can be added immutably.
  • HTTP Middleware Context: A CtxMap can be created per request and passed via context.Context. Middleware can add information (e.g., authentication data, rate-limit status) in a thread-safe way, using prefixes to avoid key collisions.
  • Feature Flag/Experimentation Context: Store attributes about a user or request (user_id, region, plan_type). This context can be passed to a feature flag engine to make decisions. The String() method is useful for debugging which flags were evaluated.
  • Building Dynamic Queries: Assembling parameters for an API call or database query where different parts of the application contribute parameters. Prefixes can be used to group parameters (e.g., filter, sort).

✨ Features

  • High-Performance Serialization: A granular caching system dramatically speeds up repeated calls to the String() method. When only a few fields change between calls, only those fields are re-formatted, making it ideal for high-throughput structured logging.
  • Thread-Safe by Design: All operations are safe for concurrent use. It uses sync.RWMutex to allow for highly concurrent reads.
  • Immutable-Style API: Methods like WithPairs, WithPrefix, and Merge return new, independent CtxMap instances. This allows you to share a base context across goroutines while derived contexts can be modified without causing data races or requiring locks.
  • Contextual Prefixes: Easily namespace keys to avoid collisions when merging contexts from different application layers (e.g., http.auth.user_id vs. db.query.user_id).
  • Fully Customizable Output: You have full control over the output format. You can specify the separator for key prefixes (., :, etc.), the separator between key-value pairs ( , |, etc.), and even provide a custom function to format each pair.
  • Zero Dependencies: A lightweight package that relies only on the Go standard library.

📌 Installation

go get github.com/balinomad/go-ctxmap@latest

🚀 Usage

Here's a basic example demonstrating the core concepts of creating a base context and deriving a new one for a specific task.

package main

import (
	"fmt"
	"log"

	"github.com/balinomad/go-ctxmap"
)

func main() {
	// 1. Create a base context for your application.
	// We'll use "." for key prefixes and " " to separate fields.
	// A nil stringer function uses the default "key=value" format.
	baseCtx := ctxmap.NewCtxMap(".", " ", nil)
	baseCtx.Set("service", "user-api")
	baseCtx.Set("version", "1.2.3")

	// 2. In a request handler, create a new map with request-specific data.
	// WithPairs is immutable; it returns a new map without modifying baseCtx.
	requestCtx := baseCtx.WithPairs(
		"request_id", "abc-123",
		"user_id", 42,
	)

	// 3. The String() method is called automatically by fmt functions.
	// It's heavily optimized and very fast on subsequent calls.
	log.Printf("handled request: %s", requestCtx)
	// Example output (order of keys is not guaranteed):
	// 2025/08/24 13:18:22 handled request: service=user-api version=1.2.3 request_id=abc-123 user_id=42
}

📘 API Reference

Constructor Functions
Function Description
NewCtxMap(keySegmentSeparator string, fieldSeparator string, stringer func(k string, v any) string) Creates a new CtxMap with custom separators and formatting.
Methods
Method Description
Get(key string) (any, bool) Retrieves a value by its raw key (prefix is not used).
GetPrefixed(key string) (any, bool) Retrieves a value by its fully prefixed key (e.g., "prefix.key").
Set(key string, value any) Sets a key-value pair. Safe for concurrent use.
SetMultiple(keyValues map[string]any) Sets multiple key-value pairs efficiently. Safe for concurrent use.
Delete(key string) Removes a key from the map.
DeletePrefixed(prefix string) Removes keys using the current prefix.
Clear() Removes all keys from the map.
ReplaceAll(keyValues map[string]any) Replaces all key-value pairs with the given map.
WithPairs(keyValues ...any) (Immutable) Returns a new map with additional key-value pairs.
WithPrefix(prefix string) (Immutable) Returns a new map with a key prefix added.
Merge(other *CtxMap) (Immutable) Returns a new map combining the receiver and another CtxMap.
Clone() *CtxMap (Immutable) Returns a deep copy of the map.
AsMap() map[string]any Returns the map's data as a map[string]any. Warning: May return the internal map for performance; do not modify.
ToMapCopy() map[string]any Returns a safe copy of the map's data.
ToSliceCopy() []any Returns a safe copy of the map's data as a slice.
Len() int Returns the number of items in the map.
Range(fn func(k, v)) Iterates over a snapshot of the map, applying prefixes to keys.
String() string Returns a cached, string representation of the map. Very fast on repeated calls.

🔧 Advanced Usage

Structured Logging Context

CtxMap is ideal for building up a structured logging context as a request flows through your application. Prefixes help organize data from different layers (middleware, services, database), and the final String() call is efficient.

package main

import (
	"log"
	"os"
	"time"

	"github.com/balinomad/go-ctxmap"
)

// Simulates a middleware that processes a request.
func handleRequest(baseCtx *ctxmap.CtxMap, requestID int) {
	// 1. Create a request-specific context. This is cheap and doesn't lock the baseCtx.
	requestCtx := baseCtx.WithPairs(
		"request_id", requestID,
		"user_id", 12345,
	)
	log.Printf("[Request Start] %s", requestCtx)

	// 2. Perform a sub-operation, like a database call, with a prefixed context.
	// This helps organize keys and avoids collisions.
	dbCtx := requestCtx.WithPrefix("db")
	performDatabaseQuery(dbCtx)

	// 3. The original request context is unchanged by the prefixed operations.
	// We can add final timing information before logging.
	log.Printf("[Request End] %s", requestCtx.WithPairs("duration_ms", 50))
}

// Simulates a database operation that adds its own context.
func performDatabaseQuery(ctx *ctxmap.CtxMap) {
	// Add query-specific details.
	queryCtx := ctx.WithPairs(
		"query_hash", "a1b2c3d4",
		"table", "users",
	)
	time.Sleep(50 * time.Millisecond) // Simulate work
	log.Printf("[DB Query] %s", queryCtx)
}

func main() {
	// Create a base logger context at startup with static application info.
	appContext := ctxmap.NewCtxMap(".", " ", nil)
	appContext.Set("service", "worker-pool")
	appContext.Set("version", "1.0.1")
	appContext.Set("pid", os.Getpid())

	// Simulate handling multiple "requests" or jobs.
	for i := 1; i <= 3; i++ {
		handleRequest(appContext, i)
		time.Sleep(100 * time.Millisecond)
	}
}

⚡ Performance & Caching

The primary performance goal of go-ctxmap is to make the String() operation extremely fast, especially when a map is serialized repeatedly with minor changes.

This is achieved through a multi-level caching strategy:

  • Full String Caching: If String() is called and no data has changed since the last call, the previously computed string is returned instantly without any new allocations or computations.
  • Granular Field Caching: When a value is set via Set() or added via WithPairs(), the map marks only the affected keys as "dirty." When String() is called next:
    • The formatted strings for "clean" (unchanged) keys are retrieved from an internal cache.
    • Only the "dirty" keys are re-formatted.
    • The final string is built by joining the cached and newly formatted parts.

This means that if you have a context with 20 fields and you only change one, the cost of the next String() call is close to formatting a single field, not all 20.

🤝 Concurrency Model

CtxMap is designed for high-concurrency environments and guarantees safety through two primary mechanisms:

  • Internal Locking: All methods that modify the map's internal state (like Set, Delete, Clear) use a sync.RWMutex to ensure that writes are serialized and that reads occurring during a write are not subject to race conditions. Reads (Get, Len, String) use a read lock, allowing multiple goroutines to read from the same map concurrently.
  • Immutability: The methods WithPairs, WithPrefix, Merge, and Clone do not modify the original map. Instead, they return a new CtxMap instance with its own data and locks. This is a powerful pattern for concurrency: you can safely pass a CtxMap to multiple goroutines, and if they need to add context, they can create their own "local" version without ever needing to lock the original. This significantly reduces lock contention in highly parallel workflows.

⚖️ License

MIT License — see LICENSE file for details.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type CtxMap

type CtxMap struct {
	// contains filtered or unexported fields
}

CtxMap is a thread-safe map for contextual data, optimized for repeated string serialization, such as in structured logging or configuration debugging.

func NewCtxMap

func NewCtxMap(keySegmentSeparator string, fieldSeparator string, stringer func(k string, v any) string) *CtxMap

NewCtxMap creates a new CtxMap.

keySegmentSeparator is used to join prefixes (e.g., "."). fieldSeparator is used to join key-value pairs in the String() output (e.g., " "). stringer formats a single key-value pair. A default "key=value" formatter is used if nil.

func (*CtxMap) AsMap

func (m *CtxMap) AsMap() map[string]any

AsMap returns the current snapshot of the map, applying the prefix to keys.

IMPORTANT: For performance, this may return the internal map if no prefix is set. The returned map MUST NOT be modified. Use ToMapCopy() if you need a mutable copy.

func (*CtxMap) Clear

func (m *CtxMap) Clear()

Clear removes all key-value pairs from the map.

func (*CtxMap) Clone

func (m *CtxMap) Clone() *CtxMap

Clone returns a deep copy of the map.

func (*CtxMap) Delete

func (m *CtxMap) Delete(key string)

Delete removes a key. The prefix is not applied.

func (*CtxMap) DeletePrefixed

func (m *CtxMap) DeletePrefixed(key string)

DeletePrefixed removes a key using the current prefix.

func (*CtxMap) Get

func (m *CtxMap) Get(key string) (any, bool)

Get retrieves the value associated with the given raw key and a boolean indicating whether the key was found in the map. The prefix is not applied.

func (*CtxMap) GetPrefixed

func (m *CtxMap) GetPrefixed(key string) (any, bool)

GetPrefixed retrieves the value by a key that includes the map's prefix. It also returns a boolean indicating whether the key was found in the map.

func (*CtxMap) Len

func (m *CtxMap) Len() int

Len returns the number of key-value pairs.

func (*CtxMap) Merge

func (m *CtxMap) Merge(other *CtxMap) *CtxMap

Merge returns a new map containing pairs from both maps. For duplicate keys, values from the 'other' map take precedence. All other properties of the receiver are preserved.

func (*CtxMap) Range

func (m *CtxMap) Range(fn func(k string, v any))

Range calls fn for each key/value pair of the current snapshot.

func (*CtxMap) ReplaceAll

func (m *CtxMap) ReplaceAll(keyValues ...any)

ReplaceAll replaces all key-value pairs in the map. Incomplete pairs are skipped.

func (*CtxMap) Set

func (m *CtxMap) Set(key string, value any)

Set sets key to value.

func (*CtxMap) SetMultiple

func (m *CtxMap) SetMultiple(pairs map[string]any)

SetMultiple sets multiple key-value pairs. This is more efficient than multiple Set() calls when setting many fields at once.

func (*CtxMap) String

func (m *CtxMap) String() string

String returns a string representation of the map, optimized with caching. This method is the primary performance focus of the package.

func (*CtxMap) ToMapCopy

func (m *CtxMap) ToMapCopy() map[string]any

ToMapCopy returns a new map containing a snapshot of the data with prefixes applied. The returned map is safe for modification.

func (*CtxMap) ToSliceCopy

func (m *CtxMap) ToSliceCopy() []any

ToSliceCopy returns a new slice of alternating key-value pairs.

func (*CtxMap) WithPairs

func (m *CtxMap) WithPairs(keyValues ...any) *CtxMap

WithPairs returns a new map with the given alternating key-value pairs merged. New pairs overwrite existing ones. Incomplete pairs are skipped. The original map is not modified.

func (*CtxMap) WithPrefix

func (m *CtxMap) WithPrefix(prefix string) *CtxMap

WithPrefix returns a new CtxMap with an added prefix. The original map is not modified.

Jump to

Keyboard shortcuts

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