Štruktúry a rozhrania v Go

Sunday, Oct 25, 2020 by Nelo
Go programovanie so štruktúrami a rozhraniami. Go ponúka objektovo orientované programovanie cez štruktúry a rozhrania.

Často potrebujeme zoskupiť dáta do typov, s ktorými sa ľahšie pracuje. V objektovo orientovaných programovacích jazykoch sa to rieši pomocou tried, ktoré zapuzdrujú dáta a metódy, ktoré nad nimi pracujú. Go ponúka objektovo orientovaný prístup, metódy a rozhrania, ale nepodporuje hierarchiu tried, podtriedy a dedenie. Vychádza z OOP princípu “composition over inheritance” a podporuje polymorfizmus prostredníctvom rozhraní, čím výrazne zjednodušuje štruktúru programov, zvyšuje flexibilitu a prepoužiteľnosť kódu.

Štruktúry: struct

Štruktúry sú hodnotové agregované typy. To znamená, že pri odovzdávaní ako parameter funkcii sa kopíruje ich obsah a pri porovnávaní == alebo != sa porovnáva ich obsah, nie adresa v pamäti. V Go sa štruktúra deklaruje type T struct {}. Naplniť sa dajú dvomi spôsobmi použitím struct literal buď vymenovaním všetkých hodnôt v poradí ako sú deklarované v štruktúre ako Point{2, 1} alebo s uvedením jednej alebo viacerých hodnôt pomocou idenfikátora ako Point{y: 1, x: 2}. Tieto spôsoby nemožno kombinovať. Druhý spôsob je preferovaný. Taktiež sa odporúča definovať konštruktor, funkciu ktorá vytvorí a vráti objekt ako func newPoint(x, y float32) Point. K poliam v štruktúrach sa pristupuje cez selektor ..

Go dovoľuje vytvárať aj anonymné štruktúry a anonymné polia. Anonymné štruktúry sa používajú ako jednorazové objekty a majú tvar premenna := struct { // polia }. Anonymné polia sa deklarujú v štruktúrach len typom bez názvu poľa a jedna štruktúra nemôže obsahovať viac anonymných polí rovnakého typu. Go umožňuje vložiť aj štruktúru do štruktúry ako anonymné pole a poskytuje zjednodušený prístup k prvkom vnorenej štruktúry.

Spustiť v playgrounde

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package main

import (
	"fmt"
	"math"
)

type Point struct {
	x, y float32
}

type ColorPoint struct {
	Point // anonymne pole
	c int
}

func newColorPoint(x, y float32, c int) ColorPoint {
	return ColorPoint{Point{x, y}, c}
}

type csvField struct {
	string
	int
	float64
}

func main() {
    // vytvorenie dvoch roznych objektov s totoznymi hodnotami
	a := Point{2, 1}       // {2 1}
	b := Point{y: 1, x: 2} // {2 1}
	fmt.Println(a == b)    // true

    // pristup k prvkom cez selektor
	b.y = 3             // {2 3}
	fmt.Println(a == b) // false

    // struktura s vnorenou strukturou ako anonymne pole
    c := newColorPoint(2, 3, 255)
    c.Point.x = 5 // totozne so zapisom o riadok nizsie
	c.x = 5
	fmt.Println(c) // {{5 3} 255}

	// anonymna struktura
	anon := struct {
		name string
		age  int
	}{"Douglas Adams", 42}
	fmt.Println(anon) // {Douglas Adams 42}

	// anonymne polia
	csv := csvField{float64: math.Pi}
	csv.int = 42
	csv.string = "zmysel vsetkeho"
	fmt.Println(csv) // {zmysel vsetkeho 42 3.141592653589793}
}

Ukazovateľe na objekty: pointer

Štruktúry zaberajú určité miesto v pamäti a povedali sme si, že ako hodnotové typy sa pri volaní funkcie vytvára ich kópia. To môže byť nielen neefektívne, ale niekedy potrebujeme zmeniť vo funkcii polia v objekte. Ukazovatele (pointre) umožňujú získať a odovzdať miesto v pamäti, kde sa nachádza objekt.

Ukazovateľ deklarujeme tak, že pred typ dáme “*” ako var intPtr *int. Adresu objektu získame tak, že pred objekt dáme “&” ako intPtr = &i. Ak chceme získať alebo zmeniť hodnotu na adrese, kam smeruje ukazovateľ, musíme ju zmeniť dereferencovaním ukazovateľa. Pred ukazovateľa dáme “*” ako *intPtr a pracujeme ako s normálnou premennou, nie adresou. Keby sme totižto napísali intPtr = 5 tak by sme žiadali zmeniť obsah intPtr, t.j. adresu uloženú v intPtr na 5.

Rovnako môžeme použiť ukazovateľ na štruktúru var p *SomeStruct, ktorý môžeme priamo inicializovať ako p := &SomeStruct{} alebo pomocou “new” p := new(SomeStruct), alebo nasmerovať na adresu existujúceho objektu p := &concreteStruct. Zápis dereferencovania (*b).x nám Go umožňuje zapísať jednoduchšie ako b.x, pretože selektor “.” automaticky dereferencuje ukazovateľa. Taktiež môžeme vložiť štruktúru do inej štruktúry buď hodnotou, alebo ako ukazovateľ.

Ukazovateľ nemusí byť inicializovaný, v takom prípade má hodnotu nil.

Go narozdiel od C/C++ nepodporuje pointrovú aritmetiku, to znamená, že nemôžme napríklad posúvať adresu ukazovateľa ptr++ alebo ptr+1, čím odpadá skupina chýb typu access violation a prepisovanie pamäti iných objektov.

Spustiť v playgrounde

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import (
	"fmt"
)

type Point struct {
	x, y float32
}

func scale(p Point, factor float32) {
	p.x *= factor
	p.y *= factor
	fmt.Println(p) // {4 6}
}

func scalePtr(p *Point, factor float32) {
	p.x *= factor
	p.y *= factor
	fmt.Println(p) // &{4 6}
}

func main() {
	var i int
	i = 2
	var intPtr *int              // ukazovatel na int
	intPtr = &i                  // adresa i
	*intPtr = 5                  // dereferencovanie a zmena hodnoty na adrese kam ukazuje
	fmt.Println(&i, i)           // 0xc0000180c0 5
	fmt.Println(intPtr, *intPtr) // 0xc0000180c0 5

	a := Point{2, 3}
	fmt.Println(a) // {2 3}
	scale(a, 2)
	fmt.Println(a) // {2 3}

	scalePtr(&a, 2)
	fmt.Println(a) // {4 6}

	var b *Point
	b = &a
	c := a
	fmt.Println(b == &a) // true, ukazovatel na rovnaku adresu
	fmt.Println(*b == a) // true, hodnota dereferencovaneho ukazovatela je rovnaka
	fmt.Println(b == &c) // false, ukazovatel ukazuje na inu adresu
	fmt.Println(*b == c) // true, hodnota dereferencovaneho ukazovatela je rovnaka

	(*b).x = 1                    // zapis je ekvivalentny zapisu o riadok nizsie
	b.x = 1                       // selektor "." automaticky dereferencuje ukazovatela
	fmt.Println(a)                // {1 6}
	fmt.Println(b)                // &{1 6}
	fmt.Printf("%p %T\n", &a, a)  // 0xc0000180f8 main.Point, adresa "a" a typ "Point"
	fmt.Printf("%p %T\n", b, b)   // 0xc0000180f8 *main.Point, adresa na ktoru ukazuje "b" a typ pointer na "Point"
	fmt.Printf("%p %T\n", &b, &b) // 0xc000006030 **main.Point, adresa "b" a typ pointer na pointer na "Point"
    fmt.Printf("%v %T\n", *b, *b) // {1 6} main.Point, dereferencovany objekt na ktory ukazuje "b" typu "Point"
    
	var p *Point
	fmt.Printf("%v %T\n", p, p) // <nil> *main.Point
}

Správa pamäti: garbage collector

Každý objekt zaberá určitú časť pamäti, prípadne má nároky na ďalšie zdroje (otvorený súbor, sieťové spojenie atď). S cieľom uľahčiť tvorbu aplikácií, zjednodušiť a skvalitniť programy a zvýšiť produktivitu sa Go runtime automaticky stará o správu pamäti.

Existujú dva spôsoby - reference counting a tracing garbage collection. Go používa druhý spôsob, pravidelne skenuje pamäť čistí nedosiahnuteľné objekty. Oblasť efektívnej správy pamäti je neustále predmetom vývoja a zlepšovania.

Pripájanie funkcií k typom: receiver

Go podporuje zviazanie funkcie k typu a vytvorenie prijímateľa (receiver). Prijímatelia sú funkčne veľmi podobní metódam v objektových jazykoch.

Existujú dva druhy prijímateľov:

  • Prijímateľ hodnoty (value receiver) func (v T) f
  • Prijímateľ ukazovateľa (pointer receiver) func (p *T) f

Rozdiel medzi nimi je ten, že pri prvom spôsobe sa skopíruje hodnota a zmenou v tele funkcie nemôžeme meniť objekt, na ktorý je funkcia zavesená. To má výhodu, ak chceme zabezpečiť len čítanie, ale nie zmenu materského objektu. V metodovych volaniach Go automaticky konvertuje medzi hodnotou a ukazovatelom.

Narozdiel od iných jazykoch, v Go môžeme zavolať metódu aj na neinicializovanom nil objekte a metóda sa zavolá bezpečne bez pádu programu. Samozrejme potom by sme mali patrične ošetriť v metóde akým spôsobom sa vykoná.

Spustiť v playgrounde

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import "fmt"

type rectangle struct {
	width, height float32
}

// prijimatel hodnoty (value receiver)
func (o rectangle) area() float32 {
	return o.width * o.height
}

// prijimatel ukazovatela (pointer receiver)
func (o *rectangle) scale(factor float32) {
	if o == nil {
		return
	}
	o.width *= factor
	o.height *= factor
}

func main() {
	t := rectangle{width: 3, height: 5}

	fmt.Println("obdlznik: ", t)      // obdlznik:  {3 5}
	fmt.Println("plocha: ", t.area()) // plocha:  15

	t.scale(2)
	fmt.Println("obdlznik: ", t) // obdlznik:  {6 10}

	p := &t
	fmt.Println("obdlznik: ", p)         // obdlznik:  &{6 10}
	fmt.Println("plocha: ", (*p).area()) // plocha:  60, mozme zapisat jednoduchsie nizsie
	fmt.Println("plocha: ", p.area())    // plocha:  60, automaticka dereferencia

	p = nil
	p.scale(2)
	fmt.Println(p) // <nil>
}

Rozhrania: interface

Rozhrania umožňujú oddeliť definíciu metód od ich implementácie, čím výrazne zvyšujú flexibilitu a rozšíriteľnosť kódu. Typy v Go nemusia explicitne deklarovať, že implementujú rozhranie, napríklad kľúčovým slovom “implements” ako v iných jazykoch. Rozhrania sú deklarované implicitne a kompilátor to vidí zo štruktúry. To sa nazýva aj “duck typing” - ak to kváka ako kačka, chodí ako kačka, tak to je kačka.

Rozhranie definujeme v tvare type I interface { // metody }. Každý typ, ktorý implementuje všetky vymenované metódy v rovnakom tvare automaticky implementuje takéto rozhranie. Všimnime si, že v rozhraní nesmú byť polia, len metódy.

Vo svojej podstate si môžme rozhranie predstaviť ako konkrétny pár (hodnota, typ). Zavolaním metódy rozhrania zavoláme metódu na danom type. Rozhraniu musíme priradiť konktétny typ ak ho chceme zavolať, ak by sme nepriradili tak by mal hodnotu aj typ nil program nám vyhodí runtime exception.

V Go existuje a je možné použiť aj prázdny interface{}. Tomu sa odporúča vyhnúť kdekoľvek sa dá, avšak tvorí dôležitú súčasť knižnice a používa ho napríklad funkcia func Printf(format string, a ...interface{}) (n int, err error).

Z rozhrania môžme spätne získať objekt, ktorý obsahuje pomocou someObject, ok := someInterface.(T), kde “T” je typ na ktorý sa snažíme objekt v rozhraní prehodiť.

Spustiť v playgrounde

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
	"fmt"
	"math"
)

// Obdlznik
type rectangle struct{ width, height float32 }

func (o rectangle) area() float32 { return o.width * o.height }

// Kruh
type circle struct{ radius float32 }

func (k circle) area() float32 { return 2.0 * math.Pi * k.radius * k.radius }

// Tvar - interface
type tvar interface{ area() float32 }

// Funkcia prijimajuca interface
func price(t tvar, v float32) float32 { return t.area() * v }

func main() {
	o := rectangle{width: 3, height: 5}
	fmt.Printf("%v %T\n", o, o) // {3 5} main.rectangle

	k := circle{radius: 4}
	fmt.Printf("%v %T\n", k, k) // {4} main.circle

	var x tvar = &o
	fmt.Printf("%v %T\n", price(x, 2.99), x) // 44.85 *main.rectangle
	x = &k
	fmt.Printf("%v %T\n", price(x, 2.99), x) // 300.5876 *main.circle

	f, ok := x.(*circle)
	fmt.Println(f, ok) // &{4} true
	g, ok := x.(*rectangle)
	fmt.Println(g, ok) // <nil> false
}

Zhrnutie

  1. struct je agregovaný hodnotový typ, ktorý umožňuje logicky zoskupiť dáta, môže byť anonymný alebo mať anonymné polia. Tiež môže obsahovať ďalšie vnorené štruktúry. Deklarovať a inicializovať môžeme napríklad ako “struct literal” s vymenovanými názvami polí a hodnotami
  2. Ukazovatele sú premenné, ktoré obsahujú pamäťovú adresu objektu. Pri odovzdávaní ako parameter funkcie sa tak prenáša len adresa namiesto kopírovania objektu a v tele funkcie je možné objekt meniť. Definujú sa *T (hviezdičkou pred typom), adresa objektu sa definuje &s (ampersand pred objektom) a konkrétny objekt sa dereferencuje *p (hviezdička pred ukazovateľom)
  3. Nepoužívané objekty nie je potrebné mazať, Go garbage collector sa postará o mazanie automaticky.
  4. V Go môžeme pripojiť k štruktúre aj funkcie, ktoré podľa toho, či sú definované nad objektom alebo ukazovateľom na objekt nazývame value receiver alebo pointer receiver
  5. interface je špeciálny typ, ktorý obsahuje iba deklarácie metód. Umožňuje abstrahovať a oddeliť metódy-vlastnosti od konkrétneho typu a ich implementácie. Môže byť definovaný ako prázdny a potom nahrádza akýkoľvek objekt, alebo neinicializovaný, použitý ako parameter funkcie. Obsahuje ukazovateľ na objekt a jeho typ. Go nevyžaduje explicitné definovanie implementácie rozhraní na type, ak obsahuje typ zodpovedajúce metódy s totožnou signatúrou, automaticky sa považuje, že implementuje konktétne rozhranie

Referencie

  1. Go by Example: Structs
  2. The Go Programming Language Specification
  3. Getting to Go: The Journey of Go&rsquo;s Garbage Collector
  4. Golang Interface

Vaše otázky, návrhy a komentáre

Verím, že vás tento návod inšpiroval a budem vďačný ak dáte spätnú väzbu a pomôže mi zamerať sa na to čo by vás zaujímalo.

TAK ČO HOVORÍŠ ?

Kontaktuj nás ak potrebuješ pomoc, veľmi radi pomôžeme.

Kontaktuj nás