Базовые вопросы по Golang
Характеристики Go
Императивный
Императивное программирование — это парадигма, основанная на составлении алгоритма действий (инструкций/команд), которые изменяют состояние (информацию/данные/память) программы.
Декларативное программирование — это парадигма, при которой описывается желаемый результат, без составления детального алгоритма его получения. В пример можно привести HTML и SQL.
Компилируемый в нативный код
Статическая типизация Стати́ческая типиза́ция — приём, широко используемый в языках программирования, при котором переменная, параметр подпрограммы, возвращаемое значение функции связывается с типом в момент объявления и тип не может быть изменён позже.
Нет классов, но есть структуры с методами
Есть интерфейсы
Нет наследования, но есть встраивание
Функции - объекты первого класса. Функции первого класса - это означает, что язык поддерживает передачу функций в качестве аргументов другим функциям, возврат их как результат других функций, присваивание их переменным или сохранение в структурах данных
Есть замыкания Замыкание - это функция, которая ссылается к переменным вне ее тела. Функция имеет доступ к связанным переменным, а также может присваивать им значения; в этом смысле функция "связана" с этими переменными.
Функции могут возвращать больше 1 значения
Есть указатели, но нет адресной арифметики
Обширные возможности для конкурентности
Сборка в 1 бинарный файл
Набор стандартный инструментов
Какие технологические преимущества экосистемы Go вы можете назвать?
Прекрасная стандартная библиотека
Стандартная библиотека Go действительно великолепна, особенно применительно к разработке сетевых протоколов или API: в ней есть HTTP-клиент и сервер, шифрование, форматы архивирования, сжатие, отправка писем и так далее. Есть даже парсер HTML и довольно мощный движок шаблонов, что позволяет создавать текст и HTML с автоматическим экранированием (automatic escaping) для защиты от XSS (к примеру, используется в Hugo).
Чем вам нравится golang?
Простой, надежный и продуктивный.
Открытый исходный код.
Быстрая компиляция и выполнение.
Простое управление зависимостями.
Качественно составленные полезные пакеты stdlib.
Простая обработка ошибок.
Один бинарный файл для управления всеми другими.
Кросс-компиляция.
Сборка мусора.
Читабельность.
Встроенное тестирование.
Профилирование.
Статическая или динамическая типизация? строготипизирован или нет
Go — язык со строгой статической типизацией. Доступен автоматический вывод типов. Для пользовательских типов используется «утиная типизация».
В 1974 году Лисков и Зиллес (англ. Liskov and Zilles) назвали сильно типизированными те языки, в которых «при передаче объекта из вызывающей функции в вызываемую тип этого объекта должен быть совместим с типом, определённым в вызываемой функции»[3].
Что вас огорчает в системе типов Go?
Отсутствие перечислимых типов. Вместо них используются группы констант, но все константы-значения фактически являются целыми числами, группы эти синтаксически не объединены, и компилятор не может контролировать их использование. Невозможно описать тип, связанный с перечислением, например, массив, содержащий по одному элементу на каждый элемент перечисления (который в Паскале описывается конструкцией вида
type EnumArray = array[EnumType] of ElementType
), создать цикл по перечислению, компилятор не может контролировать полноту списка альтернатив в конструкцииswitch
, когда в качестве селектора используется значение перечисления.Недостаточность встроенных контейнерных типов данных. Встроенные в язык контейнеры ограничиваются массивами и отображениями, а контейнеры, реализуемые средствами самого языка (в том числе входящие в стандартную библиотеку), нетипобезопасны из-за вынужденного использования в них элементов типа
interface{}
. К тому же их невозможно обходить с помощью конструкцииfor range
.Отсутствие явного указания на реализацию интерфейса типом затрудняет понимание кода, его модификацию и рефакторинг. Компилятор не может автоматически проверить тип на соответствие реализуемым интерфейсам. Также возможна (хотя и маловероятна) «случайная реализация» интерфейса, когда методы типа совпадают по сигнатурам с методами интерфейса, но по смыслу не являются реализацией представляемого интерфейсом поведения.
нет обобщённых типов (generics)
Почему на Go практически не пишут расширений для других языков и динамических библиотек?
Go - императивный или декларативный? А в чем разница?
Golang - императивный язык программирования
Императивное программирование — это парадигма, основанная на составлении алгоритма действий (инструкций/команд), которые изменяют состояние (информацию/данные/память) программы. Первыми языками программирования, основанными на таком подходе, были машинные коды и ассемблеры. Фактически, программа на этих языках — это код, который выполняется компьютером сразу, без предварительной компиляции. Из языков высокого уровня, требующих компиляции исходного кода программы в машинный код (или интерпретации), к императивным можно отнести C, C++, Java.
Декларативное программирование — это парадигма, при которой описывается желаемый результат, без составления детального алгоритма его получения. В пример можно привести HTML и SQL. При создании HTML мы с помощью тегов описываем, какую хотим получить страничку в браузере, а не то, как нарисовать на экране заголовок статьи, оглавление и текст. В SQL, если нам нужно посчитать количество сотрудников с фамилией «Сидоров», мы напишем SELECT count(*) FROM employee WHERE last_name = 'Сидоров';
. Тут ничего не сказано про то, в каком файле или области памяти находятся данные по сотрудникам, как именно выбрать из них всех Сидоровых и нужно ли вообще это делать для подсчёта их количества.
Заповеди Роба Пайка
https://habr.com/ru/post/272383/
Don't communicate by sharing memory, share memory by communicating.
«Не общайтесь разделением памяти. Разделяйте память через общение.»
Этот постулат знают все, он звучал неоднократно в докладах и постах посвященным паттернам конкурентного программирования в Go. Этот постулат призван дать понимание сути механизма работы каналов и горутин — ваши функции не делят одну и ту же память, они безопасно общаются, передавая данные по каналам.
Concurrency is not parallelism.
«Конкурентность — это не параллелизм»
Не уверен, что слово concurrency сколько-нибудь удачно переводится на русский, но суть в том, что параллелизм — это просто параллельное выполнение одного и того же кода, а софт, полноценно использующий преимущества многоядерности — конкурентный софт — это конкурентность, тоесть способ структурироания логики программы. Новички очень часто путают эти два понятия. Есть отличное выступление Пайка с одноименным названием, обязательно посмотрите, кто ещё не.
The bigger the interface, the weaker the abstraction.
Чем больше интерфейс, тем слабее абстракция
Новички в Go, особенно пришедшие с Java, часто считают, что интерфейсы должны быть большими и содержать много методов. Также часто их смущает неявное удовлетворение интерфейсов. Но самое важное в интерфейсах не это, а культура вокруг них, которая отображена в этом постулате. Чем меньше интерфейс, тем более он полезен. Пайк шутит, что три самых полезных интерфейса, которые он написал — io.Reader, io.Writer и interface{} — на троих в среднем имеют 0.666 метода.
Make the zero value useful.
Делайте нулевое значение полезным
Речь о том, что лучше делать нулевое значение типов полезным безо всяких конструкторов. Конечно, иногда функции-конструкторы необходимы, но если есть возможность сделать переменную вашего типа рабочей «прямо из коробки» — это всегда идёт на пользу. Пример — тип bytes.Buffer или sync.Mytex из стандартной библиотеки. Пустое значение — это готовый к использованию буфер, хотя и существуют конструкторы вроде bytes.NewBuffer и bytes.NewBufferString().
interface{} says nothing.
Пустой интерфейс ни о чём не говорит
Этот постулат говорит о том, что интерфейсы — «поведенческие типы» — должны что-то означать. Если вы создаёте интерфейс, это что-то означает и служит конкретной цели. Пустой же интерфейс (interface{}) ничего не означает и ни о чём не говорит. Есть ситуации, когда его нужно использовать, но они чаще исключение — не используйте interface{} без повода. Новички часто переиспользуют пустые интерфейсы, и масса вопросов на Stack Overflow именно о них.
Gofmt's style is no one's favorite, yet gofmt is everyone's favorite.
Стиль Gofmt — это не чье-то предпочтение, но gofmt предпочитают все
Многие новички, борясь с привычками, жалуются на то, что код в Go отформатирован не так, как им нравится. Gofmt — это единый стиль, он не является чьим-то личным предпочтением. Даже сам Грисмайер, автор утилиты go fmt, сам предпочёл бы другой стиль форматирования. Но с того момента, как формат был утверждён и gofmt используется везде и всегда — просто примите это как должное. Опытные программисты считают go fmt спасением, так как он очень упрощает работу с чужим кодом — ад с разными стилями форматирования в других языках ушёл в прошлое.
Примечательно, что новые языки перенимают этот подход (к примеру, rustfmt) — это то, о чём говорил Dave Cheney в своем выступлении «Наследие Go» — новые языки больше не будут считаться полноценными без единого стиля форматирования и утилиты, обеспечивающей это.
A little copying is better than a little dependency.
Небольшое копирование лучше небольшой зависимости
Это уже относится к программированию в целом, но в Go это видно и по стандартной библиотеке, и в культуре в целом. Отчасти это перекликается с известной установкой «Дупликация дешевле плохой абстракции». Пайк рассказывает, как в первые дни в Google ему сказали, что больше всего пекутся о реиспользовании кода. Если можно избежать написания одной повторяющейся строчки, сделав импорт — нужно так и поступать. По мнению Пайка, Google до сих пор разгребает проблемы с кодом из-за этого подхода. Очень часто функционал может быть похож (до первого рефакторинга), или необходима лишь часть функционала из зависимости, и зачастую бывает эффективней написать 5 строчек, чем импортировать 150кб зависимостей. Как пример — функция IsPrint из пакета strconv — она дублирует функционал unicode.IsPrint(), при этом тесты проверяют, что функции работают одинаково. Резюмируя — не бойтесь копирования кода, там где это оправдано.
Syscall must always be guarded with build tags.
Код с syscall-вызовы должен содержать build-теги
В тему недавно переведённой статьи «Как писать Go код, который легко портируется». Некоторые жалуются на пакет syscall в Go — нужно его изменить, он не портируется. Но syscall-ы это по определению платформозависимые вызовы, поэтому если вы хотите делать качественный кроссплатформенный код — всегда добавляйте build-теги в файлы, в которых используются syscall.
"Channels orchestrate; mutexes serialize"
Каналы — оркестрируют, мьютексы — сериализируют
Хороший постулат в тему предыдущей статье («Танцы с мьютексами в Go»), о том, когда использовать мьютексы, а когда каналы. Мьютексы просто сериализируют исполнение доступа к чему-либо, но горутины и каналы дают вам инструмент создавать программы со сложной архитектурой и структурой обмена данными между частями программы.
Cgo is not go
Cgo это не Go
Многие радуются тому, как легко в Go использовать C-код. Иногда, это, конечно, необходимо, но Пайк признается, что сам никогда не использовал Cgo, и в мире Go есть четкое понимание памяти, стабильность, безопасность, сборка мусора… и с Cgo всё это идёт в топку. В 90% случаев в Google, когда кто-то говорит «моя программа крашнулась, помогите разобраться» — дело оказывается в Cgo.
"With the unsafe package there are no guarantees"
С пакетом unsafe нет гарантий
Тут делается акцент на том, что код написанный с использованием пакета unsafe также не портируемый, и на него не распространяются гарантии обратной совместимости в пределах Go 1 (это написано в первой же строке документации к пакету). Пока что в нём ничего не меняли, но если вы собираетесь использовать зачем-то unsafe — будьте готовы, что в будущих версиях, придётся подстраиваться под изменения.
Clear is better than clever
Ясно лучше, чем заумно
Постулат о том, что в Go ясность и читабельность кода стоит на первом месте. И вы должны также стремиться писать ясный и понятный код, вместо заумного. Ваш код рано или поздно будут читать, улучшать и рефакторить другие программисты, и чем яснее он будет, тем проще и быстрее будет результат. Go будет всеми способами и силами вам в этом помогать и способствовать, весь дизайн языка пропитан этой целью.
Reflection is never clear
Рефлексия никогда не понятна
Рефлексия (способность программы менять собственную структуру) в Go реализована пакетом reflect, и люди нередко пытаются использовать её не к месту. Рефлексия — это очень-очень мощная вещь, но очень сложная для понимания, и на самом деле нужна очень малому количеству людей и задач. Пайк говорит, что написал, вероятно, больше всех кода с рефлексией в Go, и до сих пор ненавидит это делать. И считает, что нужно стимулировать людей не использовать рефлексию, поскольку она, как правило, не нужна так, как им это кажется.
Errors are values
Ошибки это значения
Ещё один важный постулат, который уже неоднократно освещался на хабре, и которому, в частности, посвящена статья в официальном блоге Go. Новички часто не понимают этого и спрашивают — «почему я должен везде писать эти if err != nil». Да потому что вы не программируете, вы клепаете код. Многие считают, что if err != nil — это такая альтернатива try..catch… — специальная конструкция языка для работы с ошибками. Но это не так, вы не можете программировать с try… catch… — это управляющая конструкция. С ошибками же в Go можно и нужно делать то, что требует логика программы.
Dont’ just check errors, handle them gracefully
Не просто проверяйте ошибки, обрабатывайте их красиво
Ошибки очень важно не просто проверять, но и думать, что с ними делать, чтобы ошибочная ситуация была обработана корректно. Возможно нужно дополнить ошибку какой-то информацией, возможно запомнить для обработки позднее, что-нибудь ещё. Огромная и очень важная часть программирования — это как вы обрабатываете ошибки. Не просто проверять и пробрасывать наверх, а думать о том, как правильно обработать эту ошибку. Это справедливо для всех языков, но поскольку в Go ошибки это просто значения, то это проще сделать и проще реализовать красивую и правильную обработку ошибок в коде.
"Design the architecture, name the components, document the details"
Продумывайте архитектуру, называйте компоненты, документируйте детали
Тут речь о важности правильного именования компонентов вашего кода, методов, функций и переменных. Эти имена будут потом находится в коде пользователей вашего пакета. И если имена удачны и понятны, дизайн программы будет более ясен и прост для понимания. Но, конечно, всегда есть детали, которые нужно объяснить — и вот их нужно помещать в документацию.
"Documentation is for users"
Документация — для пользователей
О том, что документация к функции должна описывать не что эта функция делает, а для чего она нужна. Это очень важное различие, которое зачастую упускается из виду. Когда пишете документацию, посмотрите на неё глазами пользователя вашего пакета.
Что такое type-switch?
switch
может быть использован для определения динамических типов интерфейсных переменных. Так, типизированный switch
использует синтаксис приведения типов, с ключевым словом type
внутри скобок. Если switch
объявляет переменную в выражении, то переменная будет иметь соответствующий тип в каждом пункте.
Переключатель типов сравнивает типы, а не значения. В остальном он аналогичен переключателю выражений. Он помечается специальным выражением-переключателем, которое имеет форму утверждения типа, используя зарезервированное слово type, а не фактический тип:
switch x.(type) {
// cases
}
Затем случаи (cases) сопоставляют фактические типы T с динамическим типом выражения x. Как и в утверждении типа, x должен иметь интерфейсный тип, и каждый неинтерфейсный тип T, указанный в случае (case), должен реализовывать тип x. Типы, перечисленные в случаях переключения типов, должны быть разными.
Вместо типа случай (case) может использовать предварительно объявленный идентификатор nil; этот случай выбирается, когда выражение в TypeSwitchGuard имеет нулевое значение интерфейса. Может быть не более одного нулевого случая.
Пример:
switch i := x.(type) {
case nil:
printString("x is nil") // тип i это тип x (interface{})
case int:
printInt(i) // тип i это int
case float64:
printFloat64(i) // тип i это float64
case func(int) float64:
printFunction(i) // тип i это func(int) float64
case bool, string:
printString("type is bool or string") // тип i это тип x (interface{})
default:
printString("don't know the type") // тип i это тип x (interface{})
}
"fallthrough" утверждение запрещено в переключателе типов.
Как сообщить компилятору, что наш тип реализует интерфейс?
Неявная имплементация
Если это выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, утка и есть.
В Go структура с методами будет удовлетворять интерфейсу просто самим фактом объявления метода. Это кажется не особо важным на маленьких программах или искусственных примерах, но оказывается ключевым в больших проектах, где приходится думать дважды перед тем как изменить какой-то класс, многократно унаследованный другими классами. Возможность легко и просто неявно реализовать различные интерфейсы позволяет программам безболезненно расти, без необходимости продумывать все возможные интерфейсы наперёд и не утопать во множественном наследовании. Это к слову о том, что Go задумывался для облегчения жизни в больших проектах. Важное и не сразу очевидное различие этого заключается в том, как, в итоге, вы строите архитектуру вашей программы — в Java или C++ вы, скорее всего, начинаете с объявления абстрактных классов и интерфейсов, и далее переходите к конкретным реализациям. В Go же наоборот — вы пишете сначала конкретный тип, определяете данные и методы, и только в том случае, если действительно появляется необходимость абстрагировать поведение — создаете отдельный интерфейс. Опять же, масштаб этого различия более ощутим на больших проектах.
Значение nil в Golang
Многие языки программирования также используют концепт nil. Среди его других названий — NULL
, null
или None
. В 2009 году перед релизом Go проектировщик языков программирования Тони Хоар выступил с презентацией под названием «Null References: The Billion Dollar Mistake«. В своей речи Хоар утверждал, что он ответственен за изобретение отсылки null в 1965 году. Он также говорил о том, что указатели в никуда были не лучшей идеей.
На заметку: В 1978 Тони Хоар также впервые описал принцип взаимодействующих последовательных процессов, или «communicating sequential processes», CSP. Его идеи лежат в основе конкурентности Go.
В Go nil является более дружелюбен и менее распространен, нежели в других языках программирования, однако и здесь нужно быть готовым к некоторым проблемам. Nil можно использовать не только по его прямому назначению, о чем говорит Франчес Кампой в своей презентации на GopherCon 2016.
Представьте созвездие, где каждая звезда указывает на ближайшую к ней соседнюю звезду. После вычислений, каждая звезда будет куда-то указывать, и нахождение ближайшей звезды становится быстрым разыменованием указателя.
Однако, пока вычисления не закончены, куда должны указывать указатели? Это тот случай, когда пригодится nil. Nil может значить ближайшую звезду, пока реальная звезда не будет найдена.
В какой еще ситуации указатель в никуда может быть полезен?
Вызывает ли nil сбои в Golang?
Если указатель никуда не указывает, попытка разыменования указателя не сработает, что показано в Листинге 1. Разыменование указателя nil приведет к сбою программы. Обычно пользователям такое совсем не нравится.
Я называю это моей ошибкой в миллиард долларов.
Тони Хоар
Листинг
Избежать сбоя несложно. Это вопрос защиты от разыменования указателя nil с оператором if, что показано в следующем листинге.Листинг 2Govar nowhere *int if nowhere != nil { fmt.Println(*nowhere) }
12345
var nowhere *int if nowhere != nil { fmt.Println(*nowhere)}
По правде говоря, сбой в программе может стать следствием многих причин, не только разыменования указателя nil. К примеру, деление на ноль также приведет к сбою, и решение проблемы будет схожим. Даже так, подумайте обо всех программах, написанных в течение последних 50 лет, количество случайных разыменований указателя nil должен быть весьма значительным.
Существование nil обременяет программиста необходимостью принятия дополнительных решений. Должен ли код проверять наличие nil, и если да, то где? Что код должен делать, если какое-то значение равно nil? После всего вышесказанного, так уж ли плох nil?
Вовсе не обязательно постоянно пытаться избегать nil. По правде говоря, в некоторых случаях nil весьма полезен. В дополнение ко всему указатели nil в Go не так популярны, как указатели null в некоторых других языках, и есть способы избежать их использования в случае нужды.
Вопрос для проверки:
Каким будет нулевое значения типа *string
?Ответ
Защита методов в Golang
Методы регулярно получают указатели на структуры, что значит приемник может быть nil, как показано в примере ниже. Происходит ли разыменование указателя явно (*p
) или неявно через получение доступа к полю структуры (p.age
), значение nil вызовет сбой.Листинг 3Gotype person struct {
age int
}
func (p *person) birthday() {
p.age++ // разыменование указателя nil
}
func main() {
var nobody *person
fmt.Println(nobody) // Выводит: <nil>
nobody.birthday()
}
1234567891011121314
type person struct { age int} func (p *person) birthday() { p.age++ // разыменование указателя nil} func main() { var nobody *person fmt.Println(nobody) // Выводит: <nil> nobody.birthday()}
Скорее всего, сбой вызван после выполнения строки p.age++
. Удалите данную строку, тогда программа запустится.
На заметку: Сравните это с аналогичной программой в Java, где приемник null приведет к сбою программы сразу после вызова метода.
Go вызывает методы даже в том случае, если у приемника значение nil. Приемник nil ведет себя так же, как и параметр nil. Это значит, что методы могут защищать от значений nil, как показано в следующем примере.Листинг 4Gofunc (p *person) birthday() { if p == nil { return } p.age++ }
123456
func (p *person) birthday() { if p == nil { return } p.age++}
Вместо проверки на наличие nil
перед вызовом метода birthday
предыдущий листинг защищает от приемников nil внутри метода.
На заметку: В Objective-C автоматический запуск метода для nil не приводит к сбою, но при вызове метода будет возвращаться нулевое значение.
С тем, как управлять nil
в Go, разобрались. Методы могут возвращать нулевые значения, возвращать ошибки или приводить к сбою.
Вопрос для проверки:
Что делает доступ к полю (p.age)
, если p
является nil?Ответ
Значения функций nil в Golang
Last updated
Was this helpful?