A zero-overhead debugging utility for Go inspired by Rust's dbg! macro.
  • Go 88.6%
  • Makefile 11.4%
Find a file
2025-12-06 21:09:50 +11:00
.gitignore Tweak gitignore 2025-12-06 21:09:50 +11:00
common.go Update timestamp handling in debug formats 2025-11-27 23:36:58 +11:00
de.go Add output handling via configurable io.Writer 2025-12-06 18:28:15 +11:00
de_debug.go Add output handling via configurable io.Writer 2025-12-06 18:28:15 +11:00
de_release.go Add output handling via configurable io.Writer 2025-12-06 18:28:15 +11:00
de_test.go Add output handling via configurable io.Writer 2025-12-06 18:28:15 +11:00
doc.go Update module import path 2025-11-19 09:24:45 +11:00
go.mod Update Go version in go.mod to 1.19 2025-12-01 11:47:26 +11:00
LICENSE Add license 2025-11-19 16:25:24 +11:00
Makefile Add output handling via configurable io.Writer 2025-12-06 18:28:15 +11:00
README.md Add output handling via configurable io.Writer 2025-12-06 18:28:15 +11:00

de

A zero-overhead debugging utility for Go inspired by Rust's dbg! macro.

Features

  • Support for any type
  • Inline usage with value passthrough
  • File location tracking (debug builds only)
  • Configurable output destination and format
  • Zero runtime overhead in release builds
  • No external dependencies

Installation

go get codeberg.org/zerodeps/de

Usage example

package main

import (
    "fmt"
    "codeberg.org/zerodeps/de"
)

func main() {
    x := 42

    // Use inline - returns the value unchanged
    result := de.Bug(x) + 10

    // Works with any type
    name := de.Bug("Alice") + " Smith"

    // Complex expressions
    sum := de.Bug(x*2) + de.Bug(x*3)

    fmt.Println(result, name, sum)
}

Testing mode output (default)

go build
./yourapp
52 Alice Smith 210

No debug output, and Bug() calls are inlined away completely by Go's compiler.

Debug mode output

go build -tags debug
./yourapp
[DEBUG] (main.go:11) 42
[DEBUG] (main.go:14) "Alice"
[DEBUG] (main.go:17) 84
[DEBUG] (main.go:17) 126
52 Alice Smith 210

Pointer receivers and method chaining

Bug() returns a copy of the value, which means chaining method calls with pointer receivers won't modify the original variable:

type Counter struct {
    Value int
}

func (c *Counter) Inc() {
    c.Value++
}

x := Counter{Value: 3}
temp := de.Bug(x)
temp.Inc()           // modifies the copy, not x
fmt.Println(x.Value) // still 3

Solution 1: Debug before modifying

Call Bug() on the variable before applying modifications:

x := Counter{Value: 3}
de.Bug(x)            // inspect the value
x.Inc()              // then modify
fmt.Println(x.Value) // now 4

Solution 2: Use BugPtr

For method chaining, use BugPtr() which works with pointers:

x := Counter{Value: 3}
de.BugPtr(&x).Inc()  // inspects and modifies x
fmt.Println(x.Value) // now 4

Trade-off: BugPtr() requires explicit pointers and dereferencing, making it less convenient for inline value inspection:

// Bug: clean inline usage
result := de.Bug(42) + 10

// BugPtr: requires temporary variable
num := 42
result := *de.BugPtr(&num) + 10

Choose Bug() for inspecting values and expressions, BugPtr() when you need to chain pointer receiver methods.

Build modes

The library has three distinct modes of operation:

Mode Build command Behaviour Output Overhead Use case
Testing (default) go build
go test
./yourapp
Functions are no-ops, returning values unchanged No debug output

Example:
52 Alice Smith 210
Zero runtime overhead (functions are inlined) Development, testing, and builds where you haven't removed debug calls yet
Debug
-tags=debug
go build -tags debug
go test -tags debug
./yourapp
Prints debug output with file locations and values Detailed debugging information

Example:
[DEBUG] (main.go:11) 42 15:04:05.123
[DEBUG] (main.go:14) "Alice" 15:04:05.124
[DEBUG] (main.go:17) 84 15:04:05.125
[DEBUG] (main.go:17) 126 15:04:05.126
52 Alice Smith 210
~200-500ns per call plus I/O costs Active debugging and development
Release
-tags=release
go build -tags release Compilation fails if any de.Bug() or de.BugPtr() calls remain Compilation error with clear message

Example:
de_release.go:10:8: package DE_REMOVE_CALLS_FOR_RELEASE is not in std
N/A (won't compile) Production/release builds that enforce removal of debug code

This ensures debug utilities cannot accidentally ship in production binaries.

Customisation

The debug output format can be customised at runtime using SetFormat() with a Format struct:

import "codeberg.org/zerodeps/de"

func init() {
    // Set debug format only
    de.SetFormat(de.Format{
        Debug: "[%[1]s:%[2]d] %[3]v\n",
    })

    // Set both debug format and timestamp format
    de.SetFormat(de.Format{
        Debug:     "[%[1]s:%[2]d] %#[3]v [%[4]s]\n",
        Timestamp: "15:04:05.000",
    })
}

The Debug field accepts format specifiers in the following order:

Position Type Description Example
1 %s Filename main.go
2 %d Line number 15
3 any Value format verb (%v, %#v, %+v, %T, %q, etc.) 42
4 %s Timestamp (optional, only if Timestamp field is set) 15:04:05.123

You can use any combination of these specifiers and reorder them using position-based syntax (e.g., %[1]s, %[2]d).

Useful format examples

Compact format (minimal output):

de.SetFormat(de.Format{
    Debug: "%[1]s:%[2]d %[3]v\n",
})
// Output: main.go:15 42

With timestamp (suffix):

de.SetFormat(de.Format{
    Debug:     "[%[1]s:%[2]d] %#[3]v [%[4]s]\n",
    Timestamp: "15:04:05.000",
})
// Output: [main.go:15] 42 [15:04:05.123]

With timestamp (prefix using indexed parameters):

de.SetFormat(de.Format{
    Debug:     "[%[4]s] (%[1]s:%[2]d) %#[3]v\n",
    Timestamp: "15:04:05.000",
})
// Output: [15:04:05.123] (main.go:15) 42

Detailed struct inspection:

de.SetFormat(de.Format{
    Debug: "[%[1]s:%[2]d]\n%+[3]v\n",
})
// Output:
// [main.go:15]
// {Name:Alice Age:30}

Type information:

de.SetFormat(de.Format{
    Debug: "[%[1]s:%[2]d] %[3]T\n",
})
// Output: [main.go:15] int

JSON-style output with timestamp:

de.SetFormat(de.Format{
    Debug:     `{"file":"%[1]s","line":%[2]d,"value":%#[3]v,"time":"%[4]s"}` + "\n",
    Timestamp: "15:04:05.000",
})
// Output: {"file":"main.go","line":15,"value":42,"time":"15:04:05.123"}

Quoted strings:

de.SetFormat(de.Format{
    Debug: "[%[1]s:%[2]d] %[3]q\n",
})
// Output: [main.go:15] "hello world"

Disable timestamps:

de.SetFormat(de.Format{
    Debug:     "[%[1]s:%[2]d] %#[3]v\n",
    Timestamp: "",
})
// Timestamp generation disabled

Common timestamp formats

Use these values in the Timestamp field of Format:

  • "15:04:05.000" - HH:MM:SS.mmm (default)
  • "15:04:05" - HH:MM:SS
  • "2006-01-02 15:04:05" - Date and time
  • "2006-01-02T15:04:05.000Z07:00" - RFC3339 with milliseconds
  • "" - No timestamp (default)

Format verbs

  • %v - Default format
  • %#v - Go-syntax representation (default in package)
  • %+v - Struct with field names
  • %T - Type of the value
  • %q - Quoted string representation

Note: SetFormat() only affects debug builds. In testing mode, it's a no-op.

Custom output destination

By default, debug output is written to os.Stderr. Use SetWriter() to redirect output to any io.Writer:

import (
    "bytes"
    "codeberg.org/zerodeps/de"
)

func main() {
    // Redirect to a buffer
    var buf bytes.Buffer
    de.SetWriter(&buf)

    de.Bug(42)
    fmt.Println("Captured:", buf.String())
}

This is particularly useful for testing:

func TestMyFunction(t *testing.T) {
    var buf bytes.Buffer
    de.SetWriter(&buf)
    defer de.SetWriter(os.Stderr) // restore default

    myFunction()

    if !strings.Contains(buf.String(), "expected value") {
        t.Error("debug output missing expected value")
    }
}

Use GetWriter() to retrieve the current output destination:

current := de.GetWriter() // returns the current io.Writer

Both functions are concurrency-safe. In testing mode (default build), SetWriter() is a no-op and GetWriter() returns nil.

Performance

All exported functions in testing mode (default, no build tags) incur zero overhead as the compiler inlines the passthrough functions. This behaviour is validated as part of the testing harness, see the test-inline target in the Makefile.

Mode overhead

  • Testing mode: Zero overhead (inlined)
  • Debug mode: ~200-500ns per call for stack inspection, plus formatting/I/O costs
  • Release mode: Code won't compile if debug calls remain

Contributing

Design principles

This library is built around three core principles:

  1. Zero overhead in non-debug builds - In testing mode (default), all functions must be inlined away completely by the compiler. This is achieved by keeping within an inline cost of 2 (budget is 80).

  2. Ease of use - The API should be minimal and intuitive. Bug(val) returns val unchanged, enabling inline usage like result := de.Bug(x) + 10. No configuration required for basic use.

  3. Build-tag safety - Three distinct modes via build tags (-tags debug, -tags release, or neither) with clear, predictable behaviour. Release mode must fail compilation if any debug code remains.

Coding conventions

Follow Go standard conventions enforced by go vet and go fmt. Key requirements:

  • No external dependencies - Only Go standard library imports are permitted. This is a zero-dependency library and must remain so.
  • Run make lint before committing to ensure code is formatted and passes static analysis.

Development workflow

  1. Consult the Makefile for the list of confgures targets to ease the development workflow.
  2. Do not invoke go build, go test, or go vet directly — the Makefile targets ensure consistent flags and build tags are applied.
  3. Commit messages should have the first line be no more than 50 characters, and follow the format "