Understanding Go's Empty Interface

Understanding Go's Empty Interface

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?


You might also be interested in these tutorials too...

Restrict Allowed Route Methods in Go Web Apps
Fri, Sep 1, 2023

Restrict Allowed Route Methods in Go Web Apps

It’s incredibly easy to build web apps and APIs using Go’s net/http package. All you really need to do is to specify a route and its handler. But, how do you restrict the methods that routes can accept? In this short tutorial, I’ll show you how.

Go Interfaces Make Development and Testing Easier
Fri, Jul 21, 2023

Go Interfaces Make Development and Testing Easier

Substitutability or the Liskov Substitution Principle (LSP) is a concept that I’ve tried to adhere to for some years when writing code. It’s beneficial for many reasons, but particularly when testing, as it can indirectly force you to write code that is more testable. Recently, I’ve started appreciating how it works in Go, and will step through how in this short article.

Restrict HTTP Request Methods in Go 1.22
Thu, Apr 11, 2024

Restrict HTTP Request Methods in Go 1.22

Restricting HTTP request methods, when working with net/http prior to Go 1.22 was a little complicated. However, that’s all changed. Now, it’s pretty trivial. In this short tutorial, I’ll show you the new approach.

Go mod tidy - A Quick Introduction
Fri, Apr 5, 2024

Go mod tidy - A Quick Introduction

What is go mod tidy and why you would use it? In this short tutorial you’ll get a brief introduction to what it is and see the essentials of how to use it.


Want more tutorials like this?

If so, enter your email address in the field below and click subscribe.

You can unsubscribe at any time by clicking the link in the footer of the emails you'll receive. Here's my privacy policy, if you'd like to know more. I use Mailchimp to send emails. You can learn more about their privacy practices here.

Join the discussion

comments powered by Disqus