Skip to content

面向对象

作者: ryan 发布于: 9/19/2025 更新于: 9/23/2025 字数: 0 字 阅读: 0 分钟

面向对象三要素

封装

封装是将对象的属性(数据)和方法(操作)捆绑在一起,通过访问控制(例如私有或公开)隐藏实现细节,只暴露必要的接口。封装的目的是提高代码的模块化、降低耦合性,并保护数据的安全性。

Go 语言没有传统面向对象语言(如 Java 或 C++)中的 class 关键字,但通过结构体(struct)和方法(method)实现封装。Go 使用大小写字母来控制访问权限:

  • 大写字母开头的字段或方法是导出的(public),可在包外访问。
  • 小写字母开头的字段或方法是非导出的(private),仅限包内访问。

继承

继承允许子类从父类继承属性和方法,从而实现代码复用。子类可以扩展或覆盖父类的属性和方法,以实现特定的行为。

同样Go语言没有传统意义上的继承,而是通过**结构体嵌入(composition)**实现类似的功能。Go 提倡“组合优于继承”(Composition over Inheritance),通过将一个结构体嵌入另一个结构体,子结构体可以直接使用嵌入结构体的字段和方法。

go
package main

import "fmt"

// 父结构体
type Animal struct {
    Name string
}

func (a *Animal) Speak() string {
    return "I am an animal"
}

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

// 覆盖父结构体的方法
func (d *Dog) Speak() string {
    return "Woof! I am a " + d.Breed
}

func main() {
    dog := Dog{
        Animal: Animal{Name: "Max"},
        Breed:  "Golden Retriever",
    }
    fmt.Println(dog.Name)        // 访问嵌入结构体的字段
    fmt.Println(dog.Speak())    // 调用覆盖后的方法
}

多态

多态是指在继承的基础上,通过方法覆盖或接口实现,使得不同子类对象在调用同一方法时表现出不同的行为。多态通常依赖于父类引用指向子类对象。

Go 语言通过**接口(interface)**实现多态。Go 的接口是一种隐式实现机制,任何类型只要实现了接口定义的方法,就自动满足该接口。

Go 语言通过结构体、方法、嵌入和接口,巧妙地实现了封装、类似继承和多态的功能,它的面向对象实现更贴近“面向接口编程”,强调行为而非类型层次结构。以一种更简洁、灵活的方式取代了传统面向对象编程的复杂性。

构造函数

Go语言没有提供类似C++、Java一样的构造函数、析构函数。GO语言中构造函数并非内置语法特性,而是一种通过约定俗成的编程模式实现的工具(习惯上,构造函数命名是New或new开头),其核心作用是通过封装初始化和验证逻辑,提升代码的安全性、可维护性和灵活性。

这个函数没有特别的要求,只要返回结构体实例或其指针即可(建议返回指针,不然返回值会拷贝)。

为什么需要构造函数? 有的结构体里面有几十个属性,自己构建太麻烦。这时提供一个构造函数,只需要把最关心的属性的参数填进去,其他参数不用手动填了 如果结构体就几个属性,就没必要构造函数。如果有多个构造函数,可以使用不同命名函数,因为Go也没有函数重载。

在Go语言中构造函数就是一个普通函数,和其他语言不一样构造方法用来返回某种类型的实例或者该实例的指针

在示例中,NewDefaultAnimal 直接返回默认值 {"nobody", 1} 而 NewAnimal 则允许外部传入参数定制化结构体字段这种封装简化了调用方操作,同时隐藏了内部实现细节。

go
type Animal struct {  
    name string  
    age  int  
}  
  
func NewDefaultAnimal() *Animal {  
    return &Animal{"nobody", 1}  
}  
  
func NewAnimal(name string, age int) *Animal {  
    return &Animal{name, age}  
}

校验逻辑

构造函数可内嵌校验逻辑,确保创建的对象符合业务约束。例如,若要求 age 必须大于0,可在 NewAnimal 中添加验证:

go
func NewAnimal(name string, age int) *Animal {
    if age <= 0 {
        panic("age must be positive") // 或返回error更健壮
    }
    return &Animal{name, age}
}

这种设计从源头杜绝了非法状态,强化了程序的健壮性。

隐藏实现细节

若结构体字段改为小写(私有),调用方无法直接通过字面量初始化(如 Animal{name: "cat"} 会编译失败)。此时构造函数成为唯一合法的创建途径:

go
type animal struct { name string; age int }
func NewAnimal(name string, age int) *animal { ... }

但是,导出函数(即首字母大写的函数)若返回未导出的类型(如小写字母开头的结构体),虽然语法上允许,但这种设计属于典型的反模式,可能引发维护性和扩展性问题。

函数重载

在Go语言中,原生不支持函数重载(overload)(同名函数根据参数类型或数量自动匹配) ,这与Java、C++等语言有本质差异

重载函数(overload)

go

func add(x,y int) int{} //add(4,5)
func add(x,y string) string{} //add("4","5")
func add(x,y,z []int) int{} {}

注意:Go不支持参数不同的同名函数

只能为不同参数类型定义独立函数名

go
func AddInt(x, y int) int {}  
func AddString(x, y string) string {}  
func AddSlice(x, y, z []int) int {}

继承

Go通过结构体嵌入(组合)实现代码复用,而非传统的类继承。这种设计避免了类继承的层级复杂性问题,支持更灵活的代码结构

父有子就有

Cat结构体通过匿名嵌入Animal结构体(Animal不指定字段名),实现了类似继承的效果。此时Cat会直接继承Animal的字段(nameAge)和方法(run()

go
package main  
  
import "fmt"  
  
type Animal struct {  
    name string  
    Age  int  
}  
  
func (*Animal) run() {  
    fmt.Println("动物跑")  
}  
  
type Cat struct {  
    Animal //模拟继承  
    color  string  
}  
  
func main() {  
    cat := new(Cat)  
    cat.name = "Tom"  
    cat.color = "red"  
    fmt.Println(cat)  
    fmt.Println("_______________________________")  
    cat.run()  
}

运行结果

go
&{{Tom 0} red}
_______________________________
动物跑

Animalrun()方法接收者为*Animal指针类型。通过结构体嵌入,Cat可以直接调用run()方法,无需通过cat.Animal.run()显式访问

覆盖

在 Go 语言中,通过结构体嵌套实现的方法覆盖机制,可以模拟面向对象编程中的方法重写行为。

go
package main  
  
import "fmt"  
  
type Animal struct {  
    name string  
    Age  int  
}  
  
func (*Animal) run() {  
    fmt.Println("动物跑")  
}  
  
type Cat struct {  
    Animal  
    color string  
}  
  
//override 覆盖或重写,覆盖父类,父类不好,优先用自己的  
  
//func (*Cat) run() {  
//  fmt.Println("优雅的跑")  
//}  
  
func (c *Cat) run() {  
    // 覆盖有两种,1、完全覆盖 2、锦上添花,增强  
    c.Animal.run()  
    fmt.Println("猫优雅的跑")  
}  
  
func main() {  
    cat := new(Cat)  
    cat.name = "tom"  
    cat.color = "red"  
    fmt.Printf("%#v", cat)  
    fmt.Println("_________________________")  
    cat.run()  
      
    //指代使用父类的方法  
    cat.Animal.run()  
}

完全覆盖

直接在外层结构体中定义同名方法,覆盖嵌入结构体的方法。此时,外层方法完全替代嵌入结构体的方法逻辑。例如取消注释示例中的 func (*Cat) run() 方法,则 cat.run() 只会输出 "优雅的跑" 或 "猫优雅的跑",不再调用 Animal 的 run 方法

增强覆盖(扩展原有逻辑)

通过显式调用嵌入结构体的方法,在保留原有逻辑的基础上扩展新功能。如示例中 Cat.run() 方法:

go
func (c *Cat) run() {
    c.Animal.run()  // 保留父类逻辑
    fmt.Println("猫优雅的跑")  // 新增扩展逻辑
}

多态

在传统面向对象语言(如 Java/C++)中,多态依赖继承关系:子类继承父类后,可重写(覆盖)父类方法,通过父类引用指向子类对象实现运行时动态绑定。

go
var a Animal //其他语言中定义父类类型变量
a = *Cat
a = *Dog

Go 语言则通过 接口(Interface) 实现类似效果

任何类型只要实现了接口的全部方法,即自动满足该接口(无需显式 implements 声明),因此通过接口类型变量(如 Runner)可持有任何满足接口的具体类型实例(如 *Cat*Dog),调用同名方法时实际执行具体类型的方法,实现多态。

go
package main  
  
import "fmt"  
  
type Runner interface {  
    run()  
}  
  
type Animal struct {  
    name string  
    Age  int  
}  
  
func (*Animal) run() {  
    fmt.Println("动物跑")  
}  
  
type Cat struct {  
    Animal  
    color string  
}  
  
type Dog struct {  
    Animal  
}  
  

func (c *Cat) run() {  
    // 覆盖有两种,1、完全覆盖 2、锦上添花,增强  
    c.Animal.run()  
    fmt.Println("猫优雅的跑")  
}  
  
func (d *Dog) run() {  
    d.Animal.run()  
    fmt.Println("狗欢快的跑")  
}  
  
func test(r Runner) {  
    r.run() //对同一种类型的实例调用同样的方法,表现出不同的态  
}  
  
func main() {  
    cat := new(Cat)  
  
    cat.run()  
    dog := new(Dog)  
    dog.run()  
  
    test(cat)  
    test(dog)  
  
}

通过 Runner 接口统一调用入口,*Cat 和 *Dog 各自实现 run() 定制行为,而结构体嵌入复用 Animal 的字段和方法。这种模式正是 Go 推崇的 面向接口编程(Interface-Oriented Programming) 的典型实践。

结构体排序

排序接口

在Go语言中,sort.Interface接口是标准库sort包的核心,用于实现对任意集合类型的自定义排序。其定义为:

go
type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}

从接口定义来看,要实现某类型的排序需要实现以下三个方法

  1. 确定排序范围: 要知道有多少个元素,Len() int 返回集合中元素的数量,用于确定排序范围。
  2. 定义排序规则: 2个指定索引的元素怎么比较大小,索引i的元素小于索引j的值返回true,反之返回false
  3. 交换位置: 如何交换指定索引上的元素

那么自定义类型,要想排序,就要实现sort包中该接口。

结构体排序

现在假设有N个学生,学生有姓名和年龄,按照年龄排序结构体实例。

go
package main  
  
import (  
    "fmt"  
    "sort")  
  
type Student struct {  
    name  string  
    score int  
}  
  
//照抄 sort.Ints  
type StudentSlice []Student  
  
// 获取切片长度  
func (a StudentSlice) Len() int {  
    return len(a)  
}  
  
// 交换元素  
func (a StudentSlice) Swap(i, j int) {  
    a[i], a[j] = a[j], a[i]  
}  
  
// 指定元素怎么比较大小  
func (a StudentSlice) Less(i, j int) bool {  
    return a[i].score < a[j].score  
}  
  
func main() {  
    var a = Student{"Tom", 98}  
    var b = Student{"Jack", 70}  
  
    fmt.Println(a, b)  
    var students = []Student{a, b}  
    sort.Sort(StudentSlice(students)) //sort.Ints  
  
    fmt.Println(students)  
}

前两个方法对所有切片都是一样,只需要自己定义第三个方法,对于不同位置上的两个元素如何比较大小? 根据 年龄还是成绩比较不知道需要自己写。

go
package main  
  
import (  
    "fmt"  
    "math/rand"    "sort"    "strconv"    "time")  
  
type Student struct {  
    name  string  
    score int  
}  
  
//照抄 sort.Ints  
type StudentSlice []Student  
  
// 获取切片长度  
func (a StudentSlice) Len() int {  
    return len(a)  
}  
  
// 交换元素  
func (a StudentSlice) Swap(i, j int) {  
    a[i], a[j] = a[j], a[i]  
}  
  
// 指定元素怎么比较大小  
func (a StudentSlice) Less(i, j int) bool {  
    return a[i].score < a[j].score  
}  
  
func main() {  
    //var a = Student{"Tom", 98}  
    //var b = Student{"Jack", 70}  
    //随机生成5个学生  
    //Go 1.20开始废弃了rand.Seed函数, rand.Seed(time.Now().UnixNano()),使用纳秒时间戳作为种子  
    //官方推荐使用rand.New(rand.NewSource(seed))替代全局的rand.Seed  
    r := rand.New(rand.NewSource(time.Now().UnixNano()))  
    students := make([]Student, 0, 5)  
    for i := 0; i < 5; i++ {  
       students = append(students, Student{  
          "Tom" + strconv.Itoa(i),  
          r.Intn(40) + 60,  
       })  
    }  
  
    fmt.Println(students)  
    // sort.Reverse 反转  
    sort.Sort(sort.Reverse(StudentSlice(students)))  
    fmt.Println(students)  
}
  • 姓名拼接strconv.Itoa(i)将整型循环变量转为字符串,拼接出"Tom0"~"Tom4"
  • 随机年龄r.Intn(40)生成0~40的整数,+60后得到60~99的年龄值。

切片排序简化方法

我们有两种方式对切片进行排序:

  1. 使用sort.Sort,需要实现sort.Interface接口(即Len, Swap, Less三个方法)。
  2. 使用sort.Slice,只需要提供一个比较函数,不需要实现整个接口。

在 Go 中,sort.Slice 可以完全替代手动实现 sort.Interface 的方式。只需使用 sort.Slice 配合简单的比较函数即可。以下是优化后的代码:

go
package main
import (
    "fmt"
    "math/rand"
    "sort"
    "strconv"
    "time"
)

type Student struct {
    name  string
    score int
}

  
func main() {

    r := rand.New(rand.NewSource(time.Now().UnixNano()))

    students := make([]Student, 5)

    for i := 0; i < 5; i++ {

        students[i] = Student{

            name:  "Tom" + strconv.Itoa(i),

            score: r.Intn(40) + 60,
        }
    }

    fmt.Println("初始:", students)

    // 降序排序(高分在前)

    sort.Slice(students, func(i, j int) bool {

        return students[i].score > students[j].score

    })

    fmt.Println("降序排序:", students)

    // 升序排序(低分在前)

    sort.Slice(students, func(i, j int) bool {
        return students[i].score < students[j].score
    })
    fmt.Println("升序排序:", students)

}

map排序

基于 Key 的排序实现

通过有序 Key 间接控制 Map 的访问顺序

go

m := make(map[int]string)  
  
m[3] = "b"  
m[1] = "c"  
m[0] = "a"  
m[2] = "d"

var keys []int
//提取所有 Key 到切片
for k := range m {
    keys = append(keys, k)  //将散列存储的 Key 存入有序的切片容器
}

//使用sort.Ints 对 Key 排序,排序后 Key 变为 [0, 1, 2, 3]
sort.Ints(keys) // 升序排列


//按排序后的 Key 遍历 Map
for _, k := range keys {
    fmt.Println(k, m[k])
}

基于 Value 的排序实现

go

//定义结构体存储键值对,排序需同时访问 Key 和 Value,而结构体可绑定二者
type Entry struct {
    key   int
    value string
}

//创建结构体切片并填充数据,切片长度与 Map 一致,避免动态扩容
entries := make([]Entry, len(m)) // 预分配长度
i := 0 //记录当前填充到切片的哪个位置(从索引 0 开始)
for k, v := range m {
    entries[i] = Entry{k, v} //将键值对存入结构体
    i++
}


//使用自定义规则排序 Value,定义排序规则比较 value 字段
sort.Slice(entries, func(i, j int) bool {
    return entries[i].value < entries[j].value // 按 Value 升序
})
  • 关键逻辑
    • k, v:每次循环从 map 中随机取出一个键值对(因 map 无序)148。
    • Entry{k, v}:将键值对包装成结构体实例(如 {key: 3, value: "b"})。
    • entries[i] = ...:按顺序将结构体放入切片指定位置(索引 i 处)。
    • i++:索引自增,指向下一个待填充位置。