Upgrade to Pro — share decks privately, control downloads, hide ads and more …

DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO

DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO

Friends of Go

October 06, 2019
Tweet

More Decks by Friends of Go

Other Decks in Programming

Transcript

  1. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO domingo, 6

    octubre de 2019 Joan López de la Franca Beltran @joanjan14 Adrián Pérez Gil @adrianpgl
  2. None
  3. DURANTE EL PASADO FIN DE AÑO… DESDE EL CAOS AL

    DOMAIN-DRIVEN DESIGN EN GO
  4. ESO NOS LLEVÓ A EMPEZAR UN “SIDE PROJECT” DESDE EL

    CAOS AL DOMAIN-DRIVEN DESIGN EN GO
  5. ¡TODO TIPO DE CONTADORES! DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN

    EN GO
  6. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO “COUNTERS” -LA

    APLICACIÓN- ‣ Registro e inicio de sesión (usuarios) ‣ Crear e incrementar (contadores) ‣ Obtener estadísticas de uso (informes) ‣ Sistema de widgets (widgets) ‣ Sistema de facturación (facturación) ‣ Y más…
  7. ASÍ QUE, ¡EMPEZAMOS! DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN

    GO
  8. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO ¿QUIÉNES SOMOS?

    (ADRIÁN) ‣ Adrián Pérez ‣ a.k.a. @adrianpgl or @aperezg
  9. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO ¿QUIÉNES SOMOS?

    (ADRIÁN) ‣ Adrián Pérez ‣ a.k.a. @adrianpgl or @aperezg
 




 ‣ Desarrollador @ Europcar Mobility Group (Ubeeqo)
  10. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO ¿QUIÉNES SOMOS?

    (ADRIÁN) ‣ Adrián Pérez ‣ a.k.a. @adrianpgl or @aperezg
 




 ‣ Desarrollador @ Europcar Mobility Group (Ubeeqo)
 






 ‣ Es su primera charla en una conferencia ‣ Aunque ya ha dado varias formaciones
  11. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO ¿QUIÉNES SOMOS?

    (JOAN) ‣ Joan López de la Franca ‣ a.k.a. @joanjan14 or @joanlopez
  12. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO ¿QUIÉNES SOMOS?

    (JOAN) ‣ Joan López de la Franca ‣ a.k.a. @joanjan14 or @joanlopez
 





 ‣ Desarrollador @ Cabify (Maxi Mobility)
  13. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO ¿QUIÉNES SOMOS?

    (JOAN) ‣ Joan López de la Franca ‣ a.k.a. @joanjan14 or @joanlopez
 





 ‣ Desarrollador @ Cabify (Maxi Mobility)
 





 ‣ Ponente júnior ‣ @GopherConRu ‣ @gopherconuk
  14. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO ¿QUIÉNES SOMOS?

    (FOGO) ‣ Nacimos a principios de año ‣ Tenemos +500 seguidores ‣ Hemos redactado +40 artículos en el blog ‣ Impartimos formaciones a empresas ‣ Grabamos un curso en CodelyTV ‣ Killgrave: mock server -the easy way-
  15. ASÍ QUE, ¡EMPEZAMOS! (AHORA EN SERIO) DESDE EL CAOS AL

    DOMAIN-DRIVEN DESIGN EN GO
  16. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO LA “NO”

    ARQUITECTURA / ├── counters.go ├── users.go ├── http.go ├── utils.go ├── main.go ├── docker-compose.yml ├── Makefile ├── README.md
  17. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO LA “NO”

    ARQUITECTURA / ├── counters.go ├── users.go ├── http.go ├── utils.go ├── main.go ├── docker-compose.yml ├── Makefile ├── README.md package main import ( "log" "net/http" ) func main() { http.HandleFunc("/counters/increment", incrementHandler) http.HandleFunc("/counters/create", createHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }
  18. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO LA “NO”

    ARQUITECTURA / ├── counters.go ├── users.go ├── http.go ├── utils.go ├── main.go ├── docker-compose.yml ├── Makefile ├── README.md package main import ( "net/http" ) func createHandler(w http.ResponseWriter, r *http.Request) { counterID, err := getKeyFromHttpRequest(r, "counter") if err != nil { w.WriteHeader(http.StatusInternalServerError) return } createCounter(counterID) w.WriteHeader(http.StatusOK) } func incrementHandler(w http.ResponseWriter, r *http.Request) { counterID, err := getKeyFromHttpRequest(r, "counter") if err != nil { w.WriteHeader(http.StatusInternalServerError) return } incrementCounter(counterID) }
  19. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO LA “NO”

    ARQUITECTURA / ├── counters.go ├── users.go ├── http.go ├── utils.go ├── main.go ├── docker-compose.yml ├── Makefile ├── README.md package main var counters = make(map[string]int) func createCounter(ID string) { counters[ID] = 0 } func incrementCounter(ID string) { counters[ID]++ }
  20. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO LA “NO”

    ARQUITECTURA / ├── counters.go ├── users.go ├── http.go ├── utils.go ├── main.go ├── docker-compose.yml ├── Makefile ├── README.md package main import ( "errors" "fmt" "log" "net/http" ) func getKeyFromHttpRequest(r *http.Request, key string) (string, error) { keys, ok := r.URL.Query()[key] if !ok || len(keys[0]) < 1 { log.Println("Url Param 'key' is missing") return "", errors.New(fmt.Sprintf("%s param is missing", key)) } return keys[0], nil }
  21. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO LA “NO”

    ARQUITECTURA (CONTRAPRESTACIONES) ‣ Paquete único (WTF!?) ‣ Pérdida de visibilidad de variables ‣ Difícil de testear ‣ Difícil de ejecutar ‣ go run main.go http.go counters.go users.go utils.go ‣ go run *.go ‣ go build -o counters && ./counters
  22. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO LA ARQUITECTURA

    DE PAQUETES / ├── counters │ ├── counters.go ├── users │ ├── users.go ├── http │ ├── handlers.go │ ├── constants.go ├── utils │ ├── http.go ├── main.go ├── docker-compose.yml ├── Makefile ├── README.md
  23. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO LA ARQUITECTURA

    DE PAQUETES (CONTRAPRESTACIONES) ‣ Variables globales (= estado global) ‣ Control de concurrencia complejo (nivel paquete) ‣ Código altamente acoplado ‣ Difícil de testear ‣ Complejo de extender
  24. – ROBERT C. MARTIN, CLEAN CODE “So if you want

    to go fast, if you want to get done quickly, if you want your code to be easy to write, make it easy to read” DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO
  25. ARQUITECTURA POR CAPAS DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN

    GO INFRASTRUCTURE APPLICATION DOMAIN
  26. VALE, PERO ¿CÓMO PUEDO ESTRUCTURAR MI APLICACIÓN GO SIGUIENDO LA

    ARQUITECTURA HEXAGONAL? DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO
  27. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO ARQUITECTURA HEXAGONAL

    INFRASTRUCTURE APPLICATION DOMAIN / ├── cmd │ ├── counters │ │ ├── main.go │ ├── pkg (o internal) │ ├── … │ ├── … │ ├── … ├── docker-compose.yml ├── Makefile ├── README.md
  28. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO ARQUITECTURA HEXAGONAL

    INFRASTRUCTURE APPLICATION DOMAIN / ├── cmd │ ├── counters │ │ ├── main.go │ ├── pkg (o internal) │ ├── application/ │ ├── domain/ │ ├── infrastructure/ ├── docker-compose.yml ├── Makefile ├── README.md
  29. – DAVE CHENEY, PRACTICAL GO “Name your package for what

    it provides, not what it contains.” DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO
  30. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO ARQUITECTURA HEXAGONAL

    / ├── cmd │ ├── counters │ │ ├── main.go │ ├── pkg (o internal) │ ├── application/ │ ├── domain/ │ ├── infrastructure/ ├── docker-compose.yml ├── Makefile ├── README.md
  31. ¿PREGUNTAS? DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO

  32. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO ARQUITECTURA HEXAGONAL

    / ├── cmd │ ├── counters-api │ │ ├── main.go │ ├── pkg (o internal) │ ├── … │ ├── … │ ├── … ├── docker-compose.yml ├── Makefile ├── README.md INFRASTRUCTURE APPLICATION DOMAIN
  33. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO ARQUITECTURA HEXAGONAL

    / ├── cmd │ ├── counters-api │ │ ├── main.go ├── pkg (or internal) │ ├── server │ ├── creating │ ├── fetching │ ├── incrementing │ ├── storage │ ├── counter.go │ ├── user.go │ ├── report.go ├── docker-compose.yml ├── Makefile ├── README.md * test files were obviated for the sake of diagram’s simplicity INFRASTRUCTURE APPLICATION DOMAIN
  34. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO INFRASTRUCTURA: SERVER

    / ├── cmd ├── pkg (or internal) │ ├── server │ │ ├── http │ │ │ ├── handlers.go │ │ │ ├── requests.go │ │ │ ├── responses.go │ │ │ ├── server.go │ ├── creating │ ├── fetching │ ├── incrementing │ ├── storage │ ├── errors │ ├── counter.go │ ├── user.go ├── … package http import … func NewServer( fetchService fetching.Service, createService creating.Service, incrementService incrementing.Service, ) (http.Handler, error) { r := gin.New() r.Use(gin.Logger(), gin.Recovery()) // Auth (JWT) handler initialization authMiddleware, _ := jwt.NewGinMiddleware(fetchService) r.POST("/users/register", createUserHandlerBuilder(createService)) r.POST("/users/login", authMiddleware.LoginHandler) auth := r.Group("") auth.Use(authMiddleware.MiddlewareFunc()) auth.GET("/users/:userID", getUserHandlerBuilder(fetchService)) auth.POST("/counters", createCounterHandlerBuilder(createService)) auth.GET("/counters/:counterID", getCounterHandlerBuilder(fetchService)) auth.POST("/counters/increment", incrementCounterHandlerBuilder(fetchService, incrementService)) return r, nil }
  35. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO INFRASTRUCTURA: SERVER

    / ├── cmd ├── pkg (or internal) │ ├── server │ │ ├── http │ │ │ ├── handlers.go │ │ │ ├── requests.go │ │ │ ├── responses.go │ │ │ ├── server.go │ ├── creating │ ├── fetching │ ├── incrementing │ ├── storage │ ├── errors │ ├── counter.go │ ├── user.go ├── … package http type CreateCounterRequest struct { Name string `json:"name"` } type IncrementCounterRequest struct { ID string `json:"id"` } type CreateUserRequest struct { Name string `json:"name"` Email string `json:"email"` Password string `json:"password"` } package http type CreateCounterResponse struct { ID string `json:"id"` Name string `json:"name"` Value uint `json:"value"` } type GetCounterResponse struct { ID string `json:"id"` Name string `json:"name"` Value uint `json:"value"` } type RegisterUserResponse struct { ID string `json:"id"` Name string `json:"name"` Email string `json:"email"` } type GetUserResponse struct { ID string `json:"id"` Name string `json:"name"` Email string `json:"email"` }
  36. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO INFRASTRUCTURA: SERVER

    / ├── cmd ├── pkg (or internal) │ ├── server │ │ ├── http │ │ │ ├── handlers.go │ │ │ ├── requests.go │ │ │ ├── responses.go │ │ │ ├── server.go │ ├── creating │ ├── fetching │ ├── incrementing │ ├── storage │ ├── errors │ ├── counter.go │ ├── user.go ├── … func createCounterHandlerBuilder(createService creating.Service) func(c *gin.Context) { return func(c *gin.Context) { var req CreateCounterRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } authorizedUserData, _ := c.Get(jwt.IdentityKey) authorizedUser := authorizedUserData.(jwt.User) counter, err := createService.CreateCounter(req.Name, authorizedUser.ID) if err != nil { if errors.IsWrongInput(err) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, CreateCounterResponse{ ID: counter.ID, Name: counter.Name, Value: counter.Value, }) } }
  37. PERSISTENCIA DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO

  38. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO REPOSITORIO /

    ├── cmd ├── pkg (or internal) │ ├── server │ ├── creating │ ├── fetching │ ├── incrementing │ ├── storage │ ├── errors │ ├── counter.go │ ├── user.go ├── … package counters import … type Counter struct { ID string Name string Value uint LastUpdate time.Time BelongsTo string } type CounterRepository interface { Get(ID string) (*Counter, error) Save(counter Counter) error }
  39. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO REPOSITORIO /

    ├── cmd ├── pkg (or internal) │ ├── server │ ├── creating │ ├── fetching │ ├── incrementing │ ├── storage │ │ ├── inmemory │ │ │ ├── counters.go │ │ │ ├── users.go │ │ ├── mysql │ ├── errors │ ├── counter.go │ ├── user.go ├── … package inmemory import … type countersRepository struct { counters map[string]counters.Counter } var ( countersOnce sync.Once countersInstance *countersRepository ) func NewCountersRepository() counters.CounterRepository { countersOnce.Do(func() { countersInstance = &countersRepository{ counters: make(map[string]counters.Counter), } }) return countersInstance } func (r *countersRepository) Get(ID string) (*counters.Counter, error) { counter, ok := r.counters[ID] if !ok { return nil, errors.NewNotFound("counter with id %s not found", ID) } return &counter, nil } func (r *countersRepository) Save(counter counters.Counter) error { r.counters[counter.ID] = counter return nil }
  40. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO SERVICIO /

    ├── cmd ├── pkg (or internal) │ ├── server │ ├── creating │ │ ├── service.go │ ├── fetching │ ├── incrementing │ ├── storage │ ├── errors │ ├── counter.go │ ├── user.go ├── … package creating import … type Service interface { CreateCounter(name, belongsTo string) (counters.Counter, error) CreateUser(name, email, password string) (counters.User, error) } type service struct { counters counters.CounterRepository users counters.UserRepository } func NewCreateService(cR counters.CounterRepository, uR counters.UserRepository) Service { return &service{counters: cR, users: uR} } func (s *service) CreateCounter(name, belongsTo string) (counters.Counter, error) { // code here } func (s *service) CreateUser(name, email, password string) (counters.User, error) { // code here }
  41. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO ENTIDADES /

    ├── cmd ├── pkg (or internal) │ ├── server │ ├── creating │ ├── fetching │ ├── incrementing │ ├── storage │ ├── errors │ ├── counter.go │ ├── user.go ├── … package counters import … const ( minNameLength = 3 defaultCounterValue = 0 ) type Counter struct { ID string Name string Value uint LastUpdate time.Time BelongsTo string } func NewCounter(name, belongsTo string) (*Counter, error) { if len(name) < minNameLength { return nil, errors.NewWrongInput("counter name %s is too short", name) } return &Counter{ ID: ulid.New(), Name: name, Value: defaultCounterValue, LastUpdate: time.Now(), BelongsTo: belongsTo, }, nil } func (c *Counter) Increment() { c.Value++ }
  42. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO ENTIDADES /

    ├── cmd ├── pkg (or internal) │ ├── server │ ├── creating │ ├── fetching │ ├── incrementing │ ├── storage │ ├── errors │ ├── counter.go │ ├── user.go ├── … package counters import … const ( minNameLength = 3 defaultCounterValue = 0 ) type Counter struct { ID string Name string Value uint LastUpdate time.Time BelongsTo string } func NewCounter(name, belongsTo string) (*Counter, error) { if len(name) < minNameLength { return nil, errors.NewWrongInput("counter name %s is too short", name) } return &Counter{ ID: ulid.New(), Name: name, Value: defaultCounterValue, LastUpdate: time.Now(), BelongsTo: belongsTo, }, nil } func (c *Counter) Increment() { c.Value++ }
  43. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO VALUE OBJECT

    / ├── cmd ├── pkg (or internal) │ ├── server │ ├── creating │ ├── fetching │ ├── incrementing │ ├── storage │ ├── errors │ ├── counter.go │ ├── user.go ├── … type Counter struct { ID *CounterID Name string Value uint LastUpdate time.Time BelongsTo string } func NewCounter(id *CounterID, name, belongsTo string) (*Counter, error) { // code here } type CounterID struct { id string } func NewCounterID() *CounterID { return &CounterID{ulid.New()} } func (c CounterID) Value() string { return c.id } func (c CounterID) String() string { return c.Value() }
  44. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO INCONVENIENTES DE

    ESTA ARQUITECTURA ‣ El código relacionado con el dominio puede crecer mucho y volverse
 

inmanejable. ‣ Varias representaciones de dominio pueden producir representaciones muy 
 

confusas. ‣ Los servicios de aplicación no tienen unos límites muy definidos.
  45. ¿CÓMO ARQUITECTURAR CON CONTEXTOS? DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN

    EN GO
  46. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO LOS CONTEXTOS

    DE NUESTRA APLICACIÓN COUNTERS BC OWNER M COUNTER M USERS BC COUNTER M USER M
  47. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO CONTEXTUALIZANDO NUESTRA

    APLICACIÓN / ├─ counters │ ├── … ├─ users │ ├── … ├─ kit │ ├── … ├── go.mod ├── go.sum ├── … * los ficheros de test han sido obviados para simplificar el diagrama
  48. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO CONTEXTUALIZANDO NUESTRA

    APLICACIÓN / ├── counters │ ├── cmd │ │ ├── counters-api │ │ │ ├── internal │ │ │ │ ├── server │ │ │ ├── main.go │ ├── internal │ │ ├── counters │ │ │ ├── fetching │ │ │ ├── incrementing │ │ │ ├── counter.go │ │ │ ├── … │ │ ├── owners │ │ │ ├── … │ ├── kit * los ficheros de test han sido obviados para simplificar el diagrama / ├── users │ ├── cmd │ │ ├── users-api │ │ │ ├── internal │ │ │ │ ├── server │ │ │ ├── main.go │ ├── internal │ │ ├── users │ │ │ ├── … │ │ ├── counters │ │ │ ├── … │ ├── kit ├── kit ├── go.mod ├── go.sum ├── …
  49. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO CONTEXTUALIZANDO NUESTRA

    APLICACIÓN / ├── counters │ ├── cmd │ │ ├── counters-api │ │ │ ├── internal │ │ │ │ ├── server │ │ │ ├── main.go │ ├── internal │ │ ├── counters │ │ │ ├── fetching │ │ │ ├── incrementing │ │ │ ├── counter.go │ │ │ ├── … │ │ ├── owners │ │ │ ├── … │ ├── kit * los ficheros de test han sido obviados para simplificar el diagrama / ├── users │ ├── cmd │ │ ├── users-api │ │ │ ├── internal │ │ │ │ ├── server │ │ │ ├── main.go │ ├── internal │ │ ├── users │ │ │ ├── … │ │ ├── counters │ │ │ ├── … │ ├── kit ├── kit ├── go.mod ├── go.sum ├── …
  50. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO CONTEXTUALIZANDO NUESTRA

    APLICACIÓN / ├── counters │ ├── cmd │ │ ├── counters-api │ │ │ ├── internal │ │ │ │ ├── server │ │ │ ├── main.go │ ├── internal │ │ ├── counters │ │ │ ├── fetching │ │ │ ├── incrementing │ │ │ ├── counter.go │ │ │ ├── … │ │ ├── owners │ │ │ ├── … │ ├── kit * los ficheros de test han sido obviados para simplificar el diagrama / ├── users │ ├── cmd │ │ ├── users-api │ │ │ ├── internal │ │ │ │ ├── server │ │ │ ├── main.go │ ├── internal │ │ ├── users │ │ │ ├── … │ │ ├── counters │ │ │ ├── … │ ├── kit ├── kit ├── go.mod ├── go.sum ├── …
  51. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO ‣ Representa

    el concepto “Shared Kernel” de DDD ‣ Lo podemos aplicar tanto: ‣ Entre módulos ‣ Entre bounded contexts ‣ Seguimos nombrando a los paquetes por lo 
 


que proveen, no lo que contienen: ‣ utils, common, etc ‣ strings, ulid, etc KIT
  52. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO PRÓXIMOS PASOS

    ‣ Seguir a los maravillosos chicos de Friends of Go ‣ (Si aún no habéis empezado con Go): ‣ A Tour of Go: https://tour.golang.org/ ‣ Curso de Codely: https://pro.codely.tv/ ‣ Colaborar con la comunidad: ‣ https://github.com/trending/go ‣ https://github.com/friendsofgo/killgrave
  53. DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO ENLACES AL

    MATERIAL ‣ https://bit.ly/bcn-crafters-fogo-talk ‣ https://github.com/friendsofgo/go-architecture-examples
  54. UNA COSITA MÁS… DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN

    GO
  55. https://leanpub.com/fctd

  56. ¿PREGUNTAS? DESDE EL CAOS AL DOMAIN-DRIVEN DESIGN EN GO @JOANJAN14

    @FRIENDSOFGOTECH ¡GRACIAS! @ADRIANPGL