skip to content

What does Go's log package write by default: which stream, what prefix, and what severity?

level: juniorimportance: must knowfreq 48%

answer

  1. one logger behind the whole package
  2. which of the two standard streams
  3. date plus time, nothing else
  4. Print, Printf, Println: same severity
  5. log.Default, SetOutput, SetFlags, SetPrefix

basics

~20 s

Go's log package writes to standard error through the one logger returned by log.Default(), prefixing each line with the date and time (log.LstdFlags). It has no severity levels: log.Print, log.Printf and log.Println all produce identical, unlabelled lines.

solid answer

~40 s

The package-level helpers all write through a single `*log.Logger` you can fetch with `log.Default()`. Its destination is `os.Stderr`, its flags are `log.LstdFlags` (`Ldate|Ltime`), and its prefix is empty, so a call prints `2026/09/01 14:03:11 message` and a newline. There is no level concept at all: `log.Print`, `log.Printf` and `log.Println` differ only in formatting, not in severity, and nothing marks a line as an error. You reconfigure that one logger process-wide with `log.SetOutput`, `log.SetFlags` and `log.SetPrefix`, or build an independent one with `log.New(w, prefix, flags)`. That combination of a fixed text prefix and no severity is exactly why a structured sink has to parse these lines, and why `log/slog` exists.

code

go · 7 lines
go
log.Println("proxy listening")
// 2026/09/01 14:03:11 proxy listening    -> os.Stderr

log.SetPrefix("proxy: ")
log.SetFlags(0)
log.Println("proxy listening")
// proxy: proxy listening

go deeper

for a junior

Recall the three facts exactly: standard error, a date and time prefix from log.LstdFlags, and no severity levels at all. Know that log.SetOutput, log.SetFlags and log.SetPrefix change one shared logger for the whole process.

for a middle

Be ready to explain the mechanics: one package-level logger reachable via log.Default(), flags as a bitmask, the mutex that keeps lines whole, and why an embedded newline in a message breaks a one-record-per-line consumer.

for a senior

Show that you know what this costs in production. Unlabelled text lines force severity to be inferred by a collector, and any flag change silently reshapes what your ingest has to parse, so the prefix format is effectively a contract.

for a principal

Own the position that the log package is still a legitimate boundary, not legacy to be purged. Standard-library components and vendored code write through it, so the strategy is where to attach a structured sink, not whether to ban the package.

## One logger behind the package Every call to `log.Print`, `log.Printf`, `log.Println` and their friends goes through a single shared `*log.Logger` value that the package owns. Since Go 1.16 you can get a handle on it with `log.Default()`, which returns that same logger, so `log.Printf(...)` and `log.Default().Printf(...)` are the same call. That logger has three configurable pieces: - **an output** - an `io.Writer`, defaulting to `os.Stderr`, changed with `log.SetOutput(w)`; - **flags** - a bitmask of what to put in front of the message, defaulting to `log.LstdFlags`, changed with `log.SetFlags(f)`; - **a prefix** - a literal string, defaulting to empty, changed with `log.SetPrefix(s)`. `log.LstdFlags` is defined as `Ldate | Ltime`: the local date as `2009/01/23` and the local time as `01:23:23`, separated by a space and followed by a space before the message. Other flags you can add are `Lmicroseconds` (fractional seconds), `Llongfile` and `Lshortfile` (the caller's file and line), `LUTC` (render the timestamp in UTC) and `Lmsgprefix` (move the prefix from the very start of the line to just before the message, rather than before the timestamp). So the default rendering of `log.Println("proxy listening")` is one line on standard error reading `2026/09/01 14:03:11 proxy listening`. The logger appends a trailing newline if the message does not already end with one, and it holds an internal mutex, so concurrent calls from many goroutines do not interleave mid-line. ## What is deliberately missing The package has **no severity levels**. `log.Print` is not "info" and there is no `log.Warn`. `log.Fatal*` and `log.Panic*` exist, but they are control-flow helpers - they print and then terminate or panic - not severity labels attached to the record. Nothing in the emitted text distinguishes a routine startup message from a serious failure unless the author wrote the distinction into the message string themselves, or set a prefix such as `log.SetPrefix("ERROR ")` on a dedicated logger. The package also has **no structure**. The output is one line of text. There are no key/value attributes, no machine-readable framing, and the timestamp is glued to the front of the message as characters. If the message itself contains a newline - a stack trace, a multi-line dump, a non-ASCII payload someone printed raw - the line breaks across several physical lines, and a downstream consumer that assumes one record per line will mis-split it. ## Why that shape matters downstream The standard logger is a fine tool for a command-line program a human reads. It becomes a problem the moment something machine-parses the output. A collector that expects JSON records will either drop these lines or, worse, wrap them as unparsed blobs; a collector that parses text has to be taught this exact prefix format, and it now has a dependency on your process's flag settings. This is the pressure that produced `log/slog` in Go 1.21: a record has a level, a message, a timestamp and typed attributes, and a handler decides the wire format. But the `log` package does not go away - the standard library itself still writes through it (for example `net/http`'s server diagnostics when you have not given it a logger of its own), and so does a great deal of existing and third-party code. That is why the interesting engineering question on this ground is not "which should I use" but "how do I get the `log` package's output into a structured pipeline without editing every call site". ## Independent loggers `log.New(w io.Writer, prefix string, flag int) *log.Logger` builds a logger unrelated to the package default, and the same three knobs exist as methods (`SetOutput`, `SetFlags`, `SetPrefix`) plus readers (`Flags`, `Prefix`, `Writer`). This matters because several standard-library types accept a `*log.Logger` as a field rather than calling the package functions - handing one of those a purpose-built logger is the injection point a structured pipeline uses. ## Things to say precisely - Default destination: `os.Stderr`, not stdout. - Default flags: `log.LstdFlags`, which is date plus time - **not** file and line, which requires `Lshortfile` or `Llongfile`. - Levels: none. Any severity you see in `log` output was typed into the message or the prefix by a human. - Concurrency: safe; the logger serialises writes.

  • How would you make the standard logger print the caller's file and line?
    Add `log.Lshortfile` (file name and line) or `log.Llongfile` (full path and line) to the flags, for example `log.SetFlags(log.LstdFlags | log.Lshortfile)`. Both are computed by walking the call stack at log time, which costs something per call, so they are usually enabled for debugging rather than left on in a hot path.
  • Two goroutines call log.Printf at the same time. Can their output interleave mid-line?
    No. `log.Logger` holds a mutex around formatting and the write, so each call emits one complete line. What is not guaranteed is ordering between goroutines, and the atomicity ends at the `io.Writer` you gave it: if that writer is itself unsafe for concurrent use, you have moved the problem rather than solved it.
  • What does log.Default() give you that the package-level functions do not?
    A `*log.Logger` value you can pass to something else. Several standard-library types take a `*log.Logger` field, and library APIs often accept one, so `log.Default()` lets you hand over the exact logger the package functions use instead of forcing that component onto its own private destination.

saying these in an interview costs you the question

  • Says log.Println writes to standard output
  • Thinks log.Print is info level and log.Fatal is error level
  • Believes the default flags include the file and line
  • Assumes each package-level call has its own logger
  • Claims log output is already machine-parseable