【Go】P14 Go语言核心利器:全面解析结构体 (Struct)

目录

在编程世界中,我们经常需要处理比简单的数字、字符串或布尔值更复杂的数据。例如,一个"用户"可能包含用户名、年龄、邮箱和地址。Go 语言提供了一种强大的方式来组织和封装这些相关数据,这就是结构体 (Struct)

Go 语言中虽然没有传统面向对象语言(如 Java 或 C++)中的 class(类) 概念。但是,Go 通过结构体来实现数据的封装,并通过方法来实现行为。这种设计赋予了 Go 语言同样的灵活性和可扩展性。

本文将带你深入探讨 Go 语言的结构体,从基础定义、实例化、方法,到高级的嵌套、JSON 转换,助你掌握这个 Go 语言的核心利器。


什么是结构体 (Struct)?

当我们需要处理复杂的事物或场景时,单一的基础数据类型(如 intstring)显然不够用。Go 语言允许我们自定义数据类型 ,将多个不同类型的基础数据封装 在一起,这种自定义的数据类型就称为结构体

简而言之,结构体是一个字段的集合。

结构体定义

在定义结构体之前,我们先要理解 Go 语言中的自定义类型。在 Go 中,我们使用 typestruct 关键字来定义一个结构体:

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,因为 p2p1 的一个完整副本。这与 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): (接收者变量 接收者类型) 是方法与结构体绑定的关键。它类似于其他语言中的 thisself
  • 接收者可以是值类型 ,也可以是指针类型

值接收者

值接收者操作的是结构体的副本。这意味着在方法内部对结构体的修改不会影响原始结构体。

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 结构体"继承"了 AnimalName 字段和 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 前往杭州途中

相关推荐
虚行2 小时前
Go学习资料整理
golang·区块链
QX_hao2 小时前
【Go】--time包的使用
开发语言·后端·golang
IT_陈寒2 小时前
Vite 3.0终极提速指南:5个鲜为人知的配置技巧让构建效率翻倍
前端·人工智能·后端
武子康2 小时前
大数据-137 ClickHouse MergeTree 实战指南|分区、稀疏索引与合并机制 存储结构 一级索引 跳数索引
大数据·后端·nosql
二十雨辰3 小时前
[作品集]-容易宝
java·开发语言·前端
亮子AI3 小时前
【NestJS】在 nest.js 项目中,如何使用 Postgresql 来做缓存?
开发语言·缓存·node.js·nest.js
图灵信徒3 小时前
R语言数据结构与数据处理基础内容
开发语言·数据挖掘·数据分析·r语言
oioihoii3 小时前
高性能推理引擎的基石:C++与硬件加速的完美融合
开发语言·c++
weixin_456904273 小时前
基于C#的文档处理
开发语言·c#