Skip to Content
Course content

239: Empty Interface Usage and any

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this mistake constantly when developers coming from Python or TypeScript start with Go: they treat any (or interface{}) as a way to "turn off" the type system. They think that by marking a variable as any, they've created a dynamic object that they can just call methods on, hoping the right method exists at runtime.

Let's look at why that's a dangerous assumption. Suppose you have a User struct and a Company struct, both of which have a Describe() method. You might try to do something like this:

type User struct{ Name string }
func (u User) Describe() string { return "User: " + u.Name }

type Company struct{ Name string }
func (c Company) Describe() string { return "Company: " + c.Name }

func PrintDetails(item any) {
    // This will NOT compile
    fmt.Println(item.Describe()) 
}

The compiler will scream at you. Why? Because any doesn't mean "this object might have a Describe method." It means "this variable can hold any type, but right now, the compiler knows absolutely nothing about it." You can't call Describe() because any itself doesn't have a Describe() method.

Thinking 'any' removes the need for type checking vs. realizing it defers it to runtime

In Go, any is just an alias for interface{}. An interface is essentially a contract. An empty interface is a contract with zero requirements. Since every single type in Go (ints, strings, custom structs) satisfies "zero requirements," every type can be stored in an any variable.

But here is the catch: while you can put anything into an empty interface, you can't get anything out of it without proving to the compiler what it is. I usually tell my juniors to think of any as a sealed box. You know there's something inside, but you can't interact with it until you open the box and verify the contents.

To actually use the value, you have two main options: a type assertion or a type switch. If you're only expecting one specific type, a type assertion is the way to go:

func ProcessValue(val any) {
    s, ok := val.(string)
    if !ok {
        fmt.Println("Expected a string, but got something else")
        return
    }
    fmt.Println("The string length is:", len(s))
}

Assuming 'any' is a magic container vs. using Type Switches to safely unpack data

When you're dealing with a variable that could truly be one of several different types—like a generic event handler or a configuration parser—don't lean on multiple if statements with assertions. That's clunky. Instead, use a type switch. It's cleaner, more idiomatic, and much easier for the next person reading your code to follow.

I use this pattern all the time when writing middleware that needs to handle different types of metadata. Here is a concrete example of how to handle a "Value" that could be a few different things:

func FormatConfigValue(val any) string {
    switch v := val.(type) {
    case int:
        return fmt.Sprintf("Integer value: %d", v)
    case string:
        return fmt.Sprintf("String value: %s", v)
    case bool:
        return fmt.Sprintf("Boolean value: %t", v)
    case nil:
        return "Value is nil"
    default:
        return fmt.Sprintf("Unknown type: %T", v)
    }
}

Notice how the variable v inside each case is automatically cast to the correct type. In the int case, v is an int, not an any. This is where the magic happens—you've safely moved from the "unknown" world of any back into the "safe" world of strong typing.

One last piece of advice: avoid using any whenever possible. If you know your function should only accept things that can be "Stringified," define an interface with a String() string method. Only reach for any when you truly have no idea what the input will be, such as when decoding JSON into a map or handling arbitrary data from an external API.




📋 Practical Task

Building a Polymorphic Notification Dispatcher

You are building a notification system that can send messages via different channels. Depending on the payload passed to the dispatcher, it should behave differently.

Requirements:

  • Define two structs: EmailPayload (with a Subject and Body string) and SMSPayload (with a PhoneNumber and Message string).
  • Create a function called DispatchNotification(payload any).
  • Inside DispatchNotification, use a type switch to:
    • If it's an EmailPayload, print: "Sending Email: [Subject] - [Body]".
    • If it's an SMSPayload, print: "Sending SMS to [PhoneNumber]: [Message]".
    • For any other type, print: "Unsupported notification type".
  • In your main function, call DispatchNotification three times: once with an email, once with an SMS, and once with a plain integer to test the default case.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.