Осторожно, ловушка
Проблема
Go замечательный, простой и понятный язык. Почти все места и конструкции прозрачны как воды Байкала. Даже указатели не пугают, в отличии от C/C++. Но есть парочка мест, которые не так очевидны, как ожидалось.
Один из таких непонятных моментов - это использование наборов методов для указателей и неуказателей в связке с интерфейсами. Сначала я опишу саму проблему, а потом приведу перевод документации про наборы методов(method sets), чтобы было понятно откуда ноги растут.
И так, начнем с простого примера. У нас есть некоторый интерфейс:
type phoner interface {
call()
}
И мы хотим реализовать этот интерфейс. Для этого создадим свой тип на базе структуры:
type phone struct {
}
func (p *phone) call() {
fmt.Println("call")
}
Тут видно, что приемник у метода call
это переменная типа *phone
, то есть это указатель.
Так как phone
реализовывает интерфейс phoner
, то теперь мы можем использовать интерфейс как тип для наших переменных:
var p phoner = &phone{}
p.call()
В этом коде нет ничего подозрительного. Метод call
использует приемник с типом указателя, а в переменную p
мы именно указатель и записываем.
Что будет, если мы вместо &phone{}
будем использовать просто phone{}
?
var p phoner = phone{}
p.call()
В таком случае, ваша программа даже не соберется, потому что типы не совпадают. Интерфейс phoner
реализован для типа *phone
, но не для типа phone
. Что, в принципе, понятно: указатель на значение и само значение это два совершенно разных типа.
Хорошо, давайте теперь реализуем интерфейс не для указателя, а для значения:
type phone struct {
}
func (p phone) call() {
fmt.Println("call")
}
И будем его использовать
var p phoner = phone{}
p.call()
И тут мы подкрались с самому непонятному месту. Вот так тоже работает:
var p phoner = &phone{}
p.call()
А чтобы разобраться почему все именно так, нужно чуть более внимательно почитать документацию.
Наборы методов(Method Sets)
Далее я перевел небольшой отрывок документации, в котором описаны наборы методов и правила их использования.
Спецификация
Наборы методов для определенного типа имеют важное значение в Go. Набор методов определяет, какой интерфейс можно использовать для значения определенного типа.
В спецификации по языку есть два важных момента, описывающих наборы методов:
Наборы методов: Тип может иметь ассоциированные методы. Набор методов интерфейсного типа это и есть его интерфейс. Набор методов любого другого именного типа type T
состоит из всех методов с указанием приемника типа T
. Набор методов для типа type *T
это набор всех методов с приемником типа *T
и/или T
(то есть, он включает набор методов T
). Любой другой тип имеет пустой набор методов. В рамках одного набора методы должны иметь уникальное название.
Вызовы: Вызов метода x.m()
является валидным если набор методов типа x
содержит m
и список аргументов соответствует списку параметров метода m
. Если x
адресуемая переменная и набор методов для &x
содержит m
, то мы можем вызвать метод как x.m()
, что, по сути, является сокращением для (&x).m()
.
Использование
Наверняка, вы будете использовать наборы методов ежедневно. Вы можете использовать методы при работе с переменными, элементами слайса, элементами map и интерфейсами.
Переменные
Если у вас есть переменная определенного типа, который ассоциирован с набором методов, то вы можете вызывать практически любые методы. Если учитывать правила, описанные в спецификации, то можно написать такой код:
type List []int
func (l List) Len() int { return len(l) }
func (l *List) Append(val int) { *l = append(*l, val) }
func main() {
// A bare value
var lst List
lst.Append(1)
fmt.Printf("%v (len: %d)\n", lst, lst.Len())
// A pointer value
plst := new(List)
plst.Append(2)
fmt.Printf("%v (len: %d)\n", plst, plst.Len())
}
Обратите внимание, что мы вызываем оба метода(и с приемником указателем, и с приемником значением) как для указателя, так и для значения. Чтобы лучше разобраться как это работает, давайте составим табличку методов:
List
- Len() int
*List
- Len() int
- Append(int)
Тут видно, что в наборе методов для типа List
не содержится метод Append(int)
, тем не менее, вы можете его вызвать и программа будет работать корректно. Это объясняется вторым правилом из спецификации и неявным преобразованием вида:
lst.Append(1)
(&lst).Append(1)
В таком случае, у выражения (&lst)
будет тип *List
который позволит вызвать метод Append(int)
. Чтобы лучше запомнить эти правила, стоит рассмотреть методы, использующие указатель или значение как приемник, отдельно от набора методов. Любой метод, в котором приемник это указатель, можно вызывать относительно любого указателя, или значения, указатель на которое мы можем получить(как это происходить в примере выше). Любой метод, в котором приемник это значение, может быть вызван для значения или для указателя, если его можно разименовать(то есть, для любого указателя).
Элементы слайса
Тут все как и с переменными. Так как элементы слайса это, по сути, указатели, то тип приемника не важен. Можно вызывать все методы, в которых приемник это указатель, или значение.
Элементы map
Элементы map не адресуемые. Таким образом, код ниже не заработает:
lists := map[string]List{}
lists["primes"].Append(7)
// не может быть переписано как (&lists["primes"]).Append(7)
Но если мы сами будем использовать указатели, то все будет хорошо работать:
lists := map[string]*List{}
lists["primes"] = new(List)
lists["primes"].Append(7)
count := lists["primes"].Len()
// может быть переписано как (*lists["primes"]).Len()
Таким образом, если элемент map это указатель, то могут быть вызваны оба типа методов. А если элемент map простое значение, то могут быть вызваны только методы, у которых приемник значение.
Интерфейсы
Конкретное значение, которое хранится в переменной с типом интерфейса, всегда неадресуемое, аналогично элементу map. Таким образом, когда вы вызываете метод у переменной с интерфейсным типом, то этот метод должен иметь соответствующий тип приемника или значение в переменной должно быть получено из соответствующего типа: для методов с приемником указателем это должен быть указатель, для методов с приемником значением это должно быть значение. При этом, методы с приемниками указателями могут вызываться для переменных интерфейсного типа, если их значения указатель, так как этот указатель можно разименовать. Но методы с приемниками значениями нельзя вызвать, так как значения, сохраненные внутри интерфейсной переменной, не адресуемые. При приведении типа к интерфейсу компилятор следит за тем, чтобы все методы, объявленные в интерфейсе, можно было вызвать и если это не так, то сообщает о ошибке компиляции. Расширим предыдущий пример и покажем валидные и невалидные использования интерфейсов:
type List []int
func (l List) Len() int { return len(l) }
func (l *List) Append(val int) { *l = append(*l, val) }
type Appender interface {
Append(int)
}
func CountInto(a Appender, start, end int) {
for i := start; i <= end; i++ {
a.Append(i)
}
}
type Lener interface {
Len() int
}
func LongEnough(l Lener) bool {
return l.Len()*10 > 42
}
func main() {
// Просто значение
var lst List
CountInto(lst, 1, 10) // Невалидно: Append ожидает указатель в приемнике
if LongEnough(lst) { // Валидно: oдинаковые типы
fmt.Printf(" - lst is long enough")
}
// Указатель на значение
plst := new(List)
CountInto(plst, 1, 10) // Валидно: одинаковые типы
if LongEnough(plst) { // Валидно: *List можно разименовать
fmt.Printf(" - plst is long enough")
}
}