Pull to refresh

Взгляд на Go глазами .NET-разработчика. Неделя #1

Reading time8 min
Views18K
Всем привет!

Меня зовут Лекс и я ведущий youtube-канала "АйТиБорода". А еще я дотнетчик со стажем в 6 лет. Недавно у меня появилось желание выйти за рамки своей основной технологии (C#/.NET), и познать суть парадокса Блаба. Я твердо решил, что попробую себя в другом языке, и выбор волей случая пал на Go.

Для того, что бы получать знания структурировано, я записался на курс от mail.ru, который вы можете найти тут: https://www.coursera.org/learn/golang-webservices-1. Собственно о том, что я вынес из первой недели обучения на этом курсе, дальше и пойдет речь. Погнали!


Начну с небольшого интро по языку. Go разрабатывался уже во времена многоядерных процессоров (2007-2009 года), поэтому с распараллеливанием работы по ядрам тут всё очень хорошо. К тому же, язык отлично работает с конкурентными (одновременными) запросами. В общем, находка для всяких там веб-сервисов и нагруженных веб-систем. Как раз, то что нужно для меня, ибо я последние года занимаюсь разработкой веб-эйпиаев (Web API).

Начало


Для работы с языком достаточно установить программный пакет «Go tools» размером 118мб, и можно начинать кодить.

Скрипты имеют расширение *.go и запускаются командой go в командной строке (я адепт windows command line). Команда gofmt расставляет все отступы-пробелы и делает из файла конфетку (code beautifier из коробки). Как и в моём любимом дотнете, в Go исполнение программы начинается с метода main.
Сразу бросилась в глаза крутая фича — можно не ставить точку с запятой в конце строки :)

Go — еще один язык (на ряду с python, c#, php, js, ts и другими), с которым удобно работать в VSCode. VSCode сама доставляет абсолютно все нужные пакеты и зависимости по ходу написания кода. При сохранении IDE любезно за вас запускает gofmt и делает код еще краше.

Переменные


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

Вообще, тут много способов объявления и инициализации переменных. Можно даже присваивать значения нескольким переменным через запятую в одну строку. И кстати, нет неявных приведений.

А еще в Go есть оператор ":=", который позволяет объявлять и сразу инициализировать новые (и почти всегда только новые) переменные. Привет паскалю ;)

Запись вида:

perem1, perem2 := 2, 3
либо создает две новых переменных, либо создает одну из них, а второй просто присваивает новое значение. Это я к тому, что если обе переменных уже существуют, то такое не прокатит и вы получите ошибку, мол используйте обычное присвоение. Оператор := обязательно должен что-то создавать :)

А еще есть забавный фейк — символ подчеркивания "_". Он обозначает отсутствие переменной, как-то так :) (в шарпах, как оказалось, он тоже есть: https://docs.microsoft.com/en-us/dotnet/csharp/discards)

image

Забавный момент со строками: стандартный подсчет длины строки строки len(str) считает байты, а в UTF8 — символ может весить больше. Для подсчета символов, именуемых рунами, нужно использовать метод utf8.RuneCountInString(str) из пакета «utf8». А, и еще — строки не изменяем (прям как в любимом дотнете).

Константы


Константы можно объявлять пачками. У констант есть интересный то ли тип, то ли что — iota. Буду называть это Йопта. Йопта позволяет делать одни константы на основании других, что-то типа итератора для констант (чем-то напомнило yield из дотнета).

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



Указатели


Да-да! Тут есть указатели.Но вроде не сильно страшные. Говорят, что они не нумерованные, и нельзя прибавив к указателю чет-та, получить следующий указатель. Тут это вроде как переменная, которая хранит ссылку на другую переменную (пока как-то так понял).

Указатель задается через амперсант либо метод new(type) (для получения указателя на тип, а не переменную):



Если обращаться напрямую к указателю, то меняется ссылка. А если через оператор "*", то меняется значение переменной, которая лежит за указателем (на которую он указывает). В общем, пока не сильно понятно, но дальше, говорят, будет яснее.

Массивы


Размер массива — часть типа данных! Так, массивы разной размерности по факту являются разными типами данных и не являются совместимыми.



Но нафиг нужны массивы, если они такие сухие и неизменяемые (их нельзя менять в рантайме). Поэтому придумали такие слайсы (срезы), основанные на массивах.



У слайса есть длина (length) и вместимость (capacity). Мне это слегка напомнило тип данных nvarchar из SQL. Т.е. можно выделять место в памяти (аллокация памяти) под свои массивы, но класть туда массивы любой длины (до capacity). Короче, штука интересная, нужно к ней привыкнуть. Для удобного использования слайсов есть всякие методы типа make(), append(), len(), cap() и, наверняка, другие. Из одного среза (слайса) можно слегка получать подсрез (срез среза). И кстати, append() расширяет капасити слайса.

Если подсрез равен срезу, то по факту они будут ссылаться на один участок памяти. И соответственно, изменение значения в одном слайсе приведет к изменению во втором слайсе. Но если кого-то из них расширить через append(), то будет выделен новый участок памяти.
Короче, с памятью тут всё серьезно :)

Map, хеш-таблица, ассоциативный массив


Всё это одно и то же. В Go есть хеш-таблицы для быстрого поиска по ключу. Инициализируются так:



Можно делать подтаблицы (мапы мап) и т.д. (любой уровень вложенности, вроде как). Есть значение по умолчанию, которое возвращается на несуществующие ключи. Берется оно из типа ключа (для bool — то false). Что бы знать, вернулась ли переменная по умолчанию, т.е. что ключ отсутствует — используют признак существования ключа (интересная штука):



Управляющие конструкции


Есть if-else. Слегка поинтереснее, чем в шарпах, ибо можно использовать условия инициализации для if.

Есть swith-case. Брейки ставить не нужно, и это отличие. Инвертация логики) Если нужно, что бы следующее условие тоже проверилось — нужно писать fallthrough.

Есть for. И всё с циклами.



С итерированием по слайсам повеселее (оператор range):



И даже по хеш-мапе можно итерировать (хотя последовательность будет часто разная, ибо мапа не направленная):



Для типа string range итерирует по рунам, а не по байтам.

Функции


Объявляется через ключевое слово func. Причем снова инверсия логики по сравнению с дотнетом — сначала указываются входящие параметры, а затем тип возвращаемого значения (можно сразу именованный).

Функции могут возвращать несколько результатов, прямо как кортежи (typle) в дотнете.

Именованные значения, которые возвращает функция, по умолчанию инициализируются со значениями по дефолту для типа.

А еще, можно делать вариативные функции, которые имеют неограниченное количество однотипных входных параметров (как param в дотнете). И тогда на вход функции приходит слайс конкретного типа.

Функции могут иметь имена, а могут быть и без имени — анонимными. И их можно вызывать, можно присваивать переменным. Похоже на JavaScript. На основе функций можно даже делать типы! Функции можно передавать как параметры (читай делегаты из дотнета)

Есть даже замыкание (ууух, страшнющее)! Можно из функции достучаться до переменных вне её (читай, в родительской функции). Вот такие пироги.

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

ПАНИКА! В Go есть такая функция — panic(). Это как throw в дотнете, но хуже. Эта функция останавливает выполнение программы. Но эту самую панику можно обрабатывать defer'ом, так как он выполняется по-любому в конце функции, даже после паники. И еще — паника, это не try-catch. Это хуже!

Структуры (почти-объекты)


Говорят, что Go не совсем ООП-парадигменный язык. Но тут есть такие штуки, как структуры. Если вы из мира дотнета, то знаете, что у нас тоже есть структуры — это отображения объектов, которые могут храниться в стеке (типы значений). В Go структура — это вроде как единственный способ сделать что-то на подобии объекта. Про типы значения пока говорить не могу, но природа взаимодействия со структурами очень похожа на дотнетную.

Каждая структура по сути является отдельным типом и может иметь набор свойств конкретных типов (в том числе типа другой структуры или типа функции):



Кроме того тут просматривается некий механизм прямого объектного наследования из дотнета. На картинке выше структура Account вложена структуру Person. Это значит, что все поля из Person будут доступны в переменной типа Account. При совпадении имен свойств (в примере выше Id, Name) конфликта не будет, а будет взято значение более вышестоящего поля:



Методы


Да, тут есть не только подобие объектов, но и методы. Почти ООП :) Методом является функция, привязанная к конкретному типу (например, к структуре). Метод от функции слегка отличается декларированием: после ключевого слова func в скобках нужно задать тип, к которому этот метод относится, и роль передачи переменной этого типа. Что за роль? Если перед типом поставить звёздочку — то переменная передастся по ссылке (вспоминаем указатели, там так же было), и в итоге внутри метода мы будем работать с конкретным типом, а не его копией.



Из картинки выше можно сделать вывод, что UpdateName смысла не имеет, ибо изменяет копию структуры, а не оригинал. И эту копию не возвращает. В то время, как SetName изменит оригинальную структуру (спасибо звездочке и передаче по ссылке).

Методы в структурах наследуются (причем по правилам наследования свойств), т.е. родительская структура имеет доступ ко всем методам вложенных в ее структур.

Методы могут быть и у других типов, не только у структур. Может показаться, что это похоже на методы расширения из дотнета, но нет. Методы в Go можно создавать только для локальных типов, т.е. типов, которые объявлены в данном пакете (о пакетах чуть дальше).

Пакеты, область видимости, неймспейсы


Только сейчас понял, что в Go нет НЕЙМСПЕЙСОВ! Вообще! Осознал это, когда после компиляции вылезла ошибка, мол в одной папке у меня два файла с методом main. Оказывается, что на этапе компиляции все из папочки вроде как склеивается в одно полотно! Папка по факту и есть неймспейс. Вот такая магия, товарищи :)

Вот, кстати, что сказано в доке:



Заметка для себя: раскурить доку
И вот еще статейка в тему: https://www.callicoder.com/golang-packages/#the-main-package-and-main-function

Вот так, говорят, выглядит базовая структура папок:


bin — собранные бинарники
pkg — временные объектные файлы
src — исходники
Пакеты именуют название папки, в которой лежат файлы пакета. Да, у пакета может быть много файлов, которые импортируются друг в друга.

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

Ключевое слово import работает в рамках файла, а не пакета.

Интерфейсы


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

Получается, что в отличие от дотнета, в Go интерфейс — характеристика функции, в которой он применяется, а не характеристика конкретной структуры (объекта).

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

Type Switch-Case


В Go есть специальный switch-case, который может работать в зависимости от входящего типа. Прикольная штука:



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



ok — булев признак того, что преобразование прошло успешно. С признаками мы уже сталкивались :)

Пустой интерфейс — зверь в мире Go. Может принимать вообще любой тип. Что-то типа dynamic или Object в дотнете. Например, используется в fmt.Println() и подобных функциях, которые используют в реализации пустой интерфейс.

Интерфейсы можно встраивать друг в друга, и таким образом делать композицию интерфейсов (то, про что говорится в букве I (interface segregation) принципов SOLID)

Тесты


В Go все функции тестов начинаются слова Test, и на вход принимает параметр тестирующего модуля testing. Файлы именуются по названию тестируемого файла + слово _test (main_test.go — тесты для файла main.go). Причем и тесты и тестируемые файлы находятся в одном пакете!



Эпилог


На этом всё! Спасибо за внимание, и готов обсуждать вопросы в комментариях. Встретимся в следующих конспектах с моих занятий!

P.S. Все мои кодовые блуждания по этой неделе обучения вы можете посмотреть на github
Tags:
Hubs:
Total votes 46: ↑20 and ↓26-6
Comments28

Articles