JavaRush /Courses /Go SELF /any / interface{}: when it’s acceptable, and when it’s ha...

any / interface{}: when it’s acceptable, and when it’s harmful

Go SELF
Level 25, Lesson 0
Available

1. What any is and why it shows up

Sometimes you want a single variable to be able to store “anything”: a string, a number, a struct, an error, even a pointer to a kitty (if you have a Cat type). At first, this looks like a superpower: fewer types — less pain. But Go is a language where “fewer types” often means “more surprises”, because the compiler stops being your bodyguard.

In real programs, the need for “anything” usually appears not in the center of business logic, but at the boundaries: where data comes from the outside, where we log it, where we build generic containers for “different values”, or where we write infrastructure code (not the domain model).

interface{} and any are the same thing

Let’s start by stating the basics without mysticism. interface{} is an interface with no methods. It doesn’t require any behavior from a value, so any value fits.

Go has an alias:

// conceptually
type any = interface{}

That is, any and interface{} are the same thing. In modern Go, people more often write any because it’s shorter and reads like “any type”.

Here is the minimal example: the variable x can change its “inner” type every time you blink.

package main

import "fmt"

func main() {
	var x any

	x = 42
	fmt.Printf("value=%v type=%T\n", x, x) // value=42 type=int

	x = "go"
	fmt.Printf("value=%v type=%T\n", x, x) // value=go type=string
}

The any model: it stores (type + value) inside

This is where even confident beginners stumble: when you put a value into any, the concrete type does not disappear. It just becomes “hidden” behind the interface wrapper.

It’s very useful to keep a simple model in mind:

any = (concrete type, concrete value)

And printing via %T in fmt.Printf is your flashlight that shows the hidden type.

The cost of flexibility: the compiler no longer knows what’s inside

Here is the price of “flexibility”: at the usage site, the compiler does not know whether x is an int, a string, or a Task. So it forbids most operations.

Let’s look at an example. It is intentionally written to show the “wall” you run into:

package main

import "fmt"

func main() {
	var x any = 10

	fmt.Println(x) // 10

	// fmt.Println(x + 5) // does NOT compile: invalid operation: x + 5
}

Why is this good? Because otherwise Go would turn into a language of “let’s try it and see whether it crashes at runtime”. And Go really doesn’t like that.

And here comes the key rule of this lecture: if you used any, you must at some point get the concrete type back; otherwise you won’t be able to work with the value properly.

2. any at the application boundary, not in the middle

Let’s tie this to the familiar style “read → parse → compute → print”. Inside compute you want rock-solid types so the logic is simple and compiler-checkable. But at read/parse you sometimes really do receive “mixed-type stuff”.

Let’s imagine a diagram of how we want types to look in an application (roughly, in our learning mini-project task manager):

flowchart TD
    A[Input/external data] --> B[Parsing/normalization]
    B --> C[Domain logic: Task, rules, checks]
    C --> D[Output/rendering: printing, logging]

    A -. any is acceptable .-> B
    D -. any is acceptable .-> D
    C -. any is harmful .-> C

Meaning: any is sometimes acceptable on input and output (especially for diagnostics/logs), but in the middle it usually makes life worse.

Example: task attributes as map[string]any

Suppose we have a Task, and we want to add “arbitrary attributes”: priority can be a number, color can be a string, an “urgent” flag can be a boolean.

And a beginner (or a tired adult) writes this:

package main

import "fmt"

type Task struct {
	ID    int
	Title string
	Attrs map[string]any
}

func main() {
	t := Task{
		ID:    1,
		Title: "learn Go",
		Attrs: map[string]any{"priority": 3, "urgent": true},
	}
	fmt.Println(t.Attrs["priority"]) // 3
}

At this point you want to say, “See, convenient!”. But now try to do real work: for example, add 1 to the priority. And you’ll immediately hit the problem: t.Attrs["priority"] has type any, not int.

So you can store anything, but you can’t use it without restoring the type.

And this is the first signal that any is not a “simpler type”, but a “more complex type” — the complexity is just deferred until later.

3. When any is acceptable and useful

So the lecture doesn’t turn into “any is evil and should be banned”, let’s go through normal scenarios where any is justified. The key is to feel the line: any is useful when the type is truly unknown in advance or irrelevant to the logic.

Logging and diagnostics: “show me what this is”

Very often any is needed exactly to “pass something into fmt”. For example, we want to write a small helper for debugging our tasks:

package main

import "fmt"

func debug(name string, v any) {
	fmt.Printf("[debug] %s=%v (type=%T)\n", name, v, v)
}

func main() {
	debug("id", 7)          // [debug] id=7 (type=int)
	debug("title", "hello") // [debug] title=hello (type=string)
}

Here any is absolutely appropriate: we are not going to do arithmetic on v, we’re just displaying the value.

Generic containers for mixed data

map[string]any often appears where, by meaning, the data truly is heterogeneous. For example, we want to collect an “operation context” for printing:

package main

import "fmt"

func main() {
	ctx := map[string]any{
		"op":   "create_task",
		"id":   10,
		"done": false,
	}
	fmt.Println(ctx["op"], ctx["id"], ctx["done"]) // create_task 10 false
}

But there’s an important nuance: if such a ctx starts living in business logic and influencing program decisions, you’re already on a slippery slope.

Signatures where “any type” is part of the contract

There are API categories that explicitly promise “we accept anything”. In the standard library, you see this, for example, in formatting (fmt) and similar tools. And that’s fine: their job is to be universal.

4. When any is harmful: three practical reasons

Now an important point: the harm of any is not that “dynamic is bad”. The harm is that you shift the compiler’s work onto yourself, and then also onto the future reader of the code (who is often you — just a week later).

any breaks the readability of the data model

If your domain model contains a field of type any, then reading the structure becomes a game of “guess what’s inside”.

Compare two designs.

Option “easier right now”:

type Task struct {
	ID    int
	Title string
	Extra any
}

And option “easier to live with later”:

type Task struct {
	ID       int
	Title    string
	Priority int
}

In the second option, the compiler guarantees that Priority is a number, not a string "high" (which also looks pretty convincing “by eye”).

any creates hidden contracts and runtime errors

If a function accepts any, it gets a hidden contract: “you can pass anything… but actually pass an int, otherwise it will hurt”.

For example:

func setPriority(t *Task, v any) {
	// later you'll still have to check v's type
}

This is much worse than honestly writing:

func setPriority(t *Task, priority int) {
	t.Priority = priority
}

Because now the function contract is visible right in the signature.

any “spreads” across the project

The most insidious thing: you added any in one place; then you need to “carry” this value further; and since it’s any, you let the next layer accept any, and the next one… and now half the project is dynamic soup.

That’s why Go has a practical rule: narrow any back to concrete types as quickly as possible. Today we won’t do this technically (that’s for the next lectures), but as a principle — let’s lock it in right now.

5. Nuances and traps people often stumble over

The nil nuance: an empty interface and “typed nil inside” are not the same

This section usually triggers the feeling of “I get it, but why is it like that”. That’s normal. You’re not the weird one — the interface model is just a bit trickier than it looks in the first 10 minutes.

If an interface value is (type + value), then two different states are possible:

  1. the interface is truly empty: there is no type and no value
  2. the interface stores a type, but the value is nil (for example, *int(nil))

Look:

package main

import "fmt"

func main() {
	var x any
	fmt.Println(x == nil) // true

	var p *int = nil
	x = p
	fmt.Println(x == nil) // false

	fmt.Printf("type=%T value=%v\n", x, x) // type=*int value=<nil>
}

And here’s why this matters: x == nil checks “is the interface completely empty”, not “is the value inside nil”.

This is especially annoying with errors, because error is an interface. We already discussed the “interface nil trap” earlier; here we just see that with any the mechanism is the same: any is also an interface.

map[any]... and panic on non-comparable types

Sometimes, seeing “any type”, you want the keys of a map to also be “any type”: map[any]string. The compiler will allow it because interfaces are comparable. But then you can get a panic if you try to use a non-comparable type as a key (for example, a slice).

The idea is: map keys must be hashable. And slices are not. So at runtime you can get a panic like “hash of unhashable type”. This example is directly illustrated in materials about type comparability: trying to use []int{} as a key results in a runtime panic.

The practical takeaway is simple: map[any]... is a tool that requires extra caution. Sometimes it’s needed, but more often it’s a sign that your data model wants more concrete types.

6. Cheat sheet and a working strategy

Mini-table: where it’s allowed and where it’s not

Code layer Is any acceptable? Why
Input/raw parsing Yes Types may be unknown; you’re only bringing the data into shape
Logging/diagnostics Yes You need to show “what arrived”, not compute
Domain model (Task, rules) Usually no Types should be explicit; invariants should be compiler-checkable
Business logic (conditions, calculations) Almost always no Otherwise the logic turns into type checks and branching
Library/utility boundaries Sometimes yes Generic helpers can accept any if that’s an honest contract

How to live with any calmly: a short life and a clear boundary

A good Go strategy sounds boring, but it works. If any appears, try to make it live for as few lines of code as possible. Ideally, it is “born” at the boundary and, a couple of steps later, becomes concrete types — and from then on the domain logic doesn’t even know that there ever was an any.

And the second point: if you see any inside a data model, stop and ask yourself: “Do I truly not know the type ahead of time? Or am I just too lazy to describe the structure properly right now?” In 80% of cases, the honest answer is the second. And in programming, laziness is not a sin — it’s just a loan. But the interest on it is usually brutal.

7. Common mistakes when working with any / interface{}

Mistake No. 1: using any as a “default type” because it’s faster.
It looks convenient in the moment, but later almost every use of the value requires getting back to a concrete type — which means checks, branches, and extra code. The result is not “faster”, but “slower, more painful, and full of surprises”.

Mistake No. 2: dragging any into the domain model (“let a task have a field Extra any”).
This makes you lose Go’s key advantage: the data structure documents the contract by itself. Instead of “a task has a priority” you get “a task has something”. The reader is forced either to search for what gets stored there or to guess.

Mistake No. 3: trying to perform operations on any, forgetting that the type is unknown.
Attempts to add an any to a number, compare it, call a method “as if it were a string” — this either doesn’t compile or leads to complex type-checking logic. The compiler isn’t being picky; it’s just honestly saying “I don’t know what this is”.

Mistake No. 4: confusing a nil interface with “typed nil inside an interface”.
The comparison x == nil checks interface emptiness, not whether nil is stored inside. Because of this, you can get odd behavior: “it seems nil, but for some reason it’s not nil”. This is a normal beginner trap; it’s fixed by understanding the model (type + value) and careful boundary checks.

Mistake No. 5: making map[any]... and then being surprised by panics on slice keys.
Yes, any is comparable as an interface, but the concrete value inside may be a non-comparable type (for example, []int). And then you won’t get a “nice error” — you’ll get a program-stopping panic. Material on type comparability shows this scenario very clearly.

1
Task
Go SELF, level 25, lesson 0
Locked
Actor's suitcase
Actor's suitcase
1
Task
Go SELF, level 25, lesson 0
Locked
Empty box
Empty box
1
Task
Go SELF, level 25, lesson 0
Locked
Event context
Event context
1
Task
Go SELF, level 25, lesson 0
Locked
Key Safe
Key Safe
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION