Go 语言:结构体:定义、初始化、方法、组合、Tag、对齐

学 Go 的时候我最容易踩坑的一块就是结构体:看起来就"一个数据集合",结果一写项目发现------初始化方式一多就乱、方法接收者选错就改不了值、组合(嵌套)和"字段提升"又容易名冲突、Tag 还经常写了但解析不到......这篇就按我的节奏,把结构体真正用顺。

目录

[1. 结构体是什么:Go 的"类"替代品](#1. 结构体是什么:Go 的“类”替代品)

[2. 定义与字段可见性(封装的核心)](#2. 定义与字段可见性(封装的核心))

[2.1 基本定义](#2.1 基本定义)

[2.2 字段命名决定"能不能被外部包访问"](#2.2 字段命名决定“能不能被外部包访问”)

[2.3 结构体零值](#2.3 结构体零值)

[3. 4 种初始化方式:怎么选不踩坑](#3. 4 种初始化方式:怎么选不踩坑)

[3.1 键值对初始化(最推荐)](#3.1 键值对初始化(最推荐))

[3.2 顺序初始化(我一般不用)](#3.2 顺序初始化(我一般不用))

[3.3 new(T) 初始化(返回指针)](#3.3 new(T) 初始化(返回指针))

[3.4 &T{} 初始化(最常见的"指针创建")](#3.4 &T{} 初始化(最常见的“指针创建”))

[4. 字段访问:值 vs 指针(语法糖别误会)](#4. 字段访问:值 vs 指针(语法糖别误会))

[4.1 值类型访问](#4.1 值类型访问)

[4.2 指针访问:Go 的语法糖很舒服](#4.2 指针访问:Go 的语法糖很舒服)

[5. 方法(Method):值接收者 vs 指针接收者(重点)](#5. 方法(Method):值接收者 vs 指针接收者(重点))

[5.1 核心区别(建议直接背表)](#5.1 核心区别(建议直接背表))

[5.2 一个"踩坑级"对比](#5.2 一个“踩坑级”对比)

[6. 组合/嵌套:Go 的"继承"替代方案](#6. 组合/嵌套:Go 的“继承”替代方案)

[6.1 具名嵌套(更清晰)](#6.1 具名嵌套(更清晰))

[6.2 匿名嵌套(字段/方法提升,像"继承")](#6.2 匿名嵌套(字段/方法提升,像“继承”))

[6.3 名冲突怎么处理](#6.3 名冲突怎么处理)

[7. 结构体 Tag:JSON/DB 映射的关键](#7. 结构体 Tag:JSON/DB 映射的关键)

[7.1 JSON 序列化示例](#7.1 JSON 序列化示例)

[8. 可比较性:能不能用 ==?能不能当 Map Key?](#8. 可比较性:能不能用 ==?能不能当 Map Key?)

[9. 内存布局与对齐:字段顺序能省内存(实用)](#9. 内存布局与对齐:字段顺序能省内存(实用))

[10. Tips & 最佳实践(写项目真的省事)](#10. Tips & 最佳实践(写项目真的省事))

[11. 总结](#11. 总结)

1. 结构体是什么:Go 的"类"替代品

Go 没有 class,但结构体是 Go 唯一的自定义复合类型。我们通常用:

  • 字段封装数据

  • 方法封装行为

  • 组合(嵌套)做复用和"类似继承"的效果

    这也是 Go 做 OOP 的主路线。


2. 定义与字段可见性(封装的核心)

2.1 基本定义

复制代码
type Person struct {
    Name string
    Age  int
}

2.2 字段命名决定"能不能被外部包访问"

这点真的太重要了,很多库就是靠这个做封装:

复制代码
type User struct {
    ID   int    // 导出字段:包外可访问(Public)
    name string // 未导出字段:仅包内可访问(Private)
}

一句话记忆:首字母大写=对外暴露,小写=包内私有

2.3 结构体零值

复制代码
var p Person // Name="" Age=0

零值规律:

  • 数值:0

  • string:""

  • bool:false

  • 指针/slice/map/chan:nil


3. 4 种初始化方式:怎么选不踩坑

我按推荐程度排个序。

初始化方式 写法 推荐度 适用场景
键值对初始化 Person{Name:"A", Age:1} 5 最常用,字段多也不怕
&T{} 指针字面量 &Person{Name:"A"} 5 需要指针、避免拷贝
new(T) new(Person) 3 想要零值对象指针
顺序初始化 Person{"A",1} 1 字段少且稳定,否则易翻车

3.1 键值对初始化(最推荐)

复制代码
p1 := Person{
    Name: "Alice",
    Age:  25,
}

p2 := Person{
    Name: "Bob", // Age 自动是 0
}

3.2 顺序初始化(我一般不用)

复制代码
p := Person{"Charlie", 30}

坑点:字段顺序一改,全项目都要跟着改;字段一多更容易写错。

3.3 new(T) 初始化(返回指针)

复制代码
p := new(Person) // *Person,字段全是零值
p.Name = "David"
p.Age = 28

3.4 &T{} 初始化(最常见的"指针创建")

复制代码
p := &Person{
    Name: "Eve",
    Age:  22,
}

4. 字段访问:值 vs 指针(语法糖别误会)

4.1 值类型访问

复制代码
var p Person
p.Name = "Frank"
fmt.Println(p.Name)

4.2 指针访问:Go 的语法糖很舒服

复制代码
p := &Person{Name: "Grace", Age: 26}
p.Name = "Grace Liu" // 等价于 (*p).Name = ...
fmt.Println(p.Age)

5. 方法(Method):值接收者 vs 指针接收者(重点)

Go 方法就是"带接收者的函数"。

复制代码
func (p Person) SayHello() {}
func (p *Person) Birthday() {}

5.1 核心区别(建议直接背表)

特性 值接收者 (p Person) 指针接收者 (p *Person)
传递方式 拷贝整个结构体 拷贝指针地址
能否修改原对象 ❌ 不行(改副本) ✅ 可以(改原对象)
性能(大结构体) 开销大 开销小
调用体验 值/指针都能调 值/指针都能调(自动取地址)

5.2 一个"踩坑级"对比

复制代码
type Person struct {
    Name string
    Age  int
}

func (p Person) WrongBirthday() {
    p.Age++ // 改的是副本
}

func (p *Person) RightBirthday() {
    p.Age++ // 改的是原对象
}

func main() {
    p := Person{Name: "Henry", Age: 30}

    p.WrongBirthday()
    fmt.Println(p.Age) // 30

    p.RightBirthday()
    fmt.Println(p.Age) // 31
}

结论

  • 需要修改结构体:用指针接收者

  • 结构体很大:也优先指针接收者

  • 小结构体且只读:值接收者更"安全"


6. 组合/嵌套:Go 的"继承"替代方案

Go 不搞继承,我们用组合。

6.1 具名嵌套(更清晰)

复制代码
type Address struct {
    Province string
    City     string
}

type User struct {
    Name    string
    Age     int
    Address Address
}

u := User{Address: Address{City: "Shenzhen"}}
fmt.Println(u.Address.City)

6.2 匿名嵌套

复制代码
type User struct {
    Name string
    Age  int
    Address // 直接写类型名
}

u := User{Address: Address{City: "Hangzhou"}}
fmt.Println(u.City) // 被提升了

6.3 名冲突怎么处理

外层同名字段会"盖住"内层的,想访问内层就显式写出来:

复制代码
type A struct{ Name string }
type B struct {
    A
    Name string
}

b := B{A: A{Name:"Inner"}, Name:"Outer"}
fmt.Println(b.Name)   // Outer
fmt.Println(b.A.Name) // Inner

7. 结构体 Tag:JSON/DB 映射的关键

Tag 是写在字段后面的"元数据",常用于 JSON、数据库映射、校验等。

复制代码
type User struct {
    ID   int    `json:"user_id" db:"id"`
    Name string `json:"user_name"`
    Age  int    `json:"-"` // 忽略
}

7.1 JSON 序列化示例

复制代码
package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    ID   int    `json:"user_id"`
    Name string `json:"user_name"`
    Age  int    `json:"-"`
}

func main() {
    u := User{ID: 1, Name: "Kate", Age: 25}
    data, _ := json.Marshal(u)
    fmt.Println(string(data)) // {"user_id":1,"user_name":"Kate"}
}

Tag 常见坑(我踩过)

  • 反引号必须是 `(不是引号)

  • 格式必须 key:"value",引号必须英文双引号

  • 多个 tag 用空格分隔


8. 可比较性:能不能用 ==?能不能当 Map Key?

规则很简单:结构体能不能比较,取决于它所有字段能不能比较

复制代码
type Comparable struct {
    A int
    B string
}

type Uncomparable struct {
    A int
    B []int // slice 不可比较
}
  • Comparable 可以用 ==

  • Uncomparable 不能用 ==,也 不能作为 map 的 key


9. 内存布局与对齐:字段顺序能省内存(实用)

Go 会做内存对齐(padding),字段顺序不合理会浪费空间。

复制代码
type BadOrder struct {
    A bool
    B int64
    C bool
} // 可能更大

type GoodOrder struct {
    A bool
    C bool
    B int64
} // 可能更小

想看真实大小可以用:

复制代码
import "unsafe"
fmt.Println(unsafe.Sizeof(BadOrder{}))
fmt.Println(unsafe.Sizeof(GoodOrder{}))

10. Tips & 最佳实践

  • 结构体是值类型:赋值/传参会拷贝,大结构体优先传指针。

  • 方法接收者怎么选:

    • 要修改对象 → 指针接收者

    • 结构体大 → 指针接收者

    • 小结构体只读 → 值接收者更稳

  • 组合优先于"模拟继承":具名嵌套更清晰,匿名嵌套更方便但冲突风险更高。

  • Tag 写规范:反引号 + key:"value" + 英文双引号,别用中文引号。


11. 总结

结构体在 Go 里就是"数据 + 行为":

  • 用字段组织数据

  • 用方法定义行为(值/指针接收者决定是否能改原对象与性能)

  • 用组合(嵌套)实现复用与"类似继承"

  • 用 Tag 做序列化/映射

  • 通过字段顺序优化内存布局

相关推荐
落日漫游2 小时前
Zabbix监控实战:Linux主机全流程配置
运维·开发语言·自动化
带娃的IT创业者2 小时前
Python 异步编程完全指南(二):深入 asyncio 核心概念
开发语言·python·协程·事件循环·asyncio·异步编程
CAACoder2 小时前
CATIA/3DE CAA二次开发-ScrollWindow滚动窗口
开发语言·c++·mfc·滚动窗口
还是大剑师兰特2 小时前
Vue3 页面权限控制实战示例(路由守卫 + 权限判断)
开发语言·前端·javascript
冉冉同学2 小时前
Vibe Coding指南【道、法、术】
前端·人工智能·后端
老前端的功夫2 小时前
【Java从入门到入土】06:String的72变:从字符串拼接到底层优化
java·开发语言
啊我不会诶2 小时前
2025 北京市大学生程序设计竞赛暨“小米杯”全国邀请赛
c++·学习·算法
程序猿(雷霆之王)2 小时前
C++——AI大模型接入SDK
开发语言·c++
会编程的土豆2 小时前
【从零学javase 第六天】网络编程(+多线程)
开发语言·网络·php