Golang Tutorial - Structs
In Go, structs are custom, user-defined data structures. Since Go does not have traditional classes or objects like Java, C++, or Python, structs serve as the primary building block for implementing Object-Oriented Programming (OOP) patterns. They allow you to group multiple fields of different data types together into a single, cohesive unit to represent complex real-world entities.
1. Why Do We Need Structs?
Basic data types like int, string, float32, and bool are sufficient for simple values. However, real-world applications deal with complex entities. For example, in an e-commerce application, an Order is not just a single value; it is a collection of related attributes:
- An ID (string)
- An Amount (float)
- A Status (string)
- A Timestamp (time)
Instead of managing these variables independently, a struct allows you to group them under a single custom type.
Real-Life Analogy
Think of a struct as a blank database form or a physical receipt. The receipt has predefined fields (Date, Items, Total Price, Payment Method). While each receipt contains different values, they all share the exact same structure.
2. Defining a Struct
To define a struct in Go, use the type and struct keywords. Struct definitions are typically placed outside the main function so they can be accessed globally within the package.
package main
import (
"fmt"
"time"
)
// Order represents an e-commerce order entity
type Order struct {
id string
amount float32
status string
createdAt time.Time // Imported from the standard "time" package
}
Note on
time.Time: The standardtimepackage in Go is highly precise, offering nanosecond-level precision for timestamps.
3. Instantiating Structs and Accessing Fields
Once a struct is defined, you can create instances of it. Go provides multiple ways to instantiate a struct and assign values to its fields.
Method 1: Short-Hand Instantiation (Struct Literals)
You can instantiate and initialize a struct inline using the := operator.
myOrder := Order{
id: "1",
amount: 50.00,
status: "received",
}
Method 2: Dot Notation for Assignment and Access
You do not have to initialize all fields at once. You can assign or update values later using the dot (.) operator.
// Assigning a value post-instantiation
myOrder.createdAt = time.Now()
// Accessing a specific field
fmt.Println("Order Status:", myOrder.status)
4. Zero Values of Struct Fields
If you instantiate a struct but omit some of its fields, Go automatically assigns those fields their respective zero values.
partialOrder := Order{
id: "2",
// amount, status, and createdAt are omitted
}
Default Zero Values in Go
5. Value Semantics: Independence of Struct Instances
Structs in Go are value types. When you create multiple instances of a struct, they are completely independent of each other in memory. Modifying one instance will never affect another.
myOrder1 := Order{id: "1", status: "received"}
myOrder2 := Order{id: "2", status: "delivered"}
// Modifying myOrder1
myOrder1.status = "paid"
fmt.Println(myOrder1.status) // Output: paid
fmt.Println(myOrder2.status) // Output: delivered (remains unaffected)
Memory Representation
6. Attaching Methods to Structs (Value vs. Pointer Receivers)
To implement behavior (methods) for a struct, Go uses receiver functions. A receiver function is a regular function attached to a specific target type.
The Naive (Incorrect) Approach: Value Receiver
If you try to modify a struct's field using a standard value receiver, the changes will not persist outside the function.
// Value Receiver (receives a COPY of the struct)
func (o Order) changeStatusNaive(newStatus string) {
o.status = newStatus // This only modifies the local copy!
}
Because Go passes arguments by value, o is a copy of the original struct. Any modification inside this method is lost once the function execution finishes.
The Correct Approach: Pointer Receiver
To modify the actual fields of the struct instance, you must pass a pointer (*) to the struct as the receiver.
// Pointer Receiver (receives the memory address of the original struct)
func (o *Order) changeStatus(newStatus string) {
o.status = newStatus // Modifies the actual instance
}
Automatic Dereferencing in Go
Note: In languages like C or C++, you would need to explicitly dereference the pointer (e.g.,
(*o).status). Go automatically handles dereferencing for structs, allowing you to write cleano.statussyntax even when working with pointers.
Read-Only Methods
If a method only reads data without modifying the struct, a value receiver is acceptable:
func (o Order) getAmount() float32 {
return o.amount
}
Industry Convention: If some methods of a struct require pointer receivers to modify state, it is common practice to use pointer receivers for all methods of that struct to maintain consistency and avoid copying overhead for large structs.
7. Constructors in Go (The New Pattern)
Go does not have a built-in constructor keyword. Instead, Go developers use a convention of writing factory functions that start with the prefix New.
Implementing a Constructor Function
// newOrder acts as a constructor and returns a pointer to the newly created Order
func newOrder(id string, amount float32, status string) *Order {
myOrder := Order{
id: id,
amount: amount,
status: status,
createdAt: time.Now(), // Internal initialization logic
}
// Return the memory address of the struct using the address-of operator (&)
return &myOrder
}
Using the Constructor
func main() {
// Instantiating via constructor
orderPtr := newOrder("101", 99.99, "processing")
// Accessing fields directly (Go automatically dereferences the pointer)
fmt.Println("Amount:", orderPtr.amount)
}
8. Anonymous (Inline) Structs
Sometimes, you need a struct for a single, one-off operation (such as parsing a specific JSON payload or defining configuration settings). Creating a named global struct for this is unnecessary. Go allows you to define and instantiate anonymous structs inline.
func main() {
// Defining and initializing an anonymous struct inline
language := struct {
name string
isGood bool
}{
name: "Golang",
isGood: true,
}
fmt.Printf("Language: %s, Is Good: %t\n", language.name, language.isGood)
}
This syntax is highly comparable to object literals in JavaScript or TypeScript.
Summary and Key Takeaways
- Custom Data Types: Structs group related fields of different types to represent complex entities.
- No Classes: Go does not have classes; structs combined with receiver methods provide object-oriented capabilities.
- Zero Values: Uninitialized struct fields are automatically assigned their type-specific zero values (
0,"",false,nil). - Value Semantics: Structs are copied by value. Modifying one instance does not affect another unless pointers are used.
- Pointer Receivers: Use pointer receivers (
*StructName) in methods to modify the struct's internal state. - Value Receivers: Use value receivers for read-only operations, though pointer receivers are often preferred for consistency and performance.
- Constructor Convention: Use factory functions prefixed with
New(e.g.,NewOrder) returning a pointer (*Order) to initialize structs. - Anonymous Structs: Use inline anonymous structs for quick, one-off data structures to keep your global namespace clean.