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