定义和创建结构体
结构体类型就是一组具名字段。用 type Name struct { ... } 声明它,再用复合字面量创建值。
输出:
Ana 31
{Name:Ana Email:ana@example.com Age:32 Admin:false}
{Name: Email: Age:0 Admin:false}
main.User{Name:"Bob", Email:"", Age:0, Admin:false}
%+v 打印字段名,%#v 打印 Go 语法。调试时,这两种是查看结构体最快的方式。
同类型的字段可以写在一行:X, Y float64。首字母大写的字段名是导出的(其他包和 encoding/json 都能看到);小写的则不是。
字面量的写法
User{Name: "Ana", Age: 31} // named fields: order-free, omitted fields are zero
User{"Ana", "a@x.com", 31, false} // positional: every field, in order
&User{Name: "Ana"} // pointer to a new struct
new(User) // pointer to a zeroed struct
优先使用带字段名的写法。按位置的字面量在有人新增字段或调整字段顺序时就会出错,对其他包的结构体类型使用它时 go vet 还会发出警告(例如 net/url.Error struct literal uses unkeyed fields)。对于 Point{1, 2} 这种很小的本地类型,按位置写没问题。
没有默认值:零值与构造函数
Go 语言本身没有字段默认值,也没有构造函数。每个字段都从其类型的零值开始:0、""、false、nil。有两种惯用做法来弥补。
让零值可以直接使用。 sync.Mutex、bytes.Buffer 和 strings.Builder 都不需要初始化就能用。能这样设计类型时,就这样设计。
写一个 NewX 函数,用于某个字段需要非零默认值,或者有东西必须先分配(比如 map)的情况:
NewServer 返回指针,因为服务器有会修改它自身的方法,而且会被共享。对小型的不可变类型,按值返回结构体也很常见(time.Date 返回一个 time.Time)。
结构体指针
访问字段时,Go 会自动对结构体指针解引用。有了 p := &User{},你写 p.Name,而不是 (*p).Name。
输出:
{11 12}
{11 12} {0 12}
{11 0}
结构体是值。赋值和函数参数都会复制它;指针则共享同一个结构体。逐字段复制是浅复制:副本中的切片或 map 字段仍然指向同一份数据。什么时候用哪种,见指针。
匿名结构体
结构体类型可以直接内联写出,不需要名字。这适合一次性的分组、表驱动测试,以及解码只读一次的 JSON。
[]struct{ in string; want int } 这种表格是 Go 测试的标准形式。
比较结构体
当所有字段都可比较时,结构体可以用 == 比较。相等意味着所有字段都相等。可比较的结构体还可以作为 map 的键。
对于含有切片或 map 字段的结构体,写一个 Equal 方法,或者在测试中用 reflect.DeepEqual 比较。
结构体标签
标签是写在字段类型后面的字符串字面量。语言本身会忽略它;库通过反射读取它。最常见的读取者是 encoding/json:
这会打印 {"id":7,"name":"Mug"}:标签给字段改了名,omitempty 去掉了为零的 Price,未导出的字段对编码器不可见。标签遵循 key:"value" 的约定,多个键之间用空格分隔:`json:"name" db:"user_name"`。格式错误的标签也能正常编译,只是会被悄悄忽略,go vet 可以发现这类问题。JSON 一节介绍了各个选项。
空结构体与字段顺序
struct{} 没有字段,占用零字节。它被用作集合的值(map[string]struct{}),以及通道上的信号(chan struct{}),这时只关心事件本身。
字段顺序会影响内存布局:编译器会按对齐要求给字段填充。由 bool, int64, bool 组成的结构体在 64 位系统上占 24 个字节,而 int64, bool, bool 只占 16 个字节。只有类型被大量存储时,这一点才重要。
常见错误
- 修改的是副本。
for _, u := range users { u.Age++ }每次改的都是副本。请用下标:users[i].Age++。 - 忘记初始化 map 字段。 结构体的零值里是一个 nil map,向它写入会 panic。请在构造函数里分配。
- 复制含有
sync.Mutex的结构体。 副本有自己的锁状态。这类结构体请传指针;go vet会报告这种复制。 - 对其他包的类型使用按位置的字面量。 一旦新增字段,代码就会出错。
常见问题
Go 中如何初始化结构体?
用复合字面量。最常见的写法是带字段名:u := User{Name: "Ana", Age: 31},省略的字段都取零值。User{} 得到所有字段都是零值的结构体,&User{...} 得到一个指针,new(User) 得到指向零值结构体的指针。
Go 的结构体字段支持默认值吗?
不支持。每个字段都从其类型的零值开始。惯用做法是写一个构造函数 func NewServer() *Server { return &Server{Port: 8080} },或者把类型设计成零值就能直接使用,就像 sync.Mutex 和 bytes.Buffer 那样。
Go 中如何比较两个结构体?
如果每个字段都可比较,就用 ==。所有对应字段都相等时,两个结构体值才相等。包含切片、map 或函数字段的结构体不能用 == 比较(会编译出错);请自己逐个比较字段,或者在测试中用 reflect.DeepEqual。
Go 中的结构体标签是什么?
写在字段类型后面的字符串字面量,例如 `json:"name,omitempty"`。编译器会忽略它们;encoding/json、数据库驱动和校验库等包通过反射读取标签,以控制字段如何编码、命名或校验。
Go 中如何打印带字段名的结构体?
用 fmt.Printf("%+v\n", s),它会打印 {Name:Ana Age:31}。%v 只打印值,%#v 打印包含类型名的 Go 语法:main.User{Name:"Ana", Age:31}。