In this article, we will discuss how to write type-safe queries in Go. We will use the sqlc
tool to generate Go code from
SQL queries. This will allow us to write type-safe queries in Go.
The main advantage of using sqlc
is that there cannot be any runtime errors due to type mismatches in the queries as
the parameters and returned row types are determined during the compile time.
We’ll also discuss how to use sqlc
alongside pgx
to implement different scenarios like transactions, connection
pools, CTEs and more.
What is sqlc
?
sqlc
is a tool that generates Go code from SQL queries.
It takes SQL files as input alongside migration files and generates Go code that can be used to execute the queries.
Let’s continue with an example to understand how sqlc
works.
Code Generation
Migration file: migrations/000001_init.up.sql
Queries file: queries.sql
Running sqlc
will generate the following Go code:
Models: users/models.go
Queries: users/queries.sql.go
Querier: users/querier.go
The generated interface witch can be used to mock the database queries in tests
is available via setting emit_interface
option in the sqlc
configuration.
You can see that structured parameters and returned types are generated based on the SQL queries and migrations.
Use cases
Before we dive into the specific usages, let’s see a basic user creation with pgxpool:
|
|
- ✅ Type-safe queries
Dependency injection
Now we change the implementation to a more common scenario in web servers:
|
|
This is not a desirable implementation as we only inject the pool and not the Querier.
The Querier should be injected to the server to make it more testable.
Imagine we want to test the CreateUser
function, we need to mock the Querier. However in the current implementation,
we need to mock specific SQL queries.
- ✅ Type-safe queries
- ✅ Transactions
- ❌ Mocking utilities
Injecting Querier
as a dependency
Having emit_methods_with_db_argument
option set in the configuration,
the generated methods also accept a DBTX
interface which is a pgxpool.Pool
instance in this case.
Now we need to inject both the Querier
and the pgxpool.Pool
to the server when instantiating it.
- ✅ Type-safe queries
- ✅ Transactions
- ✅ Mocking utilities
It’s worth mentioning that generating mocks is possible with the mockery.
Notes on Scaling and Error Handling
- It’s always good to have a transaction deferred rollback in case of an error. Even if all the error scenarios are covered at the moment, future changes might introduce security vulnerabilities.
- When a transaction is started, it should be committed or rolled back as soon as possible. Doing heavy or time taking operations such as making an HTTP request inside a transaction blocks the assigned connection until commited.
A slowdown in the HTTP request can cause a bottleneck in the connection pool and prevents other important queries from being executed.
- It’s also a good practice to have a timeout for the queries or entire transaction.
- Failure scenarios should be handled properly. A degraded response is better than a 5xx error.
Common Scenarios and Suitable Workarounds
Interface Segregation / Package Separation
When the project grows, the number of queries increases and the queries.sql
file becomes hard to maintain.
It’s a good idea to separate the queries into different files based on the domain.
This approach however does not result in interface segregation or package separation.
One solution is to have a separate package for each domain.
Nonetheless, the same models are generated multiple times in each package.
Although the generation of unused models can be prevented by setting omit_unused_structs
,
any operation on the models have to be duplicated for each package.
CTEs
Common Table Expressions (CTEs) are a powerful feature in SQL.
It might get quite tricky to use CTEs with sqlc
.
|
|
It is not required to use CTEs this way all the time but, sometimes it solves the potential bug in the code gen.
Conclusion
In general there quite few safe ways to execute queries in Go.
We can use the builtin database/sql
with pgx
for raw queries,
sqlc
for type-safe queries,
Squirrel for query building or
Gorm as a popular ORM *.
These are a spectrum of methods, Each has its own pros and cons. Over the years, the Go community has been evolving and the best practices has changed. In all these ups and downs, it’s been proven that code generation approach is the most reliable and maintainable way to provide reliable, readable and performant software.