Structs & Methods
Composition over inheritance — the Go way.
Structs & Methods
Go has no classes. The combination of struct + method is the closest equivalent — and it composes more cleanly than inheritance ever did.
Defining a struct
type User struct {
Name string
Email string
Age int
}
Field names follow the same export rule as identifiers: Name is visible from other packages, name would not be.
Creating instances
// Positional — fragile, never use except for tiny tuples.
u1 := User{"Alice", "alice@x.com", 30}
// Named fields — preferred. Order doesn't matter; missing fields
// take their zero value.
u2 := User{Name: "Alice", Email: "alice@x.com"}
u2.Age // 0 — zero value
// Just zero-value everything.
var u3 User // {Name: "", Email: "", Age: 0}
// Pointer to a new value.
u4 := &User{Name: "Bob"}
new(User) and &User{} both produce a *User pointing at a zero-valued struct. The &User{...} form is more common because you can set initial fields.
Methods
A method is a function with a receiver declared between func and the name:
func (u User) DisplayName() string {
return strings.Title(u.Name)
}
alice := User{Name: "alice"}
alice.DisplayName() // "Alice"
This is a value receiver — u is a copy. Mutations don't affect the caller's struct.
For mutating methods (and large structs), use a pointer receiver:
func (u *User) SetEmail(email string) {
u.Email = email
}
alice := &User{Name: "Alice"}
alice.SetEmail("a@x.com") // mutates the original
Go automatically takes the address when you call a pointer-receiver method on an addressable value:
alice := User{Name: "Alice"} // value, not pointer
alice.SetEmail("a@x.com") // Go calls (&alice).SetEmail(...)
Pointer or value?
Three rules:
- The method needs to mutate the receiver? Pointer receiver.
- The receiver is large (more than a few words)? Pointer receiver to skip the copy.
- All other methods on the same type use a pointer receiver? Match them for consistency.
Otherwise value receivers are fine. They're cheaper for small types, and they make the type usable as a map key.
Embedding (Go's "inheritance")
Embed a type to "promote" its fields and methods:
type Timestamped struct {
CreatedAt time.Time
UpdatedAt time.Time
}
type User struct {
Timestamped // embedded — no field name
Name string
Email string
}
u := User{
Timestamped: Timestamped{CreatedAt: time.Now()},
Name: "Alice",
}
u.CreatedAt // works — promoted from Timestamped
u.Timestamped.CreatedAt // also works — explicit access
Embedding gives you composition with sugar, not classical inheritance. There's no super(); if User defines its own String() method it simply hides the embedded one. To call the embedded method explicitly: u.Timestamped.String().
Constructors
Go has no __init__ / constructor. The convention is a New<Type> function:
func NewUser(name, email string) (*User, error) {
if !strings.Contains(email, "@") {
return nil, errors.New("invalid email")
}
return &User{
Timestamped: Timestamped{CreatedAt: time.Now()},
Name: name,
Email: email,
}, nil
}
Returning (value, error) lets the constructor reject bad inputs without panicking.
Tags
Struct fields can carry "tags" — string metadata that libraries (json, db, validate) read at runtime:
type User struct {
Name string `json:"name" db:"user_name"`
Email string `json:"email,omitempty" db:"email"`
Age int `json:"age,omitempty" db:"age"`
}
encoding/json reads the json: tag to know which JSON key to map to. ORM libraries do the same with their own tag prefix. The string format is unstructured — packages parse it however they like.
Comparing structs
Two structs are == if their fields are equal pairwise — but only if all fields are comparable types (no slices, maps, or functions inside):
type Point struct {
X, Y int
}
Point{1, 2} == Point{1, 2} // true
type Bag struct {
Items []string
}
Bag{[]string{"x"}} == Bag{[]string{"x"}} // compile error: slices not comparable
For deep comparisons, use reflect.DeepEqual (or, in tests, cmp.Diff).