Structs in Go
Define structs, use struct literals and embedding, add methods, work with struct tags, and understand anonymous fields.
Defining Structs
A struct is Go’s primary tool for grouping related data into a named type. Where other languages use classes to bundle data and behavior, Go separates the two: a struct holds data, and methods are attached to it separately. This makes the data layout explicit and the relationship between types easy to follow without navigating a class hierarchy.
type Person struct {
Name string
Age int
Email string
}
Struct Literals
Struct literals create a value of the struct type. The named-field form is strongly preferred — it is resilient to field reordering, self-documenting, and clearly shows which fields are being set. The positional form is fragile and should be avoided except in test code with very short structs. Any field not specified in the literal gets its zero value automatically.
// Named fields — preferred: readable and order-independent
alice := Person{
Name: "Alice",
Age: 30,
Email: "[email protected]",
}
// Positional — fragile: breaks silently if fields are reordered
bob := Person{"Bob", 25, "[email protected]"}
// Zero-value struct — all fields are zero-initialized
var empty Person
fmt.Println(empty) // { 0 }
// Pointer to struct — Go auto-dereferences pointers to structs, so p.Name works directly
p := &Person{Name: "Charlie", Age: 35}
fmt.Println(p.Name) // Charlie
Methods on Structs
A method is a function with a receiver — an extra parameter that binds the function to a specific type. The receiver can be a value (receives a copy) or a pointer (receives the actual struct). The key rule is: use a pointer receiver when the method needs to modify the struct or when the struct is large enough that copying it is expensive.
type Rectangle struct {
Width float64
Height float64
}
// Value receiver — works on a copy; the original is not modified
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// Pointer receiver — modifies the actual struct, not a copy
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area()) // 50
fmt.Println(rect.Perimeter()) // 30
rect.Scale(2)
fmt.Println(rect.Area()) // 200 — modified in place via pointer receiver
}
Rule of thumb: Use pointer receivers when the method modifies the struct or when the struct is large (avoids copying). Be consistent — if any method uses a pointer receiver, all methods should.
Constructor Functions
Go has no special constructor syntax, but the community convention is to write a New... function that returns a properly initialized value. This is better than bare struct literals for exported types because it lets you enforce invariants, set defaults, and change the internal representation later without breaking callers.
type Server struct {
host string
port int
timeout time.Duration
}
// NewServer is the conventional constructor — sets defaults and validates input
func NewServer(host string, port int) *Server {
return &Server{
host: host,
port: port,
timeout: 30 * time.Second, // default that callers don't need to specify
}
}
func (s *Server) Addr() string {
return fmt.Sprintf("%s:%d", s.host, s.port)
}
s := NewServer("localhost", 8080)
fmt.Println(s.Addr()) // localhost:8080
Struct Embedding
Embedding allows one struct to include another as an anonymous field. The embedded struct’s fields and methods are promoted to the outer struct — you can access them as if they were defined directly on the outer type. This is Go’s primary mechanism for code reuse and achieves most of what inheritance does in other languages, without the tight coupling.
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + " makes a sound"
}
type Dog struct {
Animal // embedded — no field name, just the type
Breed string
}
// Dog can override the promoted method with its own implementation
func (d Dog) Speak() string {
return d.Name + " says: Woof!"
}
d := Dog{
Animal: Animal{Name: "Rex"},
Breed: "Labrador",
}
fmt.Println(d.Name) // Rex — promoted from Animal, accessible directly
fmt.Println(d.Speak()) // Rex says: Woof! — Dog's method takes precedence
// The embedded struct is still accessible explicitly when needed
fmt.Println(d.Animal.Speak()) // Rex makes a sound
Anonymous Fields
Embedding is not limited to structs you define — you can embed any named type. A common pattern is embedding a shared “mixin” struct to add common fields (like timestamps or audit info) to multiple domain types without duplicating the field declarations.
type Timestamps struct {
CreatedAt time.Time
UpdatedAt time.Time
}
type User struct {
ID int
Email string
Timestamps // embedded — User now has CreatedAt and UpdatedAt directly
}
u := User{
ID: 1,
Email: "[email protected]",
Timestamps: Timestamps{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
}
fmt.Println(u.CreatedAt) // accessible directly, no need for u.Timestamps.CreatedAt
Struct Tags
Struct tags are raw string literals in backticks that attach metadata to fields. They are invisible to the compiler but readable at runtime via the reflect package. The encoding/json package is the most common consumer of tags — it uses them to control how fields are named in JSON output, whether to omit empty fields, and whether to exclude a field entirely.
type Article struct {
ID int `json:"id"`
Title string `json:"title"`
Body string `json:"body,omitempty"` // omit from JSON output if empty
CreatedAt time.Time `json:"created_at"`
Internal string `json:"-"` // always exclude from JSON
}
article := Article{
ID: 1,
Title: "Go Structs",
CreatedAt: time.Now(),
Internal: "admin-only",
}
data, _ := json.Marshal(article)
fmt.Println(string(data))
// {"id":1,"title":"Go Structs","created_at":"2024-01-15T10:00:00Z"}
// Body is omitted (empty string), Internal is omitted (tagged with "-")
// Multiple tags let different packages each read their own metadata
type DBRecord struct {
ID int `json:"id" db:"id" validate:"required"`
Name string `json:"name" db:"name" validate:"min=2,max=100"`
}
Comparing Structs
Structs are comparable with == and != if all their fields are comparable types. Two struct values are equal when all their corresponding fields are equal. Structs that contain slices, maps, or functions are not comparable — attempting to compare them is a compile error.
type Point struct{ X, Y int }
p1 := Point{1, 2}
p2 := Point{1, 2}
p3 := Point{3, 4}
fmt.Println(p1 == p2) // true — all fields match
fmt.Println(p1 == p3) // false — fields differ
// Structs with non-comparable fields cannot be compared with ==
type Container struct {
Items []int // slices are not comparable
}
// c1 == c2 would be a compile error
Practical Example — Domain Model
This example shows how structs, embedding, tags, and methods combine in a realistic domain model. The Customer type embeds Address directly, uses JSON tags for serialization, and keeps some fields unexported to enforce encapsulation.
package main
import (
"encoding/json"
"fmt"
"time"
)
type Address struct {
Street string `json:"street"`
City string `json:"city"`
Zip string `json:"zip"`
}
type Customer struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Address Address `json:"address"`
CreatedAt time.Time `json:"created_at"`
tags []string // unexported — not included in JSON output
}
func NewCustomer(id int, name, email string) *Customer {
return &Customer{
ID: id,
Name: name,
Email: email,
CreatedAt: time.Now(),
}
}
func (c *Customer) AddTag(tag string) {
c.tags = append(c.tags, tag)
}
func (c *Customer) HasTag(tag string) bool {
for _, t := range c.tags {
if t == tag {
return true
}
}
return false
}
// String implements fmt.Stringer — controls how the customer appears in print statements
func (c Customer) String() string {
return fmt.Sprintf("Customer{ID:%d, Name:%s}", c.ID, c.Name)
}
func main() {
cust := NewCustomer(1, "Alice", "[email protected]")
cust.Address = Address{
Street: "123 Gopher Lane",
City: "Go City",
Zip: "12345",
}
cust.AddTag("premium")
cust.AddTag("verified")
fmt.Println(cust) // Customer{ID:1, Name:Alice}
fmt.Println(cust.HasTag("premium")) // true
data, _ := json.MarshalIndent(cust, "", " ")
fmt.Println(string(data))
}