Skip to content

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.

Fortschritt0/5 Lektionen
11

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:

  1. Ändert sich das Verhalten? → Neu schreiben, klar designen.
  2. Ist das Verhalten stabil und komplex? → Portieren mit Round-Trip-Tests.
  3. 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:

    12

    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++ / JavaGoBedeutung
    Klasse + private Felderstruct + unexportierte Felder (lowercase)kein Klassenkonzept
    Vererbung + virtuelle MethodenInterfaces + Struct-KompositionVerhalten über Verträge
    Konstruktor / DestruktorKonstruktionsfunktion NewXxx() + deferAufräumen am Ort
    Exceptions / try-catcherror-Rückgabewerte + if err != nilFehler sind Daten
    RAII (Lock im Scope)defer lock.Unlock()garantiert am Scope-Ende
    std::vector<T> / ArrayList[]T (Slice)dynamisches Array
    std::map / HashMapmap[K]Vassoziativer Speicher
    Smart Pointer / GC-Frageneingebauter GCdu denkst nie an delete
    Templates / GenericsType Parameters [T any]seit Go 1.18
    Threads + MutexGoroutines + Channels (sync.Mutex)Nebenläufigkeit als Sprachteil
    Promise/FutureChannelsasync als Datenfluss
    std::stringstreamstrings.Builder / bytes.Buffereffizientes Anhängen
    Enum-Klassentype Kind int + iotatypisierte Konstanten
    Getter/Setterdirekte FeldzugriffeKomplexitä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:

      13

      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:

        14

        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:

          15

          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:

          1. Round-Trip-Tests — dieselbe Eingabe, gleiches Ergebnis wie das Original.
          2. Fehlerpfade — ungültige Eingaben erzeugen error, keine Panik.
          3. Table-driven Tests — der ganze Verhaltensraum, nicht nur der Happy Path.
          4. go test -race ./... — keine Daten-Races in portierter Nebenläufigkeit.
          5. 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

          CheckBefehl/WerkzeugScheitert bei
          Formatgofmt, go vet ./...unsauberem Stil, verdächtigen Stellen
          Verhaltengo test ./...Round-Trip-/Fehler-Abweichung
          Racesgo test -race ./...gleichzeitigem Schreiben
          Randfällego test -fuzz=.Panics auf Zufallsdaten
          Performancego test -bench=.Regression im Port

          Wochen-Check Modul 3

          ✅ Wichtige Erkenntnisse

          Haken setzen, um deinen Lernfortschritt zu markieren:

            This site uses essential cookies for Stripe payments. No tracking cookies.