Mastering Go's Empty Interface: Powerful Uses and Hidden Traps
Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a tool that makes generating API docs from your code ridiculously easy. Go’s empty interface, interface{}, is one of those tools that feels like a superpower at first glance. It can hold anything—numbers, strings, structs, you name it. But with great power comes... well, a bunch of headaches if you’re not careful. In this post, we’ll dive into what makes interface{} tick, explore its legitimate use cases, and flag the traps that can make your code messy or slow. Expect plenty of examples, some tables to clarify things, and tips to keep your Go code clean and robust. Let’s get started. What Is interface{}? Go’s Catch-All Type The empty interface, written as interface{}, is a special type in Go that has no methods. Since any type in Go implements at least zero methods, every value in Go satisfies interface{}. This makes it a universal container, letting you store or pass around values of any type without knowing what they are upfront. Here’s a quick example: var anything interface{} anything = 42 fmt.Println(anything) // Prints: 42 anything = "hello" fmt.Println(anything) // Prints: hello Sounds amazing, right? You can throw anything into an interface{} and deal with it later. But this flexibility comes at a cost: you lose type safety, and that can lead to runtime errors or hard-to-read code. Before we get to the pitfalls, let’s look at where interface{} shines. Where interface{} Saves the Day: Real-World Use Cases The empty interface is handy when you need flexibility that Go’s strict type system doesn’t easily allow. Here are three common scenarios where it’s a lifesaver, complete with examples. Storing Mixed Types in Collections Sometimes, you need a slice or map to hold different types—like integers, strings, or custom structs. A []interface{} or map[string]interface{} makes this possible. Example: Let’s create a slice with mixed types and print each value. package main import "fmt" func main() { mixed := []interface{}{42, "golang", true, struct{ Name string }{Name: "Alice"}} for _, item := range mixed { fmt.Printf("Value: %v, Type: %T\n", item, item) } } Output: Value: 42, Type: int Value: golang, Type: string Value: true, Type: bool Value: {Alice}, Type: struct { Name string } This is great for prototyping or when you’re handling data with unpredictable types. But you’ll often need type assertions (like val.(string)) to work with the values, which we’ll cover later. Writing Functions That Handle Any Type Ever wonder how fmt.Println can print integers, strings, and structs without breaking a sweat? It uses interface{} under the hood. You can write similar functions that accept any type. Example: A simple function to log any value. package main import "fmt" func LogValue(v interface{}) { fmt.Printf("Logged: %v\n", v) } func main() { LogValue(100) LogValue("Hello, Go!") LogValue([]int{1, 2, 3}) } Output: Logged: 100 Logged: Hello, Go! Logged: [1 2 3] This approach is perfect for utility functions that don’t care about the input’s specific type, like logging or formatting tools. Check out the official Go fmt package docs for more on how interface{} powers flexible printing. Parsing Dynamic Data Like JSON When dealing with JSON or YAML where the structure isn’t known in advance, interface{} is your go-to. Libraries like encoding/json use it to unmarshal data into map[string]interface{} or []interface{}. Example: Unmarshaling JSON with an unknown schema. package main import ( "encoding/json" "fmt" ) func main() { data := `{"name": "Bob", "age": 30, "hobbies": ["coding", "gaming"]}` var result interface{} json.Unmarshal([]byte(data), &result) m := result.(map[string]interface{}) fmt.Printf("Name: %v, Age: %v, Hobbies: %v\n", m["name"], m["age"], m["hobbies"]) } Output: Name: Bob, Age: 30, Hobbies: [coding gaming] Here, interface{} lets us handle JSON without defining a struct upfront. But accessing fields requires type assertions, which can get messy if the data structure is complex. The Dark Side of interface{}: Pitfalls to Avoid As useful as interface{} is, it’s easy to overuse or misuse. Here are the big traps to watch out for, with examples to show what goes wrong. Throwing Away Type Safety Go’s type system is there to catch errors at compile time. Using interface{} bypasses that, pushing errors to runtime. If you rely on type assertions, one wrong assumption can cause a panic. Example: A risky type assertion. package main import "fmt" func main() { var v interface{} = 42 s := v.(string) // Panic: interface conversion: interface {} is int, not string fmt.Println(s) } To avoid this, you can use a type switch or safe assertions: if s, ok := v.(string); ok {

Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a tool that makes generating API docs from your code ridiculously easy.
Go’s empty interface, interface{}
, is one of those tools that feels like a superpower at first glance.
It can hold anything—numbers, strings, structs, you name it. But with great power comes... well, a bunch of headaches if you’re not careful.
In this post, we’ll dive into what makes interface{}
tick, explore its legitimate use cases, and flag the traps that can make your code messy or slow.
Expect plenty of examples, some tables to clarify things, and tips to keep your Go code clean and robust. Let’s get started.
What Is interface{}
? Go’s Catch-All Type
The empty interface, written as interface{}
, is a special type in Go that has no methods.
Since any type in Go implements at least zero methods, every value in Go satisfies interface{}
.
This makes it a universal container, letting you store or pass around values of any type without knowing what they are upfront.
Here’s a quick example:
var anything interface{}
anything = 42
fmt.Println(anything) // Prints: 42
anything = "hello"
fmt.Println(anything) // Prints: hello
Sounds amazing, right? You can throw anything into an interface{}
and deal with it later.
But this flexibility comes at a cost: you lose type safety, and that can lead to runtime errors or hard-to-read code.
Before we get to the pitfalls, let’s look at where interface{}
shines.
Where interface{}
Saves the Day: Real-World Use Cases
The empty interface is handy when you need flexibility that Go’s strict type system doesn’t easily allow. Here are three common scenarios where it’s a lifesaver, complete with examples.
Storing Mixed Types in Collections
Sometimes, you need a slice or map to hold different types—like integers, strings, or custom structs. A []interface{}
or map[string]interface{}
makes this possible.
Example: Let’s create a slice with mixed types and print each value.
package main
import "fmt"
func main() {
mixed := []interface{}{42, "golang", true, struct{ Name string }{Name: "Alice"}}
for _, item := range mixed {
fmt.Printf("Value: %v, Type: %T\n", item, item)
}
}
Output:
Value: 42, Type: int
Value: golang, Type: string
Value: true, Type: bool
Value: {Alice}, Type: struct { Name string }
This is great for prototyping or when you’re handling data with unpredictable types. But you’ll often need type assertions (like val.(string)
) to work with the values, which we’ll cover later.
Writing Functions That Handle Any Type
Ever wonder how fmt.Println
can print integers, strings, and structs without breaking a sweat? It uses interface{}
under the hood. You can write similar functions that accept any type.
Example: A simple function to log any value.
package main
import "fmt"
func LogValue(v interface{}) {
fmt.Printf("Logged: %v\n", v)
}
func main() {
LogValue(100)
LogValue("Hello, Go!")
LogValue([]int{1, 2, 3})
}
Output:
Logged: 100
Logged: Hello, Go!
Logged: [1 2 3]
This approach is perfect for utility functions that don’t care about the input’s specific type, like logging or formatting tools.
Check out the official Go fmt package docs for more on how interface{}
powers flexible printing.
Parsing Dynamic Data Like JSON
When dealing with JSON or YAML where the structure isn’t known in advance, interface{}
is your go-to. Libraries like encoding/json
use it to unmarshal data into map[string]interface{}
or []interface{}
.
Example: Unmarshaling JSON with an unknown schema.
package main
import (
"encoding/json"
"fmt"
)
func main() {
data := `{"name": "Bob", "age": 30, "hobbies": ["coding", "gaming"]}`
var result interface{}
json.Unmarshal([]byte(data), &result)
m := result.(map[string]interface{})
fmt.Printf("Name: %v, Age: %v, Hobbies: %v\n", m["name"], m["age"], m["hobbies"])
}
Output:
Name: Bob, Age: 30, Hobbies: [coding gaming]
Here, interface{}
lets us handle JSON without defining a struct upfront. But accessing fields requires type assertions, which can get messy if the data structure is complex.
The Dark Side of interface{}
: Pitfalls to Avoid
As useful as interface{}
is, it’s easy to overuse or misuse. Here are the big traps to watch out for, with examples to show what goes wrong.
Throwing Away Type Safety
Go’s type system is there to catch errors at compile time. Using interface{}
bypasses that, pushing errors to runtime.
If you rely on type assertions, one wrong assumption can cause a panic.
Example: A risky type assertion.
package main
import "fmt"
func main() {
var v interface{} = 42
s := v.(string) // Panic: interface conversion: interface {} is int, not string
fmt.Println(s)
}
To avoid this, you can use a type switch or safe assertions:
if s, ok := v.(string); ok {
fmt.Println("It's a string:", s)
} else {
fmt.Println("Not a string, got type:", fmt.Sprintf("%T", v))
}
Table: Type Assertion vs. Type Switch
Approach | Pros | Cons |
---|---|---|
Type Assertion | Simple syntax | Panics if type is wrong |
Safe Assertion | Avoids panics with ok check |
Slightly verbose |
Type Switch | Handles multiple types cleanly | Overkill for single-type checks |
Performance Hits
Using interface{}
involves runtime type information, which can slow things down. Every time you store a value in an interface{}
, Go wraps it in an interface structure. Type assertions and switches add more overhead.
Example: Compare a typed slice vs. an interface{}
slice.
package main
import (
"fmt"
"time"
)
func sumInts(nums []int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func sumInterfaces(nums []interface{}) int {
total := 0
for _, n := range nums {
total += n.(int)
}
return total
}
func main() {
ints := []int{1, 2, 3, 4, 5}
interfaces := []interface{}{1, 2, 3, 4, 5}
start := time.Now()
sumInts(ints)
fmt.Println("Typed sum:", time.Since(start))
start = time.Now()
sumInterfaces(interfaces)
fmt.Println("Interface sum:", time.Since(start))
}
The interface{}
version is slower due to type assertions. For small datasets, it’s negligible, but it adds up in loops or large collections.
Making Code Hard to Follow
When you use interface{}
everywhere, it’s like saying, “This function accepts something.” Readers (including future you) have to dig through the code to figure out what types are expected.
Example: A vague function.
func Process(data interface{}) interface{} {
// What is data? What do we return?
return data
}
Contrast that with a clear interface:
type Processor interface {
Process() string
}
func Process(p Processor) string {
return p.Process()
}
The second version tells you exactly what’s needed: something with a Process
method.
Using interface{}
Without Regrets: Best Practices
To keep interface{}
from turning your codebase into a mess, follow these guidelines:
-
Use it sparingly. Only reach for
interface{}
when you genuinely need to handle unknown types, like parsing dynamic JSON. -
Document expectations. If a function takes
interface{}
, clarify what types are valid in comments or docs. - Check types safely. Always use safe assertions or type switches to avoid panics.
- Consider alternatives. Can a specific interface or generics solve the problem better?
Example: Refining a vague function with a custom interface.
// Bad
func PrintData(data interface{}) {
fmt.Println(data)
}
// Better
type Printable interface {
String() string
}
func PrintData(p Printable) {
fmt.Println(p.String())
}
This ensures only types with a String
method are passed, keeping things clear and safe.
Beyond interface{}
: Smarter Alternatives
Since Go 1.18, generics have given us a type-safe way to write flexible code without interface{}
. You can also define precise interfaces for specific needs. Let’s see how.
Generics for Flexible Collections
Instead of []interface{}
, use a generic slice to maintain type safety.
Example: A generic print function.
package main
import "fmt"
func PrintSlice[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}
func main() {
nums := []int{1, 2, 3}
words := []string{"go", "is", "fun"}
PrintSlice(nums)
PrintSlice(words)
}
The T any
constraint is similar to interface{}
, but it’s checked at compile time, so you avoid runtime surprises.
Custom Interfaces for Clarity
If you know what behavior you need, define an interface instead of using interface{}
.
Example: A logging interface.
type Logger interface {
Log() string
}
func ProcessAndLog(l Logger) {
fmt.Println("Processing:", l.Log())
}
This is clearer than interface{}
and ensures only types with a Log
method are used.
Table: interface{}
vs. Alternatives
Approach | Type Safety | Readability | Performance | Use Case |
---|---|---|---|---|
interface{} |
Low | Low | Slower | Dynamic data, prototyping |
Custom Interface | High | High | Fast | Specific behaviors |
Generics | High | Medium | Fast | Flexible collections, algos |
Finding the Sweet Spot
The empty interface is a powerful tool, but it’s not a one-size-fits-all solution. Use interface{}
when you need ultimate flexibility, like handling JSON with unknown schemas or writing utility functions like fmt.Println
. But don’t let it become a crutch—favor type safety and clarity with custom interfaces or generics whenever possible.
By keeping interface{}
in check, you’ll write Go code that’s easier to maintain, faster to run, and less likely to crash at 3 a.m. Experiment with the examples above, and next time you’re tempted to slap interface{}
on a function, ask yourself: Is there a better way?