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:
json—encoding/jsondb— database drivers (sqlx, gorm)validate— validation libraries (go-playground/validator)yaml— YAML parsersbson— 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
| Concept | Go Structs | Classes (Java/Python) |
|---|---|---|
| Inheritance | Embedding (composition) | Class hierarchy |
| Constructors | Factory functions | new / __init__ |
| Methods | Value or pointer receivers | Instance methods |
| Access control | Exported (uppercase) vs unexported (lowercase) | public / private |
| Interface impl | Implicit (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.
Related Articles
Creating Your First Go Module: A Step-by-Step Tutorial
Learn how to create, link, and publish Go modules — build a reusable math utilities module, use go mod init, the replace directive, go workspaces, semantic versioning, and publish to pkg.go.dev.
GoDemystifying Go's fmt.Sprintf: A Practical Guide
Master Go's fmt.Sprintf() with format verbs (%v, %s, %d, %q, %b, %x), width and padding, zero-padding, argument indexing, fmt.Fprintf for HTTP responses, fmt.Sscanf for parsing, and common pitfall fixes.
GoGo Data Types: A Comprehensive Guide for Developers
Explore every Go data type — bool, string, int, float, byte, rune, arrays, slices, maps, pointers, and interfaces — with practical examples, type conversion, and best practices for Go beginners.
Written by
Aditya RawasFull-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.