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.
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.
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:
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
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:
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"
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 registration code |
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()
}
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.
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
Check if a Value Exists in a Slice in Go
Learn how to check whether a value exists in a slice in Go, and why Go has no Python-style `in` operator for arrays or slices.
Concatenating Slices in Go with append
Learn how to concatenate two slices in Go using append and the ... operator, with examples, pitfalls, and practical usage.
Constructors in Go: Best Practice for Struct Initialization
Learn how to initialize structs with default values in Go using constructor-style functions, zero values, and idiomatic patterns.