I see Go reflect[r] ɪˈ flekt] import "reflect" to create an SQL generator

The ideas of this paper refer to: https://golangbot.com/reflection/ , the content of this article is not just a simple translation of the original text. Please see the content below ~!

  Prepare to take you to understand and master reflection knowledge points by using reflection as an example of a general SQL constructor. After reading an example written by a foreign blogger, I think the idea is very good. I improved it to enrich the implementation of the constructor.

What is reflection?

A: reflection is that in the running state, all properties and methods of any class can be known, and any method and property of any object can be called. This dynamically obtained information and the function of dynamically calling the method of the object

Why reflection?

Golang reflective understanding - Go language Chinese network - golang Chinese community

When learning reflection, the first question everyone will think of is "why do we check the type of variables at run time? Don't we have specified the type of variables in the program when they are defined?" it's true, but it's not always the case. When you see this, you may think, brother, what are you talking about, em... Let's write a simple program first and explain it.

package main

import (  
    "fmt"
)

func main() {  
    a := 100
    fmt.Printf("%d %T", a, a)
}

In the above program, the type of variable a is known at compile time. We print its value and type on the next line.

Let's understand "the need to know the type of variable at run time". Suppose we want to write a simple function that takes a structure as a parameter and uses this parameter to create an SQL insert statement.

package main

import (  
    "fmt"
)

type order struct {  
    ordId      int
    customerId int
}

func main() {  
    o := order{
        ordId:      20211103000001,
        customerId: 567,
    }
    fmt.Println(o)
}

We need to write an SQL statement that receives the structure o defined above as a parameter and returns an insert into order values (202111030000001, 567). This function definition is easy to write, such as the following.

package main

import (  
    "fmt"
)

type order struct {  
    ordId      int
    customerId int
}

func createQuery(o order) string {  
    i := fmt.Sprintf("INSERT INTO order VALUES(%d, %d)", o.ordId, o.customerId)
    return i
}

func main() {  
    o := order{
        ordId:      20211103000001,
        customerId: 567,
    }
    fmt.Println(createQuery(o))
}

The createQuery in the above example uses the parameter o   Create SQL using the OrderID and customerId fields of.

Now let's define our SQL creation function more abstractly. Let's use the program to illustrate it. For example, we want to generalize our SQL creation function to make it applicable to any structure.

package main

type order struct {  
    ordId      int
    customerId int
}

type employee struct {  
    name string
    id int
    address string
    salary int
    country string
}

func createQuery(q interface{}) string {  
}

Now our goal is to transform the createQuery function so that it can accept any structure as a parameter and create INSERT based on the structure field   Statement. For example, if the parameter passed to createQuery is no longer an order type structure, but an employee type structure

 e := employee {
        name: "Naveen",
        id: 565,
        address: "Science Park Road, Singapore",
        salary: 90000,
        country: "Singapore",
    }




### The INSERT statement it should return should be

INSERT INTO employee (name, id, address, salary, country) 
VALUES("Naveen", 565, "Science Park Road, Singapore", 90000, "Singapore") 

Because createQuery   The function needs to apply to any structure, so it needs a   For the sake of simplicity, let's assume that the createQuery function only deals with parameters containing string s   and   int   The structure of the type field.

The only way to write this createQuery function is to check the type of parameters passed to it at run time, find its fields, and then create SQL. Here is where reflection is needed. In the next steps, we will learn how to use the reflection package of Go language to achieve this.

Go language reflection package       

    The reflection package of Go language implements the function of reflection at runtime. This package can help identify the specific type and value of an interface {} type variable. After receiving an interface {} type argument, our createQuery function needs to create and return an INSERT statement according to the underlying type and value of the argument, which is the function of the reflection package.

Before writing our general SQL generator function, we need to understand several types and methods we will use in the reflect package. Next, we will learn them one by one.

reflect.Type and reflect.Value

Go language standard library document Chinese version | go language Chinese network | Golang Chinese community | Golang China

After reflection, the underlying specific type of a variable of interface {} type is represented by reflect. Type, and the underlying value is represented by reflect. Value. There are two functions in the reflect package, reflect.TypeOf()   And reflect.ValueOf()   Variables of interface {} type can be converted to reflect.Type and reflect.Value respectively. These two types are the basis for creating our SQL generator function.

package main

import (  
    "fmt"
    "reflect"
)

type order struct {  
    ordId      int
    customerId int
}

func createQuery(q interface{}) {  
    t := reflect.TypeOf(q)
    v := reflect.ValueOf(q)
    fmt.Println("Type ", t)
    fmt.Println("Value ", v)


}
func main() {  
    o := order{
        ordId:      456,
        customerId: 56,
    }
    createQuery(o)
}


Run output:

Type  main.order  
Value  {456 56} 

In the above program, the createQuery function receives an interface {} type argument and passes the argument to reflect.Typeof and reflect.Valueof   From the output, we can see that the program outputs the underlying specific types and values corresponding to the interface {} type arguments.

Three principles of Go language reflection

The three principles of reflection are:

  1. The reflection object can be reflected from the interface value.

  2. The interface value can be reflected from the reflection object.

  3. To modify a reflective object, its value must be settable.

The first rule of reflection is that we can convert the interface type variables in Go into reflection objects, such as reflect.TypeOf and   reflect.ValueOf   The second refers to that we can convert the variable of reflection type back to the interface type, and the last relates to whether the reflection value can be changed.

Next, let's continue to understand the reflection knowledge required to complete our SQL generator.

reflect.Kind

There is also a very important type in the reflect package, reflect.Kind.

The types of reflect.Kind and reflect.Type may look very similar. In terms of naming, Kind and Type can be used interchangeably in some phrases in English, but they are quite different in reflection, which can be clearly seen from the following program.

package main
import (  
    "fmt"
    "reflect"
)

type order struct {  
    ordId      int
    customerId int
}

func createQuery(q interface{}) {  
    t := reflect.TypeOf(q)
    k := t.Kind()
    fmt.Println("Type ", t)
    fmt.Println("Kind ", k)


}
func main() {  
    o := order{
        ordId:      456,
        customerId: 56,
    }
    createQuery(o)

}



Run output:
Type  main.order  
Kind  struct 

The output makes us clear the difference between the two. reflect.Type   Represents the actual type of the interface, i.e. main.order in this example   Kind indicates the type to which the type belongs, that is, main.order is a "struct" type, and kind of map[string]string should be "map".

Method for obtaining structure field by reflection  

We can obtain the type attribute of the field under the structure through the method of type reflect.StructField. reflect.StructField can be obtained through the following two methods provided by reflect.Type.

// Gets the number of fields in a structure
NumField() int
// Get the type object of the field in the structure according to the index
Field(i int) StructField
// Gets the type object of the field in the structure according to the field name
FieldByName(name string) (StructField, bool)

reflect.structField is a struct type. Through it, we can know the basic type, Tag, exported or not of the field in reflection.

type StructField struct {
 Name string
 Type      Type      // field type
 Tag       StructTag // field tag string
  ......
}

Corresponding to the method of obtaining Field information provided by reflect.Type, reflect.Value also provides the method of obtaining Field value.

We need to pay attention to this, otherwise it is easy to be confused. Let's try to get the field name and value of the order structure type through reflection

package main

import (
 "fmt"
 "reflect"
)

type order struct {
 ordId      int
 customerId int
}

func createQuery(q interface{}) {
 t := reflect.TypeOf(q)
 if t.Kind() != reflect.Struct {
  panic("unsupported argument type!")
 }
 v := reflect.ValueOf(q)
 for i:=0; i < t.NumField(); i++ {
  fmt.Println("FieldName:", t.Field(i).Name, "FiledType:", t.Field(i).Type,
   "FiledValue:", v.Field(i))
 }

}
func main() {
 o := order{
  ordId:      456,
  customerId: 56,
 }
 createQuery(o)

}


Run output:

FieldName: ordId FiledType: int FiledValue: 456
FieldName: customerId FiledType: int FiledValue: 56

In addition to obtaining the name and value of the structure field, you can also obtain the Tag of the structure field.

Convert reflect.Value to actual value

Now we are still one step away from completing our SQL generator, that is, we need to convert reflect.Value into a value of actual type. reflect.Value implements a series of Int(),   String(), Float() to complete the conversion to the actual type value.

Reflection is an SQL generator

Above, we have learned all the necessary knowledge points before writing this SQL generator function. Next, we will string them to complete the createQuery function.

The complete implementation and test code of SQL generator is as follows:

package main

import (
 "fmt"
 "reflect"
)

type order struct {
 ordId      int
 customerId int
}

type employee struct {
 name    string
 id      int
 address string
 salary  int
 country string
}

func createQuery(q interface{}) string {
 t := reflect.TypeOf(q)
 v := reflect.ValueOf(q)
 if v.Kind() != reflect.Struct {
  panic("unsupported argument type!")
 }
 tableName := t.Name() // Extract the SQL table name through the structure type
 sql := fmt.Sprintf("INSERT INTO %s ", tableName)
 columns := "("
 values := "VALUES ("
 for i := 0; i < v.NumField(); i++ {
  // Note that reflect.Value also implements numfield and kind methods
  // v.Field(i).Kind() here is equivalent to t.Field(i).Type.Kind()
  switch v.Field(i).Kind() {
  case reflect.Int:
   if i == 0 {
    columns += fmt.Sprintf("%s", t.Field(i).Name)
    values += fmt.Sprintf("%d", v.Field(i).Int())
   } else {
    columns += fmt.Sprintf(", %s", t.Field(i).Name)
    values += fmt.Sprintf(", %d", v.Field(i).Int())
   }
  case reflect.String:
   if i == 0 {
    columns += fmt.Sprintf("%s", t.Field(i).Name)
    values += fmt.Sprintf("'%s'", v.Field(i).String())
   } else {
    columns += fmt.Sprintf(", %s", t.Field(i).Name)
    values += fmt.Sprintf(", '%s'", v.Field(i).String())
   }
  }
 }
 columns += "); "
 values += "); "
 sql += columns + values
 fmt.Println(sql)
 return sql
}

func main() {
 o := order{
  ordId:      456,
  customerId: 56,
 }
 createQuery(o)

 e := employee{
  name:    "Naveen",
  id:      565,
  address: "Coimbatore",
  salary:  90000,
  country: "India",
 }
 createQuery(e)
}

Output the corresponding standard SQL insert statement
 

Clear is better than clever. Reflection is never clear.

Reflection is a very powerful and advanced concept in Go and it should be used with care. It is very difficult to write clear and maintainable code using reflection. It should be avoided wherever possible and should be used only when absolutely necessary.

Keywords: Go

Added by shimano55 on Wed, 03 Nov 2021 03:42:01 +0200