On this page
Structs and Methods
Structs: Go's Way of Modeling Data
Go has no classes. Instead, it uses structs to group related data, and methods to associate behavior with that data. This model is simpler than Java's or C#'s class inheritance, but equally powerful when combined with interfaces and embedding.
If you know C or Rust, Go's structs will feel familiar. If you come from Java or Python, think of a struct as a class — but without inheritance.
Defining Structs
// Basic definition
type Person struct {
Name string
LastName string
Age int
Email string
}
// Nested struct
type Address struct {
Street string
City string
Country string
}
type Employee struct {
Person // embedding (composition)
Company string
Salary float64
Address Address // named field (not embedding)
}Names starting with an uppercase letter are exported (visible outside the package). Names starting with a lowercase letter are unexported (only visible within the same package).
Creating Struct Instances
// 1. Named field literal (recommended)
p := Person{
Name: "Ana",
LastName: "Garcia",
Age: 28,
Email: "[email protected]",
}
// 2. Positional fields (fragile, avoid)
p2 := Person{"Luis", "Perez", 35, "[email protected]"}
// 3. Zero value — all fields initialized to their zero value
var p3 Person // Name:"", LastName:"", Age:0, Email:""
// 4. Pointer to struct
pp := &Person{Name: "Maria", Age: 22}
pp.Email = "[email protected]" // Go dereferences automaticallyConstructors: Factory Functions
Go has no special constructor syntax. The idiomatic pattern is to create New... functions that validate, initialize, and return the struct:
type Rectangle struct {
Width float64
Height float64
Color string
}
// Constructor with validation
func NewRectangle(width, height float64, color string) (*Rectangle, error) {
if width <= 0 || height <= 0 {
return nil, fmt.Errorf("invalid dimensions: width=%.2f, height=%.2f", width, height)
}
return &Rectangle{
Width: width,
Height: height,
Color: color,
}, nil
}
// Usage
r, err := NewRectangle(5, 3, "blue")
if err != nil {
fmt.Println("Error:", err)
return
}Methods: Behavior Associated with Structs
A method is a function with a receiver — a special parameter that indicates which type the method belongs to:
// Syntax: func (receiver ReceiverType) MethodName(params) (returns)
type Rectangle struct {
Width, Height float64
}
// Value receiver — receives a copy of the struct
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// Pointer receiver — can modify the original struct
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
func main() {
rect := Rectangle{Width: 5, Height: 3}
fmt.Println("Area:", rect.Area()) // 15
fmt.Println("Perimeter:", rect.Perimeter()) // 16
rect.Scale(2) // modifies rect directly
fmt.Println("New width:", rect.Width) // 10
fmt.Println("New height:", rect.Height) // 6
}Value Receiver vs. Pointer Receiver
This distinction is fundamental:
type Counter struct {
value int
}
// Value receiver: does NOT modify the original
func (c Counter) Current() int {
return c.value
}
func (c Counter) Increment() Counter {
c.value++
return c // returns a modified copy
}
// Pointer receiver: DOES modify the original
func (c *Counter) IncrementInPlace() {
c.value++
}
func main() {
c := Counter{value: 0}
c.IncrementInPlace() // c.value = 1
c.IncrementInPlace() // c.value = 2
fmt.Println(c.Current()) // 2
newC := c.Increment() // c.value stays 2
fmt.Println(newC.Current()) // 3
fmt.Println(c.Current()) // 2 (unchanged)
}Practical rule: If any method of a type needs a pointer receiver, make all methods of that type use a pointer receiver. Consistency makes interface implementation easier.
Embedding: Composition Over Inheritance
Go uses composition instead of inheritance. Embedding promotes the fields and methods of one struct into another:
type Animal struct {
Name string
Age int
}
func (a Animal) Describe() string {
return fmt.Sprintf("%s is %d years old", a.Name, a.Age)
}
type Dog struct {
Animal // embedding — Dog "inherits" Animal's fields and methods
Breed string
}
func (d Dog) Bark() string {
return "Woof!"
}
func main() {
rex := Dog{
Animal: Animal{Name: "Rex", Age: 3},
Breed: "Labrador",
}
// Direct access to embedded fields and methods
fmt.Println(rex.Name) // "Rex" (promoted from Animal)
fmt.Println(rex.Describe()) // "Rex is 3 years old" (promoted method)
fmt.Println(rex.Bark()) // "Woof!"
fmt.Println(rex.Breed) // "Labrador"
// You can also access explicitly
fmt.Println(rex.Animal.Name)
}Overriding Embedded Methods
func (d Dog) Describe() string {
// Call the embedded method and add extra information
return d.Animal.Describe() + fmt.Sprintf(", breed: %s", d.Breed)
}Struct Tags: Metadata for Serialization
Struct tags are metadata added to fields using backticks. They are used by packages like encoding/json, encoding/xml, ORMs, and validation frameworks:
type User struct {
ID int `json:"id" db:"user_id"`
Name string `json:"name" db:"name" validate:"required,min=2"`
Email string `json:"email" db:"email" validate:"required,email"`
Password string `json:"-"` // omit from JSON
CreatedAt time.Time `json:"created_at,omitempty"`
}
// JSON serialization
u := User{ID: 1, Name: "Ana", Email: "[email protected]"}
data, err := json.Marshal(u)
// {"id":1,"name":"Ana","email":"[email protected]"}
// JSON deserialization
var u2 User
json.Unmarshal([]byte(`{"id":2,"name":"Luis","email":"[email protected]"}`), &u2)Common tags:
json:"name"— field name in JSONjson:"-"— omit field from JSONjson:",omitempty"— omit if zero valuedb:"name"— column name in databasevalidate:"required"— validation with packages likego-playground/validator
Anonymous Structs
Useful for temporary data or tests:
// Anonymous struct in a variable
config := struct {
Host string
Port int
Debug bool
}{
Host: "localhost",
Port: 8080,
Debug: true,
}
// Slice of anonymous structs (common in tests)
cases := []struct {
input int
expected int
}{
{1, 1},
{5, 120},
{10, 3628800},
}Struct Comparison
Structs are comparable with == if all their fields are comparable (no slices, maps, or functions):
type Point struct{ X, Y int }
p1 := Point{1, 2}
p2 := Point{1, 2}
p3 := Point{3, 4}
fmt.Println(p1 == p2) // true
fmt.Println(p1 == p3) // falseWith structs and methods mastered, in the next lesson we will learn interfaces — the mechanism that makes polymorphism in Go elegant and implicit.
Sign in to track your progress