Effortless Database Connection Pooling in Go
The processes necessary in opening and shutting a DB connection are not costly enough to be a concern for small-scale operation. However, if your application grows larger, the ongoing opening and shutting of connections costs more and may start to affect how well your application runs. Instead of creating and closing a new connection for every activity, it often makes more sense to figure out a method to maintain existing connections and transfer them from one operation to another as needed.
What is database connection pooling?
Database connection pooling is a way to reduce the cost of opening and closing connections by maintaining a “pool” of open connections that can be passed from database operation to database operation as needed. In this manner, we avoid incurring the cost of needing to initiate and terminate a fresh connection for every query the database is requested to execute.
Database connection pooling isn’t a universally applicable solution, although it can help increase application performance. In certain cases, it might not even be a workable answer. Because keeping open connections and creating/closing connections both require resource expenditures.
Implementation
“Talk is cheap. Show me the code.” - Linus Torvalds
We will be writing a simple connection pooler in Go for TODO REST APIs.
First lets define our db table todos
CREATE TABLE todos (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
completed BOOLEAN NOT NULL DEFAULT FALSE
);
Then we will initialize our database connection and setting up the routes for our APIs.
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
_ "github.com/lib/pq"
"github.com/gorilla/mux"
)
var db *sql.DB
// Todo represents a task in the to-do list
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
func main() {
// Database connection string
connStr := "user=username password=password dbname=mydb sslmode=disable"
// Open the connection
var err error
db, err = sql.Open("postgres", connStr)
if err != nil {
log.Fatalf("Error opening database: %q", err)
}
// Set the maximum number of concurrently open connections (in-use + idle)
db.SetMaxOpenConns(25)
// Set the maximum number of idle connections
db.SetMaxIdleConns(25)
// Set the maximum amount of time a connection may be reused
db.SetConnMaxLifetime(5 * time.Minute)
// Verify the connection to the database is still alive
if err := db.Ping(); err != nil {
log.Fatalf("Error pinging database: %q", err)
}
fmt.Println("Database connection established successfully")
// Create a new router
r := mux.NewRouter()
// Register handler functions
r.HandleFunc("/todos", getTodos).Methods("GET")
r.HandleFunc("/todos/{id}", getTodo).Methods("GET")
r.HandleFunc("/todos", createTodo).Methods("POST")
r.HandleFunc("/todos/{id}", updateTodo).Methods("PUT")
r.HandleFunc("/todos/{id}", deleteTodo).Methods("DELETE")
// Start the server
log.Fatal(http.ListenAndServe(":8080", r))
}
In the above the following part of the code that configures the database connection pool:
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
db.SetMaxOpenConns(25)
- This line sets the maximum number of open connections to the database.db.SetMaxIdleConns(25)
- This line sets the maximum number of idle connections in the pool.db.SetConnMaxLifetime(5 * time.Minute)
- This line sets the maximum amount of time a connection can be reused before it is closed and replaced by a new connection.
Next we will start creating the functions for our REST APIs
func getTodos(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query("SELECT id, title, completed FROM todos")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var todos []Todo
for rows.Next() {
var todo Todo
if err := rows.Scan(&todo.ID, &todo.Title, &todo.Completed); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
todos = append(todos, todo)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(todos)
}
func getTodo(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
var todo Todo
err := db.QueryRow("SELECT id, title, completed FROM todos WHERE id = $1", id).Scan(&todo.ID, &todo.Title, &todo.Completed)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Todo not found", http.StatusNotFound)
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(todo)
}
func createTodo(w http.ResponseWriter, r *http.Request) {
var todo Todo
if err := json.NewDecoder(r.Body).Decode(&todo); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err := db.QueryRow("INSERT INTO todos (title, completed) VALUES ($1, $2) RETURNING id", todo.Title, todo.Completed).Scan(&todo.ID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(todo)
}
func updateTodo(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
var todo Todo
if err := json.NewDecoder(r.Body).Decode(&todo); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_, err := db.Exec("UPDATE todos SET title = $1, completed = $2 WHERE id = $3", todo.Title, todo.Completed, id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
func deleteTodo(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
_, err := db.Exec("DELETE FROM todos WHERE id = $1", id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}