Polymorphism in Go is the ability to treat different values through a common interface, as long as those values fulfill the expected methods.
So far we have been very strict: an int is an int and cannot be a string. Go forces us to be tidy. But how does the fmt.Println() function manage to print anything we pass to it? How do we process JSON where a field can be a number or a string?
For behavior-based polymorphism we use interfaces with methods. When we truly don’t know the type, we can resort to the empty interface, whose alias is any.
Let’s first look at implicit interfaces and then how to inspect the dynamic type stored in an any.
Implicit Interfaces
In Go we don’t write implements. A type satisfies an interface if its set of methods contains those the interface declares.
type Greeter interface {
Greet() string
}
type Person struct {
Name string
}
func (p Person) Greet() string {
return "Hello, I am " + p.Name
}
func Introduce(g Greeter) {
fmt.Println(g.Greet())
}Person can be passed to Introduce without declaring any relationship with Greeter. This independence allows the consuming package to define small interfaces based on the behavior it needs.
The empty interface (interface{}) and the any alias
In Go, an interface defines a set of methods. A type fulfills the interface if it implements those methods.
Now follow this logic:
- If an interface requires no methods (it’s empty)…
- …then all types in the universe fulfill that interface (because they all have at least “zero methods”).
The empty interface was traditionally written as interface{}.
Since Go 1.18, we have a much more readable alias: any.
package main
import "fmt"
// This function accepts ANY type of data
func PrintAnything(v any) {
fmt.Printf("Value: %v, Type: %T\n", v, v)
}
func main() {
PrintAnything(42) // Passes an int
PrintAnything("Hello") // Passes a string
PrintAnything(true) // Passes a bool
PrintAnything([]int{1, 2}) // Passes a slice
}Although any is now preferred for readability, you’ll see interface{} a lot in older code. They are exactly the same thing.
The static type of any
When we assign a value to an any variable, its dynamic type is preserved, but the compiler only allows operations defined by its static type, which in this case requires no methods.
var x any = 10
// x = x + 1 // ERROR: invalid operation: x (type any) + 1For the compiler, x has the static type any. We cannot add it or use it as an index until we check its dynamic type via a type assertion.
Type Assertions
A type assertion allows you to get the value as a concrete type or check that it also satisfies another interface.
Syntax: value := interfaceVariable.(ConcreteType)
func main() {
var i any = "Hello World"
// We assert: "I know there's a string inside 'i'. Give it to me."
s := i.(string)
fmt.Println(s) // "Hello World"
}The risk of panic
What if you are wrong and the box didn’t contain what you thought?
var i any = "Hello"
n := i.(int) // PANIC!
// The program stops: interface conversion: interface {} is string, not intThe safe way: comma, ok
Just like with Maps, we can use the second return value to verify if the conversion was successful without breaking the program.
var i any = "Hello"
if n, ok := i.(int); ok {
fmt.Println("It's an integer:", n)
} else {
fmt.Println("It wasn't an integer. Safe operation.")
}Type switches
If you have an any variable and you want to behave differently depending on whether it’s a string, a number, or a boolean, you could chain many if-else statements with assertions. But that’s ugly.
Go offers us the Type Switch. It’s a special switch where we evaluate the type (.type) instead of the value.
func Analyze(v any) {
// We use .(type) inside the switch
switch val := v.(type) {
case int:
fmt.Printf("It's an integer. Its square is %d\n", val*val)
case string:
fmt.Printf("It's a text of length %d\n", len(val))
case bool:
fmt.Printf("It's a boolean. Value: %t\n", val)
case Person: // Also works with our Structs
fmt.Println("It's a person named", val.Name)
default:
fmt.Printf("I don't know what this is: %T\n", val)
}
}Notice switch val := v.(type). The variable val inside each case already has the correct type.
- In the
case int,valis anint. - In the
case string,valis astring. Very convenient!
When to use any
Using any is tempting. You might think: “Great, I’ll declare all my functions with any and not worry about types, like in Python”.
WRONG! If you do that, you lose all the advantages of Go (compile-time safety, autocompletion, performance).
When to USE any:
- Unknown data: When you read arbitrary JSON and don’t know its structure.
- Truly heterogeneous collections: if all elements share an operation or a parameterizable type, a concrete interface or generics is preferable.
- Formatting/Logging functions: Like
fmt.Printfor loggers that must accept any message.
When to avoid any:
- When you know the data type.
- Out of “laziness” to not define a proper Interface.