An Interest In:
Web News this Week
- April 20, 2024
- April 19, 2024
- April 18, 2024
- April 17, 2024
- April 16, 2024
- April 15, 2024
- April 14, 2024
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.
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
- Endpoints
- Package Structure
- How to Cross the Border of Those Layers
- Dependency Injection
- Testing
- Naming Convention
- With Gochk
- With PostgreSQL
- Feedbacks
- License
- Author
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
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
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
- NOTICE: Please run postgres container first with this step.
GET /parameter
GET /order
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
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 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:
- Define Interface
- Take Argument as Interface and Call Functions of It
- Implement It
- 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
- Interface at Domain Layer:
package repositoryimport "github.com/resotto/goilerplate/internal/app/domain"// IParameter is interface of parameter repositorytype IParameter interface { Get() domain.Parameter}
- 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()}
- 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(¶m, 1) if result.Error != nil { panic(result.Error) } return domain.Parameter{ Funds: param.Funds, Btc: param.Btc, }}
- 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 usecasefunc Parameter(r repository.IParameter) domain.Parameter { // Take Argument as Interface return r.Get()}
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)}
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)}
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)}
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)}
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]
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.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) } }) }}
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.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", })}
// 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) } }) }}
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:
For package layout, please check:
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
And then, its result is:
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/parameteropen 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 builddocker 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 postgresEXPOSE 5432COPY ./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
Original Link: https://dev.to/resotto/clean-boilerplate-of-go-domain-driven-design-clean-architecture-gin-and-gorm-2825
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To