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:
- A function template in which the corresponding placeholder is set.
- A script that replaces text according to rules and generates new code.
- 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:
- Genny – github.com/cheekybits/genny
- Generic – github.com/taylorchu/generic
- GenGen – github.com/joeshaw/gengen
- Gen – github.com/clipperhouse/gen
(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