6, GO programming mode: GO GENERATION

In this article, we will learn how to play the code generation of go language. Go language code generation is mainly used to solve the problem of programming genericity. The main problem of generic programming is that static type languages have types. Therefore, relevant algorithms or data processing programs need to be copied because of different types, which leads to the problem of coupling between data types and algorithm functions. Generic programming can solve such problems, that is, when writing code, you don't need to care about the type of processing data, you only need to care about the processing logic. Generic programming is a very important feature of static language. Without generics, it is difficult for us to be polymorphic and abstract, which will lead to great redundancy in our code.

Analogy in reality

For example, in reality, we use a screwdriver to make tools. For example, the screwdriver is originally a screw tightening action, but because there are too many types of screws, including flat mouth, cross mouth and hexagon... Screws also have sizes and sizes, our screwdriver has to adapt to all kinds of strange screw types (styles and sizes), It leads to making all kinds of screwdrivers.

The real abstraction is that the screwdriver should not care about the type of screw. Just pay attention to whether its function is complete and make it suitable for different types of screws. As shown below, this is the practical problem to be solved by the so-called generic programming.

Type checking of Go language

Because the Go language does not support real generics at present, we can only use excessive generics such as interface {} such as void *, which leads to the need for type checking in the actual process. There are two techniques for type checking in Go language: Type Assert and Reflection.

Type Assert

This technique is generally used to analyze a variable (type), which will return two values, variable and error. The first return value is the converted type, and the second is an error if the type cannot be converted.

For example, in the following example, we have a generic type container that can perform Put(val) and Get(). Note that it uses interface {} as a generic type

//Container is a generic container, accepting anything.
type Container []interface{}

//Put adds an element to the container.
func (c *Container) Put(elem interface{}) {
    *c = append(*c, elem)
}
//Get gets an element from the container.
func (c *Container) Get() interface{} {
    elem := (*c)[0]
    *c = (*c)[1:]
    return elem
}

In use, we can use it like this

intContainer := &Container{}
intContainer.Put(7)
intContainer.Put(42)

However, when you take out the data, because the type is interface {}, you have to make a transformation. If the transformation is successful, you can carry out the subsequent operations (because interface {} is too extensive, you can put it in any type). Here is an example of Type Assert:

// assert that the actual type is int
elem, ok := intContainer.Get().(int)
if !ok {
    fmt.Println("Unable to read an int from intContainer")
}

fmt.Printf("assertExample: %d (%T)\n", elem, elem)
Reflection

For reflection, we need to modify the above code as follows:

type Container struct {
    s reflect.Value
}
func NewContainer(t reflect.Type, size int) *Container {
    if size <=0  { size=64 }
    return &Container{
        s: reflect.MakeSlice(reflect.SliceOf(t), 0, size), 
    }
}
func (c *Container) Put(val interface{})  error {
    if reflect.ValueOf(val).Type() != c.s.Type().Elem() {
        return fmt.Errorf("Put: cannot put a %T into a slice of %s", 
            val, c.s.Type().Elem()))
    }
    c.s = reflect.Append(c.s, reflect.ValueOf(val))
    return nil
}
func (c *Container) Get(refval interface{}) error {
    if reflect.ValueOf(refval).Kind() != reflect.Ptr ||
        reflect.ValueOf(refval).Elem().Type() != c.s.Type().Elem() {
        return fmt.Errorf("Get: needs *%s but got %T", c.s.Type().Elem(), refval)
    }
    reflect.ValueOf(refval).Elem().Set( c.s.Index(0) )
    c.s = c.s.Slice(1, c.s.Len())
    return nil
}

The above code is not difficult to read. This is a play method that uses reflection completely, in which

  • In NewContainer(), a Slice will be initialized according to the type of parameter
  • When Put(), it will check whether val is consistent with Slice.
  • In Get(), we need to use an input parameter, because we have no way to return reflect Value or interface {}, or Type Assert
  • However, there is a type check, so there must be a reason why the check is wrong. Therefore, you need to return error

Therefore, when using the above code, it will look like the following:

f1 := 3.1415926
f2 := 1.41421356237

c := NewMyContainer(reflect.TypeOf(f1), 16)

if err := c.Put(f1); err != nil {
  panic(err)
}
if err := c.Put(f2); err != nil {
  panic(err)
}

g := 0.0

if err := c.Get(&g); err != nil {
  panic(err)
}
fmt.Printf("%v (%T)\n", g, g) //3.1415926 (float64)
fmt.Println(c.s.Index(0)) //1.4142135623

We can see that Type Assert is not used, but the code written with reflection is still a little complicated. So is there any good way?

It is the stone of the mountain

For C + +, the best language of generic programming, this kind of problem is solved by using Template.

//Use < class T > to describe generics
template <class T> 
T GetMax (T a, T b)  { 
    T result; 
    result = (a>b)? a : b; 
    return (result); 
} 
int i=5, j=6, k; 
//Generating functions of type int
k=GetMax<int>(i,j);

long l=10, m=5, n; 
//Generate a function of type long
n=GetMax<long>(l,m); 

The C + + compiler will analyze the code at compile time and automatically generate functions or classes of related types according to different variable types. C + + is called template concretization.

This technology is a compile time problem, so we don't need any type recognition at runtime, and our program will become cleaner.

So, can we use this technology of C + + in Go? The answer is yes, but the Go compiler doesn't help you. You need to do it yourself.

Go Generator

To play Go code generation, you need three things:

  1. A function template in which the corresponding placeholder is set.
  2. A script that replaces text according to rules and generates new code.
  3. One line of comment code.
Function template

We changed our previous example into a template. Named container tmp. Go on/ Template / down

package PACKAGE_NAME
type GENERIC_NAMEContainer struct {
    s []GENERIC_TYPE
}
func NewGENERIC_NAMEContainer() *GENERIC_NAMEContainer {
    return &GENERIC_NAMEContainer{s: []GENERIC_TYPE{}}
}
func (c *GENERIC_NAMEContainer) Put(val GENERIC_TYPE) {
    c.s = append(c.s, val)
}
func (c *GENERIC_NAMEContainer) Get() GENERIC_TYPE {
    r := c.s[0]
    c.s = c.s[1:]
    return r
}

We can see that we have the following placeholders in the function template:

  • PACKAGE_NAME – package name
  • GENERIC_NAME – first name
  • GENERIC_TYPE – actual type

Other codes are the same.

Function generation script

Then, we have a generation script called gen.sh, as follows:

#!/bin/bash

set -e

SRC_FILE=${1}
PACKAGE=${2}
TYPE=${3}
DES=${4}
#uppcase the first char
PREFIX="$(tr '[:lower:]' '[:upper:]' <<< ${TYPE:0:1})${TYPE:1}"

DES_FILE=$(echo ${TYPE}| tr '[:upper:]' '[:lower:]')_${DES}.go

sed 's/PACKAGE_NAME/'"${PACKAGE}"'/g' ${SRC_FILE} | \
    sed 's/GENERIC_TYPE/'"${TYPE}"'/g' | \
    sed 's/GENERIC_NAME/'"${PREFIX}"'/g' > ${DES_FILE}

It requires four parameters:

  • Template source file
  • Package name
  • Types of materialization actually needed
  • Suffix used to construct the destination file name

Then it will replace our above function template with sed command and generate it into the target file. (for the SED command, please refer to the< sed concise tutorial>)

Generate code

Next, we just need to make a special comment in the code:

//go:generate ./gen.sh ./template/container.tmp.go gen uint32 container
func generateUint32Example() {
    var u uint32 = 42
    c := NewUint32Container()
    c.Put(u)
    v := c.Get()
    fmt.Printf("generateExample: %d (%T)\n", v, v)
}

//go:generate ./gen.sh ./template/container.tmp.go gen string container
func generateStringExample() {
    var s string = "Hello"
    c := NewStringContainer()
    c.Put(s)
    v := c.Get()
    fmt.Printf("generateExample: %s (%T)\n", v, v)
}

Among them,

  • The first comment is that the generated package name is gen, the type is uint32, and the target file name is suffixed with container
  • The second comment is that the package name is gen, the type is string, and the target file name is suffixed with container

Then, directly execute the go generate command in the project directory to generate the following two codes,

One file is called uint32_container.go

package gen

type Uint32Container struct {
    s []uint32
}
func NewUint32Container() *Uint32Container {
    return &Uint32Container{s: []uint32{}}
}
func (c *Uint32Container) Put(val uint32) {
    c.s = append(c.s, val)
}
func (c *Uint32Container) Get() uint32 {
    r := c.s[0]
    c.s = c.s[1:]
    return r
}

Another file is called string_container.go

package gen

type StringContainer struct {
    s []string
}
func NewStringContainer() *StringContainer {
    return &StringContainer{s: []string{}}
}
func (c *StringContainer) Put(val string) {
    c.s = append(c.s, val)
}
func (c *StringContainer) Get() string {
    r := c.s[0]
    c.s = c.s[1:]
    return r
}

These two codes can fully compile our code, and the price we pay is to execute one more step of the go generate command.

New Filter

Now let's look back at what we did before< Go programming mode: Map reduce >With this technology, I don't have to use those obscure reflections in the code to do runtime type checking. We can write clean code and let the compiler check whether the type is right at compile time. The following is a Fitler template file filter tmp. go:

package PACKAGE_NAME

type GENERIC_NAMEList []GENERIC_TYPE

type GENERIC_NAMEToBool func(*GENERIC_TYPE) bool

func (al GENERIC_NAMEList) Filter(f GENERIC_NAMEToBool) GENERIC_NAMEList {
    var ret GENERIC_NAMEList
    for _, a := range al {
        if f(&a) {
            ret = append(ret, a)
        }
    }
    return ret
}

So we can add relevant go generate comments where we need to use this

type Employee struct {
  Name     string
  Age      int
  Vacation int
  Salary   int
}

//go:generate ./gen.sh ./template/filter.tmp.go gen Employee filter
func filterEmployeeExample() {

  var list = EmployeeList{
    {"Hao", 44, 0, 8000},
    {"Bob", 34, 10, 5000},
    {"Alice", 23, 5, 9000},
    {"Jack", 26, 0, 4000},
    {"Tom", 48, 9, 7500},
  }

  var filter EmployeeList
  filter = list.Filter(func(e *Employee) bool {
    return e.Age > 40
  })

  fmt.Println("----- Employee.Age > 40 ------")
  for _, e := range filter {
    fmt.Println(e)
  }

  filter = list.Filter(func(e *Employee) bool {
    return e.Salary <= 5000
  })

  fmt.Println("----- Employee.Salary <= 5000 ------")
  for _, e := range filter {
    fmt.Println(e)
  }
}

Third party tools

We don't need to write tools like gen.sh by ourselves. There are many third-party tools that can be used. Here is a list:

(end of the full text) this article is not made by myself. It reprints the left ear mouse blog and its source Cool shell – CoolShell

Keywords: Go

Added by qing on Tue, 08 Feb 2022 16:00:54 +0200