Advertisement
  1. Code
  2. Go

Tiefes Eintauchen in das Go Type System

Scroll to top
Read Time: 14 min

German (Deutsch) translation by Alex Grigorovich (you can also view the original English article)

Go hat ein sehr interessantes Typensystem. Es verzichtet auf Klassen und Vererbung von Schnittstellen und Kompositionen, hat nicht nur Vorlagen oder Generika. Einzigartig ist auch der Umgang mit Sammlungen.

In diesem Tutorial erfahren Sie mehr über die Vor- und Nachteile des Go-Typsystems und wie Sie es effektiv zum Schreiben von klarem und idiomatischem Go-Code verwenden können.

Das Gesamtbild des Go Type Systems

Das Go-Typ-System unterstützt die prozeduralen, objektorientierten und funktionalen Paradigmen. Die generische Programmierung wird nur sehr eingeschränkt unterstützt. Go ist zwar eine ausgesprochen statische Sprache, bietet jedoch über Schnittstellen, erstklassige Funktionen und Reflexion genügend Flexibilität für dynamische Techniken. Dem Typsystem von Go fehlen Funktionen, die in den meisten modernen Sprachen üblich sind:

  • Es gibt keinen Ausnahmetyp, da die Fehlerbehandlung von Go auf Rückkehrcodes und der Fehlerschnittstelle basiert.
  • Es gibt keine Überlastung des Bedieners.
  • Es gibt keine Funktionsüberladung (gleicher Funktionsname mit unterschiedlichen Parametern).
  • Es gibt keine optionalen oder Standardfunktionsparameter.

Diese Auslassungen sind alle beabsichtigt, um Go so einfach wie möglich zu gestalten.

Geben Sie Aliase ein

Sie können in Go Alias-Typen erstellen und unterschiedliche Typen erstellen. Sie können einem Alias-Typ ohne Konvertierung keinen Wert des zugrunde liegenden Typs zuweisen. Beispielsweise verursacht die Zuweisung var b int = a im folgenden Programm einen Kompilierungsfehler, da der Typ Age ein Alias von int ist, aber kein int:

1
package main
2
3
4
type Age int
5
6
func main() {
7
    var a Age = 5
8
    var b int = a
9
}
10
11
12
Output:
13
14
tmp/sandbox547268737/main.go:8: cannot use a (type Age) as
15
type int in assignment

Sie können Typdeklarationen gruppieren oder eine Deklaration pro Zeile verwenden:

1
type IntIntMap map[int]int
2
StringSlice []string
3
4
type (
5
    Size   uint64
6
    Text  string
7
    CoolFunc func(a int, b bool)(int, error)
8
)

Grundtypen

Alle üblichen Verdächtigen sind hier: Bool, String, Ganzzahlen und Ganzzahlen ohne Vorzeichen mit expliziten Bitgrößen, Gleitkommazahlen (32-Bit und 64-Bit) und komplexen Zahlen (64-Bit und 128-Bit).

1
bool
2
string
3
int  int8  int16  int32  int64
4
uint uint8 uint16 uint32 uint64 uintptr
5
byte // alias for uint8
6
rune // alias for int32, represents a Unicode code point
7
float32 float64
8
complex64 complex128

Strings

Strings in Go sind UTF8-codiert und können daher jedes Unicode-Zeichen darstellen. Das Zeichenfolgenpaket bietet eine Reihe von Zeichenfolgenoperationen. Hier ist ein Beispiel dafür, wie Sie eine Reihe von Wörtern in Groß- und Kleinschreibung umwandeln und zu einem Satz zusammenfügen.

1
package main
2
3
import (
4
    "fmt"
5
    "strings"
6
)
7
8
func main() {
9
    words := []string{"i", "LikE", "the ColORS:", "RED,", 
10
                      "bLuE,", "AnD", "GrEEn"}
11
    properCase := []string{}
12
    
13
    for i, w := range words {
14
        if i == 0 {
15
            properCase = append(properCase, strings.Title(w))
16
        } else {
17
            properCase = append(properCase, strings.ToLower(w))
18
        }
19
    }
20
    
21
    sentence := strings.Join(properCase, " ") + "."
22
    fmt.Println(sentence)
23
}

Pointers

Go hat Zeiger. Der Nullzeiger (siehe Zero Value später) ist Null. Sie können mit dem Operator & einen Zeiger auf einen Wert abrufen und mit dem Operator * zurückkehren. Sie können auch Zeiger auf Zeiger haben.

1
package main
2
3
import (
4
    "fmt"
5
)
6
7
8
type S struct {
9
    a float64
10
    b string
11
}
12
13
func main() {
14
    x := 5
15
    px := &x
16
    *px = 6
17
    fmt.Println(x)
18
    ppx := &px
19
    **ppx = 7
20
    fmt.Println(x)
21
}

Objekt orientierte Programmierung

Go unterstützt die objektorientierte Programmierung über Schnittstellen und Strukturen. Es gibt keine Klassen und keine Klassenhierarchie, obwohl Sie anonyme Strukturen in Strukturen einbetten können, die eine Art Einzelvererbung bereitstellen.

Eine ausführliche Beschreibung der objektorientierten Programmierung in Go finden Sie unter Let's Go: Objektorientierte Programmierung in Golang.

Schnittstellen

Schnittstellen sind der Eckpfeiler des Go-Systems. Eine Schnittstelle ist nur eine Sammlung von Methodensignaturen. Jeder Typ, der alle Methoden implementiert, ist mit der Schnittstelle kompatibel. Hier ist ein kurzes Beispiel. Die Shape-Oberfläche definiert zwei Methoden: GetPerimeter() und GetArea(). Das Square-Objekt implementiert die Schnittstelle.

1
type Shape interface {
2
    GetPerimeter() uint
3
    GetArea() uint
4
}
5
6
type Square struct {
7
   side  uint
8
}
9
10
func (s *Square) GetPerimeter() uint {
11
    return s.side * 4
12
}
13
14
func (s *Square) GetArea() uint {
15
    return s.side * s.side
16
}

Die leere Schnittstelle interface{} ist mit jedem Typ kompatibel, da keine Methoden erforderlich sind. Die leere Schnittstelle kann dann auf ein beliebiges Objekt verweisen (ähnlich wie Javas Object- oder C/C ++ - Void-Zeiger) und wird häufig für die dynamische Typisierung verwendet. Schnittstellen sind immer Zeiger und zeigen immer auf ein konkretes Objekt.

Einen vollständigen Artikel zu Go-Schnittstellen finden Sie unter Definieren und Implementieren einer Go-Schnittstelle.

Strukturen

Strukturen sind die benutzerdefinierten Typen von Go. Eine Struktur enthält benannte Felder, bei denen es sich um Basistypen, Zeigertypen oder andere Strukturtypen handeln kann. Sie können Strukturen auch anonym in andere Strukturen als Form der Implementierungsvererbung einbetten.

Im folgenden Beispiel sind die S1- und S2-Strukturen in die S3-Struktur eingebettet, die auch ein eigenes int-Feld und einen Zeiger auf einen eigenen Typ hat:

1
package main
2
3
import (
4
    "fmt"
5
)
6
7
8
type S1 struct {
9
    f1 int
10
}
11
12
type S2 struct {
13
    f2 int
14
}
15
16
type S3 struct {
17
    S1
18
    S2
19
    f3 int
20
    f4 *S3
21
}
22
23
24
func main() {
25
    s := &S3{S1{5}, S2{6}, 7, nil}
26
    
27
    fmt.Println(s)
28
}
29
30
Output:
31
32
&{{5} {6} 7 <nil>}

Typ Assertions

Mit Zusicherungen-Typ können Sie eine Schnittstelle in ihren konkreten Typ konvertieren. Wenn Sie den zugrunde liegenden Typ bereits kennen, können Sie ihn einfach bestätigen. Wenn Sie sich nicht sicher sind, können Sie mehrere Zusicherungen ausprobieren, bis Sie den richtigen Typ gefunden haben.

Im folgenden Beispiel gibt es eine Liste von Dingen, die Zeichenfolgen und Nicht-Zeichenfolgenwerte enthalten, die als Teil leerer Schnittstellen dargestellt werden. Der Code durchläuft alle Dinge und versucht, jedes Element in eine Zeichenfolge zu konvertieren und alle Zeichenfolgen in einem separaten Slice zu speichern, das schließlich gedruckt wird.

1
package main
2
3
import "fmt"
4
5
func main() {
6
    things := []interface{}{"hi", 5, 3.8, "there", nil, "!"}
7
    strings := []string{}
8
    
9
    for _, t := range things {
10
        s, ok := t.(string)
11
        if ok {
12
            strings = append(strings, s)
13
        }
14
    }
15
    
16
    fmt.Println(strings)
17
    
18
}
19
20
Output:
21
22
[hi there !]

Reflexion

Mit dem Go reflect-Paket können Sie den Typ einer Schnittstelle ohne Typzusicherungen direkt überprüfen. Sie können den Wert einer Schnittstelle auch extrahieren und auf Wunsch in eine Schnittstelle konvertieren (nicht so nützlich).

Hier ist ein ähnliches Beispiel wie im vorherigen Beispiel, aber anstatt die Zeichenfolgen zu drucken, werden sie nur gezählt, sodass keine Konvertierung von Schnittstelle {} in Zeichenfolge erforderlich ist. Der Schlüssel ruft refle.Type() auf, um ein Typobjekt abzurufen, das über eine Kind() -Methode verfügt, mit der wir erkennen können, ob es sich um eine Zeichenfolge handelt oder nicht.

1
package main
2
3
import (
4
    "fmt"
5
    "reflect"
6
)
7
8
9
func main() {
10
    things := []interface{}{"hi", 5, 3.8, "there", nil, "!"}
11
    stringCount := 0
12
    
13
    for _, t := range things {
14
        tt := reflect.TypeOf(t)
15
        if tt != nil && tt.Kind() == reflect.String {
16
            stringCount++
17
        }
18
    }
19
    
20
    fmt.Println("String count:", stringCount)
21
}

Funktionen

Funktionen sind erstklassige Bürger in Go. Das heißt, Sie können Variablen Funktionen zuweisen, Funktionen als Argumente an andere Funktionen übergeben oder als Ergebnisse zurückgeben. Dadurch können Sie den funktionalen Programmierstil mit Go verwenden.

Das folgende Beispiel zeigt einige Funktionen, GetUnaryOp() und GetBinaryOp(), die anonyme Funktionen zurückgeben, die zufällig ausgewählt wurden. Das Hauptprogramm entscheidet anhand der Anzahl der Argumente, ob es eine unäre oder eine binäre Operation benötigt. Es speichert die ausgewählte Funktion in einer lokalen Variablen namens "op" und ruft sie dann mit der richtigen Anzahl von Argumenten auf.

1
package main
2
3
import (
4
    "fmt"
5
    "math/rand"
6
)
7
8
type UnaryOp func(a int) int
9
type BinaryOp func(a, b int) int
10
11
12
func GetBinaryOp() BinaryOp {
13
    if rand.Intn(2) == 0 {
14
        return func(a, b int) int { return a + b }
15
    } else {
16
        return func(a, b int) int { return a - b }
17
    }
18
}
19
20
func GetUnaryOp() UnaryOp {
21
    if rand.Intn(2) == 0 {
22
        return func(a int) int { return -a }
23
    } else {
24
        return func(a int) int { return a * a }
25
    }
26
}
27
28
29
func main() {
30
    arguments := [][]int{{4,5},{6},{9},{7,18},{33}}
31
    var result int
32
    for _, a := range arguments {
33
        if len(a) == 1 {
34
            op := GetUnaryOp()
35
            result = op(a[0])            
36
        } else {
37
            op := GetBinaryOp()
38
            result = op(a[0], a[1])                    
39
        }
40
        fmt.Println(result)                
41
    }
42
}

Kanäle

Kanäle sind ein ungewöhnlicher Datentyp. Sie können sich diese als Nachrichtenwarteschlangen vorstellen, die zum Weiterleiten von Nachrichten zwischen Goroutinen verwendet werden. Kanäle sind stark typisiert. Sie sind synchronisiert und verfügen über eine spezielle Syntaxunterstützung zum Senden und Empfangen von Nachrichten. Jeder Kanal kann nur empfangen, nur senden oder bidirektional sein.

Optional können auch Kanäle gepuffert werden. Sie können die Nachrichten in einem Kanal mithilfe des Bereichs durchlaufen, und Go-Routinen können mithilfe der vielseitigen Auswahloperation mehrere Kanäle gleichzeitig blockieren.

Hier ist ein typisches Beispiel, bei dem die Summe der Quadrate einer Liste von Ganzzahlen parallel von zwei Go-Routinen berechnet wird, von denen jede für die Hälfte der Liste verantwortlich ist. Die Hauptfunktion wartet auf Ergebnisse aus beiden Go-Routinen und addiert dann die Teilsummen für die Gesamtsumme. Beachten Sie, wie der Kanal c mit der integrierten Funktion make() erstellt wird und wie der Code über den speziellen Operator <- aus dem Kanal liest und in diesen schreibt.

1
package main
2
3
import "fmt"
4
5
func sum_of_squares(s []int, c chan int) {
6
    sum := 0
7
    for _, v := range s {
8
        sum += v * v
9
    }
10
    c <- sum // send sum to c
11
}
12
13
func main() {
14
    s := []int{11, 32, 81, -9, -14}
15
16
    c := make(chan int)
17
    go sum_of_squares(s[:len(s)/2], c)
18
    go sum_of_squares(s[len(s)/2:], c)
19
    sum1, sum2 := <-c, <-c // receive from c
20
    total := sum1 + sum2
21
22
    fmt.Println(sum1, sum2, total)
23
}

Dies kratzt nur die Oberfläche. Eine detaillierte Übersicht der Kanäle finden Sie unter:

Sammlungen

Go hat mehrere integrierte generische Sammlungen, in denen jeder Typ gespeichert werden kann. Diese Sammlungen sind speziell und Sie können keine eigenen generischen Sammlungen definieren. Die Sammlungen sind Arrays, Slices und Maps. Kanäle sind ebenfalls generisch und können auch als Sammlungen betrachtet werden, aber sie haben einige ziemlich einzigartige Eigenschaften, daher ziehe ich es vor, sie separat zu diskutieren.

Arrays

Arrays sind Sammlungen von Elementen desselben Typs mit fester Größe. Hier sind einige Arrays:

1
package main
2
import "fmt"
3
4
5
func main() {
6
    a1 := [3]int{1, 2, 3}
7
    var a2 [3]int
8
    a2 = a1 
9
10
    fmt.Println(a1)
11
    fmt.Println(a2)
12
    
13
    a1[1] = 7
14
15
    fmt.Println(a1)
16
    fmt.Println(a2)
17
    
18
    a3 := [2]interface{}{3, "hello"}
19
    fmt.Println(a3)
20
}

Die Größe des Arrays ist Teil seines Typs. Sie können Arrays des gleichen Typs und der gleichen Größe kopieren. Die Kopie ist nach Wert. Wenn Sie Elemente unterschiedlichen Typs speichern möchten, können Sie die Escape-Schraffur eines Arrays leerer Schnittstellen verwenden.

Slices

Arrays sind aufgrund ihrer festen Größe ziemlich begrenzt. Scheiben sind viel interessanter. Sie können sich Slices als dynamische Arrays vorstellen. Unter der Decke verwenden Slices ein Array zum Speichern ihrer Elemente. Sie können die Länge eines Slice überprüfen, Elemente und andere Slices anhängen und am meisten Spaß daran haben, Sub-Slices zu extrahieren, die dem Python-Slicing ähneln:

1
package main
2
3
import "fmt"
4
5
6
7
func main() {
8
    s1 := []int{1, 2, 3}
9
    var s2 []int
10
    s2 = s1 
11
12
    fmt.Println(s1)
13
    fmt.Println(s2)
14
15
    // Modify s1    
16
    s1[1] = 7
17
18
    // Both s1 and s2 point to the same underlying array
19
    fmt.Println(s1)
20
    fmt.Println(s2)
21
    
22
    fmt.Println(len(s1))
23
    
24
    // Slice s1
25
    s3 := s1[1:len(s1)]
26
    
27
    fmt.Println(s3)
28
}

Wenn Sie Slices kopieren, kopieren Sie einfach den Verweis auf dasselbe zugrunde liegende Array. Wenn Sie schneiden, zeigt das Unter-Slice immer noch auf dasselbe Array. Wenn Sie jedoch anhängen, erhalten Sie ein Slice, das auf ein neues Array verweist.

Sie können Arrays oder Slices mithilfe einer regulären Schleife mit Indizes oder mithilfe von Bereichen durchlaufen. Sie können auch Slices mit einer bestimmten Kapazität erstellen, die mit dem Wert Null ihres Datentyps mit der Funktion make() initialisiert werden:

1
package main
2
3
import "fmt"
4
5
6
7
func main() {
8
    // Create a slice of 5 booleans initialized to false    
9
    s1 := make([]bool, 5)
10
    fmt.Println(s1)
11
    
12
    s1[3] = true
13
    s1[4] = true
14
15
    fmt.Println("Iterate using standard for loop with index")
16
    for i := 0; i < len(s1); i++ {
17
        fmt.Println(i, s1[i])
18
    }
19
    
20
    fmt.Println("Iterate using range")
21
    for i, x := range(s1) {
22
        fmt.Println(i, x)
23
    }
24
}
25
26
Output:
27
28
[false false false false false]
29
Iterate using standard for loop with index
30
0 false
31
1 false
32
2 false
33
3 true
34
4 true
35
Iterate using range
36
0 false
37
1 false
38
2 false
39
3 true
40
4 true

Maps

Karten sind Sammlungen von Schlüssel-Wert-Paaren. Sie können ihnen Kartenliterale oder andere Karten zuweisen. Sie können auch leere Karten mit der integrierten Funktion make erstellen. Sie greifen mit eckigen Klammern auf Elemente zu. Maps unterstützen die Iteration mithilfe des range. Sie können testen, ob ein Schlüssel vorhanden ist, indem Sie versuchen, darauf zuzugreifen, und den zweiten optionalen booleschen Rückgabewert überprüfen.

1
package main
2
3
import (
4
    "fmt"
5
)
6
7
func main() {
8
    // Create map using a map literal
9
    m := map[int]string{1: "one", 2: "two", 3:"three"}
10
    
11
    // Assign to item by key
12
    m[5] = "five"
13
    // Access item by key
14
    fmt.Println(m[2])
15
    
16
    v, ok := m[4]
17
    if ok {
18
        fmt.Println(v)
19
    } else {
20
        fmt.Println("Missing key: 4")
21
    }
22
    
23
    
24
    for k, v := range m {
25
        fmt.Println(k, ":", v)
26
    }
27
}
28
29
Output:
30
31
two
32
Missing key: 4
33
5 : five
34
1 : one
35
2 : two
36
3 : three

Beachten Sie, dass die Iteration nicht in der Erstellungs- oder Einfügereihenfolge erfolgt.

Zero Values

Es gibt keine nicht initialisierten Typen in Go. Jeder Typ hat einen vordefinierten Zero Value. Wenn eine Variable eines Typs deklariert wird, ohne ihr einen Wert zuzuweisen, enthält sie ihren Zero Value. Dies ist ein wichtiges Sicherheitsmerkmal.

Für jeden Typ T gibt *new(T) einen Zero Value von T zurück.

Bei booleschen Typen ist der Zero Value "false". Bei numerischen Typen ist der Zero Value... Zero. Für Slices, Maps und Zeiger ist es gleich Zero. Bei Strukturen handelt es sich um eine Struktur, bei der alle Felder auf ihren Zero Value initialisiert werden.

1
package main
2
3
import (
4
    "fmt"
5
)
6
7
8
type S struct {
9
    a float64
10
    b string
11
}
12
13
func main() {
14
    fmt.Println(*new(bool))
15
    fmt.Println(*new(int))
16
    fmt.Println(*new([]string))
17
    fmt.Println(*new(map[int]string))
18
    x := *new([]string)
19
    if x == nil {
20
        fmt.Println("Uninitialized slices are nil")
21
    }
22
23
    y := *new(map[int]string)
24
    if y == nil {
25
        fmt.Println("Uninitialized maps are nil too")
26
    }
27
    fmt.Println(*new(S))
28
}

Was ist mit Vorlagen oder Generika?

Go hat keine. Dies ist wahrscheinlich die häufigste Beschwerde über das Typsystem von Go. Die Go-Designer sind offen für die Idee, wissen aber noch nicht, wie sie umgesetzt werden sollen, ohne die anderen der Sprache zugrunde liegenden Gestaltungsprinzipien zu verletzen. Was können Sie tun, wenn Sie einige generische Datentypen dringend benötigen? Hier einige Vorschläge:

  • Wenn Sie nur wenige Instanziierungen haben, sollten Sie nur konkrete Objekte erstellen.
  • Verwenden Sie eine leere Schnittstelle (Sie müssen irgendwann Assert zurück zu Ihrem konkreten Typ eingeben).
  • Verwenden Sie die Codegenerierung.

Schlussfolgerung

Go hat ein interessantes Typensystem. Die Go-Designer haben explizite Entscheidungen getroffen, um auf der einfachen Seite des Spektrums zu bleiben. Wenn Sie es mit der Go-Programmierung ernst meinen, sollten Sie Zeit investieren und sich über das Typsystem und die Besonderheiten informieren. Es wird Ihre Zeit wert sein.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.