Value и pointer receivers
Методы и receivers
В Go метод — это функция с дополнительным аргументом перед именем, который называется receiver. Он определяет, на каком типе вызывается метод:
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 метод получает копию значения. Любые изменения внутри метода не затрагивают оригинал:
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 получает адрес значения и может изменить его поля:
func (c *Counter) Increment() {
c.value++ // изменяем оригинал через указатель
}
func main() {
cnt := Counter{}
cnt.Increment()
cnt.Increment()
fmt.Println(cnt.Get()) // 2 — работает!
}Go автоматически берёт адрес при вызове pointer receiver метода на адресуемом значении:
cnt.Increment() // Go автоматически делает (&cnt).Increment()Когда что выбирать
Pointer receiver нужен когда:
// 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 подходит когда:
// 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 — разные:
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:
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 в одном типе без причины.
// Плохо: мешанина 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 нельзя вызвать на неадресуемых значениях:
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 проверка интерфейса
Полезная идиома: убедись на этапе компиляции, что тип реализует интерфейс:
type MyWriter struct{}
func (w *MyWriter) Write(p []byte) (int, error) {
return len(p), nil
}
// Если *MyWriter не реализует io.Writer — ошибка компиляции
var _ io.Writer = (*MyWriter)(nil)Размещай такие строки рядом с объявлением типа — они служат документацией и страховкой от регрессий.