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.
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.goGo-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.
// 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 depsPaketstruktur
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.
// 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 letterVariablen & 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.
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.
// 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") // intStrings & 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.
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).
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.
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).
// 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.
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"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.
// 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 requiredFor-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.
// 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.
// 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.
// 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 = 2Goto, 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.
// 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.
// 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
}
}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.
// 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.
// 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()) // 3Funktionen 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.
// 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)) // 7init() & 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.
// 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.
// 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.
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!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.
// 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()).
// 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.
// 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.
// 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 illegalSlice-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.
// 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.
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 funcMethoden & 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.
// 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 ShapeEmpty 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.
// 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.
// 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.
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 bothStringer-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.
// 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 errorType 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.
// 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 WriterFehlerbehandlung
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).
// 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.
// 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.
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.
// 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.
// 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 tracesDefer, 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.
// 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)
}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).
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 concurrentlyChannels
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'.
// 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.
// 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.
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.'
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.
// 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
}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.
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.
// 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_TRUNCDatei-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.
// 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") // recursiveUmgebung & 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).
// 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.0Befehle 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.
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 5sJSON & 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.
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 emptyJSON 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.
// 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.
// 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 defaultHTTP-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.
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.
// 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")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.
// 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/positiveBenchmarks
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.
// 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.
// 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.
// 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)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.
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.
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.
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 controlsync.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.
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.
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)
}
}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.
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.
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.
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.
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.
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
}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.
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.
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.
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.
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.
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)
}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.
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.
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.
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.
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.
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)
}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).
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.
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.
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).
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 unpredictableerrgroup (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.
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)
}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.
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.
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.
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).
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.
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 -> GenericsReflection
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.
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.
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.
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.
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]
}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.
# 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 timego 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.
# 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 | headgo 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.
# 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 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.goProfiling & 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.
# 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 browserDatenbank (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().
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.
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.
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.
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.
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()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.
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.
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.
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/negativeTest 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.
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.
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 MockStoreBenchmarking
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).
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/opSub-Benchmarks
Sub-Benchmarks verwenden b.Run. ResetTimer schließt Setup-Zeit aus. ReportAllocs zeigt Speicher-Allokationen. Vergleiche Performance über Eingabegrößen hinweg.
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.
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/opParallele 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.
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.
# 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%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.
import "runtime/pprof"
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Run your code here
// Analyze: go tool pprof cpu.profSpeicher-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).
// Add to code
runtime.GC()
f, _ := os.Create("mem.prof")
pprof.WriteHeapProfile(f)
f.Close()
// Or use runtime.MemProfileRate = 1 for all allocationsHTTP-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.
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Analyze live:
// go tool pprof http://localhost:6060/debug/pprof/profilepprof-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.
# 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 PNGTrace
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.
import "runtime/trace"
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
// Run code
// View: go tool trace trace.outCGO-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.
/*
#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.
/*
#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.
/*
#cgo LDFLAGS: -lm
#include <math.h>
*/
import "C"
result := float64(C.sqrt(16.0))
fmt.Println(result) // 4C 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.
/*
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.
// 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 buildWeb-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.
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.
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.
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.
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.
// 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")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.
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.
# 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 interfacesServer-Implementierung
Bette UnimplementedGreeterServer für Forward-Kompatibilität ein. Implementiere die Service-Methoden. grpc.NewServer erstellt den Server. Registriere den Service vor dem Servieren.
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.
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.
// Server streaming
rpc LotsOfReplies(HelloRequest) returns (stream HelloReply);
// Client streaming
rpc LotsOfGreetings(stream HelloRequest) returns (HelloReply);
// Bidirectional
rpc BidiHello(stream HelloRequest) returns (stream HelloReply);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.
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.
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.
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.
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.
// 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 }
}()
// ...
}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.
// 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.
// 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.
// 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.
// 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.
// 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
}Verwandte Go-Snippets
Copy-paste ready code for common tasks.
goroutine
Nebenläufigkeit mit Goroutinen implementieren.
channel
Zwischen Goroutinen über Channels kommunizieren.
select
Channels mit select multiplexen.
mutex Mutex
Geteilte Daten mit sync.Mutex schützen.
defer Verzögerter Aufruf
Verwendung und Ausführungsreihenfolge von defer.
error-Behandlung
Go's Fehlerbehandlungsmuster.
interface
Interfaces definieren und implementieren.
Struct Embedding
Komposition über Embedding implementieren.
Generics
Generics in Go 1.18+ verwenden.
context
Timeout und Abbruch mit context steuern.
Datei-Operationen
Datei-Lese- und Schreiboperationen.
HTTP-Server
Einen HTTP-Server erstellen.
HTTP-Client
HTTP-Anfragen senden.
JSON-Enkodierung/Dekodierung
Zwischen Structs und JSON konvertieren.
String-Verarbeitung
Häufige Operationen im strings-Paket.
Slice-Operationen
Häufige Slice-Operationen.
map-Operationen
CRUD-Operationen auf map.
Zeitbehandlung
Häufige Operationen im time-Paket.
Reguläre Ausdrücke
Verwendung des regexp-Pakets.
Tests
Unit-Tests schreiben.
Was this helpful?