Practical examples of defining and using structs in Go

If you’re learning Go, you’ll hit structs early and often. They’re the backbone of how you model data in real-world applications, and seeing clear examples of defining and using structs in Go is the fastest way to get comfortable. Instead of abstract theory, this guide walks through practical, production-style code: HTTP handlers, database models, configuration structs, JSON APIs, and more. Along the way, you’ll see examples of common patterns like embedding, methods on structs, value vs pointer receivers, and JSON tags. These examples of defining and using structs in Go are written with 2024–2025 Go practices in mind: using generics-aware standard library code where it makes sense, leaning on context, and organizing packages the way modern Go projects actually do. Whether you’re building a small CLI or a web service, you’ll find real examples you can copy, paste, and adapt into your own codebase.
Written by
Jamie
Published

Simple examples of defining and using structs in Go

Before jumping into bigger patterns, let’s start with a small, realistic example of a struct in Go that models a user in an internal tool:

package main

import "fmt"

type User struct {
    ID        int
    Email     string
    IsActive  bool
}

func main() {
    // Literal initialization with field names
    u1 := User{
        ID:       1,
        Email:    "alice@example.com",
        IsActive: true,
    }

    // Positional initialization (works, but easier to break)
    u2 := User{2, "bob@example.com", false}

    fmt.Println(u1)
    fmt.Println(u2)
}

This tiny snippet already shows a few examples of defining and using structs in Go:

  • Declaring a struct type with three fields.
  • Initializing it with named fields (safer, more readable).
  • Initializing it positionally (shorter, but brittle if you reorder fields).

In real projects, you almost always prefer the named style because it survives refactors.


Real examples of struct methods and pointer vs value receivers

Once you define a struct, you usually attach behavior to it with methods. Here’s an example of a User struct with methods that show how methods work in Go:

type User struct {
    ID       int
    Email    string
    IsActive bool
}

// Value receiver: does not modify original
func (u User) DisplayName() string {
    if u.Email == "" {
        return "Unknown user"
    }
    return u.Email
}

// Pointer receiver: can modify original
func (u *User) Deactivate() {
    u.IsActive = false
}

And a short example of using these methods:

func main() {
    u := User{ID: 42, Email: "dev@example.com", IsActive: true}

    fmt.Println(u.DisplayName()) // "dev@example.com"

    u.Deactivate()               // pointer receiver, modifies u
    fmt.Println(u.IsActive)      // false
}

This is one of the best examples of how Go nudges you to think about mutability. Methods with pointer receivers are common when you’re updating state, while value receivers are fine for read-only helpers.

For deeper background on Go’s type system and methods, the official Go tour remains a solid reference:
https://go.dev/tour/methods/1


HTTP handler examples of defining and using structs in Go

In 2024, a lot of Go code lives behind HTTP. Here’s a realistic example of a struct used with Go’s net/http package to implement a handler with dependencies.

package main

import (
    "encoding/json"
    "log"
    "net/http"
)

type Logger interface {
    Println(v ...any)
}

type UserService interface {
    FindByID(id int) (*User, error)
}

type User struct {
    ID    int    `json:"id"`
    Email string `json:"email"`
}

type UserHandler struct {
    Logger      Logger
    UserService UserService
}

func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // For brevity, pretend we parsed ID=1 from the URL
    user, err := h.UserService.FindByID(1)
    if err != nil {
        http.Error(w, "user not found", http.StatusNotFound)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    if err := json.NewEncoder(w).Encode(user); err != nil {
        h.Logger.Println("encode error:", err)
    }
}

This snippet shows one of the most common real examples of defining and using structs in Go:

  • User is a data struct with JSON tags.
  • UserHandler is a struct that groups dependencies (logger and service).
  • UserHandler implements http.Handler via a method on a pointer receiver.

Instead of wiring everything with global variables, you organize state with structs, which keeps your code testable and easier to reason about.


JSON API examples include tags, omitempty, and custom field names

Go’s encoding/json package is still the default choice for JSON in 2024. Here’s an example of a struct that models a typical API response:

type APIError struct {
    Code    string `json:"code"`
    Message string `json:"message"`
}

type UserResponse struct {
    ID        int       `json:"id"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`

    // Optional fields
    Phone   *string    `json:"phone,omitempty"`
    Address *UserAddress `json:"address,omitempty"`

    // Nested struct
    Errors []APIError `json:"errors,omitempty"`
}

type UserAddress struct {
    Line1   string `json:"line1"`
    City    string `json:"city"`
    State   string `json:"state"`
    ZipCode string `json:"zip_code"`
}

This gives you several concrete examples of defining and using structs in Go for JSON:

  • Using omitempty so optional fields don’t clutter responses.
  • Pointers for optional nested data (Phone, Address).
  • Nested structs for structured errors.

When you encode a UserResponse, Go respects the JSON tags and omits empty optional fields, which keeps your API clean and friendly to front-end teams.

For a reference on JSON tags and behavior, the Go encoding/json docs are worth bookmarking:
https://pkg.go.dev/encoding/json


Configuration struct example of organizing app settings

Most Go services load configuration from environment variables, flags, or files. A config struct is a simple pattern that shows up in almost every production codebase.

package config

import "time"

type HTTPConfig struct {
    Addr         string        // ":8080"
    ReadTimeout  time.Duration // 5 * time.Second
    WriteTimeout time.Duration // 10 * time.Second
}

type DBConfig struct {
    DSN            string
    MaxOpenConns   int
    MaxIdleConns   int
    ConnMaxIdle    time.Duration
    ConnMaxLifetime time.Duration
}

type AppConfig struct {
    Env  string    // "dev", "staging", "prod"
    HTTP HTTPConfig
    DB   DBConfig
}

Then in main.go you might see:

func loadConfig() AppConfig {
    return AppConfig{
        Env: "dev",
        HTTP: HTTPConfig{
            Addr:         ":8080",
            ReadTimeout:  5 * time.Second,
            WriteTimeout: 10 * time.Second,
        },
        DB: DBConfig{
            DSN:            "postgres://...",
            MaxOpenConns:   10,
            MaxIdleConns:   5,
            ConnMaxIdle:    5 * time.Minute,
            ConnMaxLifetime: 1 * time.Hour,
        },
    }
}

This is a clean example of defining and using structs in Go to group related settings:

  • AppConfig embeds HTTPConfig and DBConfig as fields.
  • Each sub-struct keeps logically related configuration together.

As Go projects grow, this pattern scales well and stays readable.


Embedding and composition: best examples for code reuse

Go doesn’t have inheritance, but it does have embedding. Here’s a practical example of using struct embedding to share common fields across multiple database models.

package model

import "time"

type BaseModel struct {
    ID        int       `db:"id"`
    CreatedAt time.Time `db:"created_at"`
    UpdatedAt time.Time `db:"updated_at"`
}

type User struct {
    BaseModel

    Email    string `db:"email"`
    IsActive bool   `db:"is_active"`
}

type Order struct {
    BaseModel

    UserID int     `db:"user_id"`
    Total  float64 `db:"total"`
}

By embedding BaseModel, both User and Order get ID, CreatedAt, and UpdatedAt directly:

func touch(m *BaseModel) {
    m.UpdatedAt = time.Now()
}

func exampleUsage() {
    u := User{Email: "alice@example.com"}
    touch(&u.BaseModel)
}

This is one of the best examples of defining and using structs in Go for code reuse that still keeps things explicit. You’re not hiding behavior behind an inheritance tree; you’re composing structs from smaller building blocks.

The official Go blog has a good discussion on embedding and composition patterns:
https://go.dev/blog/laws-of-reflection


Concurrency-safe examples include structs with mutexes

Go’s concurrency story is still a big reason teams adopt it in 2024. When you share state across goroutines, you often wrap it in a struct with a mutex.

package counter

import "sync"

type Counter struct {
    mu sync.Mutex
    n  int
}

func (c *Counter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.n++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.n
}

Usage example:

func main() {
    var wg sync.WaitGroup
    c := &Counter{}

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Inc()
        }()
    }

    wg.Wait()
    fmt.Println(c.Value()) // 100
}

This pattern shows a real example of defining and using structs in Go to safely share mutable state across goroutines. The mutex is just another field, and the struct methods enforce correct locking.


Generics-era examples of typed collections in Go

With Go 1.22 widely adopted in 2024–2025, generics are no longer experimental. You’ll see more examples of defining and using structs in Go that pair nicely with generic helpers.

Here’s a simple typed wrapper around a slice that tracks pagination metadata:

type Page[T any] struct {
    Items      []T
    Page       int
    PageSize   int
    TotalItems int
}

func (p Page[T]) TotalPages() int {
    if p.PageSize == 0 {
        return 0
    }
    n := p.TotalItems / p.PageSize
    if p.TotalItems%p.PageSize != 0 {
        n++
    }
    return n
}

Usage with a concrete type:

type Product struct {
    ID    int
    Name  string
    Price float64
}

func examplePage() {
    products := []Product{
        {ID: 1, Name: "Keyboard", Price: 49.99},
        {ID: 2, Name: "Mouse", Price: 29.99},
    }

    p := Page[Product]{
        Items:      products,
        Page:       1,
        PageSize:   20,
        TotalItems: 2,
    }

    fmt.Println("total pages:", p.TotalPages())
}

This is a modern example of defining and using structs in Go side by side with generics to make reusable, type-safe helpers.


Testing and mocking: example of using structs for fake services

Testing is where structs quietly shine. You can create small fake implementations for interfaces by defining lightweight structs.

type EmailSender interface {
    Send(to, subject, body string) error
}

type FakeEmailSender struct {
    Sent []struct {
        To      string
        Subject string
        Body    string
    }
}

func (f *FakeEmailSender) Send(to, subject, body string) error {
    f.Sent = append(f.Sent, struct {
        To      string
        Subject string
        Body    string
    }{to, subject, body})
    return nil
}

Then in a test:

func TestWelcomeEmail(t *testing.T) {
    fake := &FakeEmailSender{}

    err := SendWelcomeEmail(fake, "user@example.com")
    if err != nil {
        t.Fatal(err)
    }

    if len(fake.Sent) != 1 {
        t.Fatalf("expected 1 email, got %d", len(fake.Sent))
    }
}

This is a practical example of defining and using structs in Go to keep tests fast and independent of external systems.

For general software testing patterns and reliability concepts, even health-focused sites like the U.S. National Library of Medicine at the National Institutes of Health discuss the value of rigorous testing and validation in their research workflows:
https://www.nlm.nih.gov

The domain is different, but the mindset—test, measure, iterate—translates well to software.


FAQ: short examples-focused answers

What are some simple examples of defining and using structs in Go?

A very simple example is a User struct with a couple of fields and a helper method:

type User struct {
    ID    int
    Email string
}

func (u User) DisplayName() string {
    if u.Email == "" {
        return "Unknown"
    }
    return u.Email
}

You can then initialize and use it:

u := User{ID: 1, Email: "dev@example.com"}
fmt.Println(u.DisplayName())

Can you give an example of nested structs in Go?

Yes. A common pattern is nesting an address inside a user profile:

type Address struct {
    City  string
    State string
}

type Profile struct {
    Name    string
    Address Address
}

p := Profile{
    Name: "Alice",
    Address: Address{City: "Boston", State: "MA"},
}

This is an everyday example of defining and using structs in Go to represent structured data.

How do I choose between pointer and value receivers on struct methods?

If a method needs to modify the struct, or the struct is large enough that copying it would be wasteful, use a pointer receiver:

func (c *Counter) Inc() { c.n++ }

If the method is read-only and the struct is small, a value receiver is fine:

func (u User) DisplayName() string { return u.Email }

Being consistent within a type is more important than micro-optimizing every method.

Are there best examples of struct usage patterns I should learn first?

Yes, a few patterns show up constantly:

  • Data models with JSON tags for APIs.
  • Config structs that group settings.
  • Handler structs that bundle dependencies.
  • Embedding base structs for shared fields.

If you can read and write these examples of defining and using structs in Go comfortably, you’re in good shape for most day-to-day Go work.

Explore More Go Code Snippets

Discover more examples and insights in this category.

View All Go Code Snippets