Модуль
1Указатели: основы2Value и pointer receivers← вы здесь3Устройство типов в памяти4Стек, куча и escape analysis
Урок 2~12 минут

Value и pointer receivers

Методы и receivers

В Go метод — это функция с дополнительным аргументом перед именем, который называется receiver. Он определяет, на каком типе вызывается метод:

go
type Circle struct {
    Radius float64
}
 
// Value receiver — метод получает копию Circle
func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}
 
// Pointer receiver — метод получает указатель на Circle
func (c *Circle) Scale(factor float64) {
    c.Radius *= factor  // изменяет оригинал
}

Выбор между T и *T — один из самых частых вопросов в Go.


Value receiver: работает с копией

При value receiver метод получает копию значения. Любые изменения внутри метода не затрагивают оригинал:

go
type Counter struct {
    value int
}
 
func (c Counter) Increment() {
    c.value++  // меняем копию, оригинал не трогаем
}
 
func (c Counter) Get() int {
    return c.value
}
 
func main() {
    cnt := Counter{}
    cnt.Increment()
    cnt.Increment()
    fmt.Println(cnt.Get()) // 0 — изменения потеряны!
}

Value receiver — это явный сигнал: «метод не изменяет состояние».


Pointer receiver: работает с оригиналом

Pointer receiver получает адрес значения и может изменить его поля:

go
func (c *Counter) Increment() {
    c.value++  // изменяем оригинал через указатель
}
 
func main() {
    cnt := Counter{}
    cnt.Increment()
    cnt.Increment()
    fmt.Println(cnt.Get()) // 2 — работает!
}

Go автоматически берёт адрес при вызове pointer receiver метода на адресуемом значении:

go
cnt.Increment()   // Go автоматически делает (&cnt).Increment()

Когда что выбирать

Pointer receiver нужен когда:

go
// 1. Метод изменяет поля структуры
func (u *User) SetName(name string) {
    u.Name = name
}
 
// 2. Структура тяжёлая — дорого копировать
type BigMatrix struct {
    data [1000][1000]float64
}
func (m *BigMatrix) Transpose() { /* ... */ }
 
// 3. Нужна семантика ссылки (несколько переменных на один объект)
func (c *Cache) Set(key string, val any) {
    c.store[key] = val
}

Value receiver подходит когда:

go
// 1. Метод только читает, тип маленький
type Point struct{ X, Y float64 }
 
func (p Point) Distance() float64 {
    return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
 
// 2. Тип — это неизменяемое значение (как time.Time)
func (t Time) Format(layout string) string { /* ... */ }
 
// 3. Методы встроенных типов-алиасов
type Celsius float64
func (c Celsius) ToFahrenheit() float64 {
    return float64(c)*9/5 + 32
}

Влияние на интерфейсы

Здесь кроется важная разница. Набор методов T и *T — разные:

go
type Stringer interface {
    String() string
}
 
type MyType struct{ val int }
 
func (m MyType) String() string {
    return fmt.Sprintf("val=%d", m.val)
}
 
// Оба работают:
var s1 Stringer = MyType{42}   // ok
var s2 Stringer = &MyType{42}  // ok — *T включает методы T

Но если метод объявлен с pointer receiver:

go
func (m *MyType) String() string {
    return fmt.Sprintf("val=%d", m.val)
}
 
var s1 Stringer = MyType{42}   // ОШИБКА компиляции!
var s2 Stringer = &MyType{42}  // ok

Правило: *T включает все методы T плюс свои. T включает только свои методы с value receiver. Поэтому если нужно передавать тип в интерфейс — используй pointer receiver и передавай &T.


Правило consistency

Go рекомендует: не смешивай value и pointer receivers в одном типе без причины.

go
// Плохо: мешанина receivers
type Server struct { /* ... */ }
 
func (s Server) Name() string { return s.name }   // value
func (s *Server) Start() error { /* ... */ }       // pointer
func (s Server) Port() int { return s.port }       // value
func (s *Server) Stop() { /* ... */ }              // pointer
 
// Хорошо: все pointer receivers (есть мутирующие методы — значит все pointer)
func (s *Server) Name() string { return s.name }
func (s *Server) Start() error { /* ... */ }
func (s *Server) Port() int { return s.port }
func (s *Server) Stop() { /* ... */ }

Причина: если хоть один метод использует pointer receiver — тип де-факто требует работы через указатель. Тогда все методы должны быть консистентны.


Неадресуемые значения

Pointer receiver нельзя вызвать на неадресуемых значениях:

go
type T struct{ val int }
func (t *T) Double() { t.val *= 2 }
 
// Адресуемые — Go автоматически берёт &:
t := T{5}
t.Double()     // ok: (&t).Double()
 
// Неадресуемые — ошибка компиляции:
T{5}.Double()                          // cannot take address of T literal
getT().Double()                        // cannot take address of function return
 
// Элемент map — тоже неадресуемый:
m := map[string]T{"a": {5}}
m["a"].Double()                        // cannot take address of map element
// Решение:
v := m["a"]
v.Double()
m["a"] = v

Это следствие того, что элементы map могут перемещаться при rehashing — брать их адрес небезопасно.


Compile-time проверка интерфейса

Полезная идиома: убедись на этапе компиляции, что тип реализует интерфейс:

go
type MyWriter struct{}
 
func (w *MyWriter) Write(p []byte) (int, error) {
    return len(p), nil
}
 
// Если *MyWriter не реализует io.Writer — ошибка компиляции
var _ io.Writer = (*MyWriter)(nil)

Размещай такие строки рядом с объявлением типа — они служат документацией и страховкой от регрессий.

Pointer receiver (*T) нужен когда метод изменяет поля или структура тяжёлая. Value receiver (T) — когда метод только читает и тип маленький. Главное: не мешай оба в одном типе без причины.
Анимация вызова метода
Counter (original)
0
Go code
type Counter struct { value int } // Value receiver — gets a COPY func (c Counter) Increment() { c.value++ // only the copy changes } func main() { c := Counter{value: 0} c.Increment() fmt.Println(c.value) // 0 — original unchanged }
🎯
Миссия 1 из 4
Метод с каким receiver может изменить поля структуры?