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.
Go1package 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}
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
Go1package 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
Go1package 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}
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
Go1package 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
Go1package 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}
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.
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
Go1package 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
Go1package 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.
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!