- Go 88.6%
- Makefile 11.4%
| .gitignore | ||
| common.go | ||
| de.go | ||
| de_debug.go | ||
| de_release.go | ||
| de_test.go | ||
| doc.go | ||
| go.mod | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
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 buildgo 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 debuggo 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.12652 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:
-
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).
-
Ease of use - The API should be minimal and intuitive.
Bug(val)returnsvalunchanged, enabling inline usage likeresult := de.Bug(x) + 10. No configuration required for basic use. -
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 lintbefore committing to ensure code is formatted and passes static analysis.
Development workflow
- Consult the Makefile for the list of confgures targets to ease the development workflow.
- Do not invoke
go build,go test, orgo vetdirectly — the Makefile targets ensure consistent flags and build tags are applied. - Commit messages should have the first line be no more than 50 characters, and follow the format "