Go Basics #6 Structs and Methods
In #5 Collections you saw the tools for grouping data. This time — we’re making types that carry their own meaning: structs and methods.
Struct — user-defined types #
type User struct {
ID string
Name string
Age int
}
func main() {
u := User{
ID: "u1",
Name: "Curtis",
Age: 30,
}
fmt.Println(u) // {u1 Curtis 30}
fmt.Println(u.Name) // Curtis
}Define with type Name struct { fields }. Each field gets its type.
Several ways to create #
// named fields (recommended)
u1 := User{ID: "u1", Name: "Curtis", Age: 30}
// some fields only (rest are zero values)
u2 := User{Name: "Alice"}
// positional (not recommended — breaks if you reorder fields)
u3 := User{"u3", "Bob", 35}
// zero value
var u4 User // { 0}
// pointer via new
u5 := new(User) // *User, all fields zeroThe named-field form is the standard. Positional is fragile when fields are added or reordered.
Field access — dot notation #
u := User{Name: "Curtis", Age: 30}
fmt.Println(u.Name)
u.Age = 31You also use dot notation through pointers — Go automatically dereferences.
p := &u
fmt.Println(p.Name) // OK — it's (*p).Name but auto-dereferenced
p.Age = 32 // OKMethods — functions bound to a type #
type User struct {
Name string
Age int
}
func (u User) Greet() {
fmt.Printf("hi, %s (%d)\n", u.Name, u.Age)
}
func main() {
u := User{Name: "Curtis", Age: 30}
u.Greet() // hi, Curtis (30)
}The (u User) part of func (u User) Greet() is the receiver. This function is a method bound to the User type.
The receiver name is usually the lowercase first letter of the type (u for User). Go convention is to spell out the receiver each time, not use this/self.
Value receiver vs pointer receiver #
This is the most confusing part at first.
Value receiver — receives a copy #
func (u User) IncrementAge() {
u.Age++ // increments the copy's Age — original unchanged
}
u := User{Age: 30}
u.IncrementAge()
fmt.Println(u.Age) // 30 (no change)A value receiver copies the whole struct on call. Changes inside the method don’t affect the original.
Pointer receiver — points at the original #
func (u *User) IncrementAge() {
u.Age++ // increments the original's Age
}
u := User{Age: 30}
u.IncrementAge()
fmt.Println(u.Age) // 31A *User receiver takes a pointer — changes inside the method are reflected on the original.
Which to use? #
Guidelines:
- If you need to mutate → pointer receiver
- If the struct is large → pointer receiver (avoid the copy cost)
- Otherwise → value receiver
In practice the most-used guideline is — stay consistent within a type. Mixing pointer receivers and value receivers on the same type feels off, so typically you unify all methods on a type to one kind.
Auto-deref / auto-pointer in Go #
The caller barely thinks about either of these.
u := User{Age: 30}
p := &u
u.PointerMethod() // automatically (&u).PointerMethod()
p.ValueMethod() // automatically (*p).ValueMethod()Whether the caller has a value or a pointer, the Go compiler does the conversion. Just note that to mutate, the caller must hold an addressable value (a transient location like a slice element may not be addressable).
Constructors — the New... convention
#
Go has no class or explicit constructor. Instead, constructor functions are made by convention.
type User struct {
ID string
Name string
Age int
}
func NewUser(name string, age int) *User {
return &User{
ID: generateID(),
Name: name,
Age: age,
}
}
func main() {
u := NewUser("Curtis", 30)
fmt.Println(u.Name)
}NewUser, NewClient — New<TypeName> is the standard name. Usually returns a pointer.
Embedding — reuse via composition #
Go has no class inheritance, but you can get a similar effect with embedding.
type Animal struct {
Name string
}
func (a Animal) Speak() {
fmt.Println(a.Name, "makes a sound")
}
type Dog struct {
Animal // embedded — type only, no field name
Breed string
}
func main() {
d := Dog{
Animal: Animal{Name: "Bori"},
Breed: "Shiba",
}
d.Speak() // can call Animal's method directly
fmt.Println(d.Name) // direct access to Name on Dog
}The Animal inside Dog is embedded — written as the type only, with no field name. This means:
- You can call
Animal’s method (Speak) directly onDog - You can access
Animal’s field (Name) directly asd.Name
It’s not inheritance but promotion — from the outside it looks like Dog directly has those methods/fields.
Method override #
If you define a method with the same name on Dog, Dog’s method hides the promoted one.
func (d Dog) Speak() {
fmt.Println(d.Name, "barks (woof woof)")
}
d := Dog{Animal: Animal{Name: "Bori"}, Breed: "Shiba"}
d.Speak() // Bori barks (woof woof)
// To call Animal's method explicitly
d.Animal.Speak()Similar to JS’s super.method — d.Animal.Speak() lets you call the (sort of) parent’s method directly.
Embedding vs inheritance — the difference #
Different from traditional OOP inheritance in these ways:
- Dog has an Animal, not is an Animal
- Multiple embedding is allowed (
type X struct { A; B }) - It’s not an “is-a” relation but “has-a” + method promotion
One of Go’s design philosophies — composition over inheritance. Deep class hierarchies are intentionally blocked.
Struct comparison #
Two structs of the same type are == true when every field matches.
u1 := User{Name: "Curtis", Age: 30}
u2 := User{Name: "Curtis", Age: 30}
u3 := User{Name: "Alice", Age: 25}
u1 == u2 // true
u1 == u3 // falseBut — a struct that has slice/map/function fields is not comparable (compile error). For those cases, use reflect.DeepEqual or your own Equal method.
Anonymous struct #
A struct made on the spot without a name.
config := struct {
Host string
Port int
}{
Host: "localhost",
Port: 8080,
}
fmt.Println(config.Host, config.Port)Suitable for test cases or one-off data groupings. For data you’ll reuse, give it a name with type.
Tags — metadata #
You can attach tags (metadata) to struct fields. They are central to JSON serialization and DB mapping.
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Age int `json:"age,omitempty"`
}The encoding/json package reads these tags — mapping Go field names (ID) to JSON keys (id). omitempty says to skip the field in the result when it’s the zero value.
Detailed in Intermediate #7 Standard Library Tour and Practice #3 JSON.
Common patterns #
1) Builder pattern — method chaining #
type Query struct {
table string
limit int
where []string
}
func NewQuery() *Query {
return &Query{}
}
func (q *Query) Table(t string) *Query {
q.table = t
return q
}
func (q *Query) Where(w string) *Query {
q.where = append(q.where, w)
return q
}
func (q *Query) Limit(n int) *Query {
q.limit = n
return q
}
// usage
q := NewQuery().
Table("users").
Where("active = true").
Limit(10)Each method returns itself, enabling chaining. A common shape in library APIs.
2) Options pattern — functional options #
When a constructor needs many options.
type Server struct {
host string
port int
timeout time.Duration
}
type Option func(*Server)
func WithHost(host string) Option {
return func(s *Server) { s.host = host }
}
func WithPort(port int) Option {
return func(s *Server) { s.port = port }
}
func WithTimeout(d time.Duration) Option {
return func(s *Server) { s.timeout = d }
}
func NewServer(opts ...Option) *Server {
s := &Server{
host: "localhost",
port: 8080,
timeout: 30 * time.Second,
}
for _, opt := range opts {
opt(s)
}
return s
}
// usage
srv := NewServer(
WithPort(3000),
WithTimeout(60 * time.Second),
)Option functions gather configuration. Nearly standard in Go libraries.
Wrap-up #
What we covered:
type X struct { ... }for user-defined types- Instances — named fields are recommended
- Methods —
func (r Type) Method()form - Value vs pointer receivers — pointer when you need to mutate or the struct is large
- Keep methods on a type uniform in receiver kind
New...convention for constructor-like functions- Embedding for composition + method promotion — not inheritance
- Same-type structs support
==(when fields are comparable) - Tags for metadata — used in JSON/DB mapping
- Builder pattern, functional options pattern
In the next post (#7 Packages and Modules) — the final basics post — we cover splitting code across files/packages and managing external dependencies with go mod.