SoFunction
Updated on 2025-03-03

Detailed explanation of the idioms of hollow structures in Go language

In Go language, empty structurestruct{}It is a very special type, itNo fields are includedandNo memory space occupied. Although it may sound useless, empty structures actually have wide applications in Go programming. This article will explore in detail several typical uses of empty structures and explain why they are very useful in specific scenarios.

Empty structures do not occupy memory space

First, let’s verify whether the empty structure occupies memory space:

type Empty struct{}

var s1 struct{}
s2 := Empty{}
s3 := struct{}{}

("s1 addr: %p, size: %d\n", &s1, (s1))
("s2 addr: %p, size: %d\n", &s2, (s2))
("s3 addr: %p, size: %d\n", &s3, (s3))
("s1 == s2 == s3: %t\n", s1 == s2 && s2 == s3)

NOTE: In order to keep the code logic clear, only the main logic of the code is shown here. This will be true for all the sample codes in the following text. The complete code can be obtained in the GitHub link given at the end of the text.

In Go, we can useCalculate the number of bytes occupied by an object.

Execute the above example code and the output result is as follows:

$ go run
s1 addr: 0x1044ef4a0, size: 0
s2 addr: 0x1044ef4a0, size: 0
s3 addr: 0x1044ef4a0, size: 0
s1 == s2 == s3: true

According to the output results, we can see:

  • The memory addresses of multiple empty structures are the same.
  • The empty structure occupies 0 bytes, which means it does not occupy memory space.
  • Multiple empty structures have equal values.

The last two conclusions are easy to understand, and the first conclusion is a bit uncommon. Why are the memory addresses of empty structures instantiated by different variables the same?

Is this really the case? We can look at another example:

var (
 a struct{}
 b struct{}
 c struct{}
 d struct{}
)

println("&a:", &a)
println("&b:", &b)
println("&c:", &c)
println("&d:", &d)

println("&a == &b:", &a == &b)
x := &a
y := &b
println("x == y:", x == y)

("&c(%p) == &d(%p): %t\n", &c, &d, &c == &d)

This code defines 4 empty structures, prints their memory addresses in turn, and then compares them separately.aandbMemory address andcanddWhether the memory addresses of   are equal.

Execute the sample code and the output result is as follows:

$ go run -gcflags='-m -N -l'
# command-line-arguments
./:11:3: moved to heap: c
./:12:3: moved to heap: d
./:23:12: ... argument does not escape
./:23:50: &c == &d escapes to heap
&a: 0x1400010ae84
&b: 0x1400010ae84
&c: 0x104ec74a0
&d: 0x104ec74a0
&a == &b: false
x == y: true
&c(0x104ec74a0) == &d(0x104ec74a0): true

Use in Gogo runWhen command, you can pass-gcflagsOptions pass multiple flags to the Go compiler that affect the compiler's behavior.

  • -mFlags are used to start the compiler's memory escape analysis.
  • -NFlags are used to disable compiler optimization.
  • -lFlags are used to disable function inline.

According to the output, variables can be foundcanddMemory escape occurred, and the memory addresses of the two were the same in the end, and the result of equal comparison wastrue

andaandbThe output results of the two variables are more interesting. There is no memory escape in the two variables, and the memory addresses printed by the two are the same, but the results of the comparison are:false

Therefore, we can overturn the previous conclusion, and the new conclusion is: "Multiple empty structure memory addressespossiblesame".

In the official Go language specification, the Size and alignment guarantees section explains the memory address of the empty structure:

A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.

It roughly means: if a structure or array type does not contain any field (or element) that takes up memory size greater than zero, then its size is zero.Two different zero-size variables may have the same address in memory

Note that what is said here ispossiblemay have the same. Therefore, the conclusion that "multiple empty structures have the same memory address" mentioned above is not accurate.

NOTE: The execution results of this example are based onGo 1.22.0Version, there are both the same and different situations for the printing results of multiple empty structure memory addresses. This is related to the Go compiler implementation, and subsequent implementations may change.

In addition, for nested empty structures, the performance results are the same as those of ordinary empty structures:

type Empty struct{}

type MultiEmpty struct {
 A Empty
 B struct{}
}

s1 := Empty{}
s2 := MultiEmpty{}
("s1 addr: %p, size: %d\n", &s1, (s1))
("s2 addr: %p, size: %d\n", &s2, (s2))

Execute the sample code and the output result is as follows:

$ go run
s1 addr: 0x1044ef4a0, size: 0
s2 addr: 0x1044ef4a0, size: 0

Empty structure affects memory alignment

An empty structure does not occupy memory space at all times. For example, when an empty structure is used as another structure field, depending on the location, the size of the outer structure may be different due to memory alignment:

type A struct {
 x int
 y string
 z struct{}
}

type B struct {
 x int
 z struct{}
 y string
}

type C struct {
 z struct{}
 x int
 y string
}

a := A{}
b := B{}
c := C{}
("struct a size: %d\n", (a))
("struct b size: %d\n", (b))
("struct c size: %d\n", (c))

In the above example, three structures are definedABC, and all define three fields, the types areintstringstruct{}, the empty structure fields are placed at different positions at the last, middle and front respectively.

Execute the sample code and the output result is as follows:

$ go run
struct a size: 32
struct b size: 24
struct c size: 24

It can be found that memory alignment will be triggered when the empty structure is placed in the last field of another structure.

At this time, the outer structure will occupy more memory space, so if your program has strict memory requirements, this should be considered when using an empty structure as a field.

NOTE: I will dig a hole here first, and I will write another article about structure memory alignment in Go, and analyze why.struct{}Memory alignment will occur when placed in the structure field at the end, so stay tuned. Prevent getting lost,

Usage of empty structures

According to the previous explanation, we have already understood the characteristics of Go hollow structures and some precautions when using them. It is time to explore the uses of hollow structures.

Implement Set

First of all, the most commonly used part of empty structures is to realizeset(set)Type is now.

We know that Go language is not provided at the grammatical levelsettype. But we can use it very convenientlymap + struct{}To achievesetType, code is as follows:

// Set is based on empty structure to implement settype Set map[string]struct{}

// Add Add element to setfunc (s Set) Add(element string) {
 s[element] = struct{}{}
}

// Remove remove elements from setfunc (s Set) Remove(element string) {
 delete(s, element)
}

// Contains Check whether the set contains the specified elementfunc (s Set) Contains(element string) bool {
 _, exists := s[element]
 return exists
}

// Size returns set sizefunc (s Set) Size() int {
 return len(s)
}

// String implements 
func (s Set) String() string {
 format := "("
 for element := range s {
  format += element + " "
 }
 format = (format, " ") + ")"
 return format
}

s := make(Set)

("one")
("two")
("three")

("set: %s\n", s)
("set size: %d\n", ())
("set contains 'one': %t\n", ("one"))
("set contains 'onex': %t\n", ("onex"))

("one")

("set: %s\n", s)
("set size: %d\n", ())

Execute the sample code and the output result is as follows:

$ go run
set: (one two three)
set size: 3
set contains 'one': true
set contains 'onex': false
set: (three two)
set size: 2

usemapIt is very easy to implement with empty structuressettype.mapofkeyActually withsetThe non-repetitive characteristics are just the same, and one does not need to be cared for.valueofmapThat isset

Because of this, the empty structure type is most suitable for this one that does not require concern.valueofmapIt's because itNo space, no semantics

Maybe some people think that usinganyAsmapofvalueIt can also be achievedset. But in factanyIt will take up space.

Examples are as follows:

s := make(map[string]any)
s["t1"] = nil
s["t2"] = struct{}{}
("set t1 value: %v, size: %d\n", s["t1"], (s["t1"]))
("set t2 value: %v, size: %d\n", s["t2"], (s["t2"]))

Execute the sample code and the output result is as follows:

$ go run
set t1 value: <nil>, size: 16
set t2 value: {}, size: 16

Can be found,anyType ofvalueIt has size, so it is not suitable.

In daily development, we will also use onesetIdiom of  :

s := map[string]struct{}{
 "one":   {},
 "two":   {},
 "three": {},
}
for element := range s {
 (element)
}

This usage is also quite common, no need to declare onesetType, define a literalvalueThe empty structuremap, very convenient.

Apply for extra large capacity Array

Based on the characteristic that empty structures do not occupy memory space, we can consider creating a capacity of100 Ten thousand array

var a [1000000]string
var b [1000000]struct{}

("array a size: %d\n", (a))
("array b size: %d\n", (b))

Execute the sample code and the output result is as follows:

$ go run
array a size: 16000000
array b size: 0

Created using empty structurearrayIts size is still0

Apply for extra large capacity Slice

We also consider creating a capacity as100 Ten thousand slice

var a = make([]string, 1000000)
var b = make([]struct{}, 1000000)
("slice a size: %d\n", (a))
("slice b size: %d\n", (b))

Execute the sample code and the output result is as follows:

$ go run
slice a size: 24
slice b size: 24

Of course, it can be found that whether or not an empty structure is used,sliceOnly occupyheaderspace.

Signal notification

Another method I often use is withchannelUsed as a signal, the examples are as follows:

done := make(chan struct{})

go func() {
    (1 * ) // Perform some operations...    ("goroutine done\n")
    done &lt;- struct{}{} // Send a complete signal}()

("waiting...\n")
&lt;-done // Wait for completion("main exit\n")

This code declares a length of0ofchannel, its type ischan struct{}

Then start agoroutineExecute business logic, the main coroutine waits for the signal to exit, both usechannelCommunicate.

Execute the sample code and the output result is as follows:

$ go run
waiting...
goroutine done
main exit

The main coroutine outputs firstwaiting..., then wait for 1s,goroutineOutputgoroutine done, then the main coroutine receives the exit signal and outputsmain exitThe program execution is completed.

becausestruct{}It does not occupy memory, so in factchannelThe counter only needs to be added one internally, and it does not involve data transmission, so there is no additional memory overhead.

There is another implementation of this code:

done := make(chan struct{})

go func() {
 (1 * ) // Perform some operations... ("goroutine done\n")
 close(done) // There is no need to send struct{}{}, directly close, send the completion signal}()

("waiting...\n")
&lt;-done // Wait for completion("main exit\n")

heregoroutineThere is no need to send an empty structure in the    , directlychannelconductcloseJust do it,struct{}The role played here is more like a "placeholder".

In GocontextIt is also used in the source codestruct{}As a completion signal:

type Context interface {
 Deadline() (deadline , ok bool)
 // See /pipelines for more examples of how to use
 // a Done channel for cancellation.
 Done() <-chan struct{}
 Err() error
 Value(key any) any
}

ofDoneThe return value of the method ischan struct{}

No operation method receiver

Sometimes, we need to "combinate" some methods, and these methods do not use methods internally.Receiver, you can use it at this timestruct{}As a method receiver.

type NoOp struct{}

func (n NoOp) Perform() {
 ("Performing no operation.")
}

The code in the method is not referencedn, if replaced with another type, it will take up memory space.

In the actual development process, sometimes the code is written halfway, in order to compile and pass, we will also write this kind of code, first writing out the overall code framework, and then implementing internal details.

Implemented as an interface

usestruct{}As a method receiver, there is another purpose, which is to implement it as an interface. Commonly used to ignore unwanted output and unit tests. What does it mean? Look down.

We know there is one in GoInterface:

type Writer interface {
 Write(p []byte) (n int, err error)
}

We also know that Go'sioThere is one in the bagvariable, its main function is to provide a "black hole" device, any write toThe data of   will be consumed without any effect (this is similar to the   in Unix/dev/nullequipment).

The definition is as follows:

// Discard is a [Writer] on which all Write calls succeed
// without doing anything.
var Discard Writer = discard{}

type discard struct{}

func (discard) Write(p []byte) (int, error) {
 return len(p), nil
}

The code definition is extremely simple, it implementsInterface, and thisWriterThe implementation of the method is also extremely simple, and you will return it directly without doing anything.

It can also be found according to the comments.WriterThe purpose of the method is to do nothing and all calls will succeed, so it can be compared to the one in Unix system./dev/null

Can be used to ignore logs:

// Set the log output to ``, ignore all logs()
// This log will not be displayed anywhere("This log will not be shown anywhere")

In addition, I once wrote an articleHow to Solve MySQL Storage Dependency Issues in Go Language Unit Testing. There is a sample code like this:

type UserStore interface {
 Create(user *User) error
 Get(id int) (*User, error)
}

...

type fakeUserStore struct{}

func (f *fakeUserStore) Create(user *) error {
 return nil
}

func (f *fakeUserStore) Get(id int) (*, error) {
 return &{ID: id, Name: "test"}, nil
}

This is another use of empty structures as interfaces, for writing testsfake objectVery useful when  .

That is, we define astruct{}typefakeUserStore, and then implementUserStoreInterface, so that it can be used in unit test codefakeUserStoreTo replace the real oneUserStoreInstance object to solve the dependency problem between objects.

Identifier

Finally, let’s introduce a fun usage of an empty structure.

I believe many students have used Go directly or indirectly, its definition is as follows:

type Pool struct {
 noCopy noCopy

 local     
 localSize uintptr

 victim     
 victimSize uintptr

 New func() any
}

One of themnoCopyAttributes, their definitions are as follows:

type noCopy struct{}

func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

noCopyIt is an empty structure, and its implementation is also very simple, only two empty methods are defined.

And thisnoCopyAttributes seem useless, but in fact they have a great effect. The main function of this field is to preventCopyed unexpectedly. It is a technique to prevent structs from being miscopyed through compiler static analysis to ensure correct use and memory security.

Can be passedgo vetCommand detectionWhether it was copied unexpectedly.

Here,noCopyThe attribute has no effect on the current structure itself, but it can be used as an identifier for whether copying is allowed. With this tag, it means that the structure cannot be copied.go vetThe command can be checked.

Our customstructIt can also be embeddednoCopyAttributes to implement prohibited copying:

package main

type noCopy struct{}

func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

func main() {
 type A struct {
  noCopy noCopy
  a      string
 }

 type B struct {
  b string
 }

 a := A{a: "a"}
 b := B{b: "b"}

 _ = a
 _ = b
}

usego vetCommand checks for unexpected structure copying:

$ go vet

# command-line-arguments
# [command-line-arguments]
./:21:6: assignment copies lock value to _: contains

Can be found,go vetWe have been detected to pass_ = aCopyednoCopyStructureA

Summarize

Empty structurestruct{}Although small in Go, it has a clever purpose.

From a memory-saving perspective, it is ideal for representing the concept of empty. Semantically, usestruct{}The semantics are clearer, which means not paying attention to the value.

Due to the influence of memory alignment, the order of empty structure fields may affect the size of the outer structure. It is recommended to place the empty structure in the first field of the outer structure.

Whether it is to use empty structures to implement sets, signal notifications, method carriers, or placeholders, etc.struct{}All show their unique value.

The above is a detailed explanation of the idiom of hollow structures in Go language. For more information about empty structures in Go language, please pay attention to my other related articles!