Abstracting out Data Layer Implementation in Go
In this article, we are going to abstract out the database layer. That means the consumer will only consume abstract methods if they know where the data is stored. We aim to gain code extensibility and better maintainability using this design pattern. A new implementation can be plugged in without breaking the application. We will discuss all these in detail later in the article with a working code application.
To start with, below is the UML diagram that we are going to implement with Go Language.
Create a Go application in your preferred IDE with go.modand go.sum files, along with other files using the below project structure diagram.
Now that you have created all the files, let’s add logic.
Start with your main` method definition which is the entry point of the application. The main method is defined inside Main.go.
func main() {
err := godotenv.Load()
if err != nil {
fmt.Println("Error loading .env file")
}
dbFactory := DatabaseFactory{}
datasource, err = dbFactory.CreateDatabase()
if err != nil {
log.Fatal(err)
}
db, err = datasource.Connect()
defer db.Close()
handleRequests()
}Here, we are first of all loading the environment variables that might be present in the .envfile. Post that we are creating an object of DataBaseFactory that we will use to get the required implementation of the database. ThedbFactory.CreateDatabase() method will infer the implementation based on the environment variables and return the same. After we have got the instance of the database we are invoking connect and close. The db.Close() method is called with defer that means it will be invoked at last.
The Controller.go have all the routes defined in handleRequests()for the REST APIs along with corresponding methods to be called. Below is the code for the same. NOTE:- I have used mux for defining routes for APIs. There are other frameworks that can we use like fiber and gin .
func handleRequests() {
myRouter := mux.NewRouter().StrictSlash(true)
myRouter.HandleFunc("/users", GetUsers).Methods("GET")
myRouter.HandleFunc("/users", CreateUser).Methods("POST")
myRouter.HandleFunc("/users/{id}", DeleteUser).Methods("DELETE")
myRouter.HandleFunc("/users", DeleteUsers).Methods("DELETE")
SetSwagger(myRouter)
log.Fatal(http.ListenAndServe(":10000", myRouter))
}The SetSwagger method is just for swagger documentation of the APIs.
func SetSwagger(myRouter *mux.Router) {
endpoints := []Endpoint{
EndPoint(GET, "/users", "Users", Params(), nil, []User{}, err, "Get all users", nil),
EndPoint(POST, "/users", "Users", Params(), User{}, nil, err, "", nil),
EndPoint(DELETE, "/users/{id}", "Users", Params(IntParam("id", true, "")), nil, nil, err, "", nil),
EndPoint(DELETE, "/users", "Users", Params(), nil, nil, err, "", nil),
}
sw := CreateNewSwagger("Swagger API", "1.0")
AddEndpoints(endpoints)
myRouter.PathPrefix("/swagger").Handler(swagger.SwaggerHandler(sw.GenerateDocs()))
// if you want to export your swagger definition to a file
// sw.ExportSwaggerDocs("docs/swagger.json") // optional
}Here is the full code for Controller.go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
_ "github.com/go-sql-driver/mysql"
swg "github.com/go-swagno/swagno"
"github.com/go-swagno/swagno-http/swagger"
"github.com/gorilla/mux"
_ "github.com/lib/pq"
)
var datasource Database
func GetUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, err := validate()
if err != nil {
panic(err.Error())
}
response, err := datasource.GetAll()
if err != nil {
panic(err.Error())
}
json.NewEncoder(w).Encode(response)
}
func CreateUser(w http.ResponseWriter, r *http.Request) {
var user User
_ = json.NewDecoder(r.Body).Decode(&user)
var response string
if user.Username == "" || user.Email == "" {
response = "You are missing username or email parameter."
} else {
fmt.Println("Inserting new User with username: " + user.Username + " and email: " + user.Email)
_, err := validate()
if err != nil {
panic(err.Error())
}
response, err = datasource.CreateUsers(User{Username: user.Username, Email: user.Email})
if err != nil {
panic(err.Error())
}
}
json.NewEncoder(w).Encode(response)
}
func DeleteUser(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
var response string
if id == "" {
response = "You are missing id parameter."
} else {
_, err := validate()
if err != nil {
panic(err.Error())
}
response, err = datasource.DeleteUser(id)
if err != nil {
panic(err.Error())
}
}
json.NewEncoder(w).Encode(response)
}
func DeleteUsers(w http.ResponseWriter, r *http.Request) {
_, err := validate()
if err != nil {
panic(err.Error())
}
response, err := datasource.DeleteUsers()
if err != nil {
panic(err.Error())
}
json.NewEncoder(w).Encode(response)
}
func handleRequests() {
myRouter := mux.NewRouter().StrictSlash(true)
myRouter.HandleFunc("/users", GetUsers).Methods("GET")
myRouter.HandleFunc("/users", CreateUser).Methods("POST")
myRouter.HandleFunc("/users/{id}", DeleteUser).Methods("DELETE")
myRouter.HandleFunc("/users", DeleteUsers).Methods("DELETE")
SetSwagger(myRouter)
log.Fatal(http.ListenAndServe(":10000", myRouter))
}
func SetSwagger(myRouter *mux.Router) {
endpoints := []swg.Endpoint{
swg.EndPoint(swg.GET, "/users", "Users", swg.Params(), nil, []User{}, err, "Get all users", nil),
swg.EndPoint(swg.POST, "/users", "Users", swg.Params(), User{}, nil, err, "", nil),
swg.EndPoint(swg.DELETE, "/users/{id}", "Users", swg.Params(swg.IntParam("id", true, "")), nil, nil, err, "", nil),
swg.EndPoint(swg.DELETE, "/users", "Users", swg.Params(), nil, nil, err, "", nil),
}
sw := swg.CreateNewSwagger("Swagger API", "1.0")
swg.AddEndpoints(endpoints)
myRouter.PathPrefix("/swagger").Handler(swagger.SwaggerHandler(sw.GenerateDocs()))
// if you want to export your swagger definition to a file
// sw.ExportSwaggerDocs("docs/swagger.json") // optional
}Next, we need to update DataStoreFactory.go function to return the correct implementation based on environment variables.
package main
import (
"database/sql"
"fmt"
"strconv"
)
var db *sql.DB
var err error
type Database interface {
Connect() (*sql.DB, error)
GetAll() ([]User, error)
CreateUsers(User) (string, error)
DeleteUsers() (string, error)
DeleteUser(string) (string, error)
}
type DatabaseFactory struct{}
func (df DatabaseFactory) CreateDatabase() (Database, error) {
datasource := getenv("DATA_SOURCE", "")
switch datasource {
case "mysql":
port, _ := strconv.Atoi(getenv("DB_PORT", "3306"))
return MySQL{
host: getenv("DB_HOST", "localhost"),
port: port,
username: getenv("DB_USERNAME", "root"),
password: getenv("DB_PASSWORD", "password"),
dbname: getenv("DB_NAME", "testdb"),
}, nil
case "postgres":
port, _ := strconv.Atoi(getenv("DB_PORT", "3306"))
return PostgreSQL{
host: getenv("DB_HOST", "localhost"),
port: port,
username: getenv("DB_USERNAME", "root"),
password: getenv("DB_PASSWORD", "password"),
dbname: getenv("DB_NAME", "testdb"),
}, nil
default:
return nil, fmt.Errorf("unsupported database type")
}
}Let’s define two implementations, one for MySql and the other for PostgreSQL.
Get Utkarsh Sharma’s stories in your inbox
Join Medium for free to get updates from this writer.
MySqlImpl.go
package main
import (
"context"
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
type MySQL struct {
host string
port int
username string
password string
dbname string
}
func (m MySQL) Connect() (*sql.DB, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", m.username, m.password, m.host, m.port, m.dbname)
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
err = db.Ping()
if err != nil {
return nil, err
}
return db, nil
}
func (p MySQL) CreateUsers(user User) (string, error) {
query := "INSERT INTO `User` (`username`, `email`) VALUES (?, ?)"
insertResult, err := db.ExecContext(context.Background(), query, user.Username, user.Email)
if err != nil {
panic(err.Error())
}
id, err := insertResult.LastInsertId()
if err != nil {
return "", err
}
return "The User has been inserted successfully! Id: " + fmt.Sprint(id), nil
}
func (p MySQL) GetAll() ([]User, error) {
query := "Select * from User"
result, err := db.Query(query)
if err != nil {
return nil, err
}
defer result.Close()
var users []User
for result.Next() {
var id int
var username string
var email string
err = result.Scan(&id, &username, &email)
if err != nil {
return nil, err
}
users = append(users, User{Username: username, Email: email})
}
return users, nil
}
func (p MySQL) DeleteUsers() (string, error) {
query := "DELETE FROM User"
result, err := db.Exec(query)
if err != nil {
panic(err.Error())
}
row, err := result.RowsAffected()
if err != nil {
panic(err.Error())
}
return "All Users have been deleted successfully! Row Deleted: " + fmt.Sprint(row), nil
}
func (p MySQL) DeleteUser(id string) (string, error) {
query := "DELETE FROM User where id = ?"
result, err := db.Exec(query, id)
if err != nil {
panic(err.Error())
}
row, err := result.RowsAffected()
if err != nil {
panic(err.Error())
}
return "All Users have been deleted successfully! Row Deleted: " + fmt.Sprint(row), nil
}PostgreSqlImpl.go
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
type PostgreSQL struct {
host string
port int
username string
password string
dbname string
}
func (p PostgreSQL) Connect() (*sql.DB, error) {
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", p.host, p.port, p.username, p.password, p.dbname)
db, err = sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
err = db.Ping()
if err != nil {
return nil, err
}
return db, nil
}
func (p PostgreSQL) CreateUsers(user User) (string, error) {
query := `INSERT INTO public.User (username, email) VALUES ($1, $2) RETURNING id`
id := 0
err = db.QueryRow(query, user.Username, user.Email).Scan(&id)
if err != nil {
return "", err
}
return "The User has been inserted successfully! Id: " + fmt.Sprint(id), nil
}
func (p PostgreSQL) GetAll() ([]User, error) {
query := "Select * from public.user"
result, err := db.Query(query)
if err != nil {
return nil, err
}
defer result.Close()
var users []User
for result.Next() {
var id int
var username string
var email string
err = result.Scan(&id, &username, &email)
if err != nil {
return nil, err
}
users = append(users, User{Username: username, Email: email})
}
return users, nil
}
func (p PostgreSQL) DeleteUsers() (string, error) {
query := "DELETE FROM public.user"
result, err := db.Exec(query)
if err != nil {
panic(err.Error())
}
row, err := result.RowsAffected()
if err != nil {
panic(err.Error())
}
return "All Users have been deleted successfully! Row Deleted: " + fmt.Sprint(row), nil
}
func (p PostgreSQL) DeleteUser(id string) (string, error) {
query := "DELETE FROM public.User where id = $1"
result, err := db.Exec(query, id)
if err != nil {
panic(err.Error())
}
row, err := result.RowsAffected()
if err != nil {
panic(err.Error())
}
return "All Users have been deleted successfully! Row Deleted: " + fmt.Sprint(row), nil
}Additionally, I have kept a file for adding all the commons utility methods known as utils.go.
Utils.go
package main
import (
"errors"
"os"
)
func getenv(key, fallback string) string {
value := os.Getenv(key)
if len(value) == 0 {
return fallback
}
return value
}
func validate() (bool, error) {
datasource_name := getenv("DATA_SOURCE", "")
if datasource_name == "" {
panic(errors.New("datasource not defined, availaible options are mysql, postgres"))
}
return true, nil
}Running and Testing Your Code
- Create a
.envfile in the root directory. - Build your project using
go build - Run your project using
go run . - Try accessing
http://localhost:10000/swagger
You can try out some APIs and then again restart with different DATA_SOURCE env variable and you would see that they are saving, and fetching data to the corresponding db, and the choice of the database just depends on the environment variable.
The code from GitHub is https://github.com/codeWithUtkarsh/codeOnCloud/tree/main/Golang
- Fork this repo and start playing with the code.
- Add a ⭐ star if this added value to you.
Feel free to leave me a mail utkarshkviim@gmail.com if facing any issues.










