Package-Level Encapsulation
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 48 of 107.
While the previous lesson covered field-level visibility, Go's encapsulation actually operates at the package level. Everything within the same package—regardless of which file it's in—can access unexported fields and functions.
Consider a package with multiple files:
// account/account.go
package account
type Account struct {
balance int // unexported
}
func (a *Account) Deposit(amount int) {
a.balance += amount
}
// account/helper.go
package account
func ResetAccount(a *Account) {
a.balance = 0 // works - same package
}
Both files belong to the account package, so helper.go can directly access the unexported balance field. This is intentional—it allows you to split package logic across files while maintaining internal access.
However, code outside the package cannot access unexported members:
// main.go
package main
import "account"
func main() {
acc := &account.Account{}
acc.Deposit(100) // works - exported method
// acc.balance = 50 // error - unexported field
// account.ResetAccount // works if ResetAccount were exported
}
This package-level boundary is Go's primary encapsulation mechanism. You design packages as cohesive units where internal code collaborates freely, while external code interacts only through the exported API. Think of each package as a self-contained module with a clear public interface.
Challenge
EasyLet's build an inventory management system that demonstrates how multiple files within the same package can collaborate by accessing unexported fields directly, while keeping that data hidden from external code.
You'll create three files that work together in the main package:
product.go: Define aProductstruct with an exportedNamefield and an unexportedquantityfield (int). Add an exported methodInfo() stringthat returns the format:[Name]: [quantity] in stockinventory.go: Create helper functions that work with products by directly accessing the unexportedquantityfield (possible because they're in the same package). Implement:Restock(p *Product, amount int)- adds the amount to the product's quantitySell(p *Product, amount int) bool- subtracts the amount from quantity if sufficient stock exists, returnstrueif successful orfalseif not enough stockNewProduct(name string, initialQty int) *Product- constructor that creates a product with the given name and initial quantity
main.go: Read product details from input, create a product using the constructor, perform inventory operations, and display results. After each operation, print the product info to show the updated state.
The following inputs will be provided:
- Line 1: Product name
- Line 2: Initial quantity (integer)
- Line 3: Restock amount (integer)
- Line 4: Sell amount (integer)
After creating the product, print its initial info. Then restock and print the info again. Finally, attempt to sell and print either the updated info (if successful) or Sale failed: insufficient stock (if not enough quantity).
For example, given Laptop, 10, 5, and 8, your output should be:
Laptop: 10 in stock
Laptop: 15 in stock
Laptop: 7 in stockAnd given Phone, 3, 2, and 10, your output should be:
Phone: 3 in stock
Phone: 5 in stock
Sale failed: insufficient stockThe key insight here is that inventory.go can directly read and modify the unexported quantity field because it's in the same package as product.go. This is package-level encapsulation in action—internal collaboration happens freely while the data remains protected from outside packages.
Cheat sheet
Go's encapsulation operates at the package level, not the file level. All files within the same package can access unexported fields and functions, regardless of which file they're defined in.
Package-Level Access
Unexported members are accessible across all files in the same package:
// account/account.go
package account
type Account struct {
balance int // unexported
}
func (a *Account) Deposit(amount int) {
a.balance += amount
}
// account/helper.go
package account
func ResetAccount(a *Account) {
a.balance = 0 // works - same package
}
External Package Restrictions
Code outside the package cannot access unexported members:
// main.go
package main
import "account"
func main() {
acc := &account.Account{}
acc.Deposit(100) // works - exported method
// acc.balance = 50 // error - unexported field
}
This design allows you to split package logic across multiple files while maintaining internal access, creating cohesive units with clear public interfaces.
Try it yourself
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Read product name
name, _ := reader.ReadString('\n')
name = strings.TrimSpace(name)
// Read initial quantity
initialQtyStr, _ := reader.ReadString('\n')
initialQty, _ := strconv.Atoi(strings.TrimSpace(initialQtyStr))
// Read restock amount
restockStr, _ := reader.ReadString('\n')
restockAmount, _ := strconv.Atoi(strings.TrimSpace(restockStr))
// Read sell amount
sellStr, _ := reader.ReadString('\n')
sellAmount, _ := strconv.Atoi(strings.TrimSpace(sellStr))
// TODO: Create a new product using NewProduct constructor
// TODO: Print initial product info
// TODO: Restock the product and print info
// TODO: Attempt to sell and print result
// If sale succeeds, print the updated info
// If sale fails, print "Sale failed: insufficient stock"
fmt.Println() // Replace with actual output
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool