JavaRush /Курсы /Go SELF /Нейминг в Go: экспорт, аббревиатуры и receiver’ы

Нейминг в Go: экспорт, аббревиатуры и receiver’ы

Go SELF
37 уровень , 0 лекция
Открыта

1. Нейминг как интерфейс к коду

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

Давайте договоримся о практическом критерии хорошего имени.

Хорошее имя — это такое, после которого хочется меньше задавать вопросов.
Плохое имя — такое, после которого приходится лезть в реализацию, чтобы понять, что оно вообще делает.

Чтобы было проще держать это в голове, вот небольшая табличка «что читает мозг» (и почему он устает):

Фрагмент кода Что происходит в голове
todo.NewTask(1, "Buy milk")
«Ок, создаём задачу»
todo.New(1, "Buy milk")
«New чего? чего именно? а что за пакет?»
todo.CreateTask(1, "Buy milk")
«Ок, но почему Create, а не New? есть ли разница?»
todo.Do(1, true, false)
«…что делают эти два
bool
и почему я должен это помнить?»

Мы сегодня разберём три вещи, которые сильнее всего влияют на читаемость Go-кода: граница экспорт/неэкспорт, стиль аббревиатур и имена receiver’ов в методах.

2. Экспорт и неэкспорт: публичный контракт пакета

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

Именно поэтому Go так серьёзно относится к совместимости API: поменять тип или имя публичной сущности означает потенциально сломать пользователей пакета. Хороший пример такого «контракта» — обсуждение того, почему нельзя просто так менять тип os.Stdout: внешний код может зависеть от конкретного типа.

Простая модель: что можно менять без боли

Представьте пакет как маленький магазин. Экспортируемое — это витрина и касса. Неэкспортируемое — склад и проводка. Вы можете переставить коробки на складе как угодно, но если вы перенесёте кассу в туалет, покупатели (ваши пользователи) перестанут к вам ходить.

В нашем учебном приложении (условно назовём его todo) мы хотим прийти к простой модели: снаружи пакет предоставляет понятные типы и функции, а внутри прячет детали.

Посмотрим на мини-скелет пакета todo:

package todo

type Task struct {
	ID    int    // экспортируемое: видно снаружи
	Title string // экспортируемое: видно снаружи
	done  bool   // неэкспортируемое: внутренняя деталь
}

Эта структура уже задаёт вопрос: действительно ли поле done должно быть скрыто? Иногда да, иногда нет. Главное — понимать цену решения. Если вы экспортируете Done bool, то любой внешний код сможет поставить task.Done = true в обход ваших правил (например, не записав дату выполнения, не обновив счётчики, не вызвав нужную валидацию).

Поэтому в Go часто делают так: поля, которые нельзя менять напрямую, прячут, а доступ дают через методы.

Вот более «контрактная» версия:

package todo

type Task struct {
	id    int
	title string
	done  bool
}

func (t Task) ID() int       { return t.id }
func (t Task) Title() string { return t.title }
func (t Task) Done() bool    { return t.done }

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

Экспорт влияет на сериализацию

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

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

3. Что экспортировать, а что прятать

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

В Go рабочее правило такое: экспортируем минимально необходимое, и только то, что действительно является контрактом пакета.

Представим, что пакет todo должен уметь: создавать задачу и отмечать её выполненной. Тогда наружу можно дать конструктор и метод:

package todo

import (
	"errors"
	"strings"
)

type Task struct {
	id    int
	title string
	done  bool
}

func NewTask(id int, title string) (Task, error) {
	title = strings.TrimSpace(title)
	if id <= 0 {
		return Task{}, errors.New("id must be positive")
	}
	if title == "" {
		return Task{}, errors.New("title is empty")
	}
	return Task{id: id, title: title}, nil
}

func (t *Task) MarkDone() { t.done = true }

Обратите внимание на названия.

NewTask сразу читается как «создать корректную задачу».
MarkDone читается как «отметить выполненной».
И, что важно, мы не даём наружу прямой доступ к полям, чтобы внешний код не ломал инварианты.

Теперь, если мы решим, что done — это не просто bool, а ещё время выполнения, мы сможем поменять внутренности без переписывания чужого кода. А если бы мы экспортировали Done bool, то внешний код мог бы напрямую писать t.Done = true, и вы бы потом ловили баги из серии «выполнено, но время не выставлено».

4. Аббревиатуры: ID, URL, HTTP и другие

Аббревиатуры — это вещь, которая кажется мелкой ровно до тех пор, пока вы не увидите в одном проекте одновременно userId, UserID, userID, UserId и USERID. Дальше начинается весёлый квест: автодополнение в IDE показывает пять вариантов, тесты падают из-за сериализации, а вы думаете: «может, я просто уйду в лес и буду там компилировать палки?».

В Go принято писать распространённые аббревиатуры единым регистром внутри CamelCase. Это не занудство, а способ сделать код предсказуемым при чтении.

Вот мини-таблица, которая обычно покрывает 80% реальных случаев:

Плохо Лучше Почему
UserId
UserID
ID
читается как цельная аббревиатура
UrlConfig
URLConfig
URL
— стандартная аббревиатура
HttpServer
HTTPServer
HTTP
читается как протокол, а не как имя «Хттп»

Теперь применим это к нашему todo-домену. Часто у сущностей есть идентификатор. Если вы заведёте пользовательский тип, пишите так:

package todo

type TaskID int

А затем используйте его в структуре (если вам нужен более строгий тип, чем просто int):

package todo

type Task struct {
	id    TaskID
	title string
	done  bool
}

Имена вроде TaskId выглядят безобидно, но они делают код менее «сканируемым»: глаз цепляется, потому что ожидание в Go-экосистеме другое.

Аббревиатуры в локальных переменных и в API

С аббревиатурами есть тонкая грань: в локальных переменных можно быть чуть менее формальным (но не превращать код в ребус), а в публичных именах лучше быть строгим, потому что это контракт.

Сравните:

package main

import "fmt"

func main() {
	userID := 10
	fmt.Println(userID) // 10
}

Здесь userID нормален: это локальная переменная, контекст рядом, читатель не страдает.

Но вот так в публичном поле/методе лучше не делать:

package todo

type Task struct {
	TaskId int // спорно: в Go обычно ожидают TaskID
}

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

5. Receiver’ы: коротко и однозначно

Receiver — это параметр метода, только с особым синтаксисом. И в Go его имя видно постоянно: в каждом методе, в каждой строке, где вы пишете t.title или s.store. Поэтому receiver должен быть коротким и стабильным, иначе он превращается в визуальный шум.

Есть две крайности.

Первая — называть receiver this или self, как в других языках. В Go это выглядит инородно и обычно просто мешает: receiver — не магическое слово, а обычная переменная.

Вторая — давать receiver слишком «умное» имя, например taskService или currentTodoTask. Тогда каждая строка становится длиннее и тяжелее читается.

Правило: коротко, однозначно, стабильно

Возьмём нашу Task. Логичный receiver — t:

package todo

func (t Task) Done() bool { return t.done }

func (t *Task) MarkDone() { t.done = true }

Тут нет «философии»: t = task. Коротко, понятно, не мешает.

Для Service часто используют s:

package todo

type Service struct{}

func (s Service) Name() string { return "todo-service" }

А вот пример, который отлично показывает идею receiver как «обычного параметра», причём receiver может быть даже функцией: в одном из классических материалов по Go обработчик имеет receiver fn, потому что это функция, и код читается естественно: «вызвать fn».

Receiver не должен доминировать над смыслом

Сравним два варианта. Первый формально понятный, но тяжёлый:

package todo

type Task struct{ done bool }

func (task *Task) MarkDone() { task.done = true }

Вроде бы нормально… но если у вас десять методов, слово task будет повторяться везде и забивать экран.

Второй вариант короче и обычно читается лучше:

package todo

type Task struct{ done bool }

func (t *Task) MarkDone() { t.done = true }

При этом мы не теряем смысла: тип Task уже написан в сигнатуре метода, читатель и так знает, что t — это task.

Когда первая буква не спасает

Иногда есть соблазн назвать receiver по первой букве типа. Это часто работает, но бывают случаи, где первая буква не уникальна или выглядит странно.

Представьте типы Task и Tag. Receiver t подойдёт обоим — и вот тут начинается путаница в файле, где встречаются оба типа. Решение не универсальное, но практичное: либо держать эти типы в разных файлах/контекстах, либо выбрать разные receiver’ы, например task и tag (локально в одном месте это допустимо).

Важно не правило, а эффект: при чтении метода должно быть ясно, к чему относится receiver, и он не должен визуально доминировать над сутью.

6. Микро‑рефакторинг: улучшаем имена в todo

Сейчас сделаем упражнение “до/после” прямо на коде, но без ощущения «сейчас будет лекция по морали». Представим, что у нас был сырой код, написанный в спешке:

package todo

type task struct {
	id    int
	name  string
	isOk  bool
}

func New(id int, name string) task {
	return task{id: id, name: name}
}

func (this *task) Do() {
	this.isOk = true
}

Технически это компилируется. Но по неймингу тут целый набор проблем.

Во-первых, тип task не экспортируется, а New экспортируется — странно: мы даём наружу функцию, которая возвращает внутренний тип (это почти всегда плохой сигнал).
Во-вторых, name в домене задач чаще называют title или text, а isOk вообще не говорит, что произошло. «Ок» чего?
В-третьих, Do() не раскрывает смысл: сделать что?
В-четвёртых, receiver this стилистически не по-Go.

Приведём это к более читаемому контракту:

package todo

import "errors"

type Task struct {
	id    int
	title string
	done  bool
}

func NewTask(id int, title string) (Task, error) {
	if id <= 0 {
		return Task{}, errors.New("id must be positive")
	}
	return Task{id: id, title: title}, nil
}

func (t *Task) MarkDone() { t.done = true }

И даже не читая реализацию, по именам уже видно: у нас есть Task, у неё есть done, мы можем создать её через NewTask и отметить выполненной через MarkDone. Это и есть цель нейминга: чтобы смысл был виден раньше деталей.

Памятка‑схема: как имя проходит фильтр

Чтобы упростить принятие решений, полезно иметь в голове «ленивый алгоритм» проверки имени. Он не идеален, но спасает от многих случайных названий в духе tmp2 и dataX.

flowchart TD
    A["Придумал имя"] --> B{"Имя видно снаружи пакета?"}
    B -- "Да" --> C["Сделай точнее: это контракт Учитывай initialisms (ID/URL)"]
    B -- "Нет" --> D["Можно короче, но не в ребус Контекст рядом?"]
    C --> E{"Имя читается как фраза в коде?"}
    D --> E
    E -- "Да" --> F["Оставляем"]
    E -- "Нет" --> G["Переименовать: цель яснее, лишние слова убрать"]

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

7. Типичные ошибки

Ошибка №1: экспортировать “на всякий случай”.
Это очень распространённая привычка: сделать Task, TaskManager, TaskProcessor, TaskHelper — всё с большой буквы, чтобы “было доступно”. Проблема в том, что так вы превращаете внутренности в контракт. Потом вы захотите поменять структуру данных, а внешние зависимости не дадут. Лучше начинать с минимального экспорта и расширять API только когда есть реальный пользователь этого API (даже если этот пользователь — другой пакет внутри вашего проекта).

Ошибка №2: смешивать стили аббревиатур в одном проекте.
Когда в одном месте UserID, в другом UserId, а в третьем user_id, вы создаёте когнитивный шум: глазу тяжело сканировать код, IDE сложнее подсказывает, а в форматах данных могут возникать сюрпризы. В Go-стиле обычно выбирают ID, URL, HTTP и придерживаются этого везде, особенно в публичных именах.

Ошибка №3: делать receiver’ы “говорящими” ценой многословия.
Receiver currentTask, taskInstance, serviceObject иногда кажется “более понятным”, но на практике он засоряет каждую строчку. В Go принято, чтобы тип “нёс” смысл, а receiver не мешал. Поэтому t, s, c часто выигрывают, пока не возникает реальная неоднозначность.

Ошибка №4: называть методы глаголами без смысла (Do, Process, Handle) там, где нужна конкретика.
Метод Do() почти всегда плохой сигнал, потому что он ничего не объясняет. В домене задач гораздо яснее MarkDone, Rename, Delete, ToggleDone. Глагол в имени — это хорошо, но только если он описывает действие, а не заменяет мысль.

Ошибка №5: делать имена “техническими”, а не доменными.
Например, str1, arr2, flagX, data. Такие имена описывают форму, но не назначение. Лучше, когда имя несёт роль: title, taskID, done, createdAt (если это действительно нужно). Технические имена допустимы на очень короткой дистанции (две строки кода), но чем длиннее живёт переменная/тип, тем важнее смысловое имя.

1
Задача
Go SELF, 37 уровень, 0 лекция
Недоступна
Журнал переходов
Журнал переходов
1
Задача
Go SELF, 37 уровень, 0 лекция
Недоступна
Пакет задач
Пакет задач
1
Задача
Go SELF, 37 уровень, 0 лекция
Недоступна
Теги задачи
Теги задачи
1
Задача
Go SELF, 37 уровень, 0 лекция
Недоступна
Читаемый контракт
Читаемый контракт
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ