Go面向对象编程
结构体
结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。
结构体表示一项记录,比如保存图书馆的书籍记录,每本书有以下属性:
- Title:标题
- Author: 作者
- Subject:学科
- ID:书籍ID
结构体成员是由一系列的成员变量构成,这些成员变量也被称为"字段"。字段有以下特性:
- 字段拥有自己的类型和值。
- 字段名必须唯一。
- 字段的类型也可以是结构体,甚至是字段所在结构体的类型。
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{}
结构体指针访问
- 第3种和第4种方式返回的是==结构体指针==。
- 结构体指针访问字段的标准方式应该是:
(*person).Name = "jack"。但 go 做了一个简化,也支持person.Name = "jack"。go 编译器底层对person.Name做了转化(*person).Name。 - 如果需要使用指针,推荐
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 访问结构体的字段和方法,相当于 this 或 self。
示例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)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作。
- 隐藏实现细节
- 可以对数据进行验证,保证安全合理(Age)
封装的实现步骤:
-
将结构体、字段(属性)的首字母==小写==(不能导出,其它包不能使用,类似 private)
-
给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
-
提供一个首字母大写的 Set 方法 (类似其它语言的 public),用于对属性判断并赋值
-
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(值接收者),修改只作用在拷贝上,调用者看不到变化。
-
提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值
-
Getter 一般用==值接收者==,且如果返回的是导出字段,可直接暴露字段而不是写方法。
//示例
func (stu student) GetScore() float64 {
return stu.score
}
//值接收者就是接收者不带 *,方法获得的是对象的一个拷贝。Getter 通常只读字段,不需要修改原对象,所以值接收者更简洁
- 如果字段需要封装(私有化),应以小写开头并通过 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开发 | 云原生开发-基础