Skip to main content

Go Structs

Structs in Go are powerful data structures that allow you to create custom types by grouping together related data fields. They provide a foundation for object-oriented programming concepts in Go, including methods, embedding, and composition. Understanding structs is crucial for building complex data models and organizing code effectively. This comprehensive guide will teach you everything you need to know about Go's struct system.

Understanding Structs in Go

What Are Structs?

Structs in Go are composite data types that group together zero or more named fields of different types. They provide:

  • Custom data types - Create types that represent real-world entities
  • Field organization - Group related data together
  • Type safety - Ensure data integrity through type checking
  • Method support - Attach functions to struct types
  • Embedding support - Compose structs from other structs

Struct Characteristics

Go structs have several important characteristics:

Value Types

Structs are value types, meaning they are copied when passed to functions or assigned to variables.

Field Access

Struct fields are accessed using dot notation (.).

Zero Values

Structs have zero values where each field is set to its type's zero value.

Comparable Types

Structs are comparable if all their fields are comparable.

Struct Definition and Declaration

Basic Struct Definition

The type Keyword

The type keyword is used to define new types, including struct types.

Struct Field Syntax

Struct fields are defined with a name and type, separated by spaces.

package main

import "fmt"

func main() {
// Basic struct definition examples
fmt.Println("Basic struct definition examples:")

// Define a simple struct
type Person struct {
Name string
Age int
}

// Create struct instances
person1 := Person{Name: "Alice", Age: 30}
fmt.Printf("Person 1: %+v\n", person1)
// Output: Person 1: {Name:Alice Age:30}

// Create struct with field order
person2 := Person{"Bob", 25}
fmt.Printf("Person 2: %+v\n", person2)
// Output: Person 2: {Name:Bob Age:25}

// Create struct with zero values
var person3 Person
fmt.Printf("Person 3: %+v\n", person3)
// Output: Person 3: {Name: Age:0}

// Access struct fields
person1.Name = "Alice Smith"
person1.Age = 31
fmt.Printf("Updated Person 1: %+v\n", person1)
// Output: Updated Person 1: {Name:Alice Smith Age:31}

// Define struct with different field types
type Product struct {
ID int
Name string
Price float64
InStock bool
Categories []string
}

product := Product{
ID: 1,
Name: "Laptop",
Price: 999.99,
InStock: true,
Categories: []string{"Electronics", "Computers"},
}
fmt.Printf("Product: %+v\n", product)
// Output: Product: {ID:1 Name:Laptop Price:999.99 InStock:true Categories:[Electronics Computers]}
}

Struct Initialization Patterns

Go provides several ways to initialize structs, each with specific use cases.

package main

import "fmt"

func main() {
// Struct initialization patterns examples
fmt.Println("Struct initialization patterns examples:")

// Define a struct
type User struct {
ID int
Username string
Email string
Active bool
}

// Pattern 1: Field names (recommended)
user1 := User{
ID: 1,
Username: "alice",
Email: "[email protected]",
Active: true,
}
fmt.Printf("User 1: %+v\n", user1)
// Output: User 1: {ID:1 Username:alice Email:[email protected] Active:true}

// Pattern 2: Field order (less readable)
user2 := User{2, "bob", "[email protected]", false}
fmt.Printf("User 2: %+v\n", user2)
// Output: User 2: {ID:2 Username:bob Email:[email protected] Active:false}

// Pattern 3: Partial initialization
user3 := User{
ID: 3,
Username: "charlie",
// Email and Active will be zero values
}
fmt.Printf("User 3: %+v\n", user3)
// Output: User 3: {ID:3 Username:charlie Email: Active:false}

// Pattern 4: Zero value initialization
var user4 User
fmt.Printf("User 4: %+v\n", user4)
// Output: User 4: {ID:0 Username: Email: Active:false}

// Pattern 5: Pointer initialization
user5 := &User{
ID: 5,
Username: "david",
Email: "[email protected]",
Active: true,
}
fmt.Printf("User 5: %+v\n", user5)
// Output: User 5: &{ID:5 Username:david Email:[email protected] Active:true}

// Pattern 6: Using new function
user6 := new(User)
user6.ID = 6
user6.Username = "eve"
user6.Email = "[email protected]"
user6.Active = true
fmt.Printf("User 6: %+v\n", user6)
// Output: User 6: &{ID:6 Username:eve Email:[email protected] Active:true}
}

Struct Field Tags and Metadata

Struct fields can have tags that provide metadata about the field.

package main

import "fmt"

func main() {
// Struct field tags and metadata examples
fmt.Println("Struct field tags and metadata examples:")

// Define struct with field tags
type User struct {
ID int `json:"id" db:"user_id" validate:"required"`
Username string `json:"username" db:"username" validate:"required,min=3"`
Email string `json:"email" db:"email" validate:"required,email"`
Password string `json:"-" db:"password_hash" validate:"required,min=8"`
Active bool `json:"active" db:"is_active"`
}

// Create user instance
user := User{
ID: 1,
Username: "alice",
Email: "[email protected]",
Password: "secret123",
Active: true,
}

fmt.Printf("User: %+v\n", user)
// Output: User: {ID:1 Username:alice Email:[email protected] Password:secret123 Active:true}

// Note: Field tags are used by external libraries for:
// - JSON serialization/deserialization
// - Database mapping
// - Validation
// - XML processing
// - Other metadata processing
}

Struct Methods and Receiver Functions

Method Definition and Receivers

The func Keyword with Receivers

Methods are functions that are attached to a specific type using receiver syntax.

Value vs Pointer Receivers

Methods can have value receivers (copy) or pointer receivers (reference).

package main

import "fmt"

func main() {
// Struct methods and receiver functions examples
fmt.Println("Struct methods and receiver functions examples:")

// Define a struct
type Rectangle struct {
Width float64
Height float64
}

// Method with value receiver
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}

// Method with value receiver
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}

// Method with pointer receiver
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}

// Method with pointer receiver
func (r *Rectangle) SetDimensions(width, height float64) {
r.Width = width
r.Height = height
}

// Create rectangle instance
rect := Rectangle{Width: 5.0, Height: 3.0}
fmt.Printf("Original rectangle: %+v\n", rect)
// Output: Original rectangle: {Width:5 Height:3}

// Use value receiver methods
fmt.Printf("Area: %.2f\n", rect.Area())
fmt.Printf("Perimeter: %.2f\n", rect.Perimeter())
// Output:
// Area: 15.00
// Perimeter: 16.00

// Use pointer receiver methods
rect.Scale(2.0)
fmt.Printf("Scaled rectangle: %+v\n", rect)
// Output: Scaled rectangle: {Width:10 Height:6}

rect.SetDimensions(8.0, 4.0)
fmt.Printf("Updated rectangle: %+v\n", rect)
// Output: Updated rectangle: {Width:8 Height:4}

// Method chaining (if methods return the receiver)
// Note: Go doesn't have built-in method chaining, but you can implement it
rect.SetDimensions(10.0, 5.0)
rect.Scale(1.5)
fmt.Printf("Final rectangle: %+v\n", rect)
// Output: Final rectangle: {Width:15 Height:7.5}
}

Method Patterns and Best Practices

Following method patterns and best practices ensures clean and maintainable code.

package main

import "fmt"

func main() {
// Method patterns and best practices examples
fmt.Println("Method patterns and best practices examples:")

// Define a struct with methods
type BankAccount struct {
AccountNumber string
Balance float64
Owner string
}

// Constructor method (convention: NewTypeName)
func NewBankAccount(accountNumber, owner string, initialBalance float64) *BankAccount {
return &BankAccount{
AccountNumber: accountNumber,
Balance: initialBalance,
Owner: owner,
}
}

// Getter methods (convention: GetFieldName)
func (ba *BankAccount) GetBalance() float64 {
return ba.Balance
}

func (ba *BankAccount) GetOwner() string {
return ba.Owner
}

// Setter methods (convention: SetFieldName)
func (ba *BankAccount) SetOwner(owner string) {
ba.Owner = owner
}

// Business logic methods
func (ba *BankAccount) Deposit(amount float64) error {
if amount <= 0 {
return fmt.Errorf("deposit amount must be positive")
}
ba.Balance += amount
return nil
}

func (ba *BankAccount) Withdraw(amount float64) error {
if amount <= 0 {
return fmt.Errorf("withdrawal amount must be positive")
}
if amount > ba.Balance {
return fmt.Errorf("insufficient funds")
}
ba.Balance -= amount
return nil
}

// Utility methods
func (ba *BankAccount) IsEmpty() bool {
return ba.Balance == 0
}

func (ba *BankAccount) String() string {
return fmt.Sprintf("Account %s (Owner: %s, Balance: $%.2f)",
ba.AccountNumber, ba.Owner, ba.Balance)
}

// Create bank account
account := NewBankAccount("12345", "Alice", 1000.0)
fmt.Printf("Created: %s\n", account)
// Output: Created: Account 12345 (Owner: Alice, Balance: $1000.00)

// Use methods
if err := account.Deposit(500.0); err != nil {
fmt.Printf("Deposit error: %v\n", err)
} else {
fmt.Printf("After deposit: $%.2f\n", account.GetBalance())
}
// Output: After deposit: $1500.00

if err := account.Withdraw(200.0); err != nil {
fmt.Printf("Withdrawal error: %v\n", err)
} else {
fmt.Printf("After withdrawal: $%.2f\n", account.GetBalance())
}
// Output: After withdrawal: $1300.00

// Test error cases
if err := account.Withdraw(2000.0); err != nil {
fmt.Printf("Withdrawal error: %v\n", err)
}
// Output: Withdrawal error: insufficient funds

// Use utility methods
fmt.Printf("Is empty: %t\n", account.IsEmpty())
// Output: Is empty: false
}

Struct Embedding and Composition

Struct Embedding

The embed Keyword

Go supports struct embedding, allowing one struct to include another struct's fields and methods.

Anonymous Fields

Embedded structs are anonymous fields that provide direct access to their fields and methods.

package main

import "fmt"

func main() {
// Struct embedding and composition examples
fmt.Println("Struct embedding and composition examples:")

// Define base structs
type Person struct {
Name string
Age int
}

type Address struct {
Street string
City string
State string
Zip string
}

// Define struct with embedding
type Employee struct {
Person // Embedded struct
Address // Embedded struct
ID int
Salary float64
}

// Create employee instance
employee := Employee{
Person: Person{
Name: "Alice",
Age: 30,
},
Address: Address{
Street: "123 Main St",
City: "New York",
State: "NY",
Zip: "10001",
},
ID: 1001,
Salary: 75000.0,
}

// Access embedded fields directly
fmt.Printf("Employee: %s, Age: %d\n", employee.Name, employee.Age)
fmt.Printf("Address: %s, %s, %s %s\n", employee.Street, employee.City, employee.State, employee.Zip)
fmt.Printf("ID: %d, Salary: $%.2f\n", employee.ID, employee.Salary)
// Output:
// Employee: Alice, Age: 30
// Address: 123 Main St, New York, NY 10001
// ID: 1001, Salary: $75000.00

// Access embedded fields through the embedded struct
fmt.Printf("Person: %+v\n", employee.Person)
fmt.Printf("Address: %+v\n", employee.Address)
// Output:
// Person: {Name:Alice Age:30}
// Address: {Street:123 Main St City:New York State:NY Zip:10001}
}

Method Promotion and Composition

Embedded structs promote their methods to the embedding struct.

package main

import "fmt"

func main() {
// Method promotion and composition examples
fmt.Println("Method promotion and composition examples:")

// Define base structs with methods
type Animal struct {
Name string
Age int
}

func (a Animal) Speak() string {
return "Some generic animal sound"
}

func (a Animal) GetInfo() string {
return fmt.Sprintf("%s is %d years old", a.Name, a.Age)
}

type Flyable struct {
CanFly bool
}

func (f Flyable) Fly() string {
if f.CanFly {
return "Flying through the air"
}
return "Cannot fly"
}

type Swimmable struct {
CanSwim bool
}

func (s Swimmable) Swim() string {
if s.CanSwim {
return "Swimming in the water"
}
return "Cannot swim"
}

// Define composed structs
type Bird struct {
Animal
Flyable
Species string
}

type Duck struct {
Animal
Flyable
Swimmable
Species string
}

// Create instances
bird := Bird{
Animal: Animal{Name: "Eagle", Age: 5},
Flyable: Flyable{CanFly: true},
Species: "Golden Eagle",
}

duck := Duck{
Animal: Animal{Name: "Mallard", Age: 3},
Flyable: Flyable{CanFly: true},
Swimmable: Swimmable{CanSwim: true},
Species: "Mallard Duck",
}

// Use promoted methods
fmt.Printf("Bird: %s\n", bird.GetInfo())
fmt.Printf("Bird speaks: %s\n", bird.Speak())
fmt.Printf("Bird flies: %s\n", bird.Fly())
// Output:
// Bird: Eagle is 5 years old
// Bird speaks: Some generic animal sound
// Bird flies: Flying through the air

fmt.Printf("Duck: %s\n", duck.GetInfo())
fmt.Printf("Duck speaks: %s\n", duck.Speak())
fmt.Printf("Duck flies: %s\n", duck.Fly())
fmt.Printf("Duck swims: %s\n", duck.Swim())
// Output:
// Duck: Mallard is 3 years old
// Duck speaks: Some generic animal sound
// Duck flies: Flying through the air
// Duck swims: Swimming in the water
}

Struct Composition Patterns

Struct composition enables powerful design patterns in Go.

package main

import "fmt"

func main() {
// Struct composition patterns examples
fmt.Println("Struct composition patterns examples:")

// Pattern 1: Mixin pattern
type Timestamp struct {
CreatedAt string
UpdatedAt string
}

func (t *Timestamp) SetCreatedAt(time string) {
t.CreatedAt = time
}

func (t *Timestamp) SetUpdatedAt(time string) {
t.UpdatedAt = time
}

type User struct {
ID int
Username string
Email string
Timestamp // Embedded mixin
}

user := User{
ID: 1,
Username: "alice",
Email: "[email protected]",
}

user.SetCreatedAt("2023-12-07T10:00:00Z")
user.SetUpdatedAt("2023-12-07T10:30:00Z")

fmt.Printf("User: %+v\n", user)
// Output: User: {ID:1 Username:alice Email:[email protected] Timestamp:{CreatedAt:2023-12-07T10:00:00Z UpdatedAt:2023-12-07T10:30:00Z}}

// Pattern 2: Decorator pattern
type Logger struct {
LogLevel string
}

func (l *Logger) Log(message string) {
fmt.Printf("[%s] %s\n", l.LogLevel, message)
}

type Service struct {
Name string
Logger // Embedded decorator
}

service := Service{
Name: "UserService",
Logger: Logger{LogLevel: "INFO"},
}

service.Log("Service started")
// Output: [INFO] Service started

// Pattern 3: Strategy pattern
type PaymentStrategy interface {
Pay(amount float64) string
}

type CreditCard struct {
CardNumber string
}

func (cc CreditCard) Pay(amount float64) string {
return fmt.Sprintf("Paid $%.2f with credit card ending in %s", amount, cc.CardNumber[len(cc.CardNumber)-4:])
}

type PayPal struct {
Email string
}

func (pp PayPal) Pay(amount float64) string {
return fmt.Sprintf("Paid $%.2f with PayPal account %s", amount, pp.Email)
}

type Order struct {
ID int
Amount float64
Payment PaymentStrategy
}

order1 := Order{
ID: 1,
Amount: 99.99,
Payment: CreditCard{CardNumber: "1234567890123456"},
}

order2 := Order{
ID: 2,
Amount: 149.99,
Payment: PayPal{Email: "[email protected]"},
}

fmt.Printf("Order 1: %s\n", order1.Payment.Pay(order1.Amount))
fmt.Printf("Order 2: %s\n", order2.Payment.Pay(order2.Amount))
// Output:
// Order 1: Paid $99.99 with credit card ending in 3456
// Order 2: Paid $149.99 with PayPal account [email protected]
}

What You've Learned

Congratulations! You now have a comprehensive understanding of Go's struct system:

Struct Fundamentals

  • Understanding struct definition and declaration
  • Working with struct initialization patterns
  • Using struct field tags and metadata
  • Understanding struct characteristics and behavior

Struct Methods and Receivers

  • Defining methods with value and pointer receivers
  • Following method patterns and best practices
  • Implementing constructor and utility methods
  • Understanding receiver type choices

Struct Embedding and Composition

  • Understanding struct embedding and anonymous fields
  • Working with method promotion and composition
  • Implementing composition patterns and design patterns
  • Creating flexible and reusable struct hierarchies

Key Concepts

  • type - Defining new types including structs
  • func - Defining methods with receivers
  • Embedding - Including other structs for composition
  • Promotion - Automatic access to embedded fields and methods

Next Steps

You now have a solid foundation in Go's struct system. In the next section, we'll explore Go's interface system, which provides a way to define contracts and achieve polymorphism.

Understanding structs is crucial for creating custom data types and organizing code effectively. These concepts form the foundation for all the more advanced programming techniques we'll cover in the coming chapters.


Ready to learn about interfaces? Let's explore Go's powerful interface system and learn how to achieve polymorphism and abstraction!