Lesson 1
Encapsulation in Go: Structs and Controlled Access
Understanding Encapsulation in Go

Go, although not a traditional Object-Oriented Programming (OOP) language, provides encapsulation through the use of structs and package-level visibility. Encapsulation in Go is about controlling access to data and methods within packages, enabling you to create robust and maintainable applications.

To illustrate, consider a Go struct representing a bank account. Without encapsulation, the account balance could be directly altered. With encapsulation, however, the balance can only change through specific methods, like depositing or withdrawing.

Go
1package main 2 3import ( 4 "fmt" 5) 6 7// BankAccount struct 8type BankAccount struct { 9 balance float64 // not using encapsulation 10} 11 12// Withdraw method to withdraw money 13func (acc *BankAccount) Withdraw(amount float64) { 14 acc.balance -= amount 15} 16 17// Deposit method to deposit money 18func (acc *BankAccount) Deposit(amount float64) { 19 acc.balance += amount 20} 21 22func main() { 23 account := &bank.BankAccount{} 24 account.balance += 1000 // directly accessing the balance 25}
Encapsulation: Managing Data Privacy with Visibility

In Go, data privacy is managed through the visibility of identifiers. By convention, identifiers starting with a lowercase letter are unexported and accessible only within the same package. In contrast, identifiers that begin with an uppercase letter are exported and accessible from other packages.

For example, let's consider a Go struct named Person, which includes an unexported field name.

person/person.go

Go
1package person 2 3// Person struct with an unexported field 4type Person struct { 5 name string 6} 7 8// NewPerson is a constructor function 9func NewPerson(name string) *Person { 10 return &Person{name: name} 11}

main.go

Go
1package main 2 3import ( 4 "fmt" 5 "codesignal/person" 6) 7 8func main() { 9 person := person.NewPerson("Alice") 10 // The following line causes an error due to unexported access: 11 fmt.Println(person.name) 12}
Exported Methods for Controlled Access

In Go, encapsulation utilizes exported methods on structs to access or modify the unexported fields. Let's illustrate this through a simple example.

dog/dog.go

Go
1package dog 2 3// Dog struct with an unexported attribute 4type Dog struct { 5 name string 6} 7 8// NewDog is a constructor function 9func NewDog(name string) *Dog { 10 return &Dog{name: name} 11} 12 13// SetName is an exported method to modify the name 14func (d *Dog) SetName(name string) { 15 d.name = name 16} 17 18// Name is an exported method to retrieve the name 19func (d *Dog) Name() string { 20 return d.name 21}

main.go

Go
1package main 2 3import ( 4 "fmt" 5 "codesignal/dog" 6) 7 8func main() { 9 myDog := dog.NewDog("Max") 10 myDog.SetName("Buddy") 11 fmt.Println(myDog.Name()) // Output: Buddy 12}
The Go Way: Simplicity in Encapsulation

In Go, it's idiomatic to use the exported field name as the getter method name (e.g., method Owner for field owner), while setters can be named with a Set prefix. The Go approach emphasizes simplicity, with public fields for straightforward data types and setters/getters for types used as part of an abstraction. Use concrete types until abstraction becomes necessary.

In summary, here are scenarios where getters and setters are appropriate:

  • Validation or Business Logic: Use setters when a field assignment involves validation or extra logic.
  • Encapsulation: Use getters to reveal a field's value while preventing direct alterations.
  • Computed Properties: Use getters to calculate a value dynamically from other fields.
Practical Application

Let's apply encapsulation principles to our BankAccount struct, which includes unexported attributes like an account number and balance, alongside exported methods for withdrawals, deposits, and balance checks.

bank/bank.go

Go
1package bank 2 3// BankAccount struct with unexported attributes 4type BankAccount struct { 5 accountNo int 6 balance float64 7} 8 9// NewBankAccount is a constructor function 10func NewBankAccount(accountNo int, balance float64) *BankAccount { 11 return &BankAccount{accountNo: accountNo, balance: balance} 12} 13 14// Withdraw is an exported method to withdraw money 15func (acc *BankAccount) Withdraw(amount float64) { 16 if amount > 0 && amount <= acc.balance { 17 acc.balance -= amount 18 } else { 19 fmt.Println("Invalid amount or insufficient balance.") 20 } 21} 22 23// Deposit is an exported method to deposit money 24func (acc *BankAccount) Deposit(amount float64) { 25 if amount > 0 { 26 acc.balance += amount 27 } else { 28 fmt.Println("Invalid deposit amount.") 29 } 30} 31 32// CheckBalance is an exported method to check balance 33func (acc *BankAccount) CheckBalance() float64 { 34 return acc.balance 35}

main.go

Go
1package main 2 3import ( 4 "fmt" 5 "codesignal/bank" 6) 7 8func main() { 9 account := bank.NewBankAccount(1, 500.0) 10 account.Withdraw(100) 11 account.Deposit(50) 12 fmt.Println(account.CheckBalance()) // Prints: 450.0 13}

In this example, the BankAccount struct encapsulates account details, with methods manipulating the balance in a controlled manner, enhancing security.

Summary and Practice

Now, it's your turn to apply encapsulation in Go. Create structs with unexported fields and develop exported methods to control access and modifications. This hands-on practice will deepen your understanding of encapsulation concepts. Happy coding!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.