Разберём один из самых частых источников ошибок при работе с интерфейсами в Go:

type Storage interface {
    Save(data []byte) error
}

type FileStorage struct {
    path string
}

func (fs *FileStorage) Save(data []byte) error {
    return os.WriteFile(fs.path, data, 0644)
}

func main() {
    // Работает
    var s1 Storage = &FileStorage{path: "data.txt"}
    
    // Не компилируется
    // "Cannot use FileStorage{path: "data. txt"} (type FileStorage) as the type Storage"
    // "Type does not implement Storage as the Save method has a pointer receiver"
    var s2 Storage = FileStorage{path: "data.txt"}
}

Почему так? Ведь метод Save у нас есть. В чём подвох?

Разбор

Дело в том, что метод Save объявлен на указателе (*FileStorage), а не на значении. И тут начинается самое интересное.

Когда вы объявляете метод на указателе, Go позволяет вам вызывать его и для указателя, и для значения:

storage := FileStorage{path: "data.txt"}
storage.Save([]byte("hello"))  // Работает

ptr := &storage
ptr.Save([]byte("world"))      // Тоже работает

В первом случае Go неявно берет адрес значения storage, чтобы вызвать метод. Но с интерфейсами такое поведение не работает. Если метод объявлен на указателе, то только указатель может реализовывать интерфейс. 

Это сделано намеренно. Представьте, что у вас временное значение:

func getStorage() FileStorage {
    return FileStorage{path: "temp.txt"}
}

var storage Storage = getStorage() // Не скомпилируется

Если бы Go автоматически брал адрес значения при присваивании интерфейсу, то вы бы получили указатель на временное значение, которое исчезнет после возврата из функции.

А вот обратная ситуация работает без проблем:

type Reader interface {
    Read() string
}

type MemoryReader struct {
    data string
}

// Метод на значении
func (mr MemoryReader) Read() string {
    return mr.data
}

func main() {
    // Оба варианта работают
    var r1 Reader = MemoryReader{data: "hello"}
    var r2 Reader = &MemoryReader{data: "world"}
}

Когда метод объявлен на значении, его можно вызвать и для указателя – Go просто разыменует указатель автоматически.

Так какой же receiver выбрать?

Используйте pointer receiver, если:

  • Метод должен изменять состояние структуры
  • Структура большая и копировать её накладно
  • У других методов этого типа уже есть pointer receiver (для консистентности!)
  • Когда тип содержит sync.Mutex или другие поля, которые нельзя копировать

В остальных случаях используйте value receiver.