Introduction#
This article is a summary of the findings of someone who has studied Clean Architecture and Go for about six months. The target readers of this post are those who know the basic idea of Clean Architecture, but don’t know how to implement it with Go.
I can’t say that I fully understand it, but I have accumulated some knowledge, so I’ll summarize it here. I hope this will be helpful for those who want to implement Clean Architecture with Go.
What is Clean Architecture?#
Clean Architecture is one of the system design guidelines.
Pros:
- Flexible because of loose coupling and separation of concerns
- Easy to test
Cons:
- Code is redundant and can be difficult to read
Implementation#
It may be difficult to understand the idea of Clean Architecture by reading articles on the Internet, so the best way to understand it is to actually implement it or read actual code. The following repositories help you catch up (there are many other sample repositories on GitHub, so you can check them out):
- bxcodec/go-clean-arch — Go (Golang) Clean Architecture based on Reading Uncle Bob’s Clean Architecture
- zhashkevych/go-clean-architecture — REST API example, built by following Uncle Bob’s Clean Architecture principles
- eminetto/clean-architecture-go-v2 — Clean Architecture sample
Goal#
The goal of this article is to create a template repository of Clean Architecture using Go. I think having the template will be useful for some future study. Here is the full code:
- yagikota/clean_architecture_with_go — A sample API built with Go (echo) and SQLBoiler according to Clean Architecture
Tech Stack#
What We Implement#
We will create a simple API to retrieve student information. Check out the API document by pasting this into Swagger Editor.
Prepare Code#
Incomplete code (for implementing by yourself):
git clone -b develop git@github.com:yagikota/clean_architecture_with_go.gitFull code:
git clone git@github.com:yagikota/clean_architecture_with_go.gitFolder Structure#
pkg
├── adapter
│ └── http
│ ├── health_check.go
│ ├── router.go // Register endpoints.
│ └── student_handler.go // Implement handlers.
├── config
│ └── config.go // Implement load func of environment variables.
├── domain
│ ├── model // Models are auto generated by SQLBoiler.
│ │ ├── boil_queries.go
│ │ ├── boil_table_names.go
│ │ ├── boil_types.go
│ │ ├── boil_view_names.go
│ │ ├── mysql_upsert.go
│ │ └── students.go
│ ├── repository // Define DB operations by interface.
│ │ └── student_repository.go
│ └── service // Bridge between repository and usecase.
│ └── student_service.go
├── infra
│ ├── db_conn.go // Make a connection to the DB.
│ └── mysql
│ └── mysql.go // Implement specific operations to DB
│ // defined in domain/repository.
└── usecase
├── model // Convert (abstract) auto generated models.
│ └── student.go
└── student_usecase.go // Implement some logics.If you clone the incomplete code, one API is already implemented. So, when you build the API then access http://localhost:8080/v1/students:
[
{
"id": 1,
"name": "Yamada Ichirou",
"age": 22,
"class": 1
},
{
"id": 2,
"name": "Yamada Jirou",
"age": 22,
"class": 1
},
{
"id": 10,
"name": "Yamada Jurou",
"age": 21,
"class": 4
}
]In the following sections, we will add APIs to the current code. In the process, we will also take a deep dive into the code.
Coding#
Repository Layer#
First, we implement pkg/domain/repository/student_repository.go.
package repository
import (
"context"
"github.com/yagikota/clean_architecture_wtih_go/pkg/domain/model"
)
// I presents interface.
type IStudentRepository interface {
SelectAllStudents(ctx context.Context) (model.StudentSlice, error)
SelectStudentByID(ctx context.Context, id int) (*model.Student, error) // add
}In this layer, we define minimum operations to each DB table by interface. So, in this case, we define two operations (SelectAllStudents, SelectStudentByID) and do not write specific operations (they are written in pkg/infra/mysql/mysql.go).
To explain interface, it is a manual (specifications).
// manual
type IDummy interface {
Method1() // condition1
Method2() // condition2
}For example, the above interface presents IDummy manual. In this case, IStudentRepository means:
Manual of IStudentRepository
IStudentRepositoryhas the following methods:
SelectAllStudents(ctx context.Context) (model.StudentSlice, error)SelectStudentByID(ctx context.Context, id int) (*model.Student, error)
The merits of using interface will be mentioned in the next chapter.
Infra Layer#
Second, we implement pkg/infra/mysql/mysql.go.
type studentRepository struct {
DB *sql.DB
}
func NewRoomRepository(db *sql.DB) repository.IStudentRepository {
return &studentRepository{
DB: db,
}
}
func (sr *studentRepository) SelectAllStudents(ctx context.Context) (model.StudentSlice, error) {
// specific operation
return model.Students().All(ctx, sr.DB)
}
// add
func (sr *studentRepository) SelectStudentByID(ctx context.Context, studentID int) (*model.Student, error) {
// specific operation
whereID := fmt.Sprintf("%s = ?", model.StudentColumns.ID)
return model.Students(
qm.Where(whereID, studentID),
).One(ctx, sr.DB)
}In this layer, we implement specific operations. In this case, we add the concrete process of SelectStudentByID that we added in the repository layer. Then, *studentRepository will satisfy IStudentRepository defined in pkg/domain/repository/student_repository.go.
Therefore, it can be said that the infra and the repository layer are connected based on the manual called IStudentRepository. In this way, by connecting each layer based on the interface, loose coupling (as long as the interface is satisfied, processing in one layer can be implemented without worrying about processing in the other layer) can be realized. Testing is also easier because you only need to focus on the layer in front of you.
Service Layer#
Next, we implement pkg/domain/service/student_service.go.
package service
import (
"context"
"github.com/yagikota/clean_architecture_wtih_go/pkg/domain/model"
"github.com/yagikota/clean_architecture_wtih_go/pkg/domain/repository"
)
type IStudentService interface {
FindAllStudents(ctx context.Context) (model.StudentSlice, error)
FindStudentByID(ctx context.Context, id int) (*model.Student, error)
}
type studentService struct {
repo repository.IStudentRepository
}
func NewStudentService(sr repository.IStudentRepository) IStudentService {
return &studentService{
repo: sr,
}
}
func (ss *studentService) FindAllStudents(ctx context.Context) (model.StudentSlice, error) {
return ss.repo.SelectAllStudents(ctx)
}
// add
func (ss *studentService) FindStudentByID(ctx context.Context, id int) (*model.Student, error) {
return ss.repo.SelectStudentByID(ctx, id)
}In this layer, we implement the processes that combine operations of the repository layer. In this case, we receive process from the repository layer and pass it to the usecase layer.
Usecase Layer#
Next, we implement pkg/usecase/student_usecase.go.
import (
"context"
"github.com/yagikota/clean_architecture_wtih_go/pkg/domain/service"
"github.com/yagikota/clean_architecture_wtih_go/pkg/usecase/model"
)
type IStudentUsecase interface {
FindAllStudents(ctx context.Context) (model.StudentSlice, error)
FindStudentByID(ctx context.Context, id int) (*model.Student, error) // add
}
type studentUsecase struct {
svc service.IStudentService
}
func NewUserUsecase(ss service.IStudentService) IStudentUsecase {
return &studentUsecase{
svc: ss,
}
}
func (su *studentUsecase) FindAllStudents(ctx context.Context) (model.StudentSlice, error) {
msSlice, err := su.svc.FindAllStudents(ctx)
if err != nil {
return nil, err
}
sSlice := make(model.StudentSlice, 0, len(msSlice))
for _, ms := range msSlice {
sSlice = append(sSlice, model.StudentFromDomainModel(ms))
}
return sSlice, nil
}
// add
func (su *studentUsecase) FindStudentByID(ctx context.Context, id int) (*model.Student, error) {
ms, err := su.svc.FindStudentByID(ctx, id)
if err != nil {
return nil, err
}
return model.StudentFromDomainModel(ms), nil
}In this layer, we convert data from the service layer and pass it to the adapter layer. In this case, StudentFromDomainModel(ms) converts data and the logic is written in pkg/usecase/model/student.go.
Adapter Layer#
Handler#
Next, we implement pkg/adapter/http/student_handler.go.
type studentHandler struct {
usecase usecase.IStudentUsecase
}
func NewStudentHandler(su usecase.IStudentUsecase) *studentHandler {
return &studentHandler{
usecase: su,
}
}
func (sh *studentHandler) FindAllStudents() echo.HandlerFunc {
return func(c echo.Context) error {
ctx := c.Request().Context()
student, err := sh.usecase.FindAllStudents(ctx)
if err != nil {
return c.JSON(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, student)
}
}
func (sh *studentHandler) FindStudentByID() echo.HandlerFunc {
return func(c echo.Context) error {
ctx := c.Request().Context()
studentID, err := strconv.Atoi(c.Param("student_id"))
if err != nil {
return c.JSON(http.StatusBadRequest, err.Error())
}
student, err := sh.usecase.FindStudentByID(ctx, studentID)
if err != nil {
return c.JSON(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, student)
}
}Dependency Injection#
In the router of this layer, we register endpoints and inject dependencies (DI).
mySQLConn := infra.NewMySQLConnector()
studentRepository := mysql.NewStudentRepository(mySQLConn.Conn)
studentService := service.NewUserService(studentRepository)
studentUsecase := usecase.NewUserUsecase(studentService)
handler := NewStudentHandler(studentUsecase)The above part is DI:
studentRepository := mysql.NewStudentRepository(mySQLConn.Conn)
studentService := service.NewUserService(studentRepository)
studentUsecase := usecase.NewUserUsecase(studentService)
handler := NewStudentHandler(studentUsecase)In Clean Architecture, there is a dependency chain like this: handler -> usecase -> service -> repository. We need usecase when initializing handler, service when initializing usecase, and repository when initializing service.
When initializing them, we use NewXXX functions (constructors) implemented in each file. In other words, the handler is created by calling the constructors defined in each layer and passing them to the subsequent process — the image is to create a matryoshka doll in the handler, open one doll, pass it to usecase, and pass it to service, repository in the same way.
By using constructors whose return value is an interface, each layer can be connected by interface, enabling loose coupling.
Conclusion#
What do you think? Perhaps some of you found it helpful, and some of you did not find it helpful at all. Also, the contents of this article may not be correct. Therefore, I think it is important to read various articles and sample codes, and to actually try your hand at it. I would appreciate it if you could point out any mistakes. I will correct them accordingly.






