I’ve noticed something important about programming: a concept can look complicated when you’re studying the solution, but feel natural when you understand the problem that created the need for it.

Take passing a function to another function in Go.

1. Looking at it from the syntax perspective

You see:

func execute(fn func(int) int) {
    result := fn(10)
    fmt.Println(result)
}

And you think:

Who came up with this? Why would I ever think to pass a function into another function? How do people even come up with these abstractions?

You’re trying to understand the idea starting from its final form.

2. Looking at it from the design perspective

Imagine you’re building something.

First, you write:

func processOrder() {
    fmt.Println("Starting")

    fmt.Println("Processing order")

    fmt.Println("Finished")
}

Then you need to process a payment:

func processPayment() {
    fmt.Println("Starting")

    fmt.Println("Processing payment")

    fmt.Println("Finished")
}

You notice something.

Both functions have the same structure:

  1. Start
  2. Do something
  3. Finish

The only thing that changes is what happens in the middle.

So you ask:

“Can I write the common structure once and let the caller decide what happens in the middle?”

That question naturally leads to:

func execute(task func()) {
    fmt.Println("Starting")
    task()
    fmt.Println("Finished")
}

Now:

execute(func() {
    fmt.Println("Processing order")
})

execute(func() {
    fmt.Println("Processing payment")
})

Suddenly, passing a function to a function doesn’t feel like some genius-level invention.

It’s just the consequence of a design decision.

3. The deeper insight

I think there are two different ways to learn programming.

Approach A: Concept first

You learn:

  • Higher-order functions
  • Interfaces
  • Dependency injection
  • Generics
  • Design patterns

And then try to figure out where to use them.

This can make programming feel like memorizing clever tricks.

Approach B: Problem first

You encounter a problem.

You write the simplest solution.

You notice duplication, rigidity, or unnecessary complexity.

You ask what needs to change.

And eventually you discover an abstraction that solves it.

This is often how these concepts become intuitive.

4. But there’s an important distinction

Not every abstraction will emerge naturally from every problem.

Someone with experience recognizes patterns because they’ve encountered similar problems before.

A beginner might duplicate code five times without recognizing the opportunity to pass a function.

An experienced developer might immediately see that a callback is useful.

That doesn’t necessarily mean the experienced developer is smarter. They have developed a larger collection of mental models.

The goal isn’t to invent every programming concept independently. It’s to understand the problem each concept solves so deeply that you recognize when you need it.

And there’s something else worth remembering:

Sometimes the simplest design is not to pass a function, use an interface, or introduce an abstraction. Good design also means recognizing when those things are unnecessary.

So yes, your intuition about looking at programming from a design perspective is largely correct.

I’d suggest changing the question you ask when learning a new concept.

Instead of:

“How would I ever come up with this?”

Ask:

“What problem would make this solution feel obvious?”

That question can fundamentally change how you learn software engineering.