Hello! Welcome to your next lesson on interfaces in Go.
In our previous lesson, we saw how the fmt.Stringer interface allows us to create a custom, human-readable string representation for our known types. This is great when we know exactly what we're working with. But in many DevOps scenarios, you'll encounter data where the type isn't known ahead of time. Think about parsing a YAML configuration file that might have string, integer, or boolean values, or handling a JSON API response that returns different structures for success and failure.
This brings us to today's topic. Your learning outcome is to use empty interfaces (any) for handling values of unknown type. We'll explore what the empty interface is, why it's a cornerstone of Go's flexibility, and see its direct applications in the kinds of tools you build.
1. What is the Empty Interface (any)?
In Go, an interface is satisfied by any type that implements its methods. The empty interface, written as interface{}, is special because it has zero methods. This means that every single type in Go—int, string, bool, your custom structs, slices, maps—satisfies the empty interface.
Because it can hold a value of any type, it acts as a universal container. In Go 1.18, a new predeclared identifier any was introduced as an alias for interface{}. They are functionally identical, but any is shorter and often clearer, so you will see it used in modern Go code.
package main
import "fmt"
func main() {
var value any
// value can hold an integer
value = 42
fmt.Printf("Value: %v, Type: %T\n", value, value)
// It can also hold a string
value = "hello from Go"
fmt.Printf("Value: %v, Type: %T\n", value, value)
// Or even a custom struct
type Config struct{ Retries int }
value = Config{Retries: 3}
fmt.Printf("Value: %v, Type: %T\n", value, value)
}
Output:
Value: 42, Type: int
Value: hello from Go, Type: string
Value: {3}, Type: main.Config
This flexibility should feel familiar from your experience with dynamically-typed languages like Python, where a variable can hold values of different types. In Go, any provides this capability within its static type system.
2. Using any: The Problem and the Solution
While any can hold any value, there's a catch. Once a value is stored in a variable of type any, the compiler "forgets" its original, concrete type. You can't directly perform type-specific operations on it.
var value any = 100
// value++ // This will cause a compile error: invalid operation: value++ (non-numeric type any)
To use the value, you must first get it back into a variable of its concrete type. This is done with a type assertion.
A type assertion takes a value of an interface type and extracts its underlying concrete value. The safe way to do this is with the "comma ok" idiom.
value, ok := interfaceVariable.(ConcreteType)
- If the assertion succeeds,
valuewill hold the underlying value asConcreteType, andokwill betrue. - If the assertion fails (the value is not of that type), the program does not panic. Instead,
okwill befalse, andvaluewill be the zero value forConcreteType.
This ok check is crucial for writing robust code that handles unexpected data gracefully.
For a clear visual introduction to any and the basic mechanics of type assertions, please watch the first few minutes of this video.
Type Assertions in Go: the only guide you need!
The video 'Type Assertions in Go' by Flo Woelki provides a great visual explanation of what an empty interface is and introduces the syntax for type assertions.
Watch from the beginning until 03:33. Pay close attention to: The definition of interface{} and its alias any (00:54). How a direct type assertion can cause a panic (02:40). The introduction of the safe 'comma ok' idiom (02:59).
3. Practical Use Cases for any
Now, let's see where any is indispensable in a DevOps context.
Use Case 1: Heterogeneous Collections
Sometimes you need a collection that holds items of different types. A map[string]any is perfect for representing structured but flexible data, like a server's configuration.
The article 'Golang Interfaces explained' by Alex Edwards provides a simple and effective example of using an empty interface in a map.
Read the section 'What is the empty interface?'. Focus on the example of the person map that stores string, int, and float32 values. Note how a type assertion is required to work with the 'age' value.
This pattern is extremely common when dealing with data read from external sources.
Use Case 2: Parsing Dynamic JSON
This is perhaps the most frequent and powerful use of any in backend and DevOps development. When you receive JSON from an API or a config file, you might not know its exact structure, or it might contain fields with varying data types.
The json.Unmarshal function can unmarshal data into a map[string]any or []any, allowing you to handle any valid JSON. You can then navigate this structure using type assertions.
For example, imagine you're processing events from a webhook. A success event might look different from an error event.
// Success Event
{
"status": "ok",
"jobId": "a1b2c3d4",
"message": "Deployment completed"
}
// Error Event
{
"status": "error",
"reason": "Upstream service unavailable",
"errorCode": 503
}
The video "Advanced JSON Handling in Go" demonstrates a practical way to handle this exact scenario.
This video from Boldly Go shows how to build a flexible JSON parser. We'll focus on the part where it uses an empty interface to hold one of two different result types.
Watch the segment from 12:05 to 16:58. The key idea here is creating a Response struct with a Result field of type any (written as interface{}). Notice how the custom UnmarshalJSON function first determines the type of response based on the 'status' field and then places either a Success or Error struct into the Result field. This is a powerful pattern for handling polymorphic API responses.
After unmarshalling, you could use a type assertion to check if response.Result is a Success or Error struct and process it accordingly.
Test your understanding!
You are writing a script to parse a simple JSON configuration. The JSON is stored in a string.
{
"service_name": "auth-api",
"enabled": true,
"port": 8080,
"allowed_hosts": ["internal.corp", "api.external.com"]
}
Your task is to:
- Unmarshal this JSON string into a
map[string]any. - Use type assertions to safely extract the
portas anintandenabledas abool. - Print the extracted values.
Handle the case where a key might not exist or have the wrong type.
Show answer
Here's one way to solve it:
package main
import (
"encoding/json"
"fmt"
)
func main() {
jsonString := `{
"service_name": "auth-api",
"enabled": true,
"port": 8080,
"allowed_hosts": ["internal.corp", "api.external.com"]
}`
// 1. Unmarshal into a map[string]any
var config map[string]any
err := json.Unmarshal([]byte(jsonString), &config)
if err != nil {
panic(err)
}
// 2. Safely extract the 'port'
port, ok := config["port"].(float64) // JSON numbers are float64 by default
if !ok {
fmt.Println("Port not found or is not a number")
} else {
// We can convert it to an int
fmt.Printf("Service port: %d\n", int(port))
}
// 3. Safely extract 'enabled'
enabled, ok := config["enabled"].(bool)
if !ok {
fmt.Println("Enabled flag not found or is not a boolean")
} else {
fmt.Printf("Service enabled: %t\n", enabled)
}
}
Important Note: The standard encoding/json package unmarshals all JSON numbers into float64 by default. That's why we assert to float64 and then cast to int.
Conclusion
Today we've demystified Go's empty interface, any. It's a powerful tool for writing flexible code that can handle data of unknown or varying types, a common requirement in the world of APIs, configuration management, and automation.
Key Takeaways:
- The empty interface,
any(orinterface{}), is a universal container that can hold a value of any type. - Once a value is in an
anyvariable, you lose compile-time type information. - Type assertions are the mechanism to safely extract the concrete value from an interface variable. The
value, ok := myVar.(Type)idiom is essential for preventing panics. - A key use case for
anyis parsing dynamic data, especially from JSON or YAML, intomap[string]anyor[]any.
Next Up
We've introduced type assertions as the basic tool to get values out of an empty interface. But what if a value could be one of many possible types? A long chain of if/else statements with type assertions would be clumsy. In our next lesson, we will dive deeper into type assertions and type switches, learning the most robust and elegant patterns for safely working with values of type any.