Structures and Methods in Go

In the Go programming language, struct (structure) is one of the core ways to define data types, allowing us to combine data of different types together. Unlike object-oriented programming (OOP) languages, Go does not have the concept of a "class"; instead, it organizes related data using structs and binds behaviors to specific instances through methods. This design allows Go to retain many advantages of OOP while maintaining the language's simplicity and efficiency.
A structure is fundamentally declared as a series of fields within curly braces, where each field can have a different type. Here is a simple example:

type Person struct {
    Name string
    Age  int
}

The above code defines the Person structure, containing two fields: a string-type name and an integer-type age. When creating an instance, you simply specify the values for the fields:

p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Age: 25, Name: "Bob"} // Go supports order-independent initialization

The Go compiler automatically adds a default zero value to structures; for example, strings are empty and integers are 0. This feature allows direct use of uninitialized structure variables when needed without causing compilation errors.

Methods: Binding Behavior to Instances

Although Go lacks explicit class and method declaration mechanisms, similar functionality can be achieved by defining "methods." A method receives a pointer to the structure as an argument (typically omitting *), allowing it to directly access fields within the structure. Here is an example of how to add a method to the aforementioned Person structure:

func (p *Person) Introduce() string {
    return fmt.Sprintf("Hello, I am %s and I am %d years old.", p.Name, p.Age)
}

The defined Introduce() method receives a pointer to Person, enabling direct access to fields inside the structure. When calling this method:

fmt.Println(p1.Introduce()) // Output: Hello, I am Alice and I am 30 years old.

Using pointer passing is the convention in Go for defining methods because it allows the method to modify the receiver's state (though value types can also be used in read-only scenarios).

Nested Structures and Composition of Data

Structures can contain not only basic data types but also embed other structures or collection types such as slices and maps. This feature makes building complex data structures very intuitive:

type Employee struct {
    Person   // Directly embeds the Person field, automatically gaining all its attributes and methods
    Department string
}

func (e *Employee) GetFullInfo() string {
    return fmt.Sprintf("%s works in %s", e.Person.Name, e.Department)
}

When instantiating an Employee:

emp := Employee{Person: Person{Name: "Charlie", Age: 35}, Department: "Engineering"}
fmt.Println(emp.GetFullInfo()) // Output: Charlie works in Engineering

Through embedding, we can reuse the attributes and methods of a structure without redefining fields. This compositional approach significantly enhances code readability and maintainability.

Comparison with OOP and Advantages

Compared to class and method mechanisms in Java, C++, or Python, Go structures and methods have the following significant characteristics:

  • No Inheritance Concept: Go does not support class inheritance (inheritance), but similar effects can be achieved through embedding;
  • Methods are Function Collections: Methods are independent functions that simply receive an additional parameter—a pointer or copy of the structure;
  • Interfaces and Composition Superior to Inheritance: By defining interfaces and combining multiple structure fields, a flexible extension mechanism can be implemented.

For example, we can add a boolean field representing employment status to Person and return different introduction information based on that state:

type Person struct {
    Name      string
    Age       int
    IsWorking bool // New field added
}

func (p *Person) Introduce() string {
    if p.IsWorking {
        return fmt.Sprintf("Hi, I'm %s. Yes, I work here.", p.Name)
    } else {
        return fmt.Sprintf("Hello, my name is %s and I am not working currently.", p.Name)
    }
}

This approach is more flexible and easier to understand than polymorphism and inheritance in traditional OOP.

Practical Recommendations and Precautions

In actual development, reasonably designing structures and methods is crucial for building clear, maintainable systems:

  1. Use Pointer Passing for Method Arguments: Unless copying data is necessary to avoid side effects, always define receivers in the *T form;
  2. Avoid Field Name Conflicts: Embedded structures should not have field names that duplicate those of the parent structure;
  3. Keep Interfaces Simple and Clear: Public methods designed for structures should be semantically clear to facilitate calls from other modules.

By mastering the combination use of structures and methods in Go, developers can organize business logic more efficiently, build modular components, and fully leverage Go's advantages in performance and safety.