Obsah
1. Reflexe v programovacím jazyce Go
2. Koncept rozhraní v jazyce Go
3. Rozhraní je plnohodnotný datový typ
4. Struktura vyhovující rozhraní
5. Rozlišení konkrétních typů v čase běhu programu
6. Explicitní typové konverze (přetypování)
8. Kontroly prováděné překladačem
9. Test v runtime, zda bylo možné přetypování provést
11. Rozeskok na základě běhové typové informace
12. Ukázka rozeskoku provedeného na základě běhových typových informací
13. Standardní balíček reflect
14. Funkce reflect.ValueOf a datový typ Value
15. Příklady použití funkce reflect.ValueOf
19. Repositář s demonstračními příklady
1. Reflexe v programovacím jazyce Go
Jak již bylo napsáno v perexu dnešního článku, je možné aplikace v jazyce Go programovat takovým způsobem, že jak programátor, tak i překladač bude pro každou proměnnou a pro každý parametr přesně znát datový typ hodnot. Ovšem v případě, že se tvoří obecnější algoritmy, se situace komplikuje, protože v takových případech se většinou namísto konkrétních datových typů používají rozhraní (interface), která mohou být obecně splňována (satisfy) mnoha různými typy. A někdy se dokonce používají i prázdná rozhraní, která splňují všechny datové typy. V takových případech musíme mít možnost získání informace o tom, jaký je typ konkrétní hodnoty, s níž se pracuje – a zde již možnosti statického typového systému rozpoznávaného překladačem nemusí postačovat. Jedno z řešení tohoto problému spočívá ve využití reflexe (reflection).
V rukou programátora se jedná o mocnou zbraň, která však může být dosti nebezpečná, takže se obecně doporučuje se reflexi vyhnout. To je ovšem dosti obecné doporučení a bude dobré si ověřit, jestli je nebo není pravdivé (popř. kdy je pravdivé). Ovšem jazyk Go programátorům nabízí i dva další koncepty, které se mohou použít namísto poněkud nízkoúrovňové reflexe. Jedná se o typové aserce a konverze+rozeskoky na základě konkrétního typu hodnoty, s níž se pracuje. I s těmito koncepty se dnes ve stručnosti seznámíme.
2. Koncept rozhraní v jazyce Go
Již v úvodní části dnešního článku je nutné se alespoň ve stručnosti seznámit s konceptem takzvaných rozhraní (interface), která tvoří nedílnou součást jazyka Go a společně s gorutinami jsou tím konceptem, které Go odlišují od konkurence. Rozhraní v jazyku Go byla inspirována protokoly, s nimiž jsme se mohli setkat například v programovacím jazyku Smalltalk: ve stručnosti jde o specifikaci metod (jmen, parametrů, návratových typů), které jsou společné pro entity s nějakou sdílenou vlastností nebo vlastnostmi. V rozhraní se však nijak nespecifikuje vlastní chování, tj. těla metod. V jazyce Go navíc není nutné explicitně určovat, které záznamy (nebo obecně které datové typy) implementují dané rozhraní – tuto informaci si totiž dokáže automaticky odvodit překladač (poněkud nepřesně se toto chování nazývá duck typing).
Při deklaraci nového rozhraní (tj. při vytváření nového datového typu – samotné rozhraní je totiž taktéž datovým typem) je nutné specifikovat jak jméno rozhraní, tak i seznam hlaviček metod, které jsou součástí rozhraní (tento seznam ovšem může být prázdný, nicméně je nutné ho zapsat pomocí prázdného bloku {}). Příkladem rozhraní s jedinou metodou může být datový typ pojmenovaný OpenShape, v němž je předepsána jediná metoda length bez parametrů a s návratovou hodnotou float64 (u metody předepsané v rozhraní se ovšem neuvádí příjemce – ten si Go odvodí automaticky na základě dalšího kódu):
type OpenShape interface { length() float64 }
V rozhraní může být pochopitelně předepsáno větší množství metod:
type ClosedShape interface { area() float64 perimeter() float64 }
Nebo naopak nemusí být předepsána žádná metoda a tím získáme prázdné rozhraní:
type Shape interface { }
Následuje velmi jednoduchý demonstrační příklad, v němž je pouze deklarována trojice rozhraní, přičemž každé z nich má odlišný počet metod (0 až dvě):
package main type Shape interface { } type OpenShape interface { length() float64 } type ClosedShape interface { area() float64 perimeter() float64 } func main() { }
3. Rozhraní je plnohodnotný datový typ
Rozhraní jakožto plnohodnotný datový typ je možné použít například pro specifikaci typu parametru (parametrů) ve funkcích, popř. u specifikace návratových hodnot. Opět si to vyzkoušejme na našem jednoduchém příkladu s rozhraním nazvaným OpenShape, v němž je předepsána jediná metoda length():
type OpenShape interface { length() float64 }
Nyní můžeme napsat funkci (běžnou funkci), které se předá libovolná struktura implementující rozhraní OpenShape a tato funkce vrátí hodnotu získanou zavoláním metody OpenShape.length():
func length(shape OpenShape) float64 { return shape.length() }
func compute_open_shape_length(shape OpenShape) float64 { return shape.length() }
V dalším demonstračním příkladu se pokusíme funkci length() zavolat a předat jí strukturu/záznam Line:
package main import ( "fmt" "math" ) type OpenShape interface { length() float64 } type Line struct { x1, y1 float64 x2, y2 float64 } func length(shape OpenShape) float64 { return shape.length() } func main() { line1 := Line{x1: 0, y1: 0, x2: 100, y2: 100} fmt.Println(line1) line_length := length(line1) fmt.Println(line_length) }
Ovšem vzhledem k tomu, že struktura Line prozatím rozhraní OpenShape neimplementuje (v Go se mluví o tom, že struktura nevyhovuje rozhraní), nebude možné tento program přeložit a pochopitelně ani spustit:
./06_interface_implementation.go:12:2: imported and not used: "math" ./06_interface_implementation.go:33:23: cannot use line1 (type Line) as type OpenShape in argument to length: Line does not implement OpenShape (missing length method)
4. Struktura vyhovující rozhraní
Co přesně tedy musíme udělat pro to, aby datová struktura Line vyhovovala (satisfy) rozhraní OpenShape a v něm předepsané metodě length()? Je toho překvapivě málo, protože jediné, co musíme udělat, je implementace metody length() s příjemcem Line, která je bez parametrů a vrací float64. Tato implementace bude jednoduchá, protože metoda bude vracet délku úsečky, tj. vzdálenost mezi body [x1, y1] a [x2, y2]:
func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) }
Již vytvořením této metody jsme dosáhli toho, že Line bude vyhovovat rozhraní OpenShape! Tuto skutečnost si jazyk Go ověří jak při překladu, tak i po spuštění aplikace.
Korektní chování si otestujeme na tomto demonstračním příkladu:
package main import ( "fmt" "math" ) type OpenShape interface { length() float64 } type Line struct { x1, y1 float64 x2, y2 float64 } func length(shape OpenShape) float64 { return shape.length() } func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) } func main() { line1 := Line{x1: 0, y1: 0, x2: 100, y2: 100} fmt.Println(line1) line_length := length(line1) fmt.Println(line_length) }
Po spuštění tohoto příkladu dostaneme žádoucí výsledek:
{0 0 100 100} 141.4213562373095
Metody předepsané v rozhraní musí být implementovány zcela přesně, a to včetně návratového typu. V případě, že typ návratové hodnoty nepatrně změníme (float32 → float64), nebude Line rozhraní OpenShape vyhovovat:
package main import ( "fmt" "math" ) type OpenShape interface { length() float32 } type Line struct { x1, y1 float64 x2, y2 float64 } func length(shape OpenShape) float32 { return shape.length() } func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) } func main() { line1 := Line{x1: 0, y1: 0, x2: 100, y2: 100} fmt.Println(line1) line_length := length(line1) fmt.Println(line_length) }
Pokus o překlad nyní skončí s chybou:
./07_B_wrong_return_type.go:37:23: cannot use line1 (type Line) as type OpenShape in argument to length: Line does not implement OpenShape (wrong type for length method) have length() float64 want length() float32
A pochopitelně nám nic nebrání v tom, aby i jiné datové struktury implementovaly to samé rozhraní:
type Polyline struct { x, y []float64 } func (pline Polyline) length() float64 { // výpočet délky na základě všech vrcholů polyčáry // (není relevantní) ... return ... }
Nebo triviální případ:
type Point struct { x, y float64 } func (point Point) length() float64 { return 0.0 }
5. Rozlišení konkrétních typů v čase běhu programu
Rozhraní, která jsou použitá pro deklaraci parametrů funkcí a/nebo jejich návratových hodnot, umožňují psaní obecných algoritmů a knihovních funkcí. Například prakticky celý vstupně-výstupní systém programovacího jazyka Go (kam spadá práce se soubory, práce s paměťovými buffery, síťovými sockety atd.) je postavena nad dvojicí rozhraní nazvaných Reader a Writer, které může implementovat i libovolná uživatelem definovaná datová struktura. Programové kódy lze skutečně koncipovat tak, že parametry funkcí a návratové hodnoty budou typu rozhraní_xyz, což je poměrně elegantní řešení. Mohou však nastat situace, které vyžadují „konverzi“ předávané hodnoty na konkrétní typ. Příkladem může být funkce, která akceptuje parametr typu OpenShape (tedy 2D geometrický tvar se začátkem a koncem), ale v níž budeme chtít například přistoupit k vrcholům úsečky, pokud je předána úsečka či k souřadnici bodu, pokud je předán bod.
6. Explicitní typové konverze (přetypování)
Ovšem v takovém případě není možné provést klasickou typovou konverzi – to nám překladač jazyka Go nedovolí. Opět si to vyzkoušejme na příkladu:
package main import ( "fmt" "math" ) type OpenShape interface { length() float64 } type Line struct { x1, y1 float64 x2, y2 float64 } func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) } type Point struct { x, y float64 } func (point Point) length() float64 { return 0 } func do_something(shape OpenShape) { s := Point(shape) fmt.Println(s.x1, s.y1) } func main() { }
Ve funkci do_something se pokoušíme o typovou konverzi, což však není dovoleno. Na tuto skutečnost nás přitom upozorní již překladač a nebude tedy docházet k „náhodným“ výjimkám v runtime:
./shapes_1.go:30:13: cannot convert shape (variable of type OpenShape) to type Point: need type assertion
7. Typové aserce
Namísto přímé typové konverze je v takových případech nutné v jazyku Go použít typové aserce (type assertion). Ty se zapisují následovně:
s := shape.(Point)
kde shape je jméno proměnné nebo parametru a Point je typ, na který se má hodnota převést (pokud je to možné). Můžeme tedy například psát:
func do_something(shape OpenShape) { s := shape.(Point) fmt.Println(s.x, s.y) }
kde x a y jsou prvky datové struktury Point. To ovšem znamená, že s je taktéž typu Point. Zápis typové aserce je rozpoznán překladačem, který dokáže určit typ proměnné s. Ovšem vlastní konverze se provádí až v čase běhu. Máme zde tedy dvě časově oddělené operace: compile time a runtime. V případě, že konverzi nelze provést, tj. pokud předávaná hodnota je jiného konkrétního typu, vyhodí se běhová výjimka (překladač to – logicky – nedokáže rozpoznat).
package main import ( "fmt" "math" ) type OpenShape interface { length() float64 } type Line struct { x1, y1 float64 x2, y2 float64 } func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) } type Point struct { x, y float64 } func (point Point) length() float64 { return 0 } func do_something(shape OpenShape) { s := shape.(Point) fmt.Println(s.x, s.y) } func main() { p := Point{x: 10, y: 20} do_something(p) }
8. Kontroly prováděné překladačem
Mohlo by se zdát, že do zdrojového kódu je možné zapsat jakoukoli typovou aserci, ovšem ve skutečnosti překladač provádí kontroly, zda má zápis z pohledu statické typové analýzy smysl. Ostatně zkusme upravit funkci do_something tak, aby její parametr shape byl typu io.Writer, což je jedno z rozhraní definovaných ve standardní knihovně. A dopředu si prozraďme, že typ Point tomuto rozhraní nevyhovuje:
func do_something(shape io.Writer) { s := shape.(Point) }
Překladač v tomto případě korektně nahlásí chybu (ale už nám neprozradí, kterým všem rozhraním struktura Point vyhovuje):
./shapes_4.go:31:11: impossible type assertion: shape.(Point) Point does not implement io.Writer (missing method Write)
9. Test v runtime, zda bylo možné přetypování provést
Ve skutečnosti může řešení z předchozí kapitoly vést k běhovým výjimkám. Stane se tak v případě, kdy není možné přetypování předepsané typovou asercí provést. V našem konkrétním ukázkovém příkladu to znamená situaci, kdy do funkce do_something sice předáme hodnotu typu, která splňuje rozhraní OpenShape, ale nebude se jednat o bod (ale například o úsečku):
package main import ( "fmt" "math" ) type OpenShape interface { length() float64 } type Line struct { x1, y1 float64 x2, y2 float64 } func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) } type Point struct { x, y float64 } func (point Point) length() float64 { return 0 } func do_something(shape OpenShape) { s := shape.(Point) fmt.Println(s.x, s.y) } func main() { p := Point{x: 10, y: 20} do_something(p) l := Line{x1: 0, y1: 0, x2: 10, y2: 20} do_something(l) }
První zavolání funkce proběhne v pořádku, druhé pak skončí s výjimkou:
10 20 panic: interface conversion: main.OpenShape is main.Line, not main.Point goroutine 1 [running]: main.do_something({0x4bc958?, 0xc00009cf10?}) /home/ptisnovs/xy/shapes_2.go:30 +0xb8 main.main() /home/ptisnovs/xy/shapes_2.go:39 +0x77 exit status 2
Úprava (resp. oprava) spočívá v tom, že budeme explicitně zjišťovat, zda k přetypování skutečně mohlo dojít. K tomuto účelu se použije druhá návratová hodnota typové aserce:
package main import ( "fmt" "math" ) type OpenShape interface { length() float64 } type Line struct { x1, y1 float64 x2, y2 float64 } func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) } type Point struct { x, y float64 } func (point Point) length() float64 { return 0 } func do_something(shape OpenShape) { s, ok := shape.(Point) if ok { fmt.Println(s.x, s.y) } else { fmt.Println("can not convert") } } func main() { p := Point{x: 10, y: 20} do_something(p) l := Line{x1: 0, y1: 0, x2: 10, y2: 20} do_something(l) }
Nyní program v případě neúspěšné typové konverze neskončí běhovou výjimkou:
10 20 can not convert
10. Ukázka typových asercí
Podívejme se nyní na způsob použití typových asercí v programovacím jazyku Go. V prvním příkladu budeme ve funkci test_type, které lze předat libovolnou hodnotu, rozlišovat, zda je konkrétní předaná hodnota typu int, bool či string. Na základě zjištění typu se vypíšou odlišné zprávy a zejména skutečně získáme hodnoty konkrétního typu:
package main import "fmt" func test_type(value any) { int_val, ok := value.(int) if ok { fmt.Println("Integer value:", int_val) return } bool_val, ok := value.(bool) if ok { fmt.Println("Boolean value:", bool_val) return } string_val, ok := value.(string) if ok { fmt.Println("String value:", string_val) return } fmt.Println("Unsupported value") } func main() { x := 42 test_type(x) y := true test_type(y) z := "foobar" test_type(z) w := 1 + 2i test_type(w) }
Zprávy, které by se měly zobrazit po překladu a spuštění:
Integer value: 42 Boolean value: true String value: foobar Unsupported value
Ve druhém demonstračním příkladu jsme si situaci ještě více zkomplikovali, protože v něm je definována dvojice rozhraní Interface1, Interface2 a trojice datových typů, které jedno či obě tato rozhraní splňují (tím, že implementují předepsané metody). Ve funkci test_type potom rozlišujeme, zda konkrétní hodnota splňuje jedno či obě rozhraní (obecněji řečeno – zda je typu daného rozhraní). Hodnoty získané přes typovou aserci budou vždy typu Interface1 nebo Interface2:
package main import "fmt" type Interface1 interface { foo() } type Interface2 interface { bar() } type Type1 struct { name string } func (t Type1) foo() { } type Type2 struct { name string } func (t Type2) bar() { } type Type3 struct { name string } func (t Type3) foo() { } func (t Type3) bar() { } func test_type(value any) { interface1_val, ok := value.(Interface1) if ok { fmt.Println("Interface1 value:", interface1_val) } interface2_val, ok := value.(Interface2) if ok { fmt.Println("Interface2 value:", interface2_val) } } func main() { x := Type1{"x"} test_type(x) y := Type2{"y"} test_type(y) z := Type3{"z"} test_type(z) }
Výsledky nyní budou zajímavější, protože ukazují, že hodnota z typu Type3 splňuje obě rozhraní:
Interface1 value: {x} Interface2 value: {y} Interface1 value: {z} Interface2 value: {z}
Předchozí příklad je samozřejmě možné ještě více rozšířit, a to konkrétně tak, že ve funkci test_type se budeme z předané hodnoty libovolného typu pokoušet získat hodnoty typu Interface1, Interface2, Type1, Type2 nebo Type3:
package main import "fmt" type Interface1 interface { foo() } type Interface2 interface { bar() } type Type1 struct { name string } func (t Type1) foo() { } type Type2 struct { name string } func (t Type2) bar() { } type Type3 struct { name string } func (t Type3) foo() { } func (t Type3) bar() { } func test_type(value any) { interface1_val, ok := value.(Interface1) if ok { fmt.Println("Interface1 value:", interface1_val) } interface2_val, ok := value.(Interface2) if ok { fmt.Println("Interface2 value:", interface2_val) } type1_val, ok := value.(Type1) if ok { fmt.Println("Type1 value:", type1_val) } type2_val, ok := value.(Type2) if ok { fmt.Println("Type2 value:", type2_val) } type3_val, ok := value.(Type3) if ok { fmt.Println("Type3 value:", type3_val) } } func main() { x := Type1{"x"} test_type(x) y := Type2{"y"} test_type(y) z := Type3{"z"} test_type(z) }
Opět se podívejme na výsledky pro hodnoty tři různých typů (struktur):
Interface1 value: {x} Type1 value: {x} Interface2 value: {y} Type2 value: {y} Interface1 value: {z} Interface2 value: {z} Type3 value: {z}
Z tohoto výpisu je patrné, že každou strukturu lze převést i na příslušná rozhraní.
11. Rozeskok na základě běhové typové informace
V jazyce Go mají vývojáři k dispozici ještě jednu zajímavou řídicí strukturu, která doplňuje typovou aserci. Jedná se o rozeskok, který je proveden na základě informace o typu výrazu. Datový typ tohoto výrazu je zjištěn za běhu aplikace. Pro tento rozeskok (nebo, chcete-li, rozvětvení) se používá konstrukce switch-case, ovšem výraz uvedený za klíčovým slovem switch připomíná právě typovou aserci. A v jednotlivých větvích jsou uvedena jména datových typů (ať již typů standardních, tak i uživatelem definovaných). Navíc v jednotlivých větvích bude mít proměnná, do které se výraz za switch uložil, korektní typ – obecně v každé větvi jiný!
Podívejme se na jednoduchý příklad, z něhož bude celá konstrukce snadno pochopitelná:
package main import "fmt" func test_type(value any) { switch v := value.(type) { case int: fmt.Println("Integer value:", v) case bool: fmt.Println("Boolean value:", v) case string: fmt.Println("String value:", v) default: fmt.Println("Unsupported value") } } func main() { x := 42 test_type(x) y := true test_type(y) z := "foobar" test_type(z) w := 1 + 2i test_type(w) }
Výsledky
Integer value: 42 Boolean value: true String value: foobar Unsupported value
Povšimněte si, že proměnná v bude mít v každé větvi odlišný typ. Nejedná se tedy o jedinou proměnnou. To znamená, že můžeme program upravit do této (stále zcela korektní) podoby:
switch v := value.(type) { case int: fmt.Println("Integer value:", v*10) case bool: fmt.Println("Boolean value:", !v) case string: fmt.Println("String value:", v+"foooooooo") default: fmt.Println("Unsupported value") }
Výsledky
Integer value: 420 Boolean value: false String value: foobarfooooooo Unsupported value
12. Ukázka rozeskoku provedeného na základě běhových typových informací
Naprosto stejným způsobem můžeme v rozeskoku použít i typ rozhraní a nikoli pouze konkrétní datové typy. Vyzkoušejme si chování v situaci se dvěma rozhraními a trojicí konkrétních datových typů, které splňují jedno či obě rozhraní:
package main import "fmt" type Interface1 interface { foo() } type Interface2 interface { bar() } type Type1 struct { name string } func (t Type1) foo() { } type Type2 struct { name string } func (t Type2) bar() { } type Type3 struct { name string } func (t Type3) foo() { } func (t Type3) bar() { } func test_type(value any) { switch v := value.(type) { case Interface1: fmt.Println("Interface1 value:", v) case Interface2: fmt.Println("Interface2 value:", v) default: fmt.Println("Unsupported value") } } func main() { x := Type1{"x"} test_type(x) y := Type2{"y"} test_type(y) z := Type3{"z"} test_type(z) }
Výsledky:
Interface1 value: {x} Interface2 value: {y} Interface1 value: {z}
Jen krátké připomenutí, jak se tato programová konstrukce používá společně s dalšími knihovnami. Příkladem je zpracování událostí, které vznikají při běhu aplikace založené na knihovně SDL2. Jednotlivé události jsou reprezentovány ukazateli na hodnoty konkrétních typů:
package main import ( "log" "github.com/veandco/go-sdl2/sdl" ) func eventLoop() { var event sdl.Event done := false for !done { event = sdl.PollEvent() switch t := event.(type) { case *sdl.QuitEvent: done = true case *sdl.KeyboardEvent: keyCode := t.Keysym.Sym switch t.State { case sdl.PRESSED: switch keyCode { case sdl.K_ESCAPE: done = true case sdl.K_q: done = true } } } state.moveNPC() state.redraw() sdl.Delay(10) } log.Println("Quitting") }
A další příklad – průchod (traverzace) AST stromem. Při průchodu tímto stromem vzniklým zparsováním zdrojového kódu, je pro každý uzel volána metoda Visit, přičemž její parametr je sice typu ast.Node (obecné rozhraní), ovšem v runtime se jedná o jeden z mnoha datových typů, které toto rozhraní splňují:
package main import ( "fmt" "log" "strings" "go/ast" "go/parser" ) // výraz, který se má naparsovat const source = ` 1 + 2 * 3 + x + y * z - 1 ` // nový datový typ implementující rozhraní ast.Visitor type visitor int // implementace (jediné) funkce předepsané v rozhraní ast.Visitor func (v visitor) Visit(n ast.Node) ast.Visitor { // dosáhli jsme koncového uzlu? if n == nil { return nil } // tisk pozice a typu uzlu fmt.Printf("%3d\t", v) var s string // převod uzlu do tisknutelné podoby switch x := n.(type) { case *ast.BasicLit: s = x.Value case *ast.Ident: s = x.Name case *ast.UnaryExpr: s = x.Op.String() case *ast.BinaryExpr: s = x.Op.String() } // tisk obsahu uzlu indent := strings.Repeat(" ", int(v)) if s != "" { fmt.Printf("%s%s\n", indent, s) } else { fmt.Printf("%s%T\n", indent, n) } return v + 1 } func main() { // konstrukce parseru a parsing zdrojového kódu f, err := parser.ParseExpr(source) if err != nil { log.Fatal(err) } // hodnota typu visitor var v visitor // zahájení průchodu abstraktním syntaktickým stromem ast.Walk(v, f) }
13. Standardní balíček reflect
V mnoha případech si v praxi vystačíte s typovými asercemi a rozeskoky prováděnými na základě typové informace, což jsou koncepty popsané výše. Ovšem existují situace, v nichž je nutné s typovými informacemi (v čase běhu) pracovat sofistikovaněji. A právě v takových případech se používá reflexe. Většina funkcionality, která se týká reflexe v jazyku Go, je dostupná přes standardní balíček reflect. Veřejné funkce a další symboly tohoto balíčku jsou pochopitelně zdokumentovány a některé nejdůležitější vlastnosti budou popsány a ukázány v navazujících kapitolách. Mimochodem – celou dokumentaci si můžete přečíst na stránkách s dokumentací, nebo si je můžete zobrazit příkazem go doc reflect:
$ go doc reflect package reflect // import "reflect" Package reflect implements run-time reflection, allowing a program to manipulate objects with arbitrary types. The typical use is to take a value with static type interface{} and extract its dynamic type information by calling TypeOf, which returns a Type. A call to ValueOf returns a Value representing the run-time data. Zero takes a Type and returns a Value representing a zero value for that type. See "The Laws of Reflection" for an introduction to reflection in Go: https://golang.org/doc/articles/laws_of_reflection.html ... ... ...
V navazujících kapitolách se s možnostmi nabízenými tímto balíčkem seznámíme.
14. Funkce reflect.ValueOf a datový typ Value
Jednou ze základních operací implementovaných ve standardním balíčku reflect je získání konkrétní hodnoty předané v parametru typu any neboli interface{} (prázdné rozhraní). Co to znamená? Existují situace, zejména v případě, že píšeme obecný kód, v nichž víme, že do nějaké funkce nebo metody se může předat libovolná hodnota (tedy ono any), popř. libovolná hodnota typu, který implementuje nějaké (obecnější) rozhraní. Překladač tedy bude pracovat právě s těmito zmíněnými typy – any popř. interface X. Ovšem v čase běhu (runtime) se samozřejmě do oné funkce nebo metody předává nějaká konkrétní hodnota: většinou datová struktura (resp. datový typ), která vyhovuje (satisfy) zvolenému rozhraní. A jen pro připomenutí – prázdnému rozhraní, tj. typu any, vyhovuje hodnota libovolného typu, protože implementuje všechny předepsané metody (což je u prázdného rozhraní prázdná množina).
Vraťme se nyní k funkci reflect.ValueOf. Ta jako svůj argument akceptuje hodnotu libovolného typu a vrací strukturu (datový typ) nazvaný Value. Přes tuto strukturu budeme moci hodnotu zkoumat – získat její typ, modifikovat ji atd.:
$ go doc reflect.ValueOf package reflect // import "reflect" func ValueOf(i any) Value ValueOf returns a new Value initialized to the concrete value stored in the interface i. ValueOf(nil) returns the zero Value.
Vidíme, že funkce reflect.ValueOf skutečně akceptuje argument typu any (naprosto libovolná hodnota) a vrací jinou hodnotu, která je typu reflect.Value. Interně se jedná o datovou strukturu, ovšem nemáme přístup k prvkům této struktury (jejich jména jsou zapsána malými písmeny):
type Value struct { // Has unexported fields. }
Důležité jsou však metody, které jsou pro tuto datovou strukturu definovány. Je jich celá řada a postupně se s nimi seznámíme:
func (v Value) Addr() Value func (v Value) Bool() bool func (v Value) Bytes() []byte func (v Value) Call(in []Value) []Value func (v Value) CallSlice(in []Value) []Value func (v Value) CanAddr() bool func (v Value) CanComplex() bool func (v Value) CanConvert(t Type) bool func (v Value) CanFloat() bool func (v Value) CanInt() bool func (v Value) CanInterface() bool func (v Value) CanSet() bool func (v Value) CanUint() bool func (v Value) Cap() int func (v Value) Clear() func (v Value) Close() func (v Value) Comparable() bool func (v Value) Complex() complex128 func (v Value) Convert(t Type) Value func (v Value) Elem() Value func (v Value) Equal(u Value) bool func (v Value) Field(i int) Value func (v Value) FieldByIndex(index []int) Value func (v Value) FieldByIndexErr(index []int) (Value, error) func (v Value) FieldByName(name string) Value func (v Value) FieldByNameFunc(match func(string) bool) Value func (v Value) Float() float64 func (v Value) Grow(n int) func (v Value) Index(i int) Value func (v Value) Int() int64 func (v Value) Interface() (i any) func (v Value) InterfaceData() [2]uintptr func (v Value) IsNil() bool func (v Value) IsValid() bool func (v Value) IsZero() bool func (v Value) Kind() Kind func (v Value) Len() int func (v Value) MapIndex(key Value) Value func (v Value) MapKeys() []Value func (v Value) MapRange() *MapIter func (v Value) Method(i int) Value func (v Value) MethodByName(name string) Value func (v Value) NumField() int func (v Value) NumMethod() int func (v Value) OverflowComplex(x complex128) bool func (v Value) OverflowFloat(x float64) bool func (v Value) OverflowInt(x int64) bool func (v Value) OverflowUint(x uint64) bool func (v Value) Pointer() uintptr func (v Value) Recv() (x Value, ok bool) func (v Value) Send(x Value) func (v Value) Set(x Value) func (v Value) SetBool(x bool) func (v Value) SetBytes(x []byte) func (v Value) SetCap(n int) func (v Value) SetComplex(x complex128) func (v Value) SetFloat(x float64) func (v Value) SetInt(x int64) func (v Value) SetIterKey(iter *MapIter) func (v Value) SetIterValue(iter *MapIter) func (v Value) SetLen(n int) func (v Value) SetMapIndex(key, elem Value) func (v Value) SetPointer(x unsafe.Pointer) func (v Value) SetString(x string) func (v Value) SetUint(x uint64) func (v Value) SetZero() func (v Value) Slice(i, j int) Value func (v Value) Slice3(i, j, k int) Value func (v Value) String() string func (v Value) TryRecv() (x Value, ok bool) func (v Value) TrySend(x Value) bool func (v Value) Type() Type func (v Value) Uint() uint64 func (v Value) UnsafeAddr() uintptr func (v Value) UnsafePointer() unsafe.Pointer
15. Příklady použití funkce reflect.ValueOf
Opět si ukažme základní způsoby použití funkce reflect.ValueOf, která byla popsána v předchozí kapitole. V prvním příkladu převedeme hodnotu proměnné x typu int na hodnotu reflect.Value a následně si necháme vypsat jak x tak i takto získanou hodnotu:
package main import ( "fmt" "reflect" ) func main() { x := 42 v := reflect.ValueOf(x) fmt.Println(x) fmt.Println(v) }
Po překladu a spuštění tohoto příkladu se zobrazí dvě zdánlivě stejné hodnoty:
42 42
Ve skutečnosti jsou však obě hodnoty zcela odlišného typu, což nám prozradí následující demonstrační příklad. Připomeňme si, že formátovacím znakem „%T“ si vynutíme výpis typu hodnoty:
package main import ( "fmt" "reflect" ) func main() { x := 42 v := reflect.ValueOf(x) fmt.Printf("value %v of type %T\n", x, x) fmt.Printf("value %v of type %T\n", v, v) }
Nyní budou zobrazené zprávy odlišné (zcela podle očekávání):
value 42 of type int value 42 of type reflect.Value
Zajímavé bude zjistit, jak se bude program chovat v případě, že x bude proměnná typu prázdné rozhraní (tedy vlastně any), ovšem bude obsahovat celé číslo:
package main import ( "fmt" "reflect" ) func main() { var x interface{} = 42 v := reflect.ValueOf(x) fmt.Printf("value %v of type %T\n", x, x) fmt.Printf("value %v of type %T\n", v, v) }
Výsledky prozradí, že typ je zde zjištěn na základě hodnoty a nikoli typu proměnné:
value 42 of type int value 42 of type reflect.Value
Ovšem pozor si musíme dát (jako obvykle) na použití speciální hodnoty nil. Zde je patrné, že jak typ, tak i hodnota jsou rovny nil, což je matoucí, ovšem druhé nil je v tomto případě jméno typu:
package main import ( "fmt" "reflect" ) func main() { var x interface{} = nil v := reflect.ValueOf(x) fmt.Printf("value %v of type %T\n", x, x) fmt.Printf("value %v of type %T\n", v, v) }
Z výsledných zpráv je patrné, že slovo nil má v Go dvojí význam:
value <nil> of type <nil> value <invalid reflect.Value> of type reflect.Value
16. Složitější příklady
Z příkladů uvedených v předchozí kapitole by se mohlo zdát, že překladač dokáže už v čase překladu zjistit konkrétní typ hodnoty uložené do proměnné x a podle toho provede překlad. Ve skutečnosti tomu tak není, o čemž se ostatně můžeme poměrně snadno přesvědčit v následujícím příkladu. V něm je definována funkce test_get_type, které je skutečně možné předat hodnotu libovolného typu a následně realizovat její převod (přes reflexi) na hodnotu typu reflect.Value:
package main import ( "fmt" "reflect" ) func test_get_type(x any) { value := reflect.ValueOf(x) fmt.Println(x) fmt.Println(value) fmt.Println() } func main() { x := 42 test_get_type(x) y := true test_get_type(y) z := "foobar" test_get_type(z) w := 1 + 2i test_get_type(w) }
Výsledky:
42 42 true true foobar foobar (1+2i) (1+2i)
Popř. si ještě vypíšeme jak hodnoty x a value, tak i jejich typy, což je mnohem názornější:
package main import ( "fmt" "reflect" ) func test_get_type(x any) { value := reflect.ValueOf(x) fmt.Printf("value %v of type %T\n", x, x) fmt.Printf("value %v of type %T\n", value, value) fmt.Println() } func main() { x := 42 test_get_type(x) y := true test_get_type(y) z := "foobar" test_get_type(z) w := 1 + 2i test_get_type(w) }
Výsledky:
value 42 of type int value 42 of type reflect.Value value true of type bool value true of type reflect.Value value foobar of type string value foobar of type reflect.Value value (1+2i) of type complex128 value (1+2i) of type reflect.Value
17. Reflexe a hodnoty nil
Jak již bylo naznačeno v předchozím textu, ale i v článku Problematika nulových hodnot v Go, aneb proč nil != nil, je práce s hodnotami nil obecně problematická, protože jazyk Go při porovnávání bere v úvahu jak tuto hodnotu, tak i jí přiřazený typ (který však běžně není viditelný). A proto se může stát, že výraz x == y vrací false i v případě, kdy x=nil a y=nil, což celkem spolehlivě zmate každého, kdo s jazykem Go začíná. V kontextu dnešního článku nás bude zajímat, jak s hodnotami nil pracuje knihovna reflect; nyní konkrétně funkce reflect.ValueOf. Pokusíme se o převod několika hodnot nil různých typů:
package main import ( "fmt" "reflect" ) func test_get_type(x any) { value := reflect.ValueOf(x) fmt.Println(x) fmt.Println(value) fmt.Println() } type user struct { name string surname string } func main() { var nil1 *int = nil test_get_type(nil1) var nil2 *bool = nil test_get_type(nil2) var nil3 *string = nil test_get_type(nil3) var nil4 *user = nil test_get_type(nil4) var nil5 interface{} = nil test_get_type(nil5) var nil6 []int = nil test_get_type(nil6) }
V tomto konkrétním případě se nejprve vytisknou samé hodnoty nil, což nám příliš neprozradí o konkrétních „typech nil“. Ovšem povšimněte si zejména posledních dvou typů (prázdné rozhraní a takzvaná nulová mapa):
<nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <invalid reflect.Value> [] []
Užitečnější bude nechat si vypsat nejenom předávanou hodnotu a její typ, ale i hodnotu+typ získanou pomocí funkce reflect.ValueOf():
package main import ( "fmt" "reflect" ) func test_get_type(x any) { value := reflect.ValueOf(x) fmt.Printf("value %v of type %T\n", x, x) fmt.Printf("value %v of type %T\n", value, value) fmt.Println() } type user struct { name string surname string } func main() { var nil1 *int = nil test_get_type(nil1) var nil2 *bool = nil test_get_type(nil2) var nil3 *string = nil test_get_type(nil3) var nil4 *user = nil test_get_type(nil4) var nil5 interface{} = nil test_get_type(nil5) var nil6 []int = nil test_get_type(nil6) }
Z výsledků je patrné, že typ předávané hodnoty je stále dostupný, i když se zdánlivě jedná o stejnou hodnotu nil. Pátý výsledek říká „hodnota nil typu nil“, což platí jen pro prázdné rozhraní:
value <nil> of type *int value <nil> of type reflect.Value value <nil> of type *bool value <nil> of type reflect.Value value <nil> of type *string value <nil> of type reflect.Value value <nil> of type *main.user value <nil> of type reflect.Value value <nil> of type <nil> value <invalid reflect.Value> of type reflect.Value value [] of type []int value [] of type reflect.Value
18. Přečtení informace o typu
Prozatím jsme pro tisk typu nějaké hodnoty používali funkci fmt.Printf, přičemž ve formátovacím řetězci byl použit formátovací znak „%T“. Tímto způsobem se skutečně tiskne typ hodnoty, ovšem jak se má postupovat v případě, že s typovou informací (což je mimochodem taktéž hodnota) musíme nějak pracovat přímo ve vyvíjeném programu? Opět nám pomůže reflexe, protože typ reflect.Value poskytuje i metodu Type, která informaci o typu vrací. Tato metoda vrací hodnotu typu Type (sic), kterou si podrobněji popíšeme příště. Ovšem užitečné je, že tuto hodnotu je možné převést na řetězec. To si ukážeme v dalším demonstračním příkladu:
package main import ( "fmt" "reflect" ) func main() { x := 42 v := reflect.ValueOf(x) t := v.Type() fmt.Println("type is: ", t) }
Výsledkem by mělo být:
type is: int
Dtto pro uživatelský datový typ:
package main import ( "fmt" "reflect" ) func test_get_type(x any) { value := reflect.ValueOf(x) typ := value.Type() fmt.Println("type is: ", typ) } type user struct { name string surname string } func main() { var nil1 interface{} = user{name: "foo", surname: "bar"} test_get_type(nil1) }
Výsledek:
type is: main.user
Použití ukazatelů:
package main import ( "fmt" "reflect" ) func test_get_type(x any) { value := reflect.ValueOf(x) typ := value.Type() fmt.Println("type is: ", typ) } func main() { var nil1 *int = nil test_get_type(nil1) }
Výsledek:
type is: *int
Pozor si ovšem musíme dát u hodnoty typu prázdné rozhraní a hodnotu nil:
package main import ( "fmt" "reflect" ) func test_get_type(x any) { value := reflect.ValueOf(x) typ := value.Type() fmt.Println("type is: ", typ) } func main() { var nil1 interface{} = nil test_get_type(nil1) }
Zde dojde k běhové výjimce!
reflect.Value.typeSlow({0x0?, 0x0?, 0x10052d540?}) /usr/local/go/src/reflect/value.go:2699 +0x113 reflect.Value.Type(...) /usr/local/go/src/reflect/value.go:2694 main.test_get_type({0x0?, 0x0?}) /home/ptisnovs/src/GoCourse/lesson14/get_type_empty_interface.go:10 +0x97 main.main() /home/ptisnovs/src/GoCourse/lesson14/get_type_empty_interface.go:16 +0x17
Proč tomu tak je, si řekneme příště.
19. Repositář s demonstračními příklady
Demonstrační příklady napsané v jazyce Go, které jsou určené pro překlad s využitím standardního překladače jazyka Go, byly uloženy do Git repositáře, jenž je dostupný na adrese https://github.com/RedHatOfficial/GoCourse. Jednotlivé demonstrační příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý (dnes již poměrně rozsáhlý) repositář:
20. Odkazy na Internetu
- The Go Programming Language Specification
https://golang.org/ref/spec - Go: the Good, the Bad and the Ugly
https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/ - Go Data Structures: Interfaces
https://research.swtch.com/interfaces - How to use interfaces in Go
http://jordanorelli.com/post/32665860244/how-to-use-interfaces-in-go - Interfaces in Go (part I)
https://medium.com/golangspec/interfaces-in-go-part-i-4ae53a97479c - The Laws of Reflection
https://go.dev/blog/laws-of-reflection - Standardní balíček reflect
https://pkg.go.dev/reflect - Reflection in Go: Use cases and tutorial
https://blog.logrocket.com/reflection-go-use-cases-tutorial/ - Reflection in Golang
https://www.geeksforgeeks.org/reflection-in-golang/ - Reflexe (programování)
https://cs.wikipedia.org/wiki/Reflexe_(programov%C3%A1n%C3%AD) - Reflective programming
https://en.wikipedia.org/wiki/Reflective_programming - go2js
https://github.com/tredoe/go2js - GitHub repositář projektu GopherJS
https://github.com/gopherjs/gopherjs - How to use GopherJS to turn Go code into a JavaScript library
https://medium.com/@kentquirk/how-to-use-gopherjs-to-turn-go-code-into-a-javascript-library-1e947703db7a - Source to source compiler
https://en.wikipedia.org/wiki/Source-to-source_compiler - Binary recompiler
https://en.wikipedia.org/wiki/Binary_recompiler - py2many na GitHubu
https://github.com/py2many/py2many - py2many na PyPi
https://pypi.org/project/py2many/ - Awesome Transpilers
https://github.com/milahu/awesome-transpilers