A Go programozási nyelv

Kifejezések

A kifejezések egy elvégzendő számítást határoznak meg. A C++-hoz hasonlóan a Go-ban is nagy szerep jut a kifejezéseknek. A C++-szal ellentétben azonban itt nem követték a "minden legyen kifejezés" elvet, néhány olyan megszorítással éltek, mely a nyelvet biztonságosabbá teszi.

Összetett literálok

Az összetett literálok építenek értékeket rekordoknak, tömböknek, szeleteknek, és mapeknek. Minden egyes kiértékeléskor egy teljesen új értéket hoznak létre.

Például vegyük az alábbi egyszerű pont típust:

type Point struct { x, y, z float }

Ennek többféle módon adhatunk értéket: az értékeke megfelelő sorrendű felsorolásával, és kulcs-érték párokkal is.

x := Point{4, 5} // Az egyes elemeket sorban tölti fel // Ahová nem jut érték, 0 lesz y := Point{} // Az előzőek alapján ez az origó z := Point{x: 3, z: 6} // Lehetőség van névvel is hivatkozni hibas := Point{3, z:6} // Keverni viszont nem lehet, ez fordítási hiba

Tömböket és szeleteket is hasonló módon inicializálhatunk. Kulcsos megadásnál a kulcsoknak mindenképpen számoknak kell lenniük. Itt már lehet keverni a kulcsos és a kulcs nélküli megadást, a kulcs nélkül megadott elemek mindig az előzőnél eggyel nagyobb indexűek lesznek. Ha egy tömbnél nem akarjuk megadni az elemszámot, akkor ...-tal helyettesíthetjük. Például az alábbi három megadás ekvivalens:

a1 := [20]int{0, 2, 3, 0, 5, 0, 7, 0, 0, 0, 11, 0, 13, 0, 0, 0, 17, 0, 19} a2 := [...]int{0, 2, 3, 0, 5, 0, 7, 0, 0, 0, 11, 0, 13, 0, 0, 0, 17, 0, 19, 0} a3 := [20]int{1:2, 3, 4:5, 6:7, 8:9, 10:11, 12:13, 16:17, 18:19}

Ha a méret helyére nem írunk semmit, szeletet kapunk:

s := []int{0, 2, 3, 0, 5, 0, 7, 0, 0, 0, 11, 0, 13, 0, 0, 0, 17, 0, 19, 0}

Mapet a tömb indexes változatához hasonló formában készíthetünk, de itt feltétlenül ki kell írnunk minden kulcsértéket:

m := map[string]int{ "first": 1, "second": 1, "third": 2, "fourth": 3, "fifth": 5 }

Ha egy összetett literálon belül további összetett literálok vannak, és ezek típusa megegyezik a külső literál elemtípusával, a belső literálok típusát nem kell kiírni. Például a következő két kifejezés ugyan azt a két dimenziós szeletet adja.

c := [][]int{[]int{1, 2, 3}, []int{4, 5}} c := [][]int{{1, 2, 3}, {4, 5}}

Függvénykifejezések

A func szóval nem csak a fájl fő részében, valamint a típusdefinícióknál találkozhatunk, hanem kifejezésként is használható. Ekkor segítségével egy névtelen függvényt készíthetünk.

func(a, b int, z float) bool { return a*b < int(z) } f := func(x, y int) int { return x + y }

Ezek a függvénykifejezések zárványok (closure): hozzáférhetnek a bezáró függvény változóihoz, azok megosztásra kerülnek közöttük. Az ily módon több helyről elérhető változók addig élnek, amíg valahonnan elérhetők.

Alapkifejezések

Alapkifejezésekből építhetünk operátorokkal bonyolultabb kifejezéseket. Példák alapkifejezésekre:

2 // alaptípusú literál a // azonosító b[x+7] // indexelés t[5:10] // "szeletelés" Point{1, 2} // összetett literál pair.First // szelektor kifejezés int(5.0) // típuskonverzió x.(int) // típuskényszerítés f(1, 2, 3, 4, 5) // függvényhívás tetszőleges kifejezés zárójelek közt

Szelektorok

A szelektort a . jelzi, és egy struct egy elemét választja ki, vagy egy (a változó típusán értelmezett) metódust hív meg. A pointerek egy szintet automatikusan feloldódnak a szelektor kifejezés hatására.

type Point struct { x, y int } func (foo *Point) M() var p (*Point) Ekkor: p.y == (*p).y // illetve p.M() // meghívja az M() metódust.

Indexelés

Tömbökből, szeletekből, stringekből és map-ekből is indexeléssel választhatjuk ki az i. elemét.

a[i]

A tömbök és a szeletek is tudják a határaikat, és a kiválasztás előtt mindig történik határellenőrzés.

Szeletelés

Szeleteket képezhetünk tömbökből, stringekből és más szeletekből. Ezt az alábbi formában tehetjük meg:

a[lo : hi]

A fenti kifejezés a tömbből/szeletből/stringből választja ki a lo-tól hi indexig található elemeket. Az eredményül kapott szelet indexelése 0-tól kezdődik, és hi - lo hosszan tart. Azaz az első kiválasztott elem a lo pozíción lévő, az utolsó pedig a hi - 1 pozíción lévő.

A szeletelés egyik, vagy akár mindkét paramétere elhagyható, ekkor ezek szélsőértékekként értelmeződnek: azaz ha lo-t hagyjuk el, az alsó határ 0 lesz, míg ha hi-t hagyjuk el, a felső határ len(a)-val fog megegyezni.

Stringek szeletelésekor stringet kapunk (tehát a kapott érték is immutable). Ha tömböt vagy szeletet szeletelünk, szeletet kapunk, mely az eredeti tömbbel közös memórián osztozik, a szelet elemeinek változtatása az eredeti tömb elemeinek változását is magával vonja és viszont.

Három indexű szelet kifejezések

Az 1.2 verzióban a szelet kifejezések szintaxisa egy harmadik index lehetőségével bővült, mellyel a kapacitást lehet befolyásolni. Egy t[i:j:k] szelet kifejezés jelentése a következő: Ennek a működését az alábbi példa szemlélteti:
var array [10]int println("cap(array):",cap(array)) println("cap(array[2:4]):",cap(array[2:4])) println("cap(array[2:4:7]):",cap(array[2:4:7])) // Output: // cap(array): 10 // cap(array[2:4]): 8 // cap(array[2:4:7]): 5

Típuskényszerítés

Lehetőségünk van arra, hogy a program egy adott pontján ellenőrizzük, hogy egy változó dinamikus típusa megfelelő-e számunkra. Ha a egy interface típusú érték, akkor:

a.(T)

kifejezés ellenőrzi, hogy a típusa jó-e. Ha T nem egy interfész, akkor pontos egyezést vizsgál T-vel, ha T egy interfész, akkor azt vizsgálja, hogy a dinamikus típusa megvalósítja-e azt az interfészt.

Ha a típusa valóban megfelel a fentieknek, akkor a kifejezés eredménye a, de immáron T típussal. Egyébként (pl. akkor is, ha a értéke nil) futásidejű hibát kapunk. Azaz habár a típusa nem ismert fordítási időben, de egy helyes programtól elvárjuk, hogy T típusú legyen.

Egy gyengébb megkötést enged az alábbi forma:

v, ok = x.(T)

Ekkor ok egy logikai változó lesz, mely tartalmazza, hogy az értékadás sikeres volt-e, v-nek értékül tudtuk-e adni x-et. Ha ez mégsem teljesül, akkor v értéke T alapértéke lesz.

Operátorok

Az operátorok az operandusaikat kifejezésekké kombinálják. Az operandusok (az összehasonlító operátorok esetének és a biteltolásnak a kivételével) azonos típusúaknak kell lenniük. Ha egy típusos kifejezésre és egy nemtípusos konstansra alkalmazunk operátort, akkor a konstans (ha ez lehetséges), a másik operandus típusára konvertálódik.

Az operátorok precedenciája az alábbi:

Az unáris operátorok precedenciája a legmagasabb. Ezek: * Pointer feloldása (pl. *p) & Objektumra mutató pointer képzése (pl. &x) + Pozitív előjel (egészekre; +x == 0+x) - Negáció (egészekre; -x == 0-x) ^ Bitenkénti komplemens (egészekre) <- Adat fogadása csatornáról A bináris operátorok: Precedenciaszint Operátor 5 * / % << >> & &^ 4 + - | ^ 3 == != < <= > >= 2 && 1 ||

Az azonos szinten levő operátorok balról jobbra értékelődnek ki. A ++ és a -- nem operátorok, hanem utasítások, így gyakorlatilag ezek kötnek a leggyengébben. Szintén utasítás a csatornára való adatküldés (->). A kiértékelés sorrendje zárójelezéssel a szokásos módon befolyásolható.

Aritmetikai operátorok

Az aritmetikai operátorok numerikus típusokon értelmezettek, eredményük az első operandus típusával megegyező típusú. A négy alapművelet (+, -, *, / minden numerikus típuson értelmezett; a + operátor stringek konkatenációjára is alkalmas. A többi aritmetikai operátor csak egészekre használható.

+ összeadás numerikus típusok, stringek - kivonás numerikus típusok * szorzás numerikus típusok / osztás numerikus típusok % maradék egészek & bitenkénti "és" egészek | bitenkénti "vagy" egészek ^ bitenkénti "kizáró vagy" egészek &^ bitenkénti "és nem" egészek << biteltolás balra egész << előjel nélküli egész >> biteltolás jobbra egész >> előjel nélküli egész

Egészek között az osztás (/) eredménye egész, a nulla felé csonkolva (kivételt képez, ha az osztandó x a típusában ábrázolható legkisebb negatív szám: ekkor x / -1 == x, ill. x % -1 == 0). Nullával való osztás futásidejű hibát okoz.

Az osztásnak a maradékképzés (%) operátorral az alábbi kapcsolata áll fenn:

q := x / y r := x % y ---------- x = q*y + r, illetve |r| < |y|

A biteltolás operátorok bal operandusuk bitjeit tolják el a jobb operandus által megadott mértékben. Ha a bal operandus előjeles egész, akkor aritmetikai eltolást végeznek (tehát az előjelet nem befolyásolják), míg előjel nélküli bal operandus esetén logikai eltolást hajtanak végre.

Az eltolás mértékének felső korlátja nincs: n-nel való eltolás azonos n-szeri 1-gyel való eltolással az adott irányban.

Egész értékek túlcsordulása

Előjel nélküli egészekre a +, -, *, << operátorok modulo 2^n értelmezettek (n az ábrázoláshoz használt bitek száma), azaz túl- vagy alulcsordulás esetén az értékek "körbejárnak" (wrap around).

Előjeles egészekre ugyanezen operátorok esetén a túlcsordulás megengedett, hiba nem keletkezik és az eredménynek determinisztikusnak kell lennie. További megkötés a fordítók számára, hogy nem optimalizálhatnak a túlcsordulás lehetőségét kizárva, pl. nem tehetik fel, hogy x < x + 1 mindig igaz lesz.

Összehasonlító operátorok

Az összehasonlító operátorok két értéket hasonlítanak össze, eredményük logikai érték. Az összehasonlítás során az első operandus értékül adható kell legyen a második operandusnak, vagy fordítva.

Az ==, != operátorok operandusainak összehasonlíthatónak kell lenniük. A összehasonlítható típusok:

Szeletek, mapek és függvények nem összehasonlíthatók, de mindegyik összehasonlítható a nil értékkel (pointerek, csatornák és interface-ek ugyancsak összehasonlíthatók a nil-lel).

A rendezés operátorok operandusaitól a nyelv megköveteli a rendezhetőséget. A rendezhető típusok:

Logikai operátorok

A logikai operátorok (&&, ||, !) logikai értékeken értelmezettek a szokásos módon. A jobb oldali operandus csak szükség esetén (lustán) értékelődik ki.

Pointer-operátorok

Két pointer-operátor használható a nyelvben:

&x // x címét adja vissza *p // felold egy pointert

Címet kérni címezhető objektumtól (változó, pointer dereferencia, szelet-indexelés kifejezés, rekord mezője, címezhető tömb indexelése), illetve összetett literáltól lehet. Utóbbi esetben a literál által meghatározott új objektum létrejön és a kifejezés értéke ezen objektumra mutató pointer lesz.

nil pointer feloldásának kísérlete futásidejű hibát okoz.

Az adatfogadó operátor

A fogadó operátor unáris, operandusa egy fogadásra (is) alkalmas csatorna, eredménye pedig a csatornáról fogadott érték. A kifejezés blokkol, amíg nincs a csatornáról elérhető érték (így nil csatornáról való fogadás örökre blokkol). Példák:

x := <-ch f(<-ch) <-ch // a kapott értéket megvárja, majd eldobja (használható pl. időzítésre)

A fogadó kifejezés speciális formája a következő: x, ok := <-ch. Ebben a formában az ok változó logikai típusú és azt adja meg, hogy a fogadott x érték a csatornáról érkezett-e (ekkor ok igaz), vagy alapérték (ha a csatorna zárva van és üres).

Függvényhívások

Függvényhívás a szokásos formában történik, a függvényt eredményező kifejezés (pl. függvénynév vagy literál) után zárójelben felsoroljuk a paramétereket:

f(p1, p2, p3)

Az f függvény aktuális paramétereinek értékül adhatónak kell lennie a formális paramétereinek. A függvény paraméterei a hívás előtt, pontosan egyszer értékelődnek ki. A visszatérési érték(ek) típusa a függvény visszatérési típusának (típusainak) megfelelő.

nil értékű függvénykifejezés hívása futásidejű hibát eredményez.

Ha egy g függvény visszatérési értékei (bár többen vannak) számban és típusban megegyeznek egy f függvény paramétereivel, akkor engedélyezett az f ( g ( /* paraméterek */ ) ) egyszerűsített függvényhívás.

A ... paraméterek

Speciális eset a ... paraméter használata: ha egy függvény utolsó paramétere params ...T alakú, azt a következő két formában használhatjuk:

func összead(számok ...int64) int64 { /* megvalósítás */ } f(1, 2, 3, 4) == 10 // megfelelő típusú értékek felsorolása var t [4]int64 = [4]int64{1, 2, 3, 4} f(t...) // tömb átadása esetén ...-ot kell utána írni

Metóduskifejezések

Ha T egy olyan típus, amely rendelkezik M metódussal

T.M(p1, p2, p3)

ezt a metódust hívja meg a megfelelő paraméterekkel. A paraméterekre és a visszatérési értékre a függvényekkel azonos szabályok vonatkoznak.

Metódusok deklarálásáról lásd a Függvény- és metódusdeklarációk szakaszt.

Metódus, mint érték

Mivel a nyelvben a függvény is egy beépített típus, ezért értékül adható függvény típusú változónak, az viszont érdekes lehet, hogy hogyan viselkedhet egy típushoz rendelt metódus ilyen kontextusban. Egy func (t *T) F(a int) int definíciójú függvény a következő függvényszignatúrának felel meg: func(t *T, a int) int és akár meg is hívható ilyen formában, a következő két hívás ekvivalens: t.F(1) és T.F(t,1) Az 1.1-es verzióval vezették be a nyelv készítői a metódus értéket, ezzel egy típus adott értékéhez lerögzített függvényt kaphatunk. A müködését ez a kódrészlet szemlélteti:
type Point struct { X float64 Y float64 } func (p *Point) Distance(p2 Point) float64 { return math.Sqrt(math.Pow(p2.X-p.X,2)+math.Pow(p2.Y-p.Y,2)) } const origo = Point{0, 0} var DistanceFromOrigo = origo.Distance
A DistanceFromOrigo változó értéke egy függvény, ami a Distance metódus egy specializált példánya, mely mindig az origótól való távolságot adja meg, szignatúrája sem tartalmazza már paraméterként a Point-ot, amin meghívtuk a metódust: func(p2 Point) float64.

Konverziók

A típuskonverziók Pascal-szerű formában történnek:

Típus(változó)

Ha a típus operátorral kezdődik, akkor zárójelbe kell tenni, pl.

(*int)(változó)

A T(x) konverzió az alábbi esetekben sikeres:

A számok közötti és a szám-string konverzióban felléphet futásidejű költség, illetve pontosságvesztés; a többi konverzió csak az adott változó típusát változtatja meg, reprezentációját nem.

Pointer és egész számok közötti konverzió nem engedélyezett.

Konverzió numerikus típusok között

A numerikus típusok közötti konverzióra az alábbi szabályok vonatkoznak:

String konverziók

Egész érték stringgé konvertálása esetén a string egyetlen karaktere az egész számnak megfelelő Unicode kódpont lesz - ha nincs megfelelő kódpont, akkor a \uFFFD érték.

Ha bájt-szeletet ([]byte) konvertálunk stringgé, a szelet bájtjainak megfelelő karakterek rendre a string bájtjai lesznek. nil szelet konverziója üres stringet eredményez.

[]rune típus konverziója során az egyes rune értékek string-alakjának konkatenációja az eredmény. Ha a szelet nil, az eredmény üres string.

Stringet []byte típusra konvertálva az eredmény a string egyes bájtjainak sorozata. Ha a string üres, az eredmény []byte(nil).

Stringet []rune-ra konvertálva a string unicode kódpontjainak szekvenciáját kapjuk eredményül (üres string esetén []rune(nil) értéket).

Konstans kifejezés

A konstans kifejezésekben csak konstans részkifejezéseket és operátorokat használhatunk, és még fordítási időben kiértékelődnek. A kiértékelés mindig pontos, nincs információveszteség még akkor sem, ha a részeredmények tárolásához az ábrázolható típusoknál jelentősen nagyobb tárterületre van szükség (pl. const Huge = 1 << 1000).

A konstans kifejezésekben a típuskonverziókat szabadabban kezeli a nyelv.

Nemtípusos konstans kifejezések

Nemtípusos konstansok szabadon használhatók a konstans kifejezésekben ott, ahol a megfelelő típusos kifejezések használhatók. Bináris operátort alkalmazva két, különböző fajta nemtípusos konstansra, a művelet jellege és az eredmény típusa a két fajta közül az alábbi listában később elhelyezkedő fajtájú lesz: egész, karakter, lebegőpontos, komplex. Például ha egy nemtípusos egész konstans osztunk egy nemtípusos komplex konstanssal, akkor az eredmény egy (nemtípusos) komplex konstans lesz.

Konstans összehasonlító kifejezés mindig nemtípusos logikai konstanst eredményez.

Ha a konstans biteltolás kifejezés bal operandusa nemtípusos konstans, akkor az eredmény mindig (nemtípusos) egész konstans - különben a bal operandusnak megfelelő típusú (a biteltolás operátor bal operandusa mindig egész típusú).

Minden, a fentiektől eltérő esetben, mikor két nemtípusos konstanson alkalmazunk operátort, az eredmény azonos fajtájú nemtípusos konstans lesz.

Implementációs megszorítás: egyes fordítók a nemtípusos lebegőpontos és komplex konstans kifejezések kiértékelésénél kerekíthetnek. Ez akár azzal a hatással is járhat, hogy egy (végtelen pontosság esetén) egész értékű lebegőpontos konstans nem lesz használható egészként. Pl. az alábbi egy változó értéke nem biztos, hogy pontosan 1 lesz:

const harmad = 1.0 / 3.0 const egy = harmad * 3

Típusos konstans kifejezések

A típusos konstansok értékétől a nyelv megköveteli, hogy az pontosan ábrázolható legyen a megadott típussal. Így például az alábbiak fordítás hibát okoznak:

uint(-1) // -1 nem reprezentálható előjel nélküli egész számként int64(1<<100) // a szám túl nagy, nm reprezentálható 64 biten

Az iota konstans kifejezés

A speciális iota kifejezés egész (nemtípusos) konstansok sorozatának előállítását könnyíti meg. Kezdeti értéke 0, és minden konstans változó deklarációjánál, amelyben felhasználjuk (ld. Konstans-deklaráció), eggyel nő. Ha a fordító egy új const kulcsszót talál, az értéke újra 0 lesz.

Példa az iota használatára:

const ( // iota értéke most 0 c0 = iota // c0 == 0 c1 = iota // c1 == 1 c2 = iota + iota // c2 == 4, mivel iota értéke mindkét alkalommal 2 volt )

A nyelv nem tartalmaz felsorolástípust, viszont az iota szerepelhet konstans kifejezésben és ezek a kifejezések implicit ismételhetőek egy const blokon belül, így elérhető hasonló viselkedés:

type ByteSize float64 const ( _ = iota // ignore first value by assigning to blank identifier KB ByteSize = 1 << (10 * iota) MB GB TB PB EB ZB YB )

Kifejezések kiértékelésének sorrendje

Egy értékadás vagy kifejezés elemei közt minden funkció- illetve metódushívás valamint kommunikáció balról jobbra értékelődik ki.

y[f()], ok = g(h(), i() + x[j()], <-c), k()

A fenti értékadásban ez a következő sorrendet jelenti: f(), h(), i(), j(), <-c, g(), végül k(). Viszont a fentiekhez képest az x-nek és az y-nak, illetve az indexelések kiértékelésének sorrendjére nincs megkötés.