GitHub Repo: https://github.com/resotto/goilerplate

Goilerplate

Go的干净样板,领域驱动设计,清洁架构,Gin和GORM。


什么是Goilerplate?

谁是Goilerplate的主要用户?

  • 各种地鼠(新手到专业人士)。

为什么选择Goilerplate?

  • Go中易于应用的样板。

注意

  • 默认的应用程序/测试代码是微不足道的,因为您将编写很酷的逻辑。
  • bitbank的公共API,即位于东京的比特币交易所,默认情况下用于某些端点。

开始

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

Goilerplate via SSH

go get

默认情况下,通过 HTTPS 获取 GitHub 存储库。所以你可能会失败: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

如果您通过 SSH 运行 GitHub 存储库,请运行以下命令:go get

git config –global url.git@github.com:.insteadOf https://github.com/

然后,请再次尝试入门

入口

  • 使用模板

    • GET /
    • 注意:以下路径来自CURRENT目录,因此请从根目录运行Gin。 r.LoadHTMLGlob(“internal/app/adapter/view/*”)
  • 使用位库的公共 API

    • GET /ticker
    • GET /candlestick
    • 注意:由于其API限制,这适用于0AM ~ 3PM(UTC)。
  • 使用PostgreSQL

包结构


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

领域层

  • 清洁架构的核心。它说"实体"。

应用层

  • 来自核心的第二层。它说"用例"。

适配器层

  • 第三层来自核心。它说"控制器/网关/演示者"。

外层

  • 第四层从核心。它说"设备/ DB / 外部接口/ UI / Web"。
    • 我们在这个层中不写太多代码。

      image-20220307140549986

清洁架构

如何跨越这些图层的边界

在 Clean Architecture 中,有依赖关系规则

此规则表示源代码依赖项只能指向内部。在一个内圈里,没有任何东西可以知道关于外圈里某件事的任何事情。

换句话说,依赖注入需要遵循此规则。

因此,请遵循以下四个步骤:

  1. 定义接口
  2. 以参数为接口并调用其函数
  3. 实施它
  4. 注入依赖关系

在这里,我选取存储库的示例。

存储 库



.
└── 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. 域层接口:
package repository

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

// IParameter is interface of parameter repository
type IParameter interface {
    Get() domain.Parameter
}
  1. 应用程序层用例:
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. 适配器层的实现:
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. 适配器层控制器处的依赖关系注入:
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)
}

应用程序服务的实现也是相同的。

依赖注入

在 Goilerplate 中,依赖项是手动注入的。

  • 注意:如果Go中的其他DI工具没有成为某种应用程序框架,它也是可以接受的。

有两种传递依赖项的方法:

  • 使用位置参数
  • 使用关键字参数

使用位置参数

首先,使用接口类型的参数定义用例。

package usecase

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

其次,初始化实现并将其提供给用例。

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

使用关键字参数

首先,定义参数结构和用例。

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

然后,使用关键字参数初始化结构并将其提供给用例。

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

全局喷油器变量

在手动DI中,实现初始化成本将很昂贵。

因此,让我们使用全局注入器变量,以便仅初始化它们一次。

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

如何从Goilerplate开始

使用Goilerplate,您可以顺利地开始您的项目。

为了便于解释,让我们使用 Goilerplate 创建以下规范的 CRUD 的简单"CR"部分。

规格:

  • 有三个实体,如"客户"、“产品和订单”。
  • 订单聚合客户和产品(订单是聚合根)。
  • 只有一个用例用于创建订单。

通知:

  • 为方便起见,此处显示了最小代码。
  • 为方便起见,此说明中没有测试代码。

首先,请准备.go文件,并遵循以下软件包布局。

包装布局


└── 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

定义实体

其次,让我们创建实体、客户、产品和订单。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26


// customer.go
package domain

type Customer struct {
    ID string
    Name string
}

// product.go
package domain

type Product struct {
    ID string
    Price int
}

// order.go
package domain

type Order struct {
    ID string
    Customer Customer
    Product Product
}

定义存储库接口

定义实体后,让我们在包中准备它们的存储库。domain

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// customer.go
package repository

type ICustomer interface {
    Get(id string) domain.Customer
}

// product.go
package repository

type IProduct interface {
    Get(id string) domain.Product
}

// order.go
package repository

type IOrder interface {
    Save(order Order)
} 

定义用例

然后,让我们准备创建顺序的用例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29


// createOrder.go
package usecase

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

定义存储库实现

准备用例后,让我们在包中实现存储库接口。adapter

但是,为方便起见,此处省略了这一部分。

// order.go
package repository

import (
    "domain" // simplified for convenience
)

type Order struct{}

func (o Order) Save(order domain.Order) {
    // omitted here for convenience
}

定义控制器

最后,让我们定义控制器来调用创建订单的用例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// controller.go
package adapter

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

就是这样!

测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

~/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]

有两条规则:

  • 包含测试代码的包的名称为 。xxx_test
  • 在包装上放置模拟。testdata

测试包结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14


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

实体

请在实体所在的同一目录中编写测试。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
.
└── internal
    └── app
        └── domain
            ├── parameter.go      # Target Entity
            └── parameter_test.go # Test</code> 

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

}

用例

请在包上准备模拟(如果需要),并在与用例相同的目录中编写测试。testdata

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106


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

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

}

命名约定

接口

  • 添加前缀,如 。I``````IExchange
    • 注意:如果您可以区分接口和实现,则任何命名约定都是可以接受的。

模拟

  • 添加前缀,如 。M``````MExchange
    • 注意:如果您可以区分模拟和生产,则任何命名约定都是可以接受的。

文件

  • 文件名可以重复。
  • 对于测试,请添加后缀,如 ._test``````parameter_test.go
  • 对于模拟,请添加后缀,如 ._mock``````exchange_mock.go

与GOCHK

Gochk,用于go文件的静态依赖关系分析工具,赋予Goilerplate如此多的功能!

Gochk确认代码库遵循清洁架构依赖规则

让我们将 Gochk 合并到 CI 流程中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

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

然后,它的结果是

image-20220307140633299

使用PostgreSQL

首先,从 GitHub 容器注册表拉取 docker 映像,并使用以下命令运行容器:ghcr.io/resotto/goilerplate-pg

docker run -d -it –name pg -p 5432:5432 -e POSTGRES_PASSWORD=postgres ghcr.io/resotto/goilerplate-pg:latest

然后,让我们来看看:

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

建筑形象

如果从 GitHub 容器注册表拉取映像失败,还可以从 Dockerfile 生成 Docker 映像。

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 映像

从 GitHub 容器注册表中提取的映像是从简单的 Dockerfile 和 init.sql构建的。

1
2
3
4
5
6
7


FROM postgres

EXPOSE 5432

COPY ./init.sql /docker-entrypoint-initdb.d/</code> 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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');