Structs and Methods in Go
Structs are collections of fields that together represent a single concept or entity. Methods are functions associated with a particular type, allowing you to work with that type’s data.
What are Structs?
A struct is a composite data type that groups together variables under a single name. These variables, called fields, can be of different types.
Basic Struct Definition
type Person struct {
Name string
Age int
Email string
}Creating and Using Structs
Basic Struct Usage
package main
import "fmt"
// Define a struct
type Person struct {
Name string
Age int
Email string
}
func main() {
// Create a struct using field names
person1 := Person{
Name: "Alice Johnson",
Age: 30,
Email: "[email protected]",
}
// Create a struct without field names (order matters)
person2 := Person{"Bob Smith", 25, "[email protected]"}
// Create a struct and set fields later
var person3 Person
person3.Name = "Carol Williams"
person3.Age = 28
person3.Email = "[email protected]"
fmt.Println("Person 1:", person1)
fmt.Println("Person 2:", person2)
fmt.Println("Person 3:", person3)
// Access individual fields
fmt.Printf("%s is %d years old\n", person1.Name, person1.Age)
}Struct Pointers
package main
import "fmt"
type Car struct {
Make string
Model string
Year int
}
func main() {
// Create a struct
car := Car{Make: "Toyota", Model: "Camry", Year: 2020}
// Create a pointer to a struct
carPtr := &car
// Access fields through pointer (automatic dereferencing)
fmt.Println("Make:", carPtr.Make)
// Modify through pointer
carPtr.Year = 2021
fmt.Println("Updated year:", car.Year)
// Create pointer directly
carPtr2 := &Car{
Make: "Honda",
Model: "Civic",
Year: 2022,
}
fmt.Println("Car 2:", *carPtr2)
}Methods
Methods are functions attached to a specific type. They can access and modify the data of that type.
Value Receiver Methods
package main
import (
"fmt"
"math"
)
// Rectangle struct
type Rectangle struct {
Width float64
Height float64
}
// Method with value receiver
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
func (r Rectangle) Diagonal() float64 {
return math.Sqrt(r.Width*r.Width + r.Height*r.Height)
}
func main() {
rect := Rectangle{Width: 5, Height: 3}
fmt.Printf("Rectangle: %.1f x %.1f\n", rect.Width, rect.Height)
fmt.Printf("Area: %.2f\n", rect.Area())
fmt.Printf("Perimeter: %.2f\n", rect.Perimeter())
fmt.Printf("Diagonal: %.2f\n", rect.Diagonal())
}Pointer Receiver Methods
package main
import "fmt"
type BankAccount struct {
Owner string
Balance float64
}
// Pointer receiver method - can modify the struct
func (ba *BankAccount) Deposit(amount float64) {
if amount > 0 {
ba.Balance += amount
fmt.Printf("Deposited: $%.2f, New Balance: $%.2f\n", amount, ba.Balance)
}
}
func (ba *BankAccount) Withdraw(amount float64) bool {
if amount > 0 && ba.Balance >= amount {
ba.Balance -= amount
fmt.Printf("Withdrew: $%.2f, New Balance: $%.2f\n", amount, ba.Balance)
return true
}
fmt.Printf("Withdrawal failed: Insufficient funds\n")
return false
}
// Value receiver method - cannot modify the struct
func (ba BankAccount) GetBalance() float64 {
return ba.Balance
}
func (ba BankAccount) String() string {
return fmt.Sprintf("Account Owner: %s, Balance: $%.2f", ba.Owner, ba.Balance)
}
func main() {
account := BankAccount{Owner: "John Doe", Balance: 1000.0}
fmt.Println(account) // Uses String() method automatically
account.Deposit(500.0)
account.Withdraw(200.0)
account.Withdraw(2000.0) // Will fail
fmt.Printf("Current balance: $%.2f\n", account.GetBalance())
}Advanced Struct Features
Embedded Structs (Composition)
package main
import "fmt"
// Address struct
type Address struct {
Street string
City string
State string
ZipCode string
}
// Person struct with embedded Address
type Person struct {
Name string
Age int
Address // Embedded struct
}
func main() {
person := Person{
Name: "Alice Johnson",
Age: 30,
Address: Address{
Street: "123 Main St",
City: "New York",
State: "NY",
ZipCode: "10001",
},
}
// Access embedded fields directly
fmt.Println("Name:", person.Name)
fmt.Println("Street:", person.Street) // Direct access to embedded field
// Access embedded struct
fmt.Println("Full Address:", person.Address)
// Alternative access to embedded fields
fmt.Println("City:", person.Address.City)
}Struct Tags
Struct tags provide metadata about struct fields, commonly used for JSON encoding, database mapping, and validation:
package main
import (
"encoding/json"
"fmt"
"reflect"
)
type User struct {
ID int `json:"id" db:"user_id"`
Username string `json:"username" db:"username" validate:"required,min=3,max=20"`
Email string `json:"email" db:"email" validate:"required,email"`
Age int `json:"age,omitempty" db:"age"`
Password string `json:"-" db:"password"` // "-" means omit in JSON
CreatedAt string `json:"created_at" db:"created_at"`
}
func getStructTags() {
t := reflect.TypeOf(User{})
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("Field: %s, Tags: %s\n", field.Name, field.Tag)
}
}
func main() {
user := User{
ID: 1,
Username: "johndoe",
Email: "[email protected]",
Age: 30,
Password: "secret123",
CreatedAt: "2023-01-01",
}
// Convert to JSON
jsonData, err := json.MarshalIndent(user, "", " ")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("JSON representation:")
fmt.Println(string(jsonData))
fmt.Println("\nStruct tags:")
getStructTags()
}Anonymous Structs
package main
import "fmt"
func main() {
// Anonymous struct literal
person := struct {
Name string
Age int
City string
}{
Name: "Alice",
Age: 30,
City: "New York",
}
fmt.Printf("%+v\n", person)
// Slice of anonymous structs
employees := []struct {
ID int
Name string
Salary float64
}{
{ID: 1, Name: "John", Salary: 50000},
{ID: 2, Name: "Jane", Salary: 60000},
{ID: 3, Name: "Bob", Salary: 55000},
}
for _, emp := range employees {
fmt.Printf("ID: %d, Name: %s, Salary: $%.2f\n", emp.ID, emp.Name, emp.Salary)
}
// Function returning anonymous struct
getUser := func(id int) struct {
ID int
Name string
Role string
} {
if id == 1 {
return struct {
ID int
Name string
Role string
}{ID: 1, Name: "Admin User", Role: "Administrator"}
}
return struct {
ID int
Name string
Role string
}{ID: id, Name: "Regular User", Role: "User"}
}
user := getUser(1)
fmt.Printf("User: %+v\n", user)
}Method Sets and Interfaces
package main
import "fmt"
// Writer interface
type Writer interface {
Write(data string) error
}
// Reader interface
type Reader interface {
Read() string
}
// Document struct
type Document struct {
content string
}
// Pointer receiver method - implements Writer
func (d *Document) Write(data string) error {
d.content = data
return nil
}
// Value receiver method - implements Reader
func (d Document) Read() string {
return d.content
}
func main() {
// Create a Document
doc := &Document{}
// Use Write method (requires pointer)
err := doc.Write("Hello, Go methods!")
if err != nil {
fmt.Println("Error writing:", err)
return
}
// Use Read method (works with value or pointer)
content := doc.Read()
fmt.Println("Content:", content)
// Interface usage
var writer Writer = doc // OK because Write has pointer receiver
writer.Write("New content")
var reader Reader = *doc // OK because Read has value receiver
content2 := reader.Read()
fmt.Println("Content from interface:", content2)
// This won't compile because Read() has value receiver
// var reader2 Reader = doc
}Practical Example: Shopping Cart
package main
import (
"fmt"
"time"
)
type Item struct {
ID string
Name string
Price float64
Description string
}
type CartItem struct {
Item Item
Quantity int
}
func (ci CartItem) Subtotal() float64 {
return ci.Item.Price * float64(ci.Quantity)
}
type ShoppingCart struct {
Items []CartItem
CustomerID string
CreatedAt time.Time
}
func NewShoppingCart(customerID string) *ShoppingCart {
return &ShoppingCart{
Items: make([]CartItem, 0),
CustomerID: customerID,
CreatedAt: time.Now(),
}
}
func (sc *ShoppingCart) AddItem(item Item, quantity int) {
// Check if item already exists in cart
for i, cartItem := range sc.Items {
if cartItem.Item.ID == item.ID {
sc.Items[i].Quantity += quantity
fmt.Printf("Updated quantity for %s to %d\n", item.Name, sc.Items[i].Quantity)
return
}
}
// Add new item
sc.Items = append(sc.Items, CartItem{
Item: item,
Quantity: quantity,
})
fmt.Printf("Added %d x %s to cart\n", quantity, item.Name)
}
func (sc *ShoppingCart) RemoveItem(itemID string) bool {
for i, cartItem := range sc.Items {
if cartItem.Item.ID == itemID {
sc.Items = append(sc.Items[:i], sc.Items[i+1:]...)
fmt.Printf("Removed %s from cart\n", cartItem.Item.Name)
return true
}
}
fmt.Printf("Item with ID %s not found in cart\n", itemID)
return false
}
func (sc *ShoppingCart) UpdateQuantity(itemID string, quantity int) bool {
if quantity <= 0 {
return sc.RemoveItem(itemID)
}
for i, cartItem := range sc.Items {
if cartItem.Item.ID == itemID {
sc.Items[i].Quantity = quantity
fmt.Printf("Updated quantity for %s to %d\n", cartItem.Item.Name, quantity)
return true
}
}
fmt.Printf("Item with ID %s not found in cart\n", itemID)
return false
}
func (sc ShoppingCart) Total() float64 {
var total float64
for _, cartItem := range sc.Items {
total += cartItem.Subtotal()
}
return total
}
func (sc ShoppingCart) ItemCount() int {
count := 0
for _, cartItem := range sc.Items {
count += cartItem.Quantity
}
return count
}
func (sc ShoppingCart) Display() {
fmt.Printf("\nShopping Cart for Customer: %s\n", sc.CustomerID)
fmt.Printf("Created: %s\n", sc.CreatedAt.Format("2006-01-02 15:04:05"))
fmt.Println("Items:")
if len(sc.Items) == 0 {
fmt.Println(" (Cart is empty)")
return
}
for _, cartItem := range sc.Items {
fmt.Printf(" - %s (ID: %s)\n", cartItem.Item.Name, cartItem.Item.ID)
fmt.Printf(" Price: $%.2f x %d = $%.2f\n",
cartItem.Item.Price, cartItem.Quantity, cartItem.Subtotal())
fmt.Printf(" %s\n", cartItem.Item.Description)
}
fmt.Printf("\nTotal Items: %d\n", sc.ItemCount())
fmt.Printf("Total Price: $%.2f\n", sc.Total())
}
func main() {
// Create some items
laptop := Item{
ID: "LT001",
Name: "Laptop",
Price: 999.99,
Description: "High-performance laptop with 16GB RAM",
}
mouse := Item{
ID: "MS001",
Name: "Wireless Mouse",
Price: 29.99,
Description: "Ergonomic wireless mouse",
}
keyboard := Item{
ID: "KB001",
Name: "Mechanical Keyboard",
Price: 149.99,
Description: "RGB mechanical keyboard",
}
// Create shopping cart
cart := NewShoppingCart("CUST12345")
cart.Display()
// Add items
cart.AddItem(laptop, 1)
cart.AddItem(mouse, 2)
cart.AddItem(keyboard, 1)
cart.Display()
// Update quantity
cart.UpdateQuantity("MS001", 3)
cart.Display()
// Remove item
cart.RemoveItem("KB001")
cart.Display()
fmt.Printf("\nFinal Total: $%.2f\n", cart.Total())
}Best Practices
- Use clear, descriptive names for your structs and fields
- Group related fields together in logical structs
- Use pointer receivers when methods need to modify the struct
- Use value receivers when methods don’t need to modify the struct
- Keep structs small and focused on a single responsibility
- Use struct tags for serialization and validation metadata
- Consider embedding for composition instead of inheritance
External Resources:
Related Tutorials:
Last updated on