Go语言结构体

一、结构体定义

定义方式如下:

复制代码
type Student struct {
    ID int
    Name string
    Age int
    Score int
}

上述方法定义了一个Student类型的结构体,Student包含四个属性,分别是string类型的Nameint类型的IDAgeScore

二、结构体初始化

(一) 键值对初始化

在初始化的时候以属性:值的方式完成,如果有的属性不写,则为默认值

复制代码
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.关键点解析
  1. "has a" 而非 "is a"
    • 在传统 OOP 中,你会说 Bird 是一个 Walker(继承)。
    • 在 Go 中,我们说 Bird 有一个 Walker(组合)。Bird 是整体,Walker 是它的一部分。
  1. 方法调用的显式与隐式
    • 显式调用human.Walker.Walk()。这明确指明了调用的是哪个内嵌字段的方法。
    • 隐式调用bird.Walk()。Go 编译器会自动将这个方法"提升"到 Bird 类型上,让你可以直接调用,提供了类似继承的语法糖,但底层仍是组合。
  1. 避免多重继承问题
    • 在 C++ 中,如果一个类同时继承了 WalkerFlyer,而这两个基类又有共同的祖先,就会产生"菱形继承"问题。
    • 在 Go 中,Bird 只是简单地包含了 两个独立的组件 WalkerFlyer。它们之间没有任何关系,从根本上避免了多重继承的复杂性。如果 WalkerFlyer 有同名方法,Go 要求你必须使用显式调用来消除歧义,这保证了代码的清晰性。
  1. 灵活构建对象
    • 如果你想创建一个新的生物,比如 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 的触发机制:

  1. 自动触发 :当调用 json.Marshal() 时自动检查并调用
  2. 接口实现 :任何实现了 MarshalJSON() ([]byte, error) 方法的类型都会使用自定义序列化
  3. 优先级 :自定义 MarshalJSON 的优先级高于默认序列化逻辑
  4. 避免递归:使用别名模式来避免在自定义方法中再次触发自身

这种机制让 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

  1. 使用自定义方法返回的结果:

{"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),
    })
}
相关推荐
程序员爱钓鱼3 小时前
Go switch 详解
后端·面试·go
2651940511264806 小时前
07-告警引擎与状态机
go·监控
NutShell Wang9 小时前
Wails v3 Beta 实战:用显式对象模型重写你的第一个 Go 桌面应用
前端·人工智能·go·vibe coding
圣殿骑士-Khtangc10 小时前
Go错误处理最佳实践进阶从error到panic的完整指南
golang
运维开发笔记10 小时前
3.6 Go defer 语句学习笔记
golang
FfHUCisI11 小时前
Golang HTTP 路由设计与请求处理
开发语言·http·golang
存在morning12 小时前
【Python 开发实践 一】Python vs Go vs Java 三门语言对比
java·python·golang
SoStraw12 小时前
Go + webrpc 实战:人在外面,远程查家里 NAS 磁盘和目录
go·p2p·nas·cgo·json-rpc·webrpc·无公网ip
圣殿骑士-Khtangc1 天前
Go字符串高效拼接性能对比与底层原理分析
服务器·前端·golang
xzlAwin1 天前
Win10安装Go语言多版本管理器g工具
开发语言·golang