LINUX.ORG.RU

golang - не хочу возвращать err, хочу паниковать!

 , , обработка ошибок


0

3

Какая-то секта с этими err. Код распухает в несколько раз. Идея с defer выглядит довольно здравой - я в своё время делал такой defer для 1C и для Delphi. Но паника лучше, чем возврат err-ов. Таковой возврат ничего не упрощает. Когда выпадает исключение, сразу виден весь стек. Сгенерированный err не показывает места своего возникновения, т.е. с помощью брекпойнтов нужно много итераций, чтобы локализовать ошибку. А на fatalpanic есть чуть ли не встроенный брекпойнт, во всяком случае, у меня на fatalpanic отладка сама по себе останавливается.

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

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

★★★★★
Ответ на: комментарий от anonymous

А тут языку 10 лет

Да нет ему 10 лет. Го 10 лет назад и Го сейчас это разные языки (скорее рантаймы). И кому нужно в это лезть? Я уже сказал что 1) Го популярен - следовательно если ты обосрался можно сказать начальству что ты вот перепишешь все на Го ты красаучег 2) Какой начальник поспорит с технологией от Гугл. А писать для души это уже неприятно - это ведь не Python

dem ★★
()
Ответ на: комментарий от dem

Бабло дашь? У меня семья, жена не работает, я не работаю, ремонт в трёшке.

Я по клиент-серверной технологии вот такое наваял: https://www.youtube.com/watch?v=nMhwvZ56jHU - не сказать, что перетрудился.

Исходники тут: https://bitbucket.org/budden/clcon

Но это не биндинги, это клиент-серверная архитектура. Гуй нужно кодить на tcl.

den73 ★★★★★
() автор топика
Ответ на: комментарий от feofan

А вот это ты рискнул бы в defer засунуть?

func (db *DB) Close() error {
	db.mu.Lock()
	if db.closed { // Make DB.Close idempotent
		db.mu.Unlock()
		return nil
	}
	if db.cleanerCh != nil {
		close(db.cleanerCh)
	}
	var err error
	fns := make([]func() error, 0, len(db.freeConn))
	for _, dc := range db.freeConn {
		fns = append(fns, dc.closeDBLocked())
	}
	db.freeConn = nil
	db.closed = true
	for _, req := range db.connRequests {
		close(req)
	}
	db.mu.Unlock()
	for _, fn := range fns {
		err1 := fn()
		if err1 != nil {
			err = err1
		}
	}
	db.stop()
	return err
}
Гарантирован ли graceful shutdown, если такое будет вызываться из паники?

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

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

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

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Бабло дашь? У меня семья, жена не работает, я не работаю, ремонт в трёшке.

Сам такой же. Редкостное положение. Главное все эти Гошники отбили желание работать. Может сменить профессию, но на какую ХЗ.

dem ★★
()
Ответ на: комментарий от den73

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

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

feofan ★★★★★
()
Ответ на: комментарий от den73

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

Ну или если нет времени, собрал бы с --race и погонял бы на тестовом контуре. Либо упадет, либо не завершится, либо уже все ок.

feofan ★★★★★
()
Последнее исправление: feofan (всего исправлений: 1)
Ответ на: комментарий от feofan

Это то ли из sql, то ли из sqlx, как оно там, DB.Close(). Это я к тому, что в дефёрах может оказаться весьма нетривиальная логика. Т.е. если наши дела плохи, то os.Exit(1) существенно предпочтителен.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Ты прямо талант заводить себя в такие ситуации. У sql и sqlx огромная база пользователей. Даже если такой Close() во внутреннем коде, при первой проблеме с остановкой админ прибьет процесс с SIGKILL, а разработчики пойдут разбираться почему сервис не останавливается.

Я надеюсь, что перезапуски сервиса случаются у тебя все же чаще таких ситуаций и проблема с graceful shutdown будет обнаружена своевременно.

feofan ★★★★★
()
Последнее исправление: feofan (всего исправлений: 1)
Ответ на: комментарий от den73

Программирование близко к математике, а в математике

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

По поводу обработки ошибок: при обращении к внешней сущности с недетерминированным поведением (файловая система, сеть и пр.) ошибка – вполне ожидаемое и полноправное состояние. Естественно, это состояние должно иметь сопутствующую ветвь обработки.

Joe_Bishop
()

Владимир

https://habr.com/ru/post/443766/ Пробуем контрактное программирование С++20 уже сейчас

Интересный подход ...

anonymous
()
Ответ на: комментарий от feofan

Да нет, это не я талант, это ведь ты говоришь, что defer-ы должны обрабатываться даже если случилось что-то реально плохое. Тут пример у меня не совсем удачный, т.к. считается хорошим тоном иметь пул соединение и вообще никогда не вызывать db.Close. Не говоря о том, насколько это плохо для сервера БД (невежливо отвалившиеся коннекты могут зависать и подвешивать всю базу или требовать ручного вмешательства), пул соединений вызывает другие вопросы: например, что делать, если возникла штатная ошибка (нарушение уникальности ключа), а за ней ошибка в rollback. Просто напечатать сообщение об этом в лог и продолжать работать - это явный способ огрести следующих проблем, поскольку мы вернём в пул коннект в явно неисправном состоянии и это скажется на следующем использовании этого коннекта.

Я уже принял для себя решение, что мой сервис будет отрабатывать без падения только определённое количество заранее известных ошибок. Всё непонятное будет приводить к выходу через os.Exit(n). А выдачу 500, если сервис лежит, видимо, должен будет обезпечивать какой-нибудь нгинкс. Пока не разбирался, как это делается.

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

den73 ★★★★★
() автор топика
Последнее исправление: den73 (всего исправлений: 5)
Ответ на: комментарий от den73

Ты взял один очень специфичный пример и построил на этом общий вывод. Гораздо чаще ты работаешь с какой-либо сущностью, консистентное состояние которой ты обязан обеспечить. И если ты, борец с системой, будешь использовать os.Exit, то просто все поломаешь. И такие ситуации встречаются в реальной жизни гораздо чаще твоего надуманного примера. Надеюсь мне не придется работать в одной команде с такими, как ты. Ну а если придется,

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

feofan ★★★★★
()
Ответ на: комментарий от den73

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

feofan ★★★★★
()
Ответ на: комментарий от feofan

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

den73 ★★★★★
() автор топика
Ответ на: комментарий от feofan

Гораздо чаще ты работаешь с какой-либо сущностью, консистентное состояние которой ты обязан обеспечить

С какой, например?

den73 ★★★★★
() автор топика
Ответ на: комментарий от feofan

Почитал доку кстати, неприятный сюрприз. http://go-database-sql.org/errors.html

You don’t need to implement any logic to retry failed statements when this happens. As part of the connection pooling in database/sql, handling failed connections is built-in. If you execute a query or other statement and the underlying connection has a failure, Go will reopen a new connection (or just get another from the connection pool) and retry, up to 10 times.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

NoSQL бд, ФС, какая-то распределенная система (смесь нескольких БД) и т.д.

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

feofan ★★★★★
()
Последнее исправление: feofan (всего исправлений: 2)
Ответ на: комментарий от feofan

Смесь нескольких БД у меня уже есть, но тут я должен исходить из того, что приложение может быть убито в любой момент. На каждый из таких случаев у меня должен быть готов сценарий восстановления. Желательно, чтобы было достаточно перезапустить программу, а не чинить данные руками. После того, как такой сценарий готов, «случайно» оказывается, что процессы уже прописаны так, что exit(1) можно делать из любого места.

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

den73 ★★★★★
() автор топика
Последнее исправление: den73 (всего исправлений: 1)
Ответ на: комментарий от den73

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

Это здорово, но далеко не всегда реализуемо.

feofan ★★★★★
()
Ответ на: комментарий от feofan

Так оно _может_ быть убито, предусмотрю я это или нет. Сценарии всё равно должны быть расписаны. Удастся ли их автоматизировать и запихать в само приложение - тут я тоже сомневаюсь. Во всяком случае, тут как-то неожиданно быстро начинает всё усложняться.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

А пока придётся поискать, где отключаются эти перезапуски.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Простой пример. Есть медленное хранилище (для долгосрочного хранения). Есть очередь, из которой летит огромное количество данных. Напрямую пихать данные в хранилище нельзя, оно не вывозит такую нагрузку, нужно собирать батчи. Данные терять нельзя. Рассказывай как сделать так, чтобы при убийстве сервиса в любой момент данные не терять.

feofan ★★★★★
()
Ответ на: комментарий от feofan

Я вижу уже три варианта:

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

б) мы полагаемся только на надёжность диска. Тогда нужна перманентная очередь в файле и sync на каждое добавление в очередь.

в) вариант а в исполнении RAID - отправляем для подстраховки очередь в эн таких же «зеркальных» сервисов, и только убедившись, что из них ка < эн всё ещё здоровы, продолжаем работать. В этом случае убивать сервис можно. Хотя возникает вопрос о RAID контроллере.

den73 ★★★★★
() автор топика
Последнее исправление: den73 (всего исправлений: 2)
Ответ на: комментарий от den73

Но из практики известно, что в RAID контроллерах данные гибнут от ошибок уровня операционной системы, которая повреждает сразу всё. Т.е. это не так-то просто.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Чем дальше, тем хуже. Кое как вроде удалось накропать обработку ошибок, которая более-менее понравилась.

https://github.com/budden/a

Но! Во-первых, очень сложно получилось. Во-вторых, очень хрупко.

Есть целый ряд проблем. Этот грёбаный пул соединений. При каждой нетривиальной ошибке, типа ошибки rollback, неизвестно, в каком состоянии всё остаётся. Значит, нужно либо дальше курить доку и верить ей (а значит, влетать по любому поводу), либо падать.

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

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

Короче, даже такое простое приложение оказывается очень непросто написать :)

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

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

В общем, пока ощущение такое, что я какую-то хрень налепил. Переусложнённую. Может быть, более надёжную, чем то, что можно написать по туториалам, но в целом очень странную.

den73 ★★★★★
() автор топика

Я не голангист ни разу, но попробую. Итак, плюсы возврата ошибок через return:

Глядя на тип возвращаемого значения функции можно сразу понять, возможна ли в ней ошибка, или не возможна. Если функция паникует, то это надо указывать отдельной строкой в документации. В джаве для этого есть отдельная хренотень в сигнатурах что-то типа throws SomeKindOfException (не джавист, точный синтаксис не знаю). В питонах-рубях эксепшены кидаются как попало, и это жутко бесит. пишешь код try: ... except А что в эксепт, какой блин класс писать? Ладно если разраб либы вменяем и укажет в доке, на большая часть разрабов не таковы.

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

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

Мне лично очень не нравятся ошибки в го, так как они завязаны на то, что главное возвращаемое значение в случае ошибки nil. nil'ов в языках 21-го века быть не должно.

Нормальная обработка возвращаемых ошибок в haskell'е и rust'е.

UPDATE: немного ошибся. Паниковать можно не только строкой, но interface {}. Это хоть и лучше, но все равно не дает использовать тайпчек в обработчике ошибок.

provaton ★★★★★
()
Последнее исправление: provaton (всего исправлений: 1)
Ответ на: комментарий от provaton

немного ошибся. Паниковать можно не только строкой, но interface {}. Это хоть и лучше, но все равно не дает использовать тайпчек в обработчике ошибок

Даёт. Смотри type assertion & type switch

feofan ★★★★★
()
Ответ на: комментарий от provaton

В err гарантирована тоже только строка, а учитывая наличие в языке динамической инфы о типе и возможность напечатать что угодно с помощью %#v, разницы вообще никакой. Ничто не мешает «обработать» ошибку так:

result, _ := fn()
ошибка игнорируется, или так
result, err := fn
if err != nil { 
 return nil, err
}
Это эквивалентно коду с исключениями без блока try..except. Документировать панику - это было бы классно, но я пока ни разу не видел заявления «эта функция не паникует» в описании какой-либо функции. А значит, в любом случае, независимо от err, нужно предусмотреть панику всех сортов.

И ещё вот - out of memory. Завёл страничку, т.к. на форуме заведомо потеряется http://вики-ч115.программирование-по-русски.рф/Go/OutOfMemory

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

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

Документировать панику - это было бы классно, но я пока ни разу не видел заявления «эта функция не паникует» в описании какой-либо функции.

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

provaton ★★★★★
()
Ответ на: комментарий от den73

Да, кстати, во втором примере ошибка не игнорируется, так как ты выходишь из функции. Например, если fn - это открытие файла а после ее вызова идет чтение из этого файла, то в первом случае ты попытаешься прочитать из nil'а (лол, какой замечательный язык), а во втором случае прекратишь выполнение функции. Второй случай - не совсем игнорирование.

В rust'е первый пример невозможен в принципе, потому что там нет nil'ов. Поэтому го место на помойке.

provaton ★★★★★
()
Ответ на: комментарий от provaton

Мне лично очень не нравятся ошибки в го, так как они завязаны на то, что главное возвращаемое значение в случае ошибки nil. nil'ов в языках 21-го века быть не должно.

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

anonymous
()
Ответ на: комментарий от provaton

И это правильно. Непустые интерфейсы гораздо лучше.

feofan ★★★★★
()
Ответ на: комментарий от provaton

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

den73 ★★★★★
() автор топика
Последнее исправление: den73 (всего исправлений: 1)
Ответ на: комментарий от provaton

interface{} позволяет писать на голанге в стиле динамической типизации. Если бы interface{} не было, я бы не стал заниматься голангом. Потому что в Си и всех его производных невозможно сделать совершенно элементарный «список из любых значений», а в голанге можно.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

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

void* ?

feofan ★★★★★
()
Ответ на: комментарий от anonymous

Проблемы динамической типизации носят количественный, а не качественный характер. Во-первых. Во-вторых, есть места, где дин. типизация адекватна. Например, в REPL без неё никак невозможно. Даже в Хаскеле де-факто она есть, хотя вряд ли хоть один Хаскелист это признает. Если что-то нужно, то оно не должно создаваться костылями. Нужно дать разработчику нормальные инструменты и поверить в то, что разработчик применит их правильно. Во всяком случае, я, как разработчик, выбираю те языки, где так сделано.

den73 ★★★★★
() автор топика
Ответ на: комментарий от provaton

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

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

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Нилы очень эффективны.

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

Значит, ли это, что простой дудочке место на помойке?

Если эта дудочка раз в месяц стреляет в мозг дудящего - то думаю да, место на помойке.

provaton ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.