Structs and Methods in Go: Value vs Pointer Receivers
Model data with structs, attach behaviour with methods, choose between value and pointer receivers, and compose types with embedding.
On this page
Go has no classes. Instead, you group data with structs and attach behaviour with methods. Together they give you everything you need to model a domain, without inheritance hierarchies.
Declaring and creating structs
type User struct {
Name string
Dept string
Age int
}
sabbir := User{Name: "Sabbir", Dept: "CSE", Age: 23} // named fields (preferred)
motiur := User{"Motiur", "EEE", 22} // positional: fragile, avoid
var empty User // zero value: {"" "" 0}
fmt.Println(motiur.Dept) // EEE
empty.Name = "John" // fields are assignableAlways use named fields in literals. Positional literals break silently when someone adds or reorders a field.
Printing structs
The %+v verb prints field names, which is great for debugging:
fmt.Printf("%v\n", sabbir) // {Sabbir CSE 23}
fmt.Printf("%+v\n", sabbir) // {Name:Sabbir Dept:CSE Age:23}
fmt.Printf("%#v\n", sabbir) // main.User{Name:"Sabbir", Dept:"CSE", Age:23}Exported vs unexported
Capitalisation controls visibility in Go. Name (uppercase) is exported and visible to other packages; age (lowercase) is unexported and private to its package. The same rule applies to types, functions and methods. Packages like encoding/json can only see exported fields, which is a common surprise.
Nested structs
A struct field can itself be a struct:
type ContactInfo struct {
Email string
Phone string
ZipCode int
}
type Person struct {
FirstName string
LastName string
Contact ContactInfo
}
p := Person{
FirstName: "Jane",
LastName: "Smith",
Contact: ContactInfo{
Email: "jane@example.com",
Phone: "123-456-7890",
},
}
fmt.Println(p.Contact.Email)Methods: functions with a receiver
A method is a function with a receiver parameter written before its name. You can declare methods on any type defined in your package, not only structs.
func (u User) Describe() string {
return fmt.Sprintf("%s (%s, %d)", u.Name, u.Dept, u.Age)
}
fmt.Println(sabbir.Describe()) // Sabbir (CSE, 23)Value receivers vs pointer receivers
This is the most important decision when writing methods.
func (p Person) Print() { // value receiver: works on a copy
fmt.Printf("%+v\n", p)
}
func (p *Person) UpdateName(name string) { // pointer receiver: changes the original
p.FirstName = name // Go auto-dereferences: same as (*p).FirstName
}
p.UpdateName("Sabbir") // Go takes &p for you
p.Print() // {FirstName:Sabbir ...}Use a pointer receiver when:
sync.Mutex.Use a value receiver for small, immutable-style types. And be consistent: if any method on a type needs a pointer receiver, give them all pointer receivers.
p, calling p.UpdateName(...) works because Go automatically uses &p. But a plain Person value does not satisfy an interface whose methods use pointer receivers; only *Person does. We will see why that matters in the interfaces article.Constructors are just functions
Go has no constructors. By convention you write a NewX function that returns a ready-to-use value, validating inputs and initialising maps or defaults:
func NewUser(name, dept string, age int) (*User, error) {
if name == "" {
return nil, errors.New("name is required")
}
return &User{Name: name, Dept: dept, Age: age}, nil
}Composition with embedding
Instead of inheritance, Go embeds one type in another. The embedded type's fields and methods are promoted to the outer type:
type Timestamps struct {
CreatedAt time.Time
UpdatedAt time.Time
}
func (t *Timestamps) Touch() { t.UpdatedAt = time.Now() }
type Article struct {
Title string
Timestamps // embedded: no field name
}
a := Article{Title: "Go structs"}
a.Touch() // promoted method
fmt.Println(a.UpdatedAt) // promoted fieldThis is composition: Article has timestamps, it is not a subtype of them. It keeps code flat and avoids the fragile base-class problem.
Comparing structs
Structs are comparable with == if all their fields are comparable. User{"A", "B", 1} == User{"A", "B", 1} is true. Structs containing slices or maps cannot be compared with ==; use a method or reflect.DeepEqual in tests.
Key takeaways
%+v for debugging.Last updated on