Accessing a Relational Database in Golang
Time to read: 8 minutes
Accessing a Relational Database in Go
Every time you visit an application that requires authentication, such as allowing users to log in or register, the application must use a database. The database and the stored data in it allow the application to remember you.
There are different types of databases, but this tutorial focuses on relational databases (RDB) and how a Go application can be connected to one. Data is stored in a relational database in tables. These tables have relationships with each other.
This tutorial will explore connecting a Go application to an SQLite database, and show how to perform CRUD (Create, Read, Update, and Delete) operations, and joining queries.
Prerequisite
To follow along, the following are required
- The latest version of Go (1.22 at the time of writing)
- SQLite
- Your preferred text editor or IDE
- Prior experience with relational databases
- Working knowledge of Go
Key relational database concepts
Data in an RDB is stored in tables, similar to spreadsheets, consisting of rows (records) and columns (properties). Let's examine some fundamental concepts of RDBs:
- Table: A table in a relational database is a collection of related data. It consists of rows and columns with descriptive names.
- Row: A row is a collection of information about a particular record in a table. An example of this can be a row containing a user's first name, last name, and date of birth.
- Column: A column contains descriptive properties or attributes of the data stored in each row. For example, the user's first name, last name, and date of birth
- Primary keys: A primary key is a unique identifier for records in a row within a database table, much like a fingerprint for each record. It ensures that each entry is distinct. Similar to unique identifiers in the real world such as ISBNs for books, flight numbers for airline trips, or registration numbers for courses.
- Foreign keys: Foreign keys link two tables together. They contain the column in the joining table with the column in the joined table.
- Indexes: Indexes help speed up the retrieval of data by providing rapid access paths to the stored information. For example, an index could allow a database to quickly locate all records where the last name is "Smith" without scanning every entry in the table.
Popular relational databases
PostgreSQL, MySQL, and SQLite are among the most popular relational database systems, each effectively serving different needs.
- PostgreSQL is renowned for its robustness, extensive feature set, and substantial compliance with SQL standards, making it a favorite for enterprises and complex applications requiring advanced functionality.
- MySQL is widely acclaimed for its speed and reliability. It is commonly used in web applications and is known for its ease of use and efficient performance.
- SQLite offers a unique advantage with its serverless architecture, embedding directly into applications with minimal setup, making it ideal for environments with limited resources.
We will use SQLite for this tutorial, because it is quicker and easier to set up, and does not require a separate server or a complex configuration
Build the Go application
We need to create a fresh project using the command below to get started.
Open the main.go file and add the following code to it
The code above creates a database named school.db
, creates a table called students
, and connects our application to the database.
initDB
holds the logic for the database connection and table creation. It accepts a string named dataSourceName, which will be the database name when the function is called. It uses the *sql.DB
pointer to establish the database connection and create the table.
To get this code to work, we must run the following command to install the github.com/mattn/go-sqlite3 package .
Once the installation is complete, we can test using the command below.
If everything is done right, the following should be printed in your console.
That's it! We have successfully connected our Go application to an SQLite database. Nice and smooth.
Add CRUD functionality
Now, let's take it further by performing a create, read, update, and delete (CRUD) database operation. Let's create a struct of type Student
in your main.go file. Just below the import
statement, add the following.
Add the create operation
We need to create a function for the create operation. Below the initDB()
function, add the following.
The createData()
function adds a new student's data into the students
table, and is structured as follows:
- Parameters: It takes a
Student
struct containing two fields:Name
andAge
- Database operation: The function executes an SQL insert statement to add the student's name and age to the
students
table. The placeholders (?
) in the SQL query are replaced by the student's name and age to protect against SQL injection attacks . - Error handling: If the
createData()
function runs with an error, it immediately returns an error message with a detailed explanation of what went wrong - Retrieve ID: After successfully inserting the data, the function retrieves the newly added student's ID. This ID helps uniquely identify the student within the database.
- Return values: If the insertion is successful, it returns the new student's ID. Otherwise, if an error occurs, it returns
0
for the ID and an error message.
We need to call the function in the main()
function. To do that, add the following just below fmt.Println("Successfully connected to the database.")
in the main()
function.
We can test that this works using the command below.
You should see the following output:
Add the read operation
Let's proceed to retrieving data from the student
table. Add the following function below the function named createData()
.
The readData()
function retrieves and displays student data from a database. Let's explain how it works.
Query Execution: It executes an SQL query to fetch the id, name, and age columns from the
students
table. If the query fails, it returns an error.Data Processing: It scans the values into variables for each row and prints them out. Errors during scanning are returned immediately.
Error Handling: After processing all rows, it checks for any residual errors and returns them
Resource Management: It ensures all database resources are correctly closed after use
The next step is to call readData()
in the main()
function. Add the following code at the end of the main()
function.
Add the update operation
In this section, we will carry out update operations. To get started, let's create a function for this action. Add the following at the bottom of main.go after the readData()
function.
The updateData()
function updates a student's age in a database for the student name provided. Here’s a simple explanation of how it functions:
Updating data: The function takes a
Student
object as input and uses it to update the student's age in the database where the student's name matchesError checking: If the update command fails, it returns an error message to inform you of what went wrong
Confirming changes: This step checks how many rows in the database were affected by the update. If no rows are affected, no student with that name was found.
Returning results: If the update is successful, it returns the
Student
object. If not, it provides an error explaining whether the operation failed or the student was not found.
Let's call updateData()
in main()
by adding the following code just below the call to readData()
.
Replace <<STUDENT NAME IN DB>>
and <<STUDENT AGE IN DB>>
with the corresponding student details in your database, then run the code again to update the student.
Add a delete operation
Let's now create a function for the delete operation. This function will accept the name of the student to be deleted as an argument. Add the code below in main.go, just below the updateData()
function.
The function takes a student struct and deletes the user with the name provided in the function call. To use this function, add the following code at the bottom of the main()
function.
We have completed the CRUD operation, so this would be a good time to test. Replace the placeholders with the corresponding database value and comment out method calls that you might not need for each test. Then, use the command below to run the code.
Add some advanced database operations
SQL joins explained
You will most certainly encounter SQL joins when developing applications that handle complex data relationships. Joins allow you to combine rows from two or more tables based on a related column between them, often a foreign key.
Types of joins:
INNER JOIN: This type of join returns records with matching values in both tables
LEFT JOIN (or LEFT OUTER JOIN): In this type of join, all records from the left table and the matched records from the right table are returned. If there is no match, the result is NULL for the right table
RIGHT JOIN (or RIGHT OUTER JOIN): This is the opposite of a LEFT JOIN; it returns all records from the right table and the matched records from the left table
FULL JOIN (or FULL OUTER JOIN): Returns all records when a match exists in either the left or right table
Integrate students and courses
Let's take a practical approach by making a simple join SQL query to our students
table and a new courses
table. Let's start by creating a courses
table related to our pre-existing students
table, with a reference to students
via a foreign key.
Update the initDB()
function with the code below, and replace the placeholders with real data.
This revised function creates a courses
table where:
course_id
is the primary keycourse_name
is a text field that cannot beNULL
student_id
links each course to a student in thestudents
table
Let's use an inner join to retrieve data from these tables. We will create a new function for this. Add the following to the end of the main.go file.
The function performs an SQL inner join between the students
and courses
tables using the student's ID as the linking identifier. The function filters the results to include only those entries where the student's name matches the studentName
parameter passed to the function. It executes this query with a placeholder to prevent SQL injection by using parameterized queries.
If the query successfully retrieves rows, it iterates over them, extracting the student and course names and printing the result. If any errors occur during the query execution, row scanning, or while checking for errors post-iteration, the function handles them gracefully by returning a formatted error message.
Now, we need to call innerJoin()
in main()
. Add the following at the end of the main()
function.
For the scope of this tutorial, we will not explore joins in SQL too deeply. However, if you want to explore this topic further, check out the documentation.
That's how to access relational databases in Go
We have come to the end of this tutorial. If you followed along to this point, thumbs up. Let's recap the key concepts we explored.
We covered the essentials of setting up and accessing an SQLite database in Go. We took it further by performing CRUD operations and executing SQL join queries.
SQLite is an excellent choice for applications that require a lightweight, self-contained database engine. For further learning, visit the SQLite documentation and the database/sql package's documentation.
Moses Anumadu is a software developer and online educator who loves to write clean, maintainable code. He's creating something awesome for Laravel developers with Laravel.
The database icon in the tutorial's main image was created by phatplus on Flaticon.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.