第九课 go语言基础-面向对象编程
tags:
- golang
- 2019尚硅谷
categories:
- 结构体
- 面向对象
- 封装
- 继承
- 接口
- 多态
第一节 结构体的介绍和基本使用
1.1 结构体的说明
- Golang也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说Golang支持面向对象编程特性是比较准确的。
- Golang 没有类(class), Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解Golang是基于struct 来实现OOP特性的。
- Golang面向对象编程非常简洁,去掉了传统OOP语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等等
- Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不一样,比如继承: Golang 没有extends关键字,继承是通过匿名字段来实现。
- Golang面向对象(OOP)很优雅,OOP本身就是语言类型系统(type system)的一部分, 通过接口(interface)关联,耦合性低,也非常灵活。也就是说在Golang中面向接口编程是非常重要的特性。
- 举个例子(具体抽象为类的过程)
- 将一类事物的特性提取出来(比如猫类), 形成-一个新的数据类型, 就是一个结构体。通过这个结构体,我们可以创建多个变量(实例/对象)
- 事物可以猫类,也可以是Person,Fish或是某个工具类。。。

1.2 如何声明结构体
- 基本语法
type 结构体名称 struct {
field1 type
field2 type
}
- 举例说明。
package main
import (
"fmt"
)
//如果结构体的字段类型是:指针,slice, 和map的零值都是nil ,即还没有分配空间
//如果需要使用这样的字段,需要先make,才能使用。
type Person struct{
Name string
Age int
Scores [5]float64
ptr *int //指针
slice []int //切片
map1 map[string]string //map
}
type Monster struct {
Name string
Age int
}
func main() {
//定义结构体变量
var p1 Person
fmt.Println(p1)
if p1.ptr == nil{
fmt.Println("ok1")
}
if p1.slice == nil{
fmt.Println("ok2")
}
if p1.map1 == nil{
fmt.Println("ok3")
}
//使用slice, 再次说明,一定要make
p1.slice = make([] int, 10)
p1.slice[0] = 100 //ok
//使用map,一定要先make
p1.map1 = make(map[string]string)
p1.map1["key1"] = "tom~"
fmt.Println(p1)
//不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,
//不影响另外一个,结构体是值类型
var monster1 Monster
monster1.Name = "牛魔王"
monster1.Age = 500
monster2 := monster1 //结构体是值类型,默认为值拷贝
monster2.Name ="青牛精"
fmt.Println("monster1=", monster1) //monster1= {牛魔王500}
fmt.Println("monster2=", monster2) //monster2= {青牛精500}
}
- 从概念或叫法上看: 结构体字段=属性=field。字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。
- 注意事项和细节说明
- 字段声明语法同变量,示例:字段名字段类型
- 字段的类型可以为:基本类型、数组或引用类型
- 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:布尔类型是false ,数值是0,字符串是""。
- 数组类型的默认值和它的元素类型相关,比如score [3]int则为[0, 0, 0]指针,slice, 和map的零值都是nil ,即还没有分配空间。
- 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个,结构体是值类型。
1.3 创建结构体变量和访问结构体字段
- 方式1直接声明
- 案例演示: var person Person
- 方式2-{}
- 案例演示: var person Person = Person{}
- 方式3-&
- 案例:var person *Person = new ( Person )
- 方式4-{}
- 案例: var person *Person = &Person{}
- 说明:
- 第3种和第4种方式返回的是结构体指针。
- **结构体指针访问字段的标准方式应该是: (结构体指针).字段名,比如(person).Name = “tom”
- 但go做了一个简化,也支持结构体指针.字段名,比如person.Name = “tom”。更加符合程序员使用的习惯,*go 编译器底层对person.Name做了转化(person).Name。
package main
import (
"fmt"
)
//如果结构体的字段类型是:指针,slice, 和map的零值都是nil ,即还没有分配空间
//如果需要使用这样的字段,需要先make,才能使用。
type Person struct{
Name string
Age int
}
func main() {
//第一种方式
var p1 Person
fmt.Println(p1)
//方式2
p2 := Person{"mary", 20}
p2.Name = "tom"
p2.Age = 18
fmt.Println(p2)
//方式3-&
//案例: var person *Person = new (Person)
var p3 *Person= new(Person)
//因为p3是一个指针,因此标准的给字段赋值方式
//(*p3).Name ="smith"也可以这样写p3.Name ="smith"
//原因: go的设计者 为了程序员使用方便, 底层会对p3.Name = "smith" 进行处理
//会给p3加上取值运算
p3.Name ="smith"
(*p3).Name = "smith"
p3.Name = "john" //
(*p3).Age = 30
p3.Age = 100
fmt.Println(*p3)
//fmt.Println(p3)
//方式4-{}
//案例: var person *Person = &Person{}
//下面的语句, 也可以直接给字符赋值
//var person *Person = &Person{"mary", 60}
var person *Person = &Person{}
//因为person是一个指针, 因此标准的访间字段的方法
// (*person).Name ="scott"
// go的设计者为程序员使用方便, 也可以person.Name ="scott"
//原因和上面一样,底层会对person.Name = "scott" 进行处理, 会加上(*person)
(*person).Name = "scott"
person.Name = "scott~~"
(*person).Age = 88
person.Age = 10
fmt.Println(*person)
}
1.4 结构体变量内存分析

package main
import (
"fmt"
)
type Person struct{
Name string
Age int
}
func main() {
var p1 Person
p1.Age = 10
p1.Name ="小明"
var p2 *Person = &p1 //这里是关键-->画出示意图
fmt.Println((*p2).Age)
fmt.Println(p2.Age)
p2.Name = "tom~"
fmt.Printf("p2.Name=%v p1.Name=%v \n", p2.Name, p1.Name) // tom~ tom~
fmt.Printf("p2.Name=%v p1.Name=%v \n", (*p2).Name, p1.Name) // tom~ tom~
fmt.Printf("p1的地址%p\n", &p1)
fmt.Printf("p2的地址%p p2的值%p\n", &p2, p2)
}

1.5 结构体使用注意事项和细节
- 结构体的所有字段在内存中是连续的
- 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
- 结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转
- struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
package main
import (
"fmt"
"encoding/json"
)
type A struct {
Num int
}
type B struct {
Num int
}
// `json:"name"` 中间不能有空格呀 `json: "name"`
type Monster struct{
Name string `json:"name"` //`json:"name"`就是struct tag
Age int `json:"age"`
Skill string `json:"skill"`
}
func main() {
var a A
var b B
a = A(b) // ?可以转换,但是有要求,就是结构体的的字段要完全一样(包括:名字、 个数和类型!
fmt.Println(a, b)
//1.创建一个Monster变量
monster := Monster{"牛魔王", 500, "芭蕉扇~"}
fmt.Println(monster)
//2.将monster变量序列化为json格式字串
// json.Marshal函数中使用反射,这个讲解反射时,我会详细介绍
jsonstr, err := json.Marshal(monster)
if err != nil {
fmt.Println("json处理错误", err)
}
fmt.Println("jsonstr", string(jsonstr))
}
第二节 方法的介绍和基本使用
2.1 golang中的方法定义
- 在某些情况下,我们要需要声明(定义)方法。比如Person结构体:除了有一些字段外(年龄,姓名), Person结构体还有一些行为比如:可以说话、跑步…,通过学习,还可以做算术题。这时就要用方法才能完成。
- Golang中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct。
- 方法的定义和说明
- 参数列表:表示方法输入
- recevier type:表示这个方法和type这个类型进行绑定,或者说该方法作用于type类型
- receiver type: type可以是结构体,也可以其它的自定义类型
- receiver: 就是type类型的一个变量(实例),比如: Person 结构体的一个变量(实例)
- 返回值列表:表示返回的值,可以多个
- 方法主体:表示为了实现某一 功能代码块
- return语句不是必须的。
func (recevier type) methodNames (参数列表) (返回值列表){
方法体
return返回值
}
- 方法的基本案例
- test方法和Person类型绑定
- test方法只能通过Person类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
- func (p Person) test() }…p表示哪个Person变量调用,这个p就是它的副本,这点和函数传参非常相似。
- p这个名字,由程序员指定,不是固定,比如修改成person也是可以
package main
import (
"fmt"
)
type Person struct{
Name string
}
// 给结构体绑定一个test方法
func (p Person) test(){
fmt.Println("test() name", p.Name)
}
func main() {
var p Person
p.Name = "tom"
p.test() //调用方法
}
2.2 方法的调用和传参机制原理
package main
import (
"fmt"
)
type Person struct{
Name string
}
// 给结构体绑定一个test方法
func (p Person) test(){
fmt.Println("test() name", p.Name)
}
//给Person结构体添加speak方法,输出xxx是一个好人
func (p Person) speak() {
fmt.Println(p.Name, "是一个goodman~")
}
//给Person结构体添加jisuan 方法,可以计算从1+...+1000的结果,
//说明方法体内可以函数一样,进行各种运算
func (p Person) jisuan() {
res := 0
for i:=1;i<=2;i++ {
res += i
fmt.Println(p.Name, "计算的结果是=", res)
}
}
//给Person结构体jisuan2方法,该方法可以接收一个参数n,计算从1+..+n 的结果
func (p Person) jisuan2(n int) {
res := 0
for i:=1;i<=n;i++{
res += i
}
fmt.Println(p. Name, "计算的结果是=", res)
}
//给Person结构体添加getSum方法,可以计算两个数的和,并返回结果
func (p Person) getSum(n1 int, n2 int) int {
return n1 + n2
}
func main() {
var p Person
p.Name = "tom"
p.test() //调用方法
p.speak()
p.jisuan()
p.jisuan2(10)
res := p.getSum(1, 12)
fmt.Println("res:", res)
}
- 方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法。
- 在通过一个变量去调用方法时,其调用机制和函数–样
- 不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝)

2.3 方法的注意事项和细节
- 因为结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
- 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
- Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,
都可以有方法,而不仅仅是struct,比如 int , float32等都可以有方法 - 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母
大写,可以在本包和其它包访问。 - 如果一个类型实现了String()这个方法,那么fmt.Println 默认会调用这个变量的String()进行输
出
2.4 方法和函数区别
- 调用方式不一样
- 函数的调用方式:函数名(实参列表)
- 方法的调用方式:变量.方法名(实参列表)
- 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
- 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
- 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定
- *如果是和值类型,比如(p Person),则是值拷贝,如果和指针类型,比如是(p Person)则是地址拷贝。
第三节 结构体的赋值
3.1 创建结构体变量时指定字段值-包内
- Golang在创建结构体实例(变量)时,可以直接指定字段的值
package main
import (
"fmt"
)
type Stu struct{
Name string
Age int
}
func main() {
//在创建结构体变量时,就直接指定字段的值
var stu1 = Stu{"小明", 19}
stu2 := Stu{"小明~把", 29}
//在创建结构体变量时,把字段名和字段值写在一起,这种写法,就不依赖字段的定义顺序.
var stu3 = Stu{
Name : "jack",
Age : 13,
}
stu4 := Stu{
Name : "Tony",
Age : 15,
}
fmt.Println(stu1, stu2, stu3, stu4)
//方式2, 返回结构体的指针类型在main函数中。
//在创建结构体指针变量时,把字段名和字段值写在一起,这种写法,就不依赖字段的定义顺序
var stu5 *Stu = &Stu{"小王", 29}
stu6 := &Stu{"小紫", 21}
stu7 := &Stu{
Age: 23,
Name: "小杨",
}
var stu8 = &Stu{
Name: "小罗",
Age: 39,
}
fmt.Println(stu5, stu6, stu7, stu8)
}
3.2 工程模式->实现构造函数(包外)
- Golang的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。
- 实际问题一如下:
- 因为这里的Student的首字母S是大写的,如果我们想在其它包创建Student的实例(比如main包),引入model包后,就可以直接创建Student结构体的变量(实例)。
- 但是问题来了,如果首字母是小写的,比如是type student struct …就不不行了,怎么办? 工厂模式来解决.
package model
type Student struct {
Name string...
}
- 解决如下:
// main.go
package main
import (
"fmt"
"go_code/go_study_chapter09/demo03_oop_init/model"
)
func main() {
//如果model包的结构体变量首字母大写,引入后,直接使用,没有问题
//var stu1 = model.Student{"小明", 19.1}
var stu1 = model.NewStudent("小明", 19.1)
fmt.Println(*stu1)
fmt.Println(stu1.Name, stu1.Score)
}
// model/main.go
package model
type student struct{
Name string
Score float64
}
//因为student结构体首字母是小写,因此是只能在mode1使用
//我们通过工厂模式来解决
func NewStudent(n string, s float64) *student{
return &student{
Name : n,
Score : s,
}
}
- 实际问题二如下:
- 如果model包的student的结构体的字段Score 改成score, 我们还能正常访问吗?不能够直接访问
- 解决办法如下(在main中通过stu1.GetScore()调用):
package model
type student struct{
Name string
score float64
}
//因为student结构体首字母是小写,因此是只能在mode1使用
//我们通过工厂模式来解决
func NewStudent(n string, s float64) *student{
return &student{
Name : n,
score : s,
}
}
func (s *student) GetScore() float64{
return s.score
}
第四节 面向对象编程特性
Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不一样,本节详细的讲解Golang的三大特性是如何实现的。
4.1 面向对象编程三大特性-封装
- 封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作
- 封装的好处
- 隐藏实现细节
- 可以对数据进行验证,保证安全合理(Age)
- 封装的表现形式
- 对结构体中的属性进行封装
- 通过方法,包实现封装
- 封装的实现步骤
- 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似private)
- 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
- 提供一个首字母大写的Set方法(类似其它语言的public),用于对属性判断并赋值
- 提供-一个首字母大写的Get方法(类似其它语言的public), 用于获取属性的值
func (var 结构体类型名) SetXxx(参数列表) (返回值列表) {
//加入数据验证的业务逻辑
var.字段=参数
}
func (var结构体类型名) GetXxx() {
return var.age;
}
- 举个列子。
package model
import (
"fmt"
)
type person struct{
Name string
age int // 小写其他包不能直接访问
sal float64
}
func NewPerson(name string) *person{
return &person{
Name : name,
}
}
func (p *person) SetAge(age int) {
if age > 0 && age < 150{
p.age = age
}else{
fmt.Println("输入年龄错误!")
}
}
func (p *person) GetAge() int{
return p.age
}
func (p *person) SetSalary(sal int) {
if sal >= 3000 && sal <= 30000{
p.age = sal
}else{
fmt.Println("薪水输入错误!")
}
}
func (p *person) GetSalary() float64{
return p.sal
}
4.2 面向对象编程三大特性-继承
- 继承可以解决代码复用,让我们的编程更加靠近人类思维。
- 代码的复用性提高了
- 代码的扩展性和维护性提高了
- 当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法。
- 其它的结构体不需要重新定义这些属性和方法,只需嵌套一个Student匿名结构体。
- 在Golang中,如果一个struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。
type Goods struct {
Name string
Price int
}
type Book struct {
Goods//这里就是嵌套匿名结构体Goods
Writer string
}
- 继承的深入讨论
- 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。
- 匿名结构体字段访问可以简化
- 当我们直接通过b访问字段或方法时,其执行流程如下比如b.Name
- 编译器会先看b对应的类型有没有Name,如果有,则直接调用B类型的Name字段
- 如果没有就去看B中嵌入的匿名结构体A有没有声明Name字段,如果有就调用,如果没有继续查找,如果都找不到就报错.
- 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
- 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。
package main
import (
"fmt"
)
type A struct{
Name string
Age int
}
func (a *A) SayOk(){
fmt.Println("A SayOk", a.Name)
}
func (a *A) hello(){
fmt.Println("A hello", a.Name)
}
type B struct{
A
Name string
}
type C struct{
Name string
Score float64
}
type D struct{
A
C
}
func main() {
// 1. 通过B结构体访问 A结构体的方法 小写或者大写
var b B
b.A.Name = "tom"
b.A.Age = 12
b.A.SayOk()
b.A.hello()
// 2. 简化访问方式
b.SayOk()
b.hello()
// 3. 就近访问原则 如果B有 直接访问 当然也可以指定访问
b.Name = "xiao"
fmt.Println(b.Name)
fmt.Println(b.A.Name)
// 4. 父类中有相同字段 且子类没有字段 那么必须指定访问那个父类的字段 否则会报错
var d D
// d.Name = "ming"
// fmt.Println(d.Name)
d.A.Name = "ming"
fmt.Println(d.A.Name)
}
- 如果一个struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
package main
import (
"fmt"
)
type A struct{
Name string
Age int
}
type F struct{
a A // 有名结构体 组合关系
}
func main() {
//如果D中是一个有名结构体,则访问有名结构体的字段时,就必须带上有名结构体的名字
var f F
f.a.Name = "小军"
fmt.Println(f.a.Name)
}
- 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
- 一个struct嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方
法,从而实现了多重继承。- 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分
- 为了保证代码的简洁性,建议大家尽量不使用多重继承
package main
import (
"fmt"
)
type Goods struct{
Name string
Price float64
}
type Brand struct{
Name string
Address string
}
type TV struct{
Goods
Brand
}
type TV2 struct{
*Goods
*Brand
}
func main(){
//嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
tv1 := TV{Goods{"电视机1", 5900}, Brand{"海尔", "山东"}}
tv2 := TV{
Goods{
Price : 1324,
Name : "电视机2",
},
Brand{
Name : "夏普",
Address : "北京",
},
}
tv3 := TV2{ &Goods{"电视机3", 1200}, &Brand{"苹果", "山东"},}
tv4 := TV2{
&Goods{
Price : 1254,
Name : "电视机4",
},
&Brand{
Name : "长虹",
Address : "北京",
},
}
fmt.Println("tv1", tv1)
fmt.Println("tv2", tv2)
fmt.Println("tv3", *tv3.Goods, *tv3.Brand)
fmt.Println("tv4", *tv4.Goods, *tv4.Brand)
}
- 结构体的匿名字段是基本数据类型
package main
import (
"fmt"
)
type Monster struct{
Name string
Age int
}
type E struct{
Monster
int // 匿名字段时基本数据类型
n int
}
func main(){
//演示一下匿名字段时基本数据类型的使用
var e E
e.Name = "狐狸精"
e.Age = 300
e.int = 20
e.n = 40
fmt.Println("e=", e)
}
第五节 接口和多态
5.1 接口的定义
- Golang中多态特性主要是通过接口来体现的。
- interface类型可以定义一组方法,但是这些不需要实现。并且interface不能包含任何变量。到某个自定义类型(比如结构体Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)。
- 接口的基本语法
- 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。
- Golang 中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有implement这样的关键字

package main
import (
"fmt"
)
type Usb interface{
// 声明了两个没有实现的方法
Start()
Stop()
}
type Phone struct{
}
// 让结构体Phone实现Usb接口
func (p Phone) Start(){
fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop(){
fmt.Println("手机停止工作。。。")
}
type Camera struct{
}
// 让结构体Camera实现Usb接口
func (c Camera) Start(){
fmt.Println("相机开始工作。。。")
}
func (c Camera) Stop(){
fmt.Println("相机停止工作。。。")
}
type Computer struct{
}
// 编写一个方法Working方法,接收一个Usb接口类型变量
// 只要是实现了Usb接口(所谓实现Usb接口,就是指实现了Usb接口声明所有方法)
func (c Computer) Working(usb Usb){
usb.Start()
usb.Stop()
}
func main(){
// 测试
// 创建结构体变量
computer := Computer{}
phone := Phone{}
camera := Camera{}
//关键点
computer.Working(phone)
computer.Working(camera)
}
5.2 接口的注意事项和细节
- 重点:接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量
package main
import (
"fmt"
)
type AInterface interface{
Say()
}
type Stu struct{
Name string
}
func (stu Stu) Say(){
fmt.Println("Stu Say()")
}
func main() {
var stu Stu
var a AInterface = stu
a.Say()
}
- 接口中所有的方法都没有方法体即都是没有实现的方法。
- 在Golang中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现
了该接口。 - 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
- 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。
type integer int
func(i integer) Say(){
fmt.Println("integer say i=", i)
}
var i integer = 10
var b AInterface = i
b.Say()
- 一个自定义类型可以实现多个接口
package main
import (
"fmt"
)
type AInterface interface{
Say()
}
type BInterface interface{
Hello()
}
type Monster struct{
}
func (m Monster) Say(){
fmt.Println("Monster say~~")
}
func (m Monster) Hello(){
fmt.Println("Monster hello~~")
}
func main() {
var m Monster
var a AInterface = m
var b BInterface = m
a.Say()
b.Hello()
}
-
Golang接口中不能有任何变量

-
一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必
须将B,C接口的方法也全部实现。 -
interface 类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil
-
空接口interface{} 没有任何方法,所以所有类型都实现了空接口,即我们可以把任何一个变量赋给空接口。
type T interface{
}
type Stu struct{
Name string
}
var stu Stu
var t T = stu//ok
fmt.Println(t)
var t2 interface{} = stu
var num1 float64 = 8.8
t2 = num1
t = num1
fmt.Println(t2, t)
5.3 接口编程的最佳实践
- 实现对Hero结构体切片的排序: sort. Sort(data Interface)
- 实现Sort对应的接口,Len Less Swap
package main
import (
"fmt"
"sort"
"math/rand"
)
// 声明一个Hero的结构体
type Hero struct{
Name string
Age int
}
// 声明一个结构体切片类型
type HeroSlice []Hero
// 实现一个interface接口
func (hs HeroSlice) Len() int{
return len(hs)
}
//Less方法就是决定你使用什么标准进行排序
//1.按Hero的年龄从小到大排序!!
func (hs HeroSlice) Less(i, j int) bool {
return hs[i].Age < hs[j].Age
//修改成对Name排序
//return hs[i].Name < hs[j].Name
}
func (hs HeroSlice) Swap(i, j int) {
// temp := hs[i]
// hs[i] = hs[j]
// hs[j] = temp
//下面的一句话等价于三句话
hs[i], hs[j] = hs[j], hs[i]
}
//1.声明Student结构体
type Student struct{
Name string
Age int
Score float64
}
func main() {
//先定义一个数组/切片
var intSlice = []int{0,-1, 10, 7, 90}
//要求对intSlice 切片进行排序
//1.冒泡排序
//2.也可以使用系统提供的方法.
sort.Ints(intSlice)
fmt.Println(intSlice)
//请大家对结构体切片进行排序.
var heroes HeroSlice
for i:=0; i<10; i++ {
hero := Hero{
Name : fmt.Sprintf("英雄|%d", rand.Intn(100)),
Age : rand.Intn(100),
}
//将hero append到heroes 切片
heroes = append(heroes, hero)
}
//看看排序前的顺序
for _, v := range heroes {
fmt.Println(v)
}
//调用sort.Sort
sort.Sort(heroes)
fmt.Println("-----排序后-----")
//看看排序后的顺序
for _, v := range heroes {
fmt.Println(v)
}
}
5.4 接口VS继承

-
当A结构体继承了B结构体,那么A结构就自动的继承了B结构体的字段和方法,并且可以直接使用
-
当A结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此我们可以认为:实现接口是对继承机制的补充.

-
接口和继承解决的解决的问题不同.
- 继承的价值主要在于:解决代码的复用性和可维护性。
- 接口的价值主要在于设计:设计好各种规范(方法),让其它自定义类型去实现这些方法。
-
接口比继承更加灵活Person Student BirdAble LittleMonkey
- 接口比继承更加灵活,继承是满足is-a 的关系,而接口只需满足like-a 的关系。
- 接口在一定程度上实现代码解耦
5.5 面向对象编程三大特性-多态
- 变量(实例)具有多种形态。面向对象的第三大特征,在Go语言,多态特征是通过接口实现的。可
以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。 - 在前面的Usb接口案例,Usb usb ,既可以接收手机变量,又可以接收相机变量,就体现了Usb接
口多态特性。 - 接口体现多态的两种形式
- 多态参数(前面的Usb案例)
- 多态数组(数组中存两种结构体实例)
package main
import (
"fmt"
)
type Usb interface{
// 声明了两个没有实现的方法
Start()
Stop()
}
type Phone struct{
Name string
}
// 让结构体Phone实现Usb接口
func (p Phone) Start(){
fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop(){
fmt.Println("手机停止工作。。。")
}
type Camera struct{
Name string
}
// 让结构体Camera实现Usb接口
func (c Camera) Start(){
fmt.Println("相机开始工作。。。")
}
func (c Camera) Stop(){
fmt.Println("相机停止工作。。。")
}
type Computer struct{
}
// 编写一个方法Working方法,接收一个Usb接口类型变量
// 只要是实现了Usb接口(所谓实现Usb接口,就是指实现了Usb接口声明所有方法)
func (c Computer) Working(usb Usb){
usb.Start()
usb.Stop()
}
func main(){
//定义一个Usb接口数组,可以存放Phone和Camera的结构体变量
//这里就体现出多态数组
var usbArr [3]Usb
usbArr[0] = Phone{"vivo"}
usbArr[1]= Phone{"小米"}
usbArr[2] = Camera{"尼康"}
fmt.Println(usbArr)
}
5.6 类型断言

- 类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言,具体的如下:
//类型断言的其它案例
var x interface{}
var b2 float32 = 1.1
x = b2 //空接口,可以接收任意类型
// x=>float32 [使用类型断言]
y := x.(float32)
fmt. Printf("y的类型是%T值是=%v", y, y)
- 在进行类型断言时,如果类型不匹配,就会报panic, 因此进行类型断言时,要确保原来的空接口,指向的就是断言的类型.
- 如何在进行断言时,带上检测机制,如果成功就ok,否则也不要报panic
var x interface{}
var b2 float32 = 1.1
x = b2 //空接口,可以接收任意类型
if y, ok := x.(float32); ok{
fmt.Println("convert success")
fmt.Printf("y的类型是%T值是=%v", y, y)
}else{
fmt.Println("convert fail")
}
fmt.Println("继续执行...")
5.7 类型断言的最佳实践
- 最佳实践一
package main
import (
"fmt"
)
type Usb interface{
// 声明了两个没有实现的方法
Start()
Stop()
}
type Phone struct{
Name string
}
// 让结构体Phone实现Usb接口
func (p Phone) Start(){
fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop(){
fmt.Println("手机停止工作。。。")
}
func (p Phone) Call(){
fmt.Println("手机正在打电话")
}
type Camera struct{
Name string
}
// 让结构体Camera实现Usb接口
func (c Camera) Start(){
fmt.Println("相机开始工作。。。")
}
func (c Camera) Stop(){
fmt.Println("相机停止工作。。。")
}
type Computer struct{
}
// 编写一个方法Working方法,接收一个Usb接口类型变量
// 只要是实现了Usb接口(所谓实现Usb接口,就是指实现了Usb接口声明所有方法)
func (c Computer) Working(usb Usb){
usb.Start()
//如果usb是指向Phone结构体变量,则还需要调用Call方法
//类型判断.[注意体会!!]
if phone, ok := usb.(Phone); ok {
phone.Call()
}
usb.Stop()
}
func main(){
//定义一个Usb接口数组,可以存放Phone和Camera的结构体变量
//这里就体现出多态数组
var usbArr [3]Usb
usbArr[0] = Phone{"vivo"}
usbArr[1]= Phone{"小米"}
usbArr[2] = Camera{"尼康"}
//Phone还有一个特有的方法call(), 请遍历Usb数组, 如果是Phone变量,
//除了调用Usb接口声明的方法外, 还需要调用Phone 特有方法call.=》类型断言
var computer Computer
for _, v := range usbArr{
computer.Working(v)
fmt.Println()
}
fmt.Println(usbArr)
}
- 最佳实践二
package main
import (
"fmt"
)
type student struct{
Name string
}
//编写一个函数,可以判断输入的参数是什么类型
// ...可变参数(可接收任意多个任意类型的参数)
func TypeJudge(items... interface{}) {
for index, x := range items {
switch x.(type) {
case bool:
fmt.Printf("第%v个参数是bool 类型, 值是%v\n", index, x)
case float32:
fmt.Printf("第%v个参数是float32 类型, 值是%v\n", index, x)
case float64:
fmt.Printf("第%v个参数是float64 类型, 值是%y\n", index, x)
case int, int32, int64 :
fmt.Printf("第%v个参数是整数类型, 值是%v\n", index, x)
case string :
fmt.Printf("第%v个参数是string 类型, 值是%v\n", index, x)
case student :
fmt.Printf("第%v个参数是Student类型, 值是%v\n", index, x)
case *student :
fmt.Printf("第%v个参数是*student 类型, 值是%v\n", index, x)
default :
fmt.Printf("第%v个参数是类型 不确定, 值是%v\n", index, x)
}
}
}
func main() {
var n1 float32 = 1.1
var n2 float64 = 2.3
var n3 int32 = 30
var name string = "tom"
address := "北京"
n4 := 300
stu := student{"xiaoming"}
TypeJudge(n1, n2, n3, name, address, n4, stu, &stu)
}

被折叠的 条评论
为什么被折叠?



