Blog Cover

All you need to know to integrate a SQL database in Go

Author profile image
Aitor Alonso

Nov 26, 2023

Updated Jan 21, 2024

6 min read

In my previous article about creating an fully functional API in Go under 5 minutes, I showed you how to create a web server using only the standard library. There, we learned that Go standard libs are powerful enough to meet our needs.

In this article, and in a similar way to the aforementioned one, I'll show you everything you need to know about the database/sql package in Go, to quickly start integrating a SQL database in your application.

Let's see some snippets! Let's start by how to connect to a database.

Connecting to a database

Connecting to the database can be done with the sql.Open function. This function receives two parameters: the driver name and the connection string (aka data source name, or DSN). The driver name is the name of the driver that you want to use to connect to the database. The DSN is a string that contains the information needed to connect to the database. The DSN format depends on the database that you are using, but an example for PostgreSQL would be postgres://user:password@host:port/database.

// Configure the connection to the database.
db, err := sql.Open("DRIVER_NAME", "CONNECTION_STRING")
if err != nil {
	log.Fatal(err)
}
defer db.Close()

However, the sql.Open function doesn't actually open a connection to the database. It just creates it, by returning a new *sql.DB object. The connection to the database is only opened once yo tries to operate on it for the first time. Therefore, if you have an error with your connection configuration (let's say, you introduced a wrong password or a typo in the database name), you won't know until you try to insert or query some data. How to avoid that? Is a common good practice to use the Ping method of the returned *sql.DB object just after its creation to check if the connection is working.

// Configure the connection to the database.
db, err := sql.Open("DRIVER_NAME", "CONNECTION_STRING")
if err != nil {
	log.Fatal(err)
}
defer db.Close()

// Use Ping() to actually open the connection and check for any errors.
if err = db.Ping(); err != nil {
	log.Fatal(err)
}

Executing a SQL statement

The obtained *sql.DB object has a generic method to execute SQL statements: the Exec method. This method receives a query string and a list of parameters. The query string is the parametrized SQL query that you want to execute, and the parameters are the values that you want to insert into the query string.

Why like that? Why not just passing an string with the SQL query already build? That's because the Exec method already escape the parameters to avoid SQL injection attacks.

Inserting data

The Exec method can be used to insert data into the database. Let's see an example for MySQL:

result, err := db.Exec("INSERT INTO customers (name) VALUES (?)", "Alice")

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

// Note: The LastInsertID() method is not supported by PostgreSQL.
// Check the next snipped for a PostgreSQL example.
id, err := result.LastInsertId()
if err != nil {
	log.Fatal(err)
}

affected, err := result.RowsAffected()
if err != nil {
	log.Fatal(err)
}

About PostgreSQL, it depends if we want to get the ID of the inserted row or not. If we don't want the ID, it's the same as MySQL, but with proper PostgreSQL syntax (parameters are $1, $2, instead of interrogations marks). Let's see it:

result, err := db.Exec("INSERT INTO customers (name) VALUES ($1)", "Alice")

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

affected, err := result.RowsAffected()

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

If we want to get the ID of the inserted row, we need to use the RETURNING clause in the query string, and instead of using the Exec method, we need to use the QueryRow method. This method is used to fetch data from the database, and we will see it in detail in the next section about querying the database. For now, let's see how to use it to get the ID of the inserted row:

var id int
err := db.QueryRow("INSERT INTO customers (name) VALUES ($1) RETURNING id", "Alice").Scan(&id)

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

Updating and deleting data

Obviously, we can use the Exec method to update and delete data too. We just have to take care to use the correct syntax for our database engine. Let's see some examples:

// UPDATE in a MySQL syntax.
result, err := db.Exec("UPDATE customers SET name = ? WHERE id = ?", "Alice", 1)

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

affected, err := result.RowsAffected()

if err != nil {
	log.Fatal(err)
}
// DELETE in a PostgreSQL syntax.
result, err := db.Exec("DELETE FROM customers WHERE id = $1", 1)

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

affected, err := result.RowsAffected()

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

Querying the database

The *sql.DB object also have methods for querying the database. The Query method is used to execute a query that returns multiple rows, and the QueryRow method is used to execute a query that returns a single row. Both methods receive a query string and a list of parameters, similar to the Exec method that we already saw, and return a *sql.Rows object and a *sql.Row object, respectively.

Querying a single row

Let's see an example of how to use the QueryRow method to query a single row from the database. The *sql.Row object returned by the QueryRow method has a Scan method that is used to scan the values of the returned row into variables. It receives a list of pointers to the variables where the values will be stored.

var name, surname string
err := db.QueryRow("SELECT name, surname FROM customers WHERE id = ?", 1).Scan(&name, &surname)
// err := db.QueryRow("SELECT name, surname FROM customers WHERE id = $1", 1).Scan(&name, &surname)

if err == sql.ErrNoRows {
	// There is not a row with the given ID.
	log.Fatal("no rows returned")
} else if err != nil {
	log.Fatal(err)
}

fmt.Printf("Name: %s, Surname: %s\n", name, surname)

Querying multiple rows

The Query method is used to execute a query that returns multiple rows. The *sql.Rows object returned by the Query method has a Next method to iterate over the returned rows. This method returns true if there is a new row to be scanned, and false if there are no more rows to be scanned. The *sql.Rows object also has a Scan method as *sql.Row has, and it's executed over the current row.

rows, err := db.Query("SELECT name FROM customers LIMIT 10")

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

// Don't forget to close the *sql.Rows when you are done.
defer rows.Close()
names := []string{}

for rows.Next() {
	var name string
	err := rows.Scan(&name)
	if err != nil {
		log.Fatal(err)
	}
	names = append(names, name)
}

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

fmt.Printf("Names: %v\n", names)

Transactions

All statements in a transaction use the same database connection, provided by the *sql.DB object. This object has a Begin method to start a new transaction, that returns a *sql.Tx object. The *sql.Tx object has a Commit and a Rollback methods, used to commit and to rollback the transaction respectively. Let's see it.

// Start a new transaction.
tx, err := db.Begin()

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

// Execute some SQL statements inside the transaction.
_, err := tx.Exec("INSERT INTO customers ...")
if err != nil {
	// Rollback the transaction if any of the statements fails.
	tx.Rollback()
	log.Fatal(err)
}

// More SQL statements here
// ...

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

Wrapping up

And that's it. As you saw, the database/sql package has everything you need to integrate a SQL database into your golang application. You can connect to the database, insert and query data, and even use transactions, just using the standard library.

With this new knowledge, you are ready to implement a CRUD application in Golang. And if you want to make it a webapp (like an REST API), remember as I said at the beginning of this article that I wrote another one about the net/http package. Happy coding!


I hope my article has helped you, or at least, that you have enjoyed reading it. I do this for fun and I don't need money to keep the blog running. However, if you'd like to show your gratitude, you can pay for my next coffee(s) with a one-time donation of just $1.00. Thank you!