Saltar a contenido

Template

A continuacion se detallara la estructura y funcionamiento general de los nuevos templates de Go desarrollados por el equipo de Arquitectura, uno de los aspectos a tener en cuenta es el parecido a nivel estructural que mantienen con los Templates de Clean Arquitechture de .NET, esto con la finalidad de estandarizar una distribucion de carpetas que permita al desarrollador moverse entre tecnologias con las minimas complicaciones en el proceso encontrandose con similitudes para una mejor adopcion del proceso de desarrollo, mayormente si es de las primeras veces que se interactua con el lenguaje.

Api Template

.
├── src
│   └── Application
|   |    └──Common
|   |    └──UseCase
|   |
|   └── Domain
|   |   └── Common
|   |       └── Interfaces
|   |   └── Models
|   |   └── Dtos
|   |   └── ValueObjects
|   |   
|   └── Infrastructure
|   |   └── Datastore
|   |   └── Middlewares
|   |   └── Registry
|   |   └── Repository
|   |   └── Router
|   |
|   └── API
|       └── Controller
|       └── Docs
│       └── main.go
│       └── Config
|           └── app.development.env
|           └── app.env
├── .gitignore
├── Dockerfile
├── README.md

Configuracion

Para configurar el proyecto, utilizamos archivos .env, que nos permiten almacenar y acceder a las variables de entorno necesarias. Estos archivos contienen información específica de configuración, como nombres de aplicaciones, versiones, puertos de servidor, cadenas de conexión a bases de datos y otras configuraciones relevantes para el entorno de ejecución.

APLICATION_NAME=CleanArquitecture
APLICATION_VERSION=1.0.0
PORT=8080
SQLSERVER_DSN=Data Source=DBONBOARDDESA.andreani.com.ar;Initial Catalog=Onboarding;Persist Security Info=True;User ID=User_Onboarding;Password=0jp9dQALTMkOMVC0Wss2
ELASTIC_APM_SERVER_URL=https://apm-server-architecture-it-test.apps.ocptest.andreani.com.ar
ELASTIC_APM_SERVICE_NAME=golang-clean-architecture
ELASTIC_APM_ENVIRONMENT=Test

Tip

Asegúrate de que las variables estén configuradas según tus necesidades y el entorno en el que se ejecutará la aplicación.

Main

El archivo main.go se encarga de la configuracion inicial de la aplicacion de la mano de la libreria gWebHost:

package main

import (
    WebHost "github.com/architecture-it/gWebHost"
    _ "golang-clean-architecture/api/docs"
    "golang-clean-architecture/infrastructure/datastore"
    "golang-clean-architecture/infrastructure/registry"
    "golang-clean-architecture/infrastructure/router"
)

func main() {
    // Cargar la configuración desde el archivo .env
    WebHost.GetConfiguration("app.env", "./config")

    // Crear una instancia del servidor
    server := WebHost.NewServer()

    // Crear una instancia de la base de datos
    db := datastore.NewDB(server.AddHealthCheck())

    // Crear una instancia del registro
    r := registry.NewRegistry(db)

    // Configurar el enrutador de la API
    router.NewRouter(server.GetRouter(), r.NewAppController())

    // Agregar la documentación de la API, y el logging
    server.AddApiDocs()
    server.AddconfigureationLogger()

    // Iniciar el servicio
    server.ListenAndServe()
}

Datastore

El paquete Datastore es responsable de la configuración y creación de la conexión a la base de datos utilizando la biblioteca GORM. Proporciona una abstracción de la capa de persistencia y define cómo se establece la conexión.

La función NewDB recibe un objeto health.Health como parámetro (HealthCheck). Dentro de la función, se obtiene la cadena de conexión de la base de datos desde la variable de entorno SQLSERVER_DSN. Luego, se utiliza esta cadena de conexión para crear una instancia del repositorio de base de datos.

A través del repositorio de base de datos, se establece una conexión con la base de datos mediante el método GetDB. El resultado de esta operación es un objeto gorm.DB, que es una instancia de la biblioteca GORM, utilizada para interactuar con la base de datos.

Registry & Controllers

El paquete Registry en este template tiene la función de proporcionar una capa de registro e inyección de dependencias para los controladores de la API. Su objetivo principal es facilitar la creación de instancias de controladores y sus dependencias, evitando acoplamientos directos y promoviendo una estructura modular y escalable.

El archivo registry.go define una interfaz Registry y una estructura registry. La interfaz Registry expone un único método NewAppController que devuelve una instancia de AppController, que es el controller principal que encapsula otros.

La función NewRegistry(db *gorm.DB) crea una instancia de la estructura registry y recibe un objeto *gorm.DB como parámetro. Este objeto representa una conexión a la base de datos y se utiliza para crear las instancias de los controllers.

Ejemplo PersonController

Dentro de la estructura registry, el método NewAppController() crea y devuelve una instancia de AppController.

//AppController
package controller

type AppController struct {
    Person interface{ PersonController }
}

//Registry
package registry

import (
    "golang-clean-architecture/api/controller"
    "gorm.io/gorm"
)

type registry struct {
    db *gorm.DB
}

type Registry interface {
    NewAppController() controller.AppController
}

func NewRegistry(db *gorm.DB) Registry {
    return &registry{db}
}

func (r *registry) NewAppController() controller.AppController {
    return controller.AppController{
        Person: r.NewPersonController(),
    }
}
Dentro de esta función, se crea una instancia del controlador de persona (personController) utilizando las dependencias adecuadas, como el controlador de operaciones de persona (personOperation.PersonHandler) y los repositorios relacionados (repository.PersonRepository y repository.DBRepository). Estas dependencias se resuelven a través de la inyección de dependencias, asegurando una separación de responsabilidades y un bajo acoplamiento.

//PersonController
package controller

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "golang-clean-architecture/application/useCase/V1/personOperation"
    "golang-clean-architecture/domain/dtos"
    "golang-clean-architecture/domain/models"
    "golang-clean-architecture/presenter"
    "net/http"
)

type personController struct {
    personHandler personOperation.PersonHandler
}

type PersonController interface {
    GetPerson(c *gin.Context) error
    CreatePerson(c *gin.Context) error
    UpdatePerson(c *gin.Context) error
}

func NewPersonController(ph personOperation.PersonHandler) PersonController {
    return &personController{personHandler: ph}
}

func (pc *personController) GetPerson(c *gin.Context) error {
    var p []*models.Person

    p, err := pc.personHandler.Get(p)
    if err != nil {
        return err
    }

    presenter.JSONResponse(c.Writer, http.StatusOK, p)
    return nil
}

func (pc *personController) CreatePerson(c *gin.Context) error {
    var params dtos.PersonDto
    if err := c.Bind(&params); err != nil {
        presenter.JSONError(c.Writer, http.StatusBadRequest, "Invalid request parameters", "")
        return err
    }
    if err := params.Validate(); err != nil {
        presenter.JSONError(c.Writer, http.StatusBadRequest, err.Error(), "")
        return err
    }
    if p, err := pc.personHandler.Create(&models.Person{Nombre: params.Nombre, Apellido: params.Apellido}); err != nil {
        presenter.JSONError(c.Writer, http.StatusBadRequest, err.Error(), "")
    } else {
        presenter.AddHeader(c.Writer, "Nosequeotroque", "Nopseque")
        presenter.JSONResponse(c.Writer, http.StatusCreated, p)
    }
    return nil
}

func (pc *personController) UpdatePerson(c *gin.Context) error {
    var params models.Person

    if err := c.Bind(&params); err != nil {
        presenter.JSONError(c.Writer, http.StatusBadRequest, "Invalid request parameters", "")
        return err
    }

    if _, err := fmt.Sscan(c.Param("id"), &params.PersonId); err != nil {
        presenter.JSONError(c.Writer, http.StatusBadRequest, "PersonId must be a number", "")
        return err
    }

    if err := params.Validate(); err != nil {
        presenter.JSONError(c.Writer, http.StatusBadRequest, err.Error(), "")
        return err
    }

    if p, err := pc.personHandler.Update(&params); err != nil {
        presenter.JSONError(c.Writer, http.StatusBadRequest, err.Error(), "")
    } else {
        presenter.JSONResponse(c.Writer, http.StatusCreated, p)
    }

    return nil
}

Handlers

Los Handlers son los encargados de realizar la logica correspondiente para manejar las solicitudes HTTP, se implementan en el paquete Controller.

Ejemplo PersonHandler

En el archivo person_controller.go, se define la interfaz PersonController y la estructura personController. La interfaz PersonController define los métodos que deben implementarse para manejar diferentes acciones relacionadas con las personas, como obtener una persona, crear una persona o actualizar una persona.

El método NewPersonController(ph personOperation.PersonHandler) PersonController es una función que crea una nueva instancia de personController. Recibe una dependencia personOperation.PersonHandler como parámetro, que es responsable de realizar las operaciones relacionadas con las personas.

Dentro de la estructura personController, se implementan los métodos definidos en la interfaz PersonController.

//PersonHandler
package personOperation

import (
    "fmt"
    "golang-clean-architecture/domain/models"
    "golang-clean-architecture/infrastructure/repository"
)

type personHandler struct {
    PersonRepository repository.PersonRepository
    DBRepository     repository.DBRepository
}

type PersonHandler interface {
    Get(u []*models.Person) ([]*models.Person, error)
    Create(u *models.Person) (*models.PersonResponse, error)
    Update(u *models.Person) (*models.PersonResponse, error)
}

func NewPersonHandler(repo repository.PersonRepository, dbRepo repository.DBRepository) PersonHandler {
    return &personHandler{PersonRepository: repo, DBRepository: dbRepo}
}

func (ph *personHandler) Get(p []*models.Person) ([]*models.Person, error) {

    pr, err := ph.PersonRepository.FindAll(p)

    if err != nil {
        return nil, err
    }

    return pr, nil
}

func (ph *personHandler) Create(p *models.Person) (*models.PersonResponse, error) {
    data, err := ph.DBRepository.Transaction(func(i interface{}) (interface{}, error) {
        ur, err := ph.PersonRepository.Create(p)
        return ur, err
    })
    user, ok := data.(*models.PersonResponse)

    if !ok {
        return nil, fmt.Errorf("Error at Creating Person")
    }

    if err != nil {
        return nil, err
    }

    return user, nil
}

func (ph *personHandler) Update(p *models.Person) (*models.PersonResponse, error) {
    data, err := us.DBRepository.Transaction(func(i interface{}) (interface{}, error) {
    rp, err := ph.PersonRepository.Update(p)
    return u, err
    })
    user, ok := data.(*model.UserReponse)

    if !ok {
        return nil, errors.New("cast error")
    }

    if err != nil {
        return nil, err
    }

    return rp, nil
}

Repository

Aca es donde nos encargamos de generar la abstraccion y encapsulamiento de la capa de acceso a datos e interactuar con la misma mediante diversas operaciones (Get, Create, Update).

Ejemplo PersonRepository

En el archivo person_repository.go, se define la estructura personRepository que implementa la interfaz PersonRepository. Esta estructura se encarga de interactuar con la capa de almacenamiento de datos para realizar operaciones relacionadas con las personas.

La interfaz PersonRepository define los métodos que deben implementarse para interactuar con los datos de las personas, como buscar todas las personas, crear una nueva persona y actualizar una persona existente.

El método NewPersonRepository(db *gorm.DB) PersonRepository es una función que crea una nueva instancia de personRepository. Recibe una dependencia *gorm.DB como parámetro, que representa la conexión a la base de datos utilizada por el repositorio.

Dentro de la estructura personRepository, se implementan los métodos definidos en la interfaz PersonRepository.

//PersonRepository
package repository

import (
    "golang-clean-architecture/domain/models"
    "gorm.io/gorm"
)

type personRepository struct {
    db *gorm.DB
}

type PersonRepository interface {
    FindAll(u []*models.Person) ([]*models.Person, error)
    Create(u *models.Person) (*models.PersonResponse, error)
    Update(u *models.Person) (*models.PersonResponse, error)
}

func NewPersonRepository(db *gorm.DB) PersonRepository {
    return &personRepository{db}
}

func (ur *personRepository) FindAll(u []*models.Person) ([]*models.Person, error) {
    err := ur.db.Find(&u).Error

    if err != nil {
        return nil, err
    }

    return u, nil
}

func (ur *personRepository) Create(u *models.Person) (*models.PersonResponse, error) {
    if err := ur.db.Create(u).Error; err != nil {
        return nil, err
    }
    var uResp = &models.PersonResponse{Nombre: u.Nombre, Apellido: u.Apellido}
    return uResp, nil
}

func (ur *personRepository) Update(u *models.Person) (*models.PersonResponse, error) {

    if err := ur.db.Model(&u).Where("PersonId = ?", u.PersonId).Updates(u).Error; err != nil {
        return nil, err
    }
    var uResp = &models.PersonResponse{Nombre: u.Nombre, Apellido: u.Apellido}
    return uResp, nil
}

Router

El paquete Router es el encargado de enrutar las solicitudes entrantes en la API.

En el archivo router.go, se encuentra la definición del enrutador (NewRouter) y la función que maneja las rutas (NewAppController). Estas son algunas de las responsabilidades clave del Router:

  1. Creación del Router: El método NewRouter se encarga de crear y configurar un enrutador utilizando la biblioteca Gin. Recibe un parámetro engine de tipo *gin.Engine, que representa el motor de enrutamiento de Gin. Dentro de esta función, se pueden agregar configuraciones adicionales al enrutador según sea necesario.

  2. Manejo de rutas: La función NewAppController es responsable de configurar las rutas de la API y asociar manejadores a esas rutas. Recibe un parámetro router de tipo *gin.Engine, que es el enrutador creado previamente.

Tip

Notese que toda funcion que comienza con New (NewAppController, NewPersonRepository, etc) puede verse como el metodo constructor de otros lenguajes que implementan el paradigma orientado a objetos, como C#.


Worker Template

la diferencia principal entre un "worker" y una "API" es que un worker es una proceso que realiza tareas en segundo plano, mientras que una API es un proceso que expone un servicio para que otros procesos puedan consumirlo.

En este caso el worker no tiene un router ni un controller, ya que no es necesario exponer una api para que el worker pueda realizar su trabajo.

Estructura del worker

.
├── src
│    │
│    ├───application
│    │   └───useCase
│    │       └───V1
│    │           └───personOperation
│    ├───domain
│    │   ├───dtos
│    │   └───models
│    ├───infrastructure
│    │   ├───datastore
│    │   ├───registry
│    │   ├───repository
│    │   └───services
│    └───worker
│       ├── main.go
│       └───config
│             └── app.development.env
│             └── app.env
├── .gitignore
├── Dockerfile
└─ README.md

Adicionalmente, el worker cuenta con un archivo main.go que es el encargado de inicializar el worker y ejecutarlo.

package main

import (
    "golang-clean-architecture/infrastructure/datastore"
    "golang-clean-architecture/infrastructure/registry"
    "golang-clean-architecture/infrastructure/services"

    WebHost "github.com/architecture-it/gWebHost"
)

func main() {

    // load config
    WebHost.GetConfiguration("app.env", "./config")

    //Creating the server instance.
    server := WebHost.NewServer()

    db := datastore.NewDB()

    //Add API Health Check
    server.AddHealthCheck()

    // add Logger
    server.AddConfigurationLogging()

    // load registry
    r := registry.NewRegistry(db)

    // load kafka
    go datastore.NewConsumer(services.NewConsumerAceptado(r.NewPersonHandler()))

    // Run Service
    server.ListenAndServe()

}

Configuracion

La configuracion del worker son similares a la de una API, se encuentran en el archivo app.env y se puede ver de la siguiente manera:

(disclaimer: En este caso se agrega la configuracion de kafka, pero no es necesario para el funcionamiento del worker)

APLICATION_NAME=CleanArquitecture
APLICATION_VERSION=1.0.0
PORT=8080
SQLSERVER_DSN=Data Source=DBONBOARDDESA.andreani.com.ar;Initial Catalog=Onboarding;Persist Security Info=True;User ID=User_Onboarding;Password=0jp9dQALTMkOMVC0Wss2
ELASTIC_APM_SERVER_URL=https://apm-server-architecture-it-test.apps.ocptest.andreani.com.ar
ELASTIC_APM_SERVICE_NAME=golang-clean-architecture
ELASTIC_APM_ENVIRONMENT=Test
# AMQStreams Configuration
AMQSTREAMS_APPLICATION_NAME=ApplicationName
AMQSTREAMS_GROUP_ID=ExampleGroup
AMQSTREAMS_BOOTSTRAP_SERVER=SSL://cluster-kafka-bootstrap-amq-streams-test.apps.ocptest.andreani.com.ar:443
AMQSTREAMS_SCHEMA_URL=http://apicurioregistry.apps.ocptest.andreani.com.ar/apis/ccompat/v6/andreani/
AMQSTREAMS_PROTOCOL=Ssl
AMQSTREAMS_SSL_CERTIFICATE_LOCATION=./kafka.pem
AMQSTREAMS_ENABLE_SSL_CERTIFICATE_VERIFICATION=false
AMQSTREAMS_AUTO_OFFSET_RESET=Earliest

Registry

El paquete Registry es el encargado de registrar los servicios que se utilizaran en el worker.

En el archivo registry.go, se encuentra la definición del registro (NewRegistry). Estas son algunas de las responsabilidades clave del Registry:

package registry

import (
    "golang-clean-architecture/application/useCase/V1/personOperation"

    "gorm.io/gorm"
)

type registry struct {
    db *gorm.DB
}

type Registry interface {
    NewPersonHandler() personOperation.PersonHandler
}

func NewRegistry(db *gorm.DB) Registry {
    return &registry{db}
}

en este caso, el registry solo registra el servicio personOperation.PersonHandler que es el encargado de realizar las operaciones de negocio.

PersonHandler

package registry

import (
    "golang-clean-architecture/application/useCase/V1/personOperation"
    "golang-clean-architecture/infrastructure/repository"
)


func (r *registry ) NewPersonHandler() personOperation.PersonHandler {
    return personOperation.NewPersonHandler(
        repository.NewPersonRepository(r.db),
        repository.NewDBRepository(r.db),
    )
}

Services

En el paquete services se encuentran los servicios que se utilizaran en el worker.

en este caso, con la lib de gAMQStream se utiliza kafka para consumir los eventos.

aca se puede ver como se utiliza el personHandler para realizar la operacion de negocio.

package services

import (
    "golang-clean-architecture/application/useCase/V1/personOperation"
    "golang-clean-architecture/domain/models"
    "time"

    "github.com/architecture-it/gAMQStream"
    KafkaDemoEvents "github.com/architecture-it/integracion-schemas-event-go/KafkaDemo"
    "github.com/rs/zerolog/log"
)

type consumerAceptado struct {
    personHandler personOperation.PersonHandler
}

type ConsumerAceptado interface {
    Handler(event interface{}, metadata gAMQStream.ConsumerMetadata) error
}

func NewConsumerAceptado(ph personOperation.PersonHandler) ConsumerAceptado {
    return &consumerAceptado{personHandler: ph}
}

// Handler for event
func (c *consumerAceptado) Handler(event interface{}, metadata gAMQStream.ConsumerMetadata) error {
    e := event.(*KafkaDemoEvents.KafkaDemo)

    log.Print("e received: ", e)
    if e == nil {
        log.Print("e is nil")
        return nil
    }

    param := models.Person{
        PersonId: uint(e.Id),
        Nombre:   e.Me.Name,
        Apellido: e.Me.Surname,
    }

    p, err := c.personHandler.Update(&param)

    if err != nil {
        return err
    }
    log.Print("updated to:", p)

    time.Sleep(100 * time.Millisecond)

    return nil
}

En relacion con en template de la API, se puede ver que el worker es mas simple, ya que solo se encarga de consumir los eventos y ejecutar la operacion de negocio.