Pull to refresh

Comments 749

UFO just landed and posted this here

С этим есть нюансы.


  • Некоторые утилиты для go generate и средства работы с кодом для IDE до сих пор в некоторых случаях работают не так хорошо, как с пакетами в GOPATH, а то и вовсе не работают. С этим постепенно справляются, но тем не менее.
  • Не слишком понятно, почему требуется "v" в начале имени тега.
  • Возможно, только у меня, но принудительный вендоринг иногда выкидывает поддиректории из репозиториев, из которых подключается несколько библиотек (например, если из github.com/a/b импортируются /c и /d, в папке vendor может быть github.com/a/b/c, то /d не будет), происходило с gogo-proto

В целом удобно, т.к. гвоздями прибитая структура пакетов не есть хорошо, но GOPATH удобный, когда через go get устанавливаются исполняемые файлы. И поломанные утилиты вроде stringer печалят конечно.

В вышеприведенном коде нет обработки ошибок, только их переброс наверх. Подход Go подталкивает к обработке ошибок, а не на отсылку их выше в надежде, что кто-нибудь разберется.
Есть понятие ошибки и есть понятие исключительных ситуаций.
Их следует четко различать:
Ошибки логически ожидаются, и их как правило стараются обработать тут же.
Exception`ы возникают в ситуациях, когда нормальная работа какой-то подсистемы не возможна и следует «оповестить» об этом вышестоящий код.

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

Конечно можно обойтись одними ошибками (в виде кодов возврата и т.д.), но оверхеда становится очень много.
Когда нормальная работа подсистемы невозможна, то она должна быть отключена. Для этого нужно ее соответствующим образом запрограммировать (в виде процесса или потока). А исключительные ситуации удобны для разработки библиотек, а в обычном коде ошибки надо обрабатывать, а не перебрасывать их «вышестоящему коду».
UFO just landed and posted this here
Я по возрасту ближе к Пайку и занимаюсь промышленным программированием более тридцати лет, поэтому разделяю его подход к разработке языка. И я не считаю неопытных программистов быдлокодерами.
Слишком глуп, чтобы придумать вменяемый аргумент? Аппелируй к возрасту!
Смысл фразы во второй части предложения, а не в возрасте.
Ааа! Вы о аппеляции к авторитету? Тоже не лучший аргумент
Глупое предположение. Как можно апеллировать к авторитету в интернете? Ключевое слово «промышленный».
Такой софт живет десятки лет и переживает смену многих программистов. Для такого софта тотальная огороженность и упрощенность Go очень подходит.
Как можно апеллировать к авторитету в интернете?
Не знаю. Вы зачем-то пытаетесь и сразу два раза. Сначала пытаетесь повысить свой авторитет при помощи авторитета Пайка методом сравнения, а потом, зачем-то, аппелируете своим авторитетом. Увы, авторитета у вас нету, вы просто пустослов, потому это выглядит смешно.
Вы сказали две вещи:
1) «Я старый»
2) «Я программирую давно».

Первое — апелляция к возрасту, второе — к авторитету. Ни одного конструктивного аргумента вы не предъявили.
И в go используются и реализованы оба подхода… Ошибки это просто значения а для исключительных ситуаций есть `panic, defer, recover`

Не совсем так. Технически panic можно для этого использовать, но он предназначен для другого: для багов. Если в коде возникла ситуация, которая не должна была возникнуть при штатной работе (и ошибки, которые вполне могут возникать штатно, не важно насколько они критичны и может ли приложение продолжать работу при их возникновении сюда не входят), т.е. наличие такой ошибки является признаком наличия в коде бага — вот тогда и только тогда нужно использовать panic. Помимо этого есть только один допустимый способ использования panic а-ля исключения, т.е. для управления потоком выполнения — внутри пакета, с обязательным перехватом через recover и преобразованием в обычную ошибку на границе пакета, чтобы из его публичного интерфейса эти паники не вылетали.

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

В других языках, к сожалению, перекос обратный: исключением является нечто логически ожидаемое. Например NoDataFound для sql запроса практически классика. И справедливости ради, вывалится в вышестоящий код в Go можно: с помощью panic, хотя и работает это не так, как raise.


Тут ваши слова можно формализовать. Есть некие события, которые должны быть инициированы на уровене вложенности x, а обработаны на уровне вложенности y, где x>y. Если прокидывать эти сообщения явно, через возвращаемые значения, то, чем больше x-y, тем больше лишних действий надо описать явно. Очевидно, что на уровне x и на уровне y код будет осмысленный, а на уровне z, таком, что x>z>y, это будет код "пересылки".


При этом сама передача через возвращаемое значение не является бедой. Этот подход известен множество лет. Неприятно именно отсутствие специального синтаксиса.


Так может, старый добрый препроцессор нас спасет? Спасает же в C? Ну или хитроумные IDE с кодогенерацией, как это принято в Java.


Предложнения по добавлению сахара, появляются чуть ли не каждый день: https://github.com/golang/go/issues/21155 Пока отвергают, но думаю шанса отвертеться нет. Рано или поздно какой-то вариант появится.

UFO just landed and posted this here

Для этого в принципе нужна хорошая система типов, по типу Хаскеля. Т.е. это ещё более глубокий вопрос.

Типа Option не будет, пока не будет generics. Потому что без них он мало осмысленный.

Есть ещё одна паршивая проблема: логически ожидаемое исключение, это исключение ожидаемое вызывающим кодом, но с точки зрения кода подымающего исключение, тип этого события совсем не очевиден. И если эти два участка кода пишут разные люди, то мы получаем перекосы в разные стороны. По всей видимости подход авторов Golang: если проблема сложная, то мы просто не будем её решать — в данном случае несколько оправдан.

Допустим вы пишете класс, описывающий конфиг приложения. Конфиг должен быть всегда. Что делать, если файл конфига есть, но прочитать его не получатеся?
Фейлить старт, это же очевидно.

Как именно? Возвращать nil вместо объекта конфигурации? А что пользователю с этим делать? Он может хочет узнать, почему файл не читается.

Вот тут был мой пример из очень старого проектика. Как раз про конфиг.
Уронить приложение из-за невозможности прочитать файл? Пользователи скажут спасибо
Паника != уронить. Паника — это штатный механизм, который можно обработать.
И что вы предлагаете делать, если конфиг не читается? Загружать пустой дефолтный и пытаться работать? Ну вариант конечно, но на мой взгляд совсем не очень. Я бы лично в такой ситуации выкидывал панику с записью в лог (или консоль если это консольное приложение).
Паника != уронить

По сути — равно. Глобальный хэндлер паники, конечно же, можно настроить, но как правило он просто залоггирует что-то (потому что контекста сделать что-то полезное у него тупо нет). И если после этого он не упадет и оставит программу в потенциально некорректном состоянии, то это даже хуже, чем молчаливые падения.

Что касается вопроса «что же я предлагаю», я ответил чуть ниже: habr.com/post/434446/#comment_19557444
Нет, «уронить» — это сделать так, чтобы программа упала, совсем (ну, как грубый пример, преднамеренно написать код так, чтобы программа убивалась OOM killer в определенный момент).
Паника — это что-то вроде глобального исключения, которое можно поймать и обработать. Штатный механизм для исключительных ситуаций.

> Нет, я предлагаю не «Решать на месте», а дать разбираться с ситуацией вызывающему коду.

И что же может сделать вызывающий код, в обсуждаемом случае? Выкинуть панику? :)
Паника — это что-то вроде глобального исключения, которое можно поймать и обработать. Штатный механизм для исключительных ситуаций.

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


И что же может сделать вызывающий код, в обсуждаемом случае? Выкинуть панику? :)

Например сделать MessageBox::show(error.GetText()). Или взять конфиг по-умолчанию. Или еще что-нибудь. Вызывающий код знает, что с этим сделать. А может запаниковать, да. Он пусть решает. Хорошая статья в тему.

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

When a function encounters a panic, its execution is stopped, any deferred functions are executed and then the control returns to its caller. This process continues until all the functions of the current goroutine have returned at which point the program prints the panic message, followed by the stack trace and then terminates.

Recover is useful only when called inside deferred functions. Executing a call to recover inside a deferred function stops the panicking sequence by restoring normal execution and retrieves the error value passed to the call of panic. If recover is called outside the deferred function, it will not stop a panicking sequence.


> Например сделать MessageBox::show(error.GetText()). Или взять конфиг по-умолчанию.

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

И все же: у вас остается программа в неконсистентном состоянии. Если у нас был код типа


db.Update(reading: true);
var file = ReadFromFile();
db.Update(reading: false, filename: file.name);

Если ReadFromFile() паникует, мы не выполняем последнюю строчку и остаемся в неконсистентном состоянии. Если же выполняем, то чему будет равен file.name?


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

библиотека, работающая с JSON, которая имеет зависимость на GTK чтобы показывать сообщение об ошибке в мессадж боксе? Ну даже не знаю, что про это можно сказать :)


Заполняем там же структуру дефолтными значениями например.

Ну это и есть "молчаливое" поведение, о чем в статье выше расписано. Вы читали?


Если это рекомендуемое поведение го, то спасибо, не надо такого.

> Если у нас был код типа

А зачем так делать? О_О
Не нужно так делать никогда, запись в базу должна быть атомарной, а не размазанной вот так.
Прочитали файл — пишем в базу. Не прочитали — не пишем вообще ничего.

> библиотека, работающая с JSON, которая имеет зависимость на GTK чтобы показывать сообщение об ошибке в мессадж боксе? Ну даже не знаю, что про это можно сказать :)

Ну всяких примеров можно много разных насочинять. Я про «действовать по ситуации», по-моему, пару раз уже тут говорил.
Деньги упали на счет клиента — пишем в базу.
Не упали — не пишем ничего.

Правильно я вас понял?
Что ответить на ваш вопрос, опишите сначала полностью весь кейс.
Если вам нужно записать что-то из файла в базу, то очевидно что вам нужно прежде всего открыть и прочитать этот файл перед записью, а не в процессе.
UFO just landed and posted this here
> А вот в этом и дело: нету всего кейса. Вы пишете библиотеку, и про весь кейс будут знать только её пользователи.

Ну если нету кейса (== нет ТЗ, как минимум в голове разработчика), то о какой разработке может идти речь?
Чет мне сейчас тот анекдот про японскую пилу и суровых сибирских лесорубов вспомнился.
UFO just landed and posted this here
> В первом невозможность при инициализации загрузить вспомогательные данные означала необходимость попробовать с предыдущей версией тех же данных (после дополнительных проверок их достаточной актуальности), во втором — сразу экзепшон и крашиться.

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

Возвращаясь в контекст го — вот типовой пример — golang.org/pkg/net/http/#Server — инициализируем структурой с настройками. Если что-то не так — то ListenAndServe вернет ошибку, и типовая сферическая инициализация выглядит как-то так:

s := &http.Server{
	Addr:           ":8080",
	Handler:        myHandler,
	ReadTimeout:    10 * time.Second,
	WriteTimeout:   10 * time.Second,
	MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())


Т.е. оно не думает что и откуда прочитать для получения настроек, оно просто принимает структуру. А где и как вы её получили — дело исключительно вашего приложения.
UFO just landed and posted this here
Ну вот у вас есть «работал в двух режимах: онлайн и replay» — на мой взгляд, вполне достаточные требования, от которых можно дальше что-то конкретное делать, а не изобретать универсальный обработчик всего.
А функция, которая загружает данные и не должна по идее знать о том, как и где эти данные используются. Смогла загрузить — отдала вызывающему. Не смогла — отдала код ошибки. А вызывающий уже сам решает что со всем этим делать. Если мне память не изменяет, это один из паттернов проектирования.
UFO just landed and posted this here
Ну как это. Кейсы же уже описаны — функция загружает и отдает данные (или ошибку), вызывающее приложение решает что с ними делать. Суть примерно понятна, и уже можно писать тесты, например.
Все кейсы в подробностях мы можем и не знать на этапе начала разработки, но все довольно быстро проясняется.
Или я не понял о чем мы вообще дискутируем?
UFO just landed and posted this here
Эм, вообще-то знаю :)
Например в случае конфига какого-нибудь сервиса — надо падать, если мы не можем его получить или он кривой. Потому, что без конфига или с кривым конфигом работать этому сервису будет как-то сложно.
Или же мы можем навернуть тут абстракцию, и сначала попытаться загрузить конфиг с локального ресурса, потом посмотреть, например, в какой-нибудь etcd, или еще что-то такое. Но в любом случае, конфиг нам нужен.

Другой пример — ну скажем, условный s3cmd. Мы можем посмотреть на типовые локации конфига, и если там ничего нет — используем дефолт. Есть критические опции, без которых запуск утилиты не имеет смысла. Если их нет — тогда падаем с соотв. ошибкой.

UFO just landed and posted this here
Мы же вроде весь тред про конфиги начинали? Ну не суть.
На уровне этой функции нам и не нужно что-то решать и вообще знать что делать в случае ошибки (пытаться загрузить альтернативные данные, пытаться загрузить данные через минуту или забить). Она вызывается с путем до данных, пытается их по этому пути получить, и возвращает результаты. Все. Вся остальная логика в вызывающем коде. Можно еще навернуть там таймауты и попытки повторного запроса, но это уже опционально.
UFO just landed and posted this here
А зачем так делать? О_О
Не нужно так делать никогда, запись в базу должна быть атомарной, а не размазанной вот так.

Ну а как тут сделаешь атомарной, если там паника. Даже если какой-нибудь try finally блок делать, он по идее не выполнится.

Прочитали файл — пишем в базу. Не прочитали — не пишем вообще ничего.

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

Ну всяких примеров можно много разных насочинять. Я про «действовать по ситуации», по-моему, пару раз уже тут говорил.

Ну я вот спрашиваю про конкретную ситуацию, что делать предлагается. Ситуацию вы вполне знаете, требования простые: чтобы я мог прочитать конфиг, и сделать кто-то, если с конфигом что-то не в порядке.
> Ну а как тут сделаешь атомарной, если там паника. Даже если какой-нибудь try finally блок делать, он по идее не выполнится.

Открыли файл, прочитали его (или его блок если файл большой), записали в базу.
Если хреново открыли/прочитали, записи в базу не будет.
Ну в самом деле, это такие элементарные вещи, что как-то даже странно не знать их.

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

Какой стейт и зачем? Опишите кейс более подробно.

> Ситуацию вы вполне знаете, требования простые: чтобы я мог прочитать конфиг, и сделать кто-то, если с конфигом что-то не в порядке.

Я бы сделал именно так как уже говорил — ошибка в лог/консоль и аварийное завершение работы. Плюс в CLI еще, возможно, добавил бы опцию для генерации дефолтного конфига. Но запуск с нечитаемым/испорченным конфигом — это аварийная ситуация.
В ситуации чисто десктопного GUI приложения — да, выкидывать окошко с ошибкой. Тут уже можно поиграть на тему независимого error helper'а, запускаемого при аварийном завершении основной программы (или на тему засылки во что-то, что само покажет окошко с мессагой, ну dbus например).
Открыли файл, прочитали его (или его блок если файл большой), записали в базу.
Если хреново открыли/прочитали, записи в базу не будет.
Ну в самом деле, это такие элементарные вещи, что как-то даже странно не знать их.

Как не будет? Мы должны до записи сказать типа «мы начали читать файл», а после сказать «мы больше не читаем файл».

Какой стейт и зачем? Опишите кейс более подробно.

В данном случае БД. Например, у нас несколько инстансов, и если один инстанс начал читать файл, то он выставляет флажок, чтобы другие инстансы понимали, что файл читается, и не трогали его.

Я бы сделал именно так как уже говорил — ошибка в лог/консоль и аварийное завершение работы.

Я не хочу завершение работы, я хочу обработать ситуацию. Но это видимо не go-way?
> Как не будет? Мы должны до записи сказать типа «мы начали читать файл», а после сказать «мы больше не читаем файл».

И почему это может вызвать проблемы?
Ставим флажок «я начал читать файл». Открываем — не открывается — по деферу сбрасываем флажок. В базу ничего не попадает.

> В данном случае БД. Например, у нас несколько инстансов, и если один инстанс начал читать файл, то он выставляет флажок, чтобы другие инстансы понимали, что файл читается, и не трогали его.

Посмотрите вот тут — github.com/minio/minio/blob/master/docs/shared-backend/DESIGN.md#architecture — очень неплохой пример как надо делать такие вещи.

> Я не хочу завершение работы, я хочу обработать ситуацию. Но это видимо не go-way?

Я вас где-то заставляю делать так? Делайте как вам нужно.
И почему это может вызвать проблемы?
Ставим флажок «я начал читать файл». Открываем — не открывается" по деферу сбрасываем флажок. В базу ничего не попадает.

Дефер будет вызван в случае паники?

Ок, а что будет в случае другой паники во время обработки этого дефера?

Вторая паника будет добавлена к описанию первой, а в остальном никакой разницы, одна паника или две: https://play.golang.org/p/Of3KiMh4Tdg — пока не вызвали recover() паника продолжит раскручивать стек и выполнять defer-ы.

Да, конечно, defer вызывается при любом выходе из функции, хоть return хоть panic.

Как не будет? Мы должны до записи сказать типа «мы начали читать файл», а после сказать «мы больше не читаем файл».


Ну, т.е., если по человечески: мы должны открыть файл, прочитать файл, закрыть файл, и уже после этого начать писать в базу?

В данном случае БД. Например, у нас несколько инстансов, и если один инстанс начал читать файл, то он выставляет флажок, чтобы другие инстансы понимали, что файл читается, и не трогали его.


Чуть раньше было про конфиг, ага. Ну да ладно, Г-дь вам судья.

Понимаете, тут все несколько проще: один инстанс открывает файл на чтение и… никаких флажков, оно блокируется на уровне ФС, ресурс занят. Читает файл, закрывает файл (освобождает ресурс). Все.

Я не хочу завершение работы, я хочу обработать ситуацию. Но это видимо не go-way?


В случае с поломанным конфигом хотеть вы можете много чего, а нужно вам всегда тупо налить логов и упасть.

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

В случае с поломанным конфигом хотеть вы можете много чего, а нужно вам всегда тупо налить логов и упасть.

Еще раз, я апеллирую к тому комменту, где функциия «должна решать на месте». Если функция решает на месте, то я не могу как внешний код отреагировать на ошибку, потому что функция что-то там «порешала», а я об этом ни сном, ни духом. Если же функция каким-то образом сообщает наверх «нишмагла», то это и есть прокидывание ошибки, которое оказывается не go-way.
Если же функция каким-то образом сообщает наверх «нишмагла», то это и есть прокидывание ошибки, которое оказывается не go-way.


Вы слишком прямо восприняли призыв «решать на месте». «Решение на месте», в первую очередь, заключается в выборе «сделать с этим что-то здесь и сейчас» или «прокинуть наверх». Т.е. вся эта вещь, на которую вы упираете, на самом деле лексически-парсится в «программист обязан явно решить, делать ли что-то с ошибкой, проигнорировать возможность ее появления, либо пробросить наверх.»

Никакого запрета на проброс вверх там нет.

if err := someFuncCall(); err != nil {
    return err
}


Вот это совершенно идиоматичный Go-код. Так выглядит «проброс наверх».

Замените `return err` на обработку ошибки, и это будет тоже идиоматичный Go-код (не рекомендованный, к слову, в библиотечных пакетах).

Хотите впрямую заигнорить, `_` вам в помощь.

Единственная фишка языка в плане «должен решать на месте» — это необходимость явно указать, что вы хотите с ошибкой сделать. Нельзя в сигнатуре main написать `throws Exception` и положить на обработку ошибок как таковую.

И вот из-за этого весь сыр-бор.

и есть прокидывание ошибки, которое оказывается не go-way.


Поверьте, совершенно нормальный Go-way, прямо идиоматичный даже.

Единственная реальная трабла обработки ошибок — это многословность. Но это решается, оно уже в бете.

Тогда я согласен в сами, и не согласен с автором исходного утверждения (а транзитивно — вы тоже с ним не согласны).

В таком случае подождем его ответа, либо обсуждение исчерпано.

Спасибо за диалог.
Если ReadFromFile() паникует, мы не выполняем последнюю строчку и остаемся в неконсистентном состоянии. Если же выполняем, то чему будет равен file.name?


В случае, если ошибка с чтением произошла на этапе чтения конфига, очевидно, надо срать в лог и крашиться.
Зависит от того, насколько критичен конфиг. Eсли не можете загрузить конфиг, можно использовать дефолтный конфиг и WARN в лог или stderr вывести. Если конфиг критичен — то остановить старт.
Ну представим, что вы пишете библиотеку, и не знаете, насколько критично отсутствие файла.
Библиотека не должна принимать как параметр файл с конфигурацией.

Когда я пишу какой-нибудь сервис, который имеет конфигурационный файл, я делаю следующее:
1. Создаю дефолтный конфиг.
2. Если конфиг файл не был определен — использую дефолтный конфиг.
3. Если конфиг файл определен — читаю конфиг файл и переписываю дефолтный конфиг (тот, что был создан сначала).
4. Если, когда конфиг файл определен, но что-то идет не так — файла нету, он не читаемый или конфиг не валидный — останавливаю сервис. Тогда упадет только один инстанс, а все другие будут раниться с правильной конфигурацией (пропагация деплоймента остановится).
Библиотека не должна принимать как параметр файл с конфигурацией.

Ну как это не должна? log4rs, Microsoft.Extensions.Configuration.Json, cfg4j и все прочее…


Если, когда конфиг файл определен, но что-то идет не так — файла нету, он не читаемый или конфиг не валидный — останавливаю сервис. Тогда упадет только один инстанс, а все другие будут раниться с правильной конфигурацией (пропагация деплоймента остановится).

Ну так вдруг не надо падать, а просто сообщение куда-то вывести. Падать на любой чих это такое.

Ну как это не должна? log4rs, Microsoft.Extensions.Configuration.Json, cfg4j и все прочее

Ах интерпрайз.
IMO, библиотека должна работать с конфигурационным объектом, а не файлом.

Ну так вдруг не надо падать, а просто сообщение куда-то вывести. Падать на любой чих это такое.

Я наверное не понял контекст, ну не падайте раз так, WARNING в лог тогда. Для юзер приложений это может и лучше, для backend — лучше падать.
Ах интерпрайз.
IMO, библиотека должна работать с конфигурационным объектом, а не файлом.

А объект откуда возмьется?

Я наверное не понял контекст, ну не падайте раз так, WARNING в лог тогда. Для юзер приложений это может и лучше, для backend — лучше падать.

Ну мое мнение, что нужно прокидывать наверх до тех пор, пока не встретится код, который знает, что делать с ошибкой. А наше обсуждение различных костылей как раз и показывает, что попытка вместо этого порешать всё «на месте» имеет те или иные недостатки.
А объект откуда возмьется?


С конфиг файла, но тут уже вы решите что делать с нечитаемым файлом или его отсутствием, а не полагаться на разработчика библиотеки (вернет ли он ошибку или панику?).
Кстати, вот тут ваше мнение расходиться со второй репликой (про прокидование на верх).
Второе, чем больше библиотек, тем больше конфиг файлов? А потом еще разный формат — один хочет yaml, второй toml, третий ini.
С конфиг файла, но тут уже вы решите что делать с нечитаемым файлом или его отсутствием, а не полагаться на разработчика библиотеки (вернет ли он ошибку или панику?).

Если библиотека бросает панику — то надо руки такому разработчику вынимать.


Кстати, вот тут ваше мнение расходиться со второй репликой (про прокидование на верх).

Не расходится. В идеале паник не должно быть вообще нигде, кроме main. Либо делаем что-то, либо прокидываем ошибку выше, всё. Кодекс самурая я линковал.


Второе, чем больше библиотек, тем больше конфиг файлов? А потом еще разный формат — один хочет yaml, второй toml, третий ini.

Поэтому делайтся генерик — интерфейс (например, в случае выше это Microsoft.Extensions.Configuration), а потом делаются разные реализации (Microsoft.Extensions.Configuration.Json, Microsoft.Extensions.Configuration.Xml, Microsoft.Extensions.Configuration.Toml, ...)

Поэтому делайтся генерик — интерфейс (например, в случае выше это Microsoft.Extensions.Configuration), а потом делаются разные реализации (Microsoft.Extensions.Configuration.Json, Microsoft.Extensions.Configuration.Xml, Microsoft.Extensions.Configuration.Toml, ...)


Выглядит как какая-то жесть и попытка «впихуть невпихуемое».

То, что кто-то это делает — не повод считать что это хорошо. Библиотека на то и библиотека, что она не знает, и не должна навязывать конкретный контекст своего выполнения. Всё взаимодействие библиотеки с внешней средой — получение конфига, логирование, доступ к БД — правильнее оформлять через DI, причём принимать все эти зависимости библиотека должна как интерфейсы.


Даже если в 99% случаев использования данная библиотека действительно должна тупо читать свой конфиг из файла /etc/библиотека.yml, то она должна предоставить в своём публичном API вспомогательную функцию DefaultConfig(), которая считает этот файл и вернёт значение, подходящее для передачи DI-параметром основному конструктору/инициализатору этой библиотеки. В этом случае эти 99% кода будут содержать что-то вроде:


something := some.New(some.DefaultConfig(), mylog, mydb)
Ну хорошо, так и есть. Теперь мы ей передали строковую константу, файл такой есть, а она его открыть не может. что дальше эта библиотека должна «на месте» решить, не прокидывая ошибку вверх?

Вы не поняли, DefaultConfig возвращает не имя файла с конфигом, а значение содержащее сам конфиг. Откуда это значение получено — из файла, или из другого места — библиотеке дела нет. Использовать DefaultConfig или что-то другое — решает вызывающий код (т.е. main, которые единственный знает текущий контекст выполнения, откуда брать конфиги, куда писать логи, и как подключиться к БД).


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


Например, если для библиотеки является штатной ситуацией работа без конфига (точнее, с использованием дефолтного конфига прошитого в самой библиотеке), и отсутствие в системе /etc/библиотека.yml это типичная ситуация, то DefaultConfig() может вообще игнорировать все ошибки и возвращать nil, который библиотека трактует как "взять дефолтный конфиг". Как по мне — это немного грязно, DefaultConfig стоит хотя бы вывести в лог все ошибки кроме отсутствия файла.


Но в более типичном/общем случае это делается немного иначе, просто не в одну строку:


someConfig, err := some.Defaultconfig()
if err != nil {
    // мы в main(), тут log.Fatal можно
    log.Fatalf("failed to load config: %s", err)
}
someThing := some.New(someConfig, log, db)

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


let config = Config::default();
let config = match config.merge(Config::from_file("some/path/config.json")) {
   Ok(config) => config,
   Err(ParsingError(e)) => config.merge({timeout: 2000}),
   Err(e) => panic!("Unexpected error {:?}", e);
}

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


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


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

Какую строку Вы назвали "последней"? Последняя создаёт someThing, но дальше Вы пишете опять про конфиг, так что я несколько запутался. Если Вы говорили таки про строку с someThing, то там никто не падает, вызов some.New() в этом примере не может завершиться с ошибкой — это довольно типично для простых конструкторов. Если бы там могла возникнуть ошибка (напр. если он как-то проверяет переданный конфиг на валидность), то он просто вернул бы someThing, err.


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

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


Если либа за меня решает, что делать ей с ошибкой "на своём уровне", то я никак не могу принять решение там, где это действительно нужно.

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

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

Только библиотека знает, валидный ли файл конфига или нет. Как вы это сделаете «еще до инициализации библиотеки»?
package main

import "libA"

type config struct {
    LibA libA.Config
    ...
}

func main() {
   var cfg config
   if err := c.loadConfig(*configFile); err != nil {
       log.Fatal(err)
    }
    ...
} 

А loadConfig уже парсит конфиг файл и валидирует его в соответствии со структурой.

Только библиотека знает, валидный ли файл конфига или нет. Как вы это сделаете «еще до инициализации библиотеки»?

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


  1. Дано: есть некая библиотека, достаточно сложная, чтобы ей мог понадобиться собственный конфиг-файл. Тем не менее, хочется избежать создания жёсткой связи между библиотекой, которая может оказаться частью любого приложения выполняющегося в любых условиях, и конкретным конфиг-файлом на диске.
  2. Чтобы разорвать эту связь, мы модифицируем функцию-конструктор/инициализатор библиотеки так, чтобы она принимала параметром структуру, описывающую её конфигурацию, вместо того, чтобы самостоятельно считывать и парсить конкретный конфиг-файл. Таким образом, ответственность за считывание и парсинг конфиг-файла переносится на вызывающий код, обычно это main() конкретного приложения, знающего условия в котором оно выполняется, и решающего, откуда и как получить конфигурацию для этой библиотеки.
  3. Тем не менее, предполагая, что большинству приложений подойдёт подход по умолчанию, заключающийся в считывании конфигурации из конкретного файла, мы добавляем в библиотеку отдельную, опциональную, независимую вспомогательную функцию, которая умеет исключительно считать этот конфиг-файл и вернуть структуру с конфигурацией, необходимую для инициализации библиотеки.
  4. В результате, main() имеет возможность проинициализировать библиотеку в два шага: сначала вызвав вспомогательную функцию для считывания конфиг-файла, и потом передав структуру с конфигурацией в функцию инициализирующую библиотеку. На обоих шагах могут возникнуть ошибки, которые main() может обработать по своему разумению.

Помимо прочего, такой подход крайне рекомендуется для упрощения тестирования библиотеки, т.к. тест сможет легко подсовывать библиотеке разные структуры с конфигурацией, без необходимости создавать/изменять конфиг-файл(ы) на диске. Это и есть типичный пример тех "разных условий, в которых может выполняться библиотека", в которых безусловное чтение конфиг-файла может оказаться неуместным.

Дано: есть некая библиотека, достаточно сложная, чтобы ей мог понадобиться собственный конфиг-файл.

Нет, конечно же. Библиотека с конфиг-файлом это антипаттерн, библиотека должна конфигурироватсья через код.

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

Соответственно, всё остальное уже следствие из неверной посылки.
К конфигам чего, простите, предоставляет доступ сторонняя библиотека?
Ну вы же вот прям у себя в коде с match обработали ошибку.
Я могу точно такой же код на Go написать.

Ну так, смысл в том, что у нас есть наш app, и зависимый пакет config. Так вот, ошибка происходит в последнем, и у него есть два способа:


  1. Пробросить ошибку в app, который соответственно по ней сделает матч и примет какое-то решение
  2. Решит проблему "на месте", и сделает что-то друоге: вернет конфиг по-умолчанию, вернет nil, ко всему этому напишет в лог, запаникует и т.п.

Я всегда считал, что правильный вариант — 1, но как выше говорилось, "go way" в том, чтобы следовать варианту 2 в той или иной форме.

А зачем вам отдельный пакет config? Очень часто это просто ненужно.
Опять же, как я делаю:
1. В текущем пакет есть структура `Config`.
2. Структура `Config` содержит конфигурацию все других пакетов.
3. Все обработка конфиг файла в пакете `main`.

Если брать ваш случай, то библиотека не должна работать с конфиг файлом (только структуры и их валидация, а не как вы примеры делали с .NET). Если уже есть такое, то при любой ситуации вернуть ошибку. Библиотеки никогда не должны принимать решения, только делегация. Вот и все.
А зачем вам отдельный пакет config? Очень часто это просто ненужно.

Затем, чтобы не писать тыщи строк загрузки/валидации/парсинга/опциональных значчений/автоподгрузки изменений с разных файловых систем/черта в ступе, а иметь всё из коробки…

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

Ну как не должна. Как вы, например, сделаете, что у вас есть сервис (MVC контроллер, к примеру), который берет параметр из конфига, и при изменении конфига в ФС через DI автоматически прокинуть имзенения в контроллер, чтобы последующие вызовы его методов использовали уже новый конфиг?

Если Вам нужен очень умный самообновляющийся конфиг, то всё делается ровно так же, как я описывал — просто вместо тупой структуры с конфигурацией мы передаём в функцию инициализации библиотеки объект с методами, соответствующими интерфейсу Config. Библиотека получает значения конфига через вызов методов этого объекта в тот момент, когда они ей нужны, а откуда этот объект их берёт и каким образом обновляет на лету — библиотеке не важно. Суть остаётся ровно та же самая: мы можем передать библиотеке объект конкретного типа возвращённый функцией DefaultConfig (работающий через чтение/парсинг/перечитывание дефолтного файла с конфигурацией), а можем передать объект любого другого типа с теми же методами (напр. тестовый мок, который содержит прошитые в тесте значения конфигурации и изменяет их в момент, когда это нужно тесту).

А кто будет писать все эти «самообновления»? В моем случае, за меня уже написал майкрософт, надо просто подключить библиотеку, и дать ей файлик. А на выходе я получу распаршеное значение, которое умеет во все эти самообновления.
Кхм, поищите написанный «самообновления» под Go. Если это действительно часто бывает нужно (на самом деле, нет, конечно), то наверняка есть пара проектов на github'е, которые это умеют.
Затем, чтобы не писать тыщи строк загрузки/валидации/парсинга/опциональных значчений/автоподгрузки изменений с разных файловых систем/черта в ступе, а иметь всё из коробки…


В том и прикол, что библиотека не должна содержать в себе код работы с «парсингом разных файлов конфигурации на разных файловых системах, содержащих одного и того же черта в ступе» (за исключением тех случаев, конечно, когда это библиотека парсинга разных файлов конфигурации на разных файловых системах, содержащих одного и того же черта в ступе).

Библиотека должна получать конфиг при инстанцировании. Готовый. Не файл на диске, а готовых прочитанный/сконструированный/собранный вручную/пофиг, откуда он вообще взялся, но однозначно готовый к использованию конфиг.

Ну как не должна. Как вы, например, сделаете, что у вас есть сервис (MVC контроллер, к примеру), который берет параметр из конфига, и при изменении конфига в ФС через DI автоматически прокинуть имзенения в контроллер, чтобы последующие вызовы его методов использовали уже новый конфиг?


Ну, во-первых, вы сейчас какую-то жесть описали.

Во вторых, если уж вам динамическое обновление конфига нужно, видимо, вам нужно просто при изменении конфига перечитывать его (это же логично?) и скармливать библиотеке новый конфиг. Не файл, а готовый к употреблению конфиг. Иначе вы запустите два приложения, использующих внутри одну библиотеку, которые будут бесконечно сраться за доступ к общему файлу конфигурации.
В том и прикол, что библиотека не должна содержать в себе код работы с «парсингом разных файлов конфигурации на разных файловых системах, содержащих одного и того же черта в ступе» (за исключением тех случаев, конечно, когда это библиотека парсинга разных файлов конфигурации на разных файловых системах, содержащих одного и того же черта в ступе).

Ну так это она и есть :) Это достаточно базовй функционал, для библиотеки работы с конфигами.


Конфиг нужен моему приложению. Но я не собираюсь руками json'ы парсить, писать какой-то код по подгрузке изменений и всё это. Я отдам файл библиотеке, которая вернет мне объект, который всё это умеет.


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

Вы неправильно поняли. Не библиотеке нужен конфиг, а приложению. Библиотека является тем, что парсит конфиг, и умеет во все умные штуки.

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


Вот я сейчас одного не понимаю, зачем вам библиотека для работы с конфигами. Применения я вижу, собственно, 2:
1. У вас 100500 приложений, работающих с одной и той же конфигурацией (отличная иллюстрация к книге «Первый опыт в программировании: как делать не надо»)
2. Я боюсь даже представить, в каком виде хранится ваш конфиг, что для того, чтобы его распарсить, нужно что-то уровня SOAP2 (это в другую книжку: «Лучшие практики энтерпрайза: 10 самых оверинжениренных хреней в истории человечества»).

Но я не собираюсь руками json'ы парсить


configData, err := ioutil.ReadFile("/path/to/config")
if err != nil {
    log.Fatal(err)
}
var config map[string]interface{}
config, err := json.Unmarshall(configData, &config)
if err != nil {
    log.Fatal(err)
}


Вот это вы собираетесь в отдельную библиотеку выносить? Простите, leftpad'ом запахло.

Или вы туда еще 5 строчек работы с fs.notify принесете? Ну, как бы, да, так существование библиотеки будет более осмысленным (на самом деле нет).

Или вы хотите обработку стейта вашего приложения в зависимости от нового конфига в рантайме в библиотеку унести? А нафига, собственно, если эта библиотека будет нужна ровно одному приложению (если, конечно, у вас не 100500 совершенно одинаковых приложений)?

Я отдам файл библиотеке, которая вернет мне объект, который всё это умеет.


Реализация всего этого удовольствия займет у вас строк 50… Реально в библиотеку понесем? Отраслевым стандартом объявим, поместим его рядом с остальными 14-ю?

Не библиотеке нужен конфиг, а приложению. Библиотека является тем, что парсит конфиг, и умеет во все умные штуки.


Понимаете, приложениям бывает нужен конфиг, тут да. Но трабла в том, что каждому приложению, как правило, нужен свой конфиг (на то они и разные приложения). А писать отдельную библиотеку, которая будет отдавать конфиг для одного приложения — а нахрена она за пределами этого приложения нужна?
Есть такая гошная библиотека — github.com/spf13/viper

Так вот там все — парсинг всех форматов, перезагрузка конфига при изменении и тд.
Я ее как-то выпилил из одного сервиса — нам нужен был просто парсер yaml и все, так размер бинарника уменьшился с 10 MB до 4MB.
Выглядит как отличная библиотека. Я бы заиспользовал. О чем-то таком я и говорил, да.

Я ее как-то выпилил из одного сервиса — нам нужен был просто парсер yaml и все, так размер бинарника уменьшился с 10 MB до 4MB.

Ну тут вариант два: либо вы изначально взяли не тот инструмент и заменили его на правильный, и это хорошо, либо вы реализовали свой ограниченный и скорее всего нестабильный вариант подмножества этой библиотеки, и тогда это уже не очень. В последнем случае должен спасать LTO.
Выглядит как отличная библиотека. Я бы заиспользовал. О чем-то таком я и говорил, да

У нас с вами явно разные предпочтения.

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

Это был сервис, который достался мне в наследство.
`viper` использует `gopkg.in/yaml.v2` для парсинга yaml файлов. Я просто взял этот парсер и сам распарсил конфиг файл в структуру (вот как выше qrKot показал, 4 строки). Что тут может быть нестабильного? Потому что мне не нужно > 20 транзитивных зависимостей.
У нас с вами явно разные предпочтения.

DRY во все поля. Стараюсь соблюдать. Если есть библиотека для Х, не надо писать Х руками. Даже если это падлефт.
Тянуть 51 зависимость ради 5 строк кода — замечательно.
Если есть библиотека для Х, не надо писать Х руками. Даже если это падлефт.

А вот это действительно антипаттерн в Go.


Есть языки, в которых этот X написать самостоятельно — только нарываться на лишние проблемы. Потому что очень легко сделать не очевидные ошибки, потом придётся их долго фиксить, и, через годик фиксов, мы получим небольшого монстрика, с местами не очевидным кодом, но зато корректно работающего… и, учитывая, что ровно такого же монстрика мы могли получить ещё год назад просто взяв стороннюю библиотеку — выбор очевиден.


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

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

Свои строки, свои конфиги, своё всё… Где-то такой паттерн я уже встречал…

Каким таким образом го убирает все ошибки тоже непонятно. Больше похоже на то, что автор забьет на обработку граничных случаев (что сократит код раз в 10), а программисты на проекте которые нарушат представления автора о том, как все это должно работать окажутся сами виноваты, потому что «не надо это так использовать».

Есть языки, в которых этот X написать самостоятельно — только нарываться на лишние проблемы.

Любой home made код хуже, чем проестированный, проверенный временем специализированный код библиотеки. Мне как пользователю пофиг, сколько там зависимостей, все равно LTO вырежет все, что не используется. А вот надежность, и сэкономленное время сильно важнее.

Конечно, если у вас была зависимость на крейт с 3 звездами на гитхабе, то это другой вопрос. Но библиотека на которую была ссылка выглядит вполне здоровой.
Свои строки, свои конфиги, своё всё… Где-то такой паттерн я уже встречал…

Мимо, никто в Go не делает свои строки, очень много есть в стандартной библиотеке, свой парсер yaml/ini тоже никто в своём уме писать не станет. Но и ради leftpad тоже никто стороннюю библиотеку подключать не будет.


Любой home made код хуже, чем проестированный, проверенный временем специализированный код библиотеки.

Это не так. Опенсорсные библиотеки (а мы ведь о них говорим, да?) в основной массе и есть типичный home made код. Причём, как правило, более универсальный, функциональный и сложный, чем нужно для текущего проекта, что зачастую с лихвой перекрывает "протестированность и проверенность временем" и приводит к тому, что работает это менее надёжно, чем своя минималистичная реализация.


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


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


В качестве конкретного примера, в моём текущем проекте (50k строк) подключены 22 библиотеки, из них ровно половина это либо наши собственные опенсорсные, либо дополнения стандартной библиотеки Go (из golang.org/x/…). Из оставшихся 11 часть это (относительно) официальные библиотеки (драйвер DB, SDK сервисов вроде AWS, Stripe, etc.), без них остаётся 6, из которых только одна достаточно тривиальная, без которой можно было бы обойтись. При этом в этих 50к строк есть ровно один настоящий велосипед на 1600 строк (по ряду причин пришлось сделать свою очередь сообщений, и перед тем как решиться писать велосипед я честно неделю старался заюзать существующий NATS Streaming Server, и до сих пор не рад, что пришлось таки написать этот велосипед).


Условно говоря, если проблему можно решить в 20 строк кода, то никто ради этого библиотеку подключать даже не подумает — библиотека тупо обойдётся дороже. А если Вы не в состоянии написать эти 20 строк без багов, и этим оправдываете необходимость подключения библиотеки…

Это не так. Опенсорсные библиотеки (а мы ведь о них говорим, да?) в основной массе и есть типичный home made код. Причём, как правило, более универсальный, функциональный и сложный, чем нужно для текущего проекта, что зачастую с лихвой перекрывает «протестированность и проверенность временем» и приводит к тому, что работает это менее надёжно, чем своя минималистичная реализация.

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

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

Есть, но она копеечная. Если про тот же дотнет мы говорим, то на время компиляции зависимости вообще не влияют, а добавление того же Configuration добавляет чуть больше 100КБ на диске.

image

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


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

Действительно ли это такая страшная цена за качественное, проверенное решение?


Вы не то в цену добавляете. Цена — это не место на диске, и не время компиляции. Цена — это зависимость вашего кода от движения левой пятки стороннего разработчика. В случае импортов из стандартной библиотеки или из x, особых проблем нет — это поддерживается. В случае leftpad'ов — это всегда плохая идея.

Цена библиотек вовсе не в килобайтах и времени компиляции. Если разрабатывается прототип — цена библиотек нулевая. Но в нормальных проектах каждая зависимость создаёт дополнительные риски и увеличивает нагрузку на поддержку проекта.


Библиотеки надо отслеживать (как минимум на предмет выхода новых версий, исправляющих проблемы с безопасностью), их надо обновлять (что нередко требует внесения изменений в свой код), в проектах где важна безопасность код библиотек нужно хотя бы проглядывать перед добавлением плюс смотреть изменения при обновлениях, если возникают проблемы (а они всегда возникают, даже с отлаженными за 10 лет библиотеками) то их решение обходится намного дороже чем исправление бага или добавление фичи в своём коде (нужно потратить время на коммуникацию с автором библиотеки, если он оперативно фикс не выпустит, то нужно самим вникнуть в код библиотеки — которая довольно большая и содержит много ненужного текущему проекту — и пофиксить самостоятельно, после чего либо ждать пока автор примет PR, либо делать свой форк, в случае форка появляется дополнительная нагрузка на подтягивание в него обновлений из основного репо, etc.).


Помимо этого использование избыточно функциональных библиотек нередко приводит к трате лишних ресурсов на выполнение ненужного этому проекту кода, что отрицательно сказывается на скорости его работы и требованиям к ресурсам (RAM/CPU). Ещё одна проблема связана с тем, что быстро подключив навороченную библиотеку мы нередко добавляем в проект "бесплатные" фичи — то, что проекту не нужно, но что мы получили вместе с библиотекой (возвращаясь к примеру с конфиг-файлом такой фичей является обновление конфигурации при изменении файла на лету), что тоже удорожает поддержку (не нужные, но реализованные фичи приходится поддерживать) и в целом ведёт проект в сторону bloatware.


Цена библиотек достаточно высока. Она однозначно является приемлемой если библиотека решает достаточно сложную задачу, но для задач, которые можно самостоятельно решить в 20 строк — использование сторонних библиотек это просто вредительство.

Библиотеки надо отслеживать (как минимум на предмет выхода новых версий, исправляющих проблемы с безопасностью)

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

их надо обновлять (что нередко требует внесения изменений в свой код)

На куче проектов, что я видел, использовались версии библиотек на 2-3 мажорных версии меньше последнего стейбла. Потому что обычно разнца только в добавлении новых фич, которые не нужны особо, т.к. затраты на обновления кода и риски развала проекта перевешивают. Собственно, любое обновление библиотек всегда рассматривается с точки зрения стоимости переписывания/вероятности развала проекта в уже работающих сценариях. Безопасность библиотек вообще никак не рассматривается, ибо в них и уязвимостей-то нет. Если не верите, самый популярный пакет — Json.Net, попробуйте в нем найти релиз, который исправляет какую-то проблему с безопасностью. Если найдете — то наверное нужно признать, что сообщество просто более безответственное.

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

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

если возникают проблемы (а они всегда возникают, даже с отлаженными за 10 лет библиотеками)

Да нет, с большими библиотеками проблем не возникает. Упомянутый Json.Net решает все сценарии. Бывает, не хватает фич, но фича != баг, и система расширения библиотеки обычно предусматривает возможность приткнуть свои классы для добавления этой функциональности.

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

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

Помимо этого использование избыточно функциональных библиотек нередко приводит к трате лишних ресурсов на выполнение ненужного этому проекту кода, что отрицательно сказывается на скорости его работы и требованиям к ресурсам (RAM/CPU).

Ну это уже странные штуки пошли. Библиотека обычно ничего такого страшного не делает, если это не ORM с километровыми запросами. Хотя там, где важны каждые 1-2% производительности дотнет обычно не используют, мб в Го разница между 98 и 100 мс на ответ является клиентообразующей.

Ещё одна проблема связана с тем, что быстро подключив навороченную библиотеку мы нередко добавляем в проект «бесплатные» фичи — то, что проекту не нужно, но что мы получили вместе с библиотекой (возвращаясь к примеру с конфиг-файлом такой фичей является обновление конфигурации при изменении файла на лету), что тоже удорожает поддержку (не нужные, но реализованные фичи приходится поддерживать) и в целом ведёт проект в сторону bloatware.

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


Видимо, не следите.

CVE самого дотнета выходят как патчи на винду


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

На куче проектов, что я видел, использовались версии библиотек на 2-3 мажорных версии меньше последнего стейбла.


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

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


Т.е. принят подход «работает — не трожь». Вероятно, и убежденность в том, что фиксов уязвимостей под .net-библиотеки нет, растет оттуда же? «риски развала проекта» важнее, чем отсутствие уязвимостей в коде… Ну, ок…

Если не верите, самый популярный пакет — Json.Net


По какому, собственно, критерию, самый популярный? Самый популярный лично у вас?

Не поленился, сходил на NuGet (я же в правильном месте ищу?). В выдаче первые 3: NUnit, Newtonsoft.JSON, EntityFramework…

А вот тут про уязвимость

И тут.

Цитируем вас: «нужно признать, что сообщество просто более безответственное. „

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


Отличное решение, такЪ победимЪ.

Да нет, с большими библиотеками проблем не возникает.


Кхм, либо они недостаточно большие, либо вы не замечаете. Большие библиотеки на то и большие, что в них без проблем не может обходиться.

Хотя там, где важны каждые 1-2% производительности дотнет обычно не используют, мб в Го разница между 98 и 100 мс на ответ является клиентообразующей.


Ну, допустим, не 1-2% “зазора производительности» для дотнета нормальными считаются, а, скорее, 15-25%. Там, где важны 1-2%, юзают C. А Go ценят больше за утилизацию ресурсов.

Это наоборот плюс же.


В чем, простите, плюс?

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


С тем же успехом используемые фичи могут начать вести себя иначе, и ваш код ничего не узнает.
Не поленился, сходил на NuGet (я же в правильном месте ищу?). В выдаче первые 3: NUnit, Newtonsoft.JSON, EntityFramework…

А вот тут про уязвимость

И тут.

Цитируем вас: «нужно признать, что сообщество просто более безответственное. „

Благодарю, любопытная информация.

Отличное решение, такЪ победимЪ.

Да нет, не очень хорошо, но так вот есть.

В чем, простите, плюс?

В том, что при необходимости ими можно вспользоваться.

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

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


Они «занимают место» — это, зачастую, малозначимо, но оно таки минус.

«Плюс» того, что «этим можно воспользоваться»… Кхм, без пересборки вашего приложения — нет, нельзя. Т.е. не плюс.

По поводу безопасности Вам уже ответили с конкретными примерами, но вообще-то просто здравый смысл должен был подсказать, что проблемы с безопасностью регулярно обнаруживают в коде, любом коде, и библиотеки не являются и не могут являться исключением. Иллюзия того, что в библиотеках всё хорошо, чаще связана с тем, что авторы большинства опенсорс библиотек не напрягаются делать формальные уведомления "ааа, мы закрыли дыру, срочно обновляйтесь", многие даже changelog не ведут или пишут туда просто "fixed something". И это — ещё одна причина регулярно обновляться, даже если никто не бегает и не кричит "волки!".


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

Если у Вас в проектах практикуется ревью, то кто-то автоматически лопатит мегабайты изменений чтобы что-то найти… но только в той части кода ваших проектов, который написали вы сами… но ведь проект так же содержит много кода, который написали не вы, нередко этого стороннего кода в проекте даже больше, чем вашего собственного, причём код этот вы получили бесплатно, на условиях as is и без каких-либо гарантий. Единственная причина, по которой такое отношение (ревью только собственного кода) имеет смысл — компании наплевать на безопасность (или, скажем мягче, у компании недостаточно ресурсов на безопасность), и по факту её волнуют только критичные баги, которые дешевле и быстрее обнаружить на ревью собственного кода.


Библиотека обычно ничего такого страшного не делает

Речь о том, что в библиотеках код более общего назначения, который содержит избыточный для вашего проекта функционал. Это значит, что для реализации этого избыточного функционала библиотека выделяет больше памяти для хранения данных нужных этому функционалу, выполняет больше логики в коде для поддержки этого функционала, и, в результате, жрёт больше ресурсов и работает медленнее. Насколько — зависит от конкретной библиотеки. Для многих библиотек это может быть не критично, но полно примеров проектов, в которых накопилось очень много такого "избыточного" кода, и которые дико тормозят и/или требуют дикое количество ресурсов, хотя сами не делают ничего достаточно сложного, чтобы оправдать это. На практике эта цена становится заметна либо при подключении ненужных тяжёлых библиотек вроде упомянутого Вами ORM, либо при бездумном подключении библиотек на каждый чих (что и является темой текущего обсуждения).


Если же использовал, то фичи не бесполезные, и сэкономили вам время.

Вы не поняли, я говорил о том, что мы нередко начинаем использовать фичи библиотек не потому, что они действительно нужны текущему проекту (хороший признак — в таск-трекере должна быть соответствующая этой фиче задача, и продакт должен выбрать её как приоритетную поместив в начало бэклога — часто там оказывалась фича "автоматическое обновление конфигурации на лету регулярным перечитыванием конфиг-файла"?), а потому, что нам кажется, что добавить за пять минут крутую фичу — это хорошая идея, ведь мы получим эту фичу практически "бесплатно". И далее я описывал настоящую цену этой "бесплатности".

UFO just landed and posted this here
То есть, свои библиотеки вы пишете сразу заведомо безопасными с полностью future-proof API?

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

UFO just landed and posted this here
тут недавно был случай внедрения вредоносного кода в зависимость очень популярной js библиотеки.
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
А где валидация?


В типах.

Где обработка отсутствующих случаев?


В инициализации.

Где переопределение аргументов с командной строки?


Нигде. Аргументы командной строки ортогональны конфиг-файлу.

Где генерация хелпа?


В сторонних генераторах хелпов?

Поэтому есть вещи вроде таких.


Каким боком, простите, парсилка аргументов командной строки относится к хот-релоаду конфиг-файла?
UFO just landed and posted this here
Это в этих типах валидация?


Нет, вот в этих:

type Config struct {
   DebugMode          bool
   HotReloadEnabled bool
   MaxWorkUnits       int
}


и в подобных им.

Можете носом ткнуть? Я её не вижу


Рекомендуемый подход:

func NewConfig() *Config {
    var conf Config
    conf.DebugMode = false
    ... тут заполняем дефолты ...
    return &conf
}


UFO just landed and posted this here
Библиотека парсера JSON уже есть. И она умеет работать со структурами.

Паниками она не кидается, как и любая порядочная библиотека. Она либо возвращает корректную структуру, либо возвращает ошибку.

Какая, собственно, вам еще библиотека «работы с конфигурациями» нужна?

Собственно «обрабатывать ошибки на месте» — это вообще не про «молча обработать ошибку и как-то замести проблему под ковер». Обработка на месте а) предполагается в коде приложения (3rd-party библиотека как раз ошибку должна возвращать), б) означает необходимость совершить хотя бы какие-то действия с полученной приложением ошибкой (на крайний случай явно ее проигнорировать). Вот к такой обработке и подталкивает подход «ошибка — это значение».

Согласен. Единственное, что неудобно, что Go никак не форсит необходимость обработки ошибки. Как товарищ выше сказал, вместо Either<Result, Error> передается (Maybe<Result>, Maybe<Error>). При этом на вид отличить функцию, которая может и не может вернуть ошибку нелегко.

При этом на вид отличить функцию, которая может и не может вернуть ошибку нелегко.


Не понял. Как нелегко?

Смотрим в сигнатуру:

`func ReturnInterfaceWithoutError() interface{}` — эта возвращает просто что-то.
`func ReturnInterfaceAndError() (interface{}, error)` — эта возвращает что-то и опциональную ошибку.
Т.е. у второй нужно (в идеале) проверить, не вернулась ли ошибка, прежде чем в результат смотреть.

После этого вы делаете так:
`result := ReturnInterfaceAndError()`и получаете ошибку от компилятора, который вежливо вам подскажет, что так делать нехорошо.

И вам придется переписать так: `result, err := ReturnInterfaceAndError()` а потом еще и предпринять что-то с этой полученной ошибкой:
if err != nil {
  // ... здесь что-то нужно сделать...
  // предпринять какие-то действия, что-то намутить...
  // например, прокинуть ее вверх
  return nil, err
}


А можно проигнорить, но явно проигнорить:
`result, _ := ReturnInterfaceAndError()`

Если раньше функция была `func ReturnInterfaceWithoutError() interface{}, а в новой версии добавили error, компилятор ругнется в местах использования?


И вам придется переписать так: result, err := ReturnInterfaceAndError() а потом еще и предпринять что-то с этой полученной ошибкой:

Проглядел, перечитал, нашел. Ну, хорошо тогда :) Спасибо

UFO just landed and posted this here
А, кстати, как, если дженериков или рефлексии (что рантайм, что компилтайм) нет?

encoding/json использует рефлексию.

Рефлексия есть.

Только в Go нет способов выразить это типобезопасно.


Вы, видимо, не работали с Go.
UFO just landed and posted this here
Ну так, смысл в том, что у нас есть наш app, и зависимый пакет config.


У вас граф зависимостей поломался. config от app не зависит, это app зависит от config же.

но как выше говорилось, «go way» в том, чтобы следовать варианту 2 в той или иной форме.


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

Криво написал. конечно же app зависит от config.


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

Это прямо противоречит


Подход Go подталкивает к обработке ошибок, а не на отсылку их выше
Он подталкивает. И «взять ошибку и прокинуть выше» — это тоже обработка. Он просто заставляет сделать с ошибкой хоть что-нибудь.
Вот прямо ваш же код возьмем и перепишем, если я правильно распарсил ваши намерения:

config := conf.Deafult()

if err := config.FromFile("some/path/config.json"); err != nil {
   // ... вот тут паникуем, страдаем, фиксим, обрабатываем
   // и пофиг чем вообще занимаемся...
}

service, err := serv.NewAwesomeService(&config)

// ... дальше остальная муть
Ну как это не должна? log4rs, Microsoft.Extensions.Configuration.Json, cfg4j и все прочее…


Очевидно, вы считаете это лучшими примерами того, как нужно делать?

Ну так вдруг не надо падать, а просто сообщение куда-то вывести. Падать на любой чих это такое.


Поломанный конфиг — это не любой чих. Это заведомо некорректная рантайм-конфигурация. Это сервис, который, например, пишет не в ту базу, или возвращает отладочные результаты на запрос… И вы предлагаете его пустить в прод?
Очевидно, вы считаете это лучшими примерами того, как нужно делать?

Конечно. Лучше иметь пакет для инкапсуляции какого-то поведения, чем не иметь.


Поломанный конфиг — это не любой чих. Это заведомо некорректная рантайм-конфигурация. Это сервис, который, например, пишет не в ту базу, или возвращает отладочные результаты на запрос… И вы предлагаете его пустить в прод?

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

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


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

В контексте же сферы применения Go (я надеюсь, у вас нет мысли переписать на нем Скайрим? Если есть — немедленно бросьте, гуано получится. Точнее, ничего вообще не получится). В серверных приложениях (где Go цветет и пахнет) нет подхода «половины конфига нету, ну и срать, работаем хотя бы как-то». Либо работаем гарантированно корректно, либо не работаем вообще. Т.к. конечный пользователь, который вам деньги платит, может с некоторым пониманием отнестить с даунтайму всего сервиса на некоторое время, при условии, что он на этом ничего кроме времени не потерял. А вот к тому, что, допустим, его фотки с обнаженкой из-за левых настроек гейта ушли вместо его визави по эротической переписке в голландский филиал «Голубой устрицы» и попали в ее публичный альбом «ведущие гомосексуалисты месяца» — вот тут пользователь может сильно не понять.

Ну а если поломанный конфиг не влияет на работу сервиса от слова «совсем»… Хм, а нафига вам вообще конфиг?
Если мы не смогли распарсить текстуру какого-нибудь модного меча в Скайриме, это повод не подгрузить меч и, возможно, продолжить работу дальше (остается на совести разработчика). Но текстура меча — это вполне себе отдельная сущность, которую можно считать частью конфигурации меча, не Скайрима. Если Скайрим не сможет прочитать свою конфигурацию (да-да, целиком), он ровно так же упадет, и это правильно.

Да, но и для чтения конфига мода, и для чтения конфига меча используется одна и та же либа :) Но в одном случае надо падать, а в другом — «забить» на ошибочный мод и работать дальше. Соответственно, это был пример того, что на уровне библиотеки нельзя принять правильное решение, она не знает, насколько это критичный конфиг. Решение одно — прокинуть ошибку наверх, пусть там разбираются. Собственно, мы в этом уже сошлись, поэтому не вижу смысла на этом задерживаться.

В контексте же сферы применения Go

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


Хм, вы твердо уверены, что текстовый конфиг игры и bitmap-текстуры игровой модели (которые вы приводили как пример выше) должны парситься одной и той же библиотекой? У нас, видимо, разные с вами понятия о здравом смысле.

Но в одном случае надо падать, а в другом — «забить» на ошибочный мод и работать дальше.


Фоллбек-логику, в смысле, вы тоже предлагаете в стороннюю библиотеку отдать? И пусть игрок вместо меча с розовым фаллоимитатором в руках бегает. А чо, прикольно же.

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


Воот, библиотека может либо отдать целиком корректный конфиг, либо ругнуться наверх «конфиг поломатый» и не отдать ничего. Половинчатый или неконсистентный объект конфигурации — всегда плохая идея.

Я думал, цели го более широки, чем только лишь веб-серверы.


Чуть-чуть более, но не настолько, насколько вы представляли. Go — достаточно нишевой и строго утилитарный язык.
Хм, вы твердо уверены, что текстовый конфиг игры и bitmap-текстуры игровой модели (которые вы приводили как пример выше) должны парситься одной и той же библиотекой? У нас, видимо, разные с вами понятия о здравом смысле.

Ну возьмите вместо текстуры просто JSON с описанием характеристик меча. Текст-описание-урон, вот это всё.

Фоллбек-логику, в смысле, вы тоже предлагаете в стороннюю библиотеку отдать? И пусть игрок вместо меча с розовым фаллоимитатором в руках бегает. А чо, прикольно же.

В приложение. И не с розовым фаллоимитатором, а с черным квадратиком, tooltip missing, дефолтным мечом или чем-либо еще.
image

Воот, библиотека может либо отдать целиком корректный конфиг, либо ругнуться наверх «конфиг поломатый» и не отдать ничего. Половинчатый или неконсистентный объект конфигурации — всегда плохая идея.

Вообще ни разу не предлагал возвращать кусок конфига :D
Вообще ни разу не предлагал возвращать кусок конфига :D


А что вы предлагали?

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

Итак, остаются нерешенными 2 вопроса:
1. Зачем вам логика парсинга JSON в конструкторе этого самого «меча»?
2. Чем случай «json-файл с конфигом отсутствует» в вашем конкретном случае отличается от «json-файл с конфигом не читается» или «json-файл с конфигом содержит некорректные данные»?

Вот смотрите, у вас же, видимо, на весь «МойСкайрим_v1.0Beta» несколько больше одного конфига? У вас же, надеюсь, конфиг самой игры, содержащий разрешение экрана, режим работы, настройки управления и т.д. и конфигурация ресурсов игры — это разные файлы? Причем, вероятно, на каждый «убер-меч» конфиг свой? Или вы враг себе и игроку?

Ну вот объясните мне, что, собственно, должна ваша библиотека по загрузке конфигурации делать такого, чего не может стандартная json-парсилка, которая возвращает инстанс объекта + nil в случае успешного парсинга, и nil + инстанс ошибки с описанием проблемы в случае фейла?
1. Зачем вам логика парсинга JSON в конструкторе этого самого «меча»?

Ну, должен же меч как-то быть загружен. Обычная плагинная система — сначала скайрим идентифицирует моды, в частности «меч», потом говорит всем модам «загружайся», ну и дальше что-то пошло. Процесс загрузки состоит как раз из парсинга конфига, определения характеристик, и вот этого всего.

Чем случай «json-файл с конфигом отсутствует» в вашем конкретном случае отличается от «json-файл с конфигом не читается» или «json-файл с конфигом содержит некорректные данные»?

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

Вот смотрите, у вас же, видимо, на весь «МойСкайрим_v1.0Beta» несколько больше одного конфига? У вас же, надеюсь, конфиг самой игры, содержащий разрешение экрана, режим работы, настройки управления и т.д. и конфигурация ресурсов игры — это разные файлы? Причем, вероятно, на каждый «убер-меч» конфиг свой? Или вы враг себе и игроку?

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

Ну вот объясните мне, что, собственно, должна ваша библиотека по загрузке конфигурации делать такого, чего не может стандартная json-парсилка, которая возвращает инстанс объекта + nil в случае успешного парсинга, и nil + инстанс ошибки с описанием проблемы в случае фейла?

Мержить конфиги, например. Ну или тот же релоад, как уже упоминалось. Чтобы поправили в конфиге значение, сохранили, а у вас в игре без перезагрузки сразу обновились характеристики.
Ну, должен же меч как-то быть загружен. Обычная плагинная система — сначала скайрим идентифицирует моды, в частности «меч», потом говорит всем модам «загружайся», ну и дальше что-то пошло. Процесс загрузки состоит как раз из парсинга конфига, определения характеристик, и вот этого всего.


Хм, у вас «архитектура протекла». Процесс загрузки самой игры состоит из инициализации состояния игры в соответствии с объектом конфигурации. Не с json-файлом конфигурации, а с объектом конфигурации. Т.е. инициализация состояния и загрузка конфига из файла — это разные вещи, и чем меньше между ними связность, тем лучше (гуглить tight coupling).

Т.е. процесс инициализации знать не должен, откуда взялся конфиг. В нем не должно быть парсера json-файла, от слова «вообще». Делается это для того, чтобы потом, если вы вдруг решите, что json вам «не канает», могли заменить его на, допустим, xml, и ничего, кроме парсера конфига, не переписывали.

Если объект конфигурации по какой-то причине не вернулся, мы можем либо просто «сдохнуть», либо посмотреть в описание ошибки и сдохнуть, предварительно сообщив пользователю, что пошло не так. На этом этапе нет сценария, в котором есть смысл в продолжении работы.

Дальше ваш «инит» должен взять список модов и на каждый пнуть загрузчик модов. Просто пнуть загрузчик, не заморачиваясь на тему, откуда и как появится инициализированный мод, понимаете? Потому что видов собственно модов несколько больше одного, и грузиться они могут по-разному. При этом самому «иниту» тупо по барабану на описание и формат описания конфигурации мода. Хотите конфиг мода хранить в виде рунической записи — велкам, но об этом будет знать только загрузчик, иниту на это по барабану.

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

При этом «валидность» конфига мода ни одна универсальная библиотека вам не обеспечит. Ну не знают универсальные инструменты про то, что в вашей игре мечу не положено иметь длину выше 172см. Поэтому если загрузчик увидит параметр «этоМеч» и рядом указание длины «172.0001»см он должен точно так же вернуть пустое значение вместо мода и ошибку «у вас слишком длинный». Но это логика конкретно вашего приложения, которая универсальными средствами не решаема.

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

Ну, потому что наличие файла я могу проверить снаружи


Зачем, простите, вам в инициализации стейта игры код работы с файловой системой? Вот объясните мне, зачем вам os.Open() в одной строке и for _, mod := range mods { mod.Load() } в соседней? И чтобы дальше всенепременно было if len(sword) > 172 { return }?

Зачем вы смешиваете домен приложения с доменом системной ФС?

и не вызывать код загрузки конфига


Вам все равно его вызывать. И именно он должен сначала открыть файл на чтение.

file, err := os.Open("/path/to/file")
if err != nil {
    return nil, err
}


Все, остальное не выполнится.

А в случае, если файл есть, только библиотека знает, валидный он или нет.


Есть такое понятие, как «домен приложения». «Валидность JSON» — да, может проверить библиотека. Более того, она это и делает. Именно для этого `err := json.Unmarshall(bytesToParse, &configObject)`. Если `err != nil `, дальнейшие операции с конфигом бессмысленны, и вам по барабану, это файл поломатый, или JSON не валидный. Оно все равно не парсится.

А вот у самого приложения есть «домен», и валидность конфигурации в пределах этого домена вам никакой парсер не подскажет. Это просто невозможно, т.к. требования у каждого приложения свои.

Если вам нужна валидация по схеме… Ну, видимо, JSON — неверный выбор. Видимо вам BSON нужен, или SOAP.

чтобы было понятно, что мы не можем вставить валидацию на своей стороне


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

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


Это я к тому, что для объекта конфигурации со стороны логики приложения может быть только 2 состояния: он либо корректный, либо его нет. И вот эти сказки про то, что при любой проблеме, возникшей в ходе чтения конфига, есть сценарий, в котором тот кусок мяса, который вам вместо конфига упал пригоден для чего-либо, кроме того чтобы вычистить из под него память — ну, это сказки просто, ничего не имеющие с реальностью.

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

Мержить конфиги, например.


Это, простите, нахрена вообще?

Ну или тот же релоад, как уже упоминалось.


Хот-релоад конфига из файла по триггеру файловой системы — зло в чистом виде, мы это уже обсуждали. Тем более, что оно еще и в неконсистентное состояние вас запросто положит.

Чтобы поправили в конфиге значение, сохранили, а у вас в игре без перезагрузки сразу обновились характеристики.


Представил: для изменения разрешения в нашей игре зайдите в папку C:\Program File\Nasha Krutaya Igra\ и исправьте значение в 185-й сверху строчке на нужное вам.

UPD: Ввиду участившихся случаев краша приложения при исправлении конфига руками предоставляем список допустимых значений для указания в 185-й сверху строчке.

UPD: Пожалуйста, помните, что файл с конфигурацией нужно сохранять только после того, как вы завершили все необходимые изменения конфигурации. Не используйте редакторы с автосохранением, промежуточные сохранения ведут к вылетаниям из игры.

UPD: Конфигурацию нашей игры рекомендуется редактировать в Notepad++, т.к. Блокнот при сохранении меняет переносы в файле на \n\r, а это ломает наш парсер.

Ну вот чушь же, признайте.
Хм, у вас «архитектура протекла». Процесс загрузки самой игры состоит из инициализации состояния игры в соответствии с объектом конфигурации. Не с json-файлом конфигурации, а с объектом конфигурации. Т.е. инициализация состояния и загрузка конфига из файла — это разные вещи, и чем меньше между ними связность, тем лучше (гуглить tight coupling).

Да нет. все верно. Игра ничего не знает про файлы мода, он сам решает, что ему надо загрузить и как. Например, в WoT есть мод XVM, у него есть свой конфиг, про который игра ни сном, ни духом.

При этом «валидность» конфига мода ни одна универсальная библиотека вам не обеспечит. Ну не знают универсальные инструменты про то, что в вашей игре мечу не положено иметь длину выше 172см. Поэтому если загрузчик увидит параметр «этоМеч» и рядом указание длины «172.0001»см он должен точно так же вернуть пустое значение вместо мода и ошибку «у вас слишком длинный». Но это логика конкретно вашего приложения, которая универсальными средствами не решаема.

Валидность конфига легко решается соответствием типов. Если мы не смогли валидный JSON десериализовать в ожидаемую структуру, то валидация не прошла. Десериализацию в структуры библиотеки поддерживают.

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

Ну это вы уже не в ту степь пошли. Щас еще античиты начнем разбирать :D

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

Верно.

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

Мы уже сошлись, что прокидывать ошибку — хорошо, и вы не согласны с оригинальным комметарием про «решаем на месте». О чем спор-то? :)
Валидность конфига легко решается соответствием типов. Если мы не смогли валидный JSON десериализовать в ожидаемую структуру, то валидация не прошла. Десериализацию в структуры библиотеки поддерживают.


Вот, собственно, десериализацию с структуру поддерживает прямо стандартная JSON-парсилка в Go.

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

Ну это вы уже не в ту степь пошли. Щас еще античиты начнем разбирать :D


Не, это мы все еще в той степи. Подключаемая библиотека может валидировать json-конфиг, но ровно до уровня валидности самого json и соответствия структуре. Валидация на уровне соответствия бизнес-логике вашего приложения в 3rd-party библиотеки — неправда.

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


Ронять надо не всю игру. Ронять надо загрузчик конкретного мода. Он не должен что-то возвращать, если что-то пошло не так. Он должен маякнуть наверх «что-то пошло не так» и сдохнуть, как джентльмен)
Вот, собственно, десериализацию с структуру поддерживает прямо стандартная JSON-парсилка в Go.

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

Как я уже говорил, в том же примере выше из дотнета в качестве типов нужных полей можно указать вместо T тип IOption<T>, который будет поддерживать автоподгрузку (при получение myOption.Value будет получаться не значение на момент десериализации, а то, которое в текущий момент занесено в конфиг). Пару раз пользовался этим функционалом, довольно удобно. Линк


Не, это мы все еще в той степи. Подключаемая библиотека может валидировать json-конфиг, но ровно до уровня валидности самого json и соответствия структуре. Валидация на уровне соответствия бизнес-логике вашего приложения в 3rd-party библиотеки — неправда.

Правда. Достаточно декларативно описать требования к полям/структуре, а библиотека с помощью этих атрибутов всё проверит. Декларативно > императивно.


Ронять надо не всю игру. Ронять надо загрузчик конкретного мода. Он не должен что-то возвращать, если что-то пошло не так. Он должен маякнуть наверх «что-то пошло не так» и сдохнуть, как джентльмен)

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

Как я уже говорил, в том же примере выше из дотнета в качестве типов нужных полей можно указать вместо T тип IOption, который будет поддерживать автоподгрузку (при получение myOption.Value будет получаться не значение на момент десериализации, а то, которое в текущий момент занесено в конфиг). Пару раз пользовался этим функционалом, довольно удобно. Линк

play.golang.org/p/-iiZnh5uhy — оно?

Прямо стандартная json-парсилка. Заполняешь дефолтами, анмаршаллишь. Те поля, которые есть в файле, оверрайдятся, остальные остаются «как есть».

Достаточно декларативно описать требования к полям/структуре, а библиотека с помощью этих атрибутов всё проверит. Декларативно > императивно.


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

А моё мнение, что это вызывающий код должен решить, что делать.


Логично, именно вызывающая сторона решает, что делать, если конфиг не распарсился. Она может попробовать другой парсилкой распарсить, она может на какие-то дефолты «фейловерить». Но при любом раскладе тот конфиг, которым подавился парсер, вызывающему коду не нужен.

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


В каком месте оно вызывает глобальный хендлер?

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

У вас форматирование сломалось.


play.golang.org/p/-iiZnh5uhy — оно?

Нет. Пример:


class MyConfig 
{
   public IOption<int> X {get;set;}
}

const string filename = "appconfig.json";
var config = Config<MyConfig>.From(filename);
Console.WriteLine(config.X.CurrentValue); // 5
File.WriteAllText(filename, "{X: 10}"); // перезаписываем файл
Console.WriteLine(config.X.CurrentValue); // 10

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


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

А это и не важно. Го компилируется в ассемблер, это не значит, что они эквивалентны с точки зрения продуктивности.


Логично, именно вызывающая сторона решает, что делать, если конфиг не распарсился. Она может попробовать другой парсилкой распарсить, она может на какие-то дефолты «фейловерить». Но при любом раскладе тот конфиг, которым подавился парсер, вызывающему коду не нужен.

Вызывающему коду нужна любая информация об ошибке, а не прыжок через голову в панику. О чем я уже много раз говорил.


В каком месте оно вызывает глобальный хендлер?

Ну дык, паника же. Вообще я привык, что паника роняет приложение (целиком, всегда), но согласен в контексте го и на просто глобальный хендлер.


Выше это описывалось как go-way, и "решение проблемы на месте", о чем мы вроде бы уже говорили.

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


Смысл такого сценария — примерно околонулевой, если честно. Меняете значение в структуре, сериализуете в файл — получаете ровно аналогичный результат.

Вызывающему коду нужна любая информация об ошибке, а не прыжок через голову в панику. О чем я уже много раз говорил.


Ну вот, собственно, encoding/json именно это и делает: возвращает десериализованное значение и пустую ошибку, либо nil и ошибку. Собственно, любой модуль/библиотека должны вести себя рово так же. Паники и прочее — это механизмы уровня приложения. Пакет не должен инициировать паник.

Ну дык, паника же. Вообще я привык, что паника роняет приложение (целиком, всегда), но согласен в контексте го и на просто глобальный хендлер.


Ни одна приличная 3rd-party библиотека панику не кидает.

Выше это описывалось как go-way, и «решение проблемы на месте», о чем мы вроде бы уже говорили.


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

В связи с этим в Java вы можете к сигнатуре метода присобачить throws Exception и просто «забить» на все то, что внутри пошло не так. В Go «забивать» придется явно, и не в сигнатуре функции, а в том месте, которое вернуло ошибку. Это и называется «обработка ошибок на месте».
Смысл такого сценария — примерно околонулевой, если честно. Меняете значение в структуре, сериализуете в файл — получаете ровно аналогичный результат

Эмм, меняет конфиг кто-то снаружи. Как мне после этого получить новое значение?


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

Ну это да, я уже давно прочуствовал пользу Result<T,E> относительно исключений, её мне приводить не надо, действительно удобно :)

UFO just landed and posted this here
В чем, собственно, неудобство?

Что если библиотечный код случайно два nil вернет?

Хм… видимо, от ситуации зависит.

Если nil-значение считается валидным — то все норм, работать с nil-значением.

Если же nil-значение не считается валидным. Ну, примерно так же, как если вам dotnet-библиотека вместо вменяемого результата молча гуано возвращает. Я бы предложил либо патч в библиотеку послать с иправлением косяка, либо выкинуть каку и поискать нормальную библиотеку, которая возвращает ошибку, если что-то пошло не так.

А в dotnet'е не так принято?

В дотнете вообще эксепшны, там вся эта логика неприменима.


В русте у вас именно что T | E, и всего два варианта, вместо четырех.

Вся эта логика — набор соглашений уровня языка, по сути.

Собственно, исходя из соглашений, в Go у вас тоже два варианта — ошибка есть или ошибки нет.
UFO just landed and posted this here
UFO just landed and posted this here
Эмм, меняет конфиг кто-то снаружи. Как мне после этого получить новое значение?


Эмм, перезапустить приложение с новым конфигом? Эмм, явно пнуть приложение с указанием перечитать конфиг?

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

Ну это да, я уже давно прочуствовал пользу Result<T,E> относительно исключений, её мне приводить не надо, действительно удобно :)


Ну, собственно, не совсем понимаю, в чем же вам предоставляемый Go вариант не угодил?
файла конфига по событиям файловой системы и динамически переподгружало конфиг в случае любого изменения? Вы представляете размер той грабли, которую вы себе под ноги кладете? Или рассказать, почему это очень плохая идея?

Да, это и предлагаю. Нет, граблей никаких нет, пару раз пользовался этим функционалом (в частности, перегружал url по которому нужно стучаться при некоторых событиях), всё работало отлично на разных ОС на разных ФС.

Нет, граблей никаких нет


Аж поперхнулся…

Т.е. возможность конкуррентного блокирующего доступа к файлу вас не смущает? И ненулевая вероятность получения неконсистентного состояния конфига — это тоже не грабля? И даже наличие лимитов на количество открытых дескрипторов — это не грабля? И ограничения на пропускную способность дискового io в конкуррентной среде — тоже не грабля?

А закрывать/открывать ресурсы за вас, видимо, тоже будет сторонняя библиотека? И вероятность получить неконсистентное состояние приложения при наличии более одного конфига — тоже норм?

всё работало отлично на разных ОС на разных ФС.


А, ну если у вас все работало, тогда, конечно же… Пишем Хейлсбергу, Эккелю и Веннерсу, что они нифига не понимают в настоящей разработке, и пусть свои рекомендации куда-нибудь в другое место несут, а не в dotnet.

Ой, погодите, а кто же эти люди?..

Ну давайте посмотрим с другой стороны.


В стандартную библиотеку майкрософт добавил костыльный функционал, который никому не нужен. Я вас правильно понял?

В стандартную библиотеку майкрософт добавил костыльный функционал, который никому не нужен. Я вас правильно понял?


Вполне возможно, например. Ну, либо оно legacy.
Добавили в netcore 2.0, в 2016 году. Не очень тянет на легаси.
Смысл в таком сценарии, что мы меняем файл, и наша структура автоматически подцепляет изменения.


Не забудьте, что вам еще нужно пересоздать все обьекты, которые зависят от конфига (сделать новые коннекты, etc). Я предпочитаю делать перезагрузку всего приложения — например смотреть на конфиг, если изменился, то релоад приложения.

Хотя сейчас, когда мы используем kubernetes, смысла в этом уже нету — либо рестартануть pod (если конфиг замаунчен как раздел) или просто смотреть kubernets API на изменения конфига. В таком случае умение библиотеки изменять структура после изменения файла бесполезно.
Не забудьте, что вам еще нужно пересоздать все обьекты, которые зависят от конфига (сделать новые коннекты, etc). Я предпочитаю делать перезагрузку всего приложения — например смотреть на конфиг, если изменился, то релоад приложения.

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

Например, коннекты к базе? Или вам не нужно будет рестартить http сервер, конфиг которого изменился?
Не нужно. Потому что контроллеры живут как правило per call, создаются DI контейнером, и легко получают новые параметры на все новые запросы.

То же про коннекты. Если пул, можно подписаться на событие, и обновить коннекшн стринг. Если нет пула, то ничего делать не надо, всё заработает из коробки.
Не нужно. Потому что контроллеры живут как правило per call, создаются DI контейнером, и легко получают новые параметры на все новые запросы.


Есть еще масса параметров http сервера, когда нужно будет его рестартануть (ключи, конфиг tls, etc).

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


Не только обновить коннекшн стринг, а закрыть старые и создать новые коннекты. А что, вы создаете новый коннект к базе на каждый запрос?
Есть еще масса параметров http сервера, когда нужно будет его рестартануть (ключи, конфиг tls, etc).

Я про конфиг уровня приложения говорю.

Не только обновить коннекшн стринг, а закрыть старые и создать новые коннекты.

Старые коннекты закрывать не надо, их надо обработать. Новые открывать по новым правилам.

Как предлагается рестартом сервера это решать, я не понимаю. Вот у нас есть 2 запроса, один — завершающийся старый со старыми параметрами, другой — новый с новыми.

А что, вы создаете новый коннект к базе на каждый запрос?

Если у меня 1рпц, то почему бы и нет, если это облегчит подобные сценарии.
Я про конфиг уровня приложения говорю.


А что, http сервер уже не часть приложения?

Как предлагается рестартом сервера это решать, я не понимаю. Вот у нас есть 2 запроса, один — завершающийся старый со старыми параметрами, другой — новый с новыми.

Делать нужно graceful stop приложения (то есть не принимать новых коннектов, а только завершать старые), это должно быть уже у вас реализовано, а потом уже создавать новые коннекты, обьекты и тд — после этого принимать новые запросы.

Если у меня 1рпц, то почему бы и нет, если это облегчит подобные сценарии.

Вы только что сами придумали себе кучу проблем, вместо того чтобы просто релоаднуть приложения когда конфиг изменился (это можно уже отследить).
А что, http сервер уже не часть приложения?

nginx — не является.

сервер за ним, который отрабатывает логику — является.

Делать нужно graceful stop приложения (то есть не принимать новых коннектов, а только завершать старые), это должно быть уже у вас реализовано, а потом уже создавать новые коннекты, обьекты и тд — после этого принимать новые запросы.


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

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

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

Он не откидывает новые соединения, он исключен из балансировки.
Если у вас только один инстанс (что случай только для балансера), то дейлайте как это сделано в nginx — старые процессы обрабатывают только текущие запросы, а все новые идут на новые.

Нет, это вы предлагаете ронять целое приложение просто чтобы обновить одно значение из конфига.

Если у вас HA, то нету никаких проблем тут. Та же стратегия во время деплоймента новой версии, только вместо релоада — рестарт.
Он не откидывает новые соединения, он исключен из балансировки.
Если у вас только один инстанс (что случай только для балансера), то дейлайте как это сделано в nginx — старые процессы обрабатывают только текущие запросы, а все новые идут на новые.


Предлагаете делать кластер просто чтобы конфиги перегружать?

Если у вас только один инстанс (что случай только для балансера)

Какой балансер на 1..10 рпс?

Если у вас HA, то нету никаких проблем тут. Та же стратегия во время деплоймента новой версии, только вместо релоада — рестарт.

Всегда делал сине-зеленое развертывание, без даунтайма.
Предлагаете делать кластер просто чтобы конфиги перегружать?

Предлагаю всегда иметь HA, иначе это уже не серьезно.

Толку с ваших стратегий по обновлению конфига, если у вас упал один инстанс и уже даунтайм.
HA это про доступность сервиса.

Предлагаю всегда иметь HA, иначе это уже не серьезно.

О, пошли разговоры про «серьезность».

Ладно, удачи в вашей нелегкой работе.
Ну я же написал еще «толку с ваших стратегий по обновлению конфига, если у вас упал один инстанс и уже даунтайм».
Может с того, что он не падает просто так?

Серьезно, у нас на прошлом проекте за два года один раз уронили, и то из-за того, что криво накатили обновление. Вернули быстренько синий прод, поправили код, выкатили новое обновление.
Ага, а еще у вас уязвимостей в библиотеках нету. /s
Надежда это не стратегия, то что вам повезло, не означает, что вы правильно все делали.

ОС я так понимаю вы тоже не обновляете?
Это не надежда, это статистика.

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

Ага, а еще у вас уязвимостей в библиотеках нету. /s

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

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

А это уже зона ответственности PaaS-платформы, которой вы пользуетесь. В моем случае это был Azure. Там проблем бтв тоже не было.
В моем случае это был Azure. Там проблем бтв тоже не было.

azure.microsoft.com/en-us/status/history

А это уже зона ответственности PaaS-платформы, которой вы пользуетесь.

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

Я привел такие условия, в которых ваше решение точно не стоит использовать, но вы все равно его продавливаете.

Я-то как раз пытаюсь понять, придете ли вы к выводу «да, в таких условиях лучше сделать Х», или продолжите отстаивать свою точку зрения как единственную правильную.

Результат, собственно, перед вами.

Рад был пообщаться.
Да нет, как раз вы добавляли условий, потому что вас не устраивали ответы и вы пытались подогнать все под свою позицию. При этом вы будете игнорировать безопасность, высокодоступность, чтобы только подогнать все под ваше решение.
Библиотеку чего, простите, в контексте работы с конфигом?
Видимо, вы считаете, что лучше запустить сервис с некорректным конфигом?
Возвращать nil вместо объекта конфигурации?


А вы, видимо, предлагаете возвращать заведомо некорректную конфигурацию и с ней работать? Как бэ, определение того, «что пошло не так» и получение непосредственно конфигурации, с которой нужно работать — вопросы ортогональные.

Информацию о том, что пошло не так, вам, вероятно, вернуть надо, в каком-то виде (бодро отрапорторвать в консоль, вернуть информативное описание ошибки, щедро наложить этой информации в лог — вариантов «мульён»). Но это не отменяет того факта, что «если что-то пошло не так», возвращать «хотя бы что-нибудь, пусть поломатое, но хоть такое» и продолжать работу сервиса с заведомо косячным конфигом… Ну, как бы, за такое тапком с сервера гнать надо, имхо.

Собственно, вы упираете на «файл конфига есть, просто при чтении возникла какая-то ошибка». Чем, простите, с точки зрения корректности конфигурации, отличается ситуация «файла конфига нет» от «файл конфига не читается» или «в файле конфига какая-то труднообъяснимая ересь»? Результат с точки зрения сервиса в целом один: конфига нет.
А вы, видимо, предлагаете возвращать заведомо некорректную конфигурацию и с ней работать?

Нет, я предлагаю не «Решать на месте», а дать разбираться с ситуацией вызывающему коду. Я же отвечаю в ветке

Подход Go подталкивает к обработке ошибок, а не на отсылку их выше в надежде, что кто-нибудь разберется.

Пытаюсь понять, как должен в этой ситуации код «разобраться на месте, а не отсылать наверх».

Суть фразы "Подход Go подталкивает к обработке ошибок, а не на отсылку их выше в надежде, что кто-нибудь разберется." в том, что нужно в каждой конкретной точке задуматься, как именно здесь нужно обработать ошибку.


Да, очень часто её нужно просто прокинуть наверх, но, тем не менее, хоть это и самый частый вариант, делать его вариантом "по умолчанию" (как очень часто используют исключения) это плохая идея, именно потому, что позволяет вообще не задумываться об обработке ошибок до самого последнего момента. И да, если разработчик будет механически везде вставлять if err != nil { return err } то это будет ровно то же самое — он не задумывается об обработке ошибок, и просто тупо набирает бессмысленный код. Тем не менее, сама необходимость этот код набирать, и потом читать на ревью — увеличивает вероятность, что кто-нибудь всё-таки задумается, насколько этот код корректен в случае ошибки конкретно в этом месте.


Конечно, всем хочется набирать меньше тупого шаблонного кода, но нельзя это делать ценой потери такого полезного качества языка как подталкивание программистов задумываться о корректной обработке ошибок. Более детально почитать про это можно в описании нового синтаксиса обработки ошибок в Go2 через check/handle.

Суть фразы «Подход Go подталкивает к обработке ошибок, а не на отсылку их выше в надежде, что кто-нибудь разберется.» в том, что нужно в каждой конкретной точке задуматься, как именно здесь нужно обработать ошибку.

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

Да, очень часто её нужно просто прокинуть наверх, но, тем не менее, хоть это и самый частый вариант, делать его вариантом «по умолчанию» (как очень часто используют исключения) это плохая идея

в любом ЯП хорошая идея делать дефолтным вариантом самый частоиспользуемый.

И да, если разработчик будет механически везде вставлять if err != nil { return err } то это будет ровно то же самое — он не задумывается об обработке ошибок, и просто тупо набирает

Так он так и будет делать. В идеале, сделает сниппет, который по ctrl+ec будет вставлять этот код.

Тем не менее, сама необходимость этот код набирать, и потом читать на ревью — увеличивает вероятность, что кто-нибудь всё-таки задумается, насколько этот код корректен в случае ошибки конкретно в этом месте.

Люди — не идиоты. Например «баннерная слепота» туда же, люди автоматически игнорируют знакомые идиомы и паттерны. Это хорошо видно в исследованиях влияния подсветки кода на скорость распознавания и понимания.

Суть в том, что нужно ручками сделать хоть что-то в том месте, где надо обработать ошибку. У меня есть сниппеты, которые вставляют такие конструкции. А ещё можно ошибку тупо загасить приняв её в _ вместо переменной. Но и подчёркивание, и комбинацию для сниппета надо набрать. А значит — задуматься, какой из типичных сниппетов или подчёркивание подходит для обработки текущей ошибки. Что и требовалось. Не важно, сколько кнопок нужно нажать на клавиатуре, важно что их в принципе нужно нажать, и перед этим задуматься, какие именно.


Плюс к этому, на ревью очень плохо отслеживаются ошибки связанные с отсутствующим кодом, но зато намного лучше отслеживаются ошибки в добавленном/изменённом коде. Если для обработки каждой ошибки обязательно надо что-то в код добавить, пусть даже это одно подчёркивание или "типичный" блок прокидывания ошибки выше — это более заметно на ревью, даже с учётом "баннерной слепоты", чем отсутствие этого кода вообще.

Не важно, сколько кнопок нужно нажать на клавиатуре, важно что их в принципе нужно нажать, и перед этим задуматься, какие именно.

В тысячный раз над этим задумываться перестаешь. Проверенно типовыми throw new Exception() и прочими фрагментами.

Никто не в силах помешать делать глупости или писать код бездумно. ЯП может просто способствовать более правильному отношению либо потакать менее правильному. Смысл — зачем это делается именно так — я пояснил. Конкретная реализация — как сделано сейчас, или как в будущем будет сделано через handle/check — не имеет принципиального значения, пока сохраняется изначальный смысл. Придумаете более классную реализацию — welcome, сейчас как раз идёт обсуждение handle/check, и у Вас есть реальный шанс повлиять на дизайн языка.

Да нет, я просто не думаю, что программистов надо считать идиотами. Забывчивыми — да, ошибающимися — возможно. Но не глупыми ни разу.


Вот есть тропинка — как люди делают. Там, где в других языках на этом месте кладут асфальт (добавляют в язык фичу, которая облегчает этот паттерн), в го ставят забор, чтобы человек точно задумался, нужно ли ему срезать угол. Даже звучит глуповато.

Думаю, здесь очень тонкая грань, между главной фичей того же раста, и аналогичными фичами Go — и то и другое предназначено для того, чтобы защитить программиста от его же ошибок, и то и то можно обойти через unsafe (если уверен, что умнее компилятора). Где провести эту грань — вопрос сугубо индивидуальный, множество программистов считает, что даже строгая типизация уже "за гранью" и что не надо считать их идиотами и мешать им складывать строки с числами.

Ну возможно. Если бездумно лепить unsafe в расте то можно добиться похожего эффекта, как сниппет для IDE.

> Но и подчёркивание, и комбинацию для сниппета надо набрать. А значит — задуматься, какой из типичных сниппетов или подчёркивание подходит для обработки текущей ошибки.

Разговорился я как-то на эту тему с одним знакомым, что сейчас пишет на Go. Так вот — если он ещё как-то задумывается, то двадцать остальных коллег по отделу голову не включают от слова «совсем», и ошибки, с которыми сразу не придумали, что делать — тупо глушат. Потому что что-то с ними надо делать, в работе спешка и поготонка, а даже грамотно перетранслировать ошибку — уже заметная задержка.

Возможно, тут «хотели как лучше» ©, но…

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

Нет, я предлагаю не «Решать на месте», а дать разбираться с ситуацией вызывающему коду. Я же отвечаю в ветке


Вот именно так и «идиоматично» для Go.

Шаг #1: получаем конфиг (config, err := conf.GetConfigFromFile('/file/path/here'))
Шаг #2: смотрим на err, если оно не nil, предпринимаем действия по обработки (заметьте, именно в вызывающем коде)
Шаг #3: если конфиг все еще считается валидным, создаем инстанс сервиса (service, err := serv.NewInstance(&config)), скармливая готовый конфиг внутрь. Если сервис не смог «подняться» — смотрим в ошибку, принимаем решение, что делать дальше (опять же на стороне вызывающего кода)
Итого:
1. Нет подсветки кода в документации. — Ну нет и нет, это не тянет на недостаток.
2. Политика. — Высосано из пальца. Может быть так, а может быть иначе. Неявное разделение есть в любом языке и проекте.
3. Пакеты и GOPATH. — уже на дворе 1.11 и это исправили.
4. Обработка ошибок в Go. — скоро завезут гораздо более приятную.

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

Зато есть весьма явное высказывание о Golang от Роба Пайка о том для кого и зачем делался язык.


Пакеты и GOPATH. — уже на дворе 1.11 и это исправили.

Вы во всех своих проектах уже на модули переехали?


Обработка ошибок в Go. — скоро завезут гораздо более приятную.

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

Зато есть весьма явное высказывание о Golang от Роба Пайка о том для кого и зачем делался язык.

Автор языка может говорить всё что угодно. Произведение очень часто живёт самостоятельной жизнью отдельно от автора.
Вы во всех своих проектах уже на модули переехали?

Мы уже готовы переехать и сделаем это в ближайший месяц. Но пока и от GOPATH не сильно страдаем и vendor mode используем и как-то нормально.

Про обработку ошибок я упоминаю только потому, что довольно скоро пол этой статьи потеряет актуальность. Тем не менее в статье это подаётся как истина последней инстанции.
Я правильно понимаю, что в языках с tuple и exception можно использовать и подход Go и ловить исключения?
Можно, но на выходе возврат ошибки вторым параметром делают редко, ибо как-то получается без этого обходиться.

Обычно для обработки ошибок в языках, где для этого не принято использовать исключения используются типы-суммы, например Result из Rust или Either из Haskell.

Зато есть весьма явное высказывание о Golang от Роба Пайка о том для кого и зачем делался язык.

Я кстати не очень понимаю, почему всех так задевает, что Роб Пайк отзывается о молодых программистах в Google, как о не самых умных и опытных :)?

Программирование — очень бурно развивающаяся профессия и с большим количеством новичков, причём зачастую без профильного образования или без образования вообще. Даже Rust, который не такой сложный, как, например, Haskell, как-то не получил такой популярности, как Go.

Можно возразить, что «настоящие программисты» никогда не будут писать на Go, потому что он «для дураков», но это я бы оставил на совести того, кто так думает. Да, безусловно, доступность Go это и недостаток, потому что привлекает много новичков без опыта. Но и достаточно высококвалифицированные программисты тоже его используют, и с этим спорить достаточно сложно (хотя, например, тот же Docker я хорошим примером не считаю).
Rust, который не такой сложный, как, например, Haskell
Только все наоборот — Rust сложный язык, предназначенный для решения сложных задач в ограниченных ресурсах по памяти и производительности, а базовый Haskell (если не углубляться в редко используемые темы, вроде GADT) очень простой, с минимумом необходимых для работы концепций.
UFO just landed and posted this here
Раст намного более привычный, чем хаскель. Во-первых он императивный. Во-вторых С-подобный. Кроме лайфтаймов в нем нет ничего особо непривычного.
Там есть алгебраические типы, их же гошники не способны освоить!
Вся продукция HashiCorp есть оправдание существования Go! :)

Проблема в том, что в ответ на почти любые претензии о косяках или явном примитивизме получаешь ответ "Ты ничего не понимаешь, малыш! Так надо!". Помню как на вопрос почему вообще допустили такой косяк как non-nil nil interfaces, я получил ответ именно в таком стиле, с жонглированием значения термина "указатель". Ссылку, к сожалению, уже не найду. Ответ был не от Пайка если что.

такие как Rust,… — тоже выдают предупреждения, где это возможно

В Rust будет не предупреждение, а ошибка компиляции, если вариативный анализ тип-суммы окажется неполным.

Чего лично мне не хватает в Go:
1. Значений для аргументов функций и свойств структур по умолчанию.
Согласитесь, было бы гораздо удобнее писать так:
type Qwerty struct {
  A int = 10
  B string = "cdef"
}

func Zxc(A string, B integer = 456) {
}

2. Более прямой инициализации свойств «вложенной» структуры при наследовании.
Сейчас надо сделать как-то так:
type A struct {
  One int
}
type B struct {
  A
  Two int
}
...
B{
  A{
    One: 1,
  },
  Two: 2,
}

Согласитесь, было бы гораздо удобнее писать так:

Писать — да. Читать — нет. А легко читать намного важнее.


Более прямой инициализации свойств «вложенной» структуры при наследовании.

Так можно делать, просто не надо смешивать инициализацию по имени и по позиции: https://play.golang.org/p/5Qihnbmv1Gi

Писать — да. Читать — нет. А легко читать намного важнее.
В очень многих языках есть подобный синтаксис. Почему бы и нет? Зато сейчас приходится писать отдельную функцию для инициализации этих полей значениями по умолчанию.
Так можно делать, просто не надо смешивать инициализацию по имени и по позиции:
Я имел в виду такое:
B{
  One: 1,
  Two: 2,
}
В очень многих языках есть подобный синтаксис.

У очень многих языков есть проблема с читабельностью кода. Значения по умолчанию — это палка о двух концах, они нужны для того, чтобы опускать значения, вместо того чтобы задавать их явно. Писать такой код, очевидно, проще — меньше набирать надо, код получается компактнее, и в момент написания в голове есть весь необходимый контекст, включая опущенные значения "по умолчанию". А вот читать этот код позднее намного сложнее, потому что полного контекста в голове уже нет, и о наличии опущенных значений сначала надо догадаться, а потом посмотреть эти значения где-то в другом файле.


Я имел в виду такое:

Может добавят в будущем, а пока можно так:


var b B
b.One = 1
b.Two = 2

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


  • В Go нет такой ситуации, как, например, в C, когда в переменной может храниться что угодно и надо принудительно проинициализировать нулём, пустой строкой и т.п.
  • Как быть с типами, которые не позволяют создавать константы?
    • Инлайновое объявление какой-нибудь структуры или слайса превратит код в лапшу (и убьёт удобство)
    • Если позволять в качестве значений по-умолчанию вставлять изменяемые переменные, то непонятно, в какой момент вычислять дефолтное значение

По-второму всё довольно очевидно потому что в Go нет наследования, а есть композиция. И если понимать, что это именно вложенный самостоятельный объект, а не классическое наследование, то получается, что прямее некуда.
Например, можно делать так:


type B struct {
    *A
    Two int
}

И в этом случае B.A по умолчанию не инициализируется и nil. И для указателя нормальное дело быть инициализированным в nil по-умолчанию, так что прямее невозможно сделать как раз потому, что это и не наследование вовсе.

ээ… и это все?

I've gotten pretty familiar with it.


Если это действительно так, то не упомянуты вещи страшнее отсутствия привычных исключений и лапшевидного реагирование на ошибки:
— дедлоки каналов, по невнимательности\неопытности заруинит приложение и вообще отобьет охоту иметь дело с го
— интерфейсы (они вообще развязывают руки для любого извращения)
— горутинами можно злоупотреблять, компилятор не выдаст предупреждение, если в какой-то горутине не закрывать дескриптор ресурса (файл, соединение, етс.)
— йота — странный модификатор констант, оно мне больше напоминает «финт для продвинутых», пользы от наличия enum больше.
— неоднозначная однозначность… вроде как навязывают единый стиль, даже gofmt все в едино сводит, и даже в последних версиях пустые строки научился удалять, но опять же возможность делать одно и тоже несколькими способами вносит разногласия:

if err := f(); err != nil {
...
}
err := f()
if err != nil {
}
v1 := new(s)
v2 := &s{}


и всякое такое…

особенно раздражает возможность инициализировать переменную в любом месте… хоть через `var`, хоть через `:=`, кроме того область видимости переменной может быть лишь в пределах условия\цикла (тоесть одно и тоже имя для переменной использовать многократно в разных конструкциях в пределах одной функции)…

И куча другого субъективного, как по мне, го:
1. позволяет писать синтаксически запутанный код
2. порог вхождения не так низкий…
3. и хочется и колется… но продолжаю писать на го…

PS: честно сказать, начитавшись обзоров и т.п., загорелся, это было года 3 назад… потом еще 1 год «был занят»… и вот доходит… после состояния эйфории, десятков проектов и сопутствующей головной боли: ожидал внятный язык, до сих пор иногда сложно определиться какими правилами пользоваться для консистентности кода, а когда работаем над проектом больше равно двух — начинаем долго спорить о правилах…

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

дедлоки каналов, по невнимательности\неопытности заруинит приложение и вообще отобьет охоту иметь дело с го

Дедлоки в принципе возможны везде, где есть асинхронность, и в Go, в отличие от многих других экосистем, есть рейс-детектор в поставке по-умолчанию и отладка таких вещей намного проще, чем не в Go.


интерфейсы (они вообще развязывают руки для любого извращения)

Интерфейсами злоупотребляют в тех случаях, когда по каким-то причинам думают, что func f(interface{}) == template typename<T> f(T), а если так не делать (а так делать прямо не советуют в официальных ресурсах), то и проблем не будет


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

Но незакрытые дескрипторы вызовут проблемы и если вы оставите их открытыми в любом другом языке. При этом, опять же, везде где происходит работа с чем-то таким, в документации используют что-то вроде defer XXX.Close().


йота — странный модификатор констант, оно мне больше напоминает «финт для продвинутых», пользы от наличия enum больше.

Это разве страшно? Он не приводит к запутанному коду или содержит слишком дикую магию, но, конечно, выглядит как костыль.


возможность делать одно и тоже несколькими способами

Строго говоря, не одно и то же:


// область видимости err ограничена внутренностью if и не перезаписывает переменную err извне
if err := f(); err != nil {  
    // ...
}

err := f() // область видимости err ограничена функцией, и дальше не может быть переопределена
if err != nil {
    // ...
}

v1 := new(int) // так сделать можно
v2 := &int     // так сделать нельзя
UFO just landed and posted this here

Это верно, однако, и RAII — это не серебряная пуля. Чтобы в этом случае дескриптор закрылся — объект должен быть освобождён, а если он висит ожидая чего-то, то это не особо поможет.


В том же Go проблемы с горутинами и ресурсами, в основном, связаны не с тем, что кто-то забывает вызвать Close (к тому же финалайзеры вызываются, когда os.File собирается GC, например. эдакий недетерминированный RAII), а из-за того, что условие выхода из горутины не определено.
Про таймауты на http-соединения забывают намного чаще. А это, в принципе, мало зависит от языка.


Можно возразить, мол, не во всех языках есть "легкие треды" и будете правы, там где приходится иметь пул потоков — он забьётся и всё колом встанет, а в Go будет расти память на висящих горутинах, пока процесс не умрет по OOM.

а в Go будет расти память на висящих горутинах

Для этого есть мониторинг, у нас всегда снимаются снэпшоты состояния работающих Go процессов и такие утечки ловятся моментально, как и множество других проблем которые можно определить по состоянию рантайма.
UFO just landed and posted this here
> Дедлоки в принципе возможны везде, где есть асинхронность, и в Go, в отличие от многих других экосистем, есть рейс-детектор в поставке по-умолчанию и отладка таких вещей намного проще, чем не в Go.

Не везде. Например в pony нет дедлоков за счёт специальных аннотаций типов.
кроме того область видимости переменной может быть лишь в пределах условия\цикла (тоесть одно и тоже имя для переменной использовать многократно в разных конструкциях в пределах одной функции)…


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

1. В большинстве случаев просто возвращается ошибка с текстом (fmt.Errorf, errors.New). В этом случае необходимо ее парсить.
2. В библиотеке есть внутренние «типизированные» ошибки (var errInternal = errors.New(«package: internal»). Но они не экспортируются и так же приходится парсить.
3. Экспортирует внутренние ошибки, сравнение по значению (err == io.EOF)
4. Проверять ошибку на соответствию интерфейсу (Timeout)
5. Возвращает свою структуру (net.OpError) с доп. контекстом. Делаем swtich err.(type).

Например, пакет http возвращает их все, т.к. состоит из множества других пакетов. И если необходимо узнать причину возникновения ошибки (на уровне DNS, недоступность сервера, таймайты, ошибка прокси), то получается огромное ветвление на пару экранов, где идет сверка регулярными выражениями.

Но этим грешат и сторонние пакеты, так и подходы в других языках.

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


Но абсолютное большинство сторонних библиотек и наш собственный код вполне может экспортировать все нужные ошибки и использовать интерфейсы или типы для группировки ошибок по классам (вроде Timeout). Так что за редкими исключениями вроде ситуации с errClosing всё не так плохо.

Что мне не нравится в Го:

— отсутствие правил для распределения определений типов и функций по файлам. По идее пакет — это папка, но правила не говорят ничего о том, в какой файл надо помещать то или иное определение. При навигации в чужом коде не понять, где находится то или иное определение. При написании кода не понять, куда их помещать. Сделали бы каждый файл отдельным пакетом тогда. Вот в PHP — в 20 раз проще: каждый класс в своем файле.

— непонятная ситуация с менеджером пакетов. Почему нельзя, как в PHP, просто сделать composer install? Зачем устроили чехарду с GOPATH? Вот удовольствие, всю иерархию папок в /tmp создавать. И непонятно как выложить пакет на гитхаб. Самый работающий вариант — просто скачать зависимости и засунуть в папку.

— дурацкая идея с большими буквами для обозначения экспорта. Половина функций с маленькой буквы, половина с большой, и надо постоянно вспоминать, как именно пишется та или иная.

— невозможно нормально вызывать код на Си, так как у Го есть свои потоки, которые параллельно что-то там делают и мешают коду на Си. Почему нет функции для отключения всей этой ерунды, когда нужно? Пытаешься навесить ограничения через seccomp() и все отваливается. Ну и сам CGO так себе — надо как-то извращенно в комментариях указывать инклуды и флаги компилятора. Причем ведь, небось, не портабельно и не с любым комилятором это работает.

При навигации в чужом коде не понять, где находится то или иное определение.

Учитывая отличную поддержку go to definition во всех редакторах/IDE для Go — это никогда не является проблемой, мне давно всё-равно в каком файле написано объявление, если я могу туда моментально попасть нажав gd на идентификаторе в vim.


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


Зачем устроили чехарду с GOPATH?

Затем, что традиционные подходы к управлению пакетами не устраивали. Сейчас эта проблема уже (почти) в прошлом, поддержка модулей уже добавлена, осталось дождаться пока подтянутся утилиты вроде gorename чтобы работа вне GOPATH не создавала неудобств.


и надо постоянно вспоминать, как именно пишется

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


невозможно нормально вызывать код на Си

Это общая проблема всех языков, которые не Си. По крайней мере всех, на которых мне это приходилось делать.

Иииии… Приз за самый странный список претензий к Go уходит господину с ником Sabubu.

По идее пакет — это папка


И не по идее тоже.

но правила не говорят ничего о том, в какой файл надо помещать то или иное определение.


Собственно, кто вам мешает их написать и строжайшим образом придерживаться?

При навигации в чужом коде не понять, где находится то или иное определение


В теории, мне рассказывали, что есть такая штука, как go to definition или, на крайняк, текстовый поиск в пределах папки. Но это, конечно, не точно.

Сделали бы каждый файл отдельным пакетом тогда


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

непонятная ситуация с менеджером пакетов


go get = git clone. Куда уж понятнее, чесслово?

Почему нельзя, как в PHP, просто сделать composer install?


Чоэта нельзя? dep init && dep ensure? Да там сильно больше одного варианта, если честно.

Зачем устроили чехарду с GOPATH?


Чтобы не повторять историю с node_modules, вероятно.

Вот удовольствие, всю иерархию папок в /tmp создавать.


Честно говоря, не самое подходящее место для GOPATH, имхо.

Самый работающий вариант — просто скачать зависимости и засунуть в папку.


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

Половина функций с маленькой буквы, половина с большой, и надо постоянно вспоминать, как именно пишется та или иная.


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

невозможно нормально вызывать код на Си, так как у Го есть свои потоки, которые параллельно что-то там делают и мешают коду на Си


Ну вот это вообще странная претензия. Из языка A сложно работать с кодом на языке B. Покажите, пожалуйста, где это не так.

Почему нет функции для отключения всей этой ерунды, когда нужно?


Когда вам нужно отключить всю эту ерунду, вероятно, вам стоит писать на C. Потому что с отключенной «ерундой» Go именно в C и превращается. А у C еще и с интеропом с другим кодом на C проблем меньше.

не с любым комилятором это работает


Ну, как бы, у Go официально один компилятор…
> Собственно, кто вам мешает их написать и строжайшим образом придерживаться?

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

> В теории, мне рассказывали, что есть такая штука, как go to definition или, на крайняк, текстовый поиск в пределах папки

Это неудобно, а go to definition в моем редакторе, может быть, нету.

> go get = git clone. Куда уж понятнее, чесслово?

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

> Честно говоря, не самое подходящее место для GOPATH, имхо.

Ну мне надо загрузить код, скомпилировать, прогнать тесты и удалить. /tmp для этого самое логичное место. А редактировать код я, конечно, буду в Виндоуз, а не Линукс.

> Импортированная извне — с заглавной (а перед ней еще имя пакета, обычно), своя внутренняя — со строчной.

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

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


Ну, т.е. «синдром утёнка» в чистом виде? «Я вот привык к языку X, а Go не X, поэтому Go — говно»?

Это неудобно, а go to definition в моем редакторе, может быть, нету.


Кхм, для Go есть две известные мне IDE-шки, в обеих go to definition есть. Возможно, вам стоит просто изменить редактор?

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


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

Ну мне надо загрузить код, скомпилировать, прогнать тесты и удалить. /tmp для этого самое логичное место.


Ну, собственно, в таком подходе можно и в /tmp. Однако не совсем понятно, почему не вкорячить GOPATH в пределах вашего хомяка и после сборки/прогонки тестов достаточно будет просто вычистить GOPATH/src.

А редактировать код я, конечно, буду в Виндоуз, а не Линукс.


Вот тут я реально не понял, где тут причина для гордости, и почему «конечно».

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


Собственно, не пишите большие модули. К тому же вам никто не запретит иметь неэкспортированные методы (все) в модуле. А экспортировать их, создавая врапперы с заглавной буквы. Самый простой подход, к слову.
Ну, т.е. «синдром утёнка» в чистом виде? «Я вот привык к языку X, а Go не X, поэтому Go — говно»?

Нет, это парадокс блаба


Блаб попадает в середину континуума абстрактности. Это не самый мощный язык, но он мощнее, чем Cobol или машинный язык.
И на самом деле, наш гипотетический программист на Блабе не будет использовать ни Cobol, ни машинный код. Для машинных кодов есть компиляторы. Что же касается Cobol'а, наш программист не знает, как на этом языке вообще что-то можно сделать. В Cobol'е даже нет некой возможности X, присутствующей в Блабе.

Го, очевидно, менее мощен чем другие языки, потому что это было требованием, заложенным в него Пайком.


Ну, собственно, в таком подходе можно и в /tmp. Однако не совсем понятно, почему не вкорячить GOPATH в пределах вашего хомяка и после сборки/прогонки тестов достаточно будет просто вычистить GOPATH/src.

Что если разным тестам нужны разные gopath?


Вот тут я реально не понял, где тут причина для гордости, и почему «конечно».

Вопрос в том, удобно ли на винде разрабатывать?


Собственно, не пишите большие модули. К тому же вам никто не запретит иметь неэкспортированные методы (все) в модуле. А экспортировать их, создавая врапперы с заглавной буквы. Самый простой подход, к слову.

А при чем тут размер? Или у вас модули на 10 типов и все?

Го, очевидно, менее мощен чем другие языки, потому что это было требованием, заложенным в него Пайком.


Абсолютно верно. Он менее выразителен, чем многие другие языки. Он менее универсален, чем многие еще более другие языки. Он менее… да дофига чего менее. При этом не совсем понятно, каким образом из этого следует, что он «плохой».

Что если разным тестам нужны разные gopath?


Кхм. Дело в том, что вы, видимо, не совсем понимаете суть gopath. Дело в том, что в пределах одного проекта разным тестам не может быть нужен разный gopath.

А если у вас есть несколько разных проектов…

Ну, во первых, вполне себе завезли модули, которые, как раз, и являются реализацией «собственного» gopath на каждый проект.

До этого вполне себе был вендоринг, который решает аналогичную задачу.

При этом gopath, который, вообще-то, тупо переменная среды, вполне себе можно задать при старте тестов, т.е. на каждый старт теста вы можете заводить отдельный gopath. И даже у двух запущенных рядом тестов gopath вполне может быть раздельным.

Единственная «не надуманная» проблема в том, что вы в один бинарь до появления модулей не могли слинковать две версии одной и той же библиотеки (но достаточно простым костылем решалось и это), что, вообще то, если хорошо подумать, весьма сомнительная затея.

Вопрос в том, удобно ли на винде разрабатывать?


Вопрос в том, в чем преимущество винды в процессе разработки Go-приложений. Что-то вообще ни одного плюса в голову не приходит.

А при чем тут размер? Или у вас модули на 10 типов и все?


Ну вот, например, в Go не принято иметь «большие модули». И «большие интерфейсы» тоже, например…
Абсолютно верно. Он менее выразителен, чем многие другие языки. Он менее универсален, чем многие еще более другие языки. Он менее… да дофига чего менее. При этом не совсем понятно, каким образом из этого следует, что он «плохой».

Никто не говорил про "плохой". Речь шла про "язык, где эти правила уже есть и не надо изобретать велосипед".


Кхм. Дело в том, что вы, видимо, не совсем понимаете суть gopath. Дело в том, что в пределах одного проекта разным тестам не может быть нужен разный gopath.

Ну как это. Если я хочу протестировать работу приложения со стейблом, бетой и ночником одной и той же библиотеки, как это сделать?


Вопрос в том, в чем преимущество винды в процессе разработки Go-приложений. Что-то вообще ни одного плюса в голову не приходит.

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


Ну вот, например, в Go не принято иметь «большие модули». И «большие интерфейсы» тоже, например…

"Большой" понятие растяжимое.


В библиотеке на 20-30 типов почти наверняка описанная ситуация уже произойдет.

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

Контроль версий, просто создаете новую ветку с новой версией завендоренной библиотеки.
Ну как это. Если я хочу протестировать работу приложения со стейблом, бетой и ночником одной и той же библиотеки, как это сделать?


Ну, собственно, если уж на то пошло, то это, как раз, делается элементарно. Средствами git. Дело в том, что вас никто не обязывает пакеты в gopath go get'ом тянуть. Их можно просто git'ом положить. А потом просто git checkout делать… И это, стоит признать, для подобных сценариев скорее в плюс Go-экосистеме.

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


Окей, возможно, я не совсем верно распарсил ваше «разрабатывать конечно я буду на Windows». В контексте «конечно, исходя из моих личных предпочтений, разрабатывать буду на Windows» претензий не имею.

В библиотеке на 20-30 типов почти наверняка описанная ситуация уже произойдет.


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

Но это, собственно, вкусовщина, которая, к тому же, неплохо решается средствами IDE.
UFO just landed and posted this here
Любая IDE кроме студии на линуксе будет лучше, одним словом.
Читаю очередную статью о недостатках Go и со всем согласен. А где статьи о плюсах Go? Интереса ради бы почитал. Не считаю язык хорошим, но понять бы, что другие в нём находят.
UFO just landed and posted this here
Так можно сказать про любой язык:
легко писать, легко читать, легко собирать, легко деплоить на сервер, легко и очевидно вообще все.

Вот только на поверку оказывается, что иногда проще даже на Rust написать, чем на Go с его вырвиглазной и нечитабельной обработкой ошибок, странными naming conventions (вкупе с отсутствием нормальных IDE кроме жидбрейновской), и возможностью выстрела в ногу с горутинами. В Rust безопасность и эффективность кода получается выше, при этом код не сильно менее читабелен.
иногда проще даже на Rust написать


Вы же понимаете, что ключевое слово — «иногда». Прям, я бы сказал, сильно иногда.
при этом код не сильно менее читабелен.

Прикладной код вообще не менее читабелен, а скорее более.


Я в соседней статье попросил показать мне конкретные недостатки моей реализации телеграм-клиента на расте, из-за которой непонятно, что происходит. Пока что тишина...

Мне судить сложно, я в обоих технологиях на уровне джуна. Сужу по опен-сорцу.
Несколько лет назад не знал про Радио-Т, но сейчас умпутун там главный проповедник Go
Повзрослел человек, сменились ценности. Вместо интереса процесса стал важнее простой и надежный результат.

Если коротко, то на нём просто, быстро и приятно писать надёжный и читабельный код. Если не так коротко, то (в случайном порядке):


  • простой язык, что даёт возможность без труда выучить его "от и до", на 100%
  • гарантия обратной совместимости Go1, что даёт возможность тратить минимум времени на поддержку старого кода
  • отличный вспомогательный инструментарий, как "из коробки", так и сторонние утилиты (тут конкретику можно долго перечислять, из основного: тесты, документация, управление пакетами, мощные линтеры, кодогенерация, детектор race conditions, …)
  • очень простая кросс-компиляция и отличная портабельность стандартной библиотеки, что сильно упрощает разработку кросс-платформенных приложений
  • один бинарник, часто статический, что упрощает деплой как микросервисов, так и кросс-платформенных утилит
  • строгая статическая типизация с выводом типов, что позволяет получить соответствующие плюшки без того, чтобы вручную постоянно уточнять все типы в коде
  • горутины и каналы, что даёт возможность писать обычный синхронный код (как правило без мьютексов и коллбэков) и при этом обрабатывать миллионы параллельных соединений задействуя все CPU, имея при этом на порядок реже типичные для многопоточного кода сложные проблемы с дедлоками и race conditions
  • интерфейсы (которые, к сожалению, многие не понимают), которые позволяют проще и элегантнее проектировать код
  • defer, что упрощает контроль за освобождением ресурсов
  • отсутствие наследования, что тоже способствует написанию более ясного кода
  • регулярки RE2, что гарантирует предсказуемое время их выполнения
  • безопасный, в плане отсутствия ручного управления памятью…
  • … но при этом достаточно низкоуровневый, чтобы можно было при необходимости вручную оптимизировать код вплоть до выделения памяти
  • единый стиль оформления кода везде, что упрощает чтение кода и экономит время на согласование стиля в каждом проекте
  • мощная стандартная библиотека, что даёт возможность обходиться без дополнительных больших и сложных фреймворков в большинстве проектов
  • под линух есть возможность писать аналог обычных скриптов на Go (т.е. выполнимый файл с исходником на Go а-ля ~/bin/something.go, который можно запускать как обычную команду something.go)
  • очень быстрая компиляция, вроде бы мелочь, но как посмотришь на C++ то становится ясно, что не такая уж это и мелочь
  • в отличие от некоторых других отличных языков — на нём легко найти работу
быстро и приятно

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

Честно, вывод типов в go достаточно слабый.

Простейший пример на go.
Простейший пример на rust.

В аргументе c функции call указаны все необходимые типы для колбэка. При этом, в коде на go всё равно приходится их указывать, тогда как в коде на rust в этом уже нет необходимости.

Да и типизация из-за отсутствия дженериков иногда нуждается в подпорках

Если я правильно понял, то в Rust это не вывод типов, а то как работают замыкания — типы (сигнатура) определяется при первом использовании замыкания.

UPD: хотя да, это вывод типа анонимной функции, но не потому что в аргументе c функции call указаны все необходимые типы для колбэка.
> UPD: хотя да, это вывод типа анонимной функции, но не потому что в аргументе c функции call указаны все необходимые типы для колбэка.

А почему?

Тип анонимных функций определяется при первом использовании. Потому, если я все правильно понял.
Вот у вас функция: println!("{}", call(10, 10, |a, b| a + b));, а вот тут первое использование — c(a, b).
a и b имеют тип i64, потому у этой анонимной функции будет тип Fn(i64, i64) -> i64, а не потому что в сигнатуре where F: Fn(i64, i64) -> i64.


let example_fn = |a| a;
println!("{}", example_fn(String::from("Yo"))) // вот тут уже будет тип Fn(String) -> String

let example_fn2 = |a| a;
println!("{}", example_fn(2)) // вот тут уже будет тип Fn(i64) -> i64
Конечно, если не указывать тип у переменной example_fn, то типы в замыкании будут выведены из его аргументов.

Чтобы ваш пример соответствовал моему, надо у переменной example_fn указать конкретный тип, тогда типы аргументов замыкания будут выведены из типа переменной и если при первом использовании замыкания передать ему в качестве аргумента неправильный тип, то компилятор выдаст ошибку.

Ваш пример не равнозначен моему. В моём примере типы аргументов замыкания выводятся из сигнатуры функции call, потому что там явно указаны типы, в вашем примере — при первом использовании, потому что у вас не указан тип переменной, которая владеет замыканием, и её тип так же выводится впоследствии.

Надеюсь понятно объяснил.
Вот пример того, о чём я написал:

fn main() {
    let example_fn: &Fn(String) -> String = &|a| a;
    // example_fn Принимает только String
    println!("{}", example_fn("Hello, world!".into()));

    let example_fn2: &Fn(i64) -> i64 = &|a| a;
    // example_fn2 Принимает только i64
    println!("{}", example_fn2(64));
}

Сейчас я еще больше запутался, чем тогда это отличается от Go?
```go
func main() {
example_fn := func(x string) string { return x }
print(example_fn(«hello»))

example_fn2 := func(x int) int { return x }
print(example_fn2(10))
}
```

Как раз в первом варианте (тот что с callback), там выводится тип анонимной функции по первому использованию и после этого выведенный тип удовлетворяет сигнатуру функции **call**.

Можете кинуть ссылку на документацию по этому?

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

На расте этот случай будет описан так.

fn main() {
    let example_fn = |x: String| -> String { x };
    println!("{}", example_fn("hello".into()));

    let example_fn2 = |x: i64| -> i64 { x };
    println!("{}", example_fn2(10));
}

Тут вывод типа у переменных example_fn, example_fn2. Это равносильно примеру го.

В примере выше вывод типа происходит у аргумента замыкания. В примере с функцией call, так же вывод типа происходит у аргумента замыкания за счёт того, что в сигнатуре функции явно указан тип аргумента замыкания.
В примере выше вывод типа происходит у аргумента замыкания. В примере с функцией call, так же вывод типа происходит у аргумента замыкания за счёт того, что в сигнатуре функции явно указан тип аргумента замыкания.

Можете дать ссылку на документацию?


Потому что в примере:


let example_fn: &Fn(String) -> String = &|a| a;
println!("{}", example_fn("Hello, world!".into()));

Вы же указали тип функции и тип аргумента функции, вот же он &Fn(String). Функция example_fn ждет первым аргументом String. Если там не String — то ошибка компиляции.


Ваш изначальный пример — это свойства анонимных функций, а не вывод типа анонимной функции из сигнатуры.

То, о чем я говорю
fn call<F>(a: i64, _b: i64, c: F) -> i64
where
    F: Fn(i64) -> i64,
{
    c(a)
}

fn main() {
    let example_fn = |a| a;
    println!("{}", call(10, 10, example_fn));

    println!("{}", example_fn(String::from("hello"))); // ошибка
}


Тип example_fn определился при первом вызове.
Ладно, вот ваш же пример, но без вызова:

fn call<F>(_a: i64, _b: i64, _c: F) -> i64
where
    F: Fn(i64) -> i64,
{
    42
}

fn main() {
    let example_fn = |a| a;
    println!("{}", call(10, 10, example_fn));

    println!("{}", example_fn(String::from("hello"))); // ошибка
}


Вызова нет? Нет. Тип example_fn определён? Определён. Выводом типов, ровно из-за того, что мы передаём example_fn в функцию call с явной сигнатурой.

Запускаем. Компилятор нам любезно сообщает об ошибке о несоотвествии типов, он же в example_fn ожиадет получать i64, а мы ему String подсовываем:

error[E0308]: mismatched types
  --> src/main.rs:12:31
   |
12 |     println!("{}", example_fn(String::from("hello"))); // ошибка
   |                               ^^^^^^^^^^^^^^^^^^^^^ expected i64, found struct `std::string::String`
   |
   = note: expected type `i64`
              found type `std::string::String`


И прошу заметить что вызова с i64 не было, но он уже требует i64!
Да согласен.
Я все время ссылался на эту доку, про то, что вы писали так и не нашел.
Читаю очередную статью о недостатках

«Есть всего два типа языков программирования: те, на которые люди всё время ругаются, и те, которые никто не использует.
© Бьерн Страуструп»


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

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

Все очень просто,
отрицание, равнодушие, принятие

У меня так с каждой технологией, с начала хаю, потом становится ровно, а потом слезть не могу, а с Go я сейчас между равнодушием и принятием
За что я люблю язык Go:
1. На него есть спрос у бизнеса.
2. За него хорошо платят.
Хорошо — это в среднем по палате дешевле, чем за Java и C++, но дороже, чем за PHP.
но ведь с PHP можно уйти на JS и мимикрировать под фуллстека — и будут платить совсем хорошо!
Не факт, что это будет длиться долго.
Совершенно точно, что это НЕ будет длиться долго, если не повторится бум базвордов, как несколько лет назад. Нам критически нужны новые базворды от авторитетных компаний: что-нибудь сравнимое с tree-shaking, SPA, PWA, typescript, RxJS, войны css-препроцессоров, несколько мажорных изменений в ES, и самое главное — больше короткоживущих фреймворков: оптимально, чтобы фреймворки устаревали за полгода-год. Только так можно сохранить оплату за формошлепство на текущем уровне, только так мы можем спасти индустрию от наплыва умников из других языков.

Если большая тройка фреймворков будет продолжать жить еще год-два, зарплаты у фронтов неизбежно упадут на уровень зарплат пхпшников пятилетней давности.
Лучше уходите на JS client-side, не надо пилить на нем серверную часть. Тогда я хотя бы не буду смотреть на вас с осуждением.

Ну, либо просто уходите)
не нуачо? Разве не были убедительными аргументы продвигателей ноды, о том, что джс за время войны браузеров стал ого-го какой язык, и надо всего-то присобачить к нему библиотку врапперов системных вызовов и что-то няшное для работы с БД? Все поверили, теперь вот радуемся результату.
А что не так с нодой? Накатываем typescript и радуемся жизни :)
С нодой… с нодой почти все не так, имхо.

Хотя, вижу, вы тоже на позитиве.

Нет, к сожалению, я трезв.
У ноды есть проблемы (на мой личный взгляд), куда же без них. Но не сильно критичные, и писать, скажем, на es6/esm (а в es7 еще много вкусного сахара везут) или даже на ts вполне приятно.

А. Ну то есть ровно столько же (плюс-минус), сколько и за нормальные языки. На которые тоже есть спрос у бизнеса. Странный плюс, который у всех плюс.

Разве кто-то говорил о плюсах? Вы спросили: «сколько». Я вам ответил. Плюсы там в другом, впрочем, как и минусы.
> if err != nil {

Эм, а я тупо делал как-то вот так О_о.
По-моему, что-то такое в getting started написано было, и я не очень понимаю, почему все продолжают эти портянки тянуть в каждой статье про go :(

func (c *Config) load() {
    file, err := ioutil.ReadFile("config.json")
    failOnError(err, "Cannot open conf file")
    err = json.Unmarshal(file, &c)
    failOnError(err, "Cannot decode json")
}

func failOnError(err error, msg string) {
    if err != nil {
        log.Fatalf("%s: %s", msg, err)
        panic(fmt.Sprintf("%s: %s", msg, err))
    }
}
Потому что panic — не универсальная реакция на любую ошибку.
Ну добавьте еще хелпер anythingOnError() с нужными действиями и все.
Есть еще подходы вот вроде такого.
Моя непонятка была в куче портянок с «if err != nil {» в почти каждой статье, тогда как один из базовых принципов разработки — это вынос портянок однотипного кода в функцию.
Так в том-то и дело, что обработка ошибок — очень редко однотипный бойлерплейт, чаще всего каждой ошибке — свой подход (разве что их можно логать все, в остальном они различны).
Можно сделать кастомные ошибки, вон в той статье примерно описан принцип.
Можно хандлить этот самый err в хелпере и делать что нужно в зависимости от ошибки.
Так в том-то и дело, что обработка ошибок — очень редко однотипный бойлерплейт, чаще всего каждой ошибке — свой подход

Почти всегда на практике обработка ошибок — однотипный бойлерплейт и тогда очень иногда это не так. Посмотрите любую либу на гоу.
Если у вас *ошибки* обрабатываются однотипным бойлерплейтом — у меня для вас плохие новости. Ошибки должны быть правильно обработаны в каждом конкретном случае, иначе от них нет вообще никакого толка (можно тогда в любой ситуации panic делать).
у меня для вас плохие новости

Доктор, скажите прямо, какой диагноз?

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

То есть код, который сотни раз подряд делает однотипное return err — плохой?

Зависит от контекста. Если это библиотека — скорее всего нет. Если это приложение — скорее всего да. Но тут есть ещё много нюансов.
То есть куча
if err != null {
  return err
}

в библиотеке — это, по вашему, норм?
Для го — норм. Просто так — не норм.

только


if err != nil {
  return err
}
if err != null


null — не норм. Замените на nil, и тогда норм.
Ну добавьте еще хелпер anythingOnError() с нужными действиями и все.

Но ведь если там не будет паники, то код продолжит выполнение, так что такой прием подходит только для паники
«Пишите код» :)
Вы правда хотите чтобы я вас начал учить как писать код на go? :)
Вот чесслово, в документации и нескольких статьях все принципы и концепции прекрасно описаны, все раскуриваются за пару-тройку вечеров и потом пишется как надо под ваши потребности.
Я не говорю, что обработка ошибок в частности и язык вообще идеальны. Есть вещи, которые лично мне не нравятся. Но все эти особенности можно просто знать и писать код с их учетом.
ЗЫ:
Вот не понимаю людей «а почему в языке Х сделано не так как в моем любимом У? язык Х — говно».
А самое главное, почему просто не иcпользовать любимый Y. Так как будто все вокруг заставляют использовать Х. :)

Я писал на го полгода. Я лишь говорю, что ваш совет о anythingOnError не работает

Это был дженерик совет :)
Конечно же обработчики нужно делать под конкретно требования проекта и знания разработчика. Благо способов больше одного.
Я понимаю, что это был дженерик совет. Но он не помогает избавиться от ифов. Вам нужно использовать или лапшу ифов или панику, другого пути нету.
Если это ошибки, генерируемые третьесторонними или встроенными функциями — то смотреть по месту/требованиям. Иногда можно игнорить err и работать только с возвращаемым значением.
Если вы генерите свои ошибки, то тут уже можно сделать интересно.
Но в целом да, совсем от if'ов не уйти, но можно заметно сократить их количество.
А в других ЯП разве не так же? :)
Я лично вот сейчас играюсь со штуками вроде вот такой в TS:

type Failable<V, E> = {
    isError: true;
    error: E;
} | {
    isError: false;
    value: V;
};


Ничего не напоминает? :)
Новую обработку ошибок уже везут, она уже в пути. Узбагойтесь уже)

Вы видимо никогда не писали лапшу из and_then

Писал, очень давно. «Я был молод и мне были нужны деньги» (с)
Сейчас стараюсь избегать такого, благо с тех пор многое узнал и попробовал.

Мне Го не нравится из за агрессивной рекламой! Я уже опасаюсь включить утюг, а то и он начнет дифирамбы петь про Го.

Да что ж вы все так убиваетесь? Вы ж так никогда не убьетесь! :)
А если серьезно — да не пиши ты на Го, раз все так плохо. Переходи на 1С — самое оно…
вот будет прикол, когда гугель похоронит го в пользу еще одной своей внутренней разработке, как похоронил уже сотни похожих проектов. смешно наблюдать толпу программеров, которые всерьез воспринимают этот ужас летящий на крыльях ночи ;)
Желание гугля похоронить Go, как и мнение Роб Пайка относительно умственных способностей программистов на Go, уже не важны. Языку уже условно «18 лет» и он вправе вести жизнь отдельно от родителей с их мнением по поводу будущего своего чада. А также его умственных способностей.
возраст не имеет отношения к зрелости языка, как говорится — некоторые с возрастом становятся умнее, а некоторые просто старше. среди го тусовки наверное только Роб Пайк и еще несколько человек могут самостоятельно принимать решения о применении того или иного языка в своих проектах — остальным просто приказали или наняли по уже сформированному тз. мораль сей басни довольно проста — можно много бить себя пяткой в грудь и развешивать свое мнение на хабрах, но если гугель вдруг закроет го — то никто из этой тусовки не в состоянии будет ни финанировать, ни развивать, ни даже тупо поддерживать такой немаленький проект. более того — сверху им опять прикажут и все смирненько перейдут на питон/руби или nodejs…
возраст не имеет отношения к зрелости языка

Да, конечно я имел ввиду, что язык достаточно зрел, чтобы выжить даже при закрытии.
да, и насчет 18 лет — я бы был поскромнее. реальные проекты за пределами гугля появились всего пару лет назад. а большинство го-программистов, которых я видел были, в свое время, не в состоянии освоить c++/js/ruby. вот их и прибило к этому берегу ;)
Неосиляторы — это обратная сторона медали легкости входа.
дык, так в том то и дело. если бы го реально сотавлял конкуренцию для nodejs/C++/ruby — так нет. пока наблюдается интерес только со стороны devops, большинство которых видят в го что-то типа bash с сокетами
Вы рекрутёр? Откуда вы знаете какой интерес к Go-программистам?
Даже беглый поиск по вакансиям, показывает, что уже сейчас можно идти работать Go-программистам, не в devops отдел:
hh.ru/search/vacancy?text=Go&specialization=1.221&area=1&salary=&currency_code=RUR&experience=doesNotMatter&order_by=relevance&search_period=&items_on_page=20&no_magic=true&from=suggest_post

И второе. Go не конкурирует с Си++/JS. Только Python/Ruby в сфере бекэнда. Иногда, в особо запущенных случаях с Java/C#. Не бекэнд на Go делать не удобно, это не универсальный язык. Только бекэнд и утилиты командной строки.
первое — я CEO. второе — го действительно не конкурент c++/js. IMHO, еще лет 20 надо довести до ума язык, инфраструктуру и самое главное — людей
если бы го реально сотавлял конкуренцию для nodejs

nodejs говорите? habr.com/post/337098
Создатель Node.js: «Для серверов я не могу представить другой язык кроме Go»

Уже вот несколько лет, как я не работаю над Node, ну, где-то, примерно с 2012 или 2013


чел отвалил с разработки еще до версии 0.8.0, и потом уперся в полумертвый го. только вот с 2012 нода поменялась на 90% и продолжает меняться, а он фишку походу уже не сечет. в свое время нам хватило встроенного в ноду cluster модуля, когда нам нужно было распараллелить задачу, потом юзали такую штукенцию:

synsem.com/EMS.js

сейчас используется другое решение, но нигде нет go и все прекрасно пашет без хайпа и горутин.
Конечно же 18 лет — это метафора, говорящая о том, что уже как бы самостоятельный, но живёшь с родителями.
ага, и ничего не поделать с тем, что глухая бабушка на кухне смотрит в 100500 раз санта барбару. так сказать — полная аналогия ;)
Да, это так. Язык пока несовершенен, но в одной узкоспециализированной области чрезвычайно удобен.
ну да, докер и кубышка — вот и вся его область. даже не слышал о более менее серьезных го проектах за пределами devops. зрелость языка как раз и имеряется областью его применимости, а не левым возрастом.
Если вы не слышали, не значит, что их нет. Многие компании пилят тихонько бекэнд на Go (включая Яндекс) и рады. То, что они не пиарят это на каждом углу, ну пока не в таких объёмах, чтобы это как-то заметно просачивалось.

Но точно вам скажу, что я пилю бекэнд на Go, и в случае смены работы я найду достаточно мест, где смогу делать то же тамое.
как говорили классики — «пилите Шура, они золотые». странно думать уже о смене работы на таком «перспективном» направлении, но могу отметить, что опасения не напрасны ;)
Я не против, чтобы за вами осталось последнее слово.
справедливости ради, можно отметить, что и в PHP в свое время владывали разные компании, например мордокнига. но очень скоро поняли, что не в коня корм.
и что там мы видим? есть frontend аля Vue? есть desktop аля Electron? есть android/ios apps аля Xamarin? увы, я там вижу только дешевый хайп сырых либ и байдингов к C++. ни один вменяемый заказчик не заплатит ни цента за эту чушь.
А видим мы там, во-первых, совсем не только «докер и кубышка», и во-вторых, например, кучку утилит для работы с аудио, кучку других разных утилит (для css/sass например). Еще мы видим, например, syncthing (андроид клиент для которого написан на том же самом go), или minio. Можете даже сходить в раздел Financial или Game Development, или даже в GUI/Machine Learning.
А можете уточнить название конторы, где вы CEO? Терзают меня смутные сомнения…
а… утилиты… command line… так тут никто и не спорит. я уже говорил — тупо bash с сокетами. а целых два android приложения — это несомненная победа го над всеми языками сразу. ну и бесполезные сомнения пусть терзают дальше, так как конторами — не заведуем
Вы сейчас рассуждаете как 15 летний школьник, который вчера попробовал на React Native написать мобильное приложения и ничего не понимает в бэкэнд инфраструктуре.
Он же специально, не кормите.
так не в коня этот гавенный корм. научитесь готовить, а потом кормите.
о, люблю этот момент — когда оппонент не в состоянии ответить на весь спектр аргументов и выбирает только тот с которым он хоть как-то знаком. ok, возьмем backend — так предьявите невиданные доселе в мире c++/js бонусы, которые предоставляет go!
ok, возьмем backend


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

так предьявите невиданные доселе в мире c++/js бонусы, которые предоставляет go (для бекенда)


Относительно C++ бонус достаточно очевиден. Go проще в разработке и поддержке. Давайте говорить правду: C++ может всё. Вот просто всё, без исключения. При этом, признайте уже, наконец, везде, где есть разумная по трейд-оффу возможность не использовать C++, C++ не используется.

По JS. Кхм… JS на бэкенде? Ну вот, в самом деле, вы серьезно? Полуторагигабайтный бандл непонятного однопоточного js-лапшеобразного кода с непонятными корнями и браузером внутри, чтобы это как-то работало… Вот это вы считаете нормальным для бекенда?
так правда сурова — го не используется там, где уже давно и прочно царит C++/Python/Ruby/Node, а это практически все области ПО, включая бэкенд. и даже когда дело доходит до трейдофа, го не катит, так как писать на нем что-то большое просто невозможно, а для мелкого годится все что уже знает команда. JS на бэкенде — легко! и никаких проблем с проф уровнем программистов и их взаимозаменяемостью. про полтора гигабайта — закопайте уже phantom.
git clone https://github.com/golang/go.wiki.git
cd go.wiki
git log FromXToGo.md |grep Dat|cut -d ' ' -f 8|uniq -c

...
8 2018
8 2017
28 2016
...


походу хайп затих еще в 2016 ;)
так правда сурова — го не используется там, где уже давно и прочно царит C++/Python/Ruby/Node, а это практически все области ПО, включая бэкенд.


Немножко суровой правды, как бы вы ее ни отторгали: Go используется.
так никто и не спорит, докер и кубышка — его основная ниша на данный момент. и все это — пока гугель спонсирует движуху. зрелость языка есть сумма очень многих компонент, туда входит много чего, for ex.: кривая разивития, наличия стандартов, качественно разработанных библиотек, надежной поддержки, широта области применения, наличие проф. commnity и т.д. Так вот по совокупности факторов назвать этот язык зрелым пока не представляется возможным.
уверен что вы как яркий представитель community возьмете бразды правления в свои руки и выбьетесь со временем в топ ;)
есть frontend аля Vue?


Нет, конечно, Go не предназначен для фронта.

есть desktop аля Electron?


Нет, конечно, Go очень неудобен для гуя.

есть android/ios apps аля Xamarin?


Конечно же, нет, Go не предназначен для мобильных приложений.

Есть, конечно, усраться-универсальные языки, например Java, C++, C#. На которых можно всё. Только из-за этого «можно все» неизбежны компромиссы, при наличии которых именно каждое направление в отдельности может причинять некоторые неудобства.

И есть Go, который проектировался с одной целью — бекенд. И на нем удобно пилить бэкенд. Удобнее, чем на любом из «универсальных» языков. Поэтому он в этой нише и растет. А на остальные просто не претендует.

ни один вменяемый заказчик не заплатит ни цента за эту чушь.


Платят. И даже больше цента.
не можем, не умеем, не будем и даже не пртетендуем — что собственно и требовалось доказать. вернувшись к баранам имеем — 99% докер и еще какой-то хайп. а с таким раскладом — ему еще далеко до того как стать зрелым языком. может лет через 20…
И есть Go, который проектировался с одной целью — бекенд. И на нем удобно пилить бэкенд. Удобнее, чем на любом из «универсальных» языков.

Подозреваю, что у него нет библиотек уровня hibernate/entity framework/doctrine, подобные библиотеки позволяют не писать тонну кода.


Или есть такие библиотеки? Если нет, то в чем удобство go, перед универсальными ЯП?

Подозреваю, что у него нет библиотек уровня hibernate/entity framework/doctrine, подобные библиотеки позволяют не писать тонну кода.


Собственно, ORM'ы в Go не в моде. Да и в принципе, ORM как таковой полезен далеко не на всем спектре задач. Даже более того, полезность его ограничена сферой, требующей развесистой бизнес-логики поверх РСУБД. Собственно, «развесистая бизнес-логика» — отнюдь не вотчина Go. Там безраздельно царят Java и .NET, и никто их оттуда «вытеснять» и не собирается.

Если нет, то в чем удобство go, перед универсальными ЯП?


Понимаете, это из цикла «как что-то хорошее». В кеширующих серверах, балансировщиках, аггрегаторах, гейтах и шлюзах данных ОРМ не нужен. Более того, в ряде случаев он даже вреден…

И нет никакого «удобства перед универсальными ЯП». Есть удобство «в сравнении с каждым из универсальных ЯП». Вот эти «Go против всего мира» — это забавная олимпиадная дисциплина, но каждая задача подразумевает решение на определенном языке, а потому сравнение «1 язык против 100500 других» заведомо некорректно. Берется множество «более других языков» и поштучно сравнивается с Go. Если Go оказался предпочтительным в каждой сравниваемой паре на определенной задаче, Go хорош.

В общем и целом, в Go нет ни одной уникальной фичи, которую нельзя бы было встретить в другом языке. Однако комбинация оных делает его предпочтительным языком для целого ряда задач. А «вот эта фича есть в языке X», и «другая фича более удобна в языке Y», и «третья фича есть в языке Z» — это мы все слышали. Только вы не будете писать проект на языке XYZ. Вам придется выбрать один.

В общем, если хотите относительно объективных сравнений, не ставьте вопрос «чем Go лучше всех остальных языков». Это, в конце концов, непрофессионально. В разработке перед вами не будет выбора «Go против всех», у вас будет выбор «Go против X(где X — некоторый вполне конкретный язык)». Короче, ставьте вопрос «в каком спектре задач и почему Go лучше X(обязательно укажите интересующий вас X)».
В общем, если хотите относительно объективных сравнений, не ставьте вопрос «чем Go лучше всех остальных языков».

Я не ставил бы этот вопрос, если бы не ваше утверждение:


И на нем удобно пилить бэкенд. Удобнее, чем на любом из «универсальных» языков.



Короче, ставьте вопрос «в каком спектре задач и почему Go лучше X(обязательно укажите интересующий вас X)».

Вопрос был почти такой же, если не придираться к словам.
Вы ответили:


Там безраздельно царят Java и .NET, и никто их оттуда «вытеснять» и не собирается.



В общем и целом, в Go нет ни одной уникальной фичи, которую нельзя бы было встретить в другом языке. Однако комбинация оных делает его предпочтительным языком для целого ряда задач. А «вот эта фича есть в языке X», и «другая фича более удобна в языке Y», и «третья фича есть в языке Z» — это мы все слышали. Только вы не будете писать проект на языке XYZ. Вам придется выбрать один.

Уникальная фича никому не нужна, нужно то что упрощает разработку и уменшает ошибки.
Хотите сравнивать конкретный юзкейс с конкретным ЯП, пожалуйста, расскажите, чем Go удобнее чем Rust, для любой задачи.
Желательно с примерами кода на обеих ЯП.

Хотите сравнивать конкретный юзкейс с конкретным ЯП, пожалуйста, расскажите, чем Go удобнее чем Rust, для любой задачи.


Ну это прям вообще элементарно. В любой задаче, адекватно ложащейся на концепцию горутин, Go будет лучше и проще. Round-robin балансировщики, кеширующие прокси, REST-серверы и подобные вещи.

Связка tokio+futures-await так же удобны в использование как горутины.
Правда, для await! пока нужно использовать nightly версию языка.




А так горутины удобны, да, но это говорить лишь об одном из критерий, в котором у go хорошее решение.


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


Это сложно назвать удобством, когда элементарного шаблонного кода надо генерировать...




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

Голословное утверждение.
(в go может и удобно реализовать такие задачи, но не удобнее чем в ржавчине)

Я уже почти 4 года программирую на Go: мне не нужно генерировать шаблонный, мне не нужны дженерики, уже давно есть удобный пакетный менеджер (сейчас еще модули завезли), компилятор очень сильно помогает (зависит с чем сравнивать, да?). В основном я пишу/писал web сервисы (нагрузка от 30к до 400к RPS), стрим процессинг с Kafka, тулзы для Kubernetes, etc.
Мне нравится Go, мне нравится вот это if err != nil, после Java я совсем по другому начал относится с ошибкам.


И я совсем не понимаю, почему пользователи Хабра в каждом посте про Go начинают бегать туда-сюда с криками насколько Go ужасный язык, при этом 80% из них на Go не программируют. Начинают сравнивать отдельные фичи Go с фичами других языков — а вот в Х яызке это лучше, а вот в Y языке другое лучше:
— Но я использую Go, мне нравится X фича и для моих задач Go подходит идеально. Тем более у нас есть опыт на Go.
— Ты не понял, Go говно, там if err != nil, вот тебе Rust/C++/Haskell/JavaScript там еще лучше. Монад ведь нету в Go? Воот, ерунда ваш Go.


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

Правда, для await! пока нужно использовать nightly версию языка.


Ну т.е. в стейбле этого еще нет. И вы предлагаете nightly тащить на прод… Ну, как бы, Г-дь вам судья, конечно.

Только вот это сильно на передергивание похоже — преподносить то, чего еще нет в стабильной версии Rust'а в качестве замены тому, что уже несколько лет есть в Go. Не надо так, пожалуйста. В Rust'е нет замены горутинам (пусть это и изменится, но это будет потом, а на Go можно уже сейчас). Как всегда, вы пытаетесь «шашечки» противопоставить «ехать».

теряете например нормального менеджера пакетов, дженериков, АТД, паттерн матчинг, и помошника компилятора.


Чем вам пакетный менеджер Go не угодил?

Отсутствие дженериков — это, конечно, аргумент, но… а) не так больно, как вам кажется, б) используя ваши же методики аргументации — обещали «подвезти» в Go 1.6 => 2.0.

Это сложно назвать удобством, когда элементарного шаблонного кода надо генерировать…


Ну, как бы, за пару лет практики с необходимостью ручной генерации сталкивался пару раз. Есть, конечно, вещи вроде fasthttp или grpc, swagger и т.д. Но в fasthttp кодогенерация дает ощутимый прирост в производительности, а потому оправдана. А для grpc, swagger и прочих декларативных API-описаний, собственно, кодогенерация — это нормальный подход, везде так.

в go может и удобно реализовать такие задачи, но не удобнее чем в ржавчине


Вы же сами сказали, что в расте (пока еще) нет полноценной замены горутинам… Т.е. пока еще удобнее, а то, что будет «потом когда-нибудь»… Хм, мне код сейчас надо писать.
Отсутствие дженериков — это, конечно, аргумент, но… а) не так больно, как вам кажется, б) используя ваши же методики аргументации — обещали «подвезти» в Go 1.6 => 2.0.

Пару фич из найтли, конкретно генераторы и новая версия макросов, это не тоже самое, что и "обещали в следующей мажорной версии".


Чем вам пакетный менеджер Go не угодил?

В этой ветке узнал о том, что ввели модули.
Это снимает част вопросов, тем неменее, например не хватает репозиторий, но это уже некритично.


Ну, как бы, за пару лет практики с необходимостью ручной генерации сталкивался пару раз. Есть, конечно, вещи вроде fasthttp или grpc, swagger и т.д. Но в fasthttp кодогенерация дает ощутимый прирост в производительности, а потому оправдана. А для grpc, swagger и прочих декларативных API-описаний, собственно, кодогенерация — это нормальный подход, везде так.

Ок, видимо избегают его.


Вы же сами сказали, что в расте (пока еще) нет полноценной замены горутинам… Т.е. пока еще удобнее, а то, что будет «потом когда-нибудь»… Хм, мне код сейчас надо писать.

Мне кажется вы видете в горутин молотока.
Меня же интерисует не только молоток, но и другие инструменты.
Вы вот промолчали про АДТ, паттерн матчинг, про компилятор с продвинутым выводом типов.
Это далеко не все, но важные вещи.




Ещё раз напомню, что мы обсуждаем (чтобы далеко не уходить от темы):


И есть Go, который проектировался с одной целью — бекенд. И на нем удобно пилить бэкенд. Удобнее, чем на любом из «универсальных» языков.

Это неверно для любого языка, особенно на бэкенде.

Пару фич из найтли, конкретно генераторы и новая версия макросов, это не тоже самое, что и «обещали в следующей мажорной версии».


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

Вы же, надеюсь, понимаете, что никто не кинется переписывать уже работающее Go-решение на Rust`е просто потому, что туда «уже скоро завезут фичу, которая сделает его конкурентноспособным»? Найтли-фичи == еще нет в языке (с точки зрения продакшена).

например не хватает репозиторий


Гитхаб вам чем не угодил, собственно?

Ок, видимо избегают его.


Хм, вы уверены, что у вас есть опыт энтерпрайз-разработки? Вот, например, в энтерпрайзе очень любят SOAP, а это всегда кодогенерация, что на Java, что на C#…

Мне кажется вы видете в горутин молотока.


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

Вы вот промолчали про АДТ, паттерн матчинг, про компилятор с продвинутым выводом типов.


АДТ, паттерн-матчинг и «продвинутый вывод типов» — это фичи, прилагаемые, как правило, к «развитым и гибким системам типов», т.е. для языков, разрабатываемых для подхода «от абстракции». В Go система типов примитивна, и без изменения оной перечисленные вами фичи, мягко выражаясь, малоосмыслены.

Это неверно для любого языка, особенно на бэкенде.


Не уловил, откуда взялся такой вывод…
Писать продакшн-код на нестабильной версии языка — моветон. Т.е. абсолютно равносильно тому, что «пока еще не завезли». При том, что в Go оно есть и работает, и не один год.

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


Гитхаб вам чем не угодил, собственно?

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


Вы же, надеюсь, понимаете, что никто не кинется переписывать уже работающее Go-решение на Rust`е просто потому, что туда «уже скоро завезут фичу, которая сделает его конкурентноспособным»? Найтли-фичи == еще нет в языке (с точки зрения продакшена).

  1. И без этого сахара раст более конкрунтоспособен чем Go, потому что в нем средство для программиста больше, код получается выразительнее и меньше, + проверки компилятора минимизирует ошибки.
  2. Никто не предлагает переписать Go-решение на Rust, вы внимательнее прочитайте пожалуйста, а то я питаюсь сравнивать возможностей языков, а вы про "переписать", я не говорил что Go не подходить для таких то задач, просто не называйте его более подходящим чем другие языки, это неправда.

Хм, вы уверены, что у вас есть опыт энтерпрайз-разработки? Вот, например, в энтерпрайзе очень любят SOAP, а это всегда кодогенерация, что на Java, что на C#…

А вы уверены, что прочитали мой коммент?
Речь о такого типа кодогенерацию:
https://github.com/clipperhouse/gen


В общем дело не только в кодогенерации, а в том что отсувствие дженериков, могут приводить к кодогенерацию в некоторых случаев, может приводить к дублированию, а ещё может приводить к interface{}...


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

Имея нормальной многпоточности, реализовать легкие потоки возможно, есть библиотеки.
К тому же, сервису необъязательно использовать горутин на каждом шагу, в большинстве случаев достаточно использовать на уровне фреймворка.


АДТ, паттерн-матчинг и «продвинутый вывод типов» — это фичи, прилагаемые, как правило, к «развитым и гибким системам типов», т.е. для языков, разрабатываемых для подхода «от абстракции». В Go система типов примитивна, и без изменения оной перечисленные вами фичи, мягко выражаясь, малоосмыслены.

И как примитивная система типов может быть удобнее?


Не уловил, откуда взялся такой вывод…

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


Я не уловил откуда у вас взялся обратное, пока ваши аргументы:


  • Собственно, «развесистая бизнес-логика» — отнюдь не вотчина Go. Там безраздельно царят Java и .NET, и никто их оттуда «вытеснять» и не собирается.

  • В Go система типов примитивна, и без изменения оной перечисленные вами фичи, мягко выражаясь, малоосмыслены.

  • Ну и про горутин конечно же, но почему то упорно игнорируете, всё остальное.
    Для вас, если в стабильной версии языка X, можно стартанут легкие потоки с go ..., и в языке Y с .and_then().and_then(), то автоматически можно игнорировать все удобство языка Y, логично!


Футуры работает в стейбл, найтли может понадобитья только для сахара.


Еще раз повторю, на всякий случай: «футуры» и «горутины» — разные вещи.

Разница в том, что этот сахар вы уже можете использовать, и мало что измениться в будущем, если вообще измениться.


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

Вы же используете библиотеки, так вот пару фич из найтли получше будет чем библиотеки


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

люди вон патчят JVM, что гораздо страшнее, и на продакшне.


Ну, как бы, сильно напоминает «ну и что, что я ковыряю в носу? Вот Паша вообще описался вчера». Ну, т.е. каким боком вообще патчи JVM относятся к обсуждению Go vs Rust?

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


Система документации ортогональна языковым фичам. Ну и, в принципе, не вижу проблемы сгенерить доку по Go-коду и «увидеть». Вот прямо не знаю, у вас какой-то запрет на видение документации, сгенеренной из Go-кода?

И без этого сахара раст более конкрунтоспособен чем Go


Конкурентноспособность — это объективная совокупность фич/признаков, позволяющая языку получить бОльшую распространенность относительно своих конкурентов. Сама по себе в отрыве от действительности не формализуется, т.е. все ваши «потому что» больше напоминают детский лепет. Конкурентноспособность может быть оценена только по результату в виде используемости языка. И вот тут мы видим, что ваше «Rust конкурентноспособней, чем Go» становится не более, чем вашей личной фантазией…

В общем дело не только в кодогенерации, а в том что отсувствие дженериков, могут приводить к кодогенерацию в некоторых случаев, может приводить к дублированию, а ещё может приводить к interface{}...


И? В чем конкретно проблема того же самого interface{}?

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


Ну да, в Rust можно накрутить сборщик мусора, реализовать горутины и… получить еще один Go? Зачем?

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


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

И как примитивная система типов может быть удобнее?


В тех случаях, когда примитивной системы типов достаточно, она удобнее сложной. Это не очевидно?

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


У нас с вами разные наблюдения. Я видел старты на Go, Java, C#, Kotlin'е, видел даже на Dart'е. Кто у нас из «свежих» на расте «стартанул»?

Для вас, если в стабильной версии языка X, можно стартанут легкие потоки с go ..., и в языке Y с .and_then().and_then(), то автоматически можно игнорировать все удобство языка Y, логично!


Еще раз уточню, на всякий случай:

go func() и ".and_then().and_then()" — это разные вещи. Существенно разные.
Еще раз повторю, на всякий случай: «футуры» и «горутины» — разные вещи.

И?
Я не утверждал обратное, они решают одни и те же задачи с разными способами.
По сути футуры можно использовать как горутин, пример: выше описанная библиотека, которая в nightly, внутри использует футуры.


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

Я позицирую его как замену в будущем, поэтому специально написал про nightly, и о том что с точки зрение удобства использование у Go хороше решение.
В общем зеленых потоков есть(библиотеки), удобного интерфейса как в го нет, надеюсь так понятнее.
Пропишите +1 за удобство использование в Go, и сравните дальше.


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

Это мягко говоря не ваше дело.


Ну, как бы, сильно напоминает «ну и что, что я ковыряю в носу? Вот Паша вообще описался вчера». Ну, т.е. каким боком вообще патчи JVM относятся к обсуждению Go vs Rust?

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


Ну да, в Rust можно накрутить сборщик мусора, реализовать горутины и… получить еще один Go? Зачем?

Библиотеки уже есть, вам не нужно накрутить сборщик мусора, для мусора можете и Go использовать.


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

Интересно, откуда такие выводы?


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


В тех случаях, когда примитивной системы типов достаточно, она удобнее сложной. Это не очевидно?

В JS тоже достаточная система типов, по вашей логике.
Ну и как вы определяете какого уровня системы типов достаточная?


И? В чем конкретно проблема того же самого interface{}?

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


У нас с вами разные наблюдения. Я видел старты на Go, Java, C#, Kotlin'е, видел даже на Dart'е. Кто у нас из «свежих» на расте «стартанул»?

Тогда ваше утверждение:


И есть Go, который проектировался с одной целью — бекенд. И на нем удобно пилить бэкенд. Удобнее, чем на любом из «универсальных» языков.

Неверная.


Еще раз уточню, на всякий случай:
go func() и ".and_then().and_then()" — это разные вещи. Существенно разные.

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

В общем зеленых потоков есть(библиотеки), удобного интерфейса как в го нет, надеюсь так понятнее.
Пропишите +1 за удобство использование в Go, и сравните дальше.


Ну вот и я о том же, что работа с green fibers в Go удобнее. И, видимо, это достаточная фишка для того, чтобы Go был популярен. Не я говорю, практика показывает.

Вы упускаете то, что в продакшне делают вещи похуже, поэтому такой пример привел,


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

когда вам реально необходимо что то из nightly, совершенно правильным будет изучить риски,


Совершенно правильным будет стараться не использовать nightly-фичи, не патчить JVM, не использовать грязные хаки до тех пор, пока это возможно. Ну, если вы, конечно, не Гугл, не Фейсбук, не Майкрософт, не (подставьте название вашей любимой миллиардной компании) — они себе и не такое позволить могут, у них есть ресурс и деньги. Вы себе поддерживать свои собственные грязные хаки позволить не можете — пупок развяжется.

и использовать nightly, если решение не сырая.


Собственно, оно на то и nightly, что решение еще сырое. Как только оно будет не сырое, оно в stable попадет. Вот тогда и используйте.

Интересно, откуда такие выводы?


Выводы, в основном, от того, что в проде этого решения еще никто не видел. Понимаете, практика применения крупными конторами — это бонус. Очень весомый бонус. Брать в прод что-то, по чему ни у кого нет экспертизы, и даже решение проблем загуглить негде — ну это такое себе (опять же, если вы не Гугл, Майкрософт, whatever)

Я уж не говорю о тулинге вокруг. Go таки успел обрасти «мясцом» в виде grpc-биндингов, swagger-генерацией и вот этими всеми вещами. В раст, пока еще, не подвезли.

Понимаете, я не говорю, что Раст плохой. Нет, он, вероятно, даже хороший. По крайней мере моя экспертиза в Rust'е (в виде чтения мануалов и пары хелловорлдов) не позволяет мне говорить, что Rust плох. У меня тупо нет на нем практики.

А вот то, что он, пока еще, не готов, говорить можно. Тем более, что вы сами это подтверждаете. Соответственно, спор, мягко выражаясь, бессмысленный. Да, в Go не все хорошо. И абстракции там текут (посмотрите хотя бы на слайсы), и дженериков временами не хватает (но редко, не настолько, насколько вам кажется), и обработка ошибок немного многословна. Но вся приколюха в том, что Go уже есть и работает, а Rust пока еще не готов. Да, собственно, и слабо верится, что он Go сможет вытеснить. Элементарно в нишу не пролезет. Rust больше в нишу c/cpp пытается вкорячиться, а Go туда и не претендует.

Ну и как вы определяете какого уровня системы типов достаточная?


Это же элементарно. Если ее достаточно для решения моей задачи, значит, она достаточная…

Насколько я понимаю, это общий интерфейс для всех типов(кроме скаляров кажетя?).


За что вы так не любите скаляры? Ровно так же пролазят в interface{}, как и все остальные.

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


Погодите-погодите. А каким образом вы передали неожиданный интерфейс?

Вы, видимо, не совсем улавливаете суть. Дженерик-типичных кейсов, собственно, два. Первый — это кейс коллекций и т.д., т.е. такие вещи, которые внутри с самим значением ничего не делают. Т.е. всяческие очереди, стеки и т.д. и т.п., которые сводятся к «положил что дали, вынул что есть». Второй — это кейс различных обработчиков и воркеров. Т.е. такой случай, когда вы что-то суете внутрь, с ним что-то там происходит, что-то считается, меняется и т.д., а на выходе вы что-то забираете.

Ну вот interface{} подходит для первого случая. И отлично подходит. Т.е. вы сунули в коллекцию что угодно, а потом оттуда забрали ровно то, что совали (самая фишка в том, что при приведении к interface{}, на самом деле, ничто ни к чему не приводится, т.е. каст почти бесплатный). В этом кейсе единственное, чего вам может действительно не хватать, это автовывода типа «на выходе». Но тут уже неудобство, простите, уровня синтаксического сахара.

А со вторым кейсом. Хм, вы, видимо, просто не привыкли к Go-шному толкованию интерфейсов. Если у вас внутри что-то происходит, значит, у вас есть ожидание того, что с переданным объектом можно что-то делать. Т.е. вы знаете, какими методами переданный объект должен обладать. А список методов, в общепринятом понимании — это интерфейс. Опишите его, не ленитесь. И тогда никаких проверок в рантайме и тем более бросания паник вам не понадобится.

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

Т.е. если есть ошибка, вы его увидите только в рантайме, и только если предусмотрели это заранее.


Нет, я просто опишу интерфейс. И в рантайме ничего не случится.

Как по мне, это всё равно лучше чем тащит зелёные потоки в ядро языка.


Это вы сетовали, что в ядре Go ОРМ нет?))

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


Это не в Go. В Go любой код конкуррентен by design.
Ну вот и я о том же, что работа с green fibers в Go удобнее. И, видимо, это достаточная фишка для того, чтобы Go был популярен. Не я говорю, практика показывает.

Популярнее != удобнее
Интересно, начали сравнивать возможностей языка, когда оказалось они у Go мягко говоря примитивна, то переключились на попуряность.
Напомю, что Go появилось раньше(стейбл), и маркетинг гугла сильнее.


Выводы, в основном, от того, что в проде этого решения еще никто не видел.

3к звезд на гитхабе вам ни о чем не говорить?


Я уж не говорю о тулинге вокруг. Go таки успел обрасти «мясцом» в виде grpc-биндингов, swagger-генерацией и вот этими всеми вещами. В раст, пока еще, не подвезли.

А зря, это адекватная критика, в отличие от.
Это пожалуй пока самое слабое место в rust.
Конечно, для swagger есть библиотеки, но нет его интеграция с конкретными фреймворками.


Ну вот interface{} подходит для первого случая. И отлично подходит. Т.е. вы сунули в коллекцию что угодно, а потом оттуда забрали ровно то, что совали (самая фишка в том, что при приведении к interface{}, на самом деле, ничто ни к чему не приводится, т.е. каст почти бесплатный). В этом кейсе единственное, чего вам может действительно не хватать, это автовывода типа «на выходе». Но тут уже неудобство, простите, уровня синтаксического сахара.

А если сунули в коллекцию разные типы?


А со вторым кейсом. Хм, вы, видимо, просто не привыкли к Go-шному толкованию интерфейсов. Если у вас внутри что-то происходит, значит, у вас есть ожидание того, что с переданным объектом можно что-то делать. Т.е. вы знаете, какими методами переданный объект должен обладать. А список методов, в общепринятом понимании — это интерфейс. Опишите его, не ленитесь. И тогда никаких проверок в рантайме и тем более бросания паник вам не понадобится.

Допустим.
Если есть интерфейс с методом add(a int), получается мы можеам использовать любой интерфейс с таким метом, верно?
Если так, это не может вызвать проблемы?


Нет, я просто опишу интерфейс. И в рантайме ничего не случится.

Я понимаю, что с интерфейсами всё можно решить, но его применение ограниченно, и вам всегда приходиться отталкиваться от этого.
Вот простой пример:


trait Foo {
    type Bar = u32 + f32;
    fn baz() -> Bar;
}

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

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


Это вы сетовали, что в ядре Go ОРМ нет?))

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


Это не в Go. В Go любой код конкуррентен by design.

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


Или вы о том, что на нем в любом месте легко можно писать горутин, что же, правильно, но на нем ещё можно в любом месте выстрелить себе в ногу(привет примитивная система типов).

Сначала это:

В общем зеленых потоков есть(библиотеки), удобного интерфейса как в го нет, надеюсь так понятнее.
Пропишите +1 за удобство использование в Go, и сравните дальше.


А теперь это:

Популярнее != удобнее


Вы уж определитесь…

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


Переключились в контексте «конкуррентноспособности Rust». Собственно, популярность — прямое следствие конкуррентноспособности. И с этим у Rust пока еще не очень. Я, в смысле, о том, что Rust просто еще не готов. Но, конечно, уже подает надежды )

Напомю, что Go появилось раньше(стейбл), и маркетинг гугла сильнее.


Rust — начало разработки 2006г. Официально представлен в 2010г.

Golang — начало разработки 2007г. Официально представлен в 2009г.

Не могу сказать, что прям офигеть какая разница в возрасте…

3к звезд на гитхабе вам ни о чем не говорить?


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

А если сунули в коллекцию разные типы?


В чем суть драмы? Ну сунули, и что?

Если есть интерфейс с методом add(a int), получается мы можеам использовать любой интерфейс с таким метом, верно?


Мы можем использовать любой объект, реализующий интерфейс с этим методом (к слову, конкретно метод `add` в интерфейсе нереализуем, видимо, должен быть `Add`). Собственно, что должен делать этот метод? Видимо, прибавлять целочисленное значение к объекту? Ну вот к структуре либо можно прибавлять, и тогда все норм, либо нельзя, и тогда не норм. Но если не норм, то и метода этого у нее нет, а значит, она «не влезет».

Если так, это не может вызвать проблемы?


Ну, как бы, не более, чем в любом другом языке с использованием интерфейсов…

type Bar = u32 + f32;


Это, видимо, union? Или ограничение на допустимые типы?

Ну, как бы, в обоих случаях забавно, но не сильно полезно.

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


Собственно, у вас так же.

Не совсем, это зависить от того как вы его применяете.


Честно говоря, совсем, как ни применяй. Любое обращение к io и т.д. — асинхрон. Так уж он устроен. Это не обязательно понимать, и не везде полезно, но «се ля ви». Ну и, собственно, писать на Go задачи, в которых конкуррентность не нужна — ну это «такое себе». Смысл?

но на нем ещё можно в любом месте выстрелить себе в ногу(привет примитивная система типов).


Сложная система типов никак не защищает от выстрелов в ногу. Честно-честно.

Понимаете, есть два полюса: «примитивность» vs «комплексность». Примитивные — понятны, комплексные — «типа, безопасны». Вы просто размениваете логические ошибки на примитивной системе типов на ошибки абстракции на комплексной. Всего то и делов.
Сложная система типов никак не защищает от выстрелов в ногу. Честно-честно.

Пример 1, реальный.
https://doc.rust-lang.org/nightly/std/fs/struct.File.html
(этот тип позволяет работать с файлами)
Сложная система типов в расте (а именно, деструкторы и правила владения) гарантирует, что файл обязательно будет закрыт. Мне вообще не нужно думать про какие-то там defer f.Close() — оно просто работает.
(Формально, конечно, можно добиться утечки, но это надо сделать явно.)
Пример 2 — зависимая типизация (Тут где-то в комментах deadf00d ходит, он хорошо умеет ее впаривать)). Она позволяет выразить на уровне типов сколь угодно сложные инварианты, в том числе:


  1. Сокет, в который записали нечетное количество байт.
  2. Тройка из векторов, где у первого и третьего одинаковый размер, а второй длиннее.
    Она позволяет вообще гарантировать отсутствие паник в рантайме и доказать корректность программы.

Таким образом, сложные системы мешают выстреливать себе в ногу — в общем-то, это и есть их главное предназначение.

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


Эталонно… В палату мер и весов, простите.

Давайте немного обсудим конкретно это ваше высказывание.

1. Про часть «в расте». Раст в этом плане не уникален. От слова «вообще». Конструкторы/деструкторы существуют практически во всех ООП-языках. А уж про RIA… Собственно, оно задолго до Rust придумано. Годах этак в 80-х прошлого века.

2. Конструкторы и деструкторы практически не имеют отношения к «сложной системе типов». RIA — это фича, основанная на «сроке жизни» объекта. Для того, чтобы это работало, необходимо быть уверенным, что деструктор будет вызван ровно в момент выхода переменной из области видимости. Т.е. это «стандарт де факто» в языках с ручным управлением памятью, в некоторых случаях неплохо работает в языках со счетчиком ссылок, вообще не работает в языках с GC. К системе типов отношения не имеет.

Она позволяет вообще гарантировать отсутствие паник в рантайме


Да-да, охотно верю…
Раст в этом плане не уникален

Естественно. Большая часть фич раста, и RAII в том числе, — баян. Даже довольно новаторские (например лайфтаймы) уже были в других языках. Но я и не говорил другого. Вы же сами предлагали сравнивать два конкретных языка, а не язык Х с объединением фич всех остальных язык.


  1. Действительно, деструкторы не относятся к система типов, они реализованы "сбоку" (по крайней мере в расте). Но лайфтаймы к системе типов относятся.

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


  1. Да-да, охотно верю…


Когда все инварианты выражены на уровне системы типов, паники уже не нужны.

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


Ну вы же взрослый человек, понимаете, вероятно, что всегда есть trade-off. Если что-то где-то «прибыло», то совершенно однозначно где-то что-то «убыло».

Вопрос же, как всегда, в стоимости разработки и поддержки. И ключевое понятие в бизнес-разработке — рентабельность при достижении удовлетворительного результата. Ручное управление памятью сложнее, а счетчит ссылок «течет» на первой же циклической конструкции.

В общем и целом, до тех пор, пока GC-язык обеспечивает производительность/стабильность в пределах спецификации/ТЗ, писать будут на GC-языках. Просто потому что дешевле. Се ля ви…

Когда все инварианты выражены на уровне системы типов, паники уже не нужны.


Конечно, не нужны. В идеальном мире.

Тут поинт, как мне кажется, в чем:

Система типов «рожается в муках» на этапе формализации/декомпозиции задачи. Т.е. непосредственно определение необходимых для реализуемой системы типов происходит на этапе проектирования. А значит, и ошибки (а все не идеальны), допущенные в типизации, смело можно относить к ошибкам проектирования системы.

Недостаток инструментов «красивой» системы типов компенсируется, как правило, вещами вроде паник и прочими «прелестями жизни». Но делается это на этапе имплементации, и ошибки имплементации остаются ошибками имплементации.

И вот тут мы и подходим к ключевому моменту «популярности Go относительно Rust»: ошибки проектирования, как правило, обходятся ощутимо дороже, чем ошибки имплементации (в части исправления).

Теперь добавим чуточку «контекста» к этому высказыванию: agile-методики стремительно набирают популярность в своем стремлении «закопать» классический «водопад».

А agile-методики и строятся-то поверх достаточно примитивной философии: «меньше проектируйте». Т.е. один большой процесс предварительного проектирования размазывается на серию краткосрочных итераций, которые подразумевают разбивку задачи на куски последовательных фич-релизов и быстрых рефакторингов.

И вот именно в этой среде Go стремительно «набирает очки» за простоту рефакторинга и возможность по-быстрому «выродить» случаи глобальных пересмотров архитектуры в атомарные фиксы.

Оттуда и «порог вхождения». Дело даже не в том, что в Go сильно меньше механик, ключевых слов или каких-то инфтрументов. Главная сложность программирования даже близко не в том, чтобы закодить какой-то алгоритм. Практически вся сложность сосредоточена в архитектуре и проектировании.

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

Отсюда все и растет. Rust дает сложные и качественные инструменты решения задач проектирования, которые, в случае умения в проектирование, сильно упростят дальнейшую поддержку и разработку. Go дает возможность размазать проектирование на итерации и «заткнуть дыры» в корявой архитектуре, если «нужно уже прямо сейчас».
Ну вы же взрослый человек

Я школьник)


Если что-то где-то «прибыло», то совершенно однозначно где-то что-то «убыло».

Прибыла необходимость (иногда) прописывать лайфтаймы, но в основном и без них всеп росто работает.


Ручное управление памятью сложнее
циклической конструкции

Опять же, по моему опыту (который, естественно, очень специфичный), подсчет ссылок нужен довольно редко (я использую его на пару с мьютексами), а ручное управление памятью — еще реже. В основном, мой код прекрасно ложится на деструкторы. И я не думаю, что добавление 100500 правил бизнес-логики приведет к циклическим ссылкак. Даже если приведет — можно конкретно для этих высокоуровненых объектов бизнес-логики включить GC (например, в Linux Kernel есть сборщик файловых дескрипторов). При этом большая часть проекта продолжит работать без него и не получит его минусов.
А по поводу его минусов — достаточно запустить CodeBlocks и CLion (TL;DR — на слабых компах, пока запустится clion или visual studio можно успеть установить CodeBlocks).

Я школьник)


А, ну тогда волноваться рано. Все еще пройдет).

Прибыла необходимость (иногда) прописывать лайфтаймы, но в основном и без них всеп росто работает.


Прибыла необходимость проектирования системы типов. В Go тип — это просто набор полей, не несущий дополнительной семантической нагрузки, его можно «набросать» по-быстрому. Там даже и классов-то нет. Структуры отдельно, методы отдельно. Структура передается в метод в виде ресивера (читай, подставляется автоматом первым параметром в вызов функции).

В Rust'е поведение типа — часть его спецификации, указывающая так же, что с ним можно делать, что делать не стоит, и как с этим жить. Тип — ключевой механизм Rust'а. Это просто так не «набросаешь».

А по поводу его минусов — достаточно запустить CodeBlocks и CLion (TL;DR — на слабых компах, пока запустится clion или visual studio можно успеть установить CodeBlocks).


Ну это уже субьективщина. Тот же visual studio написан ни разу не на GC-языке, вполне себе C++. И я бы не сказал, что IDEA тормозит больше.

Медленный код можно писать на любом языке. Вручите мне Rust, и я готов поспорить, что смогу родить достаточно тормозной код на нем, чтобы вы более не считали тот же Python тормозным).
UFO just landed and posted this here
Значит, вы необходимость продумать задачу перенесли на сильно более поздний этап.


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

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

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

И разница тут не в «монументально продуманный продукт» против «фигак-фигак и в продакшен», потому что «оптимистичный подход» совершенно не страхует от говнокода в проде (есть еще сроки и «меняющиеся бизнес-требования»). Разница тупо в приоритетах и распределении планирования. Классика: «водопад» против «гибких методик». Т.е. вам нужен либо большой и консистентный продукт сразу без промежуточных итераций, либо минимально-работающая вещь с итеративными изменениями в функционале. В первом случае «водопад» и «мощная система типов», во втором — «аджайл» и «примитивные инструменты».
UFO just landed and posted this here
Неа, как раз наоборот. И проектирую я с ошибками, и пишу с ошибками. Просто цена ошибки оказывается, вопреки стереотипам про тяжеловесность типов, маленькой. Если я понял, что ошибся, то я сажусь и меняю типы, рефакторю, и делаю это без страха за то, что уже имеющийся код сломается. Рефакторить без страха — это вообще очень круто.


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

И к слову о рефакторинге без страха — когда вы пишете тесты?


Пишу, когда они имеют смысл.
UFO just landed and posted this here
Впрочем, плюсы — это не про рефакторинг без страха


Ну вот, понимаете же, что не это главное, раз на плюсах пишете…

ADT и паттерн-матчинг, GADTs, тайпклассы (и как следствие дженерики параметрический полиморфизм, ага), DataKinds (тут сначала было долгое описание, что это такое, но потом я решил, что сослаться на конкретную фичу может быть проще), семейства типов, фундепы и вот это вот всё.


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

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


Кхм. Ну, собственно, у нас есть два сценария.

Первый — типичная коллекция, в которой нам тупо пофиг, что в нее сунули, т.к. с самими данными мы не работаем, просто сортируем, перекладываем и т.д. и т.п. В этом случае, собственно, нам более-менее достаточно иметь гошный interface{}, и при рефакторинге точно так же ничего не надо будет трогать.

Второй, условно, воркеры/обработчики, в которых мы что-то делаем с входящими данными, и, соответственно, имеем к ним некоторые требования. Здесь, ровно так же, как и с гошными непустыми интерфейсами, трогать при рефакторинге будет надо, и ничего вы с этим не поделаете…

Ну и про дженерики — ну, завезут скоро, чо придираетесь-то?
UFO just landed and posted this here
Пишу из-за производительности, имеющихся библиотек или имеющихся проектов. Во всех трёх случаях Go не подходит по очевидным причинам.


Очевидно, все три случая совершенно законно считать субъективными очевидными причинами?

Мне бы всё это и в плюсах не помешало. Императивность или функциональность не настолько сильно связана с мощностью системы типов.


Ну, собственно, в C++ только хаскеля внутри и не хватает для полного счастья, ага… Чтобы уж точно гарантировать, что никто не осилил спецификацию языка целиком…

Ага, а в С при рефакторинге void* трогать не надо, во там безопасно и удобно всё!


В тупой коллекции — не надо. А зачем, собственно?

Можно будет написать (дальше ваш код)


Свят-свят-свят. Не можно будет, слава Б-гу. Зачем превращать язык в викторину «Угадайте, что делает этот код (из раздела Вопросы для Senior-разработчиков)».
UFO just landed and posted this here
Нет, они объективные. Субъективно только конкретное множество имеющихся проектов, необходимых библиотек и всего такого, и в рамках этого же аргумента кто-то может выбрать Go вместо плюсов или хаскеля. Или, не знаю, PHP.


У вас очень странные представления об объективности и субъективности. Понимаете, объективная точка зрения — это такая штука, которая не зависит от субъекта, ее выражающего. И вы в качестве объективного аргумента представляете «имеющиеся (очевидно, у вас) проекты». Вы же понимаете, что у меня, например, проекты несколько другие, и сама идея перевести их на C++ может вызвать искреннее удивление у всех, кто в них задействован? В общем, считать аргумент «лично у меня такие проекты, для которых С++ лучше» — хм… странно как-то.

Примерно такое же отношение может вызывать и сравнение абстрактной производительности языков в вакууме, заодно с лобовым сравнением количества библиотек в целом без учета их применимости на задачах…

Ну и, думается мне, на Go числодробилки со схожей с плюсами производительностью не зря не пишут.


Правильно, потому что числодробилкам нужно ручное управление памятью и минимальный оверхед. Инструмент же под задачу выбирается, а не задача подгоняется под любимый инструмент. На С++, как правило, не пишут веб-сервисы, например (за исключением очень редких, зачастую эзотерических случаев). Да-да, есть и исключения, никто не отрицает. Есть же, в конце концов, операционная система на JS, это же не делает ее хорошей идеей…

А чего там, ядро хаскеля — то ли 70, то ли 100 страниц. Капля в море по сравнению с имеющейся спекой.


Вот это и плохо, что +-100 страниц на фоне общей комплексности языка не воспринимаются как что-то значимое… И после этого вы будете утверждать, что элементов «оверинжиниринга» и «перегруженности» в С++ нет?

Чтобы в типе коллекции явно торчал тип хранящихся там элементов, чтобы я точно поменял все связанные с ней места.


В самом типе коллекции-то он вам зачем? Ну, собственно, если уж он вам так нужен, welcome! В Go ровно две коллекции — слайс и мапа, и там вы тип вполне знаете…

UFO just landed and posted this here
Я ровно про это и написал: отсутствие необходимых библиотек или имеющаяся кодовая база — это объективный факт. Просто для моих проектов это C++, а для ваших — Go.


В этом и суть субъективизма: то, что для одного человека истинно, для другого неприменимо. «На улице -20» — объективный факт. «Я в теплой куртке, мне тепло» — субъективное наблюдение. «Вы вышли на улицу в рубашке и шортах» — субъективно вам холодно.

Ну, т.е., чувствуете разницу? Если что-то лушче подходит для вашей ситуации — это субъективный факт.

Смотря насколько ручное.


Хорошо так ручное, прямо сильно ручное, я бы сказал.

Чтобы знать, что его можно сравнивать, хешировать или чего там ещё делать.


Чтобы его можно было сравнивать, хешировать или еще что-то там делать, это таки «сценарий #2», при этом оно вполне решается интерфейсами, или компараторами и прочим, прикрученным сбоку (вас же не смущает такой подход, вы же приветствуете implicit?)

чем если оно там в рантайме через interface резолвится.


Ну, собственно, не совсем так оно происходит…
UFO just landed and posted this here
«Для разных людей одновременно могут быть наиболее комфортными разные виды одежды.» — моё утверждение.


Изначально утверждение было другое.

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


Я ровно про это и написал: отсутствие необходимых библиотек или имеющаяся кодовая база — это объективный факт. Просто для моих проектов это C++, а для ваших — Go.


Понимаете, объективный факт — вещь, не зависящая от субъекта, который его декларирует. «Имеющаяся у вас кодовая база» и «для ваших проектов» в самой своей формулировке подразумевает субъективность мнения.

Субъективность не несет в себе отрицательного эмоционального окраса, и она не является чем-то заведомо некорректным. Субъективный факт — это факт, верный с точки зрения определенного человека (субъекта). Т.е. то, что для вас и в вашей ситуации лучше — это лучше именно субъективно, в том и суть явления.

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


Ну, т.е., «в принципе задачу оно решает»…

Где это я его приветствую?


Тайпклассов, вроде, вам не хватало же? Собственно, оно позиционируется как одно из мегапреимуществ…
UFO just landed and posted this here
Эм, нет, факт обладания объективен. То, что у меня кодовая база на C++, не зависит от того, говорите обо мне вы или же я сам о себе говорю.


«отсутствие необходимых библиотек или имеющаяся кодовая база — это объективный факт» — вот это неверный постулат. Будь там «отсутствие необходимых для моих проектов библиотек или имеющаяся у меня кодовая база», вопросов бы не было, достаточно объективно, хоть и неприменимо за пределами условий, в которых оказались вы.

В исходном же посыле «производительность» достаточно абстрактна, не для всех задач C++ будет производительней Go при разумных трудозатратах.

Ну а «уже имеющиеся проекты, которые никто не будет переписывать» вообще ни насколько объективно не могут свидетельствовать о корректности выбора.

coerceTransitive : (Coercible a b, Coercible b c) => a -> c


Я правильно понимаю, что у вас есть метод, который принимает два аргумента, которые на входе могут быть либо Double, либо String, и на выход отдает String?

Ну, собственно func coerceTransitive(a, b string.Stringer) String подойдет?
UFO just landed and posted this here
Этот метод для любых трёх типов a, b, c таких, что a можно преобразовать в b, а b можно преобразовать в c, принимает a и возвращает c (делая преобразование через b, кстати, и для того, чтобы это доказать, вам достаточно посмотреть на сигнатуру функции, но это так, мысли вслух).


Если честно, строго субъективно, даже звучит жутковато. Не совсем понимаю, зачем мне может это понадобиться, тем более в процедурной парадигме.
UFO just landed and posted this here
Кстати, почему-то для разговоров о том, как тайпклассы заменить интерфейсами, суть задач и их применимость узнавать не потребовалось :)


Потому что суть задач и применимость интерфейсов очевидна?)) При том, что тайпклассы, вроде как, имеют частично схожее назначение «плюс что-то еще такое, что очень нужно, но чего нет в интерфейсах». Вот и интересно, что же это такое.
UFO just landed and posted this here
MonadReader m a, ну или результат умножения двух типов в какой-нибудь линейной алгебре, Mult lhs1 lhs2 rhs какой-нибудь. Или класс, ставящий в соответствие контейнеру его элемент. Или тот же Coercible, просто без всяких функций.


Ну т.е. вы опять сетуете, что Go не функциональный язык?)

Простите, но после вашего комментария на ум пришла вот эта паста:


фп это просто зонтичное название для "фич придуманных после 75-го года"
если язык не ФЯ, это значит, что его автор — сумашедший старый дед
плохая какая-то паста, негодная. Захожу, значится, в википедию, а мне там говорят, что ФП появилось раньше 75-го. Лисп — 58й.
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
В коде на хаскеле я не пишу юнит-тесты. Минимальная гранулярность — на уровне одной-двух функций, экспортируемых из модуля, и это скорее проверка соответствия некоторым частным случаям спеки. И сразу после рефакторинга, как только тайпчекер перестал ругаться, эти тесты снова зелёные! И, естественно, ничего не падает, не зависает, не ломается.

А как в Хаскеле с производительностью? Я тут вычитал, что быстрая сортировка в Erlang в следствии неизменяемости данных выжрет n^2 дополнительной памяти чем загубит идею быстрой сортировки.

Для иммутабельного списка в хаскеле используется сортировка слиянием. В совокупности с ленивой моделью исполнения эта сортировка не даёт лишнего оверхеда по памяти и работает достаточно быстро.


Сортировка вектора же в хаскеле использует инкапсулированную мутабельность и так же не даёт дополнительного оверхеда по памяти.

UFO just landed and posted this here
При формализации рождается, собственно, выражение задачи в терминах типов в рамках имеющейся системы типов.

Более мощные системы типов позволяют больше всего выразить в этих самых типах.


Так точно.

А более примитивные системы типов позволяют написать работающий прототип, пока пользователи более мощных «выражают задачу в терминах типов в рамках имеющейся системы типов». А потом прогнать то, что получилось, профилировщиком, и переписать «узкие места».
UFO just landed and posted this here
Связка tokio+futures-await так же удобны в использование как горутины.

Прям вот точно также удобны?
Удобство goroutine в том, что их просто стартануть (go перед любой функцией/методом) и то, что есть каналы для их взаимодействия. В Rust тоже есть каналы?

Прошу пояснить для незнающих:
1. использование go перед любой функцией принципиально чем-то отличается от например c# Task.Run(()=>Foo()), кроме более короткого способа записи?
2. Каналы можно реализовать на другом языке (на том же c#) или это фича именно GO?
  1. Не знаю что там в C#, говорю как удобно в Go (опять же начались примеры в стиле "а вот в X языке точно такая же фича"). Ну и даже краткость записи — уже ведь лучше, да? Ведь тут все хейтят Go с if err != nil. В вашем примере мне не понятно, что такое Task, это ваш класс или со стандартной библиотеки?


  2. Можно сделать все что угодно, только вот в Go это часть стандартной библиотеки (как и пакеты context и sync для управления goroutine).



В сумме все это очень упрощает написания конкурентного кода.

Ну это зря вы меня обвиняете в хейтинге. Как говорится «всё познается в сравнении», я просто знаю немного c#.

Task — это класс из стандартной библиотеки.
Короткая запись конечно лучше длинной.

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

Я думаю, в современном мире, редко пишут на одном языке. Я иногда пишу и на c#, приходилось и на java и kotlin. Думаю и GO мне пригодится, какую-нибудь утилиту для линукс написать, хорошо что получается единственный файл со всеми зависимости.
Как говорится «всё познается в сравнении», я просто знаю немного c#.


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

Я, собственно, тоже немного знаю C#, сейчас вполне себе зарабатываю на жизнь разработкой на Go. Ну вот, собственно, если мне понадобится поворочать логику, поработать с SOAP-интерфейсами, поиграть со сложными моделями данных, мне в голову не придет использовать для этого Go (правда, и C# я вряд ли для этого буду использовать, скорее склонюсь в сторону Java, но это уже моя специфика — языки сравнимы). При этом для кеширующих вещей, маршрутизации, сетевых кешей, лоад-балансировщиков ну или просто «показать API наружу» мне в голову не придет использовать C# — Go тут просто удобней.

Если каналы такие удобные, то надо просто их заюзать в своем языке


Каналы — это примитив синхронизации, там нет магии. И вы не захотите использовать каналы в C#, они там просто не нужны (потому что Task.Run() — это сильно не совсем то же самое, что и go func()).

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


Потому что не любая лучшая практика из языка X гарантированно переносится в язык Y без потери смысла?
При этом для кеширующих вещей, маршрутизации, сетевых кешей, лоад-балансировщиков ну или просто «показать API наружу» мне в голову не придет использовать C# — Go тут просто удобней.

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

— Готовое решение либо есть, либо нету. Если есть — используют, если нет — пишут.

— В подавляющем числе случаев реализацию пишут на «managed» языках, т.е. языках с автоматическим управлением памятью и сопутствующими плюшками. Т.е. выбирают то, на чем проще писать, и на чем пишут наиболее доступные специалисты. Лидируют, что очевидно, Java и C#. Со своей спецификой имеют нишу Python и с десяток других языков.

— c/cpp используются только тогда, когда на чем то более «простом» просто не получается. Элементарно потому что C/CPP — дорого.

Вот Go вклинился где-то между Java/С#/Node.js и c/cpp. А на расте… на расте, простите, не пишут. Не нашел он еще свою нишу.
Ну и даже краткость записи — уже ведь лучше, да?

Похоже нет никакой краткости. «go foo()» — выглядит кратко, но по факту этот код бесполезен. Например если нам надо отправить параллельно 2 Http-запроса, то код становится сильно больше.

У меня какое-то двоякое чувство, на простых примерах всё красиво, асинхронный код выглядит как синхронный, даже async/await не нужны. Но чуть в сторону и лапша какая-то получается…

go foo()» — выглядит кратко, но по факту этот код бесполезен


Ну, как бы, сильно зависит от того, что внутри foo(). Я вот, например, не умею определять полезность функции по ее названию…

Например если нам надо отправить параллельно 2 Http-запроса, то код становится сильно больше.


Ну да.

go query1()
go query2()


это ровно в 2 раза длиннее, чем

go query1()


В чем суть проблемы-то?

У меня какое-то двоякое чувство, на простых примерах всё красиво, асинхронный код выглядит как синхронный, даже async/await не нужны. Но чуть в сторону и лапша какая-то получается…


Рискну предположить, что дело в недостатке опыта на Go?

Для меня лично, например, код на C++ или Python выглядит местами неочевидной трудновоспринимаемой кашей, невзирая на то, что в общих чертах с обоими языками я знаком и дело с ними имел (я уже молчу по воспринимаемость языков, на которых я не писал… тот же perl выглядит для меня как трактат о смысле жизни и черной магии, записанный с использованием микенской письменности для среднестатистического третьеклассника). При этом человек, постоянно практикующий Python будет смотреть на меня «с искренним изумлением», для него-то все очевидно и понятно…
<В чем суть проблемы-то?blockquote>
В том что результат надо обработать. Ваш код красив для заманухи, но по факту он бесполезен, потому что в нем нет обработки результатов. Задача: Надо собрать данные по разным источникам (например с двух http сервисов) и вернуть одним ответом пользователю. С await сильно короче получается. Вы приведите полный код, тогда его можно будет обсудить
Предлагаю даже проще поступить, вот этот код нужно переписать чтобы получение данных выполнялось параллельно
data1:=getData1()
data2:=getData2()
result:=foo(data1, data2)
ch1, ch2 := make(chan string), make(chan string)
go getData1(ch1)
go getData2(ch2)
result = foo(<-ch1, <-ch2)
Не надо лукавить! А где запись в канал? У этих функций не было такого параметра, это могла быть какая нибудь http.get(), которую нельзя было доработать. В общем понятно, с вами конструктивного обсуждения не получится, фанатизм так и прёт.
Не надо лукавить! А где запись в канал?

В функциях getData1 и getData2, но даже вы не показали что там внутри.


У этих функций не было такого параметра, это могла быть какая нибудь http.get(), которую нельзя было доработать.

Раз уж вы еще больше выдвигаете требований, можно так:


ch1, ch2 := make(chan string), make(chan string)
go func() { ch1 <- getData1() }()
go func() { ch2 <- getData2() }()
result = foo(<-ch1, <-ch2)

Но сути это не меняет.

Все таки это не просто go перед функцией написать. В реальном коде ещё должна быть обработка ошибок, итого в одну строку не получится записать.
Я умываю руки, продолжайте и дальше бороться со своими мельницами. Удачи.
Все таки это не просто go перед функцией написать.


Собственно это именно go перед функцией написать. Перед любой, собственно. В этом и прелесть горутин.

Но горутины — это про планировщик. А за получением возвращаемых данных (в контексте многопоточного исполнения читай «синхронизацией состояния») — это к каналам, они в Go примитивом синхронизации служат. Есть еще sync.Mutex- если хочется «ручками» и «побыстрей». Но прелесть каналов в том, что они гарантировано безопасны. Т.е. вы можете обернуть любую функцию замыканием с каналом, и совершенно безопасно юзать в многопоточном контексте — в этом и прелесть.

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


В одну — не получится. В конце концов, у вас два параллельных запроса — это минимум 2 строки…
Не надо лукавить! А где запись в канал? У этих функций не было такого параметра, это могла быть какая нибудь http.get(), которую нельзя было доработать. В общем понятно, с вами конструктивного обсуждения не получится, фанатизм так и прёт.


Вы же нам сейчас покажете, как вот это

data1:=getData1()
data2:=getData2()
result:=foo(data1, data2)


можно описать более коротко и емко с использованием await?

Видимо, учитывая ваше «это могла быть какая нибудь http.get()», вы предполагаете что-то вроде такого?

//псевдокод
data1 = await http.get()
data2 = await http.get()
result = foo(data1, data2)


Ну так я вас опечалю, оно у вас не параллельно, а вполне себе последовательно получилось...
можно описать более коротко и емко с использованием await?


В идеале — так:
//псевдокод
const [ data1, data2 ] = await [ http.get(), http.get() ];
result = foo(data1, data2)
напомню что разговор был не про «короче», а про «просто». Я сказал, что очень просто запустить горутину (к этому прицепились сразу) и управлять ее, и взаимодействовать (каналы).
Я чуть ниже описал суть. http.get() создает инстанс процесса, которым мы можем управлять по своему желанию — остановить, узнать прогресс выполнения и так далее (зависимо от реализации). await означает лишь «дождаться окончания этого процесса» (это не процесс в понимании ОС, если что). К примеру в каком-то языке, это может быть инстанс класса «HttpRequest», который реализует интерфейс «IPromise».

Этим легко управлять, эту штуку легко запустить последовательно и/или паралельно, она понятная, результат её работы легко записывается в переменную…

Дело не в «короче», дело в «просто», конечно.
Это как-то делает то, что в Go становится сложно? Еще раз, разговоров о том, что в Go очень просто и удобно писать конкурентный код (горутины, каналы, контекст).
Я просто обратил внимание, что на await довольно легко пишутся паралельные запросы. Да ещё и с обработкой ошибок:
try {
  const [ data1, data2 ] = await [ http.get(), http.get() ];
  result = foo(data1, data2);
} catch (error) {
  handle(error);
}
И какого рода обработка ошибок будет? Как вы определите какая функция вернула ошибку и что потом сделаете?
Пока я не вижу преимуществ такой обработки, как часто бывает в языках с исключениями — один большой try-catch на все случаи (а часто просто с логированием).

Я вот в таких ситуациях делаю обертки с обработкой ошибок (в нем, например, retry, зависит от задачи).
Как вы определите какая функция вернулся и что потом сделаете?

Зависимо от типа ошибки. В Гоу вообще возвращаются, по сути, строковые ошибки, ничего кроме логирования с ними не сделать. Вот os.Open. То ли файла нету, то ли прав доступа, то ли я аргументы неправильно указал — всё просто нетипированная строка (тем более, если я не ошибаюсь, даже все возможные ошибки — не задокументированы). Тем более в гоу принято часто поднимать ошибки вверх, добавляя ещё и всякую отсебятину, что ещё больше усложняет понимание, какие ошибки бывают и, как результат — делают бессмысленной любую работу с ними кроме логирования.

Вы так и не ответили на вопрос, написав отсебятины про ошибки в Go (особенно «В Гоу вообще возвращаются, по сути, строковые ошибки, ничего кроме логирования с ними не сделать.»). Про ошибки в Go мы можем поговорить позже.
И все же — как вы определите какой вызов функции (из двух вызовов http.get()) вернул ошибку и как обработаете?
Если в этом есть необходимость — их обработку можно разделить и, конечно, можно сделать обертку. Если есть необходимость.

Так еще раз — вот в вашем примере, что будет делать handle(error)? Вы же пример привели с обработкой ошибок, вот мне и интересно, что там за обработка и есть ли преимущество у данной обработки. Если там будет просто логирование или возврат, то какое тут преимущество то с такой обработкой.

Обработка может быть любая, странные вопросы вы задаете. Если нужно обрабатывать каждый запрос отдельно — можно обрабатывать каждый запрос отдельно. А можно каждый отдельно плюс ещё и все вместе. Ну вот, к примеру, вы хотите попробовать сделать запрос три раза и если не получится — прокинуть наверх и пусть там залогируется. Делаете так:

try {
  const [ data1, data2 ] = await [
    httpGet3Times(), 
    httpGet3Times()
  ];
  result = foo(data1, data2);
} catch (error) {
  log(error);
}


А внутри функции httpGet3Times — пробуете делать запрос и если ломается — повторяете. Ну как-то так (псевдокод)

async httpGet3Times () {
  let tries = 0;
  while (true) {
    try {
      return await httpGet();
    } catch (NotFound | NotAllowed error) {
      throw error; // нету смысла повторять - сразу падаем
    } catch (OtherError error) {
      // можно повторить, но не более трех раз
      if (++tries >= 3) throw error;
    }
}


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


Нормальные он вопросы задает, важные. Как раз такие, которые и решают разработчики языка. Т.е. именно те вопросы, которые не позволяют ни в один язык ввести ровно то, что вы «набросали».

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

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

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


В случае, когда вы в свой await суете два вызова httpGet3Times(), и потом «прокидываете наверх, чтобы залогировать», как вы определяете, какой из вызовов кинул исключение?

async httpGet3Times ()


Вот тут собака зарыта. В подавляющем числе языков вы самому методу говорите, что он асинхронный. Т.е. любой вызов асинхронного метода производится параллельно, и вы можете его либо ждать (await), либо не ждать. Любой вызов не-асинхронного метода будет произведен последовательно, хоть упишитесь своими await'ами (с ними просто не скомпилируется).

Отсюда возникают ситуации, когда вы пилите два одинаковых метода, на одном из которых стоит пометка «асинхронный», а на втором — нет. Либо пишете async-врапперы над синхронными методами. И все это для того, чтобы иметь возможность использовать один и тот же функционал в однопоточной и многопоточной среде. А это код, достаточно много кода. Который еще и тестировать надо, и править в 2-х местах.

В Go синхронность/асинхронность вызова определяется в точке вызова. Это, конечно, накладывает свои ограничения, но тут уже trade-off.

Т.е. либо вы по дефолту пишете код с оглядкой на асинхрон, либо пишете 2 имплементации. Выбирайте, что вам лично ближе: включить голову, чтобы написать правильно один раз, или сделать проще и переписать, если вдруг «прижало».

Суть в том, что вы можете обрабатывать ошибки как хотите, потому мне не понятен ваш вопрос


В том и проблема, что не можете. `const [ data1, data2 ] = await [` не дает. Маскирует источник исключения, понимаете ли. Т.е. await [] предполагает отказ от корректной обработки исключений. Если же корректная обработка исключений важнее, вы отказываетесь от параллельности запросов…

Но это конкретно в вашем предложении «в одну строку». И именно из-за этого так и не делают. Ни в Go, ни в любом другом языке.
Тем более в гоу принято


Честно говоря, у вас достаточно странные понятия о том, что «в гоу принято».
Тем более в гоу принято часто поднимать ошибки вверх, добавляя ещё и всякую отсебятину
Честно говоря, у вас достаточно странные понятия о том, что «в гоу принято».
Раз вам это показалось странным, значит в Гоу приняты странные вещи
По прежнему не понятна суть претензии.

На всякий случай исходная фраза, чтобы не потерять нить дискуссии:

принято часто поднимать ошибки вверх, добавляя ещё и всякую отсебятину


1. Собственно перед нами библиотечный код, который должен в случае ошибки возвращать ошибку (и это не только в Go так… я бы даже сказал, что это нормально для любого языка, не?).
2. В Go принят подход «ошибка-это значение». Оно для вас, вероятно, не так привычно, и в принципе встречается реже, чем механизм исключений, но уж претензия на тему «добавляя всякую отсебятину» на фоне классических стектрейсов выглядит достаточно забавно (как бы, исключения на автомате пишут отсебятину на каждом уровне вложенности).
3. Примеры забавные… вы, видимо, не потрудились распарсить код:

— первый: констрактит ошибку уровня самой библиотеки и возвращает ее (не дописывает что-то к сущствующей, а именно возвращает новую), т.е. «низачот»;
— второй: констрактит новую ошибку уровня библиотеки и возвращает ее (анализирует класс ошибки), т.е. опять «низачот»;
— третий: парсит, если при парсинге возникла ошибка, возвращает ошибку как есть, ничего не дописывая… блин, «опять двойка».
По прежнему не понятна суть претензии.
Суть претензии — в динамичности. В гоу, по сути, ошибки — это просто строка, которая никак статически не типизируется, я не могу увидеть список возможных ошибок и я не могу адекватно обработать определенную ошибку.

И это было бы менее страшно, если бы это были какие-то строки-константы, вроде «FILE_NOT_FOUND_ERROR», но в гоу такие ошибки ловятся библиотеками, туда добавляется отсебятина и возвращается новая ошибка, вроде (это просто пример) «file read failed %path% because of FILE_NOT_FOUND_ERROR»

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

И именно в этом плане я говорил, что с ошибками сложно работать. То есть вы никак не можете работать с этой ошибкой, кроме как залогировать её или знать факт, что какая-то ошибка совершилась, но вы не знаете, какая.

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

Это чисто ваша фантазия, в Go ошибка это интерфейс, потому сама переменная — интерфейсная переменная, которая хранит тип ошибки.
Я соглашусь, сделано не идеально (ну чисто потому что позволяет делать многое с ошибками), но тут уже вы сами решаете, облегчить ли самим себе жизнь и использовать типизированые ошибки и, например, очень удобный пакет github.com/pkg/errors, или использовать throw new Exception("...") errors.New("...")

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

И это было бы менее страшно, если бы это были какие-то строки-константы, вроде «FILE_NOT_FOUND_ERROR», но в гоу такие ошибки ловятся библиотеками, туда добавляется отсебятина и возвращается новая ошибка, вроде (это просто пример) «file read failed %path% because of FILE_NOT_FOUND_ERROR»


Есть такой поинт в Go. error — это интерфейс. И это, с одной стороны, несколько непривычно и даже, может быть, спорно, с точки зрения привычки к Java, например, где все исключения типизированы, имеется даже отдельная иерархия исключений и эти исключения даже наследуются… Мощный инструмент, бесспорно! С одной оговоркой. По совместительству этот самый мощный инструмент — главная головная боль Java и причина того, что в ней до сих пор присутствует некоторая «попа» с дженериками… Ну вот Go-шный подход к обработке ошибок — он не мощный. Он гибкий…

Собственно, стоит прийти к пониманию, что есть ваш код, и есть сторонние библиотеки, которые вы «инклюдите» в проект, и которыми вы не владеете. Вот в обработке ошибок сторонних библиотек есть такая позиция: «ошибка либо есть, либо ее нет». Сторонняя библиотека — это черный ящик. Если возникла ошибка — вы один хрен не сможете как-то применить информацию о содержании этой ошибки (да, за исключением некоторых пограничных случаев, от которых таки бывает больно, но я лично предпочту простоту в 99% случаев ценой боли в оставшемся 1%, чем буду испытывать дискомфорт в 100% случаев).

А внутри вашего кода вы вполне можете ошибки типизировать, валидировать, встраивать одну в другую, приводить одну к другой, и вообще делать все, что вам заблагорассудится с сохранением возможности работать с вашим кодом стандартным механизмом ошибок до тех пор, пока каждая ваша ошибка имеет метод Error(), возвращающий текстовое описание ошибки. Собственно, реализаций типизированных, иерархических, модульно-распределенных и т.д. и т.п. ошибок для Go несколько больше… сотни)))

Собственно, «все в ваших руках», и это скорее хорошо, чем плохо. Признайтесь уже, наконец, что в 99% случаев в той же Java при использовании 3rd-party библиотек на тип исключения не смотрят — выдачу считают невалидной просто по факту возникновения исключения.

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


Честно говоря, и эта «боль» растет ровно оттуда же, откуда ностальгия по иерархии исключений. Аннотации — это, бесспорно, хорошо, и даже Java-реализация, например, смотрится вполне неплохо. Только слегка избыточно, например. Поинт, опять же — делаем то, что просто (собственно, строка отдается в рантайм как есть). И если много народу просит переделать — переделываем. Норм подход, собственно.
Признайтесь уже, наконец, что в 99% случаев в той же Java при использовании 3rd-party библиотек на тип исключения не смотрят — выдачу считают невалидной просто по факту возникновения исключения.
Вообще не спорю, что не смотрят. В большинстве случаев просто логируют ошибку, возвращают пользователю какую-то инфу об этом и дают попробовать сделать как-то иначе. В таких случаях удобно сделать именно так, как я говорил:

try {
  const [ data1, data2 ] = await [ a(), b() ];
  result = foo(data1, data2);
} catch (error) {
  log(error);
}


Для 99% случаев этого кода на 100% достаточно, вы ведь сами настаиваете, что 99% — самые важные. Вот только хороший язык дает хорошие инструменты для остальных 1% случаев.

Норм подход, собственно.
Ну пожалуйста, давайте без стокгольмского синдрома. Прошу вас. Я понимаю холивар «ошибки в гоу против исключений». Понимаю, почему вам нравится подход к многопоточности в Гоу. Но флаги — это без сомнений отвратительная фича со всех сторон без единого преимущества. Она неудобна ни для пользователей библиотек, ни для их создателей. Она отвратительная с эстетической точки зрения. Она отвратительная с точки зрения поддержки и рефакторинга. У неё ни одной хорошей черты. Нету никакой избыточности в написании

@json("text")
Text string;
// вместо
Text string `json:"text"`


Никакой. Одни плюсы.

просто (собственно, строка отдается в рантайм как есть)
Нет, в этом НИЧЕГО простого. Нету никакого оправдания для того, чтобы делать что-то в таком языке динамически, если это что-то можно сделать статически. Знаете, с остальными обсуждениями сегодня я не был категоричен — просто рассказывал вам преимущества другой стороны, но флаги — это однозначный провал по всем пунктам, признайте это как минимум для себя. Ими пользоваться неудобно если вы хотите сделать что-то простое и ещё более неудобно, если что-то сложное.
Вот только хороший язык дает хорошие инструменты для остальных 1% случаев.


Поправочка: старается по возможности дать.

Есть один язык, ставящий себе целью дать хорошие инструменты для 100% случаев. C++ называется. Суровая тетка статистика говорит, что широкое применение он имеет там и только там, где без него ну вообще прямо никак. Есть некоторая сильно ненулевая вероятность, что эта статистика кореллирует с тем фактом, что покрытие кейсов использования инструмента находится в обратной зависимости с его удобством. Вспомните швейцарский нож. Да, в нем есть штопор, но я лично предпочту штопор в виде отдельного девайса, и как-нибудь переживу тот факт, что штопором очень неудобно закручивать шурупы.

Короче, сорян, но именно попытками дать хорошие инструменты даже для 1% пограничных случаев выстлана дорога в программерский ад.

Но флаги — это без сомнений отвратительная фича со всех сторон без единого преимущества.


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

Однако… Теги — наколеночный инструмент по принципу «чтобы работало» для рефлексии, которая сама по себе уродлива в контексте строго типизированного языка. Собственно, «действительно красиво» сделать один фиг не получится. Да и красоту синтаксиса аннотаций вы несколько переоцениваете.

Приведенный вами пример, конечно, в виде аннотации выглядит симпатичнее, ага. Но это очень узенький такой примерчик. Т.к. `json:«text,omitempty,string»`превратится в уже гораздо менее симпатичное что-то вроде:

@json("text")
@json("omitempty")
@json("string")
Text string


Если относиться к аннотациям как к части типа, возможно, это и оправдано, однако на этапе, когда эта аннотация воспринимается как побочная вещь, служащая для сериализации, и только мешает восприятию/читаемости кода — ну такое себе.

А если вы еще аннотации не только для json написали, но и, допустим, для xml, то вот такое, предположим

type Foo struct {
  Text   string `json:"text,omitempty" xml:"text,omitempty"`
  Field2 string `json:"field,omitempty" xml:"field,omitempty,null"` 
}


легким движением руки превращается вот в это:

type Foo struct {
  @json("text")
  @json("omitempty")
  @xml("field")
  @xmlattr("omitempty")
  Text   string
  @json("field")
  @json("omitempty")
  @xml("field")
  @xmlattr("omitempty")
  @xmlval("null")
  Field2 string
}


И вот тут я теряюсь, что из этого более уродливо, если быть до конца честным…
Уточню — это не флаги, а тэги, флаги вот golang.org/pkg/flag
Сделали так тэги, чтобы вся информация о даном поле был на одной линии, когда грепните поле — вся информация будет сразу же перед вами. Тоже самое во время редактирования — все на одной строке, а не несколько, тяжелее сделать ошибку.
А вот почему это «уродливо» кроме как вашей вкусовщины, я не понял. Тем более не понял, почему это неудобно. Надо аргументировать, а не 10 раз говорить «уродливо».
Поправочка:

const [ data1, data2 ] = await [ http.get(), http.get() ];

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

на await довольно легко пишутся паралельные запросы


Ну, в смысле, не пишутся. У вас просто фантазия на тему «могли бы так писаться». Но не пишутся, и не будут, по вполне объективным причинам.
Да, я не знаю языка, где так пишется, потому я и сказал, что в идеале.

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

Но не пишутся, и не будут, по вполне объективным причинам.
В JS есть довольно близкая запись:

const [ data1, data2 ] = await Promise.all([
    http.get(), http.get()
]);


И в данном примере запросы будут выполнены паралельно. Я не вижу никаких препятствий для сахара, в котором await может работать с List<IPromise>.
Нет, если присмотреться внимательно, то они будут выполнены паралельно.


Вот именно, давайте присмотримся внимательно!

Точка-с-запятой в конце строки, а также фигурные скобки для ограничения блоков в вашем коде неиллюзорно намекают нам, что ваш псевдокод мимикрирует под c-style нотацию. Во всех идейных наследниках с-шного синтаксиса квадратные скобки подразумевают массив/список/хештаблицу или что-то подобное.

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

await в вашем примере относится к блоку, обрамленному квадратными скобками, который мы на фоне всего перечисленного смело можем причислять к инициализации массива либо указателями на функции-коллбеки (если в языке есть функции-члены первого порядка), либо на последовательное заполнение массива значениями, возвращаемыми этими функциями. Собственно, в js это будут, видимо, коллбеки (причем последовательно сложенные в массив). В Java/C# такая нотация именно последовательно вызовет функции и положит в массив возвращенные значения.

Итого, на выходе мы имеем указание:

а) заполнить (последовательно) массив указателями на функции, дождаться завершения этой операции, полученный результат вернуть в ресивер и «анбоксить» в 2 переменных (js-нотация);
б) последовательно вызвать метод http.get дважды, положить результат в массив, чего-то подождать (видимо, заполнения массива), вернуть массив в ресивер и анбоксить.

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

Короче, этого нет ни в одном языке, и не будет. Async/await нотация таки накладывает свои ограничения.

При этом async, повторюсь, это часть сигнатуры вызываемого метода. Асинхронный метод не может быть вызван синхронно, синхронный пошлет вас лесом при попытке await'а его результата. go func() позволяет любой метод вызвать асинхронно.

В JS есть довольно близкая запись:

const [ data1, data2 ] = await Promise.all([
http.get(), http.get()
]);




Ну, во первых, такой записи в js нет.

Promise не возвращает значение, он резолвится. Будет вот так:

Promise.all([p1, p2, p3]).then(values => {/*здесь обработка*/});

При этом p1, p2 и p3 — это не произвольные функции, а промисы. Что само по себе требует либо реализации/переписывания вызываемой функции в виде промиса, либо обертки оной в промис. Ну т.е. «нельзя просто так взять функцию и засунуть в Promise.all».

Т.е. вы опять фантазируете…

А асинхрон в js — это сильно больнее, чем Go-шный, уж поверьте человеку, который работает с обоими.

И в данном примере запросы будут выполнены паралельно.


Еще веселее то, что параллельность этих запросов, вообще-то, не гарантируется, если уж мы про js…

Я не вижу никаких препятствий для сахара, в котором await может работать с List.


Конечно, с List препятствий нет, никто и не говорил, что они есть. Собственно препятствие-то только одно: сначала надо заимплементить IPromise, а потом уже пихать его в List. А заимплементить IPromise будет однозначно длиннее, чем go func(). Честно-честно, я проверял!
Ну, во первых, такой записи в js нет.
Как самоуверенно и безаппеляционно! Увы, вы категорически ошибаетесь. Такая запись есть и она работает именно так, как я говорю.



Вот, сами в консоли на хабре проверьте:

(async function () {
  const [ a, b ] = await Promise.all([ fetch('/'), fetch('/') ]);
  console.log(a, b);
})();


И вот подумайте теперь — можно ли теоретически добавить такой сахар и будет ли он работать: «если await получает массив, то обернуть этот массив при помощи Promise.all»

А асинхрон в js — это сильно больнее, чем Go-шный, уж поверьте человеку, который работает с обоими.

Увы, теперь доверия к вам нету. Я, кстати, тоже с обоими работал. Кажется, в js вы разбираетесь поверхностно. Поверьте, в современном JS работать с этим довольно просто, а большинство устаревших асинхронных функций оборачиваются при помощи promisify. Я понимаю, что это недостаток, но читайте ниже.

Т.е. вы опять фантазируете…
Да, вы совершенно правы, это я не скрываю. Именно потому в моем первом комментарии в этой ветке я написал: «в идеале...». Правда, к сожалению, вы плохо понимаете как работает await и почему моя гипотетическая идея прекрасно будет работать.

анбоксинг возвращаемого результата.

Да, тут вы правы.

Собственно, в js это будут, видимо, коллбеки (причем последовательно сложенные в массив).
Промисы, последовательно сложенные в массив.

Итого, на выходе мы имеем указание:

а) заполнить (последовательно) массив указателями на функции, дождаться завершения этой операции, полученный результат вернуть в ресивер и «анбоксить» в 2 переменных (js-нотация);

Указателями на какие функции? Ладно. Давайте я вам объясню на более простом псевдокоде. Вот такой пример:

ch1 := go async1();
ch2 := go async2();
ch3 := go async3();


Вот представьте, что «go» возвращает ну, к примеру, инстанс потока

Теперь у нас есть асинхронных функции, которые там что-то долго и параллельно делают и три ссылки на ответственные за них каналы. Представим, что рано или поздно эти функции закончат свою работу и запишут куда-то результат, а мы хотим этого дождаться. Что мы делаем? К примеру, у нас есть сахар, который позволяет это сделать:


await [ ch1, ch2, ch3 ];
// тут можно сделать что-то после того, как эти три канала параллельно отработают.


При оборачивание всего этого в try вы никогда не узнаете, какой из вызовов кинул исключение.
Не всегда это нужно.

В Java/C# такая нотация именно последовательно вызовет функции и положит в массив возвращенные значения.
В Шарпе афаик оно положет в массив не значение, а IEnumerable

go func() позволяет любой метод вызвать асинхронно.
Да, тут вы правы, отчасти это плюс. Но только отчасти. Видите ли, я изначально начал с того, что говорил «в идеале», JS — язык старый и в нем множество вещей, которые можно было бы сделать лучше. Так вот почему это одновременно плюс и минус для Гоу, в гоу по-умолчанию функции синхронные, они — блокируют поток. И читаем файл в Гоу мы, обычно, так:


dat, err := ioutil.ReadFile("/tmp/dat")


А если есть необходимость не блокировать потом — появляются неудобности, потому что мы не можем сделать так:

dat, err := go ioutil.ReadFile("/tmp/dat")


В языке а-ля JS, в идеале, все такие операции возвращают не результат — а инстанс процесса (к примеру промис). Как результат — мы не можем написать блокирующий поток код, а сразу пишем удобный и неблокирующий код:


result, err := await ioutil.ReadFile("/tmp/dat")


Более того, мы даже можем как-то управлять этим, к примеру так:


process := ioutil.ReadFile("/tmp/dat")
setTimeout(() => process.stop(), 5000);
result, err := await process;


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

И вот подумайте теперь — можно ли теоретически добавить такой сахар и будет ли он работать: «если await получает массив, то обернуть этот массив при помощи Promise.all»


Ну тогда еще более интересный вопрос: а зачем? Зачем в await добавлять логику обработки массива промисов, если этот массив с тем же успехом можно скормить Promise.all?

При том, что, в случае с Go у нас уже есть sync.WaitGroup, чтобы добиться ровно такого же ожидания выполнения нескольких горутин?

Промисы, последовательно сложенные в массив.


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

Да, я понимаю, что обернуть произвольную функцию в промис в JS вообще не проблема, можно сделать, в конце концов, 1 раз, враппер и кормить ему. Но цена этому — типизация…

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


Юзаем sync.WaitGroup?)))

Не всегда это нужно.


Но никто на уровне языка не будет от этого отказываться. Тем более заради экономии строк.

В Шарпе афаик оно положет в массив не значение, а IEnumerable


Видимо, от типа массива зависит. В теории, чо попросишь, то и положит.

dat, err := go ioutil.ReadFile("/tmp/dat")


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

Ну либо Java-style вариант. Выродить select до нетипизированной вещи над interface{} и попутно притащить рефлексию select'у в зависимости.

Короче, давайте лучше, пока что, оставим «как есть»!)

result, err := await ioutil.ReadFile("/tmp/dat")


Вот это заблокирует текущую горутину, пока не придет result. Т.е. мы просто логику синхронизации, за которую ответственны каналы, просто размазали, и ничего не получили взамен. В теории, конечно, await против go func() можно было бы заиметь в плане отказа от select'а для await… Но, опять же, игра не стоит свеч, и комбинация go func()/await в конечном итоге кого-нибудь погубит.

Более того, мы даже можем как-то управлять этим, к примеру так:


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

Зачем в await добавлять логику обработки массива промисов, если этот массив с тем же успехом можно скормить Promise.all?
Совсем не обязательно, это ведь был лишь пример, но я рад, что вы, наконец, поняли, как оно работает.

Да, я понимаю, что обернуть произвольную функцию в промис в JS вообще не проблема, можно сделать, в конце концов, 1 раз, враппер и кормить ему. Но цена этому — типизация…
Есть такая безумно мощная штука, «Дженерики» называется. В тайпскрипте промисы типизированые. Выглядит как Promise. Так что никакой цены

При том, что, в случае с Go у нас уже есть sync.WaitGroup
Можно, вот только удобно ли это? Как думаете, почему на Гоу чаще работа с файлами делается синхронно, а на ЖС — асинхронно?

Да, я понимаю, что обернуть произвольную функцию в промис в JS вообще не проблема, можно сделать, в конце концов, 1 раз, враппер и кормить ему. Но цена этому — типизация…
Вы понимаете, что большинство функций для которых это может понадобится и так уже возвращают промисы. Как говорил один мой знакомый:" я лично предпочту простоту в 99% случаев ценой боли в оставшемся 1%, чем буду испытывать дискомфорт в 100% случаев)."

Но никто на уровне языка не будет от этого отказываться. Тем более заради экономии строк.
Я же привел выше пример кода — мы можем обработать и каждую функцию в отдельности (с такой же сложностью, как и в гоу) и еще и дополнительно все функции вместе. Видите в чем преимущество? У нас есть и тот и другой вариант. А не только один. А в 99% случаев (вы уж очень любите это повторять) нету необходимости обрабатывать каждую функцию отдельно. Обычно как раз есть необходимость реализовать поведение «или всё или ничего».

Вот это заблокирует текущую горутину, пока не придет result. Т.е. мы просто логику синхронизации, за которую ответственны каналы, просто размазали, и ничего не получили взамен
Ну в Гоу — возможно. Но ведь если какому-то потоку кода необходимо дождаться данных из асинхронного канала — их необходимо дождаться и когда мы делаем await — этот поток освобождается пока не придет асинхронный ответ. Если вам не нужно ждать — вы не пишете await, если нужно — пишете.

именно на этом месте принести с собой дженерики (вот тут уже не обойдешься).
То есть такие сложности из отсутствия дженериков? Ну ок.
Есть такая безумно мощная штука, «Дженерики» называется. В тайпскрипте промисы типизированые. Выглядит как Promise. Так что никакой цены


Ну, как бы, цена есть — в промис надо «обернуть». При этом я не совсем понимаю, как вам с точки зрения типобезопасности в момент «оборачивания» дженерики помогут. Предположим, вы, заради использования синхронных функций в асинхронном контексте пишете враппер:

Promise<T> getPromice(T sourceFunc) {
  /*тут мы сознательно опустили код констракта промиса*/
}


И вот это выглядит как фейл, если честно. Потому что ничто не помешает мне сделать вот так:

Int i = 1;
var myAwesomePromise = getPromice<Int>(i);


А потом искренне, естественно, удивляться, почему нифига не работает. Ну и нафига в таком контектсте такая типизация, и чем, собственно, помогли дженерики? Собственно, такую обертку можно и на Go запилить. Что-то вроде GetPromise(func(..interface{})). Только в Go это не нужно, там go func() уже есть.

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

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

Можно, вот только удобно ли это? Как думаете, почему на Гоу чаще работа с файлами делается синхронно, а на ЖС — асинхронно?


Наверное потому, что io-операции в Go асинхронные by design? А дергаем мы синхронные врапперы, которые как раз ожидают возврата за нас?

А в JS такую роскошь не подвезли, да и с распределением по ядрам у него несколько сложнее. Поэтому и делается ручками?

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


Неа, не понимаю. Вообще для такой статистики неплохо бы источник показать.

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

Мне вот почему-то кажется, что синхронного кода на гитхабе сильно больше, чем асинхронного. А вот если порыться в примерах использования — мы с вами оба рискуем удивиться, увидев, какие методы кому-то понадобилось запускать асинхронно…

Поэтому вот тут у Go большой бонус. Ты просто берешь синхронный go-код и запускаешь в горутине. И все. Ни тебе промисов, ни тебе тасков.

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


Я про то, что в Go ваш await ничего не даст. Он будет делать ровно то, что делает этот метод без await'а. Т.е. бессмысленный код у вас получился.

То есть такие сложности из отсутствия дженериков? Ну ок.


Такие сложности из-за нежелания отказаться от select'а и того факта, что при сохранении селекта дженерики ситуацию не разрешат.

Самая главная фишка-то, собственно! Чуть не забыл!

Вот вы все демонстрируете пример, в котором на определенном этапе нужно дождаться возврата из 2 асинхронных методов. Пытаетесь оптимизировать то, что и без этого отлично без особой боли реализовано. Ну будет у вас на 1 строку больше — ну и что?

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

(await Result1, await Result2) будет до упора ждать возврата Result1, совершенно не заморачиваясь на тот факт, что Result2 уже полчаса как отработал. А если вы пинаете 2 метода и хотите скормить вывод каждого из них следующему обработчику в цепочке — вы ждете завершения обоих, а потом начинаете в той же очередности скармливать дальше.

А теперь смотрим в Go, в ту часть, которая про select. Вы просто скармливаете select'у оба канала с результатом, и тупо отрабатываете тот, что вернулся первым. Замечательно же!

Покажите мне реализацию того же на await…

Ну либо пните метод, который в цикле вычисляет, допустим, 50 значений. Дальше оберните его в async, придумайте, как эти 50 значений отдать наружу, запустите два таких в параллель и попробуйте await'ом разрулить вывод вычисленных значений в порядке вычисления.
Кстати, интересный вопрос:

Как на async/await механике worker-pool реализовать. Ну, т.е., предположим, у меня есть запросы, много, бесконечное количество запросов, которые идут на вход. Я должен по возможности их все принять и обработать. Причем каждый я должен распарсить, потом по цепочке скормить какому-то обработчику (предположительно тяжелому), который работает, предположительно минуту, а затем отдать выдачу обработчика в сущность, которая просто положит отчет в лог отчетов.

Собственно, оно бы еще ничего, но мы добавим пару ограничений. Обработчик тяжелый, очень тяжелый, причем в основном по потребляемой памяти, и начиная с 4-го запущенного в параллель экземпляра начинает как бешеный свопиться на диск, что кладет производительность в 156 раз. Т.е. вам надо гарантировать, что более 3 инстансов одновременно запущено не будет, и при этом выдать максимальную пропускную способность, т.е. держать ровно 3 всегда живыми.

Покажите мне эту задачу на async/await, а я покажу вам на горутинах+каналах+селектах.
UFO just landed and posted this here

Я для этого использую пакет context, например:


func run(ctx context.Context, ...) {
    for {
        select {
        case <-ctx.Done():
           return
        ....
        }
    }
}

Можно просто канал передать и закрыть его, можно закрывать канал с данными и в run делать range по нему.

UFO just landed and posted this here
оберните, в чем проблема?
UFO just landed and posted this here
Ну, как бы, у вас только два варианта:

1. Руками рулить thread'ами, со всеми вытекающими + возможностью остановить/убить thread извне.
2. Использовать какую-то готовую асинхронную модель со всеми плюшками + невозможностью порулить thread'ами.

Вот Go про шедулер, достаточно разумный шедулер. C++ про «сделай все руками». Трудозатраты на сопровождение кода несколько разные — вы согласны?
UFO just landed and posted this here
Вы про эту? Ту самую, в которой написано «Note that cancel will not terminate until the thread the Async refers to has terminated. This means that cancel will block for as long said thread blocks when receiving an asynchronous exception.»? Которое переводится, видимо, примерно как «Обратите внимание, что cancel заблокирует текущий поток, пока тот поток, на который ссылается Async не крашнется по исключению». Ну, т.е., это таки классический thread, который крашится по исключению, брошенному в поток (подсказка: вариант 1).
UFO just landed and posted this here
Так вы не рулите руками-то! Вы даже не обязательно знаете про ОС-треды, вы знаете только про то, что вы там asyncом что-то запустили, и оно где-то там как-то теперь считается.


«Вот за это вас и не любят»)))… Бережнее с ресурсами надо, бережнее…
UFO just landed and posted this here
Много о ресурсах думаете, когда пишете go func?


В общих чертах думаю… 4-4,5 Кб на горутину, плюс стек самой функции. По производительности — затраты на переключение контекста. В случае переноса между системными тредами достаточно дорого, при переключении в пределах одного системного треда — подмена двух указателей.

Теперь, видимо, будем считать оверхед хаскелля?
UFO just landed and posted this here
Не используйте такой код, сделайте пулл реквест и тд.
Странные у вас претензии — вы явно запускаете в горутине не ваш метод.
UFO just landed and posted this here
Если вам не нравится библиотека, то вы ее не используете, правильно?
В чем вопрос тогда?
UFO just landed and posted this here
Вопроса на самом деле два: насколько легко на языке писать библиотеки, которые мне нравятся, и насколько легко писать библиотеки, которые не нравятся.

Да.
Примерно так же, как асинхронную таску в C# (спойлер: в общем случае никак).
UFO just landed and posted this here

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

UFO just landed and posted this here

Такой подход вполне имеет право на жизнь, просто тогда лучше обратить внимание на Erlang или Haskell, а не Go. Кому что нравится.

В тех редких случаях, когда надо останавливать потоки/циклы, вы просто заранее предусматриваете для этого отключающий костыль, как в Си++.
Ну да, Go пока не идеален во всех местах, но это не машает пользоваться его потоками и радоваться. Ну да, в функциональных языках потоки лучше продуманы, но нам и так хорошо.
UFO just landed and posted this here
Как это нельзя? Запускаете вычисление в потоке и после определённого времени прибиваете, если не завершился поток.
UFO just landed and posted this here
Кстати, а как вы корректно прибиваете поток не на плюсах так, чтобы все ресурсы корректно освобождались (файлы, память)?
UFO just landed and posted this here
Невозможность в плюсах взять и прибить занимающее слишком долго вычисление и отдать вызывающему коду таймаут была одним из моих аргументов против этого языка для одного из проектов, где это было важно. При всей моей любви к плюсам.


Хм… в плюсах что-то невозможно? Или плохо искали? Возможно, вам оно не настолько было нужно? Может, в бусте стоило поискать?
UFO just landed and posted this here
Ну, какбэ, что очевидно, не совсем произвольный код, но C++.

Понимаете ли, на C++ вполне можно написать интерпретатор хаскелля, на C же реализации вполне существуют (а может, и на C++ есть). Значит, видимо, как-то можно?
UFO just landed and posted this here
Так требование произвольности там всё-таки не зря.


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

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


Признайтесь, вам просто лень! А могли бы и написать, и комитету по стандартизации С++ подсунуть свою реализацию, и всем было бы хорошо!
UFO just landed and posted this here
В тех редких случаях, когда надо останавливать потоки/циклы, вы просто заранее предусматриваете для этого отключающий костыль, как в Си++.

Ага, в Go можно принимать context и стопать цикл при отмене.
Только прочитайте еще раз претензию 0xd34df00d, он про другой случай пишет.
Ну, как бы, в map, filter и подобных тоже не предусмотрено условие выхода. Видимо, и ими тоже пользоваться нельзя…
UFO just landed and posted this here

Это до тех пор, пока Вы не дадите на вход map/filter какой-нибудь итератор/генератор бесконечной последовательности, которые так любят использовать в функциональных языках с ленивыми вычислениями. :)

UFO just landed and posted this here
Ну, йошкин кот, опять за «образец для подражания» выдается несуществующий язык?
UFO just landed and posted this here
Note that cancel will not terminate until the thread the Async refers to has terminated. This means that cancel will block for as long said thread blocks when receiving an asynchronous exception.


Ну, т.е., ровно та же самая фигня, что и context.WithCancel? Где магия-то?
UFO just landed and posted this here
Ну т.е. планировщик над системными тредами?

И, кстати, никто не мешает cancelнуть тред из специально запущенного под этот cancel треда.


Ага, запустить один системный тред, из которого можно «кенсельнуть» другой?

Отлично, и насколько это производительно? А по ресурсам что? А пару тысяч потоков запустить?
UFO just landed and posted this here
Нет, в хаскеле гринтреды


В Go тоже…

Thread stacks (including the main thread’s stack) live on the heap. As the stack grows, new stack chunks are added as required; if the stack shrinks again, these extra stack chunks are reclaimed by the garbage collector. The default initial stack size is deliberately small, in order to keep the time and space overhead for thread creation to a minimum, and to make it practical to spawn threads for even tiny pieces of work.


Я же правильно понимаю, что тут написано, что стек у каждого треда в хаскеле отдельный, и выделяется в куче, и после «убиения треда» сборщик мусора тупо подбирает этот кусок памяти целиком?

Ну, т.е. оно действительно хорошо работает ровно в сценарии «чистых функций», память между тредами не шарится, а в остальных сценариях все это заботливо обложено граблями вида «подождать IO» и тому подобными? Ну, на некоторых сценариях разумно, в некоторых других — такое себе.
UFO just landed and posted this here
Эм, какое там IO ждать? Если у вас гринтред блокируется на FFI, например (или на системном вызове), то, насколько я знаю, RTS создаст новый системный тред, на котором продолжит выполнять хаскель-код, чтобы старый спокойно себе спал в FFI/syscall.


Ну т.е. с IO все не очень хорошо. Даже местами «больно» может получиться…

Ну, наверное, писать файлы или сокеты в миллион потоков действительно не стоит, и не только потому, что системные треды дорогие.


В миллион не стоит, ага, а в тысячу-другую, или в 10 тысяч, например, почему бы и нет. Что блоканет вас сильнее, чем системные треды?

Вот тут немного «классики» на «отработать много сетевых соединений». И там Go со своими «несовершенными горутинами» вполне неплохо себя зарекомендовал.
UFO just landed and posted this here
А как вообще рантайм может решить подобную проблему с сисколлами?


Асинхронным IO? Вроде норм, работает даже, и потоки почем зря не морозит.

Если писать в файлы — то ФС, например.


А сокеты?

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


В смысле, работу с сетью на хаскеле делать неудобно?
UFO just landed and posted this here
Это просто означает, что вы стали использовать другие сисколлы :)


Ага, не замораживающие при этом потоки…

А сокеты, в которые вам надо писать тысячами, вы вряд ли используете синхронные.


В Go, как бы, любой IO — асинхронный. Синхронный не завезли.

Вообще-то O_NONBLOCK можно ручками отключить на конкретном файловом дескрипторе, и на нём I/O станет блокирующим. Не знаю, правда, зачем это может пригодиться, но если надо — это возможно.

В идеале


Ну т.е. нет…

const [ data1, data2 ] = await [ http.get(), http.get() ];

Странная какая-то семантика.

У вас, видимо, не совсем верное представление о значении ключевого слова await. await — это про «дождаться выполнения» (т.е. про синхронизацию состояния). Асинхронное выполнение — это async.

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

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

И все это для того, чтобы сэкономить не то чтобы две строчки, а один перенос строки.

«Низачот», короче. Странные у вас идеалы, не хотел бы я на этом писать.
У вас, видимо, не совсем верное представление о значении ключевого слова await. await — это про «дождаться выполнения» (т.е. про синхронизацию состояния). Асинхронное выполнение — это async.

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

Представьте, что async-функция возвращает процесс (к примеру промис). Тогда мой пример разворачивается в следующее:

const promise1 = http.get();
const promise2 = http.get();
const [ data1, data2 ] = await [ promise1, promise2 ];


Паралельно запущены два процесса и мы ожидаем их завершения.

В этом подходе ваш await означает «дождаться, пока в отдельном потоке последовательно выполнятся два запроса и положить полученные значения в кортеж».

Потому в моем подходе await означает: «дождаться, пока выполнятся оба запроса, промисы от которых лежат в массиве».

То, о чём говорите вы — выглядело бы так:

const promise1 = http.get();
const promise2 = http.get();
const [ data1, data2 ] = [ await promise1, await promise2 ];

И, кстати, тоже вполне корректно бы работало, но совершенно не имеет смысла.

И все это для того, чтобы сэкономить не то чтобы две строчки, а один перенос строки.

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


Неверное у вас впечатление.

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


Не знаю, что там у вас в вашем псевдокоде, но во всех существующих языках await означает «не выполнять следующую строку, пока в этой не вернется значение».

Представьте, что async-функция возвращает процесс (к примеру промис).


Ну, для начала стоит определиться, процесс или промис. Да и вообще, в принципе, что вы понимаете под словами «возвращает процесс». Вернуть можно значение, можно вернуть указатель, адрес, пофиг чо, на самом деле, с одним ограничением — это всегда набор байт. Что вы подразумеваете под процессом в надежде смаппить его на набор байт — остается только догадываться.

const promise1 = http.get();
const promise2 = http.get();
const [ data1, data2 ] = await [ promise1, promise2 ];


Ну, первая претензия. Работать будет тогда и только тогда, когда http.get() уже промис. А промис — это такая штука, в которой хранится «точка входа» и табличка коллбеков. Если этот самый промис вы пишете сами… Ну, начнем с понимания, что промис это не функция, а вполне себе объект. И реализовывать вам придется не одну асинхронную функцию, а 3:

1. Саму непосредственно функцию, которая должна выполниться асинхронно.
2. resolve-колбек (функция, которая вернет из промиса результат… да-да-да, Promise не возвращает значения)
3. reject-колбек (функция, которую нужно пнуть, если что-то пошло не так, которая вам бросит исключение, вернет ошибку, откатит транзакцию, закроет незакрытые ресурсы и все вот это).

При этом мы помним, что открыли эти ресурсы мы в функции №1. И там же закрыли. Но, если что-то пошло не так, продублировать надо еще в 3-й. И отследить, что успело закрыться в 1-й, чтобы не освобождать ресурсы повторно, а то исключение словите…

И это вы предлагаете в качестве «более лучшей замены» асинхронного вызова любого метода с возвратом значения через канал? Ну, как бы, не мне вас осуждать, конечно… Но такое себе.

const [ data1, data2 ] = [ await promise1, await promise2 ];


Окей, мы уже допустили, что await'у присобачили какую-то левую семантику, которая магическим образом что-то там запускает… Это изначально плохая идея, конечно, но Г-дь нам судья.

Тогда этот код, как раз будет иметь смысл.

[promise1, promise2] — положить в массив два промиса, согласно уже существующей семантике JS (менять никто не будет). await [promise1, promise2], видимо, должен попытаться исполнить массив как функцию с «непонятно, что это должно дать».

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

А, у нас же внутри промисы! Тогда еще непонятнее, т.к. они объекты. Предположим, они должны резолвиться… Как их теперь сложить в массив? Как отрабатывать resolve/reject?

А вот [await promise1, await promise2] после наделения await'а магической силой пинания промисов, как раз сможет гарантировать, что промис сначала отрезолвится, и после этого результат резолва промиса ляжет в массив. Очень полезная методика, если нужно получить массив из двух undefined (промисы не возвращают значения)!!! Работать быстрее, чем const a = [undefined, undefined], конечно, не будет, но зато как красиво выглядит!!!

Единственное применение, извините, это заюзать then-коллбек. Т.е. это будет выглядеть, типа, как-то так:

const result = await [http.get(then=((value)=>{return value})), http.get(then=((value)=>{return value}))]

Хотя не, на жесть какую-то похоже! Давайте не будем так, ну пожалуйста!

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


Нету тут изящество. sync.WaitGroup, конечно, не настолько изящно, и на каждый асинхронный вызов по целой строчке тратится… Но, блин, работает! И «запустить параллельно, а потом дождаться всех» имеет оверхед в пару строк.
А, у нас же внутри промисы! Тогда еще непонятнее, т.к. они объекты
Смешно читать, как вы выставляете всё это нерешаемыми проблемами, когда они уже решены в существующих языках
Опечалить вы можете кого-нибудь другого. А с учетом того, что я всё это делаю ради эксперимента, то ошибиться вообще не страшно)).
Последовательный код c#:
var data1 = await GetData1();
var data2 = await GetData2();
var result = Foo(data1, data2);

Почти параллельный код (если в методе много I/O и мало CPU):
var data1 = GetData1();
var data2 = GetData2();
var result = Foo(await data1, await data2);

Параллельный код:
var data1 = Task.Run(() => GetData1());
var data2 = Task.Run(() => GetData2());
var result = Foo(await data1, await data2);
Я не то, чтобы прямо спец в C# был, но мне, почему-то, кажется, что во втором варианте у вас ничего параллельно не запустится. Пока не отрезолвится await data1 он до await data2 не дойдет.

А теперь, собственно, про ключевое отличие Go.

Представляем, что у вас уже есть два синхронных метода: GetData1() и GetData2(). Вот здесь async/await и сливает. await'ить вы можете только Task либо Task . Вот с async/await вы либо переписываете метод на асинхронную реализацию, либо, на крайняк, пишете враппер над синхронным методом, который возвращает Task.

Например что-то вроде такого:
public async Task<Data> GetData1Async()
{
    var result = GetData1();
    return new Data (result);
}
public async Task<Data> GetData2Async()
{
    var result = GetData2();
    return new Data (result);
}


И чем после этого ваш третий вариант лучше, чем следующий код, я так и не понял:

var wg sync.WaitGroup
resultChan := make(chan[Data])
go func() { 
  wg.Add(1)
  defer wg.Done()
  resultChan <- GetData1()
}()
go func() { 
  wg.Add(1)
  defer wg.Done()
  resultChan <- GetData2()
}()
wg.Wait()
почему-то, кажется, что во втором варианте у вас ничего параллельно не запустится.

Не надо гадать, надо проверить.

Представляем, что у вас уже есть два синхронных метода

Если они полностью синхронные (не содержат в себе I/O), то запускать их параллельно в нагруженном сервисе нет никакого смысла. А если они блокируют поток, то вместо них нужно использовать асинхронные аналоги.

Синхронные методы не становятся асинхронными добавлением async/await. Если в методе блокируется поток, то с этим ничего нельзя сделать кроме как переписать его.
Если они полностью синхронные (не содержат в себе I/O), то запускать их параллельно в нагруженном сервисе нет никакого смысла.


Ну конечно же, как я не подумал-то а? Пусть на 32 ядрах сервера крутится один воркер с линейной логикой! А чо, норм же! А клиенты — а чо клиенты, подождут…

А если они блокируют поток, то вместо них нужно использовать асинхронные аналоги.


Точно, заюзать асинхронные аналоги, которые ровно так же заблокируют поток. Зато это будут асинхронные аналоги!

Синхронные методы не становятся асинхронными добавлением async/await.


Да ладно!

Если в методе блокируется поток, то с этим ничего нельзя сделать кроме как переписать его.


Единственное реальное условие, по большому счету, чтобы метод был конечным. С чего у вас поток-то блокируется? Он, как раз, работает, просто занят очень. И, собственно, пока не наработается… хм, а смысл его отвлекать?
У вас с терминологией беда. Асинхронные функции как раз не могут блокировать поток.
Это логично, потому что поток — сущность синхронная.

Т.е. сами по себе асинхронные вызовы поток не заблокируют. Поток заблокирует синхронизация состояния.

В паре async/await async — это асинхронность, а await — как раз примитив синхронизации. И вот await фризит поток до тех пор, пока асинхронный вызов не вернул результат.

И вот тут мы натыкаемся на главный недостаток async/await vs goroutine/channel/select. Select'а не хватает.

Т.е. async/await — это, конечно, классика, и все такое. Однако на полноценную замену Go-шной модели асинхронности не тянет…
И вот await фризит поток до тех пор, пока асинхронный вызов не вернул результат.

Как раз таки наоборот.
Ну почему наоборот то?

var data1 = Task.Run(() => GetData1());
var data2 = Task.Run(() => GetData2());
var result = Foo(await data1, await data2);


Предположим, этот код выполняется в потоке A.

Первая строка — асинхронный вызов, стартуем поток B, в котором запускается GetData1(). Поток A не блокируется.

Вторая строка — ровно то же самое. Поток A не блокируется, стартует поток C.

Третья — await. Прямое указания заблокировать поток A, пока B и C не отработают.

await — прямое указания блокировки потока.
Насколько я понял, в go написать синхронный код невозможно, т.к. функции которые блокируют поток не добавлены, по крайней мере в стандартную библиотеку. Но если вызвать из c/cpp такую функцию, то избавиться от блокировки go не сможет.
Насколько я понял, в go написать синхронный код невозможно,


Вы что-то странное поняли.

var i int = 10
var j int = 20
k := i + j


Вроде, синхронно… Не?

т.к. функции которые блокируют поток не добавлены,


Вы про какие функции? Сисколлы? Они все в async выполняются — это да. Чтобы вы случайно раком все не поставили, и это логично. А горутина, которой нечем заняться (допустим, она сискол ждет, или io-операцию), вполне себе блокируется.

Но если вызвать из c/cpp такую функцию, то избавиться от блокировки go не сможет.


Собственно, можно и стандартными средствами все горутины в блокировку увести, без помощи c/cpp. И Go совершенно аналогично с этим ничего не сделает.

В чем суть-то?
Имеется ввиду синхронный I/O код
На уровне сисколов и системного IO все вызовы в Go асинхронны.

Но, как бы, в «классическом» подходе async — это признак асинхронного вызова, а await — костыль для возможности дернуть этот метод синхронно.

В Go у вас в этой системе есть 3 сущности. Вызов синхронен по дефолту, go func() — директива асинхронного вызова, <-chan — примитив синхронизации состояния. И в качестве добавки то, чего в async/await не хватает: select — умелка синхронизации состояния не «поштучно» в порядке запроса результата, а конкуррентно по принципу «кто первый отработал».

Вот этого в «классике» и не хватает.
await — костыль для возможности дернуть этот метод синхронно.

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

Уже даже по русски везде об этом написано и с картинками, почитайте docs.microsoft.com/ru-ru/dotnet/csharp/programming-guide/concepts/async/#BKMK_Threads
await как раз делает код похожим на синхронный


У взрослых людей это называется «синхронизирует состояние».

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


Ага, таким образом мы добиваемся синхронности вызывающего кода, об этом я вам и говорю. А вы в ответе выше это отрицаете.

В случае await func1() — да, сам поток не фризится, просто в нем начинает выполняться func1(). Т.е. мы делаем синхронный вызов func1().

Если у вас var result = Task.Run() — это явное указание асинхронного вызова, который стартует в отдельном потоке. await result после этого блокирует текущий поток, пока таск не отработает.
Я же дал ссылку, зачем придумываете от себя? Там черным по белому написано:
Выражение await в асинхронном методе не блокирует текущий поток на время выполнения ожидаемой задачи. Вместо этого выражение регистрирует остальную часть метода как продолжение и возвращает управление вызывающему объекту асинхронного метода.
Вот вы то ли в чудеса верите, то ли читаете невнимательно.

Прямо «читаем вслух»:

Выражение await в асинхронном методе


Теперь вспоминаем, что main — синхронный и снова смотрим на приведенный пример. Поясню: код предположительно из main'а. Специально для ясности добавил номера строк.

[1] var data1 = Task.Run(() => GetData1());
[2] var data2 = Task.Run(() => GetData2());
[3] var result = Foo(await data1, await data2);


Оно работает в некотором потоке, пусть будет A. Task.Run() в первой строке явно говорит: «запусти мне GetData1() в отдельном потоке исполнения». Он и запускается в потоке B (или не запускается, если у нас свободных потоков нет, а ждет, пока какой-нибудь поток заблокируется на исполнение, чтобы высвободить ресурсы). Для простоты примера решим, что потоков у нас хватает, и он запускается. При этом конкретно в потоке B. Поток A не блокируется, т.е. продолжает исполняться.

Строка 2: явно запускает GetData2() в новом потоке (пусть будет C). Поток A так же не блокируется, у него все хорошо.

Строка 3: 3 последовательных инструкции потоку A — дождаться, пока отработает поток B, затем дождаться, пока отработает поток C, затем выполнить Foo на полученных результатах.

Вот теперь объясните мне, пожалуйста, мой дорогой друг, чем занят поток A до тех пор, пока поток B не отработал? Подскажу: когда потоку нечего делать, и он чего-то ждет, он блокируется.

Небольшое пояснение про цитируемый вами отрывок:
await в асинхронном методе отличается от await'а в синхронном тем, что он отдает управление вызывающему коду, и таким образом вся эта конструкция в целом продолжает как-то работать. Синхронный код управление отдавать не умеет, и поэтому он тупо блокируется и ждет.
Вот это вас плющит))
Третий раз говорю, прочитайте ту ссылку и больше не пишите такую чушь. В той статье написано что await можно использовать только в асинхронном методе. main кстати тоже может быть асинхронным.
В той статье написано что await можно использовать только в асинхронном методе.


Вы же мне сейчас покажете конкретное место в статье, где это написано? Или с этого места окончательно и бесповоротно начинаем считать вас… кхм… «фантазёром»?
Если с помощью модификатора async указать, что метод является асинхронным, у вас появятся следующие две возможности.
— Асинхронный метод сможет использовать await для обозначения точек приостановки


Также в этой статье есть ссылки на ключевое слово await docs.microsoft.com/ru-ru/dotnet/csharp/language-reference/keywords/await там написано:
Оператор await можно использовать только в асинхронном методе, измененном с использованием ключевого слова async.



Также можно попробовать использовать await в синхронном методе, на что будет выдана ошибка:
cs4033: await operator can only be used within an async method



Поздравляю, с этого места мы будем считать вас окончательно и бесповоротно… кхм… фантазёром))

На этой веселой ноте я заканчиваю обсуждение ваших фантазий.
Также в этой статье есть ссылки на ключевое слово await


Окей, в самой статье нет, есть «глубже».

Внимательно читаем то, что написано «глубже», всматриваемся в примеры:

GetPageSizeAsync(args[1]).Wait();


Гут, славатехоспади, асинхронный метод из синхронного таки можно дернуть. А то я уж переживать начал. Правда некрасиво смотрится, «недоборщили» с синтаксическим сахаром, единообразия, на мой вкус, не хватает.

Дальше читаем саму статью, часть под красноречивым названием «Потоки».

Ключевые слова async и await не вызывают создания дополнительных потоков.


Отлично, все вышеописанное нам рассказывает про M:1 многозадачность. Отлично, но мы же белые люди в 2019, мы хотим M:N!

Метод Task.Run можно применять для перемещения операций, использующих ресурсы ЦП, в фоновый поток, однако фоновый поток не имеет смысла применять для процесса, который просто ждет результата.


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

Теперь еще раз о моем вопросе (он стал еще веселее).

Предусловие:

1. У нас есть поток, в котором выполняется main (Поток A).
2. Из него явно в виде Task.Run() пинается в отдельный поток (Поток B) асинхронный метод GetAsync1().
3. Затем явно пинается в еще один отдельный поток GetAsync2().
4. Затем main await'ит возврат из обоих потоков.

Для демонстрации повторим код:

[1] var data1 = Task.Run(() => GetData1());
[2] var data2 = Task.Run(() => GetData2());
[3] var result = Foo(await data1, await data2);


В описании написано следующее:

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


Теперь к собственно к вопросам:

1. Кому вернет управление метод GetAsync1(), если внутри него встретится await? main'у? Каким образом он вернет управление в соседний поток? Зачем main'у 2 «управления»?

2. Какому «вызывающему объекту» вернет управление main при вызове await? Кто вызвал main? Он точка входа.

3. Чем занят поток A, пока потоки B и C не отработали? Варианта-то всего 3 — заблокирован, хреначит вхолостую, убит.

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

Вот и получается один всего вариант в многопоточной среде — блокировка простаивающих потоков. И нифига вы с этим не сделаете, чудес не бывает.
await это просто синхронизация двух асинхронных методов — один асинхронный метод (methodA) ждет пока закончится второй асинхронный метод (methodB), при этом, как я понял, methodB будет выполняться в потоке методаА.
То есть это просто делает два асинхронных метода синхронными (между собой). При этом methodA все так же будет асинхронным в отношении вызывающего его метода.
Вот именно, await — это именно примитив синхронизации.

Прямо там же, по ссылке, читаем:

Метод Task.Run можно применять для перемещения операций, использующих ресурсы ЦП, в фоновый поток,


Я в примере кода прямо специально Task.Run() использовал. Это прямое указание «запустить в отдельном потоке».

Все, что было написано в статье до этого — относилось именно к кейсу «все в одном потоке». Потом упоминание «если надо в отдельном — Task.Run, но вам это не нужно». Эдакий «автослив», что в многопоточном контексте становится неудобно, и там без блокировок не получается. И сразу «к выводам».

Короче, «чуда не случилось», расходимся)
Основной поток всегда либо выполняется либо чего нибудь ждёт, хоть dotnet, хоть golang.
либо чего нибудь ждёт


Ну, т.е. мы пришли к взаимопониманию, и определились, что на время ожидания поток таки блокируется?

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

В модели dotnet есть основной поток и фоновые. Т.е. один всегда главный и он может заблокироваться тогда и только тогда, когда ожидает завершения работы фонового потока, конкретно по await.

Что это дает: предсказуемость «спаунинга» потоков, де-факто древообразная иерархия порождения оных. Фактически, дедлок (именно дедлок, а не блокировка потока — та штука, которой вы так опасаетесь) достаточно трудно достижим.

Ну, т.е. если в main'е вы породили 2 потока (условно 2-го уровня), а каждый из них еще по парочке (3-го уровня), потоки одного уровня в принципе не могут влиять на флоу соседних с ними потоков на одном уровне вложенности.

Т.е. блокироваться на время ожидания возврата (и только на время ожидания возврата) может только поток уровня N-1. Очевидно, что «словить взаимные блокировки» при соблюдении условии конечности исполнения каждого потока — задача практически нерешаемая.

Небольшим бонусом конечности Task'а служит еще и более уодбный примитив синхронизации await. Прямо вот признаю, что var data = await AsyncTask(); выглядит сильно удобнее, чем «сунуть в канал, вынуть из канала».

Короче, в первом приближении — сплошные бонусы, так и запишем: «дедлок почти недостижим», «удобный синтаксис возврата».

Естественно, всегда есть и «ложка дегтя». И она лежит ровно в том же месте, что и условие конечности Task'ов. await позволяет дождаться за одну инструкцию выполнения ровно одного Task'а. В случае параллельного запуска нескольких потоков, чего-то там считающих, а затем аггрегации возвращенных из всех потоков данных в одной инструкции это не проблема. Но есть кейс «дождаться, какой из потоков выполнится раньше и отработать конкретно его выдачу». Вот тут уже Go удобнее. Сейчас объясню, почему.

Итак, Go-шный подход.

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

Что из этого следует. Из этого следует, что системный поток в Go блокируется… примерно никогда. Ну, точнее, он блокируется в тех случаях, когда его нечем занять. Т.е. до тех пор, пока есть горутина, которой есть чем заняться, она получает в свое распоряжение освободившийся системный поток. Т.е. блокируем мы не системный поток, а горутину. Она при этом отодвигается в очередь исполнения (на тот момент, когда «дождется» требуемого ей чтения из канала), а освободившийся системный поток получает в свое распоряжение первая попавшаяся горутина из очереди.

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

Что же такое «дедлок», собственно. Великий и ужасный Goшный дедлок не имеет, на самом деле, ничего общего с блокировкой системных потоков. С ними, как раз, все хорошо, собственно, они свободны. Дедлок возникает в тот момент, когда у вас в очереди исполнения висят ожидающие горутины и планировщик не находит неблокированную горутину для исполнения на имеющихся у него ресурсах.

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

Но, видимо, раз так сделано, это кому-нибудь нужно? Ведь не просто так ведь этот геморрой? Не зря же не использовали async/await-классику?

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

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

Итак, бонус №1: select.

В dotnet'е вы можете await'ить по одной асинхронной задаче зараз. Если нужно дождаться двух — ждите по очереди, сначала первую, потом вторую.

В Go вы можете ждать значения из одной, двух сразу, третьей справа, восьмой в очереди или той, кто раньше «отстрелялась» в любых комбинациях и последовательностях. В теории, того же эффекта можно, вероятно, и в dotnet добиться. Но, как бы, вы не можете await'ить задачу из потока, который ее не запускал. Т.е. все это вам придется делать ручками и путем написания «многабукаф».

Ну и вторая фишка: каналы.

Канал — тупо примитив синхронизации, т.е. замена await. С некоторой спецификой.

Если вы просто ждете из канала какое-то значение, одно, то особого профита вы не заметите. Я уже говорил, что await на первый взгляд таки удобнее.

Но… Но канал вы можете породить в одном месте программы, закрыть другой, передавать цепочками, подписывать на события из канал несколько горутин, устраивать на нем round-robin балансировщики и делать с ним кучу других вещей.

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

А await возвращает одно значение. И только после завершения работы асинхронного метода. Вы, конечно, можете запустить бесконечный Task и из него писать ручками в какую-нибудь очередь… сами… и синхронизировать «ручками»… Но, как бы, эту вещь Go умеет из коробки.

Итого:

Если весь ваш асинхронный код выполняется в одном потоке, Go не дает никаких преимуществ перед C#, и даже менее удобен, т.к. вы имеете лишний геморрой с каналами и синхронизацией на ровном месте (собственно, в одном потоке оно все равно вырождается в синхронное исполнение).

Если же ваш код рассчитан на многопоточную среду. Вот тут Goшная модель асинхронности уделывает dotnet'ную, как Г-дь черепаху, сори за откровенность.
Вы опять несете ахинею, и делаете выводы на своих фантазиях.
Есть основной поток и пул потоков (и ещё сколько то служебных).
Task.Run() не запускает поток, а планирует выполнение задачи в пуле потоков.
В консольном приложении и в desktop код вполне может работать в основном потоке, более того, работать с интерфейсом можно только из основного потока, вам гошникам конечно это неизвестно. Но серверные приложения обрабатывают запросы в пуле потоков.
Никакой иерархии потоков нет, как я уже сказал, есть пул потоков.
Чтобы подождать выполнение одной задачи, можно использовать Task.WhenAny().
У меня складывается ощущение, что вы не понимаете что такое асинхронность, потому что постоянно говорите про потоки. Асинхронное приложение не обязательно многопоточное (кажется в браузере весь код выполняется в основном потоке, за исключением воркеров).

В общем в очередной раз вы облажались))
Давайте жгите ещё!))
работать с интерфейсом можно только из основного потока, вам гошникам конечно это неизвестно.


Работать с интерфейсом в C# можно только из основного потока. Да, для Go-шников это лишняя информация.

Никакой иерархии потоков нет


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

Чтобы подождать выполнение одной задачи, можно использовать Task.WhenAny().


Слабая замена select'у, если честно. Task.WhenAny, цитирую, «создает задачу, которая будет выполнена после выполнения любой из предоставленных задач.» До select'а по удобству все еще, как до Луны пешком.

У меня складывается ощущение, что вы не понимаете что такое асинхронность, потому что постоянно говорите про потоки.


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

весь код выполняется в основном потоке, за исключением воркеров


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

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

Слабая замена select'у, если честно. Task.WhenAny, цитирую, «создает задачу, которая будет выполнена после выполнения любой из предоставленных задач.» До select'а по удобству все еще, как до Луны пешком.

Приведите пример, где без select не обойтись, только нормальный, а не вывод в консоль. Скорее всего в c# аналогичная задача решается по другому. Вообще пользователю пофиг select или Task.WhenAny. Чего такого select умеет, чего нельзя сделать другими средствами? В общем без примера нет смысла обсуждать, давайте реальный пример.

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

Я же говорю, Вы не понимаете что такое асинхронность, она у вас почему-то ассоциируется с многопоточностью. Вот есть у нас 2 метода, пусть это будет условный http.get(), допустим нам нужно взять данные с двух разных серверов, допустим каждый вызов длится 1 сек. Вот код можно написать так, что он будет выполнен через 2 сек, а можно и через 1 сек и для этого многопоточность не нужна, потому что для ожидания поток не нужен. Это можно реализовать на go, c#, javascript и вообще на любом языке где есть асинхронные методы…
Так и есть, но возвращает управление после того как будет получен результат, а до этого будет ожидать в очереди. Я, честно, уже потерял надежду, что вы когда нибудь это поймете, потому что раз за разом вы придумываете невероятную чепуху.


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

Приведите пример, где без select не обойтись, только нормальный, а не вывод в консоль.


Вагон и маленькая тележка. Предположим, у нас есть 2 постоянных воркера, обрабатывающих тяжеловесные задачи. И некоторое количество, пусть будет N, принимающих эти задачи. Также нам нужен graceful shutdown, т.е. обрубить прием задач, отработать те, которые в очереди, и выйти.

Мы имеем канал входных задач inChan, буферизованный канал workerChan(2), канал прерывания задачи endChan.

select {
  case task := <-inChan:
    workerChan <- task
  case <-endChan:
    workerChan.Close()
}


И это все…

Скорее всего в c# аналогичная задача решается по другому.


Однозначно, решается. И однозначно по другому. В «сильно больше букв». Чтобы «букв» было мало, канал и селект должны быть не библиотечными вещами, а вещами уровня языка.

Вообще пользователю пофиг select или Task.WhenAny.


Неа, не пофиг. Task.WhenAny — де факто, тупо колбек. При этом это инструмент, который позволяет выполнить какой-то вызов при выполнении пофиг какой задачи из группы (какая первая «отстрелялась»). select позволяет выполнить код при выполнении «не пофиг какой» задачи из группы. А как раз в зависимости от того, какая отработала.

Чего такого select умеет, чего нельзя сделать другими средствами?


Наверняка какими-то другими средствами можно достичь того же, только вы так и не показали этих самых средств.

Я же говорю, Вы не понимаете что такое асинхронность, она у вас почему-то ассоциируется с многопоточностью.


Я знаю, что такое асинхронность. А еще я знаю, что такое M:N многопоточность. И Go предоставляет достаточно удобную асинхронную модель поверх M:N многопоточности. При этом модель удобна именно в случае M:N многопоточности. Писать на Go однопоточные приложения можно, но, во первых, трудно и требует дополнительных приседаний, во вторых — больно, и гораздо менее удобно, чем на множестве других языков.

В том между нами и разница: вы ассоциируете асинхронность с моделью M:1, которая наиболее распространена в C#. Я демонстрирую асинхронную модель поверх M:N.
Я вот, например, все еще, лелею надежду, что вы прочтете текст по вашей ссылке…

Не не, вы сами еще ищите подтверждение своих фантазий, с меня хватит…

И это все…

Как это все?! Не держите нас за дураков. Каналы нужно инициализировать, читать, писать и т.д. Где весь этот код? А так я тоже могу написать:
while (enable)
{
tasks.add(Task.Run(()=>{...}));
}


В том между нами и разница: вы ассоциируете асинхронность с моделью M:1, которая наиболее распространена в C#. Я демонстрирую асинхронную модель поверх M:N.

Больше не вижу смысла с вами что-то обсуждать, подучите матчасть, тогда может быть продолжим…
Вот ваша фраза:

Так и есть, но возвращает управление после того как будет получен результат, а до этого будет ожидать в очереди.


Вот это цитата из статьи по вашей ссылке:

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


Еще раз для ясности: await внутри асинхронного метода возвращает управление вызывающему коду. Бредите тут вы.

А так я тоже могу написать:
while (enable)
{
tasks.add(Task.Run(()=>{...}));
}



Написать-то вы можете, только каким образом оно сделает то же, что и предоставленный мной код?

select {
  case task := <-inChan:
    workerChan <- task
  case <-endChan:
    workerChan.Close()
}


Больше не вижу смысла с вами что-то обсуждать, подучите матчасть, тогда может быть продолжим…


Видимо, сдаетесь?
Написать-то вы можете, только каким образом оно сделает то же, что и предоставленный мной код?

А почему мой код должен делать что и ваш код? У нас задача красиво потушить сервер. Пока сервер включен, задачи добавляются в очередь с помощью Task.Run(). Когда сервер выключится цикл завершится и можно будет подождать когда выполнятся все задачи и после этого процесс завершится. Коротко и ясно! В go о таком можно только мечтать, кроме каналов ваш мозг уже не способен ничего воспринимать))

Видимо, сдаетесь?

Видимо мне пофиг, я всё выяснил что мне надо.
А почему мой код должен делать что и ваш код? У нас задача красиво потушить сервер. Пока сервер включен, задачи добавляются в очередь с помощью Task.Run(). Когда сервер выключится цикл завершится и можно будет подождать когда выполнятся все задачи и после этого процесс завершится. Коротко и ясно! В go о таком можно только мечтать, кроме каналов ваш мозг уже не способен ничего воспринимать))


А теперь чуть-чуть усложним:

select {
  case task := <- inChan:
    workerChan <- task
  case task:= <- otherChan:
    log.Println("other chan task got")
  case <-endChan:
    workerChan.Close()
}


async/await все еще хватает?

Ну и по поводу вашего «while (enable)»… Про многопоточность уже забыли? Синхронизация где? По дороге потерялась?
А теперь чуть-чуть усложним:

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

Ну и по поводу вашего «while (enable)»… Про многопоточность уже забыли? Синхронизация где? По дороге потерялась?

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

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


Вместо лога может быть что-то иное, например.

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


Ну давайте, конечно. Вот код:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 4)

	ticker := time.Tick(2 * time.Second)
	val := 0

	go func() {
		defer close(ch)
		for i := 0; i < 10; i++ {
			val++
			fmt.Printf("Put: \t%d\n", val)
			ch <- val
		}
	}()

	for value := range ch {
		<-ticker
		fmt.Printf("Work: \t%d\n", value)
	}
}


Сделайте то же самое с async/await, а мы посмотрим, что окажется удобнее.

В итоге ваш select обрастет лапшой.


В итоге посмотрим, чем обрастет ваш await…

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


Может и не требуется, а может и требуется. Герб Саттер бы на вас посмотрел, как на неофита за такое, например. Таски-то в отдельном потоке, будьте добры синхронизировать.
А в чем практический смысл данного кода? Как мы будем сравнивать два бесполезных кода? Если я сделаю точно такой же вывод в консоль этого будет достаточно? Подозреваю что нет.
И что с чем нужно синхронизировать? Я что то не вижу чтобы 2 потока использовали один ресурс.
И куда делся select? Вы опять пытаетесь обмануть меня? С жуликами мне нет желания общаться.
А в чем практический смысл данного кода? Как мы будем сравнивать два бесполезных кода?


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

Если я сделаю точно такой же вывод в консоль этого будет достаточно?


Однозначно нет. Нужно реализовать аналогичный паттерн. Если распарсить происходящее в коде для вас слишком сложно, поясню:

— В отдельном потоке генерятся задачи (для простоты тупо целое число) и складываются в очередь обработки. Очередь ограничена 4 задачами, в случае, если задач в ожидающем состоянии более 4, генерящий поток приостанавливается.
— Второй поток обрабатывает задачи, по 1 штуке в 2 секунды. Параллельные обработчики не стартуют.
— Генератор задач генерит ровно 10 штук, после этого оба потока закрываются, программа завершается корректно.
— Для простоты демонстрации задача ограничена тупым выводом полученного значения.

Покажете, насколько проще это реализуется в C#? Или вы не только Go-код парсить не умеете, но и C#-код писать не пробовали?

И что с чем нужно синхронизировать? Я что то не вижу чтобы 2 потока использовали один ресурс.


Вы плохо смотрели. Оба потока используют 1 канал, который и есть примитив синхронизации.

И куда делся select? Вы опять пытаетесь обмануть меня?


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

Остается только заподозрить, что «чукча не писатель» (в смысле написания кода), «чукча» просто диванный аналитик, который сам код не пишет, но точно знает, как его надо писать. Вы просто выше этой низменной суеты с реализацией? Просто мыслите высокими материями и не мараетесь о практику, да?

С жуликами мне нет желания общаться.


Я предоставил асинхронный код, вы — нет. Вопрос о том, кто же из нас жулик, остается открытым, видимо.
Зачем мне реализовывать паттерн на с# который используется в go? Это неинтересно. Интересно решить задачу, но задачи как таковой нет, вместо этого есть бесмысленный код. Я могу написать с использованием каналов, но это будет примерно такой же код. Могу написать с использованием Тасков, но он конечно получится другой, как мы будем сравнить разный код?

«чукча» просто диванный аналитик

После этих слов идешь ты в далекое путешествие, гошник упоротый
После этих слов идешь ты в далекое путешествие, гошник упоротый


Ad hominem, ad personam… Старо как мир, считалось порочной практикой еще древнеримскими риторами.

Зачем мне реализовывать паттерн на с# который используется в go?


Паттерн синхронизации «канал», простите, используется в Go? Вы же сами говорили, что в C# «завезли каналы»? Или их не для того, чтобы использовать, «завезли»?

Или вы про очередь сообщений? Тоже в C# не принято? Везде можно, а именно в C# «не принято»? У меня для вас плохие новости…

Интересно решить задачу


Ну вот она, как бы, есть, я даже описал. Покажите «более удобное и изящное» решение на C#, вы обещали.

Я могу написать с использованием каналов, но это будет примерно такой же код


Ну вот и напишите. Мы сможем сделать аргументированное сравнение…

Могу написать с использованием Тасков, но он конечно получится другой, как мы будем сравнить разный код?


А вот тут я вас не понял… Каким образом каналы и таски у вас стали, вдруг, взаимоисключающими вещами? Вы реально хотели сделать решение с каналами, но без тасков? И каким же, простите, образом, вы будете генератор задач в отдельном потоке держать?

Короче, видимо, вашего кода я не увижу. Вы только декларируете, пока что, что «можете» все-все-все написать, честно-честно, и я вам, видимо, должен верить на слово. И даже после того, как вы собираетесь демонстрировать многопоточное исполнение, не используя Task.Run()…
Какую проблему решает ваш код? Да никакую. Часть вычислений выполняется в одном месте, а продолжается в другом. Я бы написал все в одном месте. Кроме как продемонстрировать использование каналов, никакого смысла в этом коде нет, использование каналов в этом примере притянуто за уши.
У нас решение задачи начинается с описания проблем. Проблему можно решить разными способами, как показывает практика, разные разработчики решают задачи разными способами. Вы же изучили новую фишку и теперь везде пытаетесь ее применить. Так поступают новички.
Да, кода от меня вы не увидите, потому что проблема, которую надо решить нет. Я в отличие от вас, попробовал написать код и на go и на c#, я попробовал использовать каналы и там и там. Я даже попробовал использовать таски, бесконечную коллекцию с помощью yield, очередь и различные комбинации этого. Конкретно в этом примере использование каналов для c# не нужно.
Ну, т.е., кода не будет? Ничего другого я и не ожидал.

Кроме как продемонстрировать использование каналов, никакого смысла в этом коде нет


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

У нас решение задачи начинается с описания проблем. Проблему можно решить разными способами, как показывает практика, разные разработчики решают задачи разными способами. Вы же изучили новую фишку и теперь везде пытаетесь ее применить. Так поступают новички.


Опять «растекаемся мыслью по древу», вместо того, чтобы продемонстрировать код… Вы попросили у меня код, демонстрирующий удобство применения гошной модели асинхронности для того, чтобы иметь возможность показать, что на C# то же самое можно сделать лучше и удобнее. Я код дал, где ваш?

Да, кода от меня вы не увидите


Почему-то я не удивлен…

потому что проблема, которую надо решить нет.


Есть. Прямо «от исходного» — продемонстрировать удобство применения модели асинхронности языка. Мой код эту задачу выполняет, ваш… ах, да, его нету!

И не надо мне рассказывать, что задача «собрать очередь исполнения не более 4 задач из N источников и обработать поочередно в один поток» — это что-то Go-специфичное. Это совершенно адекватная и нормальная задача серверного кода, которому необходимо предсказуемо работать в ограниченных условиях серверного окружения.

Я в отличие от вас, попробовал написать код и на go и на c#, я попробовал использовать каналы и там и там. Я даже попробовал использовать таски, бесконечную коллекцию с помощью yield, очередь и различные комбинации этого.


Я знаю карате, джиу-джитсу, кунг-фу, самбо, тейквондо и… много других страшных слов. Ага, проходили, знаем.

Конкретно в этом примере использование каналов для c# не нужно.


Конечно же, в многопоточном коде использование примитивов синхронизации не нужно… Это же любому ребенку понятно: C# не такой язык, как все остальные. Везде требуется синхронизация, а C# избавлен от этих пережитков прошлого…
задача «собрать очередь исполнения не более 4 задач из N источников и обработать поочередно в один поток»

Ага, так то лучше. Получается нам данные нужно получить извне ( I/O bound) и обработать в один поток (CPU bound). Тогда как-то так:
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading;

namespace TestSharp
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var val = 0;
            var q = new Queue<Task<int>>();

            Func<Task<int>> getData = () =>
            {
                val++;
                Console.WriteLine($"Put : {val}");
                return Task.FromResult(val);
            };

            for (var i = 0; i < 10 + 3; i++)
            {
                if (i < 10)
                    q.Enqueue(getData());

                if (i >= 3)
                {
                    await Task.Delay(2000);
                    Console.WriteLine($"Work: {await q.Dequeue()}");
                }
            }
        }
    }
}
Получается нам данные нужно получить извне ( I/O bound) и обработать в один поток (CPU bound).


Нет, не так. Нам данные нужно получить из одного потока (CPU bound) и обработать в другом (тоже CPU bound).

На всякий случай, напомню постановку задачи:

— В отдельном потоке генерятся задачи (для простоты тупо целое число) и складываются в очередь обработки. Очередь ограничена 4 задачами, в случае, если задач в ожидающем состоянии более 4, генерящий поток приостанавливается.
— Второй поток обрабатывает задачи, по 1 штуке в 2 секунды. Параллельные обработчики не стартуют.
— Генератор задач генерит ровно 10 штук, после этого оба потока закрываются, программа завершается корректно.
— Для простоты демонстрации задача ограничена тупым выводом полученного значения.


Ваш код:

1. Однопоточный.
2. Почему-то количество сгенерированных задач выносит из логики генератора задач в общий поток. Т.е. вместо того, чтобы обработать все задачи, которые отдаст генератор, просто пинает генератор 10 раз.
3. «for (var i = 0; i < 10 + 3; i++)» — вот это, простите, «пахнет».
4. Генератор, начиная с 3-й итерации, для чего-то ждет, пока отработает воркер…
5. Самое главное: вообще никак не изящнее, чем go-шный вариант. Вы просто облепили async/await кучей костылей, чтобы это как-то работало.

Так что, простите, конечно, но этот код «пахнет». Собственно, сама идея «сгенерировать 10 задач в целях демонстрации, сложить их в очередь не более 3» выразить через «for (var i = 0; i < 10 + 3; i++)» — вы делаете всем больно.
Половину биткоина намайнил в одном потоке, а вторую спустя время во втором потоке? Это не задача, это извращение. Что это значит «генерить задачи»? Зачем генерить задачи в одном потоке, а решать в другом? Я бы решал задачу в том же потоке, в котором она генерится.
Я вот хороший пример привел с последовательным и параллельным получением данных. Go проиграл в чистую. Вы же который день не можете придумать реальную задачу.
Я бы решал задачу в том же потоке, в котором она генерится.


Поэтому вам, вероятно, и не доверяют решать задачи?

Половину биткоина намайнил в одном потоке, а вторую спустя время во втором потоке? Это не задача, это извращение. Что это значит «генерить задачи»? Зачем генерить задачи в одном потоке, а решать в другом?


Ну конечно, вы не понимаете, зачем это… а значит, оно и не нужно? Представьте, у вас сервер, обрабатывающий какие-то конкуррентные запросы от клиентов. Вы реально предлагаете не отвечать одному клиенту, пока сервер считает ответ для другого? Или вы на каждый запрос будете поток поднимать? Удачи вам, собственно…

Я вот хороший пример привел с последовательным и параллельным получением данных. Go проиграл в чистую. Вы же который день не можете придумать реальную задачу.


Я вам дал реальную задачу, которую вы «решили» куском достаточно дурно-пахнущего кода, при этом однопоточного. Ну, т.е., не решили.

Да и сами ваши потуги «уложить» задачу в ваш «божественный async/await»… Ну, видимо, в C# завезли потоки, но вы не знаете, как ими пользоваться.

Вы декларируете «модель асинхронности в C# на голову круче и изящнее, чем в Go», а потом демонстрируете свое высказывание куском кода, обвешанного костылями, ставящего раком весь поток на время «await Task.Delay(2000);». Гениально! ТакЪ победимЪ.
Thread.Sleep() блокирует поток, await Task.Delay() не блокирует.
Но вам этого никогда не понять. Говнокод ваш я тоже уже увидел. Спорить дальше не о чем.
Thread.Sleep() блокирует поток, await Task.Delay() не блокирует.


И чем же занят ваш основной поток, пока в фоновом крутится счетчик милисекунд? Вы же мне сейчас «откроете глаза»?

Говнокод ваш я тоже уже увидел.


«for (var i = 0; i < 10 + 3; i++)» — это ваш код. Покажите мне аналогичную мерзость в моем.
пока в фоновом крутится счетчик милисекунд?

)))) Сами придумали или кто-то подсказал?
Вы же мне сейчас «откроете глаза»?

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

Заодно все сильнее крепнут сомнения в том, что вы в принципе способны «родить» сколько-нибудь качественный код…

await Task.Delay() не блокирует поток, но вот этот поток, где выполнялся Main, будет выполнять Task.Delay(). Хоть у вас и два асинхронных метода, но они синхронизированы await'ом.
То есть:


await Task.Delay(2000);  // вот тут вы не запустили `Task.Delay` в отдельном потоке, то есть тут вы реально будет ждать
Console.WriteLine($"Work: {await q.Dequeue()}");

Вы же сами доку кидали на это.

Вы же сами доку кидали на это


Ну что же вы со своей «докой» в разговор о возвышенном вклиниваетесь! Неужели непонятно, что в C# совершенно более другие системные потоки, и асинхронность у него такая, которая никогда не блокируется. И вообще он весь состоит из дружбомагии и работает поверх цветочной пыльцы же.

Не разрушайте представления человека о прекрасном (даже если они выглядят как «for (var i = 0; i < 10 + 3; i++)»)! Это разобьет его сердце.)))
но вот этот поток, где выполнялся Main, будет выполнять Task.Delay()

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

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

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TestSharp
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine($"Start thread: {Thread.CurrentThread.ManagedThreadId}");
            await Task.Delay(1);
            Console.WriteLine($"Continue1 thread: {Thread.CurrentThread.ManagedThreadId}");
            for (var i = 0; i < 100; i++)
            {
                var val = i;
                var x = Task.Delay(1000 + i * 10).ContinueWith((t) => Console.WriteLine($"Run: {val} Thread: {Thread.CurrentThread.ManagedThreadId}"));
            }
            Console.WriteLine($"Continue2 thread: {Thread.CurrentThread.ManagedThreadId}");
            await Task.Delay(2000);
            Console.WriteLine($"End thread: {Thread.CurrentThread.ManagedThreadId}");
        }
    }
}

А чего сейчас у вас нету await в цикле?

Потому что его можно писать, а можно не писать, в разных случаях разное поведение. Я в данном случае решил не писать.
Потому что его можно писать, а можно не писать


Отличное ключевое слово в языке. Можно писать, а можно не писать…

в разных случаях разное поведение


Вооот. Вы демонстрируете свое толкование одного поведения примером другого поведения…

Я в данном случае решил не писать


Вы в данном случае решили показать что-то другое, не относящееся к обсуждаемому примеру.
Так зачем вы привели другой код? У вас же в первом варианте `await Task.Delay(2000);` в цикле.
Этот код не имеет никакого отношения к тому коду. Т.к. вы оба не понимаете как оно всё работает и думаете что блокируется поток, я сделал специально для вас этот пример.
Мы не думаем, мы знаем, что блокируется поток исполнения. Блокируется ли системный поток — отдельный вопрос, и зависит от реализации. А поток исполнения блокируется.
Вот вам дока — docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/await
The await operator is applied to a task in an asynchronous method to insert a suspension point in the execution of the method until the awaited task completes.

Все как я и говорил вот тут
await Task.Delay(2000);  // вот тут вы не запустили `Task.Delay` в отдельном потоке, то есть тут вы реально будет ждать
Console.WriteLine($"Work: {await q.Dequeue()}");
Я не понял, для чего запускать Task.Delay() в отдельном потоке? Я добавил эту паузу т.к. в исходном коде на go тоже была пауза 2 сек.
Вы до сих пор не поняли как await работает? Дока же есть, почитайте.
А ну ка, приведите код, как запускать Task.Delay() в отдельном потоке, просвятите незнающих)))

Причем тут это.
Вот скажите, какая разница во времени будет?


using System;
using System.Threading;
using System.Threading.Tasks;

namespace TestSharp
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine(DateTime.Now.ToString("HH:mm:ss"));

            await Task.Delay(10000);

            Console.WriteLine(DateTime.Now.ToString("HH:mm:ss"));

        }
    }
}
То есть await синхронизирует два метода и выполняются они в одном потоке. Все верно или нет?
Это как повезет, могут и в одном, могут и в разных. А про какие именно два метода идет речь?
Вызываемый и вызывающий, которые await синхронизирует
Прямо в спецификации, простите, так и написано: «как повезет»? Серьезно? )))

В доке, на которую вы ссылаетесь (а вы ее точно читали?), например, написано следующее:

«Ключевые слова async и await не вызывают создания дополнительных потоков. Асинхронные методы не требуют многопоточности, поскольку асинхронный метод не выполняется в собственном потоке.»

И вот это еще:

«Метод Task.Run можно применять для перемещения операций, использующих ресурсы ЦП, в фоновый поток, однако фоновый поток не имеет смысла применять для процесса, который просто ждет результата.»

Сорян, конечно, но никаких «как повезет». Task.Run — в отдельном. Остальное — в том же.
Вы в очередной раз не поняли как это все работает. Хотя по поводу блокируется ли поток ОС уже сомневаетесь))
Ещё раз с самого начала.
Асинхронная модель предполагает переиспользование потоков ОС. Как раз в местах где написано await и происходит остановка выполнения метода и этот поток может быть использован другой задачей. Соответственно когда мы дождемся результата и метод должен будет продолжить свою работу, первоначальный поток может быть занят другой задачей. Продолжение будет выполняться на любом потоке из пула потоков. Не всегда происходит остановка выполнения в месте использования await, например когда результат уже получен. Это как раз наш случай, Task.FromResult() сразу возвращает результат.
Аналогично вы пишите пургу и в других комментариях, поэтому я не буду писать ответы на все комментарии.
Вы в очередной раз не поняли как это все работает


«Это как повезет» — ваши слова. И они говорят о том, что это вы не понимаете, как оно работает.

Читаем официальную доку, на которую вы сами принесли ссылку:

«Ключевые слова async и await не вызывают создания дополнительных потоков. Асинхронные методы не требуют многопоточности, поскольку асинхронный метод не выполняется в собственном потоке. Метод выполняется в текущем контексте синхронизации и использует время в потоке, только когда метод активен.»

Вслух прочтите, медленно. Слов «как повезет» и даже близких к ним по значению в доке нету. Есть ровно вот эти слова: «асинхронный метод не выполняется в собственном потоке» и «Метод выполняется в текущем контексте синхронизации».

Еще раз повторю: async и await никогда не запускаются в отдельном потоке.

В отдельном потоке запускается Task.Run(). Вот это написано в доке:

«Метод Task.Run можно применять для перемещения операций, использующих ресурсы ЦП, в фоновый поток,»

Достаточно же ясно сказано: Task.Run() — в фоновом потоке, async/await — в текущем.

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


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

Вот прямо в ваш код посмотрите уже. Я вам даже для удобства дебажный вывод добавил. Чтобы вы долго не искали, еще раз даже сюда скопирую:

using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading;

namespace TestSharp
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var val = 0;
            var q = new Queue<Task<int>>();

            Func<Task<int>> getData = () =>
            {
                val++;
                Console.WriteLine($"Put : {val} : {Thread.CurrentThread.ManagedThreadId}");
                return Task.FromResult(val);
            };

            for (var i = 0; i < 10 + 3; i++)
            {
                if (i < 10)
                    q.Enqueue(getData());

                if (i >= 3)
                {
                    Console.WriteLine($"Delay here : {Thread.CurrentThread.ManagedThreadId}");
                    await Task.Delay(2000);
                    Console.WriteLine($"Work: {await q.Dequeue()} : {Thread.CurrentThread.ManagedThreadId}");
                }
            }
        }
    }
}


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

Put : 1 : 1
Put : 2 : 1
Put : 3 : 1
Put : 4 : 1
Delay here : 1
Work: 1 : 4
Put : 5 : 4
Delay here : 4
Work: 2 : 4
Put : 6 : 4
Delay here : 4
Work: 3 : 4
Put : 7 : 4
Delay here : 4
Work: 4 : 4
Put : 8 : 4
Delay here : 4
Work: 5 : 4
Put : 9 : 4
Delay here : 4
Work: 6 : 4
Put : 10 : 4
Delay here : 4
Work: 7 : 4
Delay here : 4
Work: 8 : 4
Delay here : 4
Work: 9 : 4
Delay here : 4
Work: 10 : 4


Вот прямо видно же, что код исполняется тупо последовательно, и единственная вещь, которая вызывает переключение контекста — «await Task.Delay(2000);». Причем только в первый раз. Достаточно очевидно, надеюсь, почему? И понятно, почему в последующие разы вы из 4-го ManagedThread больше не выпадаете?
Я же написал почему конкретно в этом примере так, потому что там используется Task.FromResult(). В реальной программе вместо getData() был бы настоящий i/o метод и все было бы красиво. Вы свой код упростили для демонстрации паттерна, я тоже упростил.
Я же написал почему конкретно в этом примере так, потому что там используется Task.FromResult().


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

В реальной программе вместо getData() был бы настоящий i/o метод


Откуда вы вообще взяли требование i/o метода?

и все было бы красиво.


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

Вы свой код упростили для демонстрации паттерна, я тоже упростил.


Вы не код упростили, а задачу «выродили». У вас обработчик, по факту, запрашивает новые задачи. При этом еще ждет, пока очередь заполнится, прежде чем стартовать.

Каким образом задача «положить в очередь в отдельном потоке, обработать из очереди в отдельном» вдруг выродилась в «последовательно сгенерировать задачу на месте, положить в конец очереди, выполнить первую из очереди, повторять до просветления».

Весть смысл паттерна был в том, чтобы «мухи отдельно, котлеты отдельно». Пока обработчик пережевывает первую задачу, ему еще 4 падает в очередь. Параллельно, не мешая ему работать — понимаете, в чем суть?

Ваш же код последовательно складывает 4 задачи в очередь, а потом начинает всю эту фантасмагорию «положить 1 задачу, выполнить 1 задачу, дождаться, пока выполнится, положить еще одну, вытащить ровно одну».

Эта чушь работает более-менее эффективно до тех пор, пока вам известно фиксированное время обработки задачи воркером и фиксированное время получения задачи. При этом желательно знать количество получаемых задач заранее. И тогда, заботливой и нежной настройкой 3-4 констант, раскиданных по разным кускам кода вы сможете добиться какого-то примелемого результата.

Вот не бывает в реальной жизни такого. Вы никогда не знаете заранее, сколько задач упадет в очередь и через какие промежутки времени они будут туда поступать. Вы не знаете, чаще всего, сколько времени будет обрабатываться задача. Даже если у вас есть эмпирические данные насчет среднего времени выполнения, вы никогда не сможете его гарантировать — C# вообще не подходит для realtime-вещей.

Поэтому воркер на то и воркер, чтобы обрабатывать ровно столько задач, сколько у него их есть в очереди, и терпеливо ждать новых, если вдруг задач не осталось. Он даже длину этой очереди знать не должен — потому что нарушение инкапсуляции и tight couplingю У вас с какого-то перепугу воркер знает длину очереди и конечное количество задач, которое туда упадет.

При этом ваш код работает по принципу «тик-так». Одну задачу в очередь положил, тут же с другого конца вынул. Вставьте рандомную задержку в генератор задач, и производительность вашего кода упадет чуть ниже плинтуса. Ваш генератор не работает, если воркер занят. Ваш воркер не может работать, пока генератор не отдал задачу. И нафига, простите, такая асинхронность? Ровно с тем же успехом можно просто написать линейный алгоритм, который будет делать то же самое, только без оценок необходимости переключения контекста.

Вы не код упростили, вы изначальную задачу «выродили» в линейный код.
Если задача что данные приходят откуда то, то и напишите соответствующий код. Вы же написали «val++» остальное нужно дофантозировать. Я тоже сократил немного, хотите большего, включите фантазию))
Задача была достаточно простая и конкретная: «в одном потоке складываем задачи в очередь, в другом достаем и обрабатываем». Из этой задачи вы сократили половину.
Я не понял, для чего запускать Task.Delay() в отдельном потоке?


Видимо, затем, что в текущем потоке его запустить не получится? )))

Прямо в вашем коде:

До первого await Task.Delay() все выполняется в ManagedThread 1. После него оставшаяся часть попадает в ManagedThread 4. Подсказать, почему? Или сами про системные таймеры погуглите?

При этом после первого await Task.Delay оставшийся код попадает в continuation, что логично, как и было обещано.

А вот то, что после следующего вызора await Task.Delay номер ManagedThread больше не меняется, свидетельствует о том, что системный таймер так и крутится в фоновом потоке, а основной поток программы при последующих await'ах тупо попадает в Thread.Sleep, т.е. блокируется.
Покажите как «правильно» запускать Task.Delay().
Правильно запускать Task.Delay() именно так, как вы его запустили: `await Task.Delay(2000);`. Неправильно утверждать, что он у вас в текущем потоке запустился — это неправда.

Системный таймер — это всегда отдельный поток. Как бы вы ни пытались смеяться над моим «пока в отдельном потоке тикает таймер», он именно это и делает. Посмотрите на дебажный вывод вашего кода, который я добавил, и подумайте уже, наконец, почему `await Task.Delay(2000);` вызывается из ManagedThread-1, а continuation выполняется в ManagedThread-4.

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


В доке написано, что при await поток не меняется. Никогда. Кто лжет, дока или вы?

Честно говоря лучше бы вообще все забыть что вы тут навыдумывали


Не съезжайте с темы, ответьте на вопрос выше.
Я уже ответил ранее, хотите верьте, хотите проверьте.
Там вон целые книги пишут, конечно в статье многого не написано. А я написал самую малую часть того. Все что я напишу в нескольких предложениях — это не будет соответствовать в полной мере тому как оно на самом деле работает.
Я уже ответил ранее, хотите верьте, хотите проверьте.


Вот откуда вас таких берут, а? На каком основании я должен верить вам, если в официальной документации написано прямо противоположное? Я же вам уже прямо официальную доку процитировал, прямо нужное место, чтобы вам, не дай Б-г не пришлось читать ее целиком. Там прямо написано, что вы ошибаетесь.

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

Там вон целые книги пишут, конечно в статье многого не написано. А я написал самую малую часть того.


Ну хватит уже отмазываться про то «что в статье не написано». Важно то, что написано. И этого достаточно, т.к. там написано, что await не вызывает переключение потоков, никогда.

Все что я напишу в нескольких предложениях — это не будет соответствовать в полной мере тому как оно на самом деле работает.


«не будет соответствовать в полной мере» — это какая-то слишком мягкая формулировка, «не соответствующая в полной мере» действительному положению вещей. То, что вы пишете, прямо противоречит тому, как оно на самом деле работает.
То, что вы пишете, прямо противоречит тому, как оно на самом деле работает.


А вы сами проверьте:
Console.WriteLine($"thread {Thread.CurrentThread.ManagedThreadId}");

for (var i = 0; i < 100; i++)
{
    var http = new HttpClient();
    var r = await http.GetAsync("https://google.com/");
    Console.WriteLine($"thread {Thread.CurrentThread.ManagedThreadId}");
}


а потом попробуйте вместо «await http.GetAsync()» использовать «await Task.FromResult(1)» и расскажите нам почему так происходит. У вас есть удивительная способность находить объяснения всему, даже чего вы не знаете. Правда логика в этих объяснениях отсутствует, но зато это весело))

И наконец-то признайтесь что все это время писали чушь.
и расскажите нам почему так происходит


Видимо, придется таки рассказать.

HttpClient.GetAsync() относится к «потокобезопасным» методам. Можете сходить в доку по этому классу и ознакомиться со списком потокобезопасных методов:
собственно дока

Потокобезопасность может быть достигнута всего двумя способами:

1. В атомарном методе (однозначно не наш кейс)
2. При наличии синхронизации состояния внтури метода.

Вы сейчас удивитесь, вероятно, но переключение потока происходит внутри метода HttpClient.GetAsync(). Это там явно запускается Task.Run(). Там же на него вешается ContinueWith, и там же внутри есть await, синхронизирующий состояние.

await — примитив синхронизации. Он не может порождать переключение потоков. Потоки порождает только Task.Run() — читайте еще раз доку.

В Task.FromResult(), что очевидно, Task.Run внутри не содержится, поэтому переключения потоков не происходит.

Т.е. запоминайте:

1. await не порождает потоки.
2. асинхронные методы внутри могут содержать Task.Run, а могут не содержать.
3. Те методы, которые содержат, порождают поток, и ваш await падает в continuation тому потоку, в котором завершился метод. Не await поток переключает, а Task.Run внутри метода.
4. Те методы, которые не содержат Task.Run, выполняются в том же потоке, из которого были вызваны.

Т.е., для совсем уж маленьких:

1. Ваше «как повезет» — бред. Так не бывает.
2. HttpClient.GetAsync() всегда выполняется в фоновом потоке, и ваш код await падает в continuation тому потоку, в котором завершился GetAsync.
3. Task.FromResult() всегда выполнится ровно в том же потоке, в котором его вызвали.

И наконец-то признайтесь что все это время писали чушь.


Просто признайтесь, что вы нихрена не понимаете ни в асинхронности, ни в многопоточности, и перестаньте позориться. Документацию лучше почитайте.
Потокобезопасность означает что объект можно использовать одновременно из разных потоков. Например в пространстве System.Collections.Concurrent есть потокобезпаосные аналоги коллекций из System.Collections. Да короче сами читайте docs.microsoft.com/ru-ru/dotnet/api/system.collections.concurrent?view=netcore-2.2
Но вам это не поможет, горбатого могила исправит.

1. await не порождает потоки.

Я никогда не говорит, что await порождает потоки. Я говорил, что после await продолжение может выполняться в другом потоке. Все задачи выполняются в пуле потоков, напрямую потоки мы не запускаем, даже с помощью Task.Run().

4. Те методы, которые не содержат Task.Run, выполняются в том же потоке, из которого были вызваны.

Эта ваша выдумка. Ожидание не выполняется ни в каком потоке. Смотрите класс TaskCompletionSource например, ожидание можно вообще реализовать без потоков.

Давайте жгите еще, я с радостью в очередной раз скажу, что вы не разбираетесь ни в чем.

Я никогда не говорит, что await порождает потоки. Я говорил, что после await продолжение может выполняться в другом потоке. Все задачи выполняются в пуле потоков, напрямую потоки мы не запускаем, даже с помощью Task.Run().


Ну ведь чушь же. В документации прямо написано, что чушь.

после await продолжение может выполняться в другом потоке


После await продолжение (continuation) будет выполняться ровно в том потоке, результат которого мы ждем.

Все задачи выполняются в пуле потоков


Все задачи выполняются в одном потоке, до тех пор, пока мы явно с использованием Task.Run() не укажем выполнять какую-то из них в фоновом.

Давайте жгите еще, я с радостью в очередной раз скажу, что вы не разбираетесь ни в чем.


Давайте, жгу, вот код:

using System;
using System.Threading.Tasks;
using System.Threading;

namespace TestSharp
{
    class Program
    {

        static Func<Task<int>> JustFunc(int val = 0)
        {
            return new Func<Task<int>>(async () =>
                {
                    Console.WriteLine($"JustFunc({val}) start: {Thread.CurrentThread.ManagedThreadId}");
                    return 0;
                });
        }
        static async Task<int> NoTaskRunInside(int val = 0)
        {
            Console.WriteLine($"NoTaskRunInside({val}) start: {Thread.CurrentThread.ManagedThreadId}");
            return 1;
        }

        static async Task<int> OneThreadOnly(bool any)
        {
            Console.WriteLine($"OneThreadOnly start: {Thread.CurrentThread.ManagedThreadId}");
            var task1 = NoTaskRunInside(21);
            Console.WriteLine($"OneThreadOnly task1 started: {Thread.CurrentThread.ManagedThreadId}");
            var task2 = NoTaskRunInside(22);
            Console.WriteLine($"OneThreadOnly task2 started: {Thread.CurrentThread.ManagedThreadId}");
            var task3 = NoTaskRunInside(23);
            var task4 = NoTaskRunInside(24);
            var task5 = NoTaskRunInside(25);
            if (any)
            {
                return Task.WaitAny(task1, task2, task3, task4, task5);
            }
            else
            {
                Task.WaitAll(task1, task2, task3, task4, task5);
                return 0;
            }
        }

        static async Task<int> WithTaskRunInside()
        {
            Console.WriteLine($"WithTaskRunInside start: {Thread.CurrentThread.ManagedThreadId}");
            var intTask = Task.Run(JustFunc(1));
            Console.WriteLine($"WithTaskRunInside before sync: {Thread.CurrentThread.ManagedThreadId}");
            var result = await intTask;
            Console.WriteLine($"WithTaskRunInside after sync (continuation): {Thread.CurrentThread.ManagedThreadId}");
            return result;
        }

        static async Task<int> WithTwoTaskRunInside()
        {
            Console.WriteLine($"WithTwoTaskRunInside start: {Thread.CurrentThread.ManagedThreadId}");
            var task1 = Task.Run(JustFunc(11));
            Console.WriteLine($"WithTwoTaskRunInside task1 started: {Thread.CurrentThread.ManagedThreadId}");
            var task2 = Task.Run(JustFunc(12));
            Console.WriteLine($"WithTwoTaskRunInside task2 started: {Thread.CurrentThread.ManagedThreadId}");
            var intVal1 = await task1;
            Console.WriteLine($"WithTwoTaskRunInside task1 sync: {Thread.CurrentThread.ManagedThreadId}");
            var intVal2 = await task2;
            Console.WriteLine($"WithTwoTaskRunInside task2 sync: {Thread.CurrentThread.ManagedThreadId}");
            return intVal1 + intVal2;
        }
        
        static async Task Main(string[] args)
        {
            Console.WriteLine($"Begin here    : {Thread.CurrentThread.ManagedThreadId}");
            int val1 = await WithTaskRunInside();
            Console.WriteLine($"Returned WithTaskRunInside : {Thread.CurrentThread.ManagedThreadId}");
            int val0 = await NoTaskRunInside();
            Console.WriteLine($"Returned NoTaskRunInside : {val0} : {Thread.CurrentThread.ManagedThreadId}");
            int val2 = await WithTwoTaskRunInside();
            Console.WriteLine($"Returned NoTaskRunInside : {val2} : {Thread.CurrentThread.ManagedThreadId}");
            int val00 = await NoTaskRunInside();
            Console.WriteLine($"Returned NoTaskRunInside : {val00} : {Thread.CurrentThread.ManagedThreadId}");
            int valAny = await OneThreadOnly(true);
            Console.WriteLine($"Returned OneThreadOnly-WaitAny : {Thread.CurrentThread.ManagedThreadId}");
            int valAll = await OneThreadOnly(false);
            Console.WriteLine($"Returned OneThreadOnly-WaitAll : {Thread.CurrentThread.ManagedThreadId}");
        }
    }
}


Я смотрю в его «выхлоп» и вижу ровно то, о чем говорится в документации, корректность которой вы отрицаете.

WithTaskRunInside — метод демонстрирующий, в какой момент происходит переключение потока. Обратите внимание, строчка перед Task.Run выполняется в первом потоке. С момента Task.Run мы имеем два параллельных потока: запущенная задача выполняется параллельно коду метода до await'а. Все, что после await — это continuation, и оно выполняется ровно в том же потоке, что и запущенная задача, которую мы ожидаем. Ровно то, о чем говорю я и документация.

Никаких «как повезет», какой поток ждем, в том и выполняется continuation. При этом никаких " Все задачи выполняются в пуле потоков". В отдельном потоке выполняется только явно запущенная в фоновом потоке задача. С момента await код однопоточный.

NoTaskRunInside — ровно такой же асинхронный метод, не содержащий внутри Task.Run. Выполняется в том потоке, откуда его вызвали, возвращается в тот же поток. Никакого «исполнения в пуле потоков».

WithTwoTaskRunInside — метод, демонстрирующий 2 параллельных вызова в фоновых потоках и последующую синхронизацию. Каждая из явно запущенных параллельно задач выполняется в своем потоке. Continuation выполняется ровно в том потоке, которого мы await'им. Соответственно, из метода мы возвращаемся ровно в тот поток, с которым синхронизировались последним.

OneThreadOnly — метод, запускающий асинхронно 5(пять) задач, и ожидающий либо завершения первой из них, либо завершения всех. Обратите внимание, ни о каком «пуле потоков» речи не идет, все 5 задач выполняются строго в одном потоке.

Так что, пожалуйста, перестаньте нести чушь. Смиритесь: документация права, а вам должно быть стыдно, что go-шник лучше вас понимает модель асинхронности dotnet.
После await продолжение (continuation) будет выполняться ровно в том потоке, результат которого мы ждем.

Вы же код тот запустили с http? Там выводится разный идентификатор потока, железное доказательоство.

Смиритесь: документация права, а вам должно быть стыдно, что go-шник лучше вас понимает модель асинхронности dotnet.

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

А вторых я не дотнетчик, и нигде об этом не писал. Я ни одной строчки не написал продуктивного кода на c#.

NoTaskRunInside — ровно такой же асинхронный метод, не содержащий внутри Task.Run. Выполняется в том потоке, откуда его вызвали, возвращается в тот же поток. Никакого «исполнения в пуле потоков».

Я другого никогда и не утверждал, этот как раз тот случай когда задачу не нужно выполнять, это тоже самое что и Task.FromResult(). Покажи где я утверждал обратное или я считаю тебя балаболом!

OneThreadOnly — метод, запускающий асинхронно 5(пять) задач, и ожидающий либо завершения первой из них, либо завершения всех. Обратите внимание, ни о каком «пуле потоков» речи не идет, все 5 задач выполняются строго в одном потоке.

Да ничего он не запускает, сначала разберитесь, потом пишите. Это случай аналогичный Task.FromResult(). Кстати вы 100% не этого не знаете, но их нужно избегать, но вы не знаете почему и возможно сейчас полезете в гугл))

Вы если приписываете мне какое-то утверждение, то будьте добры вставить цитату, иначе балабол.

Вы же код тот запустили с http? Там выводится разный идентификатор потока, железное доказательоство.


Чушь это, а не железное доказательство. Вы видели, что внутри у Http.GetAsync происходит? А почему из await Task.Delay идентификатор потока меняется задумывались? А метод WithTaskRunInside в моем примере вы смотрели? Там, так то, тоже на выходе из метода идентификатор потока меняется. Только он не в момент await меняется, а в момент Task.Run внутри метода.

У вас внутри http.GetAsync сам запрос в фоновом потоке выполняется. Всегда. Могу даже подсказать, почему. Чтобы таймауты работали. Там внутри Task.Run({GetResponse}) и Task.Run({Delay}), а потом состояние синхронизируется через Task.WaitAny. Т.е. один фоновый поток обрабатывает запрос, второй палит, не истек ли таймаут, Task.WaitAny ставит continuation в тот поток, который первым «отстрелялся». И никакой магии, никаких «как повезет».

Ровно такая же штука и с Task.Delay. Системный таймер всегда работает в фоновом потоке. В Go — в равноправной горутине, поэтому time.After(time.Second) даёт гарантии «не менее чем через 1 секунду». В dotnet'е гарантия «не более допустимой погрешности от точного времени», поэтому и в отдельном потоке.

Не может рантайм dotnet'а обрабатывать задачи в пуле потоков. Это физически невозможно. И в документации прямо сказано: «в текущем потоке». Никакого пула. Хочешь в пул — Task.Run.

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


Вы: При другом await поток также мог измениться.
Я: В доке написано, что при await поток не меняется. Никогда. Кто лжет, дока или вы?
Вы: Я уже ответил ранее, хотите верьте, хотите проверьте.

Документация: Ключевые слова async и await не вызывают создания дополнительных потоков. Асинхронные методы не требуют многопоточности, поскольку асинхронный метод не выполняется в собственном потоке. Метод выполняется в текущем контексте синхронизации и использует время в потоке, только когда метод активен. Метод Task.Run можно применять для перемещения операций, использующих ресурсы ЦП, в фоновый поток, однако фоновый поток не имеет смысла применять для процесса, который просто ждет результата.
Вы: Все задачи выполняются в пуле потоков, напрямую потоки мы не запускаем, даже с помощью Task.Run().

Документация: Асинхронные методы не требуют многопоточности, поскольку асинхронный метод не выполняется в собственном потоке.
Вы: Асинхронная модель предполагает переиспользование потоков ОС.
// Вот это, к слову, вообще, отборное, «из палаты мер и весов». Асинхронная модель, собственно, как правило ортогональна потокам ОС...

Документация: В сочетании с методом Task.Run асинхронное программирование лучше BackgroundWorker для операций, использующих ресурсы процессора, так как оно отделяет координацию выполнения кода от действий, которые Task.Run перемещает в пул потоков.
Вы: Как раз в местах где написано await и происходит остановка выполнения метода и этот поток может быть использован другой задачей. Соответственно когда мы дождемся результата и метод должен будет продолжить свою работу, первоначальный поток может быть занят другой задачей(бугага, не может).

Дока: Метод выполняется в текущем контексте синхронизации
Вы: Продолжение будет выполняться на любом потоке из пула потоков.

Еще поперечитывать, или хватит?
Вы: При другом await поток также мог измениться.
Я: В доке написано, что при await поток не меняется. Никогда. Кто лжет, дока или вы?
Вы: Я уже ответил ранее, хотите верьте, хотите проверьте.
— Документация: Ключевые слова async и await не вызывают создания дополнительных потоков. Асинхронные методы не требуют многопоточности, поскольку асинхронный метод не выполняется в собственном потоке. Метод выполняется в текущем контексте синхронизации и использует время в потоке, только когда метод активен.

И где тут написано про единственный поток? Вы трактуете документацию как-то неправильно.
А насчет контекста синхронизации забыли прочитать, что в консольном приложении он не используется и в asp.net core решили от него отказаться. До стольки лет дожили, а гуглом пользоваться не научились. Я вам в последний раз поищу. Вот например blog.stephencleary.com/2017/03/aspnetcore-synchronization-context.html

Например там написано:
With the contextless ASP.NET Core approach, when an asynchronous handler resumes execution, a thread is taken from the thread pool and executes the continuation.


На этом я завершаю свое участие. Думаю у вас так и не хватит смелости признаться в своей неправоте.
А насчет контекста синхронизации забыли прочитать, что в консольном приложении он не используется и в asp.net core решили от него отказаться.


Про «в ASP.NET Core решили отказаться» — вижу, ага. Про «в консольном приложении он не используется» — не вижу.

Вот это вижу: «However, I still recommend that you use it in your core libraries — anything that may be reused in other applications. If you have code in a library that may also run in a UI app, or legacy ASP.NET app, or anywhere else there may be a context, then you should still use ConfigureAwait(false) in that library.»

Я правильно понимаю, что async/await в ASP.NET Core теперь отличается от async/await в гуи-приложениях?

«There is one more major concern when moving from a synchronizing context to a thread pool context» — видимо, правильно понял…

Ничего, если выражу «искреннее удивление» сложившейся ситуацией? )))

Ничего, если еще одну статью вам принесу почитать? По поводу HttpClient.GetAsync кстати, и о том, почему дескриптор потока меняется?

вот эту.

Про то, как внутри HttpClient.GetStringAsync выглядит.

«The call to GetStringAsync() calls through lower-level .NET libraries (perhaps calling other async methods) until it reaches a P/Invoke interop call into a native networking library.»

Идем глубже: «After the System API call, the request is now in kernel space, making its way to the networking subsystem of the OS (such as /net in the Linux Kernel). Here the OS will handle the networking request asynchronously. „

И еще глубже: “For example, in Windows an OS thread makes a call to the network device driver and asks it to perform the networking operation via an Interrupt Request Packet (IRP) which represents the operation.»

И еще: «When the request is fulfilled and data comes back through the device driver, it notifies the CPU of new data received via an interrupt.»

«The result is queued up until the next available thread is able execute the async method and „unwrap“ the result of the completed task. Throughout this entire process, a key takeaway is that no thread is dedicated to running the task. „

Отлично, воот что такое I/O-bound операция. Она не занимает отдельного потока (номинально… на самом деле выполняется в отдельном потоке в пространстве ядра). Системное прерывание после этого обрабатывается первым попавшимся потоком из пула…

И вот отсюда рекомендация не использовать Task.Run() с I/O-bound операциями… Просто потому что (бугага, о чем я и говорил) HttpClient.GetAsync всегда де-факто работает в отдельном потоке. Ошибся я в том, что искал этот поток в пуле потоков (окей, век живи, век учись), а он вне пула потоков. Он в отдельном системном потоке в пространстве ядра выполняется.

Т.е. “опять двадцать пять», все равно не получается «как повезет». Continuation-то никуда не делся. И await http.GetAsync() всегда вернется в первый попавшийся свободный поток из пула.

И await Task.Delay() будет всегда приводить ровно к тому же результату. По абсолютно той же причине.

А дальше мы возвращаемся к CPU-bound коду. Там никакого переключения «внутри» между потоками нет… И вот тут все по прежнему: Task.Run — в фоновый поток, без Task.Run — в текущем. Чуда опять не случилось…
А теперь перепишем чуть-чуть ваш код:

using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading;

namespace TestSharp
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var val = 0;
            var q = new Queue<Task<int>>();

            Func<Task<int>> getData = () =>
            {
                val++;
                Console.WriteLine($"Put : {val} : {Thread.CurrentThread.ManagedThreadId}");
                return Task.FromResult(val);
            };

            for (var i = 0; i < 10 + 3; i++)
            {
                if (i < 10)
                    q.Enqueue(getData());

                if (i >= 3)
                {
                     Console.WriteLine($"Delay here : {Thread.CurrentThread.ManagedThreadId}");
                    await Task.Delay(2000);
                    Console.WriteLine($"Work: {await q.Dequeue()} : {Thread.CurrentThread.ManagedThreadId}");
                }
            }
        }
    }
}


И вот тут, видимо, вы уже увидите не то, что вы ожидаете. Код породил 4 ManagedThread, в каждом моменте времени выполняясь синхронно ровно в 1 поток.

После того, как вы увидите различие между «await Task.Delay(2000);» и «var x = Task.Delay(1000 + i * 10).ContinueWith(», вы, наконец, покажете мне код, в котором один поток складывает задачи в очередь, ограниченную по длине, а второй достает задачи из очереди и по очереди их отрабатывает?
наконец, покажете мне код, в котором один поток складывает задачи в очередь, ограниченную по длине, а второй достает задачи из очереди и по очереди их отрабатывает?

У вас наверное память короткая, но я напомню ваши слова:
задача «собрать очередь исполнения не более 4 задач из N источников и обработать поочередно в один поток»

Собрать данные из N источников — это I/O
обработать поочередно в один поток — это CPU

Т.к. I/O это в основном ожидание, то смысла никакого нет запускать такие задачи в отдельном потоке, об этом написано в той статье, а именно когда стоит использовать Task.Run().

в каждом моменте времени выполняясь синхронно ровно в 1 поток.

Все правильно, согласно следующему условию задачи:
обработать поочередно в один поток


«10+3» — написаны через + только потому, что на месте этих чисел должны были быть переменные.

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

Собрать данные из N источников — это I/O


Откуда такой вывод-то взялся? И откуда вообще требование «собрать данные из N источников»? У вас прямо в задаче сказано, что вы имеете очередь, в которую некий поток складывает задачи. А другой поток должен их обрабатывать.

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

Т.к. I/O это в основном ожидание, то смысла никакого нет запускать такие задачи в отдельном потоке, об этом написано в той статье, а именно когда стоит использовать Task.Run().


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

Просто запустите отдельный поток, и посмотрите, насколько удобно вам будет это делать.

Все правильно, согласно следующему условию задачи:
обработать поочередно в один поток


«Обработать поочередно в один поток» — совершенно верно. «Положить в очередь в отдельном потоке» вы почему-то проигнорировали.

«10+3» — написаны через + только потому, что на месте этих чисел должны были быть переменные.


Совершенно не важно, с какой целью написаны 10+3. В любом раскладе в Main'е им не место. В задаче же ясно было сказано: генератор задач генерит ровно 10 штук. Т.е. не «пнуть генератор 10 раз», а генератор должен вернуть ровно 10 задач.

В вашем «10+3»:
— «10» — часть логики генератора, добавленная в целях демонстрации, чтобы приложение было в принципе конечным. Поэтому в цикле запуска воркера ей вообще не место. Задачи могут поступать в неограниченном количестве, просто в целях демонстрации я обрезал их 10 штуками. Воркер, соответственно, должен отработать ровно столько штук, сколько ему в очередь упало, а потом тихо сдохнуть.
— "+3" — левый костыль, чтобы хоть как-то прикрутить ваш нежно любимый await туда, где ему делать нечего.

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


Вах-вах-вах, какая поза. Теперь берите свой комментарий с кодом и следите по цепочке вверх. А там…

Тадааааам! Постановка задачи:

— В отдельном потоке генерятся задачи (для простоты тупо целое число) и складываются в очередь обработки. Очередь ограничена 4 задачами, в случае, если задач в ожидающем состоянии более 4, генерящий поток приостанавливается.
— Второй поток обрабатывает задачи, по 1 штуке в 2 секунды. Параллельные обработчики не стартуют.
— Генератор задач генерит ровно 10 штук, после этого оба потока закрываются, программа завершается корректно.
— Для простоты демонстрации задача ограничена тупым выводом полученного значения.


И ваше «задача выполнена кратко и эффективно, полностью соответствуя условиям задачи.» превращается в пшик, заодно с обвинениями в жульнической натуре…

Ну и, собственно, по вашему коду:

1. Вы действительно считаете, что ваше «10+3» не «пахнет», и «все норм»?
2. Почему ваш воркер не начинает работать с первой задачи, а ждет, пока их станет не менее 3 в очереди? В задаче было прямо сказано, не более 4 задач, на каком основании вы выполняете условие «не менее 3». Прямо навевает:

public int getRandomInt() {
   // определено честным рандомным броском кубика
   return 4;
}


Нигде и никогда в условии не упоминалось «подождать, пока в очередь упадут хотя бы 3 задачи».

Еще раз: код «пахнет», сделайте с этим что-нибудь.
Функция getData() возвращает таск, а это значит она может выполнится в другом потоке, но для упрощения Task.Run я не писал. Если очень хочется красивый вывод можно написать что-то типа:
Func<Task<int>> getData = () =>
            {
                val++;
                var i = val;
                return Task.Run<int>(() => { Console.WriteLine($"Put : {i} : {Thread.CurrentThread.ManagedThreadId}"); return i; });
            };


В задаче было прямо сказано, не более 4 задач, на каком основании вы выполняете условие «не менее 3»

ой простите, у вас с математикой плохо? Вы оказывается школу еще не закончили? Или даже не ходили? Посмотрите еще внимательнее, счетчик начинается с 0 и обрабатываться очередь начинается когда «i >= 3». Ой простите, вы же считать не умеете, давайте перечислю все числа:
0, 1, 2, 3
неправда ли это 4?

Почему ваш воркер не начинает работать с первой задачи, а ждет, пока их станет не менее 3 в очереди?

Это вы опять ошиблись. На самом деле там запускается 4 таска, потом из очереди берется очередной из них, дожидаемся выполнения и выводим на консоль. Я изначально не хотел делать задачу с использованием каналов. Я решил задачу по другому, но суть осталась примерно та же: в очереди могут быть до 4-х задач, они запущены, но не факт что выполнены.

Если хотите на каналах, напишите сами, вот дока docs.microsoft.com/en-us/dotnet/api/system.threading.channels?view=dotnet-plat-ext-2.2

Ну или как вариант, я пишу код на c# с использованием каналов, а вы пишите код на go с использованием Task))
Функция getData() возвращает таск, а это значит она может выполнится в другом потоке, но для упрощения Task.Run я не писал.


Что за ересь вы несете, чесслово. Ну прочтите уже доку, на которую сами ссылались, а!

«для упрощения Task.Run я не писал.» — смешно уже, чесслово. Именно Task.Run указывает выполняться в отдельном потоке. Без него метод не может в параллельном потоке запуститься.

Если очень хочется красивый вывод можно написать что-то типа:


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

i >= 3


Не, это не я считать не умею. Это вы читать не умеете. «i >= 3» — читается именно как «условие: не менее 3». А дальше человек, который это будет поддерживать, видимо, должен догадаться, что означает магическое число 3…

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


Ничего там, на самом деле, не запускается…

Происходит там следующее:

1 итерация цикла: i = 0. В очередь в основном потоке синхронно добавляется task. Воркер не работает, т.к. i < 3.

2 итерация цикла: i = 1. В очередь падает еще один таск, все так же синхронно. Ни одного асинхронного вызова все еще не произошло. Воркер все так же не работает.

3 итерация цикла: i = 2. Аналогично 2-му.

4 итерация цикла: i = 3. В очередь все еще синхронно добавляется еще один таск. Наконец срабатывает «i >= 3». Мы попадаем на строку await Task.Delay(2000), в которой вас ждет первый сюрприз (учитывая ваш вопрос, зачем запускать Task.Delaу в отдельном потоке). Task.Delay запускается в отдельном потоке, которого вы и дожидаетесь, текущий поток тихо умирает, дальнейшее исполнение происходит в continuation потока, в котором произошел Task.Delay. Видимо, Task.Delay() внутри развертывается в что-то вроде Task.Run().ContinueWith(). Это поведение отлично видно на дебажном выводе, т.к. дальнейший код исполняется в ManagedThread№4. (Собственно, это единственное место в вашем коде, вызывающее переключение многопоточного контекста, весь остальной ваш код синхронный и однопоточный).
Затем вы выдергиваете из очереди задачу и «типа, отрабатывает воркер».

5 итерация цикла: вы синхронно кладете задачу в очередь, так же синхронно вынимаете оттуда.

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

Итого, единственный кусок многопоточного кода у вас — await Task.Delay(2000).

Я изначально не хотел делать задачу с использованием каналов. Я решил задачу по другому, но суть осталась примерно та же


Не та же. У вас воркер не работает, пока очередь не заполнена. Весь смысл всей этой затеи в том, что в очередь задачи падают, предположительно, быстрее, чем обрабатываются воркером. При этом поступать, вероятно, должны неравномерно. Т.е. смысл как раз в том, чтобы положить первую задачу в очередь, запустить воркер и, пока он работает, иметь возможность накидать еще задач в очередь.

Ваш же код:
1. Вообще не запускает воркер, пока очередь не заполнилась.
2. С того момента, как воркер начал работать, ситуация становится еще веселее: пока воркер работает, новые задачи в очередь не принимаются.
3. С точки зрения воркера еще более забавным выглядит тот факт, что он не имеет права чего-то обрабатывать, пока новая задача не упала в очередь.

Итого, вместо двух независимых процессов, о которых говорилось в задаче, мы имеем тупо линейное «задача пришла — задача ушла». И даже асинхронность в этом коде (не говоря уж о многозадачности) — тупо заради того «чтобы была», она не несет вообще никакой смысловой нагрузки.
Почему появление каналов в C# должно быть для меня плохой новостью. Я не болею за то, чтобы C# был ущербным по функционалу, а даже, скорее, наоборот, желаю ему только развиваться дальше, и становиться лучше и лучше с каждой итерацией разработки.

Однако с вашей стороны кажется странным сначала утверждать, что async/await лучше go/channel/select и второе, мол, не нужно, а также неудобно, а вслед за этим гордиться тем, что половину go/channel/select уже «завезли» в C#.

Однако есть еще один нюанс… select все еще нет, а учитывая то, что каналы — всего лишь библиотечная штука, удобного селекта, видимо, и не появится.

Так что, смею заверить, рад, что C# не стоит на месте, да и, в принципе, скорее соглашусь, что он один из самых приятных языков общего назначения… Однако модель асинхронности в нем все еще не дотягивает до Go-шной на ряде специфических кейсов, которые и составляют нишу Go. Ну и еще витуальная машина dotnet, мягко говоря, не доросла до JVM, например, по производительности…
Почему появление каналов в C# должно быть для меня плохой новостью.

Потому что каналы это одна из немногих киллер фич golang. Ну так у других тоже есть. По большому счету нет большой разницы между «chan < — 5» и «await chan.WriteAsync(5);»

Однако с вашей стороны кажется странным сначала утверждать, что async/await лучше go/channel/select

В некоторых сценариях прямо сильно лучше. Я же приводил пример. Допустим есть 2 асинхронных метода, например http.get() и db.query(), каждый например в ожидании находится 1 сек. При последовательном исполнении код выполняется 2 сек, я переставил await в другое место и код стал выполнятся уже за 1 сек. А в go вам пришлось сделать кучу операций:
— Инициализировать 2 канала;
— Запустить 2 горутины;
— Сделать запись в каналы;
— Прочитать из каналов;
— Закрыть каналы.
Забыли? Я вам напомню
Последовательное выполнение:
var data1 = await GetData1();
var data2 = await GetData2();
var result = Foo(data1, data2);

Параллельное выполнение:
var task1 = GetData1();
var task2 = GetData2();
var result = Foo(await task1, await task2);


Однако есть еще один нюанс… select все еще нет, а учитывая то, что каналы — всего лишь библиотечная штука, удобного селекта, видимо, и не появится.

Как я уже сказал, пользователю пофиг есть select или нет, давайте обсудим реальный сценарий, который он решает.
По большому счету нет большой разницы между «chan < — 5» и «await chan.WriteAsync(5);»


Кхм, есть. Гошный chan может быть буферизованным, например…

При последовательном исполнении код выполняется 2 сек, я переставил await в другое место и код стал выполнятся уже за 1 сек.


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

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

При этом, если GetData1 выполняется, предположим, 10 секунд, а GetData2 — 2, ранее, чем через 10 секунд, вы не сможете ничего сделать с результатом выполнения обеих функций.

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


Это вы ошибочно сказали, я выше сценарий привел.
использование go перед любой функцией принципиально чем-то отличается от например c# Task.Run(()=>Foo()), кроме более короткого способа записи?


Отличается. Сильно.

Каналы можно реализовать на другом языке (на том же c#) или это фича именно GO?


Канал — это просто примитив синхронизации. Т.е. реализовать можно на чем угодно. Только оно в C# нафиг не нужно, там горутин нет.
UFO just landed and posted this here
То что в го есть только корпоративная многозадачность

В Go не совсем кооперативный планировщик (https://github.com/golang/go/issues/10958), а еще есть предложение — https://github.com/golang/proposal/blob/master/design/24543-non-cooperative-preemption.md


Но не думаю что будут менять радикально что-то.

Проще наверное сказать где каналов нет, чем где они есть.


На всякий случай добавлю — да, естественно в Rust есть каналы.

Собственно, ORM'ы в Go не в моде.

То есть надо все писать руками. Сомнительный плюс

Да и в принципе, ORM как таковой полезен далеко не на всем спектре задач.

В любом полезном сервисе обычно есть БД, данные хранить же как-то надо, и наверное не в XML файликах локально. А если есть БД, то есть и работа с ней. Писать SQL запросы руками то еще удовольствие. А потом маппить на структуры.

Если Go оказался предпочтительным в каждой сравниваемой паре на определенной задаче, Go хорош.


Согласен

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

Отсутствие фич != фича. Скорее можно встретить «фича А есть в языках Х, У,Z, но не в го».

В общем, если хотите относительно объективных сравнений, не ставьте вопрос «чем Go лучше всех остальных языков». Это, в конце концов, непрофессионально. В разработке перед вами не будет выбора «Go против всех», у вас будет выбор «Go против X(где X — некоторый вполне конкретный язык)». Короче, ставьте вопрос «в каком спектре задач и почему Go лучше X(обязательно укажите интересующий вас X)».

Хорошо сказано.
Сомнительный плюс


Отсутствие ОРМа — это не плюс. Это просто не минус.

В любом полезном сервисе обычно есть БД


Не в любом.

Писать SQL запросы руками то еще удовольствие. А потом маппить на структуры.


Дело в том, что «не SQL единым». При этом ORM — это средство быстрой разработки, а не быстрой работы приложения. Ну, в смысле, с БД ORMы работают, как правило, далеко не оптимально.

Если вам жизнь без автогенерилки SQL не мила, ну хоть на squirrel взгляните. Генератор запросов для Go вполне себе есть. И ОРМы, собственно, тоже, даже несколько больше одного. Просто их обычно не юзают.

Дело в том, что для работы с БД ОРМ помощник в тех случаях, когда внутри приложения есть еще много-много разнотипной логики работы с данными в БД. Если у вас 5-10 относительно статичных запросов, то ОРМ не только не полезен, но даже вреден.
Отсутствие ОРМа — это не плюс. Это просто не минус.

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

Не в любом.

Ну видимо у меня другой опыт. Любая CRM/сервис расчета бонусов/… требуют информации по заказам, по клиентам, по уже выданным скидкам, по проходящим акциям…

Если вам жизнь без автогенерилки SQL не мила, ну хоть на squirrel взгляните. Генератор запросов для Go вполне себе есть. И ОРМы, собственно, тоже, даже несколько больше одного. Просто их обычно не юзают.

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


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

А вот отсутствие удобной работы с БД это офигеть какой.


Реализаций ORM для Go несколько больше одной, при этом популярности они не сыскали на данный момент. Может быть, вы несколько переоцениваете жизненную необходимость ORM, тем более в качестве неотъемлемой части языка?

Собственно ORM — это штука, состоящая «в базовой поставке» из генератора SQL-запросов, дата-биндингов и парсера ответов. Все это в Go есть, «на любой вкус», при этом применяется не слишком широко, особенно на фоне снижения популярности РСУБД в пользу документных БД и прочих nosql-решений.

А уж требование от языка уметь в миграции, валидацию схем и все вот это… Ну, как бы, один вопрос: а оно действительно надо? Особенно на фоне моды на распределенные приложения, состоящие из 100500 независимых сервисов, написанных на туче разных языков… Понимаете, фича в том, что миграция и валидация схемы БД — это инфраструктурная задача, не языковая.

Любая CRM/сервис расчета бонусов/… требуют информации по заказам, по клиентам, по уже выданным скидкам, по проходящим акциям…


Я вам одно скажу… Не пишите сервисы рассчета бонусов, скидок и прочие ERP на Go. Для таких задач есть Java…

Вопрос не в автогнерелике, а в подходе.


Вот-вот, именно в подходе вопрос. Не ORM'ом единым. Есть вполне альтернативные подходы.
За исключением тех случаев, когда наличие таковой библиотеки противоречит здравому смыслу.

ОРМ противоречит здравому смыслу?

Реализаций ORM для Go несколько больше одной, при этом популярности они не сыскали на данный момент. Может быть, вы несколько переоцениваете жизненную необходимость ORM, тем более в качестве неотъемлемой части языка?

Ну например объяснение может быть таким, что без генериков удобного АПИ такие библиотеки не могут иметь.

А уж требование от языка уметь в миграции, валидацию схем и все вот это… Ну, как бы, один вопрос: а оно действительно надо? Особенно на фоне моды на распределенные приложения, состоящие из 100500 независимых сервисов, написанных на туче разных языков… Понимаете, фича в том, что миграция и валидация схемы БД — это инфраструктурная задача, не языковая.

Да, это надо.

Вот-вот, именно в подходе вопрос. Не ORM'ом единым. Есть вполне альтернативные подходы.

Альтернатива — руками писать.
ОРМ противоречит здравому смыслу?


Например, в кеширующем сервисе, единственная задача которого заключается в том, чтобы пинать 2-3 предопределенных запроса к БД… Да, именно противоречие здравому смыслу.

Вот вы мне тут про миграции на уровне ORM рассказываете. Теперь представим: распределенное приложение, состоящее из десятка сервисов на 5-6 языках. Как вы себе представляете миграцию и валидацию схем на уровне ORM? У вас не будет одного орма на 5 языков, у вас их будет 5. И каждый из них будет «мигрировать и валидировать» — ну жесть же. Не говоря уж о том, что ОРМ — это абстракция, которая влечет за собой некоторые накладные расходы, т.к. абстракции небесплатны по определению.

Ну вот о чем я вам говорю: ОРМ, как правило, может встречаться либо в «вырожденном» виде (генератор запросов + парсилка ответов), либо в виде некоторой оверинжиниренной фигни, 90% фич из которой не нужны, а могут и вред нести.

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

Альтернатива — руками писать.


Как что-то плохое, в самом деле.
Go от сюдова =)
Я правильно понимаю, что go стал популярен из-за реализации рутин-потоков?
Да, без этого он бы не взлетел так. Но ещё из-за:
* низкой ресурсоёмкости
* близости к Си
* отсутствия головняка с компилятором и заголовочными файлами
* отсутствия головняка со сборкой
  • агрессивной политики Гугла
А в чём она проявляется?
раст лэнг, серьезно? Я думаю гугл выдает наиболее популярную выдачу по lang и undef и уж извините что это Golang. Да голэнг встречается очень часто, а вот раст лэнг никто не называет, кругом только раст. Наберите просто lang undefined и получите тоже самое, ну серьезно вы же разработчик, как бы вы организовали поиск по словам с учетом актуальности и вводимых запросов?

Ну да, самая популярная ccskrf по undef будет Go, и никак не C/C++. А lang кроме го ни один язык не называют, ни тот же С, ни какие-то еще.

Ну так как раст вы пишете Rust lang? Серьезно? Или может С lang? Может лексически вы можете так написать, но это не типично и это будет не типичный запрос на поисковую выдачу, а golang очень типично и я не вижу ничего странного что это так проиндексировано.
Простите, а как именно иначе гуглить Rust и C? Это ведь не уникальные названия как «JavaScript» и «PHP». У Раста даже сайт так называется:
www.rust-lang.org

У языка D — тоже: dlang.org
rust undefined как в примере будет вполне допустимо и даже более точно если вы посмотрите выше относительно чего был мой комент. В этом запросе сознательно расширяется выдача за счет lang который выдаст более популярный похожий запрос, которым естественно называется Golang.
Вы считаете, что в ответ на запрос «rust lang» вменяемо подставлять «go lang»?
да, поскольку ключевым словом здесь будет lang как наиболее часто встречающееся в запросах, полагаю что по этому сочетанию машина определит область программирования и найдет наиболее популярное сочетание программирования и слова lang и выдаст это как похожий запрос и это будет Go поскольку Golang популярен.
То есть если я гуглю «три мушкетера», то ключевым словом должно быть «три» как найболее часто встречающееся, а остальные — игнорироваться?
Почему игнорироваться? Да главным словом будет три, но мушкетеры значительно сузит область, поисковик не выдаст вам три стула. Не пойму как вы вообще могли придти к такому выводу… Перечитайте мой комментарий еще раз.
поисковик не выдаст вам три стула

Почему при запросе "три мушкетера" оно не выдаст "три стула"

А при запросе "lang rust" оно выдает "lang go"? Почему не произошло «сужение» области в данном случае, как должно произойти, по вашим словам?

Жирным я выделил то, что вы называете «главным» словом.

Кстати, я не думаю, что это умысел, скорее это просто неудачно сработал алгоритм. Но я не понимаю, почему вы считаете, что это нормальная работа алгоритма, а не ошибочная?
Вместе с lang rust часто ищут
rust язык

rust ide

rust lang wiki

rust vs c++

rust docs

rust язык установка

программирование на языке rust pdf

rust vs go

Это то что мне выдал гугл, вроде как все логично. Все еще не пойму что вас не устраивает?
p.s. lang rust и lang go будет не типичным запросом и естественно что алгоритм его обработает, но не так как типичный запрос. Например если набрать«Мушкетера» то гугл первой строкой выдает мне «20 лет спустя» и «три» только 7-й позицией.
у меня ничего не подставляет
Без lang по слову Rust гугл зачастую выдает результаты, связанные с одноименной игрой.
у меня совсем другие результаты.

> А lang кроме го ни один язык не называют, ни тот же С, ни какие-то еще.

Зачем вы тогда rust lang написали?
Пишу так, потому что еще есть игра с тем же названием, часто в поиск попадается.

гугл показывает персональные результаты, я гуглом не пользуюсь, вот сейчас попробовал — rust lang undef дополняется все с растом, undefined — C++.

Да что вы говорите, прям в этом гугле обперсоналились. Я пишу на Rust 1.5 года, у меня в истории запросов раста больше, чем порева, все это время сижу залогиненный в их гугл почту, но он упорно продолжает считать меня либо го разрабом (^ выше скрины от меня), либо геймером:



Скриншот сделал сегодня.

Вам мои скриншоты показать?

Давайте. Продолжайте радовать меня идиотизмом поисковой системы, которая ваши запросы по языку Rust распознает лучше, чем мои.

UFO just landed and posted this here
Теория заговора какая-то…
снимите шапочку из фольги и вбейте `rust undef` (f важна). Если результаты персонально подобраны (вроде как гугл так говорит), то вот автодополнение одно и тоже (без `f` тоже самое у меня).
UFO just landed and posted this here
Как работает автозаполнение

Подсказки подбираются с учетом таких факторов, как популярность и новизна поисковых запросов. Когда вы нажимаете на подсказку, Google выполняет поиск по выбранному запросу.
Что определяет подсказки
* Запрос, который вы вводите в строке поиска.
* Ваши предыдущие поисковые запросы на похожие темы (если вы вошли в свой аккаунт Google и у вас включена история приложений и веб-поиска).
* Запросы других пользователей. Также учитываются популярные новости Популярные запросы. Они формируются на основе самых актуальных для вашего региона тем и меняются в течение дня. Популярные новости не связаны с вашей историей поиска. Ознакомиться с ними можно на сайте Google Trends.

Обратите внимание! Подсказки – это не результаты поиска, не высказывания других пользователей и не ответы системы Google.


История включена?
UFO just landed and posted this here
я гуглом совсем не пользуюсь и результат у меня такой же, что в моем случае объясняется популярностью запросов `rust lang` и `golang` (смотрите google trends).
Вы же про историю веб-поиска в аккаунте гугл говорите?
UFO just landed and posted this here

Да, это забавно) Как если бы вместо golang гугл бы выдавал "возможно вы имели ввиду pornlang", ведь у слова porn тренд в 100 раз выше. Логика, ты где?)

Написал знакомому гуглеру. Посмотрим, починят ли =)

Кстати, не знаю, обратили внимание читающие люди, или нет, но это разные скриншоты сделанные разными людьми в разное время: например, на одном "undefi", на другом — "unde", и так далее.

и что часто в игре случается андефайнед поведение? Это все еще не типичная фраза, именно в контексте того что по игре таких запросов будет больше, поэтому lang undef в балансе выдачи будут в приоритете как более часто встречающиеся сочетания.
А фразы borrow и ownership? Так вот, оказывается — встречаются. Причем иногда бывают очень забавные ситуации (вроде поста об удалении ownership из Rust). Так что чтобы лишний раз не нарываться, лично я часто пишу «Rust lang», а в YouTube так без этого вообще не обойтись. И, заметьте, пишется в два раздельных слова. Поэтому подсказки вроде «golang» вообще не к месту.
UFO just landed and posted this here
Вы так говорите, что как будто на выбор языка программирования влияет результат поисковой выдачи.

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

Нет не влияет. Технические решения обычно принимает CTO основываясь на своём опыте и привычках. Хотя конечно для заказной разработки, типа сайтов, может влиять.

А опыт и привычки CTO значит никак не зависят от поисковой выдачи и прочего маркетинга? Как-то слишком уж наивна мысль о том, что выбор языка программирования зависит только от каких-то объективных факторов.

Маркетинг — это объективный фактор )
Скандалы, интриги, расследования.

Спецназ гугла жестоко избил ногами Растеров за отказ писать на Go.

Массовые протесты против тирании Гугла. Миллионы раст-программистов вышли на улицы, чтобы выразить свое «фи» против продвижения Go.

Жесть! Смотреть до конца! Без регистрации и СМС.

Минутка откровений, срыв покровов, рас-рас: «Все дело в зависти… — утверждает Rust-разработчик, пожелавший остаться неизвестным. — Мы тоже хотим агрессивно продвигать наш любимый язык, но у нас нет на это денег.» Объявлен сбор пожертвований в пользу продвижения Rust'а. Всем неравнодушным высылать деньги на электронный кошелек яндекс-денег «410013-456-88146». СРОЧНО! НЕ ДАЙ СВОЕМУ ЯЗЫКУ ПОГИБНУТЬ!
Гугл агрессивно продвигает Go — и это факт. Хотим мы такую же кампанию для Rust или нет — это уже другой вопрос. Лично я считаю такие маркетинговые продвижения вредными, так как в итоге популярность технология не «зарабатывает», а ей ее «покупают». Со всеми вытекающими, типа возможном быстром ее закрытии при смене коньюнктуры. Так что пусть лучше Rust рекламируют опробавшие его разработчики, чем мозилловские пиарщики (кои, кстати говоря, особо на маркетинг и не тратятся). Другое дело, что совсем без рекламы в современном мире уже не обойтись: тебя просто не заметят в море информационного шума.
Гугл агрессивно продвигает Go — и это факт.


Лично я считаю такие маркетинговые продвижения вредными


Я же прав, считая, что в ваших словах есть явно выраженная негативная оценка? Простите, конечно, но выглядит это несколько инфантильно.

Нет никакого «агрессивного» продвижения, так же как нет деления на «плохое» и «хорошее» продвижение. Единственная адекватная оценка продвижению — эффективное vs неэффективное. И вот тут, стоит признать, что продвижение Go выглядит достаточно эффективным, а продвижение Rust — неэффективным.

И вот здесь сообщество Rust, вместо того, чтобы менять неэффективную модель продвижения на что-то приносящее плоды, предпочитает обидеться на этот «несовершенный мир, в котором правят деньги». Вместо того, чтобы как-то исправлять ситуацию, сторонники Rust'а начинают просачиваться во все треды по любым языкам программирования с попытками обосновать, что Rust крут, а вокруг одно… хм… несовершенство. Видимо, потому, что другие методы… кхм… «маркетинга» им недоступны?

Детство, короче, простите за откровенность.
Не надо извиняться, мы просто с вами разных взглядов. Да, я считаю навязчивую рекламу плохим делом. Что же касается Раста, то я в целом доволен тем, как он развивается и как продвигается. Есть свои проблемы и шероховатости, но видеть Rust на месте Go по способу продвижения, я бы не хотел. Все-таки важна не только цель, но и средства.

Считаете ли вы агресивное продвижение Windows 10 успешным (Я про принудительную установку и все такое прочее)? С одной стороны, это увеличило аудиторию. С другой, некоторые квалифицированные пользователи ушли с винды.

Считаете ли вы агресивное продвижение Windows 10 успешным


Я считаю его эффективным.

С одной стороны, это увеличило аудиторию.


И вы сами это признаете. Цель-то достигнута.

С другой, некоторые квалифицированные пользователи ушли с винды.


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

Больше всего в го расстраивает именно коммьюнити. На любую критику в сторону языка следуют какие-то религиозные мантры вместо, собственно ответа:


  • А вы вообще на го сколько писали?
  • А вот Пайку виднеее
  • А вот у меня все нормально работает
  • А зато в Го я быстрее пишу микросервис, чем на джаве
  • Зато мы не спорим про форматирование
  • А вы мне покажите язык, который защищает от всех проблем. Нет такого? Ну то-то же
  • Зато мне не надо каждый год изучать новые фичи языка
  • А ты кто такой, чтобы это спрашивать?
  • И что? Так даже удобнее
  • А кому сейчас легко?
  • ...

И все эти "что позволено language design team, не позволено обычному ГО-разработичку" отдельно радуют.

На то есть разные причины. Я лично стараюсь такие ответы не давать, так что можете задать такие критикующие вопросы мне, если, конечно, Вам интереснее получить конкретные ответы, нежели просто продолжать критиковать ради самого процесса.


Конкретнее по упомянутым вариантам ответов:


  • "А вот Пайку виднее" — я на авторитеты никогда не ссылаюсь, но понимаю, почему нередко отвечают именно так. Есть определённые идеи, до которых многие просто ещё не доросли. Это не понты и не взгляд свысока, это просто факт. И касается он всех, абсолютно точно включая меня, и, я полагаю, и Пайка тоже. Есть идеи, которые рождаются раньше "своего" времени, из-за чего они оказываются проигнорированы и забыты. Моё личное мнение — множество таких идей появилось лет 30 назад, в OS Plan9, потом в OS Inferno и языке Limbo. Часть этих идей, к сожалению, так и не стали популярны до сих пор, хотя процесс идёт, иногда криво реализованные, но они проникают в современные OS, ЯП и утилиты. Я лично думаю, что дело не столько в том, что эти идеи появились слишком рано, сколько в том, что их авторы абсолютно не занимались их популяризацией. Go это первый представитель части этих идей, который стал популярен (и да, произошло это именно благодаря жёсткой раскрутке его гуглом, а не благодаря его техническому совершенству — в этом плане он на старте заметно проигрывал предыдущим разработкам). Но я бы не стал винить гугл в том, что он сильно пиарит Go — тут скорее мы сами виноваты в том, что чаще всего без такого пиара не в состоянии заметить даже очень классные вещи. Так вот, извините за длинное предисловие, суть ответа "Пайку виднее" в том, что за некоторыми особенностями Go стоят именно те идеи, до понимания которых многие ещё не доросли, но идеи от этого менее правильными не становятся, поэтому иногда нужно просто делать как делают взрослые, и, со временем, станет понятно зачем и почему это делается именно так. Да, такой ответ раздражает, но учитывайте, что для того, чтобы ответить как-то более осмысленно, отвечающему самому надо сначала дорасти до этих идей, плюс уметь внятно объяснять другим то, что понял сам.
  • "Зато мне не надо каждый год изучать новые фичи языка" — это вполне валидный ответ, как по мне. Мне категорически не нравится мелькание технологий, которое сейчас происходит на фронте, а до этого происходило под виндой. Я предпочитаю использовать более стабильный стек — мне есть что непрерывно учить помимо нового синтаксиса ЯП и что делать помимо переделывания старого кода под новые требования ЯП.
  • Насчёт остальных ответов согласен.
А можете вкратце пересказать эти идеи, своими словами?
Просто невероятно сколько на хабре недовольных языком Го, в каждой статье про него прямо таки битва разгорается, складывается впечатление что это ужаснейший язык на свете. Остается непонятным почему недовольные люди о нем читают, почему недовольные люди на нем пишут? Но больше всего удивляет агрессивность Расто-поклонников, они просто адски ненавидят Го, хотя казалось бы пишите на чем нравится…
> Остается непонятным почему недовольные люди о нем читают, почему недовольные люди на нем пишут? Но больше всего удивляет агрессивность Расто-поклонников, они просто адски ненавидят Го, хотя казалось бы пишите на чем нравится…

Я недоволен языком go. Пишу на нём потому что он используется на работе и у него есть объективные преимущества перед другими языками… и являюсь неагрессивным расто-поклонником. Кстати, по ощущениям, как-раз go-поклонники более агрессивны :P
Почитайте посты про Go на хабре.

Я вот считаю так — если вам что-то не нравится на работе, может следует что-то поменять?
Я разве написал что мне что-то не нравится на работе? Работой полностью доволен. А вот чем недоволен — написал в первом предложении.

> Почитайте посты про Go на хабре
Дайте, пожалуйста, ссылки на посты которые мне необходимо почитать.
Я разве написал, что мне что-то не нравится на работе? Работой полностью доволен. А вот чем недоволен — написал в первом предложении.

Как я понял, программирование на Go является частью вашей работы, если вам это не нравится (программировать на Go) — то вам не нравится ваша работа. Или вам нравится программировать на Go?

Дайте, пожалуйста, ссылки на посты которые мне необходимо почитать.

Комменты вот тут
habr.com/post/422049
habr.com/post/421411
Как я понял, программирование на Go является частью вашей работы, если вам это не нравится (программировать на Go) — то вам не нравится ваша работа. Или вам нравится программировать на Go?

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


Я, например, пишу в последние полгода на солидити, которые в миллион раз хуже и го, и JS, и всего, что я вообще когда-либо видел. Лучше разве что брейнфака и его вариаций. Но работаю, по причине 1, 2 и 4.

Я, например, пишу в последние полгода на солидити, которые в миллион раз хуже и го, и JS, и всего, что я вообще когда-либо видел. Лучше разве что брейнфака и его вариаций. Но работаю, по причине 1, 2 и 4.

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

Вы как-то очень избирательны в своем чтении. я

1. работаю с лучшим коллективом из 5 мест, на которых я работал
2. я работаю над интересными проектами, где узнал много нового
3. в следующем году, возможно, я буду вести проект на расте
4. моя компания очень лояльно относится к тому, что я часто работаю из дома
5.…

Вопрос: зачем мне увольняться? Только чтобы на солидити не писать? Есть и другие факторы, и они перевешивают. Вот и колюсь кактусом, но дописываю проект.
Как я понял, программирование на Go является частью вашей работы, если вам это не нравится (программировать на Go) — то вам не нравится ваша работа. Или вам нравится программировать на Go?

Я же написал что недоволен языком go, а не то что мне не нравится программировать на go.

Если появится задача для которой go будет подходящим решением, то я спокойно буду использовать его без нравится/не нравится.

Ок, вы сломали мой мозг — вам не нравится Go, но вам нравится программировать на Go?

Вы правда не видите разницу между фразами недоволен и не нравится?

Давайте тогда на примере, где нет go.

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

Так понятнее? Или вы напишете, что мне не нравится ездить на метро?

И если вернуться к go, то я недоволен языком go, но абсолютно спокойно отношусь к тому, чтобы что-то на нём сделать.
А вы ездите на метро тоже по 8 часов в день?
Очень часто забавляет сравнение Go c С, Rust, Python, Java, Pony, C#, Haskell, Erlang, Javascript, etc одновременно. Типа:
1. Go плохой, потому что в Rust <что-то> сделано лучше.
2. Go плохой, потому что в Python <что-то> сделано лучше.
3. Go плохой, потому что в Pony <что-то> сделано лучше.
4. Go плохой, потому что в Java <что-то> сделано лучше.
5. Go плохой, потому что в Haskell <что-то> сделано лучше.

Да, это, конечно же, плохое сравнение.


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

Подмножеством чего? Максимум есть связи с C. Я когда начал писать на Go, в Rust каждый день изменяли стандартную библиотеку.
Описанных языков. В 2009 горутины были прорывом (наверное. Я в C# пользовался тасками примерно тогда же). Сейчас же это мейнстрим в большинстве языков. Один бинарь — как уже выясняли, нужная в очень специфических условиях штука, причем скорее маркетинговая, направленная на питонистов «смотрите, интерпретатор не нужон». Что там еще есть? Быстрая компиляция? В питоне вообще мгновенная, в шарпе — сравнимая, т.к. основную работу JIT делает во время запуска, а транслировать C# в IL тривиально.

Ну и всё, вроде. Отсутствие фич это не фича, атеизм это не религия, а лысина — не вид прически. В итоге из реально полезных вещей — удобная асинхронность, которую за 10 лет скопировали все языки/библиотеки, которые хотели, и уже не являются какой-то уникальной штуковиной.
Я так и не понял, в чем ваша претензия?

У меня не было никакой претензии. Я говорил, что возможно "го" является подмножеством описанных языков, и объяснил, почему я так считаю.

Сейчас же это мейнстрим в большинстве языков

в каких?
Один бинарь — как уже выясняли, нужная в очень специфических условиях штука, причем скорее маркетинговая, направленная на питонистов «смотрите, интерпретатор не нужон»

лол, надо плюсовикам рассказать что они на самом деле питонисты.
Ну и всё, вроде. Отсутствие фич это не фича, атеизм это не религия, а лысина — не вид прически.

всегда можно придраться к чему-то. даже в вашем любимом языке может не быть фичи, которая есть в другом языке. это конечно повод накинуть какашек и собрать плюсцов себе в карму, но на реальное положение дел это не повлияет.
Я в C# пользовался тасками примерно тогда же

вы понимаете в чем принципиальная разница между green threads и тасками?
в каких?

C#/Котлин/..., вот это всё

лол, надо плюсовикам рассказать что они на самом деле питонисты.

У плюсовиков вообще все грустно. В 2018 году нельзя склонировать репозиторий и нажать кнопку «restore deps and build», чтобы всё заработало с первого раза. Они обрадуются вообще чему угодно :)

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

Причем тут придирка? Если в C# нет non-null types или там тайпклассов, я не буду говорить, что это фича, потому что мне не надо учить ничего нового. Я честно скажу: да, ребята, косяк, надеюсь в будущем исправят.

вы понимаете в чем принципиальная разница между green threads и тасками?

Расскажите. А то я сглупу думал, что и то и то реализация M:N многопоточности.
C#/Котлин/..., вот это всё

ну это еще не все языки. у нас например ни того ни другого нет. а есть питон, жава и го.
У плюсовиков вообще все грустно. В 2018 году нельзя склонировать репозиторий и нажать кнопку «restore deps and build», чтобы всё заработало с первого раза. Они обрадуются вообще чему угодно :)

да, поэтому го для меня был как манна небесная. я с ужасом представляю себе что нужно будет писать на плюсах проект. CMake, гора зависимостей и костыли при кросскомпиляции.
Причем тут придирка? Если в C# нет non-null types или там тайпклассов, я не буду говорить, что это фича, потому что мне не надо учить ничего нового. Я честно скажу: да, ребята, косяк, надеюсь в будущем исправят.

ну в каком-то смысле ограничения это хорошо. тот же пример — плюсы. мультипарадигменность, 100500 способов сделать одно и то же порождает разношерстный код. ограничения хороши для поддерживаемости проекта разными людьми. Но есть откровенно бесячие недостатки, с этим не поспоришь.
Расскажите. А то я сглупу думал, что и то и то реализация M:N многопоточности.

разница в том, что имея 4 потока в случае goroutine ты пишешь «синхронный» линейный IO код и не паришься, потому что под капотом планировщик и IO на самом деле асинхронный. В случае тасков если ты будешь так писать, то тебе быстро придет карачун и все зависнет
да, поэтому го для меня был как манна небесная. я с ужасом представляю себе что нужно будет писать на плюсах проект. CMake, гора зависимостей и костыли при кросскомпиляции.

Это проблема чисто плюсовая.

В нормальных языках таких бед нет. Даже в JS есть NPM.

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

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

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

разница в том, что имея 4 потока в случае goroutine ты пишешь «синхронный» линейный IO код и не паришься, потому что под капотом планировщик и IO на самом деле асинхронный. В случае тасков если ты будешь так писать, то тебе быстро придет карачун и все зависнет

Ну так, точно также с async/await пишешь линейный код, и не паришься.
Это проблема чисто плюсовая.

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

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

а мне кажется вполне себе ок. лучше очевидно и просто, чем «типа просто» но порой неочевидно и с подводными камнями.
Ну так, точно также с async/await пишешь линейный код, и не паришься.

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

Библиотеки есть, проблем с ними нет.

В худшем случае, когда в дереве зависимостей есть несколько версий одной библиотеки, существуют bindingRedirect.

Можете рассказать, что такого предоставляет go, чего не предоставляют другие пакетные менеджеры.

у нас никто не переезжает, не слышу восторженных возгласов.

Ну раз у вас никто не переезжает, значит, всё враки.
ага, посмеялся.

Давайте вместе посмеемся. В чем же невероятное преимущество, и как ужасно будет выглядеть на async/await?
Можете рассказать, что такого предоставляет go, чего не предоставляют другие пакетные менеджеры.

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

могу сказать зеркально и про вас. или у вас есть статистика переезда по всем компаниям?
Давайте вместе посмеемся. В чем же невероятное преимущество, и как ужасно будет выглядеть на async/await

Ок, давайте вот простой пример: приходит клиент по tcp, мы должны в рамках его сессии походить в несколько сервисов, отдать ответ и закрыть соединение. На с# я нашел такой пример:
ru.stackoverflow.com/questions/317005/sockets-clientserver-with-await-async-c-5-0

Task<int> TrySendAsync(byte[] buffer, int offset, int size, SocketFlags flags)
{
    return Task<int>.Factory.FromAsync(
        _clientSocket.BeginSend, _clientSocket.EndSend,
        buffer, offset, size, flags, null);
}

Task SendAsync(byte[] buffer)
{
    int sent = 0;
    int remaining = buffer.Length;
    while (remaining > 0)
    {
        var partSent = await TrySendAsync(data, sent, remaining, SocketFlags.None);
        sent += partSent;
        remaining -= partSent;
    }
}


Т.е. вот это обмазывание Task'ами с async/await вы считаете достойной альтернативой go? где ты берешь любую библиотеку и она уже будет так работать потому что это фишка языка а не нашлепка сбоку.
причем тут пакетный менеджер, мы начинали говорить про статическую компиляцию в один файл, что не надо с собой таскать зависимости и ставить их на конечную систему.

Ну, это не достоинство, а особенность. Недостатков такого подхода тоже хватает.


могу сказать зеркально и про вас. или у вас есть статистика переезда по всем компаниям?

Нет


Ок, давайте вот простой пример: приходит клиент по tcp, мы должны в рамках его сессии походить в несколько сервисов, отдать ответ и закрыть соединение

async Task<Response> HandleRequest(Request request)  {
   var foo = await service1.Foo(request.SomeParam);
   var bar = await service2.Bar(request.OtherParam);
   return new Response(foo, bar);
}
C#/Котлин/..., вот это всё


В котлине и C# есть Green Threads? Давно? Вчера не было, например.

Погуглил… Гугл говорит, что все еще нет.

А не путаете ли вы goroutine с coroutine? Ну, как бы, корутины в Котлине есть, ага. Только вот это несколько иная конструкция.

А то я сглупу думал, что и то и то реализация M:N многопоточности.


Вы упустили одну немаловажную вещь: это разные реализации M:N многопоточности.

Разница в том, кто управляет многозадачностью. Корутины сами отдают управление (yield), горутины управляются планировщиком.
А не путаете ли вы goroutine с coroutine? Ну, как бы, корутины в Котлине есть, ага. Только вот это несколько иная конструкция.

А вот тут любопытно. А то я всегда считал, что это взаимозаменяемые понятия, просто го решил свою приставку сделать. Ну типа как «смурфет, смурфтно, смурфрутины» и вот это все.

На самом деле всё не настолько идеально, и я не уверен, что приставка go вместо co несёт глубокий смысл. Многозадачность бывает кооперативная и вытесняющая. В языках с yield/await она кооперативная, в Go — тоже. Тем не менее, на Go этот код писать значительно проще, потому что программисту вручную нет необходимости об этом задумываться в 99.99% случаев (а в оставшихся случаях он может вызвать runtime.Gosched).


Помимо этого далеко не все языки реализуют кооперативные M:N, во многих там 1:N, а это значит, что любая функция, которая просто слишком долго что-то вычисляет в цикле начинает фатально мешать работе других. В результате, несмотря на формальное сходство, async/await действительно заметно проигрывает горутинам.


Дополнительным фактором в Go являются каналы, аналог которых далеко не всегда прилагается к async/await в других языках (частично потому, что в 1:N это не так критично), и которые сильно упрощают безопасную коммуникацию между горутинами.

Помимо этого далеко не все языки реализуют кооперативные M:N, во многих там 1:N, а это значит, что любая функция, которая просто слишком долго что-то вычисляет в цикле начинает фатально мешать работе других. В результате, несмотря на формальное сходство, async/await действительно заметно проигрывает горутинам.

Большинство языков (да тот же C#) реализуют эту функциональность поверх пула потоков, который делает ровно то же самое, что и менеджер корутин.


Каналы это удобно, да.

В Go проще запускать coroutine'ы, но сложно их контролировать. Такие вещи как join, cancel, await, timeout — все сложны в реализации и требуют кучи кода. В Kotlin это все реализовано куда приятней.

Я не знаю Kotlin, так что не могу сравнивать, но я не вижу что именно сложно контролировать в Go. Единственное, чего в Go нет — это внешнего контроля времени жизни горутины, т.е. одна горутина не может убить другую либо гарантированно получить какой-то сигнал при завершении другой. Всё это не является проблемой если корректно проектировать горутины (причём речь действительно о проектировании, а не о корректности/сложности реализации — с реализацией обычно проблем не возникает).


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


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


  • закрытие канала как способ сделать broadcast неопределённому количеству получателей,
  • использование select для неблокирующей операции с каналом,
  • использование nil-каналов в select для временного отключения отдельных case-ов,
  • ну и собственно использование самого select в отдельной горутине в качестве event loop или менеджера группы других горутин.
Ответ можно свести к «Все просто, только все очень сложно.»

Не, не совсем.

Вся разница, по сути, в том, кто передает управление. В модели корутин сама корутина отдает управление, а планировщик передает кому-то еще (yield). В гошной реализации планировщик отбирает управление у горутины.

В связи с этим есть очевидный баланс плюсов/минусов:

Корутины позволяют контролировать «атомарность» исполнения отрезков кода, т.е. гарантируют, что где-то между строкой 10 и строкой 11 у корутины «не отберут руль» (что таки да, может быть плюсом в некоторых отдельных сценариях, но при этом несколько «подращивает» сложность управления этим хозяйством). Из очевидных минусов: если внутри корутины до yield'а есть что-то очень трудоемкое, либо что-то, что ставит поток раком, либо какое-то долгое ожидание — нагнется целиком системный поток.

Горутины целиком рулятся шедулером. Т.е., с одной стороны, вы не можете быть уверены, что вашу горутину не стопорнут внутри операции, которую вы считаете атомарной. Более того, даже то, что одна строка кода и даже одна «условно-атомарная» операция будут выполнены без прерывания, вам никто не гарантирует. Взамен вы имеете гарантию того, что никакая горутина не может поставить колом целиком поток. Ее просто никто не будет ждать.

Любопытно, спасибо.


Первое похоже на файберы, а второе — обычную M:N многопоточность.

Это не так. Планировщик управление у горутины сам не отбирает, он это может сделать только тогда, когда горутина вызывает определённые операции (чтение/запись каналов, I/O, time.Sleep, etc.). Если горутина занята числодробилкой и ничего такого не вызывает, то она отлично "поставит колом" свой поток, и если таких горутин наберётся GOMAXPROCS штук то они заблокируют выполнение всех других горутин. Пример: https://play.golang.org/p/1gaiQVBpood (раскомментируйте строку с runtime.GOMAXPROCS чтобы увидеть разницу).

Раскомментировал:

CPUs: 1
2 calc init
0 calc init
1 calc init
2 calc done 887459712
0 calc done 887459712
1 calc done 887459712
Exit


Колом не встало.

Ну, во-первых у меня результаты немного другие:


// GOMAXPROCS=1
2 calc init
2 calc done 887459712
0 calc init
0 calc done 887459712
1 calc init
1 calc done 887459712
// GOMAXPROCS=2
2 calc init
0 calc init
2 calc done 887459712
1 calc init
0 calc done 887459712
1 calc done 887459712

А во-вторых тест не очень корректный, потому что fmt.Println это I/O, что даёт возможность шедулеру передать выполнение другой горутине, так что в теории есть возможность увидеть 3 init подряд даже при GOMAXPROCS=1, но это вовсе не означает, что горутины выполнялись параллельно. Надо заменить fmt.Println на что-то без I/O, но вряд ли это получится сделать корректно — по сути, мы пытаемся получить последовательность выполнения операций в разных горутинах без использования каких-либо примитивов синхронизации горутин (потому что они все передадут управление шедулеру горутин). Вот пример https://play.golang.org/p/7jelh_pOBav но он может глючить по вышеупомянутой причине.

Надо заменить fmt.Println на что-то без I/O, но вряд ли это получится сделать корректно
Посылать сообщение и логировать данные, а потом вывести их в конце, когда все потоки отработают? Можна даже без синхронизации — просто sleep достаточный, чтобы завершились все потоки.

Как именно "посылать сообщение"? Каналы, мьютексы — всё это передаст управление шедулеру.

Да, вы правы. А что, если логировать все сообщения, каждый в своём потоке, но с точным временем, а потом — отсортировать (даже вручную, их не так много)?

Вызов time.Now() это сискол. Хотя конкретно его заоптимизировали так, что, по-моему, сейчас он работает без реального системного вызова. В любом случае я не настолько хорошо знаю внутренности Go, чтобы быть уверенным в том, что его вызов не передаст управление шедулеру. Скорее всего это можно проверить эмпирически, но вряд ли это получится сделать в песочнице play.golang.org.

Следующий код у меня не выводит «main». Вывод: горутина может повесить всю программу. А иначе зачем тогда нужен метод runtime.Gosched()? Ксати, если его вызов вставить в цикл, то «main» будет выводиться.
func main() {
	t := 0
	runtime.GOMAXPROCS(1)
	go test(t)
	time.Sleep(1)
	fmt.Println("main")
}

func test(t int) {
	fmt.Println("start")
	for true {
		t = t + 1
	}
	fmt.Println("stop")
}
Вывод: горутина может повесить всю программу.

Не совсем так. Если N горутин, где N равно GOMAXPROCS (обычно — количеству ядер CPU), будут заняты долгими/бесконечными вычислениями без какой-либо коммуникации с другими горутинами, I/O, вызовов системных функций, etc. — то кроме них другие горутины выполняться не будут пока эти вычисления не закончатся. Так работает кооперативная многозадачность.

А зачем вы добавили runtime.GOMAXPROCS(1)?

Чтобы ограничить выполнение одним потоком. Чтобы проще было проверить. Не нравится этот код, вот вам другой:
func main() {
	for i := 0; i < 100500; i++ {
		go test(0)
	}
	time.Sleep(1)
	go fmt.Println("main 1")
	fmt.Println("main 2")
}

func test(t int) {
	fmt.Println("start")
	for true {
		t = t + 1
	}
	fmt.Println("stop")
}

У меня выводит 8 раз «start» и больше нифига. В итоге непонятно чем горутины лучше котлиновских корутин, дотнентых тасков и т.д. По большому счету нам всегда надо знать, нужно ожидать завершения метода или не нужно. В go если нужно ждать, то пишем просто «foo()», а если не нужно, то «go foo()», а в c# наоборот «await foo();» и «foo();». Я кстати не разработчик go, и не разработчик c#, так что пишите критику и одни и другие))

А чего вы хотите увидеть? Я пока вас не понимаю.
В Go кооперативный (почти) планировщик, планирует goroutine по доступным тредам — у вас 8 goroutine не блокируются, не делают системных вызовов, просто инкрементят счетчик, всегда в running состоянии.


Только вот в реально мире такого не будет. Вот добавьте всего одну строчку (fmt.Println(t) в цикле функции test) и у вас совсем другой результат.


func main() {
    for i := 1; i < 100500; i++ {
        go test(0)
    }
    time.Sleep(1)
    go fmt.Println("main 1")
    fmt.Println("main 2")
}

func test(t int) {
    fmt.Println("start")
    for true {
        t = t + 1
        fmt.Println(t)
    }
    fmt.Println("stop")
}
Вот тут qrKot пишет другое:
Взамен вы имеете гарантию того, что никакая горутина не может поставить колом целиком поток.

Захотелось проверить, но чуда не произошло

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


Чуда не произошло в вашем чудном примере.


UPD: "поставить колом целиком поток" не тоже самое, что поток будет исполняться. Поставить колом — это блокировать, например.

в той же Java на ожидание ответа системного вызова блокируется весь поток

И у этого вызова нет асинхронного аналога? И он реально долгий?

Ну частое переключение, как мне кажется, тоже имеет свои минусы, например это избыточные накладные расходы (сохранить стек от одной горутины, восстановить от другой). В глобальном плане это вообще пофигу, например когда сервис обслуживает тысячи запросов. Например одно ядро должно обслужить 2 запроса, каждый из которых требует 100 мс процессора, то нам все равно, оба запроса выполнятся параллельно и оба ответа вернутся через 200 мс, или последовательно и тогда первый ответ вернется через 100 мс, а второй через 200 мс. Пример конечно упрощенный, но думаю суть понятна.
Вы уже уходите от темы, то есть вы согласны что «никакая горутина не может поставить колом целиком поток»?

Конечно же есть затраты на парковку goroutine и context switch (стэк, кстати, у каждой горутины свой и алоцируется при создании), но это ничтожно мало с, например, ожиданием I/O. Как уже говорили, Go хорошо подходит для Web.

Вот в Go, за счет того, что заблокированая горутина паркуется и не блокирует трэд, второй запрос у вас обработается быстрее, так как во время I/O первой горутины, будет выполняться вторая.
Вы уже уходите от темы, то есть вы согласны что «никакая горутина не может поставить колом целиком поток»?

Да я откуда знаю, может есть еще и другие способы. Тем не менее, это работает не так как написал qrKot. Это не плохо и не хорошо, это примерно также как и у других.

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

Вы так говорите, как будто это уникальная фича go, да у всех асинхронных серверов так. Понятно что в старых языках типа Java остались синхронные апи для I/O, ну сам себе буратино что используешь их в асинхронном методе. Ну кстати не всегда и требуется переписывать на асинхрон, я как-то переписал консольную утилиту и она стала медленнее работать, накладных расходов потому что стало больше чем полезного кода.
Да я откуда знаю, может есть еще и другие способы. Тем не менее, это работает не так как написал qrKot. Это не плохо и не хорошо, это примерно также как и у других.


А как? Ваш пример был ошибочным, трэд там в состоянии Running и то что сказал qrKot подтвердилось вашим же примером.

Вы так говорите, как будто это уникальная фича go, да у всех асинхронных серверов так. Понятно что в старых языках типа Java остались синхронные апи для I/O, ну сам себе буратино что используешь их в асинхронном методе. Ну кстати не всегда и требуется переписывать на асинхрон, я как-то переписал консольную утилиту и она стала медленнее работать, накладных расходов потому что стало больше чем полезного кода.


Я не говорю про уникальность, это тут все любят сравнивать, я вам привел пример как сделано в Go. При этом асинхронный I/O это совсем другое (был как пример).
А как?

Да я go установил впервые только несколько дней назад, потому что стало интересно посмотреть что там за такие горутины. А Вы мне такие вопросы задаете))
Ваш пример был ошибочным, трэд там в состоянии Running и то что сказал qrKot подтвердилось вашим же примером.

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

Вы написали бредовый код и пытаетесь натянуть его на реальную картину — нет, так не работает.
Пишите код, который не будет выполнять непонятную херню.
Ну собственно когда заканчиваются аргументы…
Спасибо за ответы!
У меня?
Еще раз напишу — с точки зрения рантайма все хорошо, все рутины работают и не заблокированы, проблема только в вашем коде.
Это любимый ответ «С++»-ников — «так никто не пишет».

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

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

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


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

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


Собственно, никто и не говорил, что асинхрон/многопоточность нужны в 100% случаев. Просто там, где оно нужно, иметь его «из коробки» приятно.
Следующий код у меня не выводит «main».


Он и не должен, например. Вы же сами ограничили программу 1 потоком, в котором запустили бесконечный цикл.

Вывод: горутина может повесить всю программу.


Да, и на основе заведомо некорректного кода сделали заведомо некорректный вывод.
вот ваши же слова:
Из очевидных минусов: если внутри корутины до yield'а есть что-то очень трудоемкое, либо что-то, что ставит поток раком, либо какое-то долгое ожидание — нагнется целиком системный поток.

вот в go я запустил как раз трудоемкое. Что не так то? От своих слов отказываетесь?
А насчет долгих ожиданий, так уже давно везде реализовано асинхронное апи для I/O, все прекрасно знают об этом.
Вы же понимаете, что в вашем примере с потоком ничего не произошло?
Как это ничего не произошло?! Все потоки заняты, пользователь не сможет увидеть свой сайт.
Если вы заговорили о пользователе, то давайте реальный пример, а не бесконечный цикл.

Сейчас, с точки зрения, что говорил qrKot, у вас ничего не произошло, все потоки ранятся.
Если вы заговорили о пользователе, то давайте реальный пример

Кстати, хочу обратить внимание, что у нас всего 8 потоков на всех пользователей (если я не ошибаюсь), а не 8 потоков на каждого. Потому если для восьми пользователей запустить что-то длительное (формирование отчета ексэль) — остальным придётся ждать. Я нигде не ошибся?
Смотря как вы будете формировать отчет — вам нужно будет его откуда-то читать, да? Построчно? Вот вам уже и сисколы, уже будет переключение горутин.

Но давайте так, допустим каждая горутина просто должна спать 1 минуту (ну типа чисто в памяти что-то 1 минуту считаем, без сисколов). Сделав 9 запросов, девятый будет ждать. Только это не проблема языка или рантайма, с точки зрения рантайма все очень даже хорошо — все тредэ ранятся и нету заблокированных.
Ок, только всё началось с того, где были описаны минусы корутин:
Из очевидных минусов: если внутри корутины до yield'а есть что-то очень трудоемкое, либо что-то, что ставит поток раком, либо какое-то долгое ожидание — нагнется целиком системный поток.

Давайте код, который ставит раком поток, очень хочется убедиться что там не говнокод.
Я лишь указал вам на вашу ошибку в восприятии того кода выше (с бесконечным циклом).
У кода нет восприятия, это не философский трактат. Это просто математическое описание некоего алгоритма, с некими свойствами.
Выше было сказано, что код «ставит колом целиком поток», а это не так (все потоки выполняются). Значит неправильно понимают (может я непонятно выразился с «воспринимают»). Но сути дела это не меняет.
Вообще-то, как верно заметил powerman чуть ниже, разницы между coroutine'ами в Kotlin и goroutine'ами в Go практически никакой. И там и там cooperative scheduling.
В 2009 горутины были прорывом (наверное. Я в C# пользовался тасками примерно тогда же). Сейчас же это мейнстрим в большинстве языков.

Горутины сами по себе не были новыми и не они есть причиной успеха Go для concurrent-кода. Новинкой была имплементация CSP-подобной логики как часть языка, и этого, насколько мне известно, пока в других языках нет.
да, я тоже поначалу удивлялся а потом понял, что это просто обида. Обида что на их язык, который функциональнее Го(это правда) так мало вакансий на рынке.
Это какой-то очень нелепый и эгоцентричный взгляд на вещи.
UFO just landed and posted this here
UFO just landed and posted this here

Articles