Empty Interfaces — Junior Level¶
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Core Concepts
- Real-World Analogies
- Mental Models
- Pros & Cons
- Use Cases
- Code Examples
- Coding Patterns
- Clean Code
- Best Practices
- Edge Cases & Pitfalls
- Common Mistakes
- Common Misconceptions
- Test
- Tricky Questions
- Cheat Sheet
- Summary
- Diagrams
Introduction¶
Empty interface — an interface with no methods. Syntax:
Since Go 1.18, there is a new name for it: any.
The empty interface accepts any type — because every type satisfies "0 methods".
var x any = 42
var y any = "hello"
var z any = []int{1, 2, 3}
var w any = struct{ Name string }{Name: "Alice"}
The empty interface is Go's element of dynamic typing. However, Go's strength is static typing, so the empty interface should be used only when needed: - Heterogeneous collection (values of different types) - Pre-generics packages (before 1.18) - Working with reflect - Generic JSON unmarshal
After this file you will: - Know the difference between empty interface (interface{} and any) - Understand when to use it and when not to - Know how to determine the type via type assertion - Know what to choose when a generic alternative is available
Prerequisites¶
- Interfaces basics
- Type assertion (basic understanding)
- Method set rules
Glossary¶
| Term | Definition |
|---|---|
| Empty interface | interface{} — an interface with no methods |
any | Go 1.18+ predeclared alias for interface{} |
| Boxing | Moving a value type to the heap when assigning it to an interface |
| Type assertion | Accessing the concrete type from an interface: i.(T) |
| Type switch | Checking multiple types |
| Heterogeneous collection | Storing values of different types |
Core Concepts¶
1. Empty interface — any type¶
var x interface{} = 42 // int
var y interface{} = "hello" // string
var z interface{} = true // bool
The reason — every type satisfies "0 methods".
2. any — Go 1.18+¶
any is the newest and idiomatic form. The Go community recommends using any instead of interface{}.
3. Getting the type with type assertion¶
The two-value form is safe — no panic when ok is false.
4. Type switch for multiple types¶
func describe(i any) {
switch v := i.(type) {
case int: fmt.Println("int:", v)
case string: fmt.Println("string:", v)
case bool: fmt.Println("bool:", v)
default: fmt.Println("unknown")
}
}
describe(42) // int: 42
describe("hello") // string: hello
5. Slice/map/function argument¶
items := []any{1, "hello", true, 3.14}
for _, item := range items {
fmt.Println(item)
}
// Output: 1 hello true 3.14
6. Generic JSON unmarshal¶
var data any
json.Unmarshal([]byte(`{"name":"Alice","age":30}`), &data)
// data — map[string]any{"name":"Alice", "age":30}
Real-World Analogies¶
Analogy 1 — Universal bag
The empty interface is like a universal bag. You can put anything inside: a book, a key, a phone. But you can't tell what's inside just by looking at the bag — you have to open it (type assertion).
Analogy 2 — Document folder
any is a universal document folder. You don't know what's inside — you have to open the folder and find out the document type.
Analogy 3 — JSON / dynamic language
The empty interface is like JavaScript or Python's dynamic typing in Go. But you have to do runtime type checking.
Mental Models¶
Model 1: "0 methods = satisfied"¶
interface I { /* 0 methods */ }
↓
every type automatically satisfies 0 methods
↓
every type satisfies I
Model 2: Boxing and unboxing¶
Model 3: Static vs dynamic¶
Go is dedicated to static typing — the empty interface is used only when needed.
Pros & Cons¶
| Pros | Cons |
|---|---|
| Accepts any type | Type-safety is lost |
| Heterogeneous collection possible | Type assertion adds extra code |
| Works with reflect | Boxing — heap allocation |
| JSON / dynamic data | Performance — generics are faster |
Use Cases¶
Use case 1: Heterogeneous collection¶
Use case 2: Generic container (before 1.18)¶
type List struct{ items []any }
func (l *List) Add(x any) { l.items = append(l.items, x) }
func (l *List) Get(i int) any { return l.items[i] }
In Go 1.18+ generics are preferred:
Use case 3: Generic JSON unmarshal¶
Use case 4: Working with reflect¶
Use case 5: fmt.Println arguments¶
Code Examples¶
Example 1: Basics¶
package main
import "fmt"
func main() {
var x any = 42
fmt.Println(x) // 42
fmt.Printf("%T\n", x) // int
}
Example 2: Type assertion¶
package main
import "fmt"
func main() {
var i any = "hello"
s, ok := i.(string)
if ok {
fmt.Println("got string:", s)
} else {
fmt.Println("not a string")
}
n, ok := i.(int)
if ok {
fmt.Println("got int:", n)
} else {
fmt.Println("not an int") // this runs
}
}
Example 3: Type switch¶
package main
import "fmt"
func describe(i any) string {
switch v := i.(type) {
case int:
return fmt.Sprintf("int: %d", v)
case string:
return fmt.Sprintf("string: %s", v)
case bool:
return fmt.Sprintf("bool: %t", v)
case []int:
return fmt.Sprintf("[]int with %d elements", len(v))
default:
return "unknown"
}
}
func main() {
fmt.Println(describe(42)) // int: 42
fmt.Println(describe("hello")) // string: hello
fmt.Println(describe(true)) // bool: true
fmt.Println(describe([]int{1, 2})) // []int with 2 elements
}
Example 4: Heterogeneous slice¶
package main
import "fmt"
func main() {
items := []any{1, "two", 3.0, true}
for _, item := range items {
fmt.Printf("%v (type %T)\n", item, item)
}
}
Example 5: Generic JSON¶
package main
import (
"encoding/json"
"fmt"
)
func main() {
jsonStr := `{"name":"Alice","age":30,"tags":["a","b"]}`
var data any
json.Unmarshal([]byte(jsonStr), &data)
fmt.Println(data)
// map[age:30 name:Alice tags:[a b]]
if m, ok := data.(map[string]any); ok {
fmt.Println("name:", m["name"])
}
}
Coding Patterns¶
Pattern 1: Variadic any¶
Pattern 2: Map[string]any¶
Pattern 3: Generic return¶
// Before 1.18
func GetValue(key string) any { ... }
// 1.18+
func GetValue[T any](key string) T { ... }
Clean Code¶
Rule 1: Only when needed¶
// Bad — concrete type is clear
func Process(data any) { ... }
// Good
func Process(data User) { ... }
Rule 2: Check any before unboxing¶
Rule 3: Use generics (1.18+)¶
// Bad
func Sum(xs []any) int {
total := 0
for _, x := range xs { total += x.(int) }
return total
}
// Good
func Sum[T int | float64](xs []T) T {
var total T
for _, x := range xs { total += x }
return total
}
Best Practices¶
- Prefer concrete types — type-safety
- Use generics for Go 1.18+
anyinstead ofinterface{}(modern style)- Type assertion two-value — with
ok - Type switch — for multiple types
- Use
reflectcarefully — it reduces performance - Documentation — document the accepted types
Edge Cases & Pitfalls¶
Pitfall 1: Nil any¶
var i any = nil
fmt.Println(i == nil) // true
var p *int = nil
var j any = p
fmt.Println(j == nil) // false! — type is present
Pitfall 2: Boxing¶
Check with go build -gcflags='-m'.
Pitfall 3: Type assertion panic¶
Use the two-value form.
Pitfall 4: Comparison panic¶
Common Mistakes¶
| Mistake | Solution |
|---|---|
| any instead of type-safety | Concrete type or generics |
| Type assertion single-value | Two-value form |
| Boxing in hot path | Generics |
| Slice/map comparison | Custom equality |
Common Misconceptions¶
1. "any should be used everywhere" False. Concrete types are always preferred. any is only for heterogeneous or dynamic data.
2. "any is the same as Java's Object" Partially. In Go you need to box/unbox (type assertion). In Java, methods come through inheritance.
3. "any differs from interface{}" False. any = interface{} — alias, the same.
Test¶
1. Difference between interface{} and any?¶
Answer: None. any is a Go 1.18+ alias.
2. Result of var i any = nil; i == nil?¶
Answer: true.
3. Result of var p *int = nil; var i any = p; i == nil?¶
Answer: false — type info is present.
4. The safe form of type assertion?¶
Answer: v, ok := i.(T).
5. Why does the empty interface accept any type?¶
Answer: Every type satisfies "0 methods".
Tricky Questions¶
Q1: Difference between []any and []int? Heterogeneous vs homogeneous. []any accepts various types, []int only int.
Q2: When does == on any panic? When the concrete type is non-comparable (slice, map, struct with such fields).
Q3: Choice between any and generics? Same algorithm + different types → generics. Heterogeneous collection → any.
Q4: How does fmt.Println use any? ...any variadic — accepts various types. Internally uses reflect.
Q5: Performance impact of any? Boxing — heap allocation. Type assertion — runtime check. Generics are faster.
Cheat Sheet¶
EMPTY INTERFACE
─────────────────
interface{} // classic
any // Go 1.18+ alias
PROPERTIES
─────────────────
✓ Accepts any type
✓ Heterogeneous collection
✗ No type-safety
✗ Boxing — heap alloc
✗ Type assertion adds code
ALTERNATIVE
─────────────────
Pre-generics: any
1.18+: generics
TYPE CHECKING
─────────────────
v, ok := i.(T) // assertion
switch v := i.(type) { } // switch
CARE
─────────────────
Only when needed
Generics preferred in hot path
OK for dynamic JSON
Summary¶
Empty interface (interface{}, any): - Accepts any type - Heterogeneous collection, dynamic data, JSON - Type-safety is lost — type assertion required - Generics preferred in Go 1.18+ - Boxing has a performance impact
Use only when needed. Concrete types or generics are Go's idiomatic styles.