Шаг 90.
Язык программирования Go.
Использование интерфейсов (окончание)

    На этом шаге продолжим рассматривать использование интерфейсов в Go.

    Помимо собственных пользовательских интерфейсов, значения могут также включать реализацию любых других интерфейсов, в том числе определяемых в стандартной библиотеке, как в случае с типом ПараСтрок, реализующим метод String() интерфейса fmt.Stringer. Другой пример – интерфейс io.Reader, определяющий единственный метод с сигнатурой Read([]byte) (int, error), который при вызове записывает байты данных из значения в указанный срез типа []byte. Запись носит деструктивный характер, в том смысле что каждый записанный байт удаляется из значения, относительно которого был выполнен вызов.

func (пара *ПараСтрок) Read(данные []byte) (n int, err error) {
    if пара.первая == "" && пара.вторая == "" {
        return 0, io.EOF
    }
    if пара.первая != "" {
        n = copy(данные, пара.первая)
        пара.первая = пара.первая[n:]
    }
    if n < len(данные) && пара.вторая != "" {
        m := copy(данные[n:], пара.вторая)
        пара.вторая = пара.вторая[m:]
        n += m
    }
    return n, nil
}

    Благодаря добавлению этого метода тип ПараСтрок обеспечил поддержку интерфейса io.Reader. Поэтому теперь значения типа ПараСтрок (*ПараСтрок) реализуют интерфейсы Exchanger, fmt.Stringer и io.Reader, при этом в программном коде не требуется явно указывать, что *ПараСтрок "реализует" Exchanger или какие-то другие интерфейсы.

    Метод использует встроенную функцию copy(). Эта функция может применяться для копирования данных из одного среза в другой срез того же типа, но здесь используется другая форма копирования – содержимого строки в срез типа []byte. Функция copy() никогда не копирует больше байтов, чем может уместиться в целевом срезе []byte, и возвращает количество скопированных байтов.

    Пользовательский метод ПараСтрок.Read() записывает байты из строки первая (и удаляет, которые были фактически записаны), и затем повторяет то же самое со строкой вторая. Если обе строки окажутся пустыми, метод вернет нуль и значение io.EOF.

    Передача приемника по указателю является совершенной необходимостью, потому что метод Read() изменяет значение, относительно которого вызывается. И вообще, при определении типа приемника предпочтение следует отдавать указателям, за исключением небольших по объему значений, потому что передача указателя дешевле передачи других значений, кроме самых маленьких по объему.

    Создав метод Read(), его можно использовать, как показано ниже.

const size = 16
строка3 := &ПараСтрок{"Гарри", "Поттер"}
строка4 := ПараСтрок{"Рон", "Уизли"}
for _, reader := range []io.Reader{строка3, &строка4} {
    raw, err := ToBytes(reader, size)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Printf("%q\n", raw)
}

    В этом фрагменте создаются два значения, реализующие интерфейс io.Reader. Поскольку реализованный выше метод ПараСтрок.Read() принимает указатель на приемник, требованиям интерфейса io.Reader() удовлетворяет значение типа *ПараСтрок, но не удовлетворяет значение типа ПараСтрок. В первом случае сначало было создано значение типа ПараСтрок, а затем переменной строка3 был присвоен указатель на него, во втором случае переменной строка4 было присвоено само значение типа ПараСтрок, поэтому в срезе []io.Reader пришлось использовать его адрес.

    После создания переменных выполняются итерации по ним. К каждой из них применяется пользовательская функция ToBytes(), копирующая данные в срез типа []byte, которые затем выводятся в виде строк в кавычках.

    Функция ToBytes() принимает значение, реализующее интерфейс io.Reader (то есть любое значение, имеющее метод с сигнатурой Read([]byte) (int, error), такое как *os.File), а также максимальное число копируемых байтов, и возвращает срез типа []byte, содержащий данные из указанного значения, и признак ошибки.

func ToBytes(reader io.Reader, size int) ([]byte, error) {
    data := make([]byte, size)
    n, err := reader.Read(data)
    if err != nil {
        return data, err
    }
    return data[:n], nil // Отсечет все неиспользованные байты
} 


Рис.1. Результат работы программы

    Архив с примерами можно взять здесь.

    Эта функция ничего не знает о конкретном типе переданного ей значения – ей достаточно, чтобы оно содержало реализацию интерфейса io.Reader.

    В случае успешного чтения данных длина среза data уменьшается до количества фактически прочитанных байтов. Если этого не сделать, то при большом значении параметра size будет получен срез, где за прочитанными байтами следуют нулевые байты (со значением 0x00). Например, без отсечения лишних байтов при чтении данных из значения строка4 можно было бы получить вывод "РонУизли\x00\x00\x00\x00\x00\x00\x00\x00".

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

    На следующем шаге рассмотрим встраивание интерфейсов в Go.


Предыдущий шаг Содержание Следующий шаг