skip to content

In Go, can you declare methods on a non-struct type such as type Money int64?

level: middleimportance: nice to knowfreq 40%

answer

  1. methods are not a struct privilege
  2. they attach to a defined type
  3. type Money int64 can carry behaviour
  4. same package, and not a pointer or interface
  5. wrapping time.Duration gives you none of its methods

basics

~20 s

Yes. Methods attach to any defined type declared in the same package — an integer, a string, a slice, a map or a function type — not only to structs. The receiver base type may not be a pointer type or an interface type.

solid answer

~40 s

Methods in Go belong to a *defined type*, and a defined type does not have to be a struct. `type Money int64` can have `func (m Money) String() string`, and `type Statuses []string` can have `func (s Statuses) Has(name string) bool`. Two rules constrain it. First, the receiver base type must be declared in the same package as the method, which is why you cannot hang a method on `int` or on a type imported from elsewhere — you declare your own type first. Second, the base type may itself be neither a pointer type nor an interface type. Everything else carries over unchanged: `func (m *Money) Add(n Money)` mutates through the pointer, `func (m Money) Add(n Money)` mutates a copy and loses it.

code

go · 21 lines
go
// Money holds an amount in minor units (cents); assume non-negative here.
type Money int64

func (m Money) String() string {
	return fmt.Sprintf("%d.%02d", int64(m)/100, int64(m)%100)
}

func (m Money) Plus(n Money) Money {
	return m + n
}

type Statuses []string

func (s Statuses) Has(name string) bool {
	for _, v := range s {
		if v == name {
			return true
		}
	}
	return false
}

go deeper

for a junior

Know that a method does not need a struct: a type declared as an integer, string or slice can have methods too, provided you declared that type yourself.

for a middle

State both constraints — the receiver base type must be declared in this package, and it may not be a pointer or interface type — and show the pointer form on a scalar, where the dereference becomes explicit.

for a senior

Argue when a defined scalar type is worth its conversion friction: money in minor units, identifiers that are all integers, units that get mixed up. Note that such a type does not inherit the underlying type's methods.

for a principal

Own the call about how far unit types spread through a codebase, weighing compiler-enforced correctness against the boundary conversions they impose on serialisation, storage and other teams' call sites.

## Methods are not a struct feature Engineers arriving from class-based languages expect methods to require something class-shaped. In Go a method is attached to a **defined type**, and any defined type will do: ```go type Money int64 // amounts in minor units, e.g. cents type Statuses []string type Handler func(string) error type Index map[string]int64 ``` Each of these can carry methods. This is how a domain concept that is 'really just a number' still gets behaviour — validation, formatting, arithmetic that respects the unit — without wrapping it in a one-field struct. ## The two rules **The receiver base type must be defined in the same package as the method.** You cannot write `func (d time.Duration) Half() time.Duration` in your own package, and you cannot write methods on the predeclared `int`. What you do instead is declare your own type: `type Timeout time.Duration`, then give `Timeout` methods. Note that a defined type declared this way starts with an empty method set of its own — it does **not** inherit methods from the type it was defined from, so `Timeout` gets none of `time.Duration`'s. That is the price of the wrapper, and it is deliberate: the new type is a new type, not a subtype. **The receiver base type must not be a pointer type or an interface type.** `type P *Order` cannot receive methods, and neither can an interface type — the whole point of an interface is that its method set is declared, not implemented. ## Everything about receivers carries over The value-versus-pointer rule is identical for a defined integer type: ```go func (m Money) Add(n Money) { m += n } // writes to the copy; lost func (m *Money) Add(n Money) { *m += n } // writes through; visible ``` With a non-struct base type the pointer form needs an explicit dereference — `*m += n` — because there is no field selector doing it implicitly, and that visual difference is a useful reminder of what the pointer receiver is actually doing in the struct case too. Most defined-scalar types end up with value receivers, and that is usually right: they are small, they are conceptually values, and the operations you want are 'produce a new amount' rather than 'edit this amount in place'. `func (m Money) Plus(n Money) Money { return m + n }` is more idiomatic than an in-place `Add`. ## Why it is worth doing The payoff is that the compiler starts enforcing your unit. If `TotalMinor` is an `int64`, nothing stops a caller adding a value that was actually in major units, or in another currency. If it is a `Money`, an accidental `int64` mixes in only with an explicit conversion, which is exactly the moment a reviewer wants to see. Attaching `String() string` to the type gives it a printed form everywhere `fmt` formats it, so the unit shows up in logs without every call site remembering to divide by a hundred. The cost is friction at boundaries — JSON decoding, database scanning and arithmetic with plain integers all need conversions or extra methods — so this is a judgment call per type, not a blanket rule. A good middle ground is to define the type where the unit is genuinely error-prone (money, durations you did not get from `time`, identifiers of different entities that are all integers) and leave general-purpose counters alone. ## A note on generic types A defined generic type carries its type parameters in the receiver: `func (s *Stack[T]) Push(v T)`. That has been the shape since generics arrived. Separately, Go 1.27 allows a *method* to declare its own type parameters; such a generic method cannot implement an interface method, so it is a tool for helper methods rather than for widening an abstraction.

  • Why can't you declare a method whose receiver type is time.Duration?
    Because the receiver base type must be declared in the same package as the method, and `time.Duration` belongs to `time`. Declare your own defined type — `type Timeout time.Duration` — and give that methods. Be aware the new type starts with no methods of its own: it does not inherit `time.Duration`'s.
  • Does the value-versus-pointer receiver rule change for a defined integer type?
    No. `func (m Money) Add(n Money) { m += n }` mutates a copy and the write is lost; `func (m *Money) Add(n Money) { *m += n }` writes through to the caller's variable. The only visible difference is that the pointer form needs an explicit `*m` because there is no field selector to hide the dereference.
  • What does defining a Money type actually buy over using int64 directly?
    The compiler starts enforcing the unit: a plain `int64` no longer mixes in without an explicit conversion, which surfaces at review time. Attaching a `String` method also gives the amount a correct printed form wherever `fmt` formats it. The cost is conversions at JSON, database and arithmetic boundaries.

saying these in an interview costs you the question

  • Says only structs can have methods in Go
  • Tries to declare a method on int or on an imported type
  • Thinks a defined type inherits the methods of its underlying type
  • Believes a pointer type or an interface type can be a receiver base type
  • Wraps everything in a one-field struct to get methods