skip to content

Does type MyMutex sync.Mutex give MyMutex a Lock method, and why?

level: middleimportance: should knowfreq 45%

answer

  1. methods belong to the name, not the shape
  2. the fields come across, what about behaviour?
  3. an alias keeps them, a definition does not
  4. embedding is the way back

basics

~20 s

No. A type definition copies the underlying type - here sync.Mutex's struct fields - but never the methods declared on the source type, so MyMutex starts with an empty method set. An alias or embedding keeps the methods.

solid answer

~50 s

No. `type MyMutex sync.Mutex` gives `MyMutex` the same underlying struct — same fields, same size — but methods are declared on a *named type*, not on a shape, so `Lock` and `Unlock` stay with `sync.Mutex`. `MyMutex` begins with an empty method set, and you cannot add those methods yourself either, since the state fields are unexported. Two ways to keep the behaviour: an alias, `type MyMutex = sync.Mutex`, keeps the type identical and therefore keeps every method; or embed it, `type Counter struct { mu sync.Mutex; n int }`, and call `c.mu.Lock()` — embedding anonymously would promote `Lock` and `Unlock` into your exported API. Two subtleties: if the underlying type is an *interface*, as in `type MyReader io.Reader`, the method set does come along, because an interface type's method set belongs to the type itself; and defining from a struct with embedded fields keeps the *promoted* methods.

code

go · 14 lines
go
type MyMutex sync.Mutex // same fields as sync.Mutex, none of its methods

type Base struct{ sync.Mutex }
type Derived Base // Lock is promoted from the embedded field, so it survives

func demo() {
	var m MyMutex
	_ = m
	// m.Lock() // compile error: m.Lock undefined

	var d Derived
	d.Lock()
	d.Unlock()
}

go deeper

for a junior

Remember the headline: defining a type from another type keeps the data layout and drops the methods. Know that embedding is how you reuse behaviour in Go.

for a middle

Explain that a method is bound to the named type in its receiver, so a new named type has an empty method set, and name the alias and embedding alternatives precisely.

for a senior

Diagnose the real-world versions of this — a redefined time.Time or bytes.Buffer that compiles but does nothing — and weigh embedding against a named field for what it exposes in a public API.

for a principal

Set the guidance for the codebase on when a wrapper type should embed, forward explicitly, or alias, given that embedding permanently widens the exported surface other teams will call.

## Methods belong to a name, not to a shape When you write `type MyMutex sync.Mutex`, you are saying: create a new named type whose **underlying type** is whatever `sync.Mutex`'s underlying type is — a struct with those fields, that layout, that size. You are not saying: copy `sync.Mutex`. A method in Go is declared with a receiver, and it is bound to the *named type* in that receiver: `func (m *Mutex) Lock()` in package `sync` attaches `Lock` to `sync.Mutex`, and to nothing else. Your `MyMutex` is a different named type, so it has an empty method set. `var m MyMutex; m.Lock()` does not compile. This is the deliberate design. Go has no inheritance; a type definition is not subclassing. It gives you a fresh type that reuses a representation, which is precisely what you want when the reason you are defining the type is to give it *your own* methods. ### Why `MyMutex` in particular is a trap With `sync.Mutex` the loss is total: the struct's fields are unexported, so you cannot even reimplement `Lock` yourself outside package `sync`. You are left with a struct-shaped value that can be copied around and does nothing. The same shape of mistake shows up with `type MyTime time.Time` (you lose `Format`, `Add`, `Before`, and `time.Time`'s fields are unexported) and `type MyBuffer bytes.Buffer` (you lose `Write`, `String`, `Len`). ### The three ways to keep behaviour 1. **Alias** — `type MyMutex = sync.Mutex`. There is no new type, so the method set is `sync.Mutex`'s, complete. Use this when you genuinely want the same type under another name, for example while a type moves between packages. 2. **Embed** — `type Counter struct { sync.Mutex; n int }`. `Counter` is a new struct type whose method set includes the *promoted* methods of its embedded field, so `c.Lock()` works and forwards to the embedded mutex. This is Go's composition mechanism, and it is the honest answer to "I want that behaviour plus some of my own". Be aware it puts `Lock` and `Unlock` in `Counter`'s exported API, letting callers lock your internals; a named field, `mu sync.Mutex`, keeps the lock private and is usually the better default. 3. **Wrap and forward** — a named field plus explicit methods you write, when you want to control exactly which operations you expose. ### The two cases where methods *do* come along **Underlying type is an interface.** `type MyReader io.Reader` gives `MyReader` the same method set as `io.Reader`, because an interface type's method set is part of the type itself rather than a set of separately declared methods. A `MyReader` value has `Read`, and any `io.Reader` may be stored in one. **Underlying struct has embedded fields.** Given `type Base struct { sync.Mutex }` and `type Derived Base`, `Derived`'s underlying type is still `struct { sync.Mutex }`, and promotion is derived from the struct's fields — so `Derived` has `Lock` and `Unlock` promoted, even though any methods declared *on* `Base` itself were dropped. This surprises people the first time, and it is a nice test of whether someone really understands the rule: the definition drops the source type's own methods and keeps whatever the underlying type's structure implies. ### Why define a type at all, then? Because the operations you actually need usually come from the underlying type, not from methods. `type IDs []string` still indexes, ranges, slices and appends, because those are properties of the underlying slice type. What the definition buys you is a name you own, and therefore a place to declare `func (ids IDs) Sorted() IDs`, `MarshalJSON`, `String` — behaviour the raw `[]string` could never have. ### Checking it quickly If you are unsure whether a type has a method, ask the compiler with a one-line assertion such as `var _ fmt.Stringer = MyType{}`, or read `go doc yourpkg.MyType`, which lists exactly the methods that type has. Do not reason from the fact that the source type had them.

  • What about type MyReader io.Reader - does a MyReader value have Read?
    Yes. The underlying type is an interface, and an interface type's method set is part of the type itself rather than a collection of separately declared methods, so `MyReader` has `Read`. That makes defining a type over an interface a cheap way to give an interface a package-local name, though it is often clearer to redeclare the interface.
  • If methods are dropped, why is type MySlice []int useful at all?
    Because the operations come from the underlying type, not from methods: indexing, ranging, slicing, `len`, `cap` and `append` all still work. What you gain is a type you own, so you can declare `Sum`, `String` or `MarshalJSON` on it — which is impossible on a raw `[]int`.
  • Why is embedding sync.Mutex anonymously in an exported struct usually a bad idea?
    Promotion puts `Lock` and `Unlock` into the type's public API, so callers can lock and unlock your internals and you can no longer change the locking strategy without breaking them. A named unexported field, `mu sync.Mutex`, keeps the mechanism private while giving you exactly the same behaviour internally.

saying these in an interview costs you the question

  • Says the new type inherits the source type's methods
  • Describes a type definition as subclassing or inheritance
  • Thinks embedding and defining a type do the same thing
  • Expects sync.Mutex's Lock to work on a redefined copy
  • Cannot say what a definition does copy from the source type