Golang reflect usage guide

Go s provide features such as variables, slices, structs, and so on, which can be easily defined and used.For example, when you want to define the type of a structure, simply define:

type A struct {
  Name string
}

However, when it comes to processing dynamic data structures, we don't know the structure of unknown data at the compilation stage. One of the most classic scenarios for using Json strings is Marshal.At this point, the reflect package comes out, providing the ability to create, update, and obtain various types of information at runtime. With it, we can not only effectively handle dynamic data types, but also greatly improve the reusability and readability of code.

Type

In the reflect package, Type is used to describe the type of an object in Go, and a series of methods are provided to get information about the type, typically by calling TypeOf to get the type Type of an arbitrary variable.

For example, Name() returns the specific name of the type, and String() returns the string representation of the type.

It's worth noting that the Kind() method returns a category of this type, which seems a bit awkward, but it's actually quite understandable. For example, type A struct {}, which is type A and category struct.Typically, at the beginning, we will determine the category of the incoming interface to avoid panic.Because some methods only apply to a certain category, arbitrary code can be easily panic, such as the NumField() method, which can only be used to get the number of fields whose Kind is a structure.

There is also a method, Elem(), that returns the Type of the child element of Type.For example, if Type is a pointer, Elem() returns the Type pointed to by the pointer, and if it is a slice, Elem() returns the type Type of the slice element.For example, *[]int, its Elem() method returns the Type of []int.The Elem() method of []int returns the Type of int.

import (
	"fmt"
	"reflect"
)

type A []int

func printInfo(t reflect.Type) {
	fmt.Printf("Kind = %s\tName = %s\n", t.Kind(), t.Name())
}

func main() {
	a := &A{}
	printInfo(reflect.TypeOf(a))
	printInfo(reflect.TypeOf(a).Elem())
	printInfo(reflect.TypeOf(a).Elem().Elem())
}

The output is as follows:

Kind = ptr      Name = 
Kind = slice    Name = A
Kind = int      Name = int

Value

Value describes the value of an object while Go Running. We can add, delete, and change the value of an object by using the ValueOf method.

Normally, we can modify the value of a variable through the Set() method.For example, the following code

	var a = 1
	val := reflect.ValueOf(&a)
	val.Elem().Set(reflect.ValueOf(2))
	fmt.Printf("a = %d", a)

Output:

a = 2

As you can see, the value of variable a has been modified from 1 to 2.

Use examples

Dynamic Initialization of Structures

In practice, struct is often used to represent a data structure (or object) and is very concise and understandable.However, the disadvantage is also evident, i.e., if you want to specify a default value for a field, you have to specify it manually in the constructor.It works, but it's not elegant enough to be readable.

type DS struct {
	FieldOne string
}

func NewDS() *DS {
	return &DS{
		FieldOne: "something",
	}
}

So how to optimize?It is very simple, that is, to use the tag information of the field.For example, in the code below, I set a default value in the tag.

type DS struct {
	FieldOne string `default:"something"`
}

I then use an initialization function, initStruct(), to read the tag and set the field defaults.

func NewDS() *DS {
	ds := &DS{}
	initStruct(ds)
	fmt.Printf("FieldOne = %s", ds.FieldOne)
	return ds
}

func initStruct(v interface{}) error {
   e := reflect.Indirect(reflect.ValueOf(v))
   if e.Kind() != reflect.Struct {
      return errors.New("v must be struct")
   }
   et, ev := e.Type(), e
   for i := 0; i < et.NumField(); i++ {
      field, val := et.Field(i), ev.Field(i)
      defaultValue, ok := field.Tag.Lookup("default")
      if !ok {
         continue
      }
      switch field.Type.Kind() {
      case reflect.String:
         val.SetString(defaultValue)
      case reflect.Int:
         if x, err := strconv.ParseInt(defaultValue, 10, 64); err != nil {
            val.SetInt(x)
         }
      // Convert defaultValue to corresponding type and assign values for different Kind s
      ...
      }
   }
   return nil
}

At this point, we can easily and elegantly set default values for the structure, and of course, you can set other dynamic properties in the tag to dynamically change the structure.

Dynamic Map Creation

Typically, we create a map by making, and with the reflect package, we can also create a map dynamically by using the reflet package.

Here, we have a requirement to convert a rectangular structure into a map with additional requirements, such as a floating-point field that retains only two decimal places and converts to a string.

First, define a structure called Rectangle to represent a rectangle

type Rectangle struct {
	Name   string
	Unit   string
	Length float64
	Width  float64
}

Then, use a convert function to convert it to a map.

func convert(rectangle *Rectangle) (res map[string]string, err error) {
	e := reflect.Indirect(reflect.ValueOf(rectangle))
	if e.Kind() != reflect.Struct {
		return nil, errors.New("v must be struct")
	}
	et, ev := e.Type(), e

	var mapStringType = reflect.TypeOf(make(map[string]string))
	mapReflect := reflect.MakeMap(mapStringType)
	for i := 0; i < et.NumField(); i++ {
		field, val := et.Field(i), ev.Field(i)
		switch field.Type.Kind() {
		case reflect.String:
			mapReflect.SetMapIndex(reflect.ValueOf(field.Name), reflect.ValueOf(val.String()))
		case reflect.Float64:
			s := strconv.FormatFloat(val.Float(), 'f', 2, 64)
			mapReflect.SetMapIndex(reflect.ValueOf(field.Name), reflect.ValueOf(s))
		// other cases
		...
		}
	}
	return mapReflect.Interface().(map[string]string), nil
}

Finally, we can print out the converted map.

func main() {
	res, _ := convert(&Rectangle{
		Name: "rec-1",
		Unit: "cm",
		Length: 12.121764,
		Width: 5.989681,
	})
	fmt.Printf("res = %+v", res)
}

Output:

res = map[Length:12.12 Name:rec-1 Unit:cm Width:5.99]

summary

Now that you've finished a brief introduction to reflect, I'm sure you already have a general idea.Do you think this bag is powerful and you want to try it?But before you do, you should be reminded to keep the following points in mind.

  1. Reflection is mostly only applicable in scenarios with dynamic data types and is dangerous, so use native types whenever possible.
  2. Be careful with your writing, incorrect use of reflect makes it easy to panic, and you need to make sure your type uses the correct associated method and returns the error ahead of time.
  3. There is no silver bullet in the programming world, so reflect is not everything, for example, because you cannot create structures dynamically.

I am a light educated person and there are some inadequacies in this article. We welcome your comments and pointing out.

Reference resources

Tags: Go Programming JSON

Posted on Tue, 05 May 2020 10:41:26 -0700 by nocturne