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