Zum Inhalt springen

The Power of Nothing: Exploring Go’s Empty Struct

Cover

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

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!

Try Leapcell

Follow us on X: @LeapcellHQ

Read on our blog

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert