GO语言中的结构体struct
结构体
- 结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。
- 结构体是由0个或多个任意类型的值聚合成的实体,每个值都可以被称为结构体的成员。
特性
- 结构体的成员也可以被称为"字段",具有以下特性:
- 字段拥有自己的类型和值。
- 字段名必须唯一。
- 字段的类型也可以是结构体;甚至是字段所在结构体的类型的指针类型。
- 字段的首字母决定其可访问性。
自定义类型
package main
import "fmt"
type t1 int
type t2 t1
func main() {
var a t1 = 42
var b t2 = 41
fmt.Println(a, b)
fmt.Printf("a type of is %T\n", a)
fmt.Printf("b type of is %T\n", b)
t3 := t2(a)
fmt.Println(t3)
}
//运行结果如下:
42 41
a type of is main.t1
b type of is main.t2
42
如果一个新类型是基于某个 Go 原生类型定义的,那么我们就叫 Go 原生类型为新类型的底层类型(Underlying Type)
类型别名
官网示例
type T = S // type 别名 = 类型名
示例
package main
import "fmt"
func main() {
type T = string
var str1 string = "hello world string"
var str T = str1
fmt.Println(str)
fmt.Printf("T: %T str1: %T\n", str, str1)
}
//运行结果如下:
hello world string
T: string str1: string
自定义类型与类型别名的区别
- 自定义类型:使用
type
定义一个新的类型,它与现有的类型没有直接关系,即使底层类型相同;这意味着自定义类型不能直接赋值给其他类型,也不能比较。 - 类型别名:使用
type
定义一个类型别名,它与现有的类型是完全相同的,类型别名可以直接赋值给其底层类型,也可以直接比较。
定义一个结构体类型
声明形式
type T struct{
field1 T1
filed2 T2
....
}
说明
:
T
表示自定义结构体的名称,在同一包内不能包含重复的类型名。struct{}
表示结构体类型;type T struct{}
可以理解为将 struct 结构体定义为类型名的类型。
定义一个空结构体
package main
import "unsafe"
type ks struct{}
func main(){
var s ks
println(unsafe.Sizeof(s))
}
输出的空结构体类型变量的大小为 0,也就是说,空结构体类型变量的内存占用为 0。基于空结构体类型内存零开销这样的特性,我们在日常 Go 开发中会经常使用空结构体类型元素,作为一种“事件”信息进行 Goroutine 之间的通信。
使用其他结构体作为自定义结构体中字段的类型
package main
import "fmt"
type Address struct {
City, Country string
}
type Person struct {
Name string
Age int
Address
}
func main() {
address := Address{"kaizhou", "China chongqing"}
person := Person{
"xiaoming",
18,
address,
}
fmt.Println(person)
fmt.Println(person.Address.City, person.Address.Country)
fmt.Println(person.Name, person.Age)
}
//运行结构如下:
{xiaoming 18 {kaizhou China chongqing}}
kaizhou China chongqing
xiaoming 18
结构体变量的声明与初始化
零值可用
在Go语言中,当声明一个变量但没有显式赋值时,这个变量会被初始化为其对应类型的零值。对于结构体类型来说,使用结构体的零值作为初始值意味着将所有字段都设置为其类型的零值。
package main
import "fmt"
type Address struct {
City string
Country string
}
func main() {
var addr Address = Address{}
fmt.Println("Address : ", addr)
}
//运行结果如下:
Address : { }
使用复合字面值
- 对结构体变量进行显示初始化,按顺序依次给每个结构体字段进行赋值(不推荐)。
package main
import "fmt"
type Address struct {
City, Country string
}
type Person struct {
Name string
Age int
Address
}
func main() {
address := Address{"kaizhou", "China chongqing"}
person := Person{
"xiaoming",
18,
address,
}
fmt.Println(person)
fmt.Println(person.Address.City, person.Address.Country)
fmt.Println(person.Name, person.Age)
}
//运行结构如下:
{xiaoming 18 {kaizhou China chongqing}}
kaizhou China chongqing
xiaoming 18
弊端
-
依赖字段顺序:使用显示初始化时,需要按照结构体定义中字段的顺序给字段赋值。如果结构体的字段很多,或者字段顺序发生变化,就需要小心确保赋值的正确顺序。
-
可读性下降:当结构体具有大量字段时,逐个指定字段值可能会导致代码变得冗长和难以阅读。
2、Go 推荐我们用 “field:value” 形式的符合字面值,对结构体类型进行显示初始化。
package main
import "fmt"
type Address struct {
City, Country string
}
type Person struct {
Name string
Age int
Address
}
func main() {
address := Address{"kaizhou", "China chongqing"}
person := Person{
Name:"xiaoming",
Age:18,
Address:address,
}
fmt.Println(person)
fmt.Println(person.Address.City, person.Address.Country)
fmt.Println(person.Name, person.Age)
}
//运行结构如下:
{xiaoming 18 {kaizhou China chongqing}}
kaizhou China chongqing
xiaoming 18
3、使用特定的构造函数
package main
import "fmt"
type Person struct {
Name string
Age int
}
func NewPerson(name string, age int) Person {
return Person{
Name: name,
Age: age,
}
}
func main() {
p := NewPerson("John", 18)
fmt.Printf("%+v\n", p)
fmt.Println(p.Name)
fmt.Println(p.Age)
}
//运行结果如下:
使用这种方式也有其利弊;如下:
- 封装复杂逻辑:构造函数可以封装一些复杂的逻辑,例如对字段进行验证或设置默认值等,以确保结构体对象的合法性。
- 提供灵活初始化选项:构造函数可以接收不同的参数组合,以提供灵活的初始化选项,使得创建结构体对象更加方便。
- 隐藏实现细节:通过使用构造函数,可以隐藏结构体的内部实现细节,使得代码更具模块化和封装性。
然而,使用构造函数的方式也存在一些潜在的弊端:
- 需要显式调用构造函数:相对于使用字面值初始化结构体对象的方式,使用构造函数需要显式调用函数来创建对象,增加了一些额外的代码。
- 需要定义额外的构造函数:如果结构体有多个初始化逻辑或需要提供不同的初始化选项,可能需要定义多个构造函数,增加了一些代码复杂性。
组合结构体
通过在一个结构体中嵌入另一个结构体,可以实现组合关系。嵌入的结构体可以访问被嵌入结构体的字段和方法,从而实现了一种类似继承的效果。
package main
import "fmt"
type Person struct {
Name string
Age int
}
type Class struct {
Person
}
func (P Person) NewPerson(name string, age int) Person {
return Person{
Name: name,
Age: age,
}
}
func main() {
C1 := Class{}
p := C1.NewPerson("John", 18)
fmt.Printf("%+v\n", p)
fmt.Println(p.Name)
fmt.Println(p.Age)
}
//运行原因如下:
{Name:John Age:18}
John
18
接口
通过定义接口,可以实现多态,使不同的类型可以被统一处理。一个类型只需要实现了接口中定义的方法,就可以被视为该接口的实现类型。
package main
import "fmt"
type Animal interface {
Speak()
}
type Cat struct {
Name string
Color string
}
type Dog struct {
Name string
Breed string
}
func (c Cat) Speak() {
fmt.Println("Cat", c)
}
func (d Dog) Speak() {
fmt.Println("Dog", d)
}
func main() {
animals := []Animal{
Cat{Name: "Cat", Color: "red"},
Dog{Name: "Dog", Breed: "dog"},
}
for _, animal := range animals {
animal.Speak()
}
}
//运行结果如下:
Cat {Cat red}
Dog {Dog dog}
注意
:
- 组合的限制:通过组合来实现类似继承的效果时,无法直接访问被嵌入结构体的私有字段和方法。只能通过嵌入结构体的公开字段和方法间接访问。
- 方法重写:通过组合或接口实现的方法,可以在子类型中进行重写,以改变其行为。这样可以实现多态性。
- 父子类型转换:通过组合或接口实现的父类型可以被转换为子类型,但需要进行显式的类型断言或类型转换操作。
结构体标签
在 Go 语言中,结构体字段标签(Struct Tag)是一种用于为结构体字段附加元数据的机制。字段标签是一个字符串,可以在结构体字段的定义中使用反引号括起来,位于字段类型和字段名之间。
结构体字段标签的主要作用是为结构体的字段提供额外的信息,例如字段的序列化格式、数据库映射、表单验证等。标签字符串可以被反射机制读取和解析,以便在运行时根据标签的信息进行相应的处理。
Tag 序列化的格式有以下几种
- JSON 格式
type Person struct {
Name string `json:"name"`
Age int `json:"age,string"`
Height float64 `json:"height,number"`
Email string `json:"email,omitempty"`
}
- 可以使用
json
标签来指定字段在 JSON 序列化和反序列化时的名称和行为。常用的标签选项有:omitempty
:如果字段的值为空值(零值或空引用),则在序列化时忽略该字段。string
:将字段的值转换为 JSON 字符串。number
:将字段的值转换为 JSON 数字。omitempty,number
:如果字段的值为空值,则在序列化时忽略该字段;否则,将字段的值转换为 JSON 数字。
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age,string"`
Height float64 `json:"height,number"`
Email string `json:"email,omitempty"`
}
func main() {
p := Person{
Name: "Alice",
Age: 25,
Height: 170.5,
Email: "",
}
// JSON 序列化
jsonData, err := json.Marshal(p)
if err != nil {
fmt.Println("JSON serialization error:", err)
return
}
fmt.Println(string(jsonData)) // {"name":"Alice","age":"25","height":170.5}
// JSON 反序列化
var p2 Person
err = json.Unmarshal(jsonData, &p2)
if err != nil {
fmt.Println("JSON deserialization error:", err)
return
}
fmt.Println(p2) // {Alice 25 170.5 }
}
- XML 格式
type Person struct {
Name string `xml:"name"`
Age int `xml:"age"`
Gender string `xml:"gender,omitempty"`
}
- 可以使用
xml
标签来指定字段在 XML 序列化和反序列化时的名称和行为。常用的标签选项有:attr
:将字段序列化为 XML 元素的属性而不是子元素。omitempty
:如果字段的值为空值(零值或空引用),则在序列化时忽略该字段。
package main
import (
"encoding/xml"
"fmt"
)
type Person struct {
Name string `xml:"name"`
Age int `xml:"age,omitempty"`
Height int `xml:"height,attr"`
}
func main() {
p := Person{
Name: "Alice",
Age: 0,
Height: 170,
}
// XML 序列化
xmlData, err := xml.Marshal(p)
if err != nil {
fmt.Println("XML serialization error:", err)
return
}
fmt.Println(string(xmlData)) // <Person height="170"><name>Alice</name></Person>
// XML 反序列化
var p2 Person
err = xml.Unmarshal(xmlData, &p2)
if err != nil {
fmt.Println("XML deserialization error:", err)
return
}
fmt.Println(p2) // {Alice 0 170}
}
- CSV 格式
- 可以使用
csv
标签来指定字段在 CSV 序列化和反序列化时的名称和行为。
type Person struct {
Name string `csv:"name"`
Age int `csv:"age"`
Gender string `csv:"gender"`
}
package main
import (
"encoding/csv"
"fmt"
"strings"
)
type Person struct {
Name string `csv:"name"` // 指定字段在CSV序列化和反序列化时的名称为"name"
Age int `csv:"age"` // 指定字段在CSV序列化和反序列化时的名称为"age"
Height int `csv:"-"` // 忽略该字段在CSV序列化和反序列化时的行为
}
func main() {
p := Person{
Name: "Forest",
Age: 24,
Height: 170,
}
// CSV 序列化
csvData, err := toCSV(p)
if err != nil {
fmt.Println("CSV serialization error:", err)
return
}
fmt.Println(csvData) // name,age,height,Forest,24,170
// CSV 反序列化
p2, err := fromCSV(csvData)
if err != nil {
fmt.Println("CSV deserialization error:", err)
return
}
fmt.Println(p2) // {age 0 0}
}
// toCSV将给定的Person结构体转换为CSV格式的字符串
func toCSV(p Person) (string, error) {
fields := make([]string, 0)
values := make([]string, 0)
// 将name字段添加到fields和values切片中
fields = append(fields, "name")
values = append(values, p.Name)
// 将age字段添加到fields和values切片中
fields = append(fields, "age")
values = append(values, fmt.Sprintf("%d", p.Age))
// 如果Height字段不为0,则将height字段添加到fields和values切片中
if p.Height != 0 {
fields = append(fields, "height")
values = append(values, fmt.Sprintf("%d", p.Height))
}
// 创建一个空的字符串切片来保存CSV记录
record := make([]string, 0)
// 将fields切片的内容追加到record切片中
record = append(record, fields...)
// 将values切片的内容追加到record切片中
record = append(record, values...)
// 创建一个strings.Builder来构建CSV数据
w := &strings.Builder{}
// 创建一个csv.Writer,并将其与strings.Builder关联
csvWriter := csv.NewWriter(w)
// 将record作为CSV记录写入csv.Writer
if err := csvWriter.Write(record); err != nil {
return "", err
}
// 刷新csv.Writer以确保所有数据都写入strings.Builder
csvWriter.Flush()
// 检查csv.Writer是否有错误
if err := csvWriter.Error(); err != nil {
return "", err
}
// 返回strings.Builder中的CSV数据作为字符串
return w.String(), nil
}
// fromCSV将给定的CSV格式字符串转换回Person结构体
func fromCSV(csvData string) (Person, error) {
// 创建一个csv.Reader,将其与给定的CSV数据关联
r := csv.NewReader(strings.NewReader(csvData))
// 读取CSV数据中的一行记录
record, err := r.Read()
if err != nil {
return Person{}, err
}
// 创建一个空的Person结构体
p := Person{}
// 遍历CSV记录中的每个字段和值对
for i := 0; i < len(record); i += 2 {
field := record[i]
value := record[i+1]
// 根据字段的名称将值分配给Person结构体的相应字段
switch field {
case "name":
p.Name = value
case "age":
fmt.Sscanf(value, "%d", &p.Age)
}
}
// 返回解析后的Person结构体
return p, nil
}