The blank identifier is a discard signal
You write a function in Go that returns two values. You only need the first one. You try to assign the result to a single variable, but the syntax demands you handle both. You come from Python or JavaScript, where you can assign a variable and never use it without the compiler complaining. In Go, the compiler is strict. You write result, err := doSomething(), use result, and forget to check err. The build fails. You try to just drop the error by assigning it to nothing, but Go doesn't have "nothing" in the assignment syntax. You need a placeholder that says "I see this value, and I am intentionally throwing it away." That placeholder is the blank identifier, written as an underscore _.
The blank identifier is a special symbol in Go that represents a value that is discarded. It is not a variable. You cannot read from it. You cannot assign to it twice in the same scope to capture different things. It is a black hole for data.
Think of a form with five fields. You only care about field three. The blank identifier is like writing "N/A" on the other four fields. You acknowledge the field exists, you satisfy the requirement of filling out the form, but you explicitly mark the data as irrelevant. The compiler sees the underscore and stops checking for usage.
Minimal example
Here is the simplest use case. You have a function that returns two values. You only want the first one.
package main
import "fmt"
// splitName returns first and last name.
func splitName(full string) (string, string) {
// Split on space for demonstration.
parts := []string{"Alice", "Smith"}
return parts[0], parts[1]
}
func main() {
// Capture first name, discard last name using _.
first, _ := splitName("Alice Smith")
// Print only the value we kept.
fmt.Println(first)
}
The underscore appears on the left side of the assignment. The function returns both values, but the compiler generates code to compute the second value and immediately drop it. No variable is created for the last name.
How the compiler handles _
When the compiler sees _, it generates no storage for that value. The value is computed by the right-hand side, but the result is immediately dropped. There is no variable created in memory. If you try to read _, the compiler rejects the program with invalid operation: cannot assign to _. The underscore is write-only. It exists purely to satisfy the syntax of multiple assignment or import declarations.
The blank identifier is exempt from the variable declaration rules. You can assign to _ multiple times in the same block. The compiler treats _ as a unique token that never conflicts. This allows you to reuse _ across multiple statements without creating shadowing issues.
package main
import "fmt"
func main() {
// Reuse _ in the same scope. This is allowed.
a, _ := getValue()
b, _ := getOther()
fmt.Println(a, b)
}
func getValue() (int, error) { return 1, nil }
func getOther() (int, error) { return 2, nil }
The underscore does not count as a variable for the := rule. You can use _ in a short variable declaration even if all other variables on the left are already declared in the scope. This is useful when you need to update one variable and discard a new value for another.
The underscore is a black hole. Put data in, get nothing out.
Realistic patterns
Real code often involves imports for side effects or ignoring loop indices.
Here is how you import a package just to run its initialization code. In Go, you don't always call functions from a package. Sometimes just importing it runs an init function that registers the package with a framework.
package main
import (
"database/sql"
// Import driver for side effects. The init() function registers the driver.
_ "github.com/lib/pq"
"fmt"
)
func main() {
// Open connection using the registered driver name.
db, err := sql.Open("postgres", "dbname=test")
if err != nil {
// Handle connection error.
fmt.Println(err)
return
}
// Close connection when done.
defer db.Close()
fmt.Println("Connected")
}
The import _ "github.com/lib/pq" tells the compiler to load the package and run its init functions, but you never reference any names from the package in your code. Without the underscore, the compiler would reject the program with imported and not used: github.com/lib/pq.
Convention aside: Side-effect imports are rare. Use them only when the package registers itself, like database drivers, template parsers, or plugin systems. If you are importing a package just to use a function, remove the underscore and call the function.
Here is how you ignore the index in a range loop.
package main
import "fmt"
func main() {
items := []string{"apple", "banana", "cherry"}
// Iterate over values, ignore the index.
for _, item := range items {
fmt.Println(item)
}
}
The range clause produces two values: the index and the value. The underscore discards the index. This is common when you only need the data and don't care about the position.
The zero value trap in type assertions
The blank identifier is dangerous when used with type assertions. A type assertion checks if an interface value holds a specific type. The idiom is val, ok := interface.(Type). The ok boolean tells you if the assertion succeeded.
If you discard ok with _, you lose the safety check. If the type does not match, val becomes the zero value of the target type. Your code continues with a zero value, which might cause a panic later or silent data corruption.
package main
import "fmt"
func main() {
var i interface{} = "hello"
// Discard ok. If type fails, s becomes empty string.
s, _ := i.(string)
fmt.Println(s) // prints "hello"
var j interface{} = 42
// Discard ok. Type fails. s becomes empty string.
s, _ = j.(string)
fmt.Println(s) // prints "" (empty string)
}
In the second assertion, j is an integer. The assertion fails. s becomes an empty string. The program does not crash. It just gets wrong data. This is a subtle bug that is hard to track down.
Convention aside: The community prefers checking the ok result or using a type switch. Discarding the ok result is acceptable only when you are certain the type matches, such as after a type switch or in test code where the setup guarantees the type.
Discarding the ok result hides type mismatches. Check the boolean or use a type switch.
Pitfalls and errors
The most common pitfall is using _ to discard an error. Errors in Go are values that indicate something went wrong. Discarding an error hides the failure. The program continues as if nothing happened, which often leads to incorrect behavior or data loss.
Convention aside: if err != nil { return err } is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. Discarding an error with _ is acceptable only when the error is truly irrelevant, like closing a writer that you don't care about failing, or in tests where the error is expected and handled by the test framework. Never discard a database error silently.
If you try to use _ as a type, the compiler rejects the program with invalid type _. The underscore is an identifier, not a type. You cannot declare a variable of type _.
If you try to assign to _ on the right side of an assignment, the compiler rejects the program with invalid operation: cannot assign to _. The underscore is write-only. You can only use it on the left side of an assignment or in an import declaration.
Discard values, never discard errors. The compiler helps you keep errors visible.
Decision matrix
Use the blank identifier when a function returns multiple values and you only need a subset of them. Use the blank identifier in a range loop when you need the values but don't care about the index. Use the blank identifier in an import declaration when you need a package's side effects, such as registering a driver. Use the blank identifier to discard the boolean result of a type assertion when you are confident the type matches and want to panic on failure. Use a named variable when you need to inspect or use the value later in the function. Use explicit error handling when the error could indicate a failure that affects program correctness. Use a type switch when you need to handle multiple possible types safely. Use the comma-ok idiom with a named boolean when you need to handle the failure case explicitly.