Giter VIP home page Giter VIP logo

goilerplate's Introduction

Goilerplate

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


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 minutes
cd ${GOPATH}/src/github.com/resotto/goilerplate
go run cmd/app/main.go                          # from root directory
open http://0.0.0.0:8080

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/goilerplate
Cloning into '/Users/resotto/go/src/github.com/resotto/goilerplate'...
fatal: could not read Username for 'https://github.com': terminal prompts disabled
package github.com/resotto/goilerplate: exit status 128

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

git config --global [email protected]:.insteadOf https://github.com/

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

#fffacd Domain Layer

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

#f08080 Application Layer

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

#98fb98 Adapter Layer

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

#87cefa 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.

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
  1. Interface at Domain Layer:
package repository

import "github.com/resotto/goilerplate/internal/app/domain"

// IParameter is interface of parameter repository
type IParameter interface {
	Get() domain.Parameter
}
  1. Usecase at Application Layer:
package usecase

// NOTICE: This usecase DON'T depend on Adapter layer
import (
	"github.com/resotto/goilerplate/internal/app/domain"
	"github.com/resotto/goilerplate/internal/app/domain/repository"
)

// Parameter is the usecase of getting parameter
func Parameter(r repository.IParameter) domain.Parameter {
	return r.Get()
}
  1. Implementation at Adapter Layer:
package repository

// Parameter is the repository of domain.Parameter
type Parameter struct{}

// Get gets parameter
func (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,
	}
}
  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)
}

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 usecase

func Parameter(r repository.IParameter) domain.Parameter { // Take Argument as Interface
	return r.Get()
}

Second, initialize implementation and give it to the usecase.

package adapter

var (
	parameterRepository = repository.Parameter{}        // Initialize Implementation
)

func (ctrl Controller) parameter(c *gin.Context) {
	parameter := usecase.Parameter(parameterRepository) // Inject Implementation to Usecase
	c.JSON(200, parameter)
}

With Keyword Arguments

First, define argument struct and usecase taking it.

package usecase

// OhlcArgs are arguments of Ohlc usecase
type 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)
}

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

package adapter

var (
	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)
}

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 adapter

var (
	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)
}

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.204s
ok      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]

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

Entity

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

.
└── internal
    └── app
        └── domain
            ├── parameter.go      # Target Entity
            └── parameter_test.go # Test
// parameter_test.go
package domain_test

import (
	"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)
			}
		})
	}
}

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
// exchange_mock.go
package testdata

import "github.com/resotto/goilerplate/internal/app/domain/valueobject"

// MExchange is mock of service.IExchange
type 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",
	})
}
// ticker_test.go
package usecase_test

import (
	"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)
			}
		})
	}
}

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: test

on:
  push:
    branches:
      - master
    paths-ignore:
      - "**/*.md"
  pull_request:
    branches:
      - master

jobs:
  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

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

Then, let's check it out:

open http://0.0.0.0:8080/parameter
open http://0.0.0.0:8080/order

Building Image

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

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

Docker Image

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

FROM postgres

EXPOSE 5432

COPY ./init.sql /docker-entrypoint-initdb.d/
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');

Feedbacks

Feel free to write your thoughts

License

GNU General Public License v3.0.

Author

Resotto

goilerplate's People

Contributors

resotto avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.