A Go programozási nyelv

Típusok

Alaptípusok

A Go-ban lévő beépített alaptípusok:

Logikai típus

A logikai típushoz (bool) két előre definiált érték tartozik: a true és a false.

Numerikus típusok

A nyelv kétféle numerikus típus különböztet meg:

Az architektúrafüggetlen típusok mérete minden esetben pontosan előre definiált. Ilyenek az uint8, uint16, uint32, uint64 előjel nélküli egész típusok, az int8, int16, int32, int64 előjeles egész típusok, a float32, float64 lebegőpontos típusok (ezek az IEEE-754 szabványnak megfelelően ábrázoltak), valamint a complex64 és complex128 komplex típusok. A komplex számok valós, illetve képzetes része a komplex típus mérete felének megfelelő méretű két float értékként tárolódnak.

Architektúrafüggő (kényelmi) típusok az uint, int (32 vagy 64 bites egészek) és az uintptr. Az uintptr olyan előjel nélküli egész szám, melybe az adott architektúrán elfér egy tetszőleges nyers memóriacím.

Kényelmi okokból a byte név a uint8, míg a rune az int32 szinonimája. E két szinonimát leszámítva minden, fent felsorolt típus különböző, közöttük minden esetben konverzió szükséges (pl. uint és uint32 még akkor sem identikus típusok, ha az adott architektúrán azonos a méretük).

String

A (string) típus bájtok immutable tömbje (szelete). Azaz egy string értéke nem változtatható meg (pl. egy karaktert nem cserélhetünk benne egy másikra). Ha egy string típusú változónak megváltoztatjuk az értékét, akkor valójában egy új objektum jön létre (és a régit a szemétgyűjtő esetleg felszabadítja).

Összetett típusok

Saját típusokat a type kulcsszó segítségével definiálhatunk Go-ban. Ennek a formája:

type név típus

A típus lehet valamelyik alaptípusa a nyelvnek, illetve egy a lehetséges összetett típusok közül. Ha egy beépített típussal definiálunk egy új típust, akkor is egy új típus jön létre, nem lesznek egymásnak értékül adhatók az értékeik explicit típuskonverzió nélkül. A lehetséges összetett típusok:

Pl.:

type egész int // egy 'egész' nevű int típus type függvény func(int, int) string // egy 'függvény' nevű két int paraméterű, // string visszatérési értékű függvény típus.

Tömbök

A tömbök azonos típusú elemek véges szekvenciáját tárolják. A Go-ban a tömb értéktípus (paraméterátadáskor érték szerint adódik át) és 0-tól folytonosan indexelt. A tömb hossza lekérdezhető a len() beépített függvénnyel, meg nem változtatható. A tömb típus szintaxisa:

[ hossz ] típus

Ahol a hossz egy konstans kifejezés, melynek értéke nemnegatív egész szám, a típus pedig az elemek típusa. Ez tetszőleges típus lehet.

Az elemek a tömb létrehozásakor az alapértelmezett értékeikkel inicializálódnak. Többdimenziós tömbök nincsenek, de az egydimenziósak tetszőleges mélységben egymásba ágyazhatók. Mivel a tömb típus tartalmazza a tömb hosszát, a beágyazott tömbök csak egyforma hosszúságúak lehetnek, így csak 'téglalap' jellegű többdimenziós tömb hozható létre, szabálytalan alakú nem.

A tömbtípus megadása jobbasszociatív, azaz:

[2][2][2]float64 // azonos ezzel: [2]([2]([2]float64))

Példák:

type tömb [10]int // a 'tömb' egy tízhosszúságú int elemeket tartalmazó tömb típus var t [10]int // a 't' egy 10 hosszúságú int elemeket tartalmazó tömb var t2 tömb // a 't' egy 10 hosszúságú int elemeket tartalmazó tömb, (de nem adható értékül t-nek)

Szeletek

A szelet egy folytonos tömbrészletre vonatkozó referencia. Hossza a len() beépített függvénnyel lekérdezhető. Az inicializálatlan szelet értéke nil. Miután értéket kapott, mindig egy tömb tartozik hozzá, ennek az elemeinek egy részsorozatára hivatkozik a szelet. A szelet értékei 0-tól indexeltek. A szintaxisa:

[ ] típus

Ekkor a szelet típus az olyan típusú tömbökre hivatkozhat, amelyek elemtípusa típus típusú.
Pl.:

[] int // int típusú elemeket tartalmazó tömbre hivatkozó szelet

A szelet rendelkezik úgynevezett kapacitással: ez a hivatkozott tömbnek a szelet vége után még fennmaradó része, hozzáadva a szelet hosszához. A szelet eddig a hosszig (az ún. slicing segítségével) kibővíthető. A kapacitást a cap() beépített függvénnyel kérdezhetjük le.

A szeletek a tömbökhöz hasonlóan egydimenziósak, de azokkal ellentétben különböző méretű szeletek is egymásba ágyazhatók (ilyenkor minden belső szeletet önállóan allokálni kell, ld. allokáció make()-kel).

Struct

A struct kulcsszóval rekord típusok definiálhatók. Ezeknek bármennyi tetszőleges típusú mezője lehet. Az összes adattag elérhető kívülről a csomagon belül, csomagon kívül csak azok, amelyek neve nagybetűvel kezdődik. A mezőneveknek egy struktúrán belül egyedinek kell lenniük.

struct { mező1 mező2 ... }

A mezők között lehetnek üres nevű mezők (a _ azonosítóval), ezeket helykitöltésre (padding) használhatjuk, mivel semmilyen módon nem hivatkozhatók.

A mezők között lehetnek beágyazott (vagy névtelen) mezők is. Adott típusú beágyazott mezőből legfeljebb egy adható meg. Beágyazni csak egyszerű típust, vagy nem interface-re, ill. pointerre mutató pointer típust lehet. Ezekre az alaptípusuk nevével hivatkozhatunk (pl. egy P rekordba ágyazott *T típusú mezőre P.T formában).

A go nyelvben a metódusokkal ellátott struct típus hasonlít leginkább az objektumorientált programozási nyelvekben megszokott osztály fogalmára. A nyelv nem támogatja az öröklődést, viszont kód-újrafelhasználásra ad lehetőséget a beágyazás segítségével. Ha egy A struct típusba beágyazunk egy B típust, az A a B minden metódusával rendelkezni fog. Ez gyakorlatilag úgy működik, mintha az A egyik mezője B típusú lenne, és ennek továbbítanánk a megfelelő metódushívásokat. Ezt azonban nem kell a programozónak megírnia, a fordító gondoskodik róla.
Pl. az alábbi esetben a ReaderWriter típusú struktúrák minden olyan metódussal rendelkeznek, amivel a Reader vagy Writer típusúak:

type ReaderWriter struct { *Reader *Writer }

Minden rekordmező deklarációja mellé opcionálisan megadható egy string konstans (tag). Ezek az annotációk Unsafe csomag reflection interface-én keresztül érhetőek el. Nyelvi jelentésük nincs.

Pointer

Pointer típusokat tetszőleges típushoz definiálhatunk a * operátorral. Inicializálatlan pointer értéke nil. Pointeraritmetika nincs a nyelvben.

* típus

Pl.:

* string // stringre mutató pointer * []int // egészek szeletére mutató pointer

Függvény típus

Függvény típussal azonos paraméter és visszatérési típusú függvényeket foghatunk össze a func kulcsszóval.

func (paraméterlista) (visszatérési értékek)

A paraméterlista név-típus párok vesszővel tagolt sorozata. A nevek opcionálisan elhagyhatók, de ekkor az összes nevet el kell hagyni a paraméterlistából. Nem használt paraméterek jelzésére az üres azonosító (_) szolgálhat.
A visszatérési értéket nem kötelező zárójelbe tenni, ha csak egy (nem nevesített) visszatérési érték van. Ha a visszatérési értéket megnevezzük, vagy a függvénynek több visszatérési értéke van, akkor a zárójel kötelező. A visszatérési értékek is rendelkezhetnek névvel, de itt is igaz a fenti szabály: vagy az összes nevet elhagyjuk, vagy mindet meg kell adnunk.

A paraméterlistában az utolsó elem neve és típusa közé tett ... azt jelenti, hogy a függvénynek nulla vagy több plusz paramétere is van. Ha params ...T az utolsó elem, ahol T egy típus, akkor a plusz paramétereknek olyan típusúnak kell lenniük, amik értékadás kompatibilisek T-vel.
Pl.:

func (x, y int) (int, string) func(bool, ... int) bool // egy bool és tetszőleges számú int paraméterű függvény típus

Interface típus

Mivel a Go-ban nincs öröklődés, és nincsenek igazi osztályok sem, az objektumorientált programozást támogató egyetlen nyelvi elem az interface. Ennek segítségével a hasonló funkcionalitást megvalósító típusokat tudjuk összefogni.

interface { függvénydeklaráció1 függvénydeklaráció2 ... }

Az interfész függvényeit metódusnak nevezzük. A metódusok nevei az interfészen belül egyediek.

Azt, hogy egy típus megvalósít egy interface-t, nem kell explicit módon jelölni. Akkor valósítja meg, ha megvalósítja az összes – az interface által megkövetelt – metódust a megfelelő szignatúrával. Egy típus tetszőlegesen sok interface-t megvalósíthat. Az üres interface-t (interface {}) minden típus megvalósítja.
Pl.:

type Egész int func (Egész) Avg(x, y Egész) Egész { return (x + y) / 2 } type InterFace interface { Avg(Egész, Egész) Egész } // Az 'Egész' típus megvalósítja az 'InterFace'-t, mert megvalósítja az Avg metódust.

Az interfészek is támogatják a beágyazást. Az interfész definíció egyik sorába a beágyazni kívánt interfész nevét írva az interfész örökli a beágyazott interfész összes metódusát. Például a Reader és Writer interfészekből létrehozható a ReadWriter interfész:

type ReadWriter interface { Reader Writer }

Ekkor az ReadWriter a másik két interfész uniója lesz.

Sem interfészek, sem rekordok nem ágyazhatók egymásba rekurzívan.

Map

A map típussal asszociatív tömböket tudunk definiálni, amik kulcs-érték párokat tartalmaznak.

map [ kulcs típus ] elem típus

A kulcs típusa nem lehet függvény, map vagy szelet (mivel ezekre az == és != operátorok nincsenek definiálva). Amennyiben a kulcs interfész típusú, és a kulcsértékek dinamikus típusa nem valósítja meg a fenti operátorokat, futásidejű hibát kapunk.

Az érték tetszőleges típusú lehet.

A map referencia típus, ezért kezdőértéke nil. Az elemek száma a len() függvénnyel kérdezhető le.
Pl.:

type asszoc_tömb [string] int // string kulcsokkal int elemeket tartalmazó map típus

Egy maphez új elemeket még nem létező kulccsal való indexeléssel adhatunk hozzá, ekkor a map mérete nő. Az elemek lekérdezése szintén indexeléssel történik. Elemek törölhetők a delete() beépített függvény segítségével.

Csatorna

A Go a párhuzamosságot típusos adatcsatornákon történő üzenetküldésekkel támogatja.

chan - kétirányú csatorna <- chan - fogadó csatorna chan <- - küldő csatorna

Az elemek típusa tetszőleges. A csatornák referencia típusok, az inicializálatlan csatorna értéke nil. Új Csatornát a make() függvénnyel hozhatunk létre:

make(típus, kapacitás)

A típus egy csatorna típus kell, hogy legyen, a kapacitás a puffer mérete az elemek számában mérve, ez elhagyható, ekkor 0. Ha a puffer méret nem 0, és még nincs tele, akkor a küldés-fogadás aszinkron, különben blokkol. A kapacitás a cap() függvénnyel kérdezhető le.

Csatornákat le lehet zárni e a close() beépített függvénnyel. Zárt csatorna nem írható, de amíg a pufferében van adat, addig olvasható.

Allokáció

A Go-ban speciális beépített konstrukciókkal allokálhatunk dinamikus tárterületet: a new() és a make() beépített függvényekkel.

Allokáció new()-val

A new() beépített függvény ugyanazt csinálja, mint a névrokonai más nyelvekből: new(T) egy T típusú érték számára allokál helyet a memóriában és visszaadja annak címét, ami *T típusú. A Go terminológiában egy mutatót ad vissza, ami egy újonnan allokált, alapértékkel inicializált, T típusú értékre mutat.

Mivel a memória, amit a new() ad vissza, alapértékkel inicializált, ezért a mögöttes objektum további inicializáció nélkül használható. Ez azt jelenti, hogy felhasználó az adatstruktúrát létrehozza new()-al és rögtön használhatja. Például, a bytes.Buffer dokumentációja azt mondja ki, hogy a Buffer nullértéke egy üres buffer ami használatra készen áll. Hasonlóan a sync.Mutex sem rendelkezik explicit konstruktorral vagy Init metódussal. Helyette, a sync.Mutex nullértéke unlocked mutex-ként van definiálva.

Allokáció make()-el

A make(T, args) konstrukcióval különböző típusú értékek hozhatók létre: slice, map vagy channel. Visszatérési értéke egy alapértékre inicializált T típusú érték (nem *T). Például:

make([]int, 10, 100)

allokál egy 100 int-ből álló tömböt, aztán létrehoz egy szeletet 10 hosszal, (és 100 kapacitással), az első 10 elemre mutatván. Ellenkezőleg a new([]int)-nél amely egy újonnan allokált nullázott szeletet ad vissza, amely egy nil szelet értékre mutató pointer.

Make hívása T típusa Az eredmény make(T, n) szelet T alaptípusú szelet típus n mérettel és kapacitással make(T, n, m) szelet T alaptípusú szelet típus n mérettel és m kapacitással make(T) map T alaptípusú map make(T, n) map T alaptípusú map, előre lefoglalt hellyel n darab érték számára make(T) csatorna T típusú, 0-pufferméretű (szinkron) csatorna make(T, n) csatorna T típusú, n pufferméretű (aszinkron) csatorna

A következő példák illusztrálják a különbséget new() és make() között:

var p *[]int = new([]int) // allokalja a slice sztrukturat; *p == nil; ritkan hasznos var v []int = make([]int, 100) // a v slice most a egy új 100 int-es tömbre van állítva // Szükségtelenül bonyolult var p *[]int = new([]int) *p = make([]int, 100, 100) // Legegyszerűbb forma: v := make([]int, 100)

Emlékezzünk tehát arra, hogy a make() csak a map, slice és channel típusokra használatos és nem ad vissza pointert. Annak érdekében, hogy explicit pointert nyerjünk, allokáljunk new()-val.