Почему мне нравятся интерфейсы в Go

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

    Go вполне SOLID'ный язык


    Простите за каламбур, не удержался

    На тему «Является ли Go объектно-ориентированным языком?» написано очень много статей, в том числе и на Хабре. Но не так часто, когда поднимается эта тема, говорят о SOLID принципах и вообще редко о последнем(по порядку, но не по значимости) из них — принципе инверсии зависимости(DIP). Если подзабыли, то обычно его формулируют так:
    Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций
    Наверное самый мощный инструмент, который придумали для реализации это принципа — интерфейсы. Если хотите прояснить для себя инверсию зависимостей, то вот хорошая статья. Кстати картинка оттуда.



    Допустим, что Foo и Bar находятся в разных модулях: F и B. Но тогда, чтобы реализовать интерфейс, мы должны импортировать из F интерфейс IBar в B, а затем его реализацию как-то обратно передать в F. В некотором смысле образуется циклическая зависимость между пакетами (не с точки зрения поведения, а с точки зрения импортирования). Вот тут нам на помощь и приходит неявная реализация интерфейсов. Мы можем импортировать Bar из B в F, а Go уже сам разберётся, реализует он IBar или нет. В таком случае, явное указание интерфейсов при реализации становится избыточным.

    Пара слов напоследок


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

    А что вам нравится / не нравится в системе типов Go?
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 59

      +1
      Я в восторге от горутин, каналов, инструментов тестирования и бенчмаркинга. Вошла вполне и поддержка версионности с последних версий.
      Что немного задалбывает — это if err!=nil… но с другой стороны, когда проникаешься идеей выталкивания ошибок на верхний уровень и их специфичной обработкой прямо на месте, то начинаешь относится к таким ифам как необходимой обвязке, c которой приходится мириться… правда проникнуться этим получилось не сразу.
      Интерфейсами как-то до конца не проникся. Но просто видимо задач под них еще не попадалось.
        +1
        >то начинаешь относится к таким ифам как необходимой обвязке

        В том-то и дело, что они ни в коей мере не «необходимы». Просто посмотрите, например, на Rust (или Swift) и вы поймёте насколько безнадёжно убога обработка ошибок в Go.

        То же самое касается и утиных интерфейсов вместо нормальных типажей.
          –6

          Спасибо, посмотрел.
          Тот же велосипед, вид сбоку. Чем он лучше — мне не понятно.
          ИМХО, решение в go (текущее, мы же не обсуждаемых пока то, что планируется в go2) даже более компактное и очевидное чем эти пляски с бубенцами в расте.

            +5

            Т.е. вам действительно нужно объяснять чем ? (который для вас внезапно менее компактен), которое может делать преобразования типов ошибок при необходимости, компилятор который не даёт забыть проверку ошибок, а так же мощные комбинаторы, на порядки лучше пачки копипащенных if err!=nil? Настоятельно рекомендую получше разобраться в подходе Rust'а прежде чем говорить про "велосипеды", иначе глупо выглядите.

              –4
              Простите, но вы дали ссылку и я ознакомился — на основании этого и сделал _персональный_, очень поверхностный вывод без претензии на абсолют.

              В чем конкретно у меня претензия: зачем-то нужно обрабатывать не только ошибку, но еще и успех описывать. Зачем? Что это дает кроме набухшего кода?

              А вы, кстати, посмотрите на обработку предлагаемую в go2 — вполне удобно будет, но это опять же мое ИМХО, вам не навязываю (что бы у вас в глазах глупо не выглядеть :) ).
                +1
                Go 2 уже зарелизили? Или вы рассуждаете о возможностях того чего нет?
                  0
                  Я упомянул обработку ошибок предложенную в go2 как пример того, что язык Go еще довольно активно развивается.
                    +1
                    Посмотрите как развиваются другие современные языки Swift/Kotlin/Rust, по сравнению с ними, Go стоит на месте много лет. А Go 2 в лучшем случае (я оптимист) к 2022г выйдет.
                      –1
                      Простите а куда смотреть?
                      Swift:
                      2014-09-09 Swift 1.0
                      2014-10-22 Swift 1.1
                      2015-04-08 Swift 1.2
                      2015-09-21 Swift 2.0
                      2016-09-13 Swift 3.0
                      2017-09-19 Swift 4.0
                      2018-03-29 Swift 4.1
                      2018-09-17 Swift 4.2
                      2019-03-25 Swift 5.0

                      Go:
                      go1.12 (released 2019/02/25)
                      go1.11 (released 2018/08/24)
                      go1.10 (released 2018/02/16)
                      go1.9 (released 2017/08/24)
                      go1.8 (released 2017/02/16)
                      go1.7 (released 2016/08/15)
                      go1.6 (released 2016/02/17)
                      go1.5 (released 2015/08/19)
                      go1.4 (released 2014/12/10)
                      go1.3 (released 2014/06/18)
                      go1.2 (released 2013/12/01)
                      go1.1 (released 2013/05/13)
                      go1 (released 2012/03/28)

                      Kotlin:
                      Kotlin 1.3.30 April 12, 2019
                      Kotlin 1.3.20 January 23, 2019
                      Kotlin 1.3 October 29, 2018
                      Kotlin 1.2.70 September 13, 2018
                      Kotlin/Native v0.9 September 5, 2018
                      Kotlin/Native 0.8.2 August 24, 2018

                      Rust-а не нашел в удобном виде — но суть та же: если судить по версиям то все языки вполне развиваются, Go на фоне остальных никак не выделяется. Где он у вас стоит — не понятно.
                        +3
                        Под словом «развитие» я имел в виду, количество внедренных фич, а не соревнование по счетчикам.
                          0
                          количество внедренных фич

                          А как же качество и необходимость?

                  +2

                  Вы потрудились хотя бы до сюда дочитать? Или встретили первый пример и сказали "да ну фигня!"? (на самом деле книга чуть дальше немного привирает, ибо ? можно использовать не только с Result, но и с Option, а в будущем и с user-defined типами через типаж Try)


                  То что предлагается сделать в go2 это несомненно улучшение по сравнению с существующим ужасом, но, на мой взгляд, продолжает следовать худшим традициям языка в виде создания ad hoc решений вместо создания прочного и выразительного фундамента.

                    –1
                    Я потрудился.

                    Но по сути пример с Ок() — это не обработка ошибки, а обычный условный вызов эксепшена.
                      +1

                      Нет. Просто нет. Вы совершенно не поняли подход Rust-а… Это всё равно что go-шные err-ы называть эксепшонами. В некотором смысле Rust и Go достаточно похожи тут тем, что основной канал обработки ошибок делается через значения (в Go через соглашение использующее multiple return values, а в Rust-е через enum'ы), в обоих языках есть паника, которая отводится на совершенно непредвиденные ошибки и каждая паника должна интерпретироваться как баг в софте (но в Rust-е нам не нужен defer). Основная разница заключается в том, что Rust за счёт более основательного и выразительного фундамента даёт программисту намного более мощные, эргономичные и надёжные инструменты для работы с ошибками.

                        0
                        Скажите тогда что делает Ok() в конце функции в которой нет ни строчки обработки ошибок?
                          +2

                          Вы в этом примере даже обработки ошибок не заметили… Вот поэтому и в первую очередь нужно читать всю главу, а не просто примеры рассматривать.


                          В Расте обработка ошибок в основном делается с помощью энума Result, который может принимать два варианта "всё хорошо" (Ok) и "ошибка" (Err). Каждый вариант может содержать сведения, например, Ok будет содержать файл, а Err IO ошибку возникшую при попытке открытия файла. Вы не сможете получить доступ к файлу не обработав потенциальную ошибку каким-либо образом (т.е. невозможно забыть обработку err как в Go и получить nil вместо файла). При этом никакой магии тут совершенно нет, при желании вы можете определить абсолютно такой же тип в сторонней библиотеке.


                          let mut f = File::open("hello.txt")? "записывает" в переменную f файл если всё прошло хорошо, в случае же ошибки из-за использования ? ошибка сразу "всплывает" и функция read_username_from_file возвращает значение с типом Result, которое имеет вариант "ошибка" (Result::Err(io_error)). ? является не более чем сахаром, который разворачивается в конкретную конструкцию, которая объясняется в главе. В самом конце в примере конструируется значение с типом Result, но на этот раз с вариантом "всё хорошо". Ok(s) является просто удобным сокращением Result::Ok(s).

                            –2
                            Спасибо, теперь понял. Но без вашего пояснения это было не очевидно.
                            If err!=nil {
                            return nil, err
                            }
                            мягко говоря гораздо более очевидно чем магия сахарного вопроса.

                            Но тут на вкус и цвет — у всех тапки разного цвета.

                            ЗЫ а нафиг нужен Ok() в конце — для меня все равно осталось загадкой…
                              0

                              Сахар позволяет писать достаточно длинные цепочки комбинаторов вида school.get_class("5A")?.students().get_by_id(id)?.name, которые весьма удобно читать и писать, вместо создания кучи промежуточных переменных. Да и вообще ? лаконичен и после того как к нему привыкнешь его жутко не хватает в языках обрабатывающих ошибки без эксепшенов.


                              а нафиг нужен Ok() в конце — для меня все равно осталось загадкой…

                              Если вы напишите просто s, вы будете возвращать значение с типом String, тогда как функция должна возвращать тип Result<String, io::Error>, т.е. прежде чем возвращать значение "всё хорошо" вам нужно обернуть его в Result::Ok. Т.е. это как если бы вы вместо return val, nil писали в Go просто return val. Есть пропозалы которые добавляют в язык новый "сахарный" сорт функций try fn, в которых возврат будет оборачиваться в Ok автоматически, но их добавления в ближайшем будущем ожидать не стоит.

                                +2
                                тсс… не говорите ему про монады, ибо его императивное мышление пошатнется)
                                0
                                ЗЫ а нафиг нужен Ok() в конце — для меня все равно осталось загадкой…
                                Потому что функция возвращает значения. Вы же понимаете зачем нужно возвращать значения?
                                  0

                                  В go можно название возвращаемой переменной прямо в описании функции указать и тогда надобность в return retVal отпадает.

                              0

                              Если у вас уровень английского недостаточен для свободного чтения, то можете воспользоваться вот этой статьёй. Правда она немного устарела и макрос try! стал deprecated и вместо него используется постфикс оператор ?, но принципы остались те же самые.

                                0
                                Спасибо что вы беспокоитесь о моем английском, но с ним у меня все хорошо, что не очень хорошо — так это с восприятием таких магических операторов.
                                С try то оно как раз было чуть более очевидно. Но опять же это просто вопрос цвета тапок.
                              0
                              И причем тут defer?
                              Или вы считаете defer обработкой ошибок?
                              Ок, он может быть и связан с обработкой ошибок, но не стой стороны откуда начался наш разговор.
                                0

                                Нет, я просто отметил некоторую разницу того как работают паники в Расте и Го, можете пока проигнорировать этот момент.

                    +1

                    Сколько раз было сказано, но холиварность темы языков не даёт фразе уйти на покой — "есть языки, которые все ругают, а есть те, на которых никто не пишет". Go не просто так получил такое широкое распространение, а по вполне обоснованным причинам. И чтобы понять философию языка, тоже следует ознакомиться с материалами, желательно от авторов. Язык же не только про обработку ошибок, хотя и в ней есть плюсы по сравнению с тем же С, например.

                      +4

                      Я на Go писал два года, ещё до релиза Rust 1.0, поэтому, поверьте, с "философией языка" я достаточно неплохо знаком и меня совершенно не тянет возвращаться обратно. (правда, признаю, про Go2 знаю относительно немного) И большую часть "широкого распространения" я бы списал на активное и пассивное проталкивание Гуглом, а не на свойства языка. А смотреть на полурелигиозные утверждения фанатов про необходимость (и, в особо запущенных случаях, про элегантность) if err!=nil, про ненужность дженериков и прочие "достоинства" языка мне просто грустно.


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

                        0

                        Ну я бы поспорил с "проталкиванием" Гуглом, потому что:


                        1. Активно используют Java и С++, но не способствуют популяризации
                        2. Сделали Dart, который не взлетел
                        3. Перешли на Kotlin в Android SDK
                        4. Хотели Go в Android, но не зашло (в этом пункте я не очень уверен, но ранняя версия инструментария точно была)

                        В итоге лично у меня складывается ощущение, что контора все же не стремится всю индустрию нагнуть под себя. И я очень, очень сомневаюсь, что тот же OpenShift в третьей версии переписали на Go после Ruby именно из-за рекламы Гугла. Скорее язык все же завоевал сердца. Но он не для всех, однозначно. Точно не для тех, кто привык удивляться языку или строить какие-то хитрые элегантные, но никому без мана непонятные конструкции...

                          0

                          Не знаю как с С++ с Go конкурирует в системных и сетевых приложения, но как по мне, то в моделировании бизнес-процессов он Java и ко не конкурент. Именно потому, что на нём для этого нужно строить никому без мана непонятные конструкции.

                            0
                            Сделали Dart, который не взлетел

                            Очень даже взлетел в составе Flutter и стремительно набирает высоту.
                              +1
                              Судите по количеству индусов на медиуме осиливших Hello World, или уже кто то серьёзный типа Микрософт, написал что то вроде скайпа?
                                0
                                Судите по количеству индусов на медиуме осиливших Hello World

                                Нет, сужу по тому, что представляют на конференциях, включая последнюю Google I/0 2019 и по личному опыту использования.

                                То, что сейчас есть и направление развития радует.


                                  +1
                                  Я как бы про реальный продакшн, а не про ваши с гуглом хотелки. Микрософт тоже Xamarin по конференциям показывает, однако скайп пилит на React Native, потому что под него всё есть и не нужно изобретать костыли, бизнесу дешевле, разработчикам меньше боли.
                                    0
                                    Микрософт тоже Xamarin по конференциям показывает, однако скайп пилит на React Native

                                    Меня особе не волнует, на чем Microsoft пилит скайп, это его дело.
                                    У меня свои задачи, никакой Microsoft их за меня не решит.

                                    React Native, потому что под него всё есть и не нужно изобретать костыли

                                    Что-то я не понял, почему под React Native не нужно изобретать костыли. Это какая-то магическая платформа, на нее не действуют законы природы?

                                    Каждому, кто использует ReactNative в подарок высылают волшебную палочку?
                                0

                                Так делали на замену JS, там он точно не набрал критмассу в свое время. А сейчас это уже второй раунд!

                            +2
                            Это описывает необходимое, а не достаточное условие.
                            Любой язык имеет проблемы. И понятно что у популярного языка все недочеты публично и активно обсуждаются.

                            Го популярен потому что одновременно
                            а) активно рекламируется/разрабатывается большой компанией
                            б) хорошо (на фоне популярных конкурентов) решает существующую проблему — асинхронность и паралельность.

                            При том что ерланг (и потомки), не обладает мне известными недостатками по сравнению с го. Лучше решает именно те задачи, где рекламируют го. Но не проталкивается таким гигантом. Хотя если бы Ериксон проспонсировали рекламу самого языка еще тогда, веб был бы совершенно другим сейчас.
                              0

                              Erlang слишком не похож на С, чтобы получить широкое распространение в короткие сроки. По мне это очень сильно влияет.

                                +2
                                Учитывая как развивался интернет (перл, пхп1-4, руби да даже питон) — это не было проблемой в раннем интернете.
                        –15
                        SOLID это УГ, от ООП фриков. По сабжу — как в Go без документации узнать что структура реализует интерфейс?
                          +2
                          Прежде чем пытаться это выяснить, стоит понять — а надо ли оно вам? Структуры пишут так, чтобы они реализовывали интерфейсы, а не наоборот.

                          Про SOLID не вижу смысла отвечать пока не услышу какой-то конкретной критики
                            –4
                            Ну если вам не надо, то мне даже очень, хочется «джамп ту дефинишн» и видеть что реализовано а что нет, без копания в неудобной документации. Про SOLID это состоявшийся факт, ответа не требует.
                              –5
                              Вам компилятор сам говорит, почему вы не можете использовать данную структуру как реализацию интерфейса
                              Например
                              package main
                              
                              type Interface interface {
                                      Read()
                                      Write()
                                      Close()
                              }
                              
                              type Implementation struct{}
                              
                              func (impl Implementation) Read() {}
                              
                              func (impl Implementation) Write() {}
                              
                              func main() {
                                      var obj Interface
                                      obj = new(Implementation)
                              }
                              


                              Выдаст вам ошибку
                              # command-line-arguments
                              ./file.go:15:6: cannot use new(Implementation) (type *Implementation) as type Interface in assignment:
                              *Implementation does not implement Interface (missing Close method)
                                +7
                                То есть, вы билдите, смотрите ругается компилятор или нет, и таким образом узнаете реализован интерфейс? Очень удобно, я бы не догадался.
                                  0
                                  Включите подсказки в IDE и не мучайтесь.
                                  VSCode с плагином Go на подобную каку прекрасно орёт.
                                    +2
                                    Зачем мне «орёт», когда я хочу знать что реализует структура?
                                      0
                                      Определитесь уже, что вы хотите. Если вы хотите написать структуру, которая реализует интерфейс, то человек выше написал про IDE, а если что-то иное, то сообщите
                                        +2
                                        Чего я хочу я написал в 18:27. Повторю ещё раз, утиная типизация не прозрачна, от этого не удобна. И в языке для «тупых» (так его вроде позиционируют) это минус. Что с вами компилятор разговаривает я выше видел.
                                          +3
                                          Коллеги стесняются сказать, что нет других способов указания реализуемых интерфейсов, кроме как прописать это в комментариях к коду на человеческом языке (английском, русском, ...).
                                      +1

                                      Гитхаб, например, тоже орёт при просмотре мерж-реквеста?

                                      –1
                                      Не нужно билдить, нужно в IDE настроить статический анализатор кода — он прекрасно справится с такими предупреждениями.

                                      ЗЫ но в сборке go тоже очень быстр, в отличии… Так что даже проверка сборкой — не так напрягает.
                              0
                              Здесь ведь нет, цикличных зависимостей в привычном понятиями и известными недостатками?
                              Высокоуровневый модуль F не зависит от конкретной реализации, а B лишь реализует интерфейс, зачем полагаться на утиную типизацию и лишаться проверок компилятора?
                                0
                                В таком случае пришлось бы разрешить циклический импорт между пакетами, а так его нет

                                Кстати, вы не лишаетесь проверок компилятора. См. мой другой комментарий
                                  0

                                  Интерфейс в в отдельном пакете, из которого его импортируют и клиент, и сервис. Если помещаем интерфейс в пакет сервиса, то мы заставляем клиента импортировать и реализацию интерфейса, которая ему может быть и не нужна. А, главное, что делать другим реализациям сервисам? Импортировать из первой? Копипастить при создании и не забывать синкать при изменении в в первой реализации?

                                +2

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


                                То есть, например, если в каком-нибудь C# я вижу:


                                class Foo : IEquatable<Foo>

                                я знаю не только то, что существует Foo.Equals(other), а еще и то, что он задает отношение эквивалентности.

                                  –5
                                  Мне кажется, что сигнатура является достаточным контрактом. Мне очень сложно представить, что ещё может делать Equals(other). Если сравнивать хэши, то я бы предпочёл назвать HashEquals() и т. д.
                                    0
                                    Часто использующийся как пример контракт — баланс не может быть меньше нуля. Какая сигнатура должна быть у метода списывающего средства с баланса, чтобы по нему было явно это заметно?
                                    Пока могу представить лишь throws с говорящим именем исключения в языках с checked exceptions, типа Java
                                      –1

                                      Интерфейс не обеспечивает такой контракт. Кроме редких язьіков.


                                      С другой стороньі подчеркнуть существование такого контракта лучше интерфейсом. А утиной типизацией даже заявить о существовании не получится.

                                  +3
                                  Господа, не путайте утиную типизацию и структурную типизацию (Structural type system).

                                  В Go нет утиной типизации.

                                  Визуально утиная типизация и структурная типизация могут быть похожи, но это не одно и тоже.

                                  ...Duck typing is similar to, but distinct from structural typing. Structural typing is a static typing system that determines type compatibility and equivalence by a type's structure, whereas duck typing is dynamic and determines type compatibility by only that part of a type's structure that is accessed during run time...
                                    0

                                    Некоторые используют термин «статическая утиная типизация»; например, в применении к шаблонам C++.

                                  Only users with full accounts can post comments. Log in, please.