Connect to MySQL, query rows, and insert data using Go's database/sql package. A visual companion to the official Go tutorial.
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.
database/sql patterns (Open, Query, QueryRow, Exec) apply to any relational database. Only the driver import and connection string change.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:
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 | +----+---------------+----------------+-------+
rows.ScanGo'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.
Create main.go with the necessary imports:
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
init() function calls sql.Register("mysql", …) automatically. You reference it by the name "mysql" when opening a connection — no explicit registration needed.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.
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!
DBUSER and DBPASS — set them before running go run.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:
type Album struct { ID int64 Title string Artist string Price float32 }
Then write the query function:
// 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:
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}]
Query loop pattern: query → iterate with Next → Scan each row → append → Close? parameter placeholders instead of string formatting. This lets the driver send values separately from the SQL text, preventing SQL injection attacks.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.
// 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 }
// 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}
Returns *sql.Rows — a cursor. You loop with Next(), call Scan each iteration, then Close().
Returns *sql.Row — one shot. Call Scan directly. If no row matches, Scan returns sql.ErrNoRows.
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).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.
// 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 }
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
Query for multiple rows, QueryRow for one, Exec for writesPing() to verify.
Open to catch config errors early.
defer rows.Close() and check rows.Err().
Scan.
Result with LastInsertId() and RowsAffected().
&.