Applying Clean Architecture, Dependency Injection, and Testing in a Go Backend Project
The article demonstrates how to structure a Go backend using Clean Architecture’s four‑layer model, interface‑driven design, and compile‑time dependency injection with Google Wire, providing concrete examples of repository, service, and API implementations and unit‑testing each layer with sqlmock, gomock, and httptest.
Because Go does not have a unified coding convention like Java, this article adopts the ideas of Clean Architecture to propose a rational package and layer organization for Go projects.
Project directory layout (simplified):
├── cmd/
│ └── main.go // entry point
├── etc/
│ └── dev_conf.yaml // configuration
├── global/
│ └── global.go // global variables (DB, Kafka, etc.)
├── internal/
│ ├── service/
│ │ └── xxx_service.go // business logic
│ ├── model/
│ │ └── xxx_info.go // data structures
│ ├── api/
│ │ └── xxx_api.go // route handlers
│ ├── router/
│ │ └── router.go // routing
│ └── pkg/
│ ├── datetool // time utilities
│ └── jsontool // JSON utilitiesThe above division is only a simple functional split. In practice, several problems arise, such as unclear data flow, excessive coupling, and difficulty in refactoring when switching databases or services.
Clean Architecture principles (as described in the article):
Independent of frameworks
Testable
Independent of UI
Independent of databases
Independent of external agencies
These principles lead to a four‑layer structure:
Entities : core business objects.
Use Cases : application‑level business rules.
Interface Adapters : adapters that convert data between use cases and external systems (e.g., DB, web).
Framework & Drivers : external tools such as web frameworks and databases.
The author maps these layers to their project:
models → Entities
repo → Interface Adapters (data access)
service → Use Cases (business logic)
api → Framework & Drivers (HTTP handlers)
Interface‑driven programming is emphasized: higher layers depend on interfaces, not concrete implementations. Example interface and implementation for the repository layer:
package repo
import (
"context"
"my-clean-rchitecture/models"
"time"
)
// IArticleRepo represents the article repository contract
type IArticleRepo interface {
Fetch(ctx context.Context, createdDate time.Time, num int) (res []models.Article, err error)
}Implementation for MySQL:
type mysqlArticleRepository struct {
DB *gorm.DB
}
func NewMysqlArticleRepository(DB *gorm.DB) IArticleRepo {
return &mysqlArticleRepository{DB}
}
func (m *mysqlArticleRepository) Fetch(ctx context.Context, createdDate time.Time, num int) (res []models.Article, err error) {
err = m.DB.WithContext(ctx).Model(&models.Article{}).
Select("id,title,content, updated_at, created_at").
Where("created_at > ?", createdDate).
Limit(num).
Find(&res).Error
return
}When switching to another storage (e.g., MongoDB), only a new struct implementing IArticleRepo is needed, leaving the service layer unchanged.
Dependency Injection (DI) is introduced to avoid global variables and to make testing easier. The article recommends Google’s wire for compile‑time DI. Example wire.go:
//+build wireinject
package main
import (
"github.com/google/wire"
"my-clean-rchitecture/api"
"my-clean-rchitecture/api/handlers"
"my-clean-rchitecture/app"
"my-clean-rchitecture/repo"
"my-clean-rchitecture/service"
)
func InitServer() *app.Server {
wire.Build(
app.InitDB,
repo.NewMysqlArticleRepository,
service.NewArticleService,
handlers.NewArticleHandler,
api.NewRouter,
app.NewServer,
app.NewGinEngine,
)
return &app.Server{}
}Running wire generates wire_gen.go, which assembles all constructors automatically.
Testing strategy for each layer:
Models : simple unit tests, no dependencies.
Repo : use sqlmock together with GORM to mock MySQL queries.
Service : mock the repository interface with gomock.
API : mock the service layer and use httptest to simulate HTTP requests (Gin in test mode).
Example test for the repository layer (MySQL):
func Test_mysqlArticleRepository_Fetch(t *testing.T) {
createAt := time.Now()
updateAt := time.Now()
articles := []models.Article{{1, "test1", "content", updateAt, createAt}, {2, "test2", "content2", updateAt, createAt}}
limit := 2
mock, db := getSqlMock()
mock.ExpectQuery("SELECT id,title,content, updated_at, created_at FROM `articles` WHERE created_at > ? LIMIT 2").
WithArgs(createAt).
WillReturnRows(sqlmock.NewRows([]string{"id", "title", "content", "updated_at", "created_at"}).
AddRow(articles[0].ID, articles[0].Title, articles[0].Content, articles[0].UpdatedAt, articles[0].CreatedAt).
AddRow(articles[1].ID, articles[1].Title, articles[1].Content, articles[1].UpdatedAt, articles[1].CreatedAt))
repository := NewMysqlArticleRepository(db)
result, err := repository.Fetch(context.TODO(), createAt, limit)
assert.Nil(t, err)
assert.Equal(t, articles, result)
}Example test for the service layer using gomock:
func Test_articleService_Fetch(t *testing.T) {
ctl := gomock.NewController(t)
defer ctl.Finish()
now := time.Now()
mockRepo := mock.NewMockIArticleRepo(ctl)
mockRepo.EXPECT().Fetch(context.TODO(), now, 10).Return(nil, nil)
service := NewArticleService(mockRepo)
_, _ = service.Fetch(context.TODO(), now, 10)
}Example test for the API layer with httptest and Gin:
func TestArticleHandler_FetchArticle(t *testing.T) {
ctl := gomock.NewController(t)
defer ctl.Finish()
createAt, _ := time.Parse("2006-01-02", "2021-12-26")
mockService := mock.NewMockIArticleService(ctl)
mockService.EXPECT().Fetch(gomock.Any(), createAt, 10).Return(nil, nil)
handler := NewArticleHandler(mockService)
gin.SetMode(gin.TestMode)
r := gin.Default()
r.GET("/articles", handler.FetchArticle)
req, _ := http.NewRequest(http.MethodGet, "/articles?num=10&create_date=2021-12-26", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("Expected status %d but got %d", http.StatusOK, w.Code)
}
}Finally, the article concludes that applying Clean Architecture, interface‑driven design, and DI helps keep the codebase maintainable, testable, and flexible for future refactoring.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
