Funktionen
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.
Inhaltsverzeichnis
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:
- Packages: Logische Gruppierung von Code
- Funktionen: Ausführbare Einheiten
- 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
func CalculateArea(width, height float64) float64 {
return width * height
}Methode (Funktion mit Receivern)
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.
func <Name>(<Parameter>) <Rückgabetyp> {
<Funktionskörper>
}Einfachste Funktion
func sayHello() {
fmt.Println("Hello world")
}Was haben wir hier?
func: Keyword für FunktionsdefinitionsayHello: Funktionsname (CameCase-Konvention)(): Leere Parameterliste (keine Parameter)- Kein Rückgabetyp angegeben (implizit “void”)
{}: Funktionskörper
Funktion mit Parameter
func greet(name string) {
fmt.Println("Hello", name)
}Was ist hier neu?
name string: Ein Parameter vom Typstring- Die Syntax:
<Name> <Typ> - Der Parameter ist innerhalb der Funktion als lokale Variable verfügbar
Funktion mit Rückgabewert
func add(a int, b int) int {
return a + b
}Neu hier
intnach der Parameterliste: Rückgabetypreturn: 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.
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.
// Exported
func PublicFunction() {
fmt.Println("I am public")
}
// Unexportet
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.
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:
- Alle importierten Packages werden initalisiert (rekursiv)
- Alle
init()Funktionen werden ausgeführt - 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 mod init mibeon.com/init-funcpackage lib
import "fmt"
func init() {
SayHello()
}
func SayHello() {
fmt.Println("Greetings from lib package")
}Nun werden wir dieses Package in unserer Haupt-Datei importieren.
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.
Greetings from lib package
Running mainParameter 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
func add(a, b int) int { // a und b sind Parameter
return a + b
}
res := add(3, 4) // 3 und 4 sind ArgumenteParameter-Typen
In Go müssen alle Parameter typisiert sein. Es gibt keine implizite Typen oder Type Inference bei Parametern.
func process(x int, y float64, name string) {
// ...
}func process(x, y name) {
// ...
}Mehrere Parameter gleichen Typs
Wie bereits erwähnt, können aufeinanderfolgende Parameter desselben Typs zusammengefasst werden.
// 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.
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.
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)
}Innerhalb Funktion: 100
Außerhalb Funktion: 42Was passiert in diesem Beispiel?
numhat den Wert42- Bei
modify(num)wird eine Kopie von42erstellt - Diese Kopie wird dem Parameter
xzugewiesen - Änderungen an
xbetreffen nur die Kopie - Das Original
numbleibt 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.
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)
}Innerhalb Funktion: 100
Außerhalb Funktion: 100Was passiert nun in diesem Beispiel? Was ist hier anders?
numhat den Wert42&numgibt die Adresse vonnumzurück- Diese Adresse wird by Value kopiert, allerdings in diesem Fall wird der Pointer kopiert
*x = 100ändert den Wert an der Adresse- Das Original
numwird 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.
package main
import "fmt"
func modifySlice(s []int) {
s[0] = 999
}
func main() {
nums := []int{1, 2, 3, 4}
modifySlice(nums)
fmt.Println(nums)
}[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.
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.
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)
}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.
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)
}Nach Änderung: [1 2 3 4 999]Oder, als Alternative, einfach einen Pointer verwenden.
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)
}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.
| Typ | Verhalten | Pointer nötig? |
|---|---|---|
Primitive Werte (int, float, bool) | Kopie | Ja, für Modifikation |
| Structs | Kopie | Ja, für Modifikation oder Performance |
| Arrays | Kopie (gesamtes Array) | Ja, für Modifikation |
| Slices | Kopie der Slice-Struktur, aber gemeinsames zugrundeliegende Array | Nur für Append/Neuzuweiseung |
| Maps | Kopie der Map-Pointers | Nein |
| Channels | Kopie der Channel-Referenz | Nein |
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.
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.
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)
}3 2Sobald 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.
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)
}Resultat: 5Konvention:
- 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.
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)
}
}Fehler: division by zero errorBenannte 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.
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}Was genau passiert hier?
xundysind benannte Rückgabewerte- Sie werden automatisch als Variablen im Funktionsscope definiert
- Sie haben den Zero Value ihres Typs (hier:
0) returnohne Argumente (Naked Return) gibt sie zurück
Im Grunde ist das obere Beispiel ein Äquivalent zu folgendem Code.
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.
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.
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))
}Wie funktioniert es intern?
Der Compiler konvertiert die Argumente in ein Slice.
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.
// ✅ 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.
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?
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.
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)
}8Was ist hier wichtig?
operationist eine Variable vom Typfunc(int, int) intoperation = addweist die Funktionaddder Variable zuoperation(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:
func(int, int) int
| | | |
| | | +-- Rueckgabetyp
| | +------- Zweiter Parameter
| +------------- Erster Parameter
+------------------ Function KeywordSchauen wir uns ein Beispiel an.
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))
}8
15
2Wie 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.
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)
}[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.
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))
}10
15Was passiert hier?
- Die Funktion
makeMultipliergibt eine neue Funktion zurück - Diese neue Funktion “erinnert” sich an
factor - Die Funktionen
doubleundtriplesind unterschiedliche Funktionen mit unterschiedlichenfactorWerten
Funktionen in Datenstrukturen
Funktionen können in Slices, Maps und Structs gespeichert werden.
In einem 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))
}
}Operation 0: 15
Operation 1: 5
Operation 2: 50
Operation 3: 2In einer 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)
}42In einem 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))
}15
5Strategy Pattern
Ein klassisches Design-Pattern mit Fist-Class Functions.
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)
}Paying 97.50 with credit card
Paying 75.25 with PayPalWas ist hier der Flow?
- Wir definieren einen Typ
PaymentStrategy. Jede Funktion in unserem Code, welche die Signaturfunc(amount float64) errorhat, entspricht automatisch diesem Typ. - Wir erstellen zwei Funktionen, welche genau dieser Signatur entsprechen.
creditCardPayment(amount float64) errorpaypalPayment(amount float64) error
- Wir erstellen eine Funktion, welche beliebige Payment-Arten verarbeiten kann, die der Signatur
PaymentStrategyentsprechen. Diese Funktion nimmt den tatsächlichenamountWert an und die verwendete Funktion, mit einer passenden Signatur. - In unserer
mainFunktion rufen wir zwei mal die verarbeitende FunktionprocessPaymentauf und übergeben die jeweilige Konfiguration.
Callback Pattern
Callbacks sind Funktionen, die an andere Funktionen übergeben werden, um später aufgerufen zu werden.
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)
}Result: 42Method 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.
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)
}Schauen wir uns noch ein Beispiel an.
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)
}2Method 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.
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)
}15 105Was genau passiert hier?
- Es gibt einen Typ
Calculator, welcher eine Eigenschaftvaluehat. - 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
addFunczu(*Calculator).AddFunktion. 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.
package main
import "fmt"
func main() {
var f func(int) int
if f == nil {
fmt.Println("f ist nil")
}
}f is nilEs empfiehlt sich bei solchen Szenarien immer eine Funktion auf nil Wert zu prüfen, da sonst eine Panic ausgelöst wird.
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)
}Hello from funcAnonyme Funktionen
Anonyme Funktionen (auch Function Literals genannt) sind Funktionen ohne Namen. Sie werden inline definiert.
Syntax
func(parameter) returnType {
// ...
}Hier gibt es keine Namensangabe zwischen func und den Parametern. Zudem werden anonyme Funktionen oft direkt aufgerufen.
package main
import "fmt"
func main() {
func() {
fmt.Println("Hi aus anonymer Funktion")
}()
}Hi aus anonymer FunktionMit Parametern
Man kann auch eine anonyme Funktion parametrisieren.
package main
import "fmt"
func main() {
func(name string) {
fmt.Println("Hi,", name)
}("Alice")
}Hi, AliceMit Rückgabewert
Anonyme Funktionen können auch Rückgabewert haben.
package main
import "fmt"
func main() {
res := func(a, b) int {
return a + b
}(5, 3)
fmt.Println(res)
}8Anonyme Funktionen einer Variable zuweisen
Dies ist in der Tat sehr praktisch und kann nützlich sein.
package main
import "fmt"
func main() {
greet := func(name string) {
fmt.Println("Hi,", name)
}
greet("John")
greet("Tom")
}Hi, John
Hi, TomUse Case: Einmalige Logik
Wenn eine Funktion nur an einer Stelle benötigt wird, kann der Einsatz von anonymen Funktionen ebenfalls sehr hilfreich sein.
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()
}[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.
package main
import (
"fmt"
"time"
)
func main() {
go func() {
fmt.Println("Running in goroutine")
}()
time.Sleep(100 * time.Millisecond)
}Running in goroutineClosures - 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.
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())
}1
2
3Was passiert hier?
makeCounter()erstellt eine lokale Variablecount- Die zurückgegebene anonyme Funktion “umschließt”
count(ein Closure) - Jeder Aufruf von
counter()greift auf dieselbecountVariable zu count“überlebt” den Return vonmakeCounter()
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 countwird daher auf dem Heap statt dem Stack alloziert- Die Closure speichert eine Referenz auf
count - Solange die Closure existiert, bleibt
countim 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.
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())
}1
2
1
0Closure 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.
package main
import "fmt"
func example() {
x := 10
defer func() {
fmt.Println("x ist:", x)
}()
x = 20
}
func main() {
example()
}x ist: 20Im Gegensatz zu dieser Verwendung, bei welcher die Argumente sofort evaluiert.
package main
import "fmt"
func example() {
x := 10
defer fmt.Println("x ist:", x)
x = 20
}
func main() {
example()
}x ist: 10Defer - 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.
package main
import "fmt"
func example() {
defer fmt.Println("Go")
fmt.Println("Hello")
}
func main() {
example()
}Hello
GoWann wird defer ausgeführt? Deferred Funktionen werden ausgeführt:
- Nach dem
returnStatement (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.
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 nachos.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.
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()
}Function body
3
2
1Hier noch ein Beispiel mit mehr Bezug zu Realität.
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:
conn.Close()(zuletzt geöffnet, zuerst geschlossen)file.Close()db.Close()(zuerst geöffnet, zuletzt geschlossen)
Defer kann Rückgabewerte modifizieren
Mit Named Returns kann defer Rückgabewerte modifizieren.
package main
import "fmt"
func increment() (result int) {
defer func(
result++
) {}()
return 5
}
func main() {
fmt.Println(increment())
}6Defer 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.
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.
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.
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.
package main
import "fmt"
func example() {
defer fmt.Println("This always runs")
panic("Some panic here")
fmt.Println("This never runs")
}This always runs
panic: Some panic here
... Rest der Panic-Meldung