目录
- [什么是结构体 (Struct)?](#什么是结构体 (Struct)?)
- 结构体的初始化与实例化
-
- [方式一:var 声明(零值实例化)](#方式一:var 声明(零值实例化))
- [方式二:使用 new 关键字](#方式二:使用 new 关键字)
- 方式三:字面量初始化(获取值)
- 方式四:字面量初始化(获取指针)【*推荐】
- 方式五:顺序初始化(不推荐)
- 结构体方法
- 结构体的嵌套与"继承"
- [结构体与 JSON 转换](#结构体与 JSON 转换)
-
- [序列化 (Marshal):结构体 -> JSON](#序列化 (Marshal):结构体 -> JSON)
- [反序列化 (Unmarshal):JSON -> 结构体](#反序列化 (Unmarshal):JSON -> 结构体)
- 结构体标签 (Struct Tag)
- 总结

在编程世界中,我们经常需要处理比简单的数字、字符串或布尔值更复杂的数据。例如,一个"用户"可能包含用户名、年龄、邮箱和地址。Go 语言提供了一种强大的方式来组织和封装这些相关数据,这就是结构体 (Struct)。
Go 语言中虽然没有传统面向对象语言(如 Java 或 C++)中的 class(类) 概念。但是,Go 通过结构体来实现数据的封装,并通过方法来实现行为。这种设计赋予了 Go 语言同样的灵活性和可扩展性。
本文将带你深入探讨 Go 语言的结构体,从基础定义、实例化、方法,到高级的嵌套、JSON 转换,助你掌握这个 Go 语言的核心利器。
什么是结构体 (Struct)?
当我们需要处理复杂的事物或场景时,单一的基础数据类型(如 int 或 string)显然不够用。Go 语言允许我们自定义数据类型 ,将多个不同类型的基础数据封装 在一起,这种自定义的数据类型就称为结构体。
简而言之,结构体是一个字段的集合。
结构体定义
在定义结构体之前,我们先要理解 Go 语言中的自定义类型。在 Go 中,我们使用 type 和 struct 关键字来定义一个结构体:
go
type Person struct {
Name string
Age int
Sex string
}
这里,我们定义了一个名为 Person 的新类型。它有三个字段:Name(字符串类型)、Age(整型)和 Sex(字符串类型)。
重要提示:字段的可见性
Go 语言使用首字母大小写来控制可见性(公有或私有):
- 首字母大写 :如
Name表示该字段是公有的,支持在包外被访问和修改。 - 首字母小写 :如
name表示该字段是私有的,只能在当前包内部使用。
这种规则适用于结构体本身 以及结构体内参数字段的限制。
结构体是值类型
这是一个核心概念:Go 语言中的结构体是值类型。
这意味着当一个结构体被赋值给另一个变量,或者作为参数传递给一个函数时,Go 会复制整个结构体,而不是复制该结构体的地址。
我们通过一个示例来证明:
go
package main
import "fmt"
type Person struct {
Name string
Age int
Sex string
}
func main() {
p1 := Person{Name: "张三", Age: 20}
// 将 p1 赋值给 p2,这里发生了值拷贝
p2 := p1
// 修改 p2 的 Name
p2.Name = "李四"
// p1 的 Name 并没有改变
fmt.Printf("p1: %v\n", p1) // 输出: p1: {张三 20 }
fmt.Printf("p2: %v\n", p2) // 输出: p2: {李四 20 }
}
如上所示,修改 p2 丝毫没有影响 p1,因为 p2 是 p1 的一个完整副本。这与 Java 或 Python 中的对象(它们是引用类型)的行为截然不同。
结构体的初始化与实例化
定义结构体只是一种蓝图,我们必须实例化 它才会真正分配内存。有多种方法可以实例化结构体。
假设我们有以下结构体:
go
type Person struct {
Name string
Age int
Sex string
}
方式一:var 声明(零值实例化)
我们可以像声明普通变量一样声明结构体,此时结构体的所有字段都会被初始化为其类型的零值 (string 的零值是 "",int 的零值是 0)。然后我们再对其中属性进行赋值。
go
func main() {
var p1 Person
p1.Name = "张三"
p1.Age = 20
p1.Sex = "男"
fmt.Printf("值:%v 类型:%T\n", p1, p1)
// 值:{张三 20 男} 类型:main.Person
fmt.Printf("值:%#v 类型:%T\n", p1, p1)
// 值:main.Person{Name:"张三", Age:20, Sex:"男"} 类型:main.Person
}
方式二:使用 new 关键字
new 关键字用于分配内存 。new(T) 会为类型 T 分配零值内存,并返回一个指向该内存地址的指针 (*T)。且 Go 语言为指针访问字段提供了语法糖,我们赋值时无需使用 (*p2).xx,如下述示例。
go
func main() {
// p2 是一个指向 Person 结构体的指针
var p2 *Person = new(Person)
// Go 语言为指针访问字段提供了语法糖,
// 我们不需要写 (*p2).Name,可以直接写 p2.Name
p2.Name = "李四"
p2.Age = 30
fmt.Printf("p2 值:%#v 类型:%T\n", p2, p2)
// p2 值:&main.Person{Name:"李四", Age:30, Sex:""} 类型:*main.Person
}
方式三:字面量初始化(获取值)
这是最常用的方式之一。我们可以在声明时直接使用键值对初始化字段。
go
func main() {
// 实例化一个 Person 值
p3 := Person{
Name: "王五",
Age: 40,
Sex: "男",
}
fmt.Printf("p3 值:%#v 类型:%T\n", p3, p3)
// p3 值:main.Person{Name:"王五", Age:40, Sex:"男"} 类型:main.Person
}
注意:
- 可以只初始化部分字段,未初始化的字段将是零值。
- 字段顺序可以任意。
方式四:字面量初始化(获取指针)【*推荐】
这是 Go 中最推荐 的实例化方式。它通过 & 操作符获取结构体字面量的地址,得到一个结构体指针。
go
func main() {
// 实例化一个 *Person 指针
p4 := &Person{
Name: "赵六",
Age: 50,
// Sex 字段未初始化,将是零值 ""
}
fmt.Printf("p4 值:%#v 类型:%T\n", p4, p4)
// p4 值:&main.Person{Name:"赵六", Age:50, Sex:""} 类型:*main.Person
}
这种方式结合了 new 的便利(获得指针)和字面量初始化的灵活(设置初始值)。
方式五:顺序初始化(不推荐)
Go 允许在初始化时不写字段名,而是按顺序提供值:
go
// 极不推荐
p5 := Person{"田七", 60, "男"}
强烈不推荐这种写法。它非常脆弱,如果未来结构体字段的顺序改变或增加了新字段,代码将编译失败或产生严重的 bug。
结构体方法
如果说结构体字段(属性)是数据,那么 方法 (Methods) 就是绑定到该数据的行为。这类似于 Java 中的类方法。Go 方法的定义格式如下:
go
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
// 方法体
}
- 接收者 (Receiver):
(接收者变量 接收者类型)是方法与结构体绑定的关键。它类似于其他语言中的this或self。 - 接收者可以是值类型 ,也可以是指针类型。
值接收者
值接收者操作的是结构体的副本。这意味着在方法内部对结构体的修改不会影响原始结构体。
go
// (p Person) 是值接收者
func (p Person) printInfo() {
fmt.Printf("Name: %s, Age: %d\n", p.Name, p.Age)
}
func main() {
p1 := Person{Name: "张三", Age: 20}
p1.printInfo() // 输出: Name: 张三, Age: 20
}
值接收者通常用于只读操作。
指针接收者
指针接收者操作的是结构体的指针,方法内部对结构体的修改会影响原始结构体,这是我们修改结构体状态时最常用的方式。
go
// (p *Person) 是指针接收者
func (p *Person) setName(newName string) {
p.Name = newName
}
func main() {
p1 := &Person{Name: "张三", Age: 20} // p1 是 *Person
fmt.Println("修改前:", p1.Name) // 修改前: 张三
p1.setName("张三丰")
fmt.Println("修改后:", p1.Name) // 修改后: 张三丰
}
什么时候使用值接收者 vs 指针接收者?
- 需要修改原始结构体吗?
- 是: 必须使用指针接收者。
- 否: 两者皆可。
- 结构体很大吗?
- 是: 使用指针接收者。因为值接收者会复制整个结构体,开销很大。指针接收者只复制一个指针。
- 否(如结构体只有几个小字段): 使用值接收者更安全(避免意外修改)。
经验法则: 如果不确定,优先使用指针接收者。这更高效,也符合"方法修改对象"的直觉。
结构体的嵌套与"继承"
Go 语言没有继承,但它通过 结构体嵌套(组合) 提供了更灵活的功能。
结构体嵌套(组合)
一个结构体的字段可以是另一个结构体。
go
// 收货地址
type Address struct {
Province string
City string
}
// 用户
type User struct {
Name string
Age int
Addr Address // 嵌套 Address 结构体
}
func main() {
user := User{
Name: "Alice",
Age: 25,
Addr: Address{
Province: "广东",
City: "深圳",
},
}
fmt.Println(user.Name) // 输出: Alice
fmt.Println(user.Addr.City) // 输出: 深圳
}
我们也可以嵌套结构体指针、切片或 map:
go
type Post struct {
Title string
Tags []string // 切片
Meta map[string]string // Map
Author *User // 结构体指针
}
匿名嵌套("继承")
Go 允许字段在声明时只有类型,没有字段名,这称为匿名字段 。当一个结构体匿名嵌套另一个结构体时,就实现了类似"继承"的效果。
go
type Animal struct {
Name string
}
func (a *Animal) Eat() {
fmt.Printf("%s is eating...\n", a.Name)
}
// Dog 匿名嵌套了 Animal
type Dog struct {
Animal // 匿名字段
Breed string
}
func (d *Dog) Bark() {
fmt.Printf("%s is barking...\n", d.Name)
}
func main() {
d := &Dog{
Animal: Animal{Name: "Buddy"}, // 需要这样初始化
Breed: "Golden Retriever",
}
// 字段提升:可以直接访问 Animal 的字段
fmt.Println(d.Name) // 输出: Buddy
// 方法提升:可以直接调用 Animal 的方法
d.Eat() // 输出: Buddy is eating...
d.Bark() // 输出: Buddy is barking...
}
Dog 结构体"继承"了 Animal 的 Name 字段和 Eat 方法。这种特性称为字段提升和方法提升。
这在 Go 中是实现代码复用的主要方式,它本质上是组合 (HAS-A),而非继承 (IS-A),但提供了类似继承的便利。
结构体与 JSON 转换
在现代 Web 开发中,JSON 是最常用的数据交换格式。Go 的 encoding/json 包提供了结构体与 JSON 之间无缝转换的能力。
核心要求: 一个结构体字段若想被 JSON 包处理(序列化或反序列化 ),它必须是公有的(首字母大写)。
序列化 (Marshal):结构体 -> JSON
json.Marshal 函数将 Go 结构体转换为 JSON 格式的字节切片 ([]byte)。
go
import (
"encoding/json"
"fmt"
)
type Server struct {
ServerName string
ServerIP string
Port int
privateInfo string // 私有字段
}
func main() {
s1 := Server{
ServerName: "WebServer",
ServerIP: "127.0.0.1",
Port: 8080,
privateInfo: "secret", // 此字段不会被序列化
}
// 序列化
jsonByte, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal error:", err)
return
}
// jsonByte 是 []byte 类型,我们转为 string 打印
fmt.Println(string(jsonByte))
// 输出: {"ServerName":"WebServer","ServerIP":"127.0.0.1","Port":8080}
}
反序列化 (Unmarshal):JSON -> 结构体
json.Unmarshal 函数将 JSON 格式的字节切片解析到 Go 结构体中。
go
func main() {
jsonStr := `{"ServerName":"CacheServer","ServerIP":"192.168.1.100","Port":6379}`
var s2 Server
// 反序列化
// 注意:第二个参数必须是结构体的指针,否则函数无法修改 s2
err := json.Unmarshal([]byte(jsonStr), &s2)
if err != nil {
fmt.Println("json unmarshal error:", err)
return
}
fmt.Printf("%#v\n", s2)
// 输出: main.Server{ServerName:"CacheServer", ServerIP:"192.168.1.100", Port:6379, privateInfo:""}
}
结构体标签 (Struct Tag)
我们经常遇到 Go 结构体字段与 JSON 字段不一致的情况。Go 提供了结构体标签来解决这个问题。
go
type Order struct {
OrderID string `json:"order_id"` // JSON 中的 key 变为 order_id
Amount float64 `json:"amount"`
CustomerID string `json:"customer_id,omitempty"` // omitempty 表示如果该字段为零值,则序列化时忽略它
password string `json:"-"` // - 表示无论如何都忽略此字段
}
func main() {
o1 := Order{
OrderID: "20241027",
Amount: 99.9,
}
jsonByte, _ := json.Marshal(o1)
fmt.Println(string(jsonByte))
// 输出: {"order_id":"20241027","amount":99.9}
// CustomerID 因 omitempty 被省略了
}
总结
Go 语言的结构体是其类型系统的基石。它虽然简单,但通过值/指针接收者、匿名嵌套(组合)以及强大的 encoding/json 包,构建出了一个高效、灵活且易于维护的数据模型。
掌握结构体,是从 Go 新手迈向资深开发者的关键一步。希望本文能为你打下坚实的基础。
2025.10.27 G33 前往杭州途中