3 minutes
Poor man’s generics in Golang (Go) [or pick your poison]
Golang does not have generics. Period.
With that in mind, I just wish to share how I am thinking of implementing generic code. For this example, I am going to create a generic Stack data-structure.
type Stack struct {
T reflect.Type
arr []interface{}
}
Yes, it wraps around a slice of interface{}
type. And yes, that means loosing static safety. I also do store the Type
of values that are permitted to be added to the stack. Storing this Type
is what would help us to prevent dirty data from entering our Stack
and wreaking havoc later.
Next, is the constructor function for our Stack
.
func New(T reflect.Type) *Stack {
return &Stack{T: T}
}
Also, lets define some errors that we shall use for indicating unacceptable conditions in our Stack
implementation.
var TypeMismatchError = errors.New("new value does not match the type of the stack")
var EmptyStackError = errors.New("unable to Peek/Pop and empty stack")
While the API for our Stack may contain many functions, I am going to demonstrate the two that are the most useful, namely the Pop()
and Push()
functions.
func (s *Stack) Pop() interface{} {
var val interface{}
val, s.arr = s.arr[len(s.arr)-1], s.arr[:len(s.arr)-1]
return val
}
Nothing special there. It pops the last element from the arr
slice and returns it as an interface{}
type. This means, that we need to convert the popped value back to the original type, before we can use it in the client code.
func (s *Stack) Push(e interface{}) (*Stack, bool, error) {
if reflect.TypeOf(e) != s.T {
return s, false, TypeMismatchError
}
s.arr = append(s.arr, e)
return s, true, nil
}
The Push
function is where I stop unsupported data type from entering our Stack
and limit the side effects of using the interface{}
type a bit. Rather than throwing a panic
, we return the success/failure status as a bool and the error (TypeMismatchError in case, addition of unsupported type was attempted). By stopping dirty data from entering the Stack, we can be assured that our code wont break when converting the popped values back to their original types.
The following code demonstrates the usage of our generic Stack.
func main() {
// create a new Stack to hold the value of type MyStruct
st := NewStack(reflect.TypeOf(MyStruct{}))
// push some values onto the stack
st.Push(MyStruct{"hello"})
st.Push(MyStruct{"world"})
st.Push(MyStruct{"again"})
// pop values from stack
v := st.Pop().(MyStruct) // need to convert the interface{} to MyStruct
fmt.Println(v.msg) // "again"
v = st.Pop().(MyStruct)
fmt.Println(v.msg) // "world"
// try pushing some illegal value onto stack
var success bool
var err error
st, success, err = st.Push(1)
fmt.Println("1 pushed to stack", success) // "1 pushed to stack false"
fmt.Println("error returned via pushing to stack:", err) // "error returned via pushing to stack: new value does not match the type of the stack"
}
The above code is available for demo here
In conclusion:
- Golang doesnt have generics and there is nothing you can do to get true generics in Go.
- Writing generic code involves converting the data to and from
interface{}
values - This leads to decrease in performance. For example a stack of
int
type was 15 times faster in my personal benchmarks. And thats a lot ! - Using
interface{}
everywhere leads to the loss of the safety provided by static typing.
In short, pick your poison - Generic (like) code vs Static Safety and speed !