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 Goilerplate via SSH
- Endpoints
- Package Structure
- How to cross the border of those layers
- Testing
- Naming Convention
- With PostgreSQL
- Feedbacks
- License
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
GET /parameter
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
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.
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:
- Define Interface
- Take Argument as Interface and Call Functions of It
- Implement It
- 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
- Interface at Domain Layer:
package repository
// IParameter is interface of parameter repository
type IParameter interface {
Get() domain.Parameter
Save(domain.Parameter)
}
- Usecase at Application Layer:
package usecase
// Parameter is the usecase of getting parameter
func Parameter(r repository.IParameter) domain.Parameter {
return r.Get()
}
- 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(¶m, 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
}
- 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
likeIExchange
.- NOTICE: If you can distinguish interface from implementation, any naming convention will be acceptable.
Mock
- Add prefix
M
likeMExchange
.- 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
likeparameter_test.go
. - For mock, add suffix
_mock
likeexchange_mock.go
.
Package
- For package name, please check following posts:
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