Implementing Clean Architecture in a Go project involves structuring the project properly, writing the use cases, and connecting interfaces and frameworks. Below, we will go through each step to build a sample blog application.
A well-structured Go project using Clean Architecture might look like this:
gomyapp/
│
├── cmd/
│ └── myapp/
│ └── main.go
├── internal/
│ ├── adapters/
│ │ ├── controllers/
│ │ │ └── post_controller.go
│ │ ├── presenters/
│ │ │ └── post_presenter.go
│ │ └── repositories/
│ │ └── post_repository.go
│ ├── entities/
│ │ └── post.go
│ ├── frameworks/
│ │ └── database/
│ │ └── mysql.go
│ └── usecases/
│ └── post_usecase.go
├── pkg/
│ └── config/
│ └── config.go
└── go.mod
Use cases represent the application-specific business rules and interact with entities and repositories.
go// internal/usecases/post_usecase.go
package usecases
import "myapp/internal/entities"
// PostRepository defines the interface for post repository
type PostRepository interface {
GetByID(id int) (*entities.Post, error)
Save(post *entities.Post) error
}
// PostUseCase struct
type PostUseCase struct {
Repo PostRepository
}
// NewPostUseCase creates a new PostUseCase
func NewPostUseCase(repo PostRepository) *PostUseCase {
return &PostUseCase{Repo: repo}
}
// GetPost gets a post by ID
func (uc *PostUseCase) GetPost(id int) (*entities.Post, error) {
return uc.Repo.GetByID(id)
}
// CreatePost creates a new post
func (uc *PostUseCase) CreatePost(post *entities.Post) error {
return uc.Repo.Save(post)
}
go// internal/entities/post.go
package entities
type Post struct {
ID int
Title string
Content string
}
go// internal/adapters/repositories/post_repository.go
package repositories
import (
"database/sql"
"myapp/internal/entities"
"myapp/internal/usecases"
)
type PostGORMRepo struct {
DB *sql.DB
}
func NewPostGORMRepo(db *sql.DB) *PostGORMRepo {
return &PostGORMRepo{DB: db}
}
func (repo *PostGORMRepo) GetByID(id int) (*entities.Post, error) {
post := &entities.Post{}
row := repo.DB.QueryRow("SELECT id, title, content FROM posts WHERE id = ?", id)
err := row.Scan(&post.ID, &post.Title, &post.Content)
if err != nil {
return nil, err
}
return post, nil
}
func (repo *PostGORMRepo) Save(post *entities.Post) error {
_, err := repo.DB.Exec("INSERT INTO posts (title, content) VALUES (?, ?)", post.Title, post.Content)
return err
}
go// internal/adapters/controllers/post_controller.go
package controllers
import (
"encoding/json"
"myapp/internal/entities"
"myapp/internal/usecases"
"net/http"
"strconv"
)
type PostController struct {
UseCase *usecases.PostUseCase
}
func NewPostController(useCase *usecases.PostUseCase) *PostController {
return &PostController{UseCase: useCase}
}
func (pc *PostController) GetPost(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
post, err := pc.UseCase.GetPost(id)
if err != nil {
http.Error(w, "Post not found", http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(post)
}
func (pc *PostController) CreatePost(w http.ResponseWriter, r *http.Request) {
var post entities.Post
if err := json.NewDecoder(r.Body).Decode(&post); err != nil {
http.Error(w, "Invalid input", http.StatusBadRequest)
return
}
if err := pc.UseCase.CreatePost(&post); err != nil {
http.Error(w, "Could not create post", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
}
go// internal/frameworks/database/mysql.go
package database
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func NewMySQLDB(dataSourceName string) (*sql.DB, error) {
db, err := sql.Open("mysql", dataSourceName)
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
return nil, err
}
return db, nil
}
go// pkg/config/config.go
package config
import (
"os"
)
func GetEnv(key string, defaultValue string) string {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
return value
}
go// cmd/myapp/main.go
package main
import (
"myapp/internal/adapters/controllers"
"myapp/internal/adapters/repositories"
"myapp/internal/frameworks/database"
"myapp/internal/usecases"
"myapp/pkg/config"
"net/http"
)
func main() {
db, err := database.NewMySQLDB(config.GetEnv("MYSQL_DSN", "user:password@tcp(127.0.0.1:3306)/myapp"))
if err != nil {
panic(err)
}
postRepo := repositories.NewPostGORMRepo(db)
postUseCase := usecases.NewPostUseCase(postRepo)
postController := controllers.NewPostController(postUseCase)
http.HandleFunc("/post", postController.GetPost)
http.HandleFunc("/create", postController.CreatePost)
http.ListenAndServe(":8080", nil)
}
Separation of Concerns: Each layer should have distinct responsibilities. Entities handle core business logic, use cases handle application logic, repositories handle data persistence, and controllers handle HTTP interactions.
Dependency Injection: Use dependency injection to pass dependencies (like repositories) to use cases and controllers. This makes testing easier and increases flexibility.
Interfaces for Abstraction: Define interfaces for repositories and other dependencies to allow easy swapping of implementations.
Error Handling: Implement proper error handling in all layers. Use consistent error messages and HTTP status codes in controllers.
Configuration Management: Use environment variables for configuration, and provide default values to ensure the application can run in different environments (development, testing, production).
Tight Coupling: Avoid tight coupling between layers. Ensure that each layer interacts only through defined interfaces.
Leaking Abstractions: Do not leak implementation details from one layer to another. For instance, database-specific errors should not propagate to the use case layer.
Monolithic Use Cases: Keep use cases focused on specific tasks. Do not bundle too much logic into a single use case.
Lack of Testing: Not writing tests for each layer, especially for use cases and repositories, can lead to fragile code. Use mocks and stubs to test interactions between layers.
Ignoring Performance: Ensure that the architecture does not introduce unnecessary performance bottlenecks. Optimize database queries and minimize redundant operations.
By following these guidelines and being mindful of common pitfalls, you can implement a robust and maintainable application using Clean Architecture principles in Go.
Implementing Clean Architecture in a Go project involves clearly structuring the project, writing use cases to handle business logic, and connecting the different layers through well-defined interfaces. By adhering to the principles of Clean Architecture, you can create a system that is easy to maintain, test, and scale. This approach also ensures that changes in one part of the system do not adversely affect other parts, promoting a high degree of modularity and separation of concerns.