Interface Satisfaction Rules
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 43 of 107.
While Go uses duck typing for implicit interface satisfaction, there are specific rules that determine whether a type actually satisfies an interface. Understanding these rules helps you avoid subtle bugs.
The most important rule involves pointer receivers. If a method is defined with a pointer receiver, only a pointer to that type satisfies the interface—not the value itself:
type Saver interface {
Save() string
}
type Document struct{ Name string }
func (d *Document) Save() string { // pointer receiver
return "Saved: " + d.Name
}
func Process(s Saver) {
fmt.Println(s.Save())
}
Because Save() has a pointer receiver, only *Document satisfies Saver:
func main() {
doc := Document{Name: "report.txt"}
Process(&doc) // works - pointer satisfies interface
// Process(doc) // compile error - value doesn't satisfy interface
}
However, the reverse is more flexible. If a method has a value receiver, both the value and pointer types satisfy the interface. Go automatically dereferences pointers when calling value receiver methods:
func (d Document) Info() string { // value receiver
return d.Name
}
// Both Document and *Document satisfy an interface requiring Info()
This asymmetry exists because Go can always get a value from a pointer (by dereferencing), but it cannot always get a pointer from a value (the value might not be addressable). Keeping this rule in mind prevents confusing compiler errors when working with interfaces.
Challenge
EasyLet's build a configuration system that demonstrates how pointer and value receivers affect interface satisfaction. You'll create types where the receiver choice determines whether values, pointers, or both can be used with an interface.
You'll organize your code across three files:
config.go: Define aConfigurableinterface that requires two methods:GetValue() stringandSetValue(string). Then create two configuration types:ReadOnlyConfigwith aValuefield—use a value receiver forGetValue()(returns the Value) and a pointer receiver forSetValue()(updates the Value)Settingwith aDatafield—use value receivers for both methods (GetValue returns Data, SetValue just prints "Cannot modify" without changing anything)
processor.go: Create a function calledProcessConfigthat accepts aConfigurableand a new value string. It should print the current value usingGetValue(), callSetValue()with the new value, then print the value again to show any changes.main.go: Read configuration details from input and demonstrate the interface satisfaction rules:- Create a
ReadOnlyConfigand pass a pointer toProcessConfig(required becauseSetValuehas a pointer receiver) - Create a
Settingand pass the value directly toProcessConfig(works because both methods have value receivers)
- Create a
The following inputs will be provided:
- Line 1: Initial value for ReadOnlyConfig
- Line 2: New value to set for ReadOnlyConfig
- Line 3: Initial value for Setting
- Line 4: New value to attempt for Setting
Your ProcessConfig function should print in this format:
Current: [value]
Current: [value after SetValue]For example, given debug, production, localhost, and remote, your output should be:
Current: debug
Current: production
Current: localhost
Cannot modify
Current: localhostNotice how ReadOnlyConfig actually changes its value (because we passed a pointer), while Setting remains unchanged (its SetValue with a value receiver can't modify the original). The key insight is that ReadOnlyConfig values alone wouldn't satisfy Configurable—only pointers do—while Setting values work directly because all its methods use value receivers.
Cheat sheet
Go uses duck typing for implicit interface satisfaction, but there are specific rules about pointer and value receivers:
Pointer Receiver Rule
If a method has a pointer receiver, only a pointer to that type satisfies the interface:
type Saver interface {
Save() string
}
type Document struct{ Name string }
func (d *Document) Save() string { // pointer receiver
return "Saved: " + d.Name
}
func main() {
doc := Document{Name: "report.txt"}
Process(&doc) // works - pointer satisfies interface
// Process(doc) // compile error - value doesn't satisfy interface
}Value Receiver Rule
If a method has a value receiver, both the value and pointer types satisfy the interface:
func (d Document) Info() string { // value receiver
return d.Name
}
// Both Document and *Document satisfy an interface requiring Info()Why This Asymmetry Exists
Go can always get a value from a pointer (by dereferencing), but cannot always get a pointer from a value (the value might not be addressable).
Try it yourself
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Read inputs
scanner.Scan()
rocInitial := scanner.Text()
scanner.Scan()
rocNew := scanner.Text()
scanner.Scan()
settingInitial := scanner.Text()
scanner.Scan()
settingNew := scanner.Text()
// TODO: Create a ReadOnlyConfig with rocInitial value
// TODO: Pass a POINTER to ProcessConfig (required because SetValue has pointer receiver)
// TODO: Create a Setting with settingInitial value
// TODO: Pass the VALUE directly to ProcessConfig (works because both methods have value receivers)
// Use these variables to avoid unused variable errors
_ = rocInitial
_ = rocNew
_ = settingInitial
_ = settingNew
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool