1 Basics of Struct

In Go language, a struct is a composite data type used to aggregate different or identical types of data into a single entity. Structs hold a significant position in Go as they serve as a fundamental aspect of object-oriented programming, albeit with slight differences from traditional object-oriented programming languages.

The need for structs arises from the following aspects:

  • Organizing variables with strong relevance together to enhance code maintainability.
  • Providing a means to simulate "classes," facilitating encapsulation and aggregation features.
  • When interacting with data structures such as JSON, database records, etc., structs offer a convenient mapping tool.

Organizing data with structs allows for a clearer representation of real-world object models such as users, orders, etc.

2 Defining a Struct

The syntax for defining a struct is as follows:

type StructName struct {
    Field1 FieldType1
    Field2 FieldType2
    // ... other member variables
}
  • The type keyword introduces the definition of the struct.
  • StructName is the name of the struct type, following Go's naming conventions, is usually capitalized to indicate its exportability.
  • The struct keyword signifies that this is a struct type.
  • Within the curly braces {}, member variables (fields) of the struct are defined, with each followed by its type.

The type of struct members can be any type, including basic types (such as int, string, etc.) and complex types (such as arrays, slices, another struct, etc.).

For example, defining a struct representing a person:

type Person struct {
    Name   string
    Age    int
    Emails []string // can include complex types, such as slices
}

In the above code, the Person struct has three member variables: Name of string type, Age of integer type, and Emails of string slice type, indicating that a person may have multiple email addresses.

3 Creating and Initializing a Struct

3.1 Creating a Struct Instance

There are two ways to create a struct instance: direct declaration or using the new keyword.

Direct declaration:

var p Person

The above code creates an instance p of type Person, where each member variable of the struct is its corresponding type's zero value.

Using the new keyword:

p := new(Person)

Creating a struct using the new keyword results in a pointer to the struct. The variable p at this point is of type *Person, pointing to a newly allocated variable of type Person where the member variables have been initialized to zero values.

3.2 Initializing Struct Instances

Struct instances can be initialized in one go when they are created, using two methods: with field names or without field names.

Initializing with Field Names:

p := Person{
    Name:   "Alice",
    Age:    30,
    Emails: []string{"[email protected]", "[email protected]"},
}

When initializing with field assignment form, the order of initialization does not need to be the same as the order of declaring the struct, and any uninitialized fields will retain their zero values.

Initializing without Field Names:

p := Person{"Bob", 25, []string{"[email protected]"}}

When initializing without field names, make sure the initial values of each member variable are in the same order as when the struct was defined, and no fields can be omitted.

Additionally, structs can be initialized with specific fields, and any unspecified fields will take on zero values:

p := Person{Name: "Charlie"}

In this example, only the Name field is initialized, while Age and Emails will both take their corresponding zero values.

4 Accessing Struct Members

Accessing the member variables of a struct in Go is very straightforward, achieved by using the dot (.) operator. If you have a struct variable, you can read or modify its member values in this way.

Example:

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    // Create a variable of type Person
    p := Person{"Alice", 30}

    // Access struct members
    fmt.Println("Name:", p.Name)
    fmt.Println("Age:", p.Age)

    // Modify member values
    p.Name = "Bob"
    p.Age = 25

    // Access the modified member values again
    fmt.Println("\nUpdated Name:", p.Name)
    fmt.Println("Updated Age:", p.Age)
}

In this example, we first define a Person struct with two member variables, Name and Age. We then create an instance of this struct and demonstrate how to read and modify these members.

5 Struct Composition and Embedding

Structs can not only exist independently but can also be composed and nested together to create more complex data structures.

5.1 Anonymous Structs

An anonymous struct does not explicitly declare a new type, but rather directly uses the struct definition. This is useful when you need to create a struct once and use it simply, avoiding the creation of unnecessary types.

Example:

package main

import "fmt"

func main() {
    // Define and initialize an anonymous struct
    person := struct {
        Name string
        Age  int
    }{
        Name: "Eve",
        Age:  40,
    }

    // Access the members of the anonymous struct
    fmt.Println("Name:", person.Name)
    fmt.Println("Age:", person.Age)
}

In this example, instead of creating a new type, we directly define a struct and create an instance of it. This example demonstrates how to initialize an anonymous struct and access its members.

5.2 Struct Embedding

Struct embedding involves nesting one struct as a member of another struct. This allows us to build more complex data models.

Example:

package main

import "fmt"

// Define the Address struct
type Address struct {
    City    string
    Country string
}

// Embed the Address struct in the Person struct
type Person struct {
    Name    string
    Age     int
    Address Address
}

func main() {
    // Initialize a Person instance
    p := Person{
        Name: "Charlie",
        Age:  28,
        Address: Address{
            City:    "New York",
            Country: "USA",
        },
    }

    // Access the members of the embedded struct
    fmt.Println("Name:", p.Name)
    fmt.Println("Age:", p.Age)
    // Access the members of the Address struct
    fmt.Println("City:", p.Address.City)
    fmt.Println("Country:", p.Address.Country)
}

In this example, we define an Address struct and embed it as a member in the Person struct. When creating an instance of the Person, we also create an instance of Address simultaneously. We can access the members of the embedded struct using dot notation.

6 Struct Methods

Object-oriented programming (OOP) features can be implemented through struct methods.

6.1 Basic Concepts of Methods

In the Go language, although there is no traditional concept of classes and objects, similar OOP features can be achieved by binding methods to structs. A struct method is a special type of function that associates with a specific type of struct (or a pointer to a struct), allowing that type to have its own set of methods.

// Define a simple struct
type Rectangle struct {
    length, width float64
}

// Define a method for the Rectangle struct to calculate the area of the rectangle
func (r Rectangle) Area() float64 {
    return r.length * r.width
}

In the above code, the method Area is associated with the struct Rectangle. In the method definition, (r Rectangle) is the receiver, which specifies that this method is associated with the type Rectangle. The receiver appears before the method name.

6.2 Value Receivers and Pointer Receivers

Methods can be categorized as value receivers and pointer receivers based on the type of receiver. Value receivers use a copy of the struct to call the method, while pointer receivers use a pointer to the struct and can modify the original struct.

// Define a method with a value receiver
func (r Rectangle) Perimeter() float64 {
    return 2 * (r.length + r.width)
}

// Define a method with a pointer receiver, which can modify the struct
func (r *Rectangle) SetLength(newLength float64) {
    r.length = newLength // can modify the original struct's value
}

In the above example, Perimeter is a value receiver method, calling it will not change the value of Rectangle. However, SetLength is a pointer receiver method, and calling this method will affect the original Rectangle instance.

6.3 Method Invocation

You can call methods of a struct using the struct variable and its pointer.

func main() {
    rect := Rectangle{length: 10, width: 5}

    // Call the method with a value receiver
    fmt.Println("Area:", rect.Area())

    // Call the method with a value receiver
    fmt.Println("Perimeter:", rect.Perimeter())

    // Call the method with a pointer receiver
    rect.SetLength(20)

    // Call the method with a value receiver again, note that the length has been modified
    fmt.Println("After modification, Area:", rect.Area())
}

When you call a method using a pointer, Go automatically handles the conversion between values and pointers, regardless of whether your method is defined with a value receiver or a pointer receiver.

6.4 Receiver Type Selection

When defining methods, you should decide whether to use a value receiver or a pointer receiver based on the situation. Here are some common guidelines:

  • If the method needs to modify the content of the structure, use a pointer receiver.
  • If the structure is large and the cost of copying is high, use a pointer receiver.
  • If you want the method to modify the value the receiver points to, use a pointer receiver.
  • For efficiency reasons, even if you don't modify the structure content, it is reasonable to use a pointer receiver for a large structure.
  • For small structures, or when only reading data without the need for modification, a value receiver is often simpler and more efficient.

Through struct methods, we can simulate some features of object-oriented programming in Go, such as encapsulation and methods. This approach in Go simplifies the concept of objects while providing enough capability to organize and manage related functions.

7 Struct and JSON Serialization

In Go, it is often necessary to serialize a struct into JSON format for network transmission or as a configuration file. Similarly, we also need to be able to deserialize JSON into struct instances. The encoding/json package in Go provides this functionality.

Here's an example of how to convert between a struct and JSON:

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

// Define the Person structure, and use json tags to define the mapping between struct fields and JSON field names
type Person struct {
	Name   string   `json:"name"`
	Age    int      `json:"age"`
	Emails []string `json:"emails,omitempty"`
}

func main() {
	// Create a new instance of Person
	p := Person{
		Name:   "John Doe",
		Age:    30,
		Emails: []string{"[email protected]", "[email protected]"},
	}

	// Serialize to JSON
	jsonData, err := json.Marshal(p)
	if err != nil {
		log.Fatalf("JSON marshaling failed: %s", err)
	}
	fmt.Printf("JSON format: %s\n", jsonData)

	// Deserialize into a struct
	var p2 Person
	if err := json.Unmarshal(jsonData, &p2); err != nil {
		log.Fatalf("JSON unmarshaling failed: %s", err)
	}
	fmt.Printf("Recovered Struct: %#v\n", p2)
}

In the above code, we defined a Person structure, including a slice type field with the "omitempty" option. This option specifies that if the field is empty or missing, it will not be included in the JSON.

We used the json.Marshal function to serialize a struct instance into JSON, and the json.Unmarshal function to deserialize JSON data into a struct instance.

8 Advanced Topics in Structs

8.1 Comparison of Structs

In Go, it is allowed to directly compare two instances of structs, but this comparison is based on the values of the fields within the structs. If all field values are equal, then the two instances of the structs are considered equal. It should be noted that not all field types can be compared. For example, a struct containing slices cannot be directly compared.

Below is an example of comparing structs:

package main

import "fmt"

type Point struct {
	X, Y int
}

func main() {
	p1 := Point{1, 2}
	p2 := Point{1, 2}
	p3 := Point{1, 3}

fmt.Println("p1 == p2:", p1 == p2) // Output: p1 == p2: true
fmt.Println("p1 == p3:", p1 == p3) // Output: p1 == p3: false
}

In this example, p1 and p2 are considered equal because all their field values are the same. And p3 is not equal to p1 because the value of Y is different.

8.2 Copying Structs

In Go, instances of structs can be copied by assignment. Whether this copy is a deep copy or a shallow copy depends on the types of the fields within the struct.

If the struct only contains basic types (such as int, string, etc.), the copy is a deep copy. If the struct contains reference types (such as slices, maps, etc.), the copy will be a shallow copy, and the original instance and the newly copied instance will share the memory of the reference types.

The following is an example of copying a struct:

package main

import "fmt"

type Data struct {
Numbers []int
}

func main() {
// Initialize an instance of the Data struct
original := Data{Numbers: []int{1, 2, 3}}

// Copy the struct
copied := original

// Modify the elements of the copied slice
copied.Numbers[0] = 100

// View the elements of the original and copied instances
fmt.Println("Original:", original.Numbers) // Output: Original: [100 2 3]
fmt.Println("Copied:", copied.Numbers) // Output: Copied: [100 2 3]
}

As shown in the example, the original and copied instances share the same slice, so modifying the slice data in copied will also affect the slice data in original.

To avoid this issue, you can achieve true deep copying by explicitly copying the slice contents to a new slice:

newNumbers := make([]int, len(original.Numbers))
copy(newNumbers, original.Numbers)
copied := Data{Numbers: newNumbers}

This way, any modifications to copied will not affect original.