Язык Go — интерфейсы: гибкость и методы
Экспортировать интерфейс вместо типа, конструкторы возвращающие интерфейс, http.Handler на структуре, числе, канале и функции — как интерфейсы делают код по-настоящему гибким.
Содержание
Интерфейс в Go — это не просто инструмент для полиморфизма. Это способ скрывать детали реализации, уменьшать связность кода и делать замену алгоритмов локальным изменением. Разберём, как это выглядит на практике.
Экспортировать интерфейс, а не тип
Если тип существует только для того, чтобы реализовать интерфейс, и никаких других публичных методов у него нет — нет смысла экспортировать сам тип. Достаточно экспортировать интерфейс.
Это даёт два преимущества:
- Пользователю понятно: у значения нет никакого интересного поведения, кроме описанного в интерфейсе
- Не нужно дублировать документацию для каждого общего метода на каждой реализации
В таких случаях конструктор должен возвращать интерфейс, а не конкретный тип. Пример из стандартной библиотеки — пакеты хэширования:
// обе функции возвращают hash.Hash32, а не конкретный тип
h1 := crc32.NewIEEE() // тип внутри — *digest, но снаружи мы видим только hash.Hash32
h2 := adler32.New() // то же самое, другая реализация
// чтобы сменить алгоритм — меняем только одну строку (вызов конструктора)
// весь остальной код работает с hash.Hash32 и не замечает разницы
Замена CRC-32 на Adler-32 — это изменение одного вызова конструктора. Весь остальной код продолжает работать без изменений, потому что работает с интерфейсом.
Пример из crypto: Block и Stream
Тот же подход используется в пакетах шифрования. Интерфейс Block описывает блочный шифр — шифрование одного блока данных:
type Block interface {
BlockSize() int // размер блока в байтах
Encrypt(dst, src []byte) // зашифровать один блок
Decrypt(dst, src []byte) // расшифровать один блок
}
type Stream interface {
XORKeyStream(dst, src []byte) // применить потоковый шифр к данным
}
На основе любого Block можно построить потоковый шифр. Вот конструктор режима CTR (счётчика):
// NewCTR возвращает Stream, который шифрует/дешифрует данные
// в режиме счётчика, используя переданный Block.
// Длина iv должна совпадать с размером блока.
func NewCTR(block Block, iv []byte) Stream
NewCTR не знает, какой конкретно блочный шифр внутри — AES, DES или любой другой. Ему достаточно, что переданное значение реализует Block. Результат — тип Stream, и вызывающий код тоже не знает деталей.
Хотите заменить CTR на другой режим шифрования? Меняете один вызов конструктора. Остальной код работает с Stream и не замечает разницы.
Интерфейсы и методы: почти любой тип
В Go метод можно определить почти для любого типа — не только для структур. А раз есть методы, тип может реализовать интерфейс. Рассмотрим это на примере http.Handler.
Пакет net/http определяет интерфейс Handler:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Любой тип с методом ServeHTTP может обрабатывать HTTP-запросы. Посмотрим, насколько разными могут быть такие типы.
Структура как хендлер
Самый очевидный вариант — структура с полями:
// Counter хранит счётчик посещений страницы
type Counter struct {
n int
}
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctr.n++
fmt.Fprintf(w, "counter = %d\n", ctr.n) // Fprintf пишет в ResponseWriter, как в любой io.Writer
}
// подключаем хендлер к URL
ctr := new(Counter)
http.Handle("/counter", ctr)
Заметьте: fmt.Fprintf принимает io.Writer, а http.ResponseWriter реализует Write — поэтому он подходит как io.Writer без каких-либо преобразований.
В реальном сервере доступ к ctr.n нужно защитить от гонки данных — для этого есть пакеты sync и sync/atomic.
Число как хендлер
Зачем структура, если нужно хранить только одно целое число? Тип может быть и числом:
// Counter — просто int, не структура
type Counter int
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
*ctr++ // разыменовываем указатель, чтобы изменить значение
fmt.Fprintf(w, "counter = %d\n", *ctr)
}
Ресивер — указатель, иначе инкремент был бы виден только внутри метода.
Канал как хендлер
Если нужно уведомлять другую горутину о каждом посещении — можно сделать хендлером сам канал:
// Chan — канал запросов; при каждом посещении страницы запрос отправляется в канал
// (лучше использовать буферизованный канал, чтобы не блокировать хендлер)
type Chan chan *http.Request
func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ch <- req // отправляем запрос в канал — другая горутина его получит
fmt.Fprint(w, "notification sent")
}
Функция как хендлер
Предположим, нам нужно показывать на /args аргументы командной строки, с которыми запущен сервер. Есть простая функция:
func ArgServer() {
fmt.Println(os.Args)
}
Как превратить её в хендлер? Можно определить какой-нибудь тип и навесить на него этот вызов — но есть чище. В Go можно определить метод для функционального типа. Именно так устроен http.HandlerFunc:
// HandlerFunc — адаптер, позволяющий использовать обычные функции как HTTP-хендлеры.
// Если f имеет правильную сигнатуру, HandlerFunc(f) — это Handler, который вызывает f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP вызывает f(w, req) — ресивер здесь является функцией
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
f(w, req)
}
Ресивер метода ServeHTTP — это сама функция f. Метод просто вызывает её. Это похоже на то, как ресивером был канал, и метод отправлял в него данные — необычно, но логично.
Чтобы использовать ArgServer как хендлер, сначала приводим его сигнатуру к нужной:
// ArgServer теперь имеет сигнатуру func(ResponseWriter, *Request)
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args) // пишем аргументы прямо в ответ
}
И регистрируем:
http.Handle("/args", http.HandlerFunc(ArgServer))
http.HandlerFunc(ArgServer) — это конвертация типа, точно такая же как sort.IntSlice(s) из предыдущей статьи. ArgServer получает тип HandlerFunc, у которого есть метод ServeHTTP. Когда придёт запрос на /args, сервер вызовет HandlerFunc.ServeHTTP, тот вызовет f(w, req), то есть ArgServer(w, req), и аргументы будут выведены.
Итоги
- Если тип существует только для реализации интерфейса — экспортируйте интерфейс, а не тип
- Конструктор, возвращающий интерфейс, позволяет менять реализацию изменением одной строки
- Метод можно определить для почти любого типа: структуры, числа, канала, функции
http.Handler— хороший пример: его реализует и структура, иint, иchan, и функцияhttp.HandlerFunc— адаптер, превращающий функцию с нужной сигнатурой вHandlerчерез конвертацию типа- Интерфейсы — это наборы методов, и методы можно определить почти для чего угодно; в этом и есть их сила