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.a
andb
Memory address andc
andd
Whether 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 run
When command, you can pass-gcflags
Options pass multiple flags to the Go compiler that affect the compiler's behavior.
-
-m
Flags are used to start the compiler's memory escape analysis. -
-N
Flags are used to disable compiler optimization. -
-l
Flags are used to disable function inline.
According to the output, variables can be foundc
andd
Memory escape occurred, and the memory addresses of the two were the same in the end, and the result of equal comparison wastrue
。
anda
andb
The 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 ispossible:may 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.0
Version, 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 definedA
、B
、C
, and all define three fields, the types areint
、string
、struct{}
, 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 levelset
type. But we can use it very convenientlymap
+ struct{}
To achieveset
Type, 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
usemap
It is very easy to implement with empty structuresset
type.map
ofkey
Actually withset
The non-repetitive characteristics are just the same, and one does not need to be cared for.value
ofmap
That isset
。
Because of this, the empty structure type is most suitable for this one that does not require concern.value
ofmap
It's because itNo space, no semantics。
Maybe some people think that usingany
Asmap
ofvalue
It can also be achievedset
. But in factany
It 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,any
Type ofvalue
It has size, so it is not suitable.
In daily development, we will also use oneset
Idiom of :
s := map[string]struct{}{ "one": {}, "two": {}, "three": {}, } for element := range s { (element) }
This usage is also quite common, no need to declare oneset
Type, define a literalvalue
The 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 structurearray
Its 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,slice
Only occupyheader
space.
Signal notification
Another method I often use is withchannel
Used as a signal, the examples are as follows:
done := make(chan struct{}) go func() { (1 * ) // Perform some operations... ("goroutine done\n") done <- struct{}{} // Send a complete signal}() ("waiting...\n") <-done // Wait for completion("main exit\n")
This code declares a length of0
ofchannel
, its type ischan struct{}
。
Then start agoroutine
Execute business logic, the main coroutine waits for the signal to exit, both usechannel
Communicate.
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,goroutine
Outputgoroutine done
, then the main coroutine receives the exit signal and outputsmain exit
The program execution is completed.
becausestruct{}
It does not occupy memory, so in factchannel
The 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") <-done // Wait for completion("main exit\n")
heregoroutine
There is no need to send an empty structure in the , directlychannel
conductclose
Just do it,struct{}
The role played here is more like a "placeholder".
In Gocontext
It 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 }
of
Done
The 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'sio
There is one in the bagvariable, its main function is to provide a "black hole" device, any write to
The data of will be consumed without any effect (this is similar to the in Unix
/dev/null
equipment).
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 implements
Interface, and this
Writer
The 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.Writer
The 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 object
Very useful when .
That is, we define astruct{}
typefakeUserStore
, and then implementUserStore
Interface, so that it can be used in unit test codefakeUserStore
To replace the real oneUserStore
Instance 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 themnoCopy
Attributes, their definitions are as follows:
type noCopy struct{} func (*noCopy) Lock() {} func (*noCopy) Unlock() {}
noCopy
It is an empty structure, and its implementation is also very simple, only two empty methods are defined.
And thisnoCopy
Attributes 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 vet
Command detectionWhether it was copied unexpectedly.
Here,noCopy
The 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 vet
The command can be checked.
Our customstruct
It can also be embeddednoCopy
Attributes 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 vet
Command 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 vet
We have been detected to pass_ = a
CopyednoCopy
StructureA
。
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!