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.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Applying Clean Architecture, Dependency Injection, and Testing in a Go Backend Project

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 utilities

The 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

testingBackend DevelopmentGoWire
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.