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())
}

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, von auf der Ebene der Module.

Innerhalb des selben Moduls (package) kann jeder Code auf die Elemelte 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.

Mit Hilfe von Interfaces kann Polymorphie realisiert werden. Ein Interfacetyp kann zur Definition von Variablen verwendet werden; diesen können dann Objekte aller Typen zugewiesen werden, die dieses Interface implementieren.

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)
}