Comments 35
func upgradeUser(endpoint, username string) error {
getEndpoint := fmt.Sprintf("%s/oldusers/%s", endpoint, username)
postEndpoint := fmt.Sprintf("%s/newusers/%s", endpoint, username)
_, err := compose(
http.Get,
func(req *http.Response) ([]byte, error) {
return ioutil.ReadAll(req.Body)
},
newUserFromJson,
newUserFromUser,
json.Marshal,
func(buf []byte) (*http.Response, error) {
return http.Post(
postEndpoint,
"application/json",
bytes.NewBuffer(buf),
)
},
)(getEndpoint)
return err
}
Если описать словами, то это не «а, ну тут все понятно», а что-то ближе к «wtf здесь происходит вообще?»
Когда же я вижу первоначальный код (до конвертации в monad-style), то там все как-то сразу понятно. И не надо думать, как бы извернуться (в случае monad-style), если надо сделать что-то чуток отходящее от шаблона, типа дополнительную проверку вставить — просто вставляешь её куда надо и готово.
На всякий случай напоминаю, что при дизайне Go немаловажную роль играла простота и читаемость кода )
Я считаю, что лучше иметь на экране десяток проверок err != nil, чем сложнозавернутую конструкцию, которую и сам с трудом разберешь через пару месяцев. Не в PerlGolf же играем.
Стиль монад конечно подкупает if err != nil, но в одном из проектов я его попробовал (правда не знал, что это называется монадой)) ). Выглядит красивше, но фактическая последовательность выполнения у вас реализуется в рантайме, а не в коде, как следствие дебажить подобный код — занятие не из приятных, особенно в случае, если вызываемых функций штук 30+.
Посему к предложенному способу рекомендую относиться с большой осторожностью.
err, a = getA()
if err = nil {
err, b = getB(a)
if err = nil {
return getC(b)
}
}
return err, nil
Не попроще будет?
Monadic-style computation без синтаксической поддержки со стороны языка — это всегда печально и неудобно. В Haskell есть do-нотация. В JavaScript, к примеру, когда появились Promises (это по сути монады для асинхронных вычислений), все очень страдали, пока не появился async/await синтаксис для них.
А вообще мне дико странно видеть, что в языке принципиально нет поддержки исключений, но при этом также нет никакого вменяемой альтернативы этому (типа монад с поддержкой на уровне синтаксиса). Мне кажется, это какой-то неправильный язык для неправильных пчел. My 2 cents.
нет поддержки исключений, но при этом также нет никакого вменяемой альтернативы этому
panic + defer + recover. Во многих ситуациях этот механизм гибче и удобнее, чем классический throw + catch
Да ладно?
defer реально удобне finally, но с ним реальный код финализации определяется только на момент исполнения.
panic c throw — те же яйца вид в профиль.
А вот recover хуже catch: вся обработка конкретных ошибок и проброс выше только вручную.
В целом механизмы полностью эквивалентные с точностью до синтаксического сахара.
Но что есть, то есть: механизм исключений в Go имеется и вполне рабочий, просто авторы языка исключения сильно не любят.
Чего только люди не сделают, лишь бы не добавлять в язык null-safe цепочки в духе a1?.getA2()?.getA3()
Костыльно-ориентированное программирование.
это как убраться запихав все вещи в шкаф. Типа шкаф открываешь и всё ооттуда вываливается.
Я лично не понимаю почему эти кнструкции так напрягают людей, код как код.
На самом деле ума не приложу, почему они не сделали автоматический неявный возврат ошибок и отдельный defer-оператор для них, а вместо этого сделали недоошибки и паники. Вот например код.
func connect(addr string) (*connection, error) {
con, err := db.Connect(addr)
if err != nil {
return nil, err
}
err = prepare(con)
if err != nil {
con.Close()
return nil, err
}
return con, nil
}
Ошибка в нем следующая: если prepare() вывалится с паникой, коннект утечет (кто сказал, что fd закрывается в файналайзере?). Городить ли defer с recover? Наверное не городить. Месяца через три петух на продакшене хорошо прожарится и наточит клюв.
Всего этого безобразия можно было бы избежать, если бы ошибки были бы частью языка, а паник как понятия не существовало:
func connect(addr string) *connection {
con := db.Connect(addr)
onError con.Close()
prepare(con)
return con
}
Не положил в переменную — эквивалентно старой доброй мантре. У всех функций неявное последнее возвращаемое значение — ошибка. onError == defer с проверкой на err != nil. Остается единственный вопрос со стектрейсом, но собственно и все.
Но нет. Мы будем страдать. Спасибо google.
К сожалению, в go это не работает. И не очень ясно хорошо это, или нет. PR'ы и просто предложения разворачиваются на месте, потому что они противоречат религиозным убеждениям core team. Как противоположность — swift. Я не люблю swift, но там, в отличии от go, имеет смысл вкладывать силы и время. Их changelog каждый год впечатляет, и все это делает комюнити. Это круто.
func e(error error) bool {
if error != nil {
return true
}
return false
}
//...
pool, error = pgx.NewConnPool(connPoolConfig)
if e(error) {
log.Crit("Unable to create connection pool", "error", error)
for e(error) {
log.Debug("Try to create connection pool", "warning", error)
pool, error = pgx.NewConnPool(connPoolConfig)
}
}
И в моём мирке всё спокойно (очень уж вымораживает писать «error != nil»). Есть ещё парочка похожих обёрток, более специализированных (для логирования и вывода спец json-ответа об ошибке). Мне норм.Ах вот ты где! Я из-за твоего патча страдаю. Просто знай.
Выпад про синие экраны смерти тут некорректен. Язык действительно тебя заставляет либо как идиоту писать после каждой 1 строчки мантру из 4 строк, либо использовать паники. Но в последнем случае тебя Go-сообщество справедливо закидает заранее подготовленным и свеже-подогретым говном. Вот сидим, пишем эту хреноту, да себя и всех вокруг убеждаем, что это хорошо.
Все таки, лучше не писать хреноту, а понять почему так сделано и почему язык популярен.
Еще один тезис — синтаксис нужно оценивать, исходя из качества написанных библиотек, а не скорости написания кода. Качество библиотек на Go достаточно высоко, значит синтаксис достаточно хорош, осталось понять почему, а ответ на этот вопрос дан уже много раз, осталось поверить, что это не бредни полоумных работорговцев из гугла.
Еще один тезис — в конце концов синтаксис вообще мало что значит, решают возможности языка, компилятор и инструменты.
Для меня единственный спорный вопрос в Go — дженерики, но этот вопрос не решен нигде. Некоторые языки предоставляют возможности мета-программирования, потом декораторы, потом контракты и где-то на пятом уровне абстракции у нас возникает желание заняться машинным обучением, чтобы с этим кодом на Java/C# разбирался наш виртуальный друг.
func upgradeUser(endpoint, username string) error {
getEndpoint := fmt.Sprintf("%s/oldusers/%s", endpoint, username)
postEndpoint := fmt.Sprintf("%s/newusers/%s", endpoint, username)
req, err := http.Get(genEndpoint)
if err != nil {
return err
}
data, err := ioutil.ReadAll(req.Body)
if err != nil {
return err
}
olduser, err := user.NewFromJson(data)
if err != nil {
return err
}
newuser, err := user.NewUserFromUser(olduser),
if err != nil {
return err
}
buf, err := json.Marshal(newuser)
if err != nil {
return err
}
_, err = http.Post(
postEndpoint,
"application/json",
bytes.NewBuffer(buf),
)
return err
}
Функция upgradeUser реализует какой-то сценарий бизнес логики, соответственно она должна абстрагировать от деталей реализации. Но почему-то функция не абстрагирует от низкоуровневых ошибок, которые возвращают используемые в ней функций. Например: мы хотим проапгрейдить юзера, а в результате получаем ошибку ввода-вывода сети или http.ProtocolError или url.Error. Как я должен различать и обрабатывать их? Их даже заллогировать корректно нельзя, т.к. непонятен контекст. Часть ошибок может быть по моей вине, если я указал некорректный endpoint, с остальными ошибками, я скорее всего ничего сделать не могу. Также не забываем, что код, который будет использовать функцию upgradeUser, по хорошему, ничего не должен знать о деталях реализации. И что делать, если в процессе поддержки приложения мы отказались от http и перешли на бинарный протокол? Переписывать все места, где используется upgradeUser чтобы корректно обработать ошибки?
Делаем выводы:
1) Реализация этой функции далека от совершенства, т.к. в ней текут абстракции — не пишите так;
2) Если вы собираетесь вернуть полученную ошибку «наверх» без изменения (заворачивания в другую) трижды подумайте;
3) Следует четко понимать и определять какие ошибки может возвращать ваша функция, детали реализации функций скрывайте, в части обработки ошибок: заворачивайте их в более высокоуровневые либо логгируйте и возвращайте другую;
4) При правильной работе с ошибками разницы нет какой механизм вы используете, если писать корректный код на js с теми же async/await то рука будет болеть к вечеру от написания тонны try {} catch(), особенно если учитывать что там смешан поток обработки ожидаемый ошибочных и неожиданных исключительных ситуаций, и всегда надо будет прокидывать ошибку наверх вручную, если мы не знаем как ее обрабатывать.
Монады для Go-программистов