Explain the 3 usage scenarios of Go empty structure in detail

煎鱼
中文

Hello everyone, I am fried fish.

In the Go language, there is a special type that is often asked by friends who are new to Go, or they don't understand it.

It is the use of the empty structure (struct) in Go, and it is often seen that someone uses it:

ch := make(chan struct{})

Repay the use of structure, no other types. Highly common, it is not an occasional phenomenon, there must be some reason behind it.

Today’s fried fish article will take you to understand why you want to use it this way, and know why.

Let's happily start the road of fish-sucking together.

Why use

To put it bluntly, I just want to save space. However, the new question is here again, why can't it be done with other types?

This involves the concept of "width" in the Go language. The width describes the number of bytes of storage space occupied by an instance of a type.

Width is a type attribute. Every value in the Go language has a type. The width of the value is defined by its type and is always a multiple of 8 bits.

In Go language, we can use the unsafe.Sizeof method to obtain:

// Sizeof takes an expression x of any type and returns the size in bytes
// of a hypothetical variable v as if v was declared via var v = x.
// The size does not include any memory possibly referenced by x.
// For instance, if x is a slice, Sizeof returns the size of the slice
// descriptor, not the size of the memory referenced by the slice.
// The return value of Sizeof is a Go constant.
func Sizeof(x ArbitraryType) uintptr

This method can get the width of the value, and naturally you can know the width corresponding to its type.

Let's take a look at the width of several common types in the Go language:

func main() {
    var a int
    var b string
    var c bool
    var d [3]int32
    var e []string
    var f map[string]bool

    fmt.Println(
        unsafe.Sizeof(a),
        unsafe.Sizeof(b),
        unsafe.Sizeof(c),
        unsafe.Sizeof(d),
        unsafe.Sizeof(e),
        unsafe.Sizeof(f),
    )
}

Output result:

8 16 1 12 24 8

You can find that the several types we have listed are merely declarations, and we haven't done anything. They still occupy a certain width.

If our scene is just a placeholder, what should we do, the overhead in the system is so wasteful?

The peculiarities of empty structures

One of the reasons why empty structures frequently appear in various systems is the need for a placeholder. It just so happens that the width of the Go empty structure is special.

as follows:

func main() {
    var s struct{}
    fmt.Println(unsafe.Sizeof(s))
}

Output result:

0

The width of the empty structure is very straightforward, even if it is deformed:

type S struct {
    A struct{}
    B struct{}
}

func main() {
    var s S
    fmt.Println(unsafe.Sizeof(s))
}

The final output result is also 0, which perfectly meets people's basic demands for placeholders, which is to occupy the hole and satisfy the basic input and output.

But at this time the problem reappeared. Why is this special treatment only available for empty structures, but not for other types?

This is an optimization item made by the Go compiler during memory allocation

// 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)
    }
}

When the size is found to be 0, it will directly return zerobase , which is the base address of all 0 bytes and does not occupy any width.

Therefore, the wide use of empty structures is that Go developers have used this small optimization to achieve the purpose of placeholders.

scenes to be used

After understanding why the empty structure is used as a placeholder, we further understand its real usage scenarios.

It is mainly divided into three parts:

  • Implement the method receiver.
  • Implement collection types.
  • Realize an empty channel.

Implement method receiver

In the business scenario, we need to combine the methods, which means it is a "group", which is convenient for subsequent expansion and maintenance.

But if we use:

type T string

func (s *T) Call()

It also seems a bit unfriendly, because as a string type, it occupies a certain amount of space.

In this case, we will use the empty structure method, which will also facilitate the increase of public fields for this type in the future. as follows:

type T struct{}

func (s *T) Call() {
    fmt.Println("脑子进煎鱼了")
}

func main() {
    var s T
    s.Call()
}

In this scenario, the use of empty structures is the most appropriate to consider from multiple dimensions, easy to expand, space saving, and most structured.

In addition, you will find that you have already done this subconsciously in your daily development. You can understand it as an alternative case of combining design patterns with daily life.

Implement collection types

In the Go language standard library, there is no related implementation of Set, so we generally use map instead of map for convenience in the code.

But there is a problem, that is, the use of collection types, only the key (key) is needed, not the value (value).

This is the scene of the empty structure battle skill:

type Set map[string]struct{}

func (s Set) Append(k string) {
    s[k] = struct{}{}
}

func (s Set) Remove(k string) {
    delete(s, k)
}

func (s Set) Exist(k string) bool {
    _, ok := s[k]
    return ok
}

func main() {
    set := Set{}
    set.Append("煎鱼")
    set.Append("咸鱼")
    set.Append("蒸鱼")
    set.Remove("煎鱼")

    fmt.Println(set.Exist("煎鱼"))
}

The empty structure as a placeholder will not add unnecessary memory overhead, and it is very convenient to solve it.

Realize empty channels

In the use scenarios of Go channel, notification channels are often encountered, which do not need to send any data, but are used to coordinate the operation of Goroutine, to transfer various states or to control concurrency.

as follows:

func main() {
    ch := make(chan struct{})
    go func() {
        time.Sleep(1 * time.Second)
        close(ch)
    }()

    fmt.Println("脑子好像进...")
    <-ch
    fmt.Println("煎鱼了!")
}

Output result:

脑子好像进...
煎鱼了!

The program will first output "The brain seems to enter...", and then sleep for a period of time before outputting "Fried fish!" to achieve the effect of intermittently controlling the channel.

Since the channel uses an empty structure, it will not bring additional memory overhead.

Summarize

In today's article, I introduced you to the width of several common types in the Go language, and analyzed it based on the question "empty structure" at the beginning.

Finally, it analyzes the three most common modes of code in the industry and enters the real scene. I wonder if you have had any doubts similar to this article before?

Welcome everyone to leave a message and exchange in the comment area:)

If you have any questions, welcome feedback and exchanges in the comment area. The best relationship between , and your likes is the biggest motivation for the creation of fried fish

The article is continuously updated, and you can read it on search [the brain is fried fish], this article 161690adf5bf51 GitHub github.com/eddycjy/blog has been included, welcome to Star to remind you to update.
阅读 998

煎鱼的清汤锅
今天写代码了吗 :-) 博客地址:[链接]
7.7k 声望
11.5k 粉丝
0 条评论
7.7k 声望
11.5k 粉丝
文章目录
宣传栏