go
66 lines · 8 steps
Deduplicating log attributes in Go's slog
A custom slog.Handler wrapper that collapses duplicate attribute keys so later values win before logs are emitted.
Explained by
highlit
1package logging
2
3import (
4 "context"
5 "log/slog"
6)
7
8type DedupeHandler struct {
9 inner slog.Handler
10 attrs []slog.Attr
11}
12
13func NewDedupeHandler(inner slog.Handler) *DedupeHandler {
14 return &DedupeHandler{inner: inner}
15}
16
17func (h *DedupeHandler) Enabled(ctx context.Context, level slog.Level) bool {
18 return h.inner.Enabled(ctx, level)
19}
20
21func (h *DedupeHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
22 return &DedupeHandler{inner: h.inner, attrs: mergeAttrs(h.attrs, attrs)}
23}
24
25func (h *DedupeHandler) WithGroup(name string) slog.Handler {
26 return &DedupeHandler{inner: h.inner.WithGroup(name), attrs: h.attrs}
27}
28
29func (h *DedupeHandler) Handle(ctx context.Context, r slog.Record) error {
30 recordAttrs := make([]slog.Attr, 0, r.NumAttrs())
31 r.Attrs(func(a slog.Attr) bool {
32 recordAttrs = append(recordAttrs, a)
33 return true
34 })
35
36 final := mergeAttrs(h.attrs, recordAttrs)
37
38 clean := slog.NewRecord(r.Time, r.Level, r.Message, r.PC)
39 clean.AddAttrs(final...)
40 return h.inner.Handle(ctx, clean)
41}
42
43func mergeAttrs(base, incoming []slog.Attr) []slog.Attr {
44 index := make(map[string]int, len(base)+len(incoming))
45 merged := make([]slog.Attr, 0, len(base)+len(incoming))
46
47 upsert := func(a slog.Attr) {
48 if a.Equal(slog.Attr{}) {
49 return
50 }
51 if i, ok := index[a.Key]; ok {
52 merged[i] = a
53 return
54 }
55 index[a.Key] = len(merged)
56 merged = append(merged, a)
57 }
58
59 for _, a := range base {
60 upsert(a)
61 }
62 for _, a := range incoming {
63 upsert(a)
64 }
65 return merged
66}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Wrapping an existing interface lets you inject behavior without changing callers or the underlying implementation.
- 2Returning fresh copies from WithAttrs and WithGroup keeps handlers safe to share across goroutines.
- 3A key-to-index map turns last-write-wins deduplication into a single linear pass while preserving order.
Related explainers
go
package streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
go
package middleware import ( "net/http"
Per-plan export limits in Gin middleware
middleware
rate-limiting
authorization
Intermediate
7 steps
go
package httputil import ( "net"
Safely extracting the real client IP in Go
security
http
ip-spoofing
Intermediate
7 steps
go
package signup var wizardCodec = securecookie.New( []byte(os.Getenv("WIZARD_HASH_KEY")),
A cookie-backed signup wizard in Gin
cookies
encryption
multi-step-form
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/deduplicating-log-attributes-in-go-s-slog-explained-go-54f4/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.