1、OOP思想
面向对象编程思想,go语言不是面向对象的语言,无法像Java 定义Class来封装一个类,但是通过理解面向对象思想,go可以通过一些方法来模拟面向对象
面向对象就是把所有东西都分析拆解成一个个的类(属性、方法)
物以类聚
,oop就是拟人化的思想,
类:抽象的模版,定义一个类(属性、方法),实例化出来一个对象(具体的),
面向对象的三大核心:封装、继承、多态
- 语句多了,函数出现了
- 变量多了,结构体出现了
- 将功能相近的变量和函数聚到一起,类和对象出现了
面向对象的思维模式
分类思维模式
- 首先思考解决问题,需要哪些分类,
- 然后对这些分类进行单独思考,
- 最后,才对某个分类下的细节进行面向过程的思索
这样就可以形成很好的协作分工,比如架构师分了10个类,然后将10个类交给10个人分别进行详细设计和编码。
显然面向对象适合处理复杂的问题,适合处理需要多人协作的问题
一个复杂的项目,我们先从宏观上进行分析,使用面向对象的思路来拆分整个系统,拆分出功能接口,每个接口仍然需要面向过程的思路去处理
面向对象:可以解决复杂的问题,将现实中的问题,拟人化(用人的思想去解决),适合多人协作
面向过程:适合解决简单的问题,个人就可以完成
面向对象的核心三大特征:
-
封装
模版-产出商品(对象)
-
继承
生物,大型物种
-
多态(接口,不同的实现类)
一个事物可能有多种形态,比如 动物(猫、狗)
2、继承与提升字段
子结构体嵌套了父结构体做为匿名字段,父结构体的属性就叫做提升字段,可以直接访问
Go 模拟继承的方法:结构体嵌套,使用匿名字段实现继承
/**
Go 语言的结构体嵌套
1、模拟继承性 is-a 关系
type A struct{
field
}
type B struct{
A // 匿名字段
}
// b 就可以直接访问a的属性了
2、模拟聚合关系 has-a 关系
type C struct{
field
}
type D struct{
c C // 聚合关系
}
// d 就不能直接访问c的属性,而是需要通过D.c.xxx访问
*/
// 模拟继承
// 1、定义一个父类 结构体
type Person struct {
name string
age int
}
// 2、定义一个子类,结构体
type Student struct {
Person // 匿名字段,继承了结构体Person
school string
}
func main() {
p1 := Person{"kuangshen", 18}
fmt.Println(p1.name)
s1 := Student{Person{"秦疆", 20}, "清华"}
fmt.Println(s1.Person.name)
// 提升字段
// 对于Student来说,Person是匿名字段,Person中的属性name、age就叫做提升字段
// 提升字段可以通过名字直接访问,不需要经过结构体Person
fmt.Println(s1.name)
var s3 Student
s3.name = "feige"
s3.school = "北大"
fmt.Println(s3) // { {feige 0} 北大}
}
执行结果
kuangshen
秦疆
秦疆
{ {feige 0} 北大}
go里,结构体可以是多继承的,而java只能单继承
继承让子结构体拥有了父结构体的属性和方法。
3、方法讲解
与函数的区别
Go语言中同时拥有函数和方法
// 函数,所有人都可以调用
func funName(param list) return list{}
// 方法,多了一个结构体接受者,并且只有这个接收者类型才可以调用
func (t Type) funName(param list) return list{}
// 接受者,可以是这个结构体类型或这个结构体类型的指针
func (接受者) funName(param list) return list{}
-
方法
某个类别的行为功能,需要指定结构体接受者,比如狗的eat方法,猫的eat方法
一段独立的功能代码
-
函数
命名不能冲突
一段独立的功能代码
方法是有调用者对象的,而函数没有。
定义结构体的方法
type Dog struct {
name string
age int
}
// 方法定义,指定结构体接受者是Dog
func (dog Dog) eat() { //
fmt.Println("Dog eating")
}
type Cat struct {
name string
age int
}
// 方法定义,指定结构体接受者是Cat
func (cat Cat) eat() {
fmt.Println("Cat eating")
}
func main() {
// 结构体有了属性和方法,就是模拟了面向对象语言的Class 有成员变量和方法了
dog := Dog{"旺财", 2}
dog.eat()
cat := Cat{"喵喵", 1}
cat.eat()
执行结果:
Dog eating
Cat eating
函数同一个包中,不能重名,方法可以重名,只要属于不同的结构体调用者
方法必须依赖对象的存在
值接收者与指针接收者
原文:https://cloud.tencent.com/developer/article/2196117
直接上代码,看执行结果
type Person struct {
name string
age int
}
// 值针接收者
func (p Person) GetName() string {
return p.name
}
// 指针接收者
func (p *Person) GetAge() int {
return p.age
}
func main() {
// 定义了一个【值类型】的Person对象
t := Person{
name: "DaYu",
age: int(28),
}
// 调用值方法
fmt.Println(t.GetName())
// 调用指针方法
fmt.Println(t.GetAge())
}
-----运行结果------
DaYu
28
修改为指针类型变量
type Person struct {
name string
age int
}
func (p *Person) GetName() string {
return p.name
}
func (p Person) GetAge() int {
return p.age
}
func main() {
// 注意,其它地方都没有改,只是这里改变了类型
t := &Person{
name: "DaYu",
age: int(28),
}
fmt.Println(t.GetName())
fmt.Println(t.GetAge())
}
-----运行结果-------
DaYu
28
- 值类型变量,可以调用值接收的方法,也可以调用指针接收者的方法;
- 指针类型变量,可以调用值接收的方法,也可以调用指针接收者的方法。
看起来没有差别,用结构体Person声明值类型变量或者指针类型变量的时候,如果用接口声明变量,结构体Person实现了某个接口,就是面向对象的的多态特性,代码如下:
// 新增的接口
type Animal interface {
GetName() string
GetAge() int
}
type Person struct {
name string
age int
}
func (p *Person) GetName() string {
return p.name
}
func (p Person) GetAge() int {
return p.age
}
func main() {
// 定义的接口变量
var ani Animal
// person 实现了 Animal 接口,赋值给了 ani 变量
// 但是,这里编译会通不过,错误如下:
// Cannot use 'Person{ name: "DaYu", age: int(28), }' (type Person) as the type Animal Type does not implement 'Animal' as the 'GetName' method has a pointer receiver
ani = Person{
name: "DaYu",
age: int(28),
}
ani.GetName()
ani.GetAge()
}
错误提醒很明显了:Person 没有实现 Animal 的 GetName 方法。因为在上面的代码中,我们实现 GetName 方法的是 (*Person) 类型。
但是为什么 GetAge 方法不报错呢?那是因为 Go 里边对于 (Type)Method 的方法,会自动让他拥有 (*Type)Method 方法的能力。
总结一下,实现接口时有下面的约束:
- 如果定义的是 (Type)Method,则该类型会隐式的声明一个 (*Type)Method;
- 如果定义的是 (*Type)Method ,则不会隐式什么一个 (Type)Method。
我们知道深拷贝是拷贝数据本身(值类型传参),浅拷贝是拷贝内存地址(引用类型传参)
如果使用值类型接收者,方法内部对接收者的属性改变是,接收者本身是不会改变,看代码:
type Animal interface {
GetName() string
GetAge() int
}
type Person struct {
name string
age int
}
func (p Person) GetName() string {
p.name = "rex"
return p.name
}
func (p *Person) GetAge() int {
p.age = 11
return p.age
}
func main() {
//var ani Animal
ani := &Person{"dayu",10}
fmt.Println(ani.GetName()) // 方法内部,改变了ani的属性name的值
fmt.Println(ani.GetAge()) // 方法内部,改变了ani的属性age的值
fmt.Println(ani) // ani的属性name不变,age改变了
}
运行结果:
所以我们使用指针类型接收者,为了避免:
- 方法内部对接收者的属性值的改变,接收者本身不改变的问题出现,因为结构体是值类型,并非引用类型。
4、方法重写
- 子类可以重写父类的方法 override
- 子类可以新增自己的属性和方法
- 子类可以直接访问父类的属性和方法
type Animal struct {
name string
age int
}
func (animal Animal) eat() {
fmt.Println(animal.name, " eating")
}
func (animal Animal) sleep() {
fmt.Println(animal.name, " sleeping")
}
type Dog struct {
Animal
}
type Cat struct {
Animal
color string // 小写首字母
}
// 重写父类的方法
func (cat Cat) eat() {
fmt.Println(cat.name, " eating--cat")
}
// 子类扩展自己的方法
func (cat Cat) miao() {
fmt.Println(cat.name, "miao miao ")
}
func main() {
// 子类,继承父类的属性和方法
dog := Dog{Animal{"旺财", 2}}
dog.eat()
cat := Cat{Animal{"喵喵", 1}, "白色"}
cat.eat()
cat.miao()
}
执行结果:
旺财 eating
喵喵 eating--cat
喵喵 miao miao
疑问:怎么在子结构体中调用父结构体的方法。
多态
一个事物拥有多种形态,
模糊 类 具体 对象
5、接口实现
接口只做方法定义,不做具体的方法实现
面向对象语言里使用implement显示声明类实现接口,Go语言里结构体不用显示的用implement声明自己实现了某个接口,只要他实现了接口定义的全部方法,那他就是实现了这个接口
1例子:
type USB interface {
oInput()
oOutput()
}
type Mouse struct {
name string
}
func (mouse Mouse) oInput() {
fmt.Println(mouse.name, "鼠标输入")
}
func (mouse Mouse) oOutput() {
fmt.Println(mouse.name, "鼠标输出")
}
func test(u USB) {
u.oInput()
u.oOutput()
}
type KeyBored struct {
name string
}
func (k KeyBored) oInput() {
fmt.Println(k.name, "键盘输入")
}
func (k KeyBored) oOutput() {
fmt.Println(k.name, "键盘输出")
}
func main() {
mouse := Mouse{"罗技"}
test(mouse)
keyBored := KeyBored{"雷蛇"}
test(keyBored)
var u USB
u = keyBored // 多态
fmt.Println(u)
// fmt.Println(u.name) // 只能访问接口约束的方法
}
执行结果:
罗技 鼠标输入
罗技 鼠标输出
雷蛇 键盘输入
雷蛇 键盘输出
{雷蛇}
6、接口实现多态
多态:一个事物拥有多种形态,是面向对象一个重要的特征
父类:动物
子类:猫
子类:狗
那么狗和猫就是多态,它们本身又是动物
go语言通过接口模拟多态
上面的鼠标键盘实现USB接口,就是多态的例子
7、空接口
go语言中非常重要的一个概念:空接口,相当于Java里的Object,它是所有数据类型的父类
- 不包含任何的方法,正因为如此,所有的类型都实现了空接口,因此空接口可以存储任意类型的数值
fmt包的Println()方法中的参数a就是一个空接口
func Println(a ...any) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
type any = interface{} // 空接口的别名是any
map[string]interface{} value是空接口,相当于Java的Map<String,Object>
type Dog struct {
name string
}
type Cat struct {
name string
color string
}
// 空接口 Dog Cat都实现了该接口,相当于Java的Object类
type A interface {
}
// 空接口作为形参数,函数可以接受任意数据类型的入参,相当于Object
func test(a A) {
fmt.Println(a)
}
// fmt 下的输出接受的参数都是这样的
func test2(a interface{}) {
fmt.Println(a)
}
func main() {
var a1 A = Dog{"旺财"}
var a2 A = Cat{"喵喵", "白色"}
var a3 A = "hhha" // 任意类型都是接口A都实现类
var a4 A = 100
fmt.Println(a1)
fmt.Println(a2)
fmt.Println(a3)
fmt.Println(a4)
test(a1) // 实现多态
// map[string]interface{} value是空接口,相当于Java的Map<String,Object>
map1 := make(map[string]interface{})
map2 := make(map[string]any)
map1["name"] = "qinjiang"
map1["age"] = 18
map1["dog"] = a1
fmt.Println(map1)
// 切片 数据元素是空接口,接受任何数据类型
s1 := make([]interface{}, 0, 10)
s1 = append(s1, a1, a2, a3, a4, map1)
fmt.Println(s1)
}
执行结果:
{旺财}
{喵喵 白色}
hhha
100
{旺财}
map[age:18 dog:{旺财} name:qinjiang]
[{旺财} {喵喵 白色} hhha 100 map[age:18 dog:{旺财} name:qinjiang]]
8、接口继承
go语言中接口是可以继承的,还可以多继承,当然也是通过嵌套来实现,就像结构体通过嵌套来实现面向对象的继承,看下面代码例子
type A interface {
test1()
}
type B interface {
test2()
}
// 嵌套A、B接口,就是继承了A 和 B接口,结构体也是嵌套了就是继承
type C interface {
A // 匿名字段 is-a关系
B // 匿名字段 is-a关系
test3()
}
type Dog struct {
name string
}
// 要实现了test1、test2、test3方法才算是实现了C接口
func (d Dog) test3() {
fmt.Println(d.name, "testing3")
}
func (d Dog) test2() { // 实现了接口B
fmt.Println(d.name, "testing3")
}
func (d Dog) test1() { // 实现了接口A
fmt.Println(d.name, "testing1")
}
func main() {
dog := Dog{"旺财"}
dog.test1()
dog.test2()
dog.test3()
// 接口嵌套中,嵌套的接口默认继承了被嵌套接口的所有方法
var ia A = dog // 多态
ia.test1() // 只能调用test1方法,因为声明的是A接口
var ib B = dog // 多态
ib.test2() // 只能调用test2方法,因为声明的是B接口
var ic C = dog
ic.test3()
ic.test2()
ic.test1()
}
执行结果:
旺财 testing1
旺财 testing3
旺财 testing3
旺财 testing1
旺财 testing3
旺财 testing3
旺财 testing3
旺财 testing1
9、接口断言
检查接口类型变量的值是否实现了期望的接口,这就是接口断言。
前面有判断map是否有某个key值,现在判断某个结构体对象是否实现了某个接口
例子:
空接口interface{}可用保存任何类型的变量,假如保存的值的数字int类型,当前值要参与数学运算,就必须转化为int类型才可以参加运算,这就需要接口断言来实现类型转换。
被断言的对象必须是接口类型,否则报错
t:=i.(T)。// T为接口类型
- 断言成功,返回t为T类型的接口值
- 断言失败则报错,panic
func main() {
assertString("haha")
assertString(100)
}
func assertString(i interface{}) {
s:=i.(string) // 接口断言入参是否string类型
fmt.Println(s)
}
执行结果:
haha
panic: interface conversion: interface {} is int, not string
可以看到断言成功打印了字符串“haha”,断言失败报panic错误(恐慌,则程序会停止)
v,ok:=i.(T)
- 断言成功,则v为T类型的接口值,ok为true
- 断言失败,则v为空值,ok为false
func main() {
assertString("haha")
// assertString(100)
assertInt(1)
assertInt("bbb")
// 通过ok-idom来判断map对象的key,value 是存在
var map1 = make(map[int]string)
map1[1] = "xuexiangban"
value, ok := map1[6]
if ok {
fmt.Println("map key 存在,value:", value)
} else {
fmt.Println("map key 不存在")
}
}
func assertString(i interface{}) {
s:=i.(string) // 接口断言入参是string类型
fmt.Println(s)
}
func assertInt(i interface{}){
v,ok :=i.(int) // ok-idom模式判断
if ok {
fmt.Println("接口变量是int类型",v)
}else{
fmt.Println("接口变量不是int类型")
}
}
执行结果:
haha
接口变量是int类型 1
接口变量不是int类型
一般使用该方式进行断言
type.switch
switch 接口变量.(type) {
case 类型1 :
// 变量是类型1时的处理
// switch默认情况下匹配成功后就不会执行其他case
case 类型2:
// 变量是类型2时的处理
break;
case nil:
// 空接口进入该流程
break;
default:
// 都不满足case的处理
}
例子代码:
type Dog struct {
name string
}
type I interface {
}
func test2(i interface{}){
switch i.(type){
case string:
fmt.Println("string 类型") // 没有fallthrough贯穿
case int:
fmt.Println("int 类型")
case nil :
fmt.Println("nil类型")
default:
fmt.Println("未知类型")
}
}
func main() {
a:="string"
b:=100
var d = Dog{}
var i I
var map1 map[int]string
test2(a)
test2(b)
test2(d)
test2(i)
test2(map1)
fmt.Println(map1==nil)
}
执行结果:
string 类型
int 类型
未知类型
nil类型
未知类型
true
场景:如果一个对象存在多种形态:接口断言 登录 20个实现类:微信登录、账号密码登录
- v,ok := i.(T)
- switch i.(T)
10、type别名
var 变量:值变量,引用变量
type 类型:结构体struct,接口interface
定义结构体、接口都用type关键字,还可以type 进行起别名
自定义类型
(在函数外面定义)需要强制类型转换才能参加运算
package main
// Diyint就是新的类型,它具有int的特性, 但是要强转才能参加运算
type Diyint int
type Diyint int
func main() {
var a Diyint =20
b:=10
c:= int(a) + b // a 要显示转换为int 才能参加运算
fmt.Println(c)
}
执行结果:
30
类型别名
(在函数里面定义)
package main
func main(){
type myint = int // 定义int类型的别名为myint
var a myint = 100 // 定义myint类型的变量
var b int = 20
// myint类型只会在代码中存在,编译完成时并不会有Myint类型
d := a + b // 无需强制类型转换
fmt.Println(d)
}
执行结果:
120
any是空接口的类型别名,看源码
type any = interface{}
自定义类型与别名就多了一个等号=
type Diyint int // 自定义类型
type Diyint = int // 类型别名,增加代码的可阅读性,项目编译时,其实还是原来的类型