Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/resotto/goilerplate
Clean Boilerplate of Go, Domain-Driven Design, Clean Architecture, Gin and GORM.
https://github.com/resotto/goilerplate
clean-architecture domain-driven-design gin go golang gorm
Last synced: 7 days ago
JSON representation
Clean Boilerplate of Go, Domain-Driven Design, Clean Architecture, Gin and GORM.
- Host: GitHub
- URL: https://github.com/resotto/goilerplate
- Owner: resotto
- License: gpl-3.0
- Created: 2020-09-21T15:30:48.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2023-05-05T02:28:51.000Z (over 1 year ago)
- Last Synced: 2025-01-05T15:22:52.696Z (14 days ago)
- Topics: clean-architecture, domain-driven-design, gin, go, golang, gorm
- Language: Go
- Homepage:
- Size: 80.1 KB
- Stars: 450
- Watchers: 15
- Forks: 51
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
- awesome-boilerplate - Goilerplate - Driven Design, Clean Architecture, Gin and GORM. | (GO)
README
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](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html), [Gin](https://github.com/gin-gonic/gin), and [GORM](https://github.com/go-gorm/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](https://github.com/bitbankinc/bitbank-api-docs/blob/master/public-api.md#general-endpoints), which is bitcoin exchange located in Tokyo, is used for some endpoints by default.---
## Table of Contents
- [Getting Started](#getting-started)
- [go get Goilerplate via SSH](#go-get-goilerplate-via-ssh)
- [Endpoints](#endpoints)
- [Package Structure](#package-structure)
- [How to Cross the Border of Those Layers](#how-to-cross-the-border-of-those-layers)
- [Dependency Injection](#dependency-injection)
- [How to Start with Goilerplate](#how-to-start-with-goilerplate)
- [Testing](#testing)
- [Naming Convention](#naming-convention)
- [With Gochk](#with-gochk)
- [With PostgreSQL](#with-postgresql)
- [Feedbacks](#feedbacks)
- [License](#license)
- [Author](#author)## Getting Started
```zsh
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`:
```zsh
~ > 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:
```zsh
git config --global [email protected]:.insteadOf https://github.com/
```And then, please try [Getting Started](#getting-started) again.
## Endpoints
- With Template
- `GET /`
- NOTICE: Following path is from CURRENT directory, so please run Gin from root directory.
```go
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
- [NOTICE: Please run postgres container first with this step.](#with-postgresql)
- `GET /parameter`
- `GET /order`## Package Structure
```zsh
.
├── 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](https://via.placeholder.com/15/fffacd/000000?text=+) Domain Layer
- The core of Clean Architecture. It says "Entities".
### ![#f08080](https://via.placeholder.com/15/f08080/000000?text=+) Application Layer
- The second layer from the core. It says "Use Cases".
### ![#98fb98](https://via.placeholder.com/15/98fb98/000000?text=+) Adapter Layer
- The third layer from the core. It says "Controllers / Gateways / Presenters".
### ![#87cefa](https://via.placeholder.com/15/87cefa/000000?text=+) 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 [The Dependency Rule](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html#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
1. Take Argument as Interface and Call Functions of It
1. Implement It
1. Inject DependencyHere, I pick up the example of Repository.
### Repository
```zsh
.
└── 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:
```go
package repositoryimport "github.com/resotto/goilerplate/internal/app/domain"
// IParameter is interface of parameter repository
type IParameter interface {
Get() domain.Parameter
}
```2. Usecase at Application Layer:
```go
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()
}
```3. Implementation at Adapter Layer:
```go
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,
}
}
```4. Dependency Injection at Controller of Adapter Layer:
```go
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.
```go
package usecasefunc Parameter(r repository.IParameter) domain.Parameter { // Take Argument as Interface
return r.Get()
}
```Second, initialize implementation and give it to the usecase.
```go
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)
}
```### With Keyword Arguments
First, define argument struct and usecase taking it.
```go
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.
```go
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)
}
```### 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.```go
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)
}
```## How to start with Goilerplate
With Goilerplate, you can start your project smoothly.
For explanation, let's create simple "CR" part of CRUD of following specifications with Goilerplate.
Specifications:
- There are three entities such as Customer, Product, and Order.
- Order aggregates Customer and Product (Order is Aggregate Root).
- There is only one usecase to create an order.NOTICE:
- For convenience, the minimum codes are shown here.
- For convenience, there are no test codes in this explanation.First of all, please prepare .go files with following package layout.
### Package Layout
```zsh
.
└── internal
└── app
├── adapter
│ ├── controller.go # Controller
│ └── repository # Repository Implementation
│ ├── customer.go
│ ├── product.go
│ └── order.go
├── application
│ └── usecase # Usecase
│ └── createOrder.go
└── domain
├── customer.go # Entity
├── product.go # Entity
├── order.go # Entity
└── repository # Repository Interface
├── customer.go
├── product.go
└── order.go
```### Define Entities
Secondly, let's create entities, Customer, Product, and Order.
```go
// customer.go
package domaintype Customer struct {
ID string
Name string
}
``````go
// product.go
package domaintype Product struct {
ID string
Price int
}
``````go
// order.go
package domaintype Order struct {
ID string
Customer Customer
Product Product
}
```### Define Repository Interfaces
After defining entities, let's prepare their repositories in `domain` package.
```go
// customer.go
package repositorytype ICustomer interface {
Get(id string) domain.Customer
}
``````go
// product.go
package repositorytype IProduct interface {
Get(id string) domain.Product
}
``````go
// order.go
package repositorytype IOrder interface {
Save(order Order)
}
```### Define Usecase
And then, let's prepare the usecase of creating order.
```go
// createOrder.go
package usecaseimport (
"domain" // simplified for convenience
"domain/repository" // simplified for convenience
)type CreateOrderArgs struct {
CustomerID string
ProductID string
CustomerRepository repository.ICustomer
ProductRepository repository.IProduct
OrderRepository repository.IOrder
}func CreateOrder(args CreateOrderArgs) domain.Order {
customer := args.CustomerRepository.Get(args.CustomerID)
product := args.ProductRepository.Get(args.ProductID)
order := domain.Order{
ID: "123",
Customer: customer,
Product: product,
}
args.OrderRepository.Save(order)
return order
}
```### Define Repository Implementations
After preparing the usecase, let's implement repository interfaces in `adapter` package.
However, this part is omitted here for convenience.
```go
// order.go
package repositoryimport (
"domain" // simplified for convenience
)type Order struct{}
func (o Order) Save(order domain.Order) {
// omitted here for convenience
}
```### Define Controller
Finally, let's define controller to call the usecase of creating an order.
```go
// controller.go
package adapterimport (
"repository" // simplified for convenience
"usecase" // simplified for convenience"github.com/gin-gonic/gin"
)var (
customerRepository = repository.Customer{}
productRepository = repository.Product{}
orderRepository = repository.Order{}
)type Controller struct{}
func Router() *gin.Engine {
r := gin.Default()
ctrl := Controller{}
r.POST("/order", ctrl.createOrder)
return r
}func (ctrl Controller) createOrder(c *gin.Context) {
customerID := c.Query("customerId")
productID := c.Query("productId")
args := usecase.CreateOrderArgs{
CustomerID: customerID,
ProductID: productID,
CustomerRepository: customerRepository,
ProductRepository: productRepository,
OrderRepository: orderRepository,
}
order := usecase.CreateOrder(args)
c.JSON(200, order)
}
```That's it!
## Testing
```zsh
~/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
```zsh
.
├── 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.
```zsh
.
└── internal
└── app
└── domain
├── parameter.go # Target Entity
└── parameter_test.go # Test
``````go
// parameter_test.go
package 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)
}
})
}
}```
### Usecase
Please prepare mock on `testdata` package (if needed) and write tests in the same directory as the usecase.
```zsh
.
├── 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
``````go
// exchange_mock.go
package testdataimport "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",
})
}
``````go
// ticker_test.go
package 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)
}
})
}
}
```## 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
- For package name, please check following posts:
- [Package names](https://blog.golang.org/package-names)
- [Names](https://golang.org/doc/effective_go.html#names)- For package layout, please check:
- [Project Layout](https://github.com/golang-standards/project-layout)## With Gochk
[Gochk, static dependency analysis tool for go files,](https://github.com/resotto/gochk) empowers Goilerplate so much!
**[Gochk](https://github.com/resotto/gochk) confirms that codebase follows [Clean Architecture The Dependency Rule](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html#the-dependency-rule).**
Let's merge Gochk into CI process.
```yml
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
```And then, [its result is](https://github.com/resotto/goilerplate/runs/1367461573):
![Gochk Result in GitHub Actions](https://user-images.githubusercontent.com/19743841/98438959-6f56b680-2131-11eb-8b6e-d835e56239e0.png)
## With PostgreSQL
First, you pull the docker image `ghcr.io/resotto/goilerplate-pg` from GitHub Container Registry and run container with following command:
```zsh
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:
```zsh
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.
```zsh
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.
```Dockerfile
FROM postgresEXPOSE 5432
COPY ./init.sql /docker-entrypoint-initdb.d/
``````sql
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](https://github.com/resotto/goilerplate/issues/1)
## License
[GNU General Public License v3.0](https://github.com/resotto/goilerplate/blob/master/LICENSE).
## Author
Resotto