├── .gitignore ├── README.md ├── README_en.md ├── go_cache ├── .gitignore ├── README.md ├── README_en.md ├── go.mod ├── go.sum ├── main.go ├── model │ └── profile │ │ ├── dao.go │ │ ├── fine.go │ │ ├── service.go │ │ ├── service_test.go │ │ └── wrong.go ├── rest │ ├── middlewares │ │ └── error.go │ └── routes │ │ ├── get_device_profile_id.go │ │ └── rest.go └── utils │ └── memoize │ ├── memoize.go │ ├── memoize_test.go │ └── safe_memoize.go ├── go_declarative ├── .gitignore ├── README.md ├── README_en.md ├── controllers │ ├── get_hello_username copy.go │ ├── get_hello_username_test.go │ ├── get_ping.go │ └── router.go ├── dao │ └── hello_dao.go ├── go.mod ├── go.sum ├── main.go ├── middlewares │ ├── error.go │ └── error_test.go ├── service │ ├── hello_service.go │ └── hello_service_test.go └── utils │ ├── dialog │ └── builder.go │ ├── errors │ └── custom_error.go │ ├── gu │ └── send_utis.go │ ├── strings │ └── shorten.go │ └── test │ ├── response_writer.go │ └── response_writer_test.go ├── go_di_ioc ├── .gitignore ├── README.md ├── README_en.md ├── ejemplo_tradicional │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── model │ │ └── hello │ │ ├── dao │ │ └── hello_dao.go │ │ └── service │ │ ├── hello_service.go │ │ └── hello_service_test.go └── ioc_factory │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── model │ └── hello │ ├── dao │ └── hello_dao.go │ └── service │ ├── hello_service.go │ └── hello_service_test.go ├── go_di_ioc2 ├── .gitignore ├── README.md └── README_en.md ├── go_directories ├── .gitignore ├── README.md ├── README_en.md ├── controllers │ ├── middlewares │ │ └── error.go │ └── router │ │ ├── get_parallel_user_id.go │ │ ├── get_user_id.go │ │ └── rest.go ├── go.mod ├── go.sum ├── main.go ├── model │ ├── profile │ │ └── service.go │ └── user │ │ └── service.go └── utils │ └── errors │ └── custom_error.go ├── go_functional ├── .gitignore ├── README.md ├── README_en.md ├── go.mod ├── go.sum ├── main.go └── model │ └── hello │ ├── dao │ └── hello_dao.go │ └── service │ ├── hello_service.go │ └── hello_service_test.go ├── go_functional_polimorfism ├── .gitignore ├── README.md ├── README_en.md ├── go.mod ├── go.sum ├── main.go ├── model │ └── profile │ │ └── service.go ├── rest │ ├── middlewares │ │ └── error.go │ └── routes │ │ ├── get_device_profile_id.go │ │ └── rest.go └── utils │ └── errors │ └── custom_error.go ├── go_libs ├── .gitignore ├── README.md ├── README_en.md ├── go.mod ├── go.sum ├── main.go ├── model │ └── profile │ │ └── service.go ├── rest │ ├── middlewares │ │ └── error.go │ └── routes │ │ ├── get_device_profile_id.go │ │ └── rest.go └── utils │ └── errors │ └── custom_error.go ├── go_rest_controller ├── .gitignore ├── README.md ├── README_en.md ├── go.mod ├── go.sum ├── main.go ├── model │ └── hello │ │ ├── dao │ │ └── hello_dao.go │ │ └── service │ │ ├── hello_service.go │ │ └── hello_service_test.go └── rest │ └── routes │ ├── get_hello_username.go │ ├── get_ping.go │ └── rest.go ├── go_router_builder ├── .gitignore ├── README.md ├── README_en.md ├── go.mod ├── go.sum ├── main.go ├── model │ ├── profile │ │ └── service.go │ └── user │ │ └── service.go ├── rest │ ├── middlewares │ │ └── error.go │ └── routes │ │ ├── get_parallel_user_id.go │ │ ├── get_user_id.go │ │ └── rest.go └── utils │ └── errors │ └── custom_error.go └── go_router_design ├── .gitignore ├── README.md ├── README_en.md ├── go.mod ├── go.sum ├── img └── cor.png ├── main.go ├── model └── hello │ ├── dao │ └── hello_dao.go │ └── service │ ├── hello_service.go │ └── hello_service_test.go ├── rest ├── middlewares │ ├── error.go │ └── error_test.go └── routes │ ├── get_hello_username.go │ ├── get_hello_username_test.go │ ├── get_ping.go │ └── rest.go └── utils ├── errors └── custom_error.go └── test ├── response_writer.go └── response_writer_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | .DS_Store 3 | .vscode 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [English version](README_en.md) 4 | 5 | # Una guía sobe Go en ambientes de microservicios 6 | 7 | Este es una serie de notas sobre patrones de programación y arquitectura aplicadas a Go, mayormente para ser usadas en microservicios y sistemas pequeños, donde planteo tips y soluciones no tan populares, muy efectivas, que no por eso carecen de fundamentos, sino mas bien todo lo contrario, intento proponer un balance ideal entre código sencillo, eficiente y mantenible. 8 | 9 | --- 10 | 11 | Un buen arquitecto es aquel que diseña arquitecturas sencillas que resuelven el problema tan elegantemente, que todos pueden entender. 12 | 13 | --- 14 | 15 | ## Tabla de Contenidos 16 | 17 | Cada entrada es un repositorio que se enfoca en ciertos conceptos en particular, pudiendo no respetar otros que no se discuten en esos ejemplos. Recomiendo leerlos en orden, para obtener un contexto adecuado en los capítulos posteriores. 18 | 19 | [DI e IoC](go_di_ioc/README.md) 20 | 21 | > De donde sale la inyección de dependencias ? - Sirve en Go? - Estrategias mas simples ? 22 | 23 | [Un enfoque mas funcional](go_functional/README.md) 24 | 25 | > Go no es Orientado a Objetos - Beneficios de prog. funcional - Estrategias de mock sin interfaces innecesarias 26 | 27 | [Repasemos DI e IoC funcional](go_di_ioc2/README.md) 28 | 29 | > En programación funcional, la inyección de dependencias no existe como en OO. 30 | 31 | [REST Controllers en Go](go_rest_controller/README.md) 32 | 33 | > El MVC simple y bien explicado - Organizar correctamente nuestros controllers - Organizar el código desde el controller - REST en servicios de negocio 34 | 35 | [Router Design Pattern](go_router_design/README.md) 36 | 37 | > Que es el Router Pattern - Porque digo que no lo aprovechamos totalmente - Estrategias 38 | 39 | [Un poco mas declarativos](go_declarative/README.md) 40 | 41 | > Que es ser declarativo y como le podemos sacar ventaja en simplificar el código 42 | 43 | [Builder Pattern en Router](go_router_builder/README.md) 44 | 45 | > Como podemos aprovechar el Router mucho mas usándolo como Builder - Procesamiento en paralelo muy simple 46 | 47 | [Definiendo cross libs](go_libs/README.md) 48 | 49 | > Consejos para definir librerías compartidas y algunas buenas practicas para convivir con terceros 50 | 51 | [Polimorfismo con Funciones](go_functional_polimorfism/README.md) 52 | 53 | > Evitando interfaces innecesarias - Estrategias para hacer polimorfismo con funciones 54 | 55 | [Estructuras de Directorios](go_directories/README.md) 56 | 57 | > Dividir en capas el código - Como organizar el código inteligentemente para que encontremos cada cosa en su lugar. 58 | 59 | [Una forma adecuada de hacer cache](go_cache/README.md) 60 | 61 | > Hacer cache no es nada sencillo, se explican problemas comunes y se da una solución de cache para un caso muy puntual, cachear una respuesta remota. 62 | 63 | ## Que me motiva a escribir estas notas ? 64 | 65 | Go es un lenguaje muy particular, es y no es muchas cosas, tiene una comunidad muy compleja de entender. 66 | 67 | Si ya leíste la documentación oficial de Go, y algún que otro tutorial, pero no sabes bien como desarrollar en Go, este sitio puede ayudarte. 68 | 69 | En arquitecturas de Microservicios, Go se ha vuelto popular. Sin embargo existe un vacío enorme sobre como implementar correctamente un microservicio. 70 | 71 | Estamos viviendo momentos donde existe demasiada información, muchos autores reescriben la rueda, creando conceptos y soluciones extravagantes, y generalmente son mal interpretadas. 72 | 73 | Lineamientos como Clean Architecture, Domain Driven Design, son geniales, intentan poner los pies en la tierra, pero exponen información sin un contexto simple de definir, y con tanto ruido en el medio los programadores terminan con mas problemas que soluciones. 74 | 75 | Microservicios nos abre un mundo nuevo, cada microservicio es un sistema que ataca un problema puntual, esto nos beneficia en gran medida, porque nuestro microservicio expone una interfaz y nos da libertar de implementar internamente usando el código justo y necesario que resuelve el problema. 76 | 77 | Si no tenemos en cuenta el contexto de un microservicio y programamos con la misma receta, terminamos abarrotados, implementando patrones que resuelven problemas que no tenemos, pensamos en diseños hexagonales, desacoplamos el código, encapsulamos negocio, preparamos nuestra app para que sea políglota, y muchas otras cosas "para que escale bien", que son precisamente los problemas que en microservicios se resuelven desde la arquitectura y no desde un microservicio puntual. 78 | 79 | El error mas común, es que muchos consideran que mientras mas patrones pongamos en nuestro código, mejor se vuelve, las ideas son buenas cuando resuelven un problema, si no tenemos ese problema, la idea no sirve. 80 | 81 | > La forma mas facil de llegar de un punto a otro es en linea recta. 82 | 83 | ## Mas 84 | 85 | Ejemplos de microservicios en Go que siguen las notas en este documento : 86 | 87 | [imagego](https://github.com/nmarsollier/imagego). 88 | 89 | [authgo](https://github.com/nmarsollier/authgo). 90 | 91 | [resourcesgo](https://github.com/nmarsollier/resourcesgo) 92 | 93 | Puedes ver algunas notas sobre mi perfil en [el indice](https://github.com/nmarsollier/index). 94 | 95 | ``` 96 | 97 | ``` 98 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | [Version en Español](README.md) 2 | 3 | # A guide to code Go microservices 4 | 5 | Go is a very particular language, it is and not many things, has a complex to understand community. 6 | 7 | I you have already read official Go documentation, and others, but you don't know how to properly develop good code, this site can help you. 8 | 9 | It talks about a series of coding patterns, and architecture notes applied to Go language, mostly to be used in a small systems and microservices. 10 | 11 | All the times I talk about non popular solutions, but very effective, not because they are not popular they don't have fundaments, by contrary, I try to do a good balance between simple code, efficient and maintainable code. 12 | 13 | These are advanced ideas, to solve specific problems in an specific context, so please do not generalize then and use them everywhere. 14 | 15 | --- 16 | 17 | A good architect is the one that designs architectures that solve problems so elegantly simple, that anyone can understand it. 18 | 19 | --- 20 | 21 | ## Code samples 22 | 23 | Academic Microservices samples using the notes of this guide : 24 | 25 | [imagego](https://github.com/nmarsollier/imagego). 26 | 27 | [authgo](https://github.com/nmarsollier/authgo). 28 | 29 | [resourcesgo](https://github.com/nmarsollier/resourcesgo) 30 | 31 | ## Content Table 32 | 33 | In all these topis I talk about a particular concept, and sometimes to focus in a concept, i could not respect other important concept, so please just evaluate the concept that I'm expressing in the topic. Also note that each repository contains code, so you can see samples. 34 | 35 | I recommend to read them in the correct order. 36 | 37 | [DI and IoC](go_di_ioc/README_en.md) 38 | 39 | > Dependency Injection origins ? - It is useful in Go? - Simpler strategies ? 40 | 41 | [A functional way](go_functional/README_en.md) 42 | 43 | > Go is not Object Oriented - Benefit or functional style - Mocking strategies without interfaces 44 | 45 | [Review of DI and functional programming](go_di_ioc2/README_en.md) 46 | 47 | > In Go dependency injection is not like OO 48 | 49 | [REST Controllers in Go](go_rest_controller/README_en.md) 50 | 51 | > The MVC easy and well explained - Correct controller organization from controllers - business REST services 52 | 53 | [Router Design Pattern](go_router_design/README_en.md) 54 | 55 | > What is the Router Pattern - Why I say that we are not fully using it - Strategies 56 | 57 | [A little bit more declarative](go_declarative/README_en.md) 58 | 59 | > What is to be declarative ? - Hwo we can get the advantages of that ? 60 | 61 | [Builder Pattern in Router](go_router_builder/README_en.md) 62 | 63 | > More advance use of the Router Pattern, using it as a Builder - A simpler parallel processing engine 64 | 65 | [Polymorphism in Functional Paradigm](go_functional_polimorfism/README_en.md) 66 | 67 | > Avoiding unwanted interfaces - Strategies of polymorphism in functional programming 68 | 69 | [Directory Structure](go_directories/README_en.md) 70 | 71 | > Split the code in layers - How to organize the code intelligently so we can find each thing in their place. 72 | 73 | [A correct way to cache](go_cache/README_en.md) 74 | 75 | > Caching is not easy, I explain common cproblems of cache, and I present a solution to a common data remote cache strategy. 76 | 77 | ## Why I'm writing these notes ? 78 | 79 | With the necessity to scale in microservices environments, go has become very popular. But I see a huge vacuum about how to implement a single microservice. 80 | 81 | We are living times where there is too much information, many writers that redo the wheel, making new extravagant concepts, that generally are wrong interpreted. 82 | 83 | Clean Architecture, Domain Driven Design, guidelines, are wonderful, they try to put feet on the ground, but expose information without too much context, and with the medium noise, developers are implementing code that are mostly problems than solutions. 84 | 85 | Microservices opens a new world, each single microservice si a systems that cover a single problem, and that is a huge benefic, because a microservice exposes an interface, and we have the freedom to implement internally the correct code to resolve the problem, as a black box. 86 | 87 | But if we code without the microservice context in ming, and we use the same recipe used for monolyths, we ends crowded, implementing patterns that solves problems that we don´t have, we adopt hexagonal designs, we decouple too many layers, we prepare microservices to scale, with polyglot databases, and things like that, and many of those things are things that the microservice architecture solves by itself. 88 | 89 | The most common mistake, is to use too much patterns thinking that more is better, the patterns are good when they solve a problem, without the problem, the pattern is wrong. 90 | -------------------------------------------------------------------------------- /go_cache/.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | .DS_Store 3 | .vscode 4 | -------------------------------------------------------------------------------- /go_cache/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nmarsollier/go_cache 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | github.com/stretchr/testify v1.6.1 8 | gopkg.in/go-playground/assert.v1 v1.2.1 9 | ) 10 | -------------------------------------------------------------------------------- /go_cache/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 6 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 7 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 8 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 9 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 10 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 11 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 12 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 13 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 14 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 15 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 16 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 17 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 18 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 19 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 20 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 21 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 22 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 23 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 24 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 25 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 26 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 27 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 28 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 29 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 34 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 35 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 36 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 37 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 38 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 39 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 40 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 41 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 42 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 48 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 49 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 50 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 51 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 52 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 53 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 54 | -------------------------------------------------------------------------------- /go_cache/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nmarsollier/go_cache/rest/routes" 5 | ) 6 | 7 | func main() { 8 | routes.Start() 9 | } 10 | -------------------------------------------------------------------------------- /go_cache/model/profile/dao.go: -------------------------------------------------------------------------------- 1 | package profile 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Profile data 9 | type Profile struct { 10 | ID string 11 | Login string 12 | Name string 13 | Web string 14 | } 15 | 16 | // FetchProfile Devuelve información de Usuario 17 | func fetchProfile(id string) *Profile { 18 | fmt.Printf("Fetching Profile... %s \n", id) 19 | time.Sleep(1 * time.Second) 20 | return &Profile{ 21 | ID: id, 22 | Login: "nmarsollier", 23 | Name: "Profile # " + id, 24 | Web: "https://github.com/nmarsollier/profile", 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /go_cache/model/profile/fine.go: -------------------------------------------------------------------------------- 1 | package profile 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "time" 7 | 8 | "github.com/nmarsollier/go_cache/utils/memoize" 9 | ) 10 | 11 | var cache *memoize.Memo = nil 12 | var mutex = &sync.Mutex{} 13 | var loading int32 = 0 14 | 15 | // FetchProfile fetch the current profile 16 | func fineFetchProfile(id string) *Profile { 17 | currCache := cache 18 | if currCache != nil && currCache.Value() != nil { 19 | return currCache.Cached().(*Profile) 20 | } 21 | 22 | loadData := atomic.CompareAndSwapInt32(&loading, 0, 1) 23 | if !loadData && currCache != nil { 24 | return currCache.Cached().(*Profile) 25 | } 26 | 27 | defer mutex.Unlock() 28 | mutex.Lock() 29 | currCache = cache 30 | if loading == 0 && currCache != nil && currCache.Value() != nil { 31 | return currCache.Cached().(*Profile) 32 | } 33 | 34 | defer func() { 35 | loading = 0 36 | }() 37 | 38 | value := fetchProfile(id) 39 | currCache = memoize.Memoize(value, 10*time.Minute) 40 | cache = currCache 41 | 42 | return currCache.Cached().(*Profile) 43 | } 44 | 45 | func invalidateCache() { 46 | cache = nil 47 | } 48 | -------------------------------------------------------------------------------- /go_cache/model/profile/service.go: -------------------------------------------------------------------------------- 1 | package profile 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/nmarsollier/go_cache/utils/memoize" 7 | ) 8 | 9 | var profileMemoize = memoize.NewSafeMemoize() 10 | 11 | // FetchProfile fetch the current profile 12 | func FetchProfile(id string) *Profile { 13 | return profileMemoize.Value( 14 | func() *memoize.Memo { 15 | data := fetchProfile(id) 16 | return memoize.Memoize(data, 10*time.Minute) 17 | }, 18 | ).(*Profile) 19 | } 20 | 21 | func invalidateTSCache() { 22 | profileMemoize.InvalidateCache() 23 | } 24 | -------------------------------------------------------------------------------- /go_cache/model/profile/service_test.go: -------------------------------------------------------------------------------- 1 | package profile 2 | 3 | import ( 4 | "strconv" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/nmarsollier/go_cache/utils/memoize" 10 | ) 11 | 12 | func TestNonConcurrentWrongCache2(t *testing.T) { 13 | invalidateCache() 14 | for i := 0; i < 10; i++ { 15 | wrongCache2(strconv.Itoa(i)) 16 | } 17 | } 18 | 19 | func TestConcurrentWrongCache2(t *testing.T) { 20 | invalidateCache() 21 | for i := 0; i < 10; i++ { 22 | go wrongCache2(strconv.Itoa(i)) 23 | } 24 | 25 | for i := 0; i < 10; i++ { 26 | go wrongCache2(strconv.Itoa(i)) 27 | } 28 | } 29 | 30 | func TestConcurrentWrongCache3(t *testing.T) { 31 | invalidateCache() 32 | for i := 0; i < 10; i++ { 33 | go wrongCache3(strconv.Itoa(i)) 34 | } 35 | 36 | for i := 0; i < 10; i++ { 37 | go wrongCache3(strconv.Itoa(i)) 38 | } 39 | } 40 | 41 | func TestConcurrentFetchProfile(t *testing.T) { 42 | invalidateCache() 43 | 44 | var waitGroup sync.WaitGroup 45 | waitGroup.Add(10) 46 | for i := 0; i < 10; i++ { 47 | go func(i int) { 48 | defer waitGroup.Done() 49 | p := fineFetchProfile(strconv.Itoa(i)) 50 | t.Logf("Result Step 1 = %s = %s \n", strconv.Itoa(i), p.Name) 51 | }(i) 52 | } 53 | waitGroup.Wait() 54 | 55 | cache = memoize.Memoize(fetchProfile("Expired"), 1*time.Second) 56 | time.Sleep(2 * time.Second) 57 | 58 | waitGroup.Add(10) 59 | for i := 0; i < 10; i++ { 60 | go func(i int) { 61 | defer waitGroup.Done() 62 | p := fineFetchProfile(strconv.Itoa(i)) 63 | t.Logf("Result Step 2 = %s = %s \n", strconv.Itoa(i), p.Name) 64 | }(i) 65 | } 66 | waitGroup.Wait() 67 | 68 | p := fineFetchProfile("Final") 69 | t.Logf("Value after changes = %s \n", p.Name) 70 | } 71 | 72 | func TestSafeFetchProfile(t *testing.T) { 73 | invalidateTSCache() 74 | 75 | var waitGroup sync.WaitGroup 76 | waitGroup.Add(10) 77 | for i := 0; i < 10; i++ { 78 | go func(i int) { 79 | defer waitGroup.Done() 80 | p := FetchProfile(strconv.Itoa(i)) 81 | t.Logf("Result Step 1 = %s = %s \n", strconv.Itoa(i), p.Name) 82 | }(i) 83 | } 84 | waitGroup.Wait() 85 | 86 | profileMemoize.ReplaceMockCache(memoize.Memoize(fetchProfile("Expired"), 1*time.Second)) 87 | time.Sleep(2 * time.Second) 88 | 89 | waitGroup.Add(10) 90 | for i := 0; i < 10; i++ { 91 | go func(i int) { 92 | defer waitGroup.Done() 93 | p := FetchProfile(strconv.Itoa(i)) 94 | t.Logf("Result Step 2 = %s = %s \n", strconv.Itoa(i), p.Name) 95 | }(i) 96 | } 97 | waitGroup.Wait() 98 | 99 | // Lets wait until fetch goroutine ends 100 | time.Sleep(2 * time.Second) 101 | 102 | p := FetchProfile("Final") 103 | t.Logf("Value after changes = %s \n", p.Name) 104 | } 105 | -------------------------------------------------------------------------------- /go_cache/model/profile/wrong.go: -------------------------------------------------------------------------------- 1 | package profile 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/nmarsollier/go_cache/utils/memoize" 7 | ) 8 | 9 | func wrongCache1(id string) *Profile { 10 | if cache == nil || cache.Value() == nil { 11 | value := fetchProfile(id) 12 | cache = memoize.Memoize(value, 10*time.Minute) 13 | } 14 | 15 | return cache.Value().(*Profile) 16 | } 17 | 18 | func wrongCache2(id string) *Profile { 19 | currCache := cache 20 | 21 | if currCache == nil || currCache.Value() == nil { 22 | value := fetchProfile(id) 23 | currCache = memoize.Memoize(value, 10*time.Minute) 24 | cache = currCache 25 | } 26 | 27 | return currCache.Cached().(*Profile) 28 | } 29 | 30 | func wrongCache3(id string) *Profile { 31 | currCache := cache 32 | if currCache != nil && currCache.Value() != nil { 33 | return currCache.Cached().(*Profile) 34 | } 35 | 36 | defer mutex.Unlock() 37 | mutex.Lock() 38 | currCache = cache 39 | if currCache != nil && currCache.Value() != nil { 40 | return currCache.Cached().(*Profile) 41 | } 42 | 43 | value := fetchProfile(id) 44 | currCache = memoize.Memoize(value, 10*time.Minute) 45 | cache = currCache 46 | 47 | return currCache.Cached().(*Profile) 48 | } 49 | -------------------------------------------------------------------------------- /go_cache/rest/middlewares/error.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // ICustomError define un error con Code y Error 10 | type ICustomError interface { 11 | Code() int 12 | Error() string 13 | } 14 | 15 | // ErrorHandler a middleware to handle errors 16 | func ErrorHandler(c *gin.Context) { 17 | c.Next() 18 | 19 | handleErrorIfNeeded(c) 20 | } 21 | 22 | func handleErrorIfNeeded(c *gin.Context) { 23 | err := c.Errors.Last() 24 | if err == nil { 25 | return 26 | } 27 | 28 | switch value := err.Err.(type) { 29 | case ICustomError: 30 | c.JSON(value.Code(), 31 | gin.H{ 32 | "error": value.Error(), 33 | }) 34 | default: 35 | c.JSON(http.StatusInternalServerError, gin.H{ 36 | "error": err, 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /go_cache/rest/routes/get_device_profile_id.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/nmarsollier/go_cache/model/profile" 9 | ) 10 | 11 | // Servicio REST que nos retorna información de un dialogo a mostrar en pantalla 12 | // Vamos a usar el contexto como un Builder Pattern 13 | func init() { 14 | router().GET( 15 | "/profile", 16 | getProfile, 17 | ) 18 | } 19 | 20 | func getProfile(c *gin.Context) { 21 | data := profile.FetchProfile("123") 22 | 23 | if data == nil { 24 | c.AbortWithError(500, errors.New("Internal Server Error")) 25 | return 26 | } 27 | 28 | c.JSON(http.StatusOK, gin.H{ 29 | "login": data.Login, 30 | "web": data.Web, 31 | "name": data.Name, 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /go_cache/rest/routes/rest.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/nmarsollier/go_cache/rest/middlewares" 6 | ) 7 | 8 | // Start server in 8080 port 9 | func Start() { 10 | router().Run(":8080") 11 | } 12 | 13 | var engine *gin.Engine = nil 14 | 15 | func router() *gin.Engine { 16 | if engine == nil { 17 | engine = gin.Default() 18 | engine.Use(middlewares.ErrorHandler) 19 | } 20 | 21 | return engine 22 | } 23 | -------------------------------------------------------------------------------- /go_cache/utils/memoize/memoize.go: -------------------------------------------------------------------------------- 1 | package memoize 2 | 3 | import "time" 4 | 5 | type Memo struct { 6 | created time.Time 7 | retain time.Duration 8 | expire time.Time 9 | value interface{} 10 | } 11 | 12 | // Memoize a value for the given time, retain = 0 means forever 13 | func Memoize(value interface{}, retain time.Duration) *Memo { 14 | return &Memo{ 15 | retain: retain, 16 | created: time.Now(), 17 | expire: time.Now().Add(retain), 18 | value: value, 19 | } 20 | } 21 | 22 | func timestamp() int64 { 23 | return time.Now().UnixNano() / int64(time.Millisecond) 24 | } 25 | 26 | // Value is the cached value or nil if no longer valid 27 | func (m *Memo) Value() interface{} { 28 | if m.retain == 0 { 29 | return m.value 30 | } 31 | 32 | if time.Now().Before(m.expire) { 33 | return m.value 34 | } 35 | 36 | return nil 37 | } 38 | 39 | // Cached value 40 | func (m *Memo) Cached() interface{} { 41 | return m.value 42 | } 43 | -------------------------------------------------------------------------------- /go_cache/utils/memoize/memoize_test.go: -------------------------------------------------------------------------------- 1 | package memoize 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "gopkg.in/go-playground/assert.v1" 8 | ) 9 | 10 | func TestMemoize1Sec(t *testing.T) { 11 | memo1Sec := Memoize("hello", 1000) 12 | 13 | assert.Equal(t, memo1Sec.Value(), "hello") 14 | assert.Equal(t, memo1Sec.Cached(), "hello") 15 | 16 | time.Sleep(1 * time.Second) 17 | 18 | assert.Equal(t, memo1Sec.Value(), nil) 19 | assert.Equal(t, memo1Sec.Cached(), "hello") 20 | } 21 | -------------------------------------------------------------------------------- /go_cache/utils/memoize/safe_memoize.go: -------------------------------------------------------------------------------- 1 | package memoize 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | type SafeMemoize struct { 9 | cache *Memo 10 | mutex *sync.Mutex 11 | loading int32 12 | } 13 | 14 | // InvalidateCache invalidates the cache 15 | func (m *SafeMemoize) InvalidateCache() { 16 | m.cache = nil 17 | } 18 | 19 | // ReplaceMockCache just to mock test, this should be removed 20 | func (m *SafeMemoize) ReplaceMockCache(newCache *Memo) { 21 | m.cache = newCache 22 | } 23 | 24 | // Value get cached value, fetching data if needed 25 | func (m *SafeMemoize) Value( 26 | fetchFunc func() *Memo, 27 | ) interface{} { 28 | currCache := m.cache 29 | if currCache != nil && currCache.Value() != nil { 30 | return currCache.Cached() 31 | } 32 | 33 | loadData := atomic.CompareAndSwapInt32(&m.loading, 0, 1) 34 | if currCache == nil { 35 | // No data cached, just lock the concurrent calls 36 | currCache = m.fetchData(fetchFunc) 37 | } else if loadData { 38 | // There is a valid cache, load in goroutine 39 | go m.fetchData(fetchFunc) 40 | } 41 | 42 | // The only possibility of nil, is on first cache load error 43 | // process will retry but this one has failed 44 | if currCache == nil { 45 | return nil 46 | } 47 | 48 | return currCache.Cached() 49 | } 50 | 51 | func (m *SafeMemoize) fetchData( 52 | fetchFunc func() *Memo, 53 | ) *Memo { 54 | defer m.mutex.Unlock() 55 | m.mutex.Lock() 56 | currCache := m.cache 57 | if m.loading == 0 && currCache != nil && currCache.Value() != nil { 58 | return currCache 59 | } 60 | 61 | defer func() { m.loading = 0 }() 62 | 63 | currCache = fetchFunc() 64 | if currCache != nil { 65 | // nil means an error, to be resilent will not update chache 66 | // dev need to work on a good error handler 67 | m.cache = currCache 68 | } 69 | return currCache 70 | } 71 | 72 | // NewSafeMemoize creates new thread safe memoization 73 | func NewSafeMemoize() *SafeMemoize { 74 | return &SafeMemoize{ 75 | cache: nil, 76 | mutex: &sync.Mutex{}, 77 | loading: 0, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /go_declarative/.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | .DS_Store 3 | .vscode 4 | -------------------------------------------------------------------------------- /go_declarative/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [English Version](README_en.md) 4 | 5 | # Un poco mas declarativos 6 | 7 | Si bien a la fecha, Go es un lenguaje netamente imperativo, en este repositorio planteo algunos tips, para escribir código un poco mas declarativo. 8 | 9 | ## Que seria ser declarativo ? 10 | 11 | Go no es declarativo, por consiguiente, es una excelente pregunta. 12 | 13 | Ser declarativos es evitar escribir orientándonos a los procedimientos lógicos que nos dan un resultado, ejemplo : 14 | 15 | ```go 16 | // Paradigma imperativo 17 | func Suma(start, end int) int { 18 | suma := 0 19 | for i := start; i < end; i++ { 20 | suma += i 21 | } 22 | return suma 23 | } 24 | 25 | // Paradigma declarativo 26 | suma := Suma(1, 100) 27 | ``` 28 | 29 | La idea, entonces es, generar una seria de funciones genéricas (imperativas) que nos abstraigan un poco de la parte procedural, para poder escribir nuestra lógica de negocios en forma declarativa, de forma tal que resulte mucho mas fácil de leer. 30 | 31 | Los lenguajes funcionales en general son declarativos, y no imperativos. 32 | 33 | Muchos lenguajes OO modernos también nos dan muchas herramientas para expresar el código en forma declarativa, como Typescript, Kotlin o Swift. 34 | 35 | ### La programación procedural desaparece ? 36 | 37 | En go no, porque no es declarativo, deberemos programar funciones procedurales en algún lugar, pero la idea es que sean escritas a modo de tools, o librerías, que sean simples, que respeten el paradigma funcional, puntuales y que se usen a alto nivel. 38 | 39 | ## Y cual es la ventaja ? 40 | 41 | Existen grandes ventajas, a la hora de definir lógica de negocios, la programación declarativa nos permite ser mas expresivos, definir lo que queremos obtener en un lenguaje mas humano y por consiguiente mas fácil de leer y de mantener. 42 | 43 | ## Ejemplo: Una función para no repetir 44 | 45 | Una de las ideas principales, es intentar generar funciones para esas cosas que se repiten en todos lados, por ejemplo, del código de los tutoriales anteriores : 46 | 47 | ```go 48 | func sayHelloHandler(c *gin.Context) { 49 | userName := c.Param("userName") 50 | 51 | c.JSON(http.StatusOK, gin.H{ 52 | "answer": service.SayHello(userName), 53 | }) 54 | } 55 | ``` 56 | 57 | ```go 58 | func pingHandler(c *gin.Context) { 59 | c.JSON(http.StatusOK, gin.H{ 60 | "answer": "pong", 61 | }) 62 | } 63 | ``` 64 | 65 | Si escribimos una librería como : 66 | 67 | ```go 68 | func SendJSONAnswer(c *gin.Context, data interface{}) { 69 | c.JSON(http.StatusOK, gin.H{ 70 | "answer": data, 71 | }) 72 | } 73 | ``` 74 | 75 | Nos permite ser mas declarativo, a su vez no repetir. Vemos como queda el código nuevo: 76 | 77 | ```go 78 | func sayHelloHandler(c *gin.Context) { 79 | gu.SendJSONAnswer(c, service.SayHello(c.Param("userName"))) 80 | } 81 | ``` 82 | 83 | ```go 84 | func pingHandler(c *gin.Context) { 85 | gu.SendJSONAnswer(c, "pong") 86 | } 87 | ``` 88 | 89 | ## El nombre de las funciones es la clave 90 | 91 | Un aspecto interesante que buscamos cuando programamos en forma declarativa, es intentar que el código se parezca lo mas posible a una conversación en lenguaje natural. 92 | 93 | Si bien esta forma de pensar no esta 100% alineada con la practica de escribir lo menos posible de Go, tampoco debemos considerar que Go nos sugiere escribir de menos a la hora de ser expresivos, hay que buscar el balance justo. 94 | 95 | Por ejemplo, siguiendo el ejemplo anterior, si en vez de llamarle sayHelloHandler al método le llamamos sayHello, queda mas natural: 96 | 97 | ```go 98 | func init() { 99 | getRouter().GET( 100 | "/hello/:userName", 101 | validateUserName, 102 | sayHello, 103 | ) 104 | } 105 | ``` 106 | 107 | Y somos mas declarativos. 108 | 109 | ## Un builder 110 | 111 | Un builder es el caso típico de uso de programación declarativa 112 | 113 | ```go 114 | dialog.NewBuilder().Title("Hola Mundo").AcceptAction("Aceptar", "ok").Build() 115 | ``` 116 | 117 | Pienso que lo tiene de interesante el patrón builder, es que nos gusta porque nos permite ser declarativos. 118 | 119 | Una implementación de builder aceptable, podría ser : 120 | 121 | ```go 122 | package dialog 123 | 124 | import ( 125 | "encoding/json" 126 | ) 127 | 128 | type dialogAction struct { 129 | Label string `json:"label,omitempty"` 130 | Action string `json:"action,omitempty"` 131 | } 132 | 133 | type dialog struct { 134 | Title *string `json:"title,omitempty"` 135 | Accept *dialogAction `json:"accept,omitempty"` 136 | } 137 | 138 | type DialogBuilder struct { 139 | dialog 140 | } 141 | 142 | func NewBuilder() *DialogBuilder { 143 | return &DialogBuilder{dialog{}} 144 | } 145 | 146 | func (d *DialogBuilder) Title(value string) *DialogBuilder { 147 | return &DialogBuilder{dialog{ 148 | Title: &value, 149 | Accept: d.dialog.Accept, 150 | }} 151 | } 152 | 153 | func (d *DialogBuilder) AcceptAction(label, action string) *DialogBuilder { 154 | return &DialogBuilder{dialog{ 155 | Title: d.dialog.Title, 156 | Accept: &dialogAction{ 157 | Label: label, 158 | Action: action, 159 | }, 160 | }} 161 | } 162 | 163 | func (d *DialogBuilder) Build() string { 164 | result, _ := json.Marshal(d.dialog) 165 | return string(result) 166 | } 167 | ``` 168 | 169 | Podemos evitarnos la creación del builder, simplemente con una función, pero este tipo de patrones es mas declarativo y mas simple de leer. 170 | 171 | ## Encadenando Comportamientos 172 | 173 | Podemos escribir el resto de las apps en forma mas declarativa ? 174 | No se si justifica para un lenguaje como Go, pero seguramente alguna vez nos vamos a cruzar con código que es mas sencillo desarrollarlo con la siguiente estrategia... 175 | 176 | Veamos un ejemplo de una función en forma imperativa 177 | 178 | ```go 179 | func Shorten(name string) string { 180 | values := strings.Split(name, " ") 181 | 182 | result := "" 183 | 184 | for _, v := range values { 185 | if len(v) > 0 { 186 | result += strings.ToUpper(string(v[0])) 187 | } 188 | } 189 | 190 | return result 191 | } 192 | ``` 193 | 194 | Ya se dieron cuenta que hace ? Que pasa si lo escribimos en forma declarativa : 195 | 196 | ```go 197 | func Shorten(name string) string { 198 | return fromString(name). 199 | split(" "). 200 | mapNotEmpty(func(s string) string { 201 | return strings.ToUpper(string(s[0])) 202 | }). 203 | joinToString() 204 | } 205 | ``` 206 | 207 | Podemos notar luego de leer el código declarativo, que es mucho mas simple entender que hace, porque tal cual se lee... 208 | 209 | - tomamos un string 210 | - separamos las palabras en un arreglo segun los espacios en blanco 211 | - y concertimos en ese array en otro segun el criterio: a cada elemento le tomamos el primer char y lo convertimos a mayusculas 212 | - luego volvemos a concatenar el array en un string 213 | 214 | simplemente convierte algo como "uno dos tres" en "UDT". 215 | 216 | Ahora para que el código anterior funcione, necesitamos estas herramientas : 217 | 218 | ```go 219 | type shorten struct{ string } 220 | type shortenSlice struct{ slice []string } 221 | 222 | func fromString(value string) shorten { 223 | return shorten{value} 224 | } 225 | 226 | func (s shorten) split(separator string) shortenSlice { 227 | return shortenSlice{strings.Split(s.string, separator)} 228 | } 229 | 230 | func (values shortenSlice) mapNotEmpty(f func(string) string) shortenSlice { 231 | var result []string 232 | for _, v := range values.slice { 233 | if len(v) > 0 { 234 | result = append(result, f(v)) 235 | } 236 | } 237 | return shortenSlice{result} 238 | } 239 | 240 | func (values shortenSlice) joinToString() (result string) { 241 | for _, v := range values.slice { 242 | result += v 243 | } 244 | return result 245 | } 246 | ``` 247 | 248 | La cantidad de código a escribir parece mucha, pero consideremos que shorten y shortenSlice deberían ser librerías genéricas, y reutilizables, nuestro enfoque a la hora de leer que hace se limita a la función Shorten. 249 | 250 | Siempre es conveniente comenzar con una buena base, por ejemplo la librería https://github.com/jucardi/go-streams, nos proporciona formas declarativas básicas para manejar listas y luego le agregaremos nuestras funciones mas concretas. 251 | 252 | Creo que es importante ir revisando estas estrategias, en el futuro de Go nos han prometido Geneircs, y un manejo mas declarativo en el lenguaje, veremos. 253 | 254 | ## Nota 255 | 256 | Esta es una serie de notas sobre patrones simples de programación en GO. 257 | 258 | [Tabla de Contenidos](../README.md) 259 | -------------------------------------------------------------------------------- /go_declarative/README_en.md: -------------------------------------------------------------------------------- 1 | [Versión en Español](README.md) 2 | 3 | # A little bit more declarative 4 | 5 | Go is an imperative language so far, in this note I will trey to give some tips to write code in a more declarative way. 6 | 7 | ## What meas more declarative ? 8 | 9 | Go is not a declarative language, so, it is a good question. 10 | 11 | To be declarative, is to avoid write code in a procedural way, like : 12 | 13 | ```go 14 | // imperative 15 | func Suma(start, end int) int { 16 | suma := 0 17 | for i := start; i < end; i++ { 18 | suma += i 19 | } 20 | return suma 21 | } 22 | 23 | // declarative 24 | suma := Suma(1, 100) 25 | ``` 26 | 27 | The basic idea, is to build a set of generic functions (imperative) that are used as a high level declarative way. So business rules are expressed in a declarative format. 28 | 29 | Functional languages are in general declarative. But the concept is so powerful that many OO languages has adopted tools to write code more declarative like Typescript, Kotlin o Swift. 30 | 31 | ### Procedural programming disappears ? 32 | 33 | Not in GO, but we can build libraries and tools that let us express in a declarative way in high level control flow. 34 | 35 | ## What is the advantage ? 36 | 37 | There are many of them, at business code writhing time, the declarative way allows the code to be more expressive, we define what we need and not how, it's easy to read and maintain. 38 | 39 | ## Example: A function to not repeat 40 | 41 | One DRY concept, to avoid code repetition is to build a function that does that part of the code : 42 | 43 | ```go 44 | func sayHelloHandler(c *gin.Context) { 45 | userName := c.Param("userName") 46 | 47 | c.JSON(http.StatusOK, gin.H{ 48 | "answer": service.SayHello(userName), 49 | }) 50 | } 51 | ``` 52 | 53 | ```go 54 | func pingHandler(c *gin.Context) { 55 | c.JSON(http.StatusOK, gin.H{ 56 | "answer": "pong", 57 | }) 58 | } 59 | ``` 60 | 61 | If we write a library like : 62 | 63 | ```go 64 | func SendJSONAnswer(c *gin.Context, data interface{}) { 65 | c.JSON(http.StatusOK, gin.H{ 66 | "answer": data, 67 | }) 68 | } 69 | ``` 70 | 71 | That allow us to be more declarative, and to not repeat at the time, the new code is : 72 | 73 | ```go 74 | func sayHelloHandler(c *gin.Context) { 75 | gu.SendJSONAnswer(c, service.SayHello(c.Param("userName"))) 76 | } 77 | ``` 78 | 79 | ```go 80 | func pingHandler(c *gin.Context) { 81 | gu.SendJSONAnswer(c, "pong") 82 | } 83 | ``` 84 | 85 | ## The function name is the key 86 | 87 | An important aspect that we look for, is to write function names like the natural language. 88 | 89 | For example, if instead of call the previous example sayHelloHandler we call it sayHello, it gets more natural: 90 | 91 | ```go 92 | func init() { 93 | getRouter().GET( 94 | "/hello/:userName", 95 | validateUserName, 96 | sayHello, 97 | ) 98 | } 99 | ``` 100 | 101 | And we are being more declarative. 102 | 103 | ## A builder 104 | 105 | A builder is a typical use scenario to be declarative. 106 | 107 | ```go 108 | dialog.NewBuilder().Title("Hola Mundo").AcceptAction("Aceptar", "ok").Build() 109 | ``` 110 | 111 | I think that we like the builder pattern, because it is declarative. 112 | 113 | The implementation could be : 114 | 115 | ```go 116 | package dialog 117 | 118 | import ( 119 | "encoding/json" 120 | ) 121 | 122 | type dialogAction struct { 123 | Label string `json:"label,omitempty"` 124 | Action string `json:"action,omitempty"` 125 | } 126 | 127 | type dialog struct { 128 | Title *string `json:"title,omitempty"` 129 | Accept *dialogAction `json:"accept,omitempty"` 130 | } 131 | 132 | type DialogBuilder struct { 133 | dialog 134 | } 135 | 136 | func NewBuilder() *DialogBuilder { 137 | return &DialogBuilder{dialog{}} 138 | } 139 | 140 | func (d *DialogBuilder) Title(value string) *DialogBuilder { 141 | return &DialogBuilder{dialog{ 142 | Title: &value, 143 | Accept: d.dialog.Accept, 144 | }} 145 | } 146 | 147 | func (d *DialogBuilder) AcceptAction(label, action string) *DialogBuilder { 148 | return &DialogBuilder{dialog{ 149 | Title: d.dialog.Title, 150 | Accept: &dialogAction{ 151 | Label: label, 152 | Action: action, 153 | }, 154 | }} 155 | } 156 | 157 | func (d *DialogBuilder) Build() string { 158 | result, _ := json.Marshal(d.dialog) 159 | return string(result) 160 | } 161 | ``` 162 | 163 | We could have used a single function, with parameters, but sometimes this pattern is more expressive and simple. 164 | 165 | ## Chaining behaviors 166 | 167 | Could we write all the app declaratively ? 168 | 169 | Not sure if worth in Go, but surely many times we will see code that could become simple if we are declarative. 170 | 171 | Lets check this imperative function. 172 | 173 | ```go 174 | func Shorten(name string) string { 175 | values := strings.Split(name, " ") 176 | 177 | result := "" 178 | 179 | for _, v := range values { 180 | if len(v) > 0 { 181 | result += strings.ToUpper(string(v[0])) 182 | } 183 | } 184 | 185 | return result 186 | } 187 | ``` 188 | 189 | Have you discover what it does ? What if we define the same thing in a declarative way: 190 | 191 | ```go 192 | func Shorten(name string) string { 193 | return fromString(name). 194 | split(" "). 195 | mapNotEmpty(func(s string) string { 196 | return strings.ToUpper(string(s[0])) 197 | }). 198 | joinToString() 199 | } 200 | ``` 201 | 202 | Now after read the code we can see what it does, because : 203 | 204 | - from an string 205 | - it split the words in an array 206 | - and maps the array in another one accordin to the criteria : take the uppercase first letter of each element 207 | - then combines the array in an string 208 | 209 | It simply converts "one little thing" in "OLT". 210 | 211 | Now to make that code works, we need these libraries : 212 | 213 | ```go 214 | type shorten struct{ string } 215 | type shortenSlice struct{ slice []string } 216 | 217 | func fromString(value string) shorten { 218 | return shorten{value} 219 | } 220 | 221 | func (s shorten) split(separator string) shortenSlice { 222 | return shortenSlice{strings.Split(s.string, separator)} 223 | } 224 | 225 | func (values shortenSlice) mapNotEmpty(f func(string) string) shortenSlice { 226 | var result []string 227 | for _, v := range values.slice { 228 | if len(v) > 0 { 229 | result = append(result, f(v)) 230 | } 231 | } 232 | return shortenSlice{result} 233 | } 234 | 235 | func (values shortenSlice) joinToString() (result string) { 236 | for _, v := range values.slice { 237 | result += v 238 | } 239 | return result 240 | } 241 | ``` 242 | 243 | A lot of code, but we should consider that the shorten and shortenSlice structs are generics libraries, and we can reuse them, we are focusing in the business rule that is the Shorten function. 244 | 245 | It's also good to start from some point like library https://github.com/jucardi/go-streams, that give us some declarative functions to do basic things. 246 | 247 | ## Note 248 | 249 | This is a series of notes about advanced Go patterns, with a really simple implementation. 250 | 251 | [Content Table](../README_en.md) 252 | -------------------------------------------------------------------------------- /go_declarative/controllers/get_hello_username copy.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/nmarsollier/go_declarative/service" 6 | "github.com/nmarsollier/go_declarative/utils/errors" 7 | "github.com/nmarsollier/go_declarative/utils/gu" 8 | ) 9 | 10 | // Internal configure ping/pong service 11 | func init() { 12 | router().GET( 13 | "/hello/:userName", 14 | validateUserName, 15 | sayHello, 16 | ) 17 | } 18 | 19 | // validamos que el parametro userName tenga al menos 5 caracteres 20 | func validateUserName(c *gin.Context) { 21 | userName := c.Param("userName") 22 | 23 | if len(userName) < 5 { 24 | c.Error(errors.NewCustomError(400, "userName debe tener al menos 5 caracteres")) 25 | c.Abort() 26 | return 27 | } 28 | } 29 | 30 | func sayHello(c *gin.Context) { 31 | gu.SendJSONAnswer(c, service.SayHello(c.Param("userName"))) 32 | } 33 | -------------------------------------------------------------------------------- /go_declarative/controllers/get_hello_username_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/nmarsollier/go_declarative/utils/test" 9 | "gopkg.in/go-playground/assert.v1" 10 | ) 11 | 12 | func TestValidateUserName(t *testing.T) { 13 | response := test.ResponseWriter(t) 14 | context, _ := gin.CreateTestContext(response) 15 | context.Request, _ = http.NewRequest("GET", "/hello/abc", nil) 16 | 17 | validateUserName(context) 18 | 19 | response.Assert(0, "") 20 | assert.Equal(t, context.Errors.Last().Error(), "userName debe tener al menos 5 caracteres") 21 | } 22 | -------------------------------------------------------------------------------- /go_declarative/controllers/get_ping.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/nmarsollier/go_declarative/utils/gu" 6 | ) 7 | 8 | // Internal configure ping/pong service 9 | func init() { 10 | router().GET("/ping", ping) 11 | } 12 | 13 | func ping(c *gin.Context) { 14 | gu.SendJSONAnswer(c, "pong") 15 | } 16 | -------------------------------------------------------------------------------- /go_declarative/controllers/router.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/nmarsollier/go_declarative/middlewares" 6 | ) 7 | 8 | // Start server in 8080 port 9 | func Start() { 10 | router().Run(":8080") 11 | } 12 | 13 | var engine *gin.Engine = nil 14 | 15 | func router() *gin.Engine { 16 | if engine == nil { 17 | engine = gin.Default() 18 | engine.Use(middlewares.ErrorHandler) 19 | } 20 | 21 | return engine 22 | } 23 | -------------------------------------------------------------------------------- /go_declarative/dao/hello_dao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | // GetHello recupera de algún lado un string, podría ser desde un 4 | // endpoint remoto o una base de datos 5 | func GetHello() string { 6 | return "Holiiiiis" 7 | } 8 | -------------------------------------------------------------------------------- /go_declarative/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nmarsollier/go_declarative 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | github.com/stretchr/testify v1.6.1 8 | gopkg.in/go-playground/assert.v1 v1.2.1 9 | ) 10 | -------------------------------------------------------------------------------- /go_declarative/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 6 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 7 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 8 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 9 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 10 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 11 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 12 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 13 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 14 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 15 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 16 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 17 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 18 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 19 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 20 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 21 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 22 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 23 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 24 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 25 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 26 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 27 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 28 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 29 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 34 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 35 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 36 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 37 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 38 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 39 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 40 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 41 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 42 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 48 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 49 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 50 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 51 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 52 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 53 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 54 | -------------------------------------------------------------------------------- /go_declarative/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nmarsollier/go_declarative/controllers" 5 | ) 6 | 7 | func main() { 8 | controllers.Start() 9 | } 10 | -------------------------------------------------------------------------------- /go_declarative/middlewares/error.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // ICustomError define un error con Code y Error 10 | type ICustomError interface { 11 | Code() int 12 | Error() string 13 | } 14 | 15 | // ErrorHandler a middleware to handle errors 16 | func ErrorHandler(c *gin.Context) { 17 | c.Next() 18 | 19 | handleErrorIfNeeded(c) 20 | } 21 | 22 | func handleErrorIfNeeded(c *gin.Context) { 23 | err := c.Errors.Last() 24 | if err == nil { 25 | return 26 | } 27 | 28 | switch value := err.Err.(type) { 29 | case ICustomError: 30 | c.JSON(value.Code(), 31 | gin.H{ 32 | "error": value.Error(), 33 | }) 34 | case error: 35 | c.JSON(http.StatusInternalServerError, 36 | gin.H{ 37 | "error": value.Error(), 38 | }) 39 | default: 40 | c.JSON(http.StatusInternalServerError, gin.H{ 41 | "error": err, 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /go_declarative/middlewares/error_test.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/gin-gonic/gin" 8 | errutils "github.com/nmarsollier/go_declarative/utils/errors" 9 | "github.com/nmarsollier/go_declarative/utils/test" 10 | ) 11 | 12 | func TestCustomError(t *testing.T) { 13 | response := test.ResponseWriter(t) 14 | context, _ := gin.CreateTestContext(response) 15 | 16 | context.Error(errutils.NewCustomError(400, "Custom Test")) 17 | handleErrorIfNeeded(context) 18 | 19 | response.Assert(400, "{\"error\":\"Custom Test\"}") 20 | } 21 | 22 | func TestError(t *testing.T) { 23 | response := test.ResponseWriter(t) 24 | context, _ := gin.CreateTestContext(response) 25 | 26 | context.Error(errors.New("Error Test")) 27 | handleErrorIfNeeded(context) 28 | 29 | response.Assert(500, "{\"error\":\"Error Test\"}") 30 | } 31 | -------------------------------------------------------------------------------- /go_declarative/service/hello_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/nmarsollier/go_declarative/dao" 7 | ) 8 | 9 | // Nos va a permitir mockear respuestas para los tests 10 | // en producción deberia funcionar muy bien, porque solo 11 | // los tests pueden cambiar este valor 12 | var daoGetHello func() string = dao.GetHello 13 | 14 | // SayHello es nuestro negocio 15 | func SayHello(userName string) string { 16 | return fmt.Sprintf("%s %s", daoGetHello(), userName) 17 | } 18 | -------------------------------------------------------------------------------- /go_declarative/service/hello_service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSayHelo(t *testing.T) { 10 | defaultHelloFunc := daoGetHello 11 | 12 | // Cuando testeamos la reescribimos con el 13 | // mock que queramos 14 | daoGetHello = func() string { 15 | return "Hello" 16 | } 17 | 18 | assert.Equal(t, "Hello Pepe", SayHello("Pepe")) 19 | daoGetHello = defaultHelloFunc 20 | } 21 | -------------------------------------------------------------------------------- /go_declarative/utils/dialog/builder.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type dialogAction struct { 8 | Label string `json:"label,omitempty"` 9 | Action string `json:"action,omitempty"` 10 | } 11 | 12 | type dialog struct { 13 | Title *string `json:"title,omitempty"` 14 | Accept *dialogAction `json:"accept,omitempty"` 15 | } 16 | 17 | type DialogBuilder struct { 18 | dialog 19 | } 20 | 21 | func NewBuilder() *DialogBuilder { 22 | return &DialogBuilder{dialog{}} 23 | } 24 | 25 | func (d *DialogBuilder) Title(value string) *DialogBuilder { 26 | return &DialogBuilder{dialog{ 27 | Title: &value, 28 | Accept: d.dialog.Accept, 29 | }} 30 | } 31 | 32 | func (d *DialogBuilder) AcceptAction(label, action string) *DialogBuilder { 33 | return &DialogBuilder{dialog{ 34 | Title: d.dialog.Title, 35 | Accept: &dialogAction{ 36 | Label: label, 37 | Action: action, 38 | }, 39 | }} 40 | } 41 | 42 | func (d *DialogBuilder) Build() string { 43 | result, _ := json.Marshal(d.dialog) 44 | return string(result) 45 | } 46 | -------------------------------------------------------------------------------- /go_declarative/utils/errors/custom_error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | // NewCustomError creates a new custom error 4 | func NewCustomError(status int, message string) *CustomError { 5 | return &CustomError{ 6 | code: status, 7 | message: message, 8 | } 9 | } 10 | 11 | // CustomError es una interfaz para definir errores custom 12 | type CustomError struct { 13 | code int 14 | message string 15 | } 16 | 17 | // Code http error code 18 | func (e *CustomError) Code() int { 19 | return e.code 20 | } 21 | 22 | // Message http error message 23 | func (e *CustomError) Error() string { 24 | return e.message 25 | } 26 | -------------------------------------------------------------------------------- /go_declarative/utils/gu/send_utis.go: -------------------------------------------------------------------------------- 1 | package gu 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // SendJSONAnswer sends an answer as JSON 10 | func SendJSONAnswer(c *gin.Context, data interface{}) { 11 | c.JSON(http.StatusOK, gin.H{ 12 | "answer": data, 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /go_declarative/utils/strings/shorten.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import "strings" 4 | 5 | func Shorten(name string) string { 6 | return fromString(name). 7 | split(" "). 8 | mapNotEmpty(func(s string) string { 9 | return strings.ToUpper(string(s[0])) 10 | }). 11 | joinToString() 12 | } 13 | 14 | type shorten struct{ string } 15 | type shortenSlice struct{ slice []string } 16 | 17 | func fromString(value string) shorten { 18 | return shorten{value} 19 | } 20 | 21 | func (s shorten) split(separator string) shortenSlice { 22 | return shortenSlice{strings.Split(s.string, separator)} 23 | } 24 | 25 | func (values shortenSlice) mapNotEmpty(f func(string) string) shortenSlice { 26 | var result []string 27 | for _, v := range values.slice { 28 | if len(v) > 0 { 29 | result = append(result, f(v)) 30 | } 31 | } 32 | return shortenSlice{result} 33 | } 34 | 35 | func (values shortenSlice) joinToString() (result string) { 36 | for _, v := range values.slice { 37 | result += v 38 | } 39 | return result 40 | } 41 | -------------------------------------------------------------------------------- /go_declarative/utils/test/response_writer.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func ResponseWriter(t *testing.T) *FakeResponseWriter { 9 | return &FakeResponseWriter{ 10 | t: t, 11 | headers: make(http.Header), 12 | } 13 | } 14 | 15 | type FakeResponseWriter struct { 16 | t *testing.T 17 | headers http.Header 18 | body []byte 19 | status int 20 | } 21 | 22 | func (r *FakeResponseWriter) Header() http.Header { 23 | return r.headers 24 | } 25 | 26 | func (r *FakeResponseWriter) Write(body []byte) (int, error) { 27 | r.body = body 28 | return len(body), nil 29 | } 30 | 31 | func (r *FakeResponseWriter) WriteHeader(status int) { 32 | r.status = status 33 | } 34 | 35 | func (r *FakeResponseWriter) Assert(status int, body string) { 36 | if r.status != status { 37 | r.t.Errorf("expected status %+v to equal %+v", r.status, status) 38 | } 39 | if string(r.body) != body { 40 | r.t.Errorf("expected body %#v to equal %#v", string(r.body), body) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /go_declarative/utils/test/response_writer_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func TestResponseWriter(t *testing.T) { 10 | response := ResponseWriter(t) 11 | context, _ := gin.CreateTestContext(response) 12 | context.JSON(500, gin.H{"error": "Internal server error"}) 13 | response.Assert(500, "{\"error\":\"Internal server error\"}") 14 | } 15 | -------------------------------------------------------------------------------- /go_di_ioc/.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | .DS_Store 3 | .vscode 4 | -------------------------------------------------------------------------------- /go_di_ioc/README_en.md: -------------------------------------------------------------------------------- 1 | [Versión en Español](README.md) 2 | 3 | # DI and IoC in GO 4 | 5 | This repository talks about alternatives of dependency injection, in Go. 6 | 7 | ## Dependency Injection 8 | 9 | _Spoiler alert: This is what we can change_ 10 | 11 | Is that IoC strategy that allows us to insert dependencies in a class, to be used internally by the class. 12 | 13 | In the folder [ejemplo_tradicional](./ejemplo_tradicional/) we have code samples. 14 | 15 | Most writers recommends to use dependency injection to split and decouple logic layers. 16 | 17 | In Go the mos common strategy is to use Dependency Injection by constructor function. 18 | 19 | The code looks like: 20 | 21 | ```go 22 | srv := service.NewService(dao.NewDao()) 23 | fmt.Println(srv.SayHello()) 24 | ``` 25 | 26 | where Service is something like : 27 | 28 | ```go 29 | // IHelloDao DAO interface to inject service 30 | type IHelloDao interface { 31 | Hello() string 32 | } 33 | 34 | // HelloService this is the business service client 35 | type HelloService struct { 36 | dao IHelloDao 37 | } 38 | 39 | // NewService Its the factory, and depends on a IHelloDao implementation 40 | func NewService(dao IHelloDao) *HelloService { 41 | return &HelloService{dao} 42 | } 43 | 44 | // SayHello is the business method 45 | func (s HelloService) SayHello() string { 46 | return s.dao.Hello() 47 | } 48 | ``` 49 | 50 | It's implemented passing the service dependency instance in the client constructor. 51 | 52 | According to the bibliography this pattern allows us to : 53 | 54 | - Decouple code, so clients can be extensible 55 | - Reduce code complexity. 56 | - Clients are independent. 57 | - Allows code reusability. 58 | - It is more testable. 59 | 60 | _And that is true but up at the point_ 61 | 62 | Because we don't fully decouple, by contrary, we couple much more, our code has to create new instances in some bottstrap method, and in wrong places. So we couple a all the code in wrong files. 63 | 64 | And the client and services are not decoupled, services need to implement an interface. 65 | 66 | ## Use of Factory Methods as IoC 67 | 68 | Lets look how we can do it better. 69 | 70 | The Inversion of Control strategy, basically means to have a framework that builds dependencies on demand when the code need it, and the dependency is obtained from the "context". 71 | 72 | A service locator, is a pattern that basically is that framework, that knows dependencies, and brings dependencies when they are needed, but it has the same bootstrap problem, it couples all the services factories in a single place. 73 | 74 | Checking the responsibility assignment patterns expressed in GRASP, one of the classic ways and correct of IoC use is to use service Factory Methods. 75 | 76 | Just think in a factory method, as a framework that build dependencies, and depending on the context, it brings to the client the correct instance of a service where it's needed. 77 | 78 | The correct why to use this pattern, is to write the build strategy (factory method) in the same service package, next to the service implementation, so the build strategy is clean, and in the correct place. 79 | 80 | Using factory method, we can avoid dependency injections by client constructors, and leave the client to get the right service implementation. 81 | 82 | Lets check the code in the main method: the client creation is not coupled anymore with the service, it's simple, and decouples the main method to create a dependency. 83 | 84 | ```go 85 | srv := service.NewService() 86 | 87 | fmt.Println(srv.SayHello()) 88 | ``` 89 | 90 | That is aligned to the expert pattern. 91 | 92 | Lets check the instance creation : 93 | 94 | ```go 95 | func NewService() *HelloService { 96 | return &HelloService{ 97 | dao.NewDao(), 98 | } 99 | } 100 | ``` 101 | 102 | The service fetch a Dao implementation from the Dao factory, that is the artifact that knows how to build the dao, and that is out IoC strategy. 103 | 104 | ```go 105 | func NewDao() *HelloDao { 106 | return new(HelloDao) 107 | } 108 | ``` 109 | 110 | Inside this factory we can use many different creational patterns, like singleton, object pool, new instance, or whatever. 111 | 112 | Also there could be many factory functions, not a single one, solving different scenarios. 113 | 114 | To mock tests, we just create the struct. 115 | 116 | ```go 117 | func TestSayHelo(t *testing.T) { 118 | // Mockeamos 119 | mockedDao := new(daoMock) 120 | 121 | s := HelloService{ 122 | mockedDao, 123 | } 124 | assert.Equal(t, "Hello", s.SayHello()) 125 | } 126 | ``` 127 | 128 | Following the idea to not create interaces where do not existe the strategy pattern, the dao does not have any interface, it's just an structure, to it's easely mocked. 129 | 130 | Pros: 131 | 132 | - Allow us to encapsulate the code properly, creating the correct dependency in the place it's needed. 133 | - Reduce complexity con constructors. 134 | - Decouple constructors without bootstrap methods. 135 | - We use expert pattern properly. 136 | - We write strategies in the correct file.. 137 | 138 | ## Lets talk about my fundamentals 139 | 140 | Indeed, dependency injection is a good practice, the problem is the way in witch it is done, many strategies exposed in books does not scale, because they end distribute knowledge in incorrect places. (see GRASP patterns) 141 | 142 | ### The Strategy pattern 143 | 144 | One abuse of interfaces in many implementations is to create interfaces that does not do anything. 145 | 146 | The strategy pattern is about implement different behaviors through the definition of an interface, so we can fo polymorphism. 147 | 148 | The Strategy Pattern gives meaning to constructor Dependency Injection. 149 | 150 | We should not use DI by constructor if we don't have strategy. If it really exist an interface and the developer can implement a different bejaviur, so it makes sense. 151 | 152 | But if the only implementations are internal, of if there is only one implementation, so Factory Methods are better. 153 | 154 | Why I'm expressing this ? Because It's pretty common to observe thigs like : 155 | 156 | - Implement interfaces anyway, just to split layers. 157 | - Implement interfaces when there is only one implementation. 158 | - Use interfaces to mock tests 159 | - Or just because the books says 160 | 161 | ### What we should really consider is: 162 | 163 | - We should not have an interface if there is no polymorphism. 164 | - Neither when the options are limited. 165 | - A test is not an excuse to create an interface or DI by constructor. 166 | - Or when "just is case" we generalize, and we always do DI, we are overcoding the app. 167 | 168 | ### With are the problems when we do DI and it's not needed ? 169 | 170 | Having in mind, that dependency injection by Factory Method is a good practice, the constructor way has the problems: 171 | 172 | - Overweight the constructors. 173 | - We generate a confusion to developers, opening a door to implement custom solution to something that has not. 174 | - We couple code. For example a main.go method does not need to know that a service needs a dao. 175 | - We do code hard to read, then hard to maintain. 176 | 177 | ### When we SHOULD use Constructor DI 178 | 179 | - When we have a strategy, i.e., polymorphism to solve a problem and the client defines it (for example, a callback to subroutines). 180 | - When we are programming a module and the implementation of the behavior is defined outside the module. 181 | - When we are programming a library and want to be user-friendly for third parties who might need some kind of hacky implementation. 182 | - When we need to use dependencies of a service provided by another module. 183 | - When we access data outside our module, such as APIs or databases. 184 | 185 | ### Creational Alternatives 186 | 187 | When we have Constructor DI, we might not necessarily use a Factory Method. There are several creational patterns that could also be useful, such as Builders, Object Pool, etc. The important thing is that this creation is associated with the object being created, and not just anywhere, and in turn, instantiated in the component that needs it. 188 | 189 | ### Functional Alternative 190 | 191 | We are learning Go because we want to be pragmatic. The best way to program in Go is by using functional fundamentals, in which dependency injection takes a different direction. 192 | 193 | ## Note 194 | 195 | This is a series of notes about advanced Go patterns, with a really simple implementation. 196 | 197 | [Content Table](../README_en.md) 198 | -------------------------------------------------------------------------------- /go_di_ioc/ejemplo_tradicional/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nmarsollier/go_di_ioc/ejemplo_tradicional 2 | 3 | go 1.14 4 | 5 | require github.com/stretchr/testify v1.6.1 6 | -------------------------------------------------------------------------------- /go_di_ioc/ejemplo_tradicional/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 7 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /go_di_ioc/ejemplo_tradicional/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/nmarsollier/go_di_ioc/ejemplo_tradicional/model/hello/dao" 7 | "github.com/nmarsollier/go_di_ioc/ejemplo_tradicional/model/hello/service" 8 | ) 9 | 10 | func main() { 11 | 12 | srv := service.NewService(dao.NewDao()) 13 | 14 | fmt.Println(srv.SayHello()) 15 | } 16 | -------------------------------------------------------------------------------- /go_di_ioc/ejemplo_tradicional/model/hello/dao/hello_dao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | // HelloDao representa un DAO podria ser una base de datos o un acceso a API externo 4 | type HelloDao struct { 5 | } 6 | 7 | // NewDao es el factory 8 | func NewDao() *HelloDao { 9 | return new(HelloDao) 10 | } 11 | 12 | // Hello es nuestro metodo de negocio 13 | func (d *HelloDao) Hello() string { 14 | return "Hello" 15 | } 16 | -------------------------------------------------------------------------------- /go_di_ioc/ejemplo_tradicional/model/hello/service/hello_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | // IHelloDao interface DAO necesaria a inyectar en el service 4 | type IHelloDao interface { 5 | Hello() string 6 | } 7 | 8 | // HelloService es el servicio de negocio 9 | type HelloService struct { 10 | dao IHelloDao 11 | } 12 | 13 | // NewService Es un factory del servicio de Negocio , depende de IHelloDao 14 | func NewService(dao IHelloDao) *HelloService { 15 | return &HelloService{dao} 16 | } 17 | 18 | // SayHello es nuestro metodo de negocio 19 | func (s *HelloService) SayHello() string { 20 | return s.dao.Hello() 21 | } 22 | -------------------------------------------------------------------------------- /go_di_ioc/ejemplo_tradicional/model/hello/service/hello_service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | /** 10 | En nuestro test tendremos que mockear el dao para pasarlo como DI 11 | */ 12 | type daoMock struct { 13 | } 14 | 15 | func (d daoMock) Hello() string { 16 | return "Hello" 17 | } 18 | 19 | func TestSayHelo(t *testing.T) { 20 | s := NewService(new(daoMock)) 21 | 22 | assert.Equal(t, "Hello", s.SayHello()) 23 | } 24 | -------------------------------------------------------------------------------- /go_di_ioc/ioc_factory/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nmarsollier/go_di_ioc/ioc_factory 2 | 3 | go 1.14 4 | 5 | require github.com/stretchr/testify v1.6.1 6 | -------------------------------------------------------------------------------- /go_di_ioc/ioc_factory/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 7 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /go_di_ioc/ioc_factory/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/nmarsollier/go_di_ioc/ioc_factory/model/hello/service" 7 | ) 8 | 9 | func main() { 10 | 11 | srv := service.NewService() 12 | 13 | fmt.Println(srv.SayHello()) 14 | } 15 | -------------------------------------------------------------------------------- /go_di_ioc/ioc_factory/model/hello/dao/hello_dao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | // HelloDao Es nuestra implementacion de Dao 4 | type HelloDao struct { 5 | } 6 | 7 | // NewDao es el factory 8 | func NewDao() *HelloDao { 9 | return new(HelloDao) 10 | } 11 | 12 | // Hello es nuestro metodo de negocio 13 | func (d *HelloDao) Hello() string { 14 | return "Holiiis" 15 | } 16 | -------------------------------------------------------------------------------- /go_di_ioc/ioc_factory/model/hello/service/hello_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "github.com/nmarsollier/go_di_ioc/ioc_factory/model/hello/dao" 4 | 5 | // IHelloDao interface DAO necesaria a inyectar en el service 6 | type IHelloDao interface { 7 | Hello() string 8 | } 9 | 10 | // HelloService es el servicio de negocio 11 | type HelloService struct { 12 | dao IHelloDao 13 | } 14 | 15 | // NewService es una función que puede mockearse 16 | func NewService() *HelloService { 17 | return &HelloService{ 18 | dao.NewDao(), 19 | } 20 | } 21 | 22 | // SayHello es nuestro metodo de negocio 23 | func (s *HelloService) SayHello() string { 24 | return s.dao.Hello() 25 | } 26 | -------------------------------------------------------------------------------- /go_di_ioc/ioc_factory/model/hello/service/hello_service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | type daoMock struct { 10 | } 11 | 12 | func (d daoMock) Hello() string { 13 | return "Hello" 14 | } 15 | 16 | func TestSayHelo(t *testing.T) { 17 | // Mockeamos 18 | mockedDao := new(daoMock) 19 | 20 | s := HelloService{ 21 | mockedDao, 22 | } 23 | assert.Equal(t, "Hello", s.SayHello()) 24 | } 25 | -------------------------------------------------------------------------------- /go_di_ioc2/.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | .DS_Store 3 | .vscode 4 | -------------------------------------------------------------------------------- /go_di_ioc2/README_en.md: -------------------------------------------------------------------------------- 1 | [Versión en español](README.md) 2 | 3 | # After functional, we talk about dependency injection again 4 | 5 | We have already seen in previous articles how to apply functional programming, why it is better to use this programming style in Go, now we review dependency injection with what we already know. 6 | 7 | ## A pattern that does not exist 8 | 9 | In functional programming, dependency injection, per se, does not exist, structures are never created with the intention of accessing dependencies. 10 | 11 | > Interfaces in Go are not for injecting dependencies but allow implementing the [strategy pattern](../go_di_ioc/README_en.md), however, the strategy to be used should not be passed as parameters, but rather each function should receive the necessary parameters to find the required dependencies. 12 | 13 | When we write a function in the functional style, we basically respect the following: 14 | 15 | - Functions should do only one thing (this is key) 16 | - Clear and concise name that is self-explanatory 17 | - In Go, short and easy-to-remember name 18 | - 2 or 3 arguments maximum, as long as they are clear and concise 19 | - Functions should only receive the data they need, no more, no less. 20 | - If we have many arguments, pass a structure, it simplifies refactoring, maintenance, and the meaning of the parameters. 21 | - Functions should have only one level of abstraction. 22 | - Structures that are passed as parameters and returned should be immutable 23 | - In general, functions that correspond to the same business layer and are related should be written close together in the code. 24 | 25 | ## So, how do we pass dependencies? 26 | 27 | Passing dependencies is not a problem, each function receives the parameters necessary for its proper functioning, each function has direct access to the functions it depends on, so it is not necessary to send pointers in its parameters, for example, a service and a DAO, the service should have the logic to determine the corresponding DAO to call, this is called the expert information pattern. 28 | 29 | More strategies are explained later in this guide. 30 | 31 | ## What data should a function receive? 32 | 33 | Only what it needs, we should never pass data that the function does not need, or complex structures that are not used later, it is always preferable to receive the exact parameters and when they are many or confusing, define and pass those parameters through structures, so that it is clear that a function needs that and only that, no more, no less. 34 | 35 | Functions should be seen as closed boxes from the outside world, they are boxes that need certain information because they respond to a very clear functionality, and that data they need is enough for the user of the function to understand what is needed and sometimes just by knowing the data we already know why. 36 | 37 | > Always passing a structure as a function parameter is not good practice, we only define a structure as a parameter when the parameters are confusing to read, otherwise, it is better to pass individual parameters. 38 | 39 | A very common mistake in HTTP services is to pass the context and have functions extract values from the context, functions should receive the context only to cancel goroutines, for example, but never to extract values from it. 40 | 41 | The context is a bag of information that never makes it clear what requirements it must have to be valid, although we can use the context and we must use the context to put values, these values are restricted in their use within the controllers, when we call a service we extract those values and it should be called with the value that the service specifically needs. 42 | 43 | ## Example 44 | 45 | In this project, I adopted a strategy that allows passing a variable parameter to functions that represents a "context", but not a Go context, rather a functional context of business services that should be used. This context is generally empty, unless we want to provide different implementations to libraries (e.g., when we do unit testing, or when we want to pass a particular instance for different use cases). 46 | 47 | Specific examples: 48 | 49 | - Database connection management: We can mock a connection or the db.Get constructor itself instantiates the real database. 50 | - Log management: Logs depend on a particular context, whether it is a gin request or a Rabbit message process. Specifically, the correlation_id has different values as needed, and functions that need to perform logs must have that particular context while related operations last. 51 | 52 | Key concepts of this approach: 53 | 54 | - Functions are responsible for creating the necessary services (we do not pass the services to use as a parameter). 55 | - Functions are decoupled from the way services are created. 56 | - Services have a constructor that receives the context (var arg) and based on the context determines the instance to use. 57 | - Functions that need to use a service use the function from the previous point to access those services. 58 | 59 | Now, each Service that can have more than one implementation is responsible for 60 | 61 | [imagego](https://github.com/nmarsollier/imagego). 62 | 63 | **Dependency Construction** 64 | 65 | The context is defined as a variable parameter, which is received as a parameter when calling the constructors. Each component that we need to "inject" as a functional dependency must have a constructor that receives the functional context. If the context already provides a dependency, that dependency is used; otherwise, the constructor returns the appropriate one. 66 | 67 | This is a very elegant way to decouple instances and delegate the creation of instances correctly to the components that know how to create them. 68 | 69 | ```go 70 | func Get(ctx ...interface{}) RedisClient { 71 | // If the context provides an instance, we use that instance; otherwise, we return the production instance 72 | for _, o := range ctx { 73 | if client, ok := o.(RedisClient); ok { 74 | return client 75 | } 76 | } 77 | 78 | once.Do(func() { 79 | instance = redis.NewClient(&redis.Options{ 80 | Addr: env.Get().RedisURL, 81 | Password: "", 82 | DB: 0, 83 | }) 84 | }) 85 | return instance 86 | } 87 | ``` 88 | 89 | **Initialization** 90 | 91 | The context is initialized when operations start in a controller and is passed to all necessary functions. 92 | 93 | In this case, the function is defined in the context of a gin server as: 94 | 95 | ```go 96 | // Gets the context for external services 97 | func GinCtx(c *gin.Context) []interface{} { 98 | var ctx []interface{} 99 | ctx = append(ctx, ginLogger(c)) 100 | return ctx 101 | } 102 | ``` 103 | 104 | In this specific case, the context is initialized with a logger that is used to track the correlation_id. This logger instance analyzes the request for a header that requires correlation_id traceability. If it finds one, it uses it; otherwise, it creates a new logger with a new correlation_id. 105 | 106 | All subsequent calls will already have a logger instance to use. 107 | 108 | ```go 109 | func initPostImage() { 110 | server.Router().POST( 111 | "/v1/image", 112 | server.ValidateAuthentication, 113 | saveImage, 114 | ) 115 | } 116 | 117 | func saveImage(c *gin.Context) { 118 | bodyImage, err := getBodyImage(c) 119 | if err != nil { 120 | c.Error(err) 121 | return 122 | } 123 | 124 | // We get the context and then pass it to the functions that need it 125 | ctx := server.GinCtx(c) 126 | id, err := image.Insert(image.New(bodyImage), ctx...) 127 | if err != nil { 128 | c.Error(err) 129 | return 130 | } 131 | 132 | c.JSON(200, NewImageResponse{ID: id}) 133 | } 134 | ``` 135 | 136 | As we can see, image.Insert does not need to know which logger is used or which Redis instance is used; it is simply provided with a context. In the case of the logger, it initializes gin, but in the case of Redis, the Redis factory itself initializes it on demand. 137 | 138 | ## Note 139 | 140 | This is a series of notes on simple programming patterns in Go. 141 | 142 | [Table of Contents](../README_en.md) 143 | -------------------------------------------------------------------------------- /go_directories/.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | .DS_Store 3 | .vscode 4 | -------------------------------------------------------------------------------- /go_directories/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [English version](README_en.md) 4 | 5 | # Estructuras de Directorios 6 | 7 | En ninguno de los proyectos anteriores he realizado una estructura de directorios razonable, sino mas bien adaptada al ejemplo que quería mostrar. 8 | 9 | Es curioso que no hablamos mucho sobre la estructura de directorios, solo cuando lo hacemos muy mal, y nos damos cuenta que no encontramos nada, nos ponemos a reorganizar código. 10 | 11 | Es estas notas propongo una opción para estructurar los directorios, que si bien no es la única forma de ordenarnos bien las cosas, al menos es una forma ordenada. 12 | 13 | ## Programación en Capas 14 | 15 | Todos los diseños y arquitectura populares proponen una división por capas, dependiendo de lo que leamos hay diferencias, pero hay al menos 3 capas que resultan características y a la vez útiles : 16 | 17 | - Presentación: Son los que comunican con el mundo exterior 18 | - Negocio: Donde encapsulamos toda la lógica de negocios. 19 | - Infraestructura: Frameworks y librerías en general 20 | 21 | Desde mi punto de vista para la gran mayoría de los sistemas, estas 3 capas son suficientes. 22 | 23 | Entonces la división principal, que seria un buen punto de partida es dividir estas capas de esta forma. 24 | 25 | ```bash 26 | ├── controllers 27 | ├── model 28 | └── utils 29 | 30 | ``` 31 | 32 | ### Capa de Presentación (controller) 33 | 34 | Expresamos todos los servicios que nuestra aplicación brinda al exterior, todas las interfaces con el mundo exterior. 35 | 36 | Lo mas importante de los controllers es que se encargan de todo lo relacionado a la comunicación con los clientes, pero no toman ninguna decision de negocio, para eso llaman siempre a la capa de servicios. 37 | 38 | Si nuestra aplicación solamente expone un protocolo, por ejemplo rest, podríamos poner todo lo relacionado a rest directamente en este directorio, no estaría mal. 39 | 40 | Si por el contrario implementamos mas de un tipo de controller, digamos rabbit, react y protocolo buffers, podríamos organizarnos asi : 41 | 42 | ```bash 43 | ├── controllers 44 | │ ├── rest 45 | │ ├── rabbit 46 | │ └── protobuff 47 | ``` 48 | 49 | En este ejemplo puntual, de este repo, si bien tengo un solo protocolo, es complejo, y prefiero separar conceptos reutilizables de rutas no reutilizables. 50 | 51 | ```bash 52 | ├── controllers 53 | │ ├── middlewares 54 | │ └── router 55 | ``` 56 | 57 | Donde middleware son los middlewares del ruteador y router son las rutas que mi sistema proporciona en rest. 58 | 59 | Que es lo que me anima a mi a separar en mas directorios, bueno la cantidad y diversidad de archivos que estoy poniendo dentro del paquete. Si abriendo el directorio tengo alguna duda de lo que significa lo que hay adentro, es porque debería ordenarse mejor. 60 | 61 | ### Capa de Negocio (model) 62 | 63 | Dentro de esta carpeta tendríamos toda la lógica de negocio. 64 | 65 | Lo mas importante de estas carpetas de negocio, es que las mismas definan una interfaz de métodos públicos claros y que puedan utilizarse sin dudas desde cualquier otro model o controller, deben desconocer por completo los detalles de la comunicación con el cliente. 66 | 67 | Si estamos programando un microservicio que realiza una sola cosa puntual, y nada mas, por ejemplo un servidor que almacena imágenes, y que solo podemos agregar y consultar imágenes, podríamos poner todo lo relacionado al negocio en esta misma carpeta. 68 | 69 | Si por el contrario estamos modelando mas de un agregado en nuestro negocio, conviene separar esos conceptos en subcarpetas. 70 | 71 | ```bash 72 | ├── model 73 | │ ├── profile 74 | │ └── user 75 | ``` 76 | 77 | En este ejemplo, suponemos que tenemos 2 agregados, profile y user. Bueno es saludable tener 2 carpetas, cada una encapsulara um concepto particular. 78 | 79 | Lo que incluimos en cada carpeta dependerá un poco del mismo criterio anterior, si todo es simple y se entiende, podríamos tener los archivos: service.go, dao.go, api.go, etc. 80 | 81 | Muchas veces un agregado es muy complejo, podría manejar decenas de casos de uso, por lo que nos conviene separar un poco mas el archivo service.go, en estos casos conviene escribir cada caso de uso en un archivo separado, y hasta quizás incluir subcarpetas para daos, apis y estructuras de datos, por ejemplo. 82 | 83 | ### Capa de Infraestructura (utils) 84 | 85 | Esta es la carpeta de infraestructura, todas las utilidades que sean reutilizables por las otras capas, se ponen en esta carpeta. 86 | 87 | Siempre conviene separar conceptos en esta carpeta y la separación depende del criterio del desarrollador. 88 | 89 | Es muy común que a lo largo del tiempo se reestructure conforme el sistema evoluciona, por lo tanto hay que buscar una estructura que tenga sentido en este momento y no preocuparse mucho por los detalles de separación. 90 | 91 | ## Otras capas 92 | 93 | ### Capa de comunicación 94 | 95 | Muchas veces tenemos capas que son de entrada y salida, por ejemplo, cuando nuestros servicios utilizan rabbit, o protocol buffers y la comunicación es bidireccional. 96 | 97 | En estos escenarios, existe un gris muy grande entre que es infraestructura, que es controller y que es una api. 98 | 99 | Para estos casos especiales nos conviene separar desde la raíz estos términos para que queden bien claras las intenciones. 100 | 101 | ```bash 102 | ├── rest 103 | │ ├── middlewares 104 | │ └── routes 105 | ├── rabbit 106 | │ ├── consummers 107 | │ └── publishers 108 | ├── grpc 109 | │ ├── client 110 | │ └── server 111 | ├── model 112 | └── utils 113 | 114 | ``` 115 | 116 | ### Daos y Apis 117 | 118 | En general no necesitan una división particular, en general son capas que se modelan internamente en la capa de negocio, es una subcapa digamos. Está en cada uno de nosotros, y dependiendo de la complejidad de la lógica de negocio, si queremos o no tener esta capa. 119 | 120 | Cuando una api o acceso a base de datos es compartido por varios módulos, lo mejor es modelarlo y encapsularlo como un paquete mas del negocio. 121 | 122 | Ejemplo: Lectura de feature flags remotas, que se necesiten chequear desde distintos módulos. 123 | 124 | ## Nota 125 | 126 | Esta es una serie de notas sobre patrones simples de programación en GO. 127 | 128 | [Tabla de Contenidos](../README.md) 129 | -------------------------------------------------------------------------------- /go_directories/README_en.md: -------------------------------------------------------------------------------- 1 | [Versión en Español](README_en.md) 2 | 3 | # Directory Structure 4 | 5 | None of the previous notes talks about directory structures, they use any structure just to justify the code in the sample. 6 | 7 | It's curious that we don't talk too much about directory structure, just when we are doing it so wrong that we need to reorder all the things. 8 | 9 | On this notes I'm proposing a simple way to create directory structures, that are not the only way to do it properly, but works fine. 10 | 11 | --- 12 | 13 | Note that this structures are only valid on small apps or microservices. 14 | 15 | --- 16 | 17 | ## Layered programming 18 | 19 | All the popular design and architectural patterns uses the layer code separation, some has more or less, but they are at least 3 ones that are common : 20 | 21 | - Presentation: They are channels to communicate to the out world 22 | - Business: Where we do the magic 23 | - Application: Frameworks and supporting libraries 24 | 25 | From my point of view, for most system these 3 layers are enough. 26 | 27 | So thr first directory three level, that is a good starting point can be : 28 | 29 | ```bash 30 | ├── controllers 31 | ├── model 32 | └── utils 33 | 34 | ``` 35 | 36 | ### Presentation layer (controller) 37 | 38 | We express all the use cases that our app serves to the out world. 39 | 40 | The most important thing is that controllers does everything related with the communication with clients, but none business decision. 41 | 42 | If our app exposes only one communication way, like rest, we could write all the code in tis directory, may be not bad at all. 43 | 44 | If by the contrary, we implement more than one communication protocol, like rabbit, or protocol buggers, we can make subdirectories : 45 | 46 | ```bash 47 | ├── controllers 48 | │ ├── rest 49 | │ ├── rabbit 50 | │ └── protobuff 51 | ``` 52 | 53 | In this repo sample, I have only one protocol, it's complex, so I choose to split middlewares and routes : 54 | 55 | ```bash 56 | ├── controllers 57 | │ ├── middlewares 58 | │ └── router 59 | ``` 60 | 61 | What should enforce us to split a directory ? , well the amount of files and the purpose on the module. If I open the directory and I have any doubts about what a file does, then maybe we should organize the code better. 62 | 63 | ### Business layer (model) 64 | 65 | Inside this one, we have all business rules. 66 | 67 | These business rules must define a public interface that must be clean, and other modules or layers can use them without doubt about what they do. 68 | 69 | If we are coding microservices, maybe there is only one thing that out model does, maybe we can put all the business files in the same directory. 70 | 71 | If we are modeling many aggregates, we must split them in several directories: 72 | 73 | ```bash 74 | ├── model 75 | │ ├── profile 76 | │ └── user 77 | ``` 78 | 79 | In the same, we have 2 aggregates, profile and user, it's healty to put them in 2 directories. 80 | 81 | What we include in each folder depends on the same criteria, if everything is simple, we can have the files : service.go, dao.go, api.go, etc. 82 | 83 | Many times the aggregate is complex, it could contain several use case functions, so it's better to put each one, in a different file, so maybe we could create subfolders for dao, schemas, and api. 84 | 85 | ### Application layer (utils) 86 | 87 | All the utilities that we need, goes here. 88 | 89 | It's always a good thing to split this in subfolders, but how depends on dev criteria. 90 | 91 | This folder will be reestructure in the future for sure, we we must be open to refactor this libraries. 92 | 93 | ## Other layers 94 | 95 | ### Communication layer 96 | 97 | There are scenarios where communication layer are for input and outputs, like when we consume a send events to rabbit or protocol buffers then the communication is bidirectional. 98 | 99 | In those cases, there is a big gray about what is application and what is a controller or api. 100 | 101 | It's easy to handle those scenarios from the root level, with clean intentions : 102 | 103 | ```bash 104 | ├── rest 105 | │ ├── middlewares 106 | │ └── routes 107 | ├── rabbit 108 | │ ├── consummers 109 | │ └── publishers 110 | ├── grpc 111 | │ ├── client 112 | │ └── server 113 | ├── model 114 | └── utils 115 | 116 | ``` 117 | 118 | ### DAO and Apis 119 | 120 | In general does not require any particular division, they goes perfectly in the model, or internally in the aggregate package. 121 | 122 | When an api or database connection is shared, it's better to model and encapsulate it in it's own package. 123 | 124 | Example: feature flags remote reading, that need to be checked in different modules. 125 | 126 | ## Note 127 | 128 | This is a series of notes about advanced Go patterns, with a really simple implementation. 129 | 130 | [Content Table](../README_en.md) 131 | -------------------------------------------------------------------------------- /go_directories/controllers/middlewares/error.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // ICustomError define un error con Code y Error 10 | type ICustomError interface { 11 | Code() int 12 | Error() string 13 | } 14 | 15 | // ErrorHandler a middleware to handle errors 16 | func ErrorHandler(c *gin.Context) { 17 | c.Next() 18 | 19 | handleErrorIfNeeded(c) 20 | } 21 | 22 | func handleErrorIfNeeded(c *gin.Context) { 23 | err := c.Errors.Last() 24 | if err == nil { 25 | return 26 | } 27 | 28 | switch value := err.Err.(type) { 29 | case ICustomError: 30 | c.JSON(value.Code(), 31 | gin.H{ 32 | "error": value.Error(), 33 | }) 34 | case error: 35 | c.JSON(http.StatusInternalServerError, 36 | gin.H{ 37 | "error": value.Error(), 38 | }) 39 | default: 40 | c.JSON(http.StatusInternalServerError, gin.H{ 41 | "error": err, 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /go_directories/controllers/router/get_parallel_user_id.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/nmarsollier/go_directories/model/profile" 8 | "github.com/nmarsollier/go_directories/model/user" 9 | ) 10 | 11 | // Servicio REST que nos retorna información de un dialogo a mostrar en pantalla 12 | // Vamos a usar el contexto como un Builder Pattern 13 | func init() { 14 | router().GET( 15 | "/parallel/users/:id", 16 | validateUserName, 17 | inParallel( 18 | fetchUserInParallel, 19 | fetchProfileInParallel, 20 | ), 21 | build, 22 | ) 23 | } 24 | 25 | func fetchUserInParallel(c *gin.Context) { 26 | c.Set("user", user.FetchUser()) 27 | } 28 | 29 | func fetchProfileInParallel(c *gin.Context) { 30 | c.Set("profile", profile.FetchProfile()) 31 | } 32 | 33 | func inParallel(handlers ...gin.HandlerFunc) gin.HandlerFunc { 34 | return func(c *gin.Context) { 35 | var waitGroup sync.WaitGroup 36 | waitGroup.Add(len(handlers)) 37 | 38 | for _, handler := range handlers { 39 | go func(handlerFunc gin.HandlerFunc) { 40 | defer waitGroup.Done() 41 | handlerFunc(c) 42 | }(handler) 43 | } 44 | 45 | waitGroup.Wait() 46 | 47 | c.Next() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /go_directories/controllers/router/get_user_id.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/nmarsollier/go_directories/model/profile" 8 | "github.com/nmarsollier/go_directories/model/user" 9 | "github.com/nmarsollier/go_directories/utils/errors" 10 | ) 11 | 12 | // Servicio REST que nos retorna información de un dialogo a mostrar en pantalla 13 | // Vamos a usar el contexto como un Builder Pattern 14 | func init() { 15 | router().GET( 16 | "/users/:id", 17 | validateUserName, 18 | fetchUser, 19 | fetchProfile, 20 | build, 21 | ) 22 | } 23 | 24 | // Hacemos las validaciones que nos aseguran que id es valido 25 | func validateUserName(c *gin.Context) { 26 | id := c.Param("id") 27 | 28 | if len(id) < 1 { 29 | c.Error(errors.NewCustomError(400, "userName debe tener al menos 1 caracteres")) 30 | c.Abort() 31 | return 32 | } 33 | } 34 | 35 | func fetchUser(c *gin.Context) { 36 | c.Set("user", user.FetchUser()) 37 | c.Next() 38 | } 39 | 40 | func fetchProfile(c *gin.Context) { 41 | c.Set("profile", profile.FetchProfile()) 42 | c.Next() 43 | } 44 | 45 | func build(c *gin.Context) { 46 | user := c.MustGet("user").(*user.User) 47 | profile := c.MustGet("profile").(*profile.Profile) 48 | 49 | c.JSON(http.StatusOK, gin.H{ 50 | "login": user.Login, 51 | "access": user.Access, 52 | "name": profile.Name, 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /go_directories/controllers/router/rest.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/nmarsollier/go_directories/controllers/middlewares" 6 | ) 7 | 8 | // Start server in 8080 port 9 | func Start() { 10 | router().Run(":8080") 11 | } 12 | 13 | var engine *gin.Engine = nil 14 | 15 | func router() *gin.Engine { 16 | if engine == nil { 17 | engine = gin.Default() 18 | engine.Use(middlewares.ErrorHandler) 19 | } 20 | 21 | return engine 22 | } 23 | -------------------------------------------------------------------------------- /go_directories/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nmarsollier/go_directories 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | github.com/stretchr/testify v1.6.1 8 | gopkg.in/go-playground/assert.v1 v1.2.1 9 | ) 10 | -------------------------------------------------------------------------------- /go_directories/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 6 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 7 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 8 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 9 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 10 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 11 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 12 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 13 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 14 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 15 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 16 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 17 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 18 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 19 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 20 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 21 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 22 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 23 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 24 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 25 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 26 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 27 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 28 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 29 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 34 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 35 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 36 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 37 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 38 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 39 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 40 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 41 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 42 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 48 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 49 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 50 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 51 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 52 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 53 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 54 | -------------------------------------------------------------------------------- /go_directories/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nmarsollier/go_directories/rest/router" 5 | ) 6 | 7 | func main() { 8 | router.Start() 9 | } 10 | -------------------------------------------------------------------------------- /go_directories/model/profile/service.go: -------------------------------------------------------------------------------- 1 | package profile 2 | 3 | import "time" 4 | 5 | // Profile data 6 | type Profile struct { 7 | Login string 8 | Name string 9 | Web string 10 | } 11 | 12 | // FetchProfile Devuelve información de Usuario 13 | func FetchProfile() *Profile { 14 | // Un delay para simular tiempo de espera en llamadas remotas 15 | time.Sleep(1 * time.Second) 16 | 17 | return &Profile{ 18 | Login: "nmarsollier", 19 | Name: "Nestor Marsollier", 20 | Web: "https://github.com/nmarsollier/profile", 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /go_directories/model/user/service.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "time" 4 | 5 | // User data 6 | type User struct { 7 | Login string 8 | Access string 9 | } 10 | 11 | // FetchUser Devuelve información de Usuario 12 | func FetchUser() *User { 13 | // Un delay para simular tiempo de espera en llamadas remotas 14 | time.Sleep(1 * time.Second) 15 | 16 | return &User{ 17 | Login: "nmarsollier", 18 | Access: "ADMIN,USER", 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /go_directories/utils/errors/custom_error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | // NewCustomError creates a new custom error 4 | func NewCustomError(status int, message string) *CustomError { 5 | return &CustomError{ 6 | code: status, 7 | message: message, 8 | } 9 | } 10 | 11 | // CustomError es una interfaz para definir errores custom 12 | type CustomError struct { 13 | code int 14 | message string 15 | } 16 | 17 | // Code http error code 18 | func (e *CustomError) Code() int { 19 | return e.code 20 | } 21 | 22 | // Message http error message 23 | func (e *CustomError) Error() string { 24 | return e.message 25 | } 26 | -------------------------------------------------------------------------------- /go_functional/.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | .DS_Store 3 | .vscode 4 | -------------------------------------------------------------------------------- /go_functional/README_en.md: -------------------------------------------------------------------------------- 1 | [Versión en Español](README.md) 2 | 3 | # A functional way to Go 4 | 5 | On microservices environments, most of out code is private to the project, microservice encapsultaes the code, and communicate that logic using some interface. So , as a black box that it is, It does not has to be complex or scalable. 6 | 7 | So for microservices proposes solutions for a single problem, where interfaces are all that the world knows about them. 8 | 9 | ### We code Go as Java... 10 | 11 | A Go struct associated to code, and generalized with an interface is basically an OO object. 12 | 13 | You will hear that Go is not Object Oriented languaje, but in the language definition these artifacts are explicitly OO programming. 14 | 15 | Now, if instead of code like OO, what if we take a functional approach ? 16 | 17 | ### Go as functional paradigm ? 18 | 19 | Following the single responsibility pattern o interface segregation principle, it should be pretty common to have service definitions with only one public function. 20 | 21 | --- 22 | 23 | Services with mora than one responsibility are hard to follow, read and maintain, having classes and function with single implementations is key. 24 | 25 | --- 26 | 27 | Lets talk about the next struct : 28 | 29 | ```go 30 | // HelloService this is the business service 31 | type HelloService struct { 32 | dao IHelloDao 33 | } 34 | ``` 35 | 36 | This is basically encapsulates a pointer in an struct, that points to a function. This king of artifacts are an OO antipattern, very popularized in Java with EJB, when we didn't know how to split layers. 37 | 38 | I'm not against struct, but it's only and pass trough over functions, and has no meaning. 39 | 40 | What if we avoid all these things, and we just use a direct call to a function where structs does not exists to hold function pointers, but data ? 41 | 42 | The main file, does not needs any instance, and we just call a function : 43 | 44 | ```go 45 | func main() { 46 | fmt.Println(service.SayHello()) 47 | } 48 | ``` 49 | 50 | The DAO is very simple too, it is just a function. 51 | 52 | ```go 53 | func Hello() string { 54 | return "Holiiiiis" 55 | } 56 | ``` 57 | 58 | The service is a little bit more complex, but simpler that previous implementations : 59 | 60 | ```go 61 | // It's a private pointer that allow us to mock it. 62 | var sayHelloFunc func() string = dao.Hello 63 | 64 | // SayHello is the business 65 | func SayHello() string { 66 | return sayHelloFunc() 67 | } 68 | ``` 69 | 70 | Due that the service is something that we need to test, to allow mock the DAO, we define the dao pointer, I don't know better hacks to do it in golang so far. 71 | 72 | The test is : 73 | 74 | ```go 75 | // Mock the dao 76 | sayHelloFunc = func() string { 77 | return "Hello" 78 | } 79 | 80 | assert.Equal(t, "Hello", SayHello()) 81 | ``` 82 | 83 | The strategy to use a pointer, comceptually is the same as using an interface, every function exposes an interface in its declaration. 84 | 85 | Note that the mocking function is defined in the client package, not in the service that is important. 86 | 87 | --- 88 | 89 | It is a little bit hacky, but if something can be hacky is the test. 90 | 91 | It would be good to take the time to compare and note the quantity of code that we are not writhing just using this paradigm, also apps are more efficient. 92 | 93 | --- 94 | 95 | ### Pros 96 | 97 | This concept of coding Go in a functional way, simplifies the implementation compared to OO, and in the most solutions the balance between simplicity and good design is very good. 98 | 99 | Patterns in OO become obsolete, we need to read some functional techniques, but those are simplest than OO, and more intuitive. 100 | 101 | --- 102 | 103 | Functional languages are extremely expressive. In a functional language one does not need design patterns because the language is likely so high level, you end up programming in concepts that eliminate design patterns all together. 104 | 105 | --- 106 | 107 | Some pros: 108 | 109 | - Testing is simpler 110 | - The solutions are natural, we don't have to think in complex patterns 111 | - Less concurrency problems 112 | - Simpler and maintainable code 113 | - We can be more declarative 114 | - Less patterns to learn 115 | 116 | ## Cons 117 | 118 | ### Parallel testing 119 | 120 | There is a problem if we want to execute parallel testing, because changing the function pointer to some mocked result can interfere with some other parallel running test. 121 | 122 | Lucky that Go does not run tests in parallel by default, is something that we need to set up, and we can avoid strategically run tests in parallel if we have this problem. 123 | 124 | Now if we need to run tests in parallel, and it's not an option, one strategy is to mock only one time, and return different responses accoding to some parameter in the mocked function, so all the tests use the same mock. 125 | 126 | ```go 127 | fetchUserMock = func(id string) (User, error) { 128 | // here we check the id, and return different responses 129 | // according the the id 130 | 131 | ``` 132 | 133 | By contrary, if the function does not get any parameter, we can use the runtime.Caller to check the caller function, and use that to return different result. 134 | 135 | ### Some things require state 136 | 137 | The state handling that and object contains is important in some cases, like a Builder for example, or Memento pattern. Implementing things like that can be hard in FP. 138 | 139 | ## Personal opinion about OO programming 140 | 141 | OO programming is very good, but it complexity is underestimated, when projects scales the refactoring and maintenance of then becomes harder, so in general we end having spaguetti code. 142 | 143 | The book Domain Driven Design, express a ver correct way to express OO modeling and implementation, but it's very weird to see a good implementation, most of the time programmers can't follow the complexity. 144 | 145 | Many developers understand that the concept of Clean Architecture and DDD is to encapsulate the business rules, and isolate them with interfaces and layers, but forgets the fundamental part, to code sustentable applications we need have a balance between complexity and pattern usage. 146 | 147 | We can't build all the systems using the same magic receipt, every implementation requires brain energy to get good results, and that is hard to get, mostly for junior developers. 148 | 149 | Another important thing is the continuous regactoring that POO requires, it is higher that functional, most of the time teams does not maintain apps. 150 | 151 | Functional paradigm is simple, refactoring functions is simple, testing is also straight forward, layer separation can be easily done, and having in mind single function responsibilities, everything becomes much more easy to understand. 152 | 153 | ## Note 154 | 155 | This is a series of notes about advanced Go patterns, with a really simple implementation. 156 | 157 | [Content Table](../README_en.md) 158 | -------------------------------------------------------------------------------- /go_functional/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nmarsollier/go_di_ioc/go_funcional 2 | 3 | go 1.14 4 | 5 | require github.com/stretchr/testify v1.6.1 6 | -------------------------------------------------------------------------------- /go_functional/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 7 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /go_functional/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/nmarsollier/go_di_ioc/go_funcional/model/hello/service" 7 | ) 8 | 9 | func main() { 10 | fmt.Println(service.SayHello()) 11 | } 12 | -------------------------------------------------------------------------------- /go_functional/model/hello/dao/hello_dao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | // Hello esta funcion en prod va a funcionar 4 | // sin problemas, una funcion, una responsabilidad 5 | // programacion funcional, todo tiene sentido 6 | func Hello() string { 7 | return "Holiiiiis" 8 | } 9 | -------------------------------------------------------------------------------- /go_functional/model/hello/service/hello_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "github.com/nmarsollier/go_di_ioc/go_funcional/model/hello/dao" 4 | 5 | // Nos va a permitir mockear respuestas para los tests 6 | var sayHelloFunc func() string = dao.Hello 7 | 8 | // SayHello es nuestro negocio 9 | func SayHello() string { 10 | return sayHelloFunc() 11 | } 12 | -------------------------------------------------------------------------------- /go_functional/model/hello/service/hello_service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSayHelo(t *testing.T) { 10 | // Cuando testeamos la reescribimos con el 11 | // mock que queramos 12 | sayHelloFunc = func() string { 13 | return "Hello" 14 | } 15 | 16 | assert.Equal(t, "Hello", SayHello()) 17 | } 18 | -------------------------------------------------------------------------------- /go_functional_polimorfism/.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | .DS_Store 3 | .vscode 4 | -------------------------------------------------------------------------------- /go_functional_polimorfism/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [English version](README_en.md)) 4 | 5 | # Polimorfismo con Funciones 6 | 7 | Uno de los aspectos mas decisivos a la hora de crear interfaces en Go es el uso de polimorfismo. 8 | 9 | ## Una estrategia sencilla 10 | 11 | Digamos que tenemos un servicio rest que retorna datos del perfil de un usuario, pero que dependiendo de quien lo llame (mobile o web), la imagen tiene una estrategia diferente de carga de acuerdo al origen. 12 | 13 | Vamos una estrategia que me encanta como funciona : 14 | 15 | ```go 16 | var GetImage = map[string]func(id string) string{ 17 | "mobile": func(id string) string { 18 | return fmt.Sprintf("resource_%s", id) 19 | }, 20 | "web": func(id string) string { 21 | return fmt.Sprintf("http://www.images.blablabla/images/%s", id) 22 | }, 23 | } 24 | ``` 25 | 26 | Si, es un simple mapa con funciones para cada caso, en lenguajes como Kotlin podríamos definir un Enum, pero para los fines de Go un map funciona perfecto. 27 | 28 | Es un comportamiento muy simple, una función, pero si hacemos volar un poco la imaginación, podemos darnos cuenta que podría ser una estructura lo que retornemos, com punteros a funciones sobrecargadas según sea el caso, y funcionaria muy bien. 29 | 30 | Este código se usa como: 31 | 32 | ```go 33 | profile.GetImage[device](id) 34 | ``` 35 | 36 | Sencillo y elegante. 37 | 38 | Incluso nos proporciona una función de validación de parámetros que es muy directa, y nos permite agregar nuevos devices sin mayores costos : 39 | 40 | ```go 41 | // IsValidDevice checks if device is valid 42 | func IsValidDevice(device string) bool { 43 | _, ok := GetImage[device] 44 | return ok 45 | } 46 | ``` 47 | 48 | ### Desventajas 49 | 50 | - Bueno, la llamada a la función no es algo típico de ver 51 | - Funciona solo para una función, pero si tenemos en cuenta el principio de single responsability, puedo vivir con ello. 52 | - Si la sobrecarga del modulo es muy grande, bueno ya no conviene mucho esta estrategia. 53 | - Mockear los tests puede ser un poquito mas hacky. 54 | 55 | ## Pero veamos que nos ahorramos, por si no lo notamos 56 | 57 | ### Si lo hubiéramos hecho con funciones al estilo clásico funcional 58 | 59 | ```go 60 | func GetImage(device, id string) string{ 61 | switch device { 62 | case "mobile": 63 | return fmt.Sprintf("resource_%s", id) 64 | case "web": 65 | return fmt.Sprintf("http://www.images.blablabla/images/%s", id) 66 | } 67 | } 68 | ``` 69 | 70 | No parece gran cosa, pero la complejidad ciclomática de esa función es mayor a la del ejemplo anterior. 71 | 72 | Agregar nuevos valores es un poco mas complejo. 73 | 74 | Y Deberíamos programar una función para validar que no es tan directa. 75 | 76 | ### Si lo hubiéramos hecho con un estilo orientado a objetos 77 | 78 | Esta sin lugar a dudas es la opción que elegiríamos como primera elección, cuando nos enfrentamos a polimorfismos. 79 | 80 | Tendríamos que programar : 81 | 82 | - Una interfaz que exponga el método 83 | - 2 estructuras que implementen la interfaz 84 | - Una función factory que dependiendo del device devuelva la implementación correcta, que tendría un switch 85 | - Una función de validación igual de fea que en el caso anterior 86 | 87 | Ya no me dan ganas ni de programar el ejemplo, no quiero ni contar las lineas de código. 88 | 89 | Incluso un enfoque funcional nos permite definir sobrecargas de comportamientos para funciones puntuales, cuando muchas veces nos quejamos que Go no maneja Herencias, con la estrategia original, solo sobrecargamos comportamiento donde debe ser. 90 | 91 | Lo mas probable que la próxima vez que enfrentes un caso de polimorfismo te preguntes, ¿ porque tengo que programar todo esto ? 92 | 93 | ## Nota 94 | 95 | Esta es una serie de notas sobre patrones simples de programación en GO. 96 | 97 | [Tabla de Contenidos](../README.md) 98 | -------------------------------------------------------------------------------- /go_functional_polimorfism/README_en.md: -------------------------------------------------------------------------------- 1 | [Version en Español](README.md)) 2 | 3 | # Polymorphism in Functional Paradigm 4 | 5 | Polymorphism is the decisive point to create an interface. I will try to express different functional strategies than interfaces to avoid OO style. 6 | 7 | ## A simple strategy 8 | 9 | Lets say that we have a rest service that returns user profile data, but depending on the calling device (mobile or web). the profile picture takes different strategy and then different result values. 10 | 11 | A working strategy that works pretty well, and personally I like is : 12 | 13 | ```go 14 | var GetImage = map[string]func(id string) string{ 15 | "mobile": func(id string) string { 16 | return fmt.Sprintf("resource_%s", id) 17 | }, 18 | "web": func(id string) string { 19 | return fmt.Sprintf("http://www.images.blablabla/images/%s", id) 20 | }, 21 | } 22 | ``` 23 | 24 | Yes, it's a simple map with functions, in languages like Kotlin we have enums, that are really powerful, but in Go, this strategy works perfectly. 25 | 26 | That code is used like: 27 | 28 | ```go 29 | profile.GetImage[device](id) 30 | ``` 31 | 32 | Simple and elegant. 33 | 34 | This behavior is simple, a function, but we could try, for example, map a struct with function pointers, we could be doing more than a single action, and would work the same. 35 | 36 | Indeed is so good, that allows us to write very direct validation function for available devices, without neuronal cost : 37 | 38 | ```go 39 | // IsValidDevice checks if device is valid 40 | func IsValidDevice(device string) bool { 41 | _, ok := GetImage[device] 42 | return ok 43 | } 44 | ``` 45 | 46 | ### Cons 47 | 48 | - The function call is not so natural 49 | - Works very well for a function, but is we follow the Single Responsibility principle, we can live with t hat. 50 | - If the module overload big, we should avoid this strategy. 51 | - Mocking tests is hacky, but tests are hacky by definition. 52 | 53 | ## Lets look at the code that we don't write 54 | 55 | ### Doing the last example just with a function 56 | 57 | ```go 58 | func GetImage(device, id string) string{ 59 | switch device { 60 | case "mobile": 61 | return fmt.Sprintf("resource_%s", id) 62 | case "web": 63 | return fmt.Sprintf("http://www.images.blablabla/images/%s", id) 64 | } 65 | } 66 | ``` 67 | 68 | It's not a big deal, it's still good, but the cyclomatic complexity of the code is bigger. 69 | 70 | Adding new values requires more work. 71 | 72 | And we need to provide a function to validate the available devices, that is not so direct. 73 | 74 | ### What if we do this sample in an OO way 75 | 76 | This, without questions, is the first choice most of the time, when we deal with polymorphism. 77 | 78 | We should code : 79 | 80 | - An interface, that exposes the method 81 | - 2 structs with different implementations 82 | - A Factory function to get the correct implementation, based on the device, using the same switch that the last example. 83 | - A custom validation function, to check device names. 84 | 85 | And just writhing the list, I lost the interest to write the code, also i don't want to count the lines of codes needed to do it. 86 | 87 | Most of the times, we point that Go does not handles inheritance, and for good or bad, using the map functional polymorphism approach we are doing the strategy in a single module function, like high level module inheritance. 88 | 89 | It's highly likely that the next time that you need to write a polimorphism for just a funcion, you will ask yourself, why i'm doing interfaces for this ? 90 | 91 | ## Note 92 | 93 | This is a series of notes about advanced Go patterns, with a really simple implementation. 94 | 95 | [Content Table](../README_en.md) 96 | -------------------------------------------------------------------------------- /go_functional_polimorfism/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nmarsollier/go_functional_polimorfism 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | github.com/stretchr/testify v1.6.1 8 | gopkg.in/go-playground/assert.v1 v1.2.1 9 | ) 10 | -------------------------------------------------------------------------------- /go_functional_polimorfism/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 6 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 7 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 8 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 9 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 10 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 11 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 12 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 13 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 14 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 15 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 16 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 17 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 18 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 19 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 20 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 21 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 22 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 23 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 24 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 25 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 26 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 27 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 28 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 29 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 34 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 35 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 36 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 37 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 38 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 39 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 40 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 41 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 42 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 48 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 49 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 50 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 51 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 52 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 53 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 54 | -------------------------------------------------------------------------------- /go_functional_polimorfism/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nmarsollier/go_functional_polimorfism/rest/routes" 5 | ) 6 | 7 | func main() { 8 | routes.Start() 9 | } 10 | -------------------------------------------------------------------------------- /go_functional_polimorfism/model/profile/service.go: -------------------------------------------------------------------------------- 1 | package profile 2 | 3 | import "fmt" 4 | 5 | // Profile data 6 | type Profile struct { 7 | ID string 8 | Login string 9 | Name string 10 | Web string 11 | } 12 | 13 | // GetImage return the profile image resource 14 | var GetImage = map[string]func(id string) string{ 15 | "mobile": func(id string) string { 16 | return fmt.Sprintf("resource_%s", id) 17 | }, 18 | "web": func(id string) string { 19 | return fmt.Sprintf("http://www.images.blablabla/images/%s", id) 20 | }, 21 | } 22 | 23 | // IsValidDevice checks if device is valid 24 | func IsValidDevice(device string) bool { 25 | _, ok := GetImage[device] 26 | return ok 27 | } 28 | 29 | // FetchProfile Devuelve información de Usuario 30 | func FetchProfile(id string) *Profile { 31 | return &Profile{ 32 | ID: id, 33 | Login: "nmarsollier", 34 | Name: "Nestor Marsollier", 35 | Web: "https://github.com/nmarsollier/profile", 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /go_functional_polimorfism/rest/middlewares/error.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // ICustomError define un error con Code y Error 10 | type ICustomError interface { 11 | Code() int 12 | Error() string 13 | } 14 | 15 | // ErrorHandler a middleware to handle errors 16 | func ErrorHandler(c *gin.Context) { 17 | c.Next() 18 | 19 | handleErrorIfNeeded(c) 20 | } 21 | 22 | func handleErrorIfNeeded(c *gin.Context) { 23 | err := c.Errors.Last() 24 | if err == nil { 25 | return 26 | } 27 | 28 | switch value := err.Err.(type) { 29 | case ICustomError: 30 | c.JSON(value.Code(), 31 | gin.H{ 32 | "error": value.Error(), 33 | }) 34 | case error: 35 | c.JSON(http.StatusInternalServerError, 36 | gin.H{ 37 | "error": value.Error(), 38 | }) 39 | default: 40 | c.JSON(http.StatusInternalServerError, gin.H{ 41 | "error": err, 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /go_functional_polimorfism/rest/routes/get_device_profile_id.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/nmarsollier/go_functional_polimorfism/model/profile" 8 | "github.com/nmarsollier/go_functional_polimorfism/utils/errors" 9 | ) 10 | 11 | // Servicio REST que nos retorna información de un dialogo a mostrar en pantalla 12 | // Vamos a usar el contexto como un Builder Pattern 13 | func init() { 14 | router().GET( 15 | "/:device/profile/:id", 16 | validateUserName, 17 | getProfile, 18 | ) 19 | } 20 | 21 | // Hacemos las validaciones que nos aseguran que id es valido 22 | func validateUserName(c *gin.Context) { 23 | device := c.Param("device") 24 | 25 | if !profile.IsValidDevice(device) { 26 | c.Error(errors.NewCustomError(400, "device debe ser mobile o web")) 27 | c.Abort() 28 | return 29 | } 30 | } 31 | 32 | func getProfile(c *gin.Context) { 33 | device := c.Param("device") 34 | id := c.Param("id") 35 | 36 | data := profile.FetchProfile(id) 37 | image := profile.GetImage[device](id) 38 | 39 | c.JSON(http.StatusOK, gin.H{ 40 | "login": data.Login, 41 | "web": data.Web, 42 | "name": data.Name, 43 | "image": image, 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /go_functional_polimorfism/rest/routes/rest.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/nmarsollier/go_functional_polimorfism/rest/middlewares" 6 | ) 7 | 8 | // Start server in 8080 port 9 | func Start() { 10 | router().Run(":8080") 11 | } 12 | 13 | var engine *gin.Engine = nil 14 | 15 | func router() *gin.Engine { 16 | if engine == nil { 17 | engine = gin.Default() 18 | engine.Use(middlewares.ErrorHandler) 19 | } 20 | 21 | return engine 22 | } 23 | -------------------------------------------------------------------------------- /go_functional_polimorfism/utils/errors/custom_error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | // NewCustomError creates a new custom error 4 | func NewCustomError(status int, message string) *CustomError { 5 | return &CustomError{ 6 | code: status, 7 | message: message, 8 | } 9 | } 10 | 11 | // CustomError es una interfaz para definir errores custom 12 | type CustomError struct { 13 | code int 14 | message string 15 | } 16 | 17 | // Code http error code 18 | func (e *CustomError) Code() int { 19 | return e.code 20 | } 21 | 22 | // Message http error message 23 | func (e *CustomError) Error() string { 24 | return e.message 25 | } 26 | -------------------------------------------------------------------------------- /go_libs/.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | .DS_Store 3 | .vscode 4 | -------------------------------------------------------------------------------- /go_libs/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [English version](README_en.md) 4 | 5 | # Las librerías compartidas 6 | 7 | Cuando implementamos un microservicio puntual, normalmente esta implementación debería resolver un problema puntual de nuestro negocio, y su interfaz con el mundo exterior esta definida a través de rest, message broker o algo similar. Su definición interna no es muy importante a los ojos de los usuarios, y podemos darnos el lujo de ser muy flexibles como lo venimos viendo en esta guía. 8 | 9 | Pero cuando implementamos librerías compartidas, como pueden ser: 10 | 11 | - Factories para servicios remotos 12 | - Log de errores 13 | - Apis de acceso (brokers de mensajería o bases de datos) 14 | - Acceso internacionalizado 15 | - Acceso a imágenes y recursos compartidos 16 | - Trazabilidad 17 | - Utilidades en general 18 | - Utilidades de testing (http o bases de datos mock) 19 | 20 | Estas librerías, tienen como funcionalidad no repetir código en los microservicios y ayudan a los equipos de desarrollo a centralizar decisiones y controlar lógica común. 21 | 22 | Pero no se desarrollan como el código que hemos venido trabajando en esta guía, sino que debemos hacerlas mas robustas para que puedan ser mas flexibles y adaptarse a nuevas implementaciones. 23 | 24 | ## Interfaces y estructuras 25 | 26 | Las estructuras en go definen datos relacionados en un contexto puntual. 27 | 28 | Por ejemplo 29 | 30 | ```go 31 | type Profile struct { 32 | ID string 33 | Login string 34 | Name string 35 | Web string 36 | } 37 | ``` 38 | 39 | Profile representa el perfil de un usuario. 40 | 41 | Las interfaces nos permiten definir comportamientos en forma desacoplada de las implementaciones. Por ejemplo, si definimos : 42 | 43 | ```go 44 | type Profile interface { 45 | ID() string 46 | Login() string 47 | Name() string 48 | Web() string 49 | } 50 | 51 | func FetchProfile(id string) Profile { 52 | ... 53 | } 54 | ``` 55 | 56 | Estamos definiendo una serie de comportamientos que se deben cumplir en las implementaciones, y que existe una función FetchProfile, que retorna esos comportamientos. 57 | 58 | Las implementaciones de las interfaces se hacen con estructuras y métodos asociadas a ellas : 59 | 60 | ```go 61 | type profile struct { 62 | id string 63 | login string 64 | name string 65 | web string 66 | } 67 | 68 | func (p *profile) ID() string { 69 | return p.id 70 | } 71 | 72 | func (p *profile) Login() string { 73 | return p.login 74 | } 75 | 76 | func (p *profile) Name() string { 77 | return p.name 78 | } 79 | 80 | func (p *profile) Web() string { 81 | return p.web 82 | } 83 | ``` 84 | 85 | Entonces podemos definir la funcion FetchProfile como : 86 | 87 | ```go 88 | func FetchProfile(id string) Profile { 89 | return &profile{ 90 | id: id, 91 | login: "nmarsollier", 92 | name: "Nestor Marsollier", 93 | web: "https://github.com/nmarsollier/profile", 94 | } 95 | } 96 | ``` 97 | 98 | > El uso de interfaces es lo que podríamos denominar polimorfismo en Go, ya que podemos implementar diferentes estructuras con diferentes comportamientos, pero respetando un set de funciones determinado. 99 | 100 | ## Y todo esto para que ? 101 | 102 | El esquema anterior es muy importante en el uso de librerías compartidas, ya que nos permite definir un comportamiento esperado, pero no los detalles de implementación interna, esto nos deja cierta flexibilidad a la hora de adaptarnos a cambios y a su vez a la hora de implementaciones personalizadas de cada negocio. 103 | 104 | Encapsulamos yu desacoplamos, abrimos posibilidades a otras implementaciones, podemos lograr mejor testing pudiendo mockear mejor llamadas y resultados. 105 | 106 | ## Atención a las interfaces implícitas 107 | 108 | En go las interfaces son implícitas, lo que quiere decir que no debemos ser específicos de que estructura implementa una interfaz, simplemente con que la estructura defina todos los métodos de la interfaz, ya la esta cumpliendo 109 | 110 | > Debemos ser específicos con los nombres de funciones en las interfaces. Si nuestra función se llama 'Serialize() string' en dos interfaces diferentes XMLSerializer y JSONSerializer, podemos incurrir en un error, porque para el compilador si una estructura implementa Serialize, ya esta implementando ambas interfaces. 111 | 112 | ## Donde definimos las interfaces ? 113 | 114 | Normalmente se definen en las librerías, a veces en las implementaciones de los microservicios necesitamos ser mas flexibles, y requieren de una definición de que es lo que necesitan para operar, que básicamente es un subset muy reducido de lo que la interfaz de la librería expone, y queremos generalizar para que pueda operar con otras opciones. 115 | 116 | Por ejemplo, un cliente de base de datos o cliente http, podría exponer muchos métodos, pero a nuestros DAOs no queremos habilitar todos los métodos que pueda exponer un cliente de bases de datos, por lo que creamos una interfaz con un subset de lo que necesitamos en nuestra app, y operamos con ese subset, para evitar usar métodos que no deseamos utilizar del cliente original. 117 | 118 | > En algunas bibliografías se recomienda definir la interfaz en cada función, de modo que en cada punto que se define una función se definen las interfaces necesarias, si bien esto parece que nos da flexibilidad, en realidad, terminamos teniendo demasiadas interfaces y muchos código que mantener, no lo recomiendo en lo personal. 119 | 120 | ## Reglas de las interfaces 121 | 122 | > Robustness principle: Be conservative with what you do, be liberal with you accept 123 | 124 | Nuestras funciones deberían poder funcionar correctamente con cualquier estructura de código que implemente una interfaz, sin embargo, debemos usar solo lo que nuestra interfaz define. Nunca debemos hacer interfaces que definan mas de lo que usamos. 125 | 126 | > Accept interfaces, return structs 127 | 128 | Las funciones deben definirse con parámetros y retornos en Interfaces, sin embargo nuestro código retorna una implementación especifica de una estructura 129 | 130 | ```go 131 | func FetchProfile(id string) Profile { 132 | return &profile{ 133 | id: id, 134 | login: "nmarsollier", 135 | name: "Nestor Marsollier", 136 | web: "https://github.com/nmarsollier/profile", 137 | } 138 | } 139 | ``` 140 | 141 | > The empty interface says nothing 142 | 143 | Limitamos el uso de interface{} al mínimo posible, solo lo usamos cuando no podemos mapear los resultados de una librería de terceros, o bien en alguna implementación muy puntual como mapas para cache. 144 | 145 | ## Nota 146 | 147 | Esta es una serie de notas sobre patrones simples de programación en GO. 148 | 149 | [Tabla de Contenidos](https://github.com/nmarsollier/go_index/blob/main/README.md) 150 | -------------------------------------------------------------------------------- /go_libs/README_en.md: -------------------------------------------------------------------------------- 1 | [Version in español](README.md) 2 | 3 | # Shared Libraries 4 | 5 | When we implement a specific microservice, this implementation should typically solve a specific business problem, and its interface with the outside world is defined through REST, message broker, or something similar. Its internal definition is not very important in the eyes of the users, and we can afford to be very flexible as we have seen in this guide. 6 | 7 | But when we implement shared libraries, such as: 8 | 9 | - Factories for remote services 10 | - Error logging 11 | - Access APIs (message brokers or databases) 12 | - Internationalized access 13 | - Access to images and shared resources 14 | - Traceability 15 | - General utilities 16 | - Testing utilities (HTTP or mock databases) 17 | 18 | These libraries aim to avoid code repetition in microservices and help development teams centralize decisions and control common logic. 19 | 20 | However, they are not developed like the code we have been working on in this guide; instead, we must make them more robust so they can be more flexible and adapt to new implementations. 21 | 22 | ## Interfaces and Structures 23 | 24 | Structures in Go define related data in a specific context. 25 | 26 | For example: 27 | 28 | ```go 29 | type Profile struct { 30 | ID string 31 | Login string 32 | Name string 33 | Web string 34 | } 35 | ``` 36 | 37 | Profile represents a user's profile. 38 | 39 | Interfaces allow us to define behaviors decoupled from implementations. For example, if we define: 40 | 41 | ```go 42 | type Profile interface { 43 | ID() string 44 | Login() string 45 | Name() string 46 | Web() string 47 | } 48 | 49 | func FetchProfile(id string) Profile { 50 | ... 51 | } 52 | ``` 53 | 54 | We are defining a series of behaviors that must be fulfilled in the implementations, and there is a function FetchProfile that returns those behaviors. 55 | 56 | The implementations of the interfaces are done with structures and methods associated with them: 57 | 58 | ```go 59 | type profile struct { 60 | id string 61 | login string 62 | name string 63 | web string 64 | } 65 | 66 | func (p *profile) ID() string { 67 | return p.id 68 | } 69 | 70 | func (p *profile) Login() string { 71 | return p.login 72 | } 73 | 74 | func (p *profile) Name() string { 75 | return p.name 76 | } 77 | 78 | func (p *profile) Web() string { 79 | return p.web 80 | } 81 | ``` 82 | 83 | Then we can define the FetchProfile function as: 84 | 85 | ```go 86 | func FetchProfile(id string) Profile { 87 | return &profile{ 88 | id: id, 89 | login: "nmarsollier", 90 | name: "Nestor Marsollier", 91 | web: "https://github.com/nmarsollier/profile", 92 | } 93 | } 94 | ``` 95 | 96 | > The use of interfaces is what we could call polymorphism in Go, as we can implement different structures with different behaviors, but respecting a set of defined functions. 97 | 98 | ## And all this for what? 99 | 100 | The above scheme is very important in the use of shared libraries, as it allows us to define an expected behavior, but not the details of internal implementation. This gives us some flexibility when adapting to changes and also when implementing customizations for each business. 101 | 102 | We encapsulate and decouple, open possibilities for other implementations, and can achieve better testing by being able to better mock calls and results. 103 | 104 | ## Attention to Implicit Interfaces 105 | 106 | In Go, interfaces are implicit, which means we do not need to be specific about which structure implements an interface. Simply by defining all the methods of the interface, the structure is already fulfilling it. 107 | 108 | > We must be specific with function names in interfaces. If our function is called 'Serialize() string' in two different interfaces XMLSerializer and JSONSerializer, we could run into an error because for the compiler, if a structure implements Serialize, it is already implementing both interfaces. 109 | 110 | ## Where do we define the interfaces? 111 | 112 | They are usually defined in libraries. Sometimes in microservice implementations, we need to be more flexible and require a definition of what they need to operate, which is basically a very reduced subset of what the library interface exposes, and we want to generalize so it can operate with other options. 113 | 114 | For example, a database client or HTTP client might expose many methods, but we do not want to enable all the methods that a database client might expose to our DAOs, so we create an interface with a subset of what we need in our app and operate with that subset to avoid using methods we do not want to use from the original client. 115 | 116 | > Some literature recommends defining the interface in each function, so that at each point where a function is defined, the necessary interfaces are defined. While this seems to give us flexibility, in reality, we end up having too many interfaces and a lot of code to maintain. I do not personally recommend it. 117 | 118 | ## Interface Rules 119 | 120 | > Robustness principle: Be conservative with what you do, be liberal with what you accept 121 | 122 | Our functions should be able to work correctly with any code structure that implements an interface; however, we should only use what our interface defines. We should never make interfaces that define more than what we use. 123 | 124 | > Accept interfaces, return structs 125 | 126 | Functions should be defined with parameters and returns in Interfaces; however, our code returns a specific implementation of a structure. 127 | 128 | ```go 129 | func FetchProfile(id string) Profile { 130 | return &profile{ 131 | id: id, 132 | login: "nmarsollier", 133 | name: "Nestor Marsollier", 134 | web: "https://github.com/nmarsollier/profile", 135 | } 136 | } 137 | ``` 138 | 139 | > The empty interface says nothing 140 | 141 | We limit the use of interface{} to the minimum possible; we only use it when we cannot map the results of a third-party library or in a very specific implementation such as maps for cache. 142 | 143 | ## Note 144 | 145 | This is a series of notes on simple programming patterns in Go. 146 | 147 | [Table of Contents](../README_en.md) 148 | -------------------------------------------------------------------------------- /go_libs/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nmarsollier/go_libs 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | github.com/stretchr/testify v1.6.1 8 | gopkg.in/go-playground/assert.v1 v1.2.1 9 | ) 10 | -------------------------------------------------------------------------------- /go_libs/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 6 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 7 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 8 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 9 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 10 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 11 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 12 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 13 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 14 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 15 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 16 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 17 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 18 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 19 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 20 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 21 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 22 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 23 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 24 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 25 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 26 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 27 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 28 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 29 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 34 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 35 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 36 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 37 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 38 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 39 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 40 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 41 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 42 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 48 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 49 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 50 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 51 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 52 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 53 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 54 | -------------------------------------------------------------------------------- /go_libs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nmarsollier/go_libs/rest/routes" 5 | ) 6 | 7 | func main() { 8 | routes.Start() 9 | } 10 | -------------------------------------------------------------------------------- /go_libs/model/profile/service.go: -------------------------------------------------------------------------------- 1 | package profile 2 | 3 | type Profile interface { 4 | ID() string 5 | Login() string 6 | Name() string 7 | Web() string 8 | } 9 | 10 | // Profile data 11 | type profile struct { 12 | id string 13 | login string 14 | name string 15 | web string 16 | } 17 | 18 | func (p *profile) ID() string { 19 | return p.id 20 | } 21 | 22 | func (p *profile) Login() string { 23 | return p.login 24 | } 25 | 26 | func (p *profile) Name() string { 27 | return p.name 28 | } 29 | 30 | func (p *profile) Web() string { 31 | return p.web 32 | } 33 | 34 | func FetchProfile(id string) Profile { 35 | return &profile{ 36 | id: id, 37 | login: "nmarsollier", 38 | name: "Nestor Marsollier", 39 | web: "https://github.com/nmarsollier/profile", 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /go_libs/rest/middlewares/error.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // ICustomError define un error con Code y Error 10 | type ICustomError interface { 11 | Code() int 12 | Error() string 13 | } 14 | 15 | // ErrorHandler a middleware to handle errors 16 | func ErrorHandler(c *gin.Context) { 17 | c.Next() 18 | 19 | handleErrorIfNeeded(c) 20 | } 21 | 22 | func handleErrorIfNeeded(c *gin.Context) { 23 | err := c.Errors.Last() 24 | if err == nil { 25 | return 26 | } 27 | 28 | switch value := err.Err.(type) { 29 | case ICustomError: 30 | c.JSON(value.Code(), 31 | gin.H{ 32 | "error": value.Error(), 33 | }) 34 | case error: 35 | c.JSON(http.StatusInternalServerError, 36 | gin.H{ 37 | "error": value.Error(), 38 | }) 39 | default: 40 | c.JSON(http.StatusInternalServerError, gin.H{ 41 | "error": err, 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /go_libs/rest/routes/get_device_profile_id.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/nmarsollier/go_libs/model/profile" 8 | ) 9 | 10 | // Servicio REST que nos retorna información de un dialogo a mostrar en pantalla 11 | // Vamos a usar el contexto como un Builder Pattern 12 | func init() { 13 | router().GET( 14 | "/:device/profile/:id", 15 | getProfile, 16 | ) 17 | } 18 | 19 | func getProfile(c *gin.Context) { 20 | id := c.Param("id") 21 | 22 | data := profile.FetchProfile(id) 23 | 24 | c.JSON(http.StatusOK, gin.H{ 25 | "login": data.Login(), 26 | "web": data.Web(), 27 | "name": data.Name(), 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /go_libs/rest/routes/rest.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/nmarsollier/go_libs/rest/middlewares" 6 | ) 7 | 8 | // Start server in 8080 port 9 | func Start() { 10 | router().Run(":8080") 11 | } 12 | 13 | var engine *gin.Engine = nil 14 | 15 | func router() *gin.Engine { 16 | if engine == nil { 17 | engine = gin.Default() 18 | engine.Use(middlewares.ErrorHandler) 19 | } 20 | 21 | return engine 22 | } 23 | -------------------------------------------------------------------------------- /go_libs/utils/errors/custom_error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | // NewCustomError creates a new custom error 4 | func NewCustomError(status int, message string) *CustomError { 5 | return &CustomError{ 6 | code: status, 7 | message: message, 8 | } 9 | } 10 | 11 | // CustomError es una interfaz para definir errores custom 12 | type CustomError struct { 13 | code int 14 | message string 15 | } 16 | 17 | // Code http error code 18 | func (e *CustomError) Code() int { 19 | return e.code 20 | } 21 | 22 | // Message http error message 23 | func (e *CustomError) Error() string { 24 | return e.message 25 | } 26 | -------------------------------------------------------------------------------- /go_rest_controller/.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | .DS_Store 3 | .vscode 4 | -------------------------------------------------------------------------------- /go_rest_controller/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [English Version](README_en.md) 4 | 5 | # REST Controllers en go 6 | 7 | Este repositorio plantea una forma simple y efectiva de organizar nuestros servicios REST en un entorno de microservicios. 8 | 9 | ## El controlador 10 | 11 | - Nuestros controllers se organizan en orden a las solicitudes Http 12 | - Cada archivo contiene una sola definición de una entrada a un controlador 13 | - Estructuramos los directorios del controladores en base a eso 14 | - Los nombres del archivo hacen referencia a la entrada http 15 | 16 | Por consiguiente tenemos la estructura : 17 | 18 | ```go 19 | - controllers 20 | get_hello_username.go 21 | get_ping.go 22 | rest.go 23 | ``` 24 | 25 | Donde rest.go contiene la inicialización del framework, pero no las rutas. 26 | 27 | ```go 28 | func Start() { 29 | getRouter().Run(":8080") 30 | } 31 | 32 | var router *gin.Engine = nil 33 | 34 | func getRouter() *gin.Engine { 35 | if router == nil { 36 | router = gin.Default() 37 | } 38 | 39 | return router 40 | } 41 | ``` 42 | 43 | Tenemos un archivo por método Rest (get_resource, put_resource, get_resources_id, etc) Cada uno maneja una sola función Rest. 44 | 45 | En estos archivos definimos todo lo que tenga que ver con esa ruta desde la definición de la misma hasta la implementación del controlador. 46 | 47 | ```go 48 | // Internal configure ping/pong service 49 | func init() { 50 | getRouter().GET("/ping", pingHandler) 51 | } 52 | 53 | func pingHandler(c *gin.Context) { 54 | c.JSON(http.StatusOK, gin.H{ 55 | "answer": "pong", 56 | }) 57 | } 58 | ``` 59 | 60 | Podemos notar que toda la implementación de la ruta queda contenida en un solo archivo. 61 | 62 | Este mismo concepto se puede adaptar y utilizar con cualquier protocolo como GRPC o sistemas de Mensajerías. 63 | 64 | ### Ventajas 65 | 66 | - Simplifica el código desde su estructura, separa conceptos claramente 67 | - Logramos responsabilidades únicas por archivo 68 | - Orienta nuestras apps y negocio específicamente a hacer algo puntual (una responsabilidad), desde el controlador 69 | - Orienta el código a responsabilidades simples 70 | - Simplifica la lectura y búsqueda del controlador 71 | - Simplifica el testing 72 | - Encapsula correctamente cada controlador 73 | - Desacopla las inicialización de rutas, haciendo la definición de la misma algo sustentable y mantenible 74 | - Permite una lectura clara de los middlewares de cada controlador 75 | 76 | ## Fundamentos 77 | 78 | ### MVC en Microservicios 79 | 80 | Porque microservicios es algo diferente a un monolito ? 81 | 82 | - Un microservicio en general maneja un solo aspecto puntual de todo el modelo de negocio 83 | - Posee una interfaz mucho mas simple 84 | - En general, la arquitectura de microservicios define como se deben comunicar los microservicios, y muchas veces se une el concepto de Vista y Controlador, ya que nos enfocamos muy pocos protocolos de E/S. 85 | - Si existen diferentes View, en general se manejan con diferentes microservicios Api Gateways. 86 | - No hay tanta segregación a implementar. 87 | 88 | ### El Controlador 89 | 90 | El en enfoque clásico MVC, el controlador y View poseen las siguientes funciones: 91 | 92 | - Interpreta un Request 93 | - Valida los datos de entrada 94 | - Adaptar el request a una solicitud del negocio 95 | - Llama al negocio 96 | - Adapta la respuesta del negocio de acuerdo al cliente 97 | - Maneja errores 98 | 99 | Y en general en los microservicios, conviene usar un framework especifico que resuelva cómodamente estos aspectos. 100 | 101 | ### Notas finales sobre la definición del protocolo REST 102 | 103 | No voy a entrar en detalles de como debería ser un protocolo REST, sino mas bien una simple introducción. En general podemos organizar nuestros repositorios de dos formas : 104 | 105 | #### Resource Centric 106 | 107 | Es donde el centro de la información es un recurso en particular. Hay mucha información, se le llama [RestFul](https://en.wikipedia.org/wiki/Representational_state_transfer). 108 | 109 | Hay mucha info de ésto. 110 | 111 | #### Use Case Centric 112 | 113 | El formato RestFul no es util cuando tenemos múltiples casos de uso sobre un recurso, por ejemplo una factura, podemos necesitar hacer diversas consultas y diversas acciones sobre la misma. Por lo que es conveniente definir el protocolo en base a casos de uso. 114 | 115 | De esto si que no hay un estándar. Existen varias formas de hacerlo, una forma simple y efectiva, es : 116 | 117 | - Utilizamos GET para consultar (queries) 118 | - Utilizamos POST para cambiar el estados (commands) 119 | - Agregamos un /sustantivo al final de los métodos GET para saber que recurso queremos 120 | - Agregamos un /verbo al final de un POST para saber que caso de uso queremos ejecutar 121 | 122 | Ejemplo : 123 | 124 | GET http://go_rest_controller.com/facturas/resumen 125 | 126 | GET http://go_rest_controller.com/facturas/total_consolidado 127 | 128 | GET http://go_rest_controller.com/facturas/:id/recibo 129 | 130 | GET http://go_rest_controller.com/facturas/:id/detalles 131 | 132 | GET http://go_rest_controller.com/facturas/:id/total 133 | 134 | POST http://go_rest_controller.com/facturas/:id/enviar_correo 135 | 136 | POST http://go_rest_controller.com/facturas/:id/pagar 137 | 138 | POST http://go_rest_controller.com/facturas/:id/cancelar 139 | 140 | ## Nota 141 | 142 | Esta es una serie de notas sobre patrones simples de programación en GO. 143 | 144 | [Tabla de Contenidos](../README.md) 145 | -------------------------------------------------------------------------------- /go_rest_controller/README_en.md: -------------------------------------------------------------------------------- 1 | [Versión en Español](README.md) 2 | 3 | # REST Controllers in go 4 | 5 | This notes are about a single and effective way to organize the rest services in a microservices environment. 6 | 7 | ## The Controller 8 | 9 | - The controllers are organized according to HTTP requests 10 | - Each file contains a single definition of a controller entry 11 | - We structure the controller directories based on this 12 | - The file names refer to the HTTP entry 13 | 14 | File names represent HTTP Rest entries very clear : 15 | 16 | ```go 17 | - controllers 18 | get_hello_username.go 19 | get_ping.go 20 | rest.go 21 | ``` 22 | 23 | Where rest.go contains the framework initialization only. 24 | 25 | ```go 26 | func Start() { 27 | getRouter().Run(":8080") 28 | } 29 | 30 | var router *gin.Engine = nil 31 | 32 | func getRouter() *gin.Engine { 33 | if router == nil { 34 | router = gin.Default() 35 | } 36 | 37 | return router 38 | } 39 | ``` 40 | 41 | We have a single file for each rest entry (get_resource, put_resource, get_resources_id, etc) each one handles the single request. 42 | 43 | ```go 44 | // Internal configure ping/pong service 45 | func init() { 46 | getRouter().GET("/ping", pingHandler) 47 | } 48 | 49 | func pingHandler(c *gin.Context) { 50 | c.JSON(http.StatusOK, gin.H{ 51 | "answer": "pong", 52 | }) 53 | } 54 | ``` 55 | 56 | We can note that all the implementation is contained in the same file. 57 | 58 | The same concept can be done with other frameworks like GRPC or any message framework. 59 | 60 | ### Pros 61 | 62 | - Simplifies the code structure, clearly separating concepts 63 | - Achieves single responsibility per file 64 | - Directs our apps and business to perform specific tasks (one responsibility) from the controller 65 | - Focuses the code on simple responsibilities 66 | - Simplifies reading and searching for the controller 67 | - Simplifies testing 68 | - Correctly encapsulates each controller 69 | - Decouples route initialization, making its definition sustainable and maintainable 70 | - Allows clear reading of each controller's middleware 71 | 72 | ## Fundaments 73 | 74 | ### MVC in Microservicios 75 | 76 | Why MS are different tha monoliths ? 77 | 78 | - A microservice in general handles only one aspect of our business 79 | - The interfaces are simpler, and limited 80 | - A single microservice defines most of the times has a single communication way, the View and Controller definitions are mixed and we don't focus too much in that layer separation. 81 | - If there are different Views most of the time those are different Gateways. 82 | 83 | ### The Controller 84 | 85 | The classic MVC, Views and controllers has to do: 86 | 87 | - Interpret Requests 88 | - Validate the entry data 89 | - Adapt the request to the business 90 | - Call the model 91 | - Adapt the response to the client 92 | - Handle errors 93 | 94 | And in general there is a framework that support us to do all these things. 95 | 96 | ### Final notes on REST 97 | 98 | I'm will not detail REST protocols, just will give a concept separation about ways to organize routes, we can be : 99 | 100 | #### Resource Centric 101 | 102 | Where the key is the resource, it is called [RestFul](https://en.wikipedia.org/wiki/Representational_state_transfer). 103 | 104 | #### Or Use Case Centric 105 | 106 | When the RestFul verbs are not enough to represent the use cases, over a resource, it's is better to adopt this path definition, for example a receipt, re can pay, decline, add articles, print, and so. So it's important to specify a protocol based on use cases. 107 | 108 | There is no standard, and there are several ways, one that I like is : 109 | 110 | - We use GET to ask for data (queries) 111 | - We use POST to change the server state (commands) 112 | - We add a substantive, at the end of GET to know the resource to get 113 | - We add a verb at the end to know with case use run on a resource update (POST) 114 | 115 | Examples : 116 | 117 | GET http://go_rest_controller.com/order/stats 118 | 119 | GET http://go_rest_controller.com/order/totals 120 | 121 | GET http://go_rest_controller.com/order/:id/receipt 122 | 123 | GET http://go_rest_controller.com/order/:id/details 124 | 125 | GET http://go_rest_controller.com/order/:id/totals 126 | 127 | POST http://go_rest_controller.com/order/:id/send_email 128 | 129 | POST http://go_rest_controller.com/order/:id/paid 130 | 131 | POST http://go_rest_controller.com/order/:id/cancel 132 | 133 | ## Note 134 | 135 | This is a series of notes about advanced Go patterns, with a really simple implementation. 136 | 137 | [Content Table](../README_en.md) 138 | -------------------------------------------------------------------------------- /go_rest_controller/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nmarsollier/go_rest_controller 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | github.com/stretchr/testify v1.6.1 8 | ) 9 | -------------------------------------------------------------------------------- /go_rest_controller/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 6 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 7 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 8 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 9 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 10 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 11 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 12 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 13 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 14 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 15 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 16 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 17 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 18 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 19 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 20 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 21 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 22 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 23 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 24 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 25 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 26 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 27 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 28 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 29 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 34 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 35 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 36 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 37 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 38 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 39 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 40 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 41 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 42 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 48 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 49 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 50 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 51 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 52 | -------------------------------------------------------------------------------- /go_rest_controller/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/nmarsollier/go_rest_controller/rest/routes" 4 | 5 | func main() { 6 | routes.Start() 7 | } 8 | -------------------------------------------------------------------------------- /go_rest_controller/model/hello/dao/hello_dao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | // Hello esta funcion en prod va a funcionar 4 | // sin problemas, una funcion, una responsabilidad 5 | // programacion funcional, todo tiene sentido 6 | func Hello() string { 7 | return "Holiiiiis" 8 | } 9 | -------------------------------------------------------------------------------- /go_rest_controller/model/hello/service/hello_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "github.com/nmarsollier/go_rest_controller/model/hello/dao" 4 | 5 | // Nos va a permitir mockear respuestas para los tests 6 | var daoHelloFunc func() string = dao.Hello 7 | 8 | // SayHello es nuestro negocio 9 | func SayHello(userName string) string { 10 | return daoHelloFunc() + " " + userName 11 | } 12 | -------------------------------------------------------------------------------- /go_rest_controller/model/hello/service/hello_service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSayHelo(t *testing.T) { 10 | defaultHelloFunc := daoHelloFunc 11 | 12 | // Cuando testeamos la reescribimos con el 13 | // mock que queramos 14 | daoHelloFunc = func() string { 15 | return "Hello" 16 | } 17 | 18 | assert.Equal(t, "Hello Pepe", SayHello("Pepe")) 19 | daoHelloFunc = defaultHelloFunc 20 | } 21 | -------------------------------------------------------------------------------- /go_rest_controller/rest/routes/get_hello_username.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/nmarsollier/go_rest_controller/model/hello/service" 8 | ) 9 | 10 | // Internal configure ping/pong service 11 | func init() { 12 | getRouter().GET("/hello/:userName", sayHelloHandler) 13 | } 14 | 15 | func sayHelloHandler(c *gin.Context) { 16 | userName := c.Param("userName") 17 | 18 | c.JSON(http.StatusOK, gin.H{ 19 | "answer": service.SayHello(userName), 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /go_rest_controller/rest/routes/get_ping.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // Internal configure ping/pong service 10 | func init() { 11 | getRouter().GET("/ping", pingHandler) 12 | } 13 | 14 | func pingHandler(c *gin.Context) { 15 | c.JSON(http.StatusOK, gin.H{ 16 | "answer": "pong", 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /go_rest_controller/rest/routes/rest.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | // Start server in 8080 port 8 | func Start() { 9 | getRouter().Run(":8080") 10 | } 11 | 12 | var router *gin.Engine = nil 13 | 14 | func getRouter() *gin.Engine { 15 | if router == nil { 16 | router = gin.Default() 17 | } 18 | 19 | return router 20 | } 21 | -------------------------------------------------------------------------------- /go_router_builder/.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | .DS_Store 3 | .vscode 4 | -------------------------------------------------------------------------------- /go_router_builder/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [English Version](README_en.md) 4 | 5 | # Builder Pattern en Router 6 | 7 | ## Usar middlewares como Builder Pattern ? 8 | 9 | En las notas previas vimos que podríamos usar el router para conformar una respuesta, si pensamos como funciona el patrón builder por ejemplo : 10 | 11 | ```go 12 | dialog.NewBuilder().Title("hola").AcceptAction("Aceptar", "ok").Build() 13 | ``` 14 | 15 | Esto podemos expresarlo en algo como : 16 | 17 | ```go 18 | getRouter().GET( 19 | "/dialog", 20 | setTitle, 21 | setAcceptAction, 22 | build 23 | ) 24 | ``` 25 | 26 | Este repositorio plantea ejemplos de como usar efectivamente el patrón Builder en un Router de Gin. 27 | 28 | ## Manejemos información compleja 29 | 30 | Podríamos pensar en algún escenario donde la información se genere de formas complejas y nos convenga generar un respuesta en forma segregada utilizando el router como patrón Builder. 31 | 32 | Supongamos un microservicio que pretende obtener información del usuario y del perfil, en una sola llamada. 33 | 34 | Si bien, no es puntualmente el mismo caso de builder del ejemplo anterior, el uso del router es el mismo: 35 | 36 | ```go 37 | router().GET( 38 | "/users/:id", 39 | validateUserName, 40 | fetchUser, 41 | fetchProfile, 42 | build, 43 | ) 44 | ``` 45 | 46 | Como siempre, primero validamos los datos del request, luego buscamos la información necesaria para dar nuestra respuesta : Buscamos el perfil, y el usuario. 47 | 48 | Como ultimo paso en la función build armaremos la respuesta final. 49 | 50 | Las funciones fetchUser y fetchProfile, son simples, llaman a un servicio del negocio, y ponen los datos en el contexto de gin. 51 | 52 | ```go 53 | func fetchUser(c *gin.Context) { 54 | c.Set("user", user.FetchUser()) 55 | c.Next() 56 | } 57 | ``` 58 | 59 | NOTA: Mucho cuidado con el contexto del gin, solo debemos usarlo en el controller, seria incorrecto enviar información a los servicios usando este sistema. 60 | 61 | Para armar nuestra respuesta, simplemente buscamos lo que tenemos en el contexto 62 | 63 | ```go 64 | func build(c *gin.Context) { 65 | user := c.MustGet("user").(*user.User) 66 | profile := c.MustGet("profile").(*profile.Profile) 67 | 68 | c.JSON(http.StatusOK, gin.H{ 69 | "login": user.Login, 70 | "access": user.Access, 71 | "name": profile.Name, 72 | }) 73 | } 74 | ``` 75 | 76 | ## Ejecución en paralelo 77 | 78 | Las funciones en los services tienen un delay de 1 segundo, para simular una conexión de red. Si vemos el log de respuesta, una llamada tiene una duración de 2 segundos aproximadamente. 79 | 80 | Esto lo podemos resolver simplemente ejecutando los ruteos FETCH en forma paralela. En get_parallel_user_id.go vamos a hacer uso de las mismas funciones de ruteo, solo que llamaremos los Fetch en paralelo. 81 | 82 | ```go 83 | router().GET( 84 | "/parallel/users/:id", 85 | validateUserName, 86 | inParallel( 87 | fetchUserInParallel, 88 | fetchProfileInParallel, 89 | ), 90 | build, 91 | ) 92 | ``` 93 | 94 | Debemos crear una función que nos permita la ejecución en paralelo, inParallel esta escrita simple, pero efectiva, el tiempo de respuesta es de 1 segundo, por lo que las llamadas remotas se ejecutan en paralelo. 95 | 96 | ```go 97 | func inParallel(handlers ...gin.HandlerFunc) gin.HandlerFunc { 98 | return func(c *gin.Context) { 99 | var waitGroup sync.WaitGroup 100 | waitGroup.Add(len(handlers)) 101 | 102 | for _, handler := range handlers { 103 | go func(handlerFunc gin.HandlerFunc) { 104 | defer waitGroup.Done() 105 | handlerFunc(c) 106 | }(handler) 107 | } 108 | 109 | waitGroup.Wait() 110 | 111 | c.Next() 112 | } 113 | } 114 | ``` 115 | 116 | Es genérica, por lo que debería estar en algún paquete de utilidades reutilizables. 117 | 118 | En las funciones fetch, lo único a tener en cuenta es que ahora los handlers no llamaran a Next, porque lo hace inParallel. 119 | 120 | ```go 121 | func fetchUserInParallel(c *gin.Context) { 122 | c.Set("user", user.FetchUser()) 123 | } 124 | ``` 125 | 126 | ## Nota 127 | 128 | Esta es una serie de notas sobre patrones simples de programación en GO. 129 | 130 | [Tabla de Contenidos](../README.md) 131 | -------------------------------------------------------------------------------- /go_router_builder/README_en.md: -------------------------------------------------------------------------------- 1 | [Versión en Español](README.md) 2 | 3 | # Builder Pattern in Routers 4 | 5 | ## Can we use router middlewares as builder pattern ? 6 | 7 | On previous notes we saw that we can use many handlers to prepare a result, if we think how a builder works : 8 | 9 | ```go 10 | dialog.NewBuilder().Title("hola").AcceptAction("Aceptar", "ok").Build() 11 | ``` 12 | 13 | That can be expressed in the route as : 14 | 15 | ```go 16 | getRouter().GET( 17 | "/dialog", 18 | setTitle, 19 | setAcceptAction, 20 | build 21 | ) 22 | ``` 23 | 24 | ## Handling complex data 25 | 26 | We could think in an scenario where the data is generated in a complex way, and then it could be convenient to segregate the response in many handlers. 27 | 28 | Lets suppose a route that wants to mix user and profile information, in the same response. 29 | 30 | It's not exactly the same case as previous but the solution is the same: 31 | 32 | ```go 33 | router().GET( 34 | "/users/:id", 35 | validateUserName, 36 | fetchUser, 37 | fetchProfile, 38 | build, 39 | ) 40 | ``` 41 | 42 | As always, we check parameters at first, then we get necessary information about the user and profile, using the context to hold answers. 43 | 44 | Finally we build the response. 45 | 46 | Functions fetchUser y fetchProfile, are simple, the call the model, and put the result in the context. 47 | 48 | ```go 49 | func fetchUser(c *gin.Context) { 50 | c.Set("user", user.FetchUser()) 51 | c.Next() 52 | } 53 | ``` 54 | 55 | --- 56 | 57 | NOTE 58 | 59 | Caution adding data to go context, we should use them only in controllers, we cannot call services with that context to get that cached values, it would be incorrect because gin could not be the only one controller around. 60 | 61 | --- 62 | 63 | To build the response we get data from context, and put al the data in the response. 64 | 65 | ```go 66 | func build(c *gin.Context) { 67 | user := c.MustGet("user").(*user.User) 68 | profile := c.MustGet("profile").(*profile.Profile) 69 | 70 | c.JSON(http.StatusOK, gin.H{ 71 | "login": user.Login, 72 | "access": user.Access, 73 | "name": profile.Name, 74 | }) 75 | } 76 | ``` 77 | 78 | ## Parallel execution 79 | 80 | Model functions contains 1 second delay to simulate along call, If we check the response time, it's around 2 seconds. 81 | 82 | We can call both calls in parallel using this single trick: 83 | 84 | ```go 85 | router().GET( 86 | "/parallel/users/:id", 87 | validateUserName, 88 | inParallel( 89 | fetchUserInParallel, 90 | fetchProfileInParallel, 91 | ), 92 | build, 93 | ) 94 | ``` 95 | 96 | we need to code a function to run in parallel, inParallel it is a simple function but effective, response time now is around 1 second. 97 | 98 | ```go 99 | func inParallel(handlers ...gin.HandlerFunc) gin.HandlerFunc { 100 | return func(c *gin.Context) { 101 | var waitGroup sync.WaitGroup 102 | waitGroup.Add(len(handlers)) 103 | 104 | for _, handler := range handlers { 105 | go func(handlerFunc gin.HandlerFunc) { 106 | defer waitGroup.Done() 107 | handlerFunc(c) 108 | }(handler) 109 | } 110 | 111 | waitGroup.Wait() 112 | 113 | c.Next() 114 | } 115 | } 116 | ``` 117 | 118 | The only thing to notice, is that fetch functions does not call Next, because inParallel does that at the end. 119 | 120 | ```go 121 | func fetchUserInParallel(c *gin.Context) { 122 | c.Set("user", user.FetchUser()) 123 | } 124 | ``` 125 | 126 | ## Note 127 | 128 | This is a series of notes about advanced Go patterns, with a really simple implementation. 129 | 130 | [Content Table](../README_en.md) 131 | -------------------------------------------------------------------------------- /go_router_builder/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nmarsollier/go_router_builder 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | github.com/stretchr/testify v1.6.1 8 | gopkg.in/go-playground/assert.v1 v1.2.1 9 | ) 10 | -------------------------------------------------------------------------------- /go_router_builder/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 6 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 7 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 8 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 9 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 10 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 11 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 12 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 13 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 14 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 15 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 16 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 17 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 18 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 19 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 20 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 21 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 22 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 23 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 24 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 25 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 26 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 27 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 28 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 29 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 34 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 35 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 36 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 37 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 38 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 39 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 40 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 41 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 42 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 48 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 49 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 50 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 51 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 52 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 53 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 54 | -------------------------------------------------------------------------------- /go_router_builder/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/nmarsollier/go_router_builder/rest/routes" 4 | 5 | func main() { 6 | routes.Start() 7 | } 8 | -------------------------------------------------------------------------------- /go_router_builder/model/profile/service.go: -------------------------------------------------------------------------------- 1 | package profile 2 | 3 | import "time" 4 | 5 | // Profile data 6 | type Profile struct { 7 | Login string 8 | Name string 9 | Web string 10 | } 11 | 12 | // FetchProfile Devuelve información de Usuario 13 | func FetchProfile() *Profile { 14 | // Un delay para simular tiempo de espera en llamadas remotas 15 | time.Sleep(1 * time.Second) 16 | 17 | return &Profile{ 18 | Login: "nmarsollier", 19 | Name: "Nestor Marsollier", 20 | Web: "https://github.com/nmarsollier/profile", 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /go_router_builder/model/user/service.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "time" 4 | 5 | // User data 6 | type User struct { 7 | Login string 8 | Access string 9 | } 10 | 11 | // FetchUser Devuelve información de Usuario 12 | func FetchUser() *User { 13 | // Un delay para simular tiempo de espera en llamadas remotas 14 | time.Sleep(1 * time.Second) 15 | 16 | return &User{ 17 | Login: "nmarsollier", 18 | Access: "ADMIN,USER", 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /go_router_builder/rest/middlewares/error.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // ICustomError define un error con Code y Error 10 | type ICustomError interface { 11 | Code() int 12 | Error() string 13 | } 14 | 15 | // ErrorHandler a middleware to handle errors 16 | func ErrorHandler(c *gin.Context) { 17 | c.Next() 18 | 19 | handleErrorIfNeeded(c) 20 | } 21 | 22 | func handleErrorIfNeeded(c *gin.Context) { 23 | err := c.Errors.Last() 24 | if err == nil { 25 | return 26 | } 27 | 28 | switch value := err.Err.(type) { 29 | case ICustomError: 30 | c.JSON(value.Code(), 31 | gin.H{ 32 | "error": value.Error(), 33 | }) 34 | case error: 35 | c.JSON(http.StatusInternalServerError, 36 | gin.H{ 37 | "error": value.Error(), 38 | }) 39 | default: 40 | c.JSON(http.StatusInternalServerError, gin.H{ 41 | "error": err, 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /go_router_builder/rest/routes/get_parallel_user_id.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/nmarsollier/go_router_builder/model/profile" 8 | "github.com/nmarsollier/go_router_builder/model/user" 9 | ) 10 | 11 | // Servicio REST que nos retorna información de un dialogo a mostrar en pantalla 12 | // Vamos a usar el contexto como un Builder Pattern 13 | func init() { 14 | router().GET( 15 | "/parallel/users/:id", 16 | validateUserName, 17 | inParallel( 18 | fetchUserInParallel, 19 | fetchProfileInParallel, 20 | ), 21 | build, 22 | ) 23 | } 24 | 25 | func fetchUserInParallel(c *gin.Context) { 26 | c.Set("user", user.FetchUser()) 27 | } 28 | 29 | func fetchProfileInParallel(c *gin.Context) { 30 | c.Set("profile", profile.FetchProfile()) 31 | } 32 | 33 | func inParallel(handlers ...gin.HandlerFunc) gin.HandlerFunc { 34 | return func(c *gin.Context) { 35 | var waitGroup sync.WaitGroup 36 | waitGroup.Add(len(handlers)) 37 | 38 | for _, handler := range handlers { 39 | go func(handlerFunc gin.HandlerFunc) { 40 | defer waitGroup.Done() 41 | handlerFunc(c) 42 | }(handler) 43 | } 44 | 45 | waitGroup.Wait() 46 | 47 | c.Next() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /go_router_builder/rest/routes/get_user_id.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/nmarsollier/go_router_builder/model/profile" 8 | "github.com/nmarsollier/go_router_builder/model/user" 9 | "github.com/nmarsollier/go_router_builder/utils/errors" 10 | ) 11 | 12 | // Servicio REST que nos retorna información de un dialogo a mostrar en pantalla 13 | // Vamos a usar el contexto como un Builder Pattern 14 | func init() { 15 | router().GET( 16 | "/users/:id", 17 | validateUserName, 18 | fetchUser, 19 | fetchProfile, 20 | build, 21 | ) 22 | } 23 | 24 | // Hacemos las validaciones que nos aseguran que id es valido 25 | func validateUserName(c *gin.Context) { 26 | id := c.Param("id") 27 | 28 | if len(id) < 1 { 29 | c.Error(errors.NewCustomError(400, "userName debe tener al menos 1 caracteres")) 30 | c.Abort() 31 | return 32 | } 33 | } 34 | 35 | func fetchUser(c *gin.Context) { 36 | c.Set("user", user.FetchUser()) 37 | c.Next() 38 | } 39 | 40 | func fetchProfile(c *gin.Context) { 41 | c.Set("profile", profile.FetchProfile()) 42 | c.Next() 43 | } 44 | 45 | func build(c *gin.Context) { 46 | user := c.MustGet("user").(*user.User) 47 | profile := c.MustGet("profile").(*profile.Profile) 48 | 49 | c.JSON(http.StatusOK, gin.H{ 50 | "login": user.Login, 51 | "access": user.Access, 52 | "name": profile.Name, 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /go_router_builder/rest/routes/rest.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/nmarsollier/go_router_builder/rest/middlewares" 6 | ) 7 | 8 | // Start server in 8080 port 9 | func Start() { 10 | router().Run(":8080") 11 | } 12 | 13 | var engine *gin.Engine = nil 14 | 15 | func router() *gin.Engine { 16 | if engine == nil { 17 | engine = gin.Default() 18 | engine.Use(middlewares.ErrorHandler) 19 | } 20 | 21 | return engine 22 | } 23 | -------------------------------------------------------------------------------- /go_router_builder/utils/errors/custom_error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | // NewCustomError creates a new custom error 4 | func NewCustomError(status int, message string) *CustomError { 5 | return &CustomError{ 6 | code: status, 7 | message: message, 8 | } 9 | } 10 | 11 | // CustomError es una interfaz para definir errores custom 12 | type CustomError struct { 13 | code int 14 | message string 15 | } 16 | 17 | // Code http error code 18 | func (e *CustomError) Code() int { 19 | return e.code 20 | } 21 | 22 | // Message http error message 23 | func (e *CustomError) Error() string { 24 | return e.message 25 | } 26 | -------------------------------------------------------------------------------- /go_router_design/.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | .DS_Store 3 | .vscode 4 | -------------------------------------------------------------------------------- /go_router_design/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [English version](README_en.md) 4 | 5 | # Router Design Pattern 6 | 7 | Este repositorio plantea ejemplos de como usar efectivamente el patrón de diseño Router en los framework REST. 8 | 9 | ## Router Design Pattern 10 | 11 | Este patrón de diseño surge de lenguajes funcionales, muchos los frameworks modernos lo adoptan, como express, gin, etc. 12 | 13 | El concepto es muy simple, definimos una ruta y llamamos a los handlers que queremos que se ejecuten. 14 | 15 | ```go 16 | func init() { 17 | getRouter().GET("/ping", pingHandler) 18 | } 19 | ``` 20 | 21 | En su forma mas simple, es una ruta y un handler. 22 | 23 | ## Chain Of Responsibility 24 | 25 | Pero el concepto de Route pattern va mas allá, para comprenderlo, necesitamos conocer un poco mas del patrón Chain of Responsibility (CoR). 26 | 27 | - Es un concepto viejo de programación funcional 28 | - Permite reutilizar código 29 | - Separa mejor las responsabilidades en funciones simples 30 | - Permite hacer las responsabilidades de controlador de forma muy simplificada 31 | - Permite armar una respuesta compleja en forma ordenada y por etapas 32 | 33 | ![Chain of Responsibility](./img/cor.png) 34 | 35 | Como podemos ver, la idea es descomponer una rutina grande en funciones que realicen una sola actividad, y continúen o bloqueen el proceso de ser necesario. 36 | 37 | ## Volviendo al Router Design Pattern 38 | 39 | El router pattern está basado en CoR pattern. 40 | 41 | Existen 2 conceptos middlewares y handlers 42 | 43 | En todos los casos tenemos la opción de controlar completamente el control del request. 44 | 45 | ### Middlewares 46 | 47 | En general se les llama los handlers, y actúan sobre todas las rutas y se definen a nivel servidor. 48 | 49 | Es muy util para realizar operaciones como : 50 | 51 | - Seguridad : Autorización y Autenticación 52 | - Carga de recursos i18n 53 | - Manejo de errores 54 | - Validar CORS 55 | - Validar seguridad de empaquetado y estructuras 56 | - Prevenir Hacks 57 | - Manejo de contextos de transacciones 58 | - Precarga de recursos relacionados al request 59 | - Loggers de estadísticas 60 | 61 | Y muchas otras cosas mas. 62 | 63 | En este repositorio, podemos encontrar un ejemplo en middlewares/errors.go 64 | 65 | En la ruta lo configuramos en la configuración global del servidor: 66 | 67 | ```go 68 | router = gin.Default() 69 | router.Use(middlewares.ErrorHandler) 70 | ``` 71 | 72 | Y la implementación : 73 | 74 | ```go 75 | // ErrorHandler a middleware to handle errors 76 | func ErrorHandler(c *gin.Context) { 77 | c.Next() 78 | 79 | handleErrorIfNeeded(c) 80 | } 81 | 82 | func handleErrorIfNeeded(c *gin.Context) { 83 | err := c.Errors.Last() 84 | ... 85 | ``` 86 | 87 | Este handler llama primero a c.Next, que corresponde a la llamada del siguiente elemento en la cadena de responsabilidades, una vez ejecutado el código, se analizara si se obtuvo una respuesta a través de un error, en cuyo caso se enviara al cliente una respuesta acorde al error obtenido. 88 | 89 | En otras implementaciones podríamos bloquear la llamada a Next de ser necesario y responder con algún error puntual. 90 | 91 | ### Handlers de ruta 92 | 93 | El funcionamiento es el mismo que el de middleware, solo que aplican a una ruta en particular. 94 | 95 | Son muy útiles para 96 | 97 | - Validar requests 98 | - Validar autorización a recursos 99 | - Validar estructuras de datos 100 | - Precarga de datos relacionados 101 | 102 | En general son la herramienta mas importante para producir un early exit o guard clause. 103 | 104 | Con ellos podemos cortar la cadena de responsabilidades forzando errores, de una manera muy sencilla y puntual. 105 | 106 | Permite desacoplar todo el manejo del controller en funciones simples, fáciles de testear, de leer, y mantener. 107 | 108 | Ademas nos permite reutilizar funciones, ya que los handlers son funciones, y al tener una sola responsabilidad es común que podamos reutilizarlas. Por ejemplo para validar algún parámetro de url que utilicemos en varias rutas. 109 | 110 | Como vemos en el ejemplo get_hello_username.go 111 | 112 | ```go 113 | // Internal configure ping/pong service 114 | func init() { 115 | getRouter().GET( 116 | "/hello/:userName", 117 | validateUserName, 118 | sayHelloHandler, 119 | ) 120 | } 121 | 122 | // validamos que el parámetro userName tenga al menos 5 caracteres 123 | func validateUserName(c *gin.Context) { 124 | userName := c.Param("userName") 125 | 126 | if len(userName) < 5 { 127 | c.Error(errors.NewCustomError(400, "userName debe tener al menos 5 caracteres")) 128 | c.Abort() 129 | return 130 | } 131 | } 132 | ``` 133 | 134 | La función validateUserName se define como un middleware de ruta, y valida el parámetro de url para que sea correcto, en caso de error aborta la cadena de responsabilidades. Este patrón se le llama early exit o guard clause. 135 | 136 | Una vez realizadas todas las validaciones, el código del handler es muy simple y muy fácil de leer, porque a estas alturas nos aseguramos que todo este correcto para ejecutarlo. 137 | 138 | ```go 139 | func sayHelloHandler(c *gin.Context) { 140 | userName := c.Param("userName") 141 | 142 | c.JSON(http.StatusOK, gin.H{ 143 | "answer": service.SayHello(userName), 144 | }) 145 | } 146 | ``` 147 | 148 | Los tests son mucho mas sencillos, porque testeamos función por función en forma individual, por ejemplo : 149 | 150 | ```go 151 | func TestValidateUserName(t *testing.T) { 152 | response := test.ResponseWriter(t) 153 | context, _ := gin.CreateTestContext(response) 154 | context.Request, _ = http.NewRequest("GET", "/hello/abc", nil) 155 | 156 | validateUserName(context) 157 | 158 | response.Assert(0, "") 159 | assert.Equal(t, context.Errors.Last().Error(), "userName debe tener al menos 5 caracteres") 160 | } 161 | 162 | ``` 163 | 164 | ### Middlewares para precarga de datos 165 | 166 | Muchas veces necesitaremos cierta precarga de datos en el contexto, como por ejemplo información del usuario logreado, para que posteriormente podamos realizar validaciones o llenar datos acorde a esa precarga. 167 | 168 | Es totalmente factible hacerlo, con las siguientes advertencias : 169 | 170 | - Los datos que se agreguen en el contexto solo se deben acceder por controllers o middlewares, no desde servicios de negocio. 171 | - No conviene encapsular la función que obtiene el dato del contexto en el mismo middleware. 172 | - No conviene abusar de esta estrategia. 173 | 174 | Un ejemplo ilustrativo podría ser : 175 | 176 | ```go 177 | func LoadCurrentUser(c *gin.Context) { 178 | token, err := c.GetHeader("Authorization") 179 | if err != nil { 180 | return 181 | } 182 | 183 | userProfile := userDao.FindUserByToken(token) 184 | 185 | c.Set("profile", userProfile) 186 | } 187 | 188 | func CurrentUserProfile(c *gin.Context) *users.Profile { 189 | if t, _ := c.Get("profile"); t != nil { 190 | return t.(*users.Profile) 191 | } 192 | return nil 193 | } 194 | ``` 195 | 196 | En el ejemplo anterior, el middleware es LoadCurrentUser, donde se buscara de acuerdo al token el perfil de usuario correspondiente. 197 | 198 | Cuando algún controlador quiera obtener este dato, llamara a CurrentUserProfile, que nos retornara el dato en el contexto. 199 | 200 | De esta forma controlamos, encapsulamos y desacoplamos la estrategia de cache en contextos. 201 | 202 | ## Nota 203 | 204 | Esta es una serie de notas sobre patrones simples de programación en GO. 205 | 206 | [Tabla de Contenidos](../README.md) 207 | -------------------------------------------------------------------------------- /go_router_design/README_en.md: -------------------------------------------------------------------------------- 1 | [Version en Español](README_en.md) 2 | 3 | # Router Design Pattern 4 | 5 | This repository talks about the effective use of router pattern in REST framework. 6 | 7 | ## The Pattern 8 | 9 | This framework comes with functional languages, and becomed addopted in modern rest frameworks like express, gin, etc. 10 | 11 | El concept is easy, we define a route, and fill the handlers that needs to be executed : 12 | 13 | ```go 14 | func init() { 15 | getRouter().GET("/ping", pingHandler) 16 | } 17 | ``` 18 | 19 | In it's simples way it's a route and a handler. 20 | 21 | ## Chain Of Responsibility 22 | 23 | But the concept goes far away from there, to understand it, we need to know better the Chain of Responsibility Pattern (CoR). 24 | 25 | - It's an old functional programming concept 26 | - Allows us to reuse code 27 | - Split responsibilities in functions 28 | - Allow users to define responsibilities in controllers in a simpler way 29 | - Controllers can build complex responses one at the time 30 | 31 | ![Chain of Responsibility](./img/cor.png) 32 | 33 | As we can see, the idea is to to Como podemos ver, la idea is to break down a big routine in small functions that does only one thing, each function can continue or break the flow. 34 | 35 | ## Back to Router Design Pattern 36 | 37 | The pattern is base on CoR. 38 | 39 | The are 2 main concepts, middlewares and handlers 40 | 41 | In both we have full control over the request. 42 | 43 | ### Middlewares 44 | 45 | Generally we call them handlers, and the are used by the router in a high level, all the routes are affected by them. 46 | 47 | The are useful for : 48 | 49 | - Security: Authentication and Authorization 50 | - Loading resources like i18n 51 | - Error handling 52 | - CORS validations 53 | - Block hacking techniques 54 | - Handle transactional contexts 55 | - Preloading of resources related 56 | - Loggers and stats 57 | 58 | And much more. 59 | 60 | In the repository we can see the error handler in middlewares/errors.go 61 | 62 | In the router we setup the global middleware: 63 | 64 | ```go 65 | router = gin.Default() 66 | router.Use(middlewares.ErrorHandler) 67 | ``` 68 | 69 | And this is the implementation : 70 | 71 | ```go 72 | // ErrorHandler a middleware to handle errors 73 | func ErrorHandler(c *gin.Context) { 74 | c.Next() 75 | 76 | handleErrorIfNeeded(c) 77 | } 78 | 79 | func handleErrorIfNeeded(c *gin.Context) { 80 | err := c.Errors.Last() 81 | ... 82 | ``` 83 | 84 | This handler calls first c.Next, that corresponds to the next call in the chain, once it's executed, it analyzes the response to see if the are errors to handle, if there is one, it responds the according error. 85 | 86 | In other implementations we could block the Next execution if needed. 87 | 88 | ### Route Handlers 89 | 90 | It's the same as the middleware, but applies to single routes. 91 | 92 | They are useful to : 93 | 94 | - Validate requests 95 | - Validate authorization 96 | - Validate body structs and parameters 97 | - Preload related data 98 | 99 | Generally they are used to do an early exit if something is wrong about the request. 100 | 101 | With them we can cut the chain, forcing errors, in a single way. 102 | 103 | And decouple the responsibilities of the controller in single functions, easy to read, test and maintain. 104 | 105 | Also we can reuse functions, like parameter validations, due that handlers are functions that we can add or remove from router in any route. 106 | 107 | As we see in the file get_hello_username.go 108 | 109 | ```go 110 | // Internal configure ping/pong service 111 | func init() { 112 | getRouter().GET( 113 | "/hello/:userName", 114 | validateUserName, 115 | sayHelloHandler, 116 | ) 117 | } 118 | 119 | // validamos que el parámetro userName tenga al menos 5 caracteres 120 | func validateUserName(c *gin.Context) { 121 | userName := c.Param("userName") 122 | 123 | if len(userName) < 5 { 124 | c.Error(errors.NewCustomError(400, "userName debe tener al menos 5 caracteres")) 125 | c.Abort() 126 | return 127 | } 128 | } 129 | ``` 130 | 131 | function validateUserName is defined as route middleware, validates the url parameter to be correct, and in error case can abort the execution. This strategy is called early exit or guard clause. 132 | 133 | Once done all validations, we can run the handler, ensuring that everything is correct. 134 | 135 | ```go 136 | func sayHelloHandler(c *gin.Context) { 137 | userName := c.Param("userName") 138 | 139 | c.JSON(http.StatusOK, gin.H{ 140 | "answer": service.SayHello(userName), 141 | }) 142 | } 143 | ``` 144 | 145 | Tests are simpler, because each function has one thing to test : 146 | 147 | ```go 148 | func TestValidateUserName(t *testing.T) { 149 | response := test.ResponseWriter(t) 150 | context, _ := gin.CreateTestContext(response) 151 | context.Request, _ = http.NewRequest("GET", "/hello/abc", nil) 152 | 153 | validateUserName(context) 154 | 155 | response.Assert(0, "") 156 | assert.Equal(t, context.Errors.Last().Error(), "userName debe tener al menos 5 caracteres") 157 | } 158 | 159 | ``` 160 | 161 | ### Middlewares lo preload data 162 | 163 | Many times we ned some data preload in the context ,like logged in user profile, to be used later in other parts of the chain. 164 | 165 | It's totally feasible, with the next warning: Preload data can only be accesses by the controller, we should never call a service with the context to fetch context info. 166 | 167 | An example : 168 | 169 | ```go 170 | func LoadCurrentUser(c *gin.Context) { 171 | token, err := c.GetHeader("Authorization") 172 | if err != nil { 173 | return 174 | } 175 | 176 | userProfile := userDao.FindUserByToken(token) 177 | 178 | c.Set("profile", userProfile) 179 | } 180 | 181 | func CurrentUserProfile(c *gin.Context) *users.Profile { 182 | if t, _ := c.Get("profile"); t != nil { 183 | return t.(*users.Profile) 184 | } 185 | return nil 186 | } 187 | ``` 188 | 189 | In the previous same, the same middleware that preload the data LoadCurrentUser, is the one that will look int to the context to get the related data. 190 | 191 | When we need to get the current user we call CurrentUserProfile. 192 | 193 | ## Note 194 | 195 | This is a series of notes about advanced Go patterns, with a really simple implementation. 196 | 197 | [Content Table](../README_en.md) 198 | -------------------------------------------------------------------------------- /go_router_design/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nmarsollier/go_router_design 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | github.com/stretchr/testify v1.6.1 8 | gopkg.in/go-playground/assert.v1 v1.2.1 9 | ) 10 | -------------------------------------------------------------------------------- /go_router_design/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 6 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 7 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 8 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 9 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 10 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 11 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 12 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 13 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 14 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 15 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 16 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 17 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 18 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 19 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 20 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 21 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 22 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 23 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 24 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 25 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 26 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 27 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 28 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 29 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 34 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 35 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 36 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 37 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 38 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 39 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 40 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 41 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 42 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 48 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 49 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 50 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 51 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 52 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 53 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 54 | -------------------------------------------------------------------------------- /go_router_design/img/cor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmarsollier/go_index/9416f6607d42503c14aaf0f0cda479e8553f27b9/go_router_design/img/cor.png -------------------------------------------------------------------------------- /go_router_design/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/nmarsollier/go_router_design/rest/routes" 4 | 5 | func main() { 6 | routes.Start() 7 | } 8 | -------------------------------------------------------------------------------- /go_router_design/model/hello/dao/hello_dao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | // Hello esta funcion en prod va a funcionar 4 | // sin problemas, una funcion, una responsabilidad 5 | // programacion funcional, todo tiene sentido 6 | func Hello() string { 7 | return "Holiiiiis" 8 | } 9 | -------------------------------------------------------------------------------- /go_router_design/model/hello/service/hello_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "github.com/nmarsollier/go_router_design/model/hello/dao" 4 | 5 | // Nos va a permitir mockear respuestas para los tests 6 | var daoHelloFunc func() string = dao.Hello 7 | 8 | // SayHello es nuestro negocio 9 | func SayHello(userName string) string { 10 | return daoHelloFunc() + " " + userName 11 | } 12 | -------------------------------------------------------------------------------- /go_router_design/model/hello/service/hello_service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSayHelo(t *testing.T) { 10 | defaultHelloFunc := daoHelloFunc 11 | 12 | // Cuando testeamos la reescribimos con el 13 | // mock que queramos 14 | daoHelloFunc = func() string { 15 | return "Hello" 16 | } 17 | 18 | assert.Equal(t, "Hello Pepe", SayHello("Pepe")) 19 | daoHelloFunc = defaultHelloFunc 20 | } 21 | -------------------------------------------------------------------------------- /go_router_design/rest/middlewares/error.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // ICustomError define un error con Code y Error 10 | type ICustomError interface { 11 | Code() int 12 | Error() string 13 | } 14 | 15 | // ErrorHandler a middleware to handle errors 16 | func ErrorHandler(c *gin.Context) { 17 | c.Next() 18 | 19 | handleErrorIfNeeded(c) 20 | } 21 | 22 | func handleErrorIfNeeded(c *gin.Context) { 23 | err := c.Errors.Last() 24 | if err == nil { 25 | return 26 | } 27 | 28 | switch value := err.Err.(type) { 29 | case ICustomError: 30 | c.JSON(value.Code(), 31 | gin.H{ 32 | "error": value.Error(), 33 | }) 34 | case error: 35 | c.JSON(http.StatusInternalServerError, 36 | gin.H{ 37 | "error": value.Error(), 38 | }) 39 | default: 40 | c.JSON(http.StatusInternalServerError, gin.H{ 41 | "error": err, 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /go_router_design/rest/middlewares/error_test.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/gin-gonic/gin" 8 | errutils "github.com/nmarsollier/go_router_design/utils/errors" 9 | "github.com/nmarsollier/go_router_design/utils/test" 10 | ) 11 | 12 | func TestCustomError(t *testing.T) { 13 | response := test.ResponseWriter(t) 14 | context, _ := gin.CreateTestContext(response) 15 | 16 | context.Error(errutils.NewCustomError(400, "Custom Test")) 17 | handleErrorIfNeeded(context) 18 | 19 | response.Assert(400, "{\"error\":\"Custom Test\"}") 20 | } 21 | 22 | func TestError(t *testing.T) { 23 | response := test.ResponseWriter(t) 24 | context, _ := gin.CreateTestContext(response) 25 | 26 | context.Error(errors.New("Error Test")) 27 | handleErrorIfNeeded(context) 28 | 29 | response.Assert(500, "{\"error\":\"Error Test\"}") 30 | } 31 | -------------------------------------------------------------------------------- /go_router_design/rest/routes/get_hello_username.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/nmarsollier/go_router_design/model/hello/service" 8 | "github.com/nmarsollier/go_router_design/utils/errors" 9 | ) 10 | 11 | // Internal configure ping/pong service 12 | func init() { 13 | router().GET( 14 | "/hello/:userName", 15 | validateUserName, 16 | sayHelloHandler, 17 | ) 18 | } 19 | 20 | // validamos que el parametro userName tenga al menos 5 caracteres 21 | func validateUserName(c *gin.Context) { 22 | userName := c.Param("userName") 23 | 24 | if len(userName) < 5 { 25 | c.Error(errors.NewCustomError(400, "userName debe tener al menos 5 caracteres")) 26 | c.Abort() 27 | return 28 | } 29 | } 30 | 31 | func sayHelloHandler(c *gin.Context) { 32 | userName := c.Param("userName") 33 | 34 | c.JSON(http.StatusOK, gin.H{ 35 | "answer": service.SayHello(userName), 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /go_router_design/rest/routes/get_hello_username_test.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/nmarsollier/go_router_design/utils/test" 9 | "gopkg.in/go-playground/assert.v1" 10 | ) 11 | 12 | func TestValidateUserName(t *testing.T) { 13 | response := test.ResponseWriter(t) 14 | context, _ := gin.CreateTestContext(response) 15 | context.Request, _ = http.NewRequest("GET", "/hello/abc", nil) 16 | 17 | validateUserName(context) 18 | 19 | response.Assert(0, "") 20 | assert.Equal(t, context.Errors.Last().Error(), "userName debe tener al menos 5 caracteres") 21 | } 22 | -------------------------------------------------------------------------------- /go_router_design/rest/routes/get_ping.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // Internal configure ping/pong service 10 | func init() { 11 | router().GET("/ping", pingHandler) 12 | } 13 | 14 | func pingHandler(c *gin.Context) { 15 | c.JSON(http.StatusOK, gin.H{ 16 | "answer": "pong", 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /go_router_design/rest/routes/rest.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/nmarsollier/go_router_design/rest/middlewares" 6 | ) 7 | 8 | // Start server in 8080 port 9 | func Start() { 10 | router().Run(":8080") 11 | } 12 | 13 | var engine *gin.Engine = nil 14 | 15 | func router() *gin.Engine { 16 | if engine == nil { 17 | engine = gin.Default() 18 | engine.Use(middlewares.ErrorHandler) 19 | } 20 | 21 | return engine 22 | } 23 | -------------------------------------------------------------------------------- /go_router_design/utils/errors/custom_error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | // NewCustomError creates a new custom error 4 | func NewCustomError(status int, message string) *CustomError { 5 | return &CustomError{ 6 | code: status, 7 | message: message, 8 | } 9 | } 10 | 11 | // CustomError es una interfaz para definir errores custom 12 | type CustomError struct { 13 | code int 14 | message string 15 | } 16 | 17 | // Code http error code 18 | func (e *CustomError) Code() int { 19 | return e.code 20 | } 21 | 22 | // Message http error message 23 | func (e *CustomError) Error() string { 24 | return e.message 25 | } 26 | -------------------------------------------------------------------------------- /go_router_design/utils/test/response_writer.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func ResponseWriter(t *testing.T) *FakeResponseWriter { 9 | return &FakeResponseWriter{ 10 | t: t, 11 | headers: make(http.Header), 12 | } 13 | } 14 | 15 | type FakeResponseWriter struct { 16 | t *testing.T 17 | headers http.Header 18 | body []byte 19 | status int 20 | } 21 | 22 | func (r *FakeResponseWriter) Header() http.Header { 23 | return r.headers 24 | } 25 | 26 | func (r *FakeResponseWriter) Write(body []byte) (int, error) { 27 | r.body = body 28 | return len(body), nil 29 | } 30 | 31 | func (r *FakeResponseWriter) WriteHeader(status int) { 32 | r.status = status 33 | } 34 | 35 | func (r *FakeResponseWriter) Assert(status int, body string) { 36 | if r.status != status { 37 | r.t.Errorf("expected status %+v to equal %+v", r.status, status) 38 | } 39 | if string(r.body) != body { 40 | r.t.Errorf("expected body %#v to equal %#v", string(r.body), body) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /go_router_design/utils/test/response_writer_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func TestResponseWriter(t *testing.T) { 10 | response := ResponseWriter(t) 11 | context, _ := gin.CreateTestContext(response) 12 | context.JSON(500, gin.H{"error": "Internal server error"}) 13 | response.Assert(500, "{\"error\":\"Internal server error\"}") 14 | } 15 | --------------------------------------------------------------------------------