Skip to content

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:

go
type Programmer struct {
  Name     string
  Age      int
  Job      string
  Language []string
}

Declaration

Declaring a struct is very simple. Example:

go
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:

go
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:

go
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.

go
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:

go
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:

go
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.

go
type PersonOptions func(p *Person)

Next, create option functions. They generally start with With, and their return value is a closure function.

go
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.

go
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.

go
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:

go
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:

go
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:

go
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.

go
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:

go
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:

go
`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:

go
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:

go
type Num struct {
  A int64
  B int32
  C int16
  D int8
    E int32
}

The byte sizes of these types are as follows:

  • int64 occupies 8 bytes
  • int32 occupies 4 bytes
  • int16 occupies 2 bytes
  • int8 occupies 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:

go
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:

go
func main() {
   type Empty struct {}
   fmt.Println(unsafe.Sizeof(Empty{}))
}

Output:

0

There 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.

Golang by www.golangdev.cn edit