Structs & Methods
📋 Jump to TakeawaysGo doesn't have classes. No inheritance, no constructors, no this. Instead, you get structs and methods, and somehow that's all you need. Docker, Kubernetes, and Terraform are all built with just structs, methods, and interfaces. No class hierarchies in sight.
Defining Structs
A struct groups related fields together.
type User struct {
Name string
Age int
}Creating Instances
There are several ways to create a struct.
// Struct literal — set fields inline
u1 := User{Name: "Alice", Age: 30}
// Zero value — fields default to their zero values
var u2 User
fmt.Println(u2.Name) // "" (empty string)
fmt.Println(u2.Age) // 0
// Pointer with fields — most common way to get a pointer
u3 := &User{Name: "Bob", Age: 25}new()
new(T) allocates a zero-value T and returns a pointer to it. It works with any type, not just structs.
p := new(int) // *int pointing to 0
fmt.Println(*p) // 0
u := new(User) // *User with all fields at zero values
fmt.Println(u.Name) // ""In practice, &User{...} is preferred for structs because you can set fields inline. new is more useful when you just need a pointer to a zero value, especially with basic types like int or bool where you can't write &0.
Accessing Fields
Use dot notation to read and write fields.
u := User{Name: "Alice", Age: 30}
fmt.Println(u.Name) // Alice
u.Age = 31
fmt.Println(u.Age) // 31Exported vs Unexported Fields
The same uppercase/lowercase visibility rule from Getting Started applies to struct fields.
type Config struct {
Host string // exported — accessible from other packages
port int // unexported — only accessible within this package
}Methods with Value Receivers
A method is a function with a receiver. Value receivers get a copy of the struct.
func (u User) Greet() string {
return "Hi, I'm " + u.Name
}
u := User{Name: "Alice"}
fmt.Println(u.Greet()) // Hi, I'm AlicePointer Receivers
Pointer receivers can modify the original struct. Use them when you need to mutate state or avoid copying large structs.
func (u *User) SetName(name string) {
u.Name = name
}
u := User{Name: "Alice"}
u.SetName("Bob")
fmt.Println(u.Name) // BobWith a value receiver, changes don't persist:
func (u User) SetAge(age int) {
u.Age = age // modifies the copy, not the original
}
u := User{Age: 30}
u.SetAge(99)
fmt.Println(u.Age) // 30 — unchangedPointers
& gets the address of a variable. * dereferences a pointer to access the value.
x := 42
p := &x // p is a pointer to x
fmt.Println(*p) // 42
*p = 100
fmt.Println(x) // 100 — x changed through the pointerThis is why pointer receivers can modify the struct. They receive the address, not a copy.
Struct Embedding
Go uses composition instead of inheritance. Embed a struct to promote its fields and methods.
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + " speaks"
}
type Dog struct {
Animal // embedded
Breed string
}
d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Lab"}
fmt.Println(d.Name) // Rex — promoted field
fmt.Println(d.Speak()) // Rex speaks — promoted methodStruct Tags
Tags attach metadata to fields. The json tag controls JSON encoding/decoding.
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
Age int `json:"-"` // excluded from JSON
}
u := User{Name: "Alice", Age: 30}
data, _ := json.Marshal(u)
fmt.Println(string(data)) // {"name":"Alice"}Key Takeaways
- Structs group related data; create them with literals,
new(), or zero values - Uppercase fields are exported, lowercase are unexported
- Value receivers get a copy; pointer receivers can modify the original
&takes an address,*dereferences — pointer receivers need this to mutate- Embedding promotes fields and methods for composition
- Struct tags control serialization behavior like JSON field names