A struct is a composite type that groups together zero or more named fields of any type. Go has no class keyword; instead, a method is declared as an ordinary function with an extra receiver parameter before its name, which binds that function to a specific named type. The receiver can be a value (func (p Point) Method()) or a pointer (func (p *Point) Method()), and the choice changes whether the method can mutate the original data.
Declaring and using structs
A struct groups named fields under one type. Struct values can be created with field names (recommended, order-independent) or positionally (order-dependent and brittle if the struct changes), and fields are accessed with dot notation.
type Point struct {
X, Y int
}
p1 := Point{X: 3, Y: 4} // named fields — preferred
p2 := Point{3, 4} // positional — works, but brittle
p1.X = 10 // dot notation to access/set fields
fmt.Println(p1.X, p1.Y) // 10 4Methods: functions with a receiver
A method looks like a regular function, but names a receiver type in parentheses before the function name. That receiver is how Go attaches behavior to a type without a class hierarchy — any named type, not just structs, can have methods.
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 { // value receiver
return r.Width * r.Height
}
rect := Rectangle{Width: 3, Height: 4}
fmt.Println(rect.Area()) // 12Value receivers vs. pointer receivers
This is the detail that trips up newcomers most: a method with a value receiver operates on a copy of the struct, so changes inside the method never affect the original. A method with a pointer receiver (*Rectangle) operates on the original data, so mutations are visible to the caller. Go automatically takes the address when you call a pointer-receiver method on an addressable value, so the call syntax looks identical either way — the difference is entirely in the method's declaration, not the call site. The idiomatic rule: if any method on a type needs a pointer receiver (to mutate, or to avoid copying a large struct), make all of that type's methods use pointer receivers, for consistency.
func (r Rectangle) Scale(factor float64) { // value receiver: no effect outside
r.Width *= factor
}
func (r *Rectangle) ScalePtr(factor float64) { // pointer receiver: mutates original
r.Width *= factor
}
rect := Rectangle{Width: 3, Height: 4}
rect.Scale(2)
fmt.Println(rect.Width) // still 3 — Scale worked on a copy
rect.ScalePtr(2)
fmt.Println(rect.Width) // 6 — ScalePtr mutated the original