JavaRush /Курсы /Go SELF /Пакет как namespace — правила имён

Пакет как namespace — правила имён

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

1. Пакеты и пространство имён

Когда вы пишете первые программы, один файл main.go выглядит нормально. Но проходит пара дней, появляется десяток функций, пара констант, несколько переменных, и внезапно файл начинает напоминать шкаф, куда вы «на минутку» сложили вещи, а потом «на минутку» сложили ещё, и вот вы уже живёте внутри шкафа.

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

Пакет как namespace: «имя перед точкой» — это не магия

Если вы уже писали fmt.Println("hello"), то вы видели самый важный симптом существования пакетов: точка.

fmt.Println читается так: «в пакете fmt возьми экспортированное имя Println». То есть пакет — это буквально пространство имён, которое защищает вас от ситуации, когда в программе есть 15 функций с названием Print() и вы не понимаете, какая из них «та самая».

Можно представить это так:

  • пакет — это фамилия,
  • функция/тип/переменная — это имя,
  • а fmt.Println — это «Петров Иван», только для кода.

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

Строка package ...: как файл относится к пакету

Любой .go-файл (кроме особых случаев, которые мы сегодня не трогаем) начинается с package-строки — package <имя>.

Важно уловить мысль: файл сам по себе не является «единицей программы». Единица — это пакет, а файл — просто кусочек пакета.

Вот минимальный пример:

package main

import "fmt"

func main() {
	fmt.Println("hi") // hi
}

Строка package main означает: «этот файл относится к пакету main».

Теперь ключевой практический факт, который экономит часы жизни: все файлы с одинаковым package в одной папке считаются одним пакетом. А значит — они делят общую область видимости на уровне пакета.

Package-level scope: видимость внутри пакета

Сейчас будет важный момент: внутри одного пакета все файлы видят объявления друг друга. Не «если импортировать», не «если подключить», а просто видят, потому что они — один пакет.

Давайте продолжим нашу учебную мини-программу. Пусть это будет простейший «баланс дня»: мы читаем доход и расход, считаем разницу и печатаем отчёт. Раньше вы бы написали всё в main.go. Сегодня мы начнём раскладывать код по файлам, но пока оставим один пакет: main.

Файл main.go

В этом файле у нас будет «сценарий»: прочитать, посчитать, вывести.

package main

import "fmt"

func main() {
	income, expense := readIncomeExpense()
	balance := income - expense

	fmt.Println(formatReport(income, expense, balance))
}

Обратите внимание: readIncomeExpense и formatReport тут не объявлены. И это нормально — они могут жить в других файлах этого же пакета.

Файл io.go

Теперь делаем чтение ввода отдельной функцией, в отдельном файле, но в том же пакете:

package main

import "fmt"

func readIncomeExpense() (int, int) {
	var income int
	var expense int

	fmt.Scan(&income, &expense)
	return income, expense
}

Файл report.go

И форматирование отчёта тоже вынесем отдельно:

package main

import "fmt"

func formatReport(income, expense, balance int) string {
	return fmt.Sprintf("income=%d expense=%d balance=%d", income, expense, balance)
}

Фокус в том, что все три файла — части одного пакета main, поэтому они спокойно вызывают функции друг друга. Это и есть package-level scope: область видимости на уровне пакета.

Чтобы это уложилось в голове, вот схема:

flowchart TD
    A[Папка проекта] --> B[main.go package main]
    A --> C[io.go package main]
    A --> D[report.go package main]
    B --> E[Один пакет: main]
    C --> E
    D --> E
    E --> F[Общие имена на уровне пакета: readIncomeExpense, formatReport, ...]

Если вы поймали себя на мысли «то есть файл — не граница?», то да: граница — пакет, а не файл. И это очень по-гошному: меньше сущностей — меньше путаницы.

2. package main и точка входа

Обычно пакет — это просто контейнер кода. Но main — это особенный случай: программа стартует из пакета main, а точкой входа служит функция main().

Можно сказать так: если пакет — это «коробка с деталями», то package main — это коробка, из которой собирается именно исполняемая программа, а не библиотека.

Ещё один полезный факт: когда Go анализирует программу, он рассматривает точки входа для каждого main-пакета — среди них есть main-функция и также есть механизмы инициализации пакета.

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

3. Экспорт: публичное и внутреннее API

Экспортируемые и неэкспортируемые имена: правило первой буквы

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

В Go есть очень простое правило:

  • имя начинается с заглавной буквы → оно экспортировано (exported) и доступно из других пакетов;
  • имя начинается со строчной буквы → оно неэкспортировано (unexported) и доступно только внутри своего пакета.

И это касается функций, переменных, констант, типов (а позже — ещё методов и полей структур).

Пример на стандартной библиотеке: fmt.Println vs «а что если fmt.println

Давайте прямо «пощупаем» это правило:

package main

import "fmt"

func main() {
	fmt.Println("ok") // ok
	// fmt.println("no") // ошибка компиляции: нет такого экспортированного имени
}

Почему Println с большой буквы существует, а println с маленькой — нет? Потому что пакет fmt экспортирует Println, то есть делает его частью публичного API.

Табличка, чтобы мозг не делал вид, что он «и так понял»

Имя Пример Доступно из другого пакета? Почему
Exported
fmt.Println, strconv.Atoi
Да Первая буква заглавная
Unexported
fmt.someHelper, internalCalc
Нет Первая буква строчная

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

Экспорт: внутри пакета и снаружи

Сейчас легко запутаться: «если у меня formatReport с маленькой буквы, значит main.go не сможет её вызвать?». Сможет, потому что main.go и report.go в одном пакете.

Экспорт/неэкспорт влияет только на доступ между пакетами. Внутри одного пакета доступны и те, и другие имена.

Давайте улучшим наш код и сделаем так, чтобы formatReport была внутренней деталью, а наружу (в перспективе) торчала более «человеческая» функция. Пока мы в одном пакете, разницы в доступе не будет, но смысл — появится.

Файл report.go (версия с внутренней функцией)

package main

import "fmt"

func formatReport(income, expense, balance int) string {
	return fmt.Sprintf("income=%d expense=%d balance=%d", income, expense, balance)
}

Это неэкспортированная функция — «внутренний помощник».

Файл main.go (использует внутреннюю функцию)

package main

import "fmt"

func main() {
	income, expense := readIncomeExpense()
	balance := income - expense

	fmt.Println(formatReport(income, expense, balance))
}

Работает, потому что пакет один.

А вот если бы мы пытались вызвать formatReport из другого пакета, она была бы недоступна. Именно в этом смысл «маленькой буквы»: «не трогай, это внутренняя кухня».

Экспорт — это контракт, а не «секретность»

Очень хочется думать, что «неэкспортированное» — это что-то про безопасность. На самом деле нет: это прежде всего про контракт и обещания.

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

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

Чтобы почувствовать идею, представьте, что вы делаете пакет «калькулятор баланса» (в будущем мы научимся оформлять такие вещи отдельными пакетами). В публичный контракт хорошо попадут вещи вроде ComputeBalance(...), а вот parseIncomeFromWeirdLegacyFormat(...) лучше оставить внутренним: он нужен, но он не должен быть «обязательством» для внешнего кода.

4. Правила имён и небольшой рефакторинг

Правила имён: как называть все

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

Начнём с самых частых граблей новичка: называть переменные именами пакетов. Это особенно обидно, потому что ошибка выглядит как «вчера работало, сегодня нет», а причина — всего одна строчка.

Пример: как случайно «сломать fmt»

package main

import "fmt"

func main() {
	fmt.Println("before") // before

	fmt := "oops"
	_ = fmt

	// fmt.Println("after") // ошибка: fmt теперь переменная, а не пакет
}

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

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

Мини-таблица «более читаемо» vs «менее читаемо»

Намерение Лучше Хуже Почему
Публичная функция
PrintReport
PRINT_REPORT
В Go не принято кричать капсом
Внутренний помощник
formatReport
Format_report
_ редко используют в именах
Локальная переменная
income
i
i норм в цикле, но не для смысла

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

Микро‑практика: прячем детали и делаем API

Чтобы закрепить «экспорт = контракт», давайте сделаем маленький рефакторинг в нашем мини-приложении. Мы добавим функцию, которая будет «главной» для отчёта, а внутренние детали спрячем за маленькими буквами.

Файл report.go (публичная + внутренняя)

package main

import "fmt"

func PrintBalanceReport(income, expense int) string {
	balance := income - expense
	return formatReport(income, expense, balance)
}

func formatReport(income, expense, balance int) string {
	return fmt.Sprintf("income=%d expense=%d balance=%d", income, expense, balance)
}

Сейчас мы всё ещё в пакете main, так что это больше упражнение «на мысль». Но оно важное: вы начинаете строить API даже внутри одного пакета, чтобы потом было проще выделять код в отдельные пакеты (когда придёт время).

Файл main.go (используем публичную функцию)

package main

import "fmt"

func main() {
	income, expense := readIncomeExpense()
	fmt.Println(PrintBalanceReport(income, expense)) // income=... expense=... balance=...
}

С точки зрения кода чтение стало приятнее: main теперь меньше знает о деталях отчёта.

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

Ошибка №1: думать, что «экспорт» влияет на доступ между файлами одного пакета.
Это частая путаница после языков, где файл — важная граница. В Go граница — пакет. Если два файла объявлены как package main и лежат в одной папке, они в одном пакете и видят друг друга на уровне package-level объявлений. Экспорт управляет доступом именно между пакетами, а не между файлами.

Ошибка №2: пытаться вызвать fmt.println или «додумать» имя в пакете.
Новичок иногда рассуждает так: «Ну Println же печатает, значит println тоже должен печатать». Не должен. В Go доступ к именам из другого пакета определяется экспортом: если имя не экспортировано, вы его не увидите. Правильная реакция — открыть документацию пакета или подсказки IDE, а не угадывать «как оно там названо».

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

Ошибка №4: «сломать пакет» переменной с тем же именем (например, fmt := ...).
Это выглядит как мелочь, но ломает чтение и вызывает очень раздражающие ошибки компиляции. Если вы импортировали пакет, не используйте его имя для переменных. Если рука тянется написать fmt := ..., лучше остановиться и выбрать имя по смыслу: msg, line, text, report.

Ошибка №5: путать смысл package main и обычных пакетов.
Иногда возникает желание писать «везде package main, чтобы работало». Оно, конечно, будет работать… какое-то время. Но main — это пакет приложения, точка входа, место для «склейки». В реальном коде вы почти всегда хотите, чтобы большая часть логики жила не в main, а в других пакетах, а main только собирал всё вместе. Пока мы учимся на небольших примерах, main нормален, но важно не сделать из него «свалку».

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