Skip to content

Objektorientierung

Harald Weidner edited this page Feb 20, 2020 · 1 revision

Objektorientierung in Go

Go ist eine imperative und modulare Programmiersprache mit funktionalen und objektorientierten Elementen. Obwohl die Entwickler Wert auf die Feststellung legen, dass es keine Klassen und keine Vererbung gibt, lässt sich in Go sehr gut objektorientiert programmieren. Viele Funktionen in der Standardbibliothek und in Modulen von Drittanbietern bieten eine objektorientierte Schnittstelle.

Methoden

Dass es in Go keine Klassen gibt, bedeutet in erster Linie, dass sich objektorientierte Eigenschaften mit jedem selbstdefinierten Datentyp gleichermaßen verwenden lassen.

package main

import "fmt"

type longint int64

func (v longint) String() string {
    return fmt.Sprintf("--- %d ---", int64(v))
}

func main() {
    var a longint = 15
    fmt.Println(a.String())    // ergibt "--- 15 ---"
}

In obigem Beispiel wird ein einfacher Datentyp longint mit einer Methode String definiert. Methoden unterscheiden sich von normalen Funktionen durch die Angabe des Datentyps (hier: (v longint)) zwischen dem Keyword func und dem Methodennamen.

In Go gibt es Verbundtypen, vergleichbar zu Struct in C/C++ oder Records in Pascal. Die Kombination aus einem Verbundtyp und Methoden entsprecht in etwa den Klassen in C++ oder Java.

Kapselung

Verglichen mit den Klassen in C++ fehlt dem Verbund in Go die Kapselung (private, protected und public). Die Kapselung erfolgt in Go nicht für einzelne Typen, sondern auf der Ebene der Module.

Innerhalb des selben Moduls (package) kann jeder Code auf die Elemente des struct zugreifen. Für die Zugriffsregelung über Modulgrenzen hinweg gilt die Regel, dass nur Symbole exportiert werden, deren Namen mit einem Großbuchstaben beginnt.

type IPv4 struct {
    Addr uint32
    cidr uint8
}

Hier wird ein Typ definiert, der eine IPv4-Nummer und die dazugehörige Netzmaske in Form der CIDR-Notation speichern kann. Auf die IP-Nummer kann, auch außerhalb des Moduls, direkt zugegriffen werden, da Addr mit einem Großbuchstaben beginnt. Die Netzmaske ist dagegen ein nichtexportiertes Feld und kann nur mit entsprechenden Getter/Setter Methoden gelesen oder geändert werden.

Die Überlegung hinter dieser Typdefinition liegt darin, dass nicht alle möglichen Werte, die in einem uint8 gespeichert sein können, gültige Netzmasken sind. Netzmasken in CIDR Notation können nur Werte von 0 bis 32 annehmen. Durch die Restriktion kann der Wert von außen nicht geändert und damit ein ungültiges Adressobjekt erzeugt werden. Dagegen ist jede 32-bit Integerzahl eine gültige IPv4-Adresse.

Interfaces und Polymorphie

Go unterstützt Interfaces. Interfaces werden in Go durch eine Liste von Methoden definiert, die ein Verbundtyp unterstützen muss. Ob ein Typ das Interface tatsächlich implementiert, wird nicht explizit im Sourcecode festgeschrieben, sondern ergibt sich aus den vorhandenen Methoden.

Eines der bekannten Interfaces der Standardbibliothek ist Stringer aus dem Package fmt:

type Stringer interface {
	String() string
}

Jeder selbstdefinierte Typ, der eine Methode String() mit der angegebenen Signatur hat, erfüllt dieses Interface.

In der Go-Standardbibliothek wird das Stringer-Interface an vielen Stellen verwendet. Beispielsweise benutzen es die Ausgabe-Funktionen fmt.Println, fmt.Fprintln() etc., wenn eine Variable dieses Typs ausgegeben werden soll. In dem obigen Beispiel sind die folgenden Ausgaben äquivalent:

fmt.Println(a.String())
fmt.Println(a)

Denn die Ausgabefunktionen prüfen, ob der auszugebende Typ das Interface implementiert, und wenn ja, verwenden die String-Methode zur Erzeugung der Ausgabe.

Mit Hilfe von Interfaces kann Polymorphie realisiert werden. Ein Interface kann wie jeder andere selbstdefinierte Typ verwendet werden; ihm können Variablen zugewiesen werden, deren Typ das Interface implementiert.

var s fmt.Stringer = longint(15)
fmt.Println(s)

Vererbung und Mehrfachvererbung

In Go gibt es das Sprachmittel der Vererbung nicht. Allerdings können Verbundtypen aus anderen Verbundtypen zusammengesetzt werden. Dank eine syntaktischen Besonderheit des Derefenzierungsoperators unterscheidet sich die Benutzung eines zusammengesetzten Verbundtyps nicht wesentlich von einer vererbten Klasse in C++.

In dem folgenden Beispiel wird ein Verbund (Klasse) A definiert. Ein Verbund B bindet A ein. Die Einbindung geschieht namenlos, d.h. im Verbund B wird der Typ A angegeben, ohne ein Attribut namentlich zu definieren. Beim Zugriff auf die einzelnen Attribute kann eine Kurzschreibweise verwendet werden, so dass die Attribute von A “flach” in B eingebettet erscheinen.

package main

import "fmt"

type A struct {
    a1 int
    a2 int
}

type B struct {
    A         // namenlose Einbindung des Verbunds A
    b1 int
    b2 int
}

func main() {
    var b B

    b.a1 = 1   // entspricht b.A.a1
    b.a2 = 2   // entspricht b.A.a2
    b.b1 = 3
    b.b2 = 4

    fmt.Println(b)
}

Die selbe Kurzschreibweise kann auch für Methoden verwendet werden.

Auch Mehrfachvererbung ist möglich, indem mehrere Verbundtypen eingebettet werden. Wenn in den verschiedenen Verbünden allerdings gleichnamige Attribute verwendet werden, kann die Kurzschreibweise wegen Mehrdeutigkeit nicht verwendet werden.

package main

import "fmt"

type A struct {
    a int
    x int
}

type B struct {
    b int
    x int
}

type C struct {
    A
    B
}

func main() {
    var c C

    c.a = 1     // entspricht c.A.a
    c.b = 2     // entspricht c.B.b

    c.x = 3     // ERROR: ambiguous selector c.x
    c.A.x = 4   // keine Kurzschreibweise möglich
    c.B.x = 5   // keine Kurzschreibweise möglich

    fmt.Println(c)
}