liveBook · Manning

Logger

As explained in Learn Go with Pocket-Sized Projects

A logger is a software component that records application messages, typically with severity levels and formatted output.

A logger records readable messages about what a program is doing. Unlike an unconditional fmt.Printf added during debugging, it gives callers a way to distinguish routine information from messages worth seeing only when investigating a problem. A three-level logger accepts debug, info, and error messages, then uses a configured threshold to decide which ones to write. section 4.0 section 4.3.2

In Go, define an exported Logger struct in a package such as pocketlog and give it receiver methods named Debugf, Infof, and Errorf. Each accepts a format string and arguments in the style of fmt.Printf. Separate methods keep call sites readable: the caller chooses the level by choosing the method. Keep the threshold field unexported and create a logger with pocketlog.New(level), rather than relying on a zero-value Logger whose configuration may be unsuitable. section 4.1.1 section 4.1.3 section 4.1.4

Listing 4.10 logger.go: Refactoring the logging methods
// Debugf formats and prints a message if the log level is debug or higher.
func (l *Logger) Debugf(format string, args ...any) {
    if l.threshold > LevelDebug {
        return
    }

    l.logf(format, args...)
}

// logf prints the message to the output.
// Add decorations here, if any. #1
func (l *Logger) logf(format string, args ...any) {
    _, _ = fmt.Fprintf(l.output, format+"\n", args...)
}

The Debugf method returns when the threshold excludes debug messages. Otherwise, it delegates formatting and writing to a private logf method shared by the exported level methods. section 4.2.3

Give each written message a newline so consecutive messages remain distinct. Writing to standard output is a useful default; accepting a custom output lets a caller use a file instead. Centralizing the write in logf avoids repeating formatting code across the level methods. In the shown implementation, the write result and error are discarded—a limitation to revisit if callers need to detect output failures. section 4.2 section 4.2.3

Listing 4.14 logger_test.go: Test helper implementation
// testWriter is a struct that implements io.Writer.
// We use it to validate that we can write to a specific output.
type testWriter struct {
    contents string
}

// Write implements the io.Writer interface.
func (tw *testWriter) Write(p []byte) (n int, err error) { #1
    tw.contents = tw.contents + string(p) #2
    return len(p), nil
}

A small io.Writer test helper captures written bytes. Pass it as the logger’s output, call the level methods, and compare its contents with the expected newline-separated messages. section 4.3.2

Test each threshold: at the debug level, expect debug, info, and error output; at stricter levels, expect fewer messages. Also vary call order so a test cannot pass merely because an implementation delays writing until Errorf is called. section 4.3.2

How Learn Go with Pocket-Sized Projects covers it

How deep it goes
Works through an exported API, threshold filtering, shared formatting, configurable output, and tests; leaves write-error handling unresolved.
Examples
Uses a three-level pocketlog implementation, a custom test writer, and expected newline-separated output at different thresholds.
What it assumes
Assumes familiarity with Go functions and formatting while developing package exports, receiver methods, and an io.Writer test helper.
What you can do after
Build it from scratch as a configurable Go logger and test its filtering and written output.
How it connects
Connects logging levels and receiver methods to Go packages, constructors, io.Writer, and table-driven tests.

Also explained in

Compare how 5 books explain Logger →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Learn Go with Pocket-Sized Projects. The section links point to the parts of the book it draws on.