Go Data Structures & Algorithms
Structs in Go
Table of Contents
Structs are one of the most powerful and flexible data structures in Go, allowing you to create custom data types by grouping related data together. They form the foundation for object-oriented programming in Go and are essential for modeling complex data relationships. In this comprehensive guide, we'll explore structs in Go, from basic usage to advanced techniques and best practices.
What are Structs?
A struct in Go is a composite data type that groups together variables of different data types under a single name. These variables, called fields, can be of any type, including other structs, arrays, slices, maps, and interfaces.
Structs serve several important purposes in Go:
- Organizing related data into a single entity
- Creating custom data types
- Implementing object-oriented programming concepts
- Modeling real-world entities and relationships
- Providing a foundation for methods and interfaces
Unlike some object-oriented languages, Go's structs are value types, not reference types. This means that when you assign a struct to a variable or pass it to a function, the entire struct is copied, not just a reference to it.
Declaring and Initializing Structs
There are several ways to declare and initialize structs in Go. Let's explore each method with examples.
Struct Declaration
You declare a struct type using the type and struct keywords:
// Define a Person struct
type Person struct {
FirstName string
LastName string
Age int
Address string
Email string
}
This declares a new struct type called Person with five fields of various types.
Creating Struct Instances
There are several ways to create instances of a struct:
Zero Value Initialization
// Create a zero-valued Person
var person Person
// All fields are initialized to their zero values:
// FirstName: "" (empty string)
// LastName: "" (empty string)
// Age: 0
// Address: "" (empty string)
// Email: "" (empty string)
Struct Literal with Field Names
// Create and initialize a Person with field names
person := Person{
FirstName: "John",
LastName: "Doe",
Age: 30,
Address: "123 Main St",
Email: "john.doe@example.com",
}
This is the most common and recommended way to initialize structs, as it's clear and maintainable. Field order doesn't matter, and you can omit fields (they'll be set to their zero values).
Struct Literal without Field Names
// Create and initialize a Person without field names
person := Person{"John", "Doe", 30, "123 Main St", "john.doe@example.com"}
This method is more concise but less maintainable. You must provide values for all fields in the exact order they were declared in the struct definition.
Using the new Function
// Create a new Person with the new function
person := new(Person)
// This creates a pointer to a zero-valued Person:
// person.FirstName: "" (empty string)
// person.LastName: "" (empty string)
// person.Age: 0
// person.Address: "" (empty string)
// person.Email: "" (empty string)
The new function allocates memory for a struct and returns a pointer to it. All fields are initialized to their zero values.
Anonymous Structs
You can create structs without declaring a named type:
// Create an anonymous struct
point := struct {
X int
Y int
}{
X: 10,
Y: 20,
}
Anonymous structs are useful for one-off data structures that don't need to be reused.
Accessing and Modifying Struct Fields
You can access and modify struct fields using the dot notation:
// Create a Person
person := Person{
FirstName: "John",
LastName: "Doe",
Age: 30,
}
// Access fields
fmt.Println(person.FirstName) // Output: John
fmt.Println(person.Age) // Output: 30
// Modify fields
person.Age = 31
person.Email = "john.doe@example.com"
fmt.Println(person.Age) // Output: 31
fmt.Println(person.Email) // Output: john.doe@example.com
When working with struct pointers, Go automatically dereferences the pointer when accessing fields:
// Create a pointer to a Person
personPtr := &Person{
FirstName: "John",
LastName: "Doe",
Age: 30,
}
// Access fields (Go automatically dereferences the pointer)
fmt.Println(personPtr.FirstName) // Output: John
// This is equivalent to (*personPtr).FirstName
// Modify fields
personPtr.Age = 31
fmt.Println(personPtr.Age) // Output: 31
Exported and Unexported Fields
In Go, field names that start with an uppercase letter are exported (visible outside the package), while those that start with a lowercase letter are unexported (only visible within the package):
type User struct {
ID int // Exported field
Username string // Exported field
password string // Unexported field
email string // Unexported field
}
This is an important aspect of encapsulation in Go. Unexported fields can only be accessed directly within the same package.
Struct Embedding
Go supports struct embedding, which allows you to include one struct type within another. This is Go's way of implementing composition, a key concept in object-oriented design:
// Define an Address struct
type Address struct {
Street string
City string
State string
ZipCode string
Country string
}
// Define a Person struct with an embedded Address
type Person struct {
FirstName string
LastName string
Age int
Address // Embedded struct (anonymous field)
Email string
}
When you embed a struct, its fields are promoted to the outer struct, allowing you to access them directly:
// Create a Person with an embedded Address
person := Person{
FirstName: "John",
LastName: "Doe",
Age: 30,
Address: Address{
Street: "123 Main St",
City: "Anytown",
State: "CA",
ZipCode: "12345",
Country: "USA",
},
Email: "john.doe@example.com",
}
// Access fields from the embedded struct directly
fmt.Println(person.City) // Output: Anytown
fmt.Println(person.Country) // Output: USA
// You can also access them through the embedded field name
fmt.Println(person.Address.City) // Output: Anytown
fmt.Println(person.Address.Country) // Output: USA
Struct embedding is a powerful feature that enables composition over inheritance, allowing you to build complex types from simpler ones.
Multiple Embedding
You can embed multiple structs in a single struct:
type Contact struct {
Email string
Phone string
}
type Employee struct {
FirstName string
LastName string
Address // Embedded Address struct
Contact // Embedded Contact struct
EmployeeID string
Department string
}
If embedded structs have fields with the same name, you must use the explicit field name to avoid ambiguity:
type A struct {
X int
}
type B struct {
X int
}
type C struct {
A
B
}
c := C{}
// c.X is ambiguous
c.A.X = 1 // Must use the explicit path
c.B.X = 2
Methods and Receivers
One of the most powerful features of structs in Go is the ability to define methods on them. A method is a function that is associated with a particular type. Methods help to encapsulate behavior with the data it operates on.
Defining Methods
You define a method by specifying a receiver parameter before the method name:
type Rectangle struct {
Width float64
Height float64
}
// Method with a value receiver
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Method with a pointer receiver
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
Value Receivers vs. Pointer Receivers
There are two types of receivers in Go: value receivers and pointer receivers. The choice between them has important implications:
Value Receivers
- The method operates on a copy of the value
- Changes to the receiver inside the method are not visible to the caller
- Good for methods that don't need to modify the receiver
- More efficient for small structs
Pointer Receivers
- The method operates on a pointer to the value
- Changes to the receiver inside the method are visible to the caller
- Necessary for methods that need to modify the receiver
- More efficient for large structs (avoids copying)
- Required for implementing interfaces with pointer methods
func main() {
rect := Rectangle{Width: 10, Height: 5}
// Using a value receiver method
area := rect.Area()
fmt.Println("Area:", area) // Output: Area: 50
// Using a pointer receiver method
rect.Scale(2)
fmt.Println("After scaling:", rect) // Output: After scaling: {20 10}
// Go automatically handles the conversion between values and pointers
rectPtr := &Rectangle{Width: 10, Height: 5}
area = rectPtr.Area() // Automatically dereferenced
fmt.Println("Area from pointer:", area) // Output: Area from pointer: 50
}
Go automatically handles the conversion between values and pointers for method calls, which makes the code more readable and less error-prone.
Method Sets
The set of methods available for a type depends on whether you have a value or a pointer:
- A value of type T has all methods with value receivers declared for type T
- A pointer of type *T has all methods with value receivers and pointer receivers declared for type T
This is important when implementing interfaces, as the method set determines whether a type satisfies an interface.
Struct Tags
Struct tags are annotations added to struct fields that provide metadata about the field. They are commonly used for encoding/decoding data, validation, and ORM mappings:
type User struct {
ID int `json:"id" db:"user_id"`
Username string `json:"username" db:"username" validate:"required,min=3,max=50"`
Email string `json:"email" db:"email" validate:"required,email"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
Struct tags are string literals that follow specific conventions. They are accessed at runtime using reflection:
func main() {
t := reflect.TypeOf(User{})
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("%s: %s\n", field.Name, field.Tag)
}
}
Common uses for struct tags include:
json: For JSON encoding/decoding with theencoding/jsonpackagexml: For XML encoding/decoding with theencoding/xmlpackagedb: For database column mapping in ORM librariesvalidate: For validation rules in validation librariesform: For form field mapping in web frameworks
JSON Encoding/Decoding Example
type Person struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Age int `json:"age,omitempty"`
Email string `json:"-"` // Will be ignored during JSON encoding/decoding
}
func main() {
person := Person{
FirstName: "John",
LastName: "Doe",
Email: "john.doe@example.com",
}
// Encode to JSON
jsonData, err := json.Marshal(person)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(string(jsonData))
// Output: {"first_name":"John","last_name":"Doe"}
// Note: Age is omitted because it's zero and has the omitempty tag
// Email is omitted because it has the "-" tag
// Decode from JSON
jsonStr := `{"first_name":"Jane","last_name":"Smith","age":28}`
var newPerson Person
err = json.Unmarshal([]byte(jsonStr), &newPerson)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("%+v\n", newPerson)
// Output: {FirstName:Jane LastName:Smith Age:28 Email:}
}
Memory Layout and Performance
Understanding the memory layout of structs is important for optimizing performance, especially in performance-critical applications.
Memory Alignment and Padding
Go aligns struct fields in memory for efficient access. This can lead to padding between fields, which increases the size of the struct:
type Inefficient struct {
A byte
B int64
C byte
}
type Efficient struct {
B int64
A byte
C byte
}
func main() {
fmt.Println(unsafe.Sizeof(Inefficient{})) // Output: 24 (with padding)
fmt.Println(unsafe.Sizeof(Efficient{})) // Output: 16 (more efficient)
}
In the Inefficient struct, padding is added after A and C to align B and the end of the struct on 8-byte boundaries. In the Efficient struct, A and C can share the same 8-byte block, reducing the total size.
Field Ordering for Performance
For optimal memory usage, you should order struct fields from largest to smallest:
// Good: Fields ordered from largest to smallest
type OptimizedStruct struct {
A int64 // 8 bytes
B int32 // 4 bytes
C int16 // 2 bytes
D byte // 1 byte
E byte // 1 byte
}
// Bad: Fields in random order
type UnoptimizedStruct struct {
D byte // 1 byte + 7 bytes padding
A int64 // 8 bytes
E byte // 1 byte + 1 byte padding
C int16 // 2 bytes
B int32 // 4 bytes
}
Value vs. Pointer Semantics
The choice between value semantics and pointer semantics has performance implications:
Value Semantics
- Copying the entire struct can be expensive for large structs
- Better cache locality (data is stored together)
- No garbage collection overhead
- Thread-safe (each goroutine works on its own copy)
Pointer Semantics
- More efficient for large structs (only the pointer is copied)
- Allows for mutation of the original data
- Introduces garbage collection overhead
- Requires synchronization for concurrent access
// Value semantics (copy)
func ProcessByValue(s SomeStruct) {
// Works on a copy of s
}
// Pointer semantics (reference)
func ProcessByPointer(s *SomeStruct) {
// Works on the original s
}
As a rule of thumb, use value semantics for small structs and pointer semantics for large structs or when you need to modify the original data.
Common Patterns
Let's explore some common patterns and idioms for working with structs in Go.
Constructor Functions
Go doesn't have constructors, but it's common to create constructor-like functions that initialize structs with default values or perform validation:
type Person struct {
FirstName string
LastName string
Age int
}
// Constructor function for Person
func NewPerson(firstName, lastName string, age int) (*Person, error) {
if firstName == "" || lastName == "" {
return nil, errors.New("first name and last name are required")
}
if age < 0 || age > 150 {
return nil, errors.New("invalid age")
}
return &Person{
FirstName: firstName,
LastName: lastName,
Age: age,
}, nil
}
Functional Options Pattern
The functional options pattern is a flexible way to configure structs with many optional fields:
type Server struct {
host string
port int
timeout time.Duration
maxConn int
tls bool
}
type ServerOption func(*Server)
func WithHost(host string) ServerOption {
return func(s *Server) {
s.host = host
}
}
func WithPort(port int) ServerOption {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout time.Duration) ServerOption {
return func(s *Server) {
s.timeout = timeout
}
}
func WithMaxConn(maxConn int) ServerOption {
return func(s *Server) {
s.maxConn = maxConn
}
}
func WithTLS(tls bool) ServerOption {
return func(s *Server) {
s.tls = tls
}
}
func NewServer(options ...ServerOption) *Server {
// Default values
server := &Server{
host: "localhost",
port: 8080,
timeout: 30 * time.Second,
maxConn: 100,
tls: false,
}
// Apply options
for _, option := range options {
option(server)
}
return server
}
// Usage
server := NewServer(
WithHost("example.com"),
WithPort(443),
WithTLS(true),
)
This pattern allows for flexible, readable configuration with default values.
Method Chaining
Method chaining is a pattern where methods return the receiver, allowing for fluent interfaces:
type QueryBuilder struct {
table string
fields []string
where string
limit int
}
func NewQueryBuilder() *QueryBuilder {
return &QueryBuilder{}
}
func (qb *QueryBuilder) From(table string) *QueryBuilder {
qb.table = table
return qb
}
func (qb *QueryBuilder) Select(fields ...string) *QueryBuilder {
qb.fields = fields
return qb
}
func (qb *QueryBuilder) Where(condition string) *QueryBuilder {
qb.where = condition
return qb
}
func (qb *QueryBuilder) Limit(limit int) *QueryBuilder {
qb.limit = limit
return qb
}
func (qb *QueryBuilder) Build() string {
query := "SELECT "
if len(qb.fields) == 0 {
query += "*"
} else {
query += strings.Join(qb.fields, ", ")
}
query += " FROM " + qb.table
if qb.where != "" {
query += " WHERE " + qb.where
}
if qb.limit > 0 {
query += " LIMIT " + strconv.Itoa(qb.limit)
}
return query
}
// Usage
query := NewQueryBuilder().
Select("id", "name", "email").
From("users").
Where("age > 18").
Limit(10).
Build()
Method chaining creates readable, fluent interfaces for complex operations.
Best Practices
Here are some best practices to follow when working with structs in Go:
Keep Structs Focused
Follow the Single Responsibility Principle: each struct should have a single, well-defined purpose. If a struct is doing too many things, consider splitting it into multiple structs.
Use Embedding for Composition
Prefer composition over inheritance by embedding structs. This creates more flexible, modular code.
Be Consistent with Receiver Types
Be consistent with your choice of value or pointer receivers for methods on a type. If one method needs a pointer receiver, consider using pointer receivers for all methods on that type.
Use Constructor Functions for Complex Initialization
If a struct requires complex initialization or validation, provide a constructor function that returns an initialized instance.
Document Exported Fields and Methods
Add comments to exported fields and methods to document their purpose and usage. This is especially important for library code.
Use Field Tags Consistently
If you use struct tags, be consistent with their format and naming conventions. Document the expected format for custom tags.
Consider Memory Layout for Performance-Critical Code
In performance-critical code, consider the memory layout of structs and order fields from largest to smallest to minimize padding.
Use Unexported Fields for Encapsulation
Use unexported (lowercase) fields to hide implementation details and provide methods for controlled access.
type User struct {
ID int
Username string
email string // Unexported field
password string // Unexported field
}
// Getter method for email
func (u *User) Email() string {
return u.email
}
// Setter method for email with validation
func (u *User) SetEmail(email string) error {
if !isValidEmail(email) {
return errors.New("invalid email format")
}
u.email = email
return nil
}
Avoid Unnecessary Pointers
Don't use pointers for small structs unless you need to modify them or implement an interface with pointer methods. Value semantics are often clearer and more efficient for small structs.
Conclusion
Structs are a fundamental building block in Go, providing a way to create custom data types and organize related data. By understanding how to effectively use structs, you can write cleaner, more maintainable, and more efficient Go code.
In the next section, we'll explore linked lists, a dynamic data structure that builds on the concepts of structs and pointers to create flexible, sequential data storage.