Go中的匿名结构体:什么是匿名结构体,如何用,何时用?

260 阅读6分钟

原文链接:www.willem.dev/articles/an…

在项目中,你是否有遇到过一个复杂的结构定义,其中一个结构体包含在另一个结构体中?或者,有人告诉你“使用内联结构定义”? 如果你对上面的问题不确定,那么这篇文章就非常适合你。 本文将讨论匿名结构体:它们是什么以及何时使用。

什么是匿名结构体?

你一定知道使用type关键词来定义一个结构体:

type Album struct {
	ID     int
	Title  string
	Artist string
}

这是定义结构体的典型方法。该方法强制你给结构体定义一个名字,在上面示例中,该结构体的名字是Album。

该结构体的名字就可以用来定义变量的类型。如下:

var named Album

然而,这也不是定义结构体唯一的方式。你还可以定义一个结构体而不给该结构体起名字,也就是所谓的匿名结构体类型。

在使用中,Go仍然需要知道匿名结构体的每一个字段的类型。所以,你需要提供完整的结构定义来代替匿名结构的使用名称。

如果我们使用匿名结构体类型来定义个新的变量,那么它看起来像如下这样:

var anon struct{
	ID     int
	Title  string
	Artist string
}

该段代码使用整个struct{...}的定义来代替了之前Album所在的位置。

在这两个例子中,变量值默认是都零值。我们也没有给该变量赋任何值。

如果我们给匿名结构体类型的变量指定初始值,那么,你会看到会产生一些重复的代码。

首先,我们给有名字的结构体类型赋初始值时是什么样的,如下:

var named Album = Album{
	ID:     101,
	Title:  "Hot Rats",
	Artist: "Frank Zappa",
}

// ... or using a short assignment:
named := Album{
	ID:     101,
	Title:  "Hot Rats",
	Artist: "Frank Zappa",
}

然后,我们再看下给匿名结构体类型变量赋值,如下:

var anon struct{
	ID     int
	Title  string
	Artist string
} = struct{
	ID     int
	Title  string
	Artist string
}{
	ID:     101,
	Title:  "Hot Rats",
	Artist: "Frank Zappa",
}

结构体定义的代码重复了。但如果我们省略了第二个 struct{...}类型标识,我们会得到如下语法错误:

syntax error: unexpected {, expected expression
syntax error: non-declaration statement outside function body

幸运的是,我们可以通过使用 := 赋值操作符来省略第一个 struct{...}类型标识符。如下:

anon := struct{
	ID int
	Title string
	Artist string
}{
	ID:     101,
	Title:  "Hot Rats",
	Artist: "Frank Zappa",
}

下面演示了一些匿名结构体类型再不同上下文中如何使用的例子。

示例一:切片元素 该示例演示了匿名结构体作为切片元素时的情景。和其他类型一样,对于每一个切片元素不必重复定义匿名结构体类型。如下:

package main

import (
	"fmt"
)

func main() {
	s := []struct{
		ID     int
		Title  string
		Artist string
	}{
		{
			ID:     101,
			Title:  "Hot Rats",
			Artist: "Frank Zappa",
		},
		{
			ID:     123,
			Title:  "Troutmask Replica",
			Artist: "Captain Beefheart and his Magic Band",
		},
	}
	
	fmt.Println(s)
}

示例二:Map元素 该示例展示了匿名结构体作为map元素的情景。同样,在map元素中,也不必重复定义匿名结构体的类型。

package main

import (
	"fmt"
)

func main() {
	m := map[string]struct{
		ID     int
		Title  string
		Artist string
	}{
		"zappa": {
			ID:     101,
			Title:  "Hot Rats",
			Artist: "Frank Zappa",
		},
		"beefheart": {
			ID:     123,
			Title:  "Troutmask Replica",
			Artist: "Captain Beefheart and his Magic Band",
		},
	}
	
	fmt.Println(m)
}

示例三:结构体字段 在该示例中,我们会看到在一个具名结构体中有一个内嵌的匿名结构体。 在初始化该类型的变量时,需要在内部再指定该类型的定义。如下:

package main

import (
	"fmt"
)

type Discography struct {
	Name     string
	Featured struct{
		ID     int
		Title  string
		Artist string
	}
}

func main() {
	d := Discography{
		Name: "Frank Zappa",
		Featured: struct{
			ID     int
			Title  string
			Artist string
		}{
			ID:     101,
			Title:  "Hot Rats",
			Artist: "Frank Zappa",
		},
	}
	
	fmt.Println(d)
}

何时使用匿名结构体

据我所知,使用匿名结构体没有技术上的原因,它不会使你的程序更高效。

它们的价值在于在源码中它们向其他开发人员或源代码读者传达的信息。

匿名结构体是一种强调数据类型仅在非常特定的情况下才相关的方法。这种情况非常具体,以至于只需要定义该类型一次即可。 如果你最终不得不在多个地方重复匿名结构体的定义,那么使用命名定义可能会容易得多。

使用示例

根据我的经验,有两种使用匿名结构的相对常见的模式。

1、表格测试

Table测试就是使用表格来组织你的测试数据。

在表格中包含被测试函数的输入和期望的输出。例如,我们要测试一个叫做 Add的函数,该函数是计算两个整数x和y的和,我们应该会像下面这样测试: image.png

通常会使用map或slice来实现该表格。如下:

package main

import (
	"fmt"
	"testing"
)

func Add(x, y int) int {
	return x + y
}

func TestAdd(t *testing.T) {
	tests := []struct {
		x    int
		y    int
		want int
	}{
		{x: 0, y: 0, want: 0},
		{x: 1, y: 0, want: 1},
		{x: 0, y: 1, want: 1},
		{x: 1, y: 1, want: 2},
		{x: -1, y: 0, want: -1},
		{x: 0, y: -1, want: -1},
		{x: -1, y: -1, want: -2},
	}

	for _, tc := range tests {
		name := fmt.Sprintf("%d+%d=%d", tc.x, tc.y, tc.want)
		t.Run(name, func(t *testing.T) {
			got := Add(tc.x, tc.y)
			if got != tc.want {
				t.Errorf("want %d got %d", tc.want, got)
			}
		})
	}
}

这里适合使用匿名结构体是因为这些表格里的数据已经具体到这个被测试的函数中的输入和输出字段了。

2、一次性反序列化

有时你需要将格式化的数据映射到一个函数。使用中间结构体来将格式化的数据反序列化到其中是有用的。

如果此反序列化仅在源代码中的某个位置完成,则匿名结构体将是此中间结构的合适数据类型。

例如,假设我们正在创建一个将JSON串反序列化到一个函数调用的HTTP处理程序。在这种情况下,我们将message字段和number字段映射到doSomething函数。

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
)

func main() {
	in := "{\"Message\": \"Hello world\", \"Number\": 101}"
	req := httptest.NewRequest("POST", "/", strings.NewReader(in))
	rr := httptest.NewRecorder()

	// handle will map the JSON data to a function call.
	handle(rr, req)
}

func handle(w http.ResponseWriter, r *http.Request) {
	// args is an anonymous struct.
	var args struct {
		Message string
		Number  int
	}

	// decode to the anonymous struct or error.
	err := json.NewDecoder(r.Body).Decode(&args)
	if err != nil {
		http.Error(w, "invalid json", http.StatusBadRequest)
		return
	}

	// map the anonymous struct to the function call.
	doSomething(args.Message, args.Number)
}

func doSomething(msg string, number int) {
	fmt.Println(msg, number)
}

在该示例中,args结构体就是用作该中间结构体的中转类型。因为不需要在其他地方使用,所以不需要将其作为导出字段。

最后,希望本文给可以让你对匿名结构体有一些概念。我们在本文中讨论了:

  • 匿名结构体的语法
  • 匿名结构体的价值在于交流而非技术层面
  • 2个实例:表格测试和反序列化