Go Object-Oriented Programming
Structs, Methods, and Interfaces
Explanation
OOP in Go
Go doesn't have classes, but it has something more powerful: composition over inheritance. Instead of complex class hierarchies, Go uses structs, methods, and interfaces.
Think of it this way:
- Structs = Data containers (like classes without methods)
- Methods = Functions attached to structs
- Interfaces = Contracts that define behavior
Key Concepts
- Struct: A collection of fields
- Method: A function with a receiver
- Interface: A set of method signatures
- Embedding: Go's alternative to inheritance
- Composition: Building complex types from simple ones
Go vs Traditional OOP
| Concept | Java/C# | Go |
|---------|---------|-----|
| Classes | class User {} | type User struct {} |
| Inheritance | extends | Embedding |
| Polymorphism | Interfaces | Interfaces (implicit) |
| Constructors | new User() | NewUser() function |
Demonstration
Example 1: Structs and Methods
package main
import (
"fmt"
"time"
)
// Define a struct
type User struct {
ID int
Name string
Email string
CreatedAt time.Time
active bool // lowercase = private
}
// Constructor function (Go convention)
func NewUser(name, email string) *User {
return &User{
ID: generateID(),
Name: name,
Email: email,
CreatedAt: time.Now(),
active: true,
}
}
// Method with value receiver (doesn't modify original)
func (u User) Greet() string {
return fmt.Sprintf("Hello, I'm %s!", u.Name)
}
// Method with pointer receiver (can modify original)
func (u *User) Deactivate() {
u.active = false
}
// Getter for private field
func (u User) IsActive() bool {
return u.active
}
// Method that returns formatted string
func (u User) String() string {
status := "active"
if !u.active {
status = "inactive"
}
return fmt.Sprintf("User{ID: %d, Name: %s, Status: %s}", u.ID, u.Name, status)
}
var idCounter = 0
func generateID() int {
idCounter++
return idCounter
}
func main() {
// Create user
user := NewUser("Arthur", "art@bpc.com")
fmt.Println(user.Greet()) // Hello, I'm Arthur!
fmt.Println(user.IsActive()) // true
user.Deactivate()
fmt.Println(user.IsActive()) // false
fmt.Println(user) // User{ID: 1, Name: Arthur, Status: inactive}
}
Example 2: Interfaces
package main
import (
"fmt"
"math"
)
// Interface definition
type Shape interface {
Area() float64
Perimeter() float64
}
// Circle struct
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
// Rectangle struct
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// Triangle struct
type Triangle struct {
A, B, C float64 // sides
}
func (t Triangle) Area() float64 {
// Heron's formula
s := (t.A + t.B + t.C) / 2
return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C))
}
func (t Triangle) Perimeter() float64 {
return t.A + t.B + t.C
}
// Function that accepts any Shape
func PrintShapeInfo(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
// Function that works with slice of shapes
func TotalArea(shapes []Shape) float64 {
total := 0.0
for _, s := range shapes {
total += s.Area()
}
return total
}
func main() {
circle := Circle{Radius: 5}
rect := Rectangle{Width: 10, Height: 5}
tri := Triangle{A: 3, B: 4, C: 5}
// All satisfy Shape interface
shapes := []Shape{circle, rect, tri}
for _, shape := range shapes {
PrintShapeInfo(shape)
}
fmt.Printf("Total area: %.2f\n", TotalArea(shapes))
}
Example 3: Embedding (Composition)
package main
import (
"fmt"
"time"
)
// Base type
type Entity struct {
ID int
CreatedAt time.Time
UpdatedAt time.Time
}
func (e *Entity) SetTimestamps() {
now := time.Now()
if e.CreatedAt.IsZero() {
e.CreatedAt = now
}
e.UpdatedAt = now
}
// User embeds Entity
type User struct {
Entity // Embedded (anonymous field)
Name string
Email string
Role string
}
func NewUser(name, email string) *User {
user := &User{
Name: name,
Email: email,
Role: "user",
}
user.SetTimestamps() // Method from Entity
return user
}
// User can have its own methods
func (u User) IsAdmin() bool {
return u.Role == "admin"
}
// Admin embeds User (multi-level embedding)
type Admin struct {
User
Permissions []string
}
func NewAdmin(name, email string) *Admin {
admin := &Admin{
User: User{
Name: name,
Email: email,
Role: "admin",
},
Permissions: []string{"read", "write", "delete"},
}
admin.SetTimestamps()
return admin
}
func (a Admin) HasPermission(perm string) bool {
for _, p := range a.Permissions {
if p == perm {
return true
}
}
return false
}
func main() {
user := NewUser("Arthur", "art@bpc.com")
admin := NewAdmin("Sarah", "sarah@example.com")
fmt.Println(user.Name) // Arthur
fmt.Println(user.ID) // 0 (from Entity)
fmt.Println(user.IsAdmin()) // false
fmt.Println(admin.Name) // Sarah (from User)
fmt.Println(admin.IsAdmin()) // true (from User)
fmt.Println(admin.HasPermission("write")) // true
}
Example 4: Interface Composition
package main
import "fmt"
// Small, focused interfaces
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Composed interfaces
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// Implementation
type Buffer struct {
data []byte
}
func (b *Buffer) Read(p []byte) (int, error) {
n := copy(p, b.data)
b.data = b.data[n:]
return n, nil
}
func (b *Buffer) Write(p []byte) (int, error) {
b.data = append(b.data, p...)
return len(p), nil
}
func (b *Buffer) Close() error {
b.data = nil
return nil
}
// Functions can accept specific interfaces
func CopyData(dst Writer, src Reader) error {
buf := make([]byte, 1024)
n, err := src.Read(buf)
if err != nil {
return err
}
_, err = dst.Write(buf[:n])
return err
}
func main() {
src := &Buffer{data: []byte("Hello, World!")}
dst := &Buffer{}
CopyData(dst, src)
result := make([]byte, 100)
n, _ := dst.Read(result)
fmt.Println(string(result[:n])) // Hello, World!
}
Key Takeaways:
- Use structs for data, methods for behavior
- Interfaces are implicit (no
implementskeyword) - Prefer composition over inheritance
- Keep interfaces small and focused
- Use pointer receivers when modifying state
Imitation
Challenge 1: Create a Repository Pattern
Task: Create a Repository interface and implement it for users.
Solution
type Repository[T any] interface {
FindAll() []T
FindByID(id int) (*T, error)
Create(item T) (*T, error)
Update(id int, item T) (*T, error)
Delete(id int) error
}
type UserRepository struct {
users map[int]User
nextID int
}
func NewUserRepository() *UserRepository {
return &UserRepository{
users: make(map[int]User),
nextID: 1,
}
}
func (r *UserRepository) FindAll() []User {
result := make([]User, 0, len(r.users))
for _, u := range r.users {
result = append(result, u)
}
return result
}
func (r *UserRepository) FindByID(id int) (*User, error) {
user, ok := r.users[id]
if !ok {
return nil, fmt.Errorf("user %d not found", id)
}
return &user, nil
}
func (r *UserRepository) Create(user User) (*User, error) {
user.ID = r.nextID
r.nextID++
r.users[user.ID] = user
return &user, nil
}
Challenge 2: Implement the Stringer Interface
Task: Make a Product struct that formats nicely when printed.
Solution
import "fmt"
type Product struct {
ID int
Name string
Price float64
Category string
InStock bool
}
func (p Product) String() string {
stock := "In Stock"
if !p.InStock {
stock = "Out of Stock"
}
return fmt.Sprintf(
"Product #%d: %s ($%.2f) [%s] - %s",
p.ID, p.Name, p.Price, p.Category, stock,
)
}
// Usage
product := Product{
ID: 1,
Name: "Laptop",
Price: 999.99,
Category: "Electronics",
InStock: true,
}
fmt.Println(product)
// Product #1: Laptop ($999.99) [Electronics] - In Stock
Practice
Exercise 1: Banking System
Difficulty: Intermediate
Create a banking system with:
Accountinterface withDeposit,Withdraw,BalancemethodsSavingsAccountwith interest calculationCheckingAccountwith overdraft protectionTransactionhistory
Exercise 2: Plugin System
Difficulty: Advanced
Build a plugin system:
Plugininterface withName,Execute,Cleanupmethods- Plugin registry to manage plugins
- Plugin loading and execution order
- Error handling for failed plugins
Summary
What you learned:
- Structs as data containers
- Methods with value and pointer receivers
- Interfaces for polymorphism
- Embedding for composition
- Interface composition patterns
Next Steps:
- Read: Go API Development
- Practice: Refactor code to use interfaces
- Build: Create a modular application
