What does Go's log package write by default: which stream, what prefix, and what severity?
answer
- one logger behind the whole package
- which of the two standard streams
- date plus time, nothing else
- Print, Printf, Println: same severity
- log.Default, SetOutput, SetFlags, SetPrefix
basics
~20 sGo'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 sThe 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 lineslog.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 listeninggo deeper
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.
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.
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.
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