These three concepts are confusing initially because a method looks almost exactly like a function. The easiest way is to learn them in this order:
Function → Struct → Method → Interface
Forget interfaces for a moment.
1. Function = a standalone piece of work
You already know this:
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
func main() {
result := add(10, 20)
fmt.Println(result)
}
Here:
func add(a int, b int) int
means:
func add (a int, b int) int
↓ ↓ ↓ ↓
function name inputs return type
And you call it directly:
add(10, 20)
Think:
Function = independent action.
2. Struct = an object/data type
Now suppose we’re making a Person.
type Person struct {
Name string
Age int
}
Then:
p := Person{
Name: "Rajesh",
Age: 42,
}
We have:
Person
├── Name
└── Age
But currently the Person only has data.
What if we want the Person to perform an action?
That’s where a method comes in.
3. Method = function attached to a type
Example:
package main
import "fmt"
type Person struct {
Name string
}
func (p Person) sayHello() {
fmt.Println("Hello, my name is", p.Name)
}
func main() {
p := Person{Name: "Rajesh"}
p.sayHello()
}
Output:
Hello, my name is Rajesh
Look carefully at this:
func (p Person) sayHello() {
Compare it with a normal function:
func sayHello() {
The only important difference is this:
(p Person)
That’s called the receiver.
It means:
sayHello()belongs toPerson.
Therefore we call it like:
p.sayHello()
rather than:
sayHello()
4. Function vs Method
This is the key distinction.
Function
func add(a int, b int) int {
return a + b
}
Call:
add(10, 20)
Method
func (p Person) sayHello() {
fmt.Println(p.Name)
}
Call:
p.sayHello()
So:
FUNCTION
────────
add(10, 20)
↑
standalone
METHOD
──────
p.sayHello()
↑ ↑
| method
|
Person object
The easiest rule:
A method is basically a function attached to a type.
5. One example with both Function and Method
package main
import "fmt"
type Person struct {
Name string
Age int
}
// Normal function
func add(a int, b int) int {
return a + b
}
// Method of Person
func (p Person) introduce() {
fmt.Println("My name is", p.Name)
}
func main() {
result := add(10, 20)
fmt.Println(result)
p := Person{
Name: "Rajesh",
Age: 42,
}
p.introduce()
}
There are now two different things:
add()
│
└── normal function
Person
│
└── introduce()
│
└── method
6. Why use methods at all?
Imagine:
type BankAccount struct {
Balance float64
}
Actions related to a bank account naturally belong to the account:
account.Deposit(1000)
account.Withdraw(500)
account.ShowBalance()
Instead of writing:
deposit(account, 1000)
withdraw(account, 500)
showBalance(account)
Go lets us associate behavior with the type.
Example:
package main
import "fmt"
type BankAccount struct {
Balance float64
}
func (a BankAccount) showBalance() {
fmt.Println("Balance:", a.Balance)
}
func main() {
account := BankAccount{
Balance: 5000,
}
account.showBalance()
}
Think:
BankAccount
│
├── Balance ← data
│
└── showBalance ← behavior/method
Now methods should hopefully make sense.
7. NOW interface
Interface sounds complicated, but its basic idea is simple:
An interface says which methods something must have.
It doesn’t contain the actual implementation.
For example:
type Speaker interface {
Speak()
}
This says:
Anything having a
Speak()method can be considered aSpeaker.
That’s it.
10 practical use cases of interfaces in Go
| # | Use case | Why use an interface? |
|---|---|---|
| 1 | Payment methods | Support Credit Card, UPI, PayPal, etc. through the same API |
| 2 | Database / Repository | Switch MySQL, PostgreSQL, memory storage, etc. without changing business logic |
| 3 | Logging | Switch console, file, Datadog, CloudWatch, etc. |
| 4 | Notifications | Handle Email, SMS, Push notifications uniformly |
| 5 | File/Storage systems | Switch local disk, S3, GCS, Azure Blob |
| 6 | Testing / Mocking | Replace real external systems with fake implementations |
| 7 | Authentication providers | Support Google, GitHub, LDAP, Keycloak, etc. |
| 8 | Message queues | Switch Kafka, RabbitMQ, SQS, etc. |
| 9 | Different shapes/types with common behavior | Circle, Rectangle, Triangle can all implement Area() |
| 10 | Standard-library interoperability | Implement interfaces such as io.Reader, io.Writer, error, fmt.Stringer |
Interface → WHAT you can do
Struct → WHAT you are / what data you have
Method → HOW you do something
API Gateway
↓
Provides one common entry point
↓
Different backend services
Go Interface
↓
Provides one common contract
↓
Different implementations
8. Simple interface example
Let’s create a Dog:
type Dog struct {
Name string
}
Give Dog a method:
func (d Dog) Speak() {
fmt.Println("Woof!")
}
Now create an interface:
type Speaker interface {
Speak()
}
Dog has:
Speak()
and Speaker requires:
Speak()
Therefore:
Dog satisfies Speaker
You don’t need to write:
Dog implements Speaker
anywhere.
Go figures it out automatically.
9. Complete example
package main
import "fmt"
type Speaker interface {
Speak()
}
type Dog struct {
Name string
}
func (d Dog) Speak() {
fmt.Println(d.Name, "says Woof!")
}
func main() {
dog := Dog{
Name: "Tommy",
}
dog.Speak()
}
So far the interface isn’t doing anything useful.
Now watch this.
10. Another type can implement the same interface
Create a Person:
type Person struct {
Name string
}
func (p Person) Speak() {
fmt.Println(p.Name, "says Hello!")
}
Now:
Dog
└── Speak()
Person
└── Speak()
Both have Speak().
Therefore both satisfy:
type Speaker interface {
Speak()
}
11. This is where interface becomes useful
Create a function:
func makeSpeak(s Speaker) {
s.Speak()
}
Notice:
s Speaker
The function doesn’t ask for:
Dog
and doesn’t ask for:
Person
It says:
Give me anything capable of speaking.
Complete program:
package main
import "fmt"
type Speaker interface {
Speak()
}
type Dog struct {
Name string
}
func (d Dog) Speak() {
fmt.Println(d.Name, "says Woof!")
}
type Person struct {
Name string
}
func (p Person) Speak() {
fmt.Println(p.Name, "says Hello!")
}
func makeSpeak(s Speaker) {
s.Speak()
}
func main() {
dog := Dog{Name: "Tommy"}
person := Person{Name: "Rajesh"}
makeSpeak(dog)
makeSpeak(person)
}
Output:
Tommy says Woof!
Rajesh says Hello!
This is the important part:
makeSpeak(dog)
makeSpeak(person)
Same function accepts completely different types.
Why?
Because both have:
Speak()
12. Visualize the relationship
Speaker interface
┌──────────────┐
│ Speak() │
└──────┬───────┘
│
requires Speak()
│
┌─────────┴─────────┐
│ │
Dog Person
│ │
Speak() Speak()
│ │
"Woof" "Hello"
The interface doesn’t care how they speak.
It only cares:
Do you have Speak()?
If yes → accepted.
13. Real-world analogy
Think about a USB port.
The USB port defines a contract:
USB Device
│
└── connect()
Many devices can satisfy it:
Keyboard ──→ connect()
Mouse ──→ connect()
Camera ──→ connect()
Drive ──→ connect()
The computer doesn’t care whether it’s:
keyboard
mouse
camera
disk
It cares that it follows the required interface.
Go interfaces work similarly.
14. Function, Method and Interface together
Here’s the mental model I recommend memorizing:
FUNCTION
========
func add(a, b int) int
Called:
add(10, 20)
Meaning:
"Do some work"
METHOD
======
func (p Person) Speak()
Called:
p.Speak()
Meaning:
"Person can do this"
INTERFACE
=========
type Speaker interface {
Speak()
}
Meaning:
"I accept anything that can Speak()"
That’s essentially the entire relationship.
The easiest way to understand an interface is to see the same problem without and with an interface.
Suppose we have an application that sends notifications.
Without Interface
We create an email sender:
package main
import "fmt"
type Email struct{}
func (e Email) Send(message string) {
fmt.Println("Email:", message)
}
func Notify(email Email, message string) {
email.Send(message)
}
func main() {
email := Email{}
Notify(email, "Server is down")
}
Notice this:
func Notify(email Email, message string)
Notify() specifically requires an Email.
Now suppose tomorrow we introduce SMS:
type SMS struct{}
func (s SMS) Send(message string) {
fmt.Println("SMS:", message)
}
We cannot do this:
sms := SMS{}
Notify(sms, "Server is down") // ❌ ERROR
Why?
Because Notify() says:
func Notify(email Email, message string)
It requires specifically:
Email
not SMS.
So we may start creating separate functions:
func NotifyEmail(email Email, message string) {
email.Send(message)
}
func NotifySMS(sms SMS, message string) {
sms.Send(message)
}
Later:
Email
SMS
WhatsApp
Slack
Push Notification
Our code becomes increasingly tied to specific types.
With Interface
Now let’s define the behavior we actually need:
type Sender interface {
Send(message string)
}
This says:
Give me anything that has a
Send(string)method.
Complete example:
package main
import "fmt"
// Interface defines the required behavior.
type Sender interface {
Send(message string)
}
// Email implementation
type Email struct{}
func (e Email) Send(message string) {
fmt.Println("Email:", message)
}
// SMS implementation
type SMS struct{}
func (s SMS) Send(message string) {
fmt.Println("SMS:", message)
}
// Notify does NOT care whether it receives
// Email, SMS, WhatsApp, etc.
//
// It only cares that the value can Send().
func Notify(sender Sender, message string) {
sender.Send(message)
}
func main() {
email := Email{}
sms := SMS{}
Notify(email, "Server is down")
Notify(sms, "Server is down")
}
Output:
Email: Server is down
SMS: Server is down
The important change is just this:
Without interface
func Notify(sender Email, message string)
Means:
Give me an Email.
With interface
func Notify(sender Sender, message string)
Means:
Give me anything that can Send().
That’s the heart of interfaces.
WITHOUT INTERFACE
Notify()
│
│ requires exact type
↓
Email
WITH INTERFACE
Sender
Send(string)
│
┌──────┼──────┐
↓ ↓ ↓
Email SMS WhatsApp
│ │ │
└──────┼──────┘
↓
Notify()
| Without interface | With interface |
|---|---|
| Depends on exact type | Depends on behavior |
Notify(Email) | Notify(Sender) |
Only Email works | Any Sender works |
| Harder to extend | Easier to extend |
| More tightly coupled | More flexible |
SMS cannot be passed | SMS can be passed if it has Send() |
One sentence to remember
Without interface:
“I need an
With interface:
“I don’t care whether you’re Email, SMS, or something else — if you can
Send(), I can use you.”
That is probably the simplest mental model for Go interfaces.
15. Look at the syntax side-by-side
Function
func hello() {
}
Method
func (p Person) hello() {
}
See the extra:
(p Person)
That’s what turns the function into a method associated with Person.
Interface
type Speaker interface {
Speak()
}
Notice there is no implementation:
Speak()
not:
func Speak() {
// implementation
}
An interface describes what must exist, not how to do it.
16. One sentence for each
Memorize these:
Function
A function performs some work.
add(10, 20)
Method
A method is a function attached to a type.
person.Speak()
Interface
An interface defines methods that a type must provide.
type Speaker interface {
Speak()
}
17. Don’t learn interfaces too deeply yet
At your current stage, I would learn Go in this sequence:
1. Variables
↓
2. if / switch
↓
3. for / range
↓
4. Functions
↓
5. Struct
↓
6. Methods
↓
7. Pointers
↓
8. Interfaces
↓
9. Error handling
↓
10. Goroutines / Channels
In particular, don’t try to master interfaces before structs and methods are comfortable.
For now, the most important relationship is:
STRUCT
Person
│
│ gets behavior through
↓
METHOD
Speak()
│
│ method can satisfy
↓
INTERFACE
Speaker
And the single most important Go interface rule is:
If your type has all the methods required by an interface,
it automatically satisfies that interface.
No implements keyword is needed in Go.
1. Your First Go Interface
package main
import "fmt"
// Speaker is an interface.
//
// An interface describes BEHAVIOR.
//
// Speaker says:
// "Any type that has a Speak() string method
// can be used as a Speaker."
type Speaker interface {
Speak() string
}
// Dog is a normal concrete struct type.
type Dog struct {
Name string
}
// Speak is a method on Dog.
//
// Because this method has exactly the same signature
// required by Speaker:
//
// Speak() string
//
// Dog automatically satisfies the Speaker interface.
//
// Go does NOT require:
// "implements Speaker"
//
// Interface implementation is implicit.
func (d Dog) Speak() string {
return d.Name + " says Woof!"
}
// printSpeech accepts Speaker instead of Dog.
//
// Because of this, the function can accept ANY type
// that provides:
//
// Speak() string
func printSpeech(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
// Create a concrete Dog value.
dog := Dog{
Name: "Buddy",
}
// Dog satisfies Speaker,
// so Dog can be passed here.
printSpeech(dog)
// Output:
//
// Buddy says Woof!
}
2. One Interface, Multiple Types
package main
import "fmt"
// Speaker describes one capability:
//
// "I can speak."
type Speaker interface {
Speak() string
}
// Dog is one completely independent type.
type Dog struct {
Name string
}
// Dog satisfies Speaker because Dog has Speak() string.
func (d Dog) Speak() string {
return d.Name + ": Woof!"
}
// Person is another completely different type.
type Person struct {
Name string
}
// Person also satisfies Speaker.
//
// Notice:
// Person and Dog do NOT inherit from each other.
//
// They simply provide the same required behavior.
func (p Person) Speak() string {
return p.Name + ": Hello!"
}
// Robot is another unrelated type.
type Robot struct {
ID string
}
// Robot also satisfies Speaker.
func (r Robot) Speak() string {
return r.ID + ": Beep beep!"
}
// announce does not care whether the value is:
//
// - Dog
// - Person
// - Robot
//
// It only cares that the value can Speak().
func announce(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
dog := Dog{Name: "Buddy"}
person := Person{Name: "Alice"}
robot := Robot{ID: "R2"}
announce(dog)
announce(person)
announce(robot)
// Output:
//
// Buddy: Woof!
// Alice: Hello!
// R2: Beep beep!
}
3. Interface with Multiple Methods
package main
import "fmt"
// Animal requires TWO methods.
//
// A type must provide BOTH methods
// to satisfy Animal.
type Animal interface {
Speak() string
Move() string
}
type Dog struct {
Name string
}
// First required method.
func (d Dog) Speak() string {
return d.Name + " says Woof!"
}
// Second required method.
func (d Dog) Move() string {
return d.Name + " is running"
}
// describeAnimal can safely call both methods
// because Animal guarantees both exist.
func describeAnimal(a Animal) {
fmt.Println(a.Speak())
fmt.Println(a.Move())
}
func main() {
dog := Dog{Name: "Buddy"}
describeAnimal(dog)
// Output:
//
// Buddy says Woof!
// Buddy is running
}
4. Missing a Required Method
package main
// Animal requires both Speak and Move.
type Animal interface {
Speak() string
Move() string
}
type Cat struct{}
// Cat has Speak().
func (Cat) Speak() string {
return "Meow"
}
// Cat does NOT have:
//
// func (Cat) Move() string
//
// Therefore Cat does NOT satisfy Animal.
func main() {
cat := Cat{}
_ = cat
// This would NOT compile:
//
// var animal Animal = cat
//
// Compiler reason:
//
// Cat does not implement Animal
// because Cat is missing Move().
}
5. Interface Method Signatures Must Match Exactly
package main
// Speaker requires:
//
// Speak() string
type Speaker interface {
Speak() string
}
type Dog struct{}
// This method does NOT satisfy Speaker.
//
// Why?
//
// Speaker requires:
//
// Speak() string
//
// But Dog provides:
//
// Speak(int) string
//
// The parameter list is different.
func (Dog) Speak(volume int) string {
if volume > 5 {
return "WOOF!"
}
return "woof"
}
func main() {
dog := Dog{}
_ = dog
// This would fail:
//
// var speaker Speaker = dog
//
// because:
//
// Speak(int) string
//
// is not the same method signature as:
//
// Speak() string
}
6. Storing a Concrete Value Inside an Interface
package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return "Woof from " + d.Name
}
func main() {
// speaker has STATIC type Speaker.
var speaker Speaker
// We store a concrete Dog inside it.
speaker = Dog{
Name: "Buddy",
}
// Conceptually the interface now contains:
//
// dynamic type = Dog
// dynamic value = Dog{Name: "Buddy"}
//
// The variable itself is still typed as Speaker.
fmt.Println(speaker.Speak())
// Output:
//
// Woof from Buddy
}
7. Practical Use Case: Notification System
package main
import "fmt"
// Notifier represents one behavior:
//
// "I know how to send a notification."
//
// The business code does not need to know
// HOW the notification is sent.
type Notifier interface {
Notify(message string) error
}
// EmailNotifier is one implementation.
type EmailNotifier struct {
Email string
}
// Notify makes EmailNotifier satisfy Notifier.
func (e EmailNotifier) Notify(message string) error {
fmt.Println("EMAIL TO:", e.Email)
fmt.Println("MESSAGE:", message)
return nil
}
// SMSNotifier is another implementation.
type SMSNotifier struct {
Phone string
}
// Same interface method,
// completely different implementation.
func (s SMSNotifier) Notify(message string) error {
fmt.Println("SMS TO:", s.Phone)
fmt.Println("MESSAGE:", message)
return nil
}
// sendAlert works with ANY Notifier.
//
// This is the important abstraction:
//
// sendAlert does not depend on EmailNotifier.
// sendAlert does not depend on SMSNotifier.
//
// It depends only on:
// "something that can Notify".
func sendAlert(n Notifier, message string) error {
return n.Notify(message)
}
func main() {
email := EmailNotifier{
Email: "alice@example.com",
}
sms := SMSNotifier{
Phone: "+123456789",
}
_ = sendAlert(email, "Server is down")
_ = sendAlert(sms, "Server is down")
}
8. Practical Use Case: Payment Processing
package main
import "fmt"
// PaymentProcessor defines what checkout needs.
//
// Checkout does not care whether payment is handled by:
//
// - credit card
// - bank
// - wallet
// - fake test implementation
//
// It only needs Charge().
type PaymentProcessor interface {
Charge(amount float64) error
}
type CreditCardProcessor struct{}
func (CreditCardProcessor) Charge(amount float64) error {
fmt.Printf("Charging credit card: $%.2f\n", amount)
return nil
}
type BankProcessor struct{}
func (BankProcessor) Charge(amount float64) error {
fmt.Printf("Charging bank account: $%.2f\n", amount)
return nil
}
// checkout depends on behavior,
// not a specific payment provider.
func checkout(
processor PaymentProcessor,
amount float64,
) error {
return processor.Charge(amount)
}
func main() {
card := CreditCardProcessor{}
bank := BankProcessor{}
_ = checkout(card, 100)
_ = checkout(bank, 250)
}
9. Value Receiver and Interface
package main
import "fmt"
type Reader interface {
Read() string
}
type Book struct {
Title string
}
// Read uses a VALUE receiver:
//
// (b Book)
//
// Therefore the method belongs to the method set
// needed for Book values.
//
// A *Book can also use value-receiver methods.
func (b Book) Read() string {
return "Reading " + b.Title
}
func printReading(r Reader) {
fmt.Println(r.Read())
}
func main() {
// Concrete value.
bookValue := Book{
Title: "Learning Go",
}
// Pointer to concrete value.
bookPointer := &Book{
Title: "Advanced Go",
}
// Both work because Read uses a value receiver.
printReading(bookValue)
printReading(bookPointer)
}
10. Pointer Receiver and Interface
package main
import "fmt"
type Saver interface {
Save()
}
type Document struct {
Name string
}
// Save uses a POINTER receiver:
//
// (d *Document)
//
// Therefore *Document satisfies Saver.
//
// Plain Document does NOT have this pointer-receiver
// method in its method set for interface satisfaction.
func (d *Document) Save() {
fmt.Println("Saving:", d.Name)
}
func main() {
// Pointer value.
document := &Document{
Name: "report.txt",
}
// This works because *Document satisfies Saver.
var saver Saver = document
saver.Save()
// This would NOT compile:
//
// var saver2 Saver = Document{
// Name: "notes.txt",
// }
//
// Reason:
//
// Save() is defined on *Document,
// not on Document.
}
11. Direct Method Calls vs Interface Method Sets
package main
import "fmt"
type Saver interface {
Save()
}
type File struct {
Name string
}
// Pointer receiver.
func (f *File) Save() {
fmt.Println("Saving:", f.Name)
}
func main() {
// file is a normal value.
file := File{
Name: "data.txt",
}
// This works.
//
// Because file is addressable,
// Go can automatically treat this approximately like:
//
// (&file).Save()
file.Save()
// But this is a DIFFERENT question:
//
// Does File satisfy Saver?
//
// No.
//
// *File satisfies Saver.
//
// Therefore this works:
var saver Saver = &file
saver.Save()
// But this would fail:
//
// var wrong Saver = file
}
12. Method-Set Rule
package main
// Reader requires Read().
type Reader interface {
Read()
}
// Writer requires Write().
type Writer interface {
Write()
}
type User struct{}
// Value receiver.
//
// Conceptually:
//
// User has Read()
// *User also has Read()
func (User) Read() {}
// Pointer receiver.
//
// Conceptually:
//
// *User has Write()
//
// Plain User does not satisfy an interface
// requiring Write().
func (*User) Write() {}
func main() {
user := User{}
// User satisfies Reader.
var r1 Reader = user
// *User also satisfies Reader.
var r2 Reader = &user
// *User satisfies Writer.
var w1 Writer = &user
_, _, _ = r1, r2, w1
// This would fail:
//
// var w2 Writer = user
}
13. Compile-Time Interface Check
package main
type Notifier interface {
Notify(string) error
}
type EmailNotifier struct{}
func (*EmailNotifier) Notify(message string) error {
return nil
}
// This line performs a compile-time interface check.
//
// It asks:
//
// "Does *EmailNotifier satisfy Notifier?"
//
// The blank identifier _ means:
//
// "We don't need to store this value."
//
// If EmailNotifier stops satisfying Notifier,
// compilation fails immediately.
var _ Notifier = (*EmailNotifier)(nil)
func main() {}
14. The Empty Interface
package main
import "fmt"
func main() {
// interface{} contains ZERO required methods.
//
// Every normal Go type satisfies an interface
// with zero method requirements.
var value interface{}
value = 100
fmt.Println(value)
value = "Hello"
fmt.Println(value)
value = true
fmt.Println(value)
value = []int{1, 2, 3}
fmt.Println(value)
// Output:
//
// 100
// Hello
// true
// [1 2 3]
}
15. any
package main
import "fmt"
// any is an alias for interface{}.
//
// These are equivalent:
//
// var x interface{}
//
// var x any
//
// In modern Go code, any is usually easier to read
// when arbitrary values are genuinely needed.
func printAnything(value any) {
fmt.Println(value)
}
func main() {
printAnything(10)
printAnything("Go")
printAnything(true)
printAnything(3.14)
printAnything([]string{"A", "B"})
}
16. Do Not Use any When a Better Type Exists
package main
// This function is clear.
//
// The compiler knows exactly what goes in
// and exactly what comes out.
func addGood(a, b int) int {
return a + b
}
// This design would be much harder to understand:
//
// func addBad(a, b any) any
//
// Problems:
//
// 1. What types are allowed?
// 2. What type is returned?
// 3. What happens for strings?
// 4. What happens for structs?
// 5. Errors move from compile time toward runtime.
//
// Use any only when arbitrary types are actually part
// of the problem you are solving.
func main() {
result := addGood(10, 20)
_ = result
}
17. Type Assertion
package main
import "fmt"
func main() {
// value has static type any.
//
// Its dynamic type is string.
var value any = "Hello Go"
// Type assertion syntax:
//
// interfaceValue.(ConcreteType)
//
// Here we are saying:
//
// "Give me the string stored inside value."
text := value.(string)
fmt.Println(text)
// Output:
//
// Hello Go
}
18. Unsafe Type Assertion
package main
func main() {
var value any = 100
// value contains an int.
//
// Therefore this assertion would panic:
//
// text := value.(string)
//
// Runtime reason:
//
// value contains int,
// but we demanded string.
_ = value
}
19. Safe Type Assertion
package main
import "fmt"
func main() {
var value any = "Hello"
// Two-value type assertion:
//
// text, ok := value.(string)
//
// text receives the value if successful.
//
// ok becomes true when successful.
//
// This avoids a panic.
text, ok := value.(string)
if !ok {
fmt.Println("value is not a string")
return
}
fmt.Println("String:", text)
// Output:
//
// String: Hello
}
20. Failed Safe Type Assertion
package main
import "fmt"
func main() {
var value any = 123
text, ok := value.(string)
// Assertion fails.
//
// ok becomes false.
//
// text receives string's zero value:
//
// ""
fmt.Printf("text = %q\n", text)
fmt.Println("ok =", ok)
// Output:
//
// text = ""
// ok = false
}
21. Type Switch
package main
import "fmt"
func describe(value any) {
// value.(type) is special syntax
// used inside a type switch.
switch v := value.(type) {
case int:
// Inside this case,
// v is an int.
fmt.Println("Integer:", v)
case string:
// Here v is a string.
fmt.Println("String:", v)
case bool:
// Here v is a bool.
fmt.Println("Boolean:", v)
case float64:
// Here v is a float64.
fmt.Println("Float:", v)
default:
// Any unmatched type arrives here.
fmt.Println("Unknown type")
}
}
func main() {
describe(10)
describe("Go")
describe(true)
describe(3.14)
describe([]int{1, 2})
// Output:
//
// Integer: 10
// String: Go
// Boolean: true
// Float: 3.14
// Unknown type
}
22. Interface Composition
package main
import "fmt"
// Reader describes reading behavior.
type Reader interface {
Read() string
}
// Writer describes writing behavior.
type Writer interface {
Write(string)
}
// ReadWriter embeds both interfaces.
//
// Therefore a value must provide:
//
// Read() string
// Write(string)
//
// to satisfy ReadWriter.
type ReadWriter interface {
Reader
Writer
}
type Document struct {
Content string
}
func (d Document) Read() string {
return d.Content
}
func (d *Document) Write(content string) {
d.Content = content
}
func useDocument(rw ReadWriter) {
rw.Write("Go Interfaces")
fmt.Println(rw.Read())
}
func main() {
// We use *Document because Write has a pointer receiver.
document := &Document{}
useDocument(document)
// Output:
//
// Go Interfaces
}
23. Small Interfaces
package main
// Small interfaces usually create lower coupling.
// Reader needs only reading behavior.
type Reader interface {
Read() string
}
// Writer needs only writing behavior.
type Writer interface {
Write(string)
}
// Deleter needs only deletion behavior.
type Deleter interface {
Delete() error
}
// Instead of forcing every consumer to depend on:
//
// type Everything interface {
// Read() string
// Write(string)
// Delete() error
// Export() error
// Backup() error
// Email() error
// }
//
// each consumer can depend only on what it actually needs.
func display(r Reader) {
// display only needs Read().
//
// Therefore requiring Writer or Deleter here
// would unnecessarily increase coupling.
_ = r.Read()
}
func main() {}
24. Consumer-Side Interface
package main
type User struct {
ID int
Name string
}
// Imagine a real database repository has many methods:
//
// Save
// Delete
// FindByID
// FindByEmail
// List
// Update
// Count
//
// But this service needs only FindByID.
//
// Therefore the consumer can define a tiny interface
// containing only what it actually uses.
type UserFinder interface {
FindByID(id int) (User, error)
}
type UserService struct {
finder UserFinder
}
func NewUserService(finder UserFinder) *UserService {
return &UserService{
finder: finder,
}
}
func (s *UserService) GetUser(id int) (User, error) {
return s.finder.FindByID(id)
}
func main() {}
25. Dependency Injection with Interfaces
package main
import "fmt"
// EmailSender describes the dependency
// needed by UserService.
type EmailSender interface {
Send(to string, message string) error
}
// UserService stores the INTERFACE,
// not a specific SMTP implementation.
type UserService struct {
emailSender EmailSender
}
// The dependency is supplied from outside.
//
// This is dependency injection.
//
// No special framework is required.
func NewUserService(
sender EmailSender,
) *UserService {
return &UserService{
emailSender: sender,
}
}
func (s *UserService) Register(email string) error {
fmt.Println("Creating user:", email)
// UserService does not know HOW email is sent.
//
// It only knows the dependency supports Send().
return s.emailSender.Send(
email,
"Welcome!",
)
}
// Production implementation.
type SMTPEmailSender struct{}
func (SMTPEmailSender) Send(
to string,
message string,
) error {
fmt.Println("SMTP email to:", to)
fmt.Println("Message:", message)
return nil
}
func main() {
sender := SMTPEmailSender{}
service := NewUserService(sender)
_ = service.Register("alice@example.com")
}
26. Testing with a Fake Implementation
package main
import "fmt"
type EmailSender interface {
Send(to string, message string) error
}
type UserService struct {
sender EmailSender
}
func NewUserService(sender EmailSender) *UserService {
return &UserService{
sender: sender,
}
}
func (s *UserService) Register(email string) error {
return s.sender.Send(
email,
"Welcome!",
)
}
// FakeEmailSender is used during tests.
//
// Instead of sending a real email,
// it records what happened.
type FakeEmailSender struct {
Called bool
To string
Message string
}
func (f *FakeEmailSender) Send(
to string,
message string,
) error {
f.Called = true
f.To = to
f.Message = message
return nil
}
func main() {
// Create fake dependency.
fake := &FakeEmailSender{}
// Inject fake dependency.
service := NewUserService(fake)
_ = service.Register("alice@example.com")
// Tests can inspect what happened.
fmt.Println(fake.Called)
fmt.Println(fake.To)
fmt.Println(fake.Message)
// Output:
//
// true
// alice@example.com
// Welcome!
}
27. Repository Interface
package main
import (
"errors"
"fmt"
)
type User struct {
ID int
Name string
}
// Business logic depends on this behavior,
// not on PostgreSQL/MySQL/etc.
type UserRepository interface {
Save(user User) error
FindByID(id int) (User, error)
}
// MemoryUserRepository is one implementation.
//
// This could be used for:
//
// - learning
// - tests
// - prototypes
type MemoryUserRepository struct {
users map[int]User
}
func NewMemoryUserRepository() *MemoryUserRepository {
return &MemoryUserRepository{
users: make(map[int]User),
}
}
func (r *MemoryUserRepository) Save(user User) error {
r.users[user.ID] = user
return nil
}
func (r *MemoryUserRepository) FindByID(
id int,
) (User, error) {
user, ok := r.users[id]
if !ok {
return User{}, errors.New("user not found")
}
return user, nil
}
func main() {
var repository UserRepository
repository = NewMemoryUserRepository()
_ = repository.Save(User{
ID: 1,
Name: "Alice",
})
user, err := repository.FindByID(1)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(user.Name)
// Output:
//
// Alice
}
28. The error Interface
package main
import "fmt"
// error is itself an interface.
//
// Conceptually:
//
// type error interface {
// Error() string
// }
//
// Therefore any type that provides:
//
// Error() string
//
// can be returned as an error.
type ValidationError struct {
Field string
Message string
}
// By implementing Error() string,
// ValidationError satisfies the built-in error interface.
func (e ValidationError) Error() string {
return e.Field + ": " + e.Message
}
func validateAge(age int) error {
if age < 18 {
// ValidationError can be returned here
// because it satisfies error.
return ValidationError{
Field: "age",
Message: "must be 18 or older",
}
}
return nil
}
func main() {
err := validateAge(15)
if err != nil {
fmt.Println(err)
// Output:
//
// age: must be 18 or older
}
}
29. Standard Interface Example: fmt.Stringer
package main
import "fmt"
type User struct {
ID int
Name string
}
// fmt.Stringer is conceptually:
//
// type Stringer interface {
// String() string
// }
//
// By implementing String(),
// User can control how fmt prints it.
func (u User) String() string {
return fmt.Sprintf(
"User(ID=%d, Name=%s)",
u.ID,
u.Name,
)
}
func main() {
user := User{
ID: 1,
Name: "Alice",
}
fmt.Println(user)
// Because User satisfies fmt.Stringer,
// fmt uses user.String().
//
// Output:
//
// User(ID=1, Name=Alice)
}
30. Standard Interface Example: io.Reader
package main
import (
"fmt"
"io"
"strings"
)
func main() {
// strings.NewReader returns a value
// that satisfies io.Reader.
reader := strings.NewReader(
"Hello Go Interfaces",
)
// io.ReadAll does not care that the concrete
// implementation came from strings.NewReader.
//
// It accepts io.Reader behavior.
data, err := io.ReadAll(reader)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(string(data))
// Output:
//
// Hello Go Interfaces
}
31. Why Standard Interfaces Are Powerful
package main
import (
"bytes"
"io"
"strings"
)
// processData accepts io.Reader.
//
// Therefore it can work with many implementations:
//
// - files
// - HTTP response bodies
// - strings.Reader
// - bytes.Buffer
// - compressed streams
// - network streams
//
// The function needs reading behavior,
// not one particular concrete type.
func processData(r io.Reader) error {
_, err := io.ReadAll(r)
return err
}
func main() {
// strings.Reader satisfies io.Reader.
stringReader := strings.NewReader("hello")
_ = processData(stringReader)
// bytes.Buffer also satisfies io.Reader.
buffer := bytes.NewBufferString("hello")
_ = processData(buffer)
}
32. Function Adapter Pattern
package main
import "fmt"
// Handler requires a method.
type Handler interface {
Handle(message string) error
}
// HandlerFunc is a NAMED function type.
type HandlerFunc func(string) error
// By giving HandlerFunc a Handle method,
// HandlerFunc satisfies Handler.
func (f HandlerFunc) Handle(
message string,
) error {
// Call the original function.
return f(message)
}
// This is a normal standalone function.
func printMessage(message string) error {
fmt.Println(message)
return nil
}
func execute(h Handler) error {
return h.Handle("Hello")
}
func main() {
// Convert the normal function
// into HandlerFunc.
handler := HandlerFunc(printMessage)
// HandlerFunc satisfies Handler.
_ = execute(handler)
// Output:
//
// Hello
}
33. Optional Capability with Type Assertion
package main
import "fmt"
type Writer interface {
Write(string)
}
type Flusher interface {
Flush()
}
type Buffer struct{}
func (Buffer) Write(data string) {
fmt.Println("Writing:", data)
}
func (Buffer) Flush() {
fmt.Println("Flushing")
}
func process(w Writer) {
w.Write("Hello")
// w is guaranteed only to be a Writer.
//
// But the concrete value MAY also support Flusher.
//
// This safe interface assertion checks
// for that optional capability.
if flusher, ok := w.(Flusher); ok {
flusher.Flush()
}
}
func main() {
buffer := Buffer{}
process(buffer)
// Output:
//
// Writing: Hello
// Flushing
}
34. Nil Interface
package main
import "fmt"
type Speaker interface {
Speak()
}
func main() {
// No concrete type.
// No concrete value.
var speaker Speaker
// Conceptually:
//
// dynamic type = nil
// dynamic value = nil
//
// Therefore the interface itself is nil.
fmt.Println(speaker == nil)
// Output:
//
// true
}
35. Typed Nil Inside an Interface
package main
import "fmt"
type Speaker interface {
Speak()
}
type Dog struct{}
func (*Dog) Speak() {}
func main() {
// dog is a nil pointer.
var dog *Dog = nil
// Store that typed nil pointer
// inside an interface.
var speaker Speaker = dog
// Conceptually speaker contains:
//
// dynamic type = *Dog
// dynamic value = nil
//
// Because the interface contains type information,
// the interface itself is NOT nil.
fmt.Println(speaker == nil)
// Output:
//
// false
}
36. Typed Nil Error Trap
package main
import "fmt"
type MyError struct {
Message string
}
// Pointer receiver means *MyError satisfies error.
func (e *MyError) Error() string {
if e == nil {
return "nil MyError"
}
return e.Message
}
func badFunction() error {
// err is a nil *MyError pointer.
var err *MyError
// Returning err puts this inside error:
//
// dynamic type = *MyError
// dynamic value = nil
//
// Therefore the returned error interface
// is not nil.
return err
}
func goodFunction() error {
// When there is no error,
// return a true nil interface.
return nil
}
func main() {
err1 := badFunction()
err2 := goodFunction()
fmt.Println(err1 == nil)
fmt.Println(err2 == nil)
// Output:
//
// false
// true
}
37. Interface Adapter for an External API
package main
import "fmt"
// Our application wants this interface.
type Notifier interface {
Notify(message string) error
}
// Imagine this type comes from another package
// and we cannot modify it.
type ExternalClient struct{}
// Its method name does not match our interface.
func (ExternalClient) SendMessage(
message string,
) error {
fmt.Println("External API:", message)
return nil
}
// Adapter wraps the external client.
type NotificationAdapter struct {
client ExternalClient
}
// Adapter translates our application's
// Notify() call into the external API's
// SendMessage() call.
func (a NotificationAdapter) Notify(
message string,
) error {
return a.client.SendMessage(message)
}
func sendNotification(n Notifier) error {
return n.Notify("Order completed")
}
func main() {
client := ExternalClient{}
adapter := NotificationAdapter{
client: client,
}
_ = sendNotification(adapter)
}
38. Decorator-Style Interface Composition
package main
import "fmt"
type User struct {
Name string
}
type UserRepository interface {
Save(User) error
}
// Real repository.
type MemoryRepository struct{}
func (MemoryRepository) Save(user User) error {
fmt.Println("Saving:", user.Name)
return nil
}
// LoggingRepository wraps another repository.
type LoggingRepository struct {
next UserRepository
}
// Save adds behavior BEFORE and AFTER
// delegating to the wrapped repository.
func (r LoggingRepository) Save(
user User,
) error {
fmt.Println("LOG: Save started")
err := r.next.Save(user)
fmt.Println("LOG: Save finished")
return err
}
func main() {
realRepository := MemoryRepository{}
loggedRepository := LoggingRepository{
next: realRepository,
}
_ = loggedRepository.Save(
User{Name: "Alice"},
)
// Output:
//
// LOG: Save started
// Saving: Alice
// LOG: Save finished
}
39. Interface Segregation
package main
// Bad design for many consumers:
//
// type HugeRepository interface {
// FindByID(int) (User, error)
// Save(User) error
// Delete(int) error
// Export() error
// Backup() error
// Restore() error
// }
//
// A consumer that only reads a user
// should not need all those methods.
type User struct {
ID int
}
// Small interface for reading.
type UserFinder interface {
FindByID(int) (User, error)
}
// Small interface for saving.
type UserSaver interface {
Save(User) error
}
// Small interface for deleting.
type UserDeleter interface {
Delete(int) error
}
// A larger interface can still be composed
// when a consumer truly needs everything.
type UserRepository interface {
UserFinder
UserSaver
UserDeleter
}
func main() {}
40. Do Not Create Interfaces Too Early
package main
// This concrete type may be perfectly sufficient
// when there is:
//
// - one implementation
// - no testing boundary
// - no substitution requirement
// - no meaningful abstraction yet
type Calculator struct{}
func (Calculator) Add(a, b int) int {
return a + b
}
// There is no need to automatically create:
//
// type CalculatorInterface interface {
// Add(a, b int) int
// }
//
// simply because Calculator exists.
//
// Introduce interfaces when they represent
// a useful behavioral boundary.
func main() {
calculator := Calculator{}
result := calculator.Add(10, 20)
_ = result
}
41. Accept Interface, Return Concrete Type
package main
type Logger interface {
Log(string)
}
type Service struct {
logger Logger
}
// The constructor ACCEPTS an interface.
//
// This allows callers to provide:
//
// - console logger
// - file logger
// - cloud logger
// - fake logger
func NewService(logger Logger) *Service {
// The constructor RETURNS *Service,
// a useful concrete type.
return &Service{
logger: logger,
}
}
func (s *Service) Run() {
s.logger.Log("service started")
}
func main() {}
42. Generic Constraint Interface
package main
import "fmt"
// Interfaces are also used as constraints
// for generic type parameters.
//
// This interface describes a TYPE SET.
//
// T may have an underlying type matching:
//
// int
// int64
// float64
type Number interface {
~int | ~int64 | ~float64
}
// T must satisfy Number.
func Add[T Number](a, b T) T {
// Addition is allowed because every type
// permitted by Number supports +.
return a + b
}
func main() {
fmt.Println(Add(10, 20))
fmt.Println(Add(1.5, 2.5))
// Output:
//
// 30
// 4
}
43. Why ~ Matters in Generic Interfaces
package main
import "fmt"
// UserID is a custom defined type.
//
// Its underlying type is int.
type UserID int
// ~int means:
//
// "int OR any defined type whose underlying type is int."
type Integer interface {
~int
}
func Double[T Integer](value T) T {
return value * 2
}
func main() {
var id UserID = 10
// UserID satisfies ~int
// because its underlying type is int.
result := Double(id)
fmt.Println(result)
// Output:
//
// 20
}
44. comparable Interface Constraint
package main
import "fmt"
// comparable is a built-in constraint.
//
// T must support:
//
// ==
// !=
func Equal[T comparable](a, b T) bool {
return a == b
}
func main() {
fmt.Println(Equal(10, 10))
fmt.Println(Equal(10, 20))
fmt.Println(Equal("Go", "Go"))
fmt.Println(Equal("Go", "Java"))
// Output:
//
// true
// false
// true
// false
}
45. Generic Set Using comparable
package main
import "fmt"
// Map keys must be comparable.
//
// Therefore T is constrained by comparable.
type Set[T comparable] map[T]struct{}
func (s Set[T]) Add(value T) {
// Empty struct{} takes no meaningful payload space.
s[value] = struct{}{}
}
func (s Set[T]) Contains(value T) bool {
_, ok := s[value]
return ok
}
func main() {
numbers := Set[int]{}
numbers.Add(10)
numbers.Add(20)
fmt.Println(numbers.Contains(10))
fmt.Println(numbers.Contains(99))
// Output:
//
// true
// false
}
46. Runtime Interface vs Generic Constraint
package main
// Runtime behavioral interface.
//
// Values can be stored in variables of this interface type.
type Speaker interface {
Speak() string
}
// Generic constraint interface.
//
// Its type terms describe what compile-time
// generic types are permitted.
type Number interface {
~int | ~float64
}
func PrintSpeaker(s Speaker) {
// Runtime polymorphism:
//
// the concrete type can vary at runtime.
_ = s.Speak()
}
func Add[T Number](a, b T) T {
// Compile-time generic reuse:
//
// the compiler works with the selected T.
return a + b
}
func main() {}
47. Interfaces vs Generics
package main
// Use an interface when the important question is:
//
// "What behavior does this value provide?"
type Saver interface {
Save() error
}
func persist(s Saver) error {
return s.Save()
}
// Use generics when the important question is:
//
// "Can this algorithm work with many compile-time types?"
func First[T any](values []T) T {
return values[0]
}
// Interfaces and generics are not competitors.
//
// Interfaces:
// - behavior abstraction
// - runtime polymorphism
// - dependency boundaries
//
// Generics:
// - reusable algorithms
// - reusable containers
// - compile-time type relationships
func main() {}
48. Real-World Use Case: Clock Interface for Testing
package main
import (
"fmt"
"time"
)
// Clock describes the only time behavior
// our service needs.
type Clock interface {
Now() time.Time
}
// RealClock is used in production.
type RealClock struct{}
func (RealClock) Now() time.Time {
return time.Now()
}
// FakeClock is useful in tests.
type FakeClock struct {
Current time.Time
}
func (f FakeClock) Now() time.Time {
return f.Current
}
type GreetingService struct {
clock Clock
}
func (s GreetingService) Greeting() string {
hour := s.clock.Now().Hour()
if hour < 12 {
return "Good morning"
}
return "Good afternoon"
}
func main() {
// Test code can control time exactly.
fake := FakeClock{
Current: time.Date(
2026,
time.August,
17,
9,
0,
0,
0,
time.UTC,
),
}
service := GreetingService{
clock: fake,
}
fmt.Println(service.Greeting())
// Output:
//
// Good morning
}
49. Real-World Use Case: ID Generator
package main
import "fmt"
// IDGenerator abstracts ID creation.
type IDGenerator interface {
Generate() string
}
// FixedIDGenerator is extremely useful in tests.
type FixedIDGenerator struct {
ID string
}
func (g FixedIDGenerator) Generate() string {
return g.ID
}
type User struct {
ID string
Name string
}
type UserService struct {
generator IDGenerator
}
func (s UserService) Create(name string) User {
return User{
// Service does not know how IDs are generated.
ID: s.generator.Generate(),
Name: name,
}
}
func main() {
generator := FixedIDGenerator{
ID: "test-123",
}
service := UserService{
generator: generator,
}
user := service.Create("Alice")
fmt.Println(user.ID)
fmt.Println(user.Name)
// Output:
//
// test-123
// Alice
}
50. Real-World Use Case: Cache
package main
import "fmt"
// Cache describes the behavior
// required by the service.
//
// Production could use Redis.
//
// Tests could use MemoryCache.
type Cache interface {
Get(key string) (string, bool)
Set(key string, value string)
}
type MemoryCache struct {
values map[string]string
}
func NewMemoryCache() *MemoryCache {
return &MemoryCache{
values: make(map[string]string),
}
}
func (c *MemoryCache) Get(
key string,
) (string, bool) {
value, ok := c.values[key]
return value, ok
}
func (c *MemoryCache) Set(
key string,
value string,
) {
c.values[key] = value
}
func main() {
var cache Cache = NewMemoryCache()
cache.Set("language", "Go")
value, found := cache.Get("language")
fmt.Println(value)
fmt.Println(found)
// Output:
//
// Go
// true
}
51. Complete Mini Project: Order Processing
package main
import "fmt"
// Order is normal application data.
type Order struct {
ID int
Email string
Amount float64
}
// OrderRepository describes persistence behavior.
type OrderRepository interface {
Save(order Order) error
}
// PaymentProcessor describes payment behavior.
type PaymentProcessor interface {
Charge(amount float64) error
}
// Notifier describes notification behavior.
type Notifier interface {
Notify(
recipient string,
message string,
) error
}
// OrderService depends only on interfaces.
//
// It does NOT directly depend on:
//
// PostgreSQL
// Stripe
// SMTP
//
// This keeps business logic separate
// from infrastructure details.
type OrderService struct {
repository OrderRepository
payment PaymentProcessor
notifier Notifier
}
// Constructor injects dependencies.
func NewOrderService(
repository OrderRepository,
payment PaymentProcessor,
notifier Notifier,
) *OrderService {
return &OrderService{
repository: repository,
payment: payment,
notifier: notifier,
}
}
// Process contains business workflow.
func (s *OrderService) Process(
order Order,
) error {
// First charge payment.
if err := s.payment.Charge(
order.Amount,
); err != nil {
return err
}
// Then save the order.
if err := s.repository.Save(
order,
); err != nil {
return err
}
// Then notify the customer.
if err := s.notifier.Notify(
order.Email,
"Your order was processed",
); err != nil {
return err
}
return nil
}
// ------------------------------
// Repository implementation
// ------------------------------
type MemoryOrderRepository struct {
orders []Order
}
func (r *MemoryOrderRepository) Save(
order Order,
) error {
r.orders = append(
r.orders,
order,
)
fmt.Println(
"Saved order:",
order.ID,
)
return nil
}
// ------------------------------
// Payment implementation
// ------------------------------
type FakePaymentProcessor struct{}
func (FakePaymentProcessor) Charge(
amount float64,
) error {
fmt.Printf(
"Charged: $%.2f\n",
amount,
)
return nil
}
// ------------------------------
// Notification implementation
// ------------------------------
type ConsoleNotifier struct{}
func (ConsoleNotifier) Notify(
recipient string,
message string,
) error {
fmt.Println(
"Notification to:",
recipient,
)
fmt.Println(
"Message:",
message,
)
return nil
}
func main() {
// Create concrete implementations.
repository := &MemoryOrderRepository{}
payment := FakePaymentProcessor{}
notifier := ConsoleNotifier{}
// Inject them through interfaces.
service := NewOrderService(
repository,
payment,
notifier,
)
order := Order{
ID: 1001,
Email: "alice@example.com",
Amount: 249.99,
}
if err := service.Process(order); err != nil {
fmt.Println("Error:", err)
return
}
// Output:
//
// Charged: $249.99
// Saved order: 1001
// Notification to: alice@example.com
// Message: Your order was processed
}
52. Testing the Order Service with Fakes
package main
import "fmt"
type Order struct {
ID int
Email string
Amount float64
}
type OrderRepository interface {
Save(Order) error
}
type PaymentProcessor interface {
Charge(float64) error
}
type Notifier interface {
Notify(string, string) error
}
type OrderService struct {
repository OrderRepository
payment PaymentProcessor
notifier Notifier
}
func (s *OrderService) Process(
order Order,
) error {
if err := s.payment.Charge(
order.Amount,
); err != nil {
return err
}
if err := s.repository.Save(
order,
); err != nil {
return err
}
return s.notifier.Notify(
order.Email,
"processed",
)
}
// Fake repository records what was saved.
type FakeRepository struct {
SavedOrder Order
Called bool
}
func (f *FakeRepository) Save(
order Order,
) error {
f.Called = true
f.SavedOrder = order
return nil
}
// Fake payment records the charged amount.
type FakePayment struct {
Amount float64
Called bool
}
func (f *FakePayment) Charge(
amount float64,
) error {
f.Called = true
f.Amount = amount
return nil
}
// Fake notifier records notification details.
type FakeNotifier struct {
Recipient string
Message string
Called bool
}
func (f *FakeNotifier) Notify(
recipient string,
message string,
) error {
f.Called = true
f.Recipient = recipient
f.Message = message
return nil
}
func main() {
repository := &FakeRepository{}
payment := &FakePayment{}
notifier := &FakeNotifier{}
service := OrderService{
repository: repository,
payment: payment,
notifier: notifier,
}
order := Order{
ID: 1,
Email: "alice@example.com",
Amount: 100,
}
_ = service.Process(order)
// A real unit test would use testing.T,
// but these prints show what can be verified.
fmt.Println(payment.Called)
fmt.Println(payment.Amount)
fmt.Println(repository.Called)
fmt.Println(repository.SavedOrder.ID)
fmt.Println(notifier.Called)
fmt.Println(notifier.Recipient)
// Output:
//
// true
// 100
// true
// 1
// true
// alice@example.com
}
53. Common Mistake: Giant Interface
package main
// Avoid creating an interface like this
// unless a consumer truly requires ALL behavior.
//
// type ApplicationManager interface {
// Create()
// Read()
// Update()
// Delete()
// Import()
// Export()
// Backup()
// Restore()
// SendEmail()
// GenerateReport()
// }
// Prefer small interfaces.
type Creator interface {
Create() error
}
type Reader interface {
Read() error
}
type Updater interface {
Update() error
}
type Deleter interface {
Delete() error
}
// Compose them only where necessary.
type CRUDService interface {
Creator
Reader
Updater
Deleter
}
func main() {}
54. Common Mistake: Interface for Every Struct
package main
type User struct {
Name string
}
// This method alone does NOT mean you need:
//
// type UserInterface interface {
// NameValue() string
// }
//
// If callers can simply use User,
// use the concrete type.
//
// Interfaces should represent useful abstraction,
// not automatic wrappers around every struct.
func (u User) NameValue() string {
return u.Name
}
func main() {
user := User{
Name: "Alice",
}
_ = user.NameValue()
}
55. Common Mistake: Overusing Type Switches
package main
// This kind of design:
//
// switch value := thing.(type) {
// case Dog:
// case Cat:
// case Person:
// case Robot:
// case Car:
// }
//
// may sometimes be necessary.
//
// But if every type performs the same conceptual
// behavior, an interface may be cleaner.
type Speaker interface {
Speak() string
}
// Now callers can simply:
//
// speaker.Speak()
//
// instead of repeatedly checking concrete types.
func announce(s Speaker) string {
return s.Speak()
}
func main() {}
56. Common Mistake: Returning any Everywhere
package main
type User struct {
ID int
Name string
}
// Poorly typed API:
//
// func FindUser(id int) any
//
// Caller would need to guess/assert the result type.
// Better API:
//
// The return type clearly tells the caller
// what to expect.
func FindUser(id int) (User, error) {
return User{
ID: id,
Name: "Alice",
}, nil
}
func main() {}
57. Common Mistake: Wrong Receiver Type
package main
type Counter interface {
Increment()
}
type NumberCounter struct {
Value int
}
// Pointer receiver is required here
// because Increment must change Value.
func (c *NumberCounter) Increment() {
c.Value++
}
func main() {
counter := NumberCounter{}
// Direct call works because Go can take
// the address of an addressable variable.
counter.Increment()
// Interface assignment requires *NumberCounter.
var incrementer Counter = &counter
incrementer.Increment()
// This would fail:
//
// var wrong Counter = counter
}
58. Interface Design Decision Example
package main
type User struct {
ID int
}
// Question:
//
// "Should I create an interface here?"
//
// Start by asking:
//
// "What does the CONSUMER actually need?"
type UserFinder interface {
FindByID(int) (User, error)
}
// This interface is useful when:
//
// 1. Multiple implementations may exist.
// 2. Tests need a fake implementation.
// 3. The dependency crosses an architectural boundary.
// 4. The consumer needs only this behavior.
//
// An interface is probably unnecessary when:
//
// 1. There is only a simple local concrete type.
// 2. No substitution is needed.
// 3. No testing boundary exists.
// 4. The abstraction adds more complexity than value.
func loadUser(
finder UserFinder,
id int,
) (User, error) {
return finder.FindByID(id)
}
func main() {}
59. Final Interface Cheat Sheet
package main
// ------------------------------------
// BASIC INTERFACE
// ------------------------------------
type Speaker interface {
Speak() string
}
// ------------------------------------
// IMPLEMENTATION
// ------------------------------------
type Dog struct{}
func (Dog) Speak() string {
return "Woof"
}
// Dog now implicitly satisfies Speaker.
// ------------------------------------
// MULTIPLE METHODS
// ------------------------------------
type Service interface {
Start() error
Stop() error
}
// ------------------------------------
// INTERFACE COMPOSITION
// ------------------------------------
type Reader interface {
Read()
}
type Writer interface {
Write()
}
type ReadWriter interface {
Reader
Writer
}
// ------------------------------------
// ANY
// ------------------------------------
// any is equivalent to interface{}.
//
// Use it when arbitrary values are genuinely needed.
func printAnything(value any) {
_ = value
}
// ------------------------------------
// TYPE ASSERTION
// ------------------------------------
func assertion(value any) {
text, ok := value.(string)
if ok {
_ = text
}
}
// ------------------------------------
// TYPE SWITCH
// ------------------------------------
func typeSwitch(value any) {
switch v := value.(type) {
case string:
_ = v
case int:
_ = v
}
}
// ------------------------------------
// COMPILE-TIME CHECK
// ------------------------------------
var _ Speaker = Dog{}
// ------------------------------------
// GENERIC CONSTRAINT
// ------------------------------------
type Number interface {
~int | ~float64
}
func Add[T Number](a, b T) T {
return a + b
}
// ------------------------------------
// MOST IMPORTANT RULE
// ------------------------------------
//
// A Go interface should answer:
//
// "What behavior does this consumer need?"
//
// not:
//
// "What interface can I create for this struct?"
//
// Good interfaces are usually:
//
// - small
// - behavior-focused
// - consumer-focused
// - easy to implement
// - easy to test
// - useful at dependency boundaries
func main() {}
60. Final Mental Model
package main
// Think about Go interfaces like this:
//
// Speaker
// |
// requires Speak()
// |
// +-----------+-----------+
// | | |
// Dog Person Robot
// | | |
// Speak() Speak() Speak()
//
// Speaker does not care:
//
// "What ARE you?"
//
// Speaker cares:
//
// "Can you Speak()?"
//
// That leads to the core Go interface idea:
//
// ------------------------------------------------
//
// INTERFACE = REQUIRED BEHAVIOR
//
// ------------------------------------------------
//
// Struct:
//
// type User struct {
// Name string
// }
//
// answers:
//
// "What data does this value contain?"
//
// ------------------------------------------------
//
// Interface:
//
// type Saver interface {
// Save() error
// }
//
// answers:
//
// "What can this value do?"
//
// ------------------------------------------------
//
// Use interfaces especially for:
//
// - databases
// - repositories
// - HTTP clients
// - payment providers
// - email systems
// - notification services
// - caches
// - file systems
// - clocks
// - external APIs
// - loggers
// - queues
// - testing dependencies
//
// ------------------------------------------------
//
// Remember these rules:
//
// 1. Interface implementation is implicit.
//
// 2. Required method signatures must match.
//
// 3. Keep interfaces small.
//
// 4. Define interfaces around consumer needs.
//
// 5. Pointer receivers affect interface satisfaction.
//
// 6. any is interface{}.
//
// 7. Use safe type assertions when type is uncertain.
//
// 8. Type switches inspect dynamic types.
//
// 9. A nil interface differs from an interface
// containing a typed nil pointer.
//
// 10. Interfaces and generics solve different problems.
//
// 11. Generic constraints can also be interfaces.
//
// 12. Do not create an interface for every struct.
//
// 13. Use interfaces where abstraction gives real value.
//
// 14. Prefer behavior-oriented designs.
//
// 15. Ask:
//
// "What behavior does this code actually need?"
//
// That question is the foundation of good
// interface design in Go.
func main() {}