4. Methods and Interfaces

Attaching behavior to types, designing small interfaces, and avoiding method-set surprises.

Question: When should you use a pointer receiver versus a value receiver for a method?

Answer: Use a pointer receiver (*T) when the method needs to modify the receiver's state or when the receiver is a large struct that would be expensive to copy. Use a value receiver (T) for small types where the method does not need to change the receiver.

Explanation: In Go, arguments are passed by value. When you use a value receiver, the method operates on a copy of the original struct. A pointer receiver gets a pointer to the original struct, allowing direct mutation and avoiding the performance overhead of copying. As a rule of thumb, if you're ever in doubt, use a pointer receiver.

type Counter struct{ n int }

// Value receiver: operates on a copy.
func (c Counter) Value() int { return c.n }

// Pointer receiver: mutates the original struct.
func (c *Counter) Inc() { c.n++ }

Note: Method sets interact with interfaces. A type with pointer receiver methods (*T) does not automatically satisfy an interface when using a value of type T. Be consistent in receiver choice across a type’s methods to avoid surprises.

Question: How do interfaces work in Go, and what does "implicit satisfaction" mean?

Answer: An interface in Go is a type that defines a set of method signatures. "Implicit satisfaction" means a type satisfies an interface if it implements all the methods declared in the interface, without needing an explicit implements keyword.

Explanation: This makes Go's interfaces very flexible and decoupled. You can define an interface for a type from a third-party package without modifying its source code. It encourages the design of small, focused interfaces (like io.Reader), as consumers can define exactly the behavior they need.

Question: What is the empty interface (any) and when is it used?

Answer: The empty interface, written as interface{} or its alias any, has zero methods. Since any type has zero or more methods, every type satisfies the empty interface. It is used when you need to handle a value of an unknown type.

Explanation: Working with any requires type assertions or type switches to determine the underlying concrete type before you can do anything useful with the value. While powerful, it should be used sparingly as it bypasses static type safety.

func PrintAny(v any) {
    // Type switch to inspect the concrete type
    switch t := v.(type) {
    case int:
        fmt.Printf("It's an int: %d\n", t)
    case string:
        fmt.Printf("It's a string: %s\n", t)
    default:
        fmt.Printf("Unknown type: %T\n", t)
    }
}

Question: Can methods be called on a nil receiver?

Answer: Yes, if the method has a pointer receiver and handles nil safely inside. This is a common pattern for types like bytes.Buffer and custom linked structures.

Explanation: Check for nil receiver at the start of the method and define semantics (e.g., no-op or zero value behavior).

Question: How do you perform type assertions and type switches safely?

Answer: Use v, ok := x.(T) to avoid panics; use a type switch for multiple possibilities.

Explanation: Prefer small, focused interfaces and avoid any unless necessary.

Question: What is fmt.Stringer and why implement it?

Answer: Stringer is an interface with String() string. Implementing it provides readable output in logs and debugging.

Question: What is a method set, and how does it affect interface satisfaction?

Answer: The method set of T includes methods with receiver T; the method set of *T includes methods with receivers T and *T. A T value implements an interface only if its method set includes all required methods.

Explanation: If methods are defined with pointer receivers, only *T satisfies interfaces requiring them. Be consistent in receiver choice.