Typen
Im vorherigen Abschnitt zu Datentypen wurden alle integrierten Datentypen von Go kurz vorgestellt. Diese integrierten Basistypen bilden die Grundlage für benutzerdefinierte Typen. Go ist eine typische statisch typisierte Sprache, bei der der Typ aller Variablen zur Kompilierzeit bestimmt wird und während der gesamten Lebensdauer des Programms unverändert bleibt. Dieser Abschnitt bietet einen Überblick über das Typsystem von Go und dessen grundlegende Verwendung.
Statisch stark typisiert
Go ist eine statisch stark typisierte Sprache. "Statisch" bedeutet, dass alle Variablentypen in Go bereits zur Kompilierzeit festgelegt sind und sich während der Laufzeit des Programms nicht ändern. Obwohl die kurze Variablendeklaration in Go einer dynamischen Sprache ähnelt, wird der Typ der Variablen vom Compiler abgeleitet. Der grundlegende Unterschied besteht darin, dass der Typ nach der Ableitung nicht mehr geändert werden kann, während dynamische Sprachen das Gegenteil tun. Daher kann der folgende Code nicht kompiliert werden, da a eine Variable vom Typ int ist und kein String zugewiesen werden kann:
func main() {
var a int = 64
a = "64"
fmt.Println(a) // cannot use "64" (untyped string constant) as int value in assignment
}"Stark typisiert" bedeutet, dass das Programm strenge Typprüfungen durchführt. Wenn Typinkompatibilitäten auftreten, wird der Programmierer sofort informiert, anstatt wie bei dynamischen Sprachen zu versuchen, mögliche Ergebnisse abzuleiten. Daher kann der folgende Code nicht kompiliert werden, da die beiden Typen unterschiedlich sind und keine Operation durchgeführt werden kann:
func main() {
fmt.Println(1 + "1") // invalid operation: 1 + "1" (mismatched types untyped int and untyped string)
}Typsuffix
Warum platziert Go die Typdeklaration hinter dem Namen und nicht davor? Dies ist weitgehend eine Lehre aus C. Hier ist ein Beispiel aus der offiziellen Dokumentation - ein Funktionszeiger:
int (*(*fp)(int (*)(int, int), int))(int, int)Ehrlich gesagt ist es ohne genaueres Hinsehen schwer zu erkennen, um welchen Typ es sich handelt. In Go würde eine ähnliche Deklaration so aussehen:
f func(func(int,int) int, int) func(int, int) intDie Deklarationsweise in Go folgt stets dem Prinzip: Name zuerst, Typ danach. Von links nach rechts gelesen, weiß man auf den ersten Blick, dass es sich um eine Funktion handelt, deren Rückgabewert func(int,int) int ist. Wenn Typen komplexer werden, ist die Typsuffix-Notation viel besser lesbar. Viele Designentscheidungen in Go dienen der Lesbarkeit.
Typdeklaration
In Go kann durch eine Typdeklaration ein benutzerdefinierter Typ mit einem neuen Namen deklariert werden. Die Deklaration eines neuen Typs erfordert normalerweise einen Typnamen und einen Basistyp. Ein einfaches Beispiel:
type MyInt int64In dieser Typdeklaration wird mit dem Schlüsselwort type ein Typ namens MyInt mit dem Basistyp int64 deklariert. In Go muss jeder neu deklarierte Typ einen entsprechenden Basistyp haben. Es wird empfohlen, den Typnamen nicht mit existierenden integrierten Bezeichnern zu wiederholen.
type MyInt int64
type MyFloat64 float64
type MyMap map[string]int
// Kann kompiliert werden, aber nicht empfohlen - dies überschreibt den ursprünglichen Typ
type int int64Durch Typdeklaration erstellte Typen sind neue Typen. Verschiedene Typen können nicht miteinander operieren, selbst wenn der Basistyp gleich ist:
type MyFloat64 float64
var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(f1 + f)invalid operation: f1 + f (mismatched types MyFloat64 and float64)Typalias
Ein Typalias unterscheidet sich von einer Typdeklaration. Ein Typalias ist nur ein Alias und erstellt keinen neuen Typ. Ein einfaches Beispiel:
type Int = intBeide sind derselbe Typ, nur mit unterschiedlichen Namen. Daher können sie miteinander operieren, und das folgende Beispiel kann kompiliert werden:
type Int = int
var a Int = 1
var b int = 2
fmt.Println(a + b)3Typaliase sind für sehr komplexe Typen sehr nützlich. Angenommen, es gibt einen Typ map[string]map[string]int, eine zweidimensionale Map. Nun hat eine Funktion einen Parameter vom Typ map[string]map[string]int:
func PrintMyMap(mymap map[string]map[string]int) {
fmt.Println(mymap)
}In diesem Fall ist es nicht notwendig, eine Typdeklaration zu verwenden, da前者 einen neuen Typ deklariert, der nicht als Parameter für diese Funktion verwendet werden kann. Ein Beispiel mit Typalias:
type TwoDMap = map[string]map[string]int
func PrintMyMap(mymap TwoDMap) {
fmt.Println(mymap)
}Mit einem Typalias sieht es prägnanter aus.
TIP
Der integrierte Typ any ist ein Typalias für interface{}. Beide sind vollständig gleichwertig, nur die Bezeichnung unterscheidet sich.
Typkonvertierung
In Go gibt es nur explizite Typkonvertierungen, keine impliziten Typkonvertierungen. Daher können Variablen unterschiedlichen Typs nicht miteinander operieren und nicht als Parameter übergeben werden. Die Voraussetzung für eine Typkonvertierung ist, dass der Typ der zu konvertierenden Variablen und der Zieltyp bekannt sind. Ein Beispiel:
type MyFloat64 float64
var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(float64(f1) + f)0.30000000000000004Durch explizite Konvertierung von MyFloat64 zu float64 kann die Addition durchgeführt werden. Eine weitere Voraussetzung für die Typkonvertierung ist: Der zu konvertierende Typ muss durch den Zieltyp repräsentiert werden können (Representability). Zum Beispiel kann int durch int64 und float64 repräsentiert werden, daher können sie explizit konvertiert werden. int kann jedoch nicht durch string oder bool repräsentiert werden, daher ist keine Typkonvertierung möglich.
TIP
Die Definition von Representability finden Sie im Referenzhandbuch - Representability.
Selbst wenn zwei Typen ineinander konvertiert werden können, ist das Ergebnis nicht immer korrekt. Betrachten Sie das folgende Beispiel:
var num1 int8 = 1
var num2 int32 = 512
fmt.Println(int32(num1), int8(num2))1 0num1 wurde korrekt zu int32 konvertiert, aber num2 nicht. Dies ist ein typisches Problem des numerischen Überlaufs. int32 kann 31-Bit-Integer darstellen, int8 nur 7-Bit-Integer. Bei der Konvertierung von hochpräzisen zu niedrigpräzisen Integer werden die hohen Bits verworfen und die niedrigen Bits beibehalten. Daher ist das Konvertierungsergebnis von num1 0. Bei numerischen Typkonvertierungen wird empfohlen, von klein zu groß zu konvertieren, nicht von groß zu klein.
Bei der Verwendung von Typkonvertierungen müssen bei einigen Typen Mehrdeutigkeiten vermieden werden:
*Point(p) // Entspricht *(Point(p))
(*Point)(p) // Konvertiert p zu Typ *Point
<-chan int(c) // Entspricht <-(chan int(c))
(<-chan int)(c) // Konvertiert c zu Typ <-chan int
(func())(x) // Konvertiert x zu Typ func()
(func() int)(x) // Konvertiert x zu Typ func() intTypassertion
Typassertion wird normalerweise verwendet, um zu prüfen, ob eine Variable eines Schnittstellentyps zu einem bestimmten Typ gehört:
var b int = 1
var a interface{} = b
if intVal, ok := a.(int); ok {
fmt.Println(intVal)
} else {
fmt.Println("error type")
}1Da interface{} ein leerer Schnittstellentyp ist, kann der leere Schnittstellentyp alle Typen repräsentieren, aber int kann interface{} nicht repräsentieren, daher kann keine Typkonvertierung verwendet werden. Die Typassertion kann prüfen, ob der zugrunde liegende Typ der gewünschte Typ ist. Eine Typassertion hat zwei Rückgabewerte: den konvertierten Wert und einen booleschen Wert für das Konvertierungsergebnis.
Typbestimmung
In Go unterstützt die switch-Anweisung eine spezielle Schreibweise. Mit dieser kann je nach unterschiedlichem case unterschiedliche Logik ausgeführt werden. Die Voraussetzung ist, dass der Eingabeparameter ein Schnittstellentyp sein muss:
var a interface{} = 2
switch a.(type) {
case int: fmt.Println("int")
case float64: fmt.Println("float")
case string: fmt.Println("string")
}intTIP
Durch die vom unsafe-Paket bereitgestellten Operationen kann das Go-Typsystem umgangen werden, um Typkonvertierungen durchzuführen, die sonst nicht kompilieren würden.
