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.