Duik diep in het Go Type-systeem

Go heeft een heel interessant type systeem. Het vermijdt klassen en overerving ten gunste van interfaces en compositie, maar aan de andere kant heeft het geen sjablonen of generieken. De manier waarop het omgaat met collecties is ook uniek. 

In deze tutorial leer je over de ins en outs van het Go-type-systeem en hoe je het effectief kunt gebruiken voor het schrijven van duidelijke en idiomatische Go-code.

De grote afbeelding van het Go Type-systeem

Het Go-type systeem ondersteunt de procedurele, object-georiënteerde en functionele paradigma's. Het heeft een zeer beperkte ondersteuning voor generieke programmering. Terwijl Go een beslist statische taal is, biedt het voldoende flexibiliteit voor dynamische technieken via interfaces, eersteklasfuncties en reflectie. Go's type systeem mist mogelijkheden die in de meeste moderne talen gebruikelijk zijn:

  • Er is geen uitzonderingstype omdat Go's foutafhandeling is gebaseerd op retourcodes en de foutinterface. 
  • Er is geen overbelasting door de operator.
  • Er is geen functieoverbelasting (dezelfde functienaam met verschillende parameters).
  • Er zijn geen optionele of standaard functieparameters.

Die omissies zijn allemaal in ontwerp om Go zo eenvoudig mogelijk te maken.

Typ Aliassen

U kunt alias typen in Go en verschillende typen maken. U kunt geen waarde van het onderliggende type toewijzen aan een gealiast type zonder conversie. Bijvoorbeeld de opdracht var b int = a in het volgende programma veroorzaakt een compilatiefout omdat het type Leeftijd is een alias van int, maar dat is het wel niet een int:

pakket hoofdtype Age int func main () var a Leeftijd = 5 var b int = a Uitvoer: tmp / sandbox547268737 / main.go: 8: kan een (type Age) niet gebruiken als type int in toewijzing 

U kunt groepstypeaangiften groeperen of één aangifte per regel gebruiken:

type IntIntMap map [int] int StringSlice [] string type (Grootte uint64 Tekst string CoolFunc func (a int, b bool) (int, fout)) 

Basistypen

Alle gebruikelijke verdachten zijn hier: bool, string, gehele getallen en niet-ondertekende gehele getallen met expliciete bitgroottes, drijvende-kommagetallen (32-bits en 64-bits) en complexe getallen (64-bits en 128-bits).

bool string int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr byte // alias for uint8 rune // alias for int32, staat voor een Unicode-codepunt float32 float64 complex64 complex128 

strings

Strings in Go zijn UTF8-gecodeerd en kunnen dus elk Unicode-teken vertegenwoordigen. Het strings-pakket biedt een reeks touwbewerkingen. Hier is een voorbeeld van het nemen van een reeks woorden, ze omzetten in een goede naam en ze bij een zin voegen.

pakket main import ("fmt" "strings") func main () words: = [] string "i", "LikE", "the colours:", "RED", "bLuE,", "AnD" , "GrEEn" properCase: = [] string  voor i, w: = bereikwoorden if i == 0 properCase = append (properCase, strings.Title (w)) else properCase = append (properCase, strings.ToLower (w)) zin: = strings.Join (properCase, "") + "." fmt.Println (zin)

pointers

Go heeft aanwijzingen. De nul-aanwijzer (zie nulwaarden later) is nul. U kunt een aanwijzer naar een waarde krijgen met behulp van de & operator en ga terug gebruik maken van de * operator. Je kunt ook verwijzingen naar pointers krijgen.

pakket main import ("fmt") type S struct a float64 b string func main () x: = 5 px: = & x * px = 6 fmt.Println (x) ppx: = & px ** ppx = 7 fmt .Println (x) 

Object georiënteerd programmeren 

Go ondersteunt objectgeoriënteerd programmeren via interfaces en structs. Er zijn geen klassen en geen klassenhiërarchie, hoewel u anonieme structs in structs kunt insluiten, die een soort van enkele overerving bieden. 

Voor een gedetailleerde verkenning van objectgeoriënteerd programmeren in Go, kijk op Let's Go: Object-Oriented Programming in Golang.

interfaces

Interfaces vormen de hoeksteen van het Go-type systeem. Een interface is slechts een verzameling handtekeningen van methoden. Elk type dat alle methoden implementeert, is compatibel met de interface. Hier is een snel voorbeeld. De Vorm interface definieert twee methoden: GetPerimeter () en GetArea (). De Plein object implementeert de interface.

type Vorm-interface GetPerimeter () uint GetArea () uint type Square struct side uint func (s * Square) GetPerimeter () uint return s.side * 4 func (s * Square) GetArea () uint ga terug s.side * s.side 

De lege interface interface is compatibel met elk type omdat er geen methoden vereist zijn. De lege interface kan dan naar elk object wijzen (vergelijkbaar met de object-of-sterlocatie van Java of C / C ++) en wordt vaak gebruikt voor dynamisch typen. Interfaces zijn altijd verwijzingen en wijzen altijd naar een concreet object.

Zie voor een volledig artikel over Go-interfaces: Hoe een Go-interface moet worden gedefinieerd en geïmplementeerd.

structs

Structuren zijn Go's door de gebruiker gedefinieerde typen. Een struct bevat benoemde velden, die basistypen, aanwijzertypen of andere struct types kunnen zijn. U kunt structs ook anoniem in andere structs insluiten als een vorm van implementatie-overname. 

In het volgende voorbeeld zijn de S1- en S2-structs ingebed in de S3 struct, die ook een eigen structuur heeft int veld en een verwijzing naar zijn eigen type:

pakket main import ("fmt") type S1 struct f1 int type S2 struct f2 int type S3 struct S1 S2 f3 int f4 * S3 func main () s: = & S3 S1 5, S2 6, 7, nihil fmt.Println (s) Uitvoer: & 5 6 7  

Type Assertions

Type-aantekeningen laten je een interface converteren naar zijn concrete type. Als je het onderliggende type al kent, kun je het gewoon beweren. Als je het niet zeker weet, kun je verschillende typen beweringen proberen totdat je het juiste type ontdekt. 

In het volgende voorbeeld is er een lijst met items met tekenreeksen en niet-tekenreekswaarden die worden weergegeven als een plak lege interfaces. De code itereert over alle dingen, probeert elk item naar een tekenreeks te converteren en slaat alle tekenreeksen op in een apart segment dat uiteindelijk wordt afgedrukt.

pakket main import "fmt" func main () things: = [] interface  "hi", 5, 3.8, "there", nil, "!" strings: = [] string  for _, t : = range things s, ok: = t. (string) if ok strings = append (strings, s) fmt.Println (strings) Output: [hallo daar!] 

Reflectie

De Go reflecteren pakket laat je direct het type interface controleren zonder type-uitspraken. Je kunt ook de waarde van een interface extraheren en deze naar een interface converteren als je dat wilt (niet zo nuttig). 

Hier is een vergelijkbaar voorbeeld als in het vorige voorbeeld, maar in plaats van de tekenreeksen af ​​te drukken, telt het ze gewoon, dus er is geen noodzaak om te converteren van interface naar draad. De sleutel is bellen reflect.Type () om een ​​type object te krijgen, dat een Soort() methode waarmee we kunnen detecteren of we te maken hebben met een string of niet.

pakket main import ("fmt" "reflect") func main () things: = [] interface  "hi", 5, 3.8, "there", nil, "!" stringCount: = 0 voor _, t: = bereik dingen tt: = reflect.TypeOf (t) if tt! = nil && tt.Kind () == reflect.String stringCount ++ fmt.Println ("String count:", stringCount) 

functies

Functies zijn eersteklasburgers in Go. Dat betekent dat u functies kunt toewijzen aan variabelen, functies kunt doorgeven als argumenten voor andere functies of ze als resultaten kunt retourneren. Dat stelt u in staat om de functionele programmeerstijl met Go te gebruiken. 

Het volgende voorbeeld demonstreert een aantal functies, GetUnaryOp () en GetBinaryOp (), die anonieme functies retourneren die willekeurig zijn geselecteerd. Het hoofdprogramma bepaalt of het een unaire bewerking of een binaire bewerking nodig heeft op basis van het aantal argumenten. Het slaat de geselecteerde functie op in een lokale variabele met de naam "op" en roept deze vervolgens aan met het juiste aantal argumenten. 

pakket main import ("fmt" "math / rand") type UnaryOp func (a int) int type BinaryOp func (a, b int) int func GetBinaryOp () BinaryOp if rand.Intn (2) == 0 return func (a, b int) int return a + b else return func (a, b int) int return a - b func GetUnaryOp () UnaryOp if rand.Intn (2) == 0  return func (a int) int return -a else return func (a int) int return a * a func main () arguments: = [] [] int 4,5, 6, 9, 7,18, 33 var result int for _, a: = range arguments if len (a) == 1 op: = GetUnaryOp () result = op (a [0]) else op: = GetBinaryOp () result = op (a [0], a [1]) fmt.Println (result) 

kanalen

Kanalen zijn een ongewoon gegevenstype. U kunt ze beschouwen als berichtenwachtrijen die worden gebruikt voor het verzenden van berichten tussen goroutines. Kanalen zijn sterk getypt. Ze zijn gesynchroniseerd en hebben speciale syntax-ondersteuning voor het verzenden en ontvangen van berichten. Elk kanaal kan alleen-ontvangen, alleen-zenden of bidirectioneel zijn. 

Kanalen kunnen ook optioneel worden gebufferd. U kunt de berichten in een kanaal itereren met behulp van bereik en routines kunnen op meerdere kanalen tegelijkertijd worden geblokkeerd met behulp van de veelzijdige selectiebewerking. 

Hier is een typisch voorbeeld waarbij de som van vierkanten van een lijst van gehele getallen parallel wordt berekend door twee go-routines, die elk verantwoordelijk zijn voor de helft van de lijst. De hoofdfunctie wacht op resultaten van beide routines en telt vervolgens de deelsommen op voor het totaal. Merk op hoe het kanaal c is gemaakt met behulp van de maken() ingebouwde functie en hoe de code leest en schrijft naar het kanaal via de special <- operator.

pakket main import "fmt" func sum_of_squares (s [] int, c chan int) sum: = 0 for _, v: = range s sum + = v * v c <- sum // send sum to c  func main()  s := []int11, 32, 81, -9, -14 c := make(chan int) go sum_of_squares(s[:len(s)/2], c) go sum_of_squares(s[len(s)/2:], c) sum1, sum2 := <-c, <-c // receive from c total := sum1 + sum2 fmt.Println(sum1, sum2, total)  

Dit schraapt alleen het oppervlak. Raadpleeg voor meer informatie over kanalen:

collecties

Go heeft verschillende ingebouwde generieke collecties die elk type kunnen opslaan. Deze collecties zijn speciaal en u kunt uw eigen generieke collecties niet definiëren. De collecties zijn arrays, slices en kaarten. Kanalen zijn ook generiek en kunnen ook als verzamelingen worden beschouwd, maar ze hebben een aantal vrij unieke eigenschappen, dus ik geef er de voorkeur aan ze afzonderlijk te bespreken.

arrays

Arrays zijn verzamelingen van vaste formaten van elementen van hetzelfde type. Hier zijn enkele arrays:

pakket main import "fmt" func main () a1: = [3] int 1, 2, 3 var a2 [3] int a2 = a1 fmt.Println (a1) fmt.Println (a2) a1 [1] = 7 fmt.Println (a1) fmt.Println (a2) a3: = [2] interface  3, "hallo" fmt.Println (a3) 

De grootte van de array maakt deel uit van dit type. U kunt arrays van hetzelfde type en dezelfde grootte kopiëren. De kopie is op waarde. Als u items van een ander type wilt opslaan, kunt u het escape-arcering van een reeks lege interfaces gebruiken.

Slices

Arrays zijn vrij beperkt vanwege hun vaste grootte. Segmenten zijn veel interessanter. Je kunt segmenten zien als dynamische arrays. Onder de omslagen gebruiken segmenten een array om hun elementen op te slaan. U kunt de lengte van een segment controleren, elementen en andere segmenten toevoegen en het leukste van alles is dat u subsegmenten kunt extraheren die vergelijkbaar zijn met Python-slicing:

pakket main import "fmt" func main () s1: = [] int 1, 2, 3 var s2 [] int s2 = s1 fmt.Println (s1) fmt.Println (s2) // Wijzig s1 s1 [ 1] = 7 // Zowel s1 als s2 wijzen naar dezelfde onderliggende array fmt.Println (s1) fmt.Println (s2) fmt.Println (len (s1)) // Slice s1 s3: = s1 [1: len ( s1)] fmt.Println (s3) 

Wanneer u segmenten kopieert, kopieert u de verwijzing naar dezelfde onderliggende array. Wanneer u snijdt, wijst de subschijf nog steeds naar dezelfde array. Maar wanneer u toevoegt, krijgt u een segment dat naar een nieuwe array wijst.

U kunt reeksen of segmenten herhalen met een gewone lus met indexen of met reeksen. U kunt ook segmenten in een gegeven capaciteit maken die met de nulwaarde van hun gegevenstype worden geïnitialiseerd met behulp van de maken() functie:

pakket main import "fmt" func main () // Maak een plak van 5 booleans geïnitialiseerd naar false s1: = make ([] bool, 5) fmt.Println (s1) s1 [3] = true s1 [4] = true fmt.Println ("Itereren met standaard voor lus met index") voor i: = 0; ik < len(s1); i++  fmt.Println(i, s1[i])  fmt.Println("Iterate using range") for i, x := range(s1)  fmt.Println(i, x)   Output: [false false false false false] Iterate using standard for loop with index 0 false 1 false 2 false 3 true 4 true Iterate using range 0 false 1 false 2 false 3 true 4 true 

Kaarten

Kaarten zijn verzamelingen sleutel / waarde-paren. U kunt ze kaartliteratuur of andere kaarten toewijzen. U kunt ook lege kaarten maken met behulp van de maken ingebouwde functie. U opent elementen met vierkante haken. Kaarten ondersteunen iteratie met reeks, en u kunt testen of een sleutel bestaat door deze te proberen te openen en de tweede optionele Booleaanse retourwaarde te controleren.

pakket main import ("fmt") func main () // Maak een kaart met een letterlijke kaart m: = kaart [int] string 1: "one", 2: "two", 3: "three" // Toewijzen aan item met sleutel m [5] = "vijf" // Toegang verkrijgen met sleutel fmt.Println (m [2]) v, ok: = m [4] if ok fmt.Println (v) else fmt .Println ("Ontbrekende toets: 4") voor k, v: = bereik m fmt.Println (k, ":", v) Uitgang: twee Ontbrekende toets: 4 5: vijf 1: één 2: twee 3: drie 

Merk op dat iteratie niet in creatie of invoegvolgorde is.

Nulwaarden

Er zijn geen niet-geïnitialiseerde typen in Go. Elk type heeft een vooraf gedefinieerde nulwaarde. Als een variabele van een type wordt gedeclareerd zonder hieraan een waarde toe te wijzen, dan bevat deze de nulwaarde ervan. Dit is een belangrijke veiligheidsfunctie van het type.

Voor elk type T, * Nieuw (T)retourneert een nulwaarde van T.

Voor boolean-typen is de nulwaarde "false". Voor numerieke typen is de nulwaarde ... nul. Voor plakjes, kaarten en aanwijzers is het nihil. Voor structs is het een struct waarbij alle velden worden geïnitialiseerd naar hun nulwaarde.

pakket main import ("fmt") type S struct a float64 b string func main () fmt.Println (* nieuw (bool)) fmt.Println (* new (int)) fmt.Println (* new ([ ] tekenreeks)) fmt.Println (* nieuw (kaart [int] tekenreeks)) x: = * nieuw ([] tekenreeks) als x == nil fmt.Println ("Niet-geïnitialiseerde segmenten zijn nul") y: = * nieuw (kaart [int] string) als y == nil fmt.Println ("Niet-geïnitialiseerde kaarten zijn ook nul") fmt.Println (* nieuw (S)) 

Hoe zit het met sjablonen of generieken?

Go heeft er geen. Dit is waarschijnlijk de meest voorkomende klacht over Go's type systeem. De Go-ontwerpers staan ​​open voor het idee, maar weten nog niet hoe ze het moeten implementeren zonder de andere ontwerpprincipes die aan de taal ten grondslag liggen te schenden. Wat kunt u doen als u bepaalde generieke gegevenstypen hard nodig hebt? Hier zijn een paar suggesties:

  • Als je maar een paar instantiaties hebt, overweeg dan om gewoon concrete objecten te maken.
  • Gebruik een lege interface (je moet op een bepaald moment een bevestiging van je betontype typen).
  • Gebruik codegeneratie.

Conclusie

Go heeft een interessant type systeem. De ontwerpers van Go hebben expliciete beslissingen genomen om aan de eenvoudige kant van het spectrum te blijven. Als je serieus wilt gaan programmeren, moet je de tijd investeren en meer te weten komen over het type systeem en de eigenaardigheden. Het zal de tijd zeker waard zijn.