Visual Go Tutorial

Accessing a Relational Database

Connect to MySQL, query rows, and insert data using Go's database/sql package. A visual companion to the official Go tutorial.

Go 1.18+ · ~20 min read · 6 steps
The big picture

How Go talks to databases

Go's database/sql package provides a generic interface for relational databases. You never talk to MySQL directly — a driver translates your calls into database-specific wire protocol. Your code stays portable across databases.

Your Go code main.go database/sql Standard library Driver go-sql-driver MySQL Swap the driver to switch databases — your code stays the same
Go's database access architecture — your code → standard library → driver → database
i
This tutorial uses MySQL, but the database/sql patterns (Open, Query, QueryRow, Exec) apply to any relational database. Only the driver import and connection string change.
Step 01

Set up the database

Create a MySQL database with a table of vintage jazz albums. This gives you real data to query against in later steps.

$ mkdir data-access && cd data-access
$ go mod init example/data-access
go: creating new go.mod: module example/data-access

Log into MySQL and create the database:

$ mysql -u root -p
Enter password:

mysql> create database recordings;
mysql> use recordings;

Create a SQL script to set up the album table with sample data:

create-tables.sql SQL
DROP TABLE IF EXISTS album;
CREATE TABLE album (
  id         INT AUTO_INCREMENT NOT NULL,
  title      VARCHAR(128) NOT NULL,
  artist     VARCHAR(255) NOT NULL,
  price      DECIMAL(5,2) NOT NULL,
  PRIMARY KEY (`id`)
);

INSERT INTO album (title, artist, price)
VALUES
  ('Blue Train', 'John Coltrane', 56.99),
  ('Giant Steps', 'John Coltrane', 63.99),
  ('Jeru', 'Gerry Mulligan', 17.99),
  ('Sarah Vaughan', 'Sarah Vaughan', 34.98);
mysql> source /path/to/create-tables.sql
mysql> select * from album;
+----+---------------+----------------+-------+
| id | title         | artist         | price |
+----+---------------+----------------+-------+
|  1 | Blue Train    | John Coltrane  | 56.99 |
|  2 | Giant Steps   | John Coltrane  | 63.99 |
|  3 | Jeru          | Gerry Mulligan | 17.99 |
|  4 | Sarah Vaughan | Sarah Vaughan  | 34.98 |
+----+---------------+----------------+-------+
MySQL table id title artist price 1 Blue Train Coltrane 56.99 2 Giant Steps Coltrane 63.99 INT VARCHAR VARCHAR DECIMAL Go struct type Album struct ID int64 Title string Artist string Price float32 Each table column maps to a struct field — Scan bridges the two
SQL columns map directly to Go struct fields via rows.Scan
Step 02

Import the database driver

Go's standard library defines the database/sql interface, but you need a third-party driver that speaks MySQL's wire protocol. The driver registers itself at init time.

data-access/
go.mod
main.go
create-tables.sql

Create main.go with the necessary imports:

main.go Go
package main

import (
    "database/sql"
    "fmt"
    "log"
    "os"

    "github.com/go-sql-driver/mysql"
)
$ go get .
go: added filippo.io/edwards25519 v1.1.0
go: added github.com/go-sql-driver/mysql v1.8.1
The driver's init() function calls sql.Register("mysql", …) automatically. You reference it by the name "mysql" when opening a connection — no explicit registration needed.
Step 03

Connect to the database

Use sql.Open to create a database handle and db.Ping to verify the connection works. The handle manages a connection pool internally and is safe for concurrent use.

main.go Go
var db *sql.DB

func main() {
    // Capture connection properties.
    cfg := mysql.NewConfig()
    cfg.User = os.Getenv("DBUSER")
    cfg.Passwd = os.Getenv("DBPASS")
    cfg.Net = "tcp"
    cfg.Addr = "127.0.0.1:3306"
    cfg.DBName = "recordings"

    // Get a database handle.
    var err error
    db, err = sql.Open("mysql", cfg.FormatDSN())
    if err != nil {
        log.Fatal(err)
    }

    pingErr := db.Ping()
    if pingErr != nil {
        log.Fatal(pingErr)
    }
    fmt.Println("Connected!")
}
$ export DBUSER=username
$ export DBPASS=password
$ go run .
Connected!
1. Build config mysql.NewConfig() User, Passwd, Addr, DB 2. Open handle sql.Open("mysql", dsn) Returns *sql.DB 3. Verify db.Ping() Actually connects now Why two steps? sql.Open validates the DSN but may not connect immediately. db.Ping forces a real connection so you catch errors early.
Three-step connection: configure → open handle → verify with Ping
!
Store credentials in environment variables, not in source code. This tutorial uses DBUSER and DBPASS — set them before running go run.
Step 04

Query for multiple rows

Use db.Query to run a SELECT that may return multiple rows, then loop through them with rows.Next() and rows.Scan to populate Go structs.

First, define the Album struct to hold row data:

main.go Go
type Album struct {
    ID     int64
    Title  string
    Artist string
    Price  float32
}

Then write the query function:

main.go Go
// albumsByArtist queries for albums that have the specified artist name.
func albumsByArtist(name string) ([]Album, error) {
    var albums []Album

    rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name)
    if err != nil {
        return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
    }
    defer rows.Close()

    // Loop through rows, using Scan to assign column data to struct fields.
    for rows.Next() {
        var alb Album
        if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
            return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
        }
        albums = append(albums, alb)
    }
    if err := rows.Err(); err != nil {
        return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
    }
    return albums, nil
}

Call it from main:

main.go — inside main() Go
albums, err := albumsByArtist("John Coltrane")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Albums found: %v\n", albums)
$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
db.Query(sql, args) Sends query to MySQL *sql.Rows Cursor over results for rows.Next() rows.Scan(&fields...) Copies columns → struct append(albums, alb) Grows the result slice defer rows.Close() Releases resources After the loop: check rows.Err() then return []Album
The Query loop pattern: query → iterate with Next → Scan each row → append → Close
Always use ? parameter placeholders instead of string formatting. This lets the driver send values separately from the SQL text, preventing SQL injection attacks.
Step 05

Query for a single row

When you know a query returns at most one row, use db.QueryRow instead. It's simpler — no loop needed, and it returns the error through Scan.

main.go Go
// albumByID queries for the album with the specified ID.
func albumByID(id int64) (Album, error) {
    var alb Album

    row := db.QueryRow("SELECT * FROM album WHERE id = ?", id)
    if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
        if err == sql.ErrNoRows {
            return alb, fmt.Errorf("albumsById %d: no such album", id)
        }
        return alb, fmt.Errorf("albumsById %d: %v", id, err)
    }
    return alb, nil
}
main.go — inside main() Go
// Hard-code ID 2 here to test the query.
alb, err := albumByID(2)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Album found: %v\n", alb)
$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
Album found: {2 Giant Steps John Coltrane 63.99}

db.Query (multiple rows)

Returns *sql.Rows — a cursor. You loop with Next(), call Scan each iteration, then Close().

db.QueryRow (single row)

Returns *sql.Row — one shot. Call Scan directly. If no row matches, Scan returns sql.ErrNoRows.

i
QueryRow doesn't return an error itself — it defers the error to Scan. The special sql.ErrNoRows error indicates the query found nothing, which you should handle as a normal case (not a crash).
Step 06

Add data

Use db.Exec to run INSERT, UPDATE, or DELETE statements — anything that doesn't return rows. Get the new row's ID from the result.

main.go Go
// addAlbum adds the specified album to the database,
// returning the album ID of the new entry
func addAlbum(alb Album) (int64, error) {
    result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)",
        alb.Title, alb.Artist, alb.Price)
    if err != nil {
        return 0, fmt.Errorf("addAlbum: %v", err)
    }
    id, err := result.LastInsertId()
    if err != nil {
        return 0, fmt.Errorf("addAlbum: %v", err)
    }
    return id, nil
}
main.go — inside main() Go
albID, err := addAlbum(Album{
    Title:  "The Modern Sound of Betty Carter",
    Artist: "Betty Carter",
    Price:  49.99,
})
if err != nil {
    log.Fatal(err)
}
fmt.Printf("ID of added album: %v\n", albID)
$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
Album found: {2 Giant Steps John Coltrane 63.99}
ID of added album: 5
Three ways to talk to the database db.Query() SELECT → multiple rows Returns *sql.Rows Loop + Scan + Close db.QueryRow() SELECT → one row Returns *sql.Row Scan only db.Exec() INSERT / UPDATE / DELETE Returns sql.Result LastInsertId / RowsAffected All three use ? placeholders for safe parameter binding db.Query("... WHERE artist = ?", name) Pick the method that matches your SQL statement's return shape
Choose the right method: Query for multiple rows, QueryRow for one, Exec for writes
Reference

database/sql cheat sheet

sql.Open(driver, dsn) Create a database handle. Does not necessarily connect immediately — call Ping() to verify.
db.Ping() Verify the database is reachable. Use right after Open to catch config errors early.
db.Query(sql, args...) Execute a SELECT returning multiple rows. Always defer rows.Close() and check rows.Err().
db.QueryRow(sql, args...) Execute a SELECT returning at most one row. Error is deferred to Scan.
db.Exec(sql, args...) Execute INSERT, UPDATE, DELETE. Returns a Result with LastInsertId() and RowsAffected().
rows.Scan(&fields...) Copy column values from the current row into Go variables. Pass pointers with &.
Summary

The full workflow

Connect Open + Ping Read many Query + loop Read one QueryRow + Scan Write Exec + Result All operations use ? placeholders and check errors at every step
Content adapted from go.dev. Original material © Google, licensed under Creative Commons Attribution 4.0. Visual tutorial design by Arafat.