Verständnis von Pointern in Go

Entdecken Sie, wie Zeiger die Art und Weise beeinflussen, wie Daten im Speicher gehandhabt werden. Wir zeigen Ihnen, wie Sie Funktionen und Methoden nutzen können, um Daten effizient zu verarbeiten und zu ändern.

Wenn Sie Software in Go schreiben, werden Sie Funktionen und Methoden erstellen. Sie übergeben Daten an diese Funktionen als Argumente. Manchmal benötigt die Funktion eine lokale Kopie der Daten, und Sie möchten nicht, dass das Original verändert wird. Zum Beispiel, wenn Sie eine Bank sind und eine Funktion haben, die dem Benutzer die Änderungen seines Kontostands je nach gewähltem Sparplan zeigt, möchten Sie den tatsächlichen Kontostand des Kunden nicht ändern, bevor er einen Plan auswählt; Sie möchten es nur für Berechnungen verwenden. Dies wird als „by value“ bezeichnet, da Sie den Wert der Variablen an die Funktion senden, jedoch nicht die Variable selbst.

Manchmal möchten Sie jedoch, dass die Funktion in der Lage ist, die Daten in der Originalvariablen zu ändern. Zum Beispiel, wenn der Bankkunde eine Einzahlung auf sein Konto tätigt, möchten Sie, dass die Einzahlungsfunktion auf den tatsächlichen Kontostand zugreifen kann, nicht auf eine Kopie. In diesem Fall müssen Sie die eigentlichen Daten nicht an die Funktion senden; Sie müssen der Funktion nur mitteilen, wo die Daten im Speicher abgelegt sind. Ein Datentyp namens Zeiger enthält die Speicheradresse der Daten, nicht die Daten selbst. Die Speicheradresse zeigt der Funktion, wo sie die Daten finden kann, aber nicht den Wert der Daten. Sie können dem Zeiger anstelle der Daten an die Funktion übergeben, und die Funktion kann dann die Originalvariable direkt ändern. Dies wird als „by reference“ bezeichnet, da der Wert der Variablen nicht an die Funktion übergeben wird, sondern nur deren Speicherort.

Definition und Verwendung von Zeigern

Wenn Sie einen Zeiger auf eine Variable verwenden, gibt es einige verschiedene Syntaxelemente, die Sie verstehen müssen. Das erste ist die Verwendung des Kaufmannszeichens (&). Wenn Sie ein Kaufmannszeichen vor den Namen einer Variablen setzen, geben Sie an, dass Sie die Adresse oder einen Zeiger auf diese Variable erhalten möchten. Das zweite Syntaxelement ist die Verwendung des Asterisk (*) oder Dereferenz-Operators. Wenn Sie eine Zeigervariable deklarieren, folgen Sie dem Variablennamen mit dem Typ der Variablen, auf den der Zeiger zeigt, vorangestellt mit einem *, wie folgt:

var myPointer *int32 = &someint

Dies erstellt myPointer als Zeiger auf eine int32-Variable und initialisiert den Zeiger mit der Adresse von someint. Der Zeiger enthält tatsächlich keine int32, sondern nur die Adresse einer solchen.

Lassen Sie uns einen Blick auf einen Zeiger auf einen String werfen. Der folgende Code deklariert sowohl einen Wert eines Strings als auch einen Zeiger auf einen String:

    package main

import "fmt"

func main() {
    var creature string = "shark"
    var pointer *string = &creature

    fmt.Println("creature =", creature)
    fmt.Println("pointer =", pointer)
}


Führen Sie das Programm mit folgendem Befehl aus:

<pre>
go run main.go
</pre>


Wenn Sie das Programm ausführen, wird der Wert der Variablen sowie die Adresse des Speicherorts, an dem die Variable gespeichert ist (die Zeigeradresse), ausgegeben. Die Speicheradresse ist eine hexadezimale Zahl und nicht zur menschlichen Lesbarkeit gedacht. In der Praxis werden Sie wahrscheinlich nie eine Speicheradresse ausgeben, um sie anzusehen. Wir zeigen sie nur zu Illustrationszwecken. Da jedes Programm in seinem eigenen Speicherbereich erstellt wird, wird der Wert des Zeigers jedes Mal, wenn Sie ihn ausführen, unterschiedlich sein und von der hier gezeigten Ausgabe abweichen.

    Ausgabe:
creature = shark
pointer = 0xc0000721e0


Die erste von uns definierte Variable haben wir „creature“ genannt und auf einen String mit dem Wert „shark“ gesetzt. Dann haben wir eine andere Variable namens „pointer“ erstellt. Dieses Mal haben wir den Wert der Zeigervariable auf die Adresse der „creature“-Variable gesetzt. Wir speichern die Adresse eines Werts in einer Variablen, indem wir das Kaufmannszeichen (&) verwenden. Das bedeutet, dass die Zeigervariable die Adresse der „creature“-Variable speichert, nicht den tatsächlichen Wert.

Deshalb haben wir, als wir den Wert des Zeigers ausgaben, den Wert 0xc0000721e0 erhalten, was die Adresse ist, an der sich die „creature“-Variable derzeit im Computerspeicher befindet.

Wenn Sie den Wert der von der Zeigervariable gezeigten Variablen ausgeben möchten, müssen Sie diese Variable dereferenzieren. Der folgende Code verwendet den *-Operator, um die Zeigervariable zu dereferenzieren und deren Wert abzurufen:

<code>
package main

import "fmt"

func main() {
var creature string = "shark"
var pointer *string = &creature

fmt.Println("creature =", creature)
fmt.Println("pointer =", pointer)

fmt.Println("*pointer =", *pointer)
}
</code>


Wenn Sie diesen Code ausführen, sehen Sie die folgende Ausgabe:

Ausgabe:
creature = shark
pointer = 0xc000010200
*pointer = shark


Die letzte Zeile, die wir hinzugefügt haben, dereferenziert jetzt die Zeigervariable und gibt den Wert aus, der an dieser Adresse gespeichert ist.

Wenn Sie den Wert ändern möchten, der an der Adresse der Zeigervariable gespeichert ist, können Sie auch den Dereferenz-Operator verwenden:

<code>
package main

import "fmt"

func main() {
var creature string = "shark"
var pointer *string = &creature

fmt.Println("creature =", creature)
fmt.Println("pointer =", pointer)

fmt.Println("*pointer =", *pointer)

*pointer = "jellyfish"
fmt.Println("*pointer =", *pointer)
}
</code>


Führen Sie diesen Code aus, um die Ausgabe zu sehen:

Ausgabe:
creature = shark
pointer = 0xc000094040
*pointer = shark
*pointer = jellyfish


Wir setzen den Wert, auf den die Zeigervariable zeigt, indem wir das Sternchen (*) vor dem Variablennamen verwenden und einen neuen Wert, „jellyfish“, angeben. Wie Sie sehen können, wenn wir den dereferenzierten Wert ausgeben, ist er jetzt auf „jellyfish“ gesetzt.

Sie haben es vielleicht nicht bemerkt, aber tatsächlich haben wir den Wert der „creature“-Variable geändert. Dies liegt daran, dass die Zeigervariable tatsächlich auf die Adresse der „creature“-Variable zeigt. Dies bedeutet, dass, wenn wir den von der Zeigervariable gezeigten Wert ändern, wir auch den Wert der „creature“-Variable ändern.

<code>
package main

import "fmt"

func main() {
var creature string = "shark"
var pointer *string = &creature

fmt.Println("creature =", creature)
fmt.Println("pointer =", pointer)

fmt.Println("*pointer =", *pointer)

*pointer = "jellyfish"
fmt.Println("*pointer =", *pointer)
fmt.Println("creature =", creature)
}
</code>



Die Ausgabe sieht so aus:

Ausgabe:
creature = shark
pointer = 0xc000010200
*pointer = shark
*pointer = jellyfish
creature = jellyfish


Obwohl dieser Code veranschaulicht, wie ein Zeiger funktioniert, ist dies nicht die typische Art und Weise, wie Sie Zeiger in Go verwenden würden. Es ist üblicher, sie beim Definieren von Funktionsargumenten und Rückgabewerten zu verwenden oder sie beim Definieren von Methoden für benutzerdefinierte Typen zu verwenden. Schauen wir uns an, wie Sie Zeiger mit Funktionen verwenden, um den Zugriff auf eine Variable gemeinsam zu nutzen.

Nochmals, beachten Sie, dass wir den Wert des Zeigers ausgeben, um zu veranschaulichen, dass es sich um einen Zeiger handelt. In der Praxis würden Sie den Wert eines Zeigers wahrscheinlich nicht verwenden, außer um auf den zugrunde liegenden Wert zuzugreifen, um diesen Wert abzurufen oder zu aktualisieren.

Funktion mit Zeigerempfänger

Wenn Sie eine Funktion schreiben, können Sie festlegen, ob Argumente per Wert oder per Referenz übergeben werden. Per Wert bedeutet, dass eine Kopie des Werts an die Funktion gesendet wird, und alle Änderungen an diesem Argument in dieser Funktion beeinflussen nur diese Variable in dieser Funktion und nicht dort, von wo sie übergeben wurde. Wenn Sie jedoch per Referenz übergeben, d.h. Sie einen Zeiger auf das Argument übergeben, können Sie den Wert in der Funktion ändern und auch den Wert der ursprünglichen Variable ändern, von der sie übergeben wurde. Sie können mehr darüber lesen, wie Sie Funktionen definieren, in unserem Artikel „Wie man Funktionen in Go definiert und aufruft“.

Die Entscheidung, wann Sie einen Zeiger anstelle eines Werts übergeben, hängt davon ab, ob Sie möchten, dass der Wert geändert wird oder nicht. Wenn Sie nicht möchten, dass der Wert geändert wird, übergeben Sie ihn als Wert. Wenn Sie möchten, dass die Funktion, der Sie Ihre Variable übergeben, sie ändern kann, dann sollten Sie sie als Zeiger übergeben.

Um den Unterschied zu sehen, werfen wir zuerst einen Blick auf eine Funktion, die ein Argument per Wert übergibt:

<code>
package main

import "fmt"

type Creature struct {
Species string
}

func main() {
var creature Creature = Creature{Species: "shark"}

fmt.Printf("1) %+v\n", creature)
changeCreature(creature)
fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature Creature) {
creature.Species = "jellyfish"
fmt.Printf("2) %+v\n", creature)
}
</code>


Die Ausgabe sieht so aus:

Ausgabe
1) {Species:shark}
2) {Species:jellyfish}
3) {Species:shark}



Zuerst haben wir einen benutzerdefinierten Typ namens „Creature“ erstellt. Er hat ein Feld namens „Species“, das ein String ist. In der Hauptfunktion haben wir eine Instanz unseres neuen Typs erstellt, die wir „creature“ genannt haben, und das Feld „Species“ auf „shark“ gesetzt. Dann haben wir die Variable ausgegeben, um den aktuellen Wert in der „creature“-Variable anzuzeigen.

Als Nächstes rufen wir die Funktion „changeCreature“ auf und übergeben eine Kopie der „creature“-Variable. Die Funktion „changeCreature“ ist so definiert, dass sie ein Argument namens „creature“ akzeptiert, und es ist vom Typ „Creature“, den wir zuvor definiert haben. Dann ändern wir den Wert des „Species“-Feldes auf „jellyfish“ und geben ihn aus. Beachten Sie, dass innerhalb der „changeCreature“-Funktion der Wert des „Species“-Felds jetzt „jellyfish“ ist, und es wird „2) {Species:jellyfish}“ ausgegeben. Dies liegt daran, dass wir in unserem Funktionsbereich Änderungen am Wert vornehmen dürfen.

Wenn jedoch die letzte Zeile der Hauptfunktion den Wert der „creature“-Variable ausgibt, ist der Wert des „Species“-Feldes immer noch „shark“. Der Grund, warum der Wert nicht geändert wurde, ist, dass wir die Variable per Wert übergeben haben. Dies bedeutet, dass eine Kopie des Werts im Speicher erstellt und an die „changeCreature“-Funktion übergeben wurde. Dies ermöglicht es uns, eine Funktion zu haben, die Änderungen an beliebigen übergebenen Argumenten vornehmen kann, beeinflusst jedoch keine dieser Variablen außerhalb der Funktion.

Als Nächstes ändern wir die „changeCreature“-Funktion, um ein Argument per Referenz zu übergeben. Dies können wir tun, indem wir den Typ von „creature“ in einen Zeiger umwandeln, indem wir den Stern (*)-Operator verwenden. Anstelle einer „creature“ übergeben wir nun einen Zeiger auf eine „creature“, also „*creature“. Im vorherigen Beispiel war „creature“ eine Struktur mit einem „Species“-Wert von „shark“. „*creature“ ist ein Zeiger, keine Struktur, also ist sein Wert eine Speicheradresse, und das ist es, was wir an „changeCreature()“ übergeben.

<code>
package main

import "fmt"

type Creature struct {
Species string
}

func main() {
var creature Creature = Creature{Species: "shark"}

fmt.Printf("1) %+v\n", creature)
changeCreature(&creature)
fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
creature.Species = "jellyfish"
fmt.Printf("2) %+v\n", creature)
}
</code>


Führen Sie diesen Code aus, um die folgende Ausgabe zu sehen:

Ausgabe:
1) {Species:shark}
2) &{Species:jellyfish}
3) {Species:jellyfish}


Beachten Sie, dass jetzt, wenn wir den Wert des „Species“-Feldes in der „changeCreature“-Funktion ändern, auch der ursprüngliche Wert in der Hauptfunktion geändert wird. Dies liegt daran, dass wir die „creature“-Variable per Referenz übergeben haben, was den Zugriff auf den ursprünglichen Wert ermöglicht und Änderungen daran ermöglicht.

Daher müssen Sie, wenn Sie möchten, dass eine Funktion den Wert ändern kann, ihn per Referenz übergeben. Um per Referenz zu übergeben, übergeben Sie den Zeiger auf die Variable und nicht die Variable selbst.

Es gibt jedoch Fälle, in denen Sie keinen tatsächlichen Wert für einen Zeiger haben. In solchen Fällen ist es möglich, dass im Programm eine Panik auftritt. Lassen Sie uns sehen, wie das passiert und wie Sie sich auf dieses potenzielle Problem vorbereiten können.

Nil-Zeiger

Alle Variablen in Go haben einen Nullwert. Das gilt auch für einen Zeiger. Wenn Sie also einen Zeiger auf einen Typ deklarieren, aber keinen Wert zuweisen, wird der Nullwert „nil“ sein. „nil“ bedeutet, dass „nichts initialisiert wurde“ für die Variable.

In folgendem Programm definieren wir einen Zeiger auf einen Creature-Typ, instanziieren jedoch keine tatsächliche Creature-Instanz und weisen die Adresse dieser Instanz der Zeigervariable „creature“ zu. Der Wert wird „nil“ sein, und wir können nicht auf die Felder oder Methoden zugreifen, die im Creature-Typ definiert wären.

<code>
package main

import "fmt"

type Creature struct {
Species string
}

func main() {
var creature *Creature

fmt.Printf("1) %+v\n", creature)
changeCreature(creature)
fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
creature.Species = "jellyfish"
fmt.Printf("2) %+v\n", creature)
}
</code>


Die Ausgabe sieht so aus:

Ausgabe:
1) <nil>
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x109ac86]

goroutine 1 [running]:
main.changeCreature(0x0)
/Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/centron/pointers/src/nil.go:18 +0x26
main.main()
/Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/centron/pointers/src/nil.go:13 +0x98
exit status 2


Wenn wir das Programm ausführen, wird der Wert der „creature“-Variable ausgedruckt, und der Wert ist „<nil>“. Dann rufen wir die Funktion „changeCreature“ auf, und wenn diese Funktion versucht, den Wert des „Species“-Feldes zu setzen, tritt eine Panik auf. Dies liegt daran, dass keine Instanz der Variable tatsächlich erstellt wurde. Daher hat das Programm keine Stelle, an der es den Wert speichern kann, und das Programm stürzt ab.

In Go ist es üblich, dass, wenn Sie ein Argument als Zeiger erhalten, Sie überprüfen, ob es „nil“ ist, bevor Sie darauf zugreifen, um zu verhindern, dass das Programm abstürzt.

Das ist ein gebräuchlicher Ansatz zum Überprüfen auf „nil“:

<code>
if someVariable == nil {
// Fehlermeldung ausgeben oder aus der Methode/Funktion zurückkehren
}
</code>


Effektiv möchten Sie sicherstellen, dass Sie keinen „nil“-Zeiger in Ihre Funktion oder Methode übergeben, da er wahrscheinlich einfach nur zurückgegeben wird oder einen Fehler zurückgibt, um anzuzeigen, dass ein ungültiges Argument an die Funktion oder Methode übergeben wurde. Der folgende Code zeigt, wie man auf „nil“ überprüft:

<code>
package main

import "fmt"

type Creature struct {
Species string
}

func main() {
var creature *Creature

fmt.Printf("1) %+v\n", creature)
changeCreature(creature)
fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
if creature == nil {
fmt.Println("creature is nil")
return
}

creature.Species = "jellyfish"
fmt.Printf("2) %+v\n", creature)
}
</code>


Wir haben eine Überprüfung in die „changeCreature“-Funktion eingefügt, um zu sehen, ob der Wert des „creature“-Arguments „nil“ war. Wenn er das war, geben wir „creature is nil“ aus und kehren aus der Funktion zurück. Andernfalls fahren wir fort und ändern den Wert des „Species“-Feldes. Wenn wir das Programm ausführen, erhalten wir jetzt folgende Ausgabe:

Ausgabe:
1) <nil>
creature is nil
3) <nil>


Beachten Sie, dass wir zwar immer noch einen „nil“-Wert für die „creature“-Variable haben, aber wir stürzen nicht mehr ab, weil wir diese Situation überprüfen.

Schließlich, wenn wir eine Instanz des Creature-Typs erstellen und der „creature“-Variablen zuweisen, ändert das Programm den Wert wie erwartet:

<code>
package main

import "fmt"

type Creature struct {
Species string
}

func main() {
var creature *Creature
creature = &Creature{Species: "shark"}

fmt.Printf("1) %+v\n", creature)
changeCreature(creature)
fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
if creature == nil {
fmt.Println("creature is nil")
return
}

creature.Species = "jellyfish"
fmt.Printf("2) %+v\n", creature)
}
</code>


Jetzt, da wir eine Instanz des Creature-Typs haben, wird das Programm ausgeführt, und wir erhalten die erwartete Ausgabe:

Ausgabe:
1) &{Species:shark}
2) &{Species:jellyfish}
3) &{Species:jellyfish}


Wenn Sie mit Zeigern arbeiten, besteht die Möglichkeit, dass das Programm abstürzt. Um Abstürze zu vermeiden, sollten Sie überprüfen, ob ein Zeigerwert „nil“ ist, bevor Sie versuchen, auf eines der Felder oder Methoden zuzugreifen, die auf ihm definiert sind.

Zuletzt schauen wir uns an, wie die Verwendung von Zeigern und Werten die Definition von Methoden für einen Typ beeinflusst.

Methoden mit Zeigerempfängern

Ein Empfänger in Go ist das Argument, das in einer Methodendeklaration definiert ist. Werfen Sie einen Blick auf den folgenden Code:

<code>
type Creature struct {
Species string
}

func (c Creature) String() string {
return c.Species
}
</code>



Der Empfänger in dieser Methode ist „c Creature“. Es besagt, dass die Instanz von „c“ vom Typ „Creature“ ist. Das bedeutet, dass „String“ eine Methode aufgerufen wird, die auf einer Instanz des „Creature“-Typs aufgerufen wird, und der Wert der Instanz ist an die Methode übergeben.

Dies bedeutet, dass, wenn wir „creature.String()“ aufrufen, die Methode „String“ aufgerufen wird und die Methode auf eine Kopie der „creature“-Variable zugreift, und die Methode gibt den Wert des „Species“-Feldes zurück. Dies ist nützlich, wenn Sie möchten, dass eine Methode den Wert eines Typs abruft, aber keine Änderungen an der Instanz vornehmen möchte.

Wenn wir jedoch Änderungen an der Instanz vornehmen möchten, können wir die Methode so definieren, dass sie einen Zeiger empfängt. Das bedeutet, dass die Methode auf dieselbe Instanz zugreift, jedoch nicht auf eine Kopie davon. Hier ist die Methode mit einem Zeigerempfänger:

<code>
func (c *Creature) String() string {
return c.Species
}
</code>


Beachten Sie, dass der Empfänger jetzt „*Creature“ ist, was bedeutet, dass die Methode auf einen Zeiger auf eine Instanz des „Creature“-Typs zugreift.

Wenn wir jetzt „creature.String()“ aufrufen, wird die Methode aufgerufen, und sie erhält keinen Wert, sondern einen Zeiger auf die „creature“-Instanz. Dies bedeutet, dass die Methode auf dasselbe Exemplar zugreifen kann und Änderungen an diesem Exemplar vornehmen kann.

Hier ist der vollständige Code:

<code>
package main

import "fmt"

type Creature struct {
Species string
}

func (c Creature) String() string {
return c.Species
}

func main() {
var creature Creature = Creature{Species: "shark"}

fmt.Printf("1) %s\n", creature.String())
changeCreature(creature)
fmt.Printf("3) %s\n", creature.String())
}

func changeCreature(c Creature) {
c.Species = "jellyfish"
fmt.Printf("2) %s\n", c.String())
}
</code>


In diesem Code hat die Methode „String“ einen Wert als Empfänger, und wir rufen die Methode „String“ auf der „creature“-Variable auf. Das Programm gibt die folgende Ausgabe aus:

Ausgabe:
1) shark
2) jellyfish
3) shark



Beachten Sie, dass, obwohl die Methode „String“ aufgerufen wurde, die „creature“-Variable außerhalb der Methode nicht geändert wurde. Das liegt daran, dass die Methode eine Kopie der „creature“-Variable erhalten hat und keine Änderungen an der ursprünglichen Variable vorgenommen hat.

Jetzt ändern wir die Methode, um einen Zeiger als Empfänger zu verwenden:

<code>
func (c *Creature) String() string {
return c.Species
}
</code>


In diesem Fall, wenn wir die Methode „String“ auf der „creature“-Variable aufrufen, erhält die Methode einen Zeiger auf die Variable, und sie kann den Wert ändern. Hier ist der aktualisierte Code:


package main import "fmt" type Creature struct { Species string } func (c *Creature) String() string { return c.Species } func main() { var creature Creature = Creature{Species: "shark"} fmt.Printf("1) %s\n", creature.String()) changeCreature(&creature) fmt.Printf("3) %s\n", creature.String()) } func changeCreature(c *Creature) { c.Species = "jellyfish" fmt.Printf("2) %s\n", c.String()) }


    <code>
package main

import "fmt"

type Creature struct {
Species string
}

func (c *Creature) String() string {
return c.Species
}

func main() {
var creature Creature = Creature{Species: "shark"}

fmt.Printf("1) %s\n", creature.String())
changeCreature(&creature)
fmt.Printf("3) %s\n", creature.String())
}

func changeCreature(c *Creature) {
c.Species = "jellyfish"
fmt.Printf("2) %s\n", c.String())
}
</code>


Wenn Sie das Programm ausführen, sehen Sie die folgende Ausgabe:

Ausgabe:
1) shark
2) jellyfish
3) jellyfish


Beachten Sie, dass diesmal die Methode „String“ in der „creature“-Variable Änderungen vorgenommen hat, weil sie einen Zeiger auf die „creature“-Variable erhalten hat. Dadurch wurde die Methode in der Lage, den Wert der „Species“-Variable zu ändern, und die Änderung wurde in der „creature“-Variable außerhalb der Methode übernommen.

Das ist ein umfassender Überblick über die Verwendung von Zeigern in Go, wie sie mit Funktionen und Methoden interagieren und wie Sie sie in Ihren Programmen einsetzen können. Die Verwendung von Zeigern ermöglicht es Ihnen, auf dieselben Daten im Speicher zuzugreifen und sie gemeinsam zu nutzen. Sie können den Wert von Variablen direkt ändern, anstatt Kopien der Werte zu übergeben. Es ist ein leistungsstarkes Konzept, und Go macht es einfach und effizient.

Kostenlosen Account erstellen

Registrieren Sie sich jetzt und erhalten Sie Zugang zu unseren Cloud Produkten.

Das könnte Sie auch interessieren: