In this lesson we cover Go structs and methods — the primary way to model real-world entities in Go. If you completed Go Arrays, Slices, and Maps, you know how to store collections. Now you will learn how to group related fields into a single type and attach functions to that type. This Go structs tutorial builds the foundation for interfaces, error handling, and the REST API you will build in Lesson 12.
Prerequisites: Lessons 1–7 completed, Go 1.22 or newer installed. Estimated time: 45–60 minutes.
1. Defining a Struct
A struct is a composite type that groups named fields together. Each field has a name and a type. Structs are Go’s answer to objects in other languages — but without class inheritance. You define a struct with the type keyword:
package main
import "fmt"
// Person is a struct with three fields
type Person struct {
Name string
Age int
City string
}
func main() {
var p Person
fmt.Println(p) // { 0 } — zero value: empty string, 0 for int
p.Name = "Ada"
p.Age = 28
p.City = "Lagos"
fmt.Println(p.Name, p.Age, p.City)
}
Field names that start with an uppercase letter are exported (visible outside the package). Lowercase field names are private to the package — the same export rule you learned for identifiers in the Basic Syntax lesson.
2. Struct Literals
Create struct values with literals. You can list fields in order (positional) or by name (recommended for clarity):
package main
import "fmt"
type Book struct {
Title string
Author string
Pages int
}
func main() {
// Named field literal (preferred)
b1 := Book{
Title: "The Go Programming Language",
Author: "Donovan & Kernighan",
Pages: 380,
}
// Positional literal — must supply all fields in order
b2 := Book{"Learning Go", "Jon Bodner", 350}
// Pointer to a struct with &
b3 := &Book{Title: "Concurrency in Go", Author: "Katherine Cox-Buday", Pages: 300}
fmt.Println(b1.Title)
fmt.Println(b2.Author)
fmt.Println(b3.Pages)
}
Named field literals are safer — they survive field reordering and make code self-documenting. Use &Type{...} when you need a pointer to a struct, which is common when passing structs to functions that should modify them.
3. Methods — Functions with a Receiver
A method is a function with a receiver — a special parameter that binds the function to a type. Methods let you write p.Greet() instead of Greet(p), which reads more naturally for types that represent entities.
package main
import "fmt"
type Rectangle struct {
Width float64
Height float64
}
// Area is a method with a value receiver
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Scale is a method with a pointer receiver
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Println("Area:", rect.Area()) // 50
rect.Scale(2)
fmt.Println("Scaled area:", rect.Area()) // 200
}
The receiver appears between func and the method name. It looks like an extra parameter, but it is not part of the call — rect.Area() passes rect implicitly as the receiver.
4. Value Receivers vs Pointer Receivers
Choosing between a value receiver (r Rectangle) and a pointer receiver (r *Rectangle) is one of the most important decisions in Go struct design:
- Value receiver — receives a copy of the struct. Methods cannot modify the original. Good for small structs and read-only operations.
- Pointer receiver — receives a pointer to the struct. Methods can modify the original. Good for large structs (avoids copying) and mutating operations.
| Aspect | Value Receiver | Pointer Receiver |
|---|---|---|
| Syntax | func (r Rect) M() |
func (r *Rect) M() |
| Can modify struct? | No (works on a copy) | Yes (modifies original) |
| Memory | Copies entire struct each call | Copies only a pointer (8 bytes) |
| Nil receiver | Not applicable | Can panic if receiver is nil |
| Best for | Small structs, getters, String() | Mutators, large structs, consistency |
Consistency rule: If any method on a type uses a pointer receiver, use pointer receivers for all methods on that type. This avoids confusion about whether a method mutates the struct or not.
package main
import "fmt"
type Counter struct {
Count int
}
// Pointer receiver — modifies the original
func (c *Counter) Increment() {
c.Count++
}
// Pointer receiver for consistency, even though it only reads
func (c *Counter) String() string {
return fmt.Sprintf("Count: %d", c.Count)
}
func main() {
c := Counter{}
c.Increment()
c.Increment()
fmt.Println(c) // Count: 2
}
5. Struct Embedding — Composition Over Inheritance
Go does not have class inheritance. Instead, it uses embedding — placing one struct type inside another anonymously. The embedded struct’s fields and methods are promoted to the outer type, so you can access them directly.
package main
import "fmt"
type Address struct {
Street string
City string
}
func (a Address) FullAddress() string {
return a.Street + ", " + a.City
}
type Employee struct {
Name string
Address // embedded — fields promoted to Employee
Salary float64
}
func main() {
emp := Employee{
Name: "Chidi",
Address: Address{
Street: "12 Broad Street",
City: "Lagos",
},
Salary: 75000,
}
// Promoted fields — no need for emp.Address.City
fmt.Println(emp.City)
fmt.Println(emp.FullAddress()) // promoted method
}
Embedding is how Go achieves code reuse without inheritance hierarchies. A type can embed multiple structs. If two embedded types have a field with the same name, you must qualify the access: emp.Address.City.
Embedding also works with interfaces — a struct that embeds an interface must satisfy that interface at runtime. You will explore this pattern in the Interfaces lesson.
6. Putting It Together — A Simple Bank Account
Here is a practical example that combines struct definition, literals, methods, and pointer receivers:
package main
import (
"fmt"
"log"
)
type Account struct {
Owner string
Balance float64
}
func NewAccount(owner string) *Account {
return &Account{Owner: owner, Balance: 0}
}
func (a *Account) Deposit(amount float64) error {
if amount <= 0 {
return fmt.Errorf("deposit amount must be positive")
}
a.Balance += amount
return nil
}
func (a *Account) Withdraw(amount float64) error {
if amount <= 0 {
return fmt.Errorf("withdrawal amount must be positive")
}
if amount > a.Balance {
return fmt.Errorf("insufficient funds")
}
a.Balance -= amount
return nil
}
func (a *Account) String() string {
return fmt.Sprintf("%s: $%.2f", a.Owner, a.Balance)
}
func main() {
acct := NewAccount("Ngozi")
if err := acct.Deposit(500); err != nil {
log.Fatal(err)
}
if err := acct.Withdraw(150); err != nil {
log.Fatal(err)
}
fmt.Println(acct) // Ngozi: $350.00
}
Notice the constructor pattern NewAccount — a common Go idiom that returns a pointer to a newly initialized struct. Error returns on Deposit and Withdraw preview the error-handling style you will master in Lesson 10.
7. Next Steps
You now understand Go structs and methods — defining types, creating literals, writing methods with value and pointer receivers, and composing types through embedding. In the next lesson we dive deeper into pointers — when to use them, how they interact with structs, and common patterns you will see in production Go code.
Continue the series: Lesson 7 – Arrays, Slices, and Maps · Lesson 9 – Pointers
Frequently Asked Questions
What is a struct in Go?
A struct groups related fields into a single type — Go’s primary way to model data. Structs do not have inheritance; use embedding for composition.
What is the difference between value and pointer receivers?
Pointer receivers can modify the struct and avoid copying large values. Value receivers copy the struct and cannot change the original. Use pointer receivers when methods mutate state or for consistency across all methods on a type.
What is struct embedding in Go?
Embedding one struct inside another anonymously promotes its fields and methods to the outer type — Go’s composition pattern instead of class inheritance.
Want live Go or backend classes? Join Alkademy for instructor-led programming courses with hands-on projects.
[…] Previous: Lesson 8: Go Structs and Methods – Organizing Data and Behavior […]