The Complete Guide to Structs in Go

What are Structs and Why Use Them?

Structs are custom data types that allow you to group related fields together into a single entity. For example, you may define a struct to represent a user, order, or other business entities.

Here‘s a basic example of declaring a struct type in Go:

type User struct {
  ID        int
  FirstName string
  LastName  string
  Email     string
}

The User struct groups together an ID, first/last name, and email into a single coherent type. You can then create User values and pass them around in your program when useful.

Some key advantages of using structs:

  • Group related data together cleanly
  • Avoid long parameter lists (pass struct instead)
  • Reusable across application
  • Attach methods to operate on the data

In short, structs allow you to model your application‘s core entities and data points in a flexible way.

Declaring and Initializing Structs

To declare a struct:

type MyStruct struct {
  field1 type
  field2 type
}

For example:

type User struct {
  ID int
  Name string
}

To create a new struct value ("instantiate"):

myStruct := MyStruct{/* field vals */} 

Example:

user := User{ID: 1, Name: "John"}

There are a few ways to initialize structs:

  • Field names provided
  • Positional (no field names)
  • Both field names and positional
  • Using new()

Here is an example of the various options:

// Field names 
user1 := User{
  ID:   1,
  Name: "John",   
}

// Positional 
user2 := User{
  1, 
  "Sarah",
}

// Mixed 
user3 := User{
  1,
  Name: "Mary",
}

// new()
user4 := new(User)
user4.ID = 2
user4.Name = "Lisa" 

As shown above, the new() function returns a pointer to an allocated struct. You then have to set the fields manually.

Accessing Struct Fields

To access individual fields of a struct, use dot (.) notation:

user := User{/*...*/}
id := user.ID
name := user.Name

You can both get and set struct fields using dots.

Embedded/nested structs also work:

type User struct {
  //...
  Address Address
}

user.Address.City // etc

Struct Methods

A powerful feature of structs is the ability to define methods on them. This allows custom logic that is tied to the struct type.

type User struct {
  Name string
  Email string
}

func (u User) FullName() {
  return u.FirstName + " " + u.LastName  
}

user := User{"John", "[email protected]"} 
fmt.Println(user.FullName()) // "John Doe"

Some notes on struct methods:

  • Define on the struct type
  • Receiver can be value (User) or pointer (*User)
  • Can access instance data via receiver

Methods effectively "attach" functionality to struct types. This keeps related logic bundled together cleanly.

Struct Equality and Comparison

Struct values are comparable/testable for equality if all the struct fields are comparable. Two struct values are considered equal if:

  • They are the same type
  • Each struct field is equal

For example:

type Point struct {
  X, Y int
}

p1 := Point{1, 2} 
p2 := Point{1, 2}

fmt.Println(p1 == p2) // true 

p3 := Point{Y: 2, X: 1} 

fmt.Println(p1 == p3) // true

The order of struct fields does not affect equality.

Note that not all types allow comparison (e.g. slices, maps, functions). If a struct contains a non-comparable type, it can not be compared directly.

For custom comparisons, define an Equals() method:

func (u User) Equals(other User) bool {

  // Check fields...
  return u.ID == other.ID && 
    u.Name == other.Name &&
    u.Email == other.Email
}

user1.Equals(user2) // bool

JSON Marshal/Unmarshal

It‘s very common to marshal Go structs to JSON for serialization and APIs. And vice versa, unmarshal JSON back into structs.

This marshaling and unmarshaling uses tags on the struct fields.

For example:

type User struct {
  ID int `json:"id"`
  Name string `json:"name"`
} 

func main() {

  user := User{1, "John"}

  // Marshal into JSON  
  b, _ := json.Marshal(user) // b is JSON bytes 

  // Unmarshal back to struct
  var fromJson User
  json.Unmarshal(b, &fromJson)

}

Some key points around JSON handling:

  • Use tags to control marshalling
  • Naming convention is lower case
  • Omit empty fields with omitempty tag
  • Handle errors!

This makes serializing your Go application data to JSON very smooth.

Passing Structs to Functions

An area where structs shine is passing related data to functions. Rather than requiring long parameter lists, you can simply pass the struct.

For example, this function extracts first/last names:

// Without struct 
func Names(f string, l string) (string, string) {
  //.. 
}

// With struct  
func Names(user User) (string, string) {
  return user.FirstName, user.LastName   
}

Benefits passing whole struct:

  • Cleaner signature
  • Related params grouped
  • Less likely to mix up order
  • Easy to add more fields

You have the choice whether to pass the struct by value or by pointer. Pointers avoid copying for larger structs.

In summary, structs really help cut down on long parameter lists and group related data together.

Struct Composition and Best Practices

Some best practices around using structs effectively:

Group related fields together

The core purpose of structs is to group related data. For example, a Post struct may contain the post ID, text content, author, etc. All bound as one logical entity.

Use composition to reuse fields

Composition allows you to embed types rather than duplicate:

type Log struct {
  Timestamp int
  Message string  
}

type ErrorLog struct {
  Log    
  Level string 
} 

type AccessLog struct {
  Log
  URL string
}

The Log struct is composed into the other two, avoiding duplication.

Make structs immutable if possible

An immutable struct can not be modified after creation. This avoids an entire class of bugs.

Use constructor functions that return structs to control initialization.

Use pointers to larger structs

This avoids expensive copying as structs get passed around or returned from functions.

Provide utility methods

Methods define logic around a type itself. Useful for validation, formatting, helpers etc.

Real World Examples

Structs shine when modeling real world systems. Some examples:

  • User profile in web app
  • Documents in NoSQL store
  • Serialized API payloads
  • Configuration structs

Imagine a document store like Mongo. You can define Go structs that get cleanly marshaled to JSON documents for storage.

type Post struct {
  ID     int     `bson:"post_id"`
  Title  string  `bson:"title"`
  Views  int     `bson:"views"` 
}

// Mongo storage
db.Posts.insert(postInstance)  

For web services, structs make excellent serialized representations:

type CreateUserRequest struct {
  Name string `json:"name"`
  Email string `json:"email"`
}

type CreateUserResponse struct {
  ID string `json:"id"`
} 

// Controller
func (c *Controller) CreateUser(req CreateUserRequest) CreateUserResponse {

  // ...
  return CreateUserResponse{
    ID: "123",
  }
}

Structs really help model web service payloads in a clean way on both the server and client side.