有基础转Go语言学习笔记(3. 面向对象篇)

发布时间:2023年12月17日

有基础转Go语言学习笔记(3. 面向对象篇)

封装

在Go语言中,封装是通过使用结构体(structs)和方法(methods)来实现的。Go语言的封装不同于传统面向对象编程语言中的类(class)系统,但它提供了类似的功能。封装在Go中主要体现在控制对结构体字段的访问权限上。

封装的实现

1. 定义结构体

结构体是组合相关属性的方式。你可以将数据封装在一个结构体内。

type person struct {
    name string
    age  int
}

在这个例子中,person结构体封装了 nameage字段。

2. 使用导出和非导出字段
  • 导出字段(Public):字段名以大写字母开头的字段是可导出的,意味着它们可以在包外部被访问。
  • 非导出字段(Private):字段名以小写字母开头的字段是不可导出的,它们只能在定义它们的包内部被访问。
type Person struct {
    Name string  // 导出字段
    age  int     // 非导出字段
}

在这个例子中,Name是一个导出字段,可以被包外的代码访问,而 age是非导出的,只能在定义它的包内部访问。

3. 使用方法进行操作

通过定义方法(函数与特定类型关联),你可以控制结构体的行为,以及如何访问和修改其内部状态。

func (p *Person) SetAge(age int) {
    p.age = age
}

func (p *Person) Age() int {
    return p.age
}

在这个例子中,SetAgeAge方法允许外部代码以受控的方式访问和修改私有字段 age

封装的好处

  1. 数据隐藏:封装强制隐藏内部实现的细节,只暴露必要的接口。这有助于减少软件系统的复杂性。
  2. 控制访问:提供了一种控制对对象状态的访问和修改的机制。
  3. 易于维护:通过封装实现的代码更易于修改和维护。

完整示例

package main

import (
    "fmt"
)

// Person 结构体,封装了个人的详细信息
type Person struct {
    Name string // 公开字段
    age  int    // 私有字段
}

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

// SetAge 方法设置Person的年龄
func (this *Person) SetAge(age int) {
    if age > 0 {
        this.age = age
    }
}

// GetAge 方法返回Person的年龄
func (this *Person) GetAge() int {
    return this.age
}

func main() {
    // 创建一个Person实例
    person := NewPerson("Alice", 30)

    // 访问并打印Person的公开字段
    fmt.Println("Name:", person.Name)

    // 访问私有字段通过方法
    fmt.Println("Age:", person.GetAge())

    // 设置新的年龄
    person.SetAge(31)
    fmt.Println("New Age:", person.GetAge())
}

输出:

Name: Alice
Age: 30
New Age: 31

接收器(Receiver)

在Go语言中,接收器(receiver)是定义在方法中的一个特殊参数,它指定了该方法绑定到哪种类型上。这允许你在特定类型的实例上调用该方法,类似于其他面向对象语言中的方法。在Go中,接收器可以是该类型的值或该类型的指针。

值接收器

值接收器使用类型的值来调用方法。当使用值接收器时,方法在调用时会接收调用对象的一个副本。

type Person struct {
    Name string
    Age  int
}

func (p Person) SayHello() {
    fmt.Println("Hello, my name is", p.Name)
}

在这个例子中,SayHello方法通过值接收器绑定到 Person类型上。当你调用这个方法时,pPerson实例的一个副本。

指针接收器

指针接收器使用指向类型的指针来调用方法。这意味着方法可以修改接收器指向的值。

func (p *Person) SetAge(newAge int) {
    p.Age = newAge
}

在这里,SetAge方法使用指针接收器,所以它可以修改 Person实例的 Age字段。

选择值接收器还是指针接收器

选择使用值接收器还是指针接收器主要取决于以下几点:

  1. 修改对象状态:如果你需要在方法中修改对象的状态,应该使用指针接收器。
  2. 效率和性能:如果对象比较大,使用指针接收器效率更高,因为它避免了复制整个对象。
  3. 一致性:为了保持一致性,如果类型的某些方法需要指针接收器,则最好让该类型的所有方法都使用指针接收器。
  4. 值类型的不变性:使用值接收器可以保持值类型的不变性,因为它总是在方法调用时创建值的副本。
接收器在方法中的作用

接收器允许你将函数与特定类型的实例关联,类似于面向对象编程中的方法。这样,你可以对这个类型的每个实例调用它的方法,就像是这个类型自己的行为一样。接收器是Go语言实现面向对象编程风格的关键元素之一。

继承

虽然Go语言没有像Python或C++中那样的类继承概念,但它使用结构体嵌入和组合来实现类似继承的功能。对于熟悉Python和C++的开发者来说,理解Go的这种继承方式需要转换一下思维方式。

结构体嵌入和组合

在Go中,继承是通过在一个结构体中嵌入另一个结构体来实现的。这种方法被称为组合(Composition)。嵌入的结构体的方法和字段可以被外层结构体直接访问,就好像它们是外层结构体的一部分。

示例

假设我们有一个基本的 Animal结构体,然后我们想要创建一个 Dog结构体,它拥有 Animal的所有特性,并添加一些自己的特性。

type Animal struct {
    Name string
}

func (a *Animal) Speak() {
    fmt.Println(a.Name, "makes a sound")
}

type Dog struct {
    Animal // 嵌入Animal
    Breed  string
}

func (d *Dog) Speak() {
    fmt.Println(d.Name, "barks")
}

在这个例子中,Dog嵌入了 Animal。这意味着 Dog自动获得了 Animal的所有字段和方法。我们还可以为 Dog添加新的方法或重写(Override)继承的方法,就像上面的 Speak方法那样。

调用方法

当调用一个嵌入结构体的方法时,如果外层结构体没有同名方法,则会调用嵌入结构体的方法。

func main() {
    d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Shepherd"}
    d.Speak() // 输出: Rex barks
    d.Animal.Speak() // 输出: Rex makes a sound
}

在这里,调用 d.Speak()时,由于 Dog结构体有自己的 Speak方法,所以调用的是 Dog的方法。而 d.Animal.Speak()则显式地调用了 AnimalSpeak方法。

对比Python和C++的继承

  • Python/C++:这些语言使用类继承,支持基类和派生类之间的直接关系,允许使用 super函数调用基类的方法。
  • Go:Go使用结构体组合而非继承。虽然没有直接的基类-派生类关系,但通过嵌入结构体,可以实现类似的功能。Go中没有 super关键字,但可以通过直接调用嵌入结构体的方法来实现类似的行为。

接口(Interface)和多态(Polymorphic)

在Go语言中,接口和多态是实现灵活、可扩展代码的核心概念。Go的接口提供了一种声明行为的方式,而多态则允许你以统一的方式使用实现了相同接口的不同类型。

接口(Interface)

接口在Go中是一组方法签名的集合。当一个类型提供了接口中所有方法的实现,它就被认为实现了该接口。这种实现是隐式的,不需要像Java或C#中那样显式声明。

定义接口
type Shape interface {
    Area() float64
    Perimeter() float64
}

这个 Shape接口定义了两个方法:AreaPerimeter。任何提供了这两个方法的类型都隐式地实现了 Shape接口。

实现接口
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)
}

在这里,Rectangle类型提供了 Shape接口要求的 AreaPerimeter方法,因此它实现了 Shape接口。

多态(Polymorphic)

多态是面向对象编程中的一个核心概念,它允许你以统一的方式处理不同类型的实例。在Go中,多态是通过接口实现的。

使用多态

你可以编写接受接口类型参数的函数或方法,然后用实现了该接口的任何类型的实例调用它。

func PrintShapeInfo(s Shape) {
    fmt.Println("Area:", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
}

func main() {
    r := Rectangle{Width: 10, Height: 5}
    PrintShapeInfo(r)
}

PrintShapeInfo函数中,参数 sShape接口类型。这意味着你可以传递任何实现了 Shape接口的类型(如 Rectangle)的实例。

接口的好处

  1. 解耦:接口提供了一种方式来解耦函数和方法的实现,从而使代码更灵活、更易于扩展和维护。
  2. 测试:接口使得单元测试变得更加容易,因为你可以使用模拟对象(mocks)来实现接口。
  3. 多态性:接口的多态性允许你编写更通用的函数和方法,可以处理不同类型的对象,只要它们实现了相同的接口。

完整示例

下面是一个完整的Go程序示例,展示了之前讨论的接口和多态概念。这个程序定义了一个 Animal接口,以及几种实现了这个接口的动物类型。程序中还包含了一个演示多态性的函数和 main函数,用于运行程序。

package main  
  
import (  
    "fmt"  
)  
  
// Animal 接口定义了一个方法 Speak  
type Animal interface {  
    Speak() string  
}  
  
// Dog 类型实现了 Animal 接口  
type Dog struct{}  
  
func (d *Dog) Speak() string {  
    return "Woof!"  
}  
  
// Cat 类型实现了 Animal 接口  
type Cat struct{}  
  
func (c *Cat) Speak() string {  
    return "Meow!"  
}  
  
// Cow 类型实现了 Animal 接口  
type Cow struct{}  
  
func (cow *Cow) Speak() string {  
    return "Moo!"  
}  
  
// AnimalSpeak 接收 Animal 接口类型的参数  
// 并调用其 Speak 方法  
func AnimalSpeak(a Animal) {  
    fmt.Println(a.Speak())  
}  
  
// main 函数是程序的入口  
func main() {  
    animals := []Animal{&Dog{}, &Cat{}, &Cow{}}  
    for _, animal := range animals {  
       AnimalSpeak(animal)  
    }  
}

在这个程序中:

  • 我们定义了一个 Animal接口,该接口有一个方法 Speak
  • DogCatCow类型分别实现了 Animal接口。
  • AnimalSpeak函数接受任何实现了 Animal接口的类型,并调用其 Speak方法。
  • main函数中,我们创建了一个包含不同动物的切片,并对每个动物调用了 AnimalSpeak函数。

空接口

在Go语言中,空接口(interface{})是一个特殊的类型,它不包含任何方法声明。由于Go中的接口是隐式实现的,任何类型都至少实现了零个方法,因此可以说所有类型都实现了空接口。这使得空接口在Go中具有独特的灵活性和用途。

相当于 Java中的 Object

空接口的类型

空接口表示为 interface{},它可以包含任何类型的值:

  • 基本类型:整型、浮点型、布尔型、字符串等。
  • 复合类型:数组、结构体、切片、映射等。
  • 函数类型:可以包含任意函数。
  • 通道、指针、接口类型:也可以赋值给空接口。

由于空接口没有定义任何方法,因此任何类型的值都可以被赋给空接口变量。

空接口的使用

1. 作为通用容器

由于空接口可以包含任何类型,因此它常被用作存储不确定类型值的通用容器。

var anything interface{}
anything = "Hello, World"
anything = 123
anything = []int{1, 2, 3}
2. 函数参数和返回值

空接口允许函数接受任意类型的参数或返回任意类型的数据。

func PrintValue(v interface{}) {
    fmt.Println(v)
}
3. 类型断言和类型判断

你可以使用类型断言来提取空接口中的实际类型。

// 判断any是否为string
if str, ok := anything.(string); ok {
    fmt.Println(str)
}
情况 1: 断言成功

anything变量实际上是一个 string类型时:

  • str, ok := anything.(string)会成功执行。
  • str将获得 anything变量的值,因为 anything确实是一个 string类型。
  • ok变量将被设置为 true,表示类型断言成功。

例如,如果 anything := "hello",那么在执行类型断言后,str将是 "hello"ok将是 true

情况 2: 断言失败

anything变量不是一个 string类型时:

  • str, ok := anything.(string)不会导致程序崩溃,而是安全地返回两个值。
  • str将被赋予 string类型的零值,即空字符串 ""。这是因为类型断言失败,str无法获得 anything的值。
  • ok变量将被设置为 false,表示类型断言没有成功。

例如,如果 anything := 42(一个 int类型),那么在执行类型断言后,str将是 ""(空字符串),ok将是 false

4. 在数据结构中使用

在诸如切片、映射等数据结构中,空接口可以用来存储各种不同类型的数据。

var mixedSlice []interface{}
mixedSlice = append(mixedSlice, 42, "foo", true)

空接口的局限性

  • 类型安全:使用空接口牺牲了类型安全。在使用空接口存储值时,你需要额外小心,特别是在类型断言和类型转换时。
  • 性能考虑:频繁地进行类型断言或反射可能会影响程序的性能。

反射机制

Go语言中的反射是一个强大且复杂的功能,它允许程序在运行时检查、修改变量的类型和值,以及调用其关联的方法。反射主要由 reflect包提供支持。理解反射的关键在于理解Go中的类型(Type)和值(Value)的概念。

基本概念

在Go的 reflect包中,主要有两个重要的类型:reflect.Typereflect.Value

  • reflect.Type:提供了关于Go值的类型信息,比如其名称、种类(如是结构体、整型、切片等)。
  • reflect.Value:包含了一个具体的Go值,以及与之相关的方法来操作这个值(比如获取或设置值)。

使用反射

获取类型信息(reflect.Type)
var x float64 = 3.4
t := reflect.TypeOf(x)
fmt.Println("Type:", t.Name()) // 输出: Type: float64

这里,reflect.TypeOf函数用于获取变量 x的类型信息。

获取值信息(reflect.Value)
v := reflect.ValueOf(x)
fmt.Println("Value:", v.Float()) // 输出: Value: 3.4

reflect.ValueOf函数返回了一个 reflect.Value类型的值,代表了变量 x的值。

修改值

要修改一个值,你需要确保这个值是可设置的(Settable)。通常这意味着你需要传递一个变量的指针给 reflect.ValueOf

var y float64 = 3.4
p := reflect.ValueOf(&y) // 注意:这里传递的是指针
v := p.Elem()
v.SetFloat(7.1)
fmt.Println(y) // 输出: 7.1

这里,p.Elem()提供了一个指针指向的值的 reflect.Value,这个值是可以设置的。

反射的使用场景

  1. 动态调用方法:反射可以在运行时动态调用对象的方法,即使这些方法在编译时并不确定。
  2. 检查类型:在需要处理多种类型,且类型在编译时不可知的情况下,反射是处理这些值的有效方式。
  3. 实现通用功能:反射常用于实现那些需要操作各种类型的通用函数,如格式化输出、序列化和反序列化等。

反射的局限性

  • 性能开销:反射操作比正常的直接调用有更多的运行时开销。
  • 复杂性和可读性:反射代码通常更复杂,可读性较差。
  • 类型安全:反射牺牲了部分类型安全性,因为许多检查都是在运行时进行的。

反射遍历复杂对象

在Go语言中,使用反射深度遍历复杂类型(如结构体中嵌套的结构体或数组)可以是一个复杂的任务。但是,通过 reflect包的功能,我们可以递归地访问和操作这些复杂类型的每个字段。以下是一个示例,展示了如何使用反射来深度遍历一个结构体,包括它的嵌套字段:

示例代码
package main

import (
    "fmt"
    "reflect"
)

// 遍历结构体的函数
func walk(v reflect.Value, depth int) {
    // 获取v的类型
    typ := v.Type()

    switch v.Kind() {
    case reflect.Struct:
        for i := 0; i < v.NumField(); i++ {
            field := v.Field(i)
            fieldType := typ.Field(i)

            fmt.Printf("%*s%s (%s): ", depth*2, "", fieldType.Name, field.Type())

            if field.CanInterface() {
                fmt.Println(field.Interface())
            } else {
                fmt.Println("unexported field")
            }

            // 递归调用walk
            walk(field, depth+1)
        }
    case reflect.Slice, reflect.Array:
        for i := 0; i < v.Len(); i++ {
            fmt.Printf("%*s%d: ", depth*2, "", i)
            walk(v.Index(i), depth+1)
        }
    }
}

type Person struct {
    Name    string
    Age     int
    Friends []string
    Parent  *Person
}

func main() {
    james := Person{
        Name:    "James",
        Age:     35,
        Friends: []string{"David", "Emma"},
        Parent:  &Person{Name: "John"},
    }

    walk(reflect.ValueOf(james), 0)
}
解释
  • 这个程序定义了一个名为 walk的函数,它使用递归来遍历结构体的每个字段。
  • 这个函数检查传入值的类型,并根据类型进行不同的处理。如果是结构体,它遍历每个字段;如果是切片或数组,它遍历每个元素。
  • 我们使用 reflect.Valuereflect.Type来获取关于值和类型的信息,并通过 Field方法访问结构体的字段。
  • 对于结构体中的每个字段,我们打印字段名、类型和值。
  • 如果字段是可导出的(即公有的),可以通过 CanInterfaceInterface方法获取其值。
  • depth参数用于在打印时生成缩进,使输出更容易阅读。
注意
  • 反射中的 CanInterface方法用于检查是否可以安全地调用 Interface方法。对于非导出(私有)字段,CanInterface会返回 false,此时调用 Interface会引发panic。
  • 递归遍历复杂类型时,特别要注意循环引用的情况,这可能导致无限递归。在实际应用中,你可能需要添加一些逻辑来处理这种情况。

反射遍历指针对象

package main  
  
import (  
    "fmt"  
    "reflect")  
  
type User struct {  
    Id   int  
    Name string  
    Age  int  
}  
  
func (this *User) Call() {  
    fmt.Println("user is called ...")  
    fmt.Printf("%v\n\n", this)  
}  
  
func main() {  
    user := User{1, "Chance", 18}  
    DoFiledAndMethod(user)  
    fmt.Println("--------------------------------------------------")  
    DoFiledAndMethodOnPtr(&user)  
}  
  
func DoFiledAndMethod(obj interface{}) {  
    // 获取obj的type  
    Type := reflect.TypeOf(obj)  
    fmt.Println("inputType is ", Type.Name())  
    // 获取obj的value  
    Value := reflect.ValueOf(obj)  
    fmt.Println("inputValue is ", Value)  
  
    //获取type里面的字段  
  
    for i := 0; i < Type.NumField(); i++ {  
       field := Type.Field(i)  
       value := Value.Field(i).Interface()  
  
       fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)  
    }  
    //通过type里面的方法  
    for i := 0; i < Type.NumMethod(); i++ {  
       method := Type.Method(i)  
  
       fmt.Printf("%s: %v\n", method.Name, method.Type)  
    }  
}  
  
func DoFiledAndMethodOnPtr(input interface{}) {  
    Type := reflect.TypeOf(input)  
    Value := reflect.ValueOf(input)  
  
    // If pointer get the underlying element≤  
    if Type.Kind() == reflect.Ptr {  
       Type = Type.Elem()  
       Value = Value.Elem()  
    }  
  
    fmt.Println("inputType is ", Type.Name())  
    fmt.Println("inputValue is ", Value)  
  
    for i := 0; i < Type.NumField(); i++ {  
       field := Type.Field(i)  
       value := Value.Field(i).Interface()  
       fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)  
    }  
  
    for i := 0; i < Type.NumMethod(); i++ {  
       method := Type.Method(i)  
       fmt.Printf("%s: %v\n", method.Name, method.Type)  
    }  
}

输出:

inputType is  User
inputValue is  {1 Chance 18}
Id: int = 1
Name: string = Chance
Age: int = 18
Call: func(main.User)
--------------------------------------------------
inputType is  User
inputValue is  {1 Chance 18}
Id: int = 1
Name: string = Chance
Age: int = 18
Call: func(main.User)

结构体标签

在Go语言中,结构体字段可以通过标签(Tag)附加元数据。这些标签不会直接影响程序的行为,但可以通过反射在运行时读取,常用于序列化/反序列化、表单验证、ORM映射等场景。

结构体标签的基本使用

结构体标签是定义在结构体字段后的字符串字面量,通常由一系列键值对组成,键和值之间使用冒号分隔,多个键值对之间用空格分隔。

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age" validate:"min=18"`
}

在这个例子中,User结构体的每个字段都有一个 json标签,用于定义JSON序列化时该字段的名称。Age字段还有一个额外的 validate标签,可能被用于字段验证。

读取标签

要读取标签,你需要使用 reflect包。以下是一个示例,演示如何读取结构体字段的标签:

func PrintTags(u User) {
    t := reflect.TypeOf(u)
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("Field: %s, Tag: '%s'\n", field.Name, field.Tag)
    }
}

func main() {
    user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
    PrintTags(user)
}

这段代码使用反射来遍历 User类型的字段,并打印出每个字段的名称和标签。

标签的应用场景
  1. JSON序列化/反序列化:通过 encoding/json包,可以控制结构体如何被转换为JSON,或从JSON转换回来。
  2. 数据库ORM映射:在使用ORM(对象关系映射)框架时,标签可用于定义数据库表的列名、类型等信息。
  3. 表单验证:标签可以用于定义字段的验证规则,例如,必填、最大长度、格式等。
注意事项
  • 结构体标签提供了元数据,但它们本身并不直接改变程序的行为。它们的作用通常体现在与特定库或框架配合时。
  • 不同的库或框架可能对标签的格式和解析方式有不同的要求。例如,jsonxmlgorm等库都有自己的标签格式。
  • 在使用标签时,应遵循库或框架的文档指南,以确保正确地应用标签。

结构体标签在Go中是一种强大的机制,用于提供关于结构体字段的额外信息,尤其在数据序列化和ORM映射等方面非常有用。

json实例

package main  
  
import (  
    "encoding/json"  
    "fmt")  
  
type Movie struct {  
    Title  string   `json:"title"` // 通过指定tag实现json序列化该字段时的key  
    Year   int      `json:"year"`  // 同上  
    Price  int      `json:"price"`  
    Actors []string `json:"actors"`  
}  
  
func main() {  
    movie := Movie{  
       Title:  "喜剧之王",  
       Year:   2000,  
       Price:  10,  
       Actors: []string{"周星驰", "张柏芝"},  
    }  
  
    jsonStr, err := json.Marshal(movie) // 返回byte切片  
    if err != nil {  
       fmt.Println("json marshal error", err)  
       return  
    }  
    fmt.Printf("%s\n", jsonStr)  
  
    inputMovie := Movie{}  
    err = json.Unmarshal(jsonStr, &inputMovie)  
    if err != nil {  
       fmt.Println("json unmarshal error", err)  
       return  
    }  
    fmt.Printf("%v\n", inputMovie)  
}

输出:

{"title":"喜剧之王","year":2000,"price":10,"actors":["周星驰","张柏芝"]}
{喜剧之王 2000 10 [周星驰 张柏芝]}
文章来源:https://blog.csdn.net/gulugulu1103/article/details/134952513
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。