skip to content

In some languages the receiver of a method is written as an explicit parameter (Python's self, Go's receiver declaration, Rust's &self) while in others it is an implicit this. What can a language express by making the receiver explicit, and what does it pay for that?

level: seniorimportance: should knowfreq 34%

answer

  1. receiver as parameter: it has a type and a mode
  2. Go value receiver mutates a copy; method set decides interface fit
  3. Rust self consumes, &mut self is exclusive
  4. Python obj.m is a bound method object; Cls.m(obj) is legal
  5. C++ ref-qualifiers and C++23 deducing this

basics

~20 s

An explicit receiver is a parameter, so its type and mode can vary. Go distinguishes value from pointer receivers, which decides interface satisfaction; Rust encodes ownership (&self, &mut self, self consumes the object); Python makes methods plain functions bound through the descriptor protocol.

solid answer

~1 min

Making the receiver a parameter lets the type system say **how the call treats the object**, not only what the object is. - **Go** — `func (t T)` copies the receiver, so mutations are discarded, and the method set of `T` excludes pointer-receiver methods. `var x T; var i I = x` fails to compile while `&x` succeeds: a storage decision leaks into interface satisfaction, which is Go's most-reported error of this shape. - **Rust** — `self` by value means the method *consumes* the receiver. `into_inner(self)` and consuming builder chains make the old handle statically unusable; `&mut self` encodes exclusive access, so two mutating calls cannot overlap. Java and C# cannot say that with an implicit `this`; they express it with a `close()` convention and an `IllegalStateException` at run time. - **Python** — `obj.m` builds a bound-method object you can pass around, and `Cls.m(obj)` is legal, which is why decorators, mixins and monkeypatching all fall out for free. The price: `self` is a convention, not a keyword, and omitting it is a runtime `TypeError`. - **C++** reaches part of the same expressiveness through member-function qualifiers (`void f() &&` is callable only on an rvalue) and, since C++23, an explicit object parameter.

code

go · 8 lines
go
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }

type Incrementer interface{ Inc() }

var c Counter
var i Incrementer = c   // compile error: Inc has pointer receiver
var j Incrementer = &c  // ok

go deeper

for a junior

Know that self or this identifies the object the method was called on, and that some languages write it and some hide it.

for a middle

Give the Go value-versus-pointer receiver consequence for mutation, and know Python methods are functions bound on access.

for a senior

Argue what the mode buys: interface satisfaction in Go, ownership and exclusivity in Rust, uniform function semantics in Python, and name the runtime workaround implicit-this languages need instead.

for a principal

Use it when specifying cross-language APIs: state which operations invalidate the receiver and accept that only some targets can enforce it, deciding whether to encode the constraint in types, in a handle-returning shape, or in runtime state.

## What the receiver actually is A method needs to know which object it was called on. Languages in the C++/Java/C# line keep that binding invisible: the compiler passes the object as a hidden first argument and exposes it as `this`. Languages in the Python/Go/Rust line write it down. The interesting part is not verbosity — it is that once the receiver is a parameter, it has a *type* and a *mode*, and both can carry meaning. ## Go: the receiver's mode changes what the type is A Go method is declared as `func (r T) Name()` or `func (r *T) Name()`. A value receiver gets a copy, so a mutation inside is invisible afterwards; that alone is a common bug. The subtler consequence is interface satisfaction. Go defines the method set of `T` as its value-receiver methods, and the method set of `*T` as both. So a type that mutates itself satisfies an interface only through a pointer: var c Counter // methods declared on *Counter var i Incrementer = c // compile error var j Incrementer = &c // fine The error message ('method has pointer receiver') is one of the first things a Go newcomer meets. The design decision it encodes is honest: whether a method mutates is part of the contract, so it is part of what the type provides. ## Rust: the receiver's mode is ownership Rust offers three receivers. `&self` borrows immutably; `&mut self` borrows exclusively; `self` takes ownership and therefore consumes the object. That last one has no analogue in the implicit-`this` languages. `fn into_inner(self) -> T` guarantees at compile time that nobody uses the wrapper afterwards; a consuming builder (`fn with_port(mut self, p: u16) -> Self`) means a stale builder value cannot be reused by mistake. In Java or C# the same intent is a runtime convention: call `close()`, then any later call throws. Rust also gets the exclusivity rule for free — you cannot hold a `&mut self` borrow and call another mutating method through a second alias, which prevents a whole class of iterator-invalidation and re-entrancy bugs that C++ leaves to discipline. ## Python: methods are just functions In Python `def m(self)` is a plain function stored on the class. Attribute access on an instance runs the descriptor protocol: functions are descriptors, so `obj.m` returns a *bound method* — a first-class object capturing the receiver, which you can store in a list, pass as a callback, or compare. `Cls.m(obj)` is equally legal. This uniformity is the reason decorators, mixins, monkeypatching and `functools.partial` all compose with methods without special rules. C++ pays for its implicit `this` here: a pointer-to-member function is not a callable on its own; you must supply the object at the call site or wrap it in a lambda or `std::bind`. The Python price is that `self` is convention only — misnaming or omitting it fails at call time, not at definition, and every method carries a visible parameter that means the same thing everywhere. ## C++ reaches the same place from another direction C++ cannot vary an implicit `this` by declaration, so it qualifies the *function* instead: `const`, `&` and `&&` ref-qualifiers on a member function restrict which receivers may call it, letting a type offer a cheap move-out overload only on temporaries. C++23's 'deducing this' finally allows writing the object parameter explicitly, mostly so that a single template can serve const and non-const cases and so that recursive lambdas become expressible. The convergence is the point: the language wanted to talk about the receiver's mode and had to invent syntax for it. ## The design lesson An object's method is a relation between a receiver and arguments. Hiding the receiver makes the common case terse and makes it impossible to say interesting things about it. Exposing it lets a language put copy-versus-alias (Go), borrow-versus-consume (Rust) and function-versus-method (Python) into the type system. If you are designing an API that must be bound into several languages, this is why 'the object becomes unusable after this call' is trivially enforceable in one target and only documentable in another.

  • Why does a Go for-range loop over a slice of structs fail to mutate the elements when you call a method on the loop variable?
    The loop variable is a copy of the element, so a pointer-receiver method called on it takes the address of the copy and mutates that. Nothing writes back into the slice. The fix is to index the slice directly (s[i].Inc()) or to hold pointers in the slice.
  • Java has no consuming receiver. How do libraries approximate 'this object is dead after the call'?
    With runtime state: a closed flag checked by every method, throwing IllegalStateException, plus try-with-resources so the compiler at least enforces the call. Some APIs return a different type from the terminal operation so continuing is awkward, but nothing prevents reusing the old reference. Rust turns the same intent into a compile error via a by-value self.

saying these in an interview costs you the question

  • Calling explicit self merely verbose boilerplate, missing that it carries mode information a hidden this cannot.
  • Believing a Go value receiver can mutate the original, or that T and *T have the same method set.
  • Thinking Rust's self, &self and &mut self are stylistic rather than ownership annotations.
  • Assuming a C++ pointer-to-member behaves like a Python bound method — it carries no receiver.
  • Saying only dynamic languages can make methods first class; Rust and C++ both offer receiver-aware forms, just not as closures.

context