Skip to content

Latest commit

 

History

History
523 lines (373 loc) · 20.9 KB

File metadata and controls

523 lines (373 loc) · 20.9 KB

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 Funktion funcName 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 and ausgabe2. 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 und map sind Referenztypen, die automatisch die Referenz (bzw. den Zeiger) der Funktion übergeben. (Achtung: Wenn Du die Länge eines slice ä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 dem 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.

  1. Relativer Pfad import ./model // Importiert ein Paket aus dem selben Verzeichnis, jedoch empfehle ich diese Methode nicht.
  2. 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.

  1. 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") einfach Printf("Hallo Welt").

  2. 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 dann f.Printf("Hallo Welt").

  3. _ 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 dessen init-Funktion ausgeführt werden soll. Jedoch sind wir uns nicht sicher, ob wir die anderen Funktionen dieses Pakets überhaupt nutzen wollen.

Links