DEV Community

Cover image for Decoding Golang Interfaces: Why Your Struct Fits an Interface It Never Met

Decoding Golang Interfaces: Why Your Struct Fits an Interface It Never Met

If you’re coming to Golang from a language like Java, C#, or other language, you’ve probably run into a piece of code that left you scratching your head. It looks something like this:

package main
import "fmt"

// 1. The Concrete Type
type A struct {
    abc string
}

// 2. The Method attached to the type
func (a *A) f1() {
    fmt.Println("f1 function: ", a.abc)
}

// 3. The Abstract Interface
type I interface {
    f1()
}

// 4. The Consumer Function
func wantsI(i I) {
    i.f1()
}

// 5. The Execution
func main() {
    a := &A{}
    a.abc = "test value"

    // ➡ The "magic" question is right here: ⬅
    wantsI(a) 

    fmt.Println("method executed!...")
}
Enter fullscreen mode Exit fullscreen mode

When you run this, it works perfectly and prints:
f1 function: test value
method executed!...

This leads to the central question that stumps many new Go developers:
Why does this work? The wantsI function clearly states it only accepts a parameter of type I (the interface). Yet, we passed it a, which is a variable of type *A (a pointer to a struct). We never wrote "struct A implements I."

How can a function expecting an interface accept a struct that seemingly has no connection to it?

The answer is the single most powerful and defining feature of the Go language: implicit interface implementation.

Let's break it down how concrete types (structs) implicitly satisfy abstract types (interfaces) & Unlock the power of polymorphism in Go.


🧱 The Building Blocks: Concrete vs. Abstract Types

First, you have to understand the two different kinds of types you created.

1. The Concrete Type: struct A

A "concrete type" is a blueprint for a real thing that holds data.

  • Definition: Your struct A is a concrete blueprint. It describes a block of memory that will be created to hold exactly one piece of data: a string named abc.

  • Example: When you write a := &A{}, you are building an actual instance of this blueprint. It's a real, tangible thing in your program's memory. Think of it as a specific worker you just hired.

2. The Abstract Type: interface I

An "abstract type," or interface, is not a thing. It’s a set of rules—a contract.

  • Definition: Your interface I holds zero data. It only defines a list of behaviors (methods).

  • The Contract: This interface says, "I don't care what you are, what data you hold, or where you come from. To be considered an I by the Go compiler, you must provide one behavior: a method called f1() that takes no arguments and returns nothing."


✨ The magic Explained: Go's Implicit Implementation

Here is the entire magic trick, explained.

In language like Java, you must explicitly state your intent: public class MyClass implements MyInterface. You are telling the compiler your intend to follow the contract.

Go doesn't work that way. "Don't tell me what you intend to do. Just show me what you can do."

🔄 The Rule in Go: A type automatically and implicitly implements an interface if it possesses all the methods that the interface requires.

Let's apply this rule to our code:

  1. The Contract (interface I): Requires one method: f1().
  2. The Candidate (type *A): Does our type *A have all the required methods?
  3. The Check: Yes. We defined this exact method: func (a *A) f1(). Its name (f1) and signature (no args, no returns) perfectly match the contract.
  4. The Verdict: Because *A has the f1() method, the Go compiler automatically concludes: "Type *A satisfies interface I."

That's it. There is no step five.

Because *A satisfies I, any variable of type *A (like your a) can be legally passed to any function that requires an I (like wantsI).

Inside the wantsI function, the parameter i is an interface value. Think of it as a box that holds two things:

  1. A label identifying its true, concrete type (in this case, *A).
  2. The data itself (the pointer to your struct).

When i.f1() is called, Go looks in the box, sees the type is *A, and calls the specific f1() method attached to *A.


(Bonus)

⚠️ The Critical Detail: Pointers vs. Values (The "Method Set")

There's one more crucial, expert-level detail here that you got right by default. Notice your method definition:
func (a *A) f1()
This is a pointer receiver.This means the f1() is not attached to the A struct itself, but to a pointer to an A struct (*A). This defines what's called the method set.

  • The method set for type *A(the pointer) includes f1().✅
  • The method set for type A(the value) doesn't include f1().❌

This means *A implements I, but the plain A Does NOT ! ✅

This is why your main function must create a pointer (a := &A{}) to work. If you tried to create a value instead, the program would fail to compile.

Example (What Doesn't Work)

func main() {
    // Create a_val as a VALUE (type A), not a pointer
    a_val := A{} 
    a_val.abc = "test value"

    // This line will cause a compile-time error!
    wantsI(a_val) 
}
Enter fullscreen mode Exit fullscreen mode

The compiler will not run while showing this error:
cannot use a_val (variable of type A) as I value in argument to wantsI: A does not implement I (method f1 has pointer receiver)

This error message is the compiler explicitly telling you the logic: A doesn't implement I because its method set is missing f1(), which is only on the pointer type.


💬 Final Takeaway

This design is the genius of Go. Interfaces are not about identity or inheritance (what a thing is). They are about behavior (what a thing can do).

This allows you to write functions like wantsI that are completely decoupled from your concrete data structures like A. You can write 100 different structs, and as long as they all have an f1() method, your wantsI function can accept every single one of them without ever knowing they exist. This is the key to flexible, testable, and maintainable software.

📜 Referance

Top comments (0)