Go面向对象编程

结构体

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。

结构体表示一项记录,比如保存图书馆的书籍记录,每本书有以下属性:

  • Title:标题
  • Author: 作者
  • Subject:学科
  • ID:书籍ID

结构体成员是由一系列的成员变量构成,这些成员变量也被称为"字段"。字段有以下特性:

  1. 字段拥有自己的类型和值。
  2. 字段名必须唯一。
  3. 字段的类型也可以是结构体,甚至是字段所在结构体的类型。

Go 中的 struct 与 class

Golang 没有类(class),Go 语言的==结构体(struct)==和其它编程语言的类(class)有同等的地位,你可以理解Golang 是基于 struct 来实现OOP(面向对象编程)特性的。

结构体定义

package main

import "fmt"

func main() {
    type Cat struct{
        Name string
        Age int
        Color string
    }

    cat1 := Cat{}
    cat1.Name = "Bob"
    cat1.Age = 1
    cat1.Color = "White"

    fmt.Printf("cat1 Name is %v, age is %v, color is %v", cat1.Name, cat1.Age, cat1.Color)
}

如果结构体的字段类型是: slice==和 ==map ,默认零值都是 nil ,即还没有分配空间。如果需要使用这样的字段,需要先make,才能使用:

package main

import "fmt"

func main() {
    type Cat struct{
        Name string
        slc []string
        mp map[string]string
    }

    cat1 := Cat{}
    cat1.Name = "Bob"

    cat1.slc = make([]string, 0, 10) // 初始化一个空切片
    cat1.slc = append(cat1.slc, "test") // 添加一个元素

    cat1.mp = make(map[string]string, 10) // 初始化一个map
    cat1.mp["a"] = "yes" // 添加一个元素

    fmt.Printf("cat1 name is %v, cat1 slc is %v, cat1 mp is %v", cat1.Name, cat1.slc, cat1.mp)
}

结构体变量声明

// 第一种方式:直接声明
var person Person

// 第二种方式:{}
var person Person = Person{}
// 短变量声明 person := Person{}

// 第三种方式:new
var person *Person = new (Person)

// 第四种方式
var person *Person = &Person{}
// 短变量声明 person := &Person{}

结构体指针访问

  1. 第3种和第4种方式返回的是==结构体指针==。
  2. 结构体指针访问字段的标准方式应该是:(*person).Name = "jack"。但 go 做了一个简化,也支持 person.Name = "jack"。go 编译器底层对 person.Name 做了转化 (*person).Name
  3. 如果需要使用指针,推荐 person := &Person{} 这样的写法;如果不需要指针,推荐 person := Person{} 的写法

结构体方法

示例1:

package main

import "fmt"

type Calculator struct {
    Sku string
}

func (cal Calculator) get_sku() {
    fmt.Println("Sku of the cal is:", cal.Sku)
}

func (cal Calculator) get_sum(m int, n int) int {
    return m + n
}

func main() {
    cal1 := Calculator{Sku: "Cascio"} //声明结构体实例的时候,同时设置属性值
    // cal1 := &Calculator{Sku: "Cascio"}
    cal1.get_sku()
    res := cal1.get_sum(1, 2)
    fmt.Println("result is:", res)
}

接收者(receiver)

(cal Calculator) 是 Go 方法的"接收者"(receiver),表示这两个函数是 Calculator 结构体的方法。
- (cal Calculator) 表示该方法属于 Calculator 类型,只有 Calculator 类型的变量才能调用这个方法。
- cal 是接收者的名字,可以在方法体内用 cal 访问结构体的字段和方法,相当于 thisself

示例2:

package main

import (
    "fmt"
)

//结构体定义
type Person struct {
    Name string
    Age  int
}

//给结构体创建方法
// p是方法接收者
// *Person 是方法接收者的类型
// 类型可以是值类型 或 指针类型
func (p *Person) GetInfo() string {
    return fmt.Sprintf("Name:%s, Age:%d", p.Name, p.Age)
}

func main() {
    p := Person{Name: "Alice", Age: 30}
    fmt.Printf("Name:%s, Age:%d", p.Name, p.Age)
    p.GetInfo()
}

工厂模式

工厂函数

Go 中的工厂模式(Factory Pattern)是一种常用的设计模式,用于封装对象的创建过程。它通过一个工厂函数(通常以 NewXXX 命名)来创建并返回结构体实例,而不是直接在外部用结构体字面量初始化。

工厂模式的优点:

可以隐藏结构体的创建细节(如初始化默认值、做参数校验等)便于后续扩展和维护。

工厂函数示例

package main

import "fmt"

type Calculator struct {
  Sku string
}

func (cal Calculator) get_sku() {
  fmt.Println("Sku of the cal is:", cal.Sku)
}

func (cal Calculator) get_sum(m int, n int) int {
    return m + n
}

func NewCalculator(sku string) *Calculator {
    return &Calculator{Sku: sku}
}

func main() {
    cal1 := NewCalculator("Cascio")
    cal1.get_sku()
    res := cal1.get_sum(1, 2)
    fmt.Println("result is:", res)
}

这样,外部只需调用 NewCalculator,无需关心结构体的具体初始化细节。这就是 Go 中的工厂模式。

跨包声明结构体

结构体名字大写

goproject/factory/model/model.go 里面定义了这样一个结构体:

package model

type Student struct {
    Name  string
    Score float64
}

如果想在goproject/factory/main/main.go里面创建Student实例:

package main

import(
    "fmt"
    "goproject/factory/model"
)

func main() {
    stu := model.Student{ //可以直接跨包引用结构体,因为源结构体是大写的Student,能跨包
        Name: "Bob",
        Score: 99,
    }
    fmt.Println(stu)
}

结构体名字小写

goproject/factory/model/model.go 里面定义了这样一个结构体。如果想在goproject/factory/main/main.go里面创建student实例,得用工厂函数实现。

goproject/factory/model/model.go:

package model

type student struct {
    Name  string
    Score float64
}

func NewStudent(name string, score float64) *student {
    return &student{
        Name: name,
        Score: score,
    }
}

goproject/factory/main/main.go:

package main

import(
    "fmt"
    "goproject/factory/model"
)

func main() {
    stu := model.NewStudent("Bob", 99)
    fmt.Println(stu)
    fmt.Println(*stu)
    fmt.Printf("stu name is %v, score is %v", stu.Name, stu.Score)
}

结构体属性也是小写

goproject/factory/model/model.go 里面定义了这样一个结构体。如果想在goproject/factory/main/main.go里面创建student实例,并且访问到里面的小写属性值,可以定义获取值的函数。

goproject/factory/model/model.go

package model

type student struct {
    Name  string
    score float64
}

func NewStudent(name string, score float64) *student {
    return &student{
        Name: name,
        score: score,
    }
}

func (stu student) GetScore() float64{
    return stu.score
}

goproject/factory/main/main.go:

package main

import (
    "fmt"
    "goproject/factory/model"
)

func main() {
    stu := model.NewStudent("Bob", 99) //虽然score小写了,但是可以用工厂函数初始化。
    fmt.Println(stu)
    fmt.Println(*stu)
    //fmt.Printf("stu name is %v, score is %v", stu.Name, stu.score) //这样访问score就报错不能跨包访问了
    fmt.Printf("stu name is %v, score is %v", stu.Name, stu.GetScore())
}

面向对象三大特性

Go 的 OOP 实现

Golang 仍然有面向对象编程的==继承==,==封装==和==多态==的特性,只是实现的方式和其它OOP语言不一样。

封装

封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作。

  1. 隐藏实现细节
  2. 可以对数据进行验证,保证安全合理(Age)

封装的实现步骤:

  1. 将结构体、字段(属性)的首字母==小写==(不能导出,其它包不能使用,类似 private)

  2. 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数

  3. 提供一个首字母大写的 Set 方法 (类似其它语言的 public),用于对属性判断并赋值

  4. SetXxx 通常用==指针接收者==,以便修改原对象;若需要失败原因,可返回 error。

//示例
func (stu *student) SetScore(score float64) error {
    if score < 0 || score > 100 {
        return fmt.Errorf("invalid score %v", score)
    }
    stu.score = score
    return nil
}
// 因为接收者是 *student,内部对 stu.score 的修改会反映到原始对象;如果改用 student(值接收者),修改只作用在拷贝上,调用者看不到变化。
  1. 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值

  2. Getter 一般用==值接收者==,且如果返回的是导出字段,可直接暴露字段而不是写方法。

//示例
func (stu student) GetScore() float64 {
    return stu.score
}
//值接收者就是接收者不带 *,方法获得的是对象的一个拷贝。Getter 通常只读字段,不需要修改原对象,所以值接收者更简洁
  1. 如果字段需要封装(私有化),应以小写开头并通过 SetXxx 校验赋值、GetXxx 暴露读取;若字段不需要校验,直接导出(大写开头)即可,Go 项目里往往不写显式 Getter。

封装完整示例

goproject/factory/model/model.go:

package model

import "fmt"

// 演示封装,创建一个Person,里面不能随意查看age和salary,并对输入的age进行验证
type person struct {
    Name string
    age  int
    sal  float64
}

//工厂函数,相当于构造函数。只初始化Name一个public参数
func NewPerson(name string) *person {
    return &person{
        Name: name,
    }
}

//为了访问age和sal,编写SetXxx和GetXxx
// SetXxx用指针接收者
func (p *person) SetAge(age int) {
    if age > 0 && age < 150 {
        p.age = age
    } else {
        fmt.Println("Please set a valid age")
    }
}

func (p *person) SetSal(sal float64) {
    if sal > 3000 && sal < 50000 {
        p.sal = sal
    } else {
        fmt.Println("Please set a valid salary")
    }
}

// GetXxx用值接收者
func (p person) GetAge() int {
    return p.age
}

func (p person) GetSal() float64 {
    return p.sal
}

goproject/factory/main/main.go:

package main

import (
    "fmt"
    "goproject/factory/model"
)

func main() {
    bob := model.NewPerson("Bob")
    bob.SetAge(28)
    bob.SetSal(10000)
    fmt.Printf("Bob name is %v, age is %v, sal is %v", bob.Name, bob.GetAge(), bob.GetSal())
}

继承

==继承==可以解决代码复用,让我们的编程更加靠近人类思维。当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体

其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个==匿名结构体==即可。如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。

package main

import "fmt"

type Student struct {
    Name string
    Age int
    Score int
}

// 定义小学生,嵌入了Student匿名结构体
type Pupil struct{
    Student
}

// 定义大学生,嵌入了Student匿名结构体
type Graduate struct{
    Student
}

//Student的构造函数(工厂函数)
func (stu *Student) SetScore(score int) {
    stu.Score = score
}

// Student的方法,Pupil和Graduate都可以使用
func (stu Student) CalSum(x int, y int) int {
    return x + y
}

//将Pupil 和 Graduate 共有的方法也绑定到 *Student
func (stu *Student) ShowInfo() {
    fmt.Printf("Name is %v, age is %v, score is %v", stu.Name, stu.Age, stu.Score)
}

//Pupil特有的方法
func (pup *Pupil) ShowPupil() {
    fmt.Println("This is a pupil")
}

//Graduate特有的方法
func (gra *Graduate) ShowGraduate() {
    fmt.Println("This is a graduate")
}

func main() {

    pupil1 := &Pupil{}
    pupil1.Student.Name = "Bob"
    pupil1.Student.Age = 10
    pupil1.ShowPupil()
    pupil1.Student.SetScore(90)
    pupil1.Student.ShowInfo()
    fmt.Println("\nUse method for Student and get sum:", pupil1.Student.CalSum(1, 2))

    gra1 := &Graduate{}
    gra1.Student.Name = "Tom"
    gra1.Student.Age = 19
    gra1.ShowGraduate()
    gra1.Student.SetScore(88)
    gra1.Student.ShowInfo()
    fmt.Println("\nUse method for Student and get sum:", pupil1.Student.CalSum(3, 4))
}

多态

接口

package main

import "fmt"

// 1) 接口中的方法没有具体实现(没有方法体),只定义方法名和参数列表。
// 2) 接口体现了多态(同一接口,不同实现)和高内聚低耦合(调用方只依赖接口,不依赖具体实现)。
// 3) Go语言的接口是隐式实现的:只要某个类型实现了接口中的所有方法,就认为它实现了该接口,无需像Java那样用implement关键字。
type Usb interface {
    // Usb接口,定义了两个方法:Start和Stop。
    Start() // 启动设备的方法
    Stop()  // 停止设备的方法
}

// PowerBank结构体,表示充电宝。
// PowerBank可以为实现了Usb接口的设备充电。
type PowerBank struct {
}

// CharingDevice方法为PowerBank绑定。
// 参数usb为Usb接口类型,表示任何实现了Usb接口的设备。
// 只要传入的对象实现了Start和Stop方法,都可以被PowerBank充电,实现多态。
func (pb PowerBank) CharingDevice(usb Usb) {
    usb.Start() // 启动充电
    fmt.Println("Charing for 5 minutes")
    usb.Stop() // 停止充电
}

// Phone结构体,表示手机。
// 下面会为Phone实现Usb接口要求的Start和Stop方法。
type Phone struct {
}

// 为Phone类型绑定Start方法,实现Usb接口的Start方法。
func (pho *Phone) Start() {
    fmt.Println("Phone is now recharging")
}

// 为Phone类型绑定Stop方法,实现Usb接口的Stop方法。
func (pho *Phone) Stop() {
    fmt.Println("Phone has stopped recharging")
}

// Pad结构体,表示平板。
// 下面会为Pad实现Usb接口要求的Start和Stop方法。
type Pad struct {
}

// 为Pad类型绑定Start方法,实现Usb接口的Start方法。
func (pad *Pad) Start() {
    fmt.Println("pad is now recharing")
}

// 为Pad类型绑定Stop方法,实现Usb接口的Stop方法。
func (pad *Pad) Stop() {
    fmt.Println("pad has stopped recharing")
}

// main函数是程序的入口。
func main() {
    pb1 := &PowerBank{} // 创建一个PowerBank实例
    pho1 := &Phone{}    // 创建一个Phone实例
    pad1 := &Pad{}      // 创建一个Pad实例

    // 通过PowerBank的CharingDevice方法,给Phone和Pad充电。
    // 这里体现了接口的多态性:CharingDevice方法只依赖Usb接口,不关心具体是Phone还是Pad。
    pb1.CharingDevice(pho1)
    pb1.CharingDevice(pad1)
}

Go 接口的特点

Go语言的接口是==隐式实现==的:只要某个类型实现了接口中的所有方法,就认为它实现了该接口,无需像Java那样用 implement 关键字。

多态

在 Go 语言,==多态==特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。

在前面的 Usb 接口案例,usb Usb ,既可以接收手机变量,又可以接收Pad变量,就体现了Usb 接口的多态特性。

类型断言

Go中的==类型断言==(type assertion)是用于将接口类型的变量转换为具体类型的语法。它的作用是:在已知接口变量的实际底层类型时,将其还原为具体类型,以便访问该类型特有的方法或字段。

为什么需要类型断言

实际开发中,很多场景下你拿到的是接口类型变量,而不是具体类型的结构体变量。接口变量无法调用某个具体类型结构体的独有方法。这时候就需要类型断言,将接口变量还原成具体类型,就能调用他的独有方法了

package main

import "fmt"

// 1) 接口中的方法没有具体实现(没有方法体),只定义方法名和参数列表。
// 2) 接口体现了多态(同一接口,不同实现)和高内聚低耦合(调用方只依赖接口,不依赖具体实现)。
// 3) Go语言的接口是隐式实现的:只要某个类型实现了接口中的所有方法,就认为它实现了该接口,无需像Java那样用implement关键字。
type Usb interface {
    // Usb接口,定义了两个方法:Start和Stop。
    Start() // 启动设备的方法
    Stop()  // 停止设备的方法
}

// PowerBank结构体,表示充电宝。
// PowerBank可以为实现了Usb接口的设备充电。
type PowerBank struct {
}

// CharingDevice方法为PowerBank绑定。
// 参数usb为Usb接口类型,表示任何实现了Usb接口的设备。
// 只要传入的对象实现了Start和Stop方法,都可以被PowerBank充电,实现多态。
func (pb PowerBank) CharingDevice(usb Usb) {
    usb.Start() // 启动充电
    fmt.Println("Charing for 5 minutes")
    usb.Stop() // 停止充电
}

// Phone结构体,表示手机。
// 下面会为Phone实现Usb接口要求的Start和Stop方法。
type Phone struct {
}

// 假设Phone有一个特有方法SendMessage
func (pho *Phone) SendMessage(msg string) {
    fmt.Println("Phone send message:", msg)
}

// 为Phone类型绑定Start方法,实现Usb接口的Start方法。
func (pho *Phone) Start() {
    fmt.Println("Phone is now recharging")
}

// 为Phone类型绑定Stop方法,实现Usb接口的Stop方法。
func (pho *Phone) Stop() {
    fmt.Println("Phone has stopped recharging")
}

// Pad结构体,表示平板。
// 下面会为Pad实现Usb接口要求的Start和Stop方法。
type Pad struct {
}

// 为Pad类型绑定Start方法,实现Usb接口的Start方法。
func (pad *Pad) Start() {
    fmt.Println("pad is now recharing")
}

// 为Pad类型绑定Stop方法,实现Usb接口的Stop方法。
func (pad *Pad) Stop() {
    fmt.Println("pad has stopped recharing")
}

// main函数是程序的入口。
func main() {
    pb1 := &PowerBank{} // 创建一个PowerBank实例
    pho1 := &Phone{}    // 创建一个Phone实例
    pad1 := &Pad{}      // 创建一个Pad实例

    // 通过PowerBank的CharingDevice方法,给Phone和Pad充电。
    // 这里体现了接口的多态性:CharingDevice方法只依赖Usb接口,不关心具体是Phone还是Pad。
    pb1.CharingDevice(pho1)
    pb1.CharingDevice(pad1)

    // ----------- 实际开发场景:只拿到接口类型变量 -----------
    // 假设我们有一个Usb接口类型的切片,里面存放了多种实现Usb接口的设备
    devices := []Usb{pho1, pad1}

    for _, dev := range devices {
        dev.Start() // 只能调用Usb接口声明的方法
        // dev.SendMessage("hello") // 编译报错:Usb接口没有SendMessage方法

        // 如果需要调用Phone特有的方法,需要先做类型断言
        // 类型断言的安全写法:if ... ok
        if phone, ok := dev.(*Phone); ok {
            fmt.Println("发现Phone类型,调用其特有方法SendMessage:")
            phone.SendMessage("hello world!")
        }
        // 如果直接写 phone := dev.(*Phone) ,当dev不是*Phone类型时会直接panic,程序崩溃:
        // phone := dev.(*Phone) // 不安全写法,断言失败会panic
        // 推荐用上面的if ... ok写法,保证程序健壮性
        dev.Stop()
    }

    // 这样可以体现:实际开发中我们经常只拿到接口类型变量,
    // 只有通过类型断言才能访问具体类型的特有功能。
}

类型断言安全写法

始终使用 if phone, ok := dev.(*Phone); ok { ... } 的双返回值写法,避免断言失败时 panic 导致程序崩溃。


相关笔记: go-基础-05-函数-包 | go-基础-02-数组-切片-map | go-基础-06-错误处理 | go-web开发 | 云原生开发-基础