Entstehung & Ziel
Go wurde 2009 bei Google von Robert Griesemer, Rob Pike und Ken Thompson entwickelt — als Antwort auf die Komplexität von C++ und die langsamen Compile-Zeiten großer Codebases. Das Ziel war eine Sprache, die sich wie eine Systemsprache anfühlt, aber so einfach schreibbar ist wie ein Skript.
Go ist opinionated by design: Es gibt meist genau einen richtigen Weg, etwas zu tun. Das erzeugt konsistenten, lesbaren Code — besonders wichtig in großen Teams.
Kernprinzipien
Was Go bewusst weglässt
// Kein Klassen-Keyword
// Keine Vererbung (nur Embedding)
// Keine Exceptions (try/catch)
// Keine Operator-Überladung
// Kein this / self
// Kein null (nur nil)
// Keine optionalen Parameter
// Dafür: strukturelle Einfachheit
// und Lesbarkeit die skaliert.
Bekannte Go-Nutzer: Google, Cloudflare, Docker, Kubernetes, Terraform, Dropbox, Twitch
Konzeptuelle Unterschiede
Syntax-Vergleich: Funktion mit Error-Handling
func fetchUser(id int) (User, error) { row, err := db.Query("SELECT …", id) if err != nil { return User{}, fmt.Errorf("fetchUser: %w", err) } return row, nil }
async function fetchUser(id: number): Promise<User> { const row = await db.query(`SELECT … WHERE id=$1`, [id]) return row.rows[0] // error propagiert als rejected Promise }
Kernunterschied: Go zwingt dich, jeden Error explizit zu behandeln. In TS/JS können Errors unbemerkt "bubblen" — diszipliniertes Error-Handling ist Konvention, nicht Pflicht.
Struct statt Class
Go hat kein class-Keyword. Stattdessen kombiniert man
Struct (Daten) +
Methods (Verhalten) +
Interface (Vertrag).
// Struct = "Klasse" type User struct { Name string Email string age int // klein = privat } // Konstruktor-Funktion (Konvention) func NewUser(name, email string) *User { return &User{Name: name, Email: email} } // Methode auf Pointer-Receiver func (u *User) Greet() string { return "Hi, " + u.Name } // Value Receiver (liest nur) func (u User) IsAdult() bool { return u.age >= 18 }
Vererbung → Embedding
Go hat keine Vererbung. Stattdessen: Struct Embedding — ein Struct wird in ein anderes eingebettet und gibt seine Felder & Methoden weiter.
type Animal struct { Name string } func (a Animal) Speak() string { return a.Name + " macht ein Geräusch" } // Dog "erbt" Animal via Embedding type Dog struct { Animal // eingebettet Breed string } // Methode überschreiben func (d Dog) Speak() string { return d.Name + " bellt" } d := Dog{Animal: Animal{Name: "Rex"}} fmt.Println(d.Speak()) // Rex bellt fmt.Println(d.Name) // direkt verfügbar
Interface = Polymorphismus
Interfaces werden implizit implementiert — kein implements.
Jeder Typ der die Methoden hat, erfüllt das Interface automatisch.
type Speaker interface { Speak() string } // Cat implementiert Speaker implizit type Cat struct { Name string } func (c Cat) Speak() string { return "Miau" } // Dog aus oben auch — kein Unterschied! func MakeNoise(s Speaker) { fmt.Println(s.Speak()) } MakeNoise(Dog{...}) // ✓ MakeNoise(Cat{...}) // ✓ // Vergleich mit TS: // class Cat implements Speaker { // speak() { return "Miau" } ← explizit // }
Goroutines — das Grundprinzip
Eine Goroutine ist ein leichtgewichtiger Thread, der von der Go-Runtime verwaltet wird. Tausende können gleichzeitig laufen. Start-Kosten: ~2 KB Stack (vs. MB bei OS-Threads).
// Goroutine starten — einfach "go" go doWork() // Mit WaitGroup synchronisieren var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func(n int) { defer wg.Done() fmt.Println("Worker", n) }(i) } wg.Wait() // alle Goroutinen abwarten // Mutex für geteilten State var mu sync.Mutex mu.Lock() counter++ mu.Unlock()
Channels — Kommunikation zwischen Goroutinen
Go's Motto: "Don't communicate by sharing memory — share memory by communicating." Channels sind typisierte, threadsichere Leitungen.
// Unbuffered — blockiert bis gelesen ch := make(chan int) go func() { ch <- 42 }() v := <-ch // empfangen // Buffered — blockiert erst wenn voll jobs := make(chan string, 10) jobs <- "job1" jobs <- "job2" close(jobs) for j := range jobs { fmt.Println(j) } // select — wie switch für Channels select { case msg := <-ch1: fmt.Println(msg) case ch2 <- "ping": fmt.Println("sent") default: fmt.Println("non-blocking") }
Context — Timeouts & Abbruch
Context trägt Deadlines, Abbruch-Signale und Request-Werte durch die Call-Chain — der Standard-Weg um Goroutinen sauber zu beenden.
// Context mit Timeout ctx, cancel := context.WithTimeout( context.Background(), 5*time.Second, ) defer cancel() // immer defer! // HTTP-Request mit Context req, _ := http.NewRequestWithContext( ctx, http.MethodGet, url, nil, ) resp, err := http.DefaultClient.Do(req) // Goroutine auf ctx.Done() hören go func() { for { select { case <-ctx.Done(): return // sauber beenden default: doWork() } } }()
vs. JS async/await: In Go gibt es kein await. Goroutinen blockieren synchron aus ihrer eigenen Perspektive — die Runtime schedulet sie nicht-blockierend auf OS-Threads. Das Ergebnis ist ähnlich zu async/await, aber ohne Callback-Ketten oder Promise-Chaining.