Funktionen sind in Go das zentrale Werkzeug, um Logik klar zu kapseln und Programme strukturiert wachsen zu lassen. Sie definieren präzise Schnittstellen, trennen Verantwortlichkeiten und ermöglichen Wiederverwendung ohne unnötige Kopien. Durch Wert- und Zeigertypen, Mehrfachrückgaben sowie explizite Fehlerbehandlung bleiben Datenflüsse transparent und nachvollziehbar. Zusammen mit First-Class-Funktionen, Closures und Methoden auf Structs entsteht ein flexibles Baukastensystem, das sich von kleinen Hilfsroutinen bis zu robusten Architekturen skalieren lässt.

Einführung

Funktionen sind so ziemlich die wichtigsten Bausteine nahezu jeder Programmiersprache. Go ist hier keine Ausnahme. Sie sind der primäre Mechanismus zur Strukturierung, Wiederverwendung und Abstraktion von Code. In Go haben Funktionen einige besondere Eigenschaften, die sie von Funktionen in anderen Sprachen unterscheiden.

Go behanldelt Funktionen als First-Class Citiziens. Das bedeutet Folgendes:

  • Funktionen sind Werte: Sie können Variablen zugewiesen werden
  • Funktionen können Parameter sein: Sie können an andere Funktionen übergeben werden
  • Funktionen können zurückgegeben werden: Eine Funktion kann eine andere Funktion als Rückgabewert haben
  • Funktionen können Closures bilden: Sie können auf Variablen aus ihrem umgebenden Scope zugreifen

Diese Eigenschaften machen Go zu einer Sprache, die sowohl prozedurale als auch funktionale Programmierparadigmen unterstützt.

Die Rolle von Funktionen in Go

Im Gegensatz zu objektorientierten Sprachen wie Java oder C++, wo Klassen und Objekte im Mittelpunkt stehen, organisiert Go Code primär durch:

  1. Packages: Logische Gruppierung von Code
  2. Funktionen: Ausführbare Einheiten
  3. Methoden: Funktionen mit Receivern (auf Typen gebunden)

Es gibt keine Klassen in Go. Stattdessen kombiniert man:

  • Structs für Datenstrukturen
  • Funktionen für Verhalten
  • Methoden (Funktionen mit Receivern) für typgebundenes Verhalten
  • Interfaces für Polymorphismus

Funktionen vs. Methoden

Funktionen und Methoden unterscheiden sich in Go durch die Bindung an einen Typ.

Funktion

Go Funktion
func CalculateArea(width, height float64) float64 {
    return width * height
}

Methode (Funktion mit Receivern)

Go Methode
type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func main() {
    r := Rectangle{
        Width: 10,
        Height: 20,
    }

    area := r.Area()
}

Syntax und Aufbau

Anatomie einer Funktion

Eine Funktion in Go hat folgende Struktur.

Go
func <Name>(<Parameter>) <Rückgabetyp> {
    <Funktionskörper>
}

Einfachste Funktion

Go Beispiel
func sayHello() {
    fmt.Println("Hello world")
}

Was haben wir hier?

  • func: Keyword für Funktionsdefinition
  • sayHello: Funktionsname (CameCase-Konvention)
  • (): Leere Parameterliste (keine Parameter)
  • Kein Rückgabetyp angegeben (implizit “void”)
  • {}: Funktionskörper

Funktion mit Parameter

Go Beispiel
func greet(name string) {
    fmt.Println("Hello", name)
}

Was ist hier neu?

  • name string: Ein Parameter vom Typ string
  • Die Syntax: <Name> <Typ>
  • Der Parameter ist innerhalb der Funktion als lokale Variable verfügbar

Funktion mit Rückgabewert

Go Beispiel
func add(a int, b int) int {
    return a + b
}

Neu hier

  • int nach der Parameterliste: Rückgabetyp
  • return: Keyword zum Zurückgeben eines Wertes
  • Der Rückgabewert muss vom angegebenen Typ sein

Es besteht auch die Möglichkeit für gleichtypisierte Parameter eine Kurzschreibweise zu verwenden.

Go Beispiel
func add(a, b int) int {
    return a + b
}

Sichtbarkeit (Exported vs. Unexported)

Wie bei allen Identifiern in Go bestimmt die Groß- und Kleinschreibung des ersten Buchstabens die Sichtbarkeit.

Go Beispiel
// Exported
func PublicFunction() {
    fmt.Println("I am public")
}

// Unexported
func privateFunction() {
    fmt.Println("I am private")
}

Konvention

  • Großbuchstabe: Exportiert (öffentlich)
  • Kleinbuchstabe: Nicht exportiert (privat)

Dies gilt für: Variablen, Typen, Struct-Felder, Methoden

Die main Funktion

Jedes ausführbare Go-Programm benötigt eine main Funktion im main Package. Die Funktion muss main heißen und es muss sich im main Package befinden.

Diese Funktion nimmt keine Parameter an und gibt nichts zurück.

Go
package main

import "fmt"

func main() {
    fmt.Println("Programm starts here")
}

Die init Funktion

In Go gibt es noch eine besondere Funktion init(). Diese Funktion wird automatisch beim Package-Import aufgerufen. Ausgeführt wird diese Funktion von main() Funktion.

Man kann diese Funktion mehrfach im selben Package definieren. Auch diese Funktion nimmt keine Parameter entgegen und gibt auch nichts zurück.

Wichtig: Die Reihenfolge der Ausführung ist folgende:

  1. Alle importierten Packages werden initalisiert (rekursiv)
  2. Alle init() Funktionen werden ausgeführt
  3. Die main() Funktion wird ausgeführt Schauen wir uns das ganze an einem Beispiel an. Da die init() Funktion in einem Package beim Import ausgeführt wird, brauchen wir ein Test-Package.

Bevor wir mit dem Erstellen der Go-Dateien beginnen, sollten wir ein Go-Modul initialisieren.

Go
go mod init mibeon.com/init-func
Go lib/lib.go
package lib

import "fmt"

func init() {
    SayHello()
}

func SayHello() {
    fmt.Println("Greetings from lib package")
}

Nun werden wir dieses Package in unserer Haupt-Datei importieren.

Go main.go
package main

import (
    "fmt"

    _ "mibeon.com/init-func/lib"
)

func main() {
    fmt.Println("Running main")
}

Wenn wir nun diesen Code mit go run main.go ausführen, erhalten wir Folgendes in der Ausgabe.

Go
Greetings from lib package
Running main

Parameter und Argumente

Terminologie

Es empfiehlt sich zwischen Parametern und Argumenten zu unterscheiden.

  • Parameter: Die Variablen in der Funktionsdefinition
  • Argumente: Die tatsächlichen Werte beim Funktionsaufruf
Go
func add(a, b int) int { // a und b sind Parameter
    return a + b
}

res := add(3, 4) // 3 und 4 sind Argumente

Parameter-Typen

In Go müssen alle Parameter typisiert sein. Es gibt keine implizite Typen oder Type Inference bei Parametern.

Go Korrekt
func process(x int, y float64, name string) {
    // ...
}
Go Fehler
func process(x, y name) {
    // ...
}

Mehrere Parameter gleichen Typs

Wie bereits erwähnt, können aufeinanderfolgende Parameter desselben Typs zusammengefasst werden.

Go Beispiel
// Langform
func calc(width float64, height float64, depth float64) float64 {
    return width * height * depth
}

// Kurzform
func calc(width, height, depth float64) float64 {
    return width * height * depth
}

Gemischte Typen, jeder Parameter mit seinem eigenen Typ, würden wie folgt aussehen.

Go Beispiel
func createUser(id int, name, email string, age int, active bool) User {
    // ...
}

Pass by Value

In Go werden Argumente immer by Value übergeben. Das bedeutet, die Funktion erhält eine Kopie der Argumente.

Schauen wir uns ein Beispiel an.

Go Beispiel
package main

import "fmt"

func modify(x int) {
    x = 100
    fmt.Println("Innerhalb Funktion:", x)
}

func main() {
    num := 42
    modify(num)
    fmt.Println("Außerhalb Funktion:", num)
}
Output
Innerhalb Funktion: 100
Außerhalb Funktion: 42

Was passiert in diesem Beispiel?

  1. num hat den Wert 42
  2. Bei modify(num) wird eine Kopie von 42 erstellt
  3. Diese Kopie wird dem Parameter x zugewiesen
  4. Änderungen an x betreffen nur die Kopie
  5. Das Original num bleibt unverändert

Um dieses Problem zu lösen, verwendet man Pointer.

Pointer-Parameter für Modifikationen

Wenn man das Original ändern möchte, übergibt man einen Pointer. Dabei ist wichtig, dass die Funktion einen korrekten Pointer-Typ als Parameter annimmt.

Go Beispiel
package main

import "fmt"

func modify(x *int) {
    *x = 100
    fmt.Println("Innerhalb Funktion:", *x)
}

func main() {
    num := 42
    modify(&num)
    fmt.Println("Außerhalb Funktion:", num)
}
Output
Innerhalb Funktion: 100
Außerhalb Funktion: 100

Was passiert nun in diesem Beispiel? Was ist hier anders?

  1. num hat den Wert 42
  2. &num gibt die Adresse von num zurück
  3. Diese Adresse wird by Value kopiert, allerdings in diesem Fall wird der Pointer kopiert
  4. *x = 100 ändert den Wert an der Adresse
  5. Das Original num wird geändert

Slices, Maps und Channels

Für Slices, Maps und Channels gilt eine Besonderheit: Sie enthalten intern bereits Pointer/Referenzen.

Wenn wir das folgende Beispiel anschauen, werden das Verhalten beobachten können.

Go Beispiel
package main

import "fmt"

func modifySlice(s []int) {
    s[0] = 999
}

func main() {
    nums := []int{1, 2, 3, 4}
    modifySlice(nums)
    fmt.Println(nums)
}
Output
[999 2 3 4]

Warum funktioniert das?

In diesem Beispiel haben wir unsere Slice-Variable nicht als einen Pointer, sondern direkt (klassisch) übergeben. Warum könnten wir dennoch einen Wert an der ersten Position im Slice ändern?

Das hängt damit zusammen, dass Slice intern folgende Struktur hat.

Go
type slice struct {
    ptr *Element  // Pointer auf das zugrundeliegende Array
    len int
    cap int
}

Beim Funktionsaufruf wird diese Struktur kopiert, aber der ptr zeigt weiterhin auf dasselbe zugrundeliegende Array. Daher sind Änderungen an den Elementen sichtbar.

Erweiterung mit append - Besonderheiten

Mit append kann man das Original-Slice in einer Funktion nicht direkt erweitern, obwohl ein Slice intern ein Pointer beinhaltet.

Go Beispiel
package main

import "fmt"

func modifySlice(s []int) {
    s = append(s, 999)
    fmt.Println("Innerhalb Funktion:", s)
}

func main() {
    mySlice := []int{1, 2, 3, 4}
    modifySlice(mySlice)
    fmt.Println("Außerhalb Funktion:", mySlice)
}
Output
Innerhalb Funktion: [1 2 3 4 999]
Außerhalb Funktion: [1 2 3 4]

Wie wir sehen können, wurde nur das Slice innerhalb der Funktion aktualisiert. Die Lösung für die Verwendung von append wäre, ein neues Slice zurückzugeben.

Go Beispiel
package main

import "fmt"

func modifySlice(s []int) []int {
    return append(s, 999)
}

func main() {
    mySlice := []int{1, 2, 3, 4}
    mySlice = modifySlice(mySlice)
    fmt.Println("Nach Änderung:", mySlice)
}
Output
Nach Änderung: [1 2 3 4 999]

Oder, als Alternative, einfach einen Pointer verwenden.

Go Beispiel
package main

import "fmt"

func modifySlice(s *[]int) {
    *s = append(*s, 999)
}

func main() {
    mySlice := []int{1, 2, 3, 4}
    modifySlice(&mySlice)
    fmt.Println("Nach Änderung:", mySlice)
}
Output
Nach Änderung: [1 2 3 4 999]

Zusammenfassung: Wann braucht man einen Pointer?

Hier eine Übersicht, wann ein Pointer benötigt wird, wenn man ein Argument an eine Funktion übergibt.

TypVerhaltenPointer nötig?
Primitive Werte (int, float, bool)KopieJa, für Modifikation
StructsKopieJa, für Modifikation oder Performance
ArraysKopie (gesamtes Array)Ja, für Modifikation
SlicesKopie der Slice-Struktur, aber gemeinsames zugrundeliegende ArrayNur für Append/Neuzuweiseung
MapsKopie der Map-PointersNein
ChannelsKopie der Channel-ReferenzNein

Rückagebwerte

Go hat ein besonderes Feature: Multiple Return Values. Das bedeutet, dass wir in Go nicht nur einen, sonderen auch mehrere Rückgabewerte bei einer Funktion haben können.

Single return value

Hierbei wird ein Wert zurückgegeben. Klassisches Schema, wie auch in einigen anderen Programmiersprachen.

Go Beispiel
func double(x int) int {
    return x * 2
}

res := double(21)

Multiple return value

Wie bereits einleitend zu diesem Abschnitt erwähnt, erlaubt Go mehrere Rückgabewerte.

Go Mehrere Rückgabewerte
package main

import "fmt"

func divMod(a, b int) (int, int) {
    quotient := a / b
    remainder := a % b
    return quotient, remainder
}

func main() {
    q, r := divMod(17, 5)
    fmt.Println(q, r)
}
Output
3 2

Sobald eine Funktion mehr als einen Rückgabewert hat, sollen diese in runde Klammern () gesetzt werden. Wenn eine Funktion mehrere Werte zurückgibt, sollen ebenfalls auf der anderen Seite mehrere Werte mittels Mehrfachzuweisung empfangen werden.

Das Error-Handling Pattern

Der häufigste Anwendungsfall für Multiple Returns ist Error-Handling. Damit hat man die Möglichkeit bei einer korrekten Funktionsweise einer Funktion den berechneten Wert und im Falle eines Fehlers einen Fehler zurückzugeben.

Go Beispiel
package main

import (
    "fmt"
    "errors"
)

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero error")
    }

    return a / b, nil
}

func main() {
    res, err := divide(10, 2)
    if err != nil {
        fmt.Println("Fehler:", err)
        return
    }

    fmt.Println("Resultat:", res)
}
Output
Resultat: 5

Konvention:

  • Ein Fehler wird immer als letzter Rückgabewert zurückgegeben
  • Bei Erfolg: return <result>, nil
  • Bei Fehler: return <zero-value>, <error>

Blank Identifier für ungenutzte Rückgabewerte

Manchmal benötigt man für die weitere Verarbeitung nicht alle Rückgabewerte einer Funktion, wenn diese mehrere Werte zurückgibt. In diesem Fall wird der sogenannte Blank Identifier, geschrieben als _ verwendet.

Wichtig: Wenn man einen Rückgabewert normal (als Variable) definiert aber nicht verwendet, gibt es einen Fehler während der Kompilierung.

Go Beispiel
package main

import (
    "fmt"
    "errors"
)   

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero error")
    }

    return a / b, nil
}

func main() {
    // Nur auf Fehler prüfen, kein Resultat speichern
    _, err := divide(10, 0)
    if err != nil {
        fmt.Println("Fehler:", err)
    }
}
Output
Fehler: division by zero error

Benannte Rückgabewerte (named return values)

Go erlaubt es, den Rückgabewerten Namen zu geben. In diesem Fall kann man einfach das Schlüsselwort return angeben, ohne spezifisch die Variablennamen dahinter zu schreiben. Diese werden automatisch zurückgegeben.

Go Beispiel
func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

Was genau passiert hier?

  • x und y sind benannte Rückgabewerte
  • Sie werden automatisch als Variablen im Funktionsscope definiert
  • Sie haben den Zero Value ihres Typs (hier: 0)
  • return ohne Argumente (Naked Return) gibt sie zurück

Im Grunde ist das obere Beispiel ein Äquivalent zu folgendem Code.

Go
func split(sum int) (int, int) {
    x := sum * 4 / 9
    y := sum - x
    return x, y
}

Named Return und defer

Named Returns können durchaus mit Verwendung von defer nützlich sein, da defer den Return Value (Rückgabewert) modifizieren kann.

Go Beispiel
package main

import "fmt"

func increment() (result int) {
    defer func() {
        result++
    }()

    return 5
}

func main() {
    fmt.Println(increment())
}

Variadic Functions - Variable Argumentenanzahl

Variadic Functions sind Funktionen, die eine variable Anzahl von Argumenten akzeptieren. Das bekannteste Beispiel ist fmt.Println.

Die ... Syntax

Variadic Parameter muss an der letzten Position in der Parameterliste stehen und wird mit ... vor dem Typ angegeben. Mit Hilfe von ... werden mehrere Werte, welche beim Aufruf der Funktion übergeben werden, in einer Variable innerhalb der Funktion gebündelt.

Go Beispiel
package main

import "fmt"

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    fmt.Println(sum())
    fmt.Println(sum(1))
    fmt.Println(sum(1, 2, 3))
    fmt.Println(sum(1, 2, 3, 4, 5, 6, 7))
}
Output
0
1
6
28

Wie funktioniert es intern?

Der Compiler konvertiert die Argumente in ein Slice.

Go
sum(1, 2, 3)

// Wird intern zu
sum([]int{1, 2, 3})

Daher ist nums im oberen Beispiel innerhalb der Funktion ein normales Slice.

Regeln für Variadic Parameters

1. Nur ein Variadic Parameter

Es kann nur ein Variadic Parameter pro Funktion geben.

2. Letzte Position

Der Variadic Parameter muss der letzte in der Parameterliste sein.

Go Beispiel
// ✅ Korrekt
func log(level string, messages ...string) {}

// ❌ Fehler - Position
func log(messages ...string, level string) {}

// ❌ Fehler - Anzahl
func log(messages ...string, errors ...error) {}

Slice entpacken mit ...

Wenn wir ein existierendes Slice an eine Variadic Funktion übergeben, müssen wir dafür sorgen, dass wir einzlene Werte übergeben bzw. korrekte Werte.

Schauen wir uns ein Beispiel an.

Go Beispiel
package main

import "fmt"

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

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

    // Mit ... entpacken
    result := sum(numbers...)

    // Ohne ... => ❌ Fehler
    // result := sum(numbers)
}

Was passiert beim entpacken?

Go
sum(numbers...)

// Ist äquivalent zu
sum(numbers[0], numbers[1], numbers[2], numbers[3])

Funktionen als First-Class Citizens

In Go sind Funktionen First-Class Citizens. Das bedeutet, sie können wie jeder andere Wert behandelt werden.

Funktionen können:

  • Einer Variable zugewiesen werden
  • Als Argument übergeben werden
  • Als Rückgabewert verwendet werden
  • In Datenstrukturen gespeichert werden

Diese Eigenschaft ermöglicht funktionale Programmierpatterns in Go.

Funktionen als Variablen

Eine Funktion kann einer Variable zugewiesen werden. Wichtig ist dabei, dass die Funktionssignatur mit dem Typ der Variable übereinstimmen.

Go Beispiel
package main

import "fmt"

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

func main() {
    // Funktion einer Variable zuweisen
    var operation func(int, int) int
    operation = add

    res := operation(5, 3)
    fmt.Println(res)
}
Output
8

Was ist hier wichtig?

  • operation ist eine Variable vom Typ func(int, int) int
  • operation = add weist die Funktion add der Variable zu
  • operation(5, 3) ruft die Funktion über die Variable auf

Function Types

Der Typ einer Funktion wird in Go durch ihre Signatur bestimmt. Wenn wir das Beispiel von oben nochmals verwenden, so würde die Typ-Anatomie wie folgt aussehen:

Go
func(int, int) int
|    |     |    |
|    |     |    +-- Rueckgabetyp
|    |     +------- Zweiter Parameter
|    +------------- Erster Parameter
+------------------ Function Keyword

Schauen wir uns ein Beispiel an.

Go Beispiel
package main

import "fmt"

// Diese Funktionen haben alle denselben Typ
func add(a, b int) int { return a + b }
func multiply(a, b int) int { return a * b }
func subtract(a, b int) int { return a - b }

// Typ-Alias für Lesbarkeit
type BinaryOperation func(int, int) int

func main() {
    var op BinaryOperation

    op = add
    fmt.Println(op(5, 3))

    op = multiply
    fmt.Println(op(5, 3))

    op = subtract
    fmt.Println(op(5, 3))
}
Output
8
15
2

Wie wir in diesem Beispiel sehen, können wir unserer op Variable jeweils eine andere Funktion zuweisen, bevor wir sie ausführen. Das ist dank der identischen Signatur der Funktionen möglich.

Funktionen als Parameter (Higher-Order Functions)

Funktionen können als Argumente an andere Funktionen übergeben werden.

Go Beispiel
package main

import "fmt"

func apply(nums []int, operation func(int) int) []int {
    res := make([]int, len(nums))
    for i, n := range nums {
        res[i] = operation(n)
    }

    return res
}

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

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

func main() {

    nums := []int{1, 2, 3, 4, 5}

    doubled := apply(nums, double)
    fmt.Println(doubled)

    squared := apply(nums, square)
    fmt.Println(squared)

}
Output
[2 4 6 8 10]
[1 4 9 16 25]

Funktionen als Rückgabewerte

In Go ist es ebenfalls möglich eine Funktion als Rückgabewert einer anderen Funktion zu haben. Hierbei sei das Stichwort Closure erwähnt.

Go Beispiel
package main

import "fmt"

func makeMultiplier(factor int) func(int) int {
    return func(x int) int {
        return x * factor
    }
}

func main() {
    double := makeMultiplier(2)
    triple := makeMultiplier(3)

    fmt.Println(double(5))
    fmt.Println(triple(5))
}
Output
10
15

Was passiert hier?

  • Die Funktion makeMultiplier gibt eine neue Funktion zurück
  • Diese neue Funktion “erinnert” sich an factor
  • Die Funktionen double und triple sind unterschiedliche Funktionen mit unterschiedlichen factor Werten

Funktionen in Datenstrukturen

Funktionen können in Slices, Maps und Structs gespeichert werden.

In einem Slice

Go Beispiel - Slice
package main

import "fmt"

// Operation ist vom Typ: func(int, int) int
type Operation func(int, int) int

func main() {
    operations := []Operation{
        func(a, b int) int { return a + b },
        func(a, b int) int { return a - b },
        func(a, b int) int { return a * b },
        func(a, b int) int { return a / b },
    }

    for i, op := range operations {
        fmt.Printf("Operation %d: %d\n", i, op(10, 5))
    }
}
Output
Operation 0: 15
Operation 1: 5
Operation 2: 50
Operation 3: 2

In einer Map

Go Beispiel - Map
package main

import "fmt"

func main() {
    operations := map[string]func(int, int) int {
        "add": func(a, b int) int { return a + b },
        "subtract": func(a, b int) int { return a - b },
        "multiply": func(a, b int) int { return a * b },
        "divide": func(a, b int) int { return a / b },
    }

    result := operations["multiply"](6, 7)
    fmt.Println(result)
}
Output
42

In einem Struct

Go Beispiel - Struct
package main

import "fmt"

type Calculator struct {
    Add func(int, int) int
    Subtract func(int, int) int
}

func main() {
    calc := Calculator{
        Add: func(a, b int) int { return a + b },
        Subtract: func(a, b int) int { return a - b },
    }

    fmt.Println(calc.Add(10, 5))
    fmt.Println(calc.Subtract(10, 5))
}
Output
15
5

Strategy Pattern

Ein klassisches Design-Pattern mit Fist-Class Functions.

Go Beispiel - Struct
package main

import "fmt"

type PaymentStrategy func(amount float64) error

func creditCardPayment(amount float64) error {
    fmt.Printf("Paying %.2f with credit card\n", amount)
    return nil
}

func paypalPayment(amount float64) error {
    fmt.Printf("Paying %.2f with PayPal\n", amount)
    return nil
}

func processPayment(amount float64, strategy PaymentStrategy) error {
    return strategy(amount)
}

func main() {
    processPayment(97.50, creditCardPayment)
    processPayment(75.25, paypalPayment)
}
Output
Paying 97.50 with credit card
Paying 75.25 with PayPal

Was ist hier der Flow?

  1. Wir definieren einen Typ PaymentStrategy. Jede Funktion in unserem Code, welche die Signatur func(amount float64) error hat, entspricht automatisch diesem Typ.
  2. Wir erstellen zwei Funktionen, welche genau dieser Signatur entsprechen.
    • creditCardPayment(amount float64) error
    • paypalPayment(amount float64) error
  3. Wir erstellen eine Funktion, welche beliebige Payment-Arten verarbeiten kann, die der Signatur PaymentStrategy entsprechen. Diese Funktion nimmt den tatsächlichen amount Wert an und die verwendete Funktion, mit einer passenden Signatur.
  4. In unserer main Funktion rufen wir zwei mal die verarbeitende Funktion processPayment auf und übergeben die jeweilige Konfiguration.

Callback Pattern

Callbacks sind Funktionen, die an andere Funktionen übergeben werden, um später aufgerufen zu werden.

Go Beispiel - Callback
package main

import (
    "fmt"
    "time"
)

type Callback func(result int)

func asyncCompute(a, b int, callback Callback) {
    go func() {
        result := a * b
        callback(result)
    }()
}

func main() {
    asyncCompute(6, 7, func(result int) {
        fmt.Println("Result:", result)
    })

    // Kurz warten, damit die Goroutine fertig wird
    time.Sleep(100 * time.Millisecond)
}
Output
Result: 42

Method Values

Ein Method Value ist eine Methode, die bereits an eine bestimmte Instanz gebunden ist. Bei diesen Methoden haben wir einen so genannten Receiver (Empfänger). Man kann eine grobe Analogie zu Klassen-Methoden hier aufbauen.

Go Beispiel
package main

import "fmt"

type Calculator struct {
    value float64
}

func (c Calculator) Add(x float64) float64 {
    return c.value + x
}

func main() {
    calc := Calculator{value: 30}

    // Method value: Bereits an calc gebunden
    addFunc := calc.Add

    // Aufruf ohne erneute Angabe des Empfängers
    res := addFunc(5)
    fmt.Println(res)
}
Output
35

Schauen wir uns noch ein Beispiel an.

Go Beispiel
package main

import "fmt"

type Counter struct {
    count int
}

func (c *Counter) Increment() {
    c.count++
}

func (c *Counter) Decrement() {
    c.count--
}

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

    // Method value - Funktion mit gebundenem Receiver
    increment := counter.Increment
    increment()
    increment()

    fmt.Println(counter.count)
}
Output
2

Method Expressions

Ein Method Expression ist eine Methode, die nocht nicht an eine Instanz gebunden ist. Es erfordert beim Aufruf die explizite Angabe des Empfängers (Receivers) als ersten Parameter.

Go Beispiel
package main

import "fmt"

type Calculator struct {
    value float64
}

func (c *Calculator) Add(x float64) float64 {
    return c.value + x
}

func main() {

    // Method Expression (Signatur)
    var addFunc func(*Calculator, float64) float64

    // Zuweisung der Method Expression
    addFunc = Calculator.Add

    calc1 := &Calculator{value: 10}
    calc2 := &Calculator{value: 100}

    res1 := addFunc(calc1, 5)
    res2 := addFunc(calc2, 5)

    fmt.Println(res1, res2)
}
Output
15 105

Was genau passiert hier?

  • Es gibt einen Typ Calculator, welcher eine Eigenschaft value hat.
  • Wir erzeugen eine Receiver-Funktion func(c *Calculator) Add(x float64) float64 {}
  • Diese Funktion erwartet einen Pointer auf einen Wert vom Typ Calculator.
  • Weiter unten erstellen wir eine Variable addFunc. Diese Variable hat folgenden Typ: func(*Calculator, float64) float64. Zunächst ist dies einfach eine lose Variable (ungebunden).
  • Es erfolgt eine Zuweisung der addFunc zu (*Calculator).Add Funktion. Wichtig ist hier, dass es ein Pointer ist ((*Calculator))

Nil Funktionen

Funktionsvariablen können nil sein. Dies ist der Fall, wenn die nur deklariert, aber nicht initialisiert werden.

Go Beispiel
package main

import "fmt"

func main() {
    var f func(int) int

    if f == nil {
        fmt.Println("f ist nil")
    }
}
Output
f is nil

Es empfiehlt sich bei solchen Szenarien immer eine Funktion auf nil Wert zu prüfen, da sonst eine Panic ausgelöst wird.

Go Beispiel
package main

import "fmt"

func execute(f func() string) {
    if f != nil {
        res := f()
        fmt.Println(res)
    } else {
        fmt.Println("No function provided")
    }
}

func runnerFunc() string {
    return "Hello from function"
}

func main() {
    execute(runnerFunc)
}
Output
Hello from func

Anonyme Funktionen

Anonyme Funktionen (auch Function Literals genannt) sind Funktionen ohne Namen. Sie werden inline definiert.

Syntax

Go
func(parameter) returnType {
    // ...
}

Hier gibt es keine Namensangabe zwischen func und den Parametern. Zudem werden anonyme Funktionen oft direkt aufgerufen.

Go Beispiel
package main

import "fmt"

func main() {
    func() {
        fmt.Println("Hi aus anonymer Funktion")
    }()
}
Output
Hi aus anonymer Funktion

Mit Parametern

Man kann auch eine anonyme Funktion parametrisieren.

Go Beispiel
package main

import "fmt"

func main() {
    func(name string) {
        fmt.Println("Hi,", name)
    }("Alice")
}
Output
Hi, Alice

Mit Rückgabewert

Anonyme Funktionen können auch Rückgabewert haben.

Go Beispiel
package main

import "fmt"

func main() {
    res := func(a, b) int {
        return a + b
    }(5, 3)

    fmt.Println(res)
}
Output
8

Anonyme Funktionen einer Variable zuweisen

Dies ist in der Tat sehr praktisch und kann nützlich sein.

Go Beispiel
package main

import "fmt"

func main() {
    greet := func(name string) {
        fmt.Println("Hi,", name)
    }

    greet("John")
    greet("Tom")
}
Output
Hi, John
Hi, Tom

Use Case: Einmalige Logik

Wenn eine Funktion nur an einer Stelle benötigt wird, kann der Einsatz von anonymen Funktionen ebenfalls sehr hilfreich sein.

Go Beispiel
package main

import "fmt"

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

    // Anonyme Funktion für einmalige Transformation
    squared := func(nums []int) []int {
        res := make([]int, len(nums))
        for i, n := range nums {
            res[i] = n * n
        }
        return res
    }(data)

    fmt.Println(squared)
}

func main() {
    processData()
}
Output
[1 4 9 16 25]

Use Case: Goroutines

Sehr oft werden anonyme Funktionen im Zusammenhang mit Goroutines verwendet. Das ist sehr praktisch, wenn man eine bestimmte Aktion als Goroutine ausführen will.

Go Beispiel
package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        fmt.Println("Running in goroutine")
    }()

    time.Sleep(100 * time.Millisecond)
}
Output
Running in goroutine

Closures - Funktionen mit Kontext

Ein Closure ist eine Funktion, die auf Variablen aus ihrem umgebenden Scope zugreifen kann. In Go werden Closures vollständig unterstützt.

Was ist ein Closure?

Ein Closure ist im Grunde eine Funktion, die eine andere Funktion zurückgibt. Werfen wir einen Blick auf ein Beispiel, damit es klarer wird.

Go Beispiel
package main

import "fmt"

func makeCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    counter := makeCounter()

    fmt.Println(counter())
    fmt.Println(counter())
    fmt.Println(counter())
}
Output
1
2
3

Was passiert hier?

  1. makeCounter() erstellt eine lokale Variable count
  2. Die zurückgegebene anonyme Funktion “umschließt” count (ein Closure)
  3. Jeder Aufruf von counter() greift auf dieselbe count Variable zu
  4. count “überlebt” den Return von makeCounter()

Wie funktioniert das?

Normalerweise wird eine lokale Variable zerstört, wenn die Funktion endet. Bei Closures ist etwas anders.

  • Der Compiler erkennt, dass count (aus dem Beispiel drüber) von der zurückgegebenen Funktion verwendet wird
  • count wird daher auf dem Heap statt dem Stack alloziert
  • Die Closure speichert eine Referenz auf count
  • Solange die Closure existiert, bleibt count im Speicher

Man spricht bei Closures auch über eine Art State-Management, weil bestimmte Elemente über die Rückgabe hinweg vorhanden sind.

Mehrere Closures - geteilter State

Im nachfolgenden Beispiel haben wir zwei Closures in einer Funktion, welche auf eine gemeinsame Variable zugreifen.

Go Beispiel
package main

import "fmt"

func makeCounter() (func() int, func() int) {
    count := 0

    increment := func() int {
        count++
        return count
    }

    decrement := func() int {
        count--
        return count
    }

    return increment, decrement
}

func main() {
    inc, dec := makeCounters()

    fmt.Println(inc())
    fmt.Println(inc())
    fmt.Println(dec())
    fmt.Println(dec())
}
Output
1
2
1
0

Closure und defer

Bei Closures die mit defer verwendet werden, werden die Variablen innerhalb der Closure-Funktion evaluiert. Das bedeutet, dass es ganz am Ende der Funktion geschieht.

Go Beispiel
package main

import "fmt"

func example() {
    x := 10

    defer func() {
        fmt.Println("x ist:", x)
    }()

    x = 20
}

func main() {
    example()
}
Output
x ist: 20

Im Gegensatz zu dieser Verwendung, bei welcher die Argumente sofort evaluiert.

Go Beispiel
package main

import "fmt"

func example() {
    x := 10

    defer fmt.Println("x ist:", x)

    x = 20
}

func main() {
    example()
}
Output
x ist: 10

Defer - Verzögerte Ausführung

defer ist ein einzigartiges Feature in Go, das die Ausführung einer Funktion oder eines Statements bis zum Ende der umgebenden Funktion verzögert.

Grundkonzept

Im folgenden Beispiel sind zwei Ausgabe-Statements definiert. Eines davon wird sofort ausgeführt. Das andere (mit defer) wird erst am Ende der Funktion ausgeführt.

Go Beispiel
package main

import "fmt"

func example() {
    defer fmt.Println("Go")
    fmt.Println("Hello")
}

func main() {
    example()
}
Output
Hello
Go

Wann wird defer ausgeführt? Deferred Funktionen werden ausgeführt:

  • Nach dem return Statement (aber bevor die Funktion tatsächlich endet)
  • Immer - auch bei panic
  • In LIFO Reihenfolge (Last In, First Out)

Verwendung: Resource Cleanup

Der häufige Anwendungsfall ist das Schließen von Ressourcen wie Dateien oder Streams.

Go Beispiel
func readFile(filename string) (string, error) {
    file, err := os.Open(filename)
    if err != nil {
        return "", err
    }

    // Wird IMMER ausgeführt
    defer file.Close()

    data, err := io.ReadAll(file)
    if err != nil {
        return "", err
    }

    return string(data), nil
}

Vorteile sind dabei:

  • file.Close() steht direkt nach os.Open() - leicht erkennbar
  • Wird garantiert ausgeführt, egal welchen Pfad die Funktion nimmt
  • Kein vergessenes Cleanup in Error-Pfaden

Ausführungsreihenfolge: LIFO (Stack)

Mehrere defers werden in umgekehrter Reihenfolge ausgeführt.

Go Beispiel
package main

import "fmt"

func example() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")

    fmt.Println("Function body")
}

func main() {
    example()
}
Output
Function body
3
2
1

Hier noch ein Beispiel mit mehr Bezug zu Realität.

Go
func process() {
    db, _ := openDatabase()
    defer db.Close()

    file, _ := os.Open("data.txt")
    defer file.Close()

    conn, _ := net.Dial("tcp", "server:8080")
    defer conn.Close()

    // ...
}

Cleanup-Reihenfolge:

  1. conn.Close() (zuletzt geöffnet, zuerst geschlossen)
  2. file.Close()
  3. db.Close() (zuerst geöffnet, zuletzt geschlossen)

Defer kann Rückgabewerte modifizieren

Mit Named Returns kann defer Rückgabewerte modifizieren.

Go Beispiel
package main

import "fmt"

func increment() (result int) {
    defer func(
        result++
    ) {}()

    return 5
}

func main() {
    fmt.Println(increment())
}
Output
6

Defer in Loops - Performance Problem

Bei Verwendung von defer in Schleifen kann es zum Problem kommen. Da alle defer(s) erst am Ende ausgeführt werden, kann es zu Überlastung/Memory-Leak oder File-Limit kommen.

Go Beispiel - Problem
func processFiles(files []string) error {
    for _, filename := range files {
        file, err := os.Open(filename)
        if err != nil {
            return err
        }
        defer file.Close() // ❌ Problem
    }

    // Process file

    return nil
}

Bei 1000 Dateien bleiben 1000 File-Handles offen. Welche Maßnahmen können hier unternommen bzw. welche Lösungen können hier verwendet werden?

Lösung 1: Explizites Close

Wir können die Dateien direkt schließen, ohne defer.

Go Lösung 1
func processFiles(files []string) error {
    for _, filename := range files {
        file, err := os.Open(filename)
        if err != nil {
            return err
        }
        // Process file

        file.Close()
    }
}

Lösung 2 - Separate Funktion

Wir können auch die Datei-Verarbeitung vollständig in eine eigene Funktion auslagern. Das empfiehlt sich auch.

Go Lösung 2
func processFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close() // Jede Datei wird geschlossen

    // Process file ...
    return nil
}

func processFiles(files []string) error {
    for _, filename := range files {
        if err := processFile(filename); err != nil {
            return err
        }
    }

    return nil
}

Defer und Panic

Deferred Funktionen werden auch bei Panic ausgeführt.

Go Beispiel
package main

import "fmt"

func example() {
    defer fmt.Println("This always runs")
    panic("Some panic here")
    fmt.Println("This never runs")
}
Output
This always runs
panic: Some panic here

... Rest der Panic-Meldung
/ Weiter

Zurück zu Grundlagen

Zur Übersicht