Working with Structs in Go
Structs are user-defined types that group together related data fields. This guide covers everything you need to know about working with structs effectively in Go.
Struct Basics
Struct Declaration
// Basic struct declaration
type Person struct {
FirstName string
LastName string
Age int
}
// Struct with tags
type User struct {
ID int `json:"id"`
Username string `json:"username" validate:"required"`
Email string `json:"email" validate:"required,email"`
CreatedAt time.Time `json:"created_at"`
}
// Anonymous struct
point := struct {
X, Y int
}{10, 20}
Creating Struct Instances
// Zero value initialization
var person Person
// Struct literal
person = Person{
FirstName: "John",
LastName: "Doe",
Age: 30,
}
// Short declaration
employee := Person{
FirstName: "Jane",
LastName: "Smith",
Age: 25,
}
// Positional initialization (avoid in production)
person = Person{"John", "Doe", 30}
// New returns a pointer to struct
personPtr := new(Person)
Accessing Fields
// Direct field access
name := person.FirstName
person.Age = 31
// Pointer to struct
ptr := &person
ptr.FirstName = "Jane" // Automatic dereferencing
(*ptr).LastName = "Smith" // Explicit dereferencing
// Nested field access
type Address struct {
Street string
City string
}
type Contact struct {
Name string
Address Address
}
contact := Contact{
Name: "John",
Address: Address{
Street: "123 Main St",
City: "New York",
},
}
city := contact.Address.City
Methods
Method Declaration
// Value receiver
func (p Person) FullName() string {
return fmt.Sprintf("%s %s", p.FirstName, p.LastName)
}
// Pointer receiver
func (p *Person) SetName(first, last string) {
p.FirstName = first
p.LastName = last
}
// Usage
person := Person{FirstName: "John", LastName: "Doe"}
fmt.Println(person.FullName())
person.SetName("Jane", "Smith")
Value vs Pointer Receivers
type Counter struct {
count int
}
// Value receiver (copy)
func (c Counter) Value() int {
return c.count
}
// Pointer receiver (modify original)
func (c *Counter) Increment() {
c.count++
}
// Usage
counter := Counter{}
counter.Increment() // Modifies counter
value := counter.Value() // Reads counter
Struct Embedding
Basic Embedding
type Address struct {
Street string
City string
State string
}
type Person struct {
Name string
Address // Embedded struct
}
// Usage
person := Person{
Name: "John",
Address: Address{
Street: "123 Main St",
City: "New York",
State: "NY",
},
}
// Direct access to embedded fields
city := person.City // Same as person.Address.City
Multiple Embedding
type Timestamp struct {
CreatedAt time.Time
UpdatedAt time.Time
}
type Metadata struct {
ID int
Active bool
}
type User struct {
Timestamp // Embedded struct
Metadata // Embedded struct
Name string
Email string
}
// Usage
user := User{
Name: "John",
Email: "[email protected]",
}
user.CreatedAt = time.Now() // From Timestamp
user.ID = 1 // From Metadata
Method Embedding
type Logger struct{}
func (l Logger) Log(message string) {
fmt.Printf("Log: %s\n", message)
}
type Server struct {
Logger // Embedded struct
Address string
}
// Usage
server := Server{Address: "localhost"}
server.Log("Server starting...") // Inherited from Logger
Struct Tags
Tag Declaration
type Product struct {
ID int `json:"id" db:"id"`
Name string `json:"name" validate:"required"`
Price float64 `json:"price" validate:"gte=0"`
SKU string `json:"sku" db:"stock_keeping_unit"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
Accessing Tags
import "reflect"
func getFieldTags(v interface{}) map[string]string {
tags := make(map[string]string)
t := reflect.TypeOf(v)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get("json")
if tag != "" {
tags[field.Name] = tag
}
}
return tags
}
// Usage
product := Product{}
tags := getFieldTags(product)
Best Practices
1. Struct Design
// Good: Related fields grouped together
type Customer struct {
// Personal information
FirstName string
LastName string
Email string
// Address information
Street string
City string
Country string
// Account information
AccountID string
CreatedAt time.Time
LastLoginAt time.Time
}
// Better: Use embedded structs for logical grouping
type PersonalInfo struct {
FirstName string
LastName string
Email string
}
type Address struct {
Street string
City string
Country string
}
type AccountInfo struct {
AccountID string
CreatedAt time.Time
LastLoginAt time.Time
}
type Customer struct {
PersonalInfo
Address
AccountInfo
}
2. Constructor Functions
// Good: Constructor function with validation
func NewUser(name, email string) (*User, error) {
if name == "" {
return nil, errors.New("name is required")
}
if !isValidEmail(email) {
return nil, errors.New("invalid email")
}
return &User{
Name: name,
Email: email,
CreatedAt: time.Now(),
}, nil
}
// Usage
user, err := NewUser("John", "[email protected]")
if err != nil {
log.Fatal(err)
}
3. Method Organization
// Good: Methods grouped by functionality
type Order struct {
ID int
Items []Item
Status string
}
// Validation methods
func (o *Order) Validate() error {
if len(o.Items) == 0 {
return errors.New("order must have items")
}
return nil
}
// Business logic methods
func (o *Order) CalculateTotal() float64 {
var total float64
for _, item := range o.Items {
total += item.Price
}
return total
}
// State management methods
func (o *Order) MarkAsShipped() error {
if o.Status != "paid" {
return errors.New("order must be paid before shipping")
}
o.Status = "shipped"
return nil
}
Common Patterns
1. Builder Pattern
type ServerBuilder struct {
server *Server
}
func NewServerBuilder() *ServerBuilder {
return &ServerBuilder{server: &Server{}}
}
func (b *ServerBuilder) WithHost(host string) *ServerBuilder {
b.server.Host = host
return b
}
func (b *ServerBuilder) WithPort(port int) *ServerBuilder {
b.server.Port = port
return b
}
func (b *ServerBuilder) Build() (*Server, error) {
if b.server.Host == "" {
return nil, errors.New("host is required")
}
return b.server, nil
}
// Usage
server, err := NewServerBuilder().
WithHost("localhost").
WithPort(8080).
Build()
2. Option Pattern
type ServerOption func(*Server)
func WithPort(port int) ServerOption {
return func(s *Server) {
s.Port = port
}
}
func WithTLS(cert, key string) ServerOption {
return func(s *Server) {
s.TLSCert = cert
s.TLSKey = key
}
}
func NewServer(options ...ServerOption) *Server {
server := &Server{
Port: 8080, // Default value
}
for _, option := range options {
option(server)
}
return server
}
// Usage
server := NewServer(
WithPort(3000),
WithTLS("cert.pem", "key.pem"),
)
3. Factory Pattern
type PaymentMethod interface {
Process(amount float64) error
}
type CreditCard struct {
Number string
CVV string
}
type PayPal struct {
Email string
}
func NewPaymentMethod(method string, config map[string]string) PaymentMethod {
switch method {
case "credit_card":
return &CreditCard{
Number: config["number"],
CVV: config["cvv"],
}
case "paypal":
return &PayPal{
Email: config["email"],
}
default:
return nil
}
}
Performance Considerations
1. Memory Layout
// Good: Struct field alignment
type Efficient struct {
ID int64 // 8 bytes
Count int64 // 8 bytes
Flag bool // 1 byte
pad [7]byte // 7 bytes padding
}
// Bad: Inefficient memory layout
type Inefficient struct {
Flag bool // 1 byte + 7 bytes padding
ID int64 // 8 bytes
Count int64 // 8 bytes
}
2. Value vs Pointer
// Use value receiver for small structs
type Point struct{ X, Y float64 }
func (p Point) Distance() float64 { /* ... */ }
// Use pointer receiver for large structs
type Image struct{ data [1024*1024]byte }
func (i *Image) Process() { /* ... */ }
3. Struct Copying
// Efficient: Copy small structs by value
type Small struct {
X, Y int
}
func process(s Small) { /* ... */ }
// Efficient: Pass large structs by pointer
type Large struct {
Data [1024]int
}
func process(l *Large) { /* ... */ }
Next Steps
- Learn about interfaces
- Explore methods
- Study reflection
- Practice with design patterns