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() or SetMarks().
  • 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:

  1. What encapsulation and data hiding are.
  2. How package-level visibility works in Go.
  3. The export rules: capitalized and lowercase names.
  4. How to give read access with getter methods.
  5. 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)
}
  • main can call New, Deposit and Balance, because they are capitalized.
  • main cannot touch balance. 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
  • Name is set directly: it has no rules.
  • marks changes only through AddMarks, 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(), not GetBalance().
  • 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. Changing list does not change the class.
  • Returning c.names directly 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 error when 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
  • NewAccount is the only way to make an account with an owner.
  • Deposit and Withdraw refuse bad changes.
  • Balance lets 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 SetX method 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

  1. Encapsulation = hide data, change it only through methods.
  2. Data hiding = lowercase fields. Go hides them from other packages.
  3. Exported = Capital first letter. Unexported = small first letter.
  4. Getter = Balance(), named after the field, no "Get".

    • Read-only = a getter with no setter. Return copies of slices.
  5. Setter = SetMarks(m) error, pointer receiver, checks first.

    • Validation = refuse bad values with an error.
  6. Put the type in its own package so the hiding really works.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Using private or publicGo has these keywordsGo uses the first letterA syntax error.
Lowercase fields in package main and expecting them hiddenHiding is per typeHiding is per packageCode in the same package can still change them.
GetBalance()Getters start with GetGo style is Balance()Works, but looks out of place.
A setter with a value receiverAny receiver changes the structSetters need *TThe change is lost.
A setter that never checksSetters only setSetters guard the rulesBad data gets in.
Returning a hidden slice directlyCallers only read itThey can change its elementsHidden data changes from outside.
Lowercase type name with an exported constructorFine everywhereCallers cannot even name the typeAwkward 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.