This guide is for Go newcomers who see import _ "some/package" and want the real reason it exists—not a trick to “hide” unused imports. It explains blank imports, how they differ from normal imports and from _ on values, when database/sql drivers and similar packages need them, and what the compiler enforces.
Go 1.24 on Linux.
Quick answer: what does underscore before import mean in Go?
Writing import _ "path/to/pkg" is a blank import: Go still loads and initializes that package, but your file does not get a package name to call.
Use it when you need the package’s init side effects—common cases are database/sql drivers (self-registration), image/jpeg or image/png (so image.Decode works), or net/http/pprof (debug handlers on the default mux). The sections below spell out how that differs from a normal import and from _ on a value.
How blank imports work in Go
Blank import meaning
In an import declaration, the name before the path is how you refer to the package in this file. With import "fmt" you write fmt.Println. With import _ "path/to/pkg", the name is discarded: the package is still linked and initialized, but no identifier is added to this file’s namespace—so a fmt.Println call after import _ "fmt" does not compile, as in this program:
package main
import _ "fmt"
func main() {
fmt.Println("hello")
}Import for side effects
During initialization, imported packages run init functions and can register with globals (sql.Register, image format tables, default HTTP mux routes, and so on). A blank import is how you depend on that behavior when you do not need the package’s exported API in this source file.
Why Go needs blank imports
Go rejects unused imports
If you import a package under its default name and never refer to it, the program does not compile. That catches dead dependencies and typos early.
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println("hello")
}Building that file fails with an error like the following (paths may differ on your machine):
# command-line-arguments
./main.go:5:2: "math" imported and not usedThe fix is either to remove the import, use math for real work, or—only when the package is designed for it—switch to a blank import.
Some packages are imported only for init side effects
The Go language specification allows importing a package without referring to exported identifiers only when that import is a blank import used for side effects (initialization). Concrete patterns (SQL drivers, image codecs, pprof) are shown in Common blank import examples below.
Why _ makes that intent explicit
A blank import states plainly that you want the package linked and initialized, not referenced by name here. That is different from a stray unused import, and different from tricks such as var _ = fmt.Println, which only silence the compiler without documenting a real side-effect dependency. If the package is not needed, delete the import; if you call its API, use a normal or aliased import.
Common blank import examples
Database drivers
With database/sql, you open databases by driver name. The concrete driver package registers that name in init. You blank-import the driver and use only database/sql in your code. See the walkthrough in SQLite3 with Golang.
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)This pattern needs your module and the driver; it is not self-contained in the browser Run sandbox, so run it on your machine with go run.
net/http/pprof
HTTP servers often add profiling routes by blank-importing net/http/pprof.
The package registers profiling handlers on http.DefaultServeMux during init. Your code still uses ordinary net/http; you do not call pprof by package name.
package main
import (
"log"
"net/http"
_ "net/http/pprof"
)
func main() {
log.Println("listening on :6060")
log.Fatal(http.ListenAndServe("localhost:6060", nil))
}Because the second argument to ListenAndServe is nil, Go uses http.DefaultServeMux. The blank import registers routes such as:
/debug/pprof/
/debug/pprof/goroutine
/debug/pprof/heap
/debug/pprof/profileYou can then open:
http://localhost:6060/debug/pprof/Important note:
Only expose pprof on localhost or behind proper access control.Profiling endpoints can reveal runtime details about your application, so avoid exposing them on a public interface by accident.
Also remember: this blank import registers handlers on http.DefaultServeMux. If your server uses a custom ServeMux, the blank import alone will not add pprof routes to that custom mux.
Image format registration
image.Decode consults registered decoders. Importing image/jpeg, image/png, and so on with _ runs each package’s init and hooks the format into image. The runnable program under Example: PNG and image.Decode shows the pattern: you call only image.Decode, while _ "image/png" supplies the side-effect registration.
Blank import vs normal import
A normal import binds a package name in this file, so you call exported APIs with that prefix:
package main
import "fmt"
func main() {
fmt.Println("hi")
}Here fmt is that package name.
A blank import still links and initializes the dependency, but it does not add a usable identifier: you cannot write png.Decode after only import _ "image/png". If you need the package’s exports, use a normal import or an alias (for example import p "image/png" and p.Decode). Reserve import _ "..." for real side-effect imports, not as a way to quiet “imported and not used” while you still meant to call the package.
What happens during blank import?
A blank-imported package is still linked like any other dependency. Before main, Go initializes imported packages—package-level variables first, then init functions, in dependency order. During that work, side-effect packages run the registrations you rely on later (sql.Register, image format hooks for image.Decode, net/http/pprof handlers on http.DefaultServeMux, and similar), so this file can call generic APIs without naming those implementation packages.
Example: PNG and image.Decode
The init function in image/png registers the decoder with image. This program only names image in code; the blank import is what makes image.Decode accept PNG bytes (try removing _ "image/png" and the decode step fails).
package main
import (
"bytes"
"encoding/base64"
"fmt"
"image"
_ "image/png"
)
func main() {
const pngB64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
raw, err := base64.StdEncoding.DecodeString(pngB64)
if err != nil {
fmt.Println(err)
return
}
img, format, err := image.Decode(bytes.NewReader(raw))
if err != nil {
fmt.Println(err)
return
}
b := img.Bounds()
fmt.Println(format, b.Dx(), b.Dy())
}You should see png, 1, and 1 on one line. That output depends on the blank import having run init registration before main.
When should you use _ import?
Use a blank import when package documentation tells you to rely on init registration (for example database/sql drivers, image/jpeg / image/png, optional net/http/pprof on DefaultServeMux, or plugin-style registries). If this file calls exported functions or uses types from the package, use a normal or aliased import instead.
Do not blank-import only to silence “imported and not used.” The math package is a common bad example: it compiles, but you get no useful registration—only a misleading dependency.
package main
import _ "math"
func main() {}It builds and runs, but it does not document a real side-effect dependency—only a habit to avoid.
Prefer deleting that import, or import "math" and calling something like math.Sqrt when you actually need the package.
Blank identifier vs blank import
The _ token is the blank identifier. As a left-hand side in an assignment, it discards a value you are not using yet:
package main
import "fmt"
func main() {
page := "GoLinuxCloud"
author := "Anonymous"
fmt.Println(page)
_ = author
}You should see one line printing GoLinuxCloud.
In import _ "path", the same character is import syntax, not a value discard: it means load and initialize that package for side effects. The language spec treats that under import declarations, not under ordinary assignment rules—same symbol, different meaning.
Mistakes to avoid
- Blank-importing a package only to hide an unused normal import when the package is not meant for side effects.
- Expecting to call exported APIs from a blank-imported package.
- Forgetting the driver blank import and then wondering why
sql.Open("sqlite3", ...)fails at runtime. - Leaving temporary
_imports in production instead of fixing the real dependency graph.
Go blank import cheat sheet
| Import style | Meaning |
|---|---|
import "fmt" |
Normal import; refer to the package as fmt. |
import f "fmt" |
Alias import; refer as f. |
import . "fmt" |
Dot import; exported names from fmt enter this file’s scope (use sparingly). |
import _ "pkg/path" |
Blank import; load and initialize pkg/path for side effects only. |
References
- Effective Go — Blank imports
- The Go Programming Language Specification — Import declarations
- The Go Programming Language Specification — Package initialization
Summary
An underscore in import _ "path" is a blank import: the package is built, initialized, and its init functions run so side effects like sql.Register or image format registration occur, but your file does not receive a package identifier to call. That is different from _ on a value, which only discards a value. Go forbids unused normal imports to keep dependencies honest; a blank import is the sanctioned way to depend on a package only for those side effects. Use real imports when you call the package API, and reserve blank imports for patterns the ecosystem documents—database drivers, image codecs, optional pprof wiring—not as a permanent way to silence “imported and not used.”
Frequently Asked Questions
1. What does underscore before an import mean in Go?
2. Why does Go require blank imports instead of silently ignoring unused imports?
3. Can I call functions from a package imported as `_`?
pkgname.Func in scope. You only get package initialization and any registration performed in init.4. Is blank import the same as assigning a value to `_`?
_ symbol, different rules. _ = x ignores a value. import _ "path" is an import declaration that means side-effect import, not a variable assignment.
