结构体和方法
结构体
什么是结构体?
在 Go 语言中,结构体(struct)是一种用户定义的数据类型,用于组合不同类型的数据项。结构体可以包含零个或多个字段(fields),每个字段都有自己的类型和名称。
结构体的定义
结构体的定义使用 type 关键字和 struct 关键字,通常形式如下:
type StructName struct {
Field1 type1
Field2 type2
// ...
}
下面是定义结构体的示例:
// 定义一个用户结构体
type User struct {
Name string
Age int
Sex string
address string
}
示例结构体说明:
- 结构体名称为
User
首字母大写,根据我们所学的首字母大写可导出的知识,它是包级可导出结构。 - 结构体中的字段
Name
、Age
、Sex
首字母大写,所以它是可导出字段,address
是小写,所以他们俩不可导出。
初始化
- 使用字段名的方式初始化:这种方式要求按照结构体定义的字段顺序依次赋值。
// 定义结构体
type Person struct {
Name string
Age int
City string
}
// 初始化结构体并赋值
var p1 Person
p1.Name = "Alice"
p1.Age = 30
p1.City = "New York"
- 使用键值对方式初始化:可以指定字段名和对应的值进行初始化,顺序可以任意。
// 初始化结构体并赋值
p2 := Person{
Name: "Bob",
City: "San Francisco",
Age: 25,
}
- 使用 new 函数初始化:使用 new 函数创建结构体的指针,并返回指向新分配的零值结构体的指针。
// 使用 new 函数初始化结构体指针
p3 := new(Person)
p3.Name = "Charlie"
p3.Age = 35
p3.City = "Chicago"
- 匿名结构体初始化:直接在初始化时定义结构体类型和字段,不需要提前定义结构体类型。
// 初始化匿名结构体并赋值
p4 := struct {
Name string
Age int
}{
Name: "David",
Age: 28,
}
结构体的访问
可以通过 .
的方式来访问结构体中的字段。
示例:
package main
import "fmt"
func main() {
// 初始化结构体
u1 := User{
name: "MagicGopher",
age: 20,
sex: "男",
address: "地球",
}
// 通过 . 的方式获取结构体的字段的值
fmt.Printf("name:%v\n", u1.name)
fmt.Printf("age:%v\n", u1.age)
fmt.Printf("sex:%v\n", u1.sex)
fmt.Printf("address:%v\n", u1.address)
}
// 定义一个结构体
type User struct {
name string
age int
sex string
address string
}
还可以通过指针来访问结构体的字段,下面是一个示例,展示如何使用指针来访问结构体的字段:
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
// 创建一个 Person 类型的指针
p := &Person{
Name: "Bob",
Age: 25,
}
// 通过指针访问结构体字段
fmt.Println("Name:", p.Name)
fmt.Println("Age:", p.Age)
// 修改结构体字段
p.Age = 26
fmt.Println("Modified Age:", p.Age)
}
结构体是值类型
在 Go 语言中,结构体是值类型。这意味着当你将一个结构体赋值给另一个结构体变量时,实际上是复制了整个结构体的内容,而不是引用同一个内存地址。
示例:
type Person struct {
Name string
Age int
}
func main() {
p1 := Person{Name: "Alice", Age: 30}
p2 := p1 // 这里是值拷贝
p2.Name = "Bob" // 修改 p2 不会影响 p1
fmt.Println(p1.Name) // 输出: Alice
fmt.Println(p2.Name) // 输出: Bob
}
在这个例子中,p1 和 p2 是两个独立的结构体变量,修改 p2 不会影响 p1。
如果你希望在多个地方共享同一个结构体的引用,可以使用指针。例如:
func main() {
p1 := &Person{Name: "Alice", Age: 30} // p1 是指向结构体的指针
p2 := p1 // 这里是指针拷贝
p2.Name = "Bob" // 修改 p2 会影响 p1
fmt.Println(p1.Name) // 输出: Bob
fmt.Println(p2.Name) // 输出: Bob
}
结构体嵌套
结构体嵌套是指在一个结构体中定义另一个结构体作为字段。这种方式可以用来建立更复杂的数据结构。
示例:
type Address struct {
City string
State string
}
type Person struct {
Name string
Address Address // 嵌套结构体
}
结构体匿名字段
在 Go 语言中,结构体的匿名字段(也称为嵌入字段)是指在结构体中声明的没有命名的字段。这种特性允许我们通过嵌入其他结构体来实现组合,从而重用字段和方法。
示例:
package main
import (
"fmt"
)
// 定义一个嵌入的结构体
type Address struct {
City string
State string
}
// 定义一个包含匿名字段的结构体
type Person struct {
Name string
Address // 匿名字段
}
func main() {
p := Person{
Name: "Alice",
Address: Address{
City: "Wonderland",
State: "Fantasy",
},
}
fmt.Println("Name:", p.Name)
fmt.Println("City:", p.City) // 直接访问匿名字段的字段
fmt.Println("State:", p.State)
}
结构体匿名字段的特点:
- 字段访问:可以直接访问匿名字段的字段,而无需通过字段名称进行间接访问。
- 方法继承:如果匿名字段是一个结构体类型,它的方法也会被继承,这样可以方便地调用。
- 灵活性:通过匿名字段,可以很容易地组合多个结构体,使得代码更加模块化和可读。
使用场景:
- 适用于需要组合多个对象的场景。
- 尤其在实现接口或重用代码时非常有用。
导出结构体和字段
在 Go 语言中,根据首字母大小写访问的原则,导出结构体和字段的规则是基于标识符的首字母大小写。例如:结构体、字段、方法或接口的名称以大写字母开头,那么它是导出的,可以被包外的代码访问,如果名称以小写字母开头,则该结构体、字段、方法或接口是未导出的,只能在定义它的包内访问。
示例:
package main
import "fmt"
// 导出结构体
type Person struct {
Name string // 导出的字段
age int // 未导出的字段
}
// 导出方法
func (p Person) GetName() string {
return p.Name
}
// 未导出方法
func (p Person) getAge() int {
return p.age
}
func main() {
p := Person{Name: "Alice", age: 30}
fmt.Println("Name:", p.GetName()) // 可以访问
// fmt.Println("Age:", p.getAge()) // 不能访问,会报错
}
导出结构体和字段注意事项
- 导出字段和方法可以被其他包的代码访问,而未导出字段和方法只能在同一包内访问。
- 在使用结构体时,通常只暴露必要的字段和方法,以保持封装性。
导出结构体和字段总结
- 导出:首字母大写
- 未导出:首字母小写
结构体比较
在 Go 语言中,结构体的比较可以通过内置的比较运算符(如 ==
和 !=
)来实现,但有一些注意事项和限制。
结构体比较的基本原则:
- 相同数据类型:只有同类型的结构体可以直接比较。
- 所有字段可比较:结构体的所有字段都必须是可比较的类型(即支持比较运算符)。例如,基本类型(如 int、string、float)和某些其他类型(如数组)是可比较的,而包含切片、映射和函数等不可比较类型的结构体则不能进行比较。
示例:
package main
import (
"fmt"
)
type Person struct {
Name string
Age int
}
func main() {
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Alice", Age: 30}
p3 := Person{Name: "Bob", Age: 25}
fmt.Println(p1 == p2) // true,内容相等
fmt.Println(p1 == p3) // false,内容不相等
}
不可比较类型:如果结构体包含切片、映射或函数字段,则无法直接使用 == 进行比较。例如:
type Example struct {
Data []int // 切片不可比较
}
func main() {
e1 := Example{Data: []int{1, 2, 3}}
e2 := Example{Data: []int{1, 2, 3}}
// fmt.Println(e1 == e2) // 这会导致编译错误
}
对于包含不可比较字段的结构体,可以通过自定义方法实现比较逻辑。例如:
type Example struct {
Data []int
}
func (e Example) Equal(other Example) bool {
if len(e.Data) != len(other.Data) {
return false
}
for i := range e.Data {
if e.Data[i] != other.Data[i] {
return false
}
}
return true
}
func main() {
e1 := Example{Data: []int{1, 2, 3}}
e2 := Example{Data: []int{1, 2, 3}}
fmt.Println(e1.Equal(e2)) // true
}
总结
- 结构体可以使用 == 和 != 进行比较,但所有字段必须是可比较的类型。
- 如果结构体包含不可比较类型的字段,可以定义自定义比较方法。
结构体作为函数的参数
在 Go 语言中,结构体(struct)是一种用户定义的数据类型,可以用来组合多个相关的字段。结构体可以作为函数的参数传递,这个特性非常强大,可以帮助我们组织和管理代码。
首先,我们需要定义一个结构体。例如,定义一个表示 Person 的结构体,如下:
type Person struct {
Name string
Age int
}
有两种方式将结构体作为函数参数传递:值传递和引用传递。
在值传递中,函数接收的是结构体的一个副本。修改副本不会影响原始结构体。
func UpdatePerson(p Person) {
p.Age += 1 // 只会修改副本
}
func main() {
person := Person{Name: "Alice", Age: 30}
UpdatePerson(person)
fmt.Println(person.Age) // 输出: 30
}
在引用传递中,函数接收的是结构体的指针。修改指针指向的内容会影响原始结构体。
func UpdatePerson(p *Person) {
p.Age += 1 // 修改原始结构体
}
func main() {
person := Person{Name: "Alice", Age: 30}
UpdatePerson(&person) // 传递指针
fmt.Println(person.Age) // 输出: 31
}
使用结构体作为函数的参数,选择传递方式:
- 值传递适用于结构体较小(例如只有几个字段),因为它会创建一个副本。如果结构体较大,使用值传递会消耗更多的内存和时间。
- 引用传递适用于结构体较大,或者当你希望在函数内修改原始结构体时。
以下是一个完整的示例,展示了如何使用结构体作为函数参数:
package main
import (
"fmt"
)
type Person struct {
Name string
Age int
}
func UpdateAge(p *Person) {
p.Age += 1 // 修改原始结构体
}
func main() {
person := Person{Name: "Alice", Age: 30}
fmt.Println("Before update:", person.Age) // 输出: 30
UpdateAge(&person) // 传递指针
fmt.Println("After update:", person.Age) // 输出: 31
}
注意事项
- 使用指针传递时,要确保指针不为 nil,否则可能导致运行时错误。
- 当结构体中包含指针或引用类型字段时,请注意可能的共享状态和并发访问问题。
方法
什么是方法?
在 Go 语言中同时有函数和方法,方法(Methods)是与特定类型(或接收者类型)关联的函数。它们允许在自定义类型上定义行为,使得类型具备执行特定操作的能力。方法是面向对象编程中的一种实现方式,但在Go语言中,它们并不局限于类或对象的概念,而是直接关联于任何自定义类型。
方法的语法
语法格式:
// receiverType:接收者类型,指定了方法关联的类型。接收者可以是结构体、数组、切片等自定义类型,但不能是指针类型的基本类型。
// methodName:方法的名称,用于在该类型的实例(或指针)上调用方法。
// parameters:方法可以接受零个或多个参数,与普通函数的参数列表类似。
// returnType:方法返回的结果的类型,可以是任何合法的类型。
// value:方法返回的数据,返回的数据一定要符合returnType定义的数据类型。
func (receiverType) methodName(parameters) returnType {
// 方法体
// 执行具体操作
return value
}
示例:
package main
import (
"fmt"
)
// 定义一个结构体
type Rectangle struct {
width, height float64
}
// 定义一个结构体方法(计算面积)
func (r Rectangle) Area() float64 {
return r.width * r.height
}
// 定义另一个结构体方法(计算周长)
func (r Rectangle) Perimeter() float64 {
return 2 * (r.width + r.height)
}
func main() {
// 创建一个 Rectangle 结构体对象
rect := Rectangle{width: 10, height: 5}
// 调用结构体方法计算面积和周长
area := rect.Area()
perimeter := rect.Perimeter()
// 打印结果
fmt.Println("Rectangle Area:", area)
fmt.Println("Rectangle Perimeter:", perimeter)
}
在 Go 语言中,一个方法可以属于多种类型,通过接收不同类型的接收者来实现。以下是一个示例,展示了一个方法如何同时属于两种不同类型的结构体。
示例:
package main
import (
"fmt"
"math"
)
// 定义一个 Circle 结构体
type Circle struct {
radius float64
}
// 定义一个 Rectangle 结构体
type Rectangle struct {
width, height float64
}
// Circle 结构体的方法:计算圆的面积
func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
// Rectangle 结构体的方法:计算矩形的面积
func (r Rectangle) Area() float64 {
return r.width * r.height
}
// 定义一个接口 Shape,包含一个 Area 方法
type Shape interface {
Area() float64
}
func main() {
// 创建一个 Circle 对象和一个 Rectangle 对象
circle := Circle{radius: 5}
rectangle := Rectangle{width: 10, height: 5}
// 将 Circle 和 Rectangle 对象都赋值给 Shape 接口
shapes := []Shape{circle, rectangle}
// 遍历计算每个形状的面积并打印
for _, shape := range shapes {
fmt.Printf("Shape type: %T, Area: %.2f\n", shape, shape.Area())
}
}
在这个示例中,我们定义了 Circle
和 Rectangle
两个结构体,并为它们分别实现了 Area()
方法。然后,我们定义了一个 Shape
接口,包含一个 Area()
方法。Circle
和 Rectangle
结构体都实现了 Shape
接口的 Area()
方法,因此它们可以被赋值给 Shape
接口类型的变量。在 main()
函数中,我们创建了一个包含 Circle
和 Rectangle
对象的切片,并通过循环调用 Area()
方法来计算并打印每个形状的面积。
方法和函数的对比
既然有函数,为什么还需要有方法呢?在 Go 语言中方法(Methods)和函数(Functions)虽然都可以用来组织代码和实现逻辑,但它们在使用和设计上有一些区别和优势。
函数是独立的代码块,用于执行特定任务或计算,并可以接受参数和返回结果。在Go中,函数可以被定义为全局函数或局部函数,通过函数名进行调用和执行。函数通常是独立于任何特定类型的,它们可以被任何代码调用,只要符合其参数和返回类型的要求。
// 不可导出
func add(a, b int) int {
return a + b
}
// 可导出
func Add(a, b int) int {
return a + b
}
方法是与特定类型关联的函数。它们是类型的一部分,可以访问和操作该类型的数据。在Go中,方法是通过将函数与一个接收者(Receiver)关联而实现的。接收者可以是任何自定义类型的实例,包括结构体(Structs)等。
type Rectangle struct {
width, height float64
}
// 方法定义为关联到 Rectangle 类型的
func (r Rectangle) Area() float64 {
return r.width * r.height
}
方法与函数的对比
- 关联性和可读性:
- 方法通过将函数与类型关联,使得代码更加直观和自文档化。例如,Rectangle 类型有一个 Area() 方法,直观地表达了计算面积的操作,而不是一个独立的函数。
- 代码组织和语义化:
- 使用方法可以更好地组织和分离代码。相关的操作和逻辑可以直接附属于类型本身,而不是散落在代码中的各个函数中。
- 访问权限:
- 方法可以访问其所属类型的私有字段(私有字段指的是只能在同一个包内可见的字段),这样可以在不暴露私有数据的情况下操作类型的内部状态。
- 代码复用和扩展性:
- 方法允许在不改变外部调用方式的情况下扩展类型的功能。通过为类型添加新的方法,可以在不改动原有函数签名的情况下扩展其功能。
为什么有了函数还需要方法?尽管函数在Go中非常强大和灵活,但方法的引入使得代码更加模块化、可维护性更高,并且提供了更好的语义化。特别是在面向对象编程中,方法可以更好地模拟对象的行为和操作,使得代码结构更加清晰和直观。因此,使用方法不仅仅是为了实现功能,更是为了提高代码的可读性、可维护性和扩展性。
当使用方法与函数的区别可以更清晰地体现在实际的代码中。让我们通过一个简单的例子来比较函数和方法在 Go 中的应用和优势。
例子:计算矩形面积
首先,我们定义一个矩形类型 Rectangle
,并为其实现计算面积的方法。
package main
import (
"fmt"
)
// 定义矩形类型
type Rectangle struct {
width, height float64
}
// 方法:计算矩形面积
func (r Rectangle) Area() float64 {
return r.width * r.height
}
// 函数:计算两个数的和
func add(a, b int) int {
return a + b
}
func main() {
// 创建一个矩形对象
rect := Rectangle{width: 3, height: 4}
// 使用方法计算矩形的面积
fmt.Println("矩形的面积(方法):", rect.Area())
// 使用函数计算两个数的和
sum := add(5, 7)
fmt.Println("两个数的和(函数):", sum)
}
解释与比较
- 方法 Area():
Area()
方法是与Rectangle
类型关联的,它通过r Rectangle
的语法定义了一个接收者。这意味着我们可以通过rect.Area()
的方式直接计算矩形的面积,代码更加清晰和语义化。方法可以访问Rectangle
结构体的私有字段(如width
和height
),并直接操作它们。
- 函数 add():
add()
是一个独立的全局函数,它可以被任何地方调用。它接受两个整数参数并返回它们的和。在我们的例子中,我们使用它来计算两个数的和,并且它与任何特定类型无关。
- 优势对比:
- 可读性和语义化:方法 Area() 直接表达了计算矩形面积的操作,使得代码更易于理解和维护。
- 代码组织性:方法允许我们将与类型相关的操作封装在一起,有助于模块化和代码复用。
- 访问控制:方法可以访问其所属类型的私有字段,而函数则无法直接访问其他类型的私有数据。
方法继承和重写
通过结构体嵌套来模拟结构体的继承性。
示例:
package main
import (
"fmt"
)
func main() {
/*
OOP中的继承性:
如果两个类(class)存在继承关系,其中一个是子类,另一个作为父类,那么:
1.子类可以直接访问父类的属性和方法
2.子类可以新增自己的属性和方法
3.子类可以重写父类的方法(override就是将父类已有的方法,重新实现)
Go语言通过结构体嵌套:
1,模拟继承性:is - a
type A struct {
field
}
type A struct {
A // 匿名字段
}
2.模拟聚合关系:has - a
type C struct {
field
}
type D struct {
c C // 聚合关系
}
*/
// 创建一个Person
p1 := Person{name: "张三", age: 18}
fmt.Println(p1.name, p1.age) // 打印父类属性
p1.eat() // 调用方法
// 创建Student
s1 := Student{Person{name: "李四", age: 20}, "SCUT"}
fmt.Println(s1.name, s1.age) // 打印子类的属性(子类访问父类的定义的字段,其实就是提升字段)
fmt.Println(s1.school) // 子类独有的字段属性
s1.eat() // 调用eat方法(没有重写调用的就是父类的,重写了就是调用子类的)
s1.study() // 子类对象访问自己的方法
s1.eat() // 再次调用eat方法
s1.Person.eat() // 调用父类的eat
// 创建Worker
w1 := Worker{Person{name: "王五", age: 21}, 10000}
fmt.Println(w1.name, w1.age) // 打印子类的属性(子类访问父类的定义的字段,其实就是提升字段)
fmt.Println(w1.salary) // 子类独有的字段属性
w1.eat() // 调用eat方法(没有重写调用的就是父类的,重写了就是调用子类的)
w1.work() // 子类对象访问自己的方法
w1.eat() // 再次调用eat方法
w1.Person.eat() // 调用父类的eat
}
// 定义一个父类(父结构体)
type Person struct {
name string
age uint
}
// 定义一个子类 Student
type Student struct {
Person // 结构体嵌套,模拟继承性(匿名结构体字段)
school string // 子类独有的字段属性
}
// 定义一个子类 Worker
type Worker struct {
Person // 结构体嵌套,模拟继承性(匿名结构体字段)
salary float64 // 子类独有的字段属性
}
// 父类的方法
func (p Person) eat() {
fmt.Println("父类的方法,吃饭方法...")
}
// 子类的方法 study 接收者:Student
func (s Student) study() {
fmt.Println("子类的方法,学生学习...")
}
// 子类重写的方法 接收者 Student 重写了父结构体的eat方法
func (s Student) eat() {
fmt.Println("子类Student重写的eat方法...")
}
// 子类的方法 work 接收者:Worker
func (w Worker) work() {
fmt.Println("子类的方法,在工作中...")
}
// 子类重写的方法 接收者 Worker 重写了父结构体的eat方法
func (w Worker) eat() {
fmt.Println("子类Worker重写的eat方法...")
}
方法接收者
在 Go 语言中,方法接收者可以是值接收者或指针接收者。选择使用哪种接收者取决于具体的需求和性能考虑。
使用结构体指针接收者的场合
- 需要修改接收者的值:如果方法需要改变结构体的字段值,必须使用指针接收者。这是因为Go语言中,所有的函数参数(包括方法接收者)都是值传递的,值传递会复制变量的值。
- 避免复制大型结构体:如果结构体很大,使用指针接收者可以避免在方法调用时复制整个结构体,提高性能。
- 一致性:如果某个类型的一些方法需要使用指针接收者,为了一致性,其他方法也应使用指针接收者。
示例:
type Dog struct {
Name string
}
// 指针接收者
func (d *Dog) SetName(name string) {
d.Name = name
}
使用值接收者的场合
- 不需要修改接收者的值:如果方法只是读取结构体的值而不修改,可以使用值接收者。
- 结构体较小:对于小的结构体,值传递的开销很小,使用值接收者不会有明显的性能问题。
- 一致性:如果某个类型的一些方法适合用值接收者,为了一致性,其他方法也应使用值接收者。
示例:
type Dog struct {
Name string
}
// 值接收者
func (d Dog) Speak() string {
return "Woof! My name is " + d.Name
}
总结
- 修改结构体:用指针接收者。
- 大结构体:用指针接收者。
- 只读操作:用值接收者(如果结构体较小)。