Introduction to Structs in Go

In Go, structs are versatile data structures used to group variables together. Structs allow you to model real-world entities clearly and efficiently. By default, when their fields are exported, structs allow read and write access to their data.

Creating a Basic Struct

Let’s start with a basic example of a struct:


package main

import "fmt"

// Defining a struct with exported fields
type Person struct {
    Name string
    Age  int
}

func main() {
    p := Person{Name: "Alice", Age: 30}
    fmt.Println(p.Name, p.Age)  // Outputs: Alice 30
}

The fields in the Person struct (Name and Age) are accessible directly due to their capitalized names, making them exported fields.

Implementing Read-Only Structs

To create a read-only struct in Go, you can define unexported fields and provide only getter methods. This way, outside code cannot modify the values directly.


package main

import "fmt"

// Defining a struct with unexported fields
type person struct {
    name string
    age  int
}

// Getter method for the name field
func (p person) Name() string {
    return p.name
}

// Getter method for the age field
func (p person) Age() int {
    return p.age
}

func main() {
    p := person{name: "Bob", age: 25}
    fmt.Println(p.Name(), p.Age())  // Outputs: Bob 25
    // Uncommenting the next lines will result in compilation errors
    // p.name = "Charlie"
    // p.age = 26
}

In this example, fields name and age are unexported by making them lowercase. Getter methods provide read-only access.

Advanced Example with Additional Functionalities

You might wish to include logic within your methods that utilize unexported fields to maintain additional state or derived properties.


package main

import (
    "fmt"
    "time"
)

type account struct {
    owner    string
    balance  float64
    startDate time.Time
}

// NewAccount initializes a new account
func NewAccount(owner string, initialBalance float64) account {
    return account{owner: owner, balance: initialBalance, startDate: time.Now()}
}

// GetOwner returns the owner of the account
func (a account) GetOwner() string {
    return a.owner
}

// BalanceDetails provides access to account balance
func (a account) BalanceDetails() string {
    duration := time.Since(a.startDate).Hours() / 24
    return fmt.Sprintf("Owner: %s, Balance: %.2f, Opened %v days ago", a.owner, a.balance, int(duration))
}

func main() {
    acc := NewAccount("Charlie", 1000.0)
    fmt.Println(acc.BalanceDetails())  // Outputs: Details about the account
}

Here, an account struct manages an owner and balance, along with the account's open date. It uses methods to expose data, keeping internal fields secure from modification.

Conclusion

Using unexported fields in Go structs, coupled with getter methods, allows you to create robust APIs by exposing only the necessary parts of your data structures. This facilitates data encapsulation while creating safe, read-only views of complex data types.