Зачем другие языки, если есть Go?
- суббота, 19 августа 2023 г. в 00:00:20
Привет, Хабр! Меня зовут Рафаэль Мустафин, я ментор на курсе «Go-разработчик» в Яндекс Практикуме. Название для статьи придумал не я: один из наших студентов назвал так тему беседы в учебном чате. Я же решил эту тему поддержать, но в другом — более широком — формате. В этой статье я расскажу о преимуществах Go для разработки, но с оговоркой, что другие языки всё же нужны)) Поехали!
Go, также известный как Golang, — это язык программирования с открытым исходным кодом. Представленный публике в 2009 году, Go был разработан для упрощения задач программирования и повышения эффективности. Он родился из потребности в языке, который был бы прост для понимания, эффективен для выполнения и прежде всего способен справиться с масштабами, в которых работает Google.
Go популярен. С 2018 по 2020 год Go был самым популярным языком, который разработчики хотели бы добавить в свой стек. В то время как популярность такого языка, как Java, упала на 13%, популярность Go выросла на 125%. Спрос на Go-разработчиков со стороны работодателей вырос на 301%.
По популярности он даже обогнал Swift и может ещё больше подняться в рейтинге. Всё это показано в исследовании:
Такие компании, как Uber, Twitch, Dropbox и сам Google, а также Yandex, VK, Avito, Selectel, Ozon, внедрили Go в свой технологический стек, что ещё раз подтверждает его практичность и надёжность. Это уже не просто язык для работы с сетями и инфраструктурой, как предполагалось изначально. С момента своего появления Go превратился в язык общего назначения, используемый в широком спектре приложений, от разработки облачных вычислений и бэкендов до распределённых сетей.
Кто использует Go? Компании, использующие Go, и для чего используется Go — Career Karma
Компании, использующие Golang — Top 7 Popular Apps You Use Everyday [2023]
Хотя Go обладает впечатляющим набором возможностей и преимуществ, он не лишён недостатков. Но если знать, как он работает, эти недостатки можно нивелировать. В этой статье мы рассмотрим неоспоримые преимущества Go, а в следующей — его недостатки и способы их эффективного преодоления.
Одна из самых веских причин использовать Go — это его простота. Синтаксис языка Go чист и прост для понимания, что делает код очень читаемым и удобным для сопровождения. Создатели Go намеренно сделали язык небольшим и опустили некоторые функции, распространённые в других языках, такие как классы и исключения, чтобы сохранить простоту языка.
Эта простота очевидна при сравнении кода на Go с кодом на других языках. Например, одна и та же функция, написанная на Python или C++, может быть значительно длиннее и сложнее, чем её аналог на Go.
Давайте сравним простоту циклов в Go с циклами в других языках.
В Go существует только одна конструкция циклов: for
. Приведём несколько примеров её использования:
Если традиционный цикл for
совпадает с большинством языков:
for i := 0; i < 10; i++ {
fmt.Println(i)
}
2. То эквивалент цикла while в Go при использовании for выглядит так:
i := 0
for i < 10 {
// Сделать что-то
i++
}
3. Бесконечный цикл:
for {
// Сделать что-то
}
В отличие от Go, в других языках кроме него есть масса подвидов: while, do/while, until, repeat, и достаточно сложно не запутаться во всём этом разнообразии.
Погрузимся на уровень глубже. Рассмотрим более прикладные задачи.
Приведём пример реализации простого HTTP-сервера, который отвечает "Hello, World!" на любой запрос.
Python (подключаем стороннюю библиотеку Flask):
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
if __name__ == '__main__':
app.run(port=8080)
Код получается коротким, но не забываем, что при этом используется интерпретируемый язык со своими ограничениями.
C++ (подключаем стороннюю библиотеку Boost.Beast):
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <cstdlib>
#include <iostream>
#include <string>
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = net::ip::tcp;
int main()
{
try
{
auto const address = net::ip::make_address("0.0.0.0");
auto const port = static_cast<unsigned short>(std::atoi("8080"));
net::io_context ioc{1};
tcp::acceptor acceptor{ioc, {address, port}};
for(;;)
{
tcp::socket socket{ioc};
acceptor.accept(socket);
http::request<http::string_body> req;
beast::flat_buffer buffer;
http::read(socket, buffer, req);
http::response<http::string_body> res{http::status::ok, req.version()};
res.body() = "Hello, World!";
res.prepare_payload();
http::write(socket, res);
}
}
catch(std::exception const& e)
{
std::cerr << "Error: " << e.what() << "\n";
return EXIT_FAILURE;
}
}
В C++ нет встроенного способа создания HTTP-серверов, поэтому нам необходимо использовать сторонние библиотеки, например такие, как Boost.Beast. В результате код становится намного сложнее.
Go:
package main
import (
"fmt"
"net/http"
)
func helloWorld(w http.ResponseWriter, r *http.Request){
fmt.Fprintf(w, "Hello, World!")
}
func main() {
http.HandleFunc("/", helloWorld)
http.ListenAndServe(":8080", nil)
}
Как видите, стандартная библиотека Go предоставляет высокоуровневый и лаконичный способ реализации HTTP-сервера, подобно Python с Flask. Версия Go более эффективна и масштабируема благодаря отличной поддержке одновременных соединений. Версия на C++, напротив, гораздо более многословна и сложна, хотя делает по сути то же самое.
Eщё один важный пункт — это статическая линковка бинарника. Вы получаете один-единственный исполняемый файл, который можно выполнять на любой целевой платформе, под которую он был собран. Сравнивая с тем же сервером на питоне, который также легко писать, мы можем получить проблему под названием dependency hell пакетов питона (и всякими “костылями” вроде venv для их решения). Не лучше обстоит дело и с вариантом на C++: нам необходимо за собой тащить библиотеку Boost либо заниматься плясками с бубном для принудительной статической линковки.
Это одна из отличительных особенностей Go. Go был разработан для того, чтобы совместить производительность языков более низкого уровня, таких как C, с удобством использования языков более высокого уровня, таких как Python. Этот баланс был достигнут на удивление удачно. Очевидно, поэтому Go так быстро завоевал популярность.
Go — статически типизированный, компилируемый язык. Это означает, что он может работать непосредственно на железе, не нуждаясь в интерпретаторе. Это даёт Go значительное преимущество в производительности по сравнению с интерпретируемыми языками, такими как Python. Фактически во многих бенчмарках производительность Go сравнима с производительностью C или Java.
Статическая типизация в Go не только помогает отлавливать ошибки во время компиляции, но и позволяет компилятору оптимизировать генерируемый машинный код для повышения производительности. Кроме того, простота Go и отсутствие таких функций, как наследование и дженерики, означает, что накладных расходов меньше, что может привести к более быстрому выполнению.
Другим видом оптимизации производительности в языке является Escape Analysis.
Escape Analysis — это техника оптимизации компилятора, которая определяет, можно ли безопасно выделить переменную в стеке, а не в куче, что может значительно повысить производительность.
В программировании стек и куча — это две области, в которых может быть выделена память. Стек обычно быстрее выделяется и удаляется, поскольку для этого нужно просто переместить указатель стека, но его размер также более ограничен. Куча, с другой стороны, больше, но выделяется и очищается медленнее и для освобождения неиспользуемой памяти требуется сборка мусора.
Когда вы создаёте переменную, компилятор Go использует escape-анализ, чтобы определить, где выделить память для этой переменной. Если компилятор может подтвердить, что переменная не будет использоваться после возврата функции, он может поместить её в стек, что быстрее и не требует сборки мусора. Если переменная может понадобиться после возвращения функции или если её адрес занят, то она «убегает» в кучу.
Escape Analysis — это одна из причин, по которой производительность Go сравнима с более низкоуровневыми языками, такими как C. Он позволяет Go использовать более быструю стековую память, когда это возможно, и в то же время гибко использовать память кучи, когда это необходимо. Это автоматическое управление памятью в сочетании с эффективным сборщиком мусора Go помогает сделать Go высокопроизводительным языком, который при этом прост в использовании.
Одной из ключевых особенностей Go, способствующих эффективности, является сборщик мусора. Сборка мусора — это часть процесса автоматического управления памятью.
Сборщик мусора в Go использует конкурентный трехцветный алгоритм mark-sweep. Разберём все три составляющие:
Конкурентный: сборщик мусора работает конкурентно с выполнением программы на Go. Это означает, что во время выполнения вашей программы сборщик мусора также работает и над очисткой неиспользуемой памяти. Это отличается от чистого сборщика мусора "stop-the-world", который приостанавливает выполнение программы, чтобы выполнить сборку мусора. Благодаря одновременной работе сборщик мусора в Go может сократить время приостановки и сделать работу вашей программы более предсказуемой.
Трехцветный: терминология «трехцветный» относится к методу отслеживания ссылок на объекты во время сборки мусора. Каждому объекту присваивается один из трёх цветов: белый, серый или чёрный. В начале сборки мусора все объекты белые. Когда объект впервые встречается на этапе Mark-sweep (см. ниже), он становится серым. Когда все дочерние объекты (т.е. объекты, на которые он ссылается) будут помечены, объект становится чёрным. Отслеживая объекты таким образом, сборщик мусора может эффективно определить, какие объекты всё еще используются, а какие нет.
Mark-sweep: это двухфазный процесс. Во время фазы маркировки сборщик мусора определяет все объекты, которые всё ещё используются, следуя ссылкам из набора корневых объектов (например, глобальных переменных и стека вызовов). Эти помеченные объекты и любые объекты, на которые они ссылаются, как известно, используются и поэтому не являются мусором. Во время фазы очистки освобождаются все объекты, которые не были помечены (т.е. не используются).
Одной из инновационных особенностей сборщика мусора Go является его "write barrier", который используется во время фазы конкурентной маркировки. Это механизм, гарантирующий, что запись в память программой не будет конфликтовать с процессом маркировки сборщика мусора. Когда барьер записи включен, любая запись в указатель должна также пометить объект, на который указывают, как серый. Это гарантирует, что сборщик мусора не пропустит ни одного используемого объекта, даже если ссылки изменятся в процессе маркировки.
Ещё одна особенность, способствующая эффективности Go, — это модель параллелизма. Примитивы параллелизма Go, горутины и каналы, позволяют с лёгкостью писать программы, выполняющие несколько одновременных рабочих задач.
Для начала, разберемся, чем одно отличается от другого.
Конкурентность: это одновременное выполнение нескольких задач, даже если система однопроцессорная. Это способ построения программного обеспечения таким образом, чтобы оно могло выполнять несколько задач одновременно, эффективно используя ресурсы.
Параллелизм, с другой стороны, заключается в одновременном выполнении нескольких задач. Для одновременного выполнения различных задач требуется несколько процессоров.
Несмотря на то, что Go умеет и то и другое, в нём при этом не делается акцент на паралеллизм. Прежде всего потому, что конкурентные программы могут быть параллельными, а могут и не быть. Но оказывается, что чаще всего хорошо спроектированная конкурентная программа обладает и прекрасными параллельными возможностями.
В основе модели параллелизма Go лежит планировщик, часть системы среды выполнения, которая управляет работой горутин. Планировщик Go является планировщиком M:N, потому что он распределяет M горутин на N потоков ОС, где M может быть намного больше N.
Планировщик Go отвечает за эффективное распределение выполняемых задач по нескольким рабочим потокам ОС. Приведём некоторые ключевые особенности и способы, используемые планировщиком Go для достижения этой цели:
Горутины: основной единицей процесса планирования в Go является goroutine. Горутины дёшевы в создании и уничтожении, поскольку им требуется лишь небольшой объём памяти для их стека (по умолчанию стек горутины начинается всего с 2 КБ).
Горутины похожи на легковесные потоки, управляемые средой выполнения Go, а не непосредственно операционной системой. Это делает их создание и уничтожение намного дешевле по сравнению с традиционными потоками. Вы можете легко создать сотни тысяч или даже миллионы горутин в одной программе.
Work stealing: чтобы равномерно распределить рабочую нагрузку между несколькими потоками, Go использует жадную стратегию Work stealing. Каждый поток ОС поддерживает локальную очередь выполняемых горутин. Когда поток завершает выполнение своих локальных горутин, он пытается забрать простаивающие горутины других потоков. Это помогает держать все потоки занятыми и использовать все доступные ядра процессора.
GOMAXPROCS: переменная GOMAXPROCS
определяет, сколько потоков ОС могут выполнять код Go одновременно. По умолчанию GOMAXPROCS
устанавливается равным количеству доступных ядер процессора. Вы также можете установить GOMAXPROCS
вручную, но, как правило, лучше всего дать Go управлять этим за вас.
Network and I/O polling:
Когда горутина выполняет блокирующий системный вызов (например, сетевой или дисковый ввод-вывод), вместо того, чтобы блокировать поток, рантайм Go может ставить эту горутину на паузу и назначать другую на этот поток. Это позволяет программам Go поддерживать высокий уровень параллелизма с помощью небольшого числа потоков.
Вытеснение: вытеснение гарантирует, что одна goroutine не захватит процессор и не помешает выполнению других. Планировщик Go не является строго вытесняющим, но он использует несколько стратегий для передачи управления обратно планировщику. К ним относятся проверка наличия преимущественного права при вызове функций и во время итерации циклов, а также периодическое прерывание выполняющихся горутин для проверки наличия приоритета.
Все эти функции и стратегии в совокупности делают планировщик Go эффективным в работе с высоким уровнем параллелизма при минимальном количестве потоков. Это значительно повышает производительность и масштабируемость приложений Go.
Каналы используются для безопасного обмена данными между горутинами. Это позволяет легко синхронизировать задачи и обмениваться данными без риска возникновения условий гонки.
Модель параллелизма Go, которую часто описывают как "share by communicating instead of communicating by sharing", позволяет эффективно использовать многоядерные процессоры. Это одна из ключевых причин, почему Go часто выбирают для сетевых приложений и других проектов, требующих высокой производительности и масштабируемости.
В традиционном многопоточном программировании потоки часто общаются путём совместного использования памяти. Это означает, что несколько потоков имеют доступ к одним и тем же участкам памяти (переменным, массивам и т.д.) и используют мьютексы, семафоры или другие механизмы синхронизации, чтобы гарантировать, что они не будут влиять друг на друга. Эта модель может быть эффективной, но, как правило, её трудно реализовать, поскольку она может привести к сложным ошибкам, таким как гонка, deadlocks, и livelocks.
В Go предлагается использовать другой подход. Вместо того, чтобы потоки обменивались данными через общую память, Go предлагает горутинам обмениваться данными через явные примитивы синхронизации — каналы.
Посмотрим на примере:
package main
import "fmt"
func sum(arr []int, c chan int) {
sum := 0
for _, v := range arr {
sum += v
}
c <- sum // send sum to c
}
func main() {
arr := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(arr[:len(arr)/2], c)
go sum(arr[len(arr)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x+y)
}
Этот пример демонстрирует, как можно использовать goroutines и каналы для одновременного выполнения задач и синхронизации результатов.
Основное преимущество этой модели заключается в том, что она делает параллельные программы более понятными. Поскольку горутины общаются явно, а синхронизация привязана к обмену данными, легче понять, какие горутины взаимодействуют и как. Это может помочь предотвратить ошибки и состояние гонки.
Одним из самых мощных моментов языка Go является поддержка кросс-компиляции. Кросс-компиляция — процесс компиляции кода на одном типе машины или операционной системы («хост») для запуска на другом типе машины или операционной системы («цель»). Эта возможность полезна для разработчиков, которые хотят выпускать свои программы для разных платформ, не прибегая к компиляции кода непосредственно на целевой платформе.
Поддержка кросс-компиляции в Go встроена в инструмент go build
. Она возможна потому, что Go имеет автономную, платформонезависимую стандартную библиотеку и среду выполнения, а значит, ему не нужно связываться с системными библиотеками, как это делают некоторые другие языки.
Для кросс-компиляции программы на Go перед запуском go build
необходимо установить переменные окружения GOOS
и GOARCH
в значения целевой операционной системы и архитектуры, соответственно. Вот пример того, как это сделать:
GOOS=linux GOARCH=amd64 go build -o myprogram_linux myprogram.go
GOOS=windows GOARCH=amd64 go build -o myprogram_windows.exe myprogram.go
GOOS=darwin GOARCH=amd64 go build -o myprogram_mac myprogram.go
В этом примере мы компилируем исходный файл myprogram.go
для Linux, Windows и macOS, все с одной хост-машины.
Переменная GOOS
может быть установлена на любую поддерживаемую целевую операционную систему: linux
, windows
, darwin
(для macOS), freebsd
, openbsd
, netbsd
, plan9
, solaris
и другие. Переменная GOARCH
может быть установлена на любую поддерживаемую целевую архитектуру: 386
, amd64
, arm
, arm64
, ppc64
, ppc64le
, mips
, mipsle
, mips64
, mips64le
, s390x
и другие.
Данная возможность упрощает процесс создания программного обеспечения, которое может работать на разных платформах, и делает Go отличным выбором для создания кросс-платформенных приложений.
Быстрая скорость компиляции — одна из ключевых особенностей, отличающих Go от многих других языков. Хорошо известно, что компилятор Go предназначен для быстрой компиляции программ, и эта скорость оказывает значительное влияние на общую скорость разработки. Чем меньше времени разработчик ждёт компиляции программы, тем больше времени он может потратить на реальную разработку. Это та область, где Go действительно блещет.
Секрет высокой скорости компиляции в Go заключается в его подходе к анализу зависимостей. Go предоставляет модель создания программного обеспечения, которая упрощает анализ зависимостей и позволяет избежать накладных расходов, связанных с включением файлов и библиотек в стиле языка Си. Такой подход не только предусмотрен дизайном, но и является одной из основных причин скорости компиляции Go.
Другим аспектом скорости компиляции, читаемости и простоты кода является строгость Go в отношении неиспользуемых переменных и мертвого кода — действительно очень важное свойство, способствующее созданию более чистого и эффективного кода. Давайте рассмотрим это более подробно. В Go компилятор не допускает неиспользуемых переменных. Если объявить переменную и не использовать её, то код не будет компилироваться. Эта особенность позволяет избежать загромождений и возможной путаницы в кодовой базе. Пример:
package main
import "fmt"
func main() {
var a int // Это приведет к ошибке компиляции
fmt.Println("Hello, world!")
}
Если попытаться выполнить этот код, то будет выдано сообщение об ошибке следующего вида:
main.go:6:6: a declared but not used
.Во многих других языках, таких как Python и JavaScript, это правило не соблюдается. Вы можете объявить переменную и никогда не использовать её без каких-либо проблем. Приведём пример на языке C:
#include <stdio.h>
int main() {
int a; // Ошибки нет, хотя 'a' никогда не используется
printf("Hello, world!\n");
return 0;
}
В данном примере переменная “a” объявлена, но не используется. Это не приведёт к ошибке времени компиляции. Однако в зависимости от компилятора и флагов, используемых при компиляции, это может привести к появлению предупреждения. Например, если в GCC использовать флаг -Wall (который включает все предупреждения), то среди множества других будет выдано предупреждение следующего вида: warning: unused variable 'a' [-Wunused-variable]. Но кто же это включает по умолчанию?
Как видите, строгость Go в отношении неиспользуемых переменных характерна не для всех языков и способствует повышению чистоты и сопровождаемости кода.
Для сравнения давайте посмотрим на время компиляции различных языков. На изображении ниже, взятом с Imgur, представлено сравнение времени компиляции произвольного кода на C++, D, Go, Pascal и Rust. Сравнение времени компиляции произвольного кода на C++, D, Go, Pascal и Rust.
Из графика видно, что скорость компиляции в Go значительно выше, чем во многих из этих языков, что ещё больше подчеркивает его эффективность.
Эта особенность Go не только экономит время, но и способствует более комфортному и лёгкому кодированию, что стало основным фактором быстрого роста популярности Go.
Другой нюанс заключается в том, что компиляторы Go не обязательно являются быстрыми. Скорее другие компиляторы медленные. Например, компиляторам С и С++ приходится разбирать большое количество заголовков, что отнимает много времени. В отличие от них Go избегает накладных расходов, и времени тратится меньше. Компиляторы Java и С# работают в виртуальной машине, что требует от операционной системы загрузки всей виртуальной машины и JIT-компиляции из байткода в нативный код. Это занимает время. Go избегает этих шагов — времени на компиляцию уходит меньше.
Go часто хвалят за его философию «батарейки в комплекте». Это означает, что язык поставляется с богатой стандартной библиотекой и встроенными инструментами, которые позволяют делать многое без необходимости полагаться на внешние пакеты или инструменты. Эта философия проявляется в таких командах, как go run
, go test
и go build
, а также в системе модулей Go.
Команды go run
, go test
и go build
являются частью набора инструментов Go. Каждая из них служит для разных целей и облегчает разработку, тестирование и сборку программ на Go:
go run
: Эта команда компилирует и запускает программы Go. Это удобный способ быстро протестировать свой код без необходимости сначала вручную компилировать его, а затем запускать двоичный файл. Например, если у вас есть Go-программа в файле с именем main.go
, вы можете протестировать её, просто выполнив команду go run main.go
.
go test
: Эта команда запускает тесты для Go-программ. В Go есть встроенная система тестирования, и go test
упрощает её использование. Она автоматически находит любые тесты в вашем коде и запускает их, предоставляя сводку результатов.
go build
: Эта команда компилирует программы на Go, создавая исполняемый двоичный файл. Она достаточно умна, чтобы перекомпилировать только те части вашей программы, которые изменились с момента последней сборки, что делает её эффективной. Полученный двоичный файл по умолчанию статичен, то есть он включает все свои зависимости и может быть запущен на любой системе, без необходимости установки дополнительных библиотек или инструментов.
Go поставляется со встроенным профилировщиком, который позволяет разработчикам анализировать производительность своих программ и выявлять потенциальные "узкие места". Профилирование является важным инструментом для понимания того, как программа использует ресурсы процессора и памяти, и помогает разработчикам оптимизировать свой код для повышения эффективности.
Для использования профилировщика необходимо всего лишь импортировать соответствующие пакеты из стандартной библиотеки (net/http/pprof
или runtime/pprof
) и запустить HTTP-сервер для получения данных профилирования. Вот простой пример:
package main
import (
_ "net/http/pprof"
"net/http"
)
func main() {
go func() {
// Запуск HTTP-сервера на localhost:6060
http.ListenAndServe("localhost:6060", nil)
}()
// Ваш основной код программы находится здесь
// ...
}
После запуска вашего приложения вы можете получить доступ к данным профилирования с помощью таких инструментов, как go tool pprof
или веб-интерфейс pprof.
Система модулей Go—- это ещё одна особенность, которая следует философии «батарейки в комплекте». Модуль Go — это набор связанных пакетов Go, которые версионируются как единое целое. Модули фиксируют точные требования к зависимостям и создают воспроизводимые сборки, упрощая управление версиями пакетов и путями импорта.
Вместе эти функции и инструменты обеспечивают комплексную интегрированную среду разработки прямо из коробки. Они облегчают начало работы с Go, обеспечивают качество кода и управление зависимостями, способствуя репутации Go как продуктивного языка как для небольших сценариев, так и для больших систем.
В заключение следует отметить, что язык программирования Go обладает целым рядом достоинств, которые способствовали заметному росту его популярности. Простота, эффективность и мощная модель параллелизма делают его отличным выбором для широкого круга приложений. От веб-разработки до системного программирования — Go зарекомендовал себя как универсальный и надёжный язык.
Кроме того, ориентация на обратную совместимость обеспечивает сохранение актуальности и сопровождаемости кода на языке Go с течением времени. В результате Go — это язык, который позволяет разработчикам создавать масштабируемые и высокопроизводительные приложения.
В быстро развивающемся мире разработки программного обеспечения язык Go является оптимальным выбором для проектов любого масштаба и сложности. Прагматичный подход, поддерживаемый развивающейся экосистемой и сообществом энтузиастов, укрепляет позиции Go как мощного и перспективного языка, заслуживающего внимания при разработке современного программного обеспечения. Поскольку разработчики продолжают искать эффективные и производительные инструменты, сильные стороны Go делают его привлекательным и жизнеспособным вариантом, выдвигая его в число ведущих языков программирования в современную эпоху цифровых технологий.
Однако Go не лишён своих ограничений. Отсутствие генериков и более развитой системы управления зависимостями может создавать проблемы в некоторых случаях. Несмотря на эти недостатки, активное сообщество разработчиков Go и их стремление к постоянному совершенствованию позволяют находить решения и обходные пути. В следующей статье я разберу типичные проблемы с Go и варианты их решения.
Научиться писать сложные приложения на Go и освоить архитектурные паттерны можно на курсе «Go-разработчик» в Яндекс Практикуме. В этом вас поддержат опытные код-ревьюеры и менторы. Студенты могут выбрать подходящий им формат обучения: в классических группах до 15 человек или в своём темпе без дедлайнов и спринтов.