【十五】Golang 结构体

发布于:2025-03-16 ⋅ 阅读:(19) ⋅ 点赞:(0)

💢欢迎来到张胤尘的开源技术站
💥开源如江河,汇聚众志成。代码似星辰,照亮行征程。开源精神长,传承永不忘。携手共前行,未来更辉煌💥

结构体

golang 语言中,结构体是一种用户自定义的数据类型,用于将多个不同类型的数据组合在一起。

定义

结构体通过关键字 struct 定义,包含一组成员变量。成员变量由名称和类型组成

type Person struct {
    Name string
    Age  int
}
  • 字段名称:字段名称是可选的,如果省略字段名称,则表示该字段是匿名字段。
  • 字段类型:字段类型可以是任意类型,包括基本类型、自定义类型、指针类型等。

初始化

结构体可以通过多种方式初始化:使用字段名初始化按字段顺序初始化使用 new 函数初始化

使用字段名初始化

指定字段名进行初始化,字段顺序可以任意。如下所示:

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func main() {
	p := Person{Name: "ycz", Age: 18}
	fmt.Println(p) // {ycz 18}
}

或者写成如下格式,但是需要注意的是,最后一个字段的结尾需要有一个逗号分隔。

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func main() {
	p := Person{
		Name: "ycz",
		Age:  18,
	}
	fmt.Println(p) // {ycz 18}
}

golang 中支持对个别字段进行初始化(未指定的字段则为零值),此时必须指定字段名称,否则会存在编译报错,如下所示:

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func main() {
	p := Person{
		Name: "ycz",
	}
	fmt.Println(p) // {ycz 0}

	// p1 := Person{"ycz"} // too few values in struct literal of type Person
	// fmt.Println(p1)
}

按字段顺序初始化

如果不指定字段名,则需要按照结构体定义中字段的顺序进行初始化。如下所示:

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func main() {
	p := Person{"ycz", 18}
	fmt.Println(p) // {ycz 18}
}

使用 new 函数初始化

使用 new 函数会分配内存并返回一个指向结构体的指针,但不会初始化结构体的字段。如下所示:

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func main() {
	p := new(Person)
	p.Name = "ycz"
	p.Age = 18
	fmt.Println(p) // &{ycz 18}
}

特别的,除了可以使用 new 函数获取指向结构体的指针,也可以结合 使用字段名初始化 或者 按字段顺序初始化 这两种方式获取指向结构体的指针。如下所示:

  • 使用字段名初始化获取指向结构体的指针
package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func main() {
	p := &Person{Name: "ycz", Age: 18}
	fmt.Println(p) // &{ycz 18}
}
  • 按字段顺序初始化获取指向结构体的指针
package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func main() {
	p := &Person{"ycz", 18}
	fmt.Println(p) // &{ycz 18}
}

这两种方式本质上是先在栈上初始化一个结构体对象(注意:不包含内存逃逸的情况),然后通过 & 取地址操作将这个结构体的地址赋值给指针变量。这种方式的核心是 先创建结构体实例,再取地址。而 new 会为 Person 结构体直接在堆空间分配内存,并将所有字段初始化为零值,然后直接返回一个指向这块内存的指针。

结构体零值

结构体的零值是其所有字段的零值的组合。其中每个字段的零值取决于该字段的类型。假设有一个结构体定义如下:

type Person struct {
    Name    string
    Age     int
    Married bool
    Address *string
}

创建一个 Person 对象,但是不进行字段的初始化,查看各个字段的零值:

package main

import "fmt"

type Person struct {
	Name    string
	Age     int
	Married bool
	Address *string
}

func main() {
	p := Person{}
	fmt.Printf("%+v\n", p) // {Name: Age:0 Married:false Address:<nil>}
}
  • Namestring 类型,零值是空字符串 ""
  • Ageint 类型,零值是 0
  • Marriedbool 类型,零值是 false
  • Address 是指针类型,零值是 nil

其他类型的零值这里不再赘述,感兴趣的同学可以查看文章《Golang 变量和常量》。

访问结构体

可访问性

结构体字段的可访问性取决于字段名的首字母。

  • 大写字母开头:字段是导出的(public),可以在包外访问。
  • 小写字母开头:字段是非导出的(private),只能在定义它的包内访问。
type Person struct {
    Name string // 可以在包外访问
    age  int    // 只能在包内访问
}

访问方式

结构体的字段可以通过点号(.)操作符访问。如下所示:

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func main() {
	p := Person{"ycz", 18}
	fmt.Println(p.Name) // ycz
    fmt.Println(p.Age)  // 18
    
    p.Age = 20
	fmt.Println(p.Age)  // 20
}

如果结构体变量是通过指针初始化的,可以直接通过指针访问字段,无需解引用。如下所示:

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func main() {
	p := new(Person)
	p.Name = "ycz"
	p.Age = 18
	
    fmt.Println(p.Name) // ycz
	fmt.Println(p.Age)  // 18
	
    p.Age = 20
	fmt.Println(p.Age) // 20
}

结构体方法

结构体方法是一种将函数与结构体类型关联的方式。通过定义方法,可以为结构体类型添加行为,使得结构体不仅能够存储数据,还能执行操作。方法的定义与普通函数类似,但需要在函数名之前指定一个接收者。如下所示:

func (接收者) 方法名(参数列表) 返回值列表 {
    // 方法体
}
  • 接收者:可以是结构体类型(T)或结构体指针(*T)。
  • 方法名:方法的名称,遵循 golang 的命名规则。
  • 参数列表:方法的输入参数。
  • 返回值列表:方法的返回值。

关于函数的更多知识点可以参考文章《Golang 函数》。

方法的调用

可访问性

方法的可访问性也同样取决于方法名称的大小写规则,这与字段的可访问性类似。

  • 大写字母开头:方法是导出的(public),可以在包外访问。
  • 小写字母开头:方法是非导出的(private),只能在定义它的包内访问。
type Person struct {
    Name string
    Age  int
}

// getName 是一个非导出的方法,只能在包内访问
func (p Person) getName() int {
    return p.Name
}

// GetAge 是一个导出的方法,可以在包外访问
func (p Person) GetAge() int {
    return p.Age
}
访问方式

方法的调用方式与字段访问类似,也是通过点号(.)操作符访问,如下所示:

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func (p Person) SayHello() {
	fmt.Printf("Hello, my name is %s.\n", p.Name)
}
func main() {
	p := Person{Name: "ycz", Age: 18}
	p.SayHello() // Hello, my name is ycz.
}

方法接收者

方法的接收者决定了方法如何与结构体实例交互。接收者可以是值类型或指针类型

值接收者

当方法的接收者是值类型(func (p Person) ...)时,调用方法时会传递结构体的一个副本。

  • 传递副本:调用方法时,结构体的值会被复制一份传递给方法。
  • 不可修改原始结构体:在方法内部对结构体的修改不会影响原始结构体,因为修改的是副本。

如下所示:

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func (p Person) SayHello() {
	fmt.Printf("Hello, my name is %s and I'm %d years old.\n", p.Name, p.Age)
	p.Age = 20
	fmt.Println(p.Age) // 20
}

func main() {
	p := Person{Name: "ycz", Age: 18}
	p.SayHello()       // Hello, my name is ycz and I'm 18 years old.
	fmt.Println(p.Age) // 18
}
指针接收者

当方法的接收者是指针类型(func (p *Person) ...)时,调用方法时会传递结构体的地址。

  • 传递地址:调用方法时,传递的是结构体的指针,而不是副本。
  • 可以修改原始结构体:在方法内部对结构体的修改会直接影响原始结构体。

如下所示:

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func (p *Person) SayHello() {
	fmt.Printf("Hello, my name is %s and I'm %d years old.\n", p.Name, p.Age)
	p.Age = 20
	fmt.Println(p.Age) // 20
}

func main() {
	p := &Person{Name: "ycz", Age: 18}
	p.SayHello()       // Hello, my name is ycz and I'm 18 years old.
	fmt.Println(p.Age) // 20
}

这里需要特别强调的一点是:如果结构体被多个协程并发访问,使用指针接收者时需要特别小心,因为指针方法可能会修改共享状态,从而导致竞争问题。在这种情况下,可能需要引入锁或其他同步机制

其他场景

即使方法的接收者是指针类型,也可以通过结构体实例调用该方法。golang 会自动将结构体实例转换为指针。如下所示:

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func (p *Person) SayHello() {
	fmt.Printf("Hello, my name is %s and I'm %d years old.\n", p.Name, p.Age)
	p.Age = 20
	fmt.Println(p.Age) // 20
}

func main() {
	p := Person{Name: "ycz", Age: 18}
	p.SayHello()       // Hello, my name is ycz and I'm 18 years old.
	fmt.Println(p.Age) // 20
}

上述代码不难看出,虽然可以通过结构体实例调用指针方法,但这种调用方式可能会掩盖一些问题,比如开发者可能误以为传递的是副本。

同样的,如果方法的接收器是值类型,也可以通过指针调用该方法,golang 会自动进行解引用指针。如下所示:

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func (p Person) SayHello() {
	fmt.Printf("Hello, my name is %s and I'm %d years old.\n", p.Name, p.Age)
	p.Age = 20
	fmt.Println(p.Age) // 20
}

func main() {
	p := &Person{Name: "ycz", Age: 18}
	p.SayHello()       // Hello, my name is ycz and I'm 18 years old.
	fmt.Println(p.Age) // 18
}

结构体的比较

结构体的比较主要用于判断两个结构体实例是否相等,使用过程中需要遵循以下基本规则:

  • 字段逐个比较:结构体的比较是通过逐个比较其字段的值来完成的。如果所有字段的值都相等,则两个结构体相等。
  • 字段顺序无关:字段的顺序不影响比较结果,只要字段的值相同即可。
  • 字段类型必须可比较:结构体的所有字段类型都必须是可比较的,否则结构体不能进行比较。

需要注意的是,并非所有类型都可以进行比较。结构体的字段类型必须满足以下条件,才能进行比较:

  • 基本类型intfloat64stringbool 等基本类型是可比较的。
  • 复合类型
    • 指针:指针类型是可比较的,比较的是指针的地址。
    • 数组:固定长度的数组是可比较的,比较的是数组中每个元素的值。
    • 结构体:结构体是可比较的,比较的是结构体中每个字段的值。
    • 切片、map 、通道:这些类型是不可比较的,因此不能作为结构体字段参与比较,如果尝试比较会导致编译错误。
package main

import "fmt"

type Point struct {
	X, Y int
}

type Line struct {
	Start, End Point
}

type LineP struct {
	Start, End *Point
}

func main() {
	p1 := Point{1, 2}
	p2 := Point{1, 2}
	fmt.Println(p1 == p2) // true

	l1 := Line{Start: p1, End: p2}
	l2 := Line{Start: p2, End: p1}
	fmt.Println(l1 == l2) // true

	lp1 := LineP{&p1, &p2}
	lp2 := LineP{&p2, &p1}
	fmt.Println(lp1 == lp2) // false
}

上述代码中,lp1lp2 中的字段 StartEnd 都是指针类型,尽管 p1p2 的值是相同的,但是比较的是它们的地址,所以输出的还是 false

另外,由于切片、map 、通道都是不可比较的字段类型,但是实际开发过程中肯定有比较的需求,所以我们可以通过手动实现一个比较方法,然后逐个字段比较。如下所示:

package main

import "fmt"

type Person struct {
	Name string
	Age  int
	Tags []string
}

func (p Person) Equal(other Person) bool {
	if p.Name != other.Name || p.Age != other.Age {
		return false
	}
	if len(p.Tags) != len(other.Tags) {
		return false
	}
	for i := range p.Tags {
		if p.Tags[i] != other.Tags[i] {
			return false
		}
	}
	return true
}

func main() {
	p1 := Person{Name: "ycz", Age: 18, Tags: []string{"developer", "gamer"}}
	p2 := Person{Name: "ycz", Age: 18, Tags: []string{"developer", "gamer"}}
	fmt.Println(p1.Equal(p2)) // true

	p3 := Person{Name: "ycz", Age: 18, Tags: []string{"developer", "gamer1"}}
	fmt.Println(p2.Equal(p3)) // false
}

对于并发场景下,如果结构体被多个协程访问或修改,比较操作可能会因为并发问题而得到不一致的结果。所以在这种情况下,需要确保结构体的访问是线程安全的

匿名结构体

匿名结构体在 golang 中是一种特殊的结构体类型,它没有显式的类型定义,而是直接定义并使用。而且,它的定义方式与普通结构体类似,但没有类型名称。可以直接在代码中定义并使用。如下所示:

package main

import "fmt"

func main() {

	// 定义并初始化一个匿名结构体
	p := struct {
		Name string
		Age  int
	}{
		Name: "ycz",
		Age:  18,
	}

	fmt.Println(p) // {ycz 18}
}

由于匿名函数使用非常的灵活,但是在实际使用过程中需要注意一些问题:

  • 匿名结构体的类型是唯一的,即使两个匿名结构体的字段完全相同,它们的类型也不同。
  • 匿名结构体的字段如果过多或嵌套过深,可能会降低代码的可读性。
  • 在初始化时,匿名结构体必须显式指定字段名(除非所有字段都按顺序初始化)。如果字段较多,初始化代码可能会显得冗长。
  • 匿名结构体没有类型名称,因此无法在其他地方复用。如果需要复用结构体类型,必须使用普通结构体。
  • 在测试和调试时会带来一些不便,因为它们的类型没有名称,调试信息可能不够直观。

结构体嵌套

结构体嵌套是指在一个结构体中包含另一个结构体作为字段。这种特性允许我们构建复杂的数据结构,实现代码的复用和层次化设计。结构体嵌套是面向对象编程中“组合”概念的体现,通过嵌套可以将多个相关的字段和行为组合在一起。

type Address struct {
	Street  string
	City    string
	Country string
}

type Person struct {
	Name    string
	Age     int
	Address Address // 嵌套结构体
}

在上述代码中,Person 结构体包含了一个 Address 类型的字段。Address 是一个独立的结构体,被嵌入到 Person 中。如果需要访问嵌套结构体的字段时,可以通过点号操作符(.)逐层访问。

package main

import "fmt"

type Address struct {
	Street  string
	City    string
	Country string
}

type Person struct {
	Name    string
	Age     int
	Address Address // 嵌套结构体
}

func main() {
	p := Person{
		Name: "ycz",
		Age:  18,
		Address: Address{
			Street:  "Changping district",
			City:    "Beijing",
			Country: "China",
		},
	}

	fmt.Println(p)                 // {ycz 18 {Changping district Beijing China}}
	p.Address.Country = "CHN"      // 修改嵌套字段
	fmt.Println(p.Address.Country) // CHN
}

嵌套结构体中的方法

嵌套结构体中的方法可以通过嵌套字段访问。如果嵌套结构体的方法与外层结构体的方法名称冲突,可以通过显式指定字段路径来解决。如下所示:

package main

import "fmt"

type Address struct {
	Street  string
	City    string
	Country string
}

func (a Address) GetCountry() string {
	return a.Country
}

type Person struct {
	Name    string
	Age     int
	Address Address // 嵌套结构体
}

func main() {
	p := Person{
		Name: "ycz",
		Age:  18,
		Address: Address{
			Street:  "Changping district",
			City:    "Beijing",
			Country: "China",
		},
	}

	fmt.Println(p)                      // {ycz 18 {Changping district Beijing China}}
	fmt.Println(p.Address.GetCountry()) // China
}

匿名字段

匿名字段是结构体中的一种字段声明方式,它没有显式的名字,而是直接使用类型名称作为字段名。匿名字段通常用于实现组合或嵌入。

  • 匿名字段的类型名称可以直接作为字段名使用。
  • 匿名字段的方法会被“提升”到外层结构体中,可以直接通过外层结构体调用。
  • 如果匿名字段的字段名或方法名与外层结构体冲突,可以通过显式指定字段路径来解决。
package main

import "fmt"

type Address struct {
	Street  string
	City    string
	Country string
}

// 匿名字段的方法
func (a Address) GetCountry() string {
	return a.Country
}

type Person struct {
	Name    string
	Age     int
	Address // 匿名字段
}

func main() {
	p := Person{
		Name: "ycz",
		Age:  18,
		Address: Address{
			Street:  "Changping district",
			City:    "Beijing",
			Country: "China",
		},
	}

	fmt.Println(p)              // {ycz 18 {Changping district Beijing China}}
	fmt.Println(p.City)         // Beijing
	fmt.Println(p.GetCountry()) // China
}

序列化与反序列化

序列化是指将数据结构或对象的状态转换为可存储或可传输的格式的过程。序列化后的数据可以保存到文件、数据库或通过网络传输。序列化的目标是将复杂的数据结构转换为一种线性的、可读或可传输的格式

反序列化是序列化的逆过程,即将序列化后的数据还原为原始的数据结构或对象。反序列化的目标是从存储或传输的格式中恢复数据的原始状态

常见的序列化格式

  • JSON:轻量级的文本格式,易于阅读和解析,广泛用于 Web 开发和跨语言通信。
  • XML:标记语言,适合复杂的数据结构,但比 JSON 更冗长。
  • Protocol BuffersGoogle 开发的二进制格式,高效且跨语言支持。
  • gobgolang 语言原生的二进制格式,仅适用于 golang 语言。
  • CSV:文本格式,适合表格数据。
  • YAML:文本格式,适合配置文件。

下面的代码主要针对 JSONXML 举例,其他格式的序列化方法这里不再赘述。

字段标签

字段标签是一种附加到结构体字段上的元数据,用于为字段提供额外的信息。字段标签在序列化、反序列化、反射等场景中非常有用,可以帮助库或框架正确地处理结构体字段。如果一个字段存在多个标签,则中间采用空格分隔。

字段标签的语法如下:

FieldName FieldType `tagKey:"tagValue" anotherKey:"anotherValue"`
  • FieldName:字段名称。
  • FieldType:字段类型。
  • tagKey:标签的键。
  • tagValue:标签的值。

JSON

golang 标准库中的 encoding/json 包提供了对 JSON 数据的序列化和反序列化支持。

序列化

将结构体对象序列化为 JSON 格式的数据,如下所示:

package main

import (
	"encoding/json"
	"fmt"
)

type Person struct {
	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`
	Age       int    `json:"age"`
}

func main() {
	p := Person{
		FirstName: "yc",
		LastName:  "z",
		Age:       18,
	}

	jsonData, err := json.Marshal(p)
	if err != nil {
		fmt.Println("error marshal JSON:", err)
		return
	}

	fmt.Println(string(jsonData)) // {"first_name":"yc","last_name":"z","age":18}

}
反序列化

JSON 格式的数据反序列化为结构体对象,如下所示:

package main

import (
	"encoding/json"
	"fmt"
)

type Person struct {
	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`
	Age       int    `json:"age"`
}

func main() {
	jsonData := []byte(`{"first_name":"yc","last_name":"z","age":18}`)
	var p Person

	err := json.Unmarshal(jsonData, &p)
	if err != nil {
		fmt.Println("error unmarshal JSON:", err)
		return
	}

	fmt.Println(p.FirstName) // yc
	fmt.Println(p.LastName)  // z
	fmt.Println(p.Age)       // 18
}

XML

golang 标准库中的 encoding/xml 包提供了对 xml 数据的序列化和反序列化支持。

序列化

将结构体对象序列化为 XML 格式的数据,如下所示:

package main

import (
	"encoding/xml"
	"fmt"
)

type Person struct {
	XMLName   xml.Name `xml:"Person"`
	FirstName string   `xml:"first_name"`
	LastName  string   `xml:"last_name"`
	Age       int      `xml:"age"`
}

func main() {
	p := Person{
		FirstName: "yc",
		LastName:  "z",
		Age:       18,
	}

	output, err := xml.MarshalIndent(p, "", "  ")
	if err != nil {
		fmt.Println("error unmarshal XML:", err)
		return
	}

	// <?xml version="1.0" encoding="UTF-8"?>
	// <Person>
	//   <first_name>yc</first_name>
	//   <last_name>z</last_name>
	//   <age>18</age>
	// </Person>
	fmt.Println(xml.Header + string(output))
}
反序列化

XML 格式的数据反序列化为结构体对象,如下所示:

package main

import (
	"encoding/xml"
	"fmt"
)

type Person struct {
	XMLName   xml.Name `xml:"Person"`
	FirstName string   `xml:"first_name"`
	LastName  string   `xml:"last_name"`
	Age       int      `xml:"age"`
}

func main() {
	data := `<?xml version="1.0" encoding="UTF-8"?>
<Person>
  <first_name>yc</first_name>
  <last_name>z</last_name>
  <age>18</age>
</Person>`

	var p Person
	err := xml.Unmarshal([]byte(data), &p)
	if err != nil {
		fmt.Println("error unmarshal XML:", err)
		return
	}

	fmt.Println(p.FirstName) // yc
	fmt.Println(p.LastName)  // z
	fmt.Println(p.Age)       // 18
}

🌺🌺🌺撒花!

如果本文对你有帮助,就点关注或者留个👍
如果您有任何技术问题或者需要更多其他的内容,请随时向我提问。
在这里插入图片描述