Structs and Methods in Go: Value vs Pointer Receivers
Chapter 01 of 06

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.

Sabbir Ahmmed3 min read0
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

go
1
2
3
4
5
6
7
8
9
10
11
12
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 assignable

Always 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:

go
1
2
3
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:

go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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.

go
1
2
3
4
5
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.

go
1
2
3
4
5
6
7
8
9
10
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:

the method needs to modify the receiver;
the struct is large and copying it on every call is wasteful;
the struct contains something that must not be copied, such as a 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.

Note
With a value 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:

go
1
2
3
4
5
6
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:

go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 field

This 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

Structs group data; methods attach behaviour; there are no classes.
Use named fields in struct literals and %+v for debugging.
Uppercase names are exported; lowercase are package-private.
Choose pointer receivers to mutate, avoid big copies, or hold a mutex. Stay consistent.
Compose types by embedding instead of inheriting.
#Golang#Structs#Methods#OOP

Last updated on

Discussion0