Question
In Go, I am running a test that prints a value for debugging, but nothing appears in the output.
For example:
func TestPrintSomething(t *testing.T) {
fmt.Println("Say hi")
}
When I run:
go test
I only see:
ok command-line-arguments 0.004s
If I use t.Error() instead:
func TestPrintSomethingAgain(t *testing.T) {
t.Error("Say hi")
}
then the message is printed, but the test fails.
How can I print debugging information in a Go test using the testing package without causing the test to fail?
Short Answer
By the end of this page, you will understand why fmt.Println output often does not appear during go test, how Go captures test output, and the correct ways to print debugging information using t.Log, t.Logf, and the -v flag.
Concept
In Go tests, output is handled differently from normal programs. When you run go test, the testing tool captures standard output and standard error produced during test execution.
This means:
fmt.Println()may not appear in the terminal for passing tests.t.Error()prints because it records a test failure.t.Log()andt.Logf()are the intended tools for test-specific debug messages.
Why does Go do this?
Because test output should usually stay clean unless:
- a test fails, or
- you explicitly ask for verbose output.
This makes large test suites easier to read. If every passing test printed debug text, the output would quickly become noisy.
The most important rule is:
- Use
t.Log()ort.Logf()for debugging inside tests. - Run tests with
go test -vif you want to see log output from passing tests.
Example:
func TestPrintSomething(t *testing.T) {
t.Log("Say hi")
}
Run with:
Mental Model
Think of go test as a quiet test supervisor.
fmt.Println()is like speaking into the room.t.Log()is like writing a note into the test report.t.Error()is like raising a red flag.
If everything passes, the supervisor keeps the room quiet and only gives a short summary.
If you ask for a detailed report with -v, it shows the notes too.
If something fails, the red flags are always shown.
So the key idea is: Go tests collect information first, then decide what to display.
Syntax and Examples
The main tools are:
fmt.Println
func TestWithFmt(t *testing.T) {
fmt.Println("debug message")
}
- Writes to standard output.
- Often hidden for passing tests.
- May appear when tests fail or in some environments, but you should not rely on it for test logs.
t.Log
func TestWithLog(t *testing.T) {
t.Log("debug message")
}
- Adds a message to the test log.
- Shown automatically when the test fails.
- Shown for passing tests only with
go test -v.
t.Logf
func TestWithLogf(t *testing.T) {
name := "Gopher"
t.Logf("hello, %s", name)
}
- Same as
t.Log, but supports formatting.
Step by Step Execution
Consider this test:
func TestPrintSomething(t *testing.T) {
t.Log("Say hi")
}
Now run:
go test -v
What happens step by step:
go testbuilds a temporary test binary.- The test runner finds functions whose names start with
Testand match the required signature. TestPrintSomethingis executed.t.Log("Say hi")stores a log message in the test context.- The test finishes without failure.
- Because
-vwas used, Go prints logs even for passing tests.
Typical output:
=== RUN TestPrintSomething
foo_test.go:6: Say hi
--- PASS: TestPrintSomething (0.00s)
PASS
ok example 0.003s
Now compare with running without -v:
go test
Step 4 still happens, but since the test passes and verbose mode is off, the log is not shown.
Real World Use Cases
This behavior is useful in real testing work.
Debugging a failing unit test
func TestAdd(t *testing.T) {
got := 2 + 2
t.Logf("computed result: %d", got)
if got != 4 {
t.Fatalf("expected 4, got %d", got)
}
}
You can inspect intermediate values without permanently cluttering normal test output.
Inspecting API responses in tests
func TestAPIResponse(t *testing.T) {
body := `{"status":"ok"}`
t.Logf("response body: %s", body)
}
This helps when checking parsed JSON, headers, or mock server responses.
Verifying table-driven tests
func TestMultiply(t *testing.T) {
tests := []struct {
a, b int
want int
}{
{2, 3, 6},
{4, 5, },
}
_, tc := tests {
t.Logf(, tc.a, tc.b)
got := tc.a * tc.b
got != tc.want {
t.Fatalf(, got, tc.want)
}
}
}
Real Codebase Usage
In real Go codebases, developers usually prefer t.Log and t.Logf over fmt.Println inside tests.
Common patterns include:
Logging useful context before an assertion
func TestUserID(t *testing.T) {
id := 42
t.Logf("generated user id: %d", id)
if id <= 0 {
t.Fatal("user id must be positive")
}
}
This helps when a CI failure needs quick diagnosis.
Using guard clauses with t.Fatal
func TestConfig(t *testing.T) {
cfg, err := loadConfig()
if err != nil {
t.Fatalf("loadConfig failed: %v", err)
}
t.Logf("loaded config: %+v", cfg)
}
If setup fails, the test stops immediately.
Logging inside table-driven tests
func {
tests := [] {
input
want
}{
{, },
{, },
}
_, tc := tests {
t.Run(tc.input, {
t.Logf(, tc.input)
got := normalize(tc.input)
got != tc.want {
t.Fatalf(, got, tc.want)
}
})
}
}
Common Mistakes
1. Using fmt.Println and expecting it to always appear
Broken expectation:
func TestSomething(t *testing.T) {
fmt.Println("hello")
}
Why it is a problem:
go testmay capture and hide this output for passing tests.
Better:
func TestSomething(t *testing.T) {
t.Log("hello")
}
Run with:
go test -v
2. Using t.Error just to print a message
Broken usage:
func TestSomething(t *testing.T) {
t.Error("debug value")
}
Why it is a problem:
t.Errormarks the test as failed.
Comparisons
| Tool | Prints a message | Fails the test | Stops execution immediately | Shown for passing tests by default |
|---|---|---|---|---|
fmt.Println | Yes | No | No | Usually no |
t.Log | Yes | No | No | No |
t.Logf | Yes | No | No | No |
t.Error | Yes | Yes | No | Yes, because test fails |
Cheat Sheet
// Log without failing
func TestExample(t *testing.T) {
t.Log("message")
t.Logf("value: %d", 42)
}
// Fail but continue
func TestExample(t *testing.T) {
t.Error("something is wrong")
t.Errorf("got %d, want %d", 1, 2)
}
// Fail and stop immediately
func TestExample(t *testing.T) {
t.Fatal("cannot continue")
t.Fatalf("bad input: %q", "")
}
# Default: quiet for passing tests
go test
# Verbose: show logs for passing tests
go test -v
Key rules:
t.Logandt.Logfare the correct ways to print debug info in tests.
FAQ
Why does fmt.Println not show output in go test?
Because the Go test runner captures test output and usually hides it for passing tests to keep output clean.
How do I print debug information in a Go test without failing it?
Use t.Log() or t.Logf().
Why does t.Error() print the message?
Because it records a failure. Go shows failure output even when verbose mode is off.
How do I see t.Log() output for successful tests?
Run tests with:
go test -v
Should I use fmt.Println or t.Log in tests?
Prefer t.Log because it is designed for the testing package and works with Go's test reporting.
What is the difference between t.Error and t.Fatal?
t.Error marks the test as failed but continues running. marks the test as failed and stops that test immediately.
Mini Project
Description
Create a small Go test file that demonstrates the difference between fmt.Println, t.Log, and t.Error. This helps you understand what appears during a normal go test run versus a verbose go test -v run.
Goal
Write tests that show how Go handles regular output, test logs, and test failures.
Requirements
Create one test that uses fmt.Println.
Create one test that uses t.Log.
Create one test that uses t.Error.
Run the tests with both go test and go test -v and observe the differences.
Keep learning
Related questions
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.
Blank Identifier Imports in Go: What `_` Means in an Import Statement
Learn what `_` means in a Go import, why blank identifier imports run package init code, and when to use them safely.
Calling Functions Across Files in the Same Go Package
Learn how Go uses packages across multiple files, why functions may appear undefined, and how to organize code correctly.