Skip to content

结构体和方法

结构体

什么是结构体?

在 Go 语言中,结构体(struct)是一种用户定义的数据类型,用于组合不同类型的数据项。结构体可以包含零个或多个字段(fields),每个字段都有自己的类型和名称。

结构体的定义

结构体的定义使用 type 关键字和 struct 关键字,通常形式如下:

go
type StructName struct {
    Field1 type1
    Field2 type2
    // ...
}

下面是定义结构体的示例:

go
// 定义一个用户结构体
type User struct {
	Name    string
	Age     int
	Sex     string
	address string
}

示例结构体说明:

  • 结构体名称为 User 首字母大写,根据我们所学的首字母大写可导出的知识,它是包级可导出结构。
  • 结构体中的字段 NameAgeSex 首字母大写,所以它是可导出字段,address 是小写,所以他们俩不可导出。

初始化

  1. 使用字段名的方式初始化:这种方式要求按照结构体定义的字段顺序依次赋值。
go
// 定义结构体
type Person struct {
    Name string
    Age  int
    City string
}

// 初始化结构体并赋值
var p1 Person
p1.Name = "Alice"
p1.Age = 30
p1.City = "New York"
  1. 使用键值对方式初始化:可以指定字段名和对应的值进行初始化,顺序可以任意。
go
// 初始化结构体并赋值
p2 := Person{
    Name: "Bob",
    City: "San Francisco",
    Age:  25,
}
  1. 使用 new 函数初始化:使用 new 函数创建结构体的指针,并返回指向新分配的零值结构体的指针。
go
// 使用 new 函数初始化结构体指针
p3 := new(Person)
p3.Name = "Charlie"
p3.Age = 35
p3.City = "Chicago"
  1. 匿名结构体初始化:直接在初始化时定义结构体类型和字段,不需要提前定义结构体类型。
go
// 初始化匿名结构体并赋值
p4 := struct {
    Name string
    Age  int
}{
    Name: "David",
    Age:  28,
}

结构体的访问

可以通过 . 的方式来访问结构体中的字段。

示例:

go
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
}

还可以通过指针来访问结构体的字段,下面是一个示例,展示如何使用指针来访问结构体的字段:

go
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 语言中,结构体是值类型。这意味着当你将一个结构体赋值给另一个结构体变量时,实际上是复制了整个结构体的内容,而不是引用同一个内存地址。

示例:

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。

如果你希望在多个地方共享同一个结构体的引用,可以使用指针。例如:

go
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
}

结构体嵌套

结构体嵌套是指在一个结构体中定义另一个结构体作为字段。这种方式可以用来建立更复杂的数据结构。

示例:

go
type Address struct {
    City  string
    State string
}

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

结构体匿名字段

在 Go 语言中,结构体的匿名字段(也称为嵌入字段)是指在结构体中声明的没有命名的字段。这种特性允许我们通过嵌入其他结构体来实现组合,从而重用字段和方法。

示例:

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 语言中,根据首字母大小写访问的原则,导出结构体和字段的规则是基于标识符的首字母大小写。例如:结构体、字段、方法或接口的名称以大写字母开头,那么它是导出的,可以被包外的代码访问,如果名称以小写字母开头,则该结构体、字段、方法或接口是未导出的,只能在定义它的包内访问。

示例:

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)和某些其他类型(如数组)是可比较的,而包含切片、映射和函数等不可比较类型的结构体则不能进行比较。

示例:

go
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,内容不相等
}

不可比较类型:如果结构体包含切片、映射或函数字段,则无法直接使用 == 进行比较。例如:

go
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) // 这会导致编译错误
}

对于包含不可比较字段的结构体,可以通过自定义方法实现比较逻辑。例如:

go
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 的结构体,如下:

go
type Person struct {
    Name string
    Age  int
}

有两种方式将结构体作为函数参数传递:值传递和引用传递。

在值传递中,函数接收的是结构体的一个副本。修改副本不会影响原始结构体。

go
func UpdatePerson(p Person) {
    p.Age += 1 // 只会修改副本
}

func main() {
    person := Person{Name: "Alice", Age: 30}
    UpdatePerson(person)
    fmt.Println(person.Age) // 输出: 30
}

在引用传递中,函数接收的是结构体的指针。修改指针指向的内容会影响原始结构体。

go
func UpdatePerson(p *Person) {
    p.Age += 1 // 修改原始结构体
}

func main() {
    person := Person{Name: "Alice", Age: 30}
    UpdatePerson(&person) // 传递指针
    fmt.Println(person.Age) // 输出: 31
}

使用结构体作为函数的参数,选择传递方式:

  • 值传递适用于结构体较小(例如只有几个字段),因为它会创建一个副本。如果结构体较大,使用值传递会消耗更多的内存和时间。
  • 引用传递适用于结构体较大,或者当你希望在函数内修改原始结构体时。

以下是一个完整的示例,展示了如何使用结构体作为函数参数:

go
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语言中,它们并不局限于类或对象的概念,而是直接关联于任何自定义类型。

方法的语法

语法格式:

go
// receiverType:接收者类型,指定了方法关联的类型。接收者可以是结构体、数组、切片等自定义类型,但不能是指针类型的基本类型。
// methodName:方法的名称,用于在该类型的实例(或指针)上调用方法。
// parameters:方法可以接受零个或多个参数,与普通函数的参数列表类似。
// returnType:方法返回的结果的类型,可以是任何合法的类型。
// value:方法返回的数据,返回的数据一定要符合returnType定义的数据类型。
func (receiverType) methodName(parameters) returnType {
    // 方法体
    // 执行具体操作
    return value
}

示例:

go
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 语言中,一个方法可以属于多种类型,通过接收不同类型的接收者来实现。以下是一个示例,展示了一个方法如何同时属于两种不同类型的结构体。

示例:

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())
	}
}

在这个示例中,我们定义了 CircleRectangle 两个结构体,并为它们分别实现了 Area() 方法。然后,我们定义了一个 Shape 接口,包含一个 Area() 方法。CircleRectangle 结构体都实现了 Shape 接口的 Area() 方法,因此它们可以被赋值给 Shape 接口类型的变量。在 main() 函数中,我们创建了一个包含 CircleRectangle 对象的切片,并通过循环调用 Area() 方法来计算并打印每个形状的面积。

方法和函数的对比

既然有函数,为什么还需要有方法呢?在 Go 语言中方法(Methods)和函数(Functions)虽然都可以用来组织代码和实现逻辑,但它们在使用和设计上有一些区别和优势。

函数是独立的代码块,用于执行特定任务或计算,并可以接受参数和返回结果。在Go中,函数可以被定义为全局函数或局部函数,通过函数名进行调用和执行。函数通常是独立于任何特定类型的,它们可以被任何代码调用,只要符合其参数和返回类型的要求。

go
// 不可导出
func add(a, b int) int {
    return a + b
}

// 可导出
func Add(a, b int) int {
    return a + b
}

方法是与特定类型关联的函数。它们是类型的一部分,可以访问和操作该类型的数据。在Go中,方法是通过将函数与一个接收者(Receiver)关联而实现的。接收者可以是任何自定义类型的实例,包括结构体(Structs)等。

go
type Rectangle struct {
    width, height float64
}

// 方法定义为关联到 Rectangle 类型的
func (r Rectangle) Area() float64 {
    return r.width * r.height
}

方法与函数的对比

  1. 关联性和可读性:
    • 方法通过将函数与类型关联,使得代码更加直观和自文档化。例如,Rectangle 类型有一个 Area() 方法,直观地表达了计算面积的操作,而不是一个独立的函数。
  2. 代码组织和语义化:
    • 使用方法可以更好地组织和分离代码。相关的操作和逻辑可以直接附属于类型本身,而不是散落在代码中的各个函数中。
  3. 访问权限:
    • 方法可以访问其所属类型的私有字段(私有字段指的是只能在同一个包内可见的字段),这样可以在不暴露私有数据的情况下操作类型的内部状态。
  4. 代码复用和扩展性:
    • 方法允许在不改变外部调用方式的情况下扩展类型的功能。通过为类型添加新的方法,可以在不改动原有函数签名的情况下扩展其功能。

为什么有了函数还需要方法?尽管函数在Go中非常强大和灵活,但方法的引入使得代码更加模块化、可维护性更高,并且提供了更好的语义化。特别是在面向对象编程中,方法可以更好地模拟对象的行为和操作,使得代码结构更加清晰和直观。因此,使用方法不仅仅是为了实现功能,更是为了提高代码的可读性、可维护性和扩展性。

当使用方法与函数的区别可以更清晰地体现在实际的代码中。让我们通过一个简单的例子来比较函数和方法在 Go 中的应用和优势。

例子:计算矩形面积

首先,我们定义一个矩形类型 Rectangle,并为其实现计算面积的方法。

go
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)
}

解释与比较

  1. 方法 Area():
    • Area() 方法是与 Rectangle 类型关联的,它通过 r Rectangle 的语法定义了一个接收者。这意味着我们可以通过 rect.Area() 的方式直接计算矩形的面积,代码更加清晰和语义化。方法可以访问 Rectangle 结构体的私有字段(如 widthheight),并直接操作它们。
  2. 函数 add():
    • add() 是一个独立的全局函数,它可以被任何地方调用。它接受两个整数参数并返回它们的和。在我们的例子中,我们使用它来计算两个数的和,并且它与任何特定类型无关。
  3. 优势对比:
    • 可读性和语义化:方法 Area() 直接表达了计算矩形面积的操作,使得代码更易于理解和维护。
    • 代码组织性:方法允许我们将与类型相关的操作封装在一起,有助于模块化和代码复用。
    • 访问控制:方法可以访问其所属类型的私有字段,而函数则无法直接访问其他类型的私有数据。

方法继承和重写

通过结构体嵌套来模拟结构体的继承性。

image-15

示例:

go
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 语言中,方法接收者可以是值接收者或指针接收者。选择使用哪种接收者取决于具体的需求和性能考虑。

使用结构体指针接收者的场合

  1. 需要修改接收者的值:如果方法需要改变结构体的字段值,必须使用指针接收者。这是因为Go语言中,所有的函数参数(包括方法接收者)都是值传递的,值传递会复制变量的值。
  2. 避免复制大型结构体:如果结构体很大,使用指针接收者可以避免在方法调用时复制整个结构体,提高性能。
  3. 一致性:如果某个类型的一些方法需要使用指针接收者,为了一致性,其他方法也应使用指针接收者。

示例:

go
type Dog struct {
    Name string
}

// 指针接收者
func (d *Dog) SetName(name string) {
    d.Name = name
}

使用值接收者的场合

  1. 不需要修改接收者的值:如果方法只是读取结构体的值而不修改,可以使用值接收者。
  2. 结构体较小:对于小的结构体,值传递的开销很小,使用值接收者不会有明显的性能问题。
  3. 一致性:如果某个类型的一些方法适合用值接收者,为了一致性,其他方法也应使用值接收者。

示例:

go
type Dog struct {
    Name string
}

// 值接收者
func (d Dog) Speak() string {
    return "Woof! My name is " + d.Name
}

总结

  • 修改结构体:用指针接收者。
  • 大结构体:用指针接收者。
  • 只读操作:用值接收者(如果结构体较小)。

根据 MIT 许可证发布