
Connecting to a SQL database in Go is straightforward with the help of the database/sql package. You can create a connection pool with the sql.Open() function, which returns a DB object. This object is used to interact with the database.
To establish a connection, you need to provide the database driver and the connection string. For example, with the PostgreSQL driver, you would use the "postgres" driver name and a connection string in the format "user:password@host:port/dbname". The connection string may vary depending on the database system you're using.
The sql.DB object returned by sql.Open() supports connection pooling, which means you can reuse existing connections to reduce overhead. This is particularly useful for applications that need to handle a large number of concurrent requests. The connection pool is automatically managed by the database/sql package, so you don't need to worry about it.
For more insights, see: Sql Server vs Azure Sql
Database Connections
Database connections are managed by the database/sql package, which aims to minimize the creation of new connections by reusing existing ones. This is achieved through the use of an open connection pool, which consists of idle and active connections.
A fresh viewpoint: Create Azure Sql Database
The maximum number of open connections can be configured using the SetMaxOpenConns method, which sets the maximum number of open connections to the database. If the new maximum is less than the current maximum, the idle connections will be reduced to match the new limit.
To configure the connection pool, you can use the following methods: SetMaxOpenConns, SetMaxIdleConns, and SetConnMaxLifetime. These methods allow you to set the maximum number of open connections, idle connections, and the maximum time a connection can be reused, respectively.
Here are some key facts to keep in mind when working with database connections:
- SetMaxOpenConns sets the maximum number of open connections to the database.
- SetMaxIdleConns sets the maximum number of idle connections in the connection pool.
- SetConnMaxLifetime sets the maximum amount of time a connection may be reused.
The database/sql package maintains its own pool of idle connections, so you don't need to close the database connection manually. However, you can use the Ping method to verify that the connection is still alive.
Handle and Connect
To handle and connect to a database in Go, you'll first need to get a database handle. This is represented by a pointer to an sql.DB struct, which provides access to a specific database.
The database handle is safe for concurrent use by multiple goroutines, and it maintains its own pool of idle connections. This means you don't need to manually close the database connection, as the sql package will automatically manage it for you.
To connect to the database, you'll need to use the sql.Open() function, which takes the database driver name and a driver-specific data source name as arguments. The data source name typically includes the database name and connection information.
For example, the connection string to the gda database would be "user=gorm password=gorm dbname=gda port=9920 sslmode=disable TimeZone=Asia/Shanghai". You can store this connection string in the environment to avoid leaking your database credentials in the code.
Here are some common database drivers for various SQL dialects:
You can use the database/sql package to connect to the database and execute queries. The package provides a uniform way to interact with different databases, making it easy to switch between them if needed.
To verify that the database connection is alive, you can use the Ping() method. This method establishes a connection if necessary and returns an error if the connection fails.
Once you have a database handle and a connection to the database, you can start working with the database using the methods provided on the sql.DB type. These methods include Query(), which executes a query that returns rows, and Close(), which closes the database and prevents new queries from starting.
If this caught your attention, see: Create a Package in Golang
Null String
NullString represents a string that may be null. It implements the Scanner interface, making it a viable scan destination.
Working with NullString is a breeze, thanks to its ability to handle null values. This is particularly useful when dealing with databases that might not always return a string value.
NullString was introduced to address the need for a string type that can accommodate null values. This type is especially useful when working with databases where null values are common.
In practice, NullString has proven to be a valuable addition to any database connection. Its ability to handle null values makes it an essential tool for any developer working with databases.
Data
Querying for data is a common task in database interactions, and Go provides two methods to do so: QueryRowContext() and QueryContext().
These methods are used in conjunction with the SELECT SQL query to retrieve data from the database.
The QueryRowContext() method is useful for retrieving a single row of data, while the QueryContext() method is used for retrieving multiple rows.
To use these methods, you'll need to have an sql.DB type instance, which is the primary interface for interacting with a database in Go.
You can then call the QueryRowContext() or QueryContext() method on the sql.DB instance, passing in the SELECT query as an argument.
Readers also liked: Go vs Golang
Query Execution
Query Execution is a fundamental aspect of working with databases in Go. You can execute queries without returning any rows using the Exec method, which is available on the DB, Stmt, and Tx types.
The Exec method is useful for executing queries like INSERT, UPDATE, and DELETE statements. For example, the ExecContext method on the DB type executes a query without returning any rows, while the ExecContext method on the Stmt type executes a prepared statement with the given arguments and returns a Result summarizing the effect of the statement.
Here are the different types of Exec methods available in Go:
The Query method on the DB type is used to execute queries that return multiple rows. This method returns both the rows and any error that occurred during the query.
Variables
Variables play a crucial role in query execution.
ErrConnDone is returned by any operation that is performed on a connection that has already been returned to the connection pool. This can happen when a connection is closed and then reused, causing unexpected errors.
Exec
Exec is a method used for executing SQL queries that don't return any rows, such as INSERT, UPDATE, and DELETE statements. It takes a query and any placeholder parameters as arguments, and it doesn't return any rows or columns. Instead, it returns a Result summarizing the effect of the statement.
To use Exec, you can call the Exec method on a DB, Stmt, or Tx object, passing in the query and any placeholder parameters. The method uses context.Background internally, but you can specify a different context using the ExecContext method.
For example, you can use the Exec method to insert a new album into a database, as shown in the addAlbum function in the article. This function takes an Album struct as an argument, inserts it into the database, and returns the ID of the new album.
Here are some key differences between Exec and other query execution methods:
In general, you should use Exec when you need to execute a query that doesn't return any rows, such as an INSERT or UPDATE statement.
Raw
The Raw function in Go's database/sql package is a powerful tool for executing raw SQL queries. It exposes the underlying driver connection for the duration of the function call, allowing for direct access to the database.
The driverConn must not be used outside of the Raw function call, as it will continue to be usable until Conn.Close is called. This is a crucial point to remember when working with Raw.
RawBytes is a byte slice that holds a reference to memory owned by the database itself. After a Rows.Scan into a RawBytes, the slice is only valid until the next call to Rows.Next, Rows.Scan, or Rows.Close.
To use Raw effectively, you need to be comfortable with manual error handling and result scanning. This can be a bit verbose, but it provides a good level of control over the query execution process.
Here are some key benefits of using Raw:
- No additional abstraction/complexity added
- Easy to debug raw SQL queries
- Performance
- Provides a good enough abstraction from different database backends
However, Raw also has some drawbacks. The code can become verbose due to the need to scan each row, define proper types, and handle errors. Additionally, there is no compile-time type safety.
*NullBool) Scan
The *NullBool Scan method is a crucial part of query execution, allowing you to scan a null boolean value into a *NullBool argument.
Scan can convert a source value of type bool into a *NullBool, but it's worth noting that the source value may also be true, false, 1, 0, or a string input parseable by strconv.ParseBool.
In a typical scenario, Scan will assign the value through the pointer if the type of the value from the source column matches the type of the *NullBool argument. However, if the source value is null, Scan will return an error.
If you're working with a database that supports null boolean values, you can use Scan to retrieve and manipulate these values in your Go application.
Intriguing read: Golang Source Code
Query Results
A Result summarizes an executed SQL command, providing information about the query's outcome.
There are two main ways to retrieve query results in Go: QueryRow and Query. QueryRow is used for single row queries, while Query is used for multiple rows.
QueryRow executes a query that is expected to return at most one row and always returns a non-nil value. If the query selects no rows, the *Row.Scan will return ErrNoRows. Otherwise, the *Row.Scan scans the first selected row and discards the rest.
The Query method returns both *sql.Rows and the error that occurred during the query (if any). After ascertaining that no error occurred, you can proceed to access the rows of data through the pattern prescribed in the example.
To query for multiple rows, you use the Query method from the database/sql package, then loop through the rows it returns. This involves defining a struct to hold the row data, such as an Album struct with fields for ID, Title, Artist, and Price.
Here's an example of how to define an Album struct and use it to query the database for albums by a specified artist:
```go
type Album struct {
ID int64
Title string
Artist string
Price float32
}
func albumsByArtist(name string) ([]Album, error) {
rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name)
if err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
Broaden your view: Golang Create Error
defer rows.Close()
var albums []Album
for rows.Next() {
var alb Album
if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
albums = append(albums, alb)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
return albums, nil
}
```
This code defines an Album struct and uses it to query the database for albums by a specified artist. The Query method is used to execute the SQL query, and the rows are looped through to extract the data into the Album struct.
You might enjoy: Golang Copy Struct
Transactions
Transactions are a crucial aspect of database management in golang sql. They allow you to execute a series of related queries as a single, all-or-nothing unit of work.
To start a transaction, you can use the BeginTx method, which is available on the Conn type since Go 1.9. This method takes a context and optional TxOptions as arguments.
A transaction must end with a call to Tx.Commit or Tx.Rollback. After a call to either of these methods, all operations on the transaction fail with ErrTxDone. The statements prepared for a transaction are closed by the call to Tx.Commit or Tx.Rollback.
Tx

The Tx type is an in-progress database transaction. It must end with a call to Tx.Commit or Tx.Rollback.
A transaction can be rolled back using Tx.Rollback, which is deferred, ensuring that if the function returns early due to an error, the transaction rolls back without saving any of the changes.
After a call to Tx.Commit or Tx.Rollback, all operations on the transaction fail with ErrTxDone. The statements prepared for a transaction by calling the transaction's Tx.Prepare or Tx.Stmt methods are closed by the call to Tx.Commit or Tx.Rollback.
Tx is a crucial part of database transactions, allowing you to execute a series of related queries while minimizing inconsistencies in the database.
Close
Closing a database or rows is crucial for efficient resource management.
The Close function for a DB closes the database and prevents new queries from starting. This is rare because the DB handle is meant to be long-lived and shared between many goroutines.
If you're working with Rows, you can close them to prevent further enumeration. This is useful when you've finished processing the results.
Close is idempotent, meaning you can call it multiple times without affecting the result of Rows.Err.
Tx Row
Tx Row is a powerful tool for executing queries within a transaction.
Executes a query that doesn't return rows, such as an INSERT or UPDATE, using context.Background internally. To specify the context, use Tx.ExecContext.
Tx Row always returns a non-nil value, even if the query selects no rows. The *Row.Scan method will return ErrNoRows in this case.
Tx Row uses context.Background internally, but you can specify the context using Tx.QueryRowContext.
You can use Tx Row to execute a query that is expected to return at most one row. This is useful for queries that select a single row, such as a SELECT statement.
The *Row.Scan method will scan the first selected row and discard the rest, unless the query selects no rows. In that case, *Row.Scan will return ErrNoRows.
Related reading: Golang Use Cases
IsolationLevel String In1.11
In Go 1.11, the IsolationLevel type gained a new method called String. This method returns the name of the transaction isolation level.
The String method is added to provide a way to get the name of the isolation level in a human-readable format. It's a simple but useful addition that makes working with transactions easier.
For example, if you have an IsolationLevel value set to "Serializable", calling the String method on it would return the string "Serializable". This can be helpful when debugging or logging transactions.
Error Handling
Error Handling is crucial when working with databases in Go. You can check for query errors without calling Row.Scan by using the Err method, which returns the error if any, that was encountered while running the query.
The Err method can be called on a Row object, and it will return a non-nil error if one occurred. This is useful for wrapping packages to check for query errors.
You can also use the Err method on a Rows object to get the error that occurred during iteration. This can be called after an explicit or implicit Rows.Close.
The QueryRow method executes a prepared query statement and returns a non-nil *Row. If an error occurs, it will be returned by a call to Scan on the returned *Row.
If the query selects no rows, the *Row.Scan will return ErrNoRows. Otherwise, it scans the first selected row and discards the rest.
It's worth noting that QueryRow uses context.Background internally, so if you want to specify the context, you should use Stmt.QueryRowContext instead.
Intriguing read: Golang Check Type
Configuration
The database/sql package in Go allows you to configure the connection pool to optimize your use case. By default, it manages the open connection pool internally, but you can customize it to suit your needs. You can configure the connection pool in four ways: configuring max/idle connections, finding and importing a database driver, using the SetMaxOpenConns method, and using the SetMaxIdleConns method.
To configure max/idle connections, you can use the SetMaxOpenConns and SetMaxIdleConns methods. The default maximum number of open connections is not restricted, but you can set a fixed threshold using SetMaxOpenConns(). The maximum number of idle connections is also configurable, and increasing it can improve performance by decreasing the waiting time for an idle connection.
Here are some key configuration options to keep in mind:
Remember to test your configuration values thoroughly through benchmarking and logging to optimize performance.
Register
Registering a database driver is a crucial step in setting up a database connection. The Register function makes a database driver available by the provided name.

If you call Register twice with the same name or if the driver is nil, it panics. This means you need to be careful when registering drivers to avoid any potential issues.
To register a driver, you can use the Register function, but be aware of the potential panic if you call it with the same name or a nil driver.
Drivers
Drivers are an essential part of any database configuration, and GORM provides several functions to work with them.
The func Drivers returns a sorted list of the names of the registered drivers.
To register a database driver, you can use the Register function, which makes a database driver available by the provided name.
If you're working with a specific database, you'll need to install a corresponding database driver, such as pgx for PostgreSQL or Go-MySQL-Driver for MySQL.
To find and import a database driver, you can visit the SQLDrivers wiki page and note the package name for the driver.

Here's a list of some popular database drivers and their corresponding package names:
Once you've imported the driver package, you can use the Open function to open a database and get a database handle.
The Open function takes a database driver name and a driver-specific data source name as arguments.
To verify that the data source name is valid, you can call DB.Ping.
The returned DB is safe for concurrent use by multiple goroutines and maintains its own pool of idle connections.
Broaden your view: Golang Func Type
Context
Context is a crucial aspect of configuration that allows you to control how long a task is allowed to take. In Go, contexts carry deadline and cancellation signals across API boundaries and between processes.
Specifying contexts can be done using the QueryContext() and QueryRowContext() methods, which allow you to specify a context. This is in contrast to the QueryRow() and Query() methods, which do not allow context specification.
Contexts can be used to inform the database service to cancel a query if too much time has elapsed to prevent performance degradation. This is especially useful when working with databases and building APIs, where a slow query can cause a client to wait for an abnormally long time.
You can enforce time constraints for queries using contexts by providing a timed context as an argument to the ExecContext() method. This ensures that the query is canceled if it takes longer than the stipulated time, and an error is returned from the ExecContext() method.
Configuring Max/Idle Connections

Configuring max/idle connections is crucial for optimizing your database use case. By default, the database/sql package manages the open connection pool internally, but you can configure it to suit your needs.
The connection pool consists of idle connections and connections that are actively performing queries. You can configure the maximum number of idle connections using the SetMaxIdleConns method.
The maximum number of idle connections is equal to the maximum number of open connections minus the current number of active connections. By default, this value is set to 2, but you can set it to a higher value.
Increasing the maximum number of idle connections can decrease the waiting time for an idle connection, improving performance. However, more is not always better, and you should configure the maximum number of idle connections with your use case in mind.
You can also configure the maximum number of time a connection may be reused using the SetConnMaxLifetime method and the maximum amount of time a connection can remain in the idle pool before being closed using the SetConnMaxIdleTime method.

Values that are set too low might result in inefficient connection pools, as connections will be repeatedly created from scratch as they would timeout too quickly or be destroyed after too little reuses. It's essential to test your values thoroughly through benchmarking and logging and optimize as needed.
The maximum number of open connections can be set using the SetMaxOpenConns method, and the default is 0, which means there is no limit on the number of open connections. However, if you have a limited amount of resources available for the database, it's wise to set a fixed threshold using SetMaxOpenConns.
By configuring the max/idle connections, you can optimize the performance of your database and ensure that your connections are reused efficiently.
Advanced Topics
In Golang, database interactions are handled by the sql package, which provides a simple and intuitive interface for executing SQL queries.
The sql package uses a connection pool to manage multiple connections to the database, allowing for efficient use of resources and improved performance.
Recommended read: Install Golang Package
To use a connection pool, you can create a sql.DB object and use its methods to execute queries and retrieve results.
For example, you can use the sql.DB object to execute a SELECT query and retrieve a slice of rows, which can then be iterated over to access the query results.
Raw Bytes
Raw Bytes are a byte slice that holds a reference to memory owned by the database itself.
This means that after a Rows.Scan into a RawBytes, the slice is only valid until the next call to Rows.Next, Rows.Scan, or Rows.Close. This can be a bit tricky to work with, as you need to be mindful of when the slice will become invalid.
RawBytes are not a self-contained data structure, but rather a pointer to a larger chunk of memory. This is why they can only be used until the next call to one of the mentioned methods.
For your interest: Next Js Sql
Enforcing Timeouts with Contexts

Using contexts is a best practice when querying a database to avoid a situation where the query hangs forever.
You can enforce time constraints for queries using contexts by providing a timed context as an argument to the ExecContext() method. This ensures that the query is canceled if it takes longer than the stipulated time.
The timed context can be created using a function that returns a context with a deadline, such as the context.TODO() method, which allows the query to run for as long as it needs.
In Go, contexts carry deadline and cancellation signals across API boundaries and between processes, allowing you to control how long a task is allowed to take.
Imagine a situation where a query takes an abnormally long time to return any results, forcing the client to wait for an equally abnormal amount of time for the API's response. With contexts, you can instruct the database to cancel a query if too much time has elapsed to avoid this situation.
You can demonstrate query timeouts using contexts by using a function like pg_sleep(10) to mimic a slow-running query, which is then canceled after a stipulated time limit.
A unique perspective: Rest Api with Golang
Migrations
Migrations are a crucial part of managing database schema changes in Go.
In Go, database migrations help ensure all environments have the same schema and developers can easily apply or roll back changes.
The main goal of database migration tools is to automate this process, saving time and effort.
GORM can handle migrations if your project uses it as its ORM, but if you use database/sql, sqlx, or sqlc, you'll need a separate project to manage them.
Two popular projects for managing database migrations are golang-migrate and goose.
Here are some key features of these tools:
- golang-migrate supports many database drivers and migration sources, and takes a simple and direct approach.
- goose also supports the main database drivers, and offers more control over the migration application process.
Both golang-migrate and goose are solid options for managing database migrations in Go.
Common Patterns
In Go, there are some commonly occurring patterns when working with SQL databases that can improve your workflow.
There are some core patterns that you can employ to improve your workflow. This is not an exhaustive list, but it covers some of the most important ones.
One of the most common patterns is database access, which is discussed in the article section "Common database access patterns in Go".
Curious to learn more? Check out: Golang Go
Examples

Let's take a look at some examples of common patterns in action.
In the tech industry, a classic example of a common pattern is the "build-measure-learn" cycle, where companies rapidly build and test products to gather feedback and iterate towards success.
This pattern is often seen in startups, where speed and adaptability are key to survival.
The "lean startup" movement, popularized by Eric Ries, is a prime example of this pattern in practice.
In product development, the "waterfall" approach is a common pattern where each stage of development is completed before moving on to the next one.
This pattern can be seen in industries where predictability and control are crucial, such as aerospace and defense.
The "agile" methodology, on the other hand, is a common pattern in software development where teams work in short cycles to deliver working software.
This pattern is often used in teams where flexibility and collaboration are essential.
In marketing, the "funnel" pattern is a common way to visualize the customer journey, where leads are funneled through a series of steps to convert them into customers.
This pattern is often used in industries where lead generation and conversion are critical, such as e-commerce and finance.
Curious to learn more? Check out: Golang App Development
NullFloat64

NullFloat64 is a type that represents a float64 that may be null. It's similar to NullString in that it implements the Scanner interface, allowing it to be used as a scan destination.
NullFloat64 is a special type that can handle null values, which is useful when working with data that might be missing or unknown. This can be particularly important in certain applications or use cases.
Common Queries
Common Queries are the building blocks of any database interaction. They are used to create, read, update, and delete data in a database.
A CREATE TABLE query is used to create a new database table. For example, you can create a table called "order" with columns like "id", "food", "quantity", and "timestamp" using the following query: CREATE TABLE order (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), food TEXT NOT NULL, quantity INTEGER NOT NULL timestamp DATE DEFAULT now());.
A SELECT query is used to select rows of data from a given table following additional constraints by WHERE and ORDER BY. For example, you can select all rows from the "order" table where the food is "Pie" and order the results by timestamp using the following query: SELECT * FROM order WHERE food = 'Pie' ORDER BY timestamp;.
INSERT, UPDATE, and DELETE queries are used to modify data in a database. For example, you can insert a new row into the "order" table with the following query: INSERT INTO order (food, quantity) VALUES ('Pie', 15);.
Here is a summary of the common queries:
These queries are the foundation of any database interaction and are used in conjunction with other database operations to perform complex tasks.
Choosing the Right Tool
Choosing the right tool for your project is crucial, and it's not always a straightforward decision.
If your project requires complex, performance-sensitive queries, it's best to avoid ORMs and query builders, as they can make debugging a tedious process.
You should build a lightweight wrapper around the database/sql package and write the queries manually, as this gives you the most control over the queries.
Query builders can be a good middle ground for projects with relatively simple queries, as they handle boilerplate code.

For small-scale projects with minimal complex queries, ORMs and query builders are good options, as they eliminate much of the boilerplate.
Regardless of the project, using database schema migration tools is always a good idea, as they alleviate a pain point when working with databases and provide ease of mind when migrating data or changing the schema.
This framework is not definitive, so you should exercise personal judgment when deciding the type of tools to use.
Generated Code from SQL
Generated Code from SQL is a unique approach that uses sqlc to generate Go code from SQL queries. This method ensures that your queries are syntactically correct and type-safe.
With sqlc, you write your schema and SQL queries, then use a CLI tool to generate Go code from it. This process requires some initial setup, where you add your schema and query to files named schema.sql and query.sql.
To generate the Go code, you need to name your query in query.sql and mark the parameters. Running sqlc generate will give you access to the generated types and functions, which make your code type-safe and quite short.

sqlc is special because it understands your database schema and uses it to validate the SQL you write. This means your SQL queries are being validated against the actual database table, and sqlc will give you a compile-time error if something is wrong.
Here are the benefits of using sqlc:
- Type safety with generated Go code.
- Still easy to debug SQL code.
- Saves quite a bit of tedious coding.
- Performance.
However, sqlc also has some limitations, including initial configuration setup for database schema and queries, and not perfect static analysis.
Performance Optimization
To optimize performance in Go, using a connection pool is crucial, as it can significantly reduce the overhead of establishing new database connections.
Using a connection pool can save up to 90% of the time spent on database operations.
The Go driver for PostgreSQL, for example, uses a connection pool by default, which can handle a large number of connections efficiently.
This is especially important when working with a large number of database connections, as seen in the example of a web application with thousands of concurrent connections.
Go's goroutine scheduling can also help with performance optimization by allowing multiple database operations to run concurrently.
Tools
Using database tools can greatly improve your quality of life when working with SQL databases in Go.
There are three categories of database tools built on top of database/sql that can help. These tools provide a different level of abstraction for database access.
Building a lightweight wrapper around the database/sql package and writing queries manually is a good option for projects with complex queries that require performance. This approach provides the most control over queries.
ORMs and query builders can be a good middle ground for projects with relatively simple queries. They handle boilerplate code, but you still need to revise and double-check generated queries to ensure they're optimal.
Using database schema migration tools is always advisable, regardless of project size. They alleviate a pain point when working with databases and provide ease of mind when migrating data across different databases or changing the database schema.
If your project requires a lot of complex queries, it's best to avoid ORMs and query builders due to transparency and debugging issues.
Featured Images: pexels.com


