Пост

Объявление методов у типа 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

Авторский пост защищен лицензией CC BY 4.0 .

Популярные теги