main.go
Si realmente quieres mejorar en el tiempo de desarrollo y especialmente en el tiempo de integración de nuevas tecnologías en tu proyecto definitivamente la Arquitectura Hexagonal es la mejor decision. Implementar esta arquitecura en algunos otros lenguages como Java o Typescript es bastante intuitivo, con Go no ocurre lo mismo, debido a que Go no cuenta con todas las herramientras de un lenguaje orientado a objetos, sin embargo puede ser implementada con conceptos equivalentes del lenguaje.
Para este blog vamos a crear un backend sencillo usando lo más escencial del lenguaje. Primero necesitamos saber la estrcutura de carpetas que vamos a usar.
cmd
main.go
internal
core
domain
user.go
ports
user_ports.go
services
user_services.go
handlers
user_handlers.go
repositories
user_repositories.go
server
server.go
go.mod
En Go no tenemos Clases(Class) pero contamos con Estructuras(Structs) y estás a su vez cuentan con algo llamado Reciver Functions que seran usados como equivalentes de los Métodos.
Para entender mejor la diferencias revisemos este ejemplo con Go y Typescript:
package domain
type User struct {
ID int
Email string
Password string
}
func NewPerson(id int, email string, password string){
return &User{
ID:id,
Email:email,
Password: password,
}
}
func (u *User) GetEmail() string {
return u.Email
}
class User {
id: number;
email: string;
password: string;
constructor(id: number, email: string, password:string){
this.id = id;
this.email = email;
this.password = password;
}
GetEmail(): string {
return this.email;
}
}
.
Como se puede apreciar en la imagen Go no tiene un constructor de manera explicita como Typescript asi que tenemos que simularlo creando una función que se encargue de devolvernos un Usuario (NewPerson en la imagen del lado izquierdo) y luego creamos un "Método" de la "Clase" usando Reciver Functions (GetEmail en la imagen del lado izquierdo). Ahora que tenemos la manera de remplazar las clases podemos empezar a construir nuestra App
Vamos a usar la estructura previa, crearemos un archivo dentro de la carpeta domain (internal/core/domain) con el nombre de user.go para entender que estaría en la carpeta domain podemos decir que es analoga a la carpeta "models" o "entitys" de algunos frameworks.
internal/core/domain/user.gopackage domain type User struct { ID int Email string Password string } func NewPerson(id int, email string, password string){ return &User{ ID:id, Email:email, Password: password, } } func (u *User) GetEmail() string { return u.Email }
Los puertos son las instructiones de como un Servicio o Repositorio debe comportarse, en esta parte le indicaremos mediante interfaces que métodos debe contener una Estructura(Clase en POO) para ser considerada un Servicio o Repositorio respectivamente.
En go las interfaces trabajan de forma distinta son implicitias eso quiere decir que no tenemos que indicar que estamos implementadola en la declaración lo único que debemos hacer es agregar los métodos y Go automaticamente lo considerara una interfaz válida.
internal/core/ports/user_ports.gopackage ports import ( "goHexagonalBlog/internal/core/domain" ) type UserRepository interface { Login(email string, password string) error Register(email string, password string) error } type UserService interface { Login(email string, password string) error Register(email string, password string, passConfirm string) error } type UserHandlers interface { Login(c *fiber.Ctx) error Register(c *fiber.Ctx) error }
Ahora gracias a las interfaces sabemos exactamente que métodos necesitamos crear para nuestro UserService, necesitamos en esta Estructura un método Login y uno Register. Pero para eso necesitamos comunicar Service con el Repository(Estrucura que se encargara de comunicarse con la base de datos) 🤔... así que Cómo lo lograremos? En la estrucura UserService agregaremos que como propiedad debe tener un UserRepository para poder funcionar. Este UserRepository sera requerido en nuestro constructor. [NewUserService(userRepository)]
internal/core/services/user_services.gopackage services import ( "errors" "goHexagonalBlog/internal/core/ports" ) type UserService struct { userRepository ports.UserRepository } //This line is for get feedback in case we are not implementing the interface correctly var _ ports.UserService = (*UserService)(nil) func NewUserService(repository ports.UserRepository) *UserService { return &UserService{ userRepository: repository, } } func (s *UserService) Login(email string, password string) error { err := s.userRepository(email, password) if err != nil { return err } return nil } func (s *UserService) Register(email string, password string, confirmPass string) error { if password != confirmPass { return errors.New("the passwords are not equal") } err := s.userRepository.Register(email, password) if err != nil { return err } return nil }
El repositorio de usuario se comunicara directamente con la base de datos, así que en la función de creación (NewUserRepository) podemos agregar la inicialización de la base de datos, para este ejemplo usaré mongo.
internal/repository/user_repositories.gopackage repositories import ( "context" "time" "goHexagonalBlog/internal/core/ports" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readpref" ) const ( MongoClientTimeout = 5 ) type UserRepository struct { client *mongo.Client database *mongo.Database collection *mongo.Collection } var _ ports.UserRepository = (*UserRepository)(nil) func NewUserRepository(conn string) *UserRepository { ctx, cancelFunc := context.WithTimeout(context.Background(), MongoClientTimeout*time.Second) defer cancelFunc() client, err := mongo.Connect(ctx, options.Client().ApplyURI( conn, )) if err != nil { return nil, err } err = client.Ping(ctx, readpref.Primary()) if err != nil { return nil, err } database := client.Database("goHexagonalBlog") collection := database.Collection("users") return &UserRepository{ client: client, database: database, collection: collection, } } func (r *UserRepository) Login(email string, password string) error { return nil } func (r *UserRepository) Register(email string, password string) error { return nil }
Ahora tenemos que exponer nuestro servicio a los handlers y estos handlers al mundo para eso usaremos GoFiber para los handlers como en las otras implementaciones haremos algo muy parecido pero esta ves UserHandlers requerira de UserService para funcionar ya que los Handlers se comunicaran con los Servicios
internal/handlers/user_handlers.gopackage handlers import ( "goHexagonalBlog/internal/core/ports" fiber "github.com/gofiber/fiber/v2" ) type UserHandlers struct { userService ports.UserService } var _ ports.UserHandlers = (*UserHandlers)(nil) func NewUserHandlers(userService ports.UserService) *UserHandlers { return &UserHandlers{ userService: userService, } } func (h *UserHandlers) Login(c *fiber.Ctx) error { var email string var password string //Extract the body and get the email and password err := h.userService.Login(email, password) if err != nil { return err } return nil } func (h *UserHandlers) Register(c *fiber.Ctx) error { var email string var password string var confirmPassword string //Extract the body and get the email and password err := h.userService.Register(email, password, confirmPassword) if err != nil { return err } return nil }
Luego de tener todas las piezas Lego lista necesitamos crear una ultima parte para inizializar nuestros programa en un servidor HTTPS, continuare usando Fiber para esto, como en las otras partes continuare usando una Estructura para hacer el programa más modular
internal/server/server.gopackage server import ( "goHexagonalBlog/internal/core/ports" "log" fiber "github.com/gofiber/fiber/v2" ) type Server struct { //We will add every new Handler here userHandlers ports.UserHandlers //middlewares ports.Middlewares //paymentHandlers ports.PaymentHandlers } func NewServer(uHandlers ports.UserHandlers) *Server { return &Server{ userHandlers: uHandlers, //paymentHandlers: pHandlers } } func (s *Server) Initialize() { app := fiber.New() v1 := app.Group("/v1") userRoutes := v1.Group("/user") userRoutes.Post("/login", s.userHandlers.Login) userRoutes.Post("/register", s.userHandlers.Register) err := app.Listen(":5000") if err != nil { log.Fatal(err) } }
Finalmente crearemos nuestro punto de entrada en la aplicación en el main.go y entenderemos mejor como esta arquitectura funciona. Recordemos que para crear nuestro UserRepository necesitamos una URL de conexión a mongo, para crear nuestro UserService necesitamos un UserRepository, para crear nuestro UserHandler necesitamos un UserService y para crear nuestro servidor necesitamos de los Hanlders.
MongoConn:string > UserRepository > UserService > UserHandler > Server
./cmd/mainpackage main import ( "goHexagonalBlog/internal/core/services" "goHexagonalBlog/internal/handlers" "goHexagonalBlog/internal/repositories" "goHexagonalBlog/internal/server" ) func main() { mongoConn := "secret🤫" //repositories userRepository := repositories.NewUserRepository(mongoConn) //services userService := services.NewUserService(userRepository) //handlers userHandlers := handlers.NewUserHandlers(userService) //server httpServer := server.NewServer( userHandlers, ) httpServer.Initialize() }
Ahora si tenemos nuestra aplicación de go construida usando Arquitectura Hexagonal y Fiber, Subiré el código a Github para que pueda ser usado como plantilla, espero que hayan disfrutado leyendo el articulo y si te gusto dejame un aplauso 👀 eso me motivara para seguir compartiendo contenido.
Visitar el repo