Recently, while working with the Twilio Lookup API, I had the opportunity to properly learn about Go’s empty interface. During the process, I realised that I didn’t understand it properly, so took the opportunity to do so. Today, I am going to step through what it is and how simple they are to work with.
So, what is the empty interface?
Here’s a short quote from the Go Tour:
An empty interface may hold values of any type.
Empty interfaces are used by code that handles values of unknown type.
They’re one of the excellent ways that Go, while being a statically-typed language, also has some of the benefits of dynamically-typed languages, such as PHP, Ruby, and Python.
In the context of an API, for example, the empty interface affords the flexibility of returning data only when it’s available.
You don’t have to set an, effectively, empty value, just for the sake of doing so.
While I’m broadly familiar with them, I’d never really worked with them that much before.
However I had to, recently, while working with Twilio’s Lookup API using Twilio’s Go Helper Library.
If you’re not familiar with the API:
The Lookup v2 API allows you to query information on a phone number so that you can make a trusted interaction with your user. With this endpoint, you can format and validate phone numbers with the free Basic Lookup request and add on data packages to get even more in-depth carrier and caller information.
Have a look at the short example below.
package main
import (
"fmt"
"github.com/twilio/twilio-go"
lookups "github.com/twilio/twilio-go/rest/lookups/v1"
)
func main() {
client := twilio.NewRestClient()
params := &lookups.FetchPhoneNumberParams{}
params.SetType([]string{"caller-name"})
resp, err := client.LookupsV1.FetchPhoneNumber("+15108675310", params)
}
The call to FetchPhoneNumber()
does a check on +15108675310
and, if available, returns caller name details in the returned LookupsV2PhoneNumber
object.
If you look at LookupsV2PhoneNumber
’s definition, you’ll see that the CallerName
property stores a phone number’s caller name details, and is defined as a pointer to an empty interface, (*interface{}
).
Defining it this way affords it the ability to store caller name details — if they’re available.
It doesn’t mandate that the information must always be available.
Unfortunately (at least for this little learner) it left me wondering how to access the information, if it was available.
To the more seasoned Go developers, it likely wouldn’t even be a thought.
But, if you’re like me and still learning Go, then (at first at least) you might not know what to do.
I kept thinking about what I might do if this were PHP
For example, when working with laminas-hydrator, I’d hydrate an object with the data in the response, as in the following example (borrowed from the documentation):
<?php
$hydrator = new Laminas\Hydrator\ArraySerializableHydrator();
$data = [
'first_name' => 'James',
'last_name' => 'Kahn',
'email_address' => 'james.kahn@example.org',
'phone_number' => '+61 419 1234 5678',
];
$object = $hydrator->hydrate($data, new ArrayObject());
Here, it’s hydrating a new ArrayObject ($object
) from the contents of $data
.
After that, I’d be able to check if an element was set by calling ArrayObject’s offsetExists()
method.
If it did exist, I’d retrieve its value by calling offsetGet()
, as in the following example.
if ($object->offsetExists('first_name')) {
printf("%s\n",$object->offsetGet('first_name'));
}
Perhaps that was my problem.
I was still used to thinking in PHP, and dynamically-typed languages more generally.
After a bit of googling and learning more about the empty interface, it became clear as to what was required.
I also had a bit of a facepalm moment when I realised just how simple it was too. 🤦🏼
Specifically, here’s what I did.
if resp.CallerName != nil {
caller := *resp.CallerName
callerDetails, _ := caller.(map[string]interface{})
if callerName, ok := callerDetails["caller_name"]; ok && callerName != "" {
message = fmt.Sprintf("%s It is registered to: %s", message, callerName)
}
}
If the CallerName
was not nil
, I dereferenced it into a new variable named caller
.
Then, I cast the value to a new object named callerDetails
.
For what it’s worth, if CallerName
is set, it will be a map with string keys.
None of the keys need to be set, because data may not be available for them either, hence why they were cast as an empty interface.
Anyway, I digress.
At this point, I checked if a key was set and had a value.
If so, then the value was printed.
How do you find a variable’s type?
While this isn’t a complete list, if you’re wondering how to find out what a variable’s type is, I’m aware of two approaches:
- Type assertions
- The reflect package’s
TypeOf()
method
Type assertions
When it comes to type assertions, there’s a broad and a narrow approach.
The broad approach is to use a switch statement with value.(type)
, as in the example below.
switch v := value.(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
fmt.Println("It's an integer")
case string:
fmt.Println("It's a string")
case nil:
fmt.Println("Oh no! It's nil!")
default:
fmt.Println("I cannae do it, captain.")
}
By passing type
, it will return value
’s type.
Bear in mind, however, that you can only use type
with a switch statement.
Now, the narrow approach tests against a specific type, using the “comma ok” idiom, as in the following example; it tests if value
is a string.
if v, ok := value.(string); ok {
fmt.Println("It's a string")
}
Using the reflect package’s TypeOf method
The next way is to use the reflect package’s TypeOf method, as in the example below.
varType := reflect.TypeOf(sheetName)
if varType.Kind() == reflect.String {
fmt.Println("sheetName is a string.")
}
The method returns a reflect.Type
object.
You then can the object’s Kind()
method, which returns a reflect.Kind
object.
This object represents the variable’s type.
From there, you just need to compare the object to the applicable Type
you’re interested in.
Be careful when using the empty interface
After sharing this tutorial on LinkedIn, a friend of mine replied as follows:
The empty interface should be avoided as much as possible. If you ever use it, you are saying that variable can be anything, even types you don’t expect. Using it means you lose all the advantages of using a strongly typed language because the compiler can’t tell you that you made a mistake. You won’t find the mistake until runtime.
It got me to thinking a little more deeply about whether I should use it or not.
I agree with him, nearly 100%.
The empty interface does afford a lot of flexibility at the cost of, partly, forgoing type safety.
However — though a bit more work on your part — you can use type assertions and/or the reflect package to partly make up for what is lost.
So, at least at this stage in my journey with Go, I still feel that the empty interface is worth having as an option.
But, be considerate in your use of it and appreciate the implications mentioned above.
That’s a broad introduction to Go’s empty interface
If, like me, you’re coming to Go from another language — especially a dynamically-typed one — Go’s empty interface might seem a little strange.
Actually, they might even seem off-putting too.
They don’t need to be!
If it helps, just think of them as allowing a value to be empty or to be set.
Then, check if the value is set,
If so, cast it appropriately, then work with the object as you otherwise would.
What do you think?
Did I over-complicate the empty interface, or is this a common experience when first encountering them?
Actually, what do you commonly do?
Join the discussion
comments powered by Disqus