Question

Blank Identifier Imports in Go: What `_` Means in an Import Statement

goimport

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.

Take Quiz

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/sql provides a generic database API.
  • go-sqlite3 provides 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:

  • image provides the generic decoding API
  • image/jpeg and image/png register decoders via init()
  • you do not call jpeg.Decode directly
  • image.Decode works 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.

Take Quiz

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 packages
  • image + 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 name fmt
  • 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

PatternWhat it doesCan you use package functions directly?Typical use
import "fmt"Imports package normallyYesCalling exported functions like fmt.Println
import alias "fmt"Imports package under a custom nameYesAvoiding name conflicts or shortening names
import . "fmt"Brings exported names into current file directlyYes, without fmt.Rare; often avoided for clarity
import _ "package/path"Imports only for side effectsNoRunning 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

ApproachExampleProsCons
Blank import_ "github.com/mattn/go-sqlite3"Simple for plugin registrationSide effects are less obvious
Explicit setupsqlite3.Register()Clear and explicitRequires manual setup call

Many Go standard patterns use blank imports where registry-based design is intended.

Blank identifier in imports vs variables

ContextExampleMeaning
Import_ "image/png"Import for side effects only
Assignment_, err := f()Ignore one returned value
Range loopfor _, v := range itemsIgnore the index

The underscore always means "I intentionally do not need a usable name here," but the exact effect depends on where it appears.

Take Quiz

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/sql or image

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.

Take Quiz

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/sql and fmt normally.
  • 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.
Take Quiz

Keep learning

Related questions

View more