Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
November 8, 2020 12:01 am GMT

Clean Boilerplate of Go, Domain-Driven Design, Clean Architecture, Gin and GORM.

GitHub Repo: https://github.com/resotto/goilerplate

Goilerplate

Clean Boilerplate of Go, Domain-Driven Design, Clean Architecture, Gin and GORM.

Goilerplate Logo

What is Goilerplate?

  • Good example of Go with Clean Architecture.
  • Rocket start guide of Go, Domain-Driven Design, Clean Architecture, Gin, and GORM.

Who is the main user of Goilerplate?

  • All kinds of Gophers (newbie to professional).

Why Goilerplate?

  • Easy-applicable boilerplate in Go.

Note

  • Default application/test code is trivial because you will write cool logic.
  • Public API of bitbank, which is bitcoin exchange located in Tokyo, is used for some endpoints by default.

Table of Contents

Getting Started

go get -u github.com/resotto/goilerplate        # might take few minutescd ${GOPATH}/src/github.com/resotto/goilerplatego run cmd/app/main.go                          # from root directoryopen http://0.0.0.0:8080
Enter fullscreen mode Exit fullscreen mode

go get Goilerplate via SSH

go get fetches GitHub repository via HTTPS by default. So you might fail go get:

~  > go get -u github.com/resotto/goilerplate# cd .; git clone -- https://github.com/resotto/goilerplate /Users/resotto/go/src/github.com/resotto/goilerplateCloning into '/Users/resotto/go/src/github.com/resotto/goilerplate'...fatal: could not read Username for 'https://github.com': terminal prompts disabledpackage github.com/resotto/goilerplate: exit status 128
Enter fullscreen mode Exit fullscreen mode

If you go get GitHub repository via SSH, please run following command:

git config --global [email protected]:.insteadOf https://github.com/
Enter fullscreen mode Exit fullscreen mode

And then, please try Getting Started again.

Endpoints

  • With Template

    • GET /
    • NOTICE: Following path is from CURRENT directory, so please run Gin from root directory.
      r.LoadHTMLGlob("internal/app/adapter/view/*")
  • With Public API of bitbank

    • GET /ticker
    • GET /candlestick
    • NOTICE: This works from 0AM ~ 3PM (UTC) due to its API constraints.
  • With PostgreSQL

Package Structure

. LICENSE README.md build                                     # Packaging and Continuous Integration  Dockerfile  init.sql cmd                                       # Main Application  app      main.go internal                                  # Private Codes  app      adapter       controller.go                 # Controller       postgresql                    # Database        conn.go        model                     # Database Model            card.go            cardBrand.go            order.go            parameter.go            payment.go            person.go       repository                    # Repository Implementation        order.go        parameter.go       service                       # Application Service Implementation        bitbank.go       view                          # Templates           index.tmpl      application       service                       # Application Service Interface        exchange.go       usecase                       # Usecase           addNewCardAndEatCheese.go           ohlc.go           parameter.go           ticker.go           ticker_test.go      domain          factory                       # Factory           order.go          order.go                      # Entity          parameter.go          parameter_test.go          person.go          repository                    # Repository Interface           order.go           parameter.go          valueobject                   # ValueObject              candlestick.go              card.go              cardbrand.go              pair.go              payment.go              ticker.go              timeunit.go testdata                                  # Test Data     exchange_mock.go
Enter fullscreen mode Exit fullscreen mode

Domain Layer

  • The core of Clean Architecture. It says "Entities".

Application Layer

  • The second layer from the core. It says "Use Cases".

Adapter Layer

  • The third layer from the core. It says "Controllers / Gateways / Presenters".

External Layer

  • The fourth layer from the core. It says "Devices / DB / External Interfaces / UI / Web".
    • We DON'T write much codes in this layer.

Clean Architecture

The Clean Architecture

How to Cross the Border of Those Layers

In Clean Architecture, there is The Dependency Rule:

This rule says that source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle.

In other words, Dependency Injection is required to follow this rule.

Therefore, please follow the next four steps:

  1. Define Interface
  2. Take Argument as Interface and Call Functions of It
  3. Implement It
  4. Inject Dependency

Here, I pick up the example of Repository.

Repository

. internal   app       adapter        controller.go    # 4. Dependency Injection        repository          parameter.go # 3. Implementation       application        usecase            parameter.go # 2. Interface Function Call       domain           parameter.go           repository             parameter.go # 1. Interface
Enter fullscreen mode Exit fullscreen mode
  1. Interface at Domain Layer:
package repositoryimport "github.com/resotto/goilerplate/internal/app/domain"// IParameter is interface of parameter repositorytype IParameter interface {    Get() domain.Parameter}
Enter fullscreen mode Exit fullscreen mode
  1. Usecase at Application Layer:
package usecase// NOTICE: This usecase DON'T depend on Adapter layerimport (    "github.com/resotto/goilerplate/internal/app/domain"    "github.com/resotto/goilerplate/internal/app/domain/repository")// Parameter is the usecase of getting parameterfunc Parameter(r repository.IParameter) domain.Parameter {    return r.Get()}
Enter fullscreen mode Exit fullscreen mode
  1. Implementation at Adapter Layer:
package repository// Parameter is the repository of domain.Parametertype Parameter struct{}// Get gets parameterfunc (r Parameter) Get() domain.Parameter {    db := postgresql.Connection()    var param model.Parameter    result := db.First(&param, 1)    if result.Error != nil {        panic(result.Error)    }    return domain.Parameter{        Funds: param.Funds,        Btc:   param.Btc,    }}
Enter fullscreen mode Exit fullscreen mode
  1. Dependency Injection at Controller of Adapter Layer:
package adapter// NOTICE: Controller depends on INNER CIRCLE so it points inward (The Dependency Rule)import (    "github.com/gin-gonic/gin"    "github.com/resotto/goilerplate/internal/app/adapter/repository"    "github.com/resotto/goilerplate/internal/app/application/usecase")var (    parameterRepository = repository.Parameter{})func (ctrl Controller) parameter(c *gin.Context) {    parameter := usecase.Parameter(parameterRepository) // Dependency Injection    c.JSON(200, parameter)}
Enter fullscreen mode Exit fullscreen mode

Implementation of Application Service is also the same.

Dependency Injection

In Goilerplate, dependencies are injected manually.

  • NOTICE: If other DI tool in Go doesn't become some kind of application framework, it will also be acceptable.

There are two ways of passing dependencies:

  • with positional arguments
  • with keyword arguments

With Positional Arguments

First, define usecase with arguments of interface type.

package usecasefunc Parameter(r repository.IParameter) domain.Parameter { // Take Argument as Interface    return r.Get()}
Enter fullscreen mode Exit fullscreen mode

Second, initialize implementation and give it to the usecase.

package adaptervar (    parameterRepository = repository.Parameter{}        // Initialize Implementation)func (ctrl Controller) parameter(c *gin.Context) {    parameter := usecase.Parameter(parameterRepository) // Inject Implementation to Usecase    c.JSON(200, parameter)}
Enter fullscreen mode Exit fullscreen mode

With Keyword Arguments

First, define argument struct and usecase taking it.

package usecase// OhlcArgs are arguments of Ohlc usecasetype OhlcArgs struct {    E service.IExchange                       // Interface    P valueobject.Pair    T valueobject.Timeunit}func Ohlc(a OhlcArgs) []valueobject.CandleStick { // Take Argument as OhlcArgs    return a.E.Ohlc(a.P, a.T)}
Enter fullscreen mode Exit fullscreen mode

And then, initialize the struct with keyword arguments and give it to the usecase.

package adaptervar (    bitbank             = service.Bitbank{}      // Implementation)func (ctrl Controller) candlestick(c *gin.Context) {    args := usecase.OhlcArgs{                    // Initialize Struct with Keyword Arguments        E: bitbank,                          // Passing the implementation        P: valueobject.BtcJpy,        T: valueobject.OneMin,    }    candlestick := usecase.Ohlc(args)            // Give Arguments to Usecase    c.JSON(200, candlestick)}
Enter fullscreen mode Exit fullscreen mode

Global Injecter Variable

In manual DI, implementation initialization cost will be expensive.

So, let's use global injecter variable in order to initialize them only once.

package adaptervar (    bitbank             = service.Bitbank{}      // Injecter Variable    parameterRepository = repository.Parameter{}    orderRepository     = repository.Order{})func (ctrl Controller) ticker(c *gin.Context) {    pair := valueobject.BtcJpy    ticker := usecase.Ticker(bitbank, pair)      // DI by passing bitbank    c.JSON(200, ticker)}
Enter fullscreen mode Exit fullscreen mode

Testing

~/go/src/github.com/resotto/goilerplate (master) > go test ./internal/app/...?       github.com/resotto/goilerplate/internal/app/adapter     [no test files]?       github.com/resotto/goilerplate/internal/app/adapter/postgresql  [no test files]?       github.com/resotto/goilerplate/internal/app/adapter/postgresql/model    [no test files]?       github.com/resotto/goilerplate/internal/app/adapter/repository  [no test files]?       github.com/resotto/goilerplate/internal/app/adapter/service     [no test files]?       github.com/resotto/goilerplate/internal/app/application/service [no test files]ok      github.com/resotto/goilerplate/internal/app/application/usecase 0.204sok      github.com/resotto/goilerplate/internal/app/domain      0.273s?       github.com/resotto/goilerplate/internal/app/domain/factory      [no test files]?       github.com/resotto/goilerplate/internal/app/domain/repository   [no test files]?       github.com/resotto/goilerplate/internal/app/domain/valueobject  [no test files]
Enter fullscreen mode Exit fullscreen mode

There are two rules:

  • Name of the package where test code included is xxx_test.
  • Place mocks on testdata package.

Test Package Structure

. internal  app      application       usecase           ticker.go      # Usecase           ticker_test.go # Usecase Test      domain          parameter.go       # Entity          parameter_test.go  # Entity Test testdata     exchange_mock.go           # Mock if needed
Enter fullscreen mode Exit fullscreen mode

Entity

Please write tests in the same directory as where the entity located.

. internal   app       domain         parameter.go      # Target Entity         parameter_test.go # Test
Enter fullscreen mode Exit fullscreen mode
// parameter_test.gopackage domain_testimport (    "testing"    "github.com/resotto/goilerplate/internal/app/domain")func TestParameter(t *testing.T) {    tests := []struct {        name                       string        funds, btc                 int        expectedfunds, expectedbtc int    }{        {"more funds than btc", 1000, 0, 1000, 0},        {"same amount", 100, 100, 100, 100},        {"much more funds than btc", 100000, 20, 100000, 20},    }    for _, tt := range tests {        tt := tt        t.Run(tt.name, func(t *testing.T) {            t.Parallel()            parameter := domain.Parameter{                Funds: tt.funds,                Btc:   tt.btc,            }            if parameter.Funds != tt.expectedfunds {                t.Errorf("got %q, want %q", parameter.Funds, tt.expectedfunds)            }            if parameter.Btc != tt.expectedbtc {                t.Errorf("got %q, want %q", parameter.Btc, tt.expectedbtc)            }        })    }}
Enter fullscreen mode Exit fullscreen mode

Usecase

Please prepare mock on testdata package (if needed) and write tests in the same directory as the usecase.

. internal  app      application        service         exchange.go    # Application Service Interface        usecase            ticker.go      # Target Usecase            ticker_test.go # Test testdata     exchange_mock.go           # Mock of Application Service Interface
Enter fullscreen mode Exit fullscreen mode
// exchange_mock.gopackage testdataimport "github.com/resotto/goilerplate/internal/app/domain/valueobject"// MExchange is mock of service.IExchangetype MExchange struct{}// Ticker is mock implementation of service.IExchange.Ticker()func (e MExchange) Ticker(p valueobject.Pair) valueobject.Ticker {    return valueobject.Ticker{        Sell:      "1000",        Buy:       "1000",        High:      "2000",        Low:       "500",        Last:      "1200",        Vol:       "20",        Timestamp: "1600769562",    }}// Ohlc is mock implementation of service.IExchange.Ohlc()func (e MExchange) Ohlc(p valueobject.Pair, t valueobject.Timeunit) []valueobject.CandleStick {    cs := make([]valueobject.CandleStick, 0)    return append(cs, valueobject.CandleStick{        Open:      "1000",        High:      "2000",        Low:       "500",        Close:     "1500",        Volume:    "30",        Timestamp: "1600769562",    })}
Enter fullscreen mode Exit fullscreen mode
// ticker_test.gopackage usecase_testimport (    "testing"    "github.com/resotto/goilerplate/internal/app/application/usecase"    "github.com/resotto/goilerplate/internal/app/domain/valueobject"    "github.com/resotto/goilerplate/testdata")func TestTicker(t *testing.T) {    tests := []struct {        name              string        pair              valueobject.Pair        expectedsell      string        expectedbuy       string        expectedhigh      string        expectedlow       string        expectedlast      string        expectedvol       string        expectedtimestamp string    }{        {"btcjpy", valueobject.BtcJpy, "1000", "1000", "2000", "500", "1200", "20", "1600769562"},    }    for _, tt := range tests {        tt := tt        t.Run(tt.name, func(t *testing.T) {            t.Parallel()            mexchange := testdata.MExchange{} // using Mock            result := usecase.Ticker(mexchange, tt.pair)            if result.Sell != tt.expectedsell {                t.Errorf("got %q, want %q", result.Sell, tt.expectedsell)            }            if result.Buy != tt.expectedbuy {                t.Errorf("got %q, want %q", result.Buy, tt.expectedbuy)            }            if result.High != tt.expectedhigh {                t.Errorf("got %q, want %q", result.High, tt.expectedhigh)            }            if result.Low != tt.expectedlow {                t.Errorf("got %q, want %q", result.Low, tt.expectedlow)            }            if result.Last != tt.expectedlast {                t.Errorf("got %q, want %q", result.Last, tt.expectedlast)            }            if result.Vol != tt.expectedvol {                t.Errorf("got %q, want %q", result.Vol, tt.expectedvol)            }            if result.Timestamp != tt.expectedtimestamp {                t.Errorf("got %q, want %q", result.Timestamp, tt.expectedtimestamp)            }        })    }}
Enter fullscreen mode Exit fullscreen mode

Naming Convention

Interface

  • Add prefix I like IExchange.
    • NOTICE: If you can distinguish interface from implementation, any naming convention will be acceptable.

Mock

  • Add prefix M like MExchange.
    • NOTICE: If you can distinguish mock from production, any naming convention will be acceptable.

File

  • File names can be duplicated.
  • For test, add suffix _test like parameter_test.go.
  • For mock, add suffix _mock like exchange_mock.go.

Package

With Gochk

Gochk, static dependency analysis tool for go files, empowers Goilerplate so much!

Gochk confirms that codebase follows Clean Architecture The Dependency Rule.

Let's merge Gochk into CI process.

name: teston:  push:    branches:      - master    paths-ignore:      - "**/*.md"  pull_request:    branches:      - masterjobs:  gochk-goilerplate:    runs-on: ubuntu-latest    container:      image: docker://ghcr.io/resotto/gochk:latest    steps:      - name: Clone Goilerplate        uses: actions/checkout@v2        with:          repository: {{ github.repository }}      - name: Run Gochk        run: |          /go/bin/gochk -c=/go/src/github.com/resotto/gochk/configs/config.json
Enter fullscreen mode Exit fullscreen mode

And then, its result is:

Gochk Result in GitHub Actions

With PostgreSQL

First, you pull the docker image ghcr.io/resotto/goilerplate-pg from GitHub Container Registry and run container with following command:

docker run -d -it --name pg -p 5432:5432 -e POSTGRES_PASSWORD=postgres ghcr.io/resotto/goilerplate-pg:latest
Enter fullscreen mode Exit fullscreen mode

Then, let's check it out:

open http://0.0.0.0:8080/parameteropen http://0.0.0.0:8080/order
Enter fullscreen mode Exit fullscreen mode

Building Image

If you fail pulling image from GitHub Container Registry, you also can build Docker image from Dockerfile.

cd builddocker build -t goilerplate-pg:latest .docker run -d -it --name pg -p 5432:5432 -e POSTGRES_PASSWORD=postgres goilerplate-pg:latest
Enter fullscreen mode Exit fullscreen mode

Docker Image

The image you pulled from GitHub Container Registry is built from the simple Dockerfile and init.sql.

FROM postgresEXPOSE 5432COPY ./init.sql /docker-entrypoint-initdb.d/
Enter fullscreen mode Exit fullscreen mode
create table parameters (    id integer primary key,    funds integer,    btc integer);insert into parameters values (1, 10000, 10);create table persons (    person_id uuid primary key,    name text not null,    weight integer);create table card_brands (    brand text primary key);create table cards (    card_id uuid primary key,    brand text references card_brands(brand) on update cascade);create table orders (    order_id uuid primary key,    person_id uuid references persons(person_id));create table payments (    order_id uuid primary key references orders(order_id),    card_id uuid references cards(card_id));insert into persons values ('f3bf75a9-ea4c-4f57-9161-cfa8f96e2d0b', 'Jerry', 1);insert into card_brands values ('VISA'), ('AMEX');insert into cards values ('3224ebc0-0a6e-4e22-9ce8-c6564a1bb6a1', 'VISA');insert into orders values ('722b694c-984c-4208-bddd-796553cf83e1', 'f3bf75a9-ea4c-4f57-9161-cfa8f96e2d0b');insert into payments values ('722b694c-984c-4208-bddd-796553cf83e1', '3224ebc0-0a6e-4e22-9ce8-c6564a1bb6a1');
Enter fullscreen mode Exit fullscreen mode

Feedbacks

Feel free to write your thoughts

License

GNU General Public License v3.0.

Author

Resotto




Original Link: https://dev.to/resotto/clean-boilerplate-of-go-domain-driven-design-clean-architecture-gin-and-gorm-2825

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To