Skip to main content

Go Structs

Introduction

In Go, structs are composite data types that group together variables under a single name. Think of them as custom data types that allow you to build more complex data structures. Structs are fundamental in Go programming and are widely used in Gin web applications for modeling data, handling requests, and organizing application logic.

Unlike arrays or slices that store elements of the same type, structs can hold different types of data - strings, integers, booleans, and even other structs or interfaces! This makes them perfect for representing real-world entities like users, products, or HTTP requests in your web applications.

Defining Structs

Let's start by learning how to define a struct:

go
type Person struct {
FirstName string
LastName string
Age int
Email string
IsActive bool
}

Here's what's happening:

  1. We use the type keyword to declare a new type
  2. Person is the name of our struct
  3. Between the curly braces {}, we define the fields (also called attributes or properties)
  4. Each field has a name followed by its type

In Go, it's conventional to capitalize field names if you want them to be exported (accessible outside the package).

Creating Struct Instances

Once you've defined a struct, you can create instances of it in several ways:

Method 1: Using a struct literal with field names

go
func main() {
// Create a new Person instance
user := Person{
FirstName: "John",
LastName: "Doe",
Age: 28,
Email: "[email protected]",
IsActive: true,
}

fmt.Println(user)
}

Output:

{John Doe 28 [email protected] true}

Method 2: Using a struct literal without field names (positional)

go
func main() {
// Create a new Person instance without field names
user := Person{"Jane", "Smith", 25, "[email protected]", false}

fmt.Println(user)
}

Output:

{Jane Smith 25 [email protected] false}

⚠️ The positional method is not recommended for structs with many fields, as it's error-prone and makes code less readable. It also becomes problematic when fields are added or reordered in the struct definition.

Method 3: Creating an empty struct and setting fields later

go
func main() {
// Create an empty Person instance
var user Person

// Set fields individually
user.FirstName = "Alex"
user.LastName = "Johnson"
user.Age = 32
user.Email = "[email protected]"
user.IsActive = true

fmt.Println(user)
}

Output:

{Alex Johnson 32 [email protected] true}

Accessing Struct Fields

You can access struct fields using the dot notation:

go
func main() {
user := Person{
FirstName: "John",
LastName: "Doe",
Age: 28,
Email: "[email protected]",
IsActive: true,
}

// Access individual fields
fmt.Println("Name:", user.FirstName, user.LastName)
fmt.Println("Age:", user.Age)
fmt.Println("Email:", user.Email)

// You can also modify fields
user.Age = 29
fmt.Println("Updated age:", user.Age)
}

Output:

Name: John Doe
Age: 28
Email: [email protected]
Updated age: 29

Nested Structs

Structs can contain other structs as fields, allowing you to create more complex data structures:

go
type Address struct {
Street string
City string
State string
ZipCode string
Country string
}

type User struct {
ID int
FirstName string
LastName string
Email string
Address Address // Nested struct
}

func main() {
user := User{
ID: 1,
FirstName: "John",
LastName: "Doe",
Email: "[email protected]",
Address: Address{
Street: "123 Main St",
City: "Boston",
State: "MA",
ZipCode: "02108",
Country: "USA",
},
}

fmt.Println("User:", user.FirstName, user.LastName)
fmt.Println("Lives in:", user.Address.City, user.Address.Country)
}

Output:

User: John Doe
Lives in: Boston USA

Anonymous Struct Fields

Go allows you to define structs with anonymous (embedded) fields:

go
type Person struct {
string // Anonymous field of type string
int // Anonymous field of type int
}

func main() {
p := Person{"John", 25}
fmt.Println("Name:", p.string)
fmt.Println("Age:", p.int)
}

Output:

Name: John
Age: 25

However, this approach is less common in production code as it reduces readability.

Struct Embedding

Go doesn't have traditional inheritance, but it supports composition through embedding. This is a powerful feature for code reuse:

go
type Person struct {
FirstName string
LastName string
Email string
}

type Employee struct {
Person // Embedded struct
EmployeeID int
Position string
}

func main() {
emp := Employee{
Person: Person{
FirstName: "John",
LastName: "Smith",
Email: "[email protected]",
},
EmployeeID: 1001,
Position: "Software Developer",
}

// You can access fields directly from the embedded struct
fmt.Println(emp.FirstName, emp.LastName) // From Person
fmt.Println(emp.Position) // From Employee

// You can also access the embedded struct explicitly
fmt.Println(emp.Person.Email)
}

Output:

John Smith
Software Developer
[email protected]

Embedding is particularly useful for sharing behavior between different types and is widely used in Go applications.

Structs with Methods

You can attach methods to structs, which is a key aspect of Go's approach to programming:

go
type Rectangle struct {
Width float64
Height float64
}

// Method to calculate area
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}

// Method to calculate perimeter
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}

func main() {
rect := Rectangle{Width: 10, Height: 5}

fmt.Printf("Rectangle: %.2f x %.2f\n", rect.Width, rect.Height)
fmt.Printf("Area: %.2f\n", rect.Area())
fmt.Printf("Perimeter: %.2f\n", rect.Perimeter())
}

Output:

Rectangle: 10.00 x 5.00
Area: 50.00
Perimeter: 30.00

Pointer Receivers vs. Value Receivers

When defining methods on structs, you can use either value receivers or pointer receivers:

go
type Counter struct {
Count int
}

// Value receiver - doesn't modify the original struct
func (c Counter) IncrementValue() {
c.Count++ // This only modifies the local copy
fmt.Println("Inside value method:", c.Count)
}

// Pointer receiver - modifies the original struct
func (c *Counter) IncrementPointer() {
c.Count++ // This modifies the original struct
fmt.Println("Inside pointer method:", c.Count)
}

func main() {
counter := Counter{Count: 0}

fmt.Println("Initial count:", counter.Count)

counter.IncrementValue()
fmt.Println("After value method:", counter.Count)

counter.IncrementPointer()
fmt.Println("After pointer method:", counter.Count)
}

Output:

Initial count: 0
Inside value method: 1
After value method: 0
Inside pointer method: 1
After pointer method: 1

When working with structs, use pointer receivers when:

  • You need to modify the receiver
  • The struct is large (for efficiency)
  • Consistency requires it (if some methods use pointer receivers, all methods should)

Structs in Web Development with Gin

Structs are extensively used in Gin web applications. Here's a practical example of how you might use structs in a Gin REST API:

go
package main

import (
"github.com/gin-gonic/gin"
"net/http"
)

// Product represents data about a product
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
InStock bool `json:"in_stock"`
}

var products = []Product{
{ID: 1, Name: "Laptop", Description: "High-performance laptop", Price: 1299.99, InStock: true},
{ID: 2, Name: "Smartphone", Description: "Latest model", Price: 899.99, InStock: true},
{ID: 3, Name: "Headphones", Description: "Wireless noise-canceling", Price: 199.99, InStock: false},
}

func main() {
router := gin.Default()

router.GET("/products", getProducts)
router.GET("/products/:id", getProductByID)
router.POST("/products", addProduct)

router.Run(":8080")
}

// Handler to get all products
func getProducts(c *gin.Context) {
c.JSON(http.StatusOK, products)
}

// Handler to get a specific product by ID
func getProductByID(c *gin.Context) {
// Parse ID parameter from URL
id := c.Param("id")

// ... logic to find product by ID ...

// For this example, just return the first product
c.JSON(http.StatusOK, products[0])
}

// Handler to add a new product
func addProduct(c *gin.Context) {
var newProduct Product

// Bind JSON body to Product struct
if err := c.ShouldBindJSON(&newProduct); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// Add product to the slice (in a real app, you'd save to a database)
products = append(products, newProduct)

c.JSON(http.StatusCreated, newProduct)
}

In this example:

  • The Product struct defines the shape of our product data
  • We use struct tags (json:"name") to control how the fields are represented in JSON
  • The struct is used for both incoming data (request binding) and outgoing data (JSON responses)

Struct Tags

You may have noticed the json:"id" syntax in the previous example. These are called struct tags and are used to provide metadata about struct fields:

go
type User struct {
Username string `json:"username" binding:"required"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"gte=18"`
}

Struct tags are parsed at runtime and used by various libraries:

  • JSON/XML encoding: control field names and options
  • Validation: specify validation rules
  • ORM: define database column mappings
  • And many more uses!

In Gin, struct tags are especially important for:

  1. Controlling JSON/XML representation
  2. Input validation
  3. Request binding from different sources (JSON, form data, query parameters)

Summary

Structs are one of Go's most powerful features for organizing and working with data. In this guide, you've learned:

  • How to define structs and create struct instances
  • Working with struct fields and methods
  • Nested structs and struct embedding
  • Value vs. pointer receivers for methods
  • Using struct tags for metadata
  • Practical examples of structs in Gin web applications

As you build Go applications with Gin, you'll find structs are essential for modeling your domain entities, handling requests and responses, and organizing your application's data flow.

Exercises

To reinforce your understanding, try these exercises:

  1. Create a Book struct with fields for title, author, publication year, and price
  2. Add methods to the Book struct to apply a discount to the price and to format the book details as a string
  3. Create a Gin endpoint that accepts a book as JSON, validates it, and returns a response
  4. Implement a struct representing a blog post with nested comments (each comment should be a struct)
  5. Create an API for a todo list application using structs to represent tasks and task lists

Further Reading



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)