Clean Architecture in Go mit go-clean-arch verwenden
Daniel Hayes
Full-Stack Engineer · Leapcell

Was ist die Code-Architektur Ihres Go-Projekts? Hexagonale Architektur? Zwiebelarchitektur? Oder vielleicht DDD? Egal welche Architektur Ihr Projekt verwendet, das Hauptziel sollte immer dasselbe sein: den Code leicht verständlich, testbar und wartbar zu machen.
Dieser Artikel beginnt mit Uncle Bobs Clean Architecture, analysiert kurz die Kernideen und taucht, in Kombination mit dem go-clean-arch-Repository, tief in die Implementierung dieser architektonischen Konzepte in einem Go-Projekt ein.
Clean Architecture
Clean Architecture ist eine von Uncle Bob vorgeschlagene Designphilosophie für Softwarearchitektur. Ihr Ziel ist es, Softwaresysteme durch eine geschichtete Struktur und klare Abhängigkeitsregeln leichter verständlich, testbar und wartbar zu machen. Die Kernidee besteht darin, Verantwortlichkeiten zu trennen und sicherzustellen, dass die Kern-Business-Logik (Use Cases) im System nicht von Implementierungsdetails (wie Frameworks, Datenbanken usw.) abhängt.
Die Kernidee von Clean Architecture ist Unabhängigkeit:
- Unabhängig von Frameworks: Sie ist nicht auf bestimmte Frameworks angewiesen (wie Gin, GRPC usw.). Frameworks sollten als Werkzeuge behandelt werden, nicht als Kern der Architektur.
- Unabhängig von der UI: Die Benutzeroberfläche kann leicht geändert werden, ohne andere Teile des Systems zu beeinträchtigen. Beispielsweise kann eine Web-UI durch eine Konsolen-UI ersetzt werden, ohne Geschäftsregeln zu ändern.
- Unabhängig von Datenbanken: Die Datenbank kann ausgetauscht werden (z. B. von MySQL zu MongoDB), ohne die Kern-Business-Logik zu beeinträchtigen.
- Unabhängig von externen Tools: Externe Abhängigkeiten (wie Bibliotheken von Drittanbietern) sollten isoliert werden, um zu vermeiden, dass sie den Systemkern direkt beeinflussen.
Strukturdiagramm
Wie im Diagramm dargestellt, wird Clean Architecture als eine Reihe konzentrischer Kreise beschrieben, wobei jede Schicht unterschiedliche Systemverantwortlichkeiten darstellt:
-
Kernentitäten
- Ort: Die innerste Schicht
- Verantwortung: Definiert die Geschäftsregeln des Systems. Entitäten sind die Kernobjekte in der Anwendung mit einem unabhängigen Lebenszyklus.
- Unabhängigkeit: Völlig unabhängig von Geschäftsregeln, ändert sich nur, wenn sich Geschäftsregeln ändern.
-
Use Cases / Services
- Ort: Die Schicht direkt neben den Entitäten
- Verantwortung: Implementiert die Business-Logik der Anwendung. Definiert den Ablauf verschiedener Operationen (Use Cases) im System und stellt sicher, dass die Benutzeranforderungen erfüllt werden.
- Rolle: Die Use-Case-Schicht ruft die Entitätsschicht auf, koordiniert den Datenfluss und bestimmt die Antworten.
-
Interface Adapters
- Ort: Die nächste äußere Schicht
- Verantwortung: Verantwortlich für die Konvertierung von Daten aus externen Systemen (wie UI, Datenbank usw.) in ein für die inneren Schichten verständliches Format und auch für die Konvertierung der Kern-Business-Logik in eine für externe Systeme verwendbare Form.
- Beispiele: Konvertieren von HTTP-Anforderungsdaten in interne Modelle (wie Klassen oder Strukturen) oder Präsentieren von Use-Case-Ausgabedaten für Benutzer.
- Komponenten: Beinhaltet Controller, Gateways, Presenter usw.
-
Frameworks & Drivers
- Ort: Die äußerste Schicht
- Verantwortung: Implementiert die Interaktion mit der Außenwelt, wie Datenbanken, UI, Message Queues usw.
- Feature: Diese Schicht hängt von den inneren Schichten ab, aber nicht umgekehrt. Dies ist der Teil des Systems, der am einfachsten auszutauschen ist.
go-clean-arch Projekt
go-clean-arch ist ein Beispiel-Go-Projekt, das Clean Architecture implementiert. Das Projekt ist in vier Domain-Schichten unterteilt:
Models Layer
Zweck: Definiert die Kerndatenstrukturen der Domäne und beschreibt die Geschäftsentitäten im Projekt, wie Artikel und Autor.
Entsprechende theoretische Schicht: Entitäten-Schicht.
Beispiel:
package domain import ( "time" ) type Article struct { ID int64 `json:"id"` Title string `json:"title" validate:"required"` Content string `json:"content" validate:"required"` Author Author `json:"author"` UpdatedAt time.Time `json:"updated_at"` CreatedAt time.Time `json:"created_at"` }
Repository Layer
Zweck: Verantwortlich für die Interaktion mit Datenquellen (wie Datenbanken und Caches) und Bereitstellung einer einheitlichen Schnittstelle für die Use-Case-Schicht, um auf Daten zuzugreifen.
Entsprechende theoretische Schicht: Frameworks & Drivers.
Beispiel:
package mysql import ( "context" "database/sql" "fmt" "github.com/sirupsen/logrus" "github.com/bxcodec/go-clean-arch/domain" "github.com/bxcodec/go-clean-arch/internal/repository" ) type ArticleRepository struct { Conn *sql.DB } // NewArticleRepository will create an object that represents the article.Repository interface func NewArticleRepository(conn *sql.DB) *ArticleRepository { return &ArticleRepository{conn} } func (m *ArticleRepository) fetch(ctx context.Context, query string, args ...interface{}) (result []domain.Article, err error) { rows, err := m.Conn.QueryContext(ctx, query, args...) if err != nil { logrus.Error(err) return nil, err } defer func() { errRow := rows.Close() if errRow != nil { logrus.Error(errRow) } }() result = make([]domain.Article, 0) for rows.Next() { t := domain.Article{} authorID := int64(0) err = rows.Scan( &t.ID, &t.Title, &t.Content, &authorID, &t.UpdatedAt, &t.CreatedAt, ) if err != nil { logrus.Error(err) return nil, err } t.Author = domain.Author{ ID: authorID, } result = append(result, t) } return result, nil } func (m *ArticleRepository) GetByID(ctx context.Context, id int64) (res domain.Article, err error) { query := `SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE ID = ?` list, err := m.fetch(ctx, query, id) if err != nil { return domain.Article{}, err } if len(list) > 0 { res = list[0] } else { return res, domain.ErrNotFound } return }
Usecase/Service Layer
Zweck: Definiert die Kernanwendungslogik des Systems und dient als Brücke zwischen Domänenmodellen und externen Interaktionen.
Entsprechende theoretische Schicht: Use Cases / Service.
Beispiel:
package article import ( "context" "time" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" "github.com/bxcodec/go-clean-arch/domain" ) type ArticleRepository interface { GetByID(ctx context.Context, id int64) (domain.Article, error) } type AuthorRepository interface { GetByID(ctx context.Context, id int64) (domain.Author, error) } type Service struct { articleRepo ArticleRepository authorRepo AuthorRepository } func NewService(a ArticleRepository, ar AuthorRepository) *Service { return &Service{ articleRepo: a, authorRepo: ar, } } func (a *Service) GetByID(ctx context.Context, id int64) (res domain.Article, err error) { res, err = a.articleRepo.GetByID(ctx, id) if err != nil { return } resAuthor, err := a.authorRepo.GetByID(ctx, res.Author.ID) if err != nil { return domain.Article{}, err } res.Author = resAuthor return }
Delivery Layer
Zweck: Verantwortlich für den Empfang externer Anfragen, den Aufruf der Use-Case-Schicht und die Rückgabe von Ergebnissen nach außen (wie HTTP-Clients oder CLI-Benutzer).
Entsprechende theoretische Schicht: Interface Adapters.
Beispiel:
package rest import ( "context" "net/http" "strconv" "github.com/bxcodec/go-clean-arch/domain" "github.com/labstack/echo" ) type ResponseError struct { Message string `json:"message"` } type ArticleService interface { GetByID(ctx context.Context, id int64) (domain.Article, error) } // ArticleHandler represents the HTTP handler for articles type ArticleHandler struct { Service ArticleService } func NewArticleHandler(e *echo.Echo, svc ArticleService) { handler := &ArticleHandler{ Service: svc, } e.GET("/articles/:id", handler.GetByID) } func (a *ArticleHandler) GetByID(c echo.Context) error { idP, err := strconv.Atoi(c.Param("id")) if err != nil { return c.JSON(http.StatusNotFound, domain.ErrNotFound.Error()) } id := int64(idP) ctx := c.Request().Context() art, err := a.Service.GetByID(ctx, id) if err != nil { return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()}) } return c.JSON(http.StatusOK, art) }
Die grundlegende Code-Architektur des go-clean-arch-Projekts ist wie folgt:
go-clean-arch/
├── internal/
│ ├── rest/
│ │ └── article.go # Delivery Layer
│ ├── repository/
│ │ ├── mysql/
│ │ │ └── article.go # Repository Layer
├── article/
│ └── service.go # Usecase/Service Layer
├── domain/
│ └── article.go # Models Layer
Im go-clean-arch-Projekt sind die Abhängigkeiten zwischen den einzelnen Schichten wie folgt:
- Die Usecase/Service-Schicht hängt von der Repository-Schnittstelle ab, kennt jedoch die Implementierungsdetails der Schnittstelle nicht.
- Die Repository-Schicht implementiert die Schnittstelle, ist jedoch eine äußere Komponente, die von den Entitäten in der Domänen-Schicht abhängt.
- Die Delivery-Schicht (wie der REST-Handler) ruft die Usecase/Service-Schicht auf und ist für die Umwandlung externer Anfragen in Business-Logik-Aufrufe verantwortlich.
Dieses Design folgt dem Dependency Inversion Principle und stellt sicher, dass die Kern-Business-Logik unabhängig von externen Implementierungsdetails ist, was zu einer höheren Testbarkeit und Flexibilität führt.
Zusammenfassung
Dieser Artikel hat anhand von Uncle Bobs Clean Architecture und dem go-clean-arch-Beispielprojekt vorgestellt, wie Clean Architecture in Go-Projekten implementiert wird. Durch die Aufteilung des Systems in Schichten wie Kernentitäten, Use Cases, Interface Adapters und externe Frameworks werden Verantwortlichkeiten klar getrennt und die Kern-Business-Logik (Use Cases) von externen Implementierungsdetails wie Frameworks und Datenbanken entkoppelt.
Die go-clean-arch-Projektarchitektur organisiert den Code auf geschichtete Weise, wobei jede Schicht klare Verantwortlichkeiten hat:
- Models Layer (Domain Layer): Definiert Kern-Business-Entitäten und ist unabhängig von externen Implementierungen.
- Usecase Layer: Implementiert Anwendungslogik, koordiniert Entitäten und externe Interaktionen.
- Repository Layer: Implementiert die spezifischen Details der Datenspeicherung.
- Delivery Layer: Behandelt externe Anfragen und gibt Ergebnisse zurück.
Dies ist nur ein Beispielprojekt. Das Architekturdesign eines tatsächlichen Projekts sollte flexibel an die praktischen Anforderungen, die Entwicklungsgewohnheiten des Teams und die Standards angepasst werden. Das Hauptziel besteht darin, den Grundsatz der Schichtung beizubehalten, sicherzustellen, dass der Code leicht verständlich, testbar und wartbar ist, und die langfristige Skalierbarkeit und Weiterentwicklung des Systems zu unterstützen.
Wir sind Leapcell, Ihre erste Wahl für das Hosten von Go-Projekten.
Leapcell ist die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis:
Multi-Language Support
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Stellen Sie unbegrenzt viele Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
Optimierte Developer Experience
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Nebenläufigkeit.
- Kein operativer Aufwand – konzentrieren Sie sich einfach auf das Bauen.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ