Инкапсуляция на уровне пакетов
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 48 из 107.
В то время как в предыдущем уроке рассматривалась видимость на уровне полей, инкапсуляция в Go на самом деле работает на уровне пакета. Все, что находится внутри одного и того же пакета — независимо от того, в каком файле оно расположено — может иметь доступ к неэкспортируемым полям и функциям.
Рассмотрим пакет с несколькими файлами:
// account/account.go
package account
type Account struct {
balance int // неэкспортируемое
}
func (a *Account) Deposit(amount int) {
a.balance += amount
}
// account/helper.go
package account
func ResetAccount(a *Account) {
a.balance = 0 // работает - тот же пакет
}
Оба файла относятся к пакету account, поэтому helper.go может напрямую обращаться к неэкспортируемому полю balance. Это сделано намеренно — это позволяет разделять логику пакета между файлами, сохраняя при этом внутренний доступ.
Однако код вне пакета не может получить доступ к неэкспортируемым членам:
// main.go
package main
import "account"
func main() {
acc := &account.Account{}
acc.Deposit(100) // работает — экспортируемый метод
// acc.balance = 50 // ошибка — неэкспортируемое поле
// account.ResetAccount // работало бы, если бы ResetAccount был экспортируемым
}
Эта граница на уровне пакета является основным механизмом инкапсуляции в Go. Вы проектируете пакеты как целостные единицы, в которых внутренний код свободно взаимодействует, в то время как внешний код взаимодействует только через экспортируемый API. Представьте каждый пакет как автономный модуль с четким публичным интерфейсом.
Задание
ЛегкоДавайте создадим систему управления запасами, которая демонстрирует, как несколько файлов внутри одного пакета могут взаимодействовать, обращаясь к неэкспортируемым полям напрямую, сохраняя при этом эти данные скрытыми от внешнего кода.
Вы создадите три файла, которые работают вместе в пакете main:
product.go: Определите структуруProductс экспортируемым полемNameи неэкспортируемым полемquantity(int). Добавьте экспортируемый методInfo() string, который возвращает строку в формате:[Name]: [quantity] in stockinventory.go: Создайте вспомогательные функции, которые работают с продуктами, напрямую обращаясь к неэкспортируемому полюquantity(это возможно, так как они находятся в одном пакете). Реализуйте:Restock(p *Product, amount int)— добавляет указанное количество к количеству товараSell(p *Product, amount int) bool— вычитает количество из запаса, если товара достаточно, возвращаетtrueв случае успеха илиfalse, если товара недостаточноNewProduct(name string, initialQty int) *Product— конструктор, который создает продукт с заданным именем и начальным количеством
main.go: Считайте данные о продукте из входных данных, создайте продукт с помощью конструктора, выполните операции с запасами и выведите результаты. После каждой операции выводите информацию о продукте, чтобы показать обновленное состояние.
Будут предоставлены следующие входные данные:
- Строка 1: Название продукта
- Строка 2: Начальное количество (целое число)
- Строка 3: Количество для пополнения (целое число)
- Строка 4: Количество для продажи (целое число)
После создания продукта выведите его начальную информацию. Затем пополните запас и снова выведите информацию. Наконец, попытайтесь продать товар и выведите либо обновленную информацию (в случае успеха), либо Sale failed: insufficient stock (если количества недостаточно).
Например, для входных данных Laptop, 10, 5 и 8 ваш вывод должен быть следующим:
Laptop: 10 in stock
Laptop: 15 in stock
Laptop: 7 in stockА для Phone, 3, 2 и 10 ваш вывод должен быть таким:
Phone: 3 in stock
Phone: 5 in stock
Sale failed: insufficient stockКлючевая идея здесь заключается в том, что inventory.go может напрямую читать и изменять неэкспортируемое поле quantity, потому что оно находится в том же пакете, что и product.go. Это инкапсуляция на уровне пакета в действии — внутреннее взаимодействие происходит свободно, в то время как данные остаются защищенными от внешних пакетов.
Шпаргалка
Инкапсуляция в Go работает на уровне пакета, а не на уровне файла. Все файлы внутри одного и того же пакета могут обращаться к неэкспортируемым полям и функциям, независимо от того, в каком файле они определены.
Доступ на уровне пакета
Неэкспортируемые члены доступны во всех файлах одного и того же пакета:
// account/account.go
package account
type Account struct {
balance int // неэкспортируемое
}
func (a *Account) Deposit(amount int) {
a.balance += amount
}
// account/helper.go
package account
func ResetAccount(a *Account) {
a.balance = 0 // работает — тот же пакет
}
Ограничения внешних пакетов
Код за пределами пакета не может получить доступ к неэкспортируемым членам:
// main.go
package main
import "account"
func main() {
acc := &account.Account{}
acc.Deposit(100) // работает — экспортируемый метод
// acc.balance = 50 // ошибка — неэкспортируемое поле
}
Такая архитектура позволяет разделять логику пакета между несколькими файлами, сохраняя при этом внутренний доступ, что позволяет создавать целостные модули с четкими публичными интерфейсами.
Попробуйте сами
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Прочитать название продукта
name, _ := reader.ReadString('\n')
name = strings.TrimSpace(name)
// Прочитать начальное количество
initialQtyStr, _ := reader.ReadString('\n')
initialQty, _ := strconv.Atoi(strings.TrimSpace(initialQtyStr))
// Прочитать количество для пополнения
restockStr, _ := reader.ReadString('\n')
restockAmount, _ := strconv.Atoi(strings.TrimSpace(restockStr))
// Прочитать количество для продажи
sellStr, _ := reader.ReadString('\n')
sellAmount, _ := strconv.Atoi(strings.TrimSpace(sellStr))
// TODO: Создать новый продукт, используя конструктор NewProduct
// TODO: Вывести начальную информацию о продукте
// TODO: Пополнить запас продукта и вывести информацию
// TODO: Попытаться продать и вывести результат
// Если продажа прошла успешно, вывести обновленную информацию
// Если продажа не удалась, вывести "Sale failed: insufficient stock"
fmt.Println() // Заменить на фактический вывод
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП в Go
Внешние файлыРабочее пространство и модули GoПакеты и импортыЭкспортируемые и неэкспортируемые именаВведение в ООП в GoСтруктуры как классыОпределение методов структурПолучатели-указатели и получатели-значенияИнициализация структурФункции-конструкторыИтоги — Простой калькулятор4Интерфейсы
Введение в интерфейсыНеявная реализацияИнтерфейс как контрактПустой интерфейс (any)Утверждение типаПереключатель типовКомпозиция интерфейсовИнтерфейсы Stringer и ErrorПовторение: Калькулятор фигур7Инкапсуляция
Экспортируемые и неэкспортируемые поляИнкапсуляция на уровне пакетовГеттеры и сеттерыСокрытие информации в GoИтоги — Записи о студентах10Обобщения (Generics) (Go 1.18+)
Введение в GenericsПараметры типовОграничения типовОбобщенные структурыОбходной путь для обобщенных методовИтоги — Обобщенная коллекция13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн Abstract FactoryПаттерн ObserverПаттерн Strategy2Глубокое погружение в типы и структуры
Базовые и составные типыОпределение пользовательских типовТеги структурАнонимные структурыВложенные структурыНулевые значения и значения по умолчаниюПовторение — Контактная книга5Композиция вместо наследования
Почему в Go нет наследованияОсновы встраивания структурПродвижение методовВстраивание нескольких структурВстраивание против агрегацииЗатенение встроенных методовИтоги — Иерархия сотрудников8Обработка ошибок и ООП
Интерфейс errorПользовательские типы ошибокОбертывание ошибок (fmt.Errorf)Sentinel-ошибкиerrors.Is() и errors.As()Panic, Defer и RecoverИтоги — Парсер файлов11Стандартная библиотека и ООП
io.Reader и io.Writersort.InterfaceИнтерфейс fmt.Stringerencoding/json со структурамиИнтерфейс http.HandlerПовторение: модели REST API14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикMiddleware как Декоратор3Указатели и память
Основы указателей в GoУказатели на структурыПередача по значению и по ссылкеФункция new()Сборка мусора в GoПовторение: Конструктор связного списка6Полиморфизм в Go
Полиморфизм через интерфейсыУтиная типизация в GoПравила реализации интерфейсовПолиморфные коллекцииВнедрение зависимостейИтоги — Обработчик платежей9Конкурентность и ООП
Основы горутинКаналы и взаимодействиеБуферизованные и небуферизованные каналыОператор selectsync.Mutex и sync.RWMutexsync.WaitGroupПроектирование потокобезопасных структурПовторение — Worker Pool