Go
MySQL
database-connection
programming
software-development

What's the recommended way to connect to MySQL from Go?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

The recommended way to connect to MySQL from Go is to use the standard database/sql package with the go-sql-driver/mysql driver. That combination gives you connection pooling, context support, prepared statements, and portability without committing you to a heavier abstraction too early.

Use database/sql With The MySQL Driver

Install the driver:

bash
go get github.com/go-sql-driver/mysql

Then open the database handle:

go
1package main
2
3import (
4    "context"
5    "database/sql"
6    "fmt"
7    "log"
8    "time"
9
10    _ "github.com/go-sql-driver/mysql"
11)
12
13func main() {
14    dsn := "appuser:secret@tcp(127.0.0.1:3306)/appdb?parseTime=true"
15
16    db, err := sql.Open("mysql", dsn)
17    if err != nil {
18        log.Fatal(err)
19    }
20    defer db.Close()
21
22    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
23    defer cancel()
24
25    if err := db.PingContext(ctx); err != nil {
26        log.Fatal(err)
27    }
28
29    fmt.Println("Connected to MySQL")
30}

Two details matter immediately:

  • 'sql.Open validates the arguments and creates a pooled handle, but it does not establish the connection yet'
  • 'PingContext is the explicit connectivity check'

That pattern is more reliable than assuming sql.Open alone proves the database is reachable.

Configure The Connection Pool

The *sql.DB value is not a single connection. It is a concurrency-safe pool manager. That is why you should create it once and reuse it across the application.

A good starting configuration:

go
1db.SetMaxOpenConns(25)
2db.SetMaxIdleConns(25)
3db.SetConnMaxLifetime(5 * time.Minute)
4db.SetConnMaxIdleTime(1 * time.Minute)

The right numbers depend on your workload and database limits, but the design principle is stable: keep one shared pool, not one new sql.Open per request.

Repeatedly opening and closing handles inside request handlers is a common performance mistake.

Use Context-Aware Queries

Once connected, use QueryContext, QueryRowContext, and ExecContext so timeouts and cancellations propagate cleanly.

go
1type User struct {
2    ID   int64
3    Name string
4}
5
6func fetchUser(ctx context.Context, db *sql.DB, id int64) (User, error) {
7    var user User
8
9    row := db.QueryRowContext(
10        ctx,
11        "SELECT id, name FROM users WHERE id = ?",
12        id,
13    )
14
15    err := row.Scan(&user.ID, &user.Name)
16    return user, err
17}

This is the standard idiomatic style in modern Go. It keeps database operations integrated with request deadlines in web servers and background job cancellation in workers.

Parse Time Values Correctly

MySQL date and datetime values often surprise Go developers if the DSN omits parseTime=true. Without it, time-related columns may scan as byte slices or strings depending on the driver behavior and query context.

That is why the DSN example included:

text
?parseTime=true

If your application reads timestamp columns, this option is usually the right default.

Keep SQL Simple And Explicit

A small repository function is often enough:

go
1func insertUser(ctx context.Context, db *sql.DB, name string) (int64, error) {
2    result, err := db.ExecContext(
3        ctx,
4        "INSERT INTO users(name) VALUES(?)",
5        name,
6    )
7    if err != nil {
8        return 0, err
9    }
10
11    return result.LastInsertId()
12}

For many services, database/sql plus a few focused query functions is simpler and easier to debug than introducing an ORM too early.

If you later want convenience helpers, libraries such as sqlx can layer on top without replacing the core model entirely.

Handle Credentials Carefully

Do not hardcode production credentials into the source file. Build the DSN from configuration or environment variables.

Example:

go
1dsn := fmt.Sprintf(
2    "%s:%s@tcp(%s:%s)/%s?parseTime=true",
3    user,
4    password,
5    host,
6    port,
7    database,
8)

That still uses the same underlying approach, but it keeps secrets out of the codebase.

Common Pitfalls

The biggest mistake is calling sql.Open for every query or every HTTP request. *sql.DB is meant to be long-lived and shared.

Another mistake is skipping PingContext during startup and discovering connectivity problems only after the application begins serving traffic.

People also forget parseTime=true, then run into awkward scanning bugs with MySQL datetime fields.

Finally, avoid string interpolation for SQL values. Use placeholders with query arguments so the driver can bind parameters safely.

Summary

  • The standard recommendation is database/sql with go-sql-driver/mysql.
  • Create one shared *sql.DB and configure its connection pool deliberately.
  • Use PingContext to verify connectivity and context-aware query methods for real work.
  • Include parseTime=true when you need proper time scanning.
  • Prefer parameterized queries and configuration-driven DSN construction.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.