Modul 3: Nach Go portieren — C++/Java-Legacy sicher migrieren
Portierungs-Praxis: C++/Java nach Go migrieren — Mapping-Katalog, Fehler als Werte statt Exceptions, Nebenläufigkeit portieren (Threads → Goroutines) und Absicherung nach dem Port.
Portieren oder neu schreiben? Die Entscheidung
Lektion 11 von 5
Das Portierungs-Versprechen und seine Falle
Millionen Zeilen C++ und Java laufen in Produktion. Go ist der häufigste Weg, solche Systeme modern zu machen: schnelle Builds, ein Binary, eingebaute Concurrency. Aber wörtliches Portieren — Zeile für Zeile — konserviert die Architektur des Alt-Codes samt seiner Design-Fehler (dasselbe Phänomen, das die C→Rust-Forschung als „Fallacy" nachgewiesen hat). Du bekommst schnelleren, einfacheren Code — aber keine Garantie, dass er besser ist.
Die ehrliche Entscheidungsfrage für jedes Modul:
- Ändert sich das Verhalten? → Neu schreiben, klar designen.
- Ist das Verhalten stabil und komplex? → Portieren mit Round-Trip-Tests.
- Ist die Komponente klein und isoliert? → Beliebig — Tests entscheiden.
Ist das Verhalten 1:1 nötig (Protokolle, Dateiformate)?
ja → portieren + Round-Trip-Tests gegen das Original
nein → neu schreiben und die Alt-API als Vertrag nehmen
Ship-of-Theseus statt Big Bang
Portiere eine Komponente nach der anderen, während das System läuft. Jede Komponente bekommt ihre eigene Grenze (Interface/HTTP/CLI) und ihren eigenen Round-Trip-Test. Erst wenn die Go-Version nachweislich dieselben Ergebnisse liefert, ersetzt du das Original. Kein riesiges Rewrite, keine monatelange Phase ohne lieferbaren Stand.
// Die Grenze, die jedes subtituierte Modul braucht:
type OpStore interface {
Load(id string) ([]Op, error)
Save(id string, ops []Op) error
}
// Das Go-Modul implementiert das Interface; der Rest spricht nur noch
// das Interface an. Die C++/Java-Seite kann jederzeit abgeschaltet werden.
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Der Mapping-Katalog: C++/Java-Idiome → Go
Lektion 12 von 5
Jedes Alt-Idiom hat ein Go-Pendant
Der schnellste Weg durch jede Portierung — die Tabelle, auf die du immer zurückkommst:
| C++ / Java | Go | Bedeutung |
|---|---|---|
| Klasse + private Felder | struct + unexportierte Felder (lowercase) | kein Klassenkonzept |
| Vererbung + virtuelle Methoden | Interfaces + Struct-Komposition | Verhalten über Verträge |
| Konstruktor / Destruktor | Konstruktionsfunktion NewXxx() + defer | Aufräumen am Ort |
| Exceptions / try-catch | error-Rückgabewerte + if err != nil | Fehler sind Daten |
| RAII (Lock im Scope) | defer lock.Unlock() | garantiert am Scope-Ende |
std::vector<T> / ArrayList | []T (Slice) | dynamisches Array |
std::map / HashMap | map[K]V | assoziativer Speicher |
| Smart Pointer / GC-Fragen | eingebauter GC | du denkst nie an delete |
| Templates / Generics | Type Parameters [T any] | seit Go 1.18 |
| Threads + Mutex | Goroutines + Channels (sync.Mutex) | Nebenläufigkeit als Sprachteil |
| Promise/Future | Channels | async als Datenfluss |
std::stringstream | strings.Builder / bytes.Buffer | effizientes Anhängen |
| Enum-Klassen | type Kind int + iota | typisierte Konstanten |
| Getter/Setter | direkte Feldzugriffe | Komplexität nur wenn nötig |
// Typisierte Konstanten statt enum class:
type Status int
const (
StatusOpen Status = iota // 0
StatusDone // 1
StatusClosed // 2
)
Interfaces statt Vererbung
Vererbungshierarchien werden zu Interfaces: klein, offen, von
verschiedenen Typen implementierbar. Ein Typ erfüllt ein Interface
automatisch durch seine Methoden — kein implements, kein extends.
// Java: interface Writer { void write(String s); }
// Go:
type Writer interface {
Write(s string) error
}
// Jeder Typ mit Write(string) error ist ein Writer. Fertig.
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Fehler als Werte: Exceptions ausziehen
Lektion 13 von 5
Vom try-catch zum if err != nil
Der größte kulturelle Umbruch beim Portieren: Exceptions verschwinden. Fehler sind Werte, die jede Funktion explizit zurückgibt. Das klingt nach Boilerplate und ist tatsächlich Fokus: Du siehst an jeder Stelle, wo etwas schiefgehen kann.
// Java: throw new IOException(...) irgendwo tief unten, catch oben.
// Go:
func readConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err // der Aufrufer entscheidet
}
cfg, err := parse(data)
if err != nil {
return nil, fmt.Errorf("config %s: %w", path, err)
}
return cfg, nil
}
Fehler verketten: %w & errors.Is
fmt.Errorf("...: %w", err) verpackt den ursprünglichen Fehler, ohne ihn zu
zerstören. Mit errors.Is / errors.As kannst du tief in der Kette prüfen,
ob ein bestimmter Fehler aufgetreten ist — das Go-Pendant zu „welche
Exception ist das?".
import "errors"
var ErrNotFound = errors.New("nicht gefunden")
// Irgendwo tief in der Kette:
if !exists {
return ErrNotFound
}
// Oben:
if errors.Is(err, ErrNotFound) {
fmt.Println("Datei oder Datensatz fehlt")
}
Panik ist nicht try/catch
panic/recover existiert in Go — aber für Programmierfehler und
Invariantenbrüche, nicht für erwartbare Fehler (Eingaben, Dateien, Netz).
Faustregel beim Portieren: Jedes throw wird ein return …error; nur
worst-Entscheidungen (nil-deref, invariant verletzt) dürfen panic.
// SO NICHT — erwartbare Fehler gehören in Rückgabewerte:
func parseAge(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
panic(err) // ✗ keine Exception-Ersatz!
}
return n
}
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Nebenläufigkeit portieren: Threads → Goroutines & Channels
Lektion 14 von 5
Vom Thread-Pool zum Goroutine-Idiom
C++/Java-Nebenläufigkeit bedeutet meist: Threads, Locks, Thread-Pools, Futures. In Go ist die Einheit die Goroutine — leichtgewichtig (ein paar KB), millionenfach erzeugbar, und Kommunikation läuft über Channels statt über geteilten Zustand.
// Java: ExecutorService + Callable + Future.get()
// Go: Goroutine + Channel
func main() {
type Result struct {
name string
err error
}
results := make(chan Result)
for _, file := range files {
file := file // Schleifenvariable lokal binden
go func() {
out, err := process(file)
results <- Result{name: file, err: err}
}()
}
for range files {
r := <-results
if r.err != nil {
fmt.Println("Fehler:", r.name)
}
}
}
Don't communicate by sharing memory; share memory by communicating
Das Go-Mantra: Statt eine Struktur mit Lock zu schützen („teile und schütze"), schickst du die Daten durch einen Channel — genau ein Besitzer ist verantwortlich. Channels koppeln nebenläufige Teile datenfluss-artig, genau wie Streams in Reactiven Systemen.
// Werte über einen Channel liefern (Stream-Ersatz):
out := make(chan int)
go func() {
defer close(out)
for i := 0; i < 5; i++ {
out <- i * i
}
}()
for v := range out {
fmt.Println(v) // 0 1 4 9 16 — close beendet die range-Schleife
}
Der Race Detector: dein Sicherheitsnetz
go test -race kompiliert dein Programm mit Instrumentierung und meldet
jeden Daten-Race zur Laufzeit. Beim Portieren von Lock-Code in
Channel-Code ist das die wichtigste Prüfung überhaupt.
go test -race ./...
# WARNING: DATA RACE → Zeile, beide Zugriffe, Stacktraces
// Klassischer Portierungs-Fehler — geteilte Variable ohne Schutz:
total := 0
for i := 0; i < 100; i++ {
go func() { total++ }() // ✗ Daten-Race!
}
// richtig: atombesetzte Zähler (sync/atomic) oder Channel
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Absichern nach dem Port: Tests, Race Detector, Benchmarks
Lektion 15 von 5
Verifiziere, was du portiert hast
„Es kompiliert und läuft" ist kein Nachweis. Für jede portierte Einheit brauchst du:
- Round-Trip-Tests — dieselbe Eingabe, gleiches Ergebnis wie das Original.
- Fehlerpfade — ungültige Eingaben erzeugen
error, keine Panik. - Table-driven Tests — der ganze Verhaltensraum, nicht nur der Happy Path.
go test -race ./...— keine Daten-Races in portierter Nebenläufigkeit.- Benchmarks — der Port darf nichts dramatisch langsamer machen.
// Fuzz-Tests: Zufallseingaben in den Port — findet Randfälle der Übersetzung.
func FuzzParses(f *testing.F) {
f.Add("hallo")
f.Add("42")
f.Fuzz(func(t *testing.T, s string) {
// Der Parser darf NIE panicken — egal welcher Eingabe.
_ = parse(s)
})
}
Die fünf Punkte-Checkliste nach jedem Port
| Check | Befehl/Werkzeug | Scheitert bei |
|---|---|---|
| Format | gofmt, go vet ./... | unsauberem Stil, verdächtigen Stellen |
| Verhalten | go test ./... | Round-Trip-/Fehler-Abweichung |
| Races | go test -race ./... | gleichzeitigem Schreiben |
| Randfälle | go test -fuzz=. | Panics auf Zufallsdaten |
| Performance | go test -bench=. | Regression im Port |
Wochen-Check Modul 3
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren: