Giter VIP home page Giter VIP logo

goilerplate's Introduction

Goilerplate

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


Why Goilerplate?

  • You can focus more on your application logic.
  • Rocket start guide of Go, Domain-Driven Design, Clean Architecture, Gin, and GORM.

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.

Requirements


Table of Contents

Getting Started

go get -u gorm.io/gorm                          # please go get gorm first
go get -u github.com/resotto/goilerplate        # might take few minutes
cd ${GOPATH}/src/github.com/resotto/goilerplate
go run main.go                                  # from root directory
open http://0.0.0.0:8080

go get Goilerplate via SSH

go get 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("cmd/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

.
├── cmd
│   └── app
│       ├── adapter
│       │   ├── controller.go        # Controller
│       │   ├── postgresql           # Database
│       │   │   ├── conn.go
│       │   │   └── model            # Database Model
│       │   │       └── parameter.go
│       │   ├── repository           # Repository Implementation
│       │   │   └── parameter.go
│       │   ├── service              # Application Service Implementation
│       │   │   └── bitbank.go
│       │   └── view                 # Templates
│       │       └── index.tmpl
│       ├── application
│       │   ├── service              # Application Service Interface
│       │   │   └── exchange.go
│       │   └── usecase              # Usecase
│       │       ├── ohlc.go
│       │       ├── parameter.go
│       │       └── ticker.go
│       └── domain
│           ├── parameter.go         # Entity
│           ├── repository           # Repository Interface
│           │   └── parameter.go
│           └── valueobject          # ValueObject
│               ├── candlestick.go
│               ├── pair.go
│               ├── ticker.go
│               └── timeunit.go
└── main.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 one main rule:

  • Anything in the inner layer CANNOT know what exists in the outer layers.
    • which means the direction of dependency is inward.

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

Therefore, please follow next four tasks:

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

Here, I pick up example of Repository whose import statements are omitted.

Repository

.
├── 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

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

// 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,
	}
}

// Save saves parameter
func (r Parameter) Save(p domain.Parameter) {
	// TODO
}
  1. Dependency Injection at Controller of Adapter Layer:
package adapter

func (ctrl Controller) parameter(c *gin.Context) {
	repository := repository.Parameter{}
	parameter := usecase.Parameter(repository) // Dependency Injection
	c.JSON(200, parameter)
}

Implementation of Application Service is also the same.

Testing

There are two rules:

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

Entity

Please write test in the same directory as the entity.

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

import (
	"testing"

	"github.com/resotto/goilerplate/cmd/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 and write test in the same directory as the usecase.

.
└── cmd
    └── 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/cmd/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/cmd/app/application/usecase"
	"github.com/resotto/goilerplate/cmd/app/domain/valueobject"
	"github.com/resotto/goilerplate/cmd/app/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 PostgreSQL

First, you pull docker image 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

Docker Image

The image you pulled from GitHub Container Registry is built from 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);

Feedbacks

Feel free to write your thoughts

License

GNU General Public License v3.0.

goilerplate's People

Contributors

resotto 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.