Tiny (~100 LoC) Go assertion library focused on crystal-clear failure messages and thoughtful source context.
- 🔍 Crystal-clear failure messages with contextual values
- 📚 Rich source context showing the exact failure location
- 🛠 Tiny and free of dependencies (~100 lines of Go)
- 💡 Elegant, idiomatic Go API
- 🎯 Two-tier assertion system with build tag support
- ⚙️ Configurable source context behavior
- ⚡ Zero-allocation hot path (~3ns per assertion)
Inspired by Tiger Style.
go get github.com/nikoksr/assert-goimport "github.com/nikoksr/assert-go"
func PaymentProcessing() {
payment := processPayment(PaymentRequest{
Amount: 99.99,
CustomerID: "cust_123",
Currency: "USD",
})
// Assert payment was processed successfully
assert.Assert(payment.Status == "completed", "payment should be completed",
// Optionally, add context to the panic
"payment_id", payment.ID,
"status", payment.Status,
"amount", payment.Amount,
"error", payment.Error,
"timestamp", payment.ProcessedAt,
)
}On failure, you get:
Assertion failed at payment_test.go:43
Message: payment should be completed
Relevant values:
[payment_id]: "pmt_789"
[status]: "failed"
[amount]: 99.99
[error]: "insufficient_funds"
[timestamp]: "2025-12-12T15:04:05Z"
Source context:
37 | payment := processPayment(PaymentRequest{
38 | Amount: 99.99,
39 | CustomerID: "cust_123",
40 | Currency: "USD",
41 | })
42 |
→ 43 | assert.Assert(payment.Status == "completed", "payment should be completed",
44 | "payment_id", payment.ID,
45 | "status", payment.Status,
46 | "amount", payment.Amount,
47 | "error", payment.Error,
48 | "timestamp", payment.ProcessedAt,
49 | )
goroutine 1 [running]:
github.com/nikoksr/assert-go.PaymentProcessing(0xc00011c000)
/app/payment.go:43 +0x1b4
# ... regular Go stacktrace continues
The library provides two types of assertions:
Assert()- Always active, meant for critical checks that should run in all environmentsDebug()- Development-time assertions that can be disabled in production
Debug assertions are disabled by default. To enable them, use the assertdebug build tag:
go test -tags assertdebug ./...
go run -tags assertdebug main.goExample usage:
// This will only be evaluated when built with -tags assertdebug
assert.Debug(len(items) < 1000, "items list too large",
"current_length", len(items),
"max_allowed", 1000,
)
// This will always be evaluated regardless of build tags
assert.Assert(response != nil, "HTTP response cannot be nil",
"status_code", response.StatusCode,
)You can configure the assertion behavior:
// Configure assertion behavior (call during initialization)
assert.SetConfig(assert.Config{
// Enable/disable source context in error messages
IncludeSource: true,
// Number of context lines to show before and after the failing line
ContextLines: 5,
})Note: SetConfig should be called during program initialization before any assertions are made. It is not thread-safe and should not be called concurrently with assertions.
Assertions are designed to have minimal performance impact:
BenchmarkAssert_Success ~3.0 ns/op 0 B/op 0 allocs/op
BenchmarkAssert_SuccessWithValues ~6.1 ns/op 0 B/op 0 allocs/op
BenchmarkDebug_Success (disabled) ~0.3 ns/op 0 B/op 0 allocs/op
Key Takeaways:
- Successful assertions are extremely cheap (~3 nanoseconds)
- Zero allocations on the hot path
- Debug assertions when disabled are essentially free (compiler optimizes them away)
- Even with contextual values, overhead remains minimal
Run benchmarks yourself: go test -bench=. -benchmem
Like many Go developers, I initially dismissed assertions as incompatible with Go's philosophy of explicit error handling. That changed when I read TigerStyle's take on assertions and decided to experiment.
Here's the problem I'd been living with: I've always felt the urge to validate internal invariants—checking that a logger I just initialized isn't nil, verifying state I just set up is correct, confirming assumptions about data flow within my own code. These aren't checks for user input or network failures. They're checks for my mistakes.
But the traditional Go approach felt wrong. Returning an error means telling my users: "Hey, handle this case where I might have screwed up." It pollutes APIs with impossible error cases, forcing callers to handle conditions that can only occur if my code is broken. I'd write these defensive checks anyway, feeling uncomfortable doing so, knowing I was treating my own bugs the same as legitimate operational failures.
Assertions solved this. They let me validate what must be true without burdening my API consumers. When an invariant is violated, there's no graceful recovery; continuing would only mask the bug and spread corrupted state. Better to fail immediately with rich context pointing directly at the problem.
I use assertions in application code where I control the full context and can make strong guarantees about internal state. I don't use them in libraries (where I can't control callers) or for validating external input (that's proper error handling territory). But for checking preconditions, postconditions, and invariants I own? They're essential.
This approach has also opened doors to patterns like negative-space testing, where assertions help verify not just what should happen, but what shouldn't. Worth exploring if you go down this path.
What started as a skeptical experiment has become fundamental to how I write Go. Assertions make my code more reliable and bugs dramatically easier to catch and diagnose.
If you find this library useful, you might also be interested in:
- notify - Dead simple Go library for sending notifications to various messaging services (3,500+ ⭐)
- typeid-zig - Complete Zig implementation of the TypeID specification, recognized as an official community implementation