Encapsulation in Go
By Syncfusion®, Inc. · Last updated
Hide data in Go with package-level visibility: export rules, getter methods for read access, read-only patterns, and setter methods with validation.
What Is Encapsulation?
Encapsulation = keeping a type's data hidden, and letting other code change it only through the type's own methods.
- The data stays safe inside.
- Outside code uses methods like
Deposit()orSetMarks(). - The methods check every change before making it.
Simple lesson: encapsulation hides the data and lets in only safe changes.
A Real-World Example
Think of the school's exam marks register.
- Teachers cannot write in it directly.
- They fill a form and give it to the exam office.
- The office checks the form: marks between 0 and 100? Correct roll number?
- Only then is the register updated.
- Anyone can ask for a mark, but only through the office.
Now the mapping:
- The register = the hidden fields.
- The exam office = the type's methods.
- Checking the form = validation inside a setter.
- Asking for a mark = a getter.
Go has no private or public keywords.
It uses the first letter of a name. You met this in Packages and Modules:
- Capital = exported: other packages can use it.
- small = unexported: only its own package can use it.
What You Will Learn
In this lesson, you will learn:
- What encapsulation and data hiding are.
- How package-level visibility works in Go.
- The export rules: capitalized and lowercase names.
- How to give read access with getter methods.
- How to control changes with setter methods and validation.
Encapsulation Fundamentals
Data Hiding
Data hiding = making a field unreachable from outside, so nobody can set a wrong value.
Without hiding:
- Anyone can write
account.Balance = -5000. - Nothing stops a negative balance.
- The bug shows up much later, far from where it happened.
With hiding:
- The field is
balance(lowercase). - The only way in is
Deposit(amount), which refuses a negative amount.
Package-Level Visibility
Go hides things at the package level, not the type level.
- Code inside the same package can see lowercase names.
- Code in another package cannot.
- So to hide a field, put the type in its own package.
school/
├── go.mod
├── main.go package main
└── bank/
└── account.go package bank
// bank/account.go
package bank
import "errors"
type Account struct {
owner string // unexported: hidden from other packages
balance int // unexported
}
func New(owner string) *Account {
return &Account{owner: owner}
}
func (a *Account) Deposit(amount int) error {
if amount <= 0 {
return errors.New("deposit must be positive")
}
a.balance += amount
return nil
}
func (a *Account) Balance() int {
return a.balance
}
// main.go
package main
import (
"fmt"
"example.com/school/bank"
)
func main() {
acc := bank.New("Asha")
acc.Deposit(500)
fmt.Println(acc.Balance()) // 500
// acc.balance = -100 // error: acc.balance undefined (cannot refer to unexported field balance)
}
maincan callNew,DepositandBalance, because they are capitalized.maincannot touchbalance. Go refuses to build.
In this lab, each program is one file in package main.
Inside one package, Go lets every function see lowercase fields. So the runnable examples below show the pattern (methods guarding the data), but they cannot show Go blocking outside access. In a real project, put the type in its own package.
Simple lesson: to hide data, make fields lowercase and put the type in its own package.
Export Rules
Capitalized Names
Exported = starts with a capital letter. Visible to every package that imports this one.
- types:
Account,Student - functions:
New,Average - methods:
Deposit,Balance - fields:
Name,Marks - constants and variables:
MaxMarks
Lowercase Names
Unexported = starts with a small letter (or _). Visible only inside its own package.
- helper functions:
total,validate - hidden fields:
balance,owner - internal constants:
maxRetries
What each package can see
Mixed Structs
A struct can have both kinds of fields.
type Student struct {
Name string // exported: anyone can read and set it
marks int // unexported: only through methods
}
- Use exported fields for plain data with no rules.
- Use unexported fields for data that must stay valid.
Some fields open, some guarded
package main
import "fmt"
type Student struct {
Name string
marks int
}
func (s *Student) AddMarks(n int) {
if n > 0 {
s.marks += n
}
}
func (s Student) Marks() int {
return s.marks
}
func main() {
s := Student{Name: "Ravi"}
s.Name = "Ravi Kumar"
s.AddMarks(40)
s.AddMarks(-100)
fmt.Println(s.Name, s.Marks())
}
Ravi Kumar 40
Nameis set directly: it has no rules.markschanges only throughAddMarks, which ignores negative numbers.
Simple lesson: a capital first letter shares a name, and a small one keeps it inside the package.
Getter Methods
Read Access
Getter = a method that returns the value of a hidden field.
- In Go, a getter is named after the field, without "Get":
Balance(), notGetBalance(). - It usually has a value receiver, or a pointer receiver if the type uses pointers.
A getter
package main
import "fmt"
type ReportCard struct {
student string
marks int
}
func (r ReportCard) Student() string {
return r.student
}
func (r ReportCard) Marks() int {
return r.marks
}
func main() {
rc := ReportCard{student: "Meena", marks: 88}
fmt.Println(rc.Student(), rc.Marks())
}
Meena 88
Read-Only Access Patterns
Read-only = outside code can see a value but never change it.
- Give a getter, and no setter.
- Return copies of slices, so the caller cannot change your hidden slice.
Return a copy, keep the original safe
package main
import "fmt"
type Class struct {
names []string
}
func (c *Class) Add(name string) {
c.names = append(c.names, name)
}
func (c *Class) Names() []string {
out := make([]string, len(c.names))
copy(out, c.names)
return out
}
func main() {
c := &Class{}
c.Add("Asha")
c.Add("Ravi")
list := c.Names()
list[0] = "HACKED"
fmt.Println(c.Names())
}
[Asha Ravi]
Names()gives a copy. Changinglistdoes not change the class.- Returning
c.namesdirectly would let callers change it. See Slices.
Simple lesson: a getter shares a value, and returning a copy keeps it read-only.
Setter Methods
Controlled Updates
Setter = a method that changes a hidden field, after checking the new value.
- Name it
SetX:SetMarks,SetPhone. - It needs a pointer receiver, because it changes the struct. See Structs and Methods.
- It usually returns an
errorwhen the value is wrong.
Validation Logic
Validation = checking that a value follows the rules before using it.
Press Play to watch a setter protect the marks.
A setter that checks every change
A bank account with rules
package main
import (
"errors"
"fmt"
)
type Account struct {
owner string
balance int
}
func NewAccount(owner string) *Account {
return &Account{owner: owner}
}
func (a *Account) Deposit(n int) error {
if n <= 0 {
return errors.New("deposit must be positive")
}
a.balance += n
return nil
}
func (a *Account) Withdraw(n int) error {
if n > a.balance {
return errors.New("not enough balance")
}
a.balance -= n
return nil
}
func (a *Account) Balance() int {
return a.balance
}
func main() {
acc := NewAccount("Kabir")
fmt.Println(acc.Deposit(500))
fmt.Println(acc.Withdraw(800))
fmt.Println(acc.Withdraw(200))
fmt.Println("Balance:", acc.Balance())
}
<nil>
not enough balance
<nil>
Balance: 300
NewAccountis the only way to make an account with an owner.DepositandWithdrawrefuse bad changes.Balancelets anyone read, but not write.
A second example: a phone number must have exactly 10 digits.
Validate a phone number
package main
import (
"errors"
"fmt"
)
type Contact struct {
phone string
}
func (c *Contact) SetPhone(p string) error {
if len(p) != 10 {
return errors.New("phone must have 10 digits")
}
for _, r := range p {
if r < '0' || r > '9' {
return errors.New("phone must have only digits")
}
}
c.phone = p
return nil
}
func (c Contact) Phone() string {
return c.phone
}
func main() {
var c Contact
fmt.Println(c.SetPhone("98450"))
fmt.Println(c.SetPhone("98450abcde"))
fmt.Println(c.SetPhone("9845012345"), c.Phone())
}
phone must have 10 digits
phone must have only digits
<nil> 9845012345
- Two rules, checked in order. The first broken rule is reported.
- Only a valid number is stored.
Easy way to remember
- Hide the field: lowercase.
- Let people read: a getter named after the field.
- Let people change it safely: a
SetXmethod that checks and returns an error. - Keep the type in its own package.
Simple lesson: a setter checks a value and refuses it with an error, so the hidden data always stays valid.
TL;DR
- Encapsulation = hide data, change it only through methods.
- Data hiding = lowercase fields. Go hides them from other packages.
- Exported = Capital first letter. Unexported = small first letter.
Getter =
Balance(), named after the field, no "Get".- Read-only = a getter with no setter. Return copies of slices.
Setter =
SetMarks(m) error, pointer receiver, checks first.- Validation = refuse bad values with an error.
- Put the type in its own package so the hiding really works.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
Using private or public | Go has these keywords | Go uses the first letter | A syntax error. |
Lowercase fields in package main and expecting them hidden | Hiding is per type | Hiding is per package | Code in the same package can still change them. |
GetBalance() | Getters start with Get | Go style is Balance() | Works, but looks out of place. |
| A setter with a value receiver | Any receiver changes the struct | Setters need *T | The change is lost. |
| A setter that never checks | Setters only set | Setters guard the rules | Bad data gets in. |
| Returning a hidden slice directly | Callers only read it | They can change its elements | Hidden data changes from outside. |
| Lowercase type name with an exported constructor | Fine everywhere | Callers cannot even name the type | Awkward to use. |
Check Yourself
Exercises
What to Remember
- Encapsulation hides data and lets it change only through methods.
- Go uses the first letter: Capital is exported, small is unexported.
- Hiding works between packages, so put the type in its own package.
- Getters are named after the field:
Balance(). - Setters use a pointer receiver, check the value, and return an error.
- Return copies of hidden slices to keep them read-only.
Next: Advanced Type Design. You will learn custom types, type aliases, method sets and interface composition.