2.3 Kontrollstrukturen und Funktionen
In diesem Kapite werfen wir einen Blick auf Kontrollstrukturen und Funktionen in Go.
Kontrollstrukturen
Die beste Erfindung in der Programmierung ist die Möglichkeit, den Ablauf einen Programms dynamisch verändern zu können. Um dies zu ermöglichen, kannst Du Kontrollstrukturen verwenden, um komplexe, logische Verknüpfungen darzustellen. Es gibt drei Arten, den Programmablauf zu ändern: Bedingungen, Schleifen, die eine Aufgabe mehrmals durchführen können und Sprungmarken.
if
if
ist das am weitverbreiteste Schlüsselwort in Deinen Programmen. Sollte eine Bedingung erfüllt sein, wird das Programm eine bestimmte Aufgabe ausführen. Wird die Bedingung nicht erfüllt, dann passiert etwas Anderes.
In Go braucht man keine runden Klammern für eine if
-Bedingung nutzen.
if x > 10 {
fmt.Println("x ist größer als 10")
} else {
fmt.Println("x ist kleiner gleich 10")
}
Der nützlichste Aspekt in Bezug auf if
ist, dass man vor der Überprüfung der Bedingung noch Startwerte vergeben kann. Die Variablen mit den Startwerten sind aber nur im if
-Block selbst abrufbar.
// Gebe x einen Startwert und überprüfe dann, ob x größer als 10 ist
if x := berechneEinenWert(); x > 10 {
fmt.Println("x ist größer als 10")
} else {
fmt.Println("x ist kleiner als 10")
}
// Der folgende Code kann nicht kompiliert werden
fmt.Println(x)
Benutze if-else für mehrere Bedingungen.
if integer == 3 {
fmt.Println("Der Integer ist gleich 3")
} else if integer < 3 {
fmt.Println("Der Integer ist kleiner als 3")
} else {
fmt.Println("Der Integer ist größer als 3")
}
goto
Go umfasst auch das goto
Schlüsselwort, aber benutze es weise. goto
verändert den Ablauf des Programms, indem es an einer vorher definierten Sprungmarke im selben Codeabschnitt weiterläuft.
func meineFunktion() {
i := 0
Hier: // Sprungmarken enden mit einem ":"
fmt.Println(i)
i++
goto Hier // Springe nun zur Sprungmarke "Hier"
}
Es wird zwischen der Groß- und Kleinschreibung von Sprungmarken unterschieden. Hier
ist nicht das Selbe wie hier
.
for
for
ist die mächtigste Kontrollstruktur in Go. Sie kann Daten lesen und sie in jedem Durchgang verändern, ähnlich wie while
.
for Ausdruck1; Ausdruck2; Ausdruck3 {
//...
}
Ausdruck1
, Ausdruck2
und Ausdruck3
sind alle Ausdrücke, aber bei Ausdruck1
und Ausdruck3
handelt es sich aber entweder um eine Deklaration von Variablen oder um Rückgabewerte von Funktionen. Ausdruck2
ist dagegen eine zu erfüllende Bedingung . Ausdruck1
wird nur einmal vor der Ausführung der Schleife verändert, aber Ausdruck3
kann mit jedem Durchlauf verändert werden.
Aber Beispiele sagen mehr als tausend Worte.
package main
import "fmt"
func main(){
summe := 0;
for index:=0; index < 10 ; index++ {
summe += index
}
fmt.Println("summe hat den Wert ", summe)
}
// Ausgabe: summe hat den Wert 45
Manchmal müssen wir am Anfang mehrere Deklarationen vornehmen. Da Go für die Trennung der Variablen über kein ,
verfügt, können die Variablen nicht einzeln deklariert werden. Ein Ausweg bietet die parallele Deklaration: i, j = i + 1, j - 1
.
Wenn nötig, können wir Ausdruck1
und Ausdruck3
auch einfach auslassen.
summe := 1
for ; summe < 1000; {
summe += summe
}
Auch das ;
kann ausgelassen werden. Kommt es Dir bekannt vor? Denn ist das gleiche Verhalten wie bei while
.
summe := 1
for summe < 1000 {
summe += summe
}
Es gibt mit break
und continue
zwei wichtige Schlüsselwörter bei der Verwendung von Schleifen. break
wird zum "Ausbrechen" aus einer Schleife genutzt, während continue
zum nächsten Durchlauf springt. Sollten verschachtelte Schleifen genutzt werden, dann kombiniere break
mit einer Sprungmarke.
for index := 10; index>0; index-- {
if index == 5{
break // oder continue
}
fmt.Println(index)
}
// break gibt 10、9、8、7、6 aus
// continue gibt 10、9、8、7、6、4、3、2、1 aus
for
kann auch die Elemente aus einem slice
oder einer map
lesen, wenn dies im Zusammenspiel mit range
geschieht.
for k, v := range map {
fmt.Println("map's Schlüssel:",k)
fmt.Println("map's Wert:",v)
}
Go unterstützt die Rückgabe mehrerer Werte und gibt eine Fehlermeldung aus, wenn nicht genauso viele Variablen zum Speichern vorhanden sind. Solltest Du einen Wert nicht brauchen, kannst Du ihn mit _
verwerfen.
for _, v := range map{
fmt.Println("map's Wert:", v)
}
switch
Manchmal findest Du Dich in einer Situation vor, indem viele Bedingungen mit if-else
geprüft werden müssen. Der Code kann dann schnell unleserlich und schwer anpassbar werden. Alternativ kannst Du Bedingungen auch mit switch
prüfen.
switch Audruck {
case Fall1:
Eine Aufgabe
case Fall3:
Eine andere Aufgabe
case Fall3:
Eine andere Aufgabe
default:
Anderer Code
}
Der Datentyp von Fall1
, Fall2
und Fall3
muss der selbe sein. switch
kann sehr flexible eingesetzt werden. Bedingungen müssen keine Konstanten sein und die einzelnen Fälle werden von oben nach unten geprüft, bis eine passender gefunden wurde. Sollte switch
kein Ausdruck folgen, dann dann wird von einem bool
ausgegangen und es wird der Code im Fall true
ausgeführt.
i := 10
switch i {
case 1:
fmt.Println("i ist gleich 1")
case 2, 3, 4:
fmt.Println("i ist gleich 2, 3 oder 4")
case 10:
fmt.Println("i ist gleich 10")
default:
fmt.Println("ich weiß nur, dass i ein Integer ist")
}
In der fünften Zeile nutzen wir gleich mehrere Fälle auf einmal. Außerdem brauchen wir nicht break
am Ende jedes Falls einfügen, wie es in anderen Programmiersprachen üblich ist. Mit dem Schlüsselwort fallthrough
kannst Du nach einem Treffer auch alle folgenden Fälle ausführen.
integer := 6
switch integer {
case 4:
fmt.Println("integer <= 4")
fallthrough
case 5:
fmt.Println("integer <= 5")
fallthrough
case 6:
fmt.Println("integer <= 6")
fallthrough
case 7:
fmt.Println("integer <= 7")
fallthrough
case 8:
fmt.Println("integer <= 8")
fallthrough
default:
fmt.Println("Standardfall")
}
Das Programm gibt folgende Informationen aus.
integer <= 6
integer <= 7
integer <= 8
Standardfall
Funktionen
Nutze das Schlüsselwort func
, um eine Funktion einzuleiten.
func funcName(eingabe1 typ1, eingabe2 typ2) (ausgabe1 typ1, ausgabe2 typ2) {
// Körper bzw. Rumpf der Funktion
// Mehrere Werte werden zurückgegeben
return wert1, wert2
}
Dem gezeigten Beispiel können wir somit folgende Informationen entnehmen:
- Nutze das Schlüsselwort
func
, um die FunktionfuncName
zu erstellen. - Funktionen können ein, mehrere oder kein Argument übergeben werden. Der Datentyp wird nach dem Argument angegeben und Argumente werden durch ein
,
getrennt. - Funktionen können mehrere Werte zurückgeben.
- Es gibt zwei Rückgabewerte mit den Namen
ausgabe1
andausgabe2
. Du kannst Ihren Namen auch einfach auslassen und nur den Datentyp im Kopf angeben. - Wenn es nur einen Rückgabewert gibt, kannst Du die Klammer um den Datentyp im Kopf weglassen.
- Wenn keine Daten zurückgegeben werden, kannst Du
return
einfach weglassen. - Um Werte im Körper der Funktion zurückzugeben, musst Du
return
verwenden.
Gucken wir uns mal ein Beispiel an, in dem wir den Größeren von zwei Werten bestimmen.
package main
import "fmt"
// Gib den größeren Wert von a und b zurück
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
x := 3
y := 4
z := 5
max_xy := max(x, y) // ruft die Funktion max(x, y) auf
max_xz := max(x, z) // ruft die Funktion max(x, z) auf
fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy)
fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz)
fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // rufe die Funktion hier auf
}
Im oberen Beispiel übergeben wir jeweils zwei Argumente vom Typ int
an die Funktion max
. Da beide Argumente denn gleichen Datentyp besitzen, reicht es, wenn wir diesen nur einmal angeben. So kannst Du statt a int, b int
einfach a, b int
schreiben. Dies gilt auch für weitere Argumente. Wie Du sehen kannst, wird nur ein Wert von max
zurückgegeben, sodass wir den Namen der Variable weglassen können. Dies wäre einfach die Kurzschreibweise.
Mehrere Rückgabewerte bei Funktionen
Die Möglichkeit, mehrere Werte zurückgeben zu können, ist ein Aspekt, in der Go der Programmiersprache C überlegen ist.
Schauen wir uns das folgende Beispiel an.
package main
import "fmt"
// Gebe die Ergebnisse von A + B und A * B zurück
func SummeUndProdukt(A, B int) (int, int) {
return A+B, A*B
}
func main() {
x := 3
y := 4
xPLUSy, xMALy := SummeUndProdukt(x, y)
fmt.Printf("%d + %d = %d\n", x, y, xPLUSy)
fmt.Printf("%d * %d = %d\n", x, y, xMALy)
}
Das obere Beispiele gibt zwei namenlose Werte zurück. Aber Du kannst sie natürlich auch bennenen, wenn Du magst. Würden wir dies tun, müssten wir mit return
nur dessen Namen zurückgeben, da die Variablen in der Funktion bereits automatisch initialisiert wurden. Bedenke, dass Funktionen mit einen großen Buchstaben anfangen müssen, wenn Du sie außerhalb eines Pakets verwenden möchtest. Es wäre auch zu empfehlen, die Rückgabewerte zu bennenen, da dies den Code verständlicher macht.
func SummeUndProdukt(A, B int) (addiert int, multipliziert int) {
addiert = A+B
multipliziert = A*B
return
}
Variablen als Argumente
Go unterstützt auch Variablen als Argumente, was bedeutet, das einer Funktionen auch eine unbekannte Anzahl an Argumenten übergeben werden kann.
func myfunc(arg ...int) {}
arg …int
besagt, dass diese Funktionen beliebig viele Argumente entgegennimmt. Beachte, dass alle Argumente vom Typ int
sind. Im Funktionskörper wird arg
zu einem slice
vom Typ int
.
for _, n := range arg {
fmt.Printf("Und die Nummer lautet: %d\n", n)
}
Werte und Zeiger übergeben
Wenn wir ein Argument einer aufgerufenen Funktion übergeben, dann bekommt diese meist eine Kopie des Arguments und kann somit das Original nicht beeinflussen.
Lass mich Dir ein Beispiel zeigen, um es zu beweisen.
package main
import "fmt"
// Simple Funktion, die a um 1 erhöht
func add1(a int) int {
a = a+1 // Wir verändern den Wert von a
return a // Wir geben den Wert von a zurück
}
func main() {
x := 3
fmt.Println("x = ", x) // Sollte "x = 3" ausgeben
x1 := add1(x) // Rufe add1(x) auf
fmt.Println("x+1 = ", x1) // Sollte "x+1 = 4" ausgeben
fmt.Println("x = ", x) // Sollte "x = 3" ausgeben
}
Siehst Du es? Selbst wenn wir der Funktion add1
die Variable x
als Argument übergeben, um sie um 1 zu erhöhen, so blieb x
selbst unverändert
Der Grund dafür ist naheliegend: wenn wir add1
aufrufen, übergeben wir ihr eine Kopie von x
, und nicht x
selbst.
Nun fragst Du dich bestimmt, wie ich das echte x
einer Funktion übergeben kann.
Dafür müssen wir Zeiger verwenden. Wir wissen, das Variablen im Arbeitspeicher hinterlegt sind und sie eine Adresse für den Speicherort besitzen. Um einen Wert zu ändern, müssen wir auch die Adresse des Speicherorts wissen. Daher muss der Funktion add1
diese Adresse von x
bekannt sein, um diese ändern zu können. Hier übergeben wir &x
der Funktion als Argument und ändern damit den Datentyp des Arguments in *int
. Beachte, dass wir eine Kopie des Zeigers übergeben, und nicht des Wertes.
package main
import "fmt"
// Simple Funktion, um a um 1 zu erhöhen
func add1(a *int) int {
*a = *a+1 // Wir verändern den Wert von a
return *a // Wir geben den Wert von a zurück
}
func main() {
x := 3
fmt.Println("x = ", x) // Sollte "x = 3" ausgeben
x1 := add1(&x) // Rufe add1(&x) auf und übergebe die Adresse des Speicherortes von x
fmt.Println("x+1 = ", x1) // Sollte "x+1 = 4" ausgeben
fmt.Println("x = ", x) // Sollte "x = 4" ausgeben
}
Nun können wir den Wert von x
in der Funktion ändern. Aber warum nutzen wir Zeiger? Was sind die Vorteile?
- Es erlaubt uns, mit mehreren Funktionen eine Variable direkt zu benutzen und zu verändern.
- Es braucht wenig Ressourcen, um die Speicheradresse (8 Bytes) zu übergeben. Kopien sind weniger effizient im Kontext von Zeit und Speichergröße.
string
,slice
undmap
sind Referenztypen, die automatisch die Referenz (bzw. den Zeiger) der Funktion übergeben. (Achtung: Wenn Du die Länge einesslice
ändern möchtest, musst Du den Zeiger explizit übergeben).
defer
Go besitzt mit defer
ein weiteres nützliches Schlüsselwort. Du kannst defer
mehrmals in einer Funktion nutzen. Sie werden in umgekehrter Reihenfolge am Ende einer Funktion ausgeführt. Im Fall, dass Dein Programm eine Datei öffnet, muss diese erst wieder geschlossen werden, bevor Fehler zurückgeben werden können. Schauen wir uns ein paar Beispiele an.
func LesenSchreiben() bool {
file.Open("Datei")
// Mache etwas
if FehlerX {
file.Close()
return false
}
if FehlerY {
file.Close()
return false
}
file.Close()
return true
}
Wir haben gesehen, dass hier der selbe Code öfters wiederhohlt wird. defer
löst dieses Problem ziemlich elegant. Es hilft Dir nicht nur sauberen Code zu schreiben, sodern fördert auch noch dessen Lesbarkeit.
func LesenSchreiben() bool {
file.Open("Datei")
defer file.Close()
if FehlerX {
return false
}
if FehlerY {
return false
}
return true
}
Wird defer
mehr als einmal genutzt, werden sie in umgekehrter Reihenfolge ausgeführt. Das folgende Beispiel wird 4 3 2 1 0
ausgeben.
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
Funktionen als Werte und Datentypen
Funktionen sind auch Variablen in Go, die wir mit type
definieren können. Funktionen mit der selben Signatur, also dem gleichen Aufbau, können als der selbe Datentyp betrachtet werden.
type schreibeName func(eingabe1 eingabeTyp1 , eingabe2 eingabeTyp2 [, ...]) (ergebnis1 ergebnisTyp1 [, ...])
Was ist der Vorteil dieser Fähigkeit? Ganz einfach: wir können somit Funktionen als Werte übergeben.
package main
import "fmt"
type testeInt func(int) bool // Definiere eine Funktion als Datentyp
func istUngerade(integer int) bool {
if integer%2 == 0 {
return false
}
return true
}
func istGerade(integer int) bool {
if integer%2 == 0 {
return true
}
return false
}
// Übergebe die Funktion f als Argument an eine Andere Funktion
func filter(slice []int, f testeInt) []int {
var ergebnis []int
for _, wert := range slice {
if f(wert) {
ergebnis = append(ergebnis, wert)
}
}
return ergebnis
}
func main(){
slice := []int {1, 2, 3, 4, 5, 7}
fmt.Println("slice = ", slice)
ungerade := filter(slice, istUngerade) // Nutze die Funktion als Wert
fmt.Println("Die ungeraden Elemente von slice lauten: ", ungerade)
gerade := filter(slice, istGerade)
fmt.Println("Die geraden Elemente von slice lauten: ", gerade)
}
Dies ist sehr nützlich, wenn wir Interfaces nutzen. Wie Du sehen kannst, wird testeInt
eine Funktion als Argument übergeben, und die Rückgabewerte besitzen genau den gleichen Datentyp. Somit können wir komplexe Zusammenhänge in unseren Programen flexible warten und erweitern.
Panic und Recover
Go besitzt keine try-catch
-Struktur, wie es in Java der Fall ist. Statt eine Exception zu erstellen, nutzt Go panic
und recover
. Auch wenn panic
sehr mächtig ist, solltest Du es nicht all zu oft benutzen.
panic
ist eine eingebaute Funktion, um den normalen Ablauf des Programms zu unterbrechen und es in eine Art Panikmodus zu versetzen. Wenn die Funktion F
panic
aufruft, wird in ihr außer defer
nichts weiter ausgeführt. Danach geht das Programm zum Ursprungsort des Fehlers zurück. Dies wird nicht geschlossen, ehe alle Funktionen einer goroutine
panic
zurückgeben. Einige Fehler erzeugen auch selbst einen Aufruf von panic
, z.B. wenn der reservierte Speicherplatz eines Arrays nicht ausreicht.
recover
ist ebenfalls eine eingebaute Fuktion, um goroutinen
vom Panikmodus zu erlösen. Das Aufrufen von recover
im Zusammenspiel mit defer
ist sehr nützlich, da normale Funktionen nicht mehr ausgeführt werden, wenn sich das Programm erst einmal im Panikmodus befindet. Die Werte von panic
werden so aufgefangen, wenn sich das Programm im Panikmodus befindet. Ansonsten wird nil
zurückgegeben.
Das folgende Beispiel soll die Nutzung von panic
verdeutlichen.
var benutzer = os.Getenv("BENUTZER")
func init() {
if benutzer == "" {
panic("Kein Wert für $BENUTZER gefunden")
}
}
Das nächste Beispiel zeigt, wie der Status von panic
aufgerufen werden kann.
func erzeugePanic(f func()) (b bool) {
defer func() {
if x := recover(); x != nil {
b = true
}
}()
f() // Wenn 'f' 'panic' verursacht, wird versucht, dies mit 'recover' zu beheben
return
}
Die main
und init
Funktion
Go hat zwei besondere Funktionen mit dem Namen main
und init
, wobei init
in jedem Paket aufgerufen werden kann und dies bei main
nur im gleichnahmigen Paket möglich ist. Beide Funktionen besitzen weder Argumente, die übergeben werden können, noch geben sie Werte zurück. Es ist möglich, die init
-Funktion innerhalb eines Pakets mehrmals aufzurufen, aber ich empfehle jedoch die Nutzung einer einzelnen, da es sonst unübersichtlich werden kann.
Go ruft init()
und main()
automatisch auf, sodass dies nicht manuell geschehen muss. Für jedes Paket ist die init
-Funktion optional, jedoch ist die main
-Funktion unter package main
obligatorisch und kann lediglich ein einziges Mal genutzt werden.
Programme werden vom main
-Paket aus gestarten. Wenn dieses Paket noch weitere Pakete importiert, geschieht dies einzig während der Kompilierung. Nach dem Importieren von Paketen werden zu erst dessen Konstanten und Variablen initialisiert, dann wird init
aufgerufen usw. Nachdem alle Pakete erfolgreich initialisiert wurden, geschieht all dies im main
-Paket. Die folgende Grafik illustriert diesen Ablauf.
Abbildung 2.6 Der Initialisierungsablauf eines Programms in Go
import
Oftmals verwenden wir import
in unseren Go-Programmen wie folgt.
import(
"fmt"
)
Dann nutzen wir die Funktionen aus dem Paket wie im Beispiel:
fmt.Println("Hallo Welt")
fmt
stammt aus Gos Standardbibliothek, welche sich unter $GOROOT/pkg befindet. Go unterstützt Pakete von Dritten auf zwei Wege.
- Relativer Pfad
import
./model
// Importiert ein Paket aus dem selben Verzeichnis, jedoch empfehle ich diese Methode nicht. - Absoluter Pfad
import
shorturl/model
// Lade das Paket mit dem Pfad "$GOPATH/pkg/shorturl/model"
Es gibt spezielle Operatoren beim Importieren von Paketen, die Anfänger oftmals verwirren.
Punkt Operator.
Manchmal sieht man, wie Pakete auf diesem Weg importiert werden.
import( . "fmt" )
Der Punkt Operator bedeutet einfach, dass der Name des Paketes beim Aufruf von dessen Funktion weggelassen werden kann. So wird aus
fmt.Printf("Hallo Welt")
einfachPrintf("Hallo Welt")
.Alias Operation.
Dieser verändert den Namen des Paketes, denn wir beim Aufruf von Funktionen angeben müssen.
import( f "fmt" )
Aus
fmt.Printf("Hallo Welt")
wird dannf.Printf("Hallo Welt")
._
Operator.Dieser Operator ist ohne Erklärung ein wenig schwer zu erklären.
import ( "database/sql" _ "github.com/ziutek/mymysql/godrv" )
Der
_
Operator bedeutet einfach nur, das wir das Paket importieren möchten und das desseninit
-Funktion ausgeführt werden soll. Jedoch sind wir uns nicht sicher, ob wir die anderen Funktionen dieses Pakets überhaupt nutzen wollen.
Links
- Inhaltsverzeichnis
- Vorheriger Abschnitt: Grundlagen von Go
- Nächster Abschnitt: Struct