Блог Антона Репушко

Пароли в Digital Ocean

После поднятия дроплета из изображения, стандартный пароль от него (например root-пароль от MySQL) пишется в файл:

/root/.digitalocean_password

Как использовать интерфейсы в Go

Это перевод статьи про интерфейсы в Go. К ним стоит относиться немного иначе, чем объектно-ориентированном программировании.
P.S. А еще в Go утиная типизация.
Антон Р.

Помимо моей основной работы я иногда консультирую по Go и делаю код-ревью. Поэтому я видел много кода, написанного разными людьми, и все чаще замечаю использования интерфейсов в «Java-стиле» (я это называю так).

Этот пост — моя рекомендация про то, как использовать интерфейсы в Go хорошо.

Мы будем работать с двумя пакетами:

animal

и

circus.

Код в посте — граница взаимодействия этих пакетов.

Не стоит делать так

Видел много таких примеров:


package animals 

type Animal interface {
	Speaks() string
}

// implementation of Animal
type Dog struct{}
func (a Dog) Speaks() string { return "woof" }



package circus

import "animals"

func Perform(a animal.Animal) { return a.Speaks() }


Это и есть «Java-стиль» использование интерфейсов. Ему соответствуют следующие шаги:

  1. Определить интерфейс.
  2. Определить тип, который соответствует интерфейсу.
  3. Написать методы, которые реализуют интерфейс.

Суммарно это можно назвать «написанием типов для реализации интерфейса». Факторы такого кода с запашком следующие:

  • самое очевидное: есть только один тип, который реализует этот интерфейс и не очевидны способы расширения,
  • функции как правило принимают конкретные типы вместо интерфейсов.

Стоит делать так

Интерфейсы в Go поощряют лень, и это хорошо. Вместо написания типов, которые реализуют ваш интерфейс, пишите интерфейсы, которые будут реализовывать использование типов.

Что я имею в виду — вместо определения Animal в пакете animals, определите его в месте использования — в пакете circus.


package animals

type Dog struct{}
func (a Dog) Speaks() string { return "woof" }



package circus

type Speaker interface {
	Speaks() string
}

func Perform(a Speaker) { return a.Speaks() }


Более правильный путь таков:

  1. Определить типы.
  2. Определить интерфейс в месте их использования.

Такой способ дает низкую зависимость на компоненты пакета animals. Уменьшение зависимости помогает делать надежное программное обеспечение.

Правило Джона Постела

Хорошее правило для написания хорошего программного обеспечения — это правило Джона Постела. Его часто произносят так:

«Будь либерален к тому, что принимаешь, и требователен к тому, что отсылаешь»

Переводя на Go:

«Принимайте интерфейсы, возвращайте структуры»

Хорошая рекомендация для создания надежных вещей. Основной единицей кода в Go является функция. Паттерн, которому стоит следовать при дизайне функций и методов:


func funcName(a INTERFACETYPE) CONCRETETYPE 


Здесь мы принимаем все, что реализует заданный интерфейс (может быть даже пустой интерфейс) и возвращаем конкретное значение. Конечно, есть ограничения на то, каким это а может быть. Как говорится:

«пустой интерфейс ни о чем не говорит»
Роб Пайк

Другими словами предпочтительно не иметь функций, которые принимают interface{}.

Пример использования: моки

Есть прекрасная демонстрация полезности правила Постела в сфере тестирования.
Есть такая функция:


func Takes(db Database) error 


Если Database — это интерфейс в тестируемом коде, то вы можете просто представить мок-реализацию интерфейса Database без обращений к реальному объекту базы данных.

Когда можно определять интерфейс заранее

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

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

  • запечатанные интерфейсы,
  • абстрактные типы данных,
  • рекурсивные интерфейсы.

Запечатанные интерфейсы

Запечатанные интерфейсы могут обсуждаться только в контексте наличия нескольких пакетов. Запечатанный интерфейс — интерфейс с неэкспортируемыми методами. Это означает, что пользователи вне пакета не могут создавать типы, которые соответствуют этому интерфейсу.

Например определим что-то такое:


type Fooer interface {
	Foo() 
	sealed()
}


Только пакет, который определяет Fooer может использовать и создавать любые валидные значения Fooer. Это позволяет выполнять исчерпывающие переключатели типов.

Запечатанный интерфейс может служить инструментом аналитики для легкого поиска любого неполного паттерн-матчинга. Пакет sumtype от Бернта Суши делает именно это.

Абстрактные типы данных

Другой пример определения интерфейса заранее — создание абстрактного типа данных. Он может быть запечатанным, а может и не быть.

Пакет sort из стандартной библиотеке отличный пример. Коллекция с возможностью сортировки определяется там так:


type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less reports whether the element with
    // index i should sort before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}


Если вы хотите использовать пакет sort, то вам придется реализовать методы интерфейса. В основном люди расстраиваются из-за необходимости ввести три дополнительные строчки.

На мой же взгляд это очень изящная форма дженериков в Go. Следует больше это поощрять.

Альтернативный элегантный дизайн требует более высоких типов. Но мы не будем это трогать в этом посте.

Рекурсивные интерфейсы

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


type Fooer interface {
	Foo() Fooer
}


Очевидно, что паттерн рекурсивного интерфейса требует, чтобы интерфейс был определен заранее. Здесь наши правила неприменимы.

Этот паттерн полезен для создания контекстов для работы. Контекстно-тяжелый код обычно является самодостаточным внутри пакета и наружу экспортируются только контексты (вроде пакета tensor), поэтому я не слишком часто такое вижу. Нужно рассказать чуточку больше о шаблонах контекстов, но оставлю это для другого поста.

Вывод

Несмотря на то, что здесь есть раздел «Не делай так», это не должно быть непоколебимой истиной. Я хочу побудить людей думать о границах: тогда-то и случается все это.

Лично я нашел объявление-в-месте-использования чрезвычайно полезным: так я не сталкиваюсь с многими проблемами, c которыми сталкиваются другие люди.

Но у меня тоже бывают ситуации, когда я случайно создаю интерфейс в «Java-стиле» — обычно после того, как я немного поработал на Python или Java. Желание оверинжениринга и «классификации всего на свете» особенно сильно в Go после написания некоторого ООП-кода.

Этот пост лишь напоминание того, как выглядит путь к безболезненному коду.

Спасибо Стрейтеру Нейросу за обзор более ранней версии этой статьи и Райтику Сриваставу за правку нескольких ошибок в примере кода.

Сервис для ведения дел Вокрфлоуи

Меня постоянно тянуло к какому-то простому сервису для ведения дел и задач. Канбан-доски (Трелло) казались слишком неудобными для кучи дел, обычные бумажные ежедневники и сервисы для заметок (Эверноут) — слишком незаполненным и пустующими.

Потом я познакомился с Ношион и влюбился — это отличный сервис по всем параметрам. Эверноут без маркетингового и бизнесового мусора. Я увидел его еще на стадии беты и ребята почти за год сделали из него конфетку. На сервисе удобно хранить объемные заметки, черновики статей, конспекты лекций или курсов, но дела вести было тяжело. Все свелось опять же к обычным канбан-доскам, только в обертке Ношиона.

И тут случилось чудо. Я нашел свой идеал. Это Воркфлоуи. Просто группируемый список, где можно создавать неограниченные уровни вложенности, завершать задачи и работать только хоткеями. Есть бэкап всего в Дропбокс. Советую попробовать — возможно это как раз то, чего вам не хватало.

5 марта   другое   хаки

Вежливый Незнакомец (@normspasibot)

Написал Нормспасибота для Телеграма. Он умеет ловить в чатах некоторые фразы и высказывать по их поводу свое мнение. Иногда к месту, иногда нет.

Бот построен на удобной библиотеке Телепот.

Из нового опыта — научился настраивать хороший деплой. При коммите в Гитхаб срабатывает хук, который заставлят билдиться контейнер в Докер-Хабе. Потом обновляется образ контейнера на сервере, где хостится бот. В итоге нужно только закоммитить, а все остальное сделается само.

Ничего умного в логике нет: это всего лишь несколько корявых ифов и поиск основ в сообщении. Код писался на коленке, когда-нибудь сделаю загрузку правил ответов из конфига. Пока что работает, иногда веселит.

Может отвечать например так...

... и так

Запуск Falcon Heavy

6 февраля в 21.30 по МСК Илон Макс запускает Falcon Heavy со своим красным родстером на орбиту Марса. Либо свершится историческое событие, либо все это безумно красиво рванет. Любой исход стоит того, чтобы на это посмотреть. Наблюдать за запуском можно на канале SpaceX.

UPD 07.02.2018
Запуск прошел успешно, приземлились два разгонных блока из трех, ракета не смогла выйти на нужную орбиту и теперь полетит в пояс астероидов.

История РПГ-игр

Вышла книга про историю компьютерных ролевых игр, над которой 4 года работали 112 волонтеров. Можно вспомнить свое детство и посмотреть, как поменялся жанр за столько лет развития. Особенно интересно искать появление новых механик и их развитие в следующих поколениях. Заканчивается вся история Фоллаутом 4 и другими современными играми.

2018   другое

Жизненный цикл докер-контейнера

Это конечный автомат состояний докер-контейнера. Кружками изображены состояния, жирным выделены консольные команды, квадратиками показывается то, что выполняется в реальности.

Ранее Ctrl + ↓