Appearance
07|结构体
结构体(struct)把一组字段放在一起,每个字段有名字和类型。运维和后端开发里,一条主机记录、一个 HTTP 请求配置、一条路由规则,都适合用结构体表达。结构体的字段在编译期就确定了类型和内存布局,访问字段时有编译期类型检查,比映射更安全。理解结构体的初始化方式、组合机制和 JSON 映射规则,是写 Go 后端代码的基本功。
一、定义与初始化
结构体用 type 和 struct 关键字定义:
go
package main
import "fmt"
type Host struct {
Name string
IP string
Role string
}
func main() {
host := Host{
Name: "web01",
IP: "192.168.10.11",
Role: "web",
}
fmt.Println(host.Name, host.IP, host.Role)
}字段名首字母大写表示导出,其他包可以访问;首字母小写表示只在当前包内可见。这个规则比 Java 的 public/private 关键字更简洁,但初学者容易忽略首字母大小写的访问控制含义。
结构体字面量有两种写法。上面是"字段名:值"的写法,字段顺序不重要。如果按定义顺序写,可以省略字段名:
go
host := Host{"web01", "192.168.10.11", "web"}省略字段名的写法在结构体字段很多时容易错位,添加新字段后还可能导致编译错误。生产代码里推荐用字段名写法,可读性更好,顺序无关。
二、零值
结构体的零值是每个字段都为对应类型的零值:
go
var host Host // Name="", IP="", Role=""这和数组的零值行为一致。结构体始终有确定的初始状态,不会因为忘记初始化而读到随机数据。
指针类型的结构体字段零值是 nil,使用前需要检查:
go
type Config struct {
Name string
Extra *Host // 零值是 nil
}
func main() {
var cfg Config
if cfg.Extra != nil {
fmt.Println(cfg.Extra.Name)
}
}三、嵌套结构体
结构体字段可以是另一个结构体,形成嵌套:
go
type Metadata struct {
Name string
Labels map[string]string
}
type ServiceCheck struct {
Metadata Metadata
URL string
Timeout int
}初始化时需要逐层指定:
go
check := ServiceCheck{
Metadata: Metadata{
Name: "api-health",
Labels: map[string]string{
"env": "prod",
},
},
URL: "https://example.com/health",
Timeout: 5,
}访问嵌套字段时逐层点取:check.Metadata.Name。
四、组合
Go 没有继承,用组合复用字段和方法。组合是把一个结构体嵌进另一个结构体(不写字段名),让外层直接访问内层的字段和方法:
go
type Base struct {
ID string
Name string
}
func (b Base) Identify() string {
return b.ID + ":" + b.Name
}
type Server struct {
Base // 组合,不是继承
IP string
Datacenter string
}
func main() {
s := Server{
Base: Base{ID: "srv01", Name: "web"},
IP: "10.0.0.1",
Datacenter: "bj",
}
fmt.Println(s.Name) // 直接访问 Base 的字段
fmt.Println(s.Identify()) // 直接调用 Base 的方法
}组合和继承的核心区别:继承是"是一个"(Server 是一个 Base),组合是"有一个"(Server 有一个 Base)。Go 的设计哲学里,组合比继承更灵活,不会产生深层继承链带来的耦合问题。
组合时如果外层和内层有同名字段,外层的字段会覆盖内层。访问内层同名字段需要显式指定内层结构体名:
go
type Outer struct {
Name string
Inner
}
type Inner struct {
Name string
}
func main() {
o := Outer{Name: "outer", Inner: Inner{Name: "inner"}}
fmt.Println(o.Name) // outer
fmt.Println(o.Inner.Name) // inner
}五、JSON 标签
结构体字段可以通过 tag 控制 JSON 序列化时的字段名和格式:
go
type Host struct {
Name string `json:"name"`
IP string `json:"ip"`
Role string `json:"role,omitempty"`
}json:"role,omitempty" 表示 Role 为空字符串时,JSON 输出里跳过这个字段。标签里的字段名用下划线风格或 camelCase,取决于接口约定。
encoding/json 包通过反射访问结构体字段,只能访问首字母大写的导出字段。首字母小写的字段会被反射跳过,JSON 输出里不会出现。这个规则常被初学者忽略,导致结构体明明有字段,序列化后却是空的。
反序列化时,JSON 字段名和 tag 对应,类型不匹配会报错:
go
raw := []byte(`{"name":"web01","ip":"192.168.10.11","port":8080}`)
var host Host
err := json.Unmarshal(raw, &host) // port 会被忽略,因为 Host 没有 port 字段如果需要严格校验未知字段,用 Decoder.DisallowUnknownFields()。
六、结构体比较
结构体只有在所有字段都可比较时才能用 == 比较:
go
type Point struct {
X int
Y int
}
a := Point{1, 2}
b := Point{1, 2}
fmt.Println(a == b) // true包含切片、映射或函数字段的结构体不能直接比较:
go
type Config struct {
Name string
Labels map[string]string // 映射不可比较
}
// c1 == c2 // 编译错误需要比较时,逐个字段对比,或用反射 reflect.DeepEqual(性能较差,慎用)。
七、常见错误
结构体字段未导出导致 JSON 为空
go
type Host struct {
name string `json:"name"` // 首字母小写,反射不可见
IP string `json:"ip"`
}name 字段不会被序列化到 JSON,无论 tag 里写什么。字段名必须首字母大写才能参与 JSON 编码。
嵌套结构体忘了逐层初始化
go
check := ServiceCheck{
Name: "api-health", // 编译错误:ServiceCheck 没有 Name 字段
}嵌套结构体的字段属于内层结构体,初始化时要写完整路径:Metadata: Metadata{Name: "api-health"}。
组合时误以为有继承多态
go
type Animal struct{}
func (a Animal) Speak() string { return "..." }
type Dog struct{ Animal }
func (d Dog) Speak() string { return "woof" }
func main() {
var a Animal = Dog{} // 编译错误:cannot use Dog as Animal
}Go 的组合不提供多态。Dog 和 Animal 是不同的类型,不能互相赋值。需要多态时用接口,不是组合。
指针字段 nil 解引用
go
type Config struct {
Host *Host
}
func main() {
var cfg Config
fmt.Println(cfg.Host.Name) // panic: nil pointer dereference
}使用指针字段前始终检查是否为 nil。