dlg provides a minimal API for printf-style debugging - a lightweight logger that completely vanishes from production builds while providing rich debugging capabilities during development.
When built without the dlg tag, all logging calls disappear entirely from your binary, resulting in no runtime overhead.
- 🚀 True zero-cost abstraction - Logging calls completely disappear from production binaries
- ⚡️ Near-zero overhead - Performance-focused design for debug builds
- 🔍 Smart stack traces - Runtime-configurable stack trace generation
- 🔒 Concurrent-safe by design - Custom writers simply implement
sync.Lockerto be safe - ✨ Minimalist API - Exposes just two functions,
PrintfandSetOutput
When compiled without the dlg build tag:
- All calls to
dlgcompile to empty functions - Go linker completely eliminates these no-ops
- Final binary contains no trace of logging code
- Zero memory overhead, zero CPU impact
go get github.com/vvvvv/dlgpackage main
import (
"fmt"
"github.com/vvvvv/dlg"
)
func risky() error {
return fmt.Errorf("unexpected error")
}
func main() {
fmt.Println("starting...")
dlg.Printf("executing risky operation")
err := risky()
if err != nil {
dlg.Printf("something failed: %v", err)
}
dlg.Printf("continuing")
}Enable debug features with the dlg build tag:
# Production build (no logging)
go build -o app
# Debug build
go build -tags dlg -o app-debug./app-debug
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * * * * * * * * * * * * DEBUG BUILD * * * * * * * * * * * * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- DLG_STACKTRACE=ERROR show stack traces on errors
- DLG_STACKTRACE=ALWAYS show stack traces always
- DLG_NO_WARN=1 disable this message (use at your own risk)
starting...
01:28:27 [2µs] main.go:16: executing risky operation
01:28:27 [21µs] main.go:19: something failed: unexpected error
01:28:27 [23µs] main.go:22: continuing
DLG_STACKTRACE=ERROR ./app-debug
# [Debug Banner omitted]
starting...
01:31:34 [2µs] main.go:16: executing risky operation
01:31:34 [21µs] main.go:19: something failed: unexpected error
goroutine 1 [running]:
github.com/vvvvv/dlg.writeStack(0x14000104ec0)
/Users/v/src/go/src/github.com/vvvvv/dlg/printf.go:86 +0x84
github.com/vvvvv/dlg.Printf({0x1002d25f4, 0x14}, {0x14000104f18, 0x1, 0x1})
/Users/v/src/go/src/github.com/vvvvv/dlg/printf.go:50 +0x170
main.main()
/Users/v/src/go/src/github.com/vvvvv/dlg/examples/example01/main.go:19 +0xc0
01:31:34 [38µs] main.go:22: continuing# Runtime configuration
DLG_STACKTRACE=ERROR ./app-debug # Traces on errors only
DLG_STACKTRACE=ALWAYS ./app-debug # Traces on every call
# Compile-time configuration
go build -tags dlg -ldflags "-X 'github.com/vvvvv/dlg.DLG_STACKTRACE=ERROR'"DLG_NO_WARN=1 ./app-debugThe debug banner cannot be disabled via linker flags. This prevents accidental deployment of debug builds to production.
While dlg.Printf is safe for concurrent use, custom writers should implement sync.Locker.
package main
import (
"bytes"
"fmt"
"sync"
"github.com/vvvvv/dlg"
)
type SafeBuffer struct {
bytes.Buffer
sync.Mutex
}
func main() {
sb := &SafeBuffer{}
dlg.SetOutput(sb) // Now fully concurrency-safe!
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for n := 0; n < 5; n++ {
dlg.Printf("from goroutine #%v: message %v", i, n)
}
}()
}
wg.Wait()
fmt.Print(sb.Buffer.String())
}The term "zero-cost" isn't just a claim - it's a verifiable compiler behavior. When dlg is disabled, the Go toolchain performs complete dead code elimination.
Consider this simple program:
package main
import (
"fmt"
"github.com/vvvvv/dlg"
)
func main() {
fmt.Println("hello world")
dlg.Printf("hello from dlg")
}
When built *without* the `dlg` tag:go build -o production_binaryThe resulting disassembly (via go tool objdump -s main.main production_binary) shows:
... [function prologue] ...
main.go:10 0x10009c6a8 b00001a5 ADRP 217088(PC), R5
main.go:10 0x10009c6ac 913480a5 ADD $3360, R5, R5
main.go:10 0x10009c6b0 f9001fe5 MOVD R5, 56(RSP)
main.go:10 0x10009c6b4 f0000265 ADRP 323584(PC), R5
main.go:10 0x10009c6b8 9135a0a5 ADD $3432, R5, R5
main.go:10 0x10009c6bc f90023e5 MOVD R5, 64(RSP)
print.go:314 0x10009c6c0 b00006db ADRP 888832(PC), R27
print.go:314 0x10009c6c4 f9479761 MOVD 3880(R27), R1
print.go:314 0x10009c6c8 90000280 ADRP 327680(PC), R0
print.go:314 0x10009c6cc 910c6000 ADD $792, R0, R0
print.go:314 0x10009c6d0 9100e3e2 ADD $56, RSP, R2
print.go:314 0x10009c6d4 b24003e3 ORR $1, ZR, R3
print.go:314 0x10009c6d8 aa0303e4 MOVD R3, R4
print.go:314 0x10009c6dc 97ffecf5 CALL fmt.Fprintln(SB) ; Only this call (fmt.Println) remains
main.go:12 0x10009c6e0 f85f83fd MOVD -8(RSP), R29
main.go:12 0x10009c6e4 f84507fe MOVD.P 80(RSP), R30
main.go:12 0x10009c6e8 d65f03c0 RET
main.go:9 0x10009c6ec aa1e03e3 MOVD R30, R3
main.go:9 0x10009c6f0 97ff3bbc CALL runtime.morestack_noctxt.abi0(SB)
main.go:9 0x10009c6f4 17ffffe7 JMP main.main(SB)
main.go:9 0x10009c6f8 00000000 ?The compiler eliminates dlg as if it was never imported.
However, the Go compiler can only eliminate dlg.Printf calls if it can prove that the arguments themselves have no side effects and are not used elsewhere.
This has two important implications:
- Referenced variables stay: If a variable is used outside the
dlg.Printfcall, the computation remains - but the call itself is still removed. - Function calls are evaluated: Even if
dlg.Printfis eliminated, any argument expressions with potential side effects (e.g. function calls) are still evaluated.
Let's look at practical examples.
✅ Referenced Variables - Printf Eliminated, Variable Remains
res := 69 * 42 // Used later -> remains
dlg.Printf("res: %v", res) // Eliminated
fmt.Println("result: ", res); res remains (used by fmt.Println)
0x10009c6c8 d2816a40 MOVD $2898, R0 ; 69*42=2898 stored
...
; fmt.Println remains
0x10009c6fc 97ffeced CALL fmt.Fprintln(SB)✅ Unused Expressions - Fully Eliminated
res := 69 * 42 // Eliminated
dlg.Printf("res: %v", res) // Eliminated
dlg.Printf("calc: %v", 69 * 42) // Eliminated; Entire function reduced to a single return:
0x100067b60 d65f03c0 RET// The call to fmt.Errorf is evaluated but the call to dlg.Printf is still eliminated
dlg.Printf("call to fn: %v", fmt.Errorf("some error")); fmt.Errorf is still executed
0x10009f21c 913ec000 ADD $4016, R0, R0
0x10009f230 97ffd8e0 CALL fmt.Errorf(SB)Avoid placing function calls or expensive computations directly inside dlg.Printf.
As long as you follow this principle, dlg maintains its promise:
No instructions.
No references.
Zero memory allocations.
Zero CPU cycles used.
identical binary size to code without dlg.
True zero-cost.
[^1]: There's a bit more nuance to this - if a function is side-effect free and returns a basic type (e.g., int, string), the compiler may still eliminate the function call.