Structs
Structs can store a group of different types of data and are a composite type. Go discards classes and inheritance, as well as constructors, intentionally weakening object-oriented functionality. Go is not a traditional OOP language, but Go still has the shadow of OOP. Through structs and methods, you can also simulate a class. Below is a simple example of a struct:
type Programmer struct {
Name string
Age int
Job string
Language []string
}Declaration
Declaring a struct is very simple. Example:
type Person struct {
name string
age int
}Both the struct itself and its fields follow the case-sensitive naming convention for exposure. For adjacent fields with the same type, you don't need to declare the type repeatedly:
type Rectangle struct {
height, width, area int
color string
}TIP
When declaring struct fields, the field name cannot be the same as a method name
Instantiation
Go does not have constructors. In most cases, structs are instantiated as follows. During initialization, just like map, specify the field name and then initialize the field value:
programmer := Programmer{
Name: "jack",
Age: 19,
Job: "coder",
Language: []string{"Go", "C++"},
}However, you can also omit field names. When omitting field names, you must initialize all fields. This approach is generally not recommended because readability is poor.
programmer := Programmer{
"jack",
19,
"coder",
[]string{"Go", "C++"}}If the instantiation process is complex, you can also write a function to instantiate the struct. As shown below, you can also think of it as a constructor:
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}However, Go does not support function or method overloading, so you cannot define different parameters for the same function or method. If you want to instantiate structs in multiple ways, either create multiple constructors or use the options pattern.
Options Pattern
The options pattern is a common design pattern in Go that allows more flexible struct instantiation, has strong extensibility, and does not require changing the constructor's function signature. Suppose you have the following struct:
type Person struct {
Name string
Age int
Address string
Salary float64
Birthday string
}Declare a PersonOptions type that accepts a parameter of type *Person. It must be a pointer because we need to assign values to Person in the closure.
type PersonOptions func(p *Person)Next, create option functions. They generally start with With, and their return value is a closure function.
func WithName(name string) PersonOptions {
return func(p *Person) {
p.Name = name
}
}
func WithAge(age int) PersonOptions {
return func(p *Person) {
p.Age = age
}
}
func WithAddress(address string) PersonOptions {
return func(p *Person) {
p.Address = address
}
}
func WithSalary(salary float64) PersonOptions {
return func(p *Person) {
p.Salary = salary
}
}The actual constructor signature is as follows. It accepts a variadic parameter of type PersonOptions.
func NewPerson(options ...PersonOptions) *Person {
// Apply options first
p := &Person{}
for _, option := range options {
option(p)
}
// Default value handling
if p.Age < 0 {
p.Age = 0
}
......
return p
}This way, different instantiation requirements can be completed with just one constructor, just by passing in different Options functions.
func main() {
pl := NewPerson(
WithName("John Doe"),
WithAge(25),
WithAddress("123 Main St"),
WithSalary(10000.00),
)
p2 := NewPerson(
WithName("Mike jane"),
WithAge(30),
)
}The functional options pattern can be seen in many open source projects. The gRPC Server instantiation also uses this design pattern. The functional options pattern is only suitable for complex instantiation. If there are only a few simple parameters, it is recommended to use a regular constructor.
Composition
In Go, relationships between structs are represented through composition. You can use explicit composition or anonymous composition. The latter is more similar to inheritance in usage, but there is essentially no difference. For example:
Explicit composition:
type Person struct {
name string
age int
}
type Student struct {
p Person
school string
}
type Employee struct {
p Person
job string
}When using it, you need to explicitly specify the field p:
student := Student{
p: Person{name: "jack", age: 18},
school: "lili school",
}
fmt.Println(student.p.name)With anonymous composition, you don't need to explicitly specify fields:
type Person struct {
name string
age int
}
type Student struct {
Person
school string
}
type Employee struct {
Person
job string
}The name of an anonymous field defaults to the type name. The caller can directly access the fields and methods of that type, but aside from being more convenient, there is no difference from the first approach.
student := Student{
Person: Person{name: "jack",age: 18},
school: "lili school",
}
fmt.Println(student.name)Pointers
For struct pointers, you can directly access the struct's contents without dereferencing. Example:
p := &Person{
name: "jack",
age: 18,
}
fmt.Println(p.age,p.name)At compile time, it will be converted to (*p).name and (*p).age. It actually still needs dereferencing, but it can be omitted during coding, which is a kind of syntactic sugar.
Tags
Struct tags are a form of metaprogramming. Combined with reflection, they can create many wonderful features. The format is as follows:
`key1:"val1" key2:"val2"`Tags are in key-value pairs, separated by spaces. Struct tags have very low fault tolerance. If the struct is not written in the correct format, it will result in inability to read properly, but there will be no errors during compilation. Below is an example:
type Programmer struct {
Name string `json:"name"`
Age int `yaml:"age"`
Job string `toml:"job"`
Language []string `properties:"language"`
}The most widely used application of struct tags is defining aliases in various serialization formats. The use of tags needs to be combined with reflection to fully realize their functionality.
Memory Alignment
The memory layout of Go struct fields follows memory alignment rules. This reduces the number of times the CPU accesses memory, but consumes more memory accordingly. It is a trade-off between space and time. Suppose you have the following struct:
type Num struct {
A int64
B int32
C int16
D int8
E int32
}The byte sizes of these types are as follows:
int64occupies 8 bytesint32occupies 4 bytesint16occupies 2 bytesint8occupies 1 byte
The total memory usage of the entire struct seems to be 8+4+2+1+4=19 bytes, but that is not the case. According to memory alignment rules, the memory usage of a struct must be at least a multiple of the largest field. If insufficient, it will be padded. The largest field in this struct is int64 occupying 8 bytes. The memory layout is as shown in the figure below:

So it actually occupies 24 bytes, of which 5 bytes are unused.
Now let's look at this struct:
type Num struct {
A int8
B int64
C int8
}After understanding the above rules, you can quickly understand that its memory usage is also 24 bytes, although it only has three fields, wasting 14 bytes.

However, we can adjust the fields and change to the following order:
type Num struct {
A int8
C int8
B int64
}This way, the memory usage becomes 16 bytes, wasting 6 bytes, reducing memory waste by 8 bytes.

Theoretically, arranging the fields in a struct in a reasonable order can reduce memory usage. However, in actual coding, there is no necessary reason to do this. It may not necessarily bring substantial improvement in reducing memory usage, but it will definitely increase the blood pressure and mental burden of developers, especially in business where some structs may have dozens or hundreds of fields. So it is only necessary to understand it.
TIP
If you really want to save memory using this method, you can check out these two libraries:
They will inspect the structs in your source code, calculate and rearrange struct fields to minimize memory usage.
Empty Struct
Empty structs have no fields and do not occupy memory space. We can use the unsafe.Sizeof function to calculate the byte size:
func main() {
type Empty struct {}
fmt.Println(unsafe.Sizeof(Empty{}))
}Output:
0There are many use cases for empty structs. As mentioned before, using it as the value type of a map allows the map to be used as a set. Alternatively, as the type of a channel, it represents a notification-only channel.
