golang学习【9】:面向对象进阶

本文介绍了Go语言中面向对象编程的深入概念,包括结构体标签的使用、静态方法与实例方法的区别、接口的定义与实现、继承和多态的运用。通过扑克游戏和工资结算系统的案例,展示了如何在实际项目中应用这些面向对象的特性。
摘要由CSDN通过智能技术生成

面向对象进阶

在前面的章节我们已经了解了面向对象的入门知识,知道了如何定义类,如何创建对象以及如何给对象发消息。为了能够更好的使用面向对象编程思想进行程序开发,我们还需要对golang中的面向对象编程进行更为深入的了解。

结构体标签

Go 语言支持在结构体字段定义时使用标签(tag),它们是紧跟在字段定义后面的双引号包围的字符串,用于提供与该字段相关的额外信息。结构体标签最常见于序列化/反序列化库(如 JSON、XML、SQL 库)的上下文中,用于指定字段的序列化名称、格式、忽略规则等。例如:

type User struct {
    ID        int    `json:"id"`
    Name      string `json:"name,omitempty"`
    Email     string `validate:"required,email"`
}

静态方法和实例方法

**静态方法:**静态方法是指没有明确接收者(receiver)定义的方法,它等同于其他编程语言中的“类方法”或“静态方法”。在 Go 语言中,静态方法实际上是属于一个包级别的函数,可以直接通过包名来调用,无需关联任何特定类型的实例。静态方法通常用于处理与特定类型相关的全局行为,而不依赖于某个特定对象的状态。

**实例方法:**实例方法是指在方法定义时指定了一个接收者(receiver),这个接收者通常是某种类型的变量,它可以是命名类型(如 struct 或 interface)的实例。接收者方法与特定类型的实例紧密相关,通过实例来调用,并且能够访问和修改该实例的属性。接收者方法可以分为值接收者方法和指针接收者方法,取决于接收者的定义是类型本身还是该类型的指针。

// 静态方法示例
func ConvertToUpperCase(s string) string {
    return strings.ToUpper(s)
}

// 使用静态方法
converted := example.ConvertToUpperCase("hello")

// 定义一个名为Person的struct
type Person struct {
    Name string
    Age  int
}

// 定义接收者方法(实例方法)
func (p Person) SayHello() string {
    return fmt.Sprintf("Hello, my name is %s and I am %d years old.", p.Name, p.Age)
}

// 使用接收者方法
person := Person{Name: "Alice", Age: 30}
greeting := person.SayHello()

// 定义一个带有指针接收者的方法
func (p *Person) IncrementAge() {
    p.Age++
}

// 使用指针接收者方法
person := &Person{Name: "Bob", Age: 25}
person.IncrementAge()
fmt.Println(person.Age) // 输出: 26

Go 语言中的静态方法类似于没有接收者的包级函数,而动态方法(接收者方法)则与特定类型的实例关联,能够访问和操作实例的状态。静态方法适用于不需要访问或修改特定对象状态的全局功能,而接收者方法更适合处理与对象自身状态紧密相关的操作

接口

Go 语言的接口是一种类型定义,它描述了一组方法签名,这些方法构成了接口所要求的行为。接口允许开发者定义类型间通用的行为规范,而无需关心具体的实现细节。任何类型,只要提供了接口所要求的所有方法,就被认为实现了该接口。这种机制支持了 Go 语言的鸭子类型(duck typing)理念:如果一个东西走起来像鸭子,叫起来像鸭子,那么它就是鸭子。在 Go 中,如果一个类型的行为满足接口的要求,那么就可以将其视为该接口的实例

//接口定义
type Printable interface {
    Print() string
}

type Book struct {
    Title  string
    Author string
}

/*
一个类型通过提供接口所需的所有方法的实现,即可隐式地实现该接口,无需显式声明。
*/
//接口实现
func (b Book) Print() string {
    return fmt.Sprintf("Title: %s, Author: %s", b.Title, b.Author)
}

接口变量可以存储任何实现了该接口的类型实例。接口变量的值部分保存了具体类型实例的值,类型部分保存了实际类型的信息。通过类型断言,可以访问接口变量内部的具体类型实例及其方法

var p Printable
p = Book{"The Catcher in the Rye", "J.D. Salinger"}

// 类型断言访问 Book 实例的方法
book := p.(Book)
fmt.Println(book.Print())

// 可以使用类型断言同时判断和转换类型
if book, ok := p.(Book); ok {
    fmt.Println(book.Print())
} else {
    fmt.Println("Not a Book")
}

insterface{}空接口是一个比较特殊的接口类型,其特点如下

  • 任何类型都满足空接口:因为空接口不包含任何方法要求,所有类型都自动实现了空接口(包括基本类型、自定义类型、结构体等)。这意味着任何类型的值都可以赋值给空接口类型的变量。
  • 通用数据容器:空接口常被用作通用数据容器,可以存储和传递任意类型的值
  • 类型断言与类型切换: 当使用空接口变量存储具体类型值时,可以通过类型断言来提取出原始值及其类型
  • 反射(Reflection)的基础: 空接口是Go语言反射机制的重要组成部分。通过将值赋给空接口类型,然后使用reflect包,可以获取关于该值的运行时类型信息、访问其字段、调用其方法等
  • 接口间的兼容性: 由于空接口是最基础的接口类型,任何非空接口(即包含至少一个方法声明的接口)都是空接口的子集。

示例

func printValue(value interface{}) {
    fmt.Printf("Received value: %v (Type: %T)\n", value, value)
}

func processValue(value interface{}) {
    if num, ok := value.(int); ok {
        fmt.Println("Integer value:", num)
    } else if str, ok := value.(string); ok {
        fmt.Println("String value:", str)
    } else {
        fmt.Println("Unknown value type")
    }
}

func main() {
    printValue(10)      // 整数
    printValue("Hello") // 字符串
    printValue(true)    // 布尔值
    printValue([]int{1, 2, 3}) // 切片

    // 注意:以下代码会导致panic,因为类型断言失败
    // processValue(3.14)
    processValue(42)
    processValue("Forty-two")

    //实现字典/json
    dictionary := make(map[string]interface{})

    dictionary["age"] = 30
    dictionary["name"] = "Alice"
    dictionary["isStudent"] = false
    dictionary["scores"] = map[string]int{"math": 90, "english": 85}

    fmt.Println(dictionary)
}

类之间的关系

简单的说,类和类之间的关系有三种:is-a、has-a和use-a关系。

  • is-a关系也叫继承或泛化,比如学生和人的关系、手机和电子产品的关系都属于继承关系。
  • has-a关系通常称之为关联,比如部门和员工的关系,汽车和引擎的关系都属于关联关系;关联关系如果是整体和部分的关联,那么我们称之为聚合关系;如果整体进一步负责了部分的生命周期(整体和部分是不可分割的,同时同在也同时消亡),那么这种就是最强的关联关系,我们称之为合成关系。
  • use-a关系通常称之为依赖,比如司机有一个驾驶的行为(方法),其中(的参数)使用到了汽车,那么司机和汽车的关系就是依赖关系。

我们可以使用一种叫做UML(统一建模语言)的东西来进行面向对象建模,其中一项重要的工作就是把类和类之间的关系用标准化的图形符号描述出来。关于UML我们在这里不做详细的介绍,有兴趣的读者可以自行阅读《UML面向对象设计基础》一书。

利用类之间的这些关系,我们可以在已有类的基础上来完成某些操作,也可以在已有类的基础上创建新的类,这些都是实现代码复用的重要手段。复用现有的代码不仅可以减少开发的工作量,也有利于代码的管理和维护,这是我们在日常工作中都会使用到的技术手段。

继承和多态

刚才我们提到了,可以在已有类的基础上创建新类,这其中的一种做法就是让一个类从另一个类那里将属性和方法直接继承下来,从而减少重复代码的编写。提供继承信息的我们称之为父类,也叫超类或基类;得到继承信息的我们称之为子类,也叫派生类或衍生类。子类除了继承父类提供的属性和方法,还可以定义自己特有的属性和方法,所以子类比父类拥有的更多的能力,在实际开发中,我们经常会用子类对象去替换掉一个父类对象,这是面向对象编程中一个常见的行为,对应的原则称之为里氏替换原则

golang当中没有类的概念,所以继承的实现需要通过结构体,结构体在上一章有所介绍,我们看回之前的例子:

package main

import "fmt"

// Animal 基础结构体
type Animal struct {
    Name string
    Age  int
}

// Dog 结构体,内嵌Animal
type Dog struct {
    Animal
    Breed string
}

func (a Animal) Speak() {
    fmt.Println("An animal speaks")
}

func (d Dog) Speak() {
    fmt.Println("A dog barks")
}

func main() {
    d := Dog{
        Animal: Animal{Name: "Buddy", Age: 3},
        Breed:  "Golden Retriever",
    }

    fmt.Println("Dog's name:", d.Name) // 访问内嵌的Animal的Name字段
    d.Speak()                           // 调用Dog的Speak方法,而非Animal的Speak方法
}

在这个例子中,Dog 结构体内嵌了 Animal 结构体。这样,Dog 就继承了 Animal 的字段 Name 和 Age。同时,Dog 结构体还有一个自己的字段 Breed。当 Dog 和 Animal 都有 Speak 方法时,Dog 的实例 d 会调用 Dog 的 Speak 方法,这展示了方法覆盖的效应。

在Go语言中,多态是通过接口(interfaces)实现的。接口是一种定义了一个或多个方法的类型。任何实现了这些方法的具体类型都自动被认为是实现了该接口。这样,不同的具体类型可以通过接口来进行交互,允许你在不知道具体类型的情况下编写函数,这些函数可以操作任何实现了接口的类型。

package main

import "fmt"

// Shape 接口定义了一个方法 Area
type Shape interface {
    Area() float64
}

// Rectangle 结构体有两个属性:Width 和 Height
type Rectangle struct {
    Width  float64
    Height float64
}

// Area 方法计算矩形的面积
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Circle 结构体有一个属性:Radius
type Circle struct {
    Radius float64
}

// Area 方法计算圆的面积
func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}

// Area 计算任何Shape的面积并打印结果
func PrintArea(s Shape) {
    fmt.Printf("The area of the shape is: %v\n", s.Area())
}

func main() {
    r := Rectangle{Width: 10, Height: 5}
    c := Circle{Radius: 3}

    PrintArea(r) // 输出矩形的面积
    PrintArea(c) // 输出圆的面积
}

在这个例子中,Shape 接口定义了一个 Area 方法。Rectangle 和 Circle 类型都实现了这个方法,因此它们都满足 Shape 接口。PrintArea 函数接受一个 Shape 接口类型的参数,并调用它的 Area 方法。由于 Rectangle 和 Circle 都实现了 Area 方法,我们可以将它们传递给 PrintArea 函数,而不需要知道它们的具体类型。这就是Go中的多态性。

综合案例

案例1:扑克游戏。
package main

import (
	"fmt"
	"math/rand"
	"time"
)

type Card struct {
	suite string
	face  int
}

func NewCard(suite string, face int) *Card {
	return &Card{suite: suite, face: face}
}

func (c *Card) String() string {
	faceStr := ""
	switch c.face {
	case 1:
		faceStr = "A"
	case 11:
		faceStr = "J"
	case 12:
		faceStr = "Q"
	case 13:
		faceStr = "K"
	default:
		faceStr = fmt.Sprintf("%d", c.face)
	}
	return c.suite + faceStr
}

type Poker struct {
	cards    []*Card
	current  int
}

func NewPoker() *Poker {
	p := &Poker{
		cards:   make([]*Card, 52),
		current: 0,
	}
	suites := []string{"♠", "♥", "♣", "♦"}
	idx := 0
	for _, suite := range suites {
		for face := 1; face < 14; face++ {
			p.cards[idx] = NewCard(suite, face)
			idx++
		}
	}
	return p
}

func (p *Poker) Shuffle() {
	p.current = 0
	rand.Seed(time.Now().UnixNano())
	rand.Shuffle(len(p.cards), func(i, j int) { p.cards[i], p.cards[j] = p.cards[j], p.cards[i] })
}

func (p *Poker) Next() *Card {
	card := p.cards[p.current]
	p.current++
	return card
}

func (p *Poker) HasNext() bool {
	return p.current < len(p.cards)
}

type Player struct {
	name         string
	cardsOnHand  []*Card
}

func NewPlayer(name string) *Player {
	return &Player{
		name:       name,
		cardsOnHand: make([]*Card, 0),
	}
}

func (p *Player) Get(card *Card) {
	p.cardsOnHand = append(p.cardsOnHand, card)
}

func (p *Player) Arrange() {
	sort.Slice(p.cardsOnHand, func(i, j int) bool {
		return p.cardsOnHand[i].suite < p.cardsOnHand[j].suite || (p.cardsOnHand[i].suite == p.cardsOnHand[j].suite && p.cardsOnHand[i].face < p.cardsOnHand[j].face)
	})
}

func main() {
	p := NewPoker()
	p.Shuffle()
	players := []*Player{
		NewPlayer("东邪"),
		NewPlayer("西毒"),
		NewPlayer("南帝"),
		NewPlayer("北丐"),
	}

	for i := 0; i < 13; i++ {
		for _, player := range players {
			player.Get(p.Next())
		}
	}

	for _, player := range players {
		fmt.Print(player.name, ": ")
		player.Arrange()
		for _, card := range player.cardsOnHand {
			fmt.Print(card, " ")
		}
		fmt.Println()
	}
}

说明: 大家可以自己尝试在上面代码的基础上写一个简单的扑克游戏,例如21点(Black Jack),游戏的规则可以自己在网上找一找。

案例2:工资结算系统。
/*
某公司有三种类型的员工 分别是部门经理、程序员和销售员
需要设计一个工资结算系统 根据提供的员工信息来计算月薪
部门经理的月薪是每月固定25000元
程序员的月薪按本月工作时间计算 每小时250元
销售员的月薪是2400元的底薪加上销售额5%的提成
*/
package main

import (
	"fmt"
	"strconv"
)

type Employee interface {
	GetSalary() float64
}

type Manager struct {
	Name string
}

func (m *Manager) GetSalary() float64 {
	return 25000.0
}

type Programmer struct {
	Name       string
	WorkingHour int
}

func (p *Programmer) GetSalary() float64 {
	return 250.0 * float64(p.WorkingHour)
}

type Salesman struct {
	Name  string
	Sales float64
}

func (s *Salesman) GetSalary() float64 {
	return 2400.0 + s.Sales*0.05
}

func main() {
	emps := []Employee{
		&Manager{Name: "刘备"},
		&Programmer{Name: "诸葛亮"},
		&Manager{Name: "曹操"},
		&Salesman{Name: "荀彧"},
		&Salesman{Name: "吕布"},
		&Programmer{Name: "张辽"},
		&Programmer{Name: "赵云"},
	}

	for _, emp := range emps {
		switch e := emp.(type) {
		case *Programmer:
			fmt.Printf("请输入%s本月工作时间: ", e.Name)
			var input string
			fmt.Scanln(&input)
			hours, err := strconv.Atoi(input)
			if err != nil {
				fmt.Println("输入错误,请输入一个整数")
				continue
			}
			e.WorkingHour = hours
		case *Salesman:
			fmt.Printf("请输入%s本月销售额: ", e.Name)
			var input string
			fmt.Scanln(&input)
			sales, err := strconv.ParseFloat(input, 64)
			if err != nil {
				fmt.Println("输入错误,请输入一个数字")
				continue
			}
			e.Sales = sales
		}
	}

	for _, emp := range emps {
		fmt.Printf("%s本月工资为: ¥%.2f元\n", emp.(interface{ GetName() string }).GetName(), emp.GetSalary())
	}
}

func (m *Manager) GetName() string {
	return m.Name
}

func (p *Programmer) GetName() string {
	return p.Name
}

func (s *Salesman) GetName() string {
	return s.Name
}
  • 20
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值