MySQL and Go: A Comprehensive Guide

In the realm of web and application development, the pairing of efficient languages with robust databases is a foundational concept. Go, often known as “Golang”, is an open-source programming language developed by Google engineers. With its simplicity, efficiency, and robustness, it has rapidly carved a niche for itself in the backdrop of backend development. MySQL, on the other hand, stands tall as one of the world’s most widely utilized relational database management systems (RDBMS). Together, Go and MySQL offer a potent combination, promising speedy development, scalability, and optimal performance.

This guide seeks to navigate developers through the nuances of integrating MySQL within Go applications. Whether you’re laying the foundation for a new project or integrating into an existing one, the symbiotic relationship between Go and MySQL can streamline workflows, ensuring your applications are both efficient and data-driven. So, strap in as we embark on this journey, elucidating best practices, handy tips, and code snippets to make your development process smoother and more efficient.

Getting Started

Before you can start using MySQL with Go, you need to install the MySQL Go driver. You can download it using Go’s package manager. Run the following command in your terminal inside your project’s root directory:

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

Connecting to MySQL from Go

With the driver installed, you can now establish a connection to MySQL from your Go application. Here’s a basic example:

package main

import (
    "database/sql"
    "log"
    _ "github.com/go-sql-driver/mysql"
)

func main() {
    // Context
    ctx, stop := context.WithCancel(context.Background())
    defer stop()

    appSignal := make(chan os.Signal, 3)
    signal.Notify(appSignal, os.Interrupt)

    go func() {
        <-appSignal
        stop()
    }()

    // Use parseTime=true to scan MySQL DATETIME type to Go time.Time
    dsn := "root:mysql@tcp(127.0.0.1:3306)/test_db?parseTime=true"

    db, err := sql.Open("mysql", dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    db.SetConnMaxLifetime(0)
    db.SetMaxIdleConns(3)
    db.SetMaxOpenConns(3)

    // Open connection
    OpenDbConnection(ctx, db)
}

func OpenDbConnection(ctx context.Context, db *sql.DB) {
   ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
   defer cancel()

   if err := db.PingContext(ctx); err != nil {
      log.Fatal("Unable to connect to database: %v", err)
   }
}

The OpenDbConnection function is used to connect to MySQL, and the connection string is specified in dsn variable using root user with mysql password. This is solely done for simplicity reasons and you should never embed credentials for your production database inside your code or ever commit to a git repository. Instead, use environmental variables or a security vault for storing your passwords.

Create Table and Insert Data

Now that the connection has been established, you can perform various database operations. Here’s a simple way to create a table:

// Drop table if exists
_, err = db.Exec("DROP TABLE IF EXISTS employee")
if err != nil {
    log.Fatal(err)
}

// Create table
_, err = db.Exec(`
    CREATE TABLE employee (
        id INT,
        name VARCHAR(20),
        start_dt DATETIME,
        is_remote TINYINT -- MySQL doesn't support BOOLEAN, use TINYINT instead
    )`)

if err != nil {
   log.Fatal(err)
}

Next, to insert data into the employee table, you could use the Exec function with an INSERT INTO SQL command:

timeout, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

_, err := db.ExecContext(timeout, `
    INSERT INTO employee (id, name, start_dt, is_remote)
    VALUES
        (1, 'John Doe', '2022-01-01 09:00:00', 1),
        (2, 'Jane Smith', '2023-03-15 10:00:00', 0)`)

if err != nil {
    log.Fatal(err)
}

When dealing with larger sets of data, consider using transactions and prepared statements for efficiency and safety. Remember, always handle errors and close your connections when done.

Query Single or Multiple Rows

To fetch data, you can use the QueryRow() function to select a single row:

// Query single row
var version string

if err = db.QueryRow("SELECT @@version").Scan(&version); err != nil {
    if err == sql.ErrNoRows {
        log.Println("No rows found.")
    } else {
        log.Fatalf("unable to execute query: %v", err)
    }
} else {
    fmt.Printf("SERVER VERSION: %s\n", version)
}

Don’t forget to handle any errors and properly close your connections and result sets, as shown above.

To select multiple rows, you can use QueryContext() function:

var id int
var name string
var startDt time.Time
var isRemote bool

timeout, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

rows, err := db.QueryContext(timeout, "SELECT id, name, start_dt, is_remote FROM employee")
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

// Print the results
fmt.Println("Results:")
for rows.Next() {
    err = rows.Scan(&id, &name, &startDt, &isRemote)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("ID:", id, "Name:", name, "Start Datetime:", startDt, "Is Remote:", isRemote)
}

err = rows.Err()
if err != nil {
    log.Fatal(err)
}

In this code:

  • We’re calling the QueryContext function with an SQL command to select all records from the employee table.
  • We then enter a loop using rows.Next(), which iterates over each row returned by the query.
  • Within the loop, we’re using the Scan function to copy the columns from the current row into the id, name, startDt, and isRemote variables.
  • We then print these variables using the fmt.Println function.
  • After the loop, we check for errors during the iteration with rows.Err(). If there’s an error, we print it using log.Fatal(err).

Error Handling and Transactions

In the real world, your Go code must be ready to handle errors and work with transactions. The sql package provides all the necessary tools:

// Error handling and transactions
tx, err := db.Begin()
if err != nil {
    log.Fatal(err)
}

defer tx.Rollback()
_, err = tx.Exec(`
    INSERT INTO employee (id, name, start_dt, is_remote)
    VALUES
       (3000000000, 'id int64 instead of int32', '2022-06-17 11:00:00', 1)`)

if err != nil {
    log.Printf("ERROR: %s\n", err.Error()) // Do not fail, just print the error in output
}

err = tx.Commit()
if err != nil {
    log.Fatal(err)
}

This code begins a transaction, tries to execute an insert statement, and then commits the transaction. If an error occurs during execution, it will rollback any changes made in that transaction.

Conclusion

As we come to the end of our exploration into integrating MySQL with Go, it’s evident that the synergies between these two technologies are profound. Go’s inherent efficiency, speed, and simplicity combined with MySQL’s robustness and reliability form a powerful duo that can cater to a wide array of application needs. Leveraging this combination enables developers to craft applications that are not only performant but also scalable and maintainable.

In the ever-evolving world of technology, it’s essential to equip ourselves with the tools and knowledge that drive seamless integration and development. The confluence of Go and MySQL is a testament to the advancements in this domain. With the insights provided in this guide, you’re now well-prepared to harness this potent pairing to its fullest potential. May your development journey with Go and MySQL be smooth, efficient, and, above all, innovative. Happy coding!

The full source code for this example could be found on my GitHub repo: https://github.com/solontsev/go-cookbook/tree/main/mysql