Example of Golang methods using interfaces to implement generics

  • 2020-07-21 08:22:27
  • OfStack

In C/C++ we can use generic methods to make code reusable, most commonly stl functions: vector < int > vint or vector < float > vfloat, etc. This article will use interface{... The} interface enables Golang to implement generics.

interface {... } is the basis for implementing generics. For example, an array element of type interface{... }, then any entity that implements the interface can be placed in the array. Note that it does not have to be an empty interface (a simple type can be implemented by converting it to a custom type). Why methods are declared in interface: Because when we need to manipulate data in an array (such as comparing sizes), we need to declare a custom method for this operation. In other words, only entities that implement this method are allowed to be added to an array.

Basic Demo

In the Demo shown below, we will implement the simplest vector and perform insert-time sorting.


type Comper interface{
  Lessthan (Comper) bool
}
type Sdata struct{
  data []Comper
}

func (t *Sdata) Push (item Comper){
  t.data = append(t.data, item)
  for k,v:=range t.data{
    if item.Lessthan(v) {  // Call the method defined by the interface 
      // The sorting operation 
      break
    }
  }
}

In this way, the simplest Demo is implemented. The Lessthan method must be implemented before using the array element of Sdata:


type Myint int

func (t Myint) Lessthan (x Comper) bool {
  return t<x.(Myint)
}
func main() {
  mydata := Sdata{make([]Comper, 0)}
  for i:=10;i>0;i--{
    mydata.Push((Myint(i)))
  }
  fmt.Println(mydata)
}

However, there are many disadvantages of this Demo. 1 is that simple type elements cannot be sorted by Sdata, and 2 is that concurrency is not supported, which can produce unexpected results in the case of concurrency.

Support for simple types of Demo via Reflect

To support simple types, we can only use an empty interface as an array element type. Here's the logic: If this is a simple type, we call the built-in" < "And" > "To compare; If this is not a simple type, we still call the Lessthan method:


type Comper interface{
  Lessthan (Comper) bool
}
type Sdata struct{
  data []interface{}
}

func (t *Sdata) Push (item interface{}){
  for _,v:=range t.data{
    if reflect.TypeOf(item).Implements(reflect.TypeOf(new(Comper)).Elem()) {
      citem:=item.(Comper)
      cv:=v.(Comper)
      if citem.Lessthan(cv) {
        // The operation to perform 
        break
      }
    }else{
      x,v:=reflect.ValueOf(item),reflect.ValueOf(v)
      switch x.Kind() {
      case reflect.Int:
      case reflect.Int8:
      case reflect.Int16:
        /*...*/
        //x, y:=x.Int(), y.Int()
        /*...*/
        break
      case reflect.Uint:
        /*...*/
      }
    }
  }
}

Use reflect to judge the type of item:

reflect.TypeOf (item).ES65en (reflect.TypeOf (new(comper)).ES70en ()), that is, whether the item type implements the comper interface type. TypeOf(new(comper)) is a pointer to ptr, and Elem() converts the pointer to a value. If this function returns true, you can force item and v from interface{} to the Comper interface, calling Lessthan(...). ; Of course, you can also use type assertion, which is simpler and more common. I'm just trying to use reflection here: if v,ok:=item. ok {... }

The value type cannot be directly compared in size:

value type cannot pass" > "And" < "Compare size directly, even though we know it's a simple type. The author has not found an easy way to convert values directly to simple types and compare them, so the method of enumeration is adopted. If there is an easier way, please let me know.

If an instance pointer is used to implement the interface:

This is a more difficult problem to spot and involves the TYPE system of golang. That is, if we implement Lessthen like func (t*Myint) Lessthan (x Comper) bool, then it is likely that your assertion item type will fail. We can look at the type of item at this time:


fmt.Println(reflect.TypeOf(t.data[0])) //main.XXX

This is not what we expected, as we know that only the *T type method set is S and *S, while the T type method set is only S. Obviously, main.XXX's method set does not include the Lessthan method, only * ES127en.XXX does. So the correct way to use it is to assign the pointer type at the beginning of the assignment:


mi := Myint(i)
mydata.Push(&mi)

Multi-interface layering Demo

Empty interface is actually just a special use case. After we generalize it, we can find that we can define multiple interfaces and declare multiple methods. Entities that implement several methods are entitled to call several functions:

For example, we can grant read permissions, write permissions and delete permissions to correspond to different requirements:


type Reader interface {
  Read () interface{}
}
type Writer interface {
  Write (Writer)
}
type ReadWriter interface {
  Reader
  Writer
}
type Remover interface {
  Remove ()
}

type Sdata struct {
  data []interface{}
}

func (t *Sdata)Get(i int)interface{}{
  if len(t.data) == 0{return nil}
  if reflect.TypeOf(t.data[0]).Implements(reflect.TypeOf(new(Reader)).Elem()) == true{
    return t.data[i].(Reader).Read()
  }
}

func (t *Sdata)Modify(i int, w Writer){
  // if reflect.TypeOf(t.data[0]).Implements(reflect.TypeOf(new(ReadWriter)).Elem()) == true
  if _,ok:=t.data[0].(ReadWriter);ok{
    t.data[i].(Writer).Write(w)
  }
}
//......

Custom Myint type and implementation of Reader, Writer interface:


type Readint int
func (t Readint) Read() interface{}{
  return int(t)
}
//---------------------------------------------
type Myint int
func (t Myint) Read() interface{}{
  return int(t)
}
func (t *Myint) Write(w Writer){
  *t = *w.(*Myint)
  return
}

func main() {
  mydata := Sdata{make([]interface{}, 1)}
  var u,v Myint = 5,6
  mydata.data[0] = &u
  fmt.Println("Myint is ", mydata.Get(0))
  mydata.Modify(0,&v)
  fmt.Println("Myint is ", mydata.Get(0))

  var ru Readint = 100
  readdata := Sdata{make([]interface{}, 1)}
  readdata.data[0] = &ru
  fmt.Println("Readint is ", readdata.Get(0))
  //var rv Readint = 101
  readdata.Modify(0,&v) // In fact, if you pass rv The compilation will not pass at all. 
  fmt.Println("Readint is ", readdata.Get(0))
}

[

Operation results:
Myint is 5
Myint is 6
Readint is 100
Readint is 100

]

Note: if the above code is passed as considered & rv does not compile without type checking, which is undesirable. Because for the empty interface interface{}, it doesn't matter the type of the entity, only whether the method is implemented or not & v is reasonable. Also, because this Demo is a simplified version, the permissions section is based solely on the permissions of the 0th element. In fact, the judgment permission should be completed at initialization time and stored in the structure variable.

Finally, regarding concurrency, apply read-write locks. Too simple no longer passes the Demo verification.


Related articles: