【Go】P14 Go语言核心利器:全面解析结构体 (Struct)
目录
- 什么是结构体 (Struct)?
- 结构体定义
- 结构体是值类型
- 结构体的初始化与实例化
- 方式一:var 声明(零值实例化)
- 方式二:使用 new 关键字
- 方式三:字面量初始化(获取值)
- 方式四:字面量初始化(获取指针)【*推荐】
- 方式五:顺序初始化(不推荐)
- 结构体方法
- 值接收者
- 指针接收者
- 结构体的嵌套与“继承”
- 结构体嵌套(组合)
- 匿名嵌套(“继承”)
- 结构体与 JSON 转换
- 序列化 (Marshal):结构体 -> JSON
- 反序列化 (Unmarshal):JSON -> 结构体
- 结构体标签 (Struct Tag)
- 总结

在编程世界中,我们经常需要处理比简单的数字、字符串或布尔值更复杂的数据。例如,一个“用户”可能包含用户名、年龄、邮箱和地址。Go 语言提供了一种强大的方式来组织和封装这些相关数据,这就是结构体 (Struct)。
Go 语言中虽然没有传统面向对象语言(如 Java 或 C++)中的 class(类) 概念。但是,Go 通过结构体来实现数据的封装,并通过方法来实现行为。这种设计赋予了 Go 语言同样的灵活性和可扩展性。
本文将带你深入探讨 Go 语言的结构体,从基础定义、实例化、方法,到高级的嵌套、JSON 转换,助你掌握这个 Go 语言的核心利器。
什么是结构体 (Struct)?
当我们需要处理复杂的事物或场景时,单一的基础数据类型(如 int 或 string)显然不够用。Go 语言允许我们自定义数据类型,将多个不同类型的基础数据封装在一起,这种自定义的数据类型就称为结构体。
简而言之,结构体是一个字段的集合。
结构体定义
在定义结构体之前,我们先要理解 Go 语言中的自定义类型。在 Go 中,我们使用 type 和 struct 关键字来定义一个结构体:
type Person struct {Name stringAge intSex string
}
这里,我们定义了一个名为 Person 的新类型。它有三个字段:Name(字符串类型)、Age(整型)和 Sex(字符串类型)。
重要提示:字段的可见性
Go 语言使用首字母大小写来控制可见性(公有或私有):
- 首字母大写:如
Name表示该字段是公有的,支持在包外被访问和修改。 - 首字母小写:如
name表示该字段是私有的,只能在当前包内部使用。
这种规则适用于结构体本身以及结构体内参数字段的限制。
结构体是值类型
这是一个核心概念:Go 语言中的结构体是值类型。
这意味着当一个结构体被赋值给另一个变量,或者作为参数传递给一个函数时,Go 会复制整个结构体,而不是复制该结构体的地址。
我们通过一个示例来证明:
package mainimport "fmt"type Person struct {Name stringAge intSex string
}func main() {p1 := Person{Name: "张三", Age: 20}// 将 p1 赋值给 p2,这里发生了值拷贝p2 := p1 // 修改 p2 的 Namep2.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 中的对象(它们是引用类型)的行为截然不同。
结构体的初始化与实例化
定义结构体只是一种蓝图,我们必须实例化它才会真正分配内存。有多种方法可以实例化结构体。
假设我们有以下结构体:
type Person struct {Name stringAge intSex string
}
方式一:var 声明(零值实例化)
我们可以像声明普通变量一样声明结构体,此时结构体的所有字段都会被初始化为其类型的零值(string 的零值是 "",int 的零值是 0)。然后我们再对其中属性进行赋值。
func main() {var p1 Personp1.Name = "张三"p1.Age = 20p1.Sex = "男"fmt.Printf("值:%v 类型:%T\n", p1, p1)// 值:{张三 20 男} 类型:main.Personfmt.Printf("值:%#v 类型:%T\n", p1, p1)// 值:main.Person{Name:"张三", Age:20, Sex:"男"} 类型:main.Person
}
方式二:使用 new 关键字
new 关键字用于分配内存。new(T) 会为类型 T 分配零值内存,并返回一个指向该内存地址的指针 (*T)。且 Go 语言为指针访问字段提供了语法糖,我们赋值时无需使用 (*p2).xx,如下述示例。
func main() {// p2 是一个指向 Person 结构体的指针var p2 *Person = new(Person) // Go 语言为指针访问字段提供了语法糖,// 我们不需要写 (*p2).Name,可以直接写 p2.Namep2.Name = "李四"p2.Age = 30fmt.Printf("p2 值:%#v 类型:%T\n", p2, p2)// p2 值:&main.Person{Name:"李四", Age:30, Sex:""} 类型:*main.Person
}
方式三:字面量初始化(获取值)
这是最常用的方式之一。我们可以在声明时直接使用键值对初始化字段。
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 中最推荐的实例化方式。它通过 & 操作符获取结构体字面量的地址,得到一个结构体指针。
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 允许在初始化时不写字段名,而是按顺序提供值:
// 极不推荐
p5 := Person{"田七", 60, "男"}
强烈不推荐这种写法。它非常脆弱,如果未来结构体字段的顺序改变或增加了新字段,代码将编译失败或产生严重的 bug。
结构体方法
如果说结构体字段(属性)是数据,那么 方法 (Methods) 就是绑定到该数据的行为。这类似于 Java 中的类方法。Go 方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {// 方法体
}
- 接收者 (Receiver):
(接收者变量 接收者类型)是方法与结构体绑定的关键。它类似于其他语言中的this或self。 - 接收者可以是值类型,也可以是指针类型。
值接收者
值接收者操作的是结构体的副本。这意味着在方法内部对结构体的修改不会影响原始结构体。
// (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
}
值接收者通常用于只读操作。
指针接收者
指针接收者操作的是结构体的指针,方法内部对结构体的修改会影响原始结构体,这是我们修改结构体状态时最常用的方式。
// (p *Person) 是指针接收者
func (p *Person) setName(newName string) {p.Name = newName
}func main() {p1 := &Person{Name: "张三", Age: 20} // p1 是 *Personfmt.Println("修改前:", p1.Name) // 修改前: 张三p1.setName("张三丰")fmt.Println("修改后:", p1.Name) // 修改后: 张三丰
}
什么时候使用值接收者 vs 指针接收者?
- 需要修改原始结构体吗?
- 是: 必须使用指针接收者。
- 否: 两者皆可。
- 结构体很大吗?
- 是: 使用指针接收者。因为值接收者会复制整个结构体,开销很大。指针接收者只复制一个指针。
- 否(如结构体只有几个小字段): 使用值接收者更安全(避免意外修改)。
经验法则: 如果不确定,优先使用指针接收者。这更高效,也符合“方法修改对象”的直觉。
结构体的嵌套与“继承”
Go 语言没有继承,但它通过 结构体嵌套(组合) 提供了更灵活的功能。
结构体嵌套(组合)
一个结构体的字段可以是另一个结构体。
// 收货地址
type Address struct {Province stringCity string
}// 用户
type User struct {Name stringAge intAddr Address // 嵌套 Address 结构体
}func main() {user := User{Name: "Alice",Age: 25,Addr: Address{Province: "广东",City: "深圳",},}fmt.Println(user.Name) // 输出: Alicefmt.Println(user.Addr.City) // 输出: 深圳
}
我们也可以嵌套结构体指针、切片或 map:
type Post struct {Title stringTags []string // 切片Meta map[string]string // MapAuthor *User // 结构体指针
}
匿名嵌套(“继承”)
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)。
import ("encoding/json""fmt"
)type Server struct {ServerName stringServerIP stringPort intprivateInfo 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 结构体中。
func main() {jsonStr := `{"ServerName":"CacheServer","ServerIP":"192.168.1.100","Port":6379}`var s2 Server// 反序列化// 注意:第二个参数必须是结构体的指针,否则函数无法修改 s2err := 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 提供了结构体标签来解决这个问题。
type Order struct {OrderID string `json:"order_id"` // JSON 中的 key 变为 order_idAmount 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 前往杭州途中
