Skip to content

Go Spickzettel

Schnelle, statisch typisierte Sprache, gebaut für Einfachheit und Nebenläufigkeit.

01

Erste Schritte

Hello World

Jedes Go-Programm startet in der main()-Funktion von package main. Verwende 'go run' zum Ausführen, 'go build' zum Kompilieren. gofmt formatiert Code automatisch (Tabs, Abstände). Die import-Anweisung bindet Pakete ein – fmt behandelt formatiertes I/O.

go
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
    fmt.Printf("Name: %s, Age: %d\n", "Alice", 30)
}

// Run: go run main.go
// Build: go build -o app main.go
// Format: gofmt -w main.go

Go-Module (go.mod)

Go-Module (go.mod) verwalten Abhängigkeiten seit Go 1.11. 'go mod init' erstellt die Modul-Datei. 'go get' fügt Abhängigkeiten hinzu. 'go mod tidy' entfernt ungenutzte Imports und fügt fehlende hinzu. Der Modulpfad ist der Import-Pfad für dein Paket.

go
// Initialize a new module
// $ go mod init github.com/user/project

// go.mod file:
module github.com/user/project

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    golang.org/x/sync v0.3.0
)

// Add dependencies:
// $ go get github.com/gin-gonic/gin
// $ go mod tidy  // clean up unused deps

Paketstruktur

Go organisiert Code in Paketen – ein Paket pro Verzeichnis. Großgeschriebene Namen (Println, Add) sind exportiert (öffentlich); kleingeschriebene Namen (private) sind paket-privat. Das 'internal'-Verzeichnis beschränkt Imports auf das übergeordnete Modul. Der Paketname sollte mit dem Verzeichnisnamen übereinstimmen.

go
// project structure:
// myproject/
//   go.mod
//   main.go          // package main
//   utils/
//     helper.go      // package utils
//     math.go
//   internal/
//     api/           // private (only importable within parent)

// main.go
package main

import (
    "fmt"
    "github.com/user/project/utils"
)

func main() {
    fmt.Println(utils.Add(1, 2))
}

// Exported names start with uppercase letter
// unexported names start with lowercase letter

Variablen & Konstanten

Verwende 'var name type = value' für explizite Deklarationen, 'name := value' für kurze Deklarationen (nur in Funktionen). Go hat Nullwerte: 0 für Zahlen, '' für Strings, false für Booleans, nil für Pointer/Slices/Maps. Konstanten (const) sind Compile-Zeit und können := nicht verwenden.

go
package main

import "fmt"

// Package-level declarations
var version = "1.0.0" // inferred type
const Pi = 3.14159

func main() {
    // var with type
    var name string = "Alice"
    var age int = 30

    // Short declaration (only inside functions)
    city := "NYC"

    // Multiple assignment
    a, b, c := 1, 2, 3

    // Zero values (default)
    var x int      // 0
    var s string   // ""
    var ok bool    // false
    var p *int     // nil

    fmt.Println(name, age, city, a, b, c, x, s, ok, p)
}

Grundtypen & Konvertierung

Go erfordert explizite Typkonvertierung – es gibt keine implizite Konvertierung wie in C. rune ist ein Alias für int32 (repräsentiert einen Unicode-Codepunkt). byte ist ein Alias für uint8. Die Konvertierung zwischen numerischen Typen kann Präzision verlieren (float zu int schneidet ab). string(65) konvertiert einen Codepunkt in sein Zeichen.

go
// Numeric types
var i int = 42
var i8 int8 = 127
var u uint = 42
var f32 float32 = 3.14
var f64 float64 = 3.141592653589793

// Other types
var b bool = true
var s string = "hello"
var r rune = 'A' // int32 alias for Unicode code point
var by byte = 255 // uint8 alias

// Type conversion (explicit, no implicit conversion)
var n int = 42
var f float64 = float64(n)  // int -> float64
var s string = string(65)   // 65 -> "A" (rune to string)
var i2 int = int(f)         // float64 -> int (truncates)

// Type inference
var x = 42        // int
var y = 3.14      // float64
var z = len("hi") // int
02

Strings & Formatierung

String-Grundlagen

Go-Strings sind unveränderliche Byte-Sequenzen. len() gibt die Byte-Anzahl zurück, nicht die Zeichen-Anzahl – verwende utf8.RuneCountInString() für Unicode. Strings werden lexikografisch mit ==, <, > verglichen. Range über einen String iteriert nach Rune (Unicode-Codepunkt) und behandelt Multi-Byte-Zeichen korrekt.

go
package main

import (
    "fmt"
    "strings"
)

func main() {
    s := "Hello, World"

    // Length (bytes, not runes!)
    fmt.Println(len(s)) // 12

    // Concatenation
    s1 := "Hello" + " " + "World"
    s2 := fmt.Sprintf("%s = %d", "age", 30)

    // Comparison
    fmt.Println("abc" == "abc") // true
    fmt.Println("a" < "b")      // true (lexicographic)

    // Iteration (by rune)
    for i, r := range "Go语" {
        fmt.Printf("%d: %c\n", i, r)
    }
}

strings-Paket

Das strings-Paket bietet gängige String-Operationen. Contains/HasPrefix/HasSuffix prüfen auf Teilstrings. Split trennt nach Trennzeichen; Join verbindet. Replace nimmt eine Anzahl (-1 für alle). TrimSpace entfernt führende/nachfolgende Leerzeichen. Alle Funktionen geben neue Strings zurück (Strings sind unveränderlich).

go
import (
    "fmt"
    "strings"
)

s := "Hello, World"

// Case
strings.ToUpper(s)         // "HELLO, WORLD"
strings.ToLower(s)         // "hello, world"
strings.Title("hello world") // "Hello World"

// Search
strings.Contains(s, "World")  // true
strings.HasPrefix(s, "Hello") // true
strings.HasSuffix(s, "World") // true
strings.Index(s, "World")     // 7 (-1 if not found)
strings.Count(s, "l")         // 3

// Split & Join
parts := strings.Split("a,b,c", ",")  // ["a", "b", "c"]
joined := strings.Join(parts, "-")    // "a-b-c"

// Replace
strings.Replace(s, "l", "L", 1)  // "HeLlo, World" (1 occurrence)
strings.ReplaceAll(s, "l", "L")  // "HeLLo, WorLd"
strings.TrimSpace("  hi  ")      // "hi"

strconv & Formatierung

strconv konvertiert zwischen Strings und Zahlen – prüfe immer den Error-Rückgabewert. fmt.Printf formatiert die Ausgabe: %d (int), %f (float), %s (string), %t (bool), %T (Typ), %x (hex), %q (quoted). Verwende %.2f für 2 Nachkommastellen, %05d für Zero-Padding. Sprintf gibt den String zurück statt zu drucken.

go
import (
    "fmt"
    "strconv"
)

// String to number
n, err := strconv.Atoi("42")     // string -> int
f, err := strconv.ParseFloat("3.14", 64)

// Number to string
s1 := strconv.Itoa(42)            // int -> string
s2 := strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"

// Printf formatting
fmt.Printf("Int: %d\n", 42)
fmt.Printf("Float: %.2f\n", 3.14159)
fmt.Printf("String: %s\n", "hi")
fmt.Printf("Bool: %t\n", true)
fmt.Printf("Type: %T\n", 42)     // int
fmt.Printf("Hex: %x\n", 255)     // ff
fmt.Printf("Pad: %05d\n", 42)    // 00042
fmt.Printf("Quote: %q\n", "hi")  // "hi"

// Sprintf returns formatted string
result := fmt.Sprintf("Name: %s, Age: %d", "Alice", 30)

Raw Strings & Mehrzeilig

Raw Strings (Backticks) bewahren alles wörtlich – keine Escape-Sequenzen, können mehrere Zeilen umfassen. Verwende für Regex, SQL, HTML-Templates. Interpreted Strings (doppelte Anführungszeichen) verarbeiten \n, \t usw. Für effiziente String-Konkatenation in Schleifen verwende strings.Builder (vermeidet O(n²)-Allokation).

go
// Raw string literal (backticks) - no escape processing
raw := `This is a
multiline string
with \n (literal backslash-n)`

// Interpreted string (double quotes) - processes escapes
interpreted := "Line1\nLine2\tTabbed"

// Raw strings are useful for:
// - Regex patterns
// - HTML/SQL templates
// - File paths on Windows
regex := `^d{4}-d{2}-d{2}$`

// String builder (efficient concatenation)
var sb strings.Builder
for i := 0; i < 1000; i++ {
    sb.WriteString("line\n")
}
result := sb.String()

Unicode & Runes

Go-Strings sind UTF-8-kodierte Byte-Sequenzen. len() gibt Bytes; utf8.RuneCountInString() gibt Zeichen. Range über einen String dekodiert UTF-8 automatisch. Um einen String zu modifizieren, konvertiere zu []rune, ändere und konvertiere zurück. Essenziell für internationalisierte Textverarbeitung.

go
import (
    "fmt"
    "unicode/utf8"
)

s := "Hello, 世界"

// Byte length vs rune length
fmt.Println(len(s))                    // 13 (bytes)
fmt.Println(utf8.RuneCountInString(s)) // 9 (runes/characters)

// Decode runes manually
for i := 0; i < len(s); {
    r, size := utf8.DecodeRuneInString(s[i:])
    fmt.Printf("%c ", r)
    i += size
}

// Range automatically decodes runes
for i, r := range s {
    fmt.Printf("%d:%c ", i, r)
}

// Rune slice (mutable)
runes := []rune("Hello")
runes[0] = 'J'
s2 := string(runes) // "Jello"
03

Kontrollfluss

If / Else

Go's if braucht keine Klammern um Bedingungen, aber geschweifte Klammern sind Pflicht (auch für einzeilige Bodies). Die Init-Anweisung (if x := f(); x > 0) ist auf den if/else-Block begrenzt – üblich für Fehlerprüfung. Dieses Pattern hält den Variablen-Scope eng.

go
// Basic if/else
score := 85
if score >= 90 {
    fmt.Println("A")
} else if score >= 80 {
    fmt.Println("B")
} else {
    fmt.Println("C")
}

// If with initialization statement
if n := computeValue(); n > 100 {
    fmt.Println("big:", n)
} else {
    fmt.Println("small:", n)
}
// n is not accessible here (scoped to if)

// No parentheses needed, but braces are required

For-Schleifen

Go hat nur ein Schleifen-Schlüsselwort: 'for'. Es behandelt C-Stil, While-Stil (for cond), Endlos (for) und Iteration (for range). Range funktioniert mit Slices, Maps, Strings und Channels. Verwende _, um Index oder Wert zu überspringen. Map-Iterationsreihenfolge ist absichtlich zufällig.

go
// C-style for
for i := 0; i < 5; i++ {
    fmt.Println(i)
}

// While-style (condition only)
n := 10
for n > 0 {
    n--
}

// Infinite loop
for {
    break
}

// Range (iterate slices, maps, strings)
nums := []int{10, 20, 30}
for index, value := range nums {
    fmt.Printf("%d: %d\n", index, value)
}

// Skip index or value with _
for _, value := range nums {
    fmt.Println(value)
}

// Range over map (unordered!)
m := map[string]int{"a": 1, "b": 2}
for key, val := range m {
    fmt.Println(key, val)
}

Switch

Go's switch fällt standardmäßig nicht durch (im Gegensatz zu C/Java) – jeder case ist ein eigener Zweig. Verwende fallthrough, um es zu erzwingen. Mehrere Werte pro case verwenden Kommas. Switch ohne Ausdruck wirkt als sauberere if/else-Kette. Switch mit Init-Anweisung begrenzt die Variable auf das Switch.

go
// Basic switch
day := 3
switch day {
case 1:
    fmt.Println("Mon")
case 2, 3, 4, 5:
    fmt.Println("Weekday")
case 6, 7:
    fmt.Println("Weekend")
default:
    fmt.Println("Invalid")
}

// Switch with no expression (like if/else chain)
switch {
case score >= 90:
    grade = "A"
case score >= 80:
    grade = "B"
default:
    grade = "C"
}

// Switch with init
switch os := runtime.GOOS; os {
case "linux":
    fmt.Println("Linux")
case "darwin":
    fmt.Println("macOS")
}

// Fallthrough (rare, goes to next case unconditionally)
switch 1 {
case 1:
    fmt.Println("one")
    fallthrough
case 2:
    fmt.Println("two") // executes even though value is 1
}

Defer

defer plant einen Funktionsaufruf zur Ausführung, wenn die umschließende Funktion zurückkehrt – Last-In-First-Out (LIFO). Verwende es für Cleanup (Dateien schließen, Locks freigeben, Verbindungen schließen). Argumente von deferrierten Aufrufen werden sofort ausgewertet, aber der Aufruf erfolgt bei Return. Defers laufen sogar, wenn die Funktion panikt.

go
// Defer runs when function returns (LIFO order)
func main() {
    defer fmt.Println("third")
    defer fmt.Println("second")
    fmt.Println("first")
}
// Output: first, second, third

// Common: resource cleanup
func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close() // runs when readFile returns

    // ... use f ...
    return nil
}

// Defer evaluates arguments immediately
i := 1
defer fmt.Println(i) // prints 1 (not 2)
i = 2

Goto, Break, Continue

break verlässt die innerste Schleife; continue springt zur nächsten Iteration. Labels (break outer) verlassen verschachtelte Schleifen. goto existiert, wird aber selten verwendet – bevorzuge strukturierten Kontrollfluss. Labels können auch mit continue verwendet werden, um zur nächsten Iteration einer äußeren Schleife zu springen.

go
// Break and continue
for i := 0; i < 10; i++ {
    if i%2 == 0 {
        continue // skip even numbers
    }
    if i > 7 {
        break // exit loop
    }
    fmt.Println(i)
}

// Break with label (for nested loops)
outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i == 1 && j == 1 {
            break outer // exits both loops
        }
    }
}

// Goto (rarely used, avoid)
i := 0
loop:
    if i < 5 {
        fmt.Println(i)
        i++
        goto loop
    }

Select (Channel-Operationen)

select ist wie switch für Channels – es wartet auf mehrere Channel-Operationen und wählt die erste bereite aus. Der default-Case macht es nicht-blockierend. Verwende select in einer Schleife für ereignisgesteuerte Patterns. time.After() erstellt einen Timeout-Channel. select wählt zufällig, wenn mehrere Cases bereit sind, und verhindert so Starvation.

go
// Select waits on multiple channel operations
select {
case msg := <-messages:
    fmt.Println("received:", msg)
case <-timeout:
    fmt.Println("timed out")
default:
    fmt.Println("no activity") // non-blocking
}

// Select with send
ch1 := make(chan int)
ch2 := make(chan int)
go func() { ch1 <- 1 }()
go func() { ch2 <- 2 }()

select {
case v := <-ch1:
    fmt.Println("ch1:", v)
case v := <-ch2:
    fmt.Println("ch2:", v)
}

// Select in a loop (event loop pattern)
for {
    select {
    case msg := <-ch:
        fmt.Println(msg)
    case <-time.After(5 * time.Second):
        return // timeout after 5s of inactivity
    }
}
04

Funktionen

Definieren & Mehrfachrückgaben

Go-Funktionen können mehrere Werte zurückgeben – das kanonische Pattern ist (result, error). Benannte Rückgaben wirken als deklarierte Variablen, initialisiert auf Nullwerte; ein 'nackter' Return verwendet sie. Benannte Rückgaben verbessern die Lesbarkeit für komplexe Funktionen, können aber bei Überbeanspruchung verwirrend sein. Prüfe Errors immer sofort.

go
// Function with multiple return values
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("divide by zero")
    }
    return a / b, nil
}

// Named return values
func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return // "naked" return (uses named values)
}

// Usage
result, err := divide(10, 2)
if err != nil {
    log.Fatal(err)
}
fmt.Println(result) // 5

x, y := split(100)
fmt.Println(x, y)

Variadic & Closures

Variadic-Funktionen (...T) akzeptieren beliebig viele Argumente, empfangen als Slice. Spread eine Slice mit .... Closures erfassen Variablen per Referenz aus ihrem umschließenden Scope – die counter()-Funktion gibt eine Closure zurück, die sich an 'count' erinnert. Closures sind nützlich für Callbacks, Iteratoren und zustandsbehaftete Funktionen.

go
// Variadic function (variable arguments)
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}
fmt.Println(sum(1, 2, 3))       // 6
fmt.Println(sum(1, 2, 3, 4, 5)) // 15

// Spread a slice
nums := []int{1, 2, 3}
fmt.Println(sum(nums...)) // 6

// Closure (function that captures variables)
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

next := counter()
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3

Funktionen als Werte

Funktionen in Go sind First-Class-Werte – sie können Variablen zugewiesen, als Argumente übergeben und in Datenstrukturen gespeichert werden. Definiere Funktionstypen mit 'type Name func(params) returns'. Maps von Funktionen sind nützlich für Dispatch-Tabellen, Command-Handler und Strategy-Patterns.

go
// Functions are first-class values
func apply(f func(int) int, x int) int {
    return f(x)
}

func double(x int) int { return x * 2 }
func square(x int) int { return x * x }

fmt.Println(apply(double, 5)) // 10
fmt.Println(apply(square, 5)) // 25

// Function type
type MathFunc func(int) int

var fn MathFunc = func(x int) int { return x + 1 }
fmt.Println(fn(10)) // 11

// Map of functions
ops := map[string]func(int, int) int{
    "add": func(a, b int) int { return a + b },
    "sub": func(a, b int) int { return a - b },
    "mul": func(a, b int) int { return a * b },
}
fmt.Println(ops["add"](3, 4)) // 7

init() & Anonyme Funktionen

init()-Funktionen laufen automatisch vor main(), in der Reihenfolge ihrer Deklaration. Verwende für Setup (Config-Laden, Validierung, Registrierung). Mehrere init()-Funktionen pro Datei sind erlaubt. Anonyme Funktionen können sofort aufgerufen (IIFE) oder Variablen zugewiesen werden. Sie werden auch für Goroutinen verwendet.

go
// init() runs before main(), once per package
// Multiple init() functions allowed, run in order declared
package main

import "fmt"

var config string

func init() {
    // Setup code: config, connections, validation
    config = "production"
    fmt.Println("init 1")
}

func init() {
    fmt.Println("init 2, config:", config)
}

func main() {
    fmt.Println("main")
}
// Output: init 1, init 2, main

// Anonymous function (IIFE)
result := func(x int) int {
    return x * 2
}(5) // immediately invoked, result = 10

// Goroutine with anonymous function
go func(msg string) {
    fmt.Println(msg)
}("async")

Generics (Go 1.18+)

Go 1.18+ unterstützt Generics mit Typparametern [T any]. Type-Constraints (Interfaces) beschränken erlaubte Typen – verwende 'any' für keine Beschränkung, 'comparable' für == / != -Operatoren. Union-Constraints (int | float64) erlauben spezifische Typen. Generics ermöglichen typsichere wiederverwendbare Datenstrukturen und Algorithmen ohne Codeduplizierung.

go
// Generic function with type parameter
func Map[T, U any](slice []T, f func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = f(v)
    }
    return result
}

nums := []int{1, 2, 3}
doubled := Map(nums, func(n int) int { return n * 2 })
// [2, 4, 6]

strs := Map(nums, func(n int) string {
    return fmt.Sprintf("num%d", n)
})
// ["num1", "num2", "num3"]

// Type constraints
type Number interface {
    int | int64 | float64
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

// comparable constraint (for maps/slices keys)
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

Methoden

Methoden sind Funktionen mit einem Receiver-Argument. Value-Receiver (r Rectangle) arbeiten auf einer Kopie – können das Original nicht modifizieren. Pointer-Receiver (r *Rectangle) können modifizieren und vermeiden das Kopieren großer Structs. Konsistenz ist wichtig: Wenn eine Methode einen Pointer-Receiver verwendet, sollten alle es tun. Methoden können auf jedem Typ im selben Paket definiert werden.

go
type Rectangle struct {
    Width, Height float64
}

// Method with value receiver
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Method with pointer receiver (can modify)
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

// Method on non-struct types
type MyString string

func (s MyString) Shout() string {
    return strings.ToUpper(string(s)) + "!"
}

// Usage
r := Rectangle{Width: 10, Height: 5}
fmt.Println(r.Area()) // 50
r.Scale(2)
fmt.Println(r.Width)  // 20

s := MyString("hello")
fmt.Println(s.Shout()) // HELLO!
05

Datenstrukturen

Arrays & Slices

Arrays haben feste Länge; Slices sind dynamisch (unterlegt von Arrays). append() fügt Elemente hinzu und vergrößert die Kapazität nach Bedarf. make([]T, len, cap) prä-allokiert für Effizienz. Slicing erstellt eine View (teilt das zugrundeliegende Array) – verwende copy() für unabhängige Daten. Prüfe immer len/cap bei der Optimierung.

go
// Array (fixed length, rarely used directly)
var arr [3]int = [3]int{1, 2, 3}
arr2 := [...]int{4, 5, 6} // size inferred

// Slice (dynamic array, most common)
nums := []int{1, 2, 3}
nums = append(nums, 4)        // [1, 2, 3, 4]
nums = append(nums, 5, 6, 7) // [1, 2, 3, 4, 5, 6, 7]

// Make a slice with capacity
s := make([]int, 3, 10) // len=3, cap=10

// Slicing
sub := nums[1:4]  // [2, 3, 4]
first := nums[:2] // [1, 2]
last := nums[3:]  // [4, 5, 6, 7]

// Copy
dst := make([]int, len(nums))
copy(dst, nums)

// Length and capacity
fmt.Println(len(nums), cap(nums))

Maps

Maps sind Go's Hash-Tabellen – ungeordnete Schlüssel/Wert-Paare. Das Comma-Ok-Pattern (val, ok := m[key]) prüft, ob ein Schlüssel existiert. delete() entfernt einen Schlüssel. Map-Iterationsreihenfolge ist absichtlich zufällig. Maps sind Referenztypen – die Übergabe an Funktionen teilt die zugrundeliegenden Daten. nil-Maps können nicht beschrieben werden (verwende make()).

go
// Create a map
m := map[string]int{
    "Alice": 30,
    "Bob":   25,
}

// Using make
ages := make(map[string]int)
ages["Charlie"] = 35

// Access
fmt.Println(m["Alice"]) // 30

// Check existence (comma-ok pattern)
age, ok := m["David"]
if !ok {
    fmt.Println("not found")
}

// Delete
delete(m, "Bob")

// Iterate (random order)
for name, age := range m {
    fmt.Printf("%s: %d\n", name, age)
}

// Length
fmt.Println(len(m))

// Nested maps
matrix := map[string]map[string]int{}
matrix["row1"] = map[string]int{"col1": 1}

Structs

Structs gruppieren verwandte Felder. Verwende benannte Initialisierung (Person{Name: ...}) für Klarheit. new() gibt einen Pointer mit Nullwerten zurück. Anonyme Structs sind nützlich für einmalige Datenformen. Struct-Embedding (ohne Feldname) promoted die Felder und Methoden des eingebetteten Structs – Go's Alternative zur Vererbung.

go
// Define a struct
type Person struct {
    Name string
    Age  int
    Address string // field names are exported (capitalized)
}

// Create instances
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{"Bob", 25, "NYC"} // positional (not recommended)
p3 := new(Person)              // returns *Person, zero values
p3.Name = "Charlie"

// Anonymous struct (one-off)
config := struct {
    Port int
    Host string
}{
    Port: 8080,
    Host: "localhost",
}

// Struct embedding (composition)
type Employee struct {
    Person          // embedded (promotes fields)
    Salary   float64
}

emp := Employee{
    Person: Person{Name: "Dave", Age: 40},
    Salary: 50000,
}
fmt.Println(emp.Name) // "Dave" (promoted from Person)

Pointer

Go-Pointer (*T) halten Speicheradressen. & nimmt die Adresse, * dereferenziert. Im Gegensatz zu C hat Go keine Pointer-Arithmetik (sicherer). Struct-Pointer erlauben Kurzschreibweise (u.Name statt (*u).Name). nil-Pointer verursachen Panics bei Dereferenzierung. Go hat Garbage Collection – kein manuelles free() nötig.

go
// Pointer basics
x := 42
p := &x          // p is *int, points to x
fmt.Println(*p)  // 42 (dereference)
*p = 100         // modify x through pointer
fmt.Println(x)   // 100

// new() allocates and returns pointer
p2 := new(int)   // *int, value 0
*p2 = 42

// Pointers to structs
type User struct{ Name string }
u := &User{Name: "Alice"} // *User
u.Name = "Bob"            // (*u).Name shorthand
fmt.Println(u.Name)       // Bob

// nil pointer
var p3 *int // nil
// *p3 // panic: nil pointer dereference

// No pointer arithmetic (unlike C)
// p++ is illegal

Slice-Operationen

Go hat kein eingebautes filter/map/reduce – schreibe sie selbst oder verwende das slices-Paket (Go 1.21+). Insert/remove erfordern das Verschieben von Elementen mit append+copy. Sei vorsichtig mit Slice-Aliasing: s[:i] und s[i+1:] teilen das zugrundeliegende Array. Für 2D-Slices allokiiere jede Zeile separat.

go
// Filter
func filter(nums []int, pred func(int) bool) []int {
    result := []int{}
    for _, n := range nums {
        if pred(n) {
            result = append(result, n)
        }
    }
    return result
}

evens := filter([]int{1,2,3,4,5}, func(n int) bool {
    return n%2 == 0
}) // [2, 4]

// Insert at index
func insert(s []int, i, v int) []int {
    s = append(s, 0)
    copy(s[i+1:], s[i:])
    s[i] = v
    return s
}

// Remove at index
func remove(s []int, i int) []int {
    return append(s[:i], s[i+1:]...)
}

// 2D slice
matrix := make([][]int, 3)
for i := range matrix {
    matrix[i] = make([]int, 4)
}

Sortieren & Suchen

sort.Ints/Strings/Float64s sortieren in-place. sort.Slice mit einem Comparator behandelt benutzerdefinierte Typen. sort.Search* macht binäre Suche auf sortierten Slices. Go 1.21+ fügt das slices-Paket mit generischen Sortierfunktionen hinzu. Alle Sortierungen sind in-place – kopiere zuerst, wenn du die ursprüngliche Reihenfolge brauchst.

go
import "sort"

// Sort a slice
nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
sort.Ints(nums) // [1 1 2 3 4 5 6 9]
fmt.Println(nums)

// Sort strings
strs := []string{"banana", "apple", "cherry"}
sort.Strings(strs) // [apple banana cherry]

// Sort with custom comparator
type Person struct {
    Name string
    Age  int
}

people := []Person{
    {"Alice", 30},
    {"Bob", 25},
    {"Charlie", 35},
}

sort.Slice(people, func(i, j int) bool {
    return people[i].Age < people[j].Age // by age ascending
})

// Binary search (sorted slice)
idx := sort.SearchInts(nums, 4) // index of 4

// Sort package (Go 1.21+): slices.Sort
import "slices"
slices.Sort(people) // needs Less method or cmp func
06

Methoden & Interfaces

Interfaces definieren

Interfaces definieren Methodensignaturen. Go verwendet Structural Typing – ein Typ implementiert ein Interface automatisch, wenn er alle erforderlichen Methoden hat (keine explizite 'implements'-Deklaration). Das ermöglicht entkoppeltes Design: Definiere Interfaces dort, wo du sie verwendest, nicht dort, wo du sie implementierst.

go
// Interface: a set of method signatures
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Implement implicitly (no 'implements' keyword)
type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// Use the interface
func describe(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n",
        s.Area(), s.Perimeter())
}

c := Circle{Radius: 5}
describe(c) // works! Circle implements Shape

Empty Interface & Type Assertions

interface{} (oder 'any' in Go 1.18+) hält jeden Wert. Type Assertion (v.(T)) extrahiert den konkreten Typ – panikt bei falschem Typ, also verwende das Comma-Ok-Pattern. Type Switch (switch v.(type)) behandelt mehrere Typen sauber. Empty Interface ist nützlich für generische Container, verliert aber Typsicherheit – bevorzuge Generics.

go
// Empty interface (any type)
// Go 1.18+: 'any' is an alias for interface{}
func printAny(v any) {
    fmt.Println(v)
}

printAny(42)
printAny("hello")
printAny([]int{1, 2, 3})

// Type assertion
var i any = "hello"
s := i.(string)    // panics if not string
fmt.Println(s)

// Safe type assertion (comma-ok)
s, ok := i.(string)
if ok {
    fmt.Println("string:", s)
}

// Type switch
func describe(v any) {
    switch x := v.(type) {
    case int:
        fmt.Printf("int: %d\n", x)
    case string:
        fmt.Printf("string: %s\n", x)
    case []int:
        fmt.Printf("int slice: %v\n", x)
    default:
        fmt.Printf("unknown type: %T\n", x)
    }
}

Interface-Komposition

Interfaces können andere Interfaces einbetten (Komposition). io.Reader und io.Writer sind Go's wichtigste Interfaces – implementiert von Dateien, Netzwerkverbindungen, Buffern usw. Das ermöglicht mächtige Abstraktionen: Funktionen, die io.Reader nehmen, funktionieren mit jeder lesbaren Quelle. Kleine, fokussierte Interfaces (1-3 Methoden) sind idiomatisch.

go
// Compose interfaces
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// ReadWriter combines Reader and Writer
type ReadWriter interface {
    Reader
    Writer
}

// io.Reader and io.Writer are built-in interfaces
// Many types implement them: *os.File, *bytes.Buffer, net.Conn

func copyData(r io.Reader, w io.Writer) error {
    buf := make([]byte, 1024)
    for {
        n, err := r.Read(buf)
        if n > 0 {
            if _, err := w.Write(buf[:n]); err != nil {
                return err
            }
        }
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }
    }
    return nil
}

Pointer- vs Value-Receiver

Pointer-Receiver können das Struct modifizieren und vermeiden das Kopieren großer Werte. Value-Receiver sind sicher (können nicht modifizieren) und erlauben die Methode auf beiden Werten und Pointern. Wenn eine Methode einen Pointer-Receiver hat, sollten alle es tun (für Konsistenz). Pointer-Receiver-Methoden erfüllen Interfaces für sowohl T als auch *T; Value-Receiver-Methoden nur für T.

go
type Counter struct {
    count int
}

// Value receiver: works on a copy, can't modify
func (c Counter) Get() int {
    return c.count
}

// Pointer receiver: can modify the original
func (c *Counter) Increment() {
    c.count++
}

c := Counter{}
c.Increment() // Go auto-takes address (&c).Increment()
c.Increment()
fmt.Println(c.Get()) // 2

// Interface implementation note:
// *Counter implements both Get() and Increment()
// Counter (value) implements only Get()
// So *Counter satisfies an interface requiring both

Stringer-Interface

Das Stringer-Interface (String() string) kontrolliert, wie ein Typ in Print/Printf erscheint – wie toString() in Java/JS. %v verwendet String(); %+v zeigt Feldnamen; %#v zeigt Go-Syntax. Das error-Interface funktioniert genauso: Implementiere Error() string, um einen beliebigen Typ zu einem Error zu machen. Dies sind Go's häufigste eingebaute Interfaces.

go
// Stringer interface (like toString() in other languages)
type Stringer interface {
    String() string
}

type Person struct {
    Name string
    Age  int
}

// Implement Stringer
func (p Person) String() string {
    return fmt.Sprintf("%s (%d years)", p.Name, p.Age)
}

p := Person{Name: "Alice", Age: 30}
fmt.Println(p)       // Alice (30 years) — uses String()
fmt.Printf("%v\n", p) // Alice (30 years)
fmt.Printf("%+v\n", p) // Name:Alice Age:30
fmt.Printf("%#v\n", p) // main.Person{Name:"Alice", Age:30}

// Error interface is similar:
// type error interface { Error() string }
// Implement Error() to make a type usable as an error

Type Embedding & Komposition

Go verwendet Komposition statt Vererbung. Das Einbetten eines Structs promoted dessen Felder und Methoden in das äußere Struct. Du kannst promoted-Methoden überschreiben, indem du eine Methode mit demselben Namen auf dem äußeren Typ definierst. Das Einbetten eines Interfaces erlaubt Decorating/Delegating – der äußere Typ erfüllt das Interface und kann Aufrufe weiterleiten.

go
// Embed a struct (composition over inheritance)
type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return a.Name + " makes a sound"
}

type Dog struct {
    Animal // embedded — promotes Name and Speak()
    Breed  string
}

d := Dog{
    Animal: Animal{Name: "Rex"},
    Breed:  "Labrador",
}

fmt.Println(d.Name)      // "Rex" (promoted)
fmt.Println(d.Speak())   // "Rex makes a sound" (promoted)
fmt.Println(d.Animal.Name) // explicit access also works

// Override a promoted method
func (d Dog) Speak() string {
    return d.Name + " barks!"
}
fmt.Println(d.Speak()) // "Rex barks!"

// Embed an interface
type Logger struct {
    io.Writer // embed interface
}
// Logger now has Write() method, delegating to the embedded Writer
07

Fehlerbehandlung

Error-Grundlagen

Go behandelt Errors als Werte, nicht als Exceptions. Funktionen geben (result, error) zurück – prüfe immer sofort err != nil. errors.New() erstellt einfache Errors; fmt.Errorf() fügt Formatierung hinzu. Sentinel-Errors (var ErrX = errors.New()) ermöglichen Vergleich mit ==. Ignoriere niemals Errors (verwende _ nur bewusst).

go
// Functions return errors as last return value
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

// Always check errors immediately
result, err := divide(10, 0)
if err != nil {
    log.Fatal(err) // or handle gracefully
}
fmt.Println(result)

// fmt.Errorf for formatted errors
func validate(age int) error {
    if age < 0 {
        return fmt.Errorf("invalid age: %d (must be positive)", age)
    }
    return nil
}

// Sentinel errors
var ErrNotFound = errors.New("not found")
if err := lookup("key"); err == ErrNotFound {
    // handle not found
}

Benutzerdefinierte Error-Typen

Benutzerdefinierte Error-Typen implementieren das error-Interface (Error() string). Sie tragen strukturierte Daten (Felder, Codes, Kontext) über eine einfache Nachricht hinaus. Verwende Type Assertion (*ValidationError), um auf die Daten zuzugreifen. Essenziell für domänenspezifische Fehlerbehandlung – z.B. HTTP-Statuscodes, Validierungsdetails, Retry-Logik.

go
// Custom error type (implements error interface)
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error on '%s': %s", e.Field, e.Message)
}

// Use the custom error
func validateUser(name string) error {
    if name == "" {
        return &ValidationError{
            Field:   "name",
            Message: "cannot be empty",
        }
    }
    return nil
}

// Type-assert to access fields
err := validateUser("")
if ve, ok := err.(*ValidationError); ok {
    fmt.Println("Field:", ve.Field)   // "name"
    fmt.Println("Message:", ve.Message) // "cannot be empty"
}

errors.Is & errors.As (Go 1.13+)

Go 1.13+ fügte Error-Wrapping mit %w (fmt.Errorf) hinzu. errors.Is() prüft, ob ein Error einem Sentinel entspricht (entfaltet die Kette). errors.As() extrahiert einen spezifischen Error-Typ aus der Kette. Das ermöglicht geschichtete Fehlerbehandlung: Low-Level-Errors werden mit Kontext gewrappt, auf hohen Ebenen geprüft. Wrappe immer mit %w (nicht %v), um die Kette zu bewahren.

go
import "errors"

// Wrapped errors with %w
var ErrNotFound = errors.New("not found")

func getUser(id int) error {
    if id == 0 {
        return fmt.Errorf("getUser(%d): %w", id, ErrNotFound)
    }
    return nil
}

// errors.Is: check if error matches (unwraps the chain)
err := getUser(0)
if errors.Is(err, ErrNotFound) {
    fmt.Println("user not found")
}

// errors.As: extract a specific error type
var ve *ValidationError
if errors.As(err, &ve) {
    fmt.Println("field:", ve.Field)
}

// errors.Unwrap: get the wrapped error
inner := errors.Unwrap(err)

Panic & Recover

panic() ist für nicht behebbare Fehler (Bugs, Invarianten-Verletzungen) – nicht für normale Fehlerbehandlung. recover() fängt Panics ab, funktioniert aber nur in deferrierten Funktionen. Verwende panic/recover für: Programmierfehler (Index außerhalb des Bereichs), Paket-Initialisierungsfehler und den Schutz von Goroutinen davor, das Programm zum Absturz zu bringen. Bevorzuge die Rückgabe von Errors für erwartete Fehlschläge.

go
// Panic: unrecoverable error (like throw)
func mustParse(s string) int {
    n, err := strconv.Atoi(s)
    if err != nil {
        panic(fmt.Sprintf("invalid number: %s", s))
    }
    return n
}

// Recover: catch a panic (only in deferred functions)
func safeCall() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered:", r)
        }
    }()
    panic("something went wrong")
}

// Practical: safe goroutine
func safeGo(fn func()) {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                log.Println("goroutine panic:", r)
            }
        }()
        fn()
    }()
}

Error-Wrapping-Patterns

Wrappe Errors mit Kontext auf jeder Ebene mit fmt.Errorf und %w. Das erstellt eine Error-Kette: Die oberste Ebene sieht den vollständigen Pfad (getUserProfile → fetchUser → sql error). Der Kontext (Funktionsname, Parameter) hilft beim Debugging. Verwende errors.Is/As, um nach spezifischen Errors in der Kette zu prüfen. Vermeide es, denselben Error mehrmals mit demselben Kontext zu wrappen.

go
// Wrap errors with context as they propagate up
func fetchUser(id int) (*User, error) {
    row := db.QueryRow("SELECT ... WHERE id = ?", id)
    var u User
    if err := row.Scan(&u.Name, &u.Age); err != nil {
        return nil, fmt.Errorf("fetchUser(%d): %w", id, err)
    }
    return &u, nil
}

func getUserProfile(id int) (*Profile, error) {
    user, err := fetchUser(id)
    if err != nil {
        return nil, fmt.Errorf("getUserProfile(%d): %w", id, err)
    }
    // ...
}

// At the top level, log the full chain
profile, err := getUserProfile(42)
if err != nil {
    log.Printf("error: %v", err)
    // Output: getUserProfile(42): fetchUser(42): sql: no rows
}

// golang.org/x/xerrors or pkg/errors for stack traces

Defer, Panic, Recover zusammen

Kombiniere defer (Cleanup), panic (fatal Errors) und recover (Panics abfangen) für robustes Ressourcenmanagement. Defers laufen in LIFO-Reihenfolge, sogar während Panics. Der benannte Rückgabewert (err error) kann in einem deferrierten recover gesetzt werden. Dieses Pattern stellt sicher, dass Dateien/Verbindungen geschlossen und Transaktionen zurückgerollt werden, sogar wenn Code panikt.

go
// Complete pattern: cleanup + panic recovery
func processFile(path string) (err error) {
    file, e := os.Open(path)
    if e != nil {
        return e
    }
    defer func() {
        // Close file regardless of panic
        file.Close()

        // Recover from panic and convert to error
        if r := recover(); r != nil {
            err = fmt.Errorf("panic in processFile: %v", r)
        }
    }()

    // Do work that might panic
    data := parseFile(file) // might panic
    return saveData(data)
}

// Defer for resource cleanup (always runs)
func withDatabase(db *sql.DB, fn func(*sql.DB) error) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    defer func() {
        if err != nil {
            tx.Rollback()
        } else {
            tx.Commit()
        }
    }()
    return fn(tx)
}
08

Nebenläufigkeit

Goroutinen

Goroutinen sind Go's leichtgewichtige Threads – starten mit 'go'. Sie sind günstig (~2KB Stack) und werden von Go's Runtime-Scheduler verwaltet (M:N-Scheduling). Die main-Funktion wartet nicht auf Goroutinen – verwende sync.WaitGroup oder Channels für Synchronisation. Verwende niemals time.Sleep zur Synchronisation in Produktion (verwende WaitGroup).

go
package main

import (
    "fmt"
    "time"
)

func sayHello(name string) {
    for i := 0; i < 3; i++ {
        fmt.Println(name, i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    // Launch goroutine with 'go' keyword
    go sayHello("Alice")
    go sayHello("Bob")

    // Anonymous goroutine
    go func() {
        fmt.Println("anonymous goroutine")
    }()

    // Wait for goroutines (simple approach)
    time.Sleep(1 * time.Second)
    fmt.Println("done")
}

// Goroutines are lightweight (~2KB stack, grows as needed)
// Millions of goroutines can run concurrently

Channels

Channels sind typisierte Leitungen für Goroutinen-Kommunikation. Unbuffered (make(chan T)) blockiert, bis sowohl Sender als auch Empfänger bereit sind (synchron). Buffered (make(chan T, n)) blockiert nur, wenn voll (asynchron). Der Sender sollte Channels schließen, niemals der Empfänger. Range über einen Channel, bis er geschlossen ist. Channels ermöglichen 'Speicher durch Kommunikation teilen'.

go
// Unbuffered channel (synchronous)
ch := make(chan string)

// Send and receive
go func() {
    ch <- "hello" // blocks until received
}()
msg := <-ch // blocks until sent
fmt.Println(msg)

// Buffered channel (asynchronous)
buf := make(chan int, 3)
buf <- 1 // doesn't block (buffer has space)
buf <- 2
buf <- 3
// buf <- 4 // would block (buffer full)
fmt.Println(<-buf) // 1

// Close a channel (sender closes, never receiver)
close(buf)

// Range over channel (until closed)
for v := range buf {
    fmt.Println(v)
}

// Check if closed
v, ok := <-buf
if !ok {
    fmt.Println("channel closed")
}

Select-Anweisung

select lässt eine Goroutine auf mehrere Channel-Operationen warten – es wählt die erste bereite aus (zufällig, wenn mehrere bereit sind). Der default-Case macht es nicht-blockierend. time.After() erstellt Timeout-Channels. Select ist das Herz von nebenläufigem Go: Event-Loops, Fan-In/Fan-Out, Timeouts. Schließe immer Timeouts ein, um Deadlocks zu vermeiden.

go
// Select: multiplex channel operations
ch1 := make(chan string)
ch2 := make(chan string)

go func() {
    time.Sleep(1 * time.Second)
    ch1 <- "one"
}()
go func() {
    time.Sleep(2 * time.Second)
    ch2 <- "two"
}()

// Wait for first to arrive
for i := 0; i < 2; i++ {
    select {
    case msg1 := <-ch1:
        fmt.Println("received:", msg1)
    case msg2 := <-ch2:
        fmt.Println("received:", msg2)
    }
}

// Timeout with select
select {
case result := <-slowOperation():
    fmt.Println(result)
case <-time.After(3 * time.Second):
    fmt.Println("timeout!")
}

// Non-blocking receive (default case)
select {
case msg := <-ch:
    fmt.Println(msg)
default:
    fmt.Println("no message") // runs if no data
}

sync.WaitGroup

sync.WaitGroup wartet auf eine Gruppe von Goroutinen, bis sie fertig sind. Add(n) inkrementiert den Zähler, Done() dekrementiert ihn (mit defer verwenden), Wait() blockiert bis null. Übergib Schleifenvariablen immer als Parameter an Goroutinen, um Closure-Capture-Bugs zu vermeiden (in Go 1.22 behoben, aber weiterhin empfohlen). WaitGroup ist einfacher als Channels für Fire-and-Forget-Nebenläufigkeit.

go
import "sync"

func main() {
    var wg sync.WaitGroup

    // Launch 5 goroutines
    for i := 0; i < 5; i++ {
        wg.Add(1) // increment counter
        go func(id int) {
            defer wg.Done() // decrement when done
            fmt.Printf("Worker %d started\n", id)
            time.Sleep(time.Duration(id) * 100 * time.Millisecond)
            fmt.Printf("Worker %d done\n", id)
        }(i)
    }

    wg.Wait() // block until all Done() called
    fmt.Println("all workers finished")
}

// Common mistake: passing loop variable to goroutine
// Always pass as parameter: go func(i int) { ... }(i)
// (Go 1.22+ fixes loop variable scoping, but still good practice)

Mutex & Sync

sync.Mutex schützt gemeinsamen Zustand vor nebenläufigem Zugriff – Lock/Unlock mit defer. RWMutex erlaubt mehrere Leser oder einen Schreiber (besser für lese-lastige Workloads). sync.Once stellt sicher, dass Initialisierung genau einmal passiert (Singleton-Pattern). Bevorzuge Channels für Kommunikation, Mutexes für den Schutz gemeinsamen Zustands. 'Teile Speicher durch Kommunikation; kommuniziere nicht durch Speicher-Teilen.'

go
import "sync"

// Mutex: mutual exclusion lock
type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock() // always unlock with defer
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

// RWMutex: multiple readers, one writer
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()         // read lock (multiple allowed)
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

func (c *Cache) Set(key, val string) {
    c.mu.Lock()          // write lock (exclusive)
    defer c.mu.Unlock()
    c.data[key] = val
}

// sync.Once: run exactly once
var (
    once sync.Once
    instance *Database
)

func GetDB() *Database {
    once.Do(func() {
        instance = &Database{}
    })
    return instance
}

Nebenläufigkeits-Patterns

Worker-Pool: Eine feste Anzahl Goroutinen verarbeitet Jobs aus einem Channel und sendet Ergebnisse an einen anderen. Das begrenzt Nebenläufigkeit und verhindert Ressourcenerschöpfung. Fan-Out/Fan-In: Verteile Arbeit über Goroutinen, dann führe Ergebnisse zusammen. Die Kern-Erkenntnis: Schließe Channels, wenn keine Daten mehr gesendet werden, damit Range-Schleifen terminieren. Diese Patterns sind die Grundlage nebenläufigen Go.

go
// Worker pool
func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        time.Sleep(time.Second) // simulate work
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // Start 3 workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Send 5 jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)

    // Collect results
    for a := 1; a <= 5; a++ {
        fmt.Println(<-results)
    }
}

// Fan-out/Fan-in
func fanOut(input <-chan int, workers int) <-chan int {
    output := make(chan int)
    var wg sync.WaitGroup
    wg.Add(workers)
    for i := 0; i < workers; i++ {
        go func() {
            defer wg.Done()
            for v := range input {
                output <- v * 2
            }
        }()
    }
    go func() { wg.Wait(); close(output) }()
    return output
}
09

Datei-I/O & OS

Dateien lesen

os.ReadFile() liest eine gesamte Datei in den Speicher (einfach, aber nicht für große Dateien). Für große Dateien öffne mit os.Open(), lese in Chunks und defer immer Close(). bufio.Scanner liest Zeile für Zeile – ideal für Textverarbeitung. Prüfe immer Errors, besonders io.EOF, um zu wissen, wann das Lesen fertig ist.

go
import (
    "io"
    "os"
)

// Read entire file (small files)
data, err := os.ReadFile("input.txt")
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))

// Open and read (large files)
file, err := os.Open("large.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

// Read in chunks
buf := make([]byte, 1024)
for {
    n, err := file.Read(buf)
    if n > 0 {
        // process buf[:n]
    }
    if err == io.EOF {
        break
    }
    if err != nil {
        log.Fatal(err)
    }
}

// Read line by line
scanner := bufio.NewScanner(file)
for scanner.Scan() {
    line := scanner.Text()
    fmt.Println(line)
}

Dateien schreiben

os.WriteFile() erstellt/schneidet ab und schreibt atomar (einfach). os.OpenFile() mit Flags gibt Kontrolle: O_APPEND (ans Ende anfügen), O_CREATE (erstellen, falls fehlend), O_TRUNC (abschneiden). Verwende bufio.Writer für viele kleine Schreibvorgänge (puffert im Speicher, flush am Ende). Dateimodus 0644: Besitzer kann lesen/schreiben, andere können lesen.

go
// Write entire file (creates or truncates)
err := os.WriteFile("output.txt", []byte("Hello, World!"), 0644)
if err != nil {
    log.Fatal(err)
}

// Open for writing (with options)
file, err := os.OpenFile("log.txt",
    os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
    log.Fatal(err)
}
defer file.Close()

// Write
if _, err := file.WriteString("log entry\n"); err != nil {
    log.Fatal(err)
}

// Buffered writer (efficient for many small writes)
writer := bufio.NewWriter(file)
for i := 0; i < 1000; i++ {
    writer.WriteString(fmt.Sprintf("line %d\n", i))
}
writer.Flush() // don't forget to flush!

// File modes: 0644 = owner read/write, others read
// os.O_APPEND, O_CREATE, O_WRONLY, O_RDONLY, O_RDWR, O_TRUNC

Datei-Operationen

os.Mkdir/MkdirAll erstellen Verzeichnisse. os.ReadDir listet Einträge effizient. filepath.WalkDir durchläuft Verzeichnisse rekursiv. os.Stat gibt Datei-Info (Größe, Mod-Zeit, Berechtigungen). os.IsNotExist prüft, ob eine Datei fehlt. os.Remove löscht eine Datei; RemoveAll löscht Verzeichnisse rekursiv. Prüfe immer Errors.

go
// Create directory
os.Mkdir("mydir", 0755)       // one level
os.MkdirAll("a/b/c", 0755)    // nested directories

// List directory
entries, err := os.ReadDir(".")
if err != nil {
    log.Fatal(err)
}
for _, entry := range entries {
    fmt.Println(entry.Name(), entry.IsDir())
}

// Walk directory tree
filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
    if err != nil {
        return err
    }
    fmt.Println(path)
    return nil
})

// File info
info, _ := os.Stat("file.txt")
fmt.Println(info.Size())      // bytes
fmt.Println(info.ModTime())   // last modified
fmt.Println(info.IsDir())

// Check existence
if _, err := os.Stat("file.txt"); os.IsNotExist(err) {
    fmt.Println("file does not exist")
}

// Remove
os.Remove("file.txt")
os.RemoveAll("directory") // recursive

Umgebung & Kommandozeile

os.Getenv/LookupEnv/Setenv verwalten Umgebungsvariablen. os.Args gibt rohe Kommandozeilen-Argumente. Das flag-Paket bietet geparste Flags mit Defaults und Hilfetext. LookupEnv unterscheidet zwischen nicht gesetzten und leeren Werten. Umgebungsvariablen sind der Standardweg, um 12-Factor-Apps zu konfigurieren (API-Keys, Datenbank-URLs).

go
// Environment variables
os.Setenv("API_KEY", "secret")
key := os.Getenv("API_KEY") // "" if not set

// With existence check
if val, ok := os.LookupEnv("HOME"); ok {
    fmt.Println("HOME:", val)
}

// All environment variables
for _, e := range os.Environ() {
    fmt.Println(e)
}

// Command line arguments
// args := os.Args // []string, args[0] is program name

// Using flag package
var port int
var host string
flag.IntVar(&port, "port", 8080, "server port")
flag.StringVar(&host, "host", "localhost", "server host")
flag.Parse()
fmt.Printf("Running on %s:%d\n", host, port)

// Run: go run main.go -port 3000 -host 0.0.0.0

Befehle ausführen

os/exec führt externe Befehle aus. Command() erstellt den Befehl; Output() erfasst stdout; Run() führt mit benutzerdefiniertem Stdout/Stderr aus. Verwende CommandContext für Timeouts (tötet den Prozess). Prüfe immer Errors – exec.ExitError zeigt nicht-null Exit-Codes an. Sei vorsichtig mit Benutzereingaben, um Command-Injection zu verhindern.

go
import "os/exec"

// Run a command and get output
cmd := exec.Command("ls", "-la", "/tmp")
output, err := cmd.Output()
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(output))

// Capture stdout and stderr separately
cmd = exec.Command("git", "status")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
fmt.Println("stdout:", stdout.String())
fmt.Println("stderr:", stderr.String())

// Pipe input to command
cmd = exec.Command("grep", "error")
cmd.Stdin = strings.NewReader("info\nerror\nwarn\n")
result, _ := cmd.Output()
fmt.Println(string(result)) // "error"

// Run with context (timeout)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd = exec.CommandContext(ctx, "sleep", "10")
err = cmd.Run() // killed after 5s
10

JSON & Encoding

JSON Marshal & Unmarshal

encoding/json konvertiert zwischen Go-Structs und JSON. Marshal (Struct → JSON), Unmarshal (JSON → Struct). Struct-Tags (json:"name") kontrollieren Feldbenennung und Sichtbarkeit. omitempty überspringt Null/Leer-Werte. json:"-" schließt ein Feld vollständig aus. Das ist der Standardweg, um API-Anfragen/-Antworten in Go zu behandeln.

go
import "encoding/json"

type User struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email,omitempty"` // omit if empty
    Password string `json:"-"`               // never serialize
    Age      int    `json:"age,omitempty"`
}

// Struct to JSON (marshal)
user := User{ID: 1, Name: "Alice", Age: 30}
data, err := json.Marshal(user)
// {"id":1,"name":"Alice","age":30}

// Pretty print
pretty, _ := json.MarshalIndent(user, "", "  ")

// JSON to struct (unmarshal)
jsonStr := `{"id":2,"name":"Bob","email":"[email protected]"}`
var u User
err := json.Unmarshal([]byte(jsonStr), &u)
fmt.Println(u.Name) // "Bob"

// Struct tags control field names:
// json:"name"        -> rename
// json:"name,omitempty" -> skip if zero value
// json:"-"           -> skip entirely
// json:",omitempty"  -> keep name, skip if empty

JSON mit Maps & Slices

Für dynamisches JSON (unbekannte Struktur) unmarshal in map[string]any (oder interface{}). JSON-Zahlen werden zu float64 – Type-Assert für Zugriff. json.Decoder/Encoder arbeiten mit Streams (Dateien, HTTP-Bodies) effizient. Verwende Struct-Unmarshaling, wenn du das Schema kennst; verwende Maps für flexible/dynamische Daten.

go
// Parse arbitrary JSON into map[string]interface{}
jsonStr := `{"name":"Alice","scores":[90,85,92],"active":true}`

var result map[string]any
json.Unmarshal([]byte(jsonStr), &result)

// Type-assert to access values
name := result["name"].(string)
scores := result["scores"].([]any)
for _, s := range scores {
    fmt.Println(s.(float64)) // JSON numbers are float64
}

// Encode a map
m := map[string]any{
    "count": 42,
    "items": []string{"a", "b", "c"},
    "meta":  map[string]any{"page": 1},
}
data, _ := json.Marshal(m)

// Decode JSON stream
dec := json.NewDecoder(strings.NewReader(jsonStr))
var v map[string]any
dec.Decode(&v)

// Encode to stream
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", "  ")
enc.Encode(user)

Benutzerdefiniertes JSON-Marshaling

Implementiere MarshalJSON/UnmarshalJSON für benutzerdefinierte Serialisierung. Nützlich für: berechnete Felder, alternative Formate (Money als '99.99 USD'), sensibler Datenumgang und Zeitformatierung. Der Method-Receiver für UnmarshalJSON muss ein Pointer sein, um das Struct zu modifizieren. time.Time serialisiert automatisch als RFC 3339-Strings.

go
// Implement MarshalJSON/UnmarshalJSON
type Money struct {
    Amount   float64
    Currency string
}

func (m Money) MarshalJSON() ([]byte, error) {
    return json.Marshal(map[string]any{
        "amount":   m.Amount,
        "currency": m.Currency,
        "display":  fmt.Sprintf("%.2f %s", m.Amount, m.Currency),
    })
}

func (m *Money) UnmarshalJSON(data []byte) error {
    var v struct {
        Amount   float64
        Currency string
    }
    if err := json.Unmarshal(data, &v); err != nil {
        return err
    }
    m.Amount = v.Amount
    m.Currency = v.Currency
    return nil
}

money := Money{Amount: 99.99, Currency: "USD"}
data, _ := json.Marshal(money)
// {"amount":99.99,"currency":"USD","display":"99.99 USD"}

// Time formatting
type Event struct {
    Time time.Time `json:"time"`
}
// time.Time marshals as RFC 3339 by default

HTTP-Server

Das net/http-Paket baut HTTP-Server. http.HandleFunc registriert Handler. http.ResponseWriter schreibt die Antwort; *http.Request liest die Anfrage. Für JSON-APIs setze Content-Type und verwende json.NewEncoder(w).Encode(data). Die Standardbibliothek ist produktionsreif – kein Framework nötig für einfache APIs. Verwende Mux-Router (gorilla/mux, chi) für komplexes Routing.

go
import (
    "encoding/json"
    "net/http"
)

type Response struct {
    Status  string `json:"status"`
    Message string `json:"message"`
}

func main() {
    // Simple handler
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
    })

    // JSON API endpoint
    http.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) {
        user := User{ID: 1, Name: "Alice"}
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(user)
    })

    // Start server
    fmt.Println("Server running on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

// Request methods:
// r.Method  -> "GET", "POST", etc.
// r.URL.Query().Get("key") -> query params
// r.Body    -> request body (io.ReadCloser)

HTTP-Client

http.Get/Post sind bequeme Abkürzungen. Für benutzerdefinierte Header, Methoden oder Bodies verwende http.NewRequest + client.Do(). Defer immer resp.Body.Close(), um Verbindungslecks zu vermeiden. Setze einen Timeout auf den Client (Standard ist kein Timeout – gefährlich). Für Produktion wiederverwende http.Client (es connection-pooled) und verwende context für Abbruch.

go
// Simple GET
resp, err := http.Get("https://api.example.com/users")
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
fmt.Println(string(body))

// Custom request with headers
req, err := http.NewRequest("GET", "https://api.example.com/data", nil)
req.Header.Set("Authorization", "Bearer token123")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
defer resp.Body.Close()

// POST with JSON body
user := User{Name: "Alice"}
body, _ := json.Marshal(user)
resp, err := http.Post(
    "https://api.example.com/users",
    "application/json",
    bytes.NewBuffer(body),
)
defer resp.Body.Close()

// With timeout
client := &http.Client{Timeout: 10 * time.Second}
resp, err = client.Get("https://slow-api.example.com")
11

Testing & Benchmarking

Unit-Tests

Test-Dateien enden mit _test.go, Test-Funktionen beginnen mit Test. Table-Driven-Tests sind das idiomatische Pattern: Definiere Testfälle in einer Slice, schleife mit t.Run für Subtests (benannt, einzeln ausführbar). Verwende t.Errorf für Fehlschläge (fährt fort), t.Fatalf für fatal Fehlschläge (stoppt). Führe aus mit 'go test -v' für ausführliche Ausgabe.

go
// math_test.go (file must end with _test.go)
package math

import "testing"

// Test function: func TestXxx(t *testing.T)
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}

// Table-driven tests (idiomatic Go)
func TestAddTable(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 2, 3, 5},
        {"negative", -1, -1, -2},
        {"zero", 0, 0, 0},
        {"mixed", -5, 10, 5},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

// Run: go test -v
// Run specific: go test -run TestAddTable/positive

Benchmarks

Benchmark-Funktionen beginnen mit Benchmark und verwenden *testing.B. Die b.N-Schleife wird von der Runtime angepasst, um stabile Messungen zu erhalten. Führe aus mit 'go test -bench=.'. b.ResetTimer() schließt Setup-Zeit aus. b.ReportAllocs() zeigt Speicher-Allokationen. Vergleiche Implementierungen mit 'benchstat', um zu verifizieren, dass Verbesserungen signifikant sind.

go
// Benchmark function: func BenchmarkXxx(b *testing.B)
func BenchmarkFibonacci(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Fibonacci(20)
    }
}

// Benchmark with allocations
func BenchmarkStringConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := ""
        for j := 0; j < 100; j++ {
            s += "a"
        }
    }
}

// Run: go test -bench=.
// Output:
// BenchmarkFibonacci-8        300     4234567 ns/op
// BenchmarkStringConcat-8     200     6789012 ns/op   100 B/op   1 allocs/op

// Benchmark with setup
func BenchmarkProcess(b *testing.B) {
    data := setup() // setup not measured
    b.ResetTimer()  // start timing here
    for i := 0; i < b.N; i++ {
        process(data)
    }
}

// Memory allocations
func BenchmarkAllocs(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        make([]int, 100)
    }
}

Test-Helfer & Mocking

t.Helper() verbessert Fehlermeldungen, indem Helfer-Funktionen in Stack-Traces übersprungen werden. Mock durch Implementierung von Interfaces (Go's Ansatz für Mocking – kein Mock-Framework nötig). t.Cleanup() registriert Cleanup-Funktionen (wie defer, aber für Test-Scope). Für komplexes Mocking verwende testify/assert und mockery oder gomock zum Generieren von Mocks aus Interfaces.

go
// t.Helper() marks helper functions (better error locations)
func assertEqual(t *testing.T, got, want int) {
    t.Helper()
    if got != want {
        t.Errorf("got %d, want %d", got, want)
    }
}

// Interfaces for mocking
type DataStore interface {
    Get(key string) (string, error)
}

type MockStore struct {
    data map[string]string
}

func (m *MockStore) Get(key string) (string, error) {
    if v, ok := m.data[key]; ok {
        return v, nil
    }
    return "", errors.New("not found")
}

func TestService(t *testing.T) {
    store := &MockStore{data: map[string]string{"key": "value"}}
    svc := NewService(store)
    result := svc.GetValue("key")
    assertEqual(t, result, "value")
}

// Subtests with t.Run for setup/teardown
func TestWithCleanup(t *testing.T) {
    t.Cleanup(func() {
        // runs after test (LIFO order)
        os.Remove("tempfile")
    })
    // test code
}

Standardbibliothek-Highlights

Go's Standardbibliothek ist umfassend. time behandelt Daten/Uhrzeiten (Hinweis: Format verwendet Referenzzeit 2006-01-02). regexp für Pattern-Matching. context für Abbruch/Timeouts über Goroutine-Grenzen. sync.Pool für Objekt-Wiederverwendung (reduziert GC-Druck). Go 1.21+ fügt die slices- und maps-Pakete mit generischen Utilities hinzu.

go
// time package
now := time.Now()
future := now.Add(24 * time.Hour)
formatted := now.Format("2006-01-02 15:04:05")
parsed, _ := time.Parse("2006-01-02", "2024-01-15")

// regexp
re := regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)
matches := re.FindString("date: 2024-01-15")
all := re.FindAllString("2024-01-15 and 2024-02-20", -1)

// context (cancellation, timeouts)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go doWork(ctx)
// cancel() or timeout stops doWork

// sync.Pool (reuse objects)
var bufPool = sync.Pool{
    New: func() any { return new(bytes.Buffer) },
}
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)

// sort (Go 1.21+ slices package)
slices.Sort(nums)
slices.Contains(nums, 42)
slices.Reverse(nums)
12

Goroutinen Deep Dive

Goroutinen starten & WaitGroup

Goroutinen sind leichtgewichtige Threads, die von der Go-Runtime verwaltet werden (keine OS-Threads) – du kannst Hunderttausende erzeugen. sync.WaitGroup koordiniert den Abschluss von Goroutinen: Add(1) vor dem Start, Done() wenn fertig (mit defer), und Wait() zum Blockieren, bis der Zähler null erreicht. Übergib immer einen Pointer an die WaitGroup, damit alle Goroutinen denselben Zähler teilen.

go
package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // signal completion when function returns
    fmt.Printf("Worker %d started\n", id)
    // ... do work ...
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1) // increment counter before starting goroutine
        go worker(i, &wg)
    }

    wg.Wait() // block until all goroutines call Done()
    fmt.Println("All workers finished")
}

GOMAXPROCS & Scheduling

GOMAXPROCS kontrolliert, wie viele OS-Threads gleichzeitig Goroutinen ausführen – es ist standardmäßig die Anzahl der CPU-Kerne und fast immer optimal. Go verwendet M:N-Scheduling: Viele Goroutinen auf wenigen OS-Threads. Blockierende I/O- oder Channel-Operationen veranlassen den Scheduler, andere Goroutinen auf demselben Thread laufen zu lassen. runtime.Gosched() gibt explizit ab. Du musst GOMAXPROCS selten anpassen.

go
package main

import (
    "fmt"
    "runtime"
)

func main() {
    // GOMAXPROCS: number of OS threads available to run goroutines
    fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // defaults to CPU cores

    // Set it manually (rarely needed — default is optimal)
    runtime.GOMAXPROCS(2)

    // Goroutines are multiplexed onto OS threads (M:N scheduling)
    // - Blocking syscalls (I/O) don't block other goroutines
    // - The scheduler steals work across threads
    // - A goroutine blocked on channel/IO yields its thread

    // Number of goroutines currently running
    go func() { runtime.Gosched() }() // yield to other goroutines
    fmt.Println("Goroutines:", runtime.NumGoroutine())
}

Goroutinen-Lecks & Prävention

Ein Goroutinen-Leck tritt auf, wenn eine Goroutine für immer blockiert (z.B. Senden auf einem unbuffered Channel, den niemand liest) – sie wird nie garbage-collected. Verhindere Lecks durch: Puffern von Channels, Verwendung von select mit einem Abbruch-Channel/context und immer einen Ausstiegspfad bereitstellen. Leckende Goroutinen akkumulieren Speicher und CPU. Verwende runtime.NumGoroutine() und pprof, um Lecks in Produktion zu erkennen.

go
package main

// BAD: goroutine leaks — this goroutine never exits
func leaky() <-chan int {
    ch := make(chan int)
    go func() {
        ch <- 42 // blocks forever if nobody reads from ch
    }()
    return ch
}

// GOOD: use context for cancellation
func safe(ctx context.Context) <-chan int {
    ch := make(chan int, 1) // buffered — sender never blocks
    go func() {
        select {
        case ch <- 42:
        case <-ctx.Done(): // exit when context is cancelled
            return
        }
    }()
    return ch
}

// GOOD: always ensure senders can exit
// 1. Buffer the channel so send doesn't block
// 2. Use select with a done/cancel channel
// 3. Use context.WithCancel/WithTimeout for lifecycle control

sync.Once (Einmalige Initialisierung)

sync.Once stellt sicher, dass eine Funktion über alle Goroutinen hinweg genau einmal ausgeführt wird – der Standardweg, um threadsichere Singletons und Lazy Initialization zu implementieren. Es ist effizienter als Mutex-geschützte Flag-Prüfung. Die Do-Methode blockiert gleichzeitige Aufrufer, bis der erste Aufruf abgeschlossen ist. sync.Once wird auch intern in vielen Standardbibliothek-Patterns wie sync.OnceValue (Go 1.21+) verwendet.

go
package main

import (
    "fmt"
    "sync"
)

var (
    instance *Database
    once     sync.Once
)

type Database struct{ name string }

func GetDB() *Database {
    // sync.Once guarantees the function runs exactly once,
    // even if called from many goroutines simultaneously
    once.Do(func() {
        instance = &Database{name: "production"}
        fmt.Println("Database initialized")
    })
    return instance
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            GetDB() // "Database initialized" prints only once
        }()
    }
    wg.Wait()
}

Worker-Pool mit Goroutinen

Das Worker-Pool-Pattern begrenzt Nebenläufigkeit auf eine feste Anzahl Goroutinen, die Jobs aus einem Channel verarbeiten. Das verhindert Ressourcenerschöpfung durch unbegrenzte Goroutinen-Erzeugung. Worker verwenden 'for job := range jobs', was beim Schließen des Channels beendet. close(jobs) signalisiert allen Workern zu stoppen. Das ist das kanonische Go-Nebenläufigkeits-Pattern für begrenzte Parallelität.

go
package main

import (
    "fmt"
    "sync"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs { // exits when jobs channel is closed
        results <- job * job // process and send result
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)
    var wg sync.WaitGroup

    // Start 3 workers
    for w := 1; w <= 3; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // Send 5 jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs) // signal workers to stop (range loop exits)

    wg.Wait()
    close(results)

    for r := range results {
        fmt.Println("Result:", r)
    }
}
13

Channels & Select

Channel-Grundlagen (Unbuffered & Buffered)

Unbuffered Channels (make(chan T)) synchronisieren Sender und Empfänger – das Senden blockiert, bis ein Empfänger bereit ist. Buffered Channels (make(chan T, n)) erlauben n Sends ohne Empfänger und entkoppeln Sender/Empfänger-Timing. Nur der Sender sollte einen Channel schließen (um 'keine Werte mehr' zu signalisieren). Empfangen von einem geschlossenen Channel gibt den Nullwert mit ok=false zurück. Range-Schleifen beenden, wenn der Channel geschlossen ist.

go
package main

import "fmt"

func main() {
    // Unbuffered: send blocks until someone receives (synchronous)
    ch := make(chan int)
    go func() {
        ch <- 42 // blocks until main reads
    }()
    fmt.Println(<-ch) // 42

    // Buffered: send blocks only when buffer is full
    buf := make(chan int, 3)
    buf <- 1 // doesn't block (buffer has space)
    buf <- 2
    buf <- 3
    // buf <- 4 // would block — buffer full
    fmt.Println(<-buf) // 1 (FIFO)

    // Close a channel (sender closes, never the receiver)
    close(buf)

    // Check if closed (ok is false when closed and empty)
    val, ok := <-buf
    fmt.Println(val, ok) // 2 true

    // Range over channel until closed
    nums := make(chan int, 3)
    nums <- 10; nums <- 20; nums <- 30
    close(nums)
    for n := range nums {
        fmt.Println(n) // 10, 20, 30
    }
}

Select-Anweisung

select lässt eine Goroutine gleichzeitig auf mehrere Channel-Operationen warten – es wählt den ersten bereiten Case aus (zufällig, wenn mehrere bereit sind). Der default-Case macht select nicht-blockierend. time.After erstellt einen Timeout-Channel. select ist das Herz von Go's Nebenläufigkeits-Koordination: Multiplexing, Timeouts, Abbruch und Fan-In/Fan-Out-Patterns bauen alle darauf auf.

go
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "from ch1"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "from ch2"
    }()

    // select waits on multiple channel operations
    // picks the first one that's ready (random if multiple ready)
    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch1:
            fmt.Println(msg)
        case msg := <-ch2:
            fmt.Println(msg)
        }
    }

    // Non-blocking receive with default
    select {
    case msg := <-ch1:
        fmt.Println(msg)
    default:
        fmt.Println("no message ready") // runs immediately
    }

    // Timeout with time.After
    select {
    case msg := <-ch1:
        fmt.Println(msg)
    case <-time.After(500 * time.Millisecond):
        fmt.Println("timed out")
    }
}

Fan-In & Fan-Out-Patterns

Fan-Out verteilt Arbeit über mehrere Goroutinen für Parallelität; Fan-In führt mehrere Channels zu einem zusammen. Zusammen bilden sie eine parallele Pipeline: Fan-Out-Worker verarbeiten unabhängig, dann sammelt Fan-In die Ergebnisse. Das Fan-In verwendet eine WaitGroup, um den zusammengeführten Channel erst zu schließen, nachdem alle Eingabe-Channels erschöpft sind. Diese Patterns sind fundamental für Go's nebenläufige Datenverarbeitung.

go
package main

import (
    "fmt"
    "sync"
)

// Fan-Out: distribute work across multiple goroutines
func fanOut(input <-chan int, n int) []<-chan int {
    outputs := make([]<-chan int, n)
    for i := 0; i < n; i++ {
        outputs[i] = process(input) // each worker reads from same input
    }
    return outputs
}

func process(input <-chan int) <-chan int {
    output := make(chan int)
    go func() {
        defer close(output)
        for v := range input {
            output <- v * v // transform
        }
    }()
    return output
}

// Fan-In: merge multiple channels into one
func fanIn(channels ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    merged := make(chan int)

    output := func(c <-chan int) {
        defer wg.Done()
        for v := range c {
            merged <- v
        }
    }

    wg.Add(len(channels))
    for _, c := range channels {
        go output(c)
    }

    go func() {
        wg.Wait()
        close(merged)
    }()
    return merged
}

Gerichtete Channels (Nur-Senden/Nur-Empfang)

Gerichtete Channel-Typen (<-chan T für nur-Empfang, chan<- T für nur-Senden) erzwingen Channel-Nutzung zur Compile-Zeit. Ein bidirektionaler Channel konvertiert implizit zu einem gerichteten Typ, wenn er an eine Funktion übergeben wird. Das dokumentiert Intent und verhindert Bugs – eine Producer-Funktion kann buchstäblich nicht von ihrem eigenen Ausgabe-Channel empfangen. Verwende gerichtete Typen in Funktionssignaturen, um Verträge klar zu machen.

go
package main

import "fmt"

// <-chan int: receive-only (can read, cannot send)
// chan<- int: send-only (can send, cannot read)
func producer(out chan<- int) { // can only send
    for i := 0; i < 3; i++ {
        out <- i
    }
    close(out)
}

func consumer(in <-chan int) { // can only receive
    for v := range in {
        fmt.Println("consumed:", v)
    }
}

func main() {
    ch := make(chan int)

    // Directional restrictions are enforced at compile time
    go producer(ch) // bidirectional chan converts to send-only
    consumer(ch)    // bidirectional chan converts to receive-only

    // This prevents bugs: producer can't accidentally read,
    // consumer can't accidentally send or close
}

Ticker & Timer-Channels

time.Ticker feuert wiederholt in Intervallen – verwende ticker.C als Channel in select für periodische Aufgaben. time.Timer feuert einmal nach einer Dauer. Rufe immer Stop() auf Tickern und Timern auf, um Ressourcen freizugeben und Lecks zu vermeiden. time.After ist ein bequemer Einweg-Timer, der einen Channel zurückgibt (kann aber nicht abgebrochen werden, also bevorzuge NewTimer in select-Schleifen, um Akkumulation zu vermeiden). Reset lässt dich einen Timer neu planen.

go
package main

import (
    "fmt"
    "time"
)

func main() {
    // Ticker: fires repeatedly at intervals
    ticker := time.NewTicker(500 * time.Millisecond)
    defer ticker.Stop() // always stop to release resources

    go func() {
        for t := range ticker.C { // ticker.C is a channel
            fmt.Println("Tick at", t)
        }
    }()

    // Timer: fires once after a duration
    timer := time.NewTimer(2 * time.Second)
    <-timer.C // blocks until timer fires
    fmt.Println("Timer fired!")

    // time.After: one-shot timer as a channel (no Stop needed)
    select {
    case <-time.After(1 * time.Second):
        fmt.Println("1 second elapsed")
    }

    // Reset a timer (cancel and reschedule)
    timer2 := time.NewTimer(5 * time.Second)
    timer2.Reset(100 * time.Millisecond)
    <-timer2.C
}
14

Context-Paket

context.WithCancel & WithTimeout

context.Context trägt Abbruch, Timeouts und anfragespezifische Werte über Goroutine-Grenzen. WithCancel gibt einen Context und eine Cancel-Funktion zurück; WithTimeout bricht automatisch nach einer Dauer ab. Goroutinen prüfen ctx.Done() (einen Channel) in einem select, um zu wissen, wann sie stoppen sollen. Rufe IMMER die Cancel-Funktion auf (mit defer), um Ressourcen freizugeben, selbst wenn der Timeout feuert – sonst leckt der Context.

go
package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done(): // cancelled or timed out
            fmt.Printf("Worker %d stopped: %v\n", id, ctx.Err())
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    // WithCancel: manual cancellation
    ctx, cancel := context.WithCancel(context.Background())
    go worker(ctx, 1)
    time.Sleep(2 * time.Second)
    cancel() // stop the worker
    time.Sleep(500 * time.Millisecond)

    // WithTimeout: auto-cancel after duration
    ctx2, cancel2 := context.WithTimeout(context.Background(), 1500*time.Millisecond)
    defer cancel2() // always call cancel to release resources
    go worker(ctx2, 2)
    time.Sleep(2 * time.Second) // worker stops after 1.5s
}

Context durch Aufrufe propagieren

Context sollte der erste Parameter jeder Funktion sein, die I/O macht, und sollte durch die gesamte Aufrufkette propagiert werden. http.Request.Context() wird automatisch abgebrochen, wenn der Client die Verbindung trennt. Das Übergeben von ctx an Datenbank-/HTTP-Operationen (QueryRowContext, NewRequestWithContext) stellt sicher, dass sie abbrechen, wenn der Context abgebrochen wird – verhindert verschwendete Arbeit und Ressourcenlecks. Speichere niemals Contexts in Structs.

go
package main

import (
    "context"
    "database/sql"
    "net/http"
)

// Context should be the FIRST parameter, named ctx
func GetUser(ctx context.Context, db *sql.DB, id int) (string, error) {
    // Pass ctx to all blocking operations so they cancel together
    var name string
    err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id=?", id).Scan(&name)
    return name, err
}

func handler(w http.ResponseWriter, r *http.Request) {
    // r.Context() is cancelled when the client disconnects
    ctx := r.Context()

    name, err := GetUser(ctx, db, 42)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    w.Write([]byte(name))
}

// BEST PRACTICE: pass context through every function in the call chain
// that might do I/O. This ensures a client disconnect or timeout
// cancels ALL in-flight work (DB queries, HTTP calls, etc.)

Context-Werte (Anfragespezifische Daten)

context.WithValue speichert anfragespezifische Daten (wie User-IDs, Trace-IDs, Auth-Tokens), die durch die Aufrufkette fließen. Verwende einen benutzerdefinierten Schlüsseltyp (keinen String), um Schlüsselkollisionen zu vermeiden. Werte sollten Daten sein, die die Anfrage braucht, keine Funktionsparameter – das Go-Team empfiehlt, es sparsam zu verwenden, hauptsächlich für Querschnittsthemen wie Tracing/Auth. Type-Assert immer beim Abrufen von Werten.

go
package main

import (
    "context"
    "fmt"
    "net/http"
)

// Define a custom key type to avoid collisions
type contextKey string

const userIDKey contextKey = "userID"

// Set a value in context
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        userID := 42 // authenticate...
        // WithValue creates a new context with the value
        ctx := context.WithValue(r.Context(), userIDKey, userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Get a value from context (must type-assert)
func getUserID(ctx context.Context) (int, bool) {
    if v, ok := ctx.Value(userIDKey).(int); ok {
        return v, true
    }
    return 0, false
}

func handler(w http.ResponseWriter, r *http.Request) {
    if uid, ok := getUserID(r.Context()); ok {
        fmt.Fprintf(w, "User ID: %d", uid)
    }
}

Graceful Shutdown mit Context

Graceful Shutdown lässt laufende Anfragen abschließen, bevor der Server beendet wird. signal.Notify erfasst OS-Signale (Ctrl+C, SIGTERM von Container-Orchestratoren). server.Shutdown(ctx) stoppt das Akzeptieren neuer Verbindungen und wartet, bis aktive fertig sind (bis zum Context-Timeout). Essenziell für Produktionsserver – ohne es werden aktive Anfragen abrupt abgebrochen, was Errors und Datenkorruption verursacht.

go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    server := &http.Server{Addr: ":8080", Handler: mux}

    // Listen for OS interrupt signals (Ctrl+C, SIGTERM)
    stop := make(chan os.Signal, 1)
    signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        log.Println("Server starting on :8080")
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()

    <-stop // block until signal received
    log.Println("Shutting down...")

    // Give in-flight requests 10 seconds to finish
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Printf("Forced shutdown: %v", err)
    }
    log.Println("Server stopped gracefully")
}

Context-Deadline & Fehlerbehandlung

context.WithDeadline bricht zu einer absoluten Zeit ab; WithTimeout nach einer relativen Dauer (WithTimeout ist einfach WithDeadline(now+timeout)). ctx.Err() gibt context.DeadlineExceeded oder context.Canceled zurück, damit du unterscheiden kannst, warum es stoppte. Verwende errors.Is(), um Context-Errors zu prüfen. Prüfe immer ctx.Err() am Anfang langer Operationen und verwende select mit ctx.Done() während blockierender Wartezeiten.

go
package main

import (
    "context"
    "errors"
    "fmt"
    "time"
)

func slowOperation(ctx context.Context) (string, error) {
    // Check if already cancelled before starting
    if err := ctx.Err(); err != nil {
        return "", err
    }

    select {
    case <-time.After(3 * time.Second): // simulate slow work
        return "result", nil
    case <-ctx.Done(): // cancelled or deadline exceeded
        return "", ctx.Err()
    }
}

func main() {
    // Deadline: cancel at a specific time
    deadline := time.Now().Add(1 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()

    result, err := slowOperation(ctx)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            fmt.Println("Operation timed out")
        } else if errors.Is(err, context.Canceled) {
            fmt.Println("Operation was cancelled")
        }
        return
    }
    fmt.Println(result)
}
15

HTTP-Server & Client

HTTP-Server (net/http)

net/http bietet einen produktionsreifen HTTP-Server. http.HandleFunc registriert Handler nach Pfad. Verwende json.NewEncoder(w).Encode() für JSON-Antworten und json.NewDecoder(r.Body).Decode() zum Parsen von Anfrage-Bodies. Setze immer Content-Type-Header und prüfe r.Method. http.Error sendet einen Error-Status. ListenAndServe startet den Server; wrappe mit log.Fatal, um Errors abzufangen.

go
package main

import (
    "encoding/json"
    "log"
    "net/http"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func getUser(w http.ResponseWriter, r *http.Request) {
    user := User{ID: 1, Name: "Alice"}

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    user.ID = 99
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func main() {
    http.HandleFunc("/users/1", getUser)
    http.HandleFunc("/users", createUser)

    log.Println("Server on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

HTTP-Client & Requests

Der Standard-http.Client hat KEIN Timeout – setze immer eines, um nicht für immer an nicht antwortenden Servern hängen zu bleiben. Verwende http.NewRequestWithContext, um einen Context für Abbruch/Timeouts anzuhängen. Defer IMMER resp.Body.Close(), um Verbindungslecks zu vermeiden. Für Produktion wiederverwende einen einzelnen http.Client (er verwaltet Connection-Pooling). http.Get ist eine Abkürzung, hat aber kein Timeout und keine Anpassung.

go
package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

func main() {
    // Simple GET
    resp, err := http.Get("https://api.example.com/users")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close() // ALWAYS close the body

    var users []map[string]any
    json.NewDecoder(resp.Body).Decode(&users)
    fmt.Println(users)

    // Custom client with timeout (default has NO timeout!)
    client := &http.Client{Timeout: 30 * time.Second}

    // POST with JSON body and context
    body, _ := json.Marshal(map[string]string{"name": "Bob"})
    req, _ := http.NewRequestWithContext(context.Background(),
        "POST", "https://api.example.com/users", bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")

    resp, err = client.Do(req)
    defer resp.Body.Close()
    fmt.Println("Status:", resp.Status)
}

Middleware-Pattern

Middleware wrappt Handler, um Querschnittsthemen (Logging, Auth, CORS, Rate-Limiting) hinzuzufügen, ohne den Handler selbst zu modifizieren. Die Signatur func(http.Handler) http.Handler ist der Standard-Middleware-Typ. Chain wendet sie in Reihenfolge an (äußere zuerst). Dieses Pattern ist die Grundlage von Frameworks wie Chi, Echo und Gin. Der ResponseWriter kann gewrappt werden, um Status-Codes für Logging zu erfassen.

go
package main

import (
    "log"
    "net/http"
    "time"
)

// Middleware wraps an http.Handler to add cross-cutting concerns
type Middleware func(http.Handler) http.Handler

func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r) // call the wrapped handler
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

func Auth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Header.Get("Authorization") == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Chain multiple middlewares
func Chain(h http.Handler, mws ...Middleware) http.Handler {
    for i := len(mws) - 1; i >= 0; i-- {
        h = mws[i](h)
    }
    return h
}

func main() {
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello!"))
    })
    // Apply middlewares: Auth -> Logging -> handler
    http.Handle("/", Chain(handler, Logging, Auth))
    http.ListenAndServe(":8080", nil)
}

Statische Dateien & Templates servieren

http.FileServer serviert statische Dateien aus einem Verzeichnis; http.StripPrefix entfernt den Pfad-Präfix, damit Dateipfade korrekt auflösen. html/template rendert HTML sicher (auto-escapet, um XSS zu verhindern). template.Must panikt bei Parse-Fehlern (in Ordnung für Startup). Templates verwenden {{.Field}} für Daten und {{range}} für Iteration. Für Produktion ziehe in Betracht, Dateien mit go:embed einzubetten statt von Festplatte zu lesen.

go
package main

import (
    "html/template"
    "net/http"
)

func main() {
    // Serve static files (CSS, JS, images)
    fs := http.FileServer(http.Dir("./static"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))

    // HTML templates
    tmpl := template.Must(template.ParseFiles("templates/index.html"))

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        data := struct {
            Title string
            Items []string
        }{
            Title: "My Page",
            Items: []string{"Apple", "Banana", "Cherry"},
        }
        tmpl.Execute(w, data)
    })

    http.ListenAndServe(":8080", nil)
}

// templates/index.html:
// <h1>{{.Title}}</h1>
// <ul>{{range .Items}}<li>{{.}}</li>{{end}}</ul>

Graceful Shutdown & go:embed

go:embed bündelt Dateien zur Build-Zeit in das kompilierte Binary – ermöglicht echte Single-Binary-Deployments ohne externe Datei-Abhängigkeiten. Verwende //go:embed-Direktiven über einer var-Deklaration. embed.FS ist ein Read-Only virtuelles Dateisystem. fs.Sub erstellt ein Sub-Dateisystem (entfernt den Verzeichnis-Präfix). Funktioniert für statische Assets, HTML-Templates, SQL-Migrationen, Config-Dateien und mehr.

go
package main

import (
    "embed"
    "io/fs"
    "net/http"
)

//go:embed static/*
var staticFiles embed.FS

func main() {
    // Embed static files into the binary (no external files needed)
    // Access the embedded filesystem
    sub, _ := fs.Sub(staticFiles, "static")
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(sub))))

    // go:embed benefits:
    // - Single binary deployment (no external files)
    // - Files are compiled into the executable
    // - Works with templates, configs, migrations, etc.

    // Embed a single file
    //go:embed config.json
    // var configData []byte

    // Embed with pattern
    //go:embed templates/*.html
    // var templates embed.FS

    http.ListenAndServe(":8080", nil)
}
16

Sync-Paket

sync.Mutex & sync.RWMutex

sync.Mutex bietet exklusives Locking – nur eine Goroutine kann es gleichzeitig halten. sync.RWMutex erlaubt mehrere gleichzeitige Leser ODER einen exklusiven Schreiber – verwende es, wenn Lesezugriffe Schreibzugriffe weit übersteigen. Paare immer Lock mit defer Unlock, um Deadlocks zu vermeiden, wenn die Funktion panikt. Bette den Mutex in das Struct ein (kleingeschrieben mu), um ihn privat zu halten. Kopiere niemals einen Mutex (verwende immer Pointer).

go
package main

import (
    "fmt"
    "sync"
)

type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock() // always unlock with defer
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

// RWMutex: allows multiple readers OR one writer
type SafeCache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *SafeCache) Get(key string) (string, bool) {
    c.mu.RLock()         // read lock (multiple readers OK)
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

func (c *SafeCache) Set(key, val string) {
    c.mu.Lock()          // write lock (exclusive)
    defer c.mu.Unlock()
    c.data[key] = val
}

sync.Map (Nebenläufige Map)

sync.Map ist eine nebenläufigkeitssichere Map, optimiert für spezifische Szenarien: Write-Once-Read-Many (Caches) oder Disjoint-Key-Zugriff über Goroutinen. Sie vermeidet Locks bei Lesezugriffen über atomare Operationen. Für allgemeine nebenläufige Maps ist jedoch eine reguläre Map, geschützt durch sync.RWMutex, oft schneller und ergonomischer. sync.Map's API verwendet any (interface{}) für Schlüssel und Werte und verliert Typsicherheit.

go
package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map

    // Store and Load (thread-safe, no locks needed)
    m.Store("name", "Alice")
    m.Store("age", 30)

    val, ok := m.Load("name")
    fmt.Println(val, ok) // Alice true

    // LoadOrStore: atomic get-or-set
    actual, loaded := m.LoadOrStore("name", "Bob")
    fmt.Println(actual, loaded) // Alice true (already existed)

    // Delete
    m.Delete("age")

    // Range over all entries
    m.Range(func(key, value any) bool {
        fmt.Printf("%v = %v\n", key, value)
        return true // continue; return false to stop
    })

    // Use sync.Map when:
    // 1. Keys are written once, read many times (caches)
    // 2. Multiple goroutines read/write disjoint keys
    // For most cases, a regular map + Mutex is simpler and faster
}

sync.Cond (Bedingungsvariablen)

sync.Cond implementiert Bedingungsvariablen – Goroutinen warten darauf, dass eine Bedingung wahr wird. Wait() gibt atomar das Lock frei und schläft; Signal() weckt einen Wartenden, Broadcast() alle. Verwende immer eine for-Schleife um Wait() (nicht if), um spurious Wakeups zu behandeln. Cond ist nützlich für Producer-Consumer-Queues und das Warten auf Zustandsänderungen, obwohl Channels oft eine einfachere Alternative bieten.

go
package main

import (
    "fmt"
    "sync"
    "time"
)

type Queue struct {
    items []int
    mu    sync.Mutex
    cond  *sync.Cond
}

func NewQueue() *Queue {
    q := &Queue{}
    q.cond = sync.NewCond(&q.mu) // cond is tied to the mutex
    return q
}

func (q *Queue) Put(item int) {
    q.mu.Lock()
    defer q.mu.Unlock()
    q.items = append(q.items, item)
    q.cond.Signal() // wake one waiting goroutine
}

func (q *Queue) Get() int {
    q.mu.Lock()
    defer q.mu.Unlock()
    for len(q.items) == 0 {
        q.cond.Wait() // atomically unlocks mu, sleeps, re-locks on wake
    }
    item := q.items[0]
    q.items = q.items[1:]
    return item
}

func main() {
    q := NewQueue()
    go func() {
        time.Sleep(1 * time.Second)
        q.Put(42)
    }()
    fmt.Println("Waiting for item...")
    fmt.Println("Got:", q.Get()) // blocks until item available
}

sync.Pool (Objekt-Wiederverwendung)

sync.Pool wiederverwendet Objekte, um Allokationen und GC-Druck zu reduzieren – ideal für häufig allokiierte kurzlebige Objekte wie bytes.Buffer. Get() gibt ein gepooltes Objekt zurück (ruft New auf, wenn leer); Put() gibt es zurück. Pools werden während GC geleert, also verlasse dich nicht auf sie für Persistenz. Setze Objekte immer vor der Wiederverwendung zurück. Die Standardbibliothek verwendet sync.Pool extensiv (http, json, fmt).

go
package main

import (
    "bytes"
    "sync"
)

var bufPool = sync.Pool{
    New: func() any {
        return new(bytes.Buffer) // create when pool is empty
    },
}

func Process(data []byte) string {
    // Get a buffer from the pool (or create via New)
    buf := bufPool.Get().(*bytes.Buffer)
    defer bufPool.Put(buf) // return it when done

    buf.Reset() // clear before reuse
    buf.Write(data)
    buf.WriteString("-processed")
    return buf.String()
}

// Benefits:
// - Reduces GC pressure by reusing objects
// - Avoids allocation overhead for short-lived objects
// - Pool entries can be garbage collected between GC cycles
//
// Use for: bytes.Buffer, json encoders, temporary slices
// NOT for: long-lived objects or when size is unpredictable

errgroup (Gruppe mit Error-Propagation)

errgroup (von golang.org/x/sync) ist eine WaitGroup, die den ersten Error zurückgibt und verbleibende Goroutinen über Context abbrechen kann. g.Go() startet eine Goroutine; g.Wait() blockiert und gibt den ersten nicht-nil Error zurück. WithContext erstellt einen Context, der abgebrochen wird, wenn eine Goroutine einen Error zurückgibt – sodass andere Goroutinen frühzeitig stoppen. Das ist der idiomatische Weg, parallele Operationen auszuführen, die alle zusammen erfolgreich sein oder fehlschlagen sollten.

go
package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/errgroup"
    "net/http"
)

// errgroup is like WaitGroup but propagates the first error
// and can cancel remaining goroutines on failure
func fetchAll(urls []string) ([]*http.Response, error) {
    g, ctx := errgroup.WithContext(context.Background())
    results := make([]*http.Response, len(urls))

    for i, url := range urls {
        i, url := i, url // capture loop variables
        g.Go(func() error {
            req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
            resp, err := http.DefaultClient.Do(req)
            if err != nil {
                return err // cancels ctx, stopping other goroutines
            }
            results[i] = resp
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        return nil, err // first error returned
    }
    return results, nil
}

func main() {
    urls := []string{
        "https://example.com",
        "https://golang.org",
    }
    resps, err := fetchAll(urls)
    fmt.Println(len(resps), err)
}
17

Generics (Go 1.18+)

Generische Funktionen & Typparameter

Go-Generics (1.18+) verwenden Typparameter in eckigen Klammern: [T any] deklariert T als beliebigen Typ. Der Compiler leitet Typen aus Argumenten ab, sodass du sie selten explizit angibst. Generics ermöglichen typsichere wiederverwendbare Funktionen wie Map/Filter/Reduce ohne interface{} und Type-Assertions. 'any' ist ein Alias für interface{}, der mit Generics eingeführt wurde.

go
package main

import "fmt"

// Type parameters in brackets: [T any] means T can be any type
func Map[T, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

func Filter[T any](slice []T, predicate func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result
}

func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
    result := initial
    for _, v := range slice {
        result = fn(result, v)
    }
    return result
}

func main() {
    nums := []int{1, 2, 3, 4, 5}
    squared := Map(nums, func(n int) int { return n * n })
    fmt.Println(squared) // [1 4 9 16 25]

    evens := Filter(nums, func(n int) bool { return n%2 == 0 })
    fmt.Println(evens) // [2 4]

    sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
    fmt.Println(sum) // 15

    // Works with any type
    words := []string{"go", "rust", "python"}
    lengths := Map(words, func(s string) int { return len(s) })
    fmt.Println(lengths) // [2 4 6]
}

Type-Constraints & comparable

Type-Constraints beschränken, welche Typen ein Generic akzeptiert. 'comparable' ist eine eingebaute Constraint für Typen, die == unterstützen (nötig für Map-Keys und Vergleiche). Benutzerdefinierte Constraints verwenden Type-Unions (int | float64), um spezifische Typen zu erlauben. Das golang.org/x/exp/constraints-Paket bietet Ordered (für <, >, usw.). Constraints können Type-Sets und Methoden kombinieren.

go
package main

import "fmt"

// comparable: types that support == and != (built-in constraint)
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

// Custom constraint with a type set
type Number interface {
    int | int64 | float64 | float32
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

// Multiple type parameters
func Pair[T, U any](first T, second U) struct{ First T; Second U } {
    return struct{ First T; Second U }{first, second}
}

func main() {
    fmt.Println(Contains([]string{"a", "b", "c"}, "b")) // true
    fmt.Println(Contains([]int{1, 2, 3}, 5))             // false

    fmt.Println(Sum([]int{1, 2, 3}))           // 6
    fmt.Println(Sum([]float64{1.5, 2.5}))      // 4

    p := Pair("Alice", 30)
    fmt.Println(p) // {Alice 30}
}

Generische Datenstrukturen

Generische Typen (Stack[T any]) erstellen typsichere Datenstrukturen, die mit jedem Typ arbeiten, ohne interface{}-Boxing oder Type-Assertions. Der Typparameter T ist Teil des Typs, also sind Stack[int] und Stack[string] unterschiedliche, zur Compile-Zeit geprüfte Typen. Das eliminiert eine ganze Klasse von Runtime-Typfehlern. Generische Structs, Methoden und Interfaces unterstützen alle Typparameter.

go
package main

import "fmt"

// Generic Stack — works with any type, type-safe
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    index := len(s.items) - 1
    item := s.items[index]
    s.items = s.items[:index]
    return item, true
}

func (s *Stack[T]) Len() int {
    return len(s.items)
}

func main() {
    // Type-safe stack of ints
    intStack := &Stack[int]{}
    intStack.Push(1)
    intStack.Push(2)
    val, _ := intStack.Pop()
    fmt.Println(val) // 2

    // Type-safe stack of strings
    strStack := &Stack[string]{}
    strStack.Push("hello")
    s, _ := strStack.Pop()
    fmt.Println(s) // hello

    // intStack.Push("oops") // compile error! type-safe
}

Generische Constraints mit ~ (Underlying Type)

Das ~-Präfix in einer Constraint matcht jeden Typ, dessen Underlying Type der benannte Typ ist – also matcht ~string sowohl string als auch type MyString string. Ohne ~ matcht eine Constraint nur den exakten benannten Typ, was für benutzerdefinierte Typen selten nützlich ist. Verwende ~, wenn du willst, dass Generics mit Type-Aliasen und benannten Typen funktionieren, die von Primitives abgeleitet sind (häufig in Domain Modeling).

go
package main

import "fmt"

// ~ allows any type whose UNDERLYING type matches
// (e.g., type MyString string would match ~string)
type StringLike interface {
    ~string
}

func Join[S StringLike](items []S, sep string) string {
    result := ""
    for i, item := range items {
        if i > 0 {
            result += sep
        }
        result += string(item) // convert underlying type to string
    }
    return result
}

// Numeric constraint with ~
type Integer interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

func Double[T Integer](v T) T {
    return v * 2
}

// Custom type based on int
type Score int

func main() {
    type MyString string
    items := []MyString{"a", "b", "c"}
    fmt.Println(Join(items, "-")) // a-b-c

    s := Double(Score(21))
    fmt.Println(s) // 42
}

Generics vs Interfaces (Wann verwenden)

Wähle Generics, wenn der Algorithmus über Typen hinweg identisch ist (Collections, Math, Transforms) – sie bieten Compile-Zeit-Typsicherheit ohne Runtime-Overhead. Wähle Interfaces, wenn verschiedene Typen verschiedene Implementierungen brauchen (Polymorphismus) oder wenn du Runtime-Dispatch brauchst (Dependency Injection, Mocking). Sie sind komplementär: Generics können durch Interfaces beschränkt werden (T Stringer) für das Beste aus beiden Welten.

go
package main

import "fmt"

// GENERICS: best for algorithms/data structures that work
// the same way regardless of type (collections, sorting, mapping)
func Max[T int | float64](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// INTERFACES: best when different types need DIFFERENT implementations
// (polymorphism, dependency injection, mocking)
type Stringer interface {
    String() string
}

type Dog struct{ Name string }
func (d Dog) String() string { return "Dog: " + d.Name }

type Cat struct{ Name string }
func (c Cat) String() string { return "Cat: " + c.Name }

func Print[T Stringer](v T) {
    fmt.Println(v.String())
}

func main() {
    fmt.Println(Max(3, 7))       // 7
    fmt.Println(Max(3.14, 2.71)) // 3.14

    Print(Dog{"Rex"}) // Dog: Rex
    Print(Cat{"Whiskers"}) // Cat: Whiskers
}

// RULE OF THUMB:
// - Same logic, different types -> Generics
// - Different logic, shared contract -> Interfaces
// - Need runtime polymorphism -> Interfaces
// - Need compile-time type safety -> Generics
18

Reflection

reflect.Type & reflect.Value

reflect.TypeOf gibt den Runtime-Typ zurück; reflect.ValueOf den Runtime-Wert. Von einem Type kannst du Struct-Felder, Methoden und Tags inspizieren. Von einem Value kannst du Felder lesen und (mit CanSet) modifizieren. Reflection ist mächtig, aber langsam (10-100x langsamer als direkter Zugriff) und umgeht Compile-Zeit-Typsicherheit – verwende sie sparsam, hauptsächlich für Serialisierung, ORMs und Frameworks.

go
package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    u := User{Name: "Alice", Age: 30}

    // Get type and value at runtime
    t := reflect.TypeOf(u)
    v := reflect.ValueOf(u)

    fmt.Println("Type:", t.Name())        // User
    fmt.Println("Kind:", t.Kind())         // struct
    fmt.Println("NumFields:", t.NumField())

    // Iterate struct fields
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        value := v.Field(i)
        tag := field.Tag.Get("json")
        fmt.Printf("%s (%s) = %v [json:%s]\n",
            field.Name, field.Type, value, tag)
    }
    // Name (string) = Alice [json:name]
    // Age (int) = 30 [json:age]
}

Werte mit Reflection modifizieren

Um einen Wert über Reflection zu modifizieren, musst du einen Pointer übergeben und .Elem() aufrufen, um zu dereferenzieren. CanSet() meldet, ob ein Feld zuweisbar ist (exportierte Felder eines addressierbaren Wertes). Unexported (kleingeschriebene) Felder können gelesen, aber nicht gesetzt werden. SetInt/SetString/Set sind zur Runtime typgeprüft und paniken bei Nichtübereinstimmung. Reflection-basierte Mutation ist die Grundlage von Config-Parsern und ORMs.

go
package main

import (
    "fmt"
    "reflect"
)

func main() {
    type Config struct {
        Port int
        Host string
    }

    cfg := Config{Port: 8080, Host: "localhost"}

    // MUST pass a pointer to modify (ValueOf of a struct is a copy)
    v := reflect.ValueOf(&cfg).Elem()

    // Check if a field is settable
    portField := v.FieldByName("Port")
    fmt.Println("Settable:", portField.CanSet()) // true

    // Modify fields by name
    portField.SetInt(9090)
    v.FieldByName("Host").SetString("0.0.0.0")

    fmt.Println(cfg) // {9090 0.0.0.0}

    // Set via interface (type-checked at runtime)
    v.FieldByName("Port").Set(reflect.ValueOf(7070))
    fmt.Println(cfg) // {7070 0.0.0.0}

    // Unexported fields are NOT settable (CanSet returns false)
}

Methoden dynamisch aufrufen

Reflection kann Methoden dynamisch nach Namen aufrufen – nützlich für RPC-Frameworks, Plugin-Systeme und Routing. MethodByName gibt einen Value zurück; Call() ruft sie mit einer Slice von reflect.Value-Argumenten auf und gibt eine Slice von Ergebnissen zurück. NumMethod/Method enumerieren Methoden. Methodenaufrufe über Reflection sind langsam und umgehen Typsicherheit, also verwende sie nur, wenn der Methodenname zur Compile-Zeit nicht bekannt ist.

go
package main

import (
    "fmt"
    "reflect"
)

type Calculator struct{}

func (c Calculator) Add(a, b int) int { return a + b }
func (c Calculator) Mul(a, b int) int { return a * b }

func main() {
    calc := Calculator{}
    v := reflect.ValueOf(calc)

    // Find a method by name
    method := v.MethodByName("Add")
    fmt.Println("Found:", method.IsValid()) // true

    // Call with arguments (each arg must be a reflect.Value)
    args := []reflect.Value{
        reflect.ValueOf(3),
        reflect.ValueOf(4),
    }
    results := method.Call(args)
    fmt.Println("3 + 4 =", results[0].Int()) // 7

    // List all methods
    t := reflect.TypeOf(calc)
    for i := 0; i < t.NumMethod(); i++ {
        m := t.Method(i)
        fmt.Printf("Method: %s, Type: %v\n", m.Name, m.Type)
    }
    // Method: Add, Type: func(main.Calculator, int, int) int
    // Method: Mul, Type: func(main.Calculator, int, int) int
}

Praktische Anwendung: Struct zu Map (Tag-basiert)

Dieses Pattern – das Iterieren über Struct-Felder und Lesen von Tags – ist wie encoding/json, YAML-Parser, ORMs und Validierungsbibliotheken unter der Haube arbeiten. reflect macht es möglich, generischen Code zu schreiben, der jedes Struct basierend auf seinen Tags verarbeitet. Die json:'-' Tag-Konvention (dieses Feld überspringen) ist Standard. Das ist eine der legitimsten Verwendungen von Reflection in Go.

go
package main

import (
    "fmt"
    "reflect"
)

// Convert any struct to a map using struct tags as keys
func StructToMap(obj any, tag string) map[string]any {
    result := make(map[string]any)
    v := reflect.ValueOf(obj)
    t := reflect.TypeOf(obj)

    // Dereference pointers
    if v.Kind() == reflect.Ptr {
        v = v.Elem()
        t = t.Elem()
    }

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        // Use the tag value if present, else the field name
        key := field.Tag.Get(tag)
        if key == "" {
            key = field.Name
        }
        // Skip fields tagged with "-"
        if key == "-" {
            continue
        }
        result[key] = v.Field(i).Interface()
    }
    return result
}

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
    Pass string `json:"-"` // excluded
}

func main() {
    u := User{Name: "Alice", Age: 30, Pass: "secret"}
    m := StructToMap(u, "json")
    fmt.Println(m) // map[age:30 name:Alice]
}
19

Build & Tooling

go build, go run & go install

go build kompiliert zu einer ausführbaren Datei; go run kompiliert zu einer temporären Datei und führt sie aus (großartig für Entwicklung); go install legt das Binary in $GOPATH/bin für globalen Zugriff. Go's herausragendes Feature ist einfache Cross-Kompilierung über GOOS/GOARCH – keine Toolchain nötig. -ldflags='-s -w' entfernt Debug-Info für ~30% kleinere Binaries. -X injiziert Werte (wie Versions-Strings) zur Build-Zeit für CI/CD.

go
# Build an executable (outputs to current directory)
go build -o myapp              # custom output name
go build -o bin/myapp ./cmd    # build a specific package

# Build for different OS/arch (cross-compilation)
GOOS=linux GOARCH=amd64 go build -o myapp-linux
GOOS=windows GOARCH=amd64 go build -o myapp.exe
GOOS=darwin GOARCH=arm64 go build -o myapp-mac  # Apple Silicon

# Run without building an executable (compiles to temp, runs, deletes)
go run main.go                 # run single file
go run ./cmd/server            # run a package

# Install: builds and puts binary in $GOPATH/bin
go install ./cmd/myapp         # now 'myapp' is on your PATH

# Build flags
go build -v                    # verbose: print packages being compiled
go build -race                 # enable race detector (for testing)
go build -ldflags="-s -w"      # strip debug info (smaller binary)
go build -ldflags="-X main.Version=1.0.0"  # inject version at build time

go mod (Modulverwaltung)

go mod verwaltet Abhängigkeiten über go.mod (Abhängigkeitsliste) und go.sum (Prüfsummen für Sicherheit). go get fügt/upgradet Abhängigkeiten; go mod tidy synchronisiert die Dateien (vor Commits ausführen). Go verwendet Semantic Import Versioning: v2+ erfordert einen /v2-Pfad-Suffix. Der Modul-Cache wird unter $GOPATH/pkg/mod geteilt. go mod vendor erstellt ein vendor/-Verzeichnis für reproduzierbare, Offline- oder auditierte Builds.

go
# Initialize a new module
go mod init github.com/user/myproject

# This creates go.mod:
# module github.com/user/myproject
# go 1.21

# Add a dependency (auto-added to go.mod)
go get github.com/gin-gonic/gin@latest
go get github.com/lib/[email protected]  # specific version

# Tidy: add missing deps, remove unused ones
go mod tidy

# Download dependencies to local cache
go mod download

# Verify dependencies haven't been modified
go mod verify

# Vendor: copy deps into ./vendor (for offline builds)
go mod vendor

# Upgrade dependencies
go get -u github.com/lib/pq       # upgrade to latest minor
go get -u=patch github.com/lib/pq # upgrade patches only
go get github.com/lib/[email protected]  # pin specific version

# View dependency graph
go mod graph | head

go test & Benchmarking

go test führt _test.go-Dateien aus; Funktionen namens TestXxx(t *testing.T) sind Tests, BenchmarkXxx(b *testing.B) sind Benchmarks. -race aktiviert den Data-Race-Detector (essenziell für nebenläufigen Code). -cover zeigt Test-Abdeckung; -coverprofile generiert einen detaillierten Bericht. Benchmarks führen b.N-Iterationen aus, wobei N automatisch getuned wird. Table-Driven-Tests (Slices von Input/Expected) sind der idiomatische Go-Test-Stil.

go
# Run all tests in the current package
go test ./...

# Verbose output
go test -v ./...

# Run a specific test
go test -run TestAdd -v

# Run benchmarks
go test -bench=. -benchmem

# Race detector (find data races)
go test -race ./...

# Coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out  # view in browser

# --- test file example (xxx_test.go) ---
# func TestAdd(t *testing.T) {
#     got := Add(2, 3)
#     if got != 5 {
#         t.Errorf("Add(2,3) = %d, want 5", got)
#     }
# }
#
# func BenchmarkAdd(b *testing.B) {
#     for i := 0; i < b.N; i++ {
#         Add(2, 3)
#     }
# }

go fmt, go vet & golangci-lint

go fmt/gofmt erzwingt ein einziges kanonisches Format – Go hat keine Formatierungs-Debatten. go vet fängt häufige Bugs ab (printf-Mismatches, Lock-Kopien, schlechte Struct-Tags). golangci-lint aggregiert dutzende Linter und ist der Industriestandard für CI. Führe gofmt und go vet vor jedem Commit aus; füge golangci-lint zu CI für tiefere Analyse hinzu. Konsistente Tooling ist ein Hauptgrund, warum Go-Codebasen einheitlich aussehen.

go
# go fmt: format code (the ONE true Go style)
go fmt ./...              # format all files
gofmt -d main.go          # show diff without changing
gofmt -w main.go          # write changes in place

# go vet: static analysis for common mistakes
go vet ./...              # check all packages
# Catches: printf format mismatches, unreachable code,
# struct tag errors, lock copies, shadowed variables

# golangci-lint: meta-linter (runs many linters)
# Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run         # run all enabled linters
golangci-lint run --enable=gosec,goconst  # enable specific linters

# Common linters:
# errcheck    - check unchecked errors
# gosec       - security issues
# govet       - go vet checks
# staticcheck - advanced static analysis
# ineffassign - detect ineffectual assignments
# unused      - find unused code

# goimports: fmt + auto-manage imports
# go install golang.org/x/tools/cmd/goimports@latest
goimports -w main.go

Profiling & pprof

pprof ist Go's eingebauter Profiler – CPU-, Speicher-, Goroutinen- und Mutex-Profiling. Für langlebige Server importiere _ 'net/http/pprof', um einen /debug/pprof/-Endpunkt für Live-Profiling ohne Neustarts freizugeben. go tool pprof gibt eine interaktive Shell (top, list, web) oder ein Web-UI. Der Execution-Tracer (go tool trace) visualisiert Goroutinen-Scheduling und Blockierungen. Profiling ist essenziell für performance-kritischen Go-Code.

go
# CPU profiling
go test -cpuprofile=cpu.prof -bench=.
go tool pprof cpu.prof
# Interactive commands: top, list FunctionName, web (graphviz)

# Memory profiling
go test -memprofile=mem.prof -bench=.
go tool pprof mem.prof

# Built-in HTTP pprof endpoint (for production servers)
import _ "net/http/pprof"
# Then visit: http://localhost:8080/debug/pprof/
# Capture a 30-second CPU profile:
# go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30

# In code:
import "runtime/pprof"
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// ... code to profile ...

# Trace (execution tracer)
go test -trace=trace.out -bench=.
go tool trace trace.out  # opens in browser
20

Datenbank (SQL)

database/sql-Grundlagen

database/sql ist die Standard-Schnittstelle für SQL-Datenbanken. Der Driver wird für seine Seiteneffekte importiert (registriert sich selbst). sql.Open stellt keine Verbindung her; verwende Ping zur Verifizierung. Defer immer db.Close().

go
import "database/sql"
import _ "github.com/lib/pq"
db, err := sql.Open("postgres", "host=localhost dbname=mydb")
if err != nil { log.Fatal(err) }
defer db.Close()
err = db.Ping()

Zeilen abfragen

Query gibt mehrere Zeilen zurück. Defer immer rows.Close(), um Ressourcen freizugeben. Scan kopiert Spaltenwerte in Variablen. Prüfe rows.Err() nach der Schleife auf Iterationsfehler.

go
rows, err := db.Query("SELECT id, name FROM users WHERE age > $1", 18)
if err != nil { log.Fatal(err) }
defer rows.Close()
for rows.Next() {
    var id int; var name string
    if err := rows.Scan(&id, &name); err != nil { log.Fatal(err) }
    fmt.Printf("%d: %s\n", id, name)
}

Einzelne Zeile abfragen

QueryRow gibt eine einzelne Zeile zurück. Scan gibt sql.ErrNoRows zurück, wenn keine Zeile übereinstimmt. Sauberer als Query für Einzelzeilen-Lookups. Behandle ErrNoRows immer explizit.

go
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = $1", 1).Scan(&name)
if err != nil {
    if err == sql.ErrNoRows { fmt.Println("Not found") } else { log.Fatal(err) }
}

Prepared Statements

Prepare erstellt eine wiederverwendbare Anweisung und verbessert die Performance für wiederholte Abfragen. Verhindert SQL-Injection. Defer immer stmt.Close(). Verwende für Abfragen, die mehrmals ausgeführt werden.

go
stmt, err := db.Prepare("INSERT INTO users(name) VALUES($1)")
if err != nil { log.Fatal(err) }
defer stmt.Close()
_, err = stmt.Exec("Alice")
_, err = stmt.Exec("Bob")

Transaktionen

Begin startet eine Transaktion. Alle Operationen innerhalb tx sind atomar. defer Rollback ist sicher: es ist ein No-Op nach Commit. Wenn eine Operation fehlschlägt, macht Rollback alle Änderungen rückgängig.

go
tx, err := db.Begin()
if err != nil { log.Fatal(err) }
defer tx.Rollback()  // Safe to call after commit
_, err = tx.Exec("UPDATE accounts SET bal = bal - 100 WHERE id = 1")
_, err = tx.Exec("UPDATE accounts SET bal = bal + 100 WHERE id = 2")
if err != nil { log.Fatal(err) }
err = tx.Commit()
21

Testing Deep Dive

Basis-Test

Test-Funktionen beginnen mit Test und nehmen *testing.T. t.Errorf protokolliert Fehlschlag und fährt fort. t.Fatalf protokolliert und stoppt. Führe aus mit go test. Verwende testify/assert für sauberere Assertions.

go
func TestAdd(t *testing.T) {
    got := Add(2, 3)
    want := 5
    if got != want {
        t.Errorf("Add(2,3) = %d; want %d", got, want)
    }
}

Table-Driven-Tests

Table-Driven-Tests sind idiomatisch in Go. Definiere Testfälle als Slice von Structs. Schleife durch und führe jeden aus. t.Run erstellt Subtests für einzelne Fehlschläge.

go
func TestAdd(t *testing.T) {
    tests := []struct {
        a, b, want int
    }{
        {1, 2, 3}, {-1, 1, 0}, {0, 0, 0},
    }
    for _, tt := range tests {
        got := Add(tt.a, tt.b)
        if got != tt.want {
            t.Errorf("Add(%d,%d)=%d; want %d", tt.a, tt.b, got, tt.want)
        }
    }
}

Subtests

Subtests verwenden t.Run mit einem Namen und einer Funktion. Führe spezifische Subtests aus mit -run-Pattern. Bietet bessere Test-Organisation und Ausgabe.

go
func TestAdd(t *testing.T) {
    t.Run("positive", func(t *testing.T) {
        if Add(1, 2) != 3 { t.Error("failed") }
    })
    t.Run("negative", func(t *testing.T) {
        if Add(-1, -2) != -3 { t.Error("failed") }
    })
}
// Run specific: go test -run TestAdd/negative

Test Main

TestMain läuft einmal für das Paket und ersetzt den Standard-Test-Runner. Verwende für globales Setup/Teardown. Muss m.Run() aufrufen, um Tests auszuführen. os.Exit propagiert den Exit-Code.

go
func TestMain(m *testing.M) {
    setup()
    code := m.Run()
    teardown()
    os.Exit(code)
}
func setup() { /* initialize DB, etc. */ }
func teardown() { /* cleanup */ }

Interfaces mocken

Go-Mocking basiert auf Interfaces. Definiere ein Interface, implementiere einen Mock und injiziere ihn. Tools wie mockery und mockgen auto-generieren Mocks. Ermöglicht Unit-Testing ohne externe Abhängigkeiten.

go
type Store interface { Get(id int) (string, error) }
type MockStore struct { data map[int]string }
func (m *MockStore) Get(id int) (string, error) {
    if v, ok := m.data[id]; ok { return v, nil }
    return "", errors.New("not found")
}
// Use in tests by injecting MockStore
22

Benchmarking

Basis-Benchmark

Benchmark-Funktionen beginnen mit Benchmark und nehmen *testing.B. b.N wird vom Framework angepasst, um stabile Messungen zu erhalten. Führe aus mit go test -bench. Ausgabe zeigt ns/op (Nanosekunden pro Operation).

go
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}
// Run: go test -bench=.
// Output: BenchmarkAdd-8    1000000000    0.3 ns/op

Sub-Benchmarks

Sub-Benchmarks verwenden b.Run. ResetTimer schließt Setup-Zeit aus. ReportAllocs zeigt Speicher-Allokationen. Vergleiche Performance über Eingabegrößen hinweg.

go
func BenchmarkSort(b *testing.B) {
    sizes := []int{100, 1000, 10000}
    for _, size := range sizes {
        b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
            data := generateData(size)
            b.ResetTimer()
            for i := 0; i < b.N; i++ {
                Sort(data)
            }
        })
    }
}

Speicher-Allokationen

ReportAllocs zeigt Speicher-Allokationen pro Operation. Das Reduzieren von Allokationen ist der Schlüssel zu Go-Performance. Verwende sync.Pool, prä-allokiiere Slices und vermeide unnötige String-Konkatenation.

go
func BenchmarkConcat(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        s := "a" + "b" + "c"
        _ = s
    }
}
// Output includes allocs/op and B/op

Parallele Benchmarks

RunParallel führt Benchmarks gleichzeitig über mehrere Goroutinen aus. Nützlich für das Testen von threadsicherem Code. pb.Next() iteriert die Arbeitsverteilung. Misst Throughput unter Nebenläufigkeit.

go
func BenchmarkParallel(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Add(2, 3)
        }
    })
}

Benchmark-Vergleich

benchstat vergleicht Benchmark-Ergebnisse statistisch. Führe Benchmarks mehrmals mit -count=10 für zuverlässigen Vergleich aus. Hilft, Performance-Verbesserungen zu verifizieren und Regressionen zu erkennen.

go
# Run with memory stats
benchstat old.txt new.txt
# Compare two runs
# name      old time/op  new time/op  delta
# Add-8     2.3ns        1.8ns        -21.7%
23

Profiling (pprof)

CPU-Profiling

StartCPUProfile schreibt ein CPU-Profil in eine Datei. StopCPUProfile flusht Daten. Analysiere mit go tool pprof. Verwende top, list, web-Befehle in pprof. Fokussiere auf Funktionen, die die meiste CPU verbrauchen.

go
import "runtime/pprof"
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Run your code here
// Analyze: go tool pprof cpu.prof

Speicher-Profiling

WriteHeapProfile erfasst den aktuellen Heap-Zustand. Rufe zuerst runtime.GC() für genaue Ergebnisse auf. Setze MemProfileRate = 1, um jede Allokation zu profilieren (langsamer, aber präzise).

go
// Add to code
runtime.GC()
f, _ := os.Create("mem.prof")
pprof.WriteHeapProfile(f)
f.Close()
// Or use runtime.MemProfileRate = 1 for all allocations

HTTP-Profiling

net/http/pprof registriert Profiling-Endpunkte auf dem Standard-Mux. Greife über HTTP auf Profile zu ohne Neustart. Nützlich für Produktion-Diagnostik. Sichere den Endpunkt in Produktion.

go
import _ "net/http/pprof"
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Analyze live:
// go tool pprof http://localhost:6060/debug/pprof/profile

pprof-Befehle

top zeigt Funktionen, die die meisten Ressourcen verbrauchen. list zeigt annotierten Quellcode. web öffnet einen SVG-Call-Graph. tree zeigt die Call-Hierarchie. Verwende focus zum Filtern.

go
# Start interactive pprof
go tool pprof cpu.prof
(pprof) top 10        # Top functions by CPU
(pprof) list Add      # Show source with timings
(pprof) web           # Open graph in browser
(pprof) tree          # Call tree
(pprof) png > out.png # Save graph as PNG

Trace

runtime/trace erfasst Execution-Traces: Goroutinen-Scheduling, GC, Syscall-Blockierungen. go tool trace öffnet ein Web-UI. Nützlich zur Diagnose von Latenz- und Nebenläufigkeitsproblemen.

go
import "runtime/trace"
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
// Run code
// View: go tool trace trace.out
24

Build-Tags

Basis-Build-Tags

Build-Tags kompilieren Dateien bedingt. Die //go:build-Syntax (Go 1.17+) ersetzt // +build. Tags können OS (linux, darwin, windows), Arch (amd64, arm64) oder benutzerdefiniert sein.

go
//go:build linux
// +build linux

package main
// This file only compiles on Linux

Mehrere Tags

&& (Komma in alter Syntax) erfordert alle Tags. || (Leerzeichen in alter Syntax) erfordert irgendein Tag. ! negiert. Verwende für plattformspezifische Implementierungen.

go
//go:build linux && amd64
// +build linux,amd64

//go:build linux || darwin
// +build linux darwin

//go:build !windows
// +build !windows

Benutzerdefinierte Tags

Benutzerdefinierte Tags ermöglichen optionale Features. Baue mit -tags-Flag. Häufige Verwendung: Debug-Builds, experimentelle Features, Enterprise vs. Community Editionen. Halte Tag-Namen kleingeschrieben.

go
//go:build debug
// +build debug

package main
const DebugMode = true
// Build with: go build -tags debug

Datei-Suffixe

Datei-Suffixe bieten implizite Build-Beschränkungen. _linux, _darwin, _windows für OS. _amd64, _arm64 für Architektur. Einfacher als Tags für plattformspezifischen Code.

go
// file: utils_linux.go    -> Linux only
// file: utils_darwin.go   -> macOS only
// file: utils_windows.go  -> Windows only
// file: utils_amd64.go    -> amd64 only
// file: utils_debug.go    -> requires -tags debug

Mit Tags bauen

Verwende -tags, um bedingte Kompilierung zu aktivieren. Mehrere Tags sind leerzeichen-getrennt. Go-Versions-Tags (go1.18) aktivieren versionsspezifischen Code. Verifiziere mit go list -tags.

go
# Build with debug tag
go build -tags debug
# Multiple tags
go build -tags "debug verbose"
# List available tags
go list -tags ./...
# Conditional in code
//go:build go1.18  // Requires Go 1.18+
25

CGO-Grundlagen

Basis-CGO

CGO ermöglicht das Aufrufen von C aus Go. C-Code steht in Kommentaren über import "C". Der Import muss unmittelbar nach dem Kommentar stehen. CGO verlangsamt Builds und verhindert Cross-Kompilierung.

go
/*
#include <stdio.h>
void hello() {
    printf("Hello from C!\n");
}
*/
import "C"
func main() {
    C.hello()
}

Strings übergeben

C.CString allokiiert einen C-String (muss mit C.free freigegeben werden). CString kopiert die Daten. Verwende unsafe.Pointer für Konvertierung. Gib immer C-allokiierten Speicher frei, um Lecks zu vermeiden.

go
/*
#include <string.h>
int len(const char* s) { return strlen(s); }
*/
import "C"
import "unsafe"
s := "Hello"cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
length := C.len(cs)

C-Bibliotheken aufrufen

#cgo LDFLAGS linkt C-Bibliotheken. #cgo CFLAGS setzt Compiler-Flags. Inkludiere System-Header mit #include. CGO verbindet Go mit existierenden C-Bibliotheken wie libm, libcrypto.

go
/*
#cgo LDFLAGS: -lm
#include <math.h>
*/
import "C"
result := float64(C.sqrt(16.0))
fmt.Println(result)  // 4

C zu Go Callback

Go-Funktionen, die mit //export exportiert werden, können von C aufgerufen werden. Die Funktion muss in package main sein. Ermöglicht C-Bibliotheken, zurück in Go aufzurufen. Verwendet in FFI-Bindings.

go
/*
extern void goCallback(int);
void callGo(int n) { goCallback(n); }
*/
import "C"
//export goCallback
func goCallback(n C.int) {
    fmt.Printf("Called with %d\n", n)
}

Performance-Hinweise

CGO-Aufrufe haben signifikanten Overhead verglichen mit Go-Funktionsaufrufen. Vermeide in performance-kritischem Code. Bündle Operationen, um grenzüberschreitende Aufrufe zu reduzieren. Setze CGO_ENABLED=0 für reine Go-Builds.

go
// CGO calls have overhead (~100ns)
// Avoid in hot paths
// Batch C calls to amortize cost
// CGO disables inlining and some optimizations
// Build: CGO_ENABLED=1 go build
26

Web-Frameworks

net/http-Server

net/http ist der Standardbibliotheks-HTTP-Server. HandleFunc registriert Handler. ListenAndServe startet den Server. Standard-Mux ist in Ordnung für einfache Apps; verwende benutzerdefinierten Mux für Produktion.

go
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
})
http.HandleFunc("/api", apiHandler)
log.Fatal(http.ListenAndServe(":8080", nil))

Gin-Framework

Gin ist ein hochperformantes HTTP-Framework. Param extrahiert URL-Parameter. JSON serialisiert Antworten. Gin bietet Routing, Middleware und JSON-Validierung. Schneller als net/http aufgrund von httprouter.

go
import "github.com/gin-gonic/gin"
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id")
    c.JSON(200, gin.H{"user": id})
})
r.Run(":8080")

Middleware

Middleware wrappt Handler, um Querschnittsthemen hinzuzufügen: Logging, Auth, CORS. c.Next() ruft den nächsten Handler auf. gin.Recovery() verhindert Abstürze durch Panics. Reihenfolge ist wichtig.

go
func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()
        fmt.Printf("%s %s %v\n", c.Request.Method, c.URL.Path, time.Since(start))
    }
}
r := gin.New()
r.Use(Logger(), gin.Recovery())

Echo-Framework

Echo ist ein weiteres beliebtes Framework, ähnlich wie Gin. Handler geben Errors zurück für zentralisierte Fehlerbehandlung. Eingebaute Middleware für CORS, JWT, Rate-Limiting. Sauberes API-Design.

go
import "github.com/labstack/echo/v4"
e := echo.New()
e.GET("/users/:id", func(c echo.Context) error {
    id := c.Param("id")
    return c.JSON(200, map[string]string{"user": id})
})
e.Start(":8080")

Statische Dateien

FileServer serviert statische Dateien. StripPrefix passt den URL-Pfad an. Nützlich für das Servieren von HTML, CSS, JS und Bildern. Für Produktion verwende ein CDN oder nginx für statische Assets.

go
// net/http
fs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
// Gin
r.Static("/assets", "./assets")
r.StaticFile("/favicon.ico", "./favicon.ico")
27

gRPC

Proto definieren

Protocol Buffers definieren den Service-Vertrag. proto3 ist die neueste Syntax. service definiert RPC-Methoden. message definiert Datenstrukturen. Generiere Go-Code mit protoc.

go
syntax = "proto3";
package greet;
service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }

Code generieren

protoc generiert Go-Code aus .proto-Dateien. --go_out generiert Message-Typen. --go-grpc_out generiert Service-Stubs. Der generierte Code wird nicht manuell bearbeitet.

go
# Install protoc and plugins
protoc --go_out=. --go-grpc_out=. \
    greet.proto
# This generates greet.pb.go and greet_grpc.pb.go
# Containing message types and service interfaces

Server-Implementierung

Bette UnimplementedGreeterServer für Forward-Kompatibilität ein. Implementiere die Service-Methoden. grpc.NewServer erstellt den Server. Registriere den Service vor dem Servieren.

go
type server struct { greet.UnimplementedGreeterServer }
func (s *server) SayHello(ctx context.Context, in *greet.HelloRequest) (*greet.HelloReply, error) {
    return &greet.HelloReply{Message: "Hello " + in.Name}, nil
}
lis, _ := net.Listen("tcp", ":50051")
grpc.NewServer().Serve(lis)

Client

grpc.Dial stellt eine Verbindung her. WithInsecure deaktiviert TLS (verwende credentials.NewTLS für Produktion). Der Client-Stub bietet typisierte Methoden. Verbindungen werden gepoolt und wiederverwendet.

go
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
client := greet.NewGreeterClient(conn)
resp, _ := client.SayHello(context.Background(), &greet.HelloRequest{Name: "Alice"})
fmt.Println(resp.Message)

Streaming

gRPC unterstützt drei Streaming-Patterns. Das stream-Schlüsselwort markiert Streaming. Server-Streaming: eine Anfrage, viele Antworten. Bidirektional: beide Seiten streamen. Nützlich für Echtzeitdaten.

go
// Server streaming
rpc LotsOfReplies(HelloRequest) returns (stream HelloReply);
// Client streaming
rpc LotsOfGreetings(stream HelloRequest) returns (HelloReply);
// Bidirectional
rpc BidiHello(stream HelloRequest) returns (stream HelloReply);
28

Error-Wrapping

Errors wrappen

Verwende das %w-Verb, um Errors zu wrappen und das Original zu bewahren. Das erstellt eine Error-Kette. Vermeide %v für Wrapping, da es die Kette verliert. Wrapping fügt Kontext hinzu, ohne die Root-Cause zu verlieren.

go
if err != nil {
    return fmt.Errorf("failed to open config: %w", err)
}

Unwrapping

errors.Is prüft, ob irgendein Error in der Kette übereinstimmt. errors.As extrahiert einen spezifischen Error-Typ aus der Kette. Verwende Is für Sentinel-Werte, As für typisierte Errors. Beide durchlaufen die Wrap-Kette.

go
err := someOperation()
if errors.Is(err, sql.ErrNoRows) {
    // Handle not found
}
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
    fmt.Println("Path:", pathErr.Path)
}

Benutzerdefinierte Errors

Benutzerdefinierte Error-Typen implementieren das error-Interface. Sie tragen strukturierte Daten für Fehlerbehandlung. Verwende errors.As, um den benutzerdefinierten Typ zu extrahieren. Bevorzuge typisierte Errors über String-Vergleichen.

go
type ValidationError struct {
    Field string
    Msg   string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
func validate(s string) error {
    if s == "" { return &ValidationError{Field: "name", Msg: "required"} }
    return nil
}

Sentinel-Errors

Sentinel-Errors sind paketweite Error-Variablen. Verwende für erwartete Error-Bedingungen. Prüfe mit errors.Is, niemals mit ==. Exportiere sie, damit Benutzer dagegen prüfen können.

go
var ErrNotFound = errors.New("not found")
func Find(id int) (*User, error) {
    if id > 100 { return nil, ErrNotFound }
    return &User{}, nil
}
// Check with errors.Is
if errors.Is(err, ErrNotFound) { /* ... */ }

Fehlerbehandlungs-Patterns

Behandle Errors sofort, wenn möglich. Für verzögerte Operationen wie Close erfasse den Error. Benannte Rückgabewerte erlauben deferrierten Funktionen, den Rückgabewert zu modifizieren. Prüfe immer Close-Errors.

go
// Immediate handling
if err := doSomething(); err != nil {
    return fmt.Errorf("operation failed: %w", err)
}
// Deferred error checking (e.g., Close)
func readFile() (err error) {
    f, e := os.Open("file.txt")
    if e != nil { return e }
    defer func() {
        if cerr := f.Close(); err == nil { err = cerr }
    }()
    // ...
}
29

Häufige Fallstricke

Goroutinen-Lecks

Goroutinen lecken, wenn sie für immer blockieren. Biete immer einen Ausstiegspfad: Context-Abbruch, Channels schließen oder gepufferte Channels. Verwende runtime.NumGoroutine(), um Lecks zu erkennen.

go
// BUG: goroutine leaks if receiver stops
func send(ch chan int) {
    go func() { ch <- 1 }()  // Blocks forever
}
// FIX: use context or buffered channel
func send(ctx context.Context, ch chan int) {
    go func() {
        select {
        case ch <- 1:
        case <-ctx.Done():
        }
    }()
}

Channel schließen

Nur der Sender sollte einen Channel schließen, niemals der Empfänger. Schließen signalisiert keine Werte mehr. Empfangen von einem geschlossenen Channel gibt den Nullwert zurück. Senden an einen geschlossenen Channel panikt.

go
// Only the sender should close a channel
// Closing from receiver causes panic
ch := make(chan int)
go func() {
    defer close(ch)  // Sender closes
    for i := 0; i < 5; i++ { ch <- i }
}()
for v := range ch { fmt.Println(v) }

Schleifenvariablen-Capture

Vor Go 1.22 wurden Schleifenvariablen über Iterationen geteilt. Goroutinen, die sie erfassen, sehen den Endwert. Go 1.22+ behebt dies, indem pro Iteration eine neue Variable erstellt wird. Übergib als Parameter für ältere Versionen.

go
// BUG (Go < 1.22): all goroutines see last value
for i := 0; i < 3; i++ {
    go func() { fmt.Println(i) }()  // Prints 3,3,3
}
// FIX: pass as parameter
for i := 0; i < 3; i++ {
    go func(i int) { fmt.Println(i) }(i)
}

Map-Nebenläufigkeit

Maps sind nicht für nebenläufige Verwendung sicher. Gleichzeitige Lese- und Schreibzugriffe verursachen eine Runtime-Panic. Verwende sync.Mutex für explizites Locking oder sync.Map für lese-lastigen nebenläufigen Zugriff.

go
// BUG: concurrent map writes panic
m := map[int]int{}
go func() { m[1] = 1 }()
go func() { m[2] = 2 }()
// FIX: use sync.Map or mutex
var mu sync.Mutex
mu.Lock(); m[1] = 1; mu.Unlock()

Nil-Interface

Ein nil-Pointer, der in ein Interface gewrappt ist, ist nicht nil. Das Interface hat einen Typ, selbst wenn der Wert nil ist. Gib immer direkt nil zurück, keinen nil typisierten Pointer. Prüfe mit reflect oder gib explizit nil zurück.

go
// BUG: nil check fails
type MyError struct{}
func (e *MyError) Error() string { return "err" }
func doSomething() error {
    var err *MyError = nil
    return err  // Non-nil interface!
}
// FIX: return nil explicitly
func doSomething() error {
    return nil
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.