Go July 1, 2025 Aditya Rawas

Understanding Structs in Go: The Foundation of Data Modeling

In Go, there are no classes like in Java or Python. Instead, Go provides something more lightweight but equally powerful — structs. Before diving in, make sure you’re familiar with Go’s basic data types — structs are built on top of them. Structs let you define your own types that group related data together. If you’re building real-world Go applications, you’ll use structs constantly.

Let’s walk through what structs are, how to use them, how to attach methods, add JSON tags, embed one struct in another, and how structs relate to Go interfaces.


Why Use Structs?

Imagine you’re building a blogging platform and want to represent a blog post. Without structs, you’d manage individual variables:

title   := "Go Structs Guide"
author  := "Aditya"
content := "This blog explains structs in Go"

Managing 100 blog posts like this quickly becomes chaos. Structs let you define a reusable blueprint:

type BlogPost struct {
    Title   string
    Author  string
    Content string
}

Now you can work with a BlogPost as a cohesive unit.


How to Create a Struct Instance

post := BlogPost{
    Title:   "Go Structs Guide",
    Author:  "Aditya",
    Content: "This blog explains structs in Go",
}

fmt.Println(post.Title)  // Go Structs Guide

Compact Syntax (Use with Care)

post := BlogPost{"Short Title", "Author Name", "Content here"}

This positional syntax works but breaks silently when you add or reorder fields. Stick to named fields.


Default Values and Zero Initialization

Declaring a struct without values initializes all fields to their zero values:

var draft BlogPost
fmt.Println(draft.Title)   // ""
fmt.Println(draft.Author)  // ""

Zero initialization is useful when building up an object field by field.


Passing Structs to Functions

Structs are passed by value in Go — the function receives a copy:

func PrintPost(post BlogPost) {
    fmt.Println("Title:", post.Title)
    post.Title = "Modified" // only affects the local copy
}

PrintPost(post)
fmt.Println(post.Title) // still "Go Structs Guide"

Working with Pointers to Modify Structs

To modify the original struct inside a function, pass a pointer:

func UpdateAuthor(post *BlogPost, newAuthor string) {
    post.Author = newAuthor
}

UpdateAuthor(&post, "New Author")
fmt.Println(post.Author) // "New Author"

Go automatically dereferences struct pointers with dot notation — you write post.Author, not (*post).Author.


Methods on Structs

Go lets you attach functions (methods) to struct types. A value receiver works on a copy:

func (p BlogPost) Summary() string {
    return p.Title + " by " + p.Author
}

fmt.Println(post.Summary()) // "Go Structs Guide by Aditya"

A pointer receiver modifies the original:

func (p *BlogPost) UpdateContent(newContent string) {
    p.Content = newContent
}

post.UpdateContent("Updated content")

Rule of thumb: Use pointer receivers when the method modifies the struct or the struct is large (to avoid copying). Be consistent — if any method uses a pointer receiver, make all methods use pointer receivers.


Constructor Functions

Go doesn’t have built-in constructors, but factory functions are the idiomatic pattern:

func NewBlogPost(title, author, content string) BlogPost {
    return BlogPost{Title: title, Author: author, Content: content}
}

post := NewBlogPost("Hello", "Aditya", "Content")

Adding Validation

Constructor functions are the right place to enforce business rules:

func NewBlogPost(title, author, content string) (BlogPost, error) {
    if title == "" {
        return BlogPost{}, fmt.Errorf("title cannot be empty")
    }
    if len(content) < 10 {
        return BlogPost{}, fmt.Errorf("content too short (min 10 chars)")
    }
    return BlogPost{Title: title, Author: author, Content: content}, nil
}

post, err := NewBlogPost("Hello", "Aditya", "Some content here")
if err != nil {
    log.Fatal(err)
}

Struct Tags

Struct tags are metadata strings attached to fields using backtick syntax. They’re read at runtime via reflection — most commonly by encoding packages like encoding/json.

type BlogPost struct {
    Title     string    `json:"title"`
    Author    string    `json:"author"`
    Content   string    `json:"content"`
    Published bool      `json:"published"`
    CreatedAt time.Time `json:"created_at"`
    Internal  string    `json:"-"` // excluded from JSON
}

JSON Marshaling with Tags

post := BlogPost{
    Title:    "Go Structs",
    Author:   "Aditya",
    Content:  "Deep dive...",
    Published: true,
}

data, err := json.Marshal(post)
// {"title":"Go Structs","author":"Aditya","content":"Deep dive...","published":true}

Without struct tags, Go uses the field names as-is (Title, Author). Tags let you control the JSON key names.

Omitting Empty Fields

type Response struct {
    Data    interface{} `json:"data"`
    Error   string      `json:"error,omitempty"` // omitted if empty string
    Message string      `json:"message,omitempty"`
}

omitempty skips the field in JSON output when it has its zero value ("", 0, false, nil).

Other Tag Types

type User struct {
    Name  string `json:"name" db:"user_name" validate:"required,min=2"`
    Email string `json:"email" db:"email" validate:"required,email"`
    Age   int    `json:"age" db:"age" validate:"min=0,max=150"`
}

Common tag keys:

  • jsonencoding/json
  • db — database drivers (sqlx, gorm)
  • validate — validation libraries (go-playground/validator)
  • yaml — YAML parsers
  • bson — MongoDB drivers

Anonymous Structs

Anonymous structs don’t have a named type. They’re useful for one-off data shapes — configuration, test fixtures, or request/response bodies you won’t reuse.

config := struct {
    Host string
    Port int
}{
    Host: "localhost",
    Port: 8080,
}

fmt.Println(config.Host, config.Port) // localhost 8080

In tests, anonymous structs make table-driven tests clean:

tests := []struct {
    input    string
    expected int
}{
    {"hello", 5},
    {"world!", 6},
    {"", 0},
}

for _, tt := range tests {
    result := len(tt.input)
    if result != tt.expected {
        t.Errorf("len(%q) = %d, want %d", tt.input, result, tt.expected)
    }
}

Struct Embedding

Go doesn’t have inheritance, but it has embedding — including one struct’s fields and methods inside another without explicit delegation.

type Author struct {
    Name  string
    Email string
}

func (a Author) Bio() string {
    return fmt.Sprintf("%s <%s>", a.Name, a.Email)
}

type BlogPost struct {
    Author              // embedded — fields and methods promoted
    Title   string
    Content string
}

Now BlogPost has direct access to Author’s fields and methods:

post := BlogPost{
    Author:  Author{Name: "Aditya", Email: "aditya@example.com"},
    Title:   "Go Embedding",
    Content: "...",
}

fmt.Println(post.Name)  // "Aditya" — promoted field
fmt.Println(post.Bio()) // "Aditya <aditya@example.com>" — promoted method

If BlogPost needs to override Bio(), it defines its own method with the same name:

func (p BlogPost) Bio() string {
    return fmt.Sprintf("%s%s", p.Author.Bio(), p.Title)
}

Structs and Interfaces

A struct implicitly satisfies an interface when it implements all the interface’s methods:

type Publisher interface {
    Publish() error
}

func (p *BlogPost) Publish() error {
    if p.Content == "" {
        return fmt.Errorf("cannot publish empty post")
    }
    p.Published = true
    return nil
}

// BlogPost now satisfies Publisher
var pub Publisher = &BlogPost{Title: "Hello", Content: "World"}
pub.Publish()

No implements keyword needed — Go’s interface satisfaction is structural and implicit.


Structs vs Classes

ConceptGo StructsClasses (Java/Python)
InheritanceEmbedding (composition)Class hierarchy
ConstructorsFactory functionsnew / __init__
MethodsValue or pointer receiversInstance methods
Access controlExported (uppercase) vs unexported (lowercase)public / private
Interface implImplicit (structural)Explicit (implements)

Key Takeaways

  • Structs are Go’s primary tool for grouping related data into a named type.
  • Use value receivers for read-only methods; pointer receivers when modifying the struct or avoiding large copies.
  • Constructor functions (NewX()) enforce validation and prevent invalid state.
  • Struct tags control JSON marshaling, database mapping, and validation — the backtick syntax is metadata for reflection.
  • Anonymous structs are perfect for one-off shapes and table-driven tests.
  • Embedding promotes fields and methods from one struct into another — Go’s answer to inheritance.
  • Structs satisfy interfaces implicitly — any struct with the right methods qualifies, no declaration needed.

To understand how Go manages struct memory and goroutine concurrency at runtime, see the Go runtime deep dive.

Aditya Rawas

Written by

Aditya Rawas

Full-stack engineer writing deep-dives on JavaScript, TypeScript, React, AWS, Docker, and Kubernetes. Passionate about making complex engineering concepts accessible to developers at every level.