一、结构体定义
定义方式如下:
type Student struct {
ID int
Name string
Age int
Score int
}
上述方法定义了一个Student类型的结构体,Student包含四个属性,分别是string类型的Name和int类型的ID,Age和Score。
二、结构体初始化
(一) 键值对初始化
在初始化的时候以属性:值的方式完成,如果有的属性不写,则为默认值
type Student struct {
Name string
Age int
}
func StructDemo() {
st := Student{"张三", 18}
fmt.Println(st)
}
运行结果:
![]()
(二) 值列表初始化
在初始化的时候直接按照属性顺序以属性值来初始化,看下面例子
package main
import "fmt"
type Student struct {
ID int
Name string
Age int
Score int
}
func main() {
st := Student{
101,
"lisi",
20,
97,
}
fmt.Printf("学生st: %v\n", st)
}
运行结果:
学生st: {101 lisi 20 97}
注意:以值列表的方式初始化结构体,值列表的个数必须等于结构体属性个数,且要按顺序,否则会报错
三、实例化结构体
结构体的定义只是一种内存布局的描述,只有当结构体实例化后,才会真正地分配内存空间。因此必须在定义结构体并实例化后才能使用结构体的字段。
结构体实例化就是根据结构体定义的格式创建一份与格式一致的内存区域。结构体的实例与实例之间是相互独立的实体。
(一) 基本的实例化形式
结构体本身是一种类型,可以像整型、字符串等类型一样,以 var 关键字的方式声明结构体即可完成实例化。基本实例化格式如下:
var ins T
- T:结构体类型。
- ins:结构体的实例名。
示例
type ServiceConfig struct {
Port string `json:"port"`
Address string `json:"address"`
}
func StructDemo() {
var service ServiceConfig
//打印结构体实例的地址和各成员变量默认值
fmt.Printf("&p=%p, p=%v\n", &service, service)
service.Port = "8080"
service.Address = "127.0.0.1"
//打印结构体成员信息
fmt.Printf("p=%v\n", service)
fmt.Printf("p=%#v\n", service)
//打印ServiceConfig
fmt.Printf("p=%#v\n", ServiceConfig{})
}
输出:

从运行结构可以得知,当结构体变量实例化成功后,其各成员的默认值是各自类型的零值。
(二) 创建指针类型结构体实例
在Go语言中,还可以使用 new 关键字对类型(包括结构体、整型、浮点型、字符串等)进行实例化,结构体在实例化后会返回结构体实例的指针,即结构体的地址。使用new实例化的格式如下:
ins := new(T)
- T:为类型,可以是 结构体、整型、浮点型、字符串等类型。
- ins:T类型被实例化后将其首地址保存到ins变量中,ins的类型为 *T,属于指针
结构体指针变量指向结构体实例后,同样也是使用点号(.)来访问结构体的成员。
示例:创建指针类型类型结构体实例
type ServiceConfig struct {
Port string `json:"port"`
Address string `json:"address"`
}
func StructNewDemo() {
var p = new(ServiceConfig)
//打印变量p的类型
fmt.Printf("p type: %T\n", p)
//打印结构体实例的地址和各成员变量默认值
fmt.Printf("p=%p, p=%#v\n", p, p)
p.Port = "8080"
p.Address = "192.168.1.12"
//打印结构体实例的端口
fmt.Printf("&Person.name=%p\n", &p.Port)
//打印结构体成员信息
fmt.Printf("p=%v\n", p) //%v:值的默认格式
fmt.Printf("p=%#v\n", p) //%#v:相应值的Go语法表示
}
运行结果:
p type: *demo.ServiceConfig
p=0xc0000603c0, p=&demo.ServiceConfig{Port:"", Address:""}
&Person.name=0xc0000603c0
p=&{8080 192.168.1.12}
p=&demo.ServiceConfig{Port:"8080", Address:"192.168.1.12"
使用new创建的结构体实例返回的是结构体实例的指针,指针变量p的值是该结构体实例的首地址,亦即指针变量p指向该结构体实例,并通过指针变量p访问结构体实例的成员。
(三) 取结构体地址的方式实例化
在Go语言中,对结构体进行取地址(&)操作时,视为对该类型进行一次new 的实例化操作。取地址格式如下:
ins := &T{}
- T:表示结构体类型。
- ins:表示结构体实例的引用,类型为 *T,即指针类型。
示例:取地址实例化结构体
type ServiceConfig struct {
Port string `json:"port"`
Address string `json:"address"`
}
func StructAddressDemo() {
//取地址实例化
var p = &ServiceConfig{}
//打印变量p的类型
fmt.Printf("p type: %T\n", p)
//打印结构体实例的地址和各成员变量默认值
fmt.Printf("p=%p, p=%#v\n", p, p)
p.Port = "8080"
p.Address = "192.168.1.12"
//打印结构体实例的首地址
fmt.Printf("&ServiceConfig.Port=%p\n", &p.Port)
//打印结构体成员信息
fmt.Printf("p=%v\n", p) //%v:值的默认格式
fmt.Printf("p=%#v\n", p) //%#v:相应值的Go语法表示
}
输出:
p type: *demo.ServiceConfig
p=0xc0000603c0, p=&demo.ServiceConfig{Port:"", Address:""}
&ServiceConfig.Port=0xc0000603c0
p=&{8080 192.168.1.12}
p=&demo.ServiceConfig{Port:"8080", Address:"192.168.1.12"}
可以看到,运行结果和上面的示例2的运行结果一样。其实这种方式和 new的方式是一样的
<提示> 取地址实例化是最广泛的一种结构体实例化方式。
四、初始化结构体的成员
结构体在实例化时可以直接对成员进行初始化。
初始化有两种方式:
一种是字段“键值对”形式,
另一种是字段值列表的形式。
键值对形式的初始化适合选择性填充字段较多的结构体;
字段值列表的初始化形式适合填充字段比较少的结构体。
没有初始化的结构体实例,其成员变量都是对应其类型的零值。如,数值类型为0,字符串为空,布尔类型为false,指针为nil等。
(一) 使用键值对初始化结构体
结构体使用“键值对”初始化结构体时,“键”对应的是结构体字段名,键的“值”对应的是字段初始化时的字面值。
键值对填充结构体是可选的,不需要初始化的字段可以不填入初始化列表中。键值对初始化的格式如下:
ins := 结构体类型名 {
字段1: 字段1的值,
字段2: 字段2的值,
字段3: 字段3的值,
...
}
说明:键值之间使用冒号隔开,键值对之间使用逗号隔开。需要注意的是,最后一个键值对后面的逗号不能省略。
示例1:使用键值对填充结构体的例子。
type ServiceConfig struct {
Port string `json:"port"`
Address string `json:"address"`
}
func StructValueDemo() {
p1 := ServiceConfig{
Port: "8080",
Address: "127.0.0.1",
}
fmt.Printf("p1=%#v\n", p1)
//对结构体指针进行键值对初始化
p2 := &ServiceConfig{
Port: "8081",
Address: "192.168.1.12",
}
fmt.Printf("p2=%#v\n", p2)
}
输出:
p1=demo.ServiceConfig{Port:"8080", Address:"127.0.0.1"}
p2=&demo.ServiceConfig{Port:"8081", Address:"192.168.1.12"
(二) 使用值的列表初始化结构体
使用值的列表初始化结构体的格式如下:
ins := 结构体类型名{
字段1的值,
字段2的值,
字段3的值,
...
}
使用值的列表的形式初始化结构体时,需要注意的事项:
- 必须初始化结构体的所有字段。
- 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 键值对和值列表的初始化形式不能混用。
示例2:值列表初始化结构体的例子。
type ServiceConfig struct {
Port string `json:"port"`
Address string `json:"address"`
}
func StructValueDemo2() {
p4 := ServiceConfig{
"8080",
"192.168.1.12",
}
fmt.Printf("p4=%#v\n", p4)
//也可以对结构体指针进行值列表的初始化
p5 := &ServiceConfig{
"8080",
"192.168.1.12",
}
fmt.Printf("p5=%#v\n", p5)
}
输出:
p4=demo.ServiceConfig{Port:"8080", Address:"192.168.1.12"}
p5=&demo.ServiceConfig{Port:"8080", Address:"192.168.1.12"}
(三) 初始化匿名结构体
匿名结构体的初始化由结构体定义和对初始化初始化两部分组成。结构体定义时没有结构体类型名,只有字段和类型定义。
1. 匿名结构体实例化并初始化方式1
// StructAnonymous1 匿名结构体实例化并初始化方式1
func StructAnonymous1() {
//匿名结构体实例化并初始化方式1
p1 := struct {
name string
city string
age int8
}{} //后面的{}不能省略
//初始化赋值
p1.name = "Linruxin"
p1.city = "Shenzhen"
p1.age = 24
fmt.Printf("p1=%#v\n", p1)
}
输出:
p1=struct { name string; city string; age int8 }{name:"Linruxin", city:"Shenzhen", age:24}
2. 匿名结构体实例化并初始化方式2
// StructAnonymous2 匿名结构体实例化并初始化方式2
func StructAnonymous2() {
p2 := struct {
name string
city string
age int8
}{ //使用键值对初始化
name: "Linruxin",
city: "Beijing",
age: 18,
}
fmt.Printf("p2=%#v\n", p2)
//当某些字段不需要初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值
p3 := struct {
name string
city string
age int8
}{
city: "Beijing",
}
fmt.Printf("p3=%#v\n", p3)
}
输出:
p2=struct { name string; city string; age int8 }{name:"Linruxin", city:"Beijing", age:18}
p3=struct { name string; city string; age int8 }{name:"", city:"Beijing", age:0}
3. 匿名结构体实例化并初始化方式3
// StructAnonymous3 匿名结构体实例化并初始化方式3
func StructAnonymous3() {
p4 := &struct {
name string
city string
age int8
}{ //使用值列表初始化
"Linruxin",
"London",
30,
}
fmt.Printf("p4=%#v\n", p4)
}
输出:
p4=&struct { name string; city string; age int8 }{name:"Linruxin", city:"London", age:30}
(四) 结构体成员访问
使用点号 . 操作符来访问结构体的成员,. 前可以是结构体变量或者结构体指针
package main
import "fmt"
type Student struct {
ID int
Name string
Age int
Score int
}
func main() {
st1 := Student{
ID : 100,
Name : "zhangsan",
Age : 18,
Score : 98,
}
fmt.Printf("学生1的姓名是: %s\n", st1.Name)
st2 := &Student{
ID : 101,
Name : "lisi",
Age : 20,
Score : 97,
}
fmt.Printf("学生2的分数是: %d\n", st2.Score)
}
运行结果:
学生1的姓名是: zhangsan
学生2的分数是: 97
五、构造函数 — 结构体和类型的一系列初始化操作的函数封装
Go语言的类型或结构体本身没有构造函数,但是我们可以自己使用函数封装实现。
<提示> 其他编程语言构造函数的一些常见功能及特性如下:
- 每个类可以构造函数,多个构造函数使用函数重载实现。
- 构造函数一般与类名同名,且没有返回值。
- 构造函数有一个静态构造函数,一般用这个特性来调用父类的构造函数。
- 对应C++来说,还有默认构造函数、拷贝构造函数等。
由于Go语言的结构体是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以构造函数返回的一般是结构体指针类型。
示例:手动实现结构体的构造函数。
type ServiceConfig struct {
Port string `json:"port"`
Address string `json:"address"`
}
// newServiceConfig1 构造函数1
func newServiceConfig1(port string, address string) *ServiceConfig {
return &ServiceConfig{
Port: port,
Address: address,
}
}
// newServiceConfig2 构造函数2
func newServiceConfig2(port string, address string) *ServiceConfig {
p := &ServiceConfig{}
p.Port = port
p.Address = address
return p
}
// StructConstruction 使用手动实现结构体的构造函数
func StructConstruction() {
p1 := newServiceConfig1("8080", "192.168.1.12")
fmt.Printf("p1=%#v\n", p1)
p2 := newServiceConfig2("8080", "192.168.1.12")
fmt.Printf("p2=%#v\n", p2)
}
输出:
p1=&demo.ServiceConfig{Port:"8080", Address:"192.168.1.12"}
p2=&demo.ServiceConfig{Port:"8080", Address:"192.168.1.12"}
六、结构体匿名字段和结构体内嵌
结构体实例化后,如果匿名字段的类型为结构体,那么可以直接访问匿名结构体里的所有成员,这种方式被称为结构体内嵌
1. 嵌套结构体
一个结构体中可以嵌套包含另一个结构体或是结构体指针
示例:结构体定义体中内嵌结构体类型字段。
// Address 地址结构体
type Address struct {
Province string
City string
}
// User 用户结构体
type User struct {
Name string
Gender string
Address Address
}
func NestedDemo1() {
user1 := User{
Name: "linruxin",
Gender: "man",
Address: Address{
Province: "GuoDong",
City: "Shenzhen",
},
}
fmt.Printf("user1=%#v\n", user1)
}
输出:
user1=demo.User{Name:"linruxin", Gender:"man", Address:demo.Address{Province:"GuoDong", City:"Shenzhen"}}
上面User结构体中嵌套的Address结构体也可以采用匿名字段的方式。修改示例的代码如下:
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address //匿名字段,这种没有结构体字段名而只有结构体类型的写法,就叫做结构体内嵌
}
var user2 User
user2.Name = "Zhangsan"
user2.Gender = "man"
user2.Address.Province = "GuoDong" //匿名字段默认使用类型名作为字段名
user2.City = "Shenzhen" //匿名字段的默认字段名也可以省略
fmt.Printf("user2=%#v\n", user2)
user2.City = "Shenzhen" 这种写法叫做结构体内嵌写法,当访问结构体成员时,会先在外部结构体中查找字段,找不到再去嵌套的匿名字段中查找。
2. 嵌套结构体的字段名重名冲突
嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。
示例:
// Address 地址结构体
type Address struct {
Province string
City string
CreateTime string
}
// Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}
// User 用户结构体
type User struct {
Name string
Gender string
Address
Email
}
func NestedDemo1() {
var user3 User
user3.Name = "linruxin"
user3.Gender = "man"
user3.Province = "GuoDong"
user3.City = "Shenzhen"
user3.Account = "linruxin@gmail.com"
//user3.CreateTime = "2019" //编译报错:ambiguous selector user3.CreateTime
//因为存在多个重名的
user3.Address.CreateTime = "2025" //指定Address结构体中的CreateTime
user3.Email.CreateTime = "2025" //指定Email结构体中的CreateTime
fmt.Printf("user3=%#v\n", user3)
}
输出:
user3=demo.User{Name:"linruxin", Gender:"man", Address:demo.Address{Province:"GuoDong", City:"Shenzhen", CreateTime:"2025"}, Email:demo.Email{Account:"linruxin@gmail.com", CreateTime:"2025"}
Address 和 Email 都是User结构体的内嵌结构体,这两个内嵌结构体都有一个CreateTime字段,如果直接使用结构体内嵌写法user3.CreateTime,编译的时候会报:ambiguous selector user3.CreateTime。因此,为了避免歧义,需要通过指定具体的内嵌结构体字段名。
七、结构体的“继承” — 组合
1. 使用组合思想描述对象特性
一、场景:人类与鸟类(Go 组合实现版)
我们首先定义两个基础“能力”单元:Walker(行走者)和 Flyer(飞行者)。
// Walker 定义行走能力
type Walker struct{}
// Walk 定义行走结构体方法
func (w Walker) Walk() {
fmt.Println("我正在行走")
}
// Flyer 定义飞行能力
type Flyer struct{}
// Fly 定义飞行结构体方法
func (f Flyer) Fly() {
fmt.Println("我正在飞行")
}
现在,我们不需要创建一个“行走类”让人类和鸟类去继承。相反,我们通过组合这些能力单元来构建具体的人物和鸟类。
(一) 1. 人类(组合行走能力)
人类“拥有”行走能力,而不是“是”一个行走者。
// Human 定义人类结构体
type Human struct {
Walker // 嵌入 Walker,实现组合
}
// Speak 可以有自己的独特方法
func (h Human) Speak() {
fmt.Println("我是人类,我会说话")
}
(二) 2. 鸟类(组合行走与飞行能力)
鸟类“拥有”行走能力和飞行能力。
// Bird 定义鸟类结构体
type Bird struct {
Walker // 组合行走能力
Flyer // 组合飞行能力
}
// Tweet 也可以有自己的独特方法
func (b Bird) Tweet() {
fmt.Println("我是小鸟,我会叽叽喳喳")
}
(三) 3. 完整示例:
// Walker 定义行走能力
type Walker struct{}
// Walk 定义行走结构体方法
func (w Walker) Walk() {
fmt.Println("我正在行走")
}
// Flyer 定义飞行能力
type Flyer struct{}
// Fly 定义飞行结构体方法
func (f Flyer) Fly() {
fmt.Println("我正在飞行")
}
// Human 定义人类结构体
type Human struct {
Walker // 嵌入 Walker,实现组合
}
// Speak 可以有自己的独特方法
func (h Human) Speak() {
fmt.Println("我是人类,我会说话")
}
// Bird 定义鸟类结构体
type Bird struct {
Walker // 组合行走能力
Flyer // 组合飞行能力
}
// Tweet 也可以有自己的独特方法
func (b Bird) Tweet() {
fmt.Println("我是小鸟,我会叽叽喳喳")
}
func InheritanceDemo() {
fmt.Println("=== 人类 ===")
human := Human{}
human.Walk() // 显式调用
human.Speak()
fmt.Println("\n=== 鸟类 ===")
bird := Bird{}
bird.Walk() // 隐式调用:Bird 可以直接调用 Walker 的方法
bird.Fly() // 隐式调用:Bird 可以直接调用 Flyer 的方法
bird.Tweet()
// 展示“has a”关系
fmt.Println("\n=== 类型关系验证 ===")
// human 包含一个 Walker
fmt.Printf("human 的类型是: %T\n", human)
// bird 包含一个 Walker 和一个 Flyer
fmt.Printf("bird 的类型是: %T\n", bird)
}
输出:
=== 人类 ===
我正在行走
我是人类,我会说话
=== 鸟类 ===
我正在行走
我正在飞行
我是小鸟,我会叽叽喳喳
=== 类型关系验证 ===
human 的类型是: demo.Human
bird 的类型是: demo.Bird
(四) 4.关键点解析
- “has a” 而非 “is a”:
-
- 在传统 OOP 中,你会说
Bird是一个Walker(继承)。 - 在 Go 中,我们说
Bird有一个Walker(组合)。Bird是整体,Walker是它的一部分。
- 在传统 OOP 中,你会说
- 方法调用的显式与隐式:
-
- 显式调用:
human.Walker.Walk()。这明确指明了调用的是哪个内嵌字段的方法。 - 隐式调用:
bird.Walk()。Go 编译器会自动将这个方法“提升”到Bird类型上,让你可以直接调用,提供了类似继承的语法糖,但底层仍是组合。
- 显式调用:
- 避免多重继承问题:
-
- 在 C++ 中,如果一个类同时继承了
Walker和Flyer,而这两个基类又有共同的祖先,就会产生“菱形继承”问题。 - 在 Go 中,
Bird只是简单地包含了两个独立的组件Walker和Flyer。它们之间没有任何关系,从根本上避免了多重继承的复杂性。如果Walker和Flyer有同名方法,Go 要求你必须使用显式调用来消除歧义,这保证了代码的清晰性。
- 在 C++ 中,如果一个类同时继承了
- 灵活构建对象:
-
- 如果你想创建一个新的生物,比如
Penguin(企鹅),它可以行走但不能飞行。你只需要组合Walker,而不组合Flyer。这种“按需组装”的方式非常灵活。 - 你还可以在任何结构体中覆盖内嵌字段的方法,实现更特异化的行为,这提供了类似子类重写父类方法的能力。
- 如果你想创建一个新的生物,比如
// 企鹅:能行走,但不能飞行
type Penguin struct {
Walker
}
func (p Penguin) Slide() {
fmt.Println("我可以在冰面上滑行!")
}
// 重写 Walk 方法
func (p Penguin) Walk() {
fmt.Println("我是企鹅,我走起路来摇摇晃晃")
}
2. 初始化结构体内嵌
结构体内嵌初始化时,将结构体内嵌的类型名作为字段名像普通结构体一样进行初始化
示例:车辆结构的组装和初始化。
// Wheel 车轮
type Wheel struct {
Size int
}
// Engine 引擎
type Engine struct {
Power int //功率
Type string //类型
}
// Car 车
type Car struct {
Wheel
Engine
}
func EmbeddedDemo() {
c := Car{
//初始化车轮
Wheel{
20,
},
Engine{
200,
"2.0T",
},
}
fmt.Printf("c=%+v\n", c)
}
输出:
c={Wheel:{Size:20} Engine:{Power:200 Type:2.0T}}
3. 初始化内嵌匿名结构体
在 前面描述车辆和引擎到示例中,有时考虑编写代码的便利性,会将结构体直接定义在嵌入的结构体内部。也就是说,结构体的定义不会被外部引用到。在初始化这个匿名嵌入的结构体时,就需要再次声明结构体才能赋值初始化。
示例:初始化内嵌匿名结构体的例子。
// Wheel 车轮
type Wheel struct {
Size int
}
// Car 车
type Car struct {
Wheel
//引擎,匿名结构体,Engine不是类型名而是匿名结构体的字段名
Engine struct {
Power int //功率
Type string //类型
}
}
func EmbeddedDemo2() {
c := Car{
//初始化车轮
Wheel: Wheel{
Size: 18,
},
//初始化引擎
Engine: struct {
Power int
Type string
}{
Power: 143,
Type: "2.0T",
},
}
fmt.Printf("%+v\n", c)
}
输出:
{Wheel:{Size:18} Engine:{Power:143 Type:2.0T}
本示例中,原来的Engine结构体被直接定义在Car结构体中。这种嵌入的写法就是将原来的结构体类型转换为struct {...}。
当需要对Car的Engine字段进行初始化时,由于Engine字段的类型并没有被单独定义,因此在初始化其字段时需要先写struct {...}声明其类型,然后再使用键值对的方式初始化。
八、结构体与JSON序列化
结构体与JSON数据的关系:
1、序列化:将Go语言中结构体字段 --> json格式的字符串。
2、反序列化:将json格式的字符串 --> Go语言中能够识别的结构体字段。
(一) 基础结构体定义
定义一个基本的结构体:
// Users 定义一个用户结构体
type Users struct {
Id int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
// 小写字段不会被导出(私有字段)
password string `json:"-"` // 使用 - 忽略该字段
}
(二) JSON 标签说明
JSON 标签控制着序列化和反序列化的行为:
json:"name"- 指定 JSON 字段名json:"-"- 忽略该字段json:"name,omitempty"- 如果字段为零值,则忽略json:",omitempty"- 使用原字段名,但忽略零值
(三) 序列化(结构体 → JSON)
基本序列化:
func JsonDemo1() {
//实例化一个用户新
user := Users{
Id: 1,
Name: "Linruxin",
Email: "Linruxin@163.com",
Age: 25,
IsActive: true,
CreatedAt: time.Now(),
password: "123456",
}
jsonData, err := json.Marshal(user)
if err != nil {
fmt.Println("序列化错误:", err)
return
}
fmt.Println("基本序列化:")
fmt.Println(string(jsonData))
}
输出:
{"id":1,"name":"Linruxin","email":"Linruxin@163.com","age":25,"is_active":true,"created_at":"2025-11-28T17:57:39.5341352+08:00"}
美化输出(缩进格式)
func JsonDemo1() {
//实例化一个用户新
user := Users{
Id: 1,
Name: "Linruxin",
Email: "Linruxin@163.com",
Age: 25,
IsActive: true,
CreatedAt: time.Now(),
password: "123456",
}
jsonData, err := json.MarshalIndent(user, "", " ")
if err != nil {
fmt.Println("序列化错误:", err)
return
}
fmt.Println("基本序列化:")
fmt.Println(string(jsonData))
}
输出:
基本序列化:
{
"id": 1,
"name": "Linruxin",
"email": "Linruxin@163.com",
"age": 25,
"is_active": true,
"created_at": "2025-11-28T18:09:41.4191236+08:00"
}
(四) 反序列化(JSON → 结构体)
// Users 定义一个用户结构体
type Users struct {
Id int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
// 小写字段不会被导出(私有字段)
password string `json:"-"` // 使用 - 忽略该字段
}
func UnmarshalExample() {
// JSON 字符串
jsonString := `{
"id": 3,
"name": "王五",
"email": "wangwu@example.com",
"age": 28,
"is_active": true,
"created_at": "2023-10-01T10:30:00.123456+08:00"
}`
var user Users
err := json.Unmarshal([]byte(jsonString), &user)
if err != nil {
fmt.Println("反序列化错误:", err)
return
}
fmt.Println("\n反序列化结果:")
fmt.Println(user)
fmt.Printf("user 的类型是: %T\n", user)
fmt.Printf("ID: %d, Name: %s, Email: %s, Age: %d\n",
user.Id, user.Name, user.Email, user.Age)
fmt.Printf("创建时间: %s\n", user.CreatedAt.Format("2006-01-02 15:04:05"))
}
输出:
反序列化结果:
{3 王五 wangwu@example.com 28 true 2023-10-01 10:30:00.123456 +0800 CST }
user 的类型是: demo.Users
ID: 3, Name: 王五, Email: wangwu@example.com, Age: 28
创建时间: 2023-10-01 10:30:00
(五) 处理复杂嵌套结构
代码:
// AddressDemo 地址结构
type AddressDemo struct {
City string `json:"city"`
Street string `json:"street"`
ZipCode string `json:"zip_code"`
}
// OrderDemo 订单结构
type OrderDemo struct {
OrderId string `json:"order_id"`
UserId int `json:"user_id"`
Products []string `json:"products"`
TotalPrice float64 `json:"total_price"`
Shipping AddressDemo `json:"shipping_address"`
CreatedAt time.Time `json:"created_at"`
}
func ComplexExample() {
order := OrderDemo{
OrderId: "order_id_001",
UserId: 1,
Products: []string{"笔记本电脑", "鼠标", "键盘"},
TotalPrice: 11.0,
Shipping: AddressDemo{
City: "广州市",
Street: "白云区",
ZipCode: "123456",
},
CreatedAt: time.Now(),
}
jsonData, err := json.MarshalIndent(order, "", " ")
if err != nil {
fmt.Println("\n序列化错误:", err)
}
fmt.Println(string(jsonData))
}
输出:
{
"order_id": "order_id_001",
"user_id": 1,
"products": [
"笔记本电脑",
"鼠标",
"键盘"
],
"total_price": 11,
"shipping_address": {
"city": "广州市",
"street": "白云区",
"zip_code": "123456"
},
"created_at": "2025-12-01T09:19:37.0530024+08:00"
}
(六) 自定义序列化逻辑
MarshalJSON 的触发机制:
- 自动触发:当调用
json.Marshal()时自动检查并调用 - 接口实现:任何实现了
MarshalJSON() ([]byte, error)方法的类型都会使用自定义序列化 - 优先级:自定义
MarshalJSON的优先级高于默认序列化逻辑 - 避免递归:使用别名模式来避免在自定义方法中再次触发自身
这种机制让 Go 的 JSON 序列化非常灵活,你可以在不修改原始数据结构的情况下,完全控制序列化的输出格式。
示例:
type Product struct {
Name string `json:"name"`
Price float64 `json:"price"`
Discount float64 `json:"discount,omitempty"`
}
// 自定义 JSON 输出格式 MarshalJSON 方法会在 json.Marshal() 时自动调用
func (p Product) MarshalJSON() ([]byte, error) {
fmt.Println("=== MarshalJSON 被调用了!===")
fmt.Printf("正在序列化产品: %s, 价格: %.2f, 折扣: %.2f\n", p.Name, p.Price, p.Discount)
// 自定义序列化逻辑
type Alias Product // 创建别名避免无限递归
return json.Marshal(&struct {
*Alias
FinalPrice float64 `json:"final_price"`
Savings float64 `json:"savings"`
}{
Alias: (*Alias)(&p),
FinalPrice: p.Price * (1 - p.Discount),
Savings: p.Price * p.Discount,
})
}
func CustomMarshalExample() {
product := Product{
Name: "智能手机",
Price: 2999.00,
Discount: 0.1, // 10% 折扣
}
fmt.Println("开始调用 json.Marshal...")
jsonData, _ := json.MarshalIndent(product, "", " ")
fmt.Println("序列化结果:")
fmt.Println(string(jsonData))
}
输出:
开始调用 json.Marshal...
=== MarshalJSON 被调用了!===
正在序列化产品: 智能手机, 价格: 2999.00, 折扣: 0.10
序列化结果:
{
"name": "智能手机",
"price": 2999,
"discount": 0.1,
"final_price": 2699.1,
"savings": 299.90000000000003
}
1. 深入理解触发过程
让我用一个更详细的例子来展示整个触发流程:
示例:
type CustomType struct {
Value int `json:"value"`
}
func (c CustomType) MarshalJSON() ([]byte, error) {
fmt.Printf("自定义 MarshalJSON 被调用! 类型: %s, 值: %d\n",
reflect.TypeOf(c), c.Value)
// 返回自定义的 JSON 格式
return []byte(fmt.Sprintf(`{"custom_value": %d, "doubled": %d}`,
c.Value, c.Value*2)), nil
}
func DemonstrateTrigger() {
fmt.Println("=== 演示触发机制 ===")
data := CustomType{
Value: 100,
}
fmt.Println("1. 准备调用 json.Marshal...")
fmt.Println("2. json.Marshal 检查类型是否实现 MarshalJSON 方法")
fmt.Println("3. 发现 CustomType 实现了 MarshalJSON,调用自定义方法")
result, err := json.Marshal(data)
if err != nil {
fmt.Println("错误:", err)
}
fmt.Println("4. 使用自定义方法返回的结果:")
fmt.Println(string(result))
}
// 对比:没有实现 MarshalJSON 的类型
type NormalType struct {
Value int `json:"value"`
}
func CompareWithNormal() {
fmt.Println("\n=== 对比普通类型 ===")
normal := NormalType{
Value: 20,
}
fmt.Println("调用 json.Marshal(normal)...")
fmt.Println("检查发现 NormalType 没有实现 MarshalJSON")
fmt.Println("使用默认序列化逻辑")
result, _ := json.Marshal(normal)
fmt.Println("默认序列化结果:", string(result))
}
输出:
=== 演示触发机制 ===
1. 准备调用 json.Marshal...
2. json.Marshal 检查类型是否实现 MarshalJSON 方法
3. 发现 CustomType 实现了 MarshalJSON,调用自定义方法
自定义 MarshalJSON 被调用! 类型: demo.CustomType, 值: 100
4. 使用自定义方法返回的结果:
{"custom_value":100,"doubled":200}
=== 对比普通类型 ===
调用 json.Marshal(normal)...
检查发现 NormalType 没有实现 MarshalJSON
使用默认序列化逻辑
默认序列化结果: {"value":20}
(七) 避免无限递归的陷阱
这是一个常见的错误,非常重要:
type ProductWithBug struct {
Name string `json:"name"`
Price float64 `json:"price"`
}
// ❌ 错误写法:会导致无限递归!
func (p ProductWithBug) MarshalJSON() ([]byte, error) {
// 这里又调用了 json.Marshal(p),会再次触发 MarshalJSON,无限循环!
return json.Marshal(p)
}
// ✅ 正确写法:使用别名模式
type CorrectProduct struct {
Name string `json:"name"`
Price float64 `json:"price"`
Discount float64 `json:"discount"`
}
func (p CorrectProduct) MarshalJSON() ([]byte, error) {
// 使用别名避免递归
type Alias CorrectProduct
return json.Marshal(&struct {
*Alias
FinalPrice float64 `json:"final_price"`
}{
Alias: (*Alias)(&p),
FinalPrice: p.Price * (1 - p.Discount),
})
}
4万+

被折叠的 条评论
为什么被折叠?



