สอนใช้งาน Blueprint Clean architecture Golang TouchTechnologies ฉบับสมบูรณ์

l3aseron By M
Touch Technologies
Published in
6 min readOct 1, 2021
Touchtechnologies

วันนี้เราจะมาแยกส่วนรายละเอียดต่างๆของ Blueprint Clean architecture Golang ของ Touch โดยจะมีเขียนรายละเอียดการใช้งานไว้ใน Readme (กรุณาอ่านด้วย)โดยต่อไปนี้จะอธิบายรายละเอียดต่างๆให้ทราบครับ

Git repo => https://github.com/touchtechnologies-product/go-blueprint-clean-architecture

Framework : Gin framework

Feature ตัวอย่าง

  • Company creation
  • Staff creation
  • Staff update
  • Get staff by company ID

System requirements Development

  • Docker
  • Jaeger
  • MongoDB
  • Golang

Design pattern : Clean architecture

Ref. Image somkiat.cc

โดยมีโครงสร้าง folder structure ดังนี้

Folder structure

โดยจากภาพการ Implement เราใช้ เทคนิคที่เรียกว่า DI (dependency injection) โดยที่เราจะฉีดสิ่งต่างๆที่เราต้องการจะใช้ โดยในที่นี้เราจะรวมจุดที่จะฉีดเราไปไว้ที่ setup.go ซึ่ง เป็น package main

Setup.go

package main

import (
"context"

"github.com/uber/jaeger-client-go/rpcmetrics"

"io"
"log"

companyWrapper "github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/company/wrapper"
staffWrapper "github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/staff/wrapper"
validatorService "github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/validator"

"github.com/opentracing/opentracing-go"
"github.com/sirupsen/logrus"

"github.com/touchtechnologies-product/go-blueprint-clean-architecture/app"
"github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/util"

jaegerConf "github.com/uber/jaeger-client-go/config"
jaegerLog "github.com/uber/jaeger-client-go/log"
"github.com/uber/jaeger-lib/metrics"

"github.com/touchtechnologies-product/go-blueprint-clean-architecture/config"
compRepo "github.com/touchtechnologies-product/go-blueprint-clean-architecture/repository/company"
staffRepo "github.com/touchtechnologies-product/go-blueprint-clean-architecture/repository/staff"
companyService "github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/company/implement"
staffService "github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/staff/implement"
)

func setupJaeger(appConfig *config.Config) io.Closer {
cfg, err := jaegerConf.FromEnv()
panicIfErr(err)

cfg.ServiceName = appConfig.AppName + "-" + appConfig.AppEnv
cfg.Sampler.Type = "const"
cfg.Sampler.Param = 1
cfg.Reporter = &jaegerConf.ReporterConfig{
LogSpans: true,
LocalAgentHostPort: appConfig.JaegerAgentHost + ":" + appConfig.JaegerAgentPort,
}

jLogger := jaegerLog.StdLogger
jMetricsFactory := metrics.NullFactory
jMetricsFactory = jMetricsFactory.Namespace(metrics.NSOptions{Name: appConfig.AppName + "-" + appConfig.AppEnv, Tags: nil})

tracer, closer, err := cfg.NewTracer(
jaegerConf.Logger(jLogger),
jaegerConf.Metrics(jMetricsFactory),
jaegerConf.Observer(rpcmetrics.NewObserver(jMetricsFactory, rpcmetrics.DefaultNameNormalizer)),
)
panicIfErr(err)
opentracing.SetGlobalTracer(tracer)

return closer
}
func newApp(appConfig *config.Config) *app.App {
ctx := context.Background()

cRepo, err := compRepo.New(ctx, appConfig.MongoDBEndpoint, appConfig.MongoDBName, appConfig.MongoDBCompanyTableName)
panicIfErr(err)
sRepo, err := staffRepo.New(ctx, appConfig.MongoDBEndpoint, appConfig.MongoDBName, appConfig.MongoDBStaffTableName)
panicIfErr(err)

validator := validatorService.New(cRepo, sRepo)
generateID, err := util.NewUUID()
panicIfErr(err)

company := companyService.New(validator, cRepo, generateID)
warpCompany := companyWrapper.WrapCompany(company)
staff := staffService.New(validator, sRepo, generateID)
wrapperStaff := staffWrapper.WrapperStaff(staff)
return app.New(wrapperStaff, warpCompany)
}

func setupLog() *logrus.Logger {
lr := logrus.New()
lr.SetFormatter(&logrus.JSONFormatter{})

return lr
}

func panicIfErr(err error) {
if err != nil {
log.Panic(err)
}
}

จาก Code ด้านบน เราจะเห็นว่า ตัว Service ต้องการใช้

  • validator
  • repository
  • generateID

เราก็จะฉีด 3 อย่างนี้เข้าไปให้ Service สามารถทำงานได้ และ ถ้าเทียบกับ ตัว Clean architecture แล้ว ส่วนนี้จะอยู่ใน Layer วงนอกสุดคือ External interfaces

ส่วนต่อไปคือ Main.go จะถูกImplement ใน package main เช่นเดียวกับ Setup.go ตัวMain.go นี้จะเป็นการ เรียกใช้งาน Route ซึ่งจาก Blueprintตัวนี้ เราใช้ Gin framework จากนั้นก็มีการ ใช้งาน Runtime และ เรียกใช้งาน newApp จาก Setup.go

main.go

package main

import (
"github.com/gin-gonic/gin"
ginLogRus "github.com/toorop/gin-logrus"

"github.com/touchtechnologies-product/go-blueprint-clean-architecture/config"
)

func main() {
// Load config
appConfig := config.Get()

// Init log format
log := setupLog()

// Gin setup
router := gin.New()

// Set custom log for gin
router.Use(ginLogRus.Logger(log), gin.Recovery())

// Jaeger setup
closer := setupJaeger(appConfig)
defer func() {
if err := closer.Close(); err != nil {
log.Error(err)
}
}()

// Register route to gin
_ = newApp(appConfig).RegisterRoute(router)

// Gin start listen
_ = router.Run()
}

Handler

ส่วนต่อไป คือ Handler คือส่วนที่อยู่ใน Folder app ซึ่งส่วนนี้จะ ค่อยจัดการภายนอกที่ต้องการเชื่อมต่อการใช้งานขาแรกเข้า หรือ เรียกว่า แดนรับแขก จะประกอบด้วย Route หรือ การเชื่อมต่อแบบ subscribe หากเทียบกับ Layer ของ Clean architecture คือส่วนของ Gateway || Prensenters โดยตัวอย่าง Folder structure ดังนี้

Handler app

เริ่มอธิบายส่วนสำคัญของส่วนแรกคือ init.go ไฟล์นี้มีส่วนสำคัญคือการจัดการ Route โดยมี Code ตัวอย่างดังนี้

init.go

package app

import (
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"

"github.com/touchtechnologies-product/go-blueprint-clean-architecture/app/company"
"github.com/touchtechnologies-product/go-blueprint-clean-architecture/app/staff"

"github.com/touchtechnologies-product/go-blueprint-clean-architecture/docs"
companyService "github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/company"
staffService "github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/staff"
)

type App struct {
staff *staff.Controller
company *company.Controller
}
//ส่วนนี้จะถูกไปประกาศใน Setup.gofunc New(staffService staffService.Service, companyService companyService.Service) *App {
return &App{
staff: staff.New(staffService),
company: company.New(companyService),
}
}

func (app *App) RegisterRoute(router *gin.Engine) *App {
docs.SwaggerInfo.Title = "Touch Tech API"
docs.SwaggerInfo.Description = "API Spec Demo."
docs.SwaggerInfo.Version = "1.0"
docs.SwaggerInfo.Host = "http://localhost:8080"
docs.SwaggerInfo.BasePath = "/api/v1"
apiRoutes := router.Group(docs.SwaggerInfo.BasePath)
{
apiRoutes.GET("/companies", app.company.Update)
apiRoutes.POST("/companies", app.company.Create)
apiRoutes.GET("/companies/:id", app.company.Read)
apiRoutes.PUT("/companies/:id", app.company.Update)
apiRoutes.DELETE("/companies/:id", app.company.Delete)

apiRoutes.GET("/staffs", app.staff.Update)
apiRoutes.POST("/staffs", app.staff.Create)
apiRoutes.GET("/staffs/:id", app.staff.Read)
apiRoutes.PUT("/staffs/:id", app.staff.Update)
apiRoutes.DELETE("/staffs/:id", app.staff.Delete)
}
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

return app
}

ใน init.go นี้จะรวมถึงการ ตั้งค่าต่างๆสำหรับ Swagger เพื่อใช้ในการทำ Api doc ส่วนไฟล์ต่างๆที่อยู่ใน app จะเป็นการเขียนเพื่อเรียกใช้ Service ตัวอย่างเช่น app->company->create.go ก็จะเขียนเชื่อมต่อกับ Service ในการ create company และมีการเขียนเพื่อ Config Swagger สำหรับ Api ต่างๆ มีการสร้าง Span เพื่อใช้งาน jaeger

app->company->create.go

package company

import (
"github.com/gin-gonic/gin"
"github.com/opentracing/opentracing-go"

"github.com/touchtechnologies-product/go-blueprint-clean-architecture/app/view"
"github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/company/companyin"
)

// Create godoc
// @Tags Companies
// @Summary Create a new company
// @Description A newly created company ID will be given in a Content-Location response header
// @Param input body companyin.CreateInput true "Input"
// @Param X-Authenticated-Userid header string true "User ID"
// @Accept json
// @Produce json
// @Success 201 {object} view.SuccessResp
// @Success 400 {object} view.ErrResp
// @Success 401 {object} view.ErrResp
// @Success 403 {object} view.ErrResp
// @Success 422 {object} view.ErrResp
// @Success 500 {object} view.ErrResp
// @Success 503 {object} view.ErrResp
// @Router /companies [post]
//สร้าง Span
func (ctrl *Controller) Create(c *gin.Context) {
span, ctx := opentracing.StartSpanFromContextWithTracer(
c.Request.Context(),
opentracing.GlobalTracer(),
"handler.company.Create",
)
defer span.Finish()

input := &companyin.CreateInput{}
if err := c.ShouldBindJSON(input); err != nil {
view.MakeErrResp(c, err)
return
}
//เรียกใช้งาน service
ID, err := ctrl.service.Create(ctx, input)
if err != nil {
view.MakeErrResp(c, err)
return
}

view.MakeCreatedResp(c, ID)
}

Service

ต่อมาจะเป็นส่วนของ Service หรือถ้ามองใน Clean architecture คือส่วนของ Use cases เป็นส่วนที่ใช้ implement business login หรือ mechanism ต่างๆในการทำงาน จะประกอบด้วย folder structure ดังนี้

folder structure Service

จาก folder structure จะอธิบายว่าแต่ละ folder มีหน้าที่อะไรจากตัวอย่างเราจะดู Company เป็นหลัก เริ่มจาก folder companyin มีหน้าที่ในการกำหนด ค่า struct data ขาเข้า ซึ่งเป็นการกำหนดด้วยว่า api เส้นนั้น จะให้ user ส่งค่าอะไรเข้ามาและมีการ Config การ Validate Field ต่างๆโดยมีตัวอย่าง Code ดังนี้

service->company->companyin->create.go

package companyin

import (
"github.com/touchtechnologies-product/go-blueprint-clean-architecture/domain"
)
//validate field
type CreateInput struct {
ID string `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
} // @Name CompanyCreateInput

func MakeTestCreateInput() (input *CreateInput) {
return &CreateInput{
ID: "test",
Name: "test",
}
}

func CreateInputToCompanyDomain(input *CreateInput) (company *domain.Company) {
return &domain.Company{
ID: input.ID,
Name: input.Name,
}
}

ส่วนต่อในเป็น folder implement มีหน้าที่เก็บ mechanism ต่างๆโดยเริ่มจาก init.go จะเป็นส่วนที่ กำหนดค่าว่า service นี้ต้องการใช้งานอะไรบาง และจะถูกฉีดสิ่งที่ต้องการใช้ เข้ามาโดยจะถูกประกาศใน setup.go ตัวอย่าง Code ดังนี้

service->company->implement->init.go

package implement

import (
"github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/company"
"github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/util"
"github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/validator"
)
// เป็นการกำหนดว่า service นี้ต้องการใช้งานอะไรบางเช่น repo validate etc
type implementation struct {
validator validator.Validator
repo util.Repository
uuid util.UUID
}
// ส่วนนี้จะเป็นส่วนที่ จะไปประกาศใน Setup.go
func New(validator validator.Validator, repo util.Repository, uuid util.UUID) (service company.Service) {
return &implementation{validator, repo, uuid}
}

ส่วนต่อไปจะเป็นส่วน ของการเขียน mechanism ต่างเราจะยกตัวอย่างเป็นการ create company โดยจะมีตัวอย่าง Code ดังนี้

service->company->implement->create.go

package implement

import (
"context"
"github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/company/companyin"
"github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/util"
)

func (impl *implementation) Create(ctx context.Context, input *companyin.CreateInput) (ID string, err error) {
//ส่วนของการ Validate
err = impl.validator.Validate(input)
if err != nil {
return "", util.ValidationCreateErr(err)
}
//ส่วนของการ map data เพื่อจะเก็บลง Database(mongo)
company := companyin.CreateInputToCompanyDomain(input)
//ส่วนการ insert data to database (mongo)
_, err = impl.repo.Create(ctx, company)
if err != nil {
return "", util.RepoCreateErr(err)
}

return company.ID, nil
}

ส่วนต่อไปจะเป็น folder out มีหน้าที่แสดงข้อมูลต่างๆในขา Read list เป็นการกำหนดหรือmap field ต่างๆ เพื่อออกไปให้ userใช้งาน

service->company->out->form.go

package out

import (
"github.com/touchtechnologies-product/go-blueprint-clean-architecture/domain"
)

type CompanyView struct {
ID string `json:"id"`
Name string `json:"name"`
} // @Name CompanyView

func CompanyToView(company *domain.Company) (view *CompanyView) {
return &CompanyView{
ID: company.ID,
Name: company.Name,
}
}

ส่วนต่อไปจะเป็น folder wrapper มีหน้าที่ wrapper data เพื่อใช้ใน jaeger ในการ สร้าง span มองง่ายๆว่าเป็นประตูขาแรกก่อนเข้า Service ซึ่งมีตัวอย่าง Code ดังนี้

service->wrapper->create.go

package wrapper

import (
"context"

"github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/company/companyin"

"github.com/opentracing/opentracing-go"
)

func (wrp *wrapper) Create(ctx context.Context, input *companyin.CreateInput) (ID string, err error) {
sp, ctx := opentracing.StartSpanFromContext(ctx, "service.company.Create")
defer sp.Finish()

sp.LogKV("ID", input.ID)
sp.LogKV("Name", input.Name)

ID, err = wrp.service.Create(ctx, input)

sp.LogKV("ID", ID)
sp.LogKV("err", err)

return ID, err
}

ส่วนต่อไปจะเป็น folder validator จะมีหน้าที่ในการทำ Validate data input หรือ การทำ custom validate โดยเริ่มแรกจะต้องไปดูที่ ไฟล์ service->validator
->init.go ซึ่งในไฟล์นี้จะมีการ initial function New ซึ่งจะถูกไปประกาศใน setup.go

service->validator->init.go

func New(companyRepo util.Repository, staffRepo util.Repository) (v *GoPlayGroundValidator) {
v = &GoPlayGroundValidator{
validate: validator.New(),
companyRepo: companyRepo,
staffRepo: staffRepo,
}

v.validate.RegisterStructValidation(v.CompanyCreateStructLevelValidation, &companyin.CreateInput{})
v.validate.RegisterStructValidation(v.CompanyUpdateStructLevelValidation, &companyin.UpdateInput{})
v.validate.RegisterStructValidation(v.PageOptionStructLevelValidation, &domain.PageOption{})

return v
}

จากนั้นก็ทำการเรียกใช้งาน Rule ต่างๆที่ต้องการจะใช้งาน เช่นในตัวอย่างจะมีการเรียกใช้งาน CompanyIDUnique ,CompanyNameUnique

service->validator->companycreate.go

func (v *GoPlayGroundValidator) CompanyCreateStructLevelValidation(structLV validator.StructLevel) {
ctx := context.Background()
company := structLV.Current().Interface().(companyin.CreateInput)

v.checkCompanyIDUnique(ctx, structLV, company.ID)
v.checkCompanyNameUnique(ctx, structLV, company.Name)
}

Domain

folder domain มีหน้าที่คล้ายกับ Model หรือ มองว่าเป็น สิ่งที่จะไม่มีการเปลี่ยนแปลง ถ้าเทียบใน Clean architecture จะอยู่ใน Layer Entities

package domain

type Company struct {
ID string `bson:"id"`
Name string `bson:"name"`
}

Repository

folder repository มีหน้าที่เชื่อมต่อกับ ภายนอกไม่ว่าจะเป็น third party ,database ,message broker จะรวมอยู่ Repository ซึ่งถ้าเทียบกับ Clean architecture จะอยู่ใน Layer นอกสุดคือ External interfaces

The end

Touch Technologies

“ เราไม่ได้ถูกต้องที่สุด แต่เราแสดงสิ่งที่เราทำ ”

--

--