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

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

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

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

Роб Пайк (надеюсь, не надо представлять этого благородного дона) в одном из старых своих постов писал…

Ah, variable names. Length is not a virtue in a name; clarity of expression is.

Андрю Джеррард (он чуть менее известен, так что вот ссылка на его гитхаб) Посвятил в 2014-м году этой теме один из первых своих докладов по go, который буквально так и называется, What's in a name

Эти идеи прослеживаются в стандартной библиотеке и Go’s Code Review comments где прямо сказано

вы должны предпочитать c вместо lineCount

Я немножко забежал вперед, просто сразу хотелось сослаться на авторитеты. Школьная учительница по риторике, всегда говорила, что ссылка на авторитеты в начале, это хорошо и надежно (спасибо вам, Марианна Юрьевна)

func countLines() int {
    // do stuff
    var linesCount int
    for i := 0; i < lines; i++ {
        linesCount += 1
    }
    return linesCount
}

// Не читается лучше чем 

func countLines() int {
    // do stuff
    var c int
    for i := 0; i < lines; i++ {
        c += 1
    }
    return c
}

Почему так?

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

Верхняя версия содержит больше текста, но смысл не стал понятнее. Напротив, дополнительная длина мешает восприятию.

В стандартной библиотеке Go короткие имена используются повсеместно. Например, функция io.Copy:

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
}

Здесь используются короткие имена: dst (destination), src (source), n (number of bytes). Они интуитивно понятны, потому что:

  • Их смысл ясен из контекста функции.

  • Область видимости переменных ограничена циклом или функцией.

Или в пакете math

func Max(x, y float64) float64 {
    if x > y {
        return x
    }
    return y
}

Имена x и y передают математическую сущность, а не логику программы. Более длинные имена, такие как firstNumber и secondNumber, только усложнили бы восприятие.

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

Когда оправдано использовать короткие имена переменных?

Если вкратце - когда они помогают снизить визуальный шум, и улучшить читаемость, за счет лаконичности

  • Когда переменная используется в небольшой функции

  • В общепринятых сокращениях, которые широко используются и понятны большинству разработчиков: err для ошибок, ctx для контекста, i, j для индексов в циклах, ok для булевых вовращаемых параметров функции и тп

  • Для временных переменных, которые используются в одном месте или в короткой области (1-7 строк) часто достаточной однобуквенной перменной, а для средней области видимости (7-15 строк) короткого слова

  • Для ресивера метода, почти никогда не нужно больше двух символов, лучше один

  • Для распространенных типов: r для io.Reader или *http.Request, w для io.Writer или http.ResponseWriter и тп

Стоит помнить, что имя локальной переменной должно отражать её содержимое и то, как она используется в текущем контексте, а не то, откуда произошло значение.

Тем не менее

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

Простое правило (примерно в таком виде оно изложено в Go code review comments)

чем дальше от места объявление используется переменная, тем подробнее должно быть имя

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

var cfg string // непонятно, что это за конфигурация
var databaseConfig string // так понятнее

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

type User struct {
    ID        int
    FirstName string
    LastName  string
}

Имена FirstName и LastName однозначно описывают содержимое переменной, в отличие от кратких Fn и Ln.

Если смысл переменной не ясен из контекста, лучше использовать описательное имя.

func CalculateDiscount(price float64, percentage float64) float64 {
    discountAmount := price * percentage / 100
    return price - discountAmount
}

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

Помните:

Лаконичность кода — это не цель, а средство для достижения читаемости и понимания.

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

Комментарии (7)


  1. Vitimbo
    11.01.2025 11:59

                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. Если код будет побольше и не очень хорошо структурирован, то придется сидеть и расшифровывать, что же хотел сказать автор.

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


    1. skovpen
      11.01.2025 11:59

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


      1. Vitimbo
        11.01.2025 11:59

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


    1. sl4mmer Автор
      11.01.2025 11:59

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

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

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

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


      1. dopusteam
        11.01.2025 11:59

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

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

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


  1. alabamaa
    11.01.2025 11:59

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


    1. sl4mmer Автор
      11.01.2025 11:59

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