跳到主要内容

Struct(结构体)

结构体(struct)把一组命名字段组合成一个值。它是 Go 表达业务实体和配置数据的基础工具;结构体本身只定义数据布局,方法通过接收者声明在命名类型上。

定义与使用结构体

使用 type 为结构体声明命名类型:

package main

import "fmt"

type User struct {
ID int64
Name string
Active bool
}

func main() {
user := User{
ID: 1,
Name: "Ada",
Active: true,
}

fmt.Println(user.Name)
user.Active = false
fmt.Println(user)
}

使用点号读取或修改字段。字段名首字母大写时,该字段可以从其他包访问;小写字段只在声明它的包内可访问。

零值与初始化

结构体的零值由所有字段的零值组成,因此下面的声明可以直接使用:

var user User
fmt.Println(user.ID) // 0
fmt.Println(user.Name) // ""
fmt.Println(user.Active) // false

设计类型时,让零值具有清晰且安全的含义,可以减少额外构造步骤。但并非所有业务对象都能用零值表示有效状态;此时可以提供校验函数或惯用的 NewType 构造函数,并明确不变量。

结构体字面量推荐使用字段名:

user := User{Name: "Ada", Active: true}

未指定字段取得零值。也可以按声明顺序省略字段名,但这种写法会与字段顺序耦合,在跨包代码和字段较多时尤其脆弱:

user := User{1, "Ada", true}

结构体是值

结构体赋值或按值传参会复制整个结构体值:

package main

import "fmt"

type Point struct {
X int
Y int
}

func main() {
original := Point{X: 2, Y: 3}
cloned := original
cloned.X = 10

fmt.Println(original.X) // 2
fmt.Println(cloned.X) // 10
}

复制是逐字段复制。如果字段包含切片、映射或指针,复制后的结构体仍可能通过这些字段共享底层数据:

type Report struct {
Tags []string
}

original := Report{Tags: []string{"go"}}
cloned := original
cloned.Tags[0] = "backend"

fmt.Println(original.Tags[0]) // backend

是否需要深复制取决于对象所有权和修改规则,Go 不会自动递归复制引用的数据。

结构体指针

可以取得结构体地址,并通过指针修改原值:

user := &User{Name: "Ada"}
user.Active = true

这里可以直接写 user.Active,不必写 (*user).Active。两者含义相同,点号语法会自动完成结构体指针的解引用。

使用值还是指针应反映语义:独立小值适合复制;需要修改同一实体、避免复制较大结构体,或类型包含不应复制的字段时,通常使用指针。

为命名类型定义方法

方法通过接收者绑定到命名类型:

package main

import "fmt"

type Rectangle struct {
Width float64
Height float64
}

func (rectangle Rectangle) Area() float64 {
return rectangle.Width * rectangle.Height
}

func (rectangle *Rectangle) Scale(factor float64) {
rectangle.Width *= factor
rectangle.Height *= factor
}

func main() {
rectangle := Rectangle{Width: 3, Height: 4}
fmt.Println(rectangle.Area())

rectangle.Scale(2)
fmt.Println(rectangle.Area())
}

值接收者获得接收者的副本,指针接收者可以修改原值。接收者选择也会影响方法集和接口满足关系;同一类型的方法通常保持一致的接收者形式。

方法值、方法表达式和完整方法集规则见 Method(方法)

结构体嵌入

省略字段名、只写类型会创建嵌入字段。嵌入字段的字段和方法可以被提升,从而简化访问:

package main

import "fmt"

type Address struct {
City string
}

type Customer struct {
Name string
Address
}

func main() {
customer := Customer{
Name: "Ada",
Address: Address{City: "London"},
}

fmt.Println(customer.City)
fmt.Println(customer.Address.City)
}

嵌入表示组合,不是继承。Customer 并不是 Address 的子类型;提升成员只是选择器语法和方法集规则提供的便利。出现同名字段或方法时,仍需按 Go 的选择器规则消除歧义。

结构体的可比较性

当所有字段类型都可比较时,结构体也可比较,并能作为映射键:

type Coordinate struct {
X int
Y int
}

left := Coordinate{X: 1, Y: 2}
right := Coordinate{X: 1, Y: 2}
fmt.Println(left == right) // true

包含切片、映射或函数字段的结构体不可使用 == 比较。业务上的相等还可能需要忽略某些字段或采用领域规则,这时应定义明确的比较函数。

空结构体 struct{} 的大小为零,可用于只表达集合成员关系或发送不携带数据的信号:

seen := map[string]struct{}{
"go": {},
}

字段标签

字段标签(struct tag)是附加在字段声明上的字符串元数据。标准库的反射和编码包可以按各自规则读取标签:

package main

import (
"encoding/json"
"fmt"
)

type Response struct {
UserID int64 `json:"userId"`
Name string `json:"name"`
Internal string `json:"-"`
}

func main() {
data, err := json.Marshal(Response{UserID: 1, Name: "Ada"})
if err != nil {
fmt.Println("marshal response:", err)
return
}
fmt.Println(string(data))
}

encoding/json 默认只处理导出字段。标签本身不会自动校验数据,也不会改变 Go 字段名;它只由识别对应键名的工具解释。拼写错误或格式不当的标签可能在编译时不报错,应通过测试和静态检查验证外部数据契约。

不要复制同步状态

包含 sync.Mutexsync.Once 等同步原语的结构体在开始使用后不应再复制,否则锁保护的状态与被保护数据可能分离。此类类型通常使用指针接收者并通过指针传递:

type Cache struct {
mu sync.RWMutex
items map[string]string
}

上面是局部类型声明,完整程序需要导入 sync。可以运行 go vet ./... 检查复制锁值等常见问题。

与 TypeScript 的关键差异

TypeScript 常用对象类型或 interface 描述对象形状,并采用结构化类型检查。Go 的命名结构体是具体值类型,字段集合、导出规则、方法集和赋值规则由 Go 类型系统决定;两个字段看起来相同的命名结构体类型也不会因此自动成为同一类型。

结构体如何隐式满足接口、指针方法集如何影响赋值,见 Interface(接口)

小结

  • 结构体把命名字段组合为一个值,所有字段都有明确零值。
  • 结构体赋值会逐字段复制;切片、映射和指针字段仍可能共享数据。
  • 值接收者操作副本,指针接收者可以修改原值并影响方法集。
  • 嵌入用于组合和成员提升,不是继承。
  • 所有字段均可比较时,结构体才可比较。
  • 字段标签由具体工具解释;包含同步原语的结构体使用后不应复制。