Menu
Coddy logo textTech

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 icon

Challenge

Easy

Let'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 a Product struct with an exported Name field and an unexported quantity field (int). Add an exported method Info() string that returns the format: [Name]: [quantity] in stock
  • inventory.go: Create helper functions that work with products by directly accessing the unexported quantity field (possible because they're in the same package). Implement:
    • Restock(p *Product, amount int) - adds the amount to the product's quantity
    • Sell(p *Product, amount int) bool - subtracts the amount from quantity if sufficient stock exists, returns true if successful or false if not enough stock
    • NewProduct(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 stock

And given Phone, 3, 2, and 10, your output should be:

Phone: 3 in stock
Phone: 5 in stock
Sale failed: insufficient stock

The 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
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming