About type switch usage in Go
25 Dec 2014One of the features that I have been relying a lot in Go is the type switch. This feature is specially useful when using interfaces since it helps us avoid having to do type assertions all the time.
A couple of examples below.
Example #1
Let’s suppose that we have 3 types A
, B
& C
,
where each one of these types can be assigned a Name
:
package main
import (
"fmt"
"reflect"
)
type A struct { Name string }
type B struct { Name string }
type C struct { Name string }
And we put them inside of a slice which can contain interfaces
:
func main() {
a := &A{Name: "A"}
b := &B{Name: "B"}
c := &C{Name: "C"}
t := make([]interface{}, 0)
t = append(t, a)
t = append(t, b)
t = append(t, c)
Without using type switch
We would end up doing something like this:
fmt.Println("--- without type switching")
for _, thing := range t {
switch thing.(type) {
case *A:
fmt.Println(reflect.TypeOf(thing), "is of type A. Name is:", thing.(*A).Name)
case *B:
fmt.Println(reflect.TypeOf(thing), "is of type B. Name is:", thing.(*B).Name)
case *C:
fmt.Println(reflect.TypeOf(thing), "is of type C. Name is:", thing.(*C).Name)
}
}
Results
--- without type switching *main.A is of type A. Name is: A *main.B is of type B. Name is: B *main.C is of type C. Name is: C
Using a type switch
Makes things a little more bearable:
fmt.Println("--- type switching on the item")
for _, thing := range t {
switch o := thing.(type) {
case *A:
fmt.Println(reflect.TypeOf(o), "is of type A. Name is:", o.Name)
case *B:
fmt.Println(reflect.TypeOf(o), "is of type B. Name is:", o.Name)
case *C:
fmt.Println(reflect.TypeOf(o), "is of type C. Name is:", o.Name)
}
}
Results
--- type switching on the item *main.A is of type A. Name is: A *main.B is of type B. Name is: B *main.C is of type C. Name is: C
Example #2
Let’s see how far we can take it. Now, let’s suppose that we want to handle A and B the same way.
Using type switch
, it works too
Since we want to handle A
and B
the same way, we could think
that we could group them into the same case
, which would work:
fmt.Println("--- Grouping A and B: ")
for _, thing := range t {
switch o := thing.(type) {
case *A, *B:
fmt.Println(reflect.TypeOf(o), "is of type A or B.")
case *C:
fmt.Println(reflect.TypeOf(o), "is of type C.")
}
}
Results
*main.A is of type A or B. *main.B is of type A or B. *main.C is of type C.
Until it doesn’t
Let’s suppose that we want to inspect the value of the Name
field. Then it breaks:
for _, thing := range t {
switch o := thing.(type) {
case *A, *B:
fmt.Println(reflect.TypeOf(o), "is of type A or B. Name is:", o.Name)
case *C:
fmt.Println(reflect.TypeOf(o), "is of type C. Name is:", o.Name)
}
}
The above would throw the following error:
o.Name undefined (type interface {} has no field or method Name)
Back to interface
What happened here is that by trying to group A
, B
types, we ended up again
with an interface
, so we cannot rely on the first type switch anymore.
We could type switch once more time then:
fmt.Println("--- Double type switch all the way")
for _, thing := range t {
switch o := thing.(type) {
case *A, *B:
switch oo := o.(type) {
case *A:
fmt.Println(reflect.TypeOf(o), "is of type A or B. Name is:", oo.Name)
case *B:
fmt.Println(reflect.TypeOf(o), "is of type A or B. Name is:", oo.Name)
}
case *C:
fmt.Println(reflect.TypeOf(o), "is of type C. Name is:", o.Name)
}
}
Results
--- Double type switch all the way
*main.A is of type A or B. Name is: A
*main.B is of type A or B. Name is: B
*main.C is of type C. Name is: C
…which looks a bit messy. A more straightforward way would be to flinch away our desire to make things “DRY”, still rely on the first type switch and just repeat more code:
fmt.Println("--- The Go Way™")
for _, thing := range t {
switch o := thing.(type) {
case *A:
fmt.Println(reflect.TypeOf(o), "is of type A or B. Name is:", o.Name)
case *B:
fmt.Println(reflect.TypeOf(o), "is of type A or B. Name is:", o.Name)
case *C:
fmt.Println(reflect.TypeOf(o), "is of type C. Name is:", o.Name)
}
}
…which results in:
--- The Go Way™ *main.A is of type A or B. Name is: A *main.B is of type A or B. Name is: B *main.C is of type C. Name is: C
Progress so far
So which one of the approaches is better?
I would say that probably the one with the
multiple case statements where the same line is repeated, since when
we type switch and have a case
statement with one more type,
we end up once again with an interface
, and we need yet another type switch
for it that generates more code which more or less says the
same thing, so it seems that it is about as DRY as it could get
for now using only type switch
.
Link to the Go playground with the example
To solve the grouping problem, there is something else that we can
try besides only using the type switch
, which is using interface
types.
Example #3
Another alternative would be to use a Nameable
interface when creating the slice.
This means first having to declare something like this:
type Nameable interface {
GetName() string
}
type BasicAttrs struct {
Name string
}
// Code generation could help with this...
type A struct { BasicAttrs }
func (o *A) GetName() string { return o.Name }
type B struct { BasicAttrs }
func (o *B) GetName() string { return o.Name }
type C struct { BasicAttrs }
func (o *C) GetName() string { return o.Name }
So that we can later on use it as follows:
func main() {
a := &A{BasicAttrs:BasicAttrs { Name: "A" }}
b := &B{BasicAttrs:BasicAttrs { Name: "B" }}
c := &C{BasicAttrs:BasicAttrs { Name: "C" }}
t := make([]Nameable, 0)
t = append(t, a)
t = append(t, b)
t = append(t, c)
fmt.Println("--- The correct Go way?")
for _, thing := range t {
switch o := thing.(type) {
case *A, *B:
fmt.Println(reflect.TypeOf(o), "is of type A or B. Name is:", o.GetName())
case *C:
fmt.Println(reflect.TypeOf(o), "is of type C. Name is:", o.GetName())
}
}
}
Results
--- The correct Go way? *main.A is of type A or B. Name is: A *main.B is of type A or B. Name is: B *main.C is of type C. Name is: C
Is this better?
I think so. Even though the implementation is a bit more verbose, the last example using a type interface
might be the way to go in case we face the grouping issue from the raw interface.