Golang - Clean Architecture in projects

Michał Szymański

13/12/2019

History of language

First release November 10, 2009.

Created and used by Google
Current version - 1.13
Example projects:
  • Docker
  • Kubernetes
  • Treafik

Main Features of go

  • staticaly typed
  • compiled
  • garbage collection
  • object oriented
  • remote package management

Clean architecture - general rules

Independent of Frameworks.

The architecture does not depend on the existence of some library.

Testable

The business logic can be tested without external elements ie UI, databse.

Independent of UI.

Independent of UI. The UI can change easily, without changing the rest of the system.

Independent of databse.

Can change to other databse.

Your business logic is not related to the database.
Independent of any external services.

In fact your business rules simply don’t know anything at all about the outside world.

Base layers

  • model/entities layer
  • repository layer
  • usecase layer
  • delivery layer

Models Layer

This layer, will store any Object’s Struct example. User, Article
They encapsulate the most general and high-level rules
Plain objects
Example code

				    import "time"

					type Article struct {
						ID        int64     `json:"id"`
						Title     string    `json:"title"`
						Edition   string    `json:"edition"`
						Content   string    `json:"content"`
						UpdatedAt time.Time `json:"updated_at"`
						CreatedAt time.Time `json:"created_at"`
					}
					

					func (article *Article) FullTitle() string {
					  return article.Title + " " + article.Edition
					}
				        

Repository Layer

Defines interfaces for the store data
This layer will act for CRUD to database only
Source of data:
  • - database
  • - file system
  • - external API
It implements the interfaces defined by the use case

				    package repository
					import (
						"database/sql"
						_ "github.com/go-sql-driver/mysql"

						"github.com/bmiles-development/shopify-game-player/player/models"
					)

					type ArticleRepo struct{
					  DB *sql.DB
					}
					


					func (repo *ArticleRepo) FindByID(id int64) (*models.Article, error) {
						result, err := repo.DB.Query("SELECT * FROM articles WHERE id = ?", id)
						if err != nil {
							panic(err.Error()) // proper error handling instead of panic in your app
						}
						defer result.Close()

						var article models.Article
						for result.Next() {
							// for each row, scan the result into our article composite object
							err = result.Scan(&article.ID, &article.Title, &article.Content)
							if err != nil {
								panic(err.Error()) // proper error handling instead of panic in your app
							}

						}

						return &article, err
					}
					

					func (repo *ArticleRepo) Save(article *models.Article) error {
						sqlQuery, err := repo.DB.Prepare("INSERT INTO articles(title, edition, content) VALUES(?, ?, ?)")
						if err != nil {
							panic(err.Error())
						}

						sqlQuery.Exec(article.Title, article.Edition, article.Content)

						return err
					}
							
						

Usecase Layer

All business logic is in a use case Entities business rules then these entities are the business objects of the application Throws business exceptions

package usecase

import (
	"github.com/bmiles-development/shopify-game-player/player/models"
	"github.com/bmiles-development/shopify-game-player/player/repository"
)

type UserUsecase struct {
	userRepo        repository.UserRepo
	userShopifyRepo repository.UserShopify
}

func NewUserUseCase(userRepo repository.UserRepo, userShopifyRepo repository.UserShopify) *UserUsecase {
	return &UserUsecase{
		userRepo:        userRepo,
		userShopifyRepo: userShopifyRepo,
	}
}

func (userUseCase *UserUsecase) SyncUsers(userCount int) []models.User {
	return userUseCase.userShopifyRepo.Fetch(userCount)
}

func (userUseCase *UserUsecase) AddTagsToShopifyUsers(userID string, tags string) models.User {
	return userUseCase.userShopifyRepo.AddTags(userID, tags)
}

Delivery Layer

This layer decide how the data will presented.
Could be as:
  • - REST API,
  • - HTML File
This layer also will accept the input from user. Sanitize the input and sent it to Usecase layer.
				 
package controller

import (
	"net/http"

	"github.com/manakuro/golang-clean-architecture/domain/model"
	"github.com/manakuro/golang-clean-architecture/usecase/interactor"
)

type userController struct {
	userInteractor interactor.UserInteractor
}

type UserController interface {
	GetUsers(c Context) error
}

func NewUserController(us interactor.UserInteractor) UserController {
	return &userController{us}
}

func (uc *userController) GetUsers(c Context) error {
	var u []*model.User

	u, err := uc.userInteractor.Get(u)
	if err != nil {
		return err
	}

	return c.JSON(http.StatusOK, u)
}
				 
				 
Code Examples
				  //Deliveries
var UserLambda *userDelivery.LambdaDelivery

//Usecases
var userUC *userUsecase.UserUsecase

func Init() error {
	var err error

	//Connections
	loadEnvs()
	dbConection := dbConnection()
	graphqlConnection := playerConnection.GraphqlConnection{
		Host:  os.Getenv("SHOPIFY_HOST"),
		Token: os.Getenv("SHOPIFY_TOKEN"),
	}

	//Repos
	userRepo := playerRepository.NewMysqlUserRepository(dbConection)
	shopifyUserRepo := playerRepository.NewPlayerGrapqlRepository(graphqlConnection)
	//levelRepo := levelRepository.NewMysqlLevelRepository(dbConection)

	//Usecases
	userUC := userUsecase.NewUserUseCase(*userRepo, *shopifyUserRepo)

	//Delivery
	UserLambda = userDelivery.NewLambdaDelivery(*userUC)

	return err
}


func loadEnvs() {
	path := calcAppPath()
	err := godotenv.Load(path + "/.env")
	if err != nil {
		//log.Fatal("Error loading .env file")
	}
}

func calcAppPath() string {
	appPath := os.Getenv("APP_PATH")
	if appPath != "" {
		return appPath
	}

	cmd := exec.Command("/bin/pwd")
	var out bytes.Buffer
	cmd.Stdout = &out
	err := cmd.Run()
	if err != nil {
		panic(err)
	}

	parts := strings.Split(strings.TrimSpace(out.String()), string(os.PathSeparator))
	return strings.Join(parts, string(os.PathSeparator))
}

func dbConnection() *sql.DB {
	dbHost := os.Getenv("MYSQL_HOST")
	dbPort := os.Getenv("MYSQL_PORT")
	dbUser := os.Getenv("MYSQL_USER")
	dbPassword := os.Getenv("MYSQL_PASSWORD")
	dbName := os.Getenv("MYSQL_DATABASE")

	dbCredentials := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbUser, dbPassword, dbHost, dbPort, dbName)

	return playerConnection.Connect(dbCredentials)
}
				  
				
Questions