Интерфейсы в Go

Теория: Встраивание интерфейсов

Когда приложение растёт, количество интерфейсов увеличивается. Сначала кажется, что всё просто: есть интерфейс для чтения, для записи, для закрытия ресурсов. Но со временем появляются функции, которые требуют сразу несколько таких интерфейсов.

Например, функция, которая копирует данные из одного места в другое, должна принимать два интерфейса — для чтения и для записи:

func copyData(reader io.Reader, writer io.Writer) error {
	buf := make([]byte, 1024)

	n, err := reader.Read(buf)
	if err != nil {
		return err
	}

	_, err = writer.Write(buf[:n])
	return err
}

С первого взгляда это нормально, но когда таких параметров становится много, функции превращаются в длинные списки аргументов. Так легко перепутать порядок, забыть нужный интерфейс или передать неправильный объект. Например, если после записи надо ещё и закрыть ресурс, функция примет уже три параметра:

func writeAndClose(writer io.Writer, closer io.Closer, data []byte) error {
	if _, err := writer.Write(data); err != nil {
		return err
	}
	return closer.Close()
}

С ростом проекта таких функций становится всё больше. Поддерживать код с длинными списками интерфейсов неудобно, появляются ошибки.

Чтобы упростить код, несколько интерфейсов объединяют в один новый, который включает методы всех вложенных интерфейсов. Этот приём называется композицией интерфейсов.

Композиция позволяет создавать более сложные интерфейсы на основе простых, не дублируя методы и не усложняя структуру кода. В Go это стандартный и широко используемый подход. Например, стандартная библиотека Go содержит интерфейсы io.Reader и io.Writer. Их можно объединить в интерфейс io.ReadWriter:

type ReadWriter interface {
	io.Reader
	io.Writer
}

Теперь функцию копирования можно переписать так:

func copyData(rw io.ReadWriter) error {
	buf := make([]byte, 1024)

	n, err := rw.Read(buf)
	if err != nil {
		return err
	}

	_, err = rw.Write(buf[:n])
	return err
}

Вместо двух параметров теперь один, и работать с функцией проще.

Другой пример — объединение интерфейсов для записи и закрытия ресурса:

type WriteCloser interface {
	io.Writer
	io.Closer
}

func saveData(wc WriteCloser, data []byte) error {
	if _, err := wc.Write(data); err != nil {
		return err
	}
	return wc.Close()
}

Так функции становятся короче, понятнее и удобнее в использовании. Передавать один интерфейс вместо двух или трёх — меньше шансов ошибиться.

Этот способ объединения интерфейсов широко применяется в Go. Он помогает структурировать код, сокращать количество параметров функций и снижать вероятность ошибок. Особенно полезен он в работе с файлами, сетевыми соединениями и тестированием, где часто нужны похожие наборы методов.

Объединение интерфейсов не усложняет реализацию, а наоборот — упрощает использование и поддержку кода.

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845