Объявление методов у типа T или *T
В Go для любого типа T существует тип *T, который является результатом выражения, принимающего адрес переменной типа T (Мы говорим T, но это просто держатель для типа, который вы объявляете). Например:
1
2
3
4
5
type T struct {
a int; b bool
}
var t T // у t тип T
var p = &t // у p тип *T
Эти два типа, T и *T, различны, но *T не может быть заменен на T. (Это правило рекурсивно: взяв адрес переменной типа *T, вы получите результат типа **T)
Вы можете объявить метод у любого типа, который принадлежит вам; то есть у типа, который вы объявили в своем пакете.
(Именно поэтому никто не может объявлять методы на примитивных типах вроде int)
Отсюда следует, что вы можете объявлять методы как у объявленного вами типа T, так и у соответствующем ему производном типе-указателе *T.
По-другому можно сказать, что методы у типа объявляются для получения копии значения их получателя или указателя на значение их получателя. (Методы в Go - это просто синтаксический сахар для функции, которая передает получателя в качестве первого формального параметра)
Возникает вопрос, какую форму лучше использовать?
Очевидно, что если ваш метод мутирует получателя, то он должен быть объявлен у типа *T. Однако если метод не мутирует получателя, безопасно ли объявлять его вместо этого у Т?
Оказывается, случаи, когда это безопасно, очень ограничены. Например, хорошо известно, что нельзя копировать значение sync.Mutex, так как это нарушает инварианты мьютекса.
Поскольку мьютексы управляют доступом к другим вещам, их часто оборачивают в структуру со значением, которое они контролируют:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package counter
import "sync"
type Val struct {
mu sync.Mutex
val int
}
func (v *Val) Get() int {
v.mu.Lock()
defer v.mu.Unlock()
return v.val
}
func (v *Val) Add(n int) {
v.mu.Lock()
defer v.mu.Unlock()
v.val += n
}
Большинство программистов на Go знают, что ошибкой будет забыть объявить методы Get или Add у получателя указателя Val. Однако любой тип, который встраивает Val, чтобы использовать его нулевое значение, также должен объявлять методы только у своего получателе-указателе (Т), иначе он может случайно скопировать содержимое значений своего встроенного типа.
1
2
3
4
5
6
7
type Stats struct {
a, b, c counter.Val
}
func (s Stats) Sum() int {
return s.a.Get() + s.b.Get() + s.c.Get() // whoops
}
Аналогичный подводный камень может возникнуть с типами, которые хранят срезы значений, и, конечно, существует возможность непреднамеренной гонки данных.
Короче говоря, я считаю, что лучше объявлять методы на *T, если у вас нет веских причин поступать иначе.
Данная статья является вольным переводом статьи Should methods be declared on T or *T