Reflexia v Go - reflection

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

  1. Keď potrebujeme dynamicky vytvoriť inštanciu typu?
  2. Zavolať všetky metódy na ľubovoľnom vstupnom type, alebo len tie s určitou signatúrou (napríklad *_test)?
  3. Zistiť typ objektu počas runtime, aké metódy a polia obsahuje?
  4. Pripojiť typ k existujúcemu objektu?
  5. Spustiť metódu alebo pristúpiť k poliam existujúceho objektu, ktoré nie sú pred spustením programu známe?
  6. 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

  1. Reflect. reflect package - reflect - pkg.go.dev. (n.d.). https://golang.org/pkg/reflect/
  2. Ramanathan, N. (2021, May 20). Reflection in Golang. Go Tutorial - Learn Go from the Basics with Code Examples. https://golangbot.com/reflection/
  3. 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.

TAK ČO HOVORÍŠ ?

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

Kontaktuj nás