To use SQLite in Go, you need a database driver - a client that allows you to connect to and perform operations on the database. Go does not provide any official driver, but it does have a common driver interface for SQL-like databases, defined in the database/sql
package. Most SQL databases implementations use it, which allows the same functions to be used for different databases. One of the most popular such drivers for SQLite is mattn/go-sqlite3, and this driver is used in this tutorial to demonstrate how to make basic operations on an SQLite database in Go.
Click here to go directly to the full code example.
Repository pattern
When exchanging data with a database, it is a good idea not to mix application logic with database logic. This can be done by abstracting the data access layer into a separate structure, responsible for storing and retrieving data from the database. This pattern is known as a Repository. It allows for:
- Reduction of code duplication - you define a data access layer for a given domain object only once.
- Improving readability of your code - business logic is not mixed with data access logic.
- Easy addition of new features and modifications to the data access code, and even easy replacement of the entire database, as the code that operates directly on the DB is hidden in a specific repository implementation.
We strongly advocate using the Repository to exchange data with the database. This tutorial demonstrates how to create a simple repository and use this pattern to create, read, update and delete data from an SQLite database.
Project structure
In our example, we want to create a Website
repository - a repository that stores information about a particular website - its name, URL, and ranking. Let’s start with our project structure tree:
sqlite-intro/
├── main.go
└── website
├── sqlite_repository.go
└── website.go
There are two main parts to this project:
main.go
file - the entry point to our application that initializes the database connection and makes basic operations on a database through the repository. We will present what is inside this file after defining the domain object -Website
, and implementing the repository.website
package - the package responsible for theWebsite
domain. It contains a definition of theWebsite
struct and the SQLite repository implementation to storeWebsite
objects in the SQLite database.
Note that we use packaging by feature in our project to have all the structures related to the
Website
in one place.
To use the repository, we need to define it first, so in the next section, we will start by defining the contents of the website
package.
The website
package
website
├── sqlite_repository.go
└── website.go
Domain object
In the website/website.go
file, we define our domain object, a Website
, which is a struct that contains data about a given website. Objects of this type will be stored in the database and retrieved from it.
package website
type Website struct {
ID int64
Name string
URL string
Rank int64
}
Repository - constructor, errors and Migrate() function
The next step is to create an SQLite implementation of the Website
repository. To do this, we initilize a website/sqlite_repository.go
file, where we define an SQLiteRepository
struct that will interact with the SQLite database. This struct will have the following methods:
Migrate() error
Create(website Website) (*Website, error)
All() ([]Website, error)
GetByName(name string) (*Website, error)
Update(id int64, updated Website) (*Website, error)
Delete(id int64) error
Note that the method definitions do not depend on SQLite at all. This is the purpose of the Repository pattern - hiding database implementation details and providing a simple API to interact with any database. In the future, you can define a Repository
interface:
type Repository interface {
Migrate() error
Create(website Website) (*Website, error)
All() ([]Website, error)
GetByName(name string) (*Website, error)
Update(id int64, updated Website) (*Website, error)
Delete(id int64) error
}
and add new databases repository implementations, for example, MySQLRepository
, PostgresRepository
, etc., if you want to change the database in your application. With the Repository pattern, using a different database is just a matter of calling a constructor of a different repository implementation.
Let’s start with the code of the SQLiteRepository
constructor, the repository errors definition, and the Migrate()
method implementation:
|
|
- In lines
17-25
, we define theSQLiteRepository
struct and its constructor. Note that it requires an instance ofsql.DB
type as a dependency. Thesql.DB
is an object representing a pool of DB connections for all drivers compatible with thedatabase/sql
interface. - In lines
10-15
, we define all errors that can be returned by methods of this repository. It is a good practice to return your own defined errors instead of the errors returned by functions ofdatabase/sql
package or driver-specific errors. This will make the repository driver-independent and easier to modify in the future. - In lines
27-39
, we create theMigrate()
method, which is responsible for migrating the repository. Migration, in this case, is creating an SQL table and initializing all the data necessary to operate on the repository. When working on a fresh database instance, this function should be called first, before reading or writing data through the repository. The logic of theMigrate()
is simple - it executes theCREATE TABLE
SQL query usingDB.Exec()
method and returns the error.
Create a new record in the SQLite database
After defining SQLiteRepository
, its constructor, and the Migrate()
method, we want to create a function to write records to the database. This is the purpose of the Create()
method, which takes a row to create and returns the row after insertion or an error if the operation fails.
|
|
Like Migrate()
, this function uses the DB.Exec()
method to execute an SQL INSERT
query. If there is an error, we check if it is an instance of sqlite3.Error
and if its code indicates an SQLite unique constraint violation. It means that a record with the same UNIQUE
field (the same name
in the websites
table) already exists, so we can map this error to ErrDuplicate
, which we defined before. In the last part, we take the inserted record ID and assign it to the returned object so that it reflects the state in the database.
Many
DB
methods, likeDB.Exec()
orDB.Query()
, take a query and arguments as input parameters. As you can see in the example above, you need to use the?
character to indicate where the subsequent arguments should be inserted into the query.
Read from SQLite database
To read Website
records from the repository, we use two methods:
All() ([]Website, error)
which returns all available records in theWebsite
repositoryGetByName(name string) (*Website, error)
that gives back aWebsite
with the specified name
|
|
The All()
method uses the DB.Query()
to return rows for the SQL SELECT
query. The difference between the DB.Query()
and DB.Exec()
methods is that the former is used for the queries that return rows, the latter for the queries that do not. As the result of the DB.Query()
, we get an sql.Rows
struct that represents a cursor to SQL rows. Notice that it should be closed at the end of the function. Using two methods: Next()
returning true if there are more rows in the result, and Scan()
that copies successive values of the result set into the given variables, we can create a slice of all websites in the database table.
The GetByName()
method works in a similar way, but instead of DB.Query()
it uses DB.QueryRow()
that returns at most one row. This eliminates the need to close the structure. To copy values to a Website
object, we use the same Scan()
method as before. We also check if the Scan()
returns a standard sql.ErrNoRows
error if there is no record in the result. In such a case, we map this error to our repository ErrNotExists
error.
Update a row of the SQLite database
The Update()
method is not significantly different from the previous ones. It uses the DB.Exec()
to execute the SQL UPDATE
query that replaces values for a record with a given ID. It then checks how many rows were affected by this update. If zero, we consider it failed and return the ErrUpdateFailed
error.
|
|
Delete a row from the SQLite database
The Delete()
method works similarly to Update()
. It executes the SQL DELETE
query to delete the row with the specified ID from the database. If no row is affected, it returns the ErrUpdateFailed
error.
|
|
In this way, we have created a complete SQLite repository with CRUD operations. Now it’s time to test it by creating a simple app that demonstrates how its functions work.
The main()
function
Connect to SQLite
|
|
- To connect to an SQLite database using the mattn/go-sqlite3 driver, it is necessary to register it as the
database/sql
driver. It is done by importing the driver package in line10
(we do this using a blank identifier, to import the package, even though it is not used by the current program). Once imported, it calls theinit()
function, which registers the driver in thedatabase/sql
interface under the namesqlite3
. - Using the
sql.Open()
function with the registeredsqlite3
driver name, you can connect to a new SQLite database. The second argument is the data source name which in the case of SQLite is a path to the database file. In our example, we want to run the program with a fresh instance each time, so the first line ofmain()
deletes the old database file if it exists.
Init the SQLite repository
|
|
- In lines
23-27
, we create a new website SQLite repository and migrate the data, which in our case means that we create a new SQLwebsites
table. - In lines
29-47
, we create newWebsite
objects and insert them into the database using theCreate()
method.
Read, update, delete in the repository
|
|
- In lines
49-54
, we retrieve the record with theGOSAMPLES
name from the database and print it to the console. - In lines
56-69
, we make a ranking update of the retrieved record and then get all records from the table to ensure that the update was performed successfully. - In lines
71-82
, we delete the second row by ID, and also get and print all records to ensure that the database state is correct.
Follow the output below to check the results of these operations:
get by name: &{ID:1 Name:GOSAMPLES URL:https://gosamples.dev Rank:2}
All websites:
website: {ID:1 Name:GOSAMPLES URL:https://gosamples.dev Rank:1}
website: {ID:2 Name:Golang official website URL:https://golang.org Rank:1}
All websites:
website: {ID:1 Name:GOSAMPLES URL:https://gosamples.dev Rank:1}
As you can see, using SQLite in Go is really simple and no different than using MySQL, Postgres, or any other SQL database, thanks to the common database/sql
interface. By using the Repository pattern, you can also make the code clean and easy to understand, where the business and data access logic are not mixed.
Full example
The example is also available on Github here.
website/website.go
|
|
website/sqlite_repository.go
|
|
main.go
|
|