Stand: 2012-11-30

Strukturen in Go

Zusammengesetzte Datentypen in Go definieren und nutzen.

Inhalt

Deklaration

Zusammengesetzte Datentypen werden in Go mit dem Schlüsselwort struct deklariert:

type Point struct {
	X int
	Y int
}

Diese Anweisung definiert den Datentyp Point als struct mit zwei Integer-Feldern namens X und Y.

Felder gleichen Typs können durch Komma getrennt auch in eine Zeile geschrieben werden:

type Point struct {
	X, Y int
}

Felder, die mit einem Großbuchstaben beginnen sind innerhalb und außerhalb des Pakets sichtbar, wohingegen Felder, die mit Kleinbuchstaben beginnen, nur innerhalb ihres Pakets sichtbar sind.

Jedes Feld kann optional einen String als sogenanntes "Tag" tragen, das per Reflection ausgewertet werden kann. Man könnte damit zum Beispiel den Spaltennamen in einer Datenbanktabelle angeben. Ansonsten haben Tags keine Auswirkung auf die Programmausführung.

type Point struct {
	X int "column_x"
	Y int "column_y"
}

Initialisierung

Eine neue Instanz wird mit der Funktion new erzeugt:

var point *Point = new(Point)

new erzeugt eine Instanz und initialisiert dessen Speicherbereich bitweise mit 0. Ein in der Struktur enthaltener Integer hat nach der Erzeugung also zum Beispiel den Wert 0 und ein bool den Wert 'false'. Idealerweise sind die Bedeutungen der Felder so definiert, dass die 0-Werte direkt nutzbar sind. Schließlich gibt new einen Zeiger auf die erzeugte Instanz zurück.

Erzeugung der Instanz und Initialisierung der Felder kann auch in einem Schritt erfolgen, wenn in geschweiften Klammern hinter dem Typnamen die Werte der Felder geschrieben werden:

var point *Point = &Point{82, 14}

Dabei müssen nicht alle Felder belegt werden, wenn die Namen angegeben werden. Im folgenden Beispiel wird nur y gesetzt und x bleibt beim Standardwert 0:

&Point{y : 14}

Durch die Angabe der Feldnamen wird außerdem das Problem vermieden, dass bei einer Änderung der Struktur alle Stellen, wo sie initialisiert wird, angepasst werden müssen. Denn die Reihenfolge und Anzahl der Felder spielt durch die Benennung der Feldnamen keine Rolle mehr. Mit dieser Form der Initialisierung können auch optionale Funktionsparameter realisiert werden.

Wenn eine Struktur Felder enthält, die Initialisiert werden müssen, bietet sich eine Initialisierungsfunktion an, die auch "Konstruktor" genannt wird:

type Something struct {
	settings map[string]string
	tags []string
}

func NewSomething() *Something {
	settings := make(map[string]string)
	tags := make([]string)
	return &Something{settings, tags}
}

Die Funktion wird nach der Konvention "New" + "Name des zu erzeugenden Typs" benannt, in dem Beispiel also NewSomething. Wenn ein Paket nur einen einzigen öffentlichen Typ hat und man sicher ist, dass es dabei bleibt, kann die Funktion auch schlicht New heißen. Zusammen mit dem Paketnamen ist es trotzdem gut lesbar, wie im Fall des ring-Pakets : ring.New()

Ob ein Zeiger auf die erzeugte Instanz oder die Instanz selbst zurückgegeben wird, hängt im Wesentlichen davon ab, wie groß die Struktur ist. Ohne Zeiger wird bei jeder Übergabe eine Kopie erzeugt, was bei großen Strukturen ineffizient sein kann. Mit Zeiger kann andererseits die Instanz von mehreren Stellen aus geändert werden kann, was möglicherweise unerwünscht ist.

Deklaration und Initialisierung in einem

Strukturen können auch auf einen Schlag deklariert und initialisiert werden:

testCase := struct{ input, output int }{1, 3}
            |-------------------------||----|
                     Deklaration        Initialisierung

Die Struktur selbst ist dabei namenlos und hat zwei Integer-Felder mit den Namen input und output. Durch die Initialisierung, {1, 3}, wird eine neue Instanz erzeugt und mit den Werten 1 und 3 belegt.

Dieses Verfahren eignet sich hervorragend, um eine Reihe von Testdaten anzulegen, die dann in einer Schleife durchlaufen werden.:

testCases := []struct {
	input, output int
}{ // hier beginnt Slice-Initialisierung
	{0, 0}, // Initialisierung des ersten Slice-Elements
	{1, 1},
	{2, 4},
	{3, 8}, // Letztes Komma nicht vergessen!
}

Der Deklarationsteil besteht aus der Angabe eines Slices vom Typ struct, das wiederum aus zwei Feldern (input und output) besteht. Die Initialisierung besteht analog ebenfalls aus zwei Ebenen: auf der ersten Ebene wird das Slice initialisiert und auf der zweiten Ebene, werden die enthaltenen Elemente initialisiert. Zu Beachten ist, dass auch hinter der Initialisierung des letzten Slice-Elements ein Komma stehen muss.

Verschachtelung von Strukturen

Strukturen können in andere Strukturen eingebettet und auf diese Weise wiederverwendet werden:

type Circle struct {
	center Point
	radius int
}

Eine Besonderheit ist die anonyme Einbettung von Strukturen, wodurch die Felder einer eingebetteten Struktur direkt in der äußeren Struktur erscheinen. Damit bietet Go ein einfaches und mächtiges Mittel zur Komposition, das ähnlich der Ableitung von Klassen in anderen Programmiersprachen ist. Im folgenden Beispiel bettet die Struktur Pixel die Struktur Point ein:

type Pixel struct {
	Point // anonymes Feld, impliziter Name entspricht dem Typ, hier "Point"
	Color int
}

Auf die Felder X und Y des eingebetten Point-structs kann dann so zugegriffen werden, als wären sie direkt in Pixel enthalten:

pixel := new(Pixel)
pixel.X = 39
pixel.Y = 18
pixel.Color = 778877

Zu Beachten ist, dass innerhalb einer Struktur von jedem Typ nur ein anonymes Feld vorkommen darf, da sonst keine eindeutige Zuordnung der Felder mehr möglich wäre. Würde man zum Beispiel zwei anonyme Point-Strukturen einbetten, wäre unklar, ob mit X nun das Feld des ersten oder zweiten Point gemeint ist.

Das anonyme Feld trägt intern den Namen der eingebetten Struktur. Diese Tatsache kann bei der Initialisierung genutzt werden:

func NewPixelWithPoint(point Point) Pixel {
	pixel := *new(Pixel)
	pixel.Point = point // 'Point' ist der implizite Feldname
	return pixel
}

func NewPixel() Pixel {
	return Pixel{Point: Point{X:9}}
}

In einem Initialisierungsblock können die Feldnamen eingebetteter Strukturen nicht direkt angegeben werden. Stattdessen ist ein untergeordneter Initialisierungsblock für die eingebettete Struktur erforderlich. Das Feld X der eingetteten Point-Instanz kann also nur wie im obigen Beispiel und nicht direkt mit Pixel{X:9} initialisiert werden.

Rekursive Strukturen durch Einbettung derselben Struktur sind nicht möglich, weil das zu endloser Rekursion führen würde. Aber es kann ein Zeiger auf dieselbe Struktur verwendet werden, womit zum Beispiel eine verkettete Liste erstellt werden kann:

type ListElement struct {
	payload string
	next *ListElement
}

Methoden auf Strukturen

Ähnlich den Klassen in anderen Sprachen können in Go Methoden auf Strukturen definiert werden, wodurch die Probleme der Vererbung vermieden werden sollen; Komposition und Delegation statt Vererbung ist die Devise. Eine Methode ist im Prinzip eine ganze normale Funktion, die jedoch eine zusätzliche Parameterliste für den sogenannten "Empfänger" hat:

func (point *Point) ShiftX(distance int) {
	point.X += distance
}

Ausführbares Beispiel

(point *Point) ist der Empfänger, also die Struktur, auf der die Methode definiert wird. Wichtig ist, dass es sich um einen Zeiger auf die Struktur handelt, falls die Werte geändert werden sollen. Innerhalb der Methode kann auf den Empfänger (point) wie auf jede andere Variable zugegriffen werden.

Bei der Einbettung anonymer Strukturen stehen neben deren Feldern auch deren Methoden direkt auf der einbettenden Struktur zur Verfügung. Die oben auf Point definierte Methode ShiftX kann also auch auf einer Pixel-Instanz direkt aufgerufen werden, so als wäre es eine Methode von Pixel.

package main

import "fmt"

type Point struct {
	X, Y int
}

func NewPoint(x, y int) Point {
	return Point{x, y}
}

func (point *Point) ShiftX(distance int) {
	point.X += distance
}


type Pixel struct {
	Point     // bettet Point transparent samt Methoden ein
	Color int
}

func NewPixel(x, y, color int) Pixel {
	return Pixel{Point{x, y}, color}
}

func main() {
	pixel := NewPixel(1, 1, 908124)
	fmt.Println(pixel)
	pixel.ShiftX(8) // Aufruf einer Methode auf dem eingetteten 'Point'
	fmt.Println(pixel)
}

Ausführbares Beispiel

Uneindeutige Methodenaufrufe

Wenn zwei eingebettete Strukturen die gleiche Schnittstelle implementieren und eine Methode dieser Schnittstelle auf der einbettenden Struktur aufgerufen wird, gibt es den Compiler-Fehler "ambiguous selector", weil der Compiler einfach nicht weiß, auf welcher inneren Struktur er Do() aufrufen soll. Gemäß der Go-Philosophie solche Zweifelsfälle im Code sichtbar zu machen, muss man den (impliziten) Namen der gewünschten inneren Struktur beim Aufruf angeben. Im Folgenden Beispiel muss statt dem fehlerhaften union.Do() also union.A.Do() oder union.B.Do() aufgerufen werden:

package main

type Doer interface {
	Do()
}

type A struct {}

func (A) Do() {}

type B struct {}

func (B) Do() {}

type Union struct {
	A
	B
}

func main() {
	union := new(Union)
	// union.Do() Geht nicht, weil Do() uneindeutig ist!
	union.A.Do()
	union.B.Do()
}

Ausführbares Beispiel

Methoden überschreiben

Methoden können auch überschrieben werden, wenn sie auf der äußeren Struktur definiert werden. Im folgenden Beispiel wird die auf Point definierte Methode func (point *Point) ShiftX(distance int) von der einbettenden Struktur Pixel durch die Methode func (point *Pixel) ShiftX(distance int) überschrieben:

package main

import "fmt"


type Point struct {
	X, Y int
}

func NewPoint(x, y int) Point {
	return Point{x, y}
}

func (point *Point) ShiftX(distance int) {
	point.X += distance
}


type Pixel struct {
	Point // Einbettung von Point
	Color	int
}

func NewPixel(x, y, color int) Pixel {
	return Pixel{Point{x, y}, color}
}

// Überschreibt Point.ShiftX
func (pixel *Pixel) ShiftX(distance int) {
	pixel.X += distance*2
}

func main() {
	pixel := NewPixel(1, 1, 908124)
	fmt.Println(pixel)
	pixel.ShiftX(8) // ruft die Methode auf Pixel und nicht auf Point auf
	fmt.Println(pixel)
}

Ausführbares Beispiel

Weitere Information

Wesentliche Änderungen