From a Log Key Mistake to Safer Go slog: Lessons and Practical Tricks

After a production incident caused by a wrong log field name, the author explores Go's standard slog library, reveals its classic ...any trap, and presents four concrete techniques—dependency‑injected logger, type‑safe LogAttrs, centralized field helpers, and sloglint enforcement—to build a reliable, maintainable logging system.

Golang Shines
Golang Shines
Golang Shines
From a Log Key Mistake to Safer Go slog: Lessons and Practical Tricks

"Unreviewed logs are not worth output" – a colleague warned the author when a recent deployment produced logs with the field order_id mistakenly replaced by !BADKEY. The mistake traced back to a missing argument in a call to slog.Info, illustrating how slog’s ...any variadic API lets key‑value mismatches slip past the compiler and surface only at runtime.

Why I briefly left zap for slog

Before Go 1.21 the author was a zap enthusiast for its performance and ecosystem. The introduction of the standard slog package sparked a reconsideration because it removes an external dependency and avoids worries about future maintenance.

Four practical tricks to make slog type‑safe and maintainable

1. Treat the logger as an injected object, not a global

Many tutorials call slog.Info(), which uses a mutable global logger. This leads to test interference and configuration clashes. Instead, define a *slog.Logger field in structs and pass it via constructors, e.g.:

type OrderService struct {
    logger *slog.Logger // explicit dependency
}

func NewOrderService(logger *slog.Logger) *OrderService {
    return &OrderService{logger: logger}
}

Initialize the logger once in main and inject a test logger during unit tests. This pattern eliminates most "why my test logs are missing" issues.

2. Use LogAttrs for compile‑time type safety

The convenience methods Info, Warn still accept ...any, relying on developer discipline. LogAttrs requires explicit attribute constructors such as slog.String or slog.Int64, causing a compile error if a wrong type is supplied.

logger.LogAttrs(ctx, slog.LevelInfo, "order placed",
    slog.String("order_id", id),
    slog.Int64("amount_cents", amount),
)

The author argues that a compile‑time failure is far cheaper than spending hours debugging malformed JSON logs in production.

3. Centralize field definitions in a helper package

Repeatedly writing slog.String("order_id", id) is error‑prone when field names change. The solution is a small internal package that defines helpers returning ready‑made slog.Attr values:

package log

import "log/slog"

func OrderID(id string) slog.Attr {
    return slog.String("order_id", id) // single source of truth
}

func AmountCents(c int64) slog.Attr {
    return slog.Int64("amount_cents", c)
}

func Err(e error) slog.Attr {
    if e == nil {
        return slog.Attr{}
    }
    return slog.String("err", e.Error())
}

Calls become concise and renaming a field only requires editing attrs.go, which propagates site‑wide.

4. Let sloglint enforce the conventions

To prevent accidental re‑introduction of the ...any style or global functions, the author configures the sloglint plugin for golangci‑lint:

linters-settings:
  sloglint:
    attr-only: true          # forbid kv style, require LogAttrs
    no-global: "all"        # forbid slog.Info, etc.
    context: "all"          # require a context argument
    static-msg: true        # log message must be a literal string
    key-naming-case: snake   # enforce snake_case keys

With this configuration, a CI run rejects code like Info("msg", "key", val) before it reaches review.

Benefits and limits

Rename safety : changing a key in attrs.go updates every log instantly.

Type protection : passing an int where int64 is required fails to compile.

Privacy control : helper functions can mask or redact sensitive fields centrally.

Performance : LogAttrs avoids the reflection and type‑assertion overhead of the generic any path, which adds up in high‑frequency logging.

However, type safety does not guarantee semantic correctness; a function like AmountCents(int64) still cannot prevent passing cents when the business expects dollars. The author’s principle is to let the type system block syntax errors and devote human effort to semantic validation.

Conclusion

Although slog may not be the fastest logger, its standard‑library status and the ability to combine LogAttrs, dependency injection, centralized helpers, and lint enforcement provide a robust, type‑safe logging foundation for production services.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

goLoggingdependency injectiontype safetylintslog
Golang Shines
Written by

Golang Shines

We share daily the latest Golang technical articles, practical resources, language news, tutorials, and real-world projects to help everyone learn and improve.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.