Descubriendo la Potencia de Go: Backend Inicial con buenas practicas
Published: 2024-04-06 03:30:23



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/imph4qzkwtxonfl7tk2t.png)





En el vasto universo de la programación, cada lenguaje tiene su brillo único, su propósito especial. Entre estos, Go (o Golang) destaca por su simplicidad, eficiencia y capacidad para manejar concurrencia. Como desarrollador apasionado por aprender y explorar, decidí embarcarme en un viaje: crear un proyecto desde cero utilizando Go. Este artículo no solo subraya la importancia de Go en el desarrollo moderno de software, sino que también comparte mi experiencia construyendo una aplicación estructurada y funcional, paso a paso.

¿Por Qué Go?
Go fue diseñado en Google para mejorar la productividad en la programación en un entorno de sistemas complejos. Su sintaxis clara y su sistema de tipos, junto con el manejo nativo de la concurrencia mediante goroutines, lo hacen excepcionalmente potente para el desarrollo de servidores web, servicios en tiempo real y herramientas de productividad.

Mi fuerte esta con NodeJS usando NestJS, en los cuales he aprendido a lo largo de mi carrera, buenas practicas aplicando:

- Arquitectura de 3 capas Repositorios, Servicios y Controladores
- Manejo de Errores
- Configuracion de base de datos
- Migraciones
- Organizacion inicial de configuraciones iniciales
- Variables de Entorno

Quise llevar todas mis experiencias y moldearlo en un backend Go, con una estructura inicial de arquitectura, por lo tanto asi comence: 

Go trabaja con un archivo principal llamado main.go, en el cual todas las instrucciones que iniciemos comenzaran en este lugar

```
package main

import "fmt"

func main() {
	fmt.Println("Hello world")
}
```


Este es el clasico hola mundo para todos, pero lo dejo para que sepas como debe de iniciar , puede tener otro nombre que ustedes quieran , pero lo importante es que se dicho archivo debe de iniciarse con la instrucción:

`go run main.go `

Antes de ponernos manos a la obra, debemos tener claro nuestra estructura de carpetas para nuestro backend, esta estructura inicial puede permitirnos tener una buena organización en la distribución de responsabilidades en nuestro backend lo cual sera el siguiente 

/api-golang
    ---/bootstrap       - Configuración del contenedor de inyección de dependencias
    ---/config          - Gestión de configuración centralizada
    ---/controller      - Controladores HTTP
    ---/database        - Conexión y configuración de la base
 de datos
    ---/dto             - Definicion de DTO para los
controladores
    ---/migrations      - Migraciones de base de datos
    ---/errors          - Definiciones de errores personalizados
    ---/models          - Modelos de datos
    ---/repositories    - Acceso y manipulación de la base de datos
    ---/services        - Lógica de negocio
    ---main.go          - Punto de entrada de la aplicación
    ---.env             - Variables de entorno

**Config**

En esta carpeta tendremos un archivo llamado config.go, en el cual tendremos configurado las variables de entorno necesarias para usar en cualquier parte de nuestro backend usando la libreria `github.com/joho/godotenv`

```
package config

import (
	"log"
	"os"

	"github.com/joho/godotenv"
)

type Config struct {
	DBUser     string
	DBPass     string
	DBHost     string
	DBPort     string
	DBName     string
	DBSSLMode  string
	ServerPort string
}

func LoadConfig() Config {
	err := godotenv.Load()
	if err != nil {
		log.Println("Warning: No .env file found")
	}

	return Config{
		DBUser:     os.Getenv("DB_USER"),
		DBPass:     os.Getenv("DB_PASSWORD"),
		DBHost:     os.Getenv("DB_HOST"),
		DBPort:     os.Getenv("DB_PORT"),
		DBName:     os.Getenv("DB_NAME"),
		DBSSLMode:  os.Getenv("DB_SSLMODE"),
		ServerPort: os.Getenv("SERVER_PORT"),
	}
}
```
el archivo .env debe de tener lo siguiente 

```
DB_USER=postgres
DB_PASSWORD=123456
DB_HOST=localhost
DB_NAME=golang
DB_PORT=5432
DB_SSLMODE=disable
```

**Database**

En esta carpeta tenemos el archivo db.go, el cual sera encargado de tener preparado nuestra conexión a la base de datos tomando en cuenta por la importacion `"api-golang/config" `, aplicando el struct de config.go en `func ConnectDatabase(cfg config.Config)`, podemos acceder a las variables de entorno necesarias para la configurar las credenciales de la base de datos

db.go
```
package database

import (
	"api-golang/config"
	"fmt"
	"log"

	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

func ConnectDatabase(cfg config.Config) (*gorm.DB, error) {
	dsn := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s",
		cfg.DBUser, cfg.DBPass, cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBSSLMode)

	database, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatalf("Failed to connect to database: %v", err)
	}
	return database, nil
}
```

**Migrations**

Es muy importante tener en nuestra estructura , un arranque inicial de una base de datos, por lo tanto por medio de la libreria `github.com/golang-migrate/migrate/v4`, tendremos las bondades de correr migraciones nuevas o modificaciones las cuales definiremos en archivos .sql, el archivo es 

/migrations/migrations.go

```
package migrations

import (
	"log"
	"path/filepath"
	"runtime"

	"github.com/golang-migrate/migrate/v4"
	_ "github.com/golang-migrate/migrate/v4/database/postgres"
	_ "github.com/golang-migrate/migrate/v4/source/file"
)

func Migrate(dsn string) {
	_, b, _, _ := runtime.Caller(0)
	dir := filepath.Join(filepath.Dir(b), "migrations")

	m, err := migrate.New("file://"+dir, dsn)
	if err != nil {
		log.Fatalf("Error while creating migrate instance: %v", err)
	}

	// Apply all the available migrations
	if err := m.Up(); err != nil && err != migrate.ErrNoChange {
		log.Fatalf("Error while applying migrations: %v", err)
	}

	log.Println("Migrations applied successfully!")
}

```
Para poder correr las migraciones por comandos cli, podemos usar un archivo Makefile, el cual es una forma de correr tareas personalizadas para GO, el cual se define asi 

```
migrateup:
	migrate -path ./migrations -database "postgres://postgres:123456@localhost:5432/golang?sslmode=disable" up

migratedown:
	migrate -path ./migrations -database "postgres://postgres:123456@localhost:5432/golang?sslmode=disable" down
```

y para correr el comando por consola `make migrateup`

al correr el comando puedes iniciar los archivos default que tengas de .sql como por ejemplo 

/migrations/1_create_products_table.up.sql
```
-- +migrate Up
CREATE TABLE IF NOT EXISTS products (
    id SERIAL PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    precio NUMERIC NOT NULL
);
```

/migrations/1_create_products_table.down.sql
```
-- +migrate Down
DROP TABLE IF EXISTS products;
```

Puedes crear todas las migraciones que necesites, la base de datos creara una tabla de control a fin de tener el seguimiento de las migraciones aplicadas.


**Errors**

Siempre es importante tener un archivo central , por el cual podemos contrar los errores por medio de un mensaje generico, el siguiente archivo tiene la configuracion inicial a usar para los errores 

/errors/errors.go

```
package errors

// ErrorType enumerates the known error types for the application
type ErrorType int

const (
	DatabaseError ErrorType = iota + 1
	ValidationError
	InternalError
)

// AppError represents an error with an associated type and message
type AppError struct {
	Type    ErrorType
	Message string
}

func (e *AppError) Error() string {
	return e.Message
}

// New crea un nuevo AppError
func New(typ ErrorType, msg string) error {
	return &AppError{
		Type:    typ,
		Message: msg,
	}
}

```

**Repositorio**

Aqui configuramos toda la logica relacionada a las consultas a la base de datos, puede ser de modo generico usando el ORM o queryBuilder, y ademas de consultas personalizadas, pero lo importante, es que la responsabilidad de manipular la base de datos solo estara en este sitio, en este caso tomando en cuenta una tabla de product, podemos tener el siguiente archivo

/repositories/product.go

```
package repositories

import (
	"api-golang/models"

	"gorm.io/gorm"
)

type ProductRepository interface {
	FindAll() ([]models.Product, error)
	Save(producto models.Product) (models.Product, error)
}

type productoRepository struct {
	db *gorm.DB
}

func NewProductoRepository(db *gorm.DB) ProductRepository {
	return &productoRepository{db: db}
}

func (r *productoRepository) FindAll() ([]models.Product, error) {
	var productos []models.Product
	result := r.db.Find(&productos)
	return productos, result.Error
}

func (r *productoRepository) Save(producto models.Product) (models.Product, error) {
	result := r.db.Create(&producto)
	return producto, result.Error
}
```

Este repositorio en Go sirve como una capa de abstracción entre la lógica de negocio de nuestra aplicación y las operaciones de base de datos específicas para Product. Utiliza el ORM Gorm, una popular biblioteca de mapeo objeto-relacional en Go, que facilita la interacción con la base de datos utilizando Go structs en lugar de consultas SQL directas, en el siguiente bloque


```
// Struct a el ORM Gorm a fin de usar las bondades del orm
type productoRepository struct {
	db *gorm.DB
}
```

`productoRepository` contiene un solo campo, db, que es un puntero a una instancia de gorm.DB. Este campo db representa una sesión de base de datos con Gorm, que se utiliza para realizar operaciones de base de datos (consultas, inserciones, actualizaciones, etc.) relacionadas con productos.

Al ser un puntero (*gorm.DB), db mantiene una referencia a la instancia de la base de datos, lo cual significa que los cambios que se realicen afectaran cualquier lado del backend, lo cual es crucial para mantener la actualizada la base de datos y al usarlo en ls siguiente funcion

```
func (r *productoRepository) FindAll() ([]models.Product, error) {
	var productos []models.Product
	result := r.db.Find(&productos)
	return productos, result.Error
}
```

r *productoRepository tiene la asociacion a las bondades del orm usando r.db.Find, ademas de usarlo con tras funciones basicas del orm para registrar, eliminar etc.

**Service**

En esta capa tenemos la logica del negocio, el cual se llamara 

/services/product.go

```
package services

import (
	"api-golang/errors"
	"api-golang/models"
	"api-golang/repositories"
)

type ProductService interface {
	GetAllProductos() ([]models.Product, error)
	CreateProduct(producto models.Product) (models.Product, error)
}

type productoService struct {
	repository repositories.ProductRepository
}

func NewProductoService(repository repositories.ProductRepository) ProductService {
	return &productoService{repository: repository}
}

func (s *productoService) GetAllProductos() ([]models.Product, error) {
	res, err := s.repository.FindAll()
	if err != nil {
		return []models.Product{}, errors.New(errors.DatabaseError, "Error find all product: "+err.Error())
	}
	return res, nil
}

func (s *productoService) CreateProduct(producto models.Product) (models.Product, error) {
	// Intenta guardar el producto y maneja tanto el producto guardado como el error
	savedProduct, err := s.repository.Save(producto)
	if err != nil {
		// Aquí manejas el error, creando un nuevo AppError con tipo DatabaseError
		return models.Product{}, errors.New(errors.DatabaseError, "Error saving product: "+err.Error())
	}
	// Si no hay error, devuelve el producto guardado y nil para el error
	return savedProduct, nil
}
```

En este archivo aplicamos nuevamente una instancia en 

```
type productoService struct {
   repository repositories.ProductRepository
}
```

en el cual instanciamos la interfaz de ProductRepository a fin de acceder a las funciones definidas como FindAll en el siguiente codigo:

```
func (s *productoService) GetAllProductos() ([]models.Product, error) {
	res, err := s.repository.FindAll()
	if err != nil {
		return []models.Product{}, errors.New(errors.DatabaseError, "Error find all product: "+err.Error())
	}
	return res, nil
}
```

En esta función accedemos a todos los productos , en el caso de que se tenga un error , se enviarn a la funcion DatabaseError del archivo definido mas arriba en `/errors/errors.go`


Controller

En esta capa tenemos el control de las solicitudes HTTP, en este lugar debemos validar lo que recibimos por payload o por params a fin de tener seguridad en el procesamiento de los datos, ademas se define la validacion de los datos usando DTO(Data Transfer Object) a fin de tener seguridad de los datos de entrada, para este caso se usaran solo dos endpoints los cuales haran el registro y obtencion de productos 

/controller/product.go

```
package controller

import (
	"api-golang/dto"
	"api-golang/models"
	"api-golang/services"
	"encoding/json"
	"net/http"
)

type ProductController struct {
	Service services.ProductService
}

func NewProductController(service services.ProductService) *ProductController {
	return &ProductController{Service: service}
}

func (c *ProductController) GetProducts(w http.ResponseWriter, r *http.Request) {
	productos, err := c.Service.GetAllProductos()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	json.NewEncoder(w).Encode(productos)
}

func (c *ProductController) CreateProduct(w http.ResponseWriter, r *http.Request) {
	var productDTO dto.ProductCreateDTO
	if err := json.NewDecoder(r.Body).Decode(&productDTO); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// DTO Validation
	if validationErrors := productDTO.Validate(); validationErrors != nil {
		http.Error(w, "Validation failed", http.StatusBadRequest)
		return
	}

	product := models.Product{Nombre: productDTO.Nombre, Precio: productDTO.Precio}
	savedProduct, err := c.Service.CreateProduct(product)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	json.NewEncoder(w).Encode(savedProduct)
}

```
Por medio de 

`type ProductController struct {
	Service services.ProductService
}`

Accedemos a la instancia de la interfaz ProductService a fin de acceder a las funciones del servicio como por ejemplo `c.Service.CreateProduct`, para tener el response final `json.NewEncoder(w).Encode(producto)` realiza la serializacion a JSON por medio de json.NewEncoder(w)

En el bloque 

`// DTO Validation
if validationErrors := productDTO.Validate(); validationErrors != nil {
		http.Error(w, "Validation failed", http.StatusBadRequest)
		return
	}`

Se realiza la validation del DTO por medio del archivo

/dto/dto.go

```
package dto

import "github.com/go-playground/validator/v10"

type ProductCreateDTO struct {
	Nombre string  `json:"name" validate:"required,min=2,max=100"`
	Precio float64 `json:"price" validate:"required,gt=0"`
}

func (p *ProductCreateDTO) Validate() []*validator.FieldError {
	validate := validator.New()
	err := validate.Struct(p)
	if err != nil {
		var fieldErrors []*validator.FieldError

		if ve, ok := err.(validator.ValidationErrors); ok {
			for _, fe := range ve {
				fieldError := fe
				fieldErrors = append(fieldErrors, &fieldError)
			}
			return fieldErrors
		}
	}
	return nil
}

```

`type ProductCreateDTO struct {
	Nombre string  `json:"name" validate:"required,min=2,max=100"`
	Precio float64 `json:"price" validate:"required,gt=0"`
}`

Usando una librera de validacion de datos `github.com/go-playground/validator/v10`, especificamos en los campos de la entidad Product por medio del struct ProductCreateDTO, los campos a validar con casos regulares de DTO como campos requeridos y tipos de datos

**Bootstrap**

En este archivo tendremos centralizado nuestros repositorios , servicios y controladores , ademas de encender la conexión de la base de datos, el cual sera el siguiente 

/bootstrap/bootstrap.go

```
package bootstrap

import (
	"api-golang/config"
	"api-golang/controller"
	"api-golang/database"
	"api-golang/repositories"
	"api-golang/services"
	"log"

	"go.uber.org/dig"
	"gorm.io/gorm"
)

func BuildContainer(cfg config.Config) *dig.Container {
	container := dig.New()

	// Database Connection Registration
	container.Provide(func() (*gorm.DB, error) {
		return database.ConnectDatabase(cfg)
	})

	// Repositories
	if err := container.Provide(repositories.NewProductoRepository); err != nil {
		log.Fatalf("Failed to provide product repository: %v", err)
	}

	// Services
	if err := container.Provide(services.NewProductoService); err != nil {
		log.Fatalf("Failed to provide product service: %v", err)
	}

	// Controller
	if err := container.Provide(controller.NewProductController); err != nil {
		log.Fatalf("Failed to provide product controller: %v", err)
	}

	return container
}

```
Es importante tener en un solo lugar, todas las incializaciones de los archivos a usar, si se requiere inicializar otros servicios, lo ideal es hacerlo en este archivo

**Main**

Finalmente en nuestro archivo main.go , tendremos todas las llamadas necesarias para iniciar nuestro backend, en este caso los importantes por los momentos es la inicializacion de las variables de entorno y de las capas definidas en la función BuildContainer de bootstrap como ademas de la definición final del route a usar para cada proposito

```
package main

import (
	"api-golang/bootstrap"
	"api-golang/config"
	"api-golang/controller"
	"log"
	"net/http"

	_ "github.com/golang-migrate/migrate/v4/database/postgres"
	_ "github.com/golang-migrate/migrate/v4/source/file"
)

func main() {
	cfg := config.LoadConfig()

	container := bootstrap.BuildContainer(cfg)

	// Invoke the container to inject the controller and configure the routes
	err := container.Invoke(func(productController *controller.ProductController) {
		http.HandleFunc("/productos", productController.GetProducts)
		http.HandleFunc("/producto", productController.CreateProduct)
	})

	if err != nil {
		log.Fatalf("Failed to invoke container: %v", err)
	}

	log.Println("Server started on port 8080")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}
```

Una ultima cosa y muy importante es tener siempre pruebas unitarias, para Go es mucho mas sencillo, podemos definir test solo con el sufijo **_test**, Go automáticamente los reconoce al correr el comando `go test`, para tener un orden de los test de nuestro backend, coloque un test del controlador, en la ruta

/controller/product_controller_test.go

```
package controller

import (
	"api-golang/dto"
	"api-golang/models"
	"api-golang/services/mocks"
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

func TestCreateProduct(t *testing.T) {

	// Prepare de ProductService Interface
	mockService := new(mocks.ProductService)
	// Prepare de The Product Instance with data
	mockProduct := models.Product{Nombre: "Test Product", Precio: 10.99}
		// Prepare de The DTO with data to validate
	mockProductDTO := dto.ProductCreateDTO{Nombre: "Test Product", Precio: 10.99}

	// Mocking the service
	mockService.On("CreateProduct", mock.AnythingOfType("models.Product")).Return(mockProduct, nil)

	// Prepare the Controller and pass the Mock Service into the controller
	controller := NewProductController(mockService)

	// Http Request
	productJSON, _ := json.Marshal(mockProductDTO)
	req, _ := http.NewRequest("POST", "/product", bytes.NewBuffer(productJSON))
	rr := httptest.NewRecorder()

    // After the http request call we use the controller to use the CreateProduct function
	handler := http.HandlerFunc(controller.CreateProduct)
	handler.ServeHTTP(rr, req)

	assert.Equal(t, http.StatusOK, rr.Code)

	// Check the mock data vs response
	var returnedProduct models.Product
	json.Unmarshal(rr.Body.Bytes(), &returnedProduct)

	assert.Equal(t, mockProduct.Nombre, returnedProduct.Nombre)
	assert.Equal(t, mockProduct.Precio, returnedProduct.Precio)

	mockService.AssertExpectations(t)
}



```

Un detalle super genial que vi con Go, es una herramienta llamada **Mockery**, la cual es una herramienta que nos ayuda a construir mocks de la interfaz del servicio ProductService, y automaticamente nos deja la logica necesaria a usar para poder realizar el test del controlador junto con la interfaz del servicio, para ejecutar automaticamente las interfaces necesarias puedes ejecutar:

`mockery --name=ProductService --output=services/mocks --outpkg=mocks --case=underscore`

luego, como tenemos nuestros test por cada archivo a usar, el comando para los test queda como `go test ./...`

Un ultimo detalle, es poder tener en nuestro desarrollo local una manera de observar nuestros cambios con hard-reload, para Go, hay una herramienta llamada **Air** la cual es excelente para iniciar nuestro servidor local, y que escuche los constantes cambios que tengamos en nuestro código, para usarlo simplemente lo configuramos con `air init` y lo iniciamos ejecutando por consola `air`


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e3uoh4p25vl849exvckw.png)

Para probar el endpoint seria `http://localhost:8080/productos`

Listo !

**Conclusion**

Tenemos un backend inicial listo para usar para nuestros proyectos, realizando un repaso sobre lo que se ha explicado en este backend tenemos lo siguiente 

-Arquitectura de 3 capas Repositorios, Servicios y Controladores
- Configuración de la base de datos
- Se aplicó la inyección de dependencias para desacoplar las capas de la aplicación.
- Se centralizaron las configuraciones utilizando variables de entorno, permitiendo que el proyecto sea fácilmente adaptable a diferentes entornos
- Manejo de Errores
- Validacion de entradas usando Data Transfer Object (DTO)
- ORM para interactuar con la base de datos
- Migraciones
- Pruebas Unitarias
- Se integraron herramientas de desarrollo como mockery para la generación de mocks y herramientas de terceros para la mejora del flujo de trabajo de desarrollo
- Hot Reloading con AIR


Siempre es bueno compartir conocimientos a nuestra comunidad, es la primera vez que realizo un articulo :D, asi que espero que este backend inicial sea util para mis colegas!

Saludos!

**Codigo:** https://github.com/jorge6242/api-golang