Go после Python: как я учу новый язык
Привет, Хабр! Меня зовут Максим Чижов, я бэкенд-инженер. Несколько месяцев назад мне понадобилось в довесок к основному языку Python выучить также Go. Расскажу, с какими проблемами я столкнулся в процессе и как их решал, а также где я сейчас.
Зачем мне новый язык
Я программирую на Python с 2010 года. Не так давно я пришёл в Авито на должность middle-инженера, и мне понадобилось изучить также Golang: множество сервисов мы пишем именно на этом языке. К счастью, у меня уже был опыт изучения новых языков программирования вдобавок к основному. К примеру, когда я был студентом, я писал на С++, а когда пошёл работать после ВУЗа, то пришлось выучить Perl, который был в то время популярен. Однако Perl постепенно отмирал и, работая в Билайне, я постепенно перешёл на Python, который тогда мне казался слишком замудрённым.
Но со временем привыкаешь ко всему, привык я и к Python. Развил свои навыки, участвовал в технических собеседованиях, в общем, чувствовал себя как рыба в воде. Но прогресс не стоит на месте.
Много моих коллег рекламировали Go как в хорошем, так и в плохом свете, и мне очень захотелось в него окунуться. Да и повод был достойный — мой юнит начал активно писать сервисы на нём. Памятуя о сложностях в изучении новых языков, я решил подойти к этому процессу системно.
Разведка
Прежде чем приступать к основной части, я решил провести верхнеуровневое расследование, чтобы понять, с чем придётся иметь дело. Бегло изучив популярные статьи, я вынес из них, что Go был создан для достижения определённых возможностей, которых были лишены существующие языки.
Например, это существенное упрощение синтаксиса и уменьшение ключевых слов до минимума. Как следствие, более строгая структура языка, в отличие, например, от С++, где одну и ту же функциональность можно написать разными способами. ИТ-сообщество пестрит мемами про это, высмеивая, например идею, что два разных программиста на С++ могут просто не понять чужой код, потому что пишут на разных С++. Так вот, с Go такого произойти не должно.
Также стоит упомянуть, что Go поддерживает параллелизм, является компилируемым, чистит мусор из коробки и имеет хороший набор для профилирования, что должно делать его сверхбыстрым. Многие согласятся, что именно этого не хватает интерпретируемым языкам, таким как Python, а уж о питоновском GIL не говорил только ленивый.
После прочтения вводных статей я сделал вывод, что Golang вобрал в себя, по мнению его создателей и респондентов, наиболее полезные фишки существующих языков. Так ли это?
Начало пути
Вопросом о том, с чего начать, задавался каждый, кто начинал изучать новый язык программирования. Первая мысль — это пройти какие-то специальные курсы. Однако я начал не с этого.
Чтобы найти точку входа, я воспользовался старым проверенным инструментом Codewars. Порешав задачи типа kyu 7–8, я понял, что это слишком просто, и принялся за более сложные 4, 5, 6. Поработал в таком режиме несколько часов и осознал, что процесс идёт довольно легко.
Но я обратил внимание на одну существенную вещь. Когда ты решил кату, то можешь видеть решения других людей. Если это Python, то решения пестрят разнообразием, от однострочного решения до решения «в лоб», а если это Go, то все решения очень похожи и все они «в лоб», за редким исключением. Чтобы не быть голословным, приведу конкретный пример.
Возьмём кату среднего уровня сложности. Я выбрал стандартную задачу с олимпиад, которую называют Улитка, или Snail. Нам надо развернуть двумерный массив по спирали, начиная с элемента под номером 0,0. Что же мы видим среди решений на Python? Вот, например, решение «в лоб»:
def snail(s):
n = len(s)
vis = [[0]*n for i in range(n)]
i = 0
j = 0
dir = 'f'
ret = []
if n == 0: return ret
if n == 1: return s[0]
while True:
if vis[i][j] == 1:
return ret
ret.append(s[i][j])
vis[i][j] = 1
if dir == 'f':
if j+1>=n or vis[i][j+1] == 1:
dir = 'd'
i += 1
else:
j+=1
continue
if dir == 'd':
if i+1>=n or vis[i+1][j] == 1:
dir = 'b'
j-=1
else:
i+=1
continue
if dir == 'b':
if j-1 < 0 or vis[i][j-1] == 1:
dir = 'u'
i-=1
else:
j-=1
continue
if dir == 'u':
if i-1<0 or vis[i-1][j] == 1:
dir = 'f'
j += 1
else:
i -= 1
continue
Решение довольно прозрачно, и его может прочитать даже человек, который просто знаком с программированием, но не знаком с Python. «Питоновские гуру» же предлагают и такое элегантное решение с рекурсией:
def snail(array):
return list(array[0]) + snail(list(reversed(zip(*array[1:])))) if array else []
Да, в комментах пишут, что решение имеет более высокую сложность, чем оптимальное, но тем не менее, это красиво. Кто-то даже предполагает божественную сущность этого решения.
Я привёл в пример лишь две крайности в решении этой задачи на Python. Если же вы решите её сами, вам будут доступны все решения пользователей, и вы увидите, что вариантов — бесчисленное количество.
Давайте теперь посмотрим, что происходит в решениях на Go. Большинство из них похожи на первое решение для Python, а остальные отличаются лишь небольшими вариациями.
package kata
func Snail(snailMap [][]int) []int {
xmin := 0
ymin := 0
xmax := len( snailMap[0]) - 1
ymax := len( snailMap) - 1
resultln := len(snailMap[0]) * len(snailMap)
result := make([]int, 0)
for len(result) < resultln {
for x := xmin; x <= xmax; x++ {
result = append(result, snailMap[ymin][x])
}
ymin++
for y := ymin; y <= ymax; y++ {
result = append(result, snailMap[y][xmax])
}
xmax--
for x := xmax; x >= xmin; x-- {
result = append(result, snailMap[ymax][x])
}
ymax--
for y := ymax; y >= ymin; y-- {
result = append(result, snailMap[y][xmin])
}
xmin++
}
return result
}
Таким образом я сделал вывод об относительной строгости синтаксиса Go, что для меня является облегчающим фактором в изучении. Код можно читать даже будучи не особо знакомым с языком.
Если вы не знаете, с чего начать, начните с малого. Codewars я взял только потому, что сам им пользуюсь. Наверняка у вас есть любимый ресурс с задачами по программированию. Решив пару задач, вы поймёте свой уровень. Исходя из этого сможете остаться и решать задачи дальше, либо перейти на следующий уровень обучения.
Следующий уровень
Я был готов перейти к следующему шагу, решив, что основы языка мне понятны. А следующим шагом выбрал продвинутый курс по Go, который включает в себя более 30 занятий по 2 часа. Потратив 10 часов своего бесценного времени, я понял, что тут что-то не то. И действительно, информацию одного урока можно было бы усвоить за 15 минут: остальное время преподавателя уходило на рассуждения и ответы на вопросы слушателей.
По моему опыту, КПД курсов составляет около 20% из-за большого количества «воды». Я принял решение не продолжать тратить время, взял перерыв на подумать и пошёл дальше.
Проверенный метод
Глянув документацию к Go, я был приятно удивлен её подробностью и лёгкостью. Но, как известно, наши люди смотрят инструкцию, только когда что-то ломается. В их числе и я. Первым делом я решил опробовать не так давно появившиеся в Go модули. Скачав GoLand, я пошёл в бой.
Попробуем создать работающую программу на Go. Создаём новый проект под названием article, GOPATH пока отключен.
Напишем простой код в файле start.go:
А теперь запустим go run start.go. Выдаёт ошибку:
Ага! Значит нужен какой-то главный пакет. Ок, переименуем его:
Теперь запускается!
Хочу теперь открыть CSV файл, например. Что мне может для этого потребоваться? Наверное, библиотека для работы с CSV файлами. Гуглим и находим вот такой вариант. Попробуем его импортировать:
Но почему он выделен красным? Видимо, его надо установить. GoLand предлагает сделать это одним нажатием:
Отлично, пакет установлен, и я могу его использовать. Попробуем открыть несуществующий файл из примера:
Странно, почему подсвечивает красным метод UnmarshalFile, ведь я установил пакет?
Да и код нормально запускается:
После небольшого гугления, идём Preferences — Go — GOPATH и ставим галочку в поле Index entire GOPATH. Красный цвет ушёл, GoLand увидел директорию с пакетами.
Отлично, теперь попробуем создать свой модуль. Несмотря на отличный туториал, я, конечно, им не воспользуюсь. Попробую сделать всё интуитивно. Создадим директорию my_mod с файлом some.go, который будет содержать следующее:
Теперь попробуем вызвать её из main:
Прошу заметить, как импортируется модуль. Однако почему-то такая функция не найдена. Немного гуглим и исправляем ситуацию:
Модуль работает! Оказывается, для импорта нужно, чтобы функция начиналась с заглавной буквы. Вполне очевидное поведение. Теперь можно и в документацию заглянуть. Create a Go module — очень хорошее пособие, но оно говорит, что мой модуль — это вовсе не модуль. Проделав все описанные в документации действия, понимаю, как на самом деле работают модули в Go. Как быть с моим способом импорта? Можно ли его использовать? Оставлю эти вопросы открытыми, точнее, отдам на откуп читателю.
Ну что ж, работа с Go довольно проста и комфортна, однако чтение документации и реализация примеров никак не добавит скиллов. Это важно, чтобы только пощупать инструменты для работы. Однако не стоит стесняться делать такие примитивные действия, они тоже полезны.
В бой
После этого многие добавляют в своё резюме новый навык и начинают искать работу, но я пошёл другим путем. Наконец-то в бэклоге спринта мне попалась задача на Go, и я с радостью взял её. За время изучения языка и решения задачи я понял о Go несколько вещей.
Стандартные инструменты. Очень не хватает стандартных инструментов, например, для работы со строками и массивами. В частности, я был крайне удивлён отсутствию сортировки слайсов. Сначала хотел достать из закромов своего разума сильно запылившийся метод быстрой сортировки, но вовремя одумался и нашёл пакет sort.
Sort как раз и предлагает тот же метод, но не гарантирует стабильности. Кроме того, он предоставляет довольно интересные функции, например symMerge, которая позволяет оптимально слить два отсортированных слайса так, чтобы результат тоже был отсортирован. К слову, эта задача довольно часто попадается на собеседованиях по алгоритмам.
Строгая статическая типизация. Это очень болезненный момент для программистов, которые привыкли к динамической. При первом рассмотрении это вызывает сильное отторжение, а в некоторых случаях даже депрессию.
Теперь на объявление переменной уходит довольно большое количество времени, так как разработчик должен предсказать её тип. А если тип назначен неверно, например int32 вместо int64, это может вызвать вал проблем в дальнейшем. Однако есть и плюсы в таком подходе. Например, значительное уменьшение количества ошибок, многие из которых отлавливаются ещё при компиляции.
Неявный return. Подводный камень, который для неопытного Go-разработчика может вылиться в неявные ошибки. Для Python-разработчика return без аргументов означает, что функция вернёт None. В Go это не так.
func Insert(docs ...interface{}) (err error) {
for i := 0; i < 3; i++ {
err = fmt.Errorf("")
if err.Error()!="EOF" {
return
}
}
return
}
В данном случае функция вернёт значение переменной err. Вроде бы это удобно, но хочется, чтобы явное было лучше, чем неявное.
Исключения. В Go нет исключений как таковых. Точнее, есть panic, но многие придерживаются мнения, что паниковать — это не Go-way. Поэтому часто функции и методы возвращают два значения: целевое и ошибку. Возврат нескольких значений мне довольно привычен после Python, но приходится явно проверять ошибки и код становится ужасным на вид:
err, val := somefunc()
if err != nil {
log.Logger.Error(err)
}
Подобные куски кода, а порой и вложенные друг в друга добавляют шума в чёткую организацию.
Оператор defer.Данный оператор стал чем-то новым для меня. Ранее я не сталкивался с подобным, и он показался мне удобным. Некий триггер возврата функции, который позволяет сократить количество кода.
ООП.Go — функциональный язык, он не предоставляет стандартных средств ООП как таковых. Это, конечно, вызывает у многих болезненные ощущения. И, несмотря на то, что с помощью конструкции Interface мы можем имитировать ООП, это всё ещё очень сырой механизм.
Типичная проблема возникает, когда нужно добавить ещё один метод для сущности. В этом случае питонист просто добавил бы этот метод в родительский класс и переопределил его там, где требуется. В Go же мы вынуждены добавлять метод во все имплементации, так как в противном случае можем нарушить один из принципов SOLID — принцип подстановки Барбары Лисков.
Тесты.Достаточно удобны и прозрачны и, скорее всего, не вызовут затруднений для питониста. Единственный неявный момент в том, что случайно назвав свой файл, например, my_program_test.go, можно не сразу понять, почему ничего не работает. А причина в том, что все тесты должны иметь шаблон названия *_test.go.
Многопоточность.После GIL настоящая многопоточность вызывает немыслимый прилив эндорфинов и прочих гормонов радости. Однако при ближайшем рассмотрении становится понятно, что этот мощный инструмент стоит использовать с крайней осторожностью, так как все прелести многопоточного программирования сразу же выливаются на несчастного Python-GIL-ограниченного разработчика. На помощь приходят примитивы синхронизации, при использовании которых невольно всплывает в подсознании С, С++, POSIX и прочая ностальгия из программистского детства.
Горутины.В Python уже давно есть асинхронный код и корутины, так что особых проблем с горутинами не возникло. Стоит отметить, что цикл событий встроен в Go. Однако не стоит забывать о проблемах многопоточности.
Каналы.Для синхронизации работы горутин используются каналы. Сначала к этому инструменту можно отнестись с недоверием, но потом становится понятно, что это довольно удобная фича языка.
У Go ещё много различных инструментов, например, профилирование из коробки, рефлексия и так далее. Однако в статье я не буду заострять на них внимание, а лишь в очередной раз скажу, что разработчики языка предоставили довольно подробную и понятную документацию.
Между делом
Очень рекомендую телеграм-бота Golangquiz. С его помощью вы сможете всегда понять свой уровень владения языком. Ну и просто развлечься небольшими задачами с собеседований.
Что дальше
Несмотря на небольшой опыт в Go, я уже вызвался проводить собеседования. Сначала как слушатель, потом с наставником, а затем уже и самостоятельно. Собеседования для меня делятся на две категории.
Первая — это скрининг. Получасовой блиц опрос об основных навыках не только в языке, но и в смежных областях: базах данных, сложности алгоритмов и т.д. Вопросы довольно стандартные, порой кажущиеся слишком простыми. Но это первый этап отбора кандидатов, и, как ни странно, на нём отсеивается много народу.
Вторая категория — это секция программирования. Здесь кандидат показывает свои способности в решении задач, связанных с алгоритмами. Сложность для собеседующего состоит в том, что кандидат может захотеть написать алгоритм на любом языке. К примеру, очень много кандидатов на PHP, который я знаю поверхностно. А что, если он захочет написать код на Java? А если на Haskell?
На момент написания статьи я нахожусь именно на этом шаге, рассчитывая, что проведение собеседований повысит мой собственный скилл. Собеседования Python-кандидатов помогли мне погрузиться в язык очень глубоко, поэтому надеюсь, что и с Go произойдёт то же самое.
Резюме
В конце хочу ещё раз подчеркнуть, что знание одного языка существенно упрощает изучение второго. Наверняка это верно не только для той связки, которую я описал в статье, но и для других языков, и не только языков программирования. Общие принципы и парадигмы программирования не сильно меняются с приходом новых языков, а некоторые вообще остаются в неизменном виде.
Наиболее сложным моментом я считаю точку входа. Если вы хотите начать, то придётся приложить немало усилий. Вам всё время будет казаться, что не хватает знаний или других ценностей для преодоления себя. Это ложное чувство стоит отбросить, так как оно не принесёт ничего, кроме вреда. Надо просто сесть и начать. Неважно, с чего.
Самая полезная часть обучения — это, по моему мнению, написание проектов с нуля. Но не просто каких-то придуманных вами, а тех, у которых есть заказчик, описание и дедлайны.
Ничего не бойтесь и начинайте изучать!