go语言 结构体

结构体

在 Go 语言中,结构体(struct)是一种复合数据类型,它允许将相关的数据组合在一起,形成一个实体。

通过结构体,开发者能够定义复杂的数据结构,这对于组织和管理数据非常有帮助。

类型别名和自定义类型

自定义类型

在Go语言中有一些基本的数据类型,

string整型浮点型布尔等数据类型,

Go语言中可以使用type关键字来定义自定义类型。

自定义类型是定义了一个全新的类型。

//将MyInt定义为int类型

type MyInt int

类型别名

type byte = uint8

type rune = int32

c 复制代码
package main

import "fmt"

func main() {

	//类型定义
	type NewInt int

	//类型别名
	type MyInt = int

	var a NewInt
	var b MyInt

	fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
	fmt.Printf("type of b:%T\n", b) //type of b:int

}
结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

结构体

c 复制代码
type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    ...
}
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- 字段名:表示结构体字段名。结构体中的字段名必须唯一。
- 字段类型:表示结构体字段的具体类型。
c 复制代码
package main

import "fmt"

// 定义一个结构体 Person
type Person struct {
    Name string // 姓名
    Age  int    // 年龄
}

公有(Exported):如果结构体的字段名(或者任何标识符,如变量、函数、方法等)的首字母是大写,那么这个字段是公有的,可以被其他包(package)访问和使用。

私有(Unexported):如果结构体的字段名的首字母是小写,那么这个字段是私有的,仅能在定义该字段的包内访问,外部包无法访问。

c 复制代码
package main

import (
    "fmt"
)

// 定义一个名为 Address 的结构体
type Address struct {
    city    string // 私有字段,首字母小写
    Country string // 公有字段,首字母大写
}

// 定义一个名为 Person 的结构体,嵌套 Address
type Person struct {
    Name    string
    Age     int
    Address // 嵌套的结构体
}

func main() {
    // 创建一个 Person 实例
    p := Person{
        Name: "Alice",
        Age:  30,
        Address: Address{
            city:    "Beijing", // 这里可以直接访问私有字段
            Country: "China",
        },
    }

    // 在包内,我们可以访问公有字段
    fmt.Println("Person Name:", p.Name)
    fmt.Println("Person Age:", p.Age)
    fmt.Println("Person Country:", p.Address.Country) // 可以访问公有字段

    // 下面这行代码将无法编译,因为 `city` 字段是私有的
    // fmt.Println("Person City:", p.Address.city) // 会报错
}

创建结构体实例

可以通过直接赋值或使用结构字面量来创建结构体的实例。

c 复制代码
func main() {
    // 直接赋值创建结构体实例
    person1 := Person{Name: "Alice", Age: 30}

    // 只指定部分字段(未指定的字段使用零值)
    person2 := Person{Age: 25} // Name 默认为空字符串

    
    var p2 = new(person)// 指针方式创建结构体实例
    
    fmt.Println(person1) // 输出: {Alice 30}
    fmt.Println(person2) // 输出: { 25}
}

访问结构体字段

可以通过点操作符(.)来访问结构体的字段。

c 复制代码
func main() {
    person := Person{Name: "Bob", Age: 40}
    fmt.Println("姓名:", person.Name) // 输出: 姓名: Bob
    fmt.Println("年龄:", person.Age)  // 输出: 年龄: 40
}

修改结构体字段

结构体字段是可以被修改的,使用点操作符来修改字段的值。

c 复制代码
func main() {
    person := Person{Name: "Charlie", Age: 35}
    person.Age = 36 // 修改年龄
    fmt.Println("修改后的年龄:", person.Age) // 输出: 修改后的年龄: 36
}

嵌套结构体

结构体可以包含其他结构体作为字段,从而形成复杂的数据结构。

c 复制代码
type Address struct {
    City    string
    ZipCode string
}

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

func main() {
    person := Person{
        Name: "David",
        Age:  28,
        Address: Address{
            City:    "New York",
            ZipCode: "10001",
        },
    }

    fmt.Println("地址:", person.Address.City, person.Address.ZipCode) // 输出: 地址: New York 10001
}

结构体方法

结构体可以定义方法,方法是与结构体类型关联的函数。使用接收者(receiver)来定义方法。

c 复制代码
func (p Person) Greet() {
    fmt.Printf("Hello, my name is %s and I am %d years old.\n", p.Name, p.Age)
}

func main() {
    person := Person{Name: "Eve", Age: 22}
    person.Greet() // 输出: Hello, my name is Eve and I am 22 years old.
}

结构体内存布局

c 复制代码
package main

import (
	"fmt"
	"unsafe"
)

对齐规则
结构体的成员变量,第一个成员变量的偏移量为 0。往后的每个成员变量的对齐值必须为编译器默认对齐长度(#pragma pack(n))或当前成员变量类型的长度(unsafe.Sizeof),取最小值作为当前类型的对齐值。其偏移量必须为对齐值的整数倍
结构体本身,对齐值必须为编译器默认对齐长度(#pragma pack(n))或结构体的所有成员变量类型中的最大长度,取最大数的最小整数倍作为对齐值
结合以上两点,可得知若编译器默认对齐长度(#pragma pack(n))超过结构体内成员变量的类型最大长度时,默认对齐长度是没有任何意义的

type Part1 struct {
	a bool  // 1字节
	b int32 // 4字节
	c int8  // 1字节
	d int64 // 8字节
	e byte  // 1字节
}

func main() {
	part1 := Part1{}
    //打印结构体的大小和对齐值
	fmt.Printf("part1 size: %d, align: %d\n", unsafe.Sizeof(part1), unsafe.Alignof(part1))
}
part1 size: 32, align: 8

空结构体

空结构体是不占用空间的。

c 复制代码
	var v struct{}
	fmt.Println(unsafe.Sizeof(v)) // 0

题 关于 range 循环的陷阱

c 复制代码
type student struct {
	name string
	age  int
}

func main() {
	m := make(map[string]*student)
	stus := []student{
		{name: "bob", age: 18},
		{name: "alice", age: 23},
		{name: "tom", age: 9000},
	}
	//打印切片地址
	fmt.Printf("切片地址1: %p\n", &stus[0])
	fmt.Printf("切片地址2: %p\n", &stus[1])
	fmt.Printf("切片地址3: %p\n", &stus[2])

	//遍历切片,将每个元素的地址存入map
	for k, _ := range stus {
		m[stus[k].name] = &stus[k]
	}
	// or 安全方式遍历切片,将每个元素的地址存入map
	for i := 0; i < len(stus); i++ {
		m[stus[i].name] = &stus[i]
	}

	//遍历的时候每次都从里面复制一份,给stu ,取出stu'的地址没用,他的地址从来不变
	for _, stu := range stus {
		fmt.Printf("当前学生 %s 的地址: %p\n", stu.name, &stu) // 打印地址
		m[stu.name] = &stu                              //此处获取的都是切片的地址
		//! Go 语言中的 for range 循环的底层实现通过使用迭代器来遍历集合。在处理切片、数组或映射时,编译器会生成代码来管理这些集合的遍历,而每个元素的值在取用时并不是引用,而是值的副本。
		//
		//! 但是在循环内部的,循环变量的地址是一直保持不变的。每次迭代时,循环变量会被新的值覆盖,导致最后的所有指针指向同一个地址。
		//! &stu 是取 stu 变量的地址。由于 stu 是在 for 循环中定义的局部变量,每次循环都会用新值来覆盖 stu。
		//! 这意味着 m[stu.name] 会存储指向 stu 的地址,而不是存储一个新副本的地址。结果是,每次循环中 stu 的值都会被更新,导致映射 m 中的所有键都指向同一个地址,也就是最后一次迭代结束后 stu 的值。
	}

	for k, v := range m {
		fmt.Println(k, "=>", v.name)
	}
}

构造函数

在 Go 语言中,构造函数并不是一个语言特性,但我们通常通过自定义函数来实现结构体的构造。

构造函数的主要功能是创建并初始化结构体的实例。

因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。

c 复制代码
package main

import "fmt"

// 定义一个结构体
type Person struct {
    Name string
    Age  int
}

// 构造函数,返回一个 Person 的实例
func NewPerson(name string, age int) *Person {
    return &Person{
        Name: name,
        Age:  age,
    }
}

func main() {
    // 使用构造函数创建一个 Person 的实例
    person := NewPerson("Alice", 30)

    // 输出实例的内容
    fmt.Println("Name:", person.Name)
    fmt.Println("Age:", person.Age)
}

方法 和 接收者

方法是与特定类型(通常是结构体)关联的函数,而接收者是方法的一个特定参数,表示调用该方法的实例。

通过接收者,方法可以访问和修改实例的数据。

定义方法

方法的定义形式如下:

c 复制代码
func (receiver ReceiverType) MethodName(parameters) returnType {
    // 方法的实现
}
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
}



- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是`self`、`this`之类的命名。例如,`Person`类型的接收者变量应该命名为 `p`,`Connector`类型的接收者变量应该命名为`c`等。
- 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:具体格式与函数定义相同。
c 复制代码
package main

import "fmt"

// 定义一个结构体
type Circle struct {
    Radius float64
}

// 计算圆的面积的方法,接收者是 Circle
func (c Circle) Area() float64 {
    return 3.14 * c.Radius * c.Radius // 使用接收者 c 计算面积
}

// 修改圆的半径的方法,接收者是 Circle 的指针
func (c *Circle) SetRadius(radius float64) {
    c.Radius = radius // 修改接收者 c 的 Radius 字段
}

func main() {
    // 创建一个 Circle 实例
    circle := Circle{Radius: 5}

    // 调用方法计算面积
    fmt.Println("Area of circle:", circle.Area())

    // 修改圆的半径
    circle.SetRadius(10)
    fmt.Println("New area of circle:", circle.Area())
}

什么时候应该使用指针类型接收者

  1. 需要修改接收者中的值
  2. 接收者是拷贝代价比较大的大对象
  3. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

任意类型添加方法

在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

c 复制代码
package main

import "fmt"

// 定义一个新的自定义类型 Int
type Int int

// 方法:计算该类型值的平方
func (i Int) Square() Int {
    return i * i  // 返回平方值
}

// 方法:判断该类型值是否为偶数
func (i Int) IsEven() bool {
    return i%2 == 0 // 判断是否为偶数
}

// 方法:计算该类型值的三倍
func (i Int) Triple() Int {
    return i * 3 // 返回三倍值
}

func main() {
    var myInt Int = 4 // 创建自定义类型的实例

    // 调用自定义类型的方法
    fmt.Println("Value:", myInt)
    fmt.Println("Square:", myInt.Square())
    fmt.Println("Is Even:", myInt.IsEven())
    fmt.Println("Triple:", myInt.Triple())
}

结构体的匿名字段

c 复制代码
package main

import "fmt"

// 定义一个结构体 Address
type Address struct {
    City    string
    Country string
}

// 定义一个结构体 Person,使用匿名字段嵌入 Address
type Person struct {
    Name    string
    Age     int
    Address // 匿名字段
}

func main() {
    // 创建一个 Person 的实例,并初始化数据
    p := Person{
        Name: "Alice",
        Age:  30,
        Address: Address{
            City:    "Beijing",
            Country: "China",
        },
    }

    // 访问 Person 的字段
    fmt.Println("Name:", p.Name)
    fmt.Println("Age:", p.Age)
    fmt.Println("City:", p.City)         // 直接访问匿名字段中的 City
    fmt.Println("Country:", p.Country)     // 直接访问匿名字段中的 Country
}

匿名字段的优势

简化结构体访问:在访问嵌入结构体的字段时,不需要指定完整的字段路径,简化了代码。

重用性:可以通过匿名字段重用结构体,避免字段名称的重复。

注意事项

如果多个嵌入结构体有相同的字段名,则需要使用完整的路径进行访问,以避免歧义。

匿名字段也可以是接口类型,这使得可以实现多态。

冲突 如果嵌入了多个具有相同字段名的结构体,访问时需要选择明确的路径:

c 复制代码
type Address struct {
    City string
}

type Location struct {
    City string
}

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

func main() {
    // 创建 Person 实例
    p := Person{
        Name: "Alice",
        Address: Address{
            City: "Beijing",
        },
        Location: Location{
            City: "Shanghai",
        },
    }

    // 访问字段
    fmt.Println("Name:", p.Name)
    fmt.Println("Address City:", p.Address.City) // 访问 Address 的 City
    fmt.Println("Location City:", p.Location.City) // 访问 Location 的 City
}

结构体的"继承"

结构体没有传统意义上的继承(如在其他面向对象编程语言中那样),

但可以通过嵌套(匿名字段)来实现类似的功能。

这种方式允许一个结构体嵌入另一个结构体,从而获得其字段和方法的访问权。这种机制可以模拟某种形式的"继承"。

c 复制代码
package main

import "fmt"

// 定义一个基础结构体 Animal
type Animal struct {
    Name string
}

// 为 Animal 类型定义方法
func (a Animal) Speak() string {
    return "Some sound"
}

// 定义一个结构体 Dog,嵌入 Animal 作为匿名字段
type Dog struct {
    Animal // 嵌入 Animal,实现继承
    Breed  string
}

// 为 Dog 类型定义一个方法,覆盖基类方法
func (d Dog) Speak() string {
    return "Woof!"
}

func main() {
    // 创建一个 Dog 的实例
    myDog := Dog{
        Animal: Animal{Name: "Buddy"},
        Breed:  "Golden Retriever",
    }

    // 访问 Dog 的字段和方法
    fmt.Println("Dog Name:", myDog.Name)       // 从 Animal继承的字段
    fmt.Println("Dog Breed:", myDog.Breed)     // Dog 自身的字段
    fmt.Println("Dog Speak:", myDog.Speak())    // Dog 自己的方法
}

代码讲解
定义基类结构体:

Animal 结构体包含一个字段 Name,和一个方法 Speak(),返回一个字符串表示动物的叫声。
继承结构体:

Dog 结构体嵌入 Animal,这样 Dog 就可以访问 Animal 的字段和方法。
Dog 结构体还包含自己的字段 Breed。
方法覆盖:

在 Dog 类型中定义了一个 Speak() 方法,从而覆盖了 Animal 的 Speak() 方法。这种方式可以让子类提供自己的实现,模拟多态行为。
创建实例:

在 main 函数中,创建了 Dog 的实例 myDog,并且通过访问字段和方法展示了继承的特性。

优势与特点

组合优于继承:Go 推荐使用组合(通过嵌套结构体)而非传统的类继承模型。通过组合,可以创建更灵活且可重用的代码结构。

多重嵌套:Go 允许多个匿名字段,这使得实现多重组合成为可能,但要注意字段名冲突的问题。

结构体与JSON序列化

在 Go 语言中,使用标准库 encoding/json 可以方便地将结构体与 JSON 数据进行序列化(编码)和反序列化(解码)。

序列化与反序列化基本概念

序列化(Serialization):将 Go 结构体编码为 JSON 格式的字符串。

反序列化(Deserialization):将 JSON 格式的字符串解码为 Go 结构体。

c 复制代码
package main

import (
    "encoding/json"
    "fmt"
    "log"
)

// 定义一个结构体,使用 `json` 标签指定 JSON 字段名称
type Person struct {
    Name    string `json:"name"`
    Age     int    `json:"age"`
    Address string `json:"address,omitempty"` // omitempty 表示如果为空则不在 JSON 中显示
}

func main() {
    // 创建一个 Person 实例
    person := Person{
        Name:    "Alice",
        Age:     30,
        Address: "123 Main St",
    }

    // 序列化结构体为 JSON
    jsonData, err := json.Marshal(person)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("JSON Output:", string(jsonData))

    // 反序列化 JSON 为结构体
    jsonInput := `{"name":"Bob","age":25,"address":"456 Elm St"}`
    var newPerson Person
    err = json.Unmarshal([]byte(jsonInput), &newPerson)
    if err != nil {
        log.Fatal(err)
    }

    // 输出反序列化后的结构体
    fmt.Println("Deserialized Person:", newPerson)
}
代码讲解
定义结构体:定义了一个 Person 结构体,并使用 json 标签指定 JSON 中的字段名称。

json:"name" 表示在序列化和反序列化时,与 JSON 中的 name 字段对应。
omitempty 表示如果 Address 字段是空字符串,则在 JSON 中不显示该字段。
序列化:

使用 json.Marshal 函数将 Person 结构体实例 person 转换为 JSON 字符串。
错误处理确保在序列化过程中出现问题时能够得到反馈。
反序列化:

使用 json.Unmarshal 函数将 JSON 字符串 jsonInput 转换为 Person 结构体实例 newPerson。
将 JSON 字符串转换为字节切片,并传入 newPerson 的地址,以便填充结构体。
输出结果:

输出序列化后的 JSON 字符串和反序列化后的结构体字段值。
c 复制代码
JSON Output: {"name":"Alice","age":30,"address":"123 Main St"}
Deserialized Person: {Bob 25 456 Elm St}

注意事项

字段可见性:只有导出的(公有的)结构体字段(首字母大写)才能被 JSON 序列化和反序列化。

json 标签:使用标签定制 JSON 字段名、忽略字段的序列化等,灵活满足不同需求。

时间类型:使用 time.Time 类型时,JSON 序列化和反序列化能够自动处理时间格式,但需要注意时区等问题。

结构体标签(Tag)

结构体标签(Tag)是 Go 语言的一种特性,允许开发者为结构体字段添加额外的信息。

这些信息通常在序列化和反序列化、数据验证等操作中使用。

标签被定义在结构体字段声明的反引号内,格式通常为 key:"value"。

使用结构体标签的场景

JSON 序列化和反序列化:指定 JSON 字段名。

数据库映射:在 ORM(对象关系映射)库中指定数据库列名。

数据验证:使用验证库进行字段值的约束和检查。

c 复制代码
package main

import (
    "encoding/json"
    "fmt"
)

// 定义一个结构体,使用标签
type User struct {
    ID       int    `json:"id"`                // JSON 输出时的字段名为 "id"
    Username string `json:"username"`          // JSON 输出时的字段名为 "username"
    Email    string `json:"email,omitempty"`   // 如果 Email 为空,则在 JSON 中省略该字段
    Age      int    `json:"age" validate:"min=0"` // JSON 输出时的字段名为 "age",且添加验证标签
}

func main() {
    // 创建一个 User 实例
    user := User{
        ID:       1,
        Username: "Alice",
        Email:    "",
        Age:      25,
    }

    // 序列化结构体为 JSON
    jsonData, err := json.Marshal(user)
    if err != nil {
        fmt.Println("Error marshaling to JSON:", err)
        return
    }

    // 输出 JSON 结果
    fmt.Println("JSON Output:", string(jsonData))

    // 反序列化 JSON 数据
    jsonInput := `{"id":2,"username":"Bob","email":"bob@example.com","age":30}`
    var newUser User
    err = json.Unmarshal([]byte(jsonInput), &newUser)
    if err != nil {
        fmt.Println("Error unmarshaling JSON:", err)
        return
    }

    // 输出反序列化后的结构体
    fmt.Println("Deserialized User:", newUser)
}
代码讲解
定义结构体:

User 结构体包含多个字段,并在每个字段后添加了标签。
使用 json 标签指定在 JSON 中对应的字段名。例如,Username 字段在 JSON 中展示为 username。
omitempty 选项表示如果该字段为空,则在 JSON 输出中省略该字段。
validate 标签可用于第三方库进行自定义验证。
序列化:

json.Marshal 方法将 User 实例转换为 JSON 格式的字符串。
错误处理确保在序列化过程中出现问题时反馈。
反序列化:

json.Unmarshal 方法将 JSON 字符串转换为 User 实例。
使用 &newUser 传入指针,以便填充结构体。

注意事项

标签格式:标签的格式是一个关键值对,可以有多个键值对,使用空间分隔。

json:"name" validate:"required"

结构体字段可见性:标签只能应用于导出的字段(首字母大写),私有字段(首字母小写)无法被外部包访问。

第三方库:标签的具体语义和使用方法通常与具体实现的库相关,例如 JSON 序列化、ORM、数据验证库等

相关推荐
编码浪子5 分钟前
构建一个rust生产应用读书笔记7-确认邮件2
开发语言·后端·rust
昙鱼16 分钟前
springboot创建web项目
java·前端·spring boot·后端·spring·maven
天之涯上上20 分钟前
JAVA开发 在 Spring Boot 中集成 Swagger
java·开发语言·spring boot
2402_8575834921 分钟前
“协同过滤技术实战”:网上书城系统的设计与实现
java·开发语言·vue.js·科技·mfc
白宇横流学长22 分钟前
基于SpringBoot的停车场管理系统设计与实现【源码+文档+部署讲解】
java·spring boot·后端
kirito学长-Java27 分钟前
springboot/ssm太原学院商铺管理系统Java代码编写web在线购物商城
java·spring boot·后端
爱学习的白杨树28 分钟前
MyBatis的一级、二级缓存
java·开发语言·spring
OTWOL33 分钟前
两道数组有关的OJ练习题
c语言·开发语言·数据结构·c++·算法
问道飞鱼37 分钟前
【前端知识】强大的js动画组件anime.js
开发语言·前端·javascript·anime.js
拓端研究室37 分钟前
R基于贝叶斯加法回归树BART、MCMC的DLNM分布滞后非线性模型分析母婴PM2.5暴露与出生体重数据及GAM模型对比、关键窗口识别
android·开发语言·kotlin