Pull to refresh

Comments 67

            nw, ew := dst.Write(buf[0:n])
            if nw > 0 {
                written += int64(nw)
            }
            if ew != nil {
                err = ew
                break
            }
            if n != nw {
                err = io.ErrShortWrite
                break
            }

Мимопроходящий шарпист негодует. В приведенном примере я не могу просто посмотреть на 9 строчку и понять что такое nw и с чем сравнивается. Надо возвращаться до объявления, смотреть что за метод вызывается, что он возвращает и только потом прыгать обратно на 9 строку, где есть еще какое-то n, которое описано еще выше. При написании с нуля, когда контекст и задача полностью в голове, это не составит проблем, но читать чужое, или свое, через какое-то время, может вызвать определенные сложности.

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

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

В современной версии пакета n переименовали в nr и стало логичней и понятней :-)
В шарпах тоже любят FileStream fs = ...

В шарпах за такое дважды должны ударить по рукам. 1 раз за FileStream вместо var, второй раз за само название. У меня на такое название даже ide заругается, что "тут опечатка, поправь, кожаный".

Если уже говорить в таком ключе, то "бить по рукам" нужно как раз таки за использование var.

Почему? В C# var никакого негативного влияния на перформанс не имеет, и никакого отношения к т.н. динамическим типам тоже не имеет.

Это всего лишь полный аналог auto в C++ и := в Go. Реальный тип переменной определяется на этапе компиляции.

Иногда довольно сложно понять с каким типом имеешь дело. Ну вот к пример, открыл я код в bitbucket/github/notepad, и как мне посмотреть глазами на тип данных? Не всегда же используется IDE, да и не всегда удобно наводить курсор тогда, когда можно было этого не делать. Сам набор текста, даже с явными типами, занимает сильно меньше времени чем отладка и сопровождение.

Аргумент. Но в то же время, когда пишете код, и нужно в новую переменную положить результат какой-то библиотечной фукнции, полную сигнатуру которой вы не помните, вам IDE подсказала только ее название и список аргументов, но возвращаемый тип у нее например std::map<time_t, const SomeType&> - такое вводить при каждом объявлении новой переменной (а сначала зайти в библиотечную функцию и скопировать этот тип) - довольно таки нудное занятие :)

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

А если создаете новый объект, то как бы и совсем смысл теряется два раза повторять тип:

std::map<time_t, const SomeType&> var1 = std::map<time_t, const SomeType&>();

// VS
auto var2 = std::map<time_t, const SomeType&>();

Если говорим о C++, то там действительно тип может иметь сложную сигнатуру и прописывать его будет нудно. Или там какие-нибудь итераторы, где в целом может быть понятен хранимый тип. Еще бывают случаи, когда программист задумывает возможность "безболезненной" смены типа данных где-то выше по коду и во всех местах auto останется без изменений, мне такой подход не нравится, но я могу согласится с некоторыми доводами в случае C++. Когда мы говорим о C#, то сейчас можно сместить тип в объявлении в левую сторону, оставив в правой только new(), таким образом код будет последователен если придерживаться отсутствия var.

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

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

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

  • Use var only when a reader can infer the type from the expression. Readers view our samples on the docs platform. They don't have hover or tool tips that display the type of variables.

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

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

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

ну на самом деле, из контекста там понятно, что nw это сокращение он number of bytes written

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

>Ведь код не всегда будет короткий и грамотно разбит на функции

А вот к этому стотит стремиться - го располагает к этому, даже гранулярность обработки паник (читай эксепшенов) на уровне функций. Естественно если функция длинная и переменная используется в широком контексте (более 15 строк) то стоило бы например в примере из io. написать numWritten например

ну на самом деле, из контекста там понятно, что nw это сокращение он number of bytes written

А почему не bw? Кажется логичнее было бы

А ew что значит?

>А почему не bw? Кажется логичнее было бы

Возможно, тут уже кому-как

>А ew что значит?

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

Возможно, тут уже кому-как

Хороший комментарий, каждый придумывает своё сокращение, но 'из контекста понятно' всем.

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

Какие то костыли в десятке строк кода) и я про сокращение спрашивал, а не про смысл) ew - error of writing? Почему не we?)

С еw понятно - ошибка если уж не err, то с e начинается, хотя бы я назвал errw, например

Какие то костыли в десятке строк кода)

Еще в таких случаях, чтобы не затенять переменную err, могут лепить переменные err2, err3 и тд )

цифры в именах переменных это мрак, конечно, очень давно такого не видал

ну на самом деле, из контекста там понятно, что nw это сокращение он number of bytes written

Понял это только после вашего уточнения, изначально читал 9 строку как "ЕСЛИ Н НЕ РАВЕН СевероЗапад, ТО ..."

не должно быть лишнего визуального шума

Это не означает, что стоит уходить в крайности. Переменная типа countOfBytesReadedFromRecentOpenedFile (хотя, я видел и даже пару раз писал и по длиннее имена) также ужасно как и nw/nbf. В данной ситуации идеально подошло бы readedBytesCount. Лаконично и полностью отражает что тут лежит.

А вот к этому стотит стремиться - го располагает к этому

Стремиться, может быть, и стоит, только любой успешный продукт рано или поздно начинает разрастаться. Мне доводилось повидать проекты, превратившиеся в монстров. Уверен и на go такие уже существуют. И то, что функция сегодня складывает 2 числа не защитит ее от того, что завтра прилетит менеджер и попросит прикрутить туда логи и тонкую настройку доступов, чтобы для проверяющих 2 + 2 было 4, а для бухгалтера столько, сколько надо. И вот, ваша функция на 2 строки с переменными a b c мутировало во что-то, чего будут бояться даже чеченцы.

Итого, мы получаем какой-то набор аббревиатур, которые известны только их создателю, и то, пока он их не забыл. И то, у одного в голове логично будет смотреться nw, а у другого bc или wbc. Вы друг друга даже не поймете. Это примерно одинаковый уровень упоротости с параметрами утилиты tar в линуксе.

Жить намного проще, когда код можно читать как простое предложение на английском языке.

Кхе-кхе, read это неправильный глагол, поэтому формы readed не существует. Примерно как по-русски кто-то бы попытался написать "победить" в будущем времени ( победю, побежду... ? )

Как-то я упустил этот момент. Аббревиатура спасает от неправильной формы глагола.

>Это не означает, что стоит уходить в крайности.

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

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

В Go очень мало визуального шума. Удивляюсь что кто-то Go называет плохо читаемым.

любопытно, разовьете мысль?

Сравните два куска псевдокода ниже, которые делают одно и то же:

func DoGoodThing() {
  DoThis();
  DoThat();
  MakeThemHappy();
}
func DoGoodThing() {
  err := DoThis()
  if err != nil {
    return err
  }
  err = DoThat()
  if err != nil {
    return err
  }
  err = MakeThemHappy()
  if err != nil {
    return err
  }
}

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

В классическом языке с исключениями, обработка каждой ошибки была бы ещё хуже:

func DoGoodThing() {
  try {
    DoThis();
  } catch {
    return
  }
  try {
    DoThat();
  } catch {
    return
  }
  try {
    MakeThemHappy();
  } catch {
    return
  }
}

И не нужно писать про то, что в try можно обернуть все 3 функции - это уже не будет обработкой индивидуальных ошибок.

`if err != nil` не идеальное, но элегантное в своей простоте решение.

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

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

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

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

всегда проще обработать ошибку там, где она случилась

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

Мне кажется пример tolyanski более показательный, нежели пример из статьи, в статье всё таки EOF обрабатывается прямо в методе.
В Go действительно для таких случаев не помешал бы какой-нибудь оператор возврата ошибки по типу "?" из Rust

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

В данном случае никакой индивидуальной обработки нет, поэтому try...catch будет на вызывающей стороне.

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

Именно. Я не топлю за исключения как таковые, я топлю за читаемость кода

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

В Го мало того, что нет перегрузки функций, так ещё и нет file scope для названий функций. Поэтому даже если функция используется в пределах одного файла, ей нужно давать имя, неконфликтующее с другими функциями во всем пакете. Почему так? Чем авторам языка так насолил обычный static из C?

так ещё и нет file scope для названий функций

Выход один: больше пакетов хороших и разных)

Почему так? Чем авторам языка так насолил обычный static из C?

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

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

Ну Роб Пайк (которого я в начале упомянул), он свои идеи о стиле, ставшие основой философии Go вынес из своего С опыта, есть его статья, 1989-го года, "Notes on Programming in C" - вам возможно будет любопытно, там и про именование есть

Основа философии Го была напрямую сформулирована Пайком: язык для тупых, но старательных. Вот прямая цитата:

They’re [Googlers] not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.

Ещё Пайк не использует подсветку синтаксиса:

Syntax highlighting is juvenile. When I was a child, I was taught arithmetic using colored rods. I grew up and today I use monochromatic numerals.

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

Неужели кто-то на самом деле уверен в том, что именование(длина) переменных, зависит от языка? И что для го не тоже самое что для пыха или питона...

Зависит. У каждого языка(комьюнити) свои общепринятые стандарты, линтеры и common knowledge.

Мне плевать на авторитеты. Тем более в цитате Пайка приветствуется ясность, а автор выдаёт ее за краткость.

Что такое nw и ew сам не догадался.

Го с его обработкой ошибок через err совсем не лаконичен, а наоборот.

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

Стадартные сокращения типа err и ctx надо соблюдать, но даже в указанном примере вместо errWrite сделали ew и сразу стало непонятно. Если это стандарт в модуле, и встрчается многократно с этим можно жить. Но если сокращения бессистемные или неоднозначные, это ужас. Например переменная c может одначать и char и count и client и черта лысого.

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

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

>Поэтому уважайте тех кто придёт за вами

В том и смысл,краткость (там где она уместна) и нужна для ясности, чтоб читающему было удобнее, не нужно задерживаться взглядом на каждой yetAnotherVar,

>Детализация может казаться излишней писателю, так как для него всё очевидно

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

Я понимаю вашу позицию, и во многом вы правы, сам я до этого очень долго писал на php/java/delphi/python и привык считать говорящие имена хорошим тоном, но стоит сказать, что за последние годы скорректировал свою позицию в сторону более взвешенной - краткость, там где она к месту, прекрасна. А бездумным применением практик - можно любой подход дискредетировать

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

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

Соглашусь, same here. Правда в моем случае не 10 а 5 лет.
Но я считаю, что мы просто уже привыкли к этому. Человек ведь такая сволочь, ко всему привыкает)

Но регулярное продолжение пописывания мной то на C++, то на C#, то на PHP, напоминает мне, от чего я отказался...

Одной из ключевых особенностей Go является ориентация на читаемость и краткость кода.

Каждый раз, когда кто-то где-то упоминает "ориентацию на читаемость в Go", я со слезами пересчитываю, сколько раз я за сегодня споткнулся об `if err != nil`, разбирая очередной спагетти...

Как 'if err != nil' мешает читаемости? Предельно простая конструкция, ничуть хуже не делает

Так же как и обсуждаемая здесь захламляемость пространства длинными именами.
if err != nil через каждые 3-5 строчек, это ни что иное как нагромождение, мешающее визуально быстро распознавать полезную логику.

В особых случаях несколько помогает IDE, у которой есть возможность "сворачивать" этих монстриков, чтобы вычитывать полезный код. Но даже сам факт, что GoLand от JetBrains сделал такую фичу под названием code folding для таких кейсов, говорит о том, что сам по себе код на Go - не идеально читаемый, если открыть его в простом блокноте. Помню похожую фичу в CLion для C++ - так она там только для автоматического сворачивания портянки импортов. А в Go прям по самому коду у каждого столба надо сворачивать мусор.

Разумеется, все познается в сравнении. Я говорю про это потому что повидал достаточно других языков, где с этим делом проблем поменьше. Да, разумеется, у других языков своих собственных проблем хватает. Но я и не утверждаю, что язык X объективно лучше языка Y.

я выше уже писал, мне чтоб полюбить if err!=nil понадобилось наверное лет 5-6, использования го как основного языка и должен сказать у этого подхода есть определенные плюсы

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

С точки зрения читаемости if err != nil делает обработку ошибок явной и легко заметной. Ошибки не “заглушаются” и не остаются незамеченными (ну в идеале)

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

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

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

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

чтоб полюбить if err!=nil понадобилось наверное лет 5-6

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

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

Но ведь это необходимое "зло". Как иначе могла бы выглядеть индивидуальная обработка ошибок? Максимум можно на одну строку сократить с вложенным if. Стандартные try catch - гораздо хуже.

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

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

Но то что это необходимо зло - полностью согласен.

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

func findPatternInFile(pattern string, filePath string) ([]string, error) {
  var err! error
  default return nil, err

  re, err! := regexp.Compile(pattern) // err != nil: default return
  file, err! := os.Open(filePath) // err != nil: default return
  result := make([]string, ...)
  ...
  return result, nil // successful return
}

Это усложняет читаемость

Наоборот упрощает, убирая монотонные повторы if err != nil.

Не согласен:

  1. Держать в голове или постоянно скроллить до default. Можно забыть и создать еще одну причину для бага.

  2. Если функция вернула err, не очевидно как она обработается и почему мы пишем return result, nil. Так же должен ругнуться компилятор на неиспользуемую переменную.

  1. Тому, кто пишет в функциях простыни больше экрана, не привыкать скроллить. Остальным скроллить не придётся, да и такое решение уменьшает количество строк.

  2. Простите, что? Если нужна нетривиальная обработка, писать её как обычно через if. А это чисто передача ошибки наверх. И откуда неиспользуемая переменная, если под капотом она проверяется на nil?

У меня нет желания с вами спорить. Напишите proposal, вам там все разъяснят.

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

Сразу не возлюбил Go, когда узнал, что есть запрет на перенос скобок, - теперь для меня все это выглядит как "гомнокод" :(

Питон бы вы наверное вообще возненавидели?

Питон вообще отвратный язык. 🙂

В принципе, всё верно указано в статье. Но еще некоторые называют переменные на основе начальных слов, построенных, например, по правилам CamelCase.

type MySuperService struct {}
func (mss *MySuperService) Do() {}

type UserService struct {}
func (us *UserService) Do {}

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

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

func Copy(destination Writer, source Reader) (totalBytesWritten int64, err error) {
    var buffer [32 * 1024]byte

    for {
        bytesRead, err := source.Read(buffer[:])

        if bytesRead > 0 {
            bytesWritten, writeError := destination.Write(buffer[:bytesRead])

            if bytesWritten > 0 {
                totalBytesWritten += int64(bytesWritten)
            }

            if writeError != nil {
                err = writeError
                break
            }

            if bytesRead != bytesWritten {
                err = io.ErrShortWrite
                break
            }
        }

        if err != nil {
            if err == io.EOF {
                err = nil
            }
            break
        }
    }

    return totalBytesWritten, err
}

не более читабельно, чем это

func Copy(dst Writer, src Reader) (written int64, err error) {
    var buf [32 * 1024]byte
    for {
        n, err := src.Read(buf[:])
        if n > 0 {
            nw, ew := dst.Write(buf[0:n])
            if nw > 0 {
                written += int64(nw)
            }
            if ew != nil {
                err = ew
                break
            }
            if n != nw {
                err = io.ErrShortWrite
                break
            }
        }
        if err != nil {
            if err == io.EOF {
                err = nil
            }
            break
        }
    }
    return written, err
}

?

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

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

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

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

Правда, суровая реальность такова, что на эту "идиоматичность" всем плевать, кроме самих авторов языка и сайтов типа go.dev или gobyexample :)

Sign up to leave a comment.

Articles