Разберём один из самых частых источников ошибок при работе с интерфейсами в 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.
Пишите в комментариях, с какими ещё сюрпризами интерфейсов вы сталкивались
🫰