Как стать автором
Обновить

Комментарии 72

Go, он как домашние тапочки. Простой, неказистый, не модный, местами бесформенный и... неожиданно удобный.

Я не могу себя заставить полюбить go. Основной источник раздражения необходимость механического набора кода, который в других языках просто не нужен. Да, читается это хорошо, но пишется с ненавистью. AI начинает помогать с этим, но эта тема еще развивается.

Как и многие другие минусы go это является также и плюсом. Я именно за это люблю go. То, что в других языках где-то или под капотом или выше в иерархии обработается, здесь обрабатывается прямо там, где возникло и в максимально простом и понятном виде. При чтении и отладке нужно держать в голове меньше других контекстов - а это дорогого стоит. Учитывая, что сама разработка составляет примерно 10% времени жизни проекта, а поддержка, развитие, чтение кода и разбор инцидентов ВСЕГДА в разы больше времени забирают, то принцип "напиши кучу бойлерплейта здесь явно, чтобы потом читателю было легче" мне кажется правильным.
Не то, чтобы с великим удовольствием, но с осознанием, что это важно и нужно - этот механический набор кода делаю. И без всякого раздражения.

Эмоциональную составляющую полностью понимаю и разделяю :) Заходил в Go 3-4 раза и разворачивался в полном отвращении от происходящего. Но как приперло — просто втянулся и нашел свой coping-механизм. Теперь не болит.

Писать ладно, но этот код же еще потом читать придётся…

Это простота очень обманчива. В жизни нельзя просто так радикально упоротся в какую то идею и всех победить. Нужен баланс. Которого в Go нет. Авторы повыкидывали что не нужно вместе с тем что нужно. Как итог сложность языка не исчезла а просто перешла из одной крайности в другую. Многие проблемы которые в других языках решены из коробки в Go теперь учат самостоятельно реализовывать в туториалах и на курсах. Взять ту же асинхронщину - на hello world все действительно выглядит просто на реальных задачах все будет обмазано кучей каналов вейтгрупп и селектов на ровном месте там где в другом языке можно было бы обойтись ключевым словом. Яркий пример, async/await сложно неудобно, загромождает код? А что же у нас в Go для такой простой задачи придется писать? И самое обидное что в Go шансов напортачить на любой шаге куда больше. Далее, обработка ошибок максимально ужасная и не из-за if err != nil, а по причине того что ошибки не информативные и очень просто их проигнорировать, например если возвращаемое значение только ошибка. Дизайн языка очень странный - то в стиле С то в стиле ООП языков, работа с мапами и слайсами это вообще просто неудобный костыль из-за отсутствия дженериков в первых версиях языка.

Кстати недавно был интересный опыт. Сначала написал по работе довольно сложный CLI на Go потом по определенным причинам переписал на C# полностью. Отличие было довольно сильное. На C# субъективно было напрого проще и приятнее писать и сложнее допускать ошибки. Не смотря на кажущуюся простоту Go.

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

Сначала написал по работе довольно сложный CLI на Go потом по определенным причинам переписал на C# полностью. Отличие было довольно сильное.

И что в Шарпе было для этой задачи удобнее (кроме, очевидно, исключений)?

Я, навскидку, большой разницы не вижу, кроме Студии конечно, которая к языку строго говоря не относится, но удобство формирует.

И что в Шарпе было для этой задачи удобнее (кроме, очевидно, исключений)?

Хороший вопрос. Ключевым фактором была удобная библиотека Spectre из-за которой и решил попробовать переписать и получилось сравнить одно и тоже.

Что оказалось удобнее на C# и в процессе выяснилось:
- несоизмеримо богаче стандартная библиотека и жить с этим куда проще чем в Go в котором кроме мапы и слайса ничего нет.
- не смотря на хайп, async/await подход оказался куда проще и минималистичнее с меньшим количеством способов отстрелить себе ногу. В Go на любой чих нужны каналы и wait groups, в C# в большинстве случаев просто await или Task.WhenAll().
- работа с ошибками
- в целом кода получилось меньше и читать его приятнее так как он не перегружен массой вспомогательных частей

Ну и главное. По итогу, Go не имеет практически никаких преимуществ в этом сравнении. На C# скомпилировал программу через AOT в исполняемый файл схожий по размерам с Go. Производительность сопоставимая. По памяти сопоставимо. По простоте кода? Ну кто запрещает писать просто на C#? При этом по удобству и комфорту C# лучше. Если в рамках проекта.

Если ,брать не работу а вообще то преимуществ еще больше, ибо на C# можно писать вообще все угодно от UI, backend, CLI до игр, в то время как экосистема Go куда более бедная.

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

Согласен, назвать Go именно простым языком у меня язык не провернется :) Он субъективно оказался легким для многих в освоении, но не простым.

Тут под просто/легко имею в виду то что имел в виду Rich Hickey в Simple Made Easy.

  • Просто — объективное и структурное понятие, противоположное комплексному и переплетенному

  • Легко — субъективное отношение, насколько что то нам знакомо и близко, противоположное трудному

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

В го нет адекватной асинхронности из коробки. Долгоживущая горутина — это ад, её всё еще проще даже на перле написать.

У меня идёт процесс ровно наоборот- по мере возможности переписываю сервисы с C# на Go. Наиболее ощутима разница когда приходится возвращаться к проекту спустя год - два после предыдущей доработки, с Go этот процесс гораздо легче.

потому что язык намеренно ограничен, статически типизирован и даёт меньше способов «сотворить магию»

Если на питон писать просто то и читать просто

Есть многое в питоне, друг Горацио, что и не снилось нашим мудрецам.

За свою практику я повидал немало легаси-кода на Python и могу уверенно заявить: талантливые люди талантливы во всём. В том числе существуют на свете такие талантливые говнокодеры, которые с лёгкостью умудряются писать на Python совершенно нечитаемый, неподдерживаемый и нетестируемый код и язык никак не препятствует этому ))

Яркий пример, async/await сложно неудобно, загромождает код? А что же у нас в Go для такой простой задачи придется писать?

Для этого в Go нужно просто писать последовательный код. Где надо, он сам засаспендится.

Замечательно это все только мне не нужно вызывать горутину в пустоту, мне нужен результата асинхронного вызова и там где все просто можно сразу получить через await, в Go будут каналы на каждый чих и кода будет по факту написано еще больше.

Простота await тоже обманчива. На деле асинхронщина нужна не так часто, как в коде раставляются async / await. Они как чума: если в каком-нибудь месте появляется await, то он сразу расползается по всему коду вверх - где надо и где не надо. async add(x: int, y: int) - легко.

Go из-за своей нарочитой корявости в этом месте, вынуждает остановится и задуматься - что мы вообще пытаемся здесь изобразить. Может нам и не понадобится асинхронная функция сложения, если весь IO вынести из нее наружу? Таким образом, на практике, код на Go получается в основном синхронным, который легко писать и тестировать - все как мы любим.

При этом, встроенные инструменты для асинхронщины в Go дают больше контроля. Здесь практически любые паттерны асинхроного control flow реализуются в полэкрана, описывая все ваши намерения явно. Это потом сильно упрощает поддержку.

Таким образом, на практике, код на Go получается в основном синхронным, который легко писать и тестировать - все как мы любим.

Если он асинхронный то синхронным он по своей природе не будет что на Go что без. Если кто-то делает функцию сложения асинхронной то это не проблема не языка и не подхода а прослойки между монитором и креслом.

Здесь практически любые паттерны асинхроного control flow реализуются в полэкрана, описывая все ваши намерения явно. Это потом сильно упрощает поддержку.

Вот пишу я бизнес приложение и у меня бизнес код. Да с async/await у меня есть лишние ключевые слова но с Go у меня вообще все кишки наружу - бизнес логика перемешана с множеством WaitGroup, каналами и селекторами. Я понимаю в чем силен подход Go - в первую очередь в том что не нужно дублировать функции на асинхронные и синхронные, но по факту реального применения с async/await код куда чище и проще - это факт. Приводил пример выше про переписывание на работе проекта с Go на C# где это было очевидно. Плюсом async/await также является большая простота, здесь очень сложно накосячить как в Go вызвав вейтгруппу неправильно или ошибиться при работе с каналом. Я молчу про то что любые паттерны асинхронного программирования пишутся от руки каждый раз.

Вы просто берете и вызываете асинхронный метод. Каналы нужны только если вы хотите запустить больше одного потока выполнения.

Вы или не понимаете что я пишу или не понимаете что то о чем пишите. Перечитайте пожалуйста мое сообщение еще раз.

Перечитал. Вы утверждаете что в Go вместо await нужно использовать каналы. Нет, не нужно. Точнее нужно, но далеко не всегда.

package main

import "net/http"

func main() {
	resp, err := http.Get("http://example.com/") // смарите, асинхронный вызов, безо всяких await'ов и каналов!
}

Тут в соседнем треде объясняют как это работает.

смарите, асинхронный вызов, безо всяких await'ов и каналов!

В Вашем примере нет асинхронных вызовов. Сначала вызовется http.Get, заблокирует и вытеснит текущую горутину и только после того как результат запроса будет получен, горутина продолжит выполнение и начнет исполнять следующее действие. Последовательность A->B->C, это называется синхронное выполнение. Для асинхронного в Go Вам нужны каналы.

заблокирует и вытеснит текущую горутину

Равно как await заблокирует и вытеснит выполнение текущего таска.

Или может быть вы пытаетесь сказать что Go не предлагает инструментов для structured concurrency (типа шарпового await Task.WhenAll(...))? Ну так оно есть в библиотеках, например.

Равно как await заблокирует и вытеснит выполнение текущего таска.

await не связан с вызовом функции, поэтому я могу вызвать 10 функций асинхронно и только потом когда мне нужно сделать await. В Go нет Future/Task как данности и поэтому приходится использовать каналы. Это не хорошо и не плохо, но объективно засоряет код сильнее чем await.

Ну так оно есть в библиотеках, например.

Ну мало ли что в каких то костыльных библиотеках есть. Это к языку не имеет отношение.

Или может быть вы пытаетесь сказать

Я пытаюсь сказать что решение Go с бесшовной асинхронностью дает определенные преимущества с одной стороны но, избавляя от async/await, но для других случае наоборот сильно захламляет код вспомогательным кодом уже проигрывая async/await.

Если мне придется решить такую простую бытовую задачу как сделать 10 параллельных запросов, то в том же C# это будет
await Task.WhenAll(...) а в Go придется городить огороды из 10 каналов захламляя все вокруг. Да можно либы, даже можно писать на коленке но это очков языку не дает.

Ну мало ли что в каких то костыльных библиотеках есть.

Аргументация 80 уровня. Давайте я скажу что весь C# и .NET костыльный?

Это к языку не имеет отношение.

А зачем тащить в язык то что решается на уровне библиотек?

P.S. А знаете ли вы что в C# хотели добавить аналог горутин / Java virtual threads, но единственная веская причина по которой не стали добавлять - это наличие в языке async-await легаси?

We have chosen to place the green threads experiment on hold and instead keep improving the existing (async/await) model for developing asynchronous code in .NET. This decision is primarily due to concerns about introducing a new programming model.

Аргументация 80 уровня. Давайте я скажу что весь C# и .NET костыльный?

Давайте без истерик. Есть язык и есть философия языка и его стандартная библиотека. Это странный аргумент апеллировать к чьей то авторской библиотеке в разговоре про язык программирования. А если кто-то в своей библиотеке напишет имитацию наследования, имитацию async/await, завезет туда монад и функторов, мы видимо должны прийти к выводу что Go в целом стал не торт и очень сложным языком?

А зачем тащить в язык то что решается на уровне библиотек?

А зачем тогда в стандартную библиотеку понадобавляли HTTP сервер с клиентом, серилизаторы JSON и XML, криптографию, БД, html, логи и прочее? Зачем в язык тащить то что решается на уровне библиотек?

P.S. А знаете ли вы что в C# хотели добавить аналог горутин / Java virtual threads, но единственная веская причина по которой не стали добавлять - это наличие в языке async-await легаси?

Позвольте немного помогу с переводом:
"place the green threads experiment on hold"
означает "отложить эксперимент с зелеными потоками" а не "хотели добавить". Если внимательнее этут тему изучить то можно узнать почему и спойлер дело не только в совместимости.

Мне уже право надоел этот диалог. Я уже потерял суть того что Вы мне пытаетесь доказать, то у Вас каналы не нужны без которых по факту никак, то в качестве примера асинхронных функций Вы пишите синхронные примеры то предлагаете вообще не Go-way а какие то левые библиотеки использовать. На этом откланиваюсь.

Горутины решают не асинхронность, а параллелизм (по задумке авторов).

Обожаю Go за то, что на этом языке намного проще разбираться в чужом коде, чем, например, в коде на Python. А большую часть времени разработчик разбирается в чужом коде, чем кодит сам.

камоон это же чисто ошибка выжившего

если вам повезло и вы не встретили отъявленный говнокод на go, то это не значит что его не существует

В код докера загляни́те. Мне этого было достаточно, чтобы никогда на пушечный выстрел к го не подходить.

Похоже что всё придумал ИИ, т.к. автор использует все языки подряд Scala, Java, Rust, Go - обычный человек так не будет делать.

А упомянутые Haskell, Erlang, Smalltalk и Lisp вас не смутили? :)

обычный человек так не будет делать.

Позволю себе попытаться разубедить вас в своей "необычности": с Java работал года с 2010, с 2014 подключилась Scala и BigData, после 2021 переключился на Go и покинул JVM-стек. Не думаю что это какой-то уникальный опыт: сам знаю много подобных моей историй.

Rust в проде не применял, но писал "для души". Так же как и на Hasekell и Clojure. Вообще очень люблю всякие философские и концептуальные штуки.

Но вообще конечно это теперь новый вид челленджа: написать так, чтобы не сойти за ИИ :)

Меня смутило, что после упоминания Smalltalk вы пишете "Go успел меня избаловать быстрой обратной связью. От идеи до работающего результата — всего пара шагов: простой запуск, моментальная компиляция, отсутствие лишних абстракций.". Как по мне, Smalltalk в подобном не побить никому.

Здесь ключевой момент в том что сравнение идет именно с моим опытом коммерческой разработки :)

С переменным успехом я немного пользовался Pharo в режиме "поиграться". В этом отношении у меня больше опыта в Lisp (Clojure) + REPL. (Не будем воевать на тему того является ли Clojure Lisp-ом)

К сожалению (или к счастью), в своем текущем контексте не могу представить себе Smalltalk или Lisp как основной инструмент для работы команды.

А вот Hylang (Lisp на Python) хорош. Там где под Clojure просто не найдешь актуальных либ, Hylang просто прозрачно подключает любые Python-библиотеки. Красота. :)

Smalltalk не production-ready, к сожалению.

spring медленный, go быстрый

поздравляю вы сравнили тёплое с мягким. напишите сначала на го фреймворк сопоставимый со spring. либо сравнивайте stdlib vs stdlib, либо для примера kubernetes попробуйте из сорцов собрать

Я сравниваю свой "пользовательский" опыт. У меня есть конкретная проблема и я хочу ее решить. Мой субъективный опыт в этом случае говорит о преимуществах Go. А каким образом оно там конкретно под капотом — это уже совсем другой разговор.

вот вы говорите го не дает адекватные concurrency примитивы - это разве хороший опыт? если язык ничего из коробки не предоставляет вы просто обязаны лезть под капот

кстати микрофреймворков под java/kotlin целая гора и там будет быстрый фидбек и лайврелоад и все что угодно

вот вы говорите го не дает адекватные concurrency примитивы - это разве хороший опыт?

А тут как раз штука в том что сталкиваться со сложностями реализации concurrency в Go не приходится в тех пресловутых 80% случаев. Это связано с тем, что Go предоставляет синхронный и понятный API, скрывая под капотом сложный асинхронный рантайм.

А так для написания микросервисов сейчас почти все есть “из-коробки”: стандартный HTTP-стек с поддержкой middleware; request-scoped context для контроля над исполнением запроса и передачи чего-либо вниз по стеку; парсинг json; логирование. Для всего есть простые и стандартные интерфейсы, которые легко интегрировать друг с другом или заменять.

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

кстати микрофреймворков под java/kotlin целая гора и там будет быстрый фидбек и лайврелоад и все что угодно

Согласен, Micronaut, Quarkus, Ktor — действительно хорошие примеры "продуктов", которые бьют в ту же нишу. У каждого уже есть богатая экосистема с хорошей функциональностью. Но нюанс в том что каждый из них выстраивает свою собственную ментальную модель и абстракции. Каждый надо изучать по-отдельности.

Подход Go — концептуально иной. Здесь достаточро простых строительных блоков и интерфейсов из стандартной библиотеки. То, что в других экосистемах требует интегрированного фреймворка (HTTP-роутеры, middleware, логгеры, драйверы БД) — в Go реализуется как лёгкие библиотеки, которые реализуют стандартные интерфейсы (http.Handler, io.Writer, sql.DB). Так что если на определенном этапе и требуется подключение внешних библиотек, то оно при этом не требует изменения архитектуры приложения, построенной вокруг стандартных интерфейсов.

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

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

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

Rust

• Похожая философия «бери нужные ящики (crate) и склей»:

– HTTP: Hyper (bare), Actix-Web, Warp, Axum.

– Middleware: tower-service/tower-layer (де-факто стандарт); все современные фреймворки умеют работать с Tower-слоями.

– Логирование: базовый crate log (макросы info!, warn!); поверх него множество back-end’ов (env_logger, fern, tracing).

– БД: асинхронный sqlx (напрямую к PostgreSQL/MySQL-и др.), Diesel (sync, compile-time схемы), sea-orm, rbatis.

. C# / .NET

• Исторически поставляется «фреймворком»: ASP.NET Core = веб-стек + DI-контейнер + middleware-пайплайн + logging-абстракция + конфигурация + host-lifecycle.

• Это удобно: всё из коробки, единый стиль. Но «легко заменить» удаётся не всегда: чтобы выкинуть встроенный DI или Pipelines, придётся заглядывать в Generic Host/ServiceProvider.

• Пакеты NuGet часто завязаны на Microsoft.Extensions.* (Options, Logging, DependencyInjection) — то есть тянут сразу полдесятка DLL

Java тоже тяжёлый

Похожая философия «бери нужные ящики (crate) и склей

такая философия есть в любом языке, называется внешние зависимости. Каждая из которых по своему уникальна, что порождает определенный вендорлок. Го прекрасен тем что большинство вещей там есть из коробки. Это его философия.

Тоже обратил на это внимание. Даже странно что автор раньше работал в java стеке, но не знает, что spring это framework, а ни как не java. И вообще, сейчас с каждой новой версией java всё проще и проще запускать свой код - уже лет 5 назад можно просто java файл запустить, без компиляции. А сейчас хотят упростить, что бы в этом java файле вообще ничего лишнего не было - ни import, ни main метода - просто сразу твой код.

Да, возможно я действительно отстал от жизни, что-то в экосистеме Java сильно поменялось и теперь все пишут без фреймворков. Тут буду рад референсам.

В моей (альтернативной) реальности:

  • Быстрый старт веб-сервиса на Go без каких-либо зависимостей может продолжать развиваться дальше без какой-то существенной переработки написанного.

  • Быстрый старт веб-сервиса с Java без фреймворка — это то что мне потом придется выбросить и переписать заново уже на Spring/Quarkus/Micronaut. То есть это тогда и не старт вовсе, просто какой-то PoC на выброс.

Поэтому сравнение с plain Java мне просто не интересно, так как не решает мою проблему как "пользователя". В конечном счете я все равно буду работать с одним из фреймворков.

Тема "распределенных бэкендов на Go" не раскрыта. Т.е. самое интересное упоминается вскользь.

Что конкретно вы писали на Go, чтобы он оказался лучшим выбором?

Каков размер команды разработчиков и размер самого проекта, чтобы его можно было вывести используя "максимально приземлённый императивный подход "?

Бэкенды сайта бронирования отелей (тут все будет максимально похоже на любой другой e-com): ETL-джобы, консьюмеры-продьюсеры, REST-сервисы, scheduler-ы и воркеры для state-машин.

Несколько команд из 30 человек поддерживает порядка 170 джоб/воркеров/сервисов.

Из всего могу сказать что конкретно в области процессинга данных (ETL/ELT/etc) и стримминговой обработки потоков данных на Kafka Go уступает в зрелости экосистеме на базе JVM.

Сколько сейчас в 2GIS Go-разработчиков и сколько Scala? На старте применения этих языков было 4 и 3 соответственно.

Тут мне сложно говорить про точные значения, могу приблизительно прикинуть.

  • Scala - порядка 15 человек

  • go - порядка 80 человек

Чем удобен Go, если он кажется тебе устаревшим и непривлекательным?

Если оценивать Go с точки зрения того насколько он красиво спроектирован и реализован — то да, он кажется где-то устаревшим и где-то непривлекательным.

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

Частично ответил про это в комментарии из другого треда:

Подход Go — концептуально иной. Здесь достаточно простых строительных блоков и интерфейсов из стандартной библиотеки. То, что в других экосистемах требует интегрированного фреймворка (HTTP-роутеры, middleware, логгеры, драйверы БД) — в Go реализуется как лёгкие библиотеки, которые реализуют стандартные интерфейсы (http.Handler, io.Writer, sql.DB). Так что если на определенном этапе и требуется подключение внешних библиотек, то оно при этом не требует изменения архитектуры приложения, построенной вокруг стандартных интерфейсов.

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

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

Что «красное» (async/await, Future, Task) гарантирует?

• Это точка, где функция МОЖЕТ приостановиться («suspend») и вернуться позже.

• Раз она может отдать управление, поток ОС в этот момент свободен для других задач.

• Тип меняется: вместо T мы получаем «обещание» (Task<T>, impl Future<Output=T>).

• Поэтому вызов необходимо сопровождать await, then, get, – иначе результата не будет.

То есть отметка обычно сопровождает длительное I/O, но формально она сигнализирует о «приостановимости».

Почему Go не делает такую отметку

• В Go каждая функция ТЕОРЕТИЧЕСКИ может блокироваться сколько угодно.

• Планировщик сам вытеснит заблокированную горутину и перепривяжет её поток. Если горутина действительно «зависла» на системном вызове или GO-рантаймовом примитиве, планировщик узнаёт об этом мгновенно и тут же передаёт P (виртуальный процессор) другой горутине. It's magic! Рантайм узнаёт о блокировке в тот момент, когда сама горутина заходит в известный рантайму участок кода (канал, мьютекс, системный вызов, poller). Это стоит очень дёшево: несколько инструкций или системный вызов, который вы всё равно делаете.

• Языку не нужно отличать «приостанавливаемую» функцию от обычной – это всегда одна и та же горутина. Он сам поймет. Зато программист не поймет. Вот и думайте, надо оно вам?

• Философия: «дешёвые горутины + вытесняющий рантайм» проще, чем усложнённые сигнатуры.

• Цена: вызывающий код не видит, что функция полезла во внешний мир – приходится доверять документации, профилировщику и context.Context.

Почему Rust и C# добавили явный маркер

C#

• Рантайм-потоки дорогие, UI/ASP-поток нельзя блокировать.

• Компилятор генерирует state-машину; без async/await это было бы неотличимо от «обычного» метода и приводило к дедлокам.

• Явный Task в сигнатуре заставляет потребителя либо await, либо сознательно запустить огнём-и-забыть.

Rust

• У языка нет «масштабного» рантайма: state-машина строится при компиляции, а не в рантайме.

• Тип-системе нужно точно знать, будет ли функция отложена → impl Future.

• Одновременно язык стремится к «zero-cost»: если метка не нужна, кода-рантайма нет вовсе.

В обоих языках приоритет на «эксплицитность»: пусть программист видит из заголовка, что перед ним «прерываемая» операция.

Вывод коротко

• Красный/async = «может приостановиться, поэтому результат приходит через обещание; поток на это время свободен».

• Go не метит: рантайм сам отслеживает блокировку; сигнатуры остаются плоскими.

• Rust и C# метят: их модель компиляции/рантайма и требования к безопасности/эргономике делают явную пометку более надёжной.

Я в общем то не вижу проблем добавить пару слов чтобы явно обозначить потенциально виснущие/долгие методы

Вот ещё нашёл прикольное что там под капотом горутины

Ключевые различия между async/await-корутинами (Rust, C#, Kotlin, JS …) и goroutine-ами Go сводятся к тому, что происходит со стеком в момент «сна».

────────────────────────────────────────

Стек полностью разматывается?

• async/await

– Да. После await метод возвращает Pending, стековый кадр функции уничтожается, остаётся лишь state-машина в куче.

– Возобновление ► кадр строится заново из полей state-машины.

• goroutine (Go)

– Нет. Стек каждой горутины остаётся целиком; «точка сна» — это просто место, где рантайм парковал G-объект (goroutine).

– При пробуждении OS-поток (М) снова ставит регистры SP/IP на тот же самый стек и продолжает инструкцию после «блокировки».

То есть у Go «контекст выполнения = регистры + собственный стек», у async/await — «контекст = маленький объект-состояние».

────────────────────────────────────────

Почему Go может держать миллионы горутин, если стек не разматывается?

Потому что:

а) Стек каждой goroutine стартует крошечным (2 КБ с Go 1.4; раньше 4 КБ).

б) Он эластичный: если рекурсия/буфер требуют больше, рантайм делает newStack — копирует текущий стек в более крупный (4 КБ, 8 КБ, …). Обратное сжатие (shrink) выполняется, когда верхняя половина долго пустует.

в) Никаких заранее выделенных 1–8 МБ, как у потоков ОС, нет.

Горутинный «контекст» в памяти:

• struct G ≈ 136 байт метаданных

• struct S (stack) 2 КБ … N КБ, растёт шагами ×2

• прочее (channel / timer узлы) только если вы их используете

Итого типичная пустая горутина ≈ 2,5–3 КБ. Именно поэтому «go func(){}» можно делать хоть сто тысяч раз.

────────────────────────────────────────

Как рантайм Go переключает горутины

M-N планировщик (M — потоки ОС, G — goroutines, P — способность):

При блокирующей операции (← канал, <-ch, time.Sleep, sys-call, netpoll) функция runtime·park записывает:

• указатель на регистр SP (stack pointer),

• указатель на PC (program counter),

• текущее значение G.

Стек остаётся где был.

Поток (M) кладёт G в очередь «waiting» и берёт следующую G из локальной или глобальной runnable-очереди.

При событии (unpark) G ставится обратно в runnable; другой M подбирает её, восстанавливает SP/IP и продолжает.

Сохранить/восстановить регистры — несколько инструкций, поэтому switch < 100 нс.

────────────────────────────────────────

Что происходит при системных вызовах

• Сетевые I/O, таймеры — неблокирующие, ядро сообщит epoll/kqueue, стек горутины «спит» в очереди netpoll.

• Действительно блокирующий sys-call (read файла, DNS через libc): рантайм временно «отвязывает» M от P, создаёт новый поток, чтобы не снижать параллелизм. Стек опять же остаётся нетронутым.

────────────────────────────────────────

Сравнение memory-и time-cost

async/await goroutine (Go)

Контекст state-machine 👉 ~40–120 B stack 2 КБ → N КБ Switch cost вызов poll() + save few regs + матч по state store G pointer Стек во сне отсутствует находится в RAM Способность к росту глубины рекурсии ограничена растёт динамически Кол-во «активов» на 1 ГБ RAM ~10–20 млн ~300–400 тыс.*

*300 000 × 2,5 КБ ≈ 750 МБ (+ метаданные).

────────────────────────────────────────

Что получится, если «перевести» код

• В Go после await-аналога (<-ch) локалы не мигрируют в кучу — они уже лежат в собственном стеке.

• В Rust/C# переносить стеки нельзя без unsafe, поэтому нужен state-machine.

Таким образом:

– «Стек полностью разматывается?» — только в async/await-мире.

– «Go создаёт очень маленький контекст» — да: стартовый стек 2 КБ + 100-байтовая структура G. Это и есть секрет лёгкости горутин.

Если честно, сам не понял до конца что там го делает. Если есть статья по теме дайте ссыль

Java не упомянули - там тоже сделали виртуальные потоки, как в Go - ничего не надо явно писать (async/await).

Здесь уже много чего написали, и о сложности го, и о том как иногда не очень приятно на нём писать.

Но самая хорошая черта - это практически отсутствие лишних абстракций.

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

Наследование это самый плохой вариант полиморфизма

Ого, прямо мой опыт, только я пришел к JavaScript/Typescript и таким же выводам, спасибо!

Обработка ошибок не парит. А вот то что тебе каждый раз чтобы извлечь все ключи из мапы надо писать бойлерплейт - напрягает. Итераторы недавно вон делали, ну так себе выглядит. И вот такие приколы языка раздражают, а я пишу на нем уже 10 лет почти. Кучу времени тратишь на написание однотипного кода преобразования данных. Гораздо больше чем на if err. Имхо конечно.

Как вариант написать скрипт на awk/bash который сгенерит код? Если код типовой и структура есть например в виде полей базы данных, или swagger документации - вполне себе решение :)

А вот то что тебе каждый раз чтобы извлечь все ключи из мапы надо писать бойлерплейт - напрягает.

Даже в Емакс завезли темплейты, не надо тут сказки рассказывать про бойлерплейты, в любой иде это давно уже не проблема

К счастью, уже не надо — с 1.23 есть в стандартной библиотеке функция maps.Keys.

С дженериками стало проще выделить общий код и переиспользовать в проектах.

Спасибо, я знал только про пакет slices. Стало чуть лучше) не хватает встроенных comprehension, чтобы выполнять типовые преобразования map<->slice. Есть сторонние либы, но не так удобно и мало кто использует.

Простота Go была бы хороша в комбинации с сильными гарантиями корректности. Но что на счёт их? Можно ли организовать состояние гонки, модифицируя одни и те же данные из разных потоков и тем самым вызвать падение? Может ли компилятор или среда выплонения от такого защитить?

Да, сильных гарантий Go не дает. Есть race detector, который помогает отлавливать в гонки в рантайме и в тестах. Но это опция, не панацея, и точно не гарантия отсутствия проблем.

На самом деле сама философия Go не совместима с желанием иметь сильные гарантии. Go с сильными гарантиями перестанет быть Go и станет уже чем-то другим. Чем-то ближе к Scala и Rust-у.

Классическая история про то что достоинства обусловлены и неотделимы от недостатков.

Может ли компилятор или среда выплонения от такого защитить?

Есть рейс детектор. Но если писать так как рекомендовано, то как правило гонки не случаются, потому что есть каналы. А уж если выявился рейс то его исправить быстрее чем делать все супер правильно.

с сильными гарантиями корректности

Если бы они были нужны всем, уже все давно писали бы на расте. Однако большая часть веб сервисов в этом не нуждается.

  в Go я всё ещё могу писать код, который легко тестируется, которым просто оперировать, с чётким разграничением между чистыми и функциями с побочными эффектами — и всё это делать без монад и tagless final (со всем уважением к ним).

Вот хотелось бы код посмотреть. Сомнительно что-то

В других языках тоже можно писать прямолинейно, но в какой-то момент в Scala стало модно писать максимально абстрактно с потенциальной возможностью подменить эффект, фреймворк и т.п. Вещи безусловно полезные для библиотек, но не столь необходимые в продуктовом коде.

В Go также можно делать абстракции. Например, везде оперировать интерфейсами. Они похожи на тайп классы — реализацию интерфейса можно сделать для любого типа. Некоторое неудобство, что надо будет явно исходный тип обернуть в реализующий нужный интерфейс.

Go, даже способствует созданию новых типов (type definition) — вполне себе ньютайпы, жаль, что сложно ограничить инварианты создания. :)

Жаль нет возможности описать объединение типов (аналог sealed trait) с проверкой на стадии компиляции, что все инварианты обработаны. Похожего поведения можно достичь через интерфейс и приватный метод — это не даст сделать его реализацию вне пакета, но всё равно внутри switch компилятор не сможет проверить, что были учтены все варианты.

В целом, обращаться абстракциями можно и в Go, равно как и не делать этого в других языках.

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

Самый просто пример где это применимо — HTTP/gRPC сервер. Использовать собственную обвязку над обработчиками, которые бы перехватывали паники и приводили их к возврату ошибки клиенту. В итоге в бизнес коде можно сократить количество ‘if err != nil‘. Самое сложное прийти к взаимным соглашениям об этом и убедится, что это существенно не влияет на производительность. И конечно определить и зафиксировать типы, используемые в паниках.

Мне казалось, что будущее — в Haskell, Erlang, Smalltalk, Lisp.

Несколько примеров:
• Обработка ошибок. В других языках у тебя исключения, try/catch, иногда монады (Either или Try). В Go у тебя if err != nil.
• Асинхронщина и планировщик.

В эрланге у вас паттерн-матчинг, который в триста раз эффективнее

   A = {ok, <<"foobar">>},
   case A of 
      {ok, <<"foo", _/binary>>} -> io:fwrite("ok");
      {ok, Other} -> io:fwrite(<<"Error: unexpected OK: ", Other/binary>>);
      {error, Msg} -> io:fwrite(<<"Error: ", Msg/binary>>)
   end.

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий