Golang / GoLang Interfaces and Object Oriented Interview Questions
In Go, an interface is a named set of method signatures. Any concrete type that implements all the methods in the set automatically satisfies the interface — no declaration, no implements keyword, no registration is required. This property is called implicit (or structural) interface satisfaction.
In Java or C#, a class must explicitly declare that it implements an interface (class Dog implements Animal). In Go, the relationship is inferred entirely by the compiler from the method set of the concrete type. This decoupling means interfaces and implementations can live in completely separate packages with no dependency between them.
// Define an interface
type Writer interface {
Write(p []byte) (n int, err error)
}
// os.File satisfies Writer — it has the right Write method
// No 'implements Writer' anywhere in the os package
var w Writer = os.Stdout // compiles because *os.File has Write([]byte)(int,error)
// Your own type
type NullWriter struct{}
func (NullWriter) Write(p []byte) (int, error) { return len(p), nil }
var w2 Writer = NullWriter{} // also satisfies Writer implicitly| Aspect | Go | Java / C# |
|---|---|---|
| Satisfaction | Implicit — compiler infers from method set | Explicit — 'implements' / ':' required |
| Coupling | Zero coupling between interface and type | Type must know about the interface |
| Retroactive satisfaction | Any type can satisfy any interface, even in other packages | Not possible without modifying the class |
| Zero-method interface | interface{} / any — satisfied by everything | Object — every class descends from it |
Internally, every non-empty interface value is a two-word pair stored on the stack (or heap if it escapes):
| Word | Name | Content |
|---|---|---|
| Word 1 | itab (type word) | Pointer to an itab struct containing: the interface type descriptor, the concrete type descriptor, a hash of the concrete type, and the method dispatch table (function pointers) |
| Word 2 | data (value word) | Pointer to the concrete value on the heap, or the value itself if it fits in one pointer and is pointer-shaped |
type Animal interface { Sound() string }
type Dog struct{ Name string }
func (d Dog) Sound() string { return "Woof" }
var a Animal = Dog{Name: "Rex"}
// Memory layout of 'a':
// word1 → itab{itype=Animal, ctype=Dog, hash=..., fun=[Dog.Sound]}
// word2 → *Dog{Name: "Rex"} (heap-allocated copy)
// Nil interface — BOTH words are nil
var a2 Animal // nil interface: word1=nil, word2=nil
fmt.Println(a2 == nil) // true
// Non-nil interface holding nil pointer — word1 is non-nil!
var d *Dog
a = d // word1 → itab{ctype=*Dog,...}, word2 → nil
fmt.Println(a == nil) // FALSE — classic trap
// Inspect with reflect
import "reflect"
v := reflect.ValueOf(a)
fmt.Println(v.IsNil()) // true — the *Dog data pointer is nil
fmt.Println(a == nil) // false — the interface itself is non-nilThe empty interface any (alias for interface{}) uses a simpler two-word layout called eface: word1 is a plain type pointer (no method table), word2 is the data pointer. Non-empty interfaces use iface with the full itab. The itab is cached globally per (interface type, concrete type) pair, so interface assignment is effectively free after the first occurrence.
This is one of the most frequently asked Go interview questions. The trap: an interface value is only nil when both its type pointer and its data pointer are zero. If you assign a nil pointer of a concrete type to an interface variable, the type pointer becomes non-zero — so the interface is not nil, even though the data it holds is nil.
type MyError struct{ code int }
func (e *MyError) Error() string { return fmt.Sprintf("error %d", e.code) }
// BUGGY function — looks like it returns nil on the happy path
func riskyOperation(fail bool) error {
var err *MyError // nil *MyError
if fail {
err = &MyError{code: 42}
}
return err // WRONG: assigns typed nil to error interface
// interface layout: {type=*MyError, data=nil}
}
e := riskyOperation(false)
if e != nil { // TRUE — interface is non-nil even though data is nil!
fmt.Println("BUG: this branch runs unexpectedly:", e)
}
// Correct fix: return untyped nil
func safeOperation(fail bool) error {
if fail {
return &MyError{code: 42}
}
return nil // untyped nil → both words zeroed → truly nil interface
}
// Another correct approach: keep return type concrete
func concreteReturn(fail bool) *MyError {
if fail { return &MyError{code: 42} }
return nil // nil pointer returned as concrete type, not interface
}
// Debug a suspicious interface with reflect
e2 := riskyOperation(false)
fmt.Println(e2 == nil) // false
fmt.Println(reflect.ValueOf(e2).IsNil()) // true — data IS nil
fmt.Printf("%T\n", e2) // *main.MyErrorRoot cause: the language spec defines interface equality as requiring both words to be zero. Assigning any concrete type (even a nil pointer of that type) populates the type word. The only way to produce a nil interface is to assign the bare nil literal or another nil interface variable.
Rule: when your function returns an interface type (like error), always return the bare nil keyword on the success path. Never return a typed nil pointer (var e *MyError; return e).
Go's implicit interface satisfaction means: if a type has the required methods, it satisfies the interface — regardless of whether the type's author ever heard of that interface. This is sometimes called structural typing or duck typing with compile-time verification.
// Third-party library defines:
type Logger interface {
Log(msg string)
}
// Your legacy type (written before the Logger interface existed):
type AppLogger struct{ prefix string }
func (a AppLogger) Log(msg string) { fmt.Println(a.prefix+":", msg) }
// AppLogger satisfies Logger with ZERO changes to either package
func useLogger(l Logger) { l.Log("hello") }
useLogger(AppLogger{prefix: "APP"}) // compiles fine
// Compile-time interface check — verify without running the code
// Idiom: blank identifier assignment causes a compile error if the interface isn't satisfied
var _ Logger = AppLogger{} // value receiver version
var _ Logger = (*AppLogger)(nil) // pointer receiver version
// Interface segregation — small interfaces are more reusable
type Reader interface { Read(p []byte) (int, error) }
type Writer interface { Write(p []byte) (int, error) }
type ReadWriter interface {
Reader // interface embedding
Writer
}
// Any type satisfying ReadWriter also satisfies Reader and Writer individually
var rw ReadWriter = os.Stdout // *os.File has both Read and Write
var r Reader = rw // ReadWriter subsumes Reader
var w Writer = rw // ReadWriter subsumes WriterThe compile-time check idiom (var _ Logger = AppLogger{}) is a Go best practice: it causes a compile error if AppLogger ever stops satisfying Logger, catching the mismatch at compile time rather than at runtime when a type assertion or assignment fails.
The empty interface interface{} — aliased as any since Go 1.18 — has no methods. Because every type implements zero or more methods, every type satisfies the empty interface. It is Go's equivalent of Java's Object or C's void*.
// any is an alias for interface{} (Go 1.18+)
func printAnything(v any) {
fmt.Printf("%T: %v\n", v, v)
}
printAnything(42) // int: 42
printAnything("hello") // string: hello
printAnything([]int{1,2}) // []int: [1 2]
// Common use: heterogeneous collections
data := map[string]any{
"name": "Alice",
"age": 30,
"tags": []string{"admin"},
}
// Recover the concrete type with type assertion
name, ok := data["name"].(string) // ok=true, name="Alice"
age, ok := data["age"].(int) // ok=true, age=30
_, ok = data["name"].(int) // ok=false — not an int
// Type switch — idiomatic multi-type handling
func describe(v any) string {
switch t := v.(type) {
case int: return fmt.Sprintf("int=%d", t)
case string: return fmt.Sprintf("str=%q", t)
case bool: return fmt.Sprintf("bool=%v", t)
default: return fmt.Sprintf("unknown(%T)", t)
}
}
// Trade-offs of any:
// PROS: flexible, universal container, JSON unmarshalling
// CONS: no compile-time type safety, requires runtime assertions,
// values often escape to heap, slower than concrete types
// PREFER generics (Go 1.18+) when the algorithm is uniform across typesSince Go 1.18, the preferred approach for type-agnostic functions is generics rather than any: generics preserve compile-time type safety and allow the compiler to generate more efficient code. Use any when you genuinely need to store or pass values of unpredictable types (JSON, configuration, middleware context).
Go methods are functions associated with a type. The receiver appears before the method name: func (t T) Method() (value receiver) or func (t *T) Method() (pointer receiver). Choosing correctly is essential for correctness and performance.
type Counter struct{ count int }
// Value receiver — operates on a COPY of Counter
func (c Counter) Value() int { return c.count }
// Pointer receiver — operates on the ORIGINAL Counter
func (c *Counter) Increment() { c.count++ }
func (c *Counter) Reset() { c.count = 0 }
c := Counter{}
c.Increment() // Go auto-takes &c; c.count becomes 1
fmt.Println(c.Value()) // 1
// Method set rules — critical for interface satisfaction:
// Value T: method set = {value receiver methods}
// Pointer *T: method set = {value receiver methods} ∪ {pointer receiver methods}
type Stringer interface{ String() string }
type MyType struct{ v int }
func (m *MyType) String() string { return fmt.Sprintf("%d", m.v) } // pointer receiver
var s Stringer = &MyType{42} // OK — *MyType has String()
// var s2 Stringer = MyType{42} // COMPILE ERROR — MyType (value) does NOT have String()
// Rule of thumb for choosing receiver type:
// Use POINTER receiver when:
// 1. The method must mutate the receiver
// 2. The receiver is large (avoid expensive copies)
// 3. The struct has non-copyable fields (sync.Mutex)
// Use VALUE receiver when:
// 1. The method is purely read-only
// 2. The type is small and immutable (time.Time, net/netip.Addr)
// Consistency rule: if ANY method uses pointer receiver, use pointer for all| Type used | Method set |
|---|---|
| T (value) | Methods with value receiver (T) |
| *T (pointer) | Methods with value receiver (T) + methods with pointer receiver (*T) |
Go interfaces can embed other interfaces. The composed interface's method set is the union of all embedded interface method sets. This is Go's primary mechanism for building larger interface contracts from smaller, focused ones — following the Interface Segregation Principle.
// Small, focused interfaces (the Go standard library style)
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Composed interfaces
type ReadWriter interface {
Reader // embeds Reader
Writer // embeds Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// *os.File satisfies all of the above
var f *os.File = os.Stdout
var rw ReadWriter = f // *os.File has Read + Write
var rwc ReadWriteCloser = f // *os.File has Read + Write + Close
var r Reader = rw // ReadWriter is assignable to Reader
// Your composed interface
type DataStore interface {
Reader
Writer
Flush() error
Stats() map[string]int64
}
// Any type with Read, Write, Flush, Stats satisfies DataStore
// without knowing anything about DataStoreDesign principle: prefer many small interfaces over one large interface. Small interfaces (io.Reader has 1 method, io.Writer has 1 method) are easy to satisfy and easy to mock in tests. Accept interfaces in your functions; return concrete types from constructors. The idiomatic Go proverb: "The bigger the interface, the weaker the abstraction."
Go achieves polymorphism through interface dispatch. When you call a method on an interface value, the runtime uses the itab's function pointer table to call the correct concrete implementation. This is equivalent to virtual method dispatch in C++ or Java.
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct{ Radius float64 }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
type Rectangle struct{ W, H float64 }
func (r Rectangle) Area() float64 { return r.W * r.H }
func (r Rectangle) Perimeter() float64 { return 2 * (r.W + r.H) }
type Triangle struct{ A, B, C float64 }
func (t Triangle) Area() float64 {
s := (t.A + t.B + t.C) / 2
return math.Sqrt(s * (s-t.A) * (s-t.B) * (s-t.C))
}
func (t Triangle) Perimeter() float64 { return t.A + t.B + t.C }
// Polymorphic function — works on any Shape
func printShapeInfo(s Shape) {
fmt.Printf("%T → area=%.2f perimeter=%.2f\n",
s, s.Area(), s.Perimeter())
}
shapes := []Shape{
Circle{Radius: 5},
Rectangle{W: 4, H: 6},
Triangle{A: 3, B: 4, C: 5},
}
for _, s := range shapes {
printShapeInfo(s) // dynamic dispatch via itab each iteration
}
// main.Circle → area=78.54 perimeter=31.42
// main.Rectangle → area=24.00 perimeter=20.00
// main.Triangle → area=6.00 perimeter=12.00Performance note: interface dispatch is one indirect function call through the itab (two pointer dereferences). For most code this is negligible. In tight loops profiling shows the bottleneck, concrete type calls or generics may be preferred. The Go compiler can devirtualise (inline) interface calls in some cases when it can prove the concrete type at compile time.
Go has no class hierarchy and no inheritance. Code reuse is achieved through composition: embedding one struct inside another promotes the embedded type's methods and fields to the outer type. This gives the outer type the embedded type's capabilities without any parent-child relationship.
// Base 'class' — a plain struct
type Animal struct {
Name string
Age int
}
func (a Animal) Describe() string {
return fmt.Sprintf("%s (age %d)", a.Name, a.Age)
}
// Composition: Dog embeds Animal
type Dog struct {
Animal // embedded — methods and fields promoted
Breed string
}
func (d Dog) Sound() string { return "Woof" }
d := Dog{
Animal: Animal{Name: "Rex", Age: 3},
Breed: "Labrador",
}
fmt.Println(d.Describe()) // promoted — same as d.Animal.Describe()
fmt.Println(d.Name) // promoted field access
fmt.Println(d.Sound()) // Dog's own method
// Method overriding via shadowing
type PoliceDog struct {
Dog
BadgeNumber int
}
// PoliceDog can shadow Dog's promoted methods:
func (p PoliceDog) Sound() string {
return "Woof! Police K9 #" + strconv.Itoa(p.BadgeNumber)
}
pd := PoliceDog{Dog: d, BadgeNumber: 42}
fmt.Println(pd.Sound()) // PoliceDog.Sound() — shadows Dog.Sound()
fmt.Println(pd.Dog.Sound()) // explicit Dog.Sound() — bypasses shadow
fmt.Println(pd.Describe()) // still promoted from AnimalKey distinction from inheritance: embedding is a mechanical promotion of methods, not an IS-A relationship. A PoliceDog is not a subtype of Dog — you cannot pass a PoliceDog where a Dog is expected (unless they share an interface). If both share an interface and implement all its methods, both can be used through that interface.
When a struct embeds another type, it promotes the embedded type's methods. If those promoted methods complete an interface's method set, the outer struct implicitly satisfies the interface — without writing any wrapper code.
type Sayer interface{ Say() string }
type Greeter struct{}
func (g Greeter) Say() string { return "Hello!" }
// By embedding Greeter, Robot satisfies Sayer without implementing Say itself
type Robot struct {
Greeter // promotes Say()
Model string
}
var s Sayer = Robot{Greeter: Greeter{}, Model: "R2D2"}
fmt.Println(s.Say()) // Hello! — dispatched to Greeter.Say()
// Override: Robot can provide its own Say() to shadow the promoted one
func (r Robot) Say() string { return "Beep boop from " + r.Model }
// Now Robot.Say() is called, not Greeter.Say()
// Partial interface satisfaction via embedding
type ReadWriter interface {
Read(p []byte) (int, error)
Write(p []byte) (int, error)
}
type MyBuffer struct {
bytes.Buffer // provides both Read and Write
}
var rw ReadWriter = &MyBuffer{} // satisfied via embedded bytes.Buffer
// Embedding an interface inside a struct
// (used to implement partial/stub implementations in tests)
type MockDB struct {
Database // interface embedded — zero value methods panic
}
// Override only the methods you care about in the test
func (m MockDB) Query(sql string) (Rows, error) { return testRows, nil }Embedding an interface inside a struct is an advanced pattern: all the interface methods are promoted and the zero value is nil (all methods panic). Override only the methods needed for a test — a compact way to implement a partial mock without satisfying every method of a large interface.
The error type is Go's built-in interface for representing error conditions. It has exactly one method:
type error interface { Error() string }Any type that has an Error() string method satisfies the error interface. This makes error handling in Go extremely flexible — you can attach arbitrary context to errors by defining custom types.
// Simple sentinel errors
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")
// Structured custom error type
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
// Function returning the error interface
func validateAge(age int) error {
if age < 0 {
return &ValidationError{Field: "age", Message: "must be non-negative"}
}
if age > 150 {
return &ValidationError{Field: "age", Message: "unrealistically large"}
}
return nil // untyped nil — truly nil interface
}
// Inspect the error type with errors.As
err := validateAge(-1)
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("Field:", ve.Field, "Message:", ve.Message)
}
// Wrapped errors (Go 1.13+) — %w preserves the chain
func openConfig(path string) error {
if err := os.Open(path); err != nil {
return fmt.Errorf("openConfig %s: %w", path, err)
}
return nil
}
err = openConfig("/missing/config.yaml")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("config file does not exist")
}
The fmt.Stringer interface is the standard Go convention for providing a human-readable string representation of a type. It is used automatically by fmt.Print, fmt.Println, and %v / %s format verbs.
// Interface definition (in the fmt package)
type Stringer interface {
String() string
}
// Custom type implementing Stringer
type Color int
const (
Red Color = iota
Green
Blue
)
func (c Color) String() string {
switch c {
case Red: return "Red"
case Green: return "Green"
case Blue: return "Blue"
default: return fmt.Sprintf("Color(%d)", int(c))
}
}
fmt.Println(Red) // Red (not '0')
fmt.Printf("%v\n", Green) // Green
fmt.Printf("%s\n", Blue) // Blue
// Another example: Point type
type Point struct{ X, Y float64 }
func (p Point) String() string {
return fmt.Sprintf("(%.2f, %.2f)", p.X, p.Y)
}
p := Point{3.14159, 2.71828}
fmt.Println(p) // (3.14, 2.72)
// GoStringer (fmt.GoStringer) — %#v verb, Go syntax representation
type Foo struct{ X int }
func (f Foo) GoString() string { return fmt.Sprintf("Foo{X: %d}", f.X) }
fmt.Printf("%#v\n", Foo{42}) // Foo{X: 42}Implementing Stringer makes debugging significantly easier: log output, error messages, and test failure messages show meaningful names rather than raw integer or struct values. It is a lightweight interface with large practical benefit — implementing it should be a habit for any named type that appears in logs or user-facing output.
A type assertion extracts the concrete value from an interface variable. There are two forms: the single-return form that panics on failure, and the comma-ok form that returns a boolean.
type Animal interface { Sound() string }
type Dog struct{ Name string }
func (d Dog) Sound() string { return "Woof" }
var a Animal = Dog{Name: "Rex"}
// Form 1: single return — panics if a does not hold a Dog
d := a.(Dog)
fmt.Println(d.Name) // Rex
// Form 2: comma-ok — safe, never panics
d2, ok := a.(Dog)
if ok {
fmt.Println(d2.Name) // Rex
}
// Wrong type — form 2 gracefully handles it
type Cat struct{}
func (Cat) Sound() string { return "Meow" }
_, ok = a.(Cat) // ok = false — a holds Dog, not Cat
// Interface-to-interface assertion — check if concrete type satisfies another interface
type Namer interface{ Name() string }
type NamedDog struct{ name string }
func (n NamedDog) Sound() string { return "Woof" }
func (n NamedDog) Name() string { return n.name }
var a2 Animal = NamedDog{name: "Buddy"}
if namer, ok := a2.(Namer); ok {
fmt.Println("name:", namer.Name()) // name: Buddy
}
// Type switch — idiomatic multi-type dispatch
func handleAnimal(a Animal) {
switch v := a.(type) {
case Dog: fmt.Println("Dog:", v.Name)
case Cat: fmt.Println("Cat")
case NamedDog: fmt.Println("NamedDog:", v.name)
default: fmt.Printf("Unknown: %T\n", v)
}
}The type switch (switch v := a.(type)) is the idiomatic way to handle multiple concrete types from an interface. In each case arm, v is already typed as the concrete type — no additional assertion needed. The default case handles any type not listed explicitly.
This principle appears in Russ Cox's writings and the Go wiki. It guides the design of clean, testable, and composable APIs.
- Accept interfaces: function parameters typed as interfaces are flexible — callers can pass any concrete type satisfying the interface, including mocks in tests.
- Return concrete types: function return types should be concrete structs or pointers, not interfaces. This gives callers full access to all methods of the returned type. If you return an interface, callers cannot access methods added later unless the interface is updated.
// GOOD: Accept interface, return concrete type
type Logger interface{ Log(string) }
type UserService struct {
db DB
logger Logger // injected — any Logger impl works (including mocks)
}
// Constructor returns *UserService (concrete), not some Service interface
func NewUserService(db DB, logger Logger) *UserService {
return &UserService{db: db, logger: logger}
}
// Method accepts io.Reader (interface) — any source of bytes works
func (s *UserService) ImportUsers(r io.Reader) error {
// reads from file, HTTP body, bytes.Buffer, etc.
}
// BAD: Returning an interface locks callers in
func NewUserService2(db DB) UserServicer { // interface return
return &UserService{db: db}
// Caller cannot call methods on *UserService not in UserServicer
// Adding a method to *UserService doesn't help callers
}
// Exception: when the return type IS genuinely polymorphic (factory pattern)
func NewStorage(driver string) Storage { // Storage is an interface
switch driver {
case "s3": return NewS3Storage()
case "local": return NewLocalStorage()
}
return nil
}Exception: returning interfaces is appropriate in genuine factory patterns where the caller should not care about the concrete type — only the contract. The error return type is the canonical example: always returned as the interface error, never as a concrete *MyError.
Because Go interfaces are satisfied implicitly, any dependency can be injected as an interface. In tests, the interface is replaced with a mock or stub — without any framework, reflection, or code generation.
// Production dependency: HTTP client
type HTTPClient interface {
Get(url string) (*http.Response, error)
}
// Service that depends on HTTPClient
type WeatherService struct {
client HTTPClient
apiKey string
}
func NewWeatherService(client HTTPClient, key string) *WeatherService {
return &WeatherService{client: client, apiKey: key}
}
func (ws *WeatherService) Forecast(city string) (string, error) {
url := fmt.Sprintf("https://api.weather.com/%s?key=%s", city, ws.apiKey)
resp, err := ws.client.Get(url) // uses injected client
if err != nil { return "", err }
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return string(body), nil
}
// In tests: mock the HTTPClient interface
type MockHTTPClient struct {
Response *http.Response
Err error
}
func (m *MockHTTPClient) Get(url string) (*http.Response, error) {
return m.Response, m.Err
}
func TestForecast(t *testing.T) {
mock := &MockHTTPClient{
Response: &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(`{"temp":"22C"}`)),
},
}
svc := NewWeatherService(mock, "test-key")
result, err := svc.Forecast("London")
// Test without any real HTTP calls
assert.NoError(t, err)
assert.Contains(t, result, "22C")
}Key insight: the mock's Get method satisfies the HTTPClient interface entirely through Go's implicit satisfaction — no mock framework, no code generation, no registration. The production code never imports the test package. This is the idiomatic Go approach to testability.
sort.Interface is one of Go's classic interface examples. Any type that implements three methods can be sorted by sort.Sort:
// sort.Interface definition:
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
// Custom type implementing sort.Interface
type Person struct{ Name string; Age int }
type ByAge []Person
func (b ByAge) Len() int { return len(b) }
func (b ByAge) Less(i, j int) bool { return b[i].Age < b[j].Age }
func (b ByAge) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
people := []Person{
{"Alice", 30}, {"Bob", 25}, {"Carol", 35},
}
sort.Sort(ByAge(people)) // convert slice to ByAge to satisfy interface
fmt.Println(people) // [{Bob 25} {Alice 30} {Carol 35}]
// Modern approach: sort.Slice (no type definition needed)
sort.Slice(people, func(i, j int) bool {
return people[i].Name < people[j].Name // sort by Name
})
// sort.SliceStable — preserves order of equal elements
sort.SliceStable(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
// Reverse sorting: sort.Reverse wraps any sort.Interface
sort.Sort(sort.Reverse(ByAge(people)))
// slices.SortFunc (Go 1.21) — generic, type-safe, faster
import "slices"
slices.SortFunc(people, func(a, b Person) int {
return strings.Compare(a.Name, b.Name) // -1, 0, +1
})The sort.Interface example illustrates how interfaces decouple the sorting algorithm from the data type. sort.Sort knows nothing about Person — it only calls Len, Less, and Swap. This is the Strategy pattern in Go: the algorithm is the same, the comparison strategy is injected via the interface.
io.Reader and io.Writer are the most influential interfaces in the Go standard library. They each have exactly one method, yet they model an enormous variety of data sources and sinks — files, network connections, byte buffers, compression streams, crypto pipes, and more.
// io.Reader and io.Writer — the entire interface
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
// Composition: ReadWriter, ReadCloser, WriteCloser
type ReadWriter interface { Reader; Writer }
type ReadCloser interface { Reader; Closer }
type WriteCloser interface { Writer; Closer }
// Any function accepting io.Reader can work with ALL of these:
func countBytes(r io.Reader) (int64, error) {
return io.Copy(io.Discard, r)
}
// Works identically for:
f, _ := os.Open("data.txt")
countBytes(f) // *os.File
countBytes(strings.NewReader("hello")) // *strings.Reader
countBytes(bytes.NewReader(data)) // *bytes.Reader
countBytes(resp.Body) // http.Response.Body
countBytes(gzip.NewReader(f)) // *gzip.Reader
// Composing transforms: gzip decompress → buffered read → count
f2, _ := os.Open("archive.gz")
gzr, _ := gzip.NewReader(f2)
buf := bufio.NewReader(gzr)
count, _ := countBytes(buf)
// Each layer is transparent to countBytes — it just calls Read()
// Writing to multiple destinations simultaneously
var buf2 bytes.Buffer
multi := io.MultiWriter(&buf2, os.Stdout)
fmt.Fprintln(multi, "hello") // writes to both buf2 and stdoutThe lesson: small interfaces with well-chosen methods compose powerfully. By accepting io.Reader instead of *os.File, a function becomes universally useful. Each layer in the stack (file, gzip, bufio) wraps the previous one, extending behaviour without modifying the original — the Decorator pattern in Go.
Go's method set rules determine which methods can be called on a value of a given type. The asymmetry is:
| Type in expression | Method set |
|---|---|
| T (value type) | Only methods declared with value receiver (func (t T)) |
| *T (pointer type) | Methods declared with value receiver + methods with pointer receiver (func (t *T)) |
type Counter struct{ n int }
func (c Counter) Get() int { return c.n } // value receiver
func (c *Counter) Add(d int) { c.n += d } // pointer receiver
// Interface requiring pointer receiver method
type Adder interface { Add(int) }
var a1 Adder = &Counter{} // OK — *Counter has Add()
// var a2 Adder = Counter{} // COMPILE ERROR: Counter does not implement Adder
// (Add has pointer receiver)
// Why the asymmetry?
// When you assign Counter{} to an interface, Go cannot take its address —
// the interface stores a copy, and pointer receiver methods need the ORIGINAL.
// A *Counter stores the address, so pointer receiver methods can mutate it.
// Addressable variables work fine without interfaces:
c := Counter{} // addressable
c.Add(5) // Go auto-takes &c → compiles fine
fmt.Println(c.Get()) // 5
// Non-addressable — method call fails
// Counter{}.Add(5) // COMPILE ERROR: cannot take the address of Counter{}
// Correct pattern: use *Counter when ANY method uses pointer receiver
var a Adder = &Counter{}
a.Add(10)
// Access Get() via type assertion:
fmt.Println(a.(*Counter).Get()) // 10Practical rule: if a type has any method with a pointer receiver, use *T everywhere (fields, slices, interface assignments). Mixing value and pointer receivers is a common source of interface satisfaction failures.
A type can implement any number of interfaces simultaneously — all it needs is the right set of methods. There is no limit and no explicit declaration. This enables a type to play many roles in different contexts.
// Several independent interfaces
type Saver interface { Save() error }
type Loader interface { Load() error }
type Deleter interface { Delete() error }
type Stringer interface { String() string }
// One type, many roles
type UserRepo struct{ db *sql.DB }
func (r *UserRepo) Save() error { /* INSERT */ return nil }
func (r *UserRepo) Load() error { /* SELECT */ return nil }
func (r *UserRepo) Delete() error { /* DELETE */ return nil }
func (r *UserRepo) String() string { return "UserRepo" }
// Use through different interface lenses
repo := &UserRepo{db: db}
var s Saver = repo // *UserRepo satisfies Saver
var l Loader = repo // *UserRepo satisfies Loader
var d Deleter = repo // *UserRepo satisfies Deleter
var st fmt.Stringer = repo // satisfies fmt.Stringer too
// Composed interface — all at once
type CRUD interface {
Saver
Loader
Deleter
}
var crud CRUD = repo // *UserRepo satisfies CRUD
// Passing to functions that only need part of the capability
func backup(s Saver) { s.Save() } // only needs Save
func restore(l Loader) { l.Load() } // only needs Load
backup(repo)
restore(repo)
// Verify all at compile time
var _ Saver = (*UserRepo)(nil)
var _ Loader = (*UserRepo)(nil)
var _ Deleter = (*UserRepo)(nil)
var _ CRUD = (*UserRepo)(nil)Passing repo to backup(s Saver) exposes only the Save method — the function cannot call Load or Delete even though the concrete type has them. This is the principle of minimal interface exposure: functions accept only the capability they need, reducing coupling and making code easier to understand and test.
Go's encapsulation is package-level, not class-level. Identifiers (types, fields, functions, methods) are exported (public) if they start with an uppercase letter, and unexported (package-private) if they start with a lowercase letter. There is no private, protected, or public keyword.
// package accounts
// Exported type — usable outside the package
type Account struct {
owner string // unexported — only accessible within package accounts
balance float64 // unexported
ID int // exported field
}
// Constructor (factory function) — controls how Account is created
func NewAccount(owner string, initialBalance float64) (*Account, error) {
if initialBalance < 0 {
return nil, fmt.Errorf("initial balance cannot be negative")
}
return &Account{owner: owner, balance: initialBalance}, nil
}
// Exported getter
func (a *Account) Balance() float64 { return a.balance }
func (a *Account) Owner() string { return a.owner }
// Exported mutator with validation
func (a *Account) Deposit(amount float64) error {
if amount <= 0 {
return fmt.Errorf("deposit amount must be positive")
}
a.balance += amount
return nil
}
// In main package:
acc, _ := accounts.NewAccount("Alice", 100.0)
fmt.Println(acc.Balance()) // 100.0 — OK
// acc.balance = 9999 // COMPILE ERROR: acc.balance is unexported
// acc.owner = "Hacker" // COMPILE ERROR
acc.Deposit(50.0) // OK — goes through validationDifferences from Java/C++: Go has no protected — there is no concept of subclass access. Unexported fields are invisible even to embedding structs in other packages. The internal package mechanism provides a stronger form of encapsulation: code in foo/internal/bar can only be imported by code rooted at foo.
Types often implement both fmt.Stringer (String() string) and error (Error() string). A subtle trap: inside String(), calling fmt.Sprintf("%v", e) on the receiver causes infinite recursion because %v checks for Stringer and calls String() again.
type AppError struct {
Code int
Message string
}
// BUGGY String() — infinite recursion
// func (e AppError) String() string {
// return fmt.Sprintf("%v", e) // calls e.String() → infinite loop
// }
// CORRECT: format fields directly, not the receiver
func (e AppError) String() string {
return fmt.Sprintf("AppError[%d]: %s", e.Code, e.Message)
}
// Implements both error and Stringer
func (e AppError) Error() string { return e.String() }
// Usage
err := AppError{Code: 404, Message: "not found"}
fmt.Println(err) // uses Stringer: AppError[404]: not found
fmt.Println(err.Error()) // error: AppError[404]: not found
var e error = err // also satisfies error interface
fmt.Println(e) // same output via error.Error()
// Safe pattern: convert to a plain type inside String()
type Point struct{ X, Y int }
func (p Point) String() string {
// Using struct literal — NOT the receiver — avoids recursion
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}Detection: the Go runtime detects some infinite-recursion panics (stack overflow), but they can be hard to trace. Always format individual fields, not the receiver itself, inside String() and Error(). Use %d, %s, %f directly on the fields.
Go 1.18 extended the interface syntax so interfaces can serve as type constraints for generic functions and types. A constraint specifies which types a type parameter can be. Constraints are just interfaces — but now interfaces can include concrete type lists in addition to method sets.
// Constraint: any type with a < operator (from golang.org/x/exp/constraints)
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64 | ~string
}
// Generic function constrained by Ordered
func Min[T Ordered](a, b T) T {
if a < b { return a }
return b
}
fmt.Println(Min(3, 5)) // 3 — T inferred as int
fmt.Println(Min(3.14, 2.71)) // 2.71 — T inferred as float64
fmt.Println(Min("foo", "bar")) // bar — T inferred as string
// The ~ (tilde) prefix means: any type whose underlying type is int
type Celsius float64
Min(Celsius(20), Celsius(30)) // works because ~float64 matches Celsius
// Interface as both constraint and regular interface
type Stringer interface { String() string }
func Describe[T Stringer](v T) string {
return ">> " + v.String()
}
// Same interface used as constraint (generic) and variable type (runtime)
var s Stringer = Color(Red) // runtime interface variable
result := Describe(Color(Red)) // compile-time generic instantiation
// any is also a valid constraint — equivalent to interface{}
func Keys[K comparable, V any](m map[K]V) []K {
keys := make([]K, 0, len(m))
for k := range m { keys = append(keys, k) }
return keys
}The key distinction: when an interface with a type list (e.g., ~int | ~float64) is used as a regular variable type, only method calls are allowed — the type list restricts which types can satisfy it. When used as a type constraint, the full type list is available to the compiler for optimisation and operator permissions (like <).
Go has no abstract class. The idiomatic equivalent is: define an interface for the contract, provide a factory function that returns the interface, and keep the concrete implementation unexported. Callers depend only on the interface — the implementation is hidden.
// database/db.go — abstract the database driver
// Exported interface — the 'abstract type'
type DB interface {
Query(sql string, args ...any) (Rows, error)
Exec(sql string, args ...any) (Result, error)
Close() error
}
// Unexported concrete implementation — callers never see this
type postgresDB struct {
conn *pg.Conn
}
func (p *postgresDB) Query(sql string, args ...any) (Rows, error) {
return p.conn.Query(sql, args...)
}
func (p *postgresDB) Exec(sql string, args ...any) (Result, error) {
return p.conn.Exec(sql, args...)
}
func (p *postgresDB) Close() error { return p.conn.Close() }
// Exported factory — returns the interface, hides the concrete type
func NewPostgresDB(dsn string) (DB, error) {
conn, err := pg.Connect(dsn)
if err != nil { return nil, fmt.Errorf("NewPostgresDB: %w", err) }
return &postgresDB{conn: conn}, nil
}
// In main/service code:
db, err := database.NewPostgresDB("postgres://localhost/mydb")
if err != nil { log.Fatal(err) }
defer db.Close()
// Caller has no knowledge of postgresDB — depends only on DB interface
// Swap the factory call to use a different DB implementation
// db, _ = database.NewSQLiteDB(":memory:")This pattern achieves the same goals as abstract class in Java: the implementation is hidden, the interface is the contract, and the factory controls creation. The advantage: the concrete type can be swapped by changing only the factory call — zero impact on the rest of the codebase.
Because Go interface satisfaction is implicit, the compiler normally checks it only at the point of actual use (assignment or function call). If a type is exported and expected to satisfy an interface, a change to the type's method set might silently break the contract — only catching the error at the site of use, not at the site of definition.
The compile-time check idiom prevents this:
// Idiom: assign nil pointer of the type to an interface variable
// This costs nothing at runtime (no allocation) but fails to compile
// if the interface is not satisfied.
type MyStore struct{ /* ... */ }
func (m *MyStore) Get(key string) (string, error) { /* ... */ return "", nil }
func (m *MyStore) Set(key, val string) error { /* ... */ return nil }
func (m *MyStore) Delete(key string) error { /* ... */ return nil }
type Cache interface {
Get(key string) (string, error)
Set(key, val string) error
Delete(key string) error
}
// Compile-time check — placed right after the type definition
var _ Cache = (*MyStore)(nil) // fails to compile if *MyStore doesn't satisfy Cache
// Alternatively, for value receivers:
var _ Cache = MyStore{} // check value type
// This check is also useful in test files:
// var _ http.Handler = (*MyHandler)(nil)
// var _ sort.Interface = ByAge(nil)
// What error you get on failure:
// cannot use (*MyStore)(nil) (type *MyStore) as type Cache in assignment:
// *MyStore does not implement Cache (missing Delete method)The idiom is especially important for exported types in libraries: the check serves as documentation ("this type is intended to implement Cache") and as a regression guard. If a developer accidentally removes a method or changes its signature, the check immediately fails with a clear error pointing to the problem.
Two interface values are equal (==) if and only if both their dynamic type and dynamic value are identical. If either interface is nil, both must be nil for equality. Comparing interfaces whose dynamic type is not comparable panics at runtime.
type Animal interface{ Sound() string }
type Dog struct{ Name string }
func (d Dog) Sound() string { return "Woof" }
var a1 Animal = Dog{Name: "Rex"}
var a2 Animal = Dog{Name: "Rex"}
var a3 Animal = Dog{Name: "Buddy"}
fmt.Println(a1 == a2) // true — same type, same value (Dog is comparable)
fmt.Println(a1 == a3) // false — same type, different Name
// Nil interface equality
var a4 Animal
fmt.Println(a4 == nil) // true — both words nil
fmt.Println(a1 == nil) // false — a1 holds a Dog
// Nil pointer in interface — NOT equal to nil
var d *Dog
var a5 Animal = d
fmt.Println(a5 == nil) // FALSE — type word is non-nil (*Dog)
// Comparing interfaces with non-comparable concrete types panics!
type BadAnimal interface{ Sound() string }
type SliceDog struct{ Tags []string }
func (s SliceDog) Sound() string { return "Woof" }
var b1 BadAnimal = SliceDog{Tags: []string{"cute"}}
var b2 BadAnimal = SliceDog{Tags: []string{"cute"}}
// fmt.Println(b1 == b2) // PANIC: runtime error: comparing uncomparable type SliceDog
// Safe comparison with reflect.DeepEqual
fmt.Println(reflect.DeepEqual(b1, b2)) // true — but slowerRule: interface comparison is safe only when you know the dynamic type is comparable (no slices, maps, or functions as fields). When in doubt, use reflect.DeepEqual for structural equality or compare fields directly after a type assertion.
io.Closer is a single-method interface used to release resources. Combined with defer, it provides Go's idiomatic resource management pattern — analogous to try-with-resources in Java or RAII in C++.
// io.Closer definition
type Closer interface { Close() error }
// Pattern 1: defer immediately after acquiring the resource
func readConfig(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil { return nil, fmt.Errorf("open: %w", err) }
defer f.Close() // guaranteed to run when function returns
return io.ReadAll(f)
}
// Pattern 2: handle Close() error (important for writable files)
func writeConfig(path string, data []byte) (retErr error) {
f, err := os.Create(path)
if err != nil { return fmt.Errorf("create: %w", err) }
defer func() {
if err := f.Close(); err != nil && retErr == nil {
retErr = fmt.Errorf("close: %w", err) // capture Close error
}
}()
_, err = f.Write(data)
return err
}
// Pattern 3: implementing Closer on your own type
type DBPool struct {
pool *sql.DB
}
func (p *DBPool) Close() error { return p.pool.Close() }
// Closer in a function signature for maximum flexibility
func mustClose(c io.Closer) {
if err := c.Close(); err != nil {
log.Printf("close error: %v", err)
}
}
// Works for files, DB connections, HTTP response bodies, etc.Common mistake: not checking the error from Close() on a write path. For files opened for reading, Close() errors are usually harmless. For files opened for writing, Close() flushes buffered data to disk — an error here means data may be lost or corrupted and must be handled.
Go deliberately omits classical inheritance. The Go FAQ explains this choice: inheritance creates tight coupling between classes, makes hierarchies rigid, and encourages deep class trees that are hard to refactor. Go replaces inheritance with composition and interfaces.
| OOP Concept | Go Equivalent | Notes |
|---|---|---|
| Class | struct + methods | No single keyword — type + methods on that type |
| Constructor | NewXxx() factory function | No special syntax — just a regular function |
| Inheritance | Struct embedding (composition) | Not IS-A, just method promotion |
| Method overriding | Shadow via embedding + own method | Outer method takes precedence; call inner explicitly |
| Abstract class | Interface + unexported concrete type + factory | Contract via interface, implementation hidden |
| Polymorphism | Interface dispatch (runtime) | Any type satisfying interface can be used |
| Protected | No equivalent | Go only has exported and unexported (package-private) |
| super() | Explicit embedded type call (EmbedType.Method()) | Must qualify explicitly — no super keyword |
// Go 'inheritance' via embedding — it's really composition
type Base struct{ ID int }
func (b Base) Identify() string { return fmt.Sprintf("ID=%d", b.ID) }
type Derived struct {
Base // promoted: Derived.Identify() → Base.Identify()
Extra string
}
// 'Override' by defining the same method on Derived
func (d Derived) Identify() string {
// 'super().Identify()' equivalent:
return d.Base.Identify() + " extra=" + d.Extra
}
d := Derived{Base: Base{ID: 1}, Extra: "foo"}
fmt.Println(d.Identify()) // ID=1 extra=foo (Derived's method)
fmt.Println(d.Base.Identify()) // ID=1 (Base's method)
// CRITICAL: Derived is NOT a subtype of Base
// This does NOT compile:
// var b Base = d // cannot use Derived as type Base
// They ARE both usable through a shared interface:
type Identifier interface { Identify() string }
var id Identifier = d // OK — Derived satisfies Identifier
id = Base{ID: 2} // OK — Base satisfies Identifier too
http.Handler is one of Go's most widely used interfaces. It models exactly one behaviour — handling an HTTP request — with a single method. The entire net/http server is built around this interface, making it extensible, testable, and composable through middleware.
// The entire http.Handler interface:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
// Implement it on a struct
type HelloHandler struct{ Greeting string }
func (h HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s, %s!", h.Greeting, r.URL.Query().Get("name"))
}
http.Handle("/hello", HelloHandler{Greeting: "Hello"})
// http.HandlerFunc — adapter: converts a function to a Handler
func greetFunc(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hi!")
}
http.Handle("/greet", http.HandlerFunc(greetFunc)) // function cast to HandlerFunc
// Middleware pattern — wraps a Handler with cross-cutting concerns
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r) // call the inner handler
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Token") == "" {
http.Error(w, "unauthorized", 401)
return
}
next.ServeHTTP(w, r)
})
}
// Chain middleware
handler := loggingMiddleware(authMiddleware(HelloHandler{Greeting: "Hello"}))
http.Handle("/secure", handler)http.HandlerFunc is a named function type that satisfies http.Handler. It is a classic Go pattern: define a named type on a function signature, then implement the interface on that type. This converts any compatible function into an interface implementor — zero overhead, no wrapper struct needed.
Go allows you to declare a named function type and attach methods to it — making it satisfy an interface. This eliminates the need for a wrapper struct when the only state needed is the function itself. It is used extensively in net/http, testing, and plugin architectures.
// Define an interface
type Transformer interface {
Transform(s string) string
}
// Named function type
type TransformFunc func(string) string
// Implement the interface on the function type
func (f TransformFunc) Transform(s string) string { return f(s) }
// Usage: pass a closure directly
func applyAll(s string, ts ...Transformer) string {
for _, t := range ts {
s = t.Transform(s)
}
return s
}
result := applyAll(" hello world ",
TransformFunc(strings.TrimSpace),
TransformFunc(strings.ToUpper),
TransformFunc(func(s string) string { return "[" + s + "]" }),
)
fmt.Println(result) // [HELLO WORLD]
// Struct implementing the same interface — full control over state
type PrefixTransformer struct{ Prefix string }
func (p PrefixTransformer) Transform(s string) string {
return p.Prefix + s
}
result2 := applyAll("world",
PrefixTransformer{Prefix: "Hello, "},
TransformFunc(strings.ToUpper),
)
fmt.Println(result2) // HELLO, WORLDThe pattern makes APIs flexible: callers can provide a simple closure or a stateful struct — both satisfy the interface. The API never needs to know which was used. This is the basis of middleware chains, plugin hooks, and event callbacks in Go.
Interface pollution means creating interfaces prematurely, needlessly, or with too many methods — when a concrete type would be simpler and clearer. It adds indirection without benefit and makes code harder to navigate.
| Anti-pattern | Problem | Fix |
|---|---|---|
| Interface with one method per struct method | Exact mirror of the struct — no abstraction added | Use the concrete type directly |
| Interface defined by the implementor, not the consumer | Forces all callers to use the interface even if only one type exists | Define the interface in the consumer package |
| Interface used to add indirection to simple logic | Every call requires vtable dispatch for no benefit | Pass concrete type; extract interface when a second impl appears |
| Returning an interface from a constructor 'for flexibility' | Callers cannot access all methods; future changes break the contract | Return the concrete type; consumers can define their own interface |
// Interface pollution — unnecessary interface
type UserGetter interface {
GetUser(id int) User
GetUserByEmail(email string) User
GetUsersByGroup(group string) []User
UpdateUser(u User) error
DeleteUser(id int) error
// ... 10 more methods
}
// If only ONE type will ever implement this, it adds noise
// Better: define the interface where it's consumed, with minimum methods
// In the notification package:
type UserFetcher interface { // only what notifications need
GetUser(id int) User
}
// Rule: define the interface in the CONSUMING package, not the producing package
// (Often called the 'interface ownership' principle)
// When interfaces are justified:
// 1. Multiple concrete implementations exist (or are expected soon)
// 2. Needed for testability (inject a mock)
// 3. Standard library patterns (io.Reader, http.Handler)
// 4. Plugin or extension points in a library
// Go proverb: 'Don't design with interfaces, discover them.'
// Start with concrete types; extract interfaces when the need becomes clearThe Go proverb "Don't design with interfaces, discover them" captures the idiomatic approach: write concrete types first. When a second concrete implementation is needed, or when testability requires a mock, extract an interface at that point. Premature abstraction costs readability without delivering flexibility.
Go's fmt package supports two string-representation interfaces for types. They serve different audiences and contexts:
| Interface | Method | Format Verb | Purpose |
|---|---|---|---|
| fmt.Stringer | String() string | %v, %s | Human-readable output for logs, CLI, user-facing text |
| fmt.GoStringer | GoString() string | %#v | Go-syntax representation for debugging — shows how to reproduce the value |
type Color struct{ R, G, B uint8 }
// Stringer — human readable
func (c Color) String() string {
return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B)
}
// GoStringer — Go syntax
func (c Color) GoString() string {
return fmt.Sprintf("Color{R: %d, G: %d, B: %d}", c.R, c.G, c.B)
}
c := Color{255, 128, 0}
fmt.Printf("%v\n", c) // #FF8000 (uses String())
fmt.Printf("%s\n", c) // #FF8000
fmt.Printf("%#v\n", c) // Color{R: 255, G: 128, B: 0} (uses GoString())
// Without GoString, %#v uses default Go syntax:
// main.Color{R:0xff, G:0x80, B:0x0}
// Practical use: panic messages, test failure output, %#v in debugging
// GoString makes the output directly copy-pasteable as Go codeGoString() is particularly valuable in test failure messages: when a test prints %#v of an unexpected value, the output can be copied directly into the test as the expected value — saving debugging time.
The I/O stack in Go is built by wrapping interfaces. Each layer satisfies io.Reader or io.Writer and wraps the previous layer, adding one transformation: buffering, compression, encryption, counting. No layer modifies the others — they compose transparently.
// Layer by layer: file → gzip → bufio → hash → tee
f, err := os.Create("output.gz")
if err != nil { log.Fatal(err) }
defer f.Close()
// Layer 1: gzip compression on top of the file
gz := gzip.NewWriter(f) // satisfies io.WriteCloser
defer gz.Close()
// Layer 2: buffering on top of gzip
buf := bufio.NewWriter(gz) // satisfies io.Writer
// Layer 3: SHA-256 hash computation alongside writing
h := sha256.New() // satisfies io.Writer
tee := io.MultiWriter(buf, h) // writes to both buf and hash
// Write data — single Write call propagates through all layers
fmt.Fprintln(tee, "Hello, World!") // → buf → gz → f; also → h
buf.Flush() // flush buffered data into gzip
sum := h.Sum(nil)
fmt.Printf("SHA-256: %x\n", sum)
// Reading side
f2, _ := os.Open("output.gz")
defer f2.Close()
gzr, _ := gzip.NewReader(f2) // io.Reader that decompresses
buf2 := bufio.NewReader(gzr) // io.Reader that buffers
limited := io.LimitReader(buf2, 1024) // io.Reader that stops at 1 KB
data, _ := io.ReadAll(limited) // reads through all three layers
fmt.Println(string(data))Each layer is oblivious to the others. gzip.NewReader doesn't know if it's wrapping a file or a network connection; bufio.NewReader doesn't know about compression. This is the Decorator pattern executed through Go interfaces: behaviours stack without modifying each other.
Both interfaces and reflection allow code to work with values of unknown types at runtime, but they differ fundamentally in safety, performance, and intent.
| Aspect | Interface | Reflection (reflect package) |
|---|---|---|
| Type safety | Compile-time method set checked | Fully runtime — panics on misuse |
| Performance | One indirect call (itab) | 10–100x slower |
| Expressiveness | Only call declared methods | Inspect any field, call any method, set values |
| Use when | You know the contract (method set) at design time | Contract unknown at compile time (marshalling, ORM, DI frameworks) |
| Discoverability | Interface name is self-documenting | Code is harder to follow and trace |
// Interface approach — compile-time safety
type Serialiser interface{ Serialise() []byte }
func saveToFile(s Serialiser, path string) error {
return os.WriteFile(path, s.Serialise(), 0644)
}
// Reflection approach — needed when structure is unknown at compile time
// (like JSON encoding)
func toMap(v any) map[string]any {
result := make(map[string]any)
val := reflect.ValueOf(v)
if val.Kind() == reflect.Ptr { val = val.Elem() }
if val.Kind() != reflect.Struct { return nil }
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
if !field.IsExported() { continue }
key := field.Tag.Get("json")
if key == "" { key = field.Name }
result[key] = val.Field(i).Interface()
}
return result
}
// Generics (Go 1.18+) — preferred over reflection for type-agnostic algorithms
func Map[T, U any](slice []T, f func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice { result[i] = f(v) }
return result
}
// No reflection, compile-time safe, fastDecision guide: (1) use an interface when the contract is known at design time; (2) use generics when the algorithm is identical across types; (3) use reflection only when you must work with arbitrary struct layouts or types unknown until runtime — JSON encoding, ORM field mapping, dependency injection containers.
In Go, the best interfaces describe what something does, not what something is. Names ending in -er (Reader, Writer, Closer, Logger, Handler) signal a single behaviour. This contrasts with Java-style interfaces like IUserRepository that mirror a class's public API.
// Behaviour-based (Go idiomatic)
type Logger interface { Log(msg string) } // what it does
type Notifier interface { Notify(event Event) } // what it does
type Closer interface { Close() error } // what it does
// Data/entity-based (anti-pattern in Go)
type IUserRepository interface { // describes a whole entity
FindByID(id int) (*User, error)
FindByEmail(email string) (*User, error)
Save(u *User) error
Delete(id int) error
Count() int
}
// Problem: every consumer is forced to implement or mock ALL 5 methods
// even if they only need FindByID
// Better: define the minimum interface in the consumer
// package email — only needs to look up users
type UserLookup interface { FindByEmail(string) (*User, error) }
// package reporting — only needs to count
type UserCounter interface { Count() int }
// The concrete *UserRepo satisfies all of the above
// without knowing about any of them
// Naming: prefer -er suffix for single-method interfaces
// io.Reader, io.Writer, io.Closer — the standard library gold standard
// fmt.Stringer, sort.Interface, http.HandlerThe behaviour framing also makes mocking trivial. If your function needs only UserLookup, the mock has one method. If it needs the full IUserRepository, the mock has five. Narrow, behaviour-focused interfaces dramatically reduce test maintenance burden.
The interface upgrade pattern lets code check whether an interface value's concrete type also satisfies a more capable (optional) interface, and use the enhanced behaviour if available — without requiring all implementations to support it. This is how the Go standard library achieves extensibility.
// Basic interface — all implementations must satisfy
type Writer interface {
Write(p []byte) (int, error)
}
// Enhanced optional interface
type StringWriter interface {
WriteString(s string) (int, error)
}
// Function accepting Writer — works with any Writer
// but uses WriteString if available (avoids []byte conversion)
func writeString(w Writer, s string) (int, error) {
// Interface upgrade: check if w also supports WriteString
if sw, ok := w.(StringWriter); ok {
return sw.WriteString(s) // faster path
}
return w.Write([]byte(s)) // fallback
}
// This is exactly how bufio.Writer in the stdlib does it:
// bufio.Writer.WriteString checks if the underlying writer
// implements WriteString and avoids the []byte allocation
// Another real example: http.Flusher
func streamResponse(w http.ResponseWriter, data []byte) {
w.Write(data)
// Check if the ResponseWriter supports streaming flush
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush() // sends bytes to client immediately
}
}
// http.Hijacker — upgrade ResponseWriter to take over TCP connection
// http.CloseNotifier — deprecated, but same pattern
// io.WriterTo, io.ReaderFrom — upgrade for efficient copyThe upgrade pattern lets the standard library evolve without breaking existing code: a new optional interface can be added, and implementations that want the enhanced behaviour can opt in. Code that only has the basic interface continues working. This is much more flexible than adding a method to an existing interface (which would break all existing implementations).
Both look syntactically similar but have very different semantics and use cases:
| Aspect | Embed a struct | Embed an interface |
|---|---|---|
| Promoted methods | Real implementations from the embedded struct | Method signatures only — calls the interface field's concrete value |
| Zero value behaviour | Embedded struct's zero value is used | Nil interface — calling any method panics |
| Memory | Inline — embedded struct's fields are part of the outer struct | Two-word interface value (type + data) |
| Primary use | Code reuse — share method implementations | Partial mocking — satisfy a large interface by overriding only some methods |
// Embedding a STRUCT — real method reuse
type Base struct{ ID int }
func (b Base) Describe() string { return fmt.Sprintf("id=%d", b.ID) }
type Derived struct { Base; Extra string }
d := Derived{Base: Base{ID: 1}, Extra: "x"}
d.Describe() // calls Base.Describe() — real implementation
// Embedding an INTERFACE — partial mock pattern
type Storage interface {
Read(key string) ([]byte, error)
Write(key string, val []byte) error
Delete(key string) error
List() ([]string, error)
}
// Test stub — only override Read; others panic (acceptable in tests)
type ReadOnlyStub struct {
Storage // embeds interface — zero value = nil
data map[string][]byte
}
// Override only Read
func (r ReadOnlyStub) Read(key string) ([]byte, error) {
v, ok := r.data[key]
if !ok { return nil, errors.New("not found") }
return v, nil
}
// ReadOnlyStub satisfies Storage (via embedded interface promotion)
// Calling Write, Delete, or List will panic — acceptable for a test stub
var s Storage = ReadOnlyStub{data: map[string][]byte{"k": []byte("v")}}
data, _ := s.Read("k") // works
// s.Write("k", nil) // panics at runtime
Interface values are not inherently goroutine-safe. An interface value is a two-word struct (type pointer + data pointer). Assigning to an interface variable is not atomic — a goroutine reading the interface while another is writing it can observe a partially written state (mismatched type and data words).
// RACE CONDITION: assigning interface value from multiple goroutines
var handler http.Handler
// goroutine 1:
go func() { handler = &MyHandlerA{} }()
// goroutine 2:
go func() { handler = &MyHandlerB{} }()
// goroutine 3 (reader):
go func() { handler.ServeHTTP(w, r) }() // may see inconsistent type+data words
// Detection: go test -race finds this immediately
// Fix 1: protect with sync.Mutex
var mu sync.Mutex
var safeHandler http.Handler
setHandler := func(h http.Handler) {
mu.Lock()
safeHandler = h
mu.Unlock()
}
getHandler := func() http.Handler {
mu.Lock()
defer mu.Unlock()
return safeHandler
}
// Fix 2: atomic.Value (stores interface{} / any atomically)
var atomicHandler atomic.Value
atomicHandler.Store(http.Handler(&MyHandlerA{}))
h := atomicHandler.Load().(http.Handler) // atomic read
h.ServeHTTP(w, r)
// Note: atomic.Value requires all stored values to be the same concrete type
// or the same interface type across callsatomic.Value (since Go 1.4) is the idiomatic way to atomically swap an interface value: it is faster than a mutex for read-heavy scenarios (like a config that is rarely updated but read on every request). All Store calls must store values of the same concrete type.
The two most important Go testing patterns together: table-driven tests for coverage and interface mocks for isolation. Combining them gives thorough, readable, and maintainable tests for any component that has external dependencies.
// System under test
type UserEmailer interface {
GetEmail(userID int) (string, error)
}
type Mailer interface {
Send(to, subject, body string) error
}
type NotificationService struct {
emailer UserEmailer
mailer Mailer
}
func (ns *NotificationService) Notify(userID int, msg string) error {
email, err := ns.emailer.GetEmail(userID)
if err != nil { return fmt.Errorf("get email: %w", err) }
return ns.mailer.Send(email, "Notification", msg)
}
// Test mocks
type fakeEmailer struct{ email string; err error }
func (f *fakeEmailer) GetEmail(int) (string, error) { return f.email, f.err }
type fakeMailer struct{ sent []string; err error }
func (m *fakeMailer) Send(to, _, _ string) error {
m.sent = append(m.sent, to)
return m.err
}
// Table-driven test combining both mocks
func TestNotify(t *testing.T) {
tests := []struct {
name string
emailErr error
mailerErr error
wantErr bool
wantSent int
}{
{"happy path", nil, nil, false, 1},
{"email lookup fails", errors.New("db error"), nil, true, 0},
{"mailer fails", nil, errors.New("smtp error"), true, 0},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
mailer := &fakeMailer{err: tc.mailerErr}
svc := &NotificationService{
emailer: &fakeEmailer{email: "u@x.com", err: tc.emailErr},
mailer: mailer,
}
err := svc.Notify(1, "hello")
if (err != nil) != tc.wantErr {
t.Errorf("want err=%v, got %v", tc.wantErr, err)
}
if len(mailer.sent) != tc.wantSent {
t.Errorf("want %d sent, got %d", tc.wantSent, len(mailer.sent))
}
})
}
}
context.Context is a four-method interface that carries deadlines, cancellation signals, and request-scoped values across API boundaries. Its design exemplifies Go's interface philosophy: small, behaviour-focused, implicitly satisfied, and composable.
// The full context.Context interface:
type Context interface {
Deadline() (deadline time.Time, ok bool) // when will this context expire?
Done() <-chan struct{} // closed when context is cancelled
Err() error // nil, context.Canceled, or context.DeadlineExceeded
Value(key any) any // request-scoped values
}
// All functions that may block or need cancellation accept context as FIRST param
func FetchUser(ctx context.Context, id int) (*User, error) {
req, _ := http.NewRequestWithContext(ctx, "GET",
fmt.Sprintf("/users/%d", id), nil)
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer resp.Body.Close()
var u User
return &u, json.NewDecoder(resp.Body).Decode(&u)
}
// Creating contexts
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // ALWAYS defer cancel
user, err := FetchUser(ctx, 42)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.Println("request timed out")
}
}
// Checking cancellation in a loop
func processItems(ctx context.Context, items []Item) error {
for _, item := range items {
select {
case <-ctx.Done(): return ctx.Err() // cancelled
default: process(item)
}
}
return nil
}Notice that context.Context is defined as an interface but almost always consumed, never implemented by user code (the standard library provides all concrete implementations: context.Background(), context.WithTimeout, etc.). This is the correct usage: it is a consumer-facing interface that unifies all context types behind one contract, enabling library code to accept any context without caring about its implementation.
The Interface Segregation Principle states: clients should not be forced to depend on methods they do not use. Go's implicit, structural interfaces make ISP trivially achievable — any consumer can define the minimal interface it needs, independently of what the concrete type exposes.
// A concrete type with many methods (a real storage backend)
type RedisClient struct{ pool *redis.Pool }
func (r *RedisClient) Get(key string) (string, error) { /* ... */ return "", nil }
func (r *RedisClient) Set(key, val string, ttl time.Duration) error { /* ... */ return nil }
func (r *RedisClient) Delete(key string) error { /* ... */ return nil }
func (r *RedisClient) Increment(key string) (int64, error) { /* ... */ return 0, nil }
func (r *RedisClient) Expire(key string, d time.Duration) error { /* ... */ return nil }
func (r *RedisClient) Ping() error { /* ... */ return nil }
// Package: cache — only needs Get and Set
type Getter interface{ Get(key string) (string, error) }
type Setter interface{ Set(key, val string, ttl time.Duration) error }
type Cache interface { Getter; Setter } // composed
func newCacheLayer(c Cache) *CacheLayer { return &CacheLayer{c: c} }
// Package: rate-limiter — only needs Increment and Expire
type Counter interface {
Increment(key string) (int64, error)
Expire(key string, d time.Duration) error
}
func newRateLimiter(c Counter) *RateLimiter { return &RateLimiter{c: c} }
// Package: health — only needs Ping
type Pinger interface{ Ping() error }
func newHealthCheck(p Pinger) *HealthCheck { return &HealthCheck{p: p} }
// RedisClient satisfies ALL of Cache, Counter, and Pinger
redis := &RedisClient{pool: pool}
cache := newCacheLayer(redis) // passes as Cache
limiter := newRateLimiter(redis) // passes as Counter
health := newHealthCheck(redis) // passes as PingerEach consumer defines its own minimal interface — without RedisClient knowing about any of them. Adding a new consumer with a new subset of methods requires zero changes to RedisClient. This is ISP at its most natural: Go's structural typing makes it the path of least resistance.
Go distinguishes between type definitions (type MyInt int) and type aliases (type MyInt = int). They behave very differently when it comes to interface satisfaction and method sets.
// TYPE DEFINITION — creates a new, distinct type
type Celsius float64
type Fahrenheit float64
// Celsius and float64 are different types — no implicit conversion
var c Celsius = 100.0
// var f float64 = c // COMPILE ERROR: cannot use Celsius as float64
var f float64 = float64(c) // explicit conversion required
// Methods can be defined on type-defined types
func (c Celsius) ToFahrenheit() Fahrenheit { return Fahrenheit(c*9/5 + 32) }
// TYPE ALIAS — another name for the same type
type MyFloat64 = float64 // alias: MyFloat64 IS float64
var x MyFloat64 = 3.14
var y float64 = x // OK — same type
// Cannot define methods on alias types (not in the same package as the original)
// Interface satisfaction:
type Stringer interface { String() string }
// Only defined types can have methods (and thus satisfy interfaces via those methods)
func (c Celsius) String() string { return fmt.Sprintf("%.1f°C", float64(c)) }
var s Stringer = Celsius(100) // OK — Celsius has String()
// Named function types (common for interface satisfaction)
type HandlerFunc func(string) error
func (f HandlerFunc) Handle(s string) error { return f(s) } // method on func type
type Handler interface { Handle(string) error }
var h Handler = HandlerFunc(func(s string) error { return nil })Key rule: you can define methods on a type-defined type (creating a new type gives it its own method set). You cannot add methods on an alias type (it's the same type — adding methods would affect the original type's package). This is why type MyInt int can satisfy a new interface that int cannot — you can add methods to MyInt.
Embedding is transitive. If type C embeds type B which embeds type A, then C's method set includes all methods from A and B. This means C can satisfy interfaces that A satisfies — through a chain of embeddings.
type Named interface { Name() string }
type Described interface { Describe() string }
type NamedAndDescribed interface { Named; Described }
// Level 1
type Animal struct{ name string }
func (a Animal) Name() string { return a.name }
// Level 2 — embeds Animal
type Pet struct {
Animal
Owner string
}
func (p Pet) Describe() string {
return fmt.Sprintf("%s owned by %s", p.Name(), p.Owner)
}
// Level 3 — embeds Pet
type ServiceAnimal struct {
Pet
BadgeID string
}
// ServiceAnimal inherits Name() from Animal (via Pet) and
// Describe() from Pet
sa := ServiceAnimal{
Pet: Pet{
Animal: Animal{name: "Rex"},
Owner: "Alice",
},
BadgeID: "K9-042",
}
var nd NamedAndDescribed = sa // ServiceAnimal satisfies NamedAndDescribed
fmt.Println(nd.Name()) // Rex
fmt.Println(nd.Describe()) // Rex owned by Alice
// Method ambiguity: what if both A and B define the same method?
type X struct { Val int }; func (x X) Tag() string { return "X" }
type Y struct { Val int }; func (y Y) Tag() string { return "Y" }
type Ambiguous struct { X; Y }
// a := Ambiguous{}; a.Tag() // COMPILE ERROR: ambiguous selector a.Tag
// Must qualify: a.X.Tag() or a.Y.Tag()Disambiguation rule: if two embedded types provide a method with the same name at the same depth, accessing that method on the outer struct is a compile error. The solution is to define the method explicitly on the outer type, which takes precedence and resolves the ambiguity.
Understanding zero values is essential for correct initialisation. Interface and struct zero values behave very differently.
// Zero value of a struct — all fields zeroed
type Config struct{ Host string; Port int; Debug bool }
var cfg Config // zero value: Config{Host:"", Port:0, Debug:false}
fmt.Println(cfg.Host) // "" — valid, ready to use
fmt.Println(cfg.Debug) // false
// Zero value of an interface — nil (both words zeroed)
var r io.Reader // nil interface
fmt.Println(r == nil) // true
// r.Read(buf) // PANIC: nil pointer dereference — no concrete impl
// Nil check before using interface
func useReader(r io.Reader) {
if r == nil {
log.Println("no reader provided")
return
}
io.Copy(os.Stdout, r)
}
// Structs designed to be useful at zero value (sync.Mutex, bytes.Buffer)
var mu sync.Mutex // zero value is a valid, unlocked mutex
mu.Lock()
defer mu.Unlock()
var buf bytes.Buffer // zero value is a valid empty buffer
buf.WriteString("hello")
// Interface containing a zero-value struct (NOT nil interface)
var c Config
var any interface{} = c // any is non-nil (type=Config, data=*Config zero)
fmt.Println(any == nil) // FALSE — interface holds a valueDesign goal: strive to make your types useful at their zero value (like sync.Mutex and bytes.Buffer). This means users do not need a constructor to get a valid instance. For interface types, the zero value is always nil — always check before dereferencing, or use the Null Object pattern to provide a safe no-op default.
Sometimes an existing type almost satisfies an interface but has a slightly different method signature. Rather than modifying the original type (which may be in another package), you can wrap it in a new type that adapts the interface.
// Standard logger — method signature doesn't match our interface
// log.Logger has: func (l *Logger) Printf(format string, v ...any)
// Our interface for injecting loggers
type Logger interface {
Log(msg string)
}
// Adapter: wraps *log.Logger to satisfy Logger
type StdLogger struct{ l *log.Logger }
func NewStdLogger(l *log.Logger) Logger { return &StdLogger{l: l} }
func (s *StdLogger) Log(msg string) { s.l.Println(msg) }
// Now log.Logger can be used anywhere Logger is expected
svc := NewService(NewStdLogger(log.Default()))
// Adapter for third-party HTTP client → our HTTPClient interface
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// *http.Client already has Do() — no adapter needed!
var client HTTPClient = http.DefaultClient // satisfied out of the box
// Reverse adapter: wrap an interface to add a method
// LimitedReader wraps io.Reader and adds a counter
type CountingReader struct {
r io.Reader
count int64
}
func (cr *CountingReader) Read(p []byte) (int, error) {
n, err := cr.r.Read(p)
cr.count += int64(n)
return n, err
}
func (cr *CountingReader) BytesRead() int64 { return cr.count }
cr := &CountingReader{r: os.Stdin}
io.Copy(os.Stdout, cr)
fmt.Println("read", cr.BytesRead(), "bytes")The adapter pattern in Go is simpler than in Java because Go interfaces are implicit. A wrapper struct that delegates to the original type and has the right method signatures automatically satisfies any compatible interface — no import of the interface's package, no extends Adapter.
A realistic scenario showing how Go's interfaces, composition, and implicit satisfaction replace classical OOP: a payment processing system that is extensible, testable, and loosely coupled — without a single inheritance relationship.
// ── Interfaces ──────────────────────────────────────────────────
type PaymentProcessor interface {
Process(amount float64, currency string) (txID string, err error)
}
type Refunder interface {
Refund(txID string, amount float64) error
}
type FullPayment interface {
PaymentProcessor
Refunder
}
// ── Concrete implementations ────────────────────────────────────
type StripeProcessor struct{ apiKey string }
func NewStripe(key string) *StripeProcessor { return &StripeProcessor{apiKey: key} }
func (s *StripeProcessor) Process(amount float64, currency string) (string, error) {
return "stripe_tx_" + uuid(), nil // call Stripe API
}
func (s *StripeProcessor) Refund(txID string, amount float64) error {
return nil // call Stripe refund API
}
// Compile-time check
var _ FullPayment = (*StripeProcessor)(nil)
// ── Service: depends on narrow interfaces ───────────────────────
type OrderService struct {
payments PaymentProcessor // only needs to charge
}
func (os *OrderService) PlaceOrder(amount float64) error {
txID, err := os.payments.Process(amount, "USD")
if err != nil { return fmt.Errorf("payment failed: %w", err) }
log.Printf("order paid, tx=%s", txID)
return nil
}
type RefundService struct {
refunds Refunder // only needs to refund
}
func (rs *RefundService) Refund(txID string, amount float64) error {
return rs.refunds.Refund(txID, amount)
}
// ── Wiring (main) ────────────────────────────────────────────────
stripe := NewStripe(os.Getenv("STRIPE_KEY"))
orderSvc := &OrderService{payments: stripe} // stripe as PaymentProcessor
refundSvc := &RefundService{refunds: stripe} // stripe as Refunder
// ── Test mock ────────────────────────────────────────────────────
type MockPayment struct{ txID string; err error }
func (m *MockPayment) Process(float64, string) (string, error) { return m.txID, m.err }
func TestPlaceOrder(t *testing.T) {
svc := &OrderService{payments: &MockPayment{txID: "mock_tx"}}
if err := svc.PlaceOrder(99.99); err != nil {
t.Fatal(err)
}
}
Interface equality has several subtle edge cases that are distinct from what most developers expect:
// Case 1: nil interface vs interface holding nil pointer
var p *int
var i interface{} = p // i has type *int, data nil
fmt.Println(p == nil) // true — p is a nil pointer
fmt.Println(i == nil) // FALSE — i has a non-nil type word
// Case 2: two different interface types wrapping the same concrete value
type A interface{ Foo() }
type B interface{ Foo() }
type S struct{}
func (S) Foo() {}
var a A = S{}
var b B = S{}
// a == b // COMPILE ERROR: invalid operation — a and b are different interface types
// But:
fmt.Println(reflect.DeepEqual(a, b)) // true — same concrete type and value
// Case 3: two interface values of the same type
var a1 A = S{}
var a2 A = S{}
fmt.Println(a1 == a2) // true — S{} == S{} (empty struct is comparable)
// Case 4: interface wrapping uncomparable type — RUNTIME PANIC
type WithSlice struct{ s []int }
func (WithSlice) Foo() {}
var a3 A = WithSlice{s: []int{1}}
var a4 A = WithSlice{s: []int{1}}
// fmt.Println(a3 == a4) // PANIC: runtime error: comparing uncomparable type main.WithSlice
// Case 5: comparing interface to its concrete value directly
var s interface{} = "hello"
fmt.Println(s == "hello") // true — right side promoted to interface, then comparedMemory aid: interface equality checks (1) that both type words are identical AND (2) that both data values are equal using the concrete type's == operator. If the concrete type is not comparable (contains slices, maps, functions), the comparison panics. Use reflect.DeepEqual for structural comparison when the type might be non-comparable.
sync.Locker is a small two-method interface from the standard library. It is satisfied by both *sync.Mutex and *sync.RWMutex, as well as any custom lock type. It is used to write generic lock-based utilities.
// sync.Locker interface:
type Locker interface {
Lock()
Unlock()
}
// Both *sync.Mutex and *sync.RWMutex satisfy Locker
var mu sync.Mutex
var rwmu sync.RWMutex
var l1 sync.Locker = &mu // *Mutex satisfies Locker
var l2 sync.Locker = &rwmu // *RWMutex satisfies Locker (Lock/Unlock = write lock)
// Generic utility using sync.Locker
func withLock(l sync.Locker, fn func()) {
l.Lock()
defer l.Unlock()
fn()
}
counter := 0
withLock(&mu, func() { counter++ })
// sync.Cond uses Locker — works with any lock
cond := sync.NewCond(&mu) // &mu satisfies Locker
// Or with an RWMutex read lock:
cond2 := sync.NewCond(rwmu.RLocker()) // RLocker() returns a Locker for the read lock
// Custom lock satisfying Locker
type SpinLock struct{ locked atomic.Bool }
func (s *SpinLock) Lock() { for !s.locked.CompareAndSwap(false, true) { runtime.Gosched() } }
func (s *SpinLock) Unlock() { s.locked.Store(false) }
var spin SpinLock
withLock(&spin, func() { counter++ }) // SpinLock works with withLock!sync.Locker is an excellent example of Go's philosophy: a small, well-named interface that abstracts just one behaviour (mutual exclusion). The withLock utility pattern is idiomatic — it ensures Unlock is always called via defer, avoiding forgotten unlocks.
This summary covers the most frequently tested interface rules in Go technical interviews:
| Rule | Detail |
|---|---|
| Implicit satisfaction | No 'implements' keyword — matching method set is sufficient |
| Method set (value) | T has only value-receiver methods in its method set |
| Method set (pointer) | *T has both value AND pointer receiver methods |
| Nil interface | Both type and data words must be zero — interface nil |
| Nil pointer in interface | Type word non-zero → interface NOT nil — classic trap |
| Return nil, not typed nil | Functions returning error must use bare 'nil', never (*MyError)(nil) |
| Two-word layout | word1=itab (type+dispatch table), word2=data pointer |
| Interface comparison | Equal iff same dynamic type AND same dynamic value (panics on non-comparable) |
| Empty interface (any) | interface{} — all types satisfy it; loses compile-time type safety |
| Interface composition | Embed interfaces to build larger contracts |
| Accept interfaces | Function parameters should be interfaces — enables mocking and flexibility |
| Return concrete | Constructors return concrete types — gives callers full method set |
| Compile-time check | var _ MyInterface = (*MyType)(nil) — asserts satisfaction at compile time |
| Small interfaces | 1–3 methods; 'bigger interface = weaker abstraction' |
| Interface ownership | Define interface in the consuming package, not the producing package |
// Quick reference — most tested patterns
// 1. Compile-time check
var _ io.Writer = (*MyWriter)(nil)
// 2. Return nil correctly
func f() error { return nil } // NOT: var e *MyError; return e
// 3. Comma-ok assertion (never panic)
if w, ok := v.(io.Writer); ok { w.Write(data) }
// 4. Type switch
switch t := v.(type) {
case int: use(t)
case string: use(t)
default: unknown(t)
}
// 5. Nil interface check
var r io.Reader // nil
if r != nil { r.Read(buf) } // safe
// 6. Non-nil interface with nil data
var p *bytes.Buffer
var w io.Writer = p // w != nil even though p == nil
// 7. Consumer-defined narrow interface
type Saver interface { Save() error } // in consumer package
// 8. Interface upgrade (optional capability)
if sw, ok := w.(io.StringWriter); ok { sw.WriteString(s) }
