Eine Sprache behandelt Funktionen als First-Class-Werte, wenn man mit ihnen alles tun kann, was man auch mit gewöhnlichen Werten wie int oder string tut: in Variablen speichern, an andere Funktionen übergeben, aus Funktionen zurückgeben, in Slices, Maps oder Structs ablegen. Go gehört zu dieser Kategorie — und nutzt sie konsequent aus. Die Stdlib ist voll davon: sort.Slice nimmt eine Vergleichsfunktion, http.HandleFunc nimmt eine Request-Funktion, strings.Map nimmt eine Transformations-Funktion. Wer das Konzept beherrscht, kann Higher-Order Functions, das Strategy-Pattern, Callbacks und Functional Options schreiben — und versteht plötzlich, warum so viele Go-APIs so aussehen, wie sie aussehen. Dieser Artikel arbeitet die Mechanik formal durch: Function Types, Function Values, Method Values, Method Expressions, die typischen Patterns und die Fallen, die man dabei umgehen muss.

Was „First-Class" konkret heißt

In manchen Sprachen sind Funktionen ein Sonderfall: man darf sie deklarieren und aufrufen, aber nicht herumreichen. Go macht keinen Unterschied zwischen einer Funktion und einem int — beide haben einen Typ, beide leben in Variablen, beide kommen als Parameter rein und als Return-Wert raus. Die Go-Spec sagt dazu knapp:

A function type denotes the set of all functions with the same parameter and result types. The value of an uninitialized variable of function type is nil.

Praktisch lassen sich daraus fünf Fähigkeiten ableiten, die wir Stück für Stück durchgehen:

FähigkeitBeispiel
In Variablen speichernvar op func(int, int) int = add
Als Parameter übergebensort.Slice(s, less)
Als Return-Wert zurückgebenmakeAdder(5) ergibt eine neue Funktion
In Datenstrukturen ablegenmap[string]func(...) (Command-Dispatch)
Anonym definierenfunc(x int) int { return x*2 } als Literal

Drei dieser Fähigkeiten kennen viele Sprachen auch als „Function Pointer" — der Unterschied ist, dass Gos Funktions-Werte echte Closures sind: Sie können auf Variablen des umschließenden Scopes zugreifen und halten diese am Leben. Closures behandeln wir im eigenen Artikel; hier konzentrieren wir uns auf die First-Class-Mechanik selbst.

Function Types — Typen aus Signaturen

Jede Funktions-Signatur in Go ist gleichzeitig ein Typ. func(int, int) int ist nicht nur eine Deklarations-Form, sondern auch eine vollwertige Typ-Bezeichnung — überall einsetzbar, wo ein Typ-Name stehen darf:

Go function_types.go
package main

import "fmt"

func add(a, b int) int { return a + b }
func mul(a, b int) int { return a * b }

func main() {
    // Variable vom Function Type
    var op func(int, int) int

    op = add
    fmt.Println(op(3, 4)) // 7

    op = mul
    fmt.Println(op(3, 4)) // 12

    // Slice von Funktions-Werten
    ops := []func(int, int) int{add, mul}
    for _, f := range ops {
        fmt.Println(f(5, 6))
    }

    // Map mit String-Keys auf Funktionen — typischer Dispatcher
    dispatch := map[string]func(int, int) int{
        "add": add,
        "mul": mul,
    }
    fmt.Println(dispatch["add"](10, 20))
}
Output
7
12
30
30
110

Drei Punkte, die hier sichtbar werden:

  • func(int, int) int ist ein vollwertiger Typ. Er steht im Variablen-Deklaration genauso wie int oder []byte. Auch als Element-Typ eines Slice oder Value-Typ einer Map.
  • Funktionen sind zuweisungs-kompatibel, wenn die Signatur exakt passt. Parameter-Namen spielen keine Rolle, Typen müssen aber stimmen. func(a, b int) int und func(x, y int) int sind derselbe Typ.
  • Zero-Value einer Funktions-Variable ist nil. Eine var op func(int, int) int ohne Zuweisung ist nil — der Aufruf wäre Panic.

Funktions-Variablen, nil und der Aufruf

Eine Funktions-Variable verhält sich exakt wie eine Pointer-Variable: kann zugewiesen, neu zugewiesen, gegen nil getestet — aber nicht direkt mit einer anderen Funktions-Variable verglichen werden (mehr dazu im Vergleichs-Abschnitt). Der nil-Aufruf ist die häufigste Falle:

Go nil_function.go
package main

import "fmt"

func main() {
    var op func(int) int

    // op ist nil — Vergleich erlaubt
    if op == nil {
        fmt.Println("op ist noch nicht gesetzt")
    }

    // op(5)  // PANIC: runtime error: invalid memory address or nil pointer dereference

    // Erst zuweisen, dann aufrufen
    op = func(x int) int { return x * x }
    fmt.Println(op(5))
}
Output
op ist noch nicht gesetzt
25

In Bibliothek-Code mit optionalen Callbacks ist die Prüfung Pflicht: Wenn ein Hook gesetzt sein kann, muss vor jedem Aufruf gegen nil getestet werden. Sonst paniciert die Bibliothek beim ersten Nutzer, der den Hook nicht setzt.

Higher-Order Functions — Funktionen als Parameter

Eine Higher-Order Function ist eine Funktion, die eine andere Funktion entweder als Parameter entgegennimmt oder zurückgibt. Der Klassiker ist das Trio Map/Filter/Reduce. In Go bis 1.18 musste man dafür Typ-spezifisch schreiben; mit Generics (ab 1.18 stabil) wird es elegant:

Go map_filter_reduce.go
package main

import "fmt"

func Map[T, U any](xs []T, f func(T) U) []U {
    out := make([]U, len(xs))
    for i, x := range xs {
        out[i] = f(x)
    }
    return out
}

func Filter[T any](xs []T, pred func(T) bool) []T {
    out := make([]T, 0, len(xs))
    for _, x := range xs {
        if pred(x) {
            out = append(out, x)
        }
    }
    return out
}

func Reduce[T, U any](xs []T, init U, f func(U, T) U) U {
    acc := init
    for _, x := range xs {
        acc = f(acc, x)
    }
    return acc
}

func main() {
    nums := []int{1, 2, 3, 4, 5}

    squared := Map(nums, func(n int) int { return n * n })
    fmt.Println(squared)

    even := Filter(nums, func(n int) bool { return n%2 == 0 })
    fmt.Println(even)

    sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
    fmt.Println(sum)
}
Output
[1 4 9 16 25]
[2 4]
15

Ein Wort zur Stil-Frage: Go-Idiom bleibt der explizite for-Loop. Map/Filter werden zwar zunehmend in eigenen Helper-Paketen verwendet (etwa in golang.org/x/exp/slices, ab Go 1.21 teilweise in der Stdlib unter slices), aber wer einen einfachen Loop hat, schreibt ihn idiomatisch direkt — kein Punkte-Abzug. Higher-Order Functions glänzen dort, wo die übergebene Funktion das Verhalten parametrisiert, nicht nur eine Schleife abkürzt.

Das beste Beispiel dafür ist die Stdlib selbst:

Go sort_slice.go
package main

import (
    "fmt"
    "sort"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    people := []Person{
        {"Alice", 30},
        {"Bob", 25},
        {"Carol", 35},
    }

    // sort.Slice nimmt eine Less-Funktion — Sortier-Strategie als Parameter
    sort.Slice(people, func(i, j int) bool {
        return people[i].Age < people[j].Age
    })
    fmt.Println(people)

    // Andere Strategie — gleicher Aufrufer, andere Funktion
    sort.Slice(people, func(i, j int) bool {
        return people[i].Name < people[j].Name
    })
    fmt.Println(people)
}
Output
[{Bob 25} {Alice 30} {Carol 35}]
[{Alice 30} {Bob 25} {Carol 35}]

sort.Slice weiß nichts über Person — und braucht es auch nicht. Der Aufrufer reicht das Wissen als Funktion mit. Das ist das Strategy-Pattern in seiner einfachsten Form.

Funktionen als Return-Wert — Factories

Spiegelbildlich zum Parameter-Fall: Eine Funktion kann auch eine Funktion zurückgeben. Das ist das Factory-Pattern — eine Funktion, die ein verhalten-konfiguriertes Funktions-Objekt erzeugt:

Go factory.go
package main

import "fmt"

// makeAdder erzeugt eine Funktion, die x zu ihrem Argument addiert.
// x lebt in der Closure weiter, auch nachdem makeAdder zurückgekehrt ist.
func makeAdder(x int) func(int) int {
    return func(y int) int {
        return x + y
    }
}

// Generischer Multiplikator
func makeMultiplier(factor float64) func(float64) float64 {
    return func(x float64) float64 {
        return x * factor
    }
}

func main() {
    add5 := makeAdder(5)
    add10 := makeAdder(10)

    fmt.Println(add5(3))  // 8
    fmt.Println(add10(3)) // 13

    double := makeMultiplier(2)
    triple := makeMultiplier(3)

    fmt.Println(double(7.5)) // 15
    fmt.Println(triple(7.5)) // 22.5
}
Output
8
13
15
22.5

Das ist mehr als ein Spielzeug-Beispiel — Middleware in net/http, Retry-Wrapper, getimete Limit-Funktionen, Token-Bucket-Limiter: alles Factory-Patterns. Eine Funktion nimmt Konfiguration entgegen und gibt eine konfigurierte Funktion zurück, die später beliebig oft aufgerufen wird.

Named Function Types — die Stdlib-Konvention

Wer einen Function Type mehrfach verwendet, gibt ihm einen Namen. Das ist nicht nur kosmetisch — der Name dient als Doku, kann eigene Methoden tragen und macht Interfaces aus normalen Funktionen:

Go named_func_type.go
package main

import "fmt"

// Named Function Type — der Standard-Trick der Stdlib
type Predicate func(int) bool

// Eine Methode auf dem Function Type
func (p Predicate) Negate() Predicate {
    return func(x int) bool { return !p(x) }
}

func main() {
    isPositive := Predicate(func(x int) bool { return x > 0 })
    isNegative := isPositive.Negate()

    fmt.Println(isPositive(5))  // true
    fmt.Println(isNegative(5))  // false
    fmt.Println(isNegative(-3)) // true
}
Output
true
false
true

Das prominenteste Stdlib-Beispiel ist http.HandlerFunc:

Go handler_func.go
// aus net/http (vereinfacht)
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

type HandlerFunc func(ResponseWriter, *Request)

// HandlerFunc implementiert das Handler-Interface, indem es sich selbst aufruft
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

Das ist ein eleganter Trick: Eine normale Funktion func(w, r) wird per Cast zu HandlerFunc — und damit zu einem Handler, weil HandlerFunc die Methode ServeHTTP trägt, die sich selbst aufruft. Der HTTP-Router akzeptiert Interfaces, der User reicht eine Funktion ein. Ohne Boilerplate-Struct dazwischen.

Method Values — Receiver schon gebunden

Wenn du auf einer konkreten Instanz eine Methode referenzierst (ohne sie aufzurufen), bekommst du einen Method Value zurück — eine Funktion, in der der Receiver bereits gebunden ist:

Go method_value.go
package main

import "fmt"

type Counter struct{ n int }

func (c *Counter) Inc()       { c.n++ }
func (c *Counter) Value() int { return c.n }

func main() {
    c := &Counter{}

    // Method Value — c ist als Receiver schon gebunden
    inc := c.Inc

    // Funktions-Aufruf, ohne den Receiver anzugeben
    inc()
    inc()
    inc()

    fmt.Println(c.Value()) // 3 — c und der Method Value teilen denselben Receiver
}
Output
3

Wichtig: inc hat die Signatur func()ohne *Counter-Parameter. Der Receiver ist schon Teil des Werts. Das macht Method Values besonders praktisch in Callbacks und Timer-Handlers:

Go method_value_callback.go
package main

import "fmt"

type Logger struct{ prefix string }

func (l *Logger) Log(msg string) {
    fmt.Println(l.prefix, msg)
}

// Higher-Order: nimmt eine Log-Funktion
func process(items []string, log func(string)) {
    for _, item := range items {
        log(&quot;verarbeite: &quot; + item)
    }
}

func main() {
    l := &Logger{prefix: &quot;[INFO]&quot;}

    // Method Value direkt als Callback übergeben — Receiver l ist gebunden
    process([]string{&quot;a&quot;, &quot;b&quot;, &quot;c&quot;}, l.Log)
}
Output
[INFO] verarbeite: a
[INFO] verarbeite: b
[INFO] verarbeite: c

Receiver-Kopie bei Value-Receivern. Achtung: Wenn die Methode einen Value-Receiver hat (nicht *T, sondern T), wird beim Erstellen des Method Value eine Kopie des Receivers gebunden. Spätere Änderungen am Original sind im Method Value nicht sichtbar:

Go method_value_copy.go
package main

import "fmt"

type Box struct{ value int }

// Value-Receiver — bekommt eine Kopie
func (b Box) Show() { fmt.Println(b.value) }

func main() {
    b := Box{value: 1}
    show := b.Show // Method Value bindet eine KOPIE von b

    b.value = 99
    show()        // 1 — sieht die alte Kopie
    b.Show()      // 99 — frischer Aufruf, frische Kopie
}
Output
1
99

Bei Pointer-Receivern (*T) wird der Pointer kopiert, nicht das Objekt — beide Aufrufe sehen denselben Zustand. Das ist meistens das, was du willst.

Method Expressions — Receiver als erster Parameter

Während ein Method Value den Receiver bindet, hält eine Method Expression den Receiver-Slot offen — du adressierst die Methode am Typ statt am Wert, und der Receiver wird zum ersten Parameter:

Go method_expression.go
package main

import "fmt"

type Counter struct{ n int }

func (c *Counter) Inc()       { c.n++ }
func (c *Counter) Value() int { return c.n }

func main() {
    // Method Expression — Counter als erster Parameter
    inc := (*Counter).Inc       // Signatur: func(*Counter)
    value := (*Counter).Value   // Signatur: func(*Counter) int

    c := &Counter{}
    inc(c)
    inc(c)
    inc(c)
    fmt.Println(value(c)) // 3

    // Anderer Receiver — selbe Expression
    d := &Counter{n: 100}
    inc(d)
    fmt.Println(value(d)) // 101
}
Output
3
101

Der Unterschied auf einen Blick:

FormSyntaxResultierende Signatur
Method Valuec.Incfunc() — Receiver gebunden
Method Expression(*Counter).Incfunc(*Counter) — Receiver als Parameter

Method Expressions sieht man seltener — typischerweise dort, wo ein Algorithmus generisch eine Methode auf vielen Instanzen anwenden soll und der Receiver pro Aufruf variiert. Method Values sind im Alltag häufiger.

Funktions-Vergleich — nur gegen nil

Die Go-Spec zählt sehr genau auf, welche Typen strikt vergleichbar sind: Boolean, Numerik, String, Pointer, Channel, Arrays mit vergleichbaren Elementen, Structs mit vergleichbaren Feldern, Interfaces. Funktions-Typen sind in dieser Liste nicht enthalten. Konsequenz:

Go func_compare.go
package main

func main() {
    f := func() int { return 1 }
    g := func() int { return 1 }

    // FEHLER: invalid operation: f == g (func can only be compared to nil)
    // _ = f == g

    // Erlaubt: Vergleich mit nil
    if f != nil {
        _ = f()
    }
    if g == nil {
        println(&quot;g ist nil&quot;)
    }
}

Warum diese Einschränkung? Zwei Funktions-Werte „gleich" zu nennen wäre semantisch unklar: Sollen sie dieselbe Adresse haben? Denselben Code? Dieselbe Closure-Umgebung? Go gibt keine Definition vor und verbietet den Vergleich.

Konsequenzen, die man im Hinterkopf haben muss:

  • Funktions-Typen können nicht als Map-Keys verwendet werden. Map-Keys brauchen Vergleichbarkeit.
  • Structs mit Funktions-Feldern sind ebenfalls nicht vergleichbar. Ein Struct mit OnClick func() darf nicht mit == verglichen werden.
  • Arrays von Funktionen sind nicht vergleichbar. Slices sowieso nie — aber auch Arrays, weil ihr Element-Typ es nicht ist.

Wer eine Identität braucht, nutzt einen separaten Key (String, int) oder vergleicht über reflect.ValueOf(f).Pointer() — letzteres ist legal, aber selten sinnvoll und nicht stabil über Versions-Grenzen.

Functional Options — das idiomatische Konfigurations-Pattern

Das Functional-Options-Pattern ist die Königsdisziplin der First-Class-Functions in Go. Es löst das Problem konfigurierbarer Konstruktoren ohne Telescoping-Parameter-Listen und ohne mutable Config-Structs. Idee: Der Konstruktor nimmt eine Variadic-Liste von Funktionen entgegen, die nacheinander auf eine interne Default-Konfiguration angewendet werden:

Go functional_options.go
package main

import (
    "fmt"
    "time"
)

type Server struct {
    host    string
    port    int
    timeout time.Duration
    tls     bool
}

// Option ist ein Function Type — eine Mutation auf *Server
type Option func(*Server)

// Konstruktor-Funktionen für die einzelnen Options
func WithHost(h string) Option {
    return func(s *Server) { s.host = h }
}

func WithPort(p int) Option {
    return func(s *Server) { s.port = p }
}

func WithTimeout(d time.Duration) Option {
    return func(s *Server) { s.timeout = d }
}

func WithTLS() Option {
    return func(s *Server) { s.tls = true }
}

// Konstruktor: Defaults + alle Options nacheinander anwenden
func NewServer(opts ...Option) *Server {
    s := &Server{
        host:    &quot;localhost&quot;,
        port:    8080,
        timeout: 30 * time.Second,
        tls:     false,
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

func main() {
    s1 := NewServer()
    fmt.Printf(&quot;%+v\n&quot;, s1)

    s2 := NewServer(
        WithHost(&quot;example.com&quot;),
        WithPort(443),
        WithTLS(),
    )
    fmt.Printf(&quot;%+v\n&quot;, s2)
}
Output
&{host:localhost port:8080 timeout:30s tls:false}
&{host:example.com port:443 timeout:30s tls:true}

Vorteile dieses Patterns:

  • Vorwärts-kompatibel. Neue Option hinzufügen bricht keine bestehenden Aufrufer.
  • Selbst-dokumentierend. WithTLS() ist klarer als true als dritter Parameter.
  • Default-Werte zentral. Im Konstruktor — Aufrufer sehen nur die Abweichungen.
  • Optionen-Reihenfolge ist Bedeutung. Wer eine Option zweimal setzt, gewinnt mit der letzten — das ist meistens gewollt, kann aber zur Falle werden.

Dieses Pattern wird in der gRPC-Bibliothek, in zap, in prometheus/client_golang und vielen weiteren prominenten Go-Bibliotheken benutzt. Wer eine Bibliothek mit mehr als zwei Konfigurations-Werten baut, sollte es kennen.

Strategy-Pattern und Command-Dispatch

Zwei weitere Patterns, die fast trivial werden, sobald Funktionen First-Class sind:

Strategy — Verhalten parametrisieren, ohne Klassen-Hierarchien:

Go strategy.go
package main

import "fmt"

type Pricer func(quantity int, unit float64) float64

func flat(qty int, unit float64) float64 {
    return float64(qty) * unit
}

func bulk(qty int, unit float64) float64 {
    if qty >= 10 {
        return float64(qty) * unit * 0.9
    }
    return float64(qty) * unit
}

func calculate(qty int, unit float64, strategy Pricer) float64 {
    return strategy(qty, unit)
}

func main() {
    fmt.Println(calculate(15, 2.0, flat)) // 30
    fmt.Println(calculate(15, 2.0, bulk)) // 27 (10% Rabatt)
}
Output
30
27

Command-Dispatch — eine Map von String-Keys auf Funktionen ersetzt den langen switch:

Go dispatch.go
package main

import "fmt"

type Command func(args []string)

var commands = map[string]Command{
    &quot;greet&quot;: func(args []string) { fmt.Println(&quot;Hallo,&quot;, args[0]) },
    &quot;sum&quot;:   func(args []string) { fmt.Println(&quot;Summe:&quot;, len(args)) },
    &quot;quit&quot;:  func(_ []string) { fmt.Println(&quot;Tschüss&quot;) },
}

func run(name string, args []string) {
    cmd, ok := commands[name]
    if !ok {
        fmt.Println(&quot;unbekannt:&quot;, name)
        return
    }
    cmd(args)
}

func main() {
    run(&quot;greet&quot;, []string{&quot;Welt&quot;})
    run(&quot;sum&quot;, []string{&quot;a&quot;, &quot;b&quot;, &quot;c&quot;})
    run(&quot;unknown&quot;, nil)
}
Output
Hallo, Welt
Summe: 3
unbekannt: unknown

Das ist effizient (Map-Lookup statt linearem switch), erweiterbar (neue Commands per Registrierung zur Laufzeit) und gut testbar (jedes Command isoliert).

Performance — was First-Class kostet

Ein indirekter Funktions-Aufruf (über eine Variable, einen Method Value oder einen Interface-Wert) ist minimal teurer als ein statischer Aufruf, weil der Compiler weder die Zieladresse zur Compile-Zeit kennt noch inlinen kann. Konkrete Größenordnung: einstellige Nanosekunden pro Call auf modernen CPUs — für 99 % aller Anwendungen vernachlässigbar.

Wo es spürbar werden kann:

  • Hot Loops mit Millionen Iterationen. Wenn die innere Schleife eines Hot-Path eine Closure aufruft, lohnt sich ein Benchmark.
  • Closures über große Capture-Sets. Eine Closure, die viele Variablen captured, kann diese auf den Heap escapen lassen — das kostet Allocation. go build -gcflags=&quot;-m&quot; zeigt es.
  • Method Values auf großen Value-Receivern. Die Receiver-Kopie wird einmal beim Erstellen gemacht — bei einem Mega-Struct merkt man das.

In allen anderen Fällen: First-Class-Funktionen sind kostenlos genug, um sie unbesorgt einzusetzen. Premature Optimization gilt auch hier.

Häufige Stolperfallen

Eine nil-Funktion aufrufen paniciert sofort.

var f func() ohne Zuweisung ist nil — f() führt zu runtime error: invalid memory address or nil pointer dereference. In Library-Code mit optionalen Callbacks ist die nil-Prüfung Pflicht: if hook != nil { hook(...) }. Niemals annehmen, der Caller habe alle Hooks gesetzt.

Funktions-Werte sind nicht vergleichbar — nur gegen nil.

f == g ist Compile-Fehler (func can only be compared to nil). Konsequenz: Structs mit Funktions-Feldern sind ebenfalls nicht vergleichbar, Funktions-Typen können nicht als Map-Key dienen und nicht in comparable-Constraints stehen. Wer eine Identität braucht, nutzt einen separaten String- oder int-Key als Handle.

Method Value bindet bei Value-Receivern eine Kopie.

mv := obj.Method mit Value-Receiver kopiert obj in den Method Value hinein. Spätere Änderungen an obj sind im Aufruf von mv() nicht sichtbar. Bei Pointer-Receivern (*T) wird der Pointer kopiert, nicht das Objekt — beide sehen dieselbe Mutation. Das ist meistens das gewünschte Verhalten, aber Wertsemantik kann zu subtil-falschen Bugs führen.

Method Value vs. Method Expression — leicht verwechselt.

c.Inc ist ein Method Value, Signatur func() (Receiver gebunden). (*Counter).Inc ist eine Method Expression, Signatur func(*Counter) (Receiver als erster Parameter). Wer den falschen Typ in einer Variable erwartet, kriegt cannot use ... as func(...) value. Im Zweifel: am Wert binden oder am Typ erfragen — beide Wege sind legitim, aber nicht austauschbar.

type X func(...) ist ein eigener Typ, kein Alias.

type Handler func(string) und ein anonymes func(string) sind nicht derselbe Typ — auch wenn sie strukturell identisch sind. Zuweisung geht ohne Cast, Funktions-Argument-Übergabe auch, aber var x Handler; var y func(string) = x ist erlaubt; in andere Richtung manchmal eine explizite Konvertierung nötig. Wer nur einen Alias will, schreibt type Handler = func(string) — selten sinnvoll, weil dann die Methoden-Anhängung an Handler nicht funktioniert.

Functional Options: Reihenfolge kann Bedeutung haben.

Werden zwei Options gesetzt, die den gleichen Wert manipulieren (WithTimeout(5s) gefolgt von WithTimeout(10s)), gewinnt die letzte. Das ist meistens nützlich (Defaults werden vom User überschrieben), kann aber überraschen, wenn Options sich gegenseitig konfigurieren. Empfehlung: jede Option sollte unabhängig sein und keine Order-Annahmen treffen — wenn sich Options brauchen, lieber im Konstruktor validieren.

Funktionen in Maps oder Channels brauchen Vorsicht wegen Non-Comparability.

Map-Keys müssen vergleichbar sein — Funktionen sind es nicht. Eine map[func()]int ist Compile-Fehler. Was geht: Funktionen als Values in Maps (map[string]func()) oder als Channel-Element-Typ (chan func()). Das deckt die meisten Use-Cases ab; wer wirklich Funktions-Identität braucht, wickelt sie in einen Struct mit Identitäts-Feld.

Closures über Loop-Variablen — Klassiker bis Go 1.21.

Wer in einem Loop Funktionen erzeugt und die Loop-Variable captured (for i := range xs { go func() { fmt.Println(i) }() }), bekam vor Go 1.22 in allen Goroutines denselben Wert (den finalen). Seit Go 1.22 ist die Loop-Variable pro Iteration neu — das alte Verhalten ist weg. Trotzdem in älteren Codebases ein Bug-Reservoir; siehe den eigenen Closure-Artikel.

Reflection auf Funktionen: möglich, aber selten sinnvoll.

reflect.ValueOf(f).Pointer() gibt einen Pointer auf die Code-Adresse zurück — verwendbar für Identitäts-Vergleiche oder Debug-Output. Aber: Closures mit unterschiedlichen Capture-Umgebungen können dieselbe Pointer-Adresse haben, der Wert ist nicht über Compile-Versions stabil, und der Aufruf ist langsam. Wer das braucht, hat meist ein Design-Problem; eine separate ID oder ein Interface mit Name() ist fast immer die bessere Lösung.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Funktionen

Zur Übersicht