Question
In the following Go code, what does the underscore before the import path mean?
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
"log"
"os"
)
Specifically, why is github.com/mattn/go-sqlite3 imported with _ instead of a normal package name, and what effect does that have in the program?
Short Answer
By the end of this page, you will understand what a blank identifier import is in Go, why _ "package/path" is used, and how it allows a package to run its initialization code without directly referencing its exported names. You will also see why this pattern is common with database drivers, image decoders, and plugin-style registration systems.
Concept
In Go, an import like this:
_ "github.com/mattn/go-sqlite3"
is called a blank identifier import or side-effect import.
The underscore _ means:
- import the package
- do not bind it to a usable name in this file
- keep only the package's side effects
The most important side effect is that the package's init() functions run.
Why this matters
Some packages are not imported because you want to call their functions directly. Instead, you import them because they register themselves with another part of the program during initialization.
In your example:
_ "github.com/mattn/go-sqlite3"
go-sqlite3 registers itself as a SQL driver when the package is initialized. Then the database/sql package can use that driver later through calls such as:
sql.Open("sqlite3", "my.db")
Even though you never write something like sqlite3.SomeFunction(), the import is still necessary because the driver registration happens automatically inside the imported package.
Why not a normal import?
If you wrote:
import "github.com/mattn/go-sqlite3"
then Go would expect you to use that package name somewhere in the file. If you did not, you would get an imported and not used compile error.
Using _ tells Go:
- yes, this import is intentional
- I know I am not calling anything from it directly
- I only want its initialization behavior
Common uses of blank imports
Blank imports are often used for:
- database drivers such as SQLite, MySQL, PostgreSQL drivers
- image format decoders such as JPEG, PNG, GIF
- plugin or registry systems where packages register handlers
- instrumentation or setup packages that install hooks during startup
This pattern matters in real programs because it supports loosely coupled design. A central package can define a registry, and other packages can attach themselves to it during initialization.
Mental Model
Think of a blank import like plugging a device into a system without directly pressing any buttons on the device.
- A normal import is like hiring a worker and then giving them tasks directly.
- A blank import is like bringing in a technician who quietly sets up the system in the background.
You never speak to that technician in your code, but their setup work makes the rest of the program function.
For go-sqlite3, the package quietly says: "I am a SQLite driver; register me with database/sql." After that, database/sql knows how to open SQLite connections.
Syntax and Examples
Core syntax
import _ "package/path"
This means:
- import the package
- execute its initialization code
- do not create a package name you can use in this file
Example: database driver
package main
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "example.db")
if err != nil {
fmt.Println("open error:", err)
return
}
defer db.Close()
fmt.Println("SQLite driver loaded successfully")
}
What is happening here?
database/sqlprovides a generic database API.go-sqlite3provides a concrete SQLite driver.- The SQLite package registers itself in its
init()function. sql.Open("sqlite3", ...)works because the driver was registered during import.
Example: image decoders
A classic standard-library example is image decoding:
package main
import (
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
"os"
)
func main() {
f, err := os.Open("photo.jpg")
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
img, format, err := image.Decode(f)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("decoded format:", format)
fmt.Println("bounds:", img.Bounds())
}
Here:
imageprovides the generic decoding APIimage/jpegandimage/pngregister decoders viainit()- you do not call
jpeg.Decodedirectly image.Decodeworks because the decoders were registered
Important rule
A blank import does not let you use package functions directly.
This will not work:
import _ "fmt"
func main() {
fmt.Println("hello") // error: fmt is not defined
}
Because _ means the package has no name in your file.
Step by Step Execution
Consider this example:
package main
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "test.db")
if err != nil {
fmt.Println(err)
return
}
defer db.Close()
fmt.Println("connected")
}
Step-by-step
1. The program starts
Go loads imported packages before running main().
2. github.com/mattn/go-sqlite3 is imported
Even though it is imported with _, Go still loads the package.
3. The package's init() function runs
Inside the SQLite driver package, an init() function registers the driver with database/sql.
Conceptually, it does something like this:
func init() {
sql.Register("sqlite3", myDriver)
}
4. main() begins
Now your code runs.
5. sql.Open("sqlite3", "test.db") is called
database/sql looks up the registered driver named sqlite3.
6. The driver is found
Because the blank import caused registration earlier, the open call succeeds.
7. The database handle is returned
Your program can now use the SQLite database through the generic database/sql API.
What if the blank import is removed?
Then the driver never registers itself. This typically leads to an error such as:
sql: unknown driver "sqlite3" (forgotten import?)
That message is a strong clue that a side-effect import is missing.
Real World Use Cases
1. Database drivers
This is the most common example.
import (
"database/sql"
_ "github.com/lib/pq"
)
Used when:
- connecting to PostgreSQL, MySQL, SQLite, or SQL Server
- keeping application code written against
database/sql - swapping drivers without changing most business logic
2. Image format support
import (
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
)
Used when:
- reading uploaded images
- processing files in scripts
- supporting multiple formats through one API
3. Plugin-style registration
A package may register:
- commands
- file parsers
- serializers
- metrics exporters
- authentication providers
This is useful when a main application discovers available functionality through a registry rather than hardcoded function calls.
4. Monitoring and instrumentation
Some libraries install tracing, metrics, or hooks during initialization. A blank import can activate them if the package is designed that way.
5. Testing helpers
In some codebases, a package may be blank-imported in tests so it registers test-only behavior before the test runs.
Real Codebase Usage
In real Go projects, blank imports are usually used deliberately and sparingly.
Common pattern: registry-based architecture
A package exposes a generic API and a registration mechanism. Another package imports implementations only for their registration side effects.
Examples:
database/sql+ database driver packagesimage+ format decoder packages- app registry + feature modules
Common pattern: configuration by import
Sometimes adding support is as simple as adding one import:
import _ "myapp/drivers/mysql"
This works well when the implementation package should stay decoupled from the caller.
Guarding startup failures
Developers often verify early that registration happened correctly:
db, err := sql.Open("sqlite3", dsn)
if err != nil {
return err
}
This is a form of early validation.
Keeping imports intentional
Because blank imports are unusual, teams often:
- place comments near them
- group them clearly
- use them only when side effects are required
Example:
import (
"database/sql"
_ "github.com/mattn/go-sqlite3" // register SQLite driver
)
In tests
Sometimes test packages blank-import optional implementations to ensure they are registered before test execution.
What developers avoid
Good Go code usually avoids hidden behavior unless there is a strong reason. If a package can be used directly and clearly, a normal import is often better than relying on side effects.
Common Mistakes
1. Thinking _ means "ignore this package completely"
It does not ignore the package. The package is still imported and initialized.
2. Expecting to call package functions after a blank import
Broken example:
package main
import _ "fmt"
func main() {
fmt.Println("hello")
}
Why it fails:
_does not create the namefmt- the package has no usable identifier in this file
Correct version:
package main
import "fmt"
func main() {
fmt.Println("hello")
}
3. Removing a blank import because it looks unused
Beginners often delete it during cleanup and then get runtime errors.
For example, removing this:
_ "github.com/mattn/go-sqlite3"
can cause:
sql: unknown driver "sqlite3" (forgotten import?)
4. Using a normal import when you do not reference the package
Broken example:
import "github.com/mattn/go-sqlite3"
If you never use the package name directly, Go reports:
imported and not used
Correct version:
import _ "github.com/mattn/go-sqlite3"
5. Overusing side-effect imports
Blank imports can hide important setup work. If used too much, code becomes harder to understand.
Avoid this by:
- using comments
- keeping imports organized
- preferring explicit calls when possible
6. Confusing _ in imports with _ elsewhere in Go
The blank identifier is used in multiple places in Go:
- unused values:
x, _ := fn() - intentionally ignored variables
- side-effect imports
Same symbol, different context.
Comparisons
| Pattern | What it does | Can you use package functions directly? | Typical use |
|---|---|---|---|
import "fmt" | Imports package normally | Yes | Calling exported functions like fmt.Println |
import alias "fmt" | Imports package under a custom name | Yes | Avoiding name conflicts or shortening names |
import . "fmt" | Brings exported names into current file directly | Yes, without fmt. | Rare; often avoided for clarity |
import _ "package/path" | Imports only for side effects | No | Running init() registration code |
Blank import vs normal import
- Normal import: use the package directly.
- Blank import: do not use the package directly; keep only initialization.
Blank import vs explicit setup function
| Approach | Example | Pros | Cons |
|---|---|---|---|
| Blank import | _ "github.com/mattn/go-sqlite3" | Simple for plugin registration | Side effects are less obvious |
| Explicit setup | sqlite3.Register() | Clear and explicit | Requires manual setup call |
Many Go standard patterns use blank imports where registry-based design is intended.
Blank identifier in imports vs variables
| Context | Example | Meaning |
|---|---|---|
| Import | _ "image/png" | Import for side effects only |
| Assignment | _, err := f() | Ignore one returned value |
| Range loop | for _, v := range items | Ignore the index |
The underscore always means "I intentionally do not need a usable name here," but the exact effect depends on where it appears.
Cheat Sheet
Quick reference
Blank import syntax
import _ "package/path"
Meaning
- imports the package
- runs
init()functions - does not create a package name in the file
Use it when
- a package registers itself automatically
- you need side effects only
- you are using registry-based APIs like
database/sqlorimage
Common examples
import _ "github.com/mattn/go-sqlite3"
import _ "image/jpeg"
import _ "image/png"
Typical result
- database driver becomes available
- file decoder becomes available
- plugin registers itself
Important limitation
This is invalid:
import _ "fmt"
func main() {
fmt.Println("hi")
}
Because fmt is not bound as a name.
Common error if missing
sql: unknown driver "sqlite3" (forgotten import?)
Rule of thumb
- If you call package functions directly, use a normal import.
- If you only need startup registration or initialization, use
_. - Add a comment if the side effect is not obvious.
FAQ
Why does Go allow _ in an import?
It allows packages to be loaded for their initialization side effects, usually through init() functions.
Does a blank import execute the package code?
It executes the package initialization code, including variable initialization and init() functions. It does not mean every function runs automatically.
Why is _ "github.com/mattn/go-sqlite3" needed with database/sql?
Because the SQLite driver registers itself with database/sql during initialization. Without that import, sql.Open("sqlite3", ...) would not know about the driver.
Can I use package functions after importing with _?
No. A blank import does not create a package name you can reference in the file.
Is a blank import the same as ignoring an unused import?
No. It is an intentional import for side effects, not a way to silence warnings carelessly.
What is the difference between _ and . in Go imports?
_ imports for side effects only. . imports exported names directly into the current file, which lets you use them without a package prefix.
Should I use blank imports often?
Usually no. Use them when the package is specifically designed for side-effect registration. Otherwise, explicit imports and function calls are clearer.
How can I tell whether a package should be blank-imported?
Check the package documentation. If it says it registers a driver, codec, or plugin on import, a blank import is likely the correct pattern.
Mini Project
Description
Build a small Go program that opens a SQLite database using database/sql and a blank-imported driver. This demonstrates exactly why side-effect imports exist: the application uses the generic SQL API, while the SQLite driver registers itself automatically during package initialization.
Goal
Create a Go program that successfully opens a SQLite database and shows what breaks if the blank import is removed.
Requirements
- Import
database/sqlandfmtnormally. - Blank-import the SQLite driver package.
- Open a SQLite database with
sql.Open("sqlite3", "demo.db"). - Print a success message if the driver loads correctly.
- Handle errors and close the database connection properly.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.