Structs & Methods
A struct groups related values under one name: a student has a name, a city and marks; an order has an ID, items and a total. Go has no classes. Instead, you define a struct for the data and attach methods (functions that belong to a type) to it. Together they do the job that classes do in Java or Python, with less ceremony.
In this lesson you will define structs, create and compare them, add methods with value and pointer receivers, write constructor functions, give types a readable String form, and reuse code by embedding one struct in another.
Defining and creating structs
A struct type lists its fields, each with a name and a type. Field names that start with a capital letter are exported, like any other Go name.
package main
import "fmt"
type Student struct {
Name string
City string
Marks int
}
func main() {
a := Student{Name: "Aarav", City: "Rohtak", Marks: 88}
b := Student{Name: "Isha"} // missing fields get zero values
var empty Student
a.Marks += 2
fmt.Println(a.Name, a.Marks)
fmt.Printf("%+v\n", b)
fmt.Printf("%+v\n", empty)
fmt.Println(a == Student{Name: "Aarav", City: "Rohtak", Marks: 90})
}Aarav 90
{Name:Isha City: Marks:0}
{Name: City: Marks:0}
trueAlways write field names in literals (Name: "Aarav"). The short form Student{"Aarav", "Rohtak", 88} also works, but it breaks as soon as someone adds or reorders a field. Two structs are equal with == when all their fields are equal, as long as every field type is comparable (slices and maps are not).
Assigning a struct copies all of its fields, just like an array. To share one struct between parts of a program, use a pointer, as you saw in Pointers.
Methods and receivers
A method is a function with a receiver: a parameter written before the method name. There are two kinds:
- A value receiver
(w Wallet)gets a copy. Use it for methods that only read. - A pointer receiver
(w *Wallet)gets the address. Use it for methods that change the struct, or when the struct is large.
package main
import "fmt"
type Wallet struct {
Owner string
Balance int
}
func (w Wallet) CanPay(amount int) bool {
return w.Balance >= amount
}
func (w *Wallet) AddMoney(amount int) {
w.Balance += amount
}
func (w *Wallet) Pay(amount int) error {
if !w.CanPay(amount) {
return fmt.Errorf("%s: balance %d is less than %d", w.Owner, w.Balance, amount)
}
w.Balance -= amount
return nil
}
func main() {
w := Wallet{Owner: "Priya", Balance: 500}
w.AddMoney(250)
if err := w.Pay(600); err != nil {
fmt.Println(err)
}
if err := w.Pay(300); err != nil {
fmt.Println(err)
}
fmt.Println("Left:", w.Balance)
}Priya: balance 150 is less than 300 Left: 150
You call w.AddMoney(250) on a plain variable, and Go automatically passes &w because the method needs a pointer. If AddMoney had a value receiver, it would add to a copy and the wallet would never change, a very common beginner bug. Many teams follow a simple rule: if any method of a type needs a pointer receiver, give all its methods pointer receivers, for consistency. The example mixes both kinds only to show the difference.
Constructors and the String method
Go has no constructors built into the language. By convention you write a function named NewTypeName that returns a ready-to-use value, usually a pointer. A type can also choose how it prints by having a method String() string; fmt uses it automatically for %v and Println.
package main
import "fmt"
type Order struct {
ID int
Items []string
Total int
}
func NewOrder(id int) *Order {
return &Order{ID: id}
}
func (o *Order) Add(item string, price int) {
o.Items = append(o.Items, item)
o.Total += price
}
func (o *Order) String() string {
return fmt.Sprintf("Order #%d: %d items, ₹%d", o.ID, len(o.Items), o.Total)
}
func main() {
o := NewOrder(1042)
o.Add("Paneer tikka", 280)
o.Add("Butter naan", 60)
fmt.Println(o)
}Order #1042: 2 items, ₹340
Because String has a pointer receiver here, fmt uses it when you print a *Order (which o is). Printing a plain Order value would show the default {1042 [Paneer tikka Butter naan] 340} instead.
Methods on your own named types
Methods are not limited to structs. Any named type you define can have them. This is how you finish the iota enumerations from Variables & Constants: give the constants their own type and a String method.
package main
import "fmt"
type Status int
const (
Pending Status = iota
Shipped
Delivered
)
func (s Status) String() string {
switch s {
case Pending:
return "Pending"
case Shipped:
return "Shipped"
case Delivered:
return "Delivered"
}
return fmt.Sprintf("Status(%d)", int(s))
}
func main() {
s := Shipped
fmt.Println(s, Delivered, Status(7))
fmt.Printf("%v %d\n", s, s)
}Shipped Delivered Status(7) Shipped 1
The separate type Status stops you from passing a random int where a status is expected by accident, and the String method makes logs readable. %d still prints the number. Inside String, converting with int(s) avoids calling String again by mistake.
Embedding: reuse without inheritance
Go has no inheritance. Instead, a struct can embed another type by listing it without a field name. The inner type's fields and methods are promoted: you can use them as if they belonged to the outer struct.
package main
import "fmt"
type Person struct {
Name string
City string
}
func (p Person) Greeting() string {
return "Namaste, I am " + p.Name + " from " + p.City
}
type Employee struct {
Person
EmpID string
Salary int
}
func main() {
e := Employee{
Person: Person{Name: "Kabir", City: "Pune"},
EmpID: "EMP-07",
}
fmt.Println(e.Name, e.EmpID)
fmt.Println(e.Greeting())
fmt.Printf("%+v\n", e)
}Kabir EMP-07
Namaste, I am Kabir from Pune
{Person:{Name:Kabir City:Pune} EmpID:EMP-07 Salary:0}e.Name is short for e.Person.Name. An Employee has a Person inside it; it is not a kind of Person, so you cannot pass an Employee where a Person is required. This "composition over inheritance" keeps Go types simple.
Common mistakes
- Value receiver on a method that changes the struct. The change is lost; use a pointer receiver.
- Positional literals.
Student{"Aarav", "Rohtak", 88}breaks when fields change; use field names. - Lower-case field names in data you share. Unexported fields are invisible to other packages, including
encoding/json. - Expecting embedding to work like inheritance. An outer type does not become the inner type.
Key points
type Name struct { ... }groups fields; literals with field names set them, the rest get zero values.- Methods have receivers: value receivers read a copy, pointer receivers can change the original.
- Write
NewXfunctions as constructors; aString() stringmethod controls howfmtprints a type. - Any named type, such as
type Status int, can have methods. - Embedding promotes fields and methods for reuse without inheritance.
Exercise
Create a Book struct for a Jaipur library with Title, Author and Copies fields. Add a pointer-receiver method Borrow() error that reduces Copies by one, or returns an error when no copies are left, and a String() method that prints like Godan by Premchand (1 left). Borrow the book three times when it starts with 2 copies, printing the book or the error after each try.
Show solution
package main
import "fmt"
type Book struct {
Title string
Author string
Copies int
}
func (b *Book) Borrow() error {
if b.Copies == 0 {
return fmt.Errorf("%q: no copies left", b.Title)
}
b.Copies--
return nil
}
func (b *Book) String() string {
return fmt.Sprintf("%s by %s (%d left)", b.Title, b.Author, b.Copies)
}
func main() {
book := &Book{Title: "Godan", Author: "Premchand", Copies: 2}
for range 3 {
if err := book.Borrow(); err != nil {
fmt.Println("Error:", err)
continue
}
fmt.Println(book)
}
}Godan by Premchand (1 left) Godan by Premchand (0 left) Error: "Godan": no copies left
Borrow needs a pointer receiver because it changes Copies. for range 3 repeats three times without a loop variable.
Quick Quiz
1. Which receiver should a method that changes a struct's field use?
A value receiver works on a copy, so only a pointer receiver can change the original.
2. What does embedding Person inside Employee give you?
Embedding is composition: the inner type's fields and methods are reachable directly from the outer type.
3. What happens to fields you leave out of a struct literal like Student{Name: "Isha"}?
Omitted fields are set to zero values.
4. Which method makes fmt.Println print a type in your own format?
fmt checks for the Stringer interface, which is a String() string method.
Learn Go with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
