4.7 Blank Identifier _: Discarding Values
Right, let’s talk about the blank identifier. You know, the underscore: _. It looks like a placeholder, a bit of syntactic punctuation, and that’s exactly what it is. We use it when we’re forced by the grammar of Go to acknowledge a value’s existence but we have absolutely zero interest in actually keeping it around. It’s the programming equivalent of saying, “I acknowledge you’ve spoken,” before immediately forgetting what was said.
Think of it as a write-only variable. You can shove values into it, but you can never read from it. Trying to do so is a compile-time error, and rightly so. What would you even expect to get back? It’s a black hole.
The Bouncer at the Assignment Door
The most common place you’ll meet _ is on the left-hand side of an assignment where you have too many values for the number of variables you want to deal with. A classic example is when a function returns multiple values, and you only care about one of them.
Take the ever-common strconv.Atoi function (string to int). It returns an int and an error. Sometimes, you’re parsing user input and you absolutely need to check that error. Other times, you’re parsing a hardcoded string you know is a number, like "42". You still have to handle that second return value. This is where _ comes in to politely usher the unused value into the void.
package main
import (
"fmt"
"strconv"
)
func main() {
// We KNOW this will work. The error is irrelevant for this scenario.
num, _ := strconv.Atoi("123")
fmt.Println("The number is:", num)
// This, however, is a recipe for disaster. Don't just blindly discard errors.
// badNum, _ := strconv.Atoi("not_a_number") // badNum will be 0. Enjoy your bug!
}
Why is this necessary? Because Go rightfully hates “unused variables.” They clutter up the namespace and are a hallmark of sloppy code. The blank identifier is your way of telling the compiler, “I know this value is here, and I am consciously choosing to ignore it. This isn’t an oversight; it’s a decision.” It keeps your code clean and the compiler happy.
The Silent Partner in for-loops and maps
Another prime spot for _ is when you’re iterating with a range loop but you don’t need one of the elements. A range on a slice or array gives you both the index and the value. What if you just want the values?
fruits := []string{"apple", "banana", "cherry"}
// If you only need the value, discard the index.
for _, fruit := range fruits {
fmt.Println(fruit)
}
// Conversely, if you only need the index (a rarer, often smellier case):
for index, _ := range fruits {
fmt.Println(index)
}
The same logic applies to maps, where range gives you the key and the value.
ages := map[string]int{"Alice": 30, "Bob": 25}
// Just want the keys? Discard the values.
for name, _ := range ages {
fmt.Println(name)
}
// Pro tip: you can even drop the ', _' part entirely here. for name := range ages { } is idiomatic.
Importing for Side Effects: A Necessary Evil
Now, here’s the weird one. It’s the one use case that feels a bit like a hack, because it is. Sometimes you import a package not for its functions or types, but for the side effects of its init() functions.
The init() function in a package runs automatically when the package is imported. Some packages use this to register themselves with a central system. The prime example from the standard library is database/sql. You import a database driver (like github.com/go-sql-driver/mysql) so its init() function can register itself with the sql package. You never actually call any functions from mysql directly.
To make this clear—and to avoid the “unused import” error—you assign the import to the blank identifier.
import (
"database/sql"
_ "github.com/go-sql-driver/mysql" // The blank identifier is crucial here
)
func main() {
// Now we can use sql.Open with "mysql" as the driver name,
// because the side-effect of the import registered it.
db, err := sql.Open("mysql", "user:password@/dbname")
// ... handle err, etc.
}
It looks strange, but it’s a well-established pattern. You’re essentially saying, “I am importing this package solely for its side effects. Do not make its exported names available to me.” It’s a clear signal of intent to anyone reading your code.
The One Rule: Never, Ever Read From It
This should be obvious by now, but it’s worth stating explicitly. The blank identifier is not a variable. It cannot be read. It cannot be re-declared. It exists only on the left-hand side of an assignment.
_ = getValue() // This is fine.
val := _ // This is a compile-time error. What did you expect to get?
So the next time Go’s assignment syntax demands a variable you don’t want, give it the _. It’s the polite way to say, “I see your value and raise you one void.”