Understanding Repository Design Pattern with Native Golang
The repository pattern is a popular design pattern used in software development to abstract data storage logic and decouples the domain logic from the persistence mechanism. Using the repository pattern, we encapsulate the logic for retrieving and manipulating data, ensuring that business logic doesn’t depend directly on data access layers.
In this article, we will implement the repository pattern in Golang with native PostgreSQL support to perform basic CRUD (Create, Read, Update, Delete) operations.
# Why Use the Repository Pattern?
- Separation of Concerns: The repository layer separates the business logic from data access logic, making the codebase more modular and maintainable.
- Testability: The repository pattern allows mocking the repository layer, enabling easier unit testing.
- Flexibility: You can switch between databases (e.g., switching from PostgreSQL to MySQL) by simply changing the repository implementation without touching the business logic.
# Recomended Project Structure for Repository Pattern using Golang
repository-pattern-example/
│
├── main.go # The entry point of the application
├── go.mod # Go module definition
├── models/ # Directory for model definitions
│ └── user.go # User model
├── repository/ # Directory for repository pattern implementation
│ └── user_repository.go # PostgreSQL User repository implementation
# Detailed Breakdown:
main.go
: This is the main file where the PostgreSQL connection is established and CRUD operations are performed using the repository pattern.go.mod
: This is the Go module definition file, which includes the project name and dependencies (e.g.,lib/pq
for PostgreSQL).models/
: Themodels
directory contains theUser
struct which represents the user entity. Each model has its own file for better organization.user.go
: Defines theUser
struct with fields likeID
,Name
,Email
, andAge
.
repository/
: Therepository
directory contains the implementation of the repository pattern.user_repository.go
: Implements theUserRepository
interface with PostgreSQL-specific logic for creating, reading, updating, and deleting users in the database.
This structure keeps the code organized and modular, adhering to the repository pattern while ensuring easy maintenance and scalability.
# Setting Up PostgreSQL and Golang
First, ensure that you have PostgreSQL installed and running on your machine. You can install PostgreSQL by following the official PostgreSQL installation guide.
Next, create a database in PostgreSQL for our example:
CREATE DATABASE go_repository_example;
Create a users
table:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
age INT
);
Now, initialize a new Go project and install the necessary dependencies:
go mod init repository-pattern-example
go get github.com/lib/pq
# Repository Interface
We define an interface for our repository that includes methods for CRUD operations:
package repository
import "github.com/repository-pattern-example/models"
// UserRepository is the interface that wraps basic CRUD operations.
type UserRepository interface {
Create(user *models.User) error
GetByID(id int) (*models.User, error)
Update(user *models.User) error
Delete(id int) error
GetAll() ([]*models.User, error)
}
# User Model
We will define a simple User
model that maps to the users
table in PostgreSQL.
package models
// User represents the structure of a user entity.
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
# PostgreSQL Implementation of the Repository
We now create a PostgreSQL-specific implementation of the UserRepository
interface. We’ll use lib/pq
as the PostgreSQL driver for Golang.
package repository
import (
"database/sql"
"fmt"
"github.com/repository-pattern-example/models"
_ "github.com/lib/pq"
)
type PostgresUserRepository struct {
db *sql.DB
}
// NewPostgresUserRepository creates a new instance of PostgresUserRepository.
func NewPostgresUserRepository(db *sql.DB) *PostgresUserRepository {
return &PostgresUserRepository{db: db}
}
func (r *PostgresUserRepository) Create(user *models.User) error {
query := "INSERT INTO users (name, email, age) VALUES ($1, $2, $3) RETURNING id"
err := r.db.QueryRow(query, user.Name, user.Email, user.Age).Scan(&user.ID)
if err != nil {
return err
}
return nil
}
func (r *PostgresUserRepository) GetByID(id int) (*models.User, error) {
user := &models.User{}
query := "SELECT id, name, email, age FROM users WHERE id = $1"
row := r.db.QueryRow(query, id)
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.Age)
if err != nil {
return nil, err
}
return user, nil
}
func (r *PostgresUserRepository) Update(user *models.User) error {
query := "UPDATE users SET name = $1, email = $2, age = $3 WHERE id = $4"
_, err := r.db.Exec(query, user.Name, user.Email, user.Age, user.ID)
if err != nil {
return err
}
return nil
}
func (r *PostgresUserRepository) Delete(id int) error {
query := "DELETE FROM users WHERE id = $1"
_, err := r.db.Exec(query, id)
if err != nil {
return err
}
return nil
}
func (r *PostgresUserRepository) GetAll() ([]*models.User, error) {
query := "SELECT id, name, email, age FROM users"
rows, err := r.db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var users []*models.User
for rows.Next() {
user := &models.User{}
err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.Age)
if err != nil {
return nil, err
}
users = append(users, user)
}
return users, nil
}
# Database Setup and Main Function
In the main function, we’ll set up a PostgreSQL connection and use the repository to perform CRUD operations.
package main
import (
"database/sql"
"fmt"
"log"
"github.com/repository-pattern-example/models"
"github.com/repository-pattern-example/repository"
_ "github.com/lib/pq"
)
const (
host = "localhost"
port = 5432
user = "postgres"
password = "your_password"
dbname = "go_repository_example"
)
func main() {
// Setup PostgreSQL connection
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
db, err := sql.Open("postgres", psqlInfo)
if err != nil {
log.Fatal(err)
}
defer db.Close()
err = db.Ping()
if err != nil {
log.Fatal(err)
}
fmt.Println("Successfully connected to PostgreSQL")
// Create a new repository
userRepo := repository.NewPostgresUserRepository(db)
// Create a new user
newUser := &models.User{Name: "John Doe", Email: "johndoe@example.com", Age: 30}
err = userRepo.Create(newUser)
if err != nil {
log.Fatal(err)
}
fmt.Printf("New user created with ID: %d\n", newUser.ID)
// Get user by ID
user, err := userRepo.GetByID(newUser.ID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("User retrieved: %+v\n", user)
// Update user
user.Name = "John Updated"
err = userRepo.Update(user)
if err != nil {
log.Fatal(err)
}
fmt.Println("User updated")
// Get all users
users, err := userRepo.GetAll()
if err != nil {
log.Fatal(err)
}
fmt.Println("All users:", users)
// Delete user
err = userRepo.Delete(user.ID)
if err != nil {
log.Fatal(err)
}
fmt.Println("User deleted")
}
# Conclusion
By using the repository pattern in Golang, we achieve clean separation between the business logic and the persistence layer. This improves testability and maintainability of the code. In this example, we implemented a repository for performing CRUD operations with PostgreSQL, but the pattern can easily be extended to other databases or even different storage mechanisms.
This approach not only helps you keep your code organized but also allows you to handle data persistence in a more structured and scalable way.