Last Updated: May 22, 2026
Go is strict about unused names. Declare a variable you never read, import a package you don't reference, and the compiler refuses to build. The blank identifier _ is the escape hatch: a write-only placeholder that says "a value goes here, but I don't need it." This lesson covers every place it appears.
_ Actually IsThe blank identifier is a single underscore character, and the Go specification gives it one rule: you can assign to it, but you can never read from it. There's no variable behind it, no memory, no type. It's a sink. Once a value lands in _, it's gone.
Try fmt.Println(_) and the compiler rejects it with cannot use _ as value. The blank identifier is not a variable name, even though it shows up where variable names go.
Why does Go need this? Because the compiler treats unused variables as errors. Declare x := 5 and never use x, and your program won't build. That rule keeps codebases clean, but it gets in the way every time a function returns more than you want. _ is the tool for those cases.
This is the most common use of _ in real Go code. Many standard library functions return a value plus an error, or a value plus an "ok" boolean. When you only care about one of them, _ skips the other.
Take parsing a quantity from a string. The strconv.Atoi function returns an int and an error. If you trust the input and only want the number:
This works, but be careful. Discarding errors is a real cost. If rawQuantity were "three", strconv.Atoi would return 0, err, and quantity would become 0 with no indication of failure. In production code, you almost always want to check the error. Use _ only when you've thought about what failure means and decided the zero value is fine.
The flip side is just as common. Sometimes you only want the error:
Here we don't care about the parsed value at all. We just want to know whether the input was a valid number. The _ discards the int and we keep the error.
Map lookups follow the same pattern. Indexing a map returns the value and a boolean that tells you whether the key was present. If you only want to check existence:
The first lookup discards the price and keeps ok. The second discards ok and keeps the price. Notice the second case is risky in the same way the Atoi example was: if "keyboard" weren't in the map, price would be 0.0 and we'd never know.
Cost: Discarding errors with _ is free at runtime but expensive in debugging time. A silent zero from Atoi or a missing-key zero from a map lookup will show up as a wrong total or a misrouted order, and the bug may take hours to find. Use _ deliberately, not by reflex.
rangeThe range keyword in a for loop produces two values for slices and arrays: an index and an element. Most of the time you want one or the other, not both.
Iterating a cart and printing the items, ignoring the index:
Without the _, you'd be forced to declare an index variable you don't use, and the compiler would refuse to build. With _, the index is discarded each iteration and the loop body sees only the element.
The reverse case shows up when you only need the position, not the value. Printing the slot number of each cart item:
Here there's no _ at all. Writing for i := range cart is the idiomatic way to ask range for the index only. You could write for i, _ := range cart, but gofmt style and go vet both prefer the shorter form. The one-value version of range is just the index, not the element.
For maps, the two values are the key and the value. Same patterns apply: for _, v := range m for values only, for k := range m for keys only.
Sometimes you need a package's init function to run, but you don't call any of its exported names. The classic case is registering a database driver:
The _ in front of "github.com/lib/pq" tells the compiler: "Import this package for its side effects, but don't bind a name in this file." Without the underscore, the compiler would complain that pq is imported but not used. With it, the package's init function still runs (registering the Postgres driver with database/sql), and your code can then open a connection using sql.Open("postgres", ...).
The Go authors call these blank imports, and they're the standard way to opt into a package's init-time setup without referencing its API directly. The blank import is how you trigger that timing from a file that doesn't otherwise touch the package.
A common idiom catches a specific bug at compile time instead of runtime. When you want to guarantee that a type satisfies an interface, you can write:
This declares a blank variable of the interface type and assigns a nil pointer of your concrete type to it. The assignment only compiles if *MyType satisfies SomeInterface. If you later remove a method or change a signature, the line fails to build, and you find out at compile time instead of when some far-away caller crashes.
A concrete example using an OrderProcessor interface:
The var _ OrderProcessor = ... line costs zero bytes at runtime, since _ doesn't allocate anything. It exists purely to make the compiler verify the relationship. If someone renames Process to Run on EmailOrderProcessor, this line breaks the build instantly, with a clear "does not implement" error. The pattern is worth recognizing when you see it.
Go doesn't have full destructuring like JavaScript, but multi-value assignments work the same way _ does for function returns. When you call a function that returns several values and you want some but not all, the _ slots into the unwanted positions.
A search function that returns the product name, price, and an error:
The middle _ discards the price, since this caller only wants to display the name. Each position in the assignment lines up with a return value, and _ reserves a slot without binding a name.
Most languages let unused variables and imports slide. Go's choice to make them compile errors comes from one observation: unused names are almost always bugs. A leftover variable from a refactor, an import for a package you stopped calling, a return value the original author cared about and you don't, these accumulate into noise that hides real problems.
By making the compiler reject them, Go forces you to either use the value or explicitly say you don't want it. The _ is the second option. It's intentional. Every _ in a Go program is a small note from the author saying "I saw this value and chose to ignore it." That's more useful than the silence other languages give you.
The trade-off is a bit of typing. You can't just write quantity := strconv.Atoi(s) and ignore the error by leaving it off; you have to acknowledge it with _. Most Go developers find this acceptable for the bugs it prevents.