Go offers a powerful way to define custom data types using structs, which allow grouping related data fields together to form a single, coherent unit. This enhances code organization, readability, and maintainability by enabling you to model real-world entities directly within your programs. Structs are fundamental for building scalable web services as they provide the foundation for representing complex data, such as API request/response bodies, database records, and configuration settings.
Defining Structs
A struct is a collection of fields (variables) of potentially different data types grouped together under a single name. You define a struct using the type keyword, followed by the struct's name and the struct keyword, enclosing its fields within curly braces {}.
Each field within a struct has a name and a data type. It is good practice to start field names with an uppercase letter if you want them to be accessible outside the package (exported), or a lowercase letter if they should only be accessible within the package (unexported). This concept of exportability is crucial in Go for controlling visibility.
`package main
import "fmt"
// User represents a user profile in our system.
// It groups related information like ID, Name, and Email.
type User struct {
ID int
Name string
Email string
// IsActive is unexported, meaning it's only accessible within the 'main' package.
isActive bool
}
// Product represents an item available for purchase.
type Product struct {
ProductID string
Name string
Description string
Price float64
Quantity int
}
func main() {
// Creating an instance of User
var user1 User
fmt.Println("Default User:", user1) // Fields will have their zero values (0 for int, "" for string, false for bool)
// Initializing a struct with field values
user2 := User{
ID: 1,
Name: "Alice Wonderland",
Email: "alice@example.com",
isActive: true, // Assigning value to unexported field
}
fmt.Println("User 2:", user2)
// Initializing a struct without field names (order matters)
// This approach is less readable and prone to errors if struct field order changes.
user3 := User{2, "Bob The Builder", "bob@example.com", false}
fmt.Println("User 3:", user3)
// Accessing individual fields
fmt.Println("User 2 Name:", user2.Name)
fmt.Println("User 3 ID:", user3.ID)
// Modifying fields
user2.Email = "alice.w@newdomain.com"
fmt.Println("User 2 updated Email:", user2.Email)
// Example with Product struct
laptop := Product{
ProductID: "LAPTOP-001",
Name: "UltraBook Pro",
Description: "Lightweight and powerful laptop",
Price: 1299.99,
Quantity: 10,
}
fmt.Println("Product:", laptop)
fmt.Println("Laptop Price:", laptop.Price)
}`
Zero Values of Structs
When a struct variable is declared but not explicitly initialized, its fields automatically take on their respective zero values. For example, int fields default to 0, string fields to "" (empty string), and bool fields to false. This behavior is consistent across Go's data types and helps prevent uninitialized data errors.
Struct Field Tags
Struct field tags are small string literals associated with each field in a struct. They provide metadata about the field, which can be used by various Go packages, particularly for encoding/decoding data (like JSON or XML) or for database mapping. Tags are defined using backticks (`) after the field's type.
A common use case is defining how a struct field should be represented when marshaling to JSON or unmarshaling from JSON. The json:"fieldName" tag specifies the JSON key name. The omitempty option in a JSON tag indicates that the field should be omitted from the JSON output if its value is the zero value for its type.
`package main
import (
"encoding/json"
"fmt"
)
// APIUser represents a user profile for API interactions.
// Tags are used to control JSON serialization/deserialization.
type APIUser struct {
ID int json:"id"
Username string json:"username"
Email string json:"email,omitempty" // omitempty will skip if Email is ""
IsAdmin bool json:"is_admin"
CreatedAt string json:"created_at" // Example: storing creation time as a string for simplicity
}
func main() {
user := APIUser{
ID: 101,
Username: "johndoe",
Email: "john.doe@example.com",
IsAdmin: false,
CreatedAt: "2023-01-15T10:00:00Z",
}
// Marshal the struct to JSON
jsonData, err := json.MarshalIndent(user, "", " ") // Use MarshalIndent for pretty printing
if err != nil {
fmt.Println("Error marshaling JSON:", err)
return
}
fmt.Println("User JSON with email:")
fmt.Println(string(jsonData))
// Create another user with an empty email to demonstrate omitempty
userWithoutEmail := APIUser{
ID: 102,
Username: "janedoe",
Email: "", // This field will be omitted due to "omitempty" tag
IsAdmin: true,
CreatedAt: "2023-01-16T11:30:00Z",
}
jsonDataWithoutEmail, err := json.MarshalIndent(userWithoutEmail, "", " ")
if err != nil {
fmt.Println("Error marshaling JSON:", err)
return
}
fmt.Println("\nUser JSON without email (omitempty in effect):")
fmt.Println(string(jsonDataWithoutEmail))
// Demonstrate unmarshaling JSON into a struct
jsonString := `
{
"id": 103,
"username": "peterg",
"email": "peter.g@example.com",
"is_admin": true,
"created_at": "2023-01-17T14:45:00Z"
}`
var newUser APIUser
err = json.Unmarshal([]byte(jsonString), &newUser) // &newUser is important (pointer)
if err != nil {
fmt.Println("Error unmarshaling JSON:", err)
return
}
fmt.Println("\nUnmarshaled User:", newUser)
fmt.Println("Unmarshaled User Username:", newUser.Username)
}`
In the json:"email,omitempty" tag, json is the tag key, "email" is the value that specifies the JSON field name, and omitempty is an option that tells the json package to omit the field if its value is the zero value for its type (e.g., empty string for string, 0 for int, false for bool, nil for pointers or slices).
Anonymous Structs
Anonymous structs are structs defined without a name. They are typically used for temporary, one-off data structures where defining a full type definition might be overkill. This is useful when you need to group a few fields together for a very specific, local purpose, like defining a temporary configuration object or a response structure within a function.
`package main
import (
"fmt"
"encoding/json"
)
func main() {
// Anonymous struct used for a temporary configuration
config := struct {
Host string
Port int
Debug bool
}{
Host: "localhost",
Port: 8080,
Debug: true,
}
fmt.Printf("Server Config: Host=%s, Port=%d, Debug=%t\n", config.Host, config.Port, config.Debug)
// Another anonymous struct, perhaps for a simple log entry
logEntry := struct {
Timestamp string `json:"timestamp"`
Message string `json:"message"`
Level string `json:"level"`
}{
Timestamp: "2023-10-26T10:30:00Z",
Message: "Application started successfully",
Level: "INFO",
}
fmt.Printf("Log Entry: %s - %s [%s]\n", logEntry.Timestamp, logEntry.Level, logEntry.Message)
// Anonymous struct for a JSON response payload
// Notice the JSON tags for proper serialization
responsePayload := struct {
Status string `json:"status"`
Code int `json:"code"`
Message string `json:"message"`
}{
Status: "success",
Code: 200,
Message: "Operation completed",
}
jsonResponse, err := json.MarshalIndent(responsePayload, "", " ")
if err != nil {
fmt.Println("Error marshaling JSON:", err)
return
}
fmt.Println("\nAnonymous Struct as JSON Response:")
fmt.Println(string(jsonResponse))
}`
Anonymous structs are particularly handy when you're dealing with one-off data structures, for example, creating a specific JSON response body or a temporary data container in a function. Their scope is limited to where they are defined.
Nested Structs and Struct Embedding
Structs can contain other structs as fields. This allows for creating complex data structures by composing smaller, more manageable structs. This is known as nested structs.
Go also supports struct embedding, which is a powerful mechanism for composition. When you embed a struct (or an interface, which will be covered in a later lesson) into another struct, the fields and methods (methods will be covered in the next lesson) of the embedded struct are promoted to the outer struct. This effectively means you can access the fields of the embedded struct directly through the outer struct's instance, as if they were fields of the outer struct itself. This promotes code reuse and can simplify data modeling.
Nested Structs
With nested structs, you access the inner struct's fields using a dot notation chain.
`package main
import "fmt"
// Address defines a postal address.
type Address struct {
Street string
City string
ZipCode string
}
// ContactInfo defines contact details.
type ContactInfo struct {
Email string
Phone string
}
// Customer represents a customer with an associated address and contact info.
type Customer struct {
ID int
Name string
Shipping Address // Nested struct
Billing Address // Another nested struct of the same type
Contact ContactInfo // Nested struct
}
func main() {
// Creating a Customer instance with nested structs
customer1 := Customer{
ID: 1001,
Name: "Global Tech Solutions",
Shipping: Address{
Street: "123 Tech Avenue",
City: "Innovation City",
ZipCode: "90210",
},
Billing: Address{
Street: "456 Finance Street",
City: "Capital Town",
ZipCode: "10001",
},
Contact: ContactInfo{
Email: "info@globaltech.com",
Phone: "555-123-4567",
},
}
fmt.Println("Customer ID:", customer1.ID)
fmt.Println("Customer Name:", customer1.Name)
fmt.Println("Shipping Street:", customer1.Shipping.Street) // Accessing nested field
fmt.Println("Billing City:", customer1.Billing.City)
fmt.Println("Contact Email:", customer1.Contact.Email)
// Modifying a nested field
customer1.Shipping.Street = "789 Enterprise Blvd"
fmt.Println("Updated Shipping Street:", customer1.Shipping.Street)
// Printing the entire struct
fmt.Printf("Customer 1: %+v\n", customer1) // %+v prints struct field names
}`
Struct Embedding
With struct embedding, you simply declare the type of the struct you want to embed without providing a field name. The embedded struct's fields and methods are then directly accessible from the outer struct.
`package main
import "fmt"
// Person represents basic personal information.
type Person struct {
Name string
Age int
}
// Employee embeds Person, meaning an Employee "is a" Person.
// It also has additional employee-specific fields.
type Employee struct {
Person // Embedded struct (no field name, just type)
EmployeeID string
Department string
}
// Developer also embeds Person, illustrating reuse.
type Developer struct {
Person // Embedded struct
Skills []string
Level string
}
func main() {
// Creating an Employee instance
emp1 := Employee{
Person: Person{ // Initialize the embedded struct
Name: "Maria Garcia",
Age: 30,
},
EmployeeID: "EMP007",
Department: "Engineering",
}
// Accessing fields of the embedded Person struct directly
fmt.Println("Employee Name:", emp1.Name) // Access Person's Name directly
fmt.Println("Employee Age:", emp1.Age) // Access Person's Age directly
fmt.Println("Employee ID:", emp1.EmployeeID)
fmt.Println("Employee Department:", emp1.Department)
// Modifying an embedded field
emp1.Age = 31
fmt.Println("Updated Employee Age:", emp1.Age)
// Creating a Developer instance
dev1 := Developer{
Person: Person{
Name: "Leo Kim",
Age: 25,
},
Skills: []string{"Go", "PostgreSQL", "Docker"},
Level: "Junior",
}
fmt.Println("\nDeveloper Name:", dev1.Name)
fmt.Println("Developer Skills:", dev1.Skills)
}`
When you embed Person into Employee, Employee automatically gains Name and Age fields. This is syntactic sugar; internally, Go treats it like a field named Person of type Person. If Employee also had a field named Name, the outer Employee.Name would take precedence, and you would access Person's Name via emp1.Person.Name. Embedding is a powerful tool for composition and achieving a form of inheritance-like behavior in Go without explicit inheritance.
Exercises
Complete the exercise to solidify your understanding of structs and custom data types.
Define and Initialize a Book Struct:
Create a struct named Book with the following fields: Title (string), Author (string), ISBN (string), Price (float64), and PublicationYear (int).
Define a Book variable and initialize it using a struct literal, providing values for all fields.
Print the Title and Author of your book.
Modify the Price of your book and print the updated price.
Top comments (0)