├── .github └── workflows │ └── go.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── api ├── api.go ├── messagectrl.go └── pingctrl.go ├── apierror └── apierror.go ├── config └── config.go ├── go.mod ├── go.sum ├── main.go ├── message ├── message.go ├── respository.go ├── service.go └── service_test.go └── restclient ├── messageapi.go └── restclient.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | 24 | - name: Get dependencies 25 | run: | 26 | go get -v -t -d ./... 27 | if [ -f Gopkg.toml ]; then 28 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 29 | dep ensure 30 | fi 31 | 32 | - name: Build 33 | run: go build -v . 34 | 35 | - name: Test 36 | run: go test -v . 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # OS X 18 | .DS_Store 19 | 20 | # Visual Studio code hidden files 21 | .vscode -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | hdlopez@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Horacio López 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Go](https://github.com/hdlopez/clean-architecture-golang/workflows/Go/badge.svg) 2 | 3 | # Clean Architecture in Go 4 | 5 | This is my humble version of how to structure a GO application. 6 | 7 | Link to [my article](https://dev.to/hdlopez/como-estructurar-tu-aplicacion-en-go-4hk2) (currently only in spanish) about "How to structure a Go Application". 8 | 9 | In order to demonstrate my idea with a simple example application, I created this API which handles messages. 10 | 11 | ## Frameworks 12 | 13 | The example uses the following frameworks: 14 | 15 | * [Gin-Gonic](https://github.com/gin-gonic/gin) to make the API layer 16 | * [Resty](https://github.com/go-resty/resty) to make API calls 17 | 18 | ## Run 19 | 20 | Clone the repository and execute the following command on the root folder 21 | 22 | ``` bash 23 | go run main.go 24 | ``` 25 | 26 | ## Test 27 | 28 | ``` bash 29 | go test ./... 30 | ``` 31 | 32 | ## Backlog 33 | 34 | > This is a work in progress. There's still a lot of work to do. PRs are welcome :) 35 | * Add (a LOT) of unit tests 36 | * Use `Context` type to pass parameters across different layers 37 | * Add an example of database handler module 38 | 39 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | // API type 8 | type API struct { 9 | pingCtrl *pingCtrl 10 | msgCtrl MessageController 11 | } 12 | 13 | // New creates a new instance of API type 14 | func New(msgCtrl MessageController) *API { 15 | api := new(API) 16 | api.msgCtrl = msgCtrl 17 | api.pingCtrl = newPingCtrl() 18 | return api 19 | } 20 | 21 | // Ctx interface is an abstraction of gin.Context. 22 | // This interface makes mocking on unit testing easier 23 | type Ctx interface { 24 | Param(key string) string 25 | 26 | String(code int, format string, values ...interface{}) 27 | JSON(code int, obj interface{}) 28 | } 29 | 30 | // Run function runs our API application 31 | func (api *API) Run() { 32 | r := gin.Default() 33 | 34 | api.configRoutes(r) 35 | 36 | r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") 37 | } 38 | 39 | // configRoutes set all the API endpoints 40 | func (api *API) configRoutes(r *gin.Engine) { 41 | r.GET("/ping", func(c *gin.Context) { api.pingCtrl.Ping(c) }) 42 | r.GET("/messages/:id", func(c *gin.Context) { api.msgCtrl.Get(c) }) 43 | } 44 | -------------------------------------------------------------------------------- /api/messagectrl.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/hdlopez/clean-architecture-golang/apierror" 7 | "github.com/hdlopez/clean-architecture-golang/message" 8 | ) 9 | 10 | type MessageController interface { 11 | Get(c Ctx) 12 | } 13 | 14 | // messageCtrl handles message entity 15 | type messageCtrl struct { 16 | srv message.Service 17 | } 18 | 19 | // NewMessageController creates a new instance of messageCtrl 20 | func NewMessageController(srv message.Service) MessageController { 21 | ctrl := new(messageCtrl) 22 | ctrl.srv = srv 23 | return ctrl 24 | } 25 | 26 | func (ctrl *messageCtrl) Get(c Ctx) { 27 | // 1 - Unmashall user parameters from gin.Context 28 | id := c.Param("id") 29 | // 2 - Check and handle validation errors (no business validation) 30 | 31 | // 3 - Make what you need with your request. In this case, get the message by ID. 32 | msg, err := ctrl.srv.Get(id) 33 | 34 | if err != nil { 35 | apiErr, ok := err.(*apierror.APIError) 36 | if ok { 37 | // in case of known API errors, build the response properly 38 | c.JSON(apiErr.Code(), apiErr) 39 | return 40 | } 41 | 42 | c.JSON(http.StatusInternalServerError, apierror.FromError(err)) 43 | return 44 | } 45 | 46 | // returns my message.Message object 47 | c.JSON(http.StatusOK, msg) 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /api/pingctrl.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type pingCtrl struct { 4 | } 5 | 6 | func newPingCtrl() *pingCtrl { 7 | return new(pingCtrl) 8 | } 9 | 10 | func (ctrl *pingCtrl) Ping(c Ctx) { 11 | c.String(200, "pong") 12 | } 13 | -------------------------------------------------------------------------------- /apierror/apierror.go: -------------------------------------------------------------------------------- 1 | package apierror 2 | 3 | // New APIError instance 4 | func New(code int, message string) *APIError { 5 | return &APIError{code, message} 6 | } 7 | 8 | func FromError(err error) *APIError { 9 | return New(500, err.Error()) 10 | } 11 | 12 | // APIError is a trivial implementation of API errors response 13 | type APIError struct { 14 | code int 15 | message string 16 | } 17 | 18 | // Error function to be compliance with error interface 19 | func (e *APIError) Error() string { 20 | return e.message 21 | } 22 | 23 | // Code funciton returns the error status code 24 | func (e *APIError) Code() int { 25 | return e.code 26 | } 27 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/go-resty/resty/v2" 5 | ) 6 | 7 | const ( 8 | MessageAPI = "MessageAPI" 9 | ) 10 | 11 | func RestClients() map[string]*resty.Client { 12 | restClients := map[string]*resty.Client{ 13 | MessageAPI: resty.New(), 14 | // Here we can add rest clients for your APIs. 15 | 16 | // You must probably need to configure timeouts 17 | // and retries strategies for all your rest clients. 18 | } 19 | 20 | return restClients 21 | } 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hdlopez/clean-architecture-golang 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.7.4 7 | github.com/go-playground/validator/v10 v10.9.0 // indirect 8 | github.com/go-resty/resty/v2 v2.6.0 9 | github.com/golang/protobuf v1.5.2 // indirect 10 | github.com/json-iterator/go v1.1.11 // indirect 11 | github.com/mattn/go-isatty v0.0.13 // indirect 12 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 13 | github.com/modern-go/reflect2 v1.0.1 // indirect 14 | github.com/ugorji/go v1.2.6 // indirect 15 | golang.org/x/text v0.3.8 // indirect 16 | google.golang.org/protobuf v1.27.1 // indirect 17 | gopkg.in/yaml.v2 v2.4.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM= 8 | github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= 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/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 12 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 13 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 14 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 15 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 16 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 17 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 18 | github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A= 19 | github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 20 | github.com/go-resty/resty/v2 v2.6.0 h1:joIR5PNLM2EFqqESUjCMGXrWmXNHEU9CEiK813oKYS4= 21 | github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q= 22 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 23 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 24 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 25 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 26 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 27 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 28 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 29 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 30 | github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= 31 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 32 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 33 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 34 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 35 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 36 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 37 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 38 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 39 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 40 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 41 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 42 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 43 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 44 | github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= 45 | github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 46 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 47 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 48 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 49 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 50 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 51 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 52 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 53 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 54 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 55 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 56 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 57 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 58 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 59 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 60 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 61 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 62 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 63 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 64 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 65 | github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E= 66 | github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= 67 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 68 | github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= 69 | github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= 70 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 71 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 72 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 73 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 74 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= 75 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 76 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 77 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 78 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 79 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 80 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 81 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= 82 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 83 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 84 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 85 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 86 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 87 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 88 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 89 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 91 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 92 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 93 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= 94 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 95 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 96 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 97 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 98 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 99 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 100 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 101 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 102 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 103 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 104 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 105 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 106 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 107 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 108 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 109 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 110 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 111 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 112 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 113 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 114 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 115 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 116 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 117 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 118 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 119 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 120 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 121 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 122 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 123 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 124 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 125 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 126 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/hdlopez/clean-architecture-golang/api" 5 | "github.com/hdlopez/clean-architecture-golang/config" 6 | "github.com/hdlopez/clean-architecture-golang/message" 7 | "github.com/hdlopez/clean-architecture-golang/restclient" 8 | ) 9 | 10 | func main() { 11 | // Dependency injection section 12 | clients := config.RestClients() 13 | 14 | msgAPI := restclient.NewMessageAPI(clients[config.MessageAPI]) 15 | 16 | msgRepo := message.NewRepository(msgAPI) 17 | 18 | msgSrv := message.Service(msgRepo) 19 | msgCtrl := api.NewMessageController(msgSrv) 20 | 21 | // Creates the API intance 22 | api := api.New(msgCtrl) 23 | 24 | // Runs the application 25 | api.Run() 26 | } 27 | -------------------------------------------------------------------------------- /message/message.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import "github.com/hdlopez/clean-architecture-golang/restclient" 4 | 5 | type Message struct { 6 | Text string `json:"text"` 7 | } 8 | 9 | func build(msg *restclient.Message) *Message { 10 | return &Message{msg.Text} 11 | } 12 | -------------------------------------------------------------------------------- /message/respository.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import "github.com/hdlopez/clean-architecture-golang/restclient" 4 | 5 | type Repository interface { 6 | Get(id string) (*Message, error) 7 | } 8 | 9 | type msgRepo struct { 10 | // It can be easly changed by a database or other storage without touching 11 | // business logic 12 | api restclient.MessageAPI 13 | } 14 | 15 | func NewRepository(api restclient.MessageAPI) Repository { 16 | repo := new(msgRepo) 17 | repo.api = api 18 | return repo 19 | } 20 | 21 | func (repo *msgRepo) Get(id string) (*Message, error) { 22 | // Getting message from the external API. 23 | msg, err := repo.api.Get(id) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return build(msg), nil 29 | } 30 | -------------------------------------------------------------------------------- /message/service.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "github.com/hdlopez/clean-architecture-golang/apierror" 5 | ) 6 | 7 | type Service interface { 8 | Get(ID string) (*Message, error) 9 | } 10 | 11 | type messageSrv struct { 12 | repo Repository 13 | } 14 | 15 | func NewService(r Repository) Service { 16 | srv := new(messageSrv) 17 | srv.repo = r 18 | return srv 19 | } 20 | 21 | func (srv *messageSrv) Get(id string) (*Message, error) { 22 | // string zero-value is an empty string 23 | 24 | // validate parameters depending your business logic 25 | if id == "" { 26 | // bad request 27 | return nil, apierror.New(400, "Message ID is required") 28 | } 29 | 30 | // business logic goes here ... 31 | 32 | // retrieve message using its repository 33 | msg, err := srv.repo.Get(id) 34 | 35 | // do some stuff 36 | 37 | return msg, err 38 | } 39 | -------------------------------------------------------------------------------- /message/service_test.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | type mockRepo struct{} 10 | 11 | func (repo *mockRepo) Get(id string) (*Message, error) { 12 | if id == "error" { 13 | return nil, errors.New("Mocked error") 14 | } 15 | return &Message{Text: "TestMessage"}, nil 16 | } 17 | 18 | func NewInmemRepository() Repository { 19 | return new(mockRepo) 20 | } 21 | 22 | func Test_messageSrv_Get(t *testing.T) { 23 | // creates in memory message repository 24 | repo := NewInmemRepository() 25 | // create new service instance in order to test 26 | srv := NewService(repo) 27 | 28 | type args struct { 29 | id string 30 | } 31 | tests := []struct { 32 | name string 33 | args args 34 | want *Message 35 | wantErr bool 36 | }{ 37 | { 38 | "test OK", 39 | args{"myMessageID"}, 40 | &Message{Text: "TestMessage"}, 41 | false, 42 | }, 43 | { 44 | "test fail", 45 | args{"error"}, 46 | nil, 47 | true, 48 | }, 49 | } 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | got, err := srv.Get(tt.args.id) 53 | if (err != nil) != tt.wantErr { 54 | t.Errorf("messageSrv.Get() error = %v, wantErr %v", err, tt.wantErr) 55 | return 56 | } 57 | 58 | if !reflect.DeepEqual(got, tt.want) { 59 | t.Errorf("messageSrv.Get() = %v, want %v", got, tt.want) 60 | } 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /restclient/messageapi.go: -------------------------------------------------------------------------------- 1 | package restclient 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/go-resty/resty/v2" 8 | ) 9 | 10 | const ( 11 | // messageAPIURL is the an example API url to get messages 12 | messageAPIURL = "http://localhost:3000/messages/%s" 13 | ) 14 | 15 | type MessageAPI interface { 16 | Get(id string) (*Message, error) 17 | } 18 | 19 | type msgAPI struct { 20 | restAPI 21 | } 22 | 23 | type Message struct { 24 | Text string `json:"text"` 25 | } 26 | 27 | func NewMessageAPI(c *resty.Client) MessageAPI { 28 | api := new(msgAPI) 29 | api.readClient = c 30 | return api 31 | } 32 | 33 | func (api *msgAPI) readURL(id string) string { 34 | return fmt.Sprintf(messageAPIURL, id) 35 | } 36 | 37 | // Get a message from our external Message API 38 | func (api *msgAPI) Get(id string) (*Message, error) { 39 | url := api.readURL(id) 40 | 41 | msg := new(Message) 42 | err := api.get(url, http.Header{}, msg) 43 | 44 | return msg, err 45 | } 46 | -------------------------------------------------------------------------------- /restclient/restclient.go: -------------------------------------------------------------------------------- 1 | package restclient 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | resty "github.com/go-resty/resty/v2" 8 | "github.com/hdlopez/clean-architecture-golang/apierror" 9 | ) 10 | 11 | type restAPI struct { 12 | readClient *resty.Client 13 | } 14 | 15 | func (api *restAPI) get(url string, h http.Header, v interface{}) error { 16 | var r *resty.Response 17 | req := api.readClient.R() 18 | req.SetError(&apierror.APIError{}) 19 | 20 | r, err := req.Get(url) 21 | 22 | if err != nil { 23 | // returns API error 24 | return err 25 | } 26 | 27 | if r.StatusCode() != 200 { 28 | // returns API error 29 | return apierror.New(r.StatusCode(), "Status code was not 200") 30 | } 31 | 32 | if err = json.Unmarshal(r.Body(), v); err != nil { 33 | // returns API error 34 | return apierror.New(500, "Unmarshal error") 35 | } 36 | return nil 37 | } 38 | --------------------------------------------------------------------------------