Reflexia je schopnosť programu skúmať a analyzovať svoju štruktúru za behu programu.
Väčšinou s premennými, typmi a funkciami v Go pracujeme priamo. Keď potrebujeme typ, jednoducho ho definujeme:
type Foo struct {
A int
B string
}
Keď potrebujeme premennú, jednoducho ju definujeme:
var f Foo
A keď potrebujeme pracovať s funkciou, jednoducho ju definujeme:
func Do(f Foo) {
fmt.Println(f.A, f.B)
}
Ale niekedy sa stane, že chceme pracovať s premennými za behu programu pomocou informácií, ktoré neexistovali, keď bol program písaný a kompilovaný. Môže to nastať napríklad vtedy, keď sa do premennej snažíme namapovať údaje zo súboru alebo zo sieťovej požiadavky. Alebo chceme vytvoriť nástroj, ktorý pracuje s rôznymi typmi a metódami známymi až po spustení programu. V týchto situáciách musíme použiť reflexiu. Reflexia nám dáva možnosť skúmať typy za behu programu. Umožňuje tiež skúmať, upravovať a vytvárať premenné, funkcie a štruktúry počas behu programu.
Čo však v situácii
- Keď potrebujeme dynamicky vytvoriť inštanciu typu?
- Zavolať všetky metódy na ľubovoľnom vstupnom type, alebo len tie s určitou signatúrou (napríklad *_test)?
- Zistiť typ objektu počas runtime, aké metódy a polia obsahuje?
- Pripojiť typ k existujúcemu objektu?
- Spustiť metódu alebo pristúpiť k poliam existujúceho objektu, ktoré nie sú pred spustením programu známe?
- Keď vytvárame framework, ale dopredu nevieme s akými typmi budú užívatelia pracovať, alebo nechceme užívateľov obmedziť?
Balík reflect
v štandardnej knižnici združuje typy a funkcie, ktoré implementujú reflexiu v Go.
Reflexia v Go je postavená na troch konceptoch:
- Typy (types)
- Druhy (kinds)
- Hodnoty (values)
Typické použitie je vziať hodnotu pomocou statického typu interface{}
a extrahovať informácie o jej dynamickom type volaním reflect.TypeOf
, ktorý vráti Type
.
Volanie funkcie reflect.ValueOf
vráti hodnotu predstavujúcu údaje za behu.
Funkcia reflect.Zero
vezme typ a vráti hodnotu predstavujúcu nulovú hodnotu pre tento typ.
package main
import (
"fmt"
"reflect"
)
type employee struct {
id int
name string
}
func display(q interface{}) {
t := reflect.TypeOf(q)
v := reflect.ValueOf(q)
z := reflect.Zero(t)
k := t.Kind()
fmt.Println("Type ", t)
fmt.Println("Value ", v)
fmt.Println("Zero ", z)
fmt.Println("Kind ", k)
}
func main() {
e := employee{
id: 5889106,
name: "Joe",
}
display(e)
}
Type main.employee
Value {5889106 Joe}
Zero {0 }
Kind struct
V balíku reflect
je ešte jedna dôležitá metóda s názvom Kind
. Kind vracia typ = druh, z ktorého je typ vyrobený - čiže napíklad slice, mapa, ukazovateľ, štruktúra, rozhranie, reťazec, pole, funkcia, integer alebo iný primitívny typ.
Rozdiel medzi Type
a Kind
môže byť zložitý na pochopenie, ale treba sa na to pozerať takto. Ak definujete štruktúru s názvom employee, druh je struct a typ je employee.
Pri používaní reflexie by ste si mali uvedomiť jednu vec: všetko v balíku
reflect
predpokladá, že viete, čo robíte, a veľa volaní funkcií a metód spôsobí paniku, ak sa použije nesprávne. Napríklad, ak zavoláme metódu reflect.Type, ktorá je priradená k inému typu ako je ten súčasný, kód spanikári.
Ak skúmaná premenná je ukazovateľ, mapa, slice, kanál alebo pole, obsiahnutý typ zistíme pomocou varType.Elem()
.
// Golang program to illustrate
// reflect.Elem() Function
package main
import (
"fmt"
"reflect"
)
type Book struct {
Id int
Title string
Price float32
Authors []string
}
// Main function
func main() {
book := Book{}
//use of Elem() method
e := reflect.ValueOf(&book).Elem()
for i := 0; i < e.NumField(); i++ {
varName := e.Type().Field(i).Name
varType := e.Type().Field(i).Type
varValue := e.Field(i).Interface()
fmt.Printf("%v %v %v\n", varName, varType, varValue)
}
}
Id int 0
Title string
Price float32 0
Authors []string []
Ak je premenná štruktúra, môžeme pomocou reflexie získať počet polí v štruktúre a získať späť štruktúru každého poľa obsiahnutú v štruktúre reflect.StructField
. Reflect.StructField
vracia názvy, poradie, typ a štruktúru tagov na prvkoch.
Ak chceme upraviť hodnotu, nezabudnite, že musí ísť o ukazovateľ, a musíme najskôr dereferencovať ukazovateľ. Na vykonanie zmeny používame refPtrVal.Elem().Set(newRefVal)
a hodnota odovzdaná do Set()
musí zodpovedať reflect.Value
.
https://golang.org/src/reflect/all_test.go
fields := []StructField{
{
Name: "S",
Tag: "s",
Type: TypeOf(""),
},
{
Name: "X",
Tag: "x",
Type: TypeOf(byte(0)),
},
{
Name: "Y",
Type: TypeOf(uint64(0)),
},
{
Name: "Z",
Type: TypeOf([3]uint16{}),
},
}
st := StructOf(fields)
v := New(st).Elem()
runtime.GC()
v.FieldByName("X").Set(ValueOf(byte(2)))
v.FieldByIndex([]int{1}).Set(ValueOf(byte(1)))
runtime.GC()
Ak chceme vytvoriť novú hodnotu, môžeme tak urobiť pomocou volania funkcie newPtrVal := reflect.New(varType)
, ktoré odovzdá reflect.Type
. Vráti hodnotu ukazovateľa, ktorú môžeme potom upraviť pomocou Elem().Set()
.
fType := reflect.TypeOf(e)
fVal := reflect.New(fType)
fVal.Elem().Field(0).SetInt(252280) // value has to be exported!
fVal.Elem().Field(1).SetString("Joseph")
f2 := fVal.Elem().Interface().(employee)
fmt.Printf("%+v, %d, %s\n", f2, f2.id, f2.name)
Okrem vytvárania inštancií vstavaných a používateľom definovaných typov môžeme tiež použiť reflexiu na vytvorenie inštancií, ktoré zvyčajne vyžadujú funkciu make
. Pomocou funkcií reflect.MakeSlice
, reflect.MakeMap
a reflect.MakeChan
môžeme vytvoriť slice, mapu alebo kanál.
intSlice := make([]int, 0)
sliceType := reflect.TypeOf(intSlice)
intSliceReflect := reflect.MakeSlice(sliceType, 0, 0)
v := 10
rv := reflect.ValueOf(v)
intSliceReflect = reflect.Append(intSliceReflect, rv)
intSlice2 := intSliceReflect.Interface().([]int)
fmt.Println(intSlice2)
mapStringInt := make(map[string]int)
mapType := reflect.TypeOf(mapStringInt)
mapReflect := reflect.MakeMap(mapType)
k := "hello"
rk := reflect.ValueOf(k)
mapReflect.SetMapIndex(rk, rv)
mapStringInt2 := mapReflect.Interface().(map[string]int)
fmt.Println(mapStringInt2)
Reflexia neumožňuje iba vytvárať nové miesta na ukladanie údajov. Pomocou reflexie môžeme vytvoriť nové funkcie použitím funkcie reflect.MakeFunc
. Táto funkcia očakáva reflect.Type
pre funkciu, ktorú chceme urobiť, a záver, ktorého vstupné parametre sú typu []reflect.Value
a ktorého výstupné parametre sú tiež typu []reflect.Value
.
package main
import (
"fmt"
"reflect"
)
func InvertSlice(args []reflect.Value) (result []reflect.Value) {
inSlice, n := args[0], args[0].Len()
outSlice := reflect.MakeSlice(inSlice.Type(), 0, n)
for i := n-1; i >= 0; i-- {
element := inSlice.Index(i)
outSlice = reflect.Append(outSlice, element)
}
return []reflect.Value{outSlice}
}
func Bind(p interface{}, f func ([]reflect.Value) []reflect.Value) {
invert := reflect.ValueOf(p).Elem()
//Use of MakeFunc() method
invert.Set(reflect.MakeFunc(invert.Type(), f))
}
// Main function
func main() {
var invertInts func([]int) []int
Bind(&invertInts, InvertSlice)
fmt.Println(invertInts([]int{1, 2, 3, 4, 2, 3, 5}))
}
Existuje ešte jedna vec, ktorú môžeme urobiť pomocou reflexie v Go. Za behu programu môžeme vytvoriť úplne nové štruktúry odovzdaním časti inštancií reflect.StructField
do funkcie reflect.StructOf
. Toto je trochu zvláštne; vyrábame nový typ, ale nemáme preň názov, takže ho skutočne nemôžeme zmeniť na „normálnu“ premennú. Môžeme vytvoriť novú inštanciu a použiť Interface()
na vloženie hodnoty do premennej typu interface{}
, ale ak na nej chceme nastaviť akékoľvek hodnoty, musíme použiť reflexiu.
func MakeStruct(vals ...interface{}) interface{} {
var sfs []reflect.StructField
for k, v := range vals {
t := reflect.TypeOf(v)
sf := reflect.StructField{
Name: fmt.Sprintf("F%d", (k + 1)),
Type: t,
}
sfs = append(sfs, sf)
}
st := reflect.StructOf(sfs)
so := reflect.New(st)
return so.Interface()
}
Existuje jedno veľké obmedzenie reflexie. Aj keď na vytvorenie nových funkcií môžeme použiť reflexiu, za behu programu nie je možné vytvoriť nové metódy. To znamená, že nemôžeme použiť reflexiu na implementáciu rozhrania za behu programu.
Zdroje
- Reflect. reflect package - reflect - pkg.go.dev. (n.d.). https://golang.org/pkg/reflect/
- Ramanathan, N. (2021, May 20). Reflection in Golang. Go Tutorial - Learn Go from the Basics with Code Examples. https://golangbot.com/reflection/
- Bodner, J. (2018, January 17). Learning to use go reflection. Medium. https://medium.com/capital-one-tech/learning-to-use-go-reflection-822a0aed74b7
Vaše otázky, návrhy a komentáre
Verím, že vás tento návod inšpiroval a budeme vďační, ak nám dáte spätnú väzbu a pomôžete nám zamerať sa na to, čo by vás zaujímalo.