Привет, Хабр! Меня зовут Рафаэль Мустафин, я ментор на курсе «Go-разработчик» в Яндекс Практикуме. Название для статьи придумал не я: один из наших студентов назвал так тему беседы в учебном чате. Я же решил эту тему поддержать, но в другом — более широком — формате. В этой статье я расскажу о преимуществах 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 — это его простота. Синтаксис языка Go чист и прост для понимания, что делает код очень читаемым и удобным для сопровождения. Создатели Go намеренно сделали язык небольшим и опустили некоторые функции, распространённые в других языках, такие как классы и исключения, чтобы сохранить простоту языка.

Эта простота очевидна при сравнении кода на Go с кодом на других языках. Например, одна и та же функция, написанная на Python или C++, может быть значительно длиннее и сложнее, чем её аналог на Go.

Давайте сравним простоту циклов в Go с циклами в других языках.

В Go существует только одна конструкция циклов: for. Приведём несколько примеров её использования:

  1. Если традиционный цикл 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, горутины и каналы, позволяют с лёгкостью писать программы, выполняющие несколько одновременных рабочих задач.

Concurrency vs Parallelism в 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 run, go test, go build, go tool pprof и система модулей

Go часто хвалят за его философию «батарейки в комплекте». Это означает, что язык поставляется с богатой стандартной библиотекой и встроенными инструментами, которые позволяют делать многое без необходимости полагаться на внешние пакеты или инструменты. Эта философия проявляется в таких командах, как go run, go test и go build, а также в системе модулей Go.

Go Run, Go Test и Go Build

Команды 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 и освоить архитектурные паттерны можно на курсе «Go-разработчик» в Яндекс Практикуме. В этом вас поддержат опытные код-ревьюеры и менторы. Студенты могут выбрать подходящий им формат обучения: в классических группах до 15 человек или в своём темпе без дедлайнов и спринтов. 

Комментарии (40)


  1. flygrounder
    18.08.2023 07:22
    +7

    В быстро развивающемся мире разработки программного обеспечения язык Go является оптимальным выбором для проектов любого масштаба и сложности.

    Очень спорное утверждение. Для написания сложной бизнес логики Java/C# куда лучше подходят.


    1. MihaTeam
      18.08.2023 07:22
      +2

      На самом деле для написания сложной бизнес логики по хорошему просто нужен человек, который умеет качественно и надёжно писать код, а также правильно проектировать эту логику. Язык же выбирается исходя немного из других параметров: скорость разработки, производительность языка, количество и компетентность людей в штате знающих определённый язык, как этот язык поддерживается,тип типизации и многое другое. К примеру, если для какого-то приложения характерно множество io-bound задач, то go,java,rust,c(++/#) не сильно выиграют по сравнению с js,python и тд. Другой пример, приложению характерны cpu-bound задачи с возможностью их распараллелить - тут уже победа будет за go,rust и тд. Но это касается средне-крупных компаний, в более маленьких компаниях зачастую не используются 10 разных языков, поэтому и выбирать не из чего.

      Ну и плюс, редко требуется писать с нуля всё приложение


      1. flygrounder
        18.08.2023 07:22

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


        1. MihaTeam
          18.08.2023 07:22
          -2

          На самом деле go отлично подходит для сложной бизнес логики, как и С, Java, Rust. Чего только стоит возможность легко превращать синхронный код в асинхронный(Правда это дает возможность по неопытности выстрелить себе в ногу датарейсами, дедлоками, лайфлоками и прочими интересными вещами). Есть и нейтральные моменты, обработка ошибок, хотя она позволяет не пропускать обработку ошибок, но и прокидывать их по стеку вызова при необходимости не очень приятно. Из неприятных моментов большое количество бойлерплейта.
          Для работы с большим числом разных данных лучше подошел бы python к примеру, для кроссплатформенности я бы до сих пор предпочел Java и тд.
          В любом случае языки приходят и уходят, это просто инструмент, любой разработчик, начиная наверное с мидла, при необходимости может поменять за 1-2 месяца яп, естественно с небольшой потерей грейда, но это быстро восполняется(ну и с условного python, js или php переходить на go/c/c++/c#/java/rust больнее, чем наоборот).
          Главное не доводить до состояния, под названием "когда у тебя в руках молоток все вокруг превращается в гвозди".


    1. itmind
      18.08.2023 07:22
      +1

      Очень спорное утверждение про Java/C#. Может для бизнес логики больше подходит 1с? Ведь именно в конфигурациях 1с работают сотни тысяч организаций по всей России.


      1. flygrounder
        18.08.2023 07:22
        +1

        Бизнес логику должны писать бизнесмены ;)


    1. gev
      18.08.2023 07:22
      +5

      Для сложной лучше Haskell =)


  1. FreakII
    18.08.2023 07:22
    +3

    Действительно, давайте выкинем кобольное легаси пятидесятилетней давности и перепишем всю мировую банковскую систему на го


    1. pin2t
      18.08.2023 07:22
      -5

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

      Ну и видимо многие из них будут на Go


      1. flygrounder
        18.08.2023 07:22
        +3

        Практика показывает, что блокчейны обычно используют Rust


        1. MrDEX123
          18.08.2023 07:22
          -4

          Не скажи, скорее Solidity какой-нибудь, являющийся диалектом С++.


  1. rsashka
    18.08.2023 07:22
    +11

    Зачем другие языки, если есть Go?

    Контр вопрос - зачем Go, если есть Rust и другие языки?


    1. Expurple
      18.08.2023 07:22
      +1

      Go по сравнению с C++/Rust нужен для быстрого изучения, единообразного кода, быстрой компиляции и отсутствия заморочек с владением памятью. Создатели довольно неплохо описали, какие проблемы C++ они пытались решить. Я б сказал, это у них получилось.

      Но, как я написал в другом комменте, в Rust гораздо сильнее типы и он мне больше нравится. Ради них приходится просто мириться с лишними заморочками с владением. Я пишу не настолько низкоуровневые вещи, чтобы они реально требовались и наличие GC/рантайма было проблемой


      1. flygrounder
        18.08.2023 07:22
        +2

        Я не уверен, что можно просто взять и добавить GC в раст. Его система владения ещё предотвращает ошибки связанные с потоками. Не хотелось бы отказываться от этого


        1. Expurple
          18.08.2023 07:22

          Взять и добавить можно. Естественно, из-за borrow checker там позволено меньше мутации, чем в типичных GC языках, но я только за. Проблема таких библиотечных решений в том, что они добавляют очень много синтаксического шума. Условно, Gc<GcCell<T>> вместо T. И то же самое со стандартными умными указателями типа Rc и так далее. Такие типы в раст коде как раз многих отпугивают от языка. Было бы интересно посмотреть, каким был бы раст, если бы все объекты были просто garbage collected T (всё ещё с контролируемой мутабельностью), а RAII не было. По сути, это уже почти Haskell, но с тонной синтаксического сахара для ST


  1. Tasta_Blud
    18.08.2023 07:22
    +9

    заголовок конечно отвратительный холиварный - не нужон это ваш сисярп/жаба/змейсо-итыпы. ну-ну, с нарочным отсутствием многих привычных концепций, с инопланетным синтаксисом и другими особенностями, вот прям всё в печку. и он даже не мультипарадигменный, не мультиплатформенный не многоцелевой (вот приведу в пример свой любимый Котлин - пиши хочешь в ооп-стиле, в функциональном, хоть в процедурном, и компиляция и в jvm-байткод, и в js, и в натив - вот это хорошая заявка), ну нет, ну не захватит он (виртуальный) мир.

    нет, я не спорю, язык неплох, и много кому зашёл. но пожалуйста, не надо писать такие провокационные заголовки...


  1. Expurple
    18.08.2023 07:22
    +7

    > nil в 2023
    > нет sum types
    > нет first-class tuples
    > нет контроля мутабельности
    > гонки горутин
    > этот код возвращает err в рантайме, а не ошибку компиляции:

    var num int
    fmt.Print("Enter an int: ")
    _, err := fmt.Scanln(num) // Тут должно быть &num
    if err != nil {
    	fmt.Println("Error: ", err)
    } else {
    	fmt.Println("You've entered: ", num)
    }
    

    Язык вроде с типами, но с очень слабыми. Как бы я хотел Rust, но со сборкой мусора...


    1. gev
      18.08.2023 07:22
      +1

      Попробуйте Ocaml или Haskell


      1. akurilov
        18.08.2023 07:22

        В случае когда в rust нет gc, команду на хаскеле не собрать, ява ест слишком много, undefined is not a function in 2023...

        Остаётся только go


      1. Expurple
        18.08.2023 07:22
        +1

        Haskell пробовал, тоже очень нравится. Rust выделяется превосходным тулингом и удобством написания контролируемой императивщины. Я уже в другом комменте написал про Rust с GC:

        По сути, это уже почти Haskell, но с тонной синтаксического сахара для ST

        Ну и сейчас понял ещё, что без контроля IO. Для кого-то это плюс, для кого-то минус.


        1. gev
          18.08.2023 07:22
          +1

          А я пересел с Си на Хаскель. Использую как и Си для микроконтролеров. Оч нравится!


          1. Expurple
            18.08.2023 07:22

            Было бы интересно статью про это, как контролируете потребление памяти и т.п. Не знаю конечно, насколько она оправдана. Может там тот же подход, что и везде. Про space leaks уже много написано


            1. gev
              18.08.2023 07:22

              Потребление памяти – статика. Использую хаскель как макро-язык на стероидах для си. Использую фреймворк https://github.com/GaloisInc/ivory.


    1. fornit1917
      18.08.2023 07:22
      -1

      > этот код возвращает err в рантайме, а не ошибку компиляции:

      Может я что-то не понимаю, но разве в каком-то языке возможно на подобное получать ошибку компиляции, а не ошибку в рантайме? Ведь мы же на этапе компиляции не знаем, что введёт пользователь и окажется ли введённая им строка целым числом.


      1. MihaTeam
        18.08.2023 07:22
        +2

        Там проблема в другом. Функция fmt.Scanln() принимает any, т.е может принять любой тип, а по факту сама функция хочет получить указатель на переменную, в которую нужно будет положить значение. Из-за того, что мы передаем не указатель, мы и получаем ошибку (type not a pointer) в рантайме, а компилятор видит, что в any кидают любой тип и его это устраивает.


      1. hangoverfsd
        18.08.2023 07:22
        +1

        rust


      1. Expurple
        18.08.2023 07:22

        Проблема не в том, что введёт пользователь. Scanln возвращает err всегда. Сразу же, не дожидаясь ввода. Можете запустить на Go Playground и посмотреть


  1. myxo
    18.08.2023 07:22
    +8

    Однако Go не лишён своих ограничений. Отсутствие генериков <...>

    Вы из какого года статью писали?


  1. AlexSky
    18.08.2023 07:22
    +6

    Например, если в GCC использовать флаг -Wall (который включает все предупреждения), то среди множества других будет выдано предупреждение следующего вида: warning: unused variable 'a' [-Wunused-variable]. Но кто же это включает по умолчанию?

    Любой нормальный программист?

    На всех проектах, в которых я участвовал, неиспользуемая переменная приводила даже не к предупреждению, а к ошибке компиляции. -Wall -Wextra -Werror в помощь.


  1. kulikser
    18.08.2023 07:22
    +8

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

    И что же там видно из графика? Все же ровно наоборот, в оригинальном сравнении это явно указано


    1. censor2005
      18.08.2023 07:22
      +2

      Главное, линия на графике выше? Выше! Что и требовалось показать )


  1. Pastoral
    18.08.2023 07:22
    +1

    Для одновременного выполнения различных задач требуется несколько процессоров.

    Спасибо за сведения, а то вдруг (shit happens) меня бы занесло на Ваши курсы. Про то, что видно из графика, выше уже написали...

    По популярности он даже обогнал Swift

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

    Поскольку разработчики продолжают искать эффективные и производительные инструменты

    Раз продолжают искать, значит не находят пока. С другой стороны, какое языкам дело до разработчиков с их исканиями? А вот то, что корпорации продолжают искать такой язык, чтобы много ума не требовал, так дешевле, и свободы не особо давал, так дисциплинированнее, влияет ещё как.


  1. longclaps
    18.08.2023 07:22
    +3

    в других языках кроме него (цикла for) есть масса подвидов: while, do/while, until, repeat, и достаточно сложно не запутаться во всём этом разнообразии.

    Детский лепет какой-то. Вы, когда кушаете, ложкой-то в рот попадаете? Не дай бог запутаться.


  1. savostin
    18.08.2023 07:22
    +4

    C++ (подключаем стороннюю библиотеку Boost.Beast):

    В C++ нет встроенного способа создания HTTP-серверов, поэтому нам необходимо использовать сторонние библиотеки, например такие, как Boost.Beast. В результате код становится намного сложнее.

    Ни в каком языке нет "встроенного способа создания HTTP сервера". Везде надо "подключать стороннюю библиотеку". То, что в Go такая библиотека включена в стандартный набор - просто спасибо разработчикам языка. Надо подключать что-то более лаконичное и специализированное, чем Boost:

    #include <httplib.h>
    
    int main(void)
    {
      using namespace httplib;
    
      Server svr;
    
      svr.Get("/hi", [](const Request& req, Response& res) {
        res.set_content("Hello World!", "text/plain");
      });
    
      svr.listen("localhost", 1234);
    }

    Вы в Вашем замечательном, не спорю, Go попробуйте REST API сервер с JSON сделать...

    С++ точно так же, почти без бубнов, собирается под все современные платформы.


    1. neolink
      18.08.2023 07:22

      В Go чтобы собрать под другую OS, и архитектуру достаточно

      GOOS=win GOARCH=amd64


      и это будет также под любой платформой и архитектурой где запускается GO

      А в с++ на арм маке насколько без бубнов можно сделать кросс компиляцию в винду? (мне правда интересно)?


      1. savostin
        18.08.2023 07:22

        К сожалению M1 под рукой нет проверить, понимаю смысл вопроса. Но это скорее проблема Apple, а не компиляторов.

        Под Intel, естественно, Mingw должен все собрать. Конечно в реальности далеко не все. Надеюсь старшие товарищи, кто каждый день кросс-компилирует, ответят более содержательно.

        Я согласен, что более молодой Go лучше вписывается в современные реалии, чем неповоротливый монстр C++.


  1. simenoff
    18.08.2023 07:22

    Обе ссылки ведут на одну страницу, в чём смысл?


    1. savostin
      18.08.2023 07:22

      Еще 3 месяца "продвигают".


  1. caballero
    18.08.2023 07:22

    Зачем GO если есть другие языки


  1. Hivemaster
    18.08.2023 07:22

    Считать в 2023-м CMS-сборщик преимуществом - это странно. Например в Java сборщик Concurrent Mark Sweep появился в 2002-м, а в 2017-м признан устаревшим. Причём в Go ещё и неуплотняющая его версия, что ведёт к неоптимальному использованию памяти и исключает долговременную работу приложения без регулярного перезапуска.

    Да и Escape Analysis тоже не эксклюзив для Go, в уже упомянутой ранее Java он тоже есть.

    Ну а простота Go не той породы, которая делает язык гибким и изящным, а той, которая делает его примитивным и ограниченным.