Golang中结构体详解

最大的遗憾,不是错过最好的人,而是遇见更好的人时,你已经把最好的自己用完了

Posted by yishuifengxiao on 2026-10-17

结构体基础

结构体是值类型,用于把一组字段组合成一个复合类型。

type User struct {
ID int
Name string
Age int
}

声明与初始化

// 零值:所有字段取各自零值
var u1 User // {0 "" 0}

// 键值初始化,推荐,可只写部分字段
u2 := User{
ID: 1,
Name: "Tom",
}

// 位置初始化:必须按字段顺序写全所有字段,不推荐
u3 := User{2, "Jerry", 18}

// 指针
u4 := &User{ID: 3, Name: "Alice"}
fmt.Println(u4.Name) // 自动解引用,等价 (*u4).Name

// new 返回零值指针
u5 := new(User)
u5.Name = "Bob"

注意:

  • 字段名大写:可导出,跨包可访问。
  • 字段名小写:包内私有。
  • 结构体赋值/传参是浅拷贝。如果字段包含 slice、map、指针,复制的是引用地址。
type Data struct {
Nums []int
}

a := Data{Nums: []int{1, 2}}
b := a
b.Nums[0] = 100
fmt.Println(a.Nums[0]) // 100,因为 slice 底层共享

结构体指针与函数传参

结构体较大时通常用指针,避免复制。

func update(u *User) {
u.Name = "new name"
}

u := User{Name: "old"}
update(&u)

指针字段可以为 nil,访问前要判断:

type User struct {
Profile *Profile
}

if u.Profile != nil {
fmt.Println(u.Profile.Bio)
}

匿名结构体

不需要提前定义类型,适合临时数据、测试、JSON 构造。

p := struct {
Name string
Age int
}{
Name: "Tom",
Age: 18,
}

users := []struct {
ID int
Name string
}{
{1, "A"},
{2, "B"},
}

匿名结构体类型由字段完全决定,字段相同则类型相同;但通常不建议滥用。


空结构体 struct{}

struct{} 不占空间,常见用途:

// 集合
set := map[string]struct{}{}
set["a"] = struct{}{}

// 信号通道
done := make(chan struct{})
close(done)

结构体方法

Go 没有类,但可以给结构体定义方法。

type Counter struct {
n int
}

// 值接收者:操作副本
func (c Counter) Get() int {
return c.n
}

// 指针接收者:可修改原对象
func (c *Counter) Inc() {
c.n++
}

使用:

c := Counter{}
c.Inc()
fmt.Println(c.Get())

选择建议:

  • 需要修改结构体:用指针接收者。
  • 结构体较大:用指针接收者。
  • 需要实现接口且方法集要求:注意 T 和 *T 的方法集不同。
  • 小结构体只读:可用值接收者。

方法集规则简记:

  • 类型 T 的方法集包含值接收者方法。
  • 类型 *T 的方法集包含值接收者和指针接收者方法。
  • 所以 *T 通常能实现更多接口。
type Incrementer interface {
Inc()
}

var _ Incrementer = &Counter{} // 可以
// var _ Incrementer = Counter{} // 不可以,Counter 没有 Inc 方法

嵌套结构体:命名字段

嵌套结构体就是把一个结构体作为另一个结构体的命名字段。

type Address struct {
City string
Zip string
}

type User struct {
ID int
Name string
Addr Address // 命名字段嵌套
}

初始化:

u := User{
ID: 1,
Name: "Tom",
Addr: Address{
City: "Beijing",
Zip: "100000",
},
}

访问:

fmt.Println(u.Addr.City)
u.Addr.City = "Shanghai"

特点:

  • 必须通过字段名 u.Addr 访问。
  • 内层结构体的字段不会自动提升到外层。
  • 适合表达“拥有一个”的关系,如用户拥有地址。

嵌套指针:

type User struct {
ID int
Addr *Address
}

u := User{ID: 1, Addr: &Address{City: "Beijing"}}
fmt.Println(u.Addr.City)

匿名嵌入:字段提升与组合

匿名嵌入是把类型名直接写在结构体中,不写字段名:

type Address struct {
City string
}

type User struct {
ID int
Address // 匿名嵌入,字段名就是 Address
}

初始化:

u := User{
ID: 1,
Address: Address{
City: "Beijing",
},
}

访问提升字段:

fmt.Println(u.City)        // 提升访问,等价 u.Address.City
u.City = "Shanghai"
fmt.Println(u.Address.City)

嵌入的方法也会被提升:

func (a Address) Full() string {
return "City: " + a.City
}

fmt.Println(u.Full()) // 等价 u.Address.Full()

嵌入指针

type User struct {
ID int
*Address
}

u := User{ID: 1, Address: &Address{City: "Beijing"}}
fmt.Println(u.City)

坑:嵌入指针可能为 nil,访问提升字段会 panic。

var u User
// fmt.Println(u.City) // panic

字段冲突

如果外层和内层有同名字段,外层优先:

type Address struct {
City string
}

type User struct {
Address
City string // 外层字段
}

u := User{Address: Address{City: "A"}, City: "B"}
fmt.Println(u.City) // B
fmt.Println(u.Address.City)// A

如果嵌入多个类型且有同名字段,同层冲突必须显式指定:

type A struct{ Name string }
type B struct{ Name string }

type C struct {
A
B
}

c := C{}
// fmt.Println(c.Name) // 编译错误:ambiguous selector c.Name
fmt.Println(c.A.Name)
fmt.Println(c.B.Name)

嵌入接口

结构体可以嵌入接口,常用于装饰器、代理、测试 mock:

type Reader interface {
Read() string
}

type File struct {
Reader
}

func (f File) Read() string {
return "file"
}

嵌套 vs 嵌入

写法 示例 访问 方法提升 语义
命名字段嵌套 Addr Address u.Addr.City 不提升 拥有一个 Address
匿名嵌入 Address u.City 或 u.Address.City 提升 组合/复用能力

示例对比:

// 嵌套
type User1 struct {
Addr Address
}

// 嵌入
type User2 struct {
Address
}

推荐:

  • 表达“有一个组件”用命名字段嵌套。
  • 表达“组合能力、方法提升、接口复用”用匿名嵌入。
  • 不要为了少写 .Addr 而滥用嵌入,嵌入会增加 API 面。

结构体标签与 JSON

结构体标签常用于序列化、ORM、校验等。

type User struct {
ID int `json:"id"`
Name string `json:"name,omitempty"`
Password string `json:"-"`
}
b, _ := json.Marshal(User{ID: 1, Name: "", Password: "123"})
fmt.Println(string(b)) // {"id":1}

嵌套结构体 JSON:

type User struct {
ID int `json:"id"`
Addr Address `json:"addr"`
}
// {"id":1,"addr":{"city":"Beijing"}}

匿名嵌入 JSON 默认会扁平化提升字段:

type User struct {
ID int `json:"id"`
Address
}
// {"id":1,"city":"Beijing"}

可通过标签控制:

type User struct {
Address `json:"address"`
}

反射读取标签:

t := reflect.TypeOf(User{})
f, _ := t.FieldByName("ID")
fmt.Println(f.Tag.Get("json"))

结构体比较、拷贝与 map key

如果所有字段都可比较,结构体可用 == 比较:

type Point struct {
X, Y int
}

p1 := Point{1, 2}
p2 := Point{1, 2}
fmt.Println(p1 == p2) // true

包含 slice、map、func 等不可比较字段时,不能用 ==:

type Data struct {
Nums []int
}
// d1 == d2 // 编译错误

需要深比较可用 reflect.DeepEqual,但注意性能和语义。

可比较结构体可作为 map key:

m := map[Point]string{
{1, 2}: "a",
}

构造函数

Go 没有构造函数,通常用 NewXxx:

type User struct {
ID int
Name string
}

func NewUser(id int, name string) *User {
return &User{
ID: id,
Name: name,
}
}

可做默认值、校验、初始化内部 map:

type Cache struct {
data map[string]string
}

func NewCache() *Cache {
return &Cache{data: make(map[string]string)}
}

常见坑与建议

  1. 结构体是值类型:赋值和传参默认复制。
  2. 浅拷贝:含 slice/map/指针字段时共享底层数据。
  3. 方法接收者选择:需要修改或大对象用指针接收者。
  4. 嵌入指针可能 nil:访问提升字段前要判断。
  5. 字段冲突要显式访问:c.A.Name。
  6. 跨包只能访问导出字段:字段名首字母大写。
  7. 不能包含自身值类型字段,但可包含 *Self:
    type Node struct {
    Next *Node
    }
  8. 字段顺序影响内存对齐,极端性能场景可调整字段顺序,一般业务不必过早优化。
  9. 匿名嵌入不是继承:只是字段和方法提升,本质是组合。
  10. 结构体字面量位置初始化易错:字段增删会破坏代码,推荐键值初始化。

Go 结构体的核心用法包括:定义与初始化、值/指针语义、方法、匿名结构体、空结构体、标签、JSON、比较、map key、构造函数。嵌套结构体分为两类:

  • 命名字段嵌套:Addr Address,访问 u.Addr.City,不提升字段和方法,语义清晰。
  • 匿名嵌入:Address,可 u.City 访问并提升方法,适合组合和接口复用,但要注意冲突和 nil 指针。