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 ®istry{db}
}
func (r *registry) NewAppController() controller.AppController {
return controller.AppController{
Person: r.NewPersonController(),
}
}
(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(¶ms); 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(¶ms); err != nil {
presenter.JSONError(c.Writer, http.StatusBadRequest, "Invalid request parameters", "")
return err
}
if _, err := fmt.Sscan(c.Param("id"), ¶ms.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(¶ms); 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:
-
Creación del Router: El método
NewRouter
se encarga de crear y configurar un enrutador utilizando la bibliotecaGin
. 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. -
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 ®istry{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(¶m)
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.