Entwickler, der außergewöhnliche CRM- und Laravel-Lösungen liefert

Als erfahrener Entwickler spezialisiere ich mich auf Laravel- und Vue.js-Entwicklung, die Implementierung von Vtiger CRM sowie auf vielfältige WordPress-Projekte. Meine Arbeit zeichnet sich durch kreative, dynamische und benutzerzentrierte Weblösungen aus, die individuell an die Bedürfnisse meiner Kunden angepasst werden.

Go 1.18 hat Generics eingeführt – ein mächtiges Werkzeug, um typsicheren, wiederverwendbaren Code zu schreiben. In diesem Artikel zeigen wir, wie wir mit Generics redundanten Datenbank-Code refaktoriert haben.

Go Generics im Einsatz: Praktisches Refactoring-Beispiel

Go 1.18 hat Generics eingeführt – ein mächtiges Werkzeug, um typsicheren, wiederverwendbaren Code zu schreiben. In diesem Artikel zeigen wir, wie wir mit Generics redundanten Datenbank-Code refaktoriert haben.

Problem: Duplizierter Code

In unserem Projekt EasyList hatten wir zwei Structs (Folder und Item), die ähnliche Insert-Methoden für die DB verwendeten:

type Folder struct {
    ID        int64         `jsonapi:"primary,folders"`
    Name      string        `jsonapi:"attr,name"`
    // ... weitere Felder
}

type Item struct {
    ID          int64     `jsonapi:"primary,items"`
    UserId      int64     `json:"-"`
    // ... weitere Felder
}

Die ursprünglichen Insert-Methoden:

// Insert für Folder
func (f FolderModel) Insert(folder *Folder) error {
    result, err := f.DB.ExecContext(ctx, query, args...)
    id, _ := result.LastInsertId()
    folder.ID = id
    // ...
}

// Insert für Item
func (i ItemModel) Insert(item *Item) error {
    result, err := i.DB.ExecContext(ctx, query, args...)
    id, _ := result.LastInsertId()
    item.ID = id
    // ...
}

Problem: Beide Methoden waren zu 90% identisch – ein klarer Fall für Refactoring!


Lösung mit Generics

1. Interface für ID-Zuweisung

Da Go keine direkte Feldzuweisung in Generics erlaubt, definieren wir ein Interface:

type Entity interface {
    SetId(id int64)
}

// Implementierung für Folder
func (f *Folder) SetId(id int64) {
    f.ID = id
}

// Implementierung für Item
func (i *Item) SetId(id int64) {
    i.ID = id
}

2. Generische Insert-Funktion

func Insert[T Entity](db *sql.DB, entity T, query string, args ...any) (T, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    result, err := db.ExecContext(ctx, query, args...)
    if err != nil {
        return entity, err
    }

    id, err := result.LastInsertId()
    if err != nil {
        return entity, err
    }

    entity.SetId(id) // ID via Interface zuweisen
    return entity, nil
}

Refactored Code

Für Folder:

func (f FolderModel) Insert(folder *Folder) error {
    query := `INSERT INTO folders (...) VALUES (...)`
    lastOrder, err := f.GetLastFolderOrder(folder.UserId.Int64)

    // Generische Funktion nutzen
    folder, err = Insert(f.DB, folder, query, folder.UserId, folder.Name, ...)
    folder.Order = int32(lastOrder)

    return nil
}

Für Item:

func (i ItemModel) Insert(item *Item) error {
    query := `INSERT INTO items (...) VALUES (...)`
    lastOrder, err := i.GetLastItemOrder(item.UserId)

    // Dieselbe generische Funktion
    item, err = Insert(i.DB, item, query, item.UserId, item.ListId, ...)
    item.Order = int32(lastOrder)

    return nil
}

Vorteile dieses Ansatzes

  1. Weniger Redundanz:
  • 70% weniger Code-Duplikation
  • Änderungen nur an einer Stelle nötig
  1. Typsicherheit:
  • Compiler prüft Typkompatibilität
  • Keine Runtime-Überraschungen
  1. Erweiterbarkeit:
  • Neue Structs benötigen nur SetId-Implementierung

Limitationen & Workarounds

  • Feldzugriff: Go erlaubt kein entity.ID = id in Generics.
    Lösung: Interface-basierte Setter-Methoden.
  • Komplexe Logik: Bei unterschiedlichen Insert-Logiken kann ein
    Strategy-Pattern sinnvoll sein:
  type InsertStrategy[T any] interface {
      PrepareQuery() string
      PrepareArgs(T) []any
  }

Fazit

Go-Generics sind ein Game-Changer für:

  • API-Clients (JSON-Unmarshalling)
  • DAL-Schichten (Datenbankzugriffe)
  • Utility-Funktionen (Sortieren, Filtern)

Weiterführende Ressourcen:

„Generics sind wie ein Schweizer Taschenmesser – nicht immer nötig, aber unbezahlbar, wenn man sie braucht.“