Structure vide élaborée : la merveilleuse utilisation du langage Go

Dans le langage Go, nous pouvons définir une structure vide (empty struct), c'est-à-dire une structure sans aucune variable membre, qui est représentée par le mot-clé struct{}. Ce type de structure semble inutile, mais en fait, il est largement utilisé dans le langage Go. Cet article présentera l'utilisation de la structure vide sous plusieurs aspects, afin que chacun puisse mieux comprendre sa fonction.

1. Définition et initialisation d'une structure vide

Une structure vide est une structure qui ne contient aucun champ. Dans Golang, vous pouvez utiliser struct{} pour définir une structure vide. Voici un exemple simple :

 package main
 ​
 import "fmt"
 ​
 func main() {
     var s struct{}
     fmt.Printf("%#v\n", s) // 输出: struct {}{}
 }
复制代码

Dans cet exemple, nous définissons une variable appelée s et l'initialisons avec une structure vide. Ensuite, nous utilisons fmt.Printf pour imprimer cette structure vide. Notez que l'espace réservé %#v est utilisé lors de l'impression, cet espace réservé peut générer la variable au format de syntaxe Go.

La sortie est struct {}{}, ce qui signifie que s est une structure vide sans aucun champ. Il convient de noter qu'une variable de structure vide n'occupe en réalité aucun espace mémoire, c'est-à-dire que sa taille est de 0 octet.

2. La taille et l'utilisation de la mémoire de la structure vide

Comme mentionné ci-dessus, la taille d'une structure vide est de 0 octet. Cela signifie qu'il ne prend pas d'espace mémoire. Cela peut être vérifié en utilisant la fonction unsafe.Sizeof :

 package main
 ​
 import (
     "fmt"
     "unsafe"
 )
 ​
 func main() {
     var s struct{}
     fmt.Printf("Size of struct{}: %v\n", unsafe.Sizeof(s)) // 输出: Size of struct{}: 0
 }
复制代码

Dans cet exemple, nous utilisons la fonction unsafe.Sizeof pour obtenir la taille de s et imprimer le résultat. Puisque s est une structure vide, sa taille est 0.

Il convient de noter que même si une structure vide a une taille de 0, cela ne signifie pas qu'elle ne peut pas être transmise en tant que paramètre de fonction ou valeur de retour. Parce que dans Go, chaque type a ses propres informations de type, qui peuvent être utilisées pour la vérification et la conversion de type. Par conséquent, même une structure vide a sa propre place et son propre rôle dans le système de types.

3. Structures vides comme espaces réservés

L'utilisation la plus courante des structures vides est celle des espaces réservés. Dans une signature de fonction ou de méthode, s'il n'y a pas de paramètres ou de valeurs de retour, une structure vide peut être utilisée pour identifier la fonction ou la méthode. Voici un exemple simple :

 package main
 ​
 import "fmt"
 ​
 func doSomething() struct{} {
     fmt.Println("Doing something")
     return struct{}{}
 }
 ​
 func main() {
     doSomething()
 }
复制代码

在这个示例中,我们定义了一个名为 doSomething 的函数,它不接受任何参数,也不返回任何值。我们可以使用空结构体来标识它的返回值。在 doSomething 函数的实现中,我们只是打印了一条消息,然后返回一个空结构体。

在 main 函数中,我们调用 doSomething 函数。由于它没有返回任何值,所以我们不需要将其结果存储在变量中。

需要注意的是,在这个示例中,我们将返回值的类型显式指定为 struct{}。这是因为如果不指定返回值的类型,那么 Go 编译器会将它默认解析为 interface{} 类型。在这种情况下,每次调用 doSomething 函数都会分配一个新的空接口对象,这可能会带来性能问题。

4. 空结构体作为通道元素

空结构体还可以用作通道的元素类型。在 Go 中,通道是一种用于在协程之间进行通信和同步的机制。使用通道时,我们需要指定通道中元素的类型。

如果我们不需要在通道中传输任何值,那么可以使用空结构体作为元素类型。下面是一个简单的示例:

 package main
 ​
 import "fmt"
 ​
 func main() {
     c := make(chan struct{})
     go func() {
         fmt.Println("Goroutine is running")
         c <- struct{}{}
     }()
     <-c
     fmt.Println("Goroutine is done")
 }
复制代码

在这个示例中,我们创建了一个名为 c 的通道,并将其元素类型指定为 struct{}。然后,我们在一个新的协程中运行一些代码,并在协程中向通道中发送一个空结构体。在 main 函数中,我们从通道中接收一个元素,这里实际上是在等待协程的结束。一旦我们接收到了一个元素,我们就会打印出 "Goroutine is done"。

需要注意的是,在这个示例中,我们并没有向通道中发送任何有用的数据。相反,我们只是使用通道来同步协程之间的执行。这种方法对于实现复杂的并发模型非常有用,因为它可以避免使用显式的互斥量或信号量来实现同步和通信。

5. 空结构体作为 map 的占位符

在 Go 中,map 是一种用于存储键值对的数据结构。如果我们只需要一个键集合,而不需要存储任何值,那么可以使用空结构体作为 map 的值类型。下面是一个简单的示例:

 package main
 ​
 import "fmt"
 ​
 func main() {
     m := make(map[string]struct{})
     m["key1"] = struct{}{}
     m["key2"] = struct{}{}
     m["key3"] = struct{}{}
     fmt.Println(len(m)) // 输出: 3
 }
复制代码

在这个示例中,我们创建了一个名为 m 的 map,并将其值类型指定为 struct{}。然后,我们向 map 中添加了三个键,它们的值都是空结构体。最后,我们打印了 map 的长度,结果为 3。

需要注意的是,在这个示例中,我们并没有使用空结构体的任何其他特性。我们只是使用它作为 map 的值类型,因为我们不需要在 map 中存储任何值。

6. 空结构体作为方法接收器

在 Go 中,方法是一种将函数与特定类型相关联的机制。如果我们不需要访问方法中的任何接收器字段,那么可以使用空结构体作为接收器类型。下面是一个简单的示例:

 package main
 ​
 import "fmt"
 ​
 type MyStruct struct{}
 ​
 func (m MyStruct) DoSomething() {
     fmt.Println("Method is called")
 }
 ​
 func main() {
     s := MyStruct{}
     s.DoSomething()
 }
复制代码

在这个示例中,我们创建了一个名为 MyStruct 的结构体,并为其定义了一个方法 DoSomething。在这个方法中,我们只是打印一条消息。

在 main 函数中,我们创建了一个 MyStruct 实例 s,然后调用了它的 DoSomething 方法。由于我们不需要在方法中访问接收器的任何字段,所以我们可以使用空结构体作为接收器类型。

需要注意的是,即使我们在方法中使用空结构体作为接收器类型,我们仍然可以将其他参数传递给该方法。例如,我们可以像下面这样修改 DoSomething 方法:

 func (m MyStruct) DoSomething(x int, y string) {
     fmt.Println("Method is called with", x, y)
 }
复制代码

在这个示例中,我们向 DoSomething 方法添加了两个参数。然而,我们仍然可以使用空结构体作为接收器类型。

7. 空结构体作为接口实现

在 Go 中,接口是一种定义对象行为的机制。如果我们不需要实现接口的任何方法,那么可以使用空结构体作为实现。下面是一个简单的示例:

 package main
 ​
 import "fmt"
 ​
 type MyInterface interface {
     DoSomething()
 }
 ​
 type MyStruct struct{}
 ​
 func (m MyStruct) DoSomething() {
     fmt.Println("Method is called")
 }
 ​
 func main() {
     s := MyStruct{}
     var i MyInterface = s
     i.DoSomething()
 }
复制代码

在这个示例中,我们定义了一个名为 MyInterface 的接口,并为其定义了一个方法 DoSomething。我们还定义了一个名为 MyStruct 的结构体,并为其实现了 DoSomething 方法。

在 main 函数中,我们创建了一个 MyStruct 实例 s,然后将其分配给 MyInterface 类型的变量i。由于 MyStruct 实现了 DoSomething 方法,所以我们可以调用 i.DoSomething 方法,并打印出一条消息。

需要注意的是,在这个示例中,我们并没有为接口实现添加任何特殊。我们只是使用空结构体作为实现,因为我们不需要实现接口的任何方法。

8. 空结构体作为信号量

在 Go 中,我们可以使用空结构体作为信号量,以控制并发访问。下面是一个简单的示例:

 package main
 ​
 import (
     "fmt"
     "sync"
 )
 ​
 func main() {
     var wg sync.WaitGroup
     var mu sync.Mutex
     var signal struct{}
 ​
     for i := 0; i < 5; i++ {
         wg.Add(1)
         go func(id int) {
             mu.Lock()
             defer mu.Unlock()
             fmt.Println("goroutine", id, "is waiting")
             wg.Wait()
             fmt.Println("goroutine", id, "is signaled")
         }(i)
     }
 ​
     fmt.Println("main thread is sleeping")
     fmt.Println("press enter to signal all goroutines")
     fmt.Scanln()
 ​
     closeCh := make(chan struct{})
     go func() {
         for {
             select {
             case <-closeCh:
                 return
             default:
                 mu.Lock()
                 signal = struct{}{}
                 mu.Unlock()
             }
         }
     }()
 ​
     fmt.Println("all goroutines are signaled")
     close(closeCh)
     wg.Wait()
     fmt.Println("all goroutines are done")
 }
复制代码

在这个示例中,我们创建了一个 WaitGroup 和一个 Mutex,以便在多个 goroutine 之间同步。我们还定义了一个名为 signal 的空结构体。

在 for 循环中,我们启动了 5 个 goroutine。在每个 goroutine 中,我们获取 Mutex 锁,并打印一条等待消息。然后,我们使用 WaitGroup 等待所有 goroutine 完成。

在 main 函数中,我们等待一段时间,然后向所有 goroutine 发送信号。为了实现这一点,我们创建了一个名为 closeCh 的信道,并在其中创建了一个无限循环。在每次循环中,我们检查是否有 closeCh 信道收到了关闭信号。如果没有,我们获取 Mutex 锁,并将 signal 变量设置为一个空结构体。这样,所有正在等待 signal 变量的 goroutine 都会被唤醒。

最后,我们等待所有 goroutine 完成,并打印一条完成消息。

需要注意的是,在这个示例中,我们使用空结构体作为信号量,以控制并发访问。由于空结构体不占用任何内存空间,所以它非常适合作为信号量。

9. 总结

本文介绍了在 Go 中使用空结构体的8个方面。我们看到了空结构体可以作为类型、map 键、信号量和方法接收器等方面的用途。我们还看到了空结构体可以帮助我们优化内存使用和控制并发访问。

虽然空结构体非常简单,但它们是 Go 语言的重要组成部分。它们提供了一种轻量级的方式来表示没有任何状态或数据的结构体,并且可以应用于各种不同的场景中。

除了本文中讨论的用途外,空结构体还可以在其他一些场景中使用。例如,在使用 context 包时,我们可以使用空结构体来表示没有任何数据的上下文。在使用 sync 包时,我们可以使用空结构体作为 Cond.Wait 方法的参数,以便在等待条件时不占用任何内存。

当然,空结构体并不是所有问题的解决方案。在某些情况下,使用其他数据结构或技术可能会更加合适。但是,当我们需要表示一个没有任何状态或数据的结构体时,空结构体是一个非常优雅且有效的解决方案。

在本文中,我们通过示例代码深入研究了空结构体的各个用途。希望这些示例可以帮助大家更好地理解 Go 语言中空结构体的概念和用法。

Guess you like

Origin juejin.im/post/7228977496514560058