GoLang Strict Typing and Interface Slices

Photo by Louise Lyshøj on Unsplash

This is a GoLang Experience Report.

…but you cannot define the rows parameter as type []RowInserter because []interface{} won’t match it, even though RowInserter is itself an interface.

Background

I am relatively new to programming Go, but I have been developing software professionally in a variety of programming languages on and off since the late 80’s. Go’s concept of a language in service of software engineering resonates with me greatly, and as such I am very interested in learning and using idiomatic Go.

Regarding my experience report, an advocate of adding generics to Go will read this post and declare it illustrates why Go needs generics. I however, am not a fan of generics, at least not how they have been implemented in other C-like languages. Thus I would prefer Go find an easier-to-reason-about method for handling concerns like I am reporting.

Scenario

Consider a scenario where you have a slice of a concrete struct. Let us call that struct’s type Item for our example. And let us also assume these Items implement a RowInserter interface which requires an InsertRow() method be declared:

type Item struct {
   id int
}
type RowInserter interface{
    InsertRow()	
}
func (i *Item) InsertRow() {
   fmt.Printf( "Item id=%d inserted.\n",i.id )
}

Since, for many of my use-cases, the insertion of rows is fairly generic I would like to create a general-purpose package with a method you just pass a slice of Items to and have it loop through the rows to insert the item by calling the items’ InsertRow() method for each item.

Here is a main() that creates there items and then calls my desired InsertRows() function as I just described:

func main() {
   items := make([]*Item,3)
   for i := range []int{1,2,3} {
	items[i] = &Item{i}
   }
   InsertRow(items) // This won't work...
}

But of course the above does not work.

Instead we need to first loop through the slice of Items and add them to a slice of interface{}.

We could have littered our main() with this code, but instead I created an InsertItems() function that does the looping for us:

func InsertItems(items []*Item) {
    rows := make([]interface{}, len(items))
    for i, item := range items {
        rows[i] = item
    }
    InsertRows(rows)
}

Now we can implement our “generic” InsertRows(). However, and here is one of the main tricks, you cannot define the rows parameter as type []RowInserter because []interface{} won’t match it, even though RowInserter is itself an interface:

func InsertRows(rows []interface{}) {
    for _,r := range rows {
        r.(RowInserter).InsertRow()
    }
}

I have added all the the above in the Go playground. Please try it for yourself.

(Or maybe I just do not know how to make this easier to work with? Please comment if you know otherwise.)

Suggestion

Since InsertRows() accepts a parameter that is a slice of interface{} it would not violate type safety, other than in the most rigid perspective, for a slice of another type to be passed in and automatically converted to a slice of interface assuming the types satisfy the interface used.

Even better would be for when we want to pass a slice of types (e.g. []*Items) that implement a specific interface (e.g. RowInserter) are passed to a function that accepts a slice of that interface (e.g. []RowInserter) then Go could validate the elements of the slice for type safety and not the entire composite type.

Then my example could be simplified greatly to the following, and would also be much easier to reason about:

package main

import (
    "fmt"
)

type Item struct {
   id int
}

func main() {
   items := make([]*Item,3)
   for i := range []int{1,2,3} {
	items[i] = &Item{i}
   }
   InsertRows(items) 
}

func (i *Item) InsertRow() {
   fmt.Printf( "Item id=%d inserted.\n",i.id )
}

type RowInserter interface{
    InsertRow()	
}

func InsertRows(rows []RowInserter) {
    for _,r := range rows {
        r.InsertRow()
    }
}

Also, this same should apply for maps as well asfor slices, assuming the map keys are the same type.