Preface
In the Go programming language, there’s a special usage that may confuse many people — the empty struct struct{}
. In this article, I’ll provide a detailed explanation of Go’s empty struct. Ready? Grab your favorite drink or tea, and let’s dive in.
What Is an Empty Struct
A struct that contains no fields is called an empty struct. It can be defined in the following two ways:
- Anonymous empty struct
var e struct{}
- Named empty struct
type EmptyStruct struct{}
var e EmptyStruct
Characteristics of Empty Structs
Empty structs have the following main characteristics:
- Zero memory allocation
- Same address
- Stateless
Zero Memory Allocation
Empty structs do not occupy any memory space. This makes them very useful for memory optimization. Let’s look at an example to verify whether they really occupy zero memory:
package main
import (
"fmt"
"unsafe"
)
func main() {
var a int
var b string
var e struct{}
fmt.Println(unsafe.Sizeof(a)) // 4
fmt.Println(unsafe.Sizeof(b)) // 8
fmt.Println(unsafe.Sizeof(e)) // 0
}
As shown by the output, the memory size of an empty struct is 0.
Same Address
No matter how many empty structs you create, they all point to the same address.
package main
import (
"fmt"
)
func main() {
var e struct{}
var e2 struct{}
fmt.Printf("%pn", &e) // 0x90b418
fmt.Printf("%pn", &e2) // 0x90b418
fmt.Println(&e == &e2) // true
}
Stateless
Since an empty struct contains no fields, it cannot hold any state. This makes it very useful for representing stateless objects or conditions.
Why Zero Memory and Same Address?
To understand why empty structs have zero size and share the same address, we need to delve into Go’s source code.
/go/src/runtime/malloc.go
// base address for all 0-byte allocations
var zerobase uintptr
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// ...
if size == 0 {
return unsafe.Pointer(&zerobase)
}
// ...
According to this excerpt from malloc.go
, when the size of the object to allocate is 0, it returns a pointer to zerobase
. zerobase
is a base address used for allocating zero-byte objects and doesn’t occupy any actual memory space.
Usage Scenarios of Empty Structs
Empty structs are mainly used in the following three scenarios:
- Implementing a Set data structure
- Used as signals in channels
- Used as method receivers
Implementing a Set Data Structure
Although Go does not have a built-in Set type, we can implement one using the map type. Since map keys are unique, we can store elements as keys, and since the values are not important, to save memory, we use empty structs as the values.
package main
import "fmt"
type Set[K comparable] map[K]struct{}
func (s Set[K]) Add(val K) {
s[val] = struct{}{}
}
func (s Set[K]) Remove(val K) {
delete(s, val)
}
func (s Set[K]) Contains(val K) bool {
_, ok := s[val]
return ok
}
func main() {
set := Set[string]{}
set.Add("Leapcell")
fmt.Println(set.Contains("Leapcell")) // true
set.Remove("Leapcell")
fmt.Println(set.Contains("Leapcell")) // false
}
Used as Channel Signals
Empty structs are often used for signaling between Goroutines, especially when we don’t care about the actual data being passed in the channel, only about the signal. For example, we can use an empty struct channel to notify a Goroutine to stop working:
package main
import (
"fmt"
"time"
)
func main() {
quit := make(chan struct{})
go func() {
// Simulate work
fmt.Println("Working...")
time.Sleep(3 * time.Second)
// Send quit signal
close(quit)
}()
// Block and wait for quit signal to close
<-quit
fmt.Println("Quit signal received, exiting...")
}
In this example, a quit
channel is created, and a separate Goroutine simulates some work. After completing the work, it closes the quit
channel to signal exit. The main function blocks at <-quit
until it receives the signal, then prints a message and exits.
Because the channel uses the empty struct type, it incurs no extra memory overhead.
In Go’s standard library, the context
package’s Context
interface has a Done()
method that returns a channel used to signal the completion status of operations. This returned channel uses an empty struct as its type.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
Used as Method Receiver
Sometimes we need to define a group of methods (usually to implement an interface) but don’t need to store any data in the implementation. In such cases, we can use an empty struct:
type Person interface {
SayHello()
Sleep()
}
type LPC struct{}
func (c LPC) SayHello() {
fmt.Println("[Leapcell] Hello")
}
func (c LPC) Sleep() {
fmt.Println("[Leapcell] Sleeping...")
}
This example defines an interface Person
and a struct LPC
, and implements the Person
interface with methods SayHello
and Sleep
.
Since LPC
is an empty struct, it adds no memory overhead.
Summary
In this article, we first introduced the concept and definitions of empty structs in the Go language, which can be defined in two ways.
Then, we explored the characteristics of empty structs, including their zero memory usage and the fact that multiple variables of this type share the same address.
Next, we went deeper into the source code, investigating why empty structs in Go have zero memory and the same address. The reason is that when the size of the object to be allocated (size
) is 0, Go returns a pointer to zerobase
.
Finally, we listed three use cases for empty structs and demonstrated some common real-world scenarios with code examples, including:
- Implementing a Set using a
map[K]struct{}
- Using empty structs for signaling in channels
- Using empty structs as receivers when no data storage is required
We are Leapcell, your top choice for hosting Go projects.
Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:
Multi-Language Support
- Develop with Node.js, Python, Go, or Rust.
Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the Documentation!
Follow us on X: @LeapcellHQ