Skip to main content

Go 类型-结构体

Overview ⚡️

  1. 结构体名,大小写都可,用于区分可见性
  2. 列表形式初始化结构体,必须将所有字段都出列出
  3. Go 语言没有默认实现构造函数,原因主要是为了保持语言的简洁性、灵活性和明确性
  4. 指针访问结构体的字段,等价直接访问结构体
  5. 理解“接收者”,在 Go 语言中,方法调用类似于向一个类型实例发送一条消息,而实例则是接收这条消息并执行相应的方法。因此,实例被称为“接收者”
  6. 当你将切片传递给函数时,实际上传递的是对底层数组的引用。这意味着在函数内部对切片元素的修改会反映到原切片上

结构体说明

  1. 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  2. 字段名:表示结构体字段名。结构体中的字段名必须唯一。
  3. 字段类型:表示结构体字段的具体类型。

在 Go 语言中,结构体(struct)是用户定义的类型。它由一组字段(属性)组成,每个字段可以是不同的类型。结构体可以用来组合数据,并且可以定义方法。

自己的理解:结构体是自定义的组合类型, 方法分为两类:读和修改

定义结构体类型

type Person struct {
Name string
Age int
}

创建结构体实例

p := Person{Name: "Alice", Age: 30}
fmt.Println(p) // 输出: {Alice 30}

使用值接收者的方法

你可以为结构体定义方法:

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

使用指针接收者的方法

如果你希望方法修改结构体的字段,可以使用指针接收者:

func (p *Person) HaveBirthday() {
p.Age++
}

结构体嵌套

结构体可以包含其他结构体:

type Address struct {
City string
Street string
}

type Person struct {
Name string
Age int
Address Address
}

示例

综合以上内容的示例:

package main

import "fmt"

type Address struct {
City string
Street string
}

type Person struct {
Name string
Age int
Address Address
}

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

func (p *Person) HaveBirthday() {
p.Age++
}

func main() {
p := Person{
Name: "Alice",
Age: 30,
Address: Address{
City: "New York",
Street: "5th Avenue",
},
}
p.Greet() // 输出: Hello, my name is Alice and I am 30 years old.
p.HaveBirthday()
fmt.Println(p.Age) // 输出: 31
}

总结

在 Go 语言中,结构体是用户定义的类型。通过定义结构体和方法,你可以创建复杂的数据结构和行为。

同名字段的情况

非指针类型同名字段

alt text

指针类型匿名字段

alt text

指针访问结构体的字段,等价直接访问结构体

在 Go 语言中,当你存储结构体的地址到映射(map)中时,你实际上存储的是指向该结构体的指针。通过这个指针,你可以访问结构体的字段。

结构体指针的使用

在 Go 中,结构体指针允许你通过指针访问结构体的字段,就像你直接访问结构体一样。假设你有一个 student 结构体和一个指向它的指针 *student,你可以通过指针直接访问结构体的字段。

示例解释

假设你有如下代码:

package main

import "fmt"

type student struct {
name string
age int
}

func main() {
m := make(map[string]*student)
stus := []student{
{name: "pprof.cn", age: 18},
{name: "测试", age: 23},
{name: "博客", age: 28},
}

for _, stu := range stus {
s := stu // 创建一个新的变量 s
m[s.name] = &s
}

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

关键点

  1. 存储结构体指针

    m[s.name] = &s

    这里 &s 是指向结构体 s 的指针。将这个指针存储到映射 m 中。

  2. 访问结构体字段

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

    在这个循环中,v 是一个指向 student 结构体的指针。通过 v.namev.age,你可以直接访问 student 结构体的字段。

为什么可以直接访问字段

在 Go 语言中,访问结构体指针的字段时,编译器会自动解引用指针。这意味着你可以像访问普通结构体字段一样访问结构体指针的字段。

例如:

stu := student{name: "Alice", age: 30}
p := &stu

// 通过指针访问字段
fmt.Println(p.name) // 等价于 (*p).name
fmt.Println(p.age) // 等价于 (*p).age

在上面的代码中,p.namep.age 实际上是 (*p).name(*p).age 的简写形式。Go 编译器自动处理了解引用操作,使代码更加简洁

总结

  • 当你存储结构体指针到映射中时,你存储的是指向结构体的地址。
  • 通过这个指针,你可以直接访问结构体的字段。
  • Go 编译器会自动解引用指针,使你可以像访问普通结构体字段一样访问结构体指针的字段。

希望这解释清楚了为什么你可以直接通过指针访问结构体的字段。如果你有更多问题,请随时提问!

结构体方法为什么叫“接收者”

理解“接收者”(Receiver)这个名称确实需要从编程语言设计和概念的角度来解释。尽管名称可能看起来不直观,但它在编程语言设计中有其特定的意义和背景。

为什么叫“接收者”?

  1. 消息传递的概念

    • 在面向对象编程(OOP)中,“消息传递”是一个重要的概念。对象之间通过发送和接收消息来进行交互。在这个背景下,方法调用可以被视为向一个对象发送一条消息,而对象则是接收这条消息的实体。
    • 在 Go 语言中,方法调用类似于向一个类型实例发送一条消息,而实例则是接收这条消息并执行相应的方法。因此,实例被称为“接收者”。
  2. 与函数的区别

    • 普通函数没有接收者,它们只是独立的代码块。方法则绑定到某个类型及其实例上,接收者就是这个绑定的实例。
    • 使用“接收者”这个术语可以明确区分方法和普通函数,强调方法是与某个类型实例绑定的。
  3. 语法和一致性

    • 在方法定义中,接收者作为方法的一个参数出现在函数签名中。虽然它是一个特殊的参数,但从语法上看,它类似于函数的参数,只是它表示的是方法所属的实例。
    • 这种设计在语法上保持了一致性,使得方法定义和函数定义在形式上非常相似。

接收者的具体作用

  1. 绑定方法到类型

    • 接收者将方法绑定到特定的类型,使得方法可以通过类型实例来调用。
  2. 访问实例字段和方法

    • 通过接收者,方法可以访问和操作类型实例的字段和其他方法。

示例解释

让我们通过一个具体的示例来进一步理解接收者:

package main

import "fmt"

type Person struct {
name string
age int
}

// 值接收者方法
func (p Person) greet() {
fmt.Printf("Hello, my name is %s and I am %d years old.\n", p.name, p.age)
}

// 指针接收者方法
func (p *Person) setName(name string) {
p.name = name
}

func main() {
p := Person{name: "Alice", age: 30}

// 调用值接收者方法
p.greet()

// 调用指针接收者方法
p.setName("Bob")

// 验证 name 字段是否被修改
p.greet()
}

在这个例子中:

  • greet 方法的接收者是 Person 类型的值。这个方法可以访问 Person 实例的字段 nameage,但无法修改它们。
  • setName 方法的接收者是 Person 类型的指针。这个方法不仅可以访问 Person 实例的字段,还可以修改它们。

总结

“接收者”这个名称源于面向对象编程中的消息传递概念,强调方法是绑定到某个类型实例的,并且实例接收并处理方法调用。虽然这个名称可能在初学时不太直观,但理解其背后的概念和设计思想后,就会发现它在编程语言设计中的合理性和一致性。

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

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

切片传递

你提供的代码展示了切片在 Go 语言中的引用传递特性。确实,切片是引用类型,所以在函数中对切片元素的修改会影响到原切片。不过,切片本身在函数中重新分配内存(例如通过 append)不会影响到原切片。

示例代码解析

package main

import "fmt"

type student struct {
id int
name string
age int
}

func demo(ce []student) {
// 切片是引用传递,是可以改变值的
ce[1].age = 999
// ce = append(ce, student{3, "xiaowang", 56})
// return ce
}

func main() {
var ce []student // 定义一个切片类型的结构体
ce = []student{
{1, "xiaoming", 22},
{2, "xiaozhang", 33},
}
fmt.Println(ce)
demo(ce)
fmt.Println(ce)
}

输出结果

[{1 xiaoming 22} {2 xiaozhang 33}]
[{1 xiaoming 22} {2 xiaozhang 999}]

关键点解释

  1. 切片是引用类型

    • 当你将切片传递给函数时,实际上传递的是对底层数组的引用。这意味着在函数内部对切片元素的修改会反映到原切片上。
  2. 修改切片元素

    • demo 函数中,ce[1].age = 999 修改了切片中第二个元素的 age 字段。这种修改会影响到原切片。
  3. 重新分配切片内存

    • 如果你在 demo 函数中使用了 append,例如 ce = append(ce, student{3, "xiaowang", 56}),这会创建一个新的切片,并且不会影响到原切片。因为 append 可能会导致分配新的底层数组,原切片的引用不会改变。

示例代码补充

如果你希望在函数中通过 append 修改切片并返回修改后的切片,你需要显式地返回新切片,并在调用处接收返回值:

package main

import "fmt"

type student struct {
id int
name string
age int
}

func demo(ce []student) []student {
// 切片是引用传递,是可以改变值的
ce[1].age = 999
ce = append(ce, student{3, "xiaowang", 56})
return ce
}

func main() {
var ce []student // 定义一个切片类型的结构体
ce = []student{
{1, "xiaoming", 22},
{2, "xiaozhang", 33},
}
fmt.Println(ce)
ce = demo(ce)
fmt.Println(ce)
}

输出结果

[{1 xiaoming 22} {2 xiaozhang 33}]
[{1 xiaoming 22} {2 xiaozhang 999} {3 xiaowang 56}]

在这个补充的示例中,demo 函数不仅修改了切片元素,还通过 append 添加了一个新元素,并返回修改后的切片。在 main 函数中,接收返回的新切片,从而反映出所有的修改。

可重新分配内存的 API

概括

在 Go 语言中,以下操作可能会导致切片重新分配内存:

  1. append 函数
  2. 重新切分切片
  3. 使用 make 创建新切片
  4. 切片合并

详细说明

1. append 函数

当使用 append 函数向切片添加元素时,如果新添加的元素数量超过当前切片的容量,Go 会分配一个更大的底层数组来容纳新元素。

s := []int{1, 2, 3}
s = append(s, 4, 5, 6) // 可能重新分配内存

2. 重新切分切片

切片的重新切分(重新调整长度和容量)可能导致内存重新分配,特别是在新的切片容量超过原有容量时。

s := []int{1, 2, 3}
s = s[:cap(s)] // 扩展切片到最大容量
s = append(s, 4) // 可能重新分配内存

3. 使用 make 创建新切片

通过 make 创建一个新的切片,并将其赋值给一个现有的切片变量,这实际上是创建了一个新的底层数组,从而导致内存重新分配。

s := []int{1, 2, 3}
s = make([]int, len(s), cap(s)*2) // 创建新切片

4. 切片合并

当你将两个切片合并为一个新的切片时,如果新切片的容量超过了原有切片的容量,也会导致内存重新分配。

s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}
s1 = append(s1, s2...) // 可能重新分配内存

通过这些详细说明,你可以更好地理解哪些操作可能导致切片重新分配内存,以及如何在编写代码时避免不必要的内存分配。