golang

Соглашения по именованию в Go: практическое руководство

  • среда, 6 мая 2026 г. в 00:00:14
https://habr.com/ru/companies/otus/articles/1031544/

Не уверены, как правильно структурировать веб-приложение на Go?

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

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

Идентификаторы

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

  • Идентификаторы могут содержать только символы Unicode, цифры и символ подчеркивания.

  • Идентификаторы не могут начинаться с цифры.

  • Нельзя использовать в качестве идентификаторов следующие ключевые слова Go:

break        default      func         interface    select
case         defer        go           map          struct
chan         else         goto         package      switch
const        fallthrough  if           range        type
continue     for          import       return       var

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

  • Для неэкспортируемых идентификаторов используйте стиль camelCase, а для экспортируемых — PascalCase. Не используйте альтернативные варианты вроде snake_case, Pascal_Snake_Case, SCREAMING_SNAKE_CASE или ALLUPPERCASE.

  • Слова, являющиеся акронимами или аббревиатурами (например, API, URL или HTTP), должны иметь единообразное написание внутри идентификатора. Например, apiKey или APIKey — корректные варианты, а ApiKey — нет. Это правило также относится к ID, когда оно используется как сокращение от «идентификатор» — следует писать userID, а не userId.

  • Хотя допустимы любые символы Unicode, использование не-ASCII символов часто ухудшает читаемость кода и усложняет его написание, поэтому на практике встречается редко. Если нет действительно веской причины, лучше использовать только ASCII-символы в идентификаторах. Например, писать pi вместо π, beta вместо β, naiveBayes вместо naïveBayes.

  • Чтобы избежать путаницы и потенциальных ошибок, не используйте имена, совпадающие со встроенными типами Go. Например, не называйте переменные int, bool или any. Аналогично, не стоит создавать функции с именами, совпадающими со встроенными функциями, такими как min, max, len или clear.

Старайтесь не включать тип в имя идентификатора — например, не используйте имена вроде fullNameString, scoreInt или float64Amount. Основное исключение — ситуация, когда вы приводите переменную к другому типу и хотите различать исходное значение и результат преобразования. В этом случае добавление типа в имя — распространенный и допустимый способ их различить. Например, такой код считается нормальным:

userID := 42
userIDStr := strconv.Itoa(userID)
  • По возможности избегайте имен, совпадающих с названиями пакетов стандартной библиотеки. Это более «мягкая» рекомендация, чем предыдущие, потому что стандартная библиотека «занимает» много хороших имен — таких как json, js, mail, user, csv, path, filepath, log, regexp, time и url — и иногда сложно придумать достойную альтернативу. Тем не менее, точно не стоит использовать идентификаторы, совпадающие с именами пакетов, которые вы реально импортируете и используете. Например, если вы импортируете пакеты url и net/mail, не используйте слова url и mail как имена в этом коде.

Здесь приведены примеры хороших и плохих имен идентификаторов:

Плохо

Причина

Лучше

order.total := 99.99

func load-user()

Недопустимы знаки пунктуации

orderTotal := 99.99

func loadUser()

const 3rdParty = "x"

func 2FactorAuth()

Имя не может начинаться с цифры

const thirdParty = "x"

func twoFactorAuth()

max_value := 10

func Fetch_user()

Нестандартный стиль написания

maxValue := 10

func FetchUser()

type HttpClient struct{}

func parseXml()

Несогласованное написание аббревиатур

type HTTPClient struct{}

func parseXML()

func GetSessionId()

type OrderId string

ID должно быть в верхнем регистре

func GetSessionID()

type OrderID string

résuméCount := 2

const Σ = 100

Не-ASCII символы

resumeCount := 2

const sum = 100

func clear()

int := cache.Internal()

Конфликт со встроенными типами или функциями

func clearQueue()

data := cache.Internal()

intCount := 42

resultSlice := []int{}

Тип включен в имя

count := 42

results := []int{}

type json struct{}

var log = newLogger()

Конфликт с именами пакетов стандартной библиотеки

type payload struct{}

var logger = newLogger()

Экспортируемые и неэкспортируемые идентификаторы

Идентификаторы в Go чувствительны к регистру. Например, apiKey, apikey и APIKey — это разные имена.

Если идентификатор начинается с заглавной буквы, он считается экспортируемым — то есть доступен коду за пределами пакета, в котором объявлен. Это означает, что регистр первой буквы имеет значение и влияет на поведение всей кодовой базы. Соответственно, не стоит начинать имена с заглавной буквы просто потому, что так «красивее» — делайте это только тогда, когда действительно хотите сделать идентификатор доступным извне.

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

Как правило, чем меньше вы экспортируете, тем проще в дальнейшем рефакторить код внутри пакета, не затрагивая другие части системы. Есть хорошая мысль из книги «Программист-прагматик (“The Pragmatic Programmer”), которую можно адаптировать под Go:

Пишите «скромный» код — пакеты, которые не раскрывают ничего лишнего другим пакетам и не зависят от их внутренней реализации.

Еще один совет: пакет main крайне редко импортируется где-либо, поэтому все идентификаторы в нем обычно должны быть неэкспортируемыми и начинаться с маленькой буквы. Самое частое исключение — когда нужно экспортировать поле структуры, чтобы оно было доступно пакетам, использующим рефлексию, например encoding/json, encoding/gob или github.com/jmoiron/sqlx.

Длина и описательность идентификаторов

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

Если область видимости узкая и идентификатор используется рядом с местом объявления, допустимо использовать короткие и не слишком описательные имена. Например, в небольших циклах for, блоках range или очень коротких функциях часто применяются короткие или даже односимвольные имена — это нормальная практика в Go.

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

Вот хороший пример, который Дэйв Чейни приводил в своей презентации Practical Go:

type Person struct {
    Name string
    Age  int
}

func AverageAge(people []Person) int {
    if len(people) == 0 {
        return 0
    }

    var count, sum int
    for _, p := range people {
        sum += p.Age
        count += 1
    }

    return sum / count
}

В этом коде внутри короткого блока range идентификатор p используется для значения из среза people. Блок range настолько маленький и компактный, что односимвольного имени здесь вполне достаточно.

А вот переменные count и sum сначала объявляются, затем используются внутри range, а потом еще раз в операторе return. Более описательные имена сразу делают код понятнее: видно, что он делает и что эти переменные обозначают. Односимвольные имена вроде c и s были бы менее понятны. При этом переменные используются только внутри функции AverageAge, поэтому еще более подробные имена вроде peopleCount и agesSum были бы уже избыточно многословными.

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

Именование пакетов

Жесткие правила для имен пакетов такие же, как для идентификаторов: они могут содержать символы Unicode, цифры и символ нижнего подчеркивания, не должны начинаться с цифры и не должны совпадать с ключевыми словами Go. Но на практике соглашения по именованию пакетов гораздо строже. Обычно:

  • Имена пакетов должны содержать только строчные ASCII-буквы и цифры.

  • Поскольку при написании кода имена пакетов приходится часто набирать вручную, имя в идеале должно быть коротким, удобным для ввода и отражать содержимое пакета. Часто хорошо работают простые существительные из одного слова, например orders, customer и slug.

  • Если вы хотите использовать в имени пакета несколько слов, их нужно писать слитно, строчными буквами и без разделителя. Например, ordermanager — обычное имя пакета, а orderManager или order_manager — нет.

  • Если имя пакета кажется слишком длинным, допустимо использовать сокращения. Это можно увидеть в некоторых именах пакетов стандартной библиотеки: например, expvar вместо exportedvariables и strconv вместо stringconversion.

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

  • Имена пакетов с префиксом . или _ «невидимы» для Go и полностью игнорируются при запуске go build, go run, go test и т. д. Поэтому не начинайте имя пакета с этих символов, если только вы специально не хотите, чтобы пакет игнорировался.

  • Каталоги с именами vendor, testdata и internal имеют в Go специальное значение, поэтому во избежание путаницы и ошибок не используйте эти слова как имена пакетов.

  • Избегайте универсальных имен пакетов вроде common, util, helpers, types или interfaces: они почти ничего не говорят о содержимом пакета. Например, что находится в пакете helpers — вспомогательные функции для валидации, форматирования, SQL? Всё сразу? По одному названию это не понять.

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

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

Плохо

Причина

Лучше

package 3rdparty

package 2fa

Имя не может начинаться с цифры

package thirdparty

package twofa

package OrderManager

package order_manager

Нестандартный регистр / использование разделителей

package ordermanager

package o

package stuff

Слишком расплывчато и неописательно

package orders

package slug

package ordermanagementsystem

Слишком длинное имя / неудобно набирать

package orders

package ordermgr

package url

package mail

Конфликт с именами пакетов стандартной библиотеки

package links

package mailer

package _cache

package .hidden

Игнорируется инструментами Go

package cache

package hidden

package internal

package vendor

package testdata

Специальные имена каталогов в Go

package internalauth

package supplier

package utils

package helpers

Универсальные имена с неясной областью ответственности

package validation

package formatting

Именование файлов

В идеальном мире имя .go-файла должно кратко описывать его содержимое, состоять из одного слова и быть полностью в нижнем регистре. Хорошие примеры из пакета net/http стандартной библиотеки: cookie.go, server.go и status.go.

Если не получается придумать удачное имя из одного слова и хочется использовать два или больше, строгого соглашения о разделении таких слов нет. Даже в самой стандартной библиотеке Go нет полной единообразности. Иногда слова в именах файлов разделяются нижним подчеркиванием, например routing_index.go и routing_tree.go, а иногда пишутся слитно, как в batchcursor.go, textreader.go и reverseproxy.go.

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

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

  • Как и пакеты, файлы с префиксом . или _ «невидимы» для инструментов Go и полностью игнорируются при запуске go build, go run, go test и т. д.

  • Файлы с суффиксом _test.go запускаются только инструментом go test. При использовании go run или go build они игнорируются.

  • Файлы с любым из следующих суффиксов будут включаться только при компиляции под соответствующую операционную систему: aix.go, android.go, darwin.go, dragonfly.go, freebsd.go, illumos.go, ios.go, js.go, linux.go, netbsd.go, openbsd.go, plan9.go, solaris.go, wasip1.go, _windows.go.

  • Аналогично, файлы с любым из следующих суффиксов будут включаться только при компиляции под соответствующую архитектуру: 386.go, amd64.go, arm.go, arm64.go, loong64.go, mips.go, mips64.go, mips64le.go, mipsle.go, ppc64.go, ppc64le.go, riscv64.go, s390x.go, wasm.go.

Как избегать лишнего повторения в именах

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

Например, если у вас есть пакет customer, то имена функций вроде NewCustomer() или CustomerOrders() будут избыточно повторять слово customer при вызове извне пакета: customer.NewCustomer() и customer.CustomerOrders(). Имен New() и Orders() достаточно, и в месте вызова они читаются лучше: customer.New() и customer.Orders().

Та же рекомендация относится и к экспортируемым типам. Например, если в пакете customer нужно представить адрес или номер телефона, достаточно и менее многословно назвать типы Address и PhoneNumber, а не CustomerAddress и CustomerPhoneNumber.

Плохо

Причина

Лучше

customer.NewCustomer()

customer.CustomerOrders()

Избыточное дублирование в вызове (chattery function call)

customer.New()

customer.Orders()

customer.CustomerAddress

customer.CustomerPhoneNumber

Избыточное дублирование в типах (chattery type reference)

customer.Address

customer.PhoneNumber

Примечание: часто возникает желание объявить экспортируемый тип с тем же именем, что и пакет. Например, пакет customer может экспортировать тип Customer, который представляет отдельного клиента. Тогда в других пакетах мы будем обращаться к этому типу как customer.Customer.

Да, это очевидно избыточно, но такого повторения трудно избежать, не сделав имя пакета или типа менее понятным. Поэтому на практике вы будете часто такое встречать. Например, в стандартной библиотеке пакет time содержит тип Time, к которому обращаются как time.Time; пакет context содержит тип Context, к которому обращаются как context.Context; а пакет regexp содержит тип Regexp, к которому обращаются как regexp.Regexp.

Как и в случае с функциями и типами, имена методов в идеале тоже не должны создавать лишних повторений при вызове. Например, если вы пишете методы для типа Token, скорее всего, метод лучше назвать Validate(), а не ValidateToken(), или IsExpired(), а не IsTokenExpired().

Получатели методов

При создании методов принято давать получателю короткое имя — обычно от 1 до 3 символов, чаще всего это сокращение от типа, для которого реализован метод. Например, если вы пишете метод для типа Customer, идиоматичное имя получателя — что-то вроде c или cus. А если для типа HighScore — хорошим вариантом будет hs.

В комментариях к код-ревью Go рекомендуется избегать слишком общих имен вроде self или me для получателя.

Также важно соблюдать единообразие: все методы одного типа должны использовать одно и то же имя получателя. Не стоит в одном методе использовать c, а в другом cus.

type Order struct {
   Items int
}

// Хорошо: короткое имя получателя
func (o *Order) Validate() bool {
   return o.Items > 0
}

// Плохо: слишком длинное имя получателя
func (order *Order) Validate() bool {
   return order.Items > 0
}

// Плохо: слишком общее имя получателя
func (self *Order) Validate() bool {
   return self.Items > 0
}

Геттеры и сеттеры для структур

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

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

При этом принято добавлять префикс Set только к сеттерам, но не к геттерам. Например:

type Customer struct {
    address string
}

func (c *Customer) Address() string {
    return c.address
}

func (c *Customer) SetAddress(addr string)  {
    c.address = addr
}

Интерфейсы

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

type Speaker interface {
    Speak() string
}

type Authorizer interface {
    Authorize(ctx context.Context, action string) error
}

type Authenticator interface {
    Authenticate(ctx context.Context) (User, error)
}

В стандартной библиотеке Go есть много примеров, следующих этому правилу: io.Reader, io.Writer, fmt.Stringer (хотя встречаются и исключения, например http.Handler).

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

Когда можно отступить от соглашений

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

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

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

Разобраться с именованием в Go полезно, но это только один слой языка. Дальше обычно начинаются вопросы про работу с базой, миграции, указатели, память и типизацию — те вещи, которые быстро влияют на качество кода уже в небольшом приложении. Этим темам посвящены бесплатные уроки Otus в рамках курса «Golang Developer. Basic»: можно посмотреть формат обучения, познакомиться с экспертами и задать вопросы по материалу.

  • 14 мая в 20:00 — «Взаимодействие с базой данных и миграции на Go»
    Практический разбор работы с базами данных в Go: создание таблиц, миграции, запросы через ORM и чистый SQL. На занятии участники соберут базу данных под небольшой веб-сервер на Go. Записаться

  • 21 мая в 20:00 — «Перестаньте бояться указателей: как Go экономит вашу память и CPU»
    Разбор строгой типизации, переменных, указателей и устройства памяти: стек, куча, статика. Урок подойдет тем, кто изучает Go и хочет спокойнее разобраться с базовыми механизмами языка. Записаться

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