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
.