Тип соответствует (удовлетворяет)
интерфейсу, если он обладает всеми методами, которые требует интерфейс. Например,
*os.File
соответствует интерфейсам io.Reader, Writer, Closer, ReadWriter
. *bytes.Buffer
соответствует интерфейсам
Reader, Writer, ReadWriter
, но не Closer
, потому что не имеет метода Close
. Для краткости программисты Go часто
говорят о том, что конкретный тип "является" типом некоторого интерфейса. Это означает, что он соответствует этому
интерфейсу. Например, *bytes.Buffer
является io.Writer
, а *os.File
является io.ReadWriter
.
Правило присваиваемости (раздел 2.4.2) интерфейсов очень простое: выражение может быть присвоено интерфейсу, только если его тип соответствует этому интерфейсу. Таким образом:
var w io.Writer
w = os.Stdout // OK: *os.File имеет метод Write
w = new(bytes.Buffer) // OK: *bytes.Buffer имеет метод Write
w = time.Second // Ошибка: у time.Duration нет метода Write
var rwc io.ReadWriteCloser
rwc = os.Stdout // OK: *os.File имеет методы Read, Write, Close
rwc = new(bytes.Buffer) // Ошибка: у *bytes.Buffer нет метода Close
Это правило применимо даже тогда, когда правая сторона сама по себе является интерфейсом:
w = rwc // OK: io.ReadWriteCloser имеет метод Write
rwc = w // Ошибка: io.Writer не имеет метода Close
Поскольку ReadWriter
и ReadWriteCloser
включают все методы Writer
, любой тип, соответствующий ReadWriter
или
ReadWriteCloser
, обязательно соответствует Writer
.
Прежде, чем мы пойдем дальше рассмотрим одно тонкое место, связанное со смыслом утверждения, что некоторый тип имеет
некий метод. Вспомним из раздела 6.2, что для каждого именованного конкретного типа T
одни из его методов имеют
получатель типа T
, в то время как другие требуют указатель *T
. Вспомним также, что вполне законным является вызов
метода *T
с аргументом типа T
при условии, что аргумент является переменной
- компилятор неявно берет ее адрес.
Но это лишь синтаксическое упрощение: значение типа T
не обладает всеми методами, которыми обладает указатель *T
, и
в результате оно может удовлетворить меньшему количеству интерфейсов.
Чтобы было понятнее, приведем пример. Методу String
типа IntSet
из раздела 6.5 необходим получатель, являющийся
указателем, так что мы не можем вызвать этот метод для неадресуемого значения IntSet
:
type IntSet struct {
/*...*/
}
func (*IntSet) String() string
var _ = IntSet{}.String() // Ошибка: String требует получатель `*IntSet`
Но можно вызвать его для переменной IntSet
:
var s IntSet
var _ = s.String() // OK: s является переменной; &s имеет метод String
Однако, поскольку только *IntSet
имеет метод String
, только *IntSet
соответствует интерфейсу fmt.Stringer
:
var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s // Ошибка: у IntSet нет метода String
В разделе 12.8 представлена программа, выводящая методы произвольных значений, а команда godoc -analysis=type
(раздел
10.7.4) отображает методы каждого типа и взаимоотношения между интерфейсами и конкретными типами.
Подобно конверту, который является оболочкой, скрывающий письмо, которое в нем хранится, интерфейс обертывает и скрывает конкретный тип и значение, которое он хранит. При этом могут вызваться только методы интерфейса, даже если конкретный тип имеет и другие методы:
os.Stdout.Write([]byte("hello")) // OK: *os.File имеет метод Write
os.Stdout.Close() // OK: *os.File имеет метод Close
var w io.Writer
w = os.Stdout
w.Write([]byte("hello")) // OK: io.Writer имеет метод Write
w.Close() // Ошибка: io.Writer не имеет метода Close
Интерфейс с бо́льшим количеством методов, такой как io.ReadWriter
, больше говорит о значениях, которые он содержит, и
устанавливает более высокие требования к типам, которые его реализуют, чем интерфейс с меньшим количеством методов,
такой как io.Reader
. Что до типа interface{}
, который вообще не имеет методов, говорит нам о конкретных типах,
которые ему соответствуют?
Именно так: ничего
. Это может казаться бесполезным, но на самом деле тип interface{}
, который называется пустым интерфейсом
, является необходимым. Поскольку тип пустого интерфейса не накладывает никаких требований на типы, которые
ему соответствуют, пустому интерфейсу можно присвоить любое значение.
var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)
Хотя это и не очевидно, мы использовали тип пустого интерфейса, начиная с первого примера это книги, потому что он
позволяет таким функциям, как fmt.Println
или errorf
из раздела 5.7 принимать аргументы любого типа.
Конечно, создав значение interface{}
, содержащие булев тип, тип с плавающей точкой, строку, карты, указатель или любой
другой тип, мы ничего не можем сделать с ним непосредственно, поскольку интерфейс не имеет методов. Нам нужен способ
вновь получить значение переданного типа. Как это сделать с помощью декларации типа
, мы узнаем в разделе 7.10
Поскольку удовлетворение интерфейсу зависит только от методов двух типов, нет необходимости объявлять отношения между
конкретным типом и интерфейсом, которому он соответствует. Тем не менее иногда полезно документировать и проверять
взаимоотношения если они запланированы, но не могут быть обеспечены программой иным способом. Объявление, приведенное
ниже, проверяет во время компиляции, что значение типа *bytes.Buffer
соответствует интерфейсу io.Writer
:
// *bytes.Buffer должен соответствовать io.Writer
var w io.Writer = new(bytes.Buffer)
Нам не нужно выделять новую переменную, поскольку любое значение типа *bytes.Buffer
будет это делать, даже nil,
который мы записываем с помощью явного преобразования как (*bytes.Bufer)(nil)
. А так как мы вообще не намерены
обращаться к переменной w
, ее можно заменить пустым идентификатором. Все эти изменения дают нам этот более скромный
вариант:
// *bytes.Buffer должен соответствовать io.Writer
var _ io.Writer = (*bytes.Buffer)(nil)
Непустым типам интерфейсов, таким как io.Writer
, чаще всегда соответствуют типы указателей, особенно когда один или
несколько методов интерфейса подразумевают некоторые изменения получателя, как метод Write
. Особенно часто
используется указатель на структуру.
Однако типы указателей - это отнюдь не единственные типы, которые соответствуют интерфейсам, и даже интерфейсы с
изменяющими получателя методами могут быть удовлетворены одним из других ссылочных типов Go. Мы видели примеры типов
срезов с методами (geomety.Path
, раздел 6.1), и типов карт с методами (url.Values
, раздел 6.2.1), а позже мы увидим
типы функций с методами (http.HandlerFunc
, раздел 7.7). Даже фундаментальные типы могут соответствовать интерфейсам.
Как мы видели в разделе 7.4, тип time.Duration
соответствует интерфейсу fmt.Stringer
.
Конкретный тип может соответствовать множеству не связанных интерфейсов. Рассмотрим программу, которая организует или продает оцифрованные произведения культуры, такие как музыка, фильмы и книги. Она может определить следующее множество конкретных типов:
Album
Book
Movie
Magazine
Podcast
TVEpisode
Track
Мы можем выразить каждую интересующую нас абстракцию как интерфейс. Одни свойства являются общими для всех произведений, такие как название, дата создания и список создателей (авторы и художники):
type Artifact interface {
Title() string
Creators() []string
Created() time.Time
}
Другие свойства ограничены только определенными видами произведений. Свойства типографского издания актуальны только для книг и журналов, тогда как только фильмы и телевизионные эпизоды имеют разрешение экрана:
type Text interface {
Pages() int
Words() int
PageSize() int
}
type Audio interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string // Например, mp3, wav
}
type Video interface {
Stream() (io.ReadCloser, error)
RunningTime time.Duration
Format() string // Например mp4, wmv, mkv
Resolution() (x, y int)
}
Эти интерфейсы представляют собой лишь один из полезных способов группировки связанных конкретных типов и выражения их
общих аспектов. Позже мы сможем обнаружить и другие способы группировки. Например, обнаружив, что нужно обрабатывать
элементы Audio
и Video
одинаково, мы сможем определить интерфейс Streamer
для представления их общих свойств без
изменения каких-либо существующий объявлений типов:
type Steramer interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string
}
Каждая группировка конкретных типов на основе их общего поведения может быть выражена как тип интерфейса. В отличие от языков на основе классов, в которых набор интерфейсов, которым соответствует класс, указывается явно, в Go мы можем определять новые абстракции или группы интересов, когда мы в них нуждаемся, не изменяя при этом объявления конкретных типов. Это особенно удобно, когда конкретный тип определен в пакете другого автора. Конечно, при этом должно удовлетворятся требование базовой общности конкретных типов.
- Чтобы тип
удовлетворял
интерфейсу, он должен иметь все методы, которые требует интерфейс; - Чтобы выражение могло быть присвоено интерфейсу, его тип должен соответствовать этому интерфейсу. Это относится к
обеим сторонам
a = b
, если они являются интерфейсами; - Для каждого типа
T
некоторые методы могут быть вызваны с помощью значения типаT
, а другие требуют указателя на тип*T
. Можно вызывать методы типа*T
с помощью переменной типаT
, но это не значит, что значение типаT
обладает всеми методами, которые есть у указателя на*T
. Поэтому, при использовании значения типаT
, может быть доступно меньшее количество методов, чем при использовании указателя на*T
; - Интерфейс скрывает конкретный тип и значения, которые он содержит. Можно использовать только методы, объявленные в интерфейсе, даже если конкретный тип имеет другие методы;
- Тип
interface{}
являетсяпустым интерфейсом
и не накладывает требований на типы, которые ему соответствуют (может быть присвоен любой тип); - Создав значение
interface{}
, с любым значением, мы ничего не можем сделать с ним непосредственно, т.к. интерфейс не имеет методов; - Обычно нет необходимости объявлять отношения между конкретным типом и интерфейсом, которому он соответствует. Однако иногда полезно документировать и проверять эти отношения;
- Объявление
var w io.Writer = new(bytes.Buffer)
проверяет, что значение типа*bytes.Buffer
соответствует интерфейсуio.Writer
во время компиляции. Нет необходимости создавать новую переменную, поскольку любое значение типа*bytes.Buffer
будет подходить, дажеnil
, который можно записать как(*bytes.Buffer)(nil)
; - Непустым типам интерфейсов, таким как
io.Writer
, чаще всегда соответствуют типы указателей, особенно когда один или несколько методов интерфейса подразумевают некоторые изменения получателя, как методWrite
. Особенно часто используется указатель на структуру; - Помимо указателей, можно использовать и другие ссылочные типы для удовлетворения интерфейсов, даже с методами, изменяющими получателя;
- Фундаментальные типы могут соответствовать интерфейсам;
- Тип
time.Duration
соответствует интерфейсуfmt.Stringer
; - Один конкретный тип может соответствовать нескольким интерфейсам;
- Используя интерфейсы, мы можем группировать конкретные типы по их общим аспектам и выражать их общие свойства;
- Каждая группировка конкретных типов может быть выражена в виде интерфейса, и мы можем определять новые интерфейсы, когда в них нуждаемся, не изменяя при этом объявления конкретных типов. Это особенно удобно, когда мы используем конкретный тип из пакета другого автора. Однако все конкретные типы, которые мы хотим группировать с помощью интерфейса, должны удовлетворять базовым общим требованиям.