go语言系列-面向对象编程
面向对象编程
结构体
一个程序就是一个世界,有很多对象(变量)
Go也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以说Go支持面向对象编程特性是比较准确的
Go没有类(class),Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,可以理解Go是基于struct来实现OOP特性的。
Go面向对象编程非常简洁,去掉了传统OOP语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等等
Go仍然有面向对象编程的继承、封装和多态的特性,只是实现的方式和其它OOP语言不一样,比如继承:Go没有extends关键字,继承是通过匿名字段来实现
- Go面向对象(OOP)很优雅,OOP本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,非常灵活。后面会充分体会这个特点。也就是说在Go中面向接口编程是非常重要的特性
结构体与结构体变量(实例/对象)的关系示意图
*注意:从猫结构体到变量,就是创建一个Cat结构体变量,也可以说是定义Cat结构体变量*
对上图的说明
将一类事物的特性提取出来(比如猫类),形成一个新的数据类型,就是一个结构体
通过这个结构体,我们可以创建多个变量(实例/对象)
事物可以猫类,也可以是Person,Fish 或是某个工具类
基本语法
type 结构体名称 struct {
field1 type
field2 type
}
type Cat struct {
Name string
Age int
Color string
Hobby string
}
func main() {
//创建一个Cat的变量
var cat1 Cat
cat1.Name = "小白"
cat1.Age = 3
cat1.Color = "白色"
cat1.Hobby = "吃 <·)))><<"
fmt.Println("cat1 = ", cat1)
fmt.Println("猫猫的信息如下:")
fmt.Println("name = ", cat1.Name)
fmt.Println("age = ", cat1.Age)
fmt.Println("color = ", cat1.Color)
fmt.Println("hobby = ", cat1.Hobby)
}
//cat1 = {小白 3 白色 吃 <·)))><<}
//猫猫的信息如下:
//name = 小白
//age = 3
//color = 白色
//hobby = 吃 <·)))><<
结构体和结构体变量(实例)的区别和联系
结构体是自定义的数据类型,代表一类事物
结构体变量(实例)是具体的,实际的,代表一个具体变量
结构体变量(实例)在内存的布局【重要】
字段/属性
基本介绍
1) 从概念或叫法上看:结构体字段 = 属性 = field
2) 字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。比如我们前面定义猫结构体的Name string 就是属性
注意事项和细节说明
-
字段声明语法同变量,示例:字段名 字段类型
-
字段的类型可以为:基本类型、数组或引用类型
-
在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则如下:
布尔类型是false,数值是0,字符串是””
数组类型的默认值和它的元素类型相关,比如score[3]int 则为[0,0,0]
指针,slice和map的零值都是nil,即还没有分配空间
//如果结构体的字段类型是:指针,slice和map的零值都是nil,即还没有分配空间
//如果需要使用这样的字段,需要先make,才能使用
type Person struct {
Name string
Age int
Scores [5]float64
ptr *int //指针
slice []int //切片
map1 map[string]string //map
}
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
//使用map,一定要先make
p1.map1 = make(map[string]string)
p1.map1["key1"] = "tom"
fmt.Println(p1)
}
//输出:{ 0 [0 0 0 0 0] <nil> [] map[]}
//ok1
//ok2
//ok3
//{ 0 [0 0 0 0 0] <nil> [100 0 0 0 0 0 0 0 0 0] map[key1:tom]}
- 不同结构体变量的字段是独立的,互不影响,一个结构体变量字段的更改,不影响另外一个,结构体是值类型
type Monster struct {
Name string
Age int
}
func main(){
var monster1 Monster
monster1.Name = "牛魔王"
monster1.Age = 500
monster2 := monster1 //结构体是值类型,默认为值拷贝
monster2.Name = "青牛精"
fmt.Println("monster1 = ",monster1)
fmt.Println("monster2 = ",monster2)
}
//输出:monster1 = {牛魔王 500}
//monster2 = {青牛精 500}
创建结构体变量和访问结构体字段
方式1:直接声明 var person Person
type Cat struct {
Name string
Age int
Color string
Hobby string
}
func main() {
//创建一个Cat的变量
var cat1 Cat
cat1.Name = "小白"
cat1.Age = 3
cat1.Color = "白色"
cat1.Hobby = "吃 <·)))><<"
方式2:{} var person Person = Person{}
type Monster struct {
Name string
Age int
}
func main(){
//{}
p := Monster{"zisefeizhu",21}
fmt.Println(p)
//输出:{zisefeizhu 21}
}
方式3: & var person *Person = new (Person)
type Monster struct {
Name string
Age int
}
func main(){
//方式3
//案例:var person *Person = new (Person)
var p3 *Monster = new(Monster)
//因为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) //{john 100}
}
方式4: {} var person *Person = &Person{}
type Monster struct {
Name string
Age int
}
func main(){
//方式4 - {}
//var person *Person = &Person{}
//下面的语句,也可以直接给字符赋值
//var person *Person = &Person{"mary",60}
var person *Monster = &Monster{}
//因为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) //{scott ~ 10}
}
说明:
1)第3种和第4种方式返回的是结构体指针
2)结构体指针访问字段的标准方式应该是:(*结构体指针)字段名,比如(*person).Name = “tom”
3)但Go做了一个简化,也支持 结构体指针.字段名,比如person.Name = “tom”。更加符合程序员使用的习惯,Go编译器底层对person.Name做了转化(*person).Name
struct类型的内存分配机制
定义一个Person结构体(包括 名字,年龄)
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)
p2.Name = "tom"
fmt.Printf("p2.Name = %v p1.Name = %v",p2.Name,p1.Name)
}
//输出:10
//p2.Name = tom p1.Name = 小明
变量总是存在内存中的,那么结构体变量在内存中究竟是怎样存在的?
画一个图说明:结构体变量在内存中如何存在
看下面代码,分析原因
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) //10
fmt.Println(p2.Age) //10
p2.Name = "tom ~"
fmt.Printf("p2.Name = %v p1.Name = %v \n",p2.Name,p1.Name) //p2.Name = tom ~ p1.Name = tom ~
fmt.Printf("p2.Name = %v p1.Name = %v \n",(*p2).Name,p1.Name) //p2.Name = tom ~ p1.Name = tom ~
fmt.Printf("p1的地址%p\n",&p1) //p1的地址0xc00004a420
fmt.Printf("p2的地址%p p2的值%p\n",&p2, p2) //p2的地址0xc000080018 p2的值0xc00004a420
fmt.Println(p2.Age) //10
p2.Name = "tom"
fmt.Printf("p2.Name = %v p1.Name = %v",p2.Name,p1.Name) //p2.Name = tom p1.Name = tom
}
结构体使用注意事项和细节
1)结构体的所有字段在内存中连续的
//结构体
type Point struct {
x int
y int
}
//结构体
type Rect struct {
leftUp, rightDown Point
}
//结构体
type Rect2 struct {
leftUp, rightDown *Point
}
func main() {
r1 := Rect{Point{1,2},Point{3,4}}
//r1有四个int,在内存中是连续分布
//打印地址
fmt.Printf("r1.leftUp.x 地址=%p r1.leftUp.y 地址=%p r1.rightDown.x 地址=%p r1.rightDown.y 地址=%p\n" ,
&r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y)
//r.leftUp.x 地址=0xc000052140 r1.leftUp.y 地址=0xc000052148 r1.rightDown.x 地址=0xc000052150 r1.rightDown.y 地址=0xc000052158
//r2有两个*Point类型,这两个*Point类型的本身地址也是连续的
//但是它们指向的地址不一定是连续
r2 := Rect2{&Point{10,20},&Point{30,40}}
//打印地址
//打印地址
fmt.Printf("r2.leftUp 本身地址 = %p r2.rightDown 本身地址 = %p \n",
&r2.leftUp, &r2.rightDown)
//r2.leftUp 本身地址 = 0xc0000401c0 r2.rightDown 本身地址 = 0xc0000401c8
//它们指向的地址不一定是连续... 这个要看系统在运行时是如何分配
fmt.Printf("r2.leftUp 指向地址 = %p r2.rightDown 指向地址 = %p \n",
r2.leftUp, r2.rightDown)
//r2.leftUp 指向地址 = 0xc000054090 r2.rightDown 指向地址 = 0xc0000540a0
}
对应的分析图
2)结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
type A struct {
Num int
}
type B struct {
Num int
}
func main() {
var a A
var b B
a = A(b) //? 可以转换,但是有要求,就是结构体的字段要完全一样(包括:名字、个数和类型)
fmt.Println(a, b) //{0} {0}
}
- 结构体进行type重新定义(相当于取别名),Go认为是新的数据类型,但是相互间可以强转
- struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用就是序列化和反序列化
序列化的常见使用
import (
"encoding/json"
"fmt"
)
type Monster struct {
Name string `json:"name"` // `json:"name"` 就是struct tag
Age int `json:"age"`
Skill string `json:"skill"`
}
func main() {
//1.创建一个Monster变量
Monster := Monster{"牛魔王",500,"芭蕉扇"}
//2.将monster变量序列化为json格式字串
// json.Marshal 函数中使用反射,这里只是用一下反射,在后面会详细介绍
jsonStr, err := json.Marshal(Monster)
if err != nil {
fmt.Println("json 处理错误",err)
}
fmt.Println("jsonStr",string(jsonStr)) //jsonStr {"name":"牛魔王","age":500,"skill":"芭蕉扇"}
}
方法
在某些情况下,需要声明(定义)方法。比如Person结构体:除了有一些字段外(年龄,姓名...),Person结构体还有一些行为比如:可以说话、跑步...,通过学习,还可以做算术题。这时就要用方法才能完成
Go中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct
方法的声明和调用
type A struct {
Num int
}
func (a A)test(){
fmt.Println(a.Num)
}
对上面的语法的说明
func (a A)test() {} 表示A结构体有 - 方法,方法名为test
(a A)体现test方法是和A类型绑定的
type Person struct {
Name string
}
//给Person类型绑定 -- 方法
func (p Person) test() {
fmt.Println("test() name = ", p.Name) //test() name = tom
}
func main() {
var p Person
p.Name = "tom"
p.test() // 调用方法
}
对上面的总结
- test方法和Person类型绑定
2)test方法只能通过Person类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
-
func (p Person)test(){}... p表示哪个Person变量调用,这个p就是它的副本,这点和函数传参非常相似
-
p这个名字,由程序员指定,不是固定,比如修改成person也是可以
type Person struct {
Name string
}
//给Person类型绑定 -- 方法
func (person Person) test() {
fmt.Println("test() name = ", person.Name) //test() name = tom
}
func main() {
var p Person
p.Name = "tom"
p.test() // 调用方法
}
快速入门
- 给Person结构体添加speak方法,输出xxx是一个好人
type Person struct {
Name string
}
//给Person结构体添加speak方法,输出xxx是一个好人
func (p Person) speak() {
fmt.Println(p.Name,"是一个goodman~") //tom 是一个goodman~
}
func main() {
var p Person
p.Name = "tom"
p.speak()
}
- 给Person结构体添加jisuan方法,可以计算从1+..+1000的1结果,说明:方法体内可以像函数一样进行各种运算
type Person struct {
Name string
}
//给Person结构题添加jisuan方法,可以计算从1+..+1000的1结果
func (p Person) jisuan() {
res := 0
for i:=1; i <= 1000; i++ {
res += i
}
fmt.Println(p.Name,"计算的结构是 = ", res) //tom 计算的结构是 = 500500
}
func main() {
var p Person
p.Name = "tom"
p.jisuan()
}
- 给Person结构体jisuan2方法,该方法可以接收一个数n,计算从1+..+n的结果
type Person struct {
Name string
}
func (p Person) jisuan2(n int) {
res := 0
for i:=1; i <= n; i++ {
res += i
}
fmt.Println(p.Name,"计算的结构是 = ", res) //tom 计算的结构是 = 210
}
func main() {
var p Person
p.Name = "tom"
p.jisuan2(20)
}
- 给Person结构体添加getSum方法,可以计算两个数的和,并返回结果
type Person struct {
Name string
}
func (p Person) getSum(n1 int, n2 int) int {
return n1 + n2
}
func main() {
var p Person
res := p.getSum(10, 20)
fmt.Println("res = ", res) //res = 30
}
方法的调用和传参机制原理[重要]
方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当作实参也传递给方法。
案例1
画出前面getSum方法的执行过程+说明
-
在通过一个变量去调用方法时,其调用机制和函数一样
-
不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝)
案例2
请编写一个程序,要求如下:
-
声明一个结构体Circle,字段为radius
-
声明一个方法area和Circle绑定,可以返回面积
-
提示:画出area执行过程+说明
方法的声明(定义)
func (recevier type) methodName (参数列表) (返回值列表) {
方法体
return 返回值
}
1)参数列表:表示方法输入
2)recevier type:表示这个方法和type这个类型进行绑定,或者说该方法作用于type类型
3)recevier type:type可以是结构体,也可以其它的自定义类型
4)recevier:就是type类型的一个变量(实例),比如:Person结构体的一个变量(实例)
5)返回值列表:表示返回的值,可以多个
6)方法主体:表示为了实现某一功能代码块
7)return语句不是必须的
方法的注意事项和细节
-
结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
-
如果程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
-
Go中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct,比如int,float32等都可以有方法
type integer int
func (i integer) print() {
fmt.Println("i = ",i)
}
//编写一个方法,可以改变i的值
func (i *integer) change() {
*i = *i + 1
}
func main() {
var i integer = 10
i.print()
i.change()
fmt.Println("i = ", i)
}
//输出:i = 10
//i = 11
-
方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问
-
如果一个类型实现了String()这个方法,那么fmt.Println默认会调用这个变量的String()进行输出
type Student struct {
Name string
Age int
}
//给*Student实现方法String()
func (stu *Student) String() string {
str := fmt.Sprintf("Name = [%v] Age = [%v]",stu.Name, stu.Age)
return str
}
func main() {
//定义一个Student变量
stu := Student{
Name: "tom",
Age: 20,
}
//如果实现了*Student 类型的string方法,就会自动调用
fmt.Println(&stu)
}
//输出:Name = [tom] Age = [20]
方法和函数的区别
- 调用方式不一样
函数的调用方式:函数名(实参列表)
方法的调用方式:变量.方法名(实参列表)
- 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
type Person struct {
Name string
}
//函数
//对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
func test01(p Person) {
fmt.Println(p.Name) //tom
}
func test02(p *Person) {
fmt.Println(p.Name) //tom
}
func main() {
p := Person{"tom"}
test01(p)
test02(&p)
}
- 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
type Person struct {
Name string
}
//3)对于方法(如struct的方法),
// 接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
func (p Person) test03() {
p.Name = "jack"
fmt.Println("test03() = ",p.Name)
}
func (p *Person) test04() {
p.Name = "mary"
- fmt.Println("test04() = ",p.Name)
}
func main() {
p := Person{"tom"}
p.test03()
fmt.Println("main() p.name = ",p.Name)
(&p).test03() //从形式上传入地址,但是本质仍然是值拷贝
fmt.Println("main() p.name = ",p.Name)
(&p).test04()
fmt.Println("main() p.name = ",p.Name)
p.test04()
}
//test03() = jack
//main() p.name = tom
//test03() = jack
//main() p.name = tom
//test04() = mary
//main() p.name = mary
//test04() = mary
总结
-
不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定
-
如果是和值类型,比如(p Person),则是值拷贝,如果和指针类型,比如是(p Person)则是地址拷贝
方法练习题
编写结构体(MethodUtils),编程一个方法,方法不需要参数,在方法中打印一个 10*8的矩形,在main方法中调用该方法
type MethodUtils struct {
//字段...
}
//给MethodUtils编写方法
func (mu MethodUtils) print() {
for i := 1; i <= 10; i++ {
for j := 1; j <= 8; j++ {
fmt.Print("*")
}
fmt.Println()
}
}
func main() {
var mu MethodUtils
mu.print()
}
//输出:
//********
//********
//********
//********
//********
//********
//********
//********
//********
//********
编写一个方法,提供m 和 n 两个参数,方法中打印一个 m * n 的矩形
type MethodUtils struct {
//字段...
}
func (mu MethodUtils) print2(m int, n int) {
for i := 1; i <= 10; i++ {
for j := 1; j <= 8; j++ {
fmt.Print("*")
}
fmt.Println()
}
}
func main() {
var mu MethodUtils
mu.print2(10,8)
}
//输出:
//********
//********
//********
//********
//********
//********
//********
//********
//********
//********
编写一个方法算该矩形的面积(可以接收长len,和宽width),将其作为方法返回值。在main方法中调用该方法,接收返回的面积值并打印
type MethodUtils struct {
//字段...
}
func (mu MethodUtils) area(len float64, width float64) (float64) {
return len * width
}
func main() {
var mu MethodUtils
fmt.Println("面积 =",mu.area(10,20)) //面积 = 200
}
编写方法:判断一个数是奇数还是偶数
type MethodUtils struct {
//字段...
}
func (mu *MethodUtils) JudgeNum(num int) {
if num % 2 == 0 {
fmt.Println(num,"是偶数..") // 10 是偶数..
} else {
fmt.Println(num,"是奇数..")
}
}
func main() {
var mu MethodUtils
mu.JudgeNum(10)
}
根据行、列、字符打印对应行数和列数的字符,比如:行:3,列:2,字符*,则打印相应的效果
type MethodUtils struct {
//字段...
}
func (mu *MethodUtils) print(n int, m int, key string) {
for i := 1; i <= n; i++ {
for j := 1; j <= m; j++ {
fmt.Print(key)
}
fmt.Println()
}
}
func main() {
var mu MethodUtils
mu.print(5,5,"+")
}
定义小小计算器结构体(Calcuator),实现加减乘除四个功能
实现形式1:分四个方法完成
实现形式2:用一个方法搞成
//实现形式1
type Calcuator struct {
Num1 float64
Num2 float64
}
func (calcuator *Calcuator) getSum() float64 {
return calcuator.Num1 + calcuator.Num2
}
func (calcuator *Calcuator) getSub() float64 {
return calcuator.Num1 - calcuator.Num2
}
package main
import "fmt"
//实现形式2
type Calcuator struct {
Num1 float64
Num2 float64
}
func (calcuator *Calcuator) getRes(operator byte) float64 {
res := 0.0
switch operator {
case '+':
res = calcuator.Num1 + calcuator.Num2
case '-':
res = calcuator.Num1 - calcuator.Num2
case '*':
res = calcuator.Num1 * calcuator.Num2
case '/':
res = calcuator.Num1 / calcuator.Num2
default:
fmt.Println("运算符输入有误...")
}
return res
}
在MerhodUtils结构体编个方法,从键盘接收整数(1-9),打印对应乘法表
type MerhodUtils struct {
//字段
}
func (m MerhodUtils) jiu(n int) {
for i := 1; i <= n; i++ {
for j := 1; j <= i; j++ {
fmt.Printf("%v * %v = %v\t",i,j,i*j)
}
fmt.Println()
}
}
func main() {
var mu MerhodUtils
var num int
fmt.Println("请键入要输入的大于等于1小于等于9的自然数: ")
fmt.Scanln(&num)
mu.jiu(num)
}
//请键入要输入的大于等于1小于等于9的自然数:
//2
//1 * 1 = 1
//2 * 1 = 2 2 * 2 = 4
编写方法,使给定的一个二维数组(3 × 3)转置
面向对象编程应用实例
步骤
-
声明(定义)结构体,确定结构体名
-
编写结构体的字段
-
编写结构体的方法
学生案例
-
编写一个Student结构体,包含name、gender、age、id、score字段,分别为string、string、int、int、float64类型
-
结构体中声明一个say方法,返回string类型,方法返回信息中包含所有字段值
-
在main方法中,创建Student结构体实例(变量),并访问say方法,并将调用结果打印输出
type Student struct {
name string
gender string
age int
id int
score float64
}
func (student *Student) say() string {
infoStr := fmt.Sprintf("student 的信息 name = [%v] gender = [%v] age = [%v] id = [%v] score = [%v]",
student.name, student.gender, student.age, student.id, student.score)
return infoStr
}
func main() {
//创建一个Student实例变量
var stu = Student{
name: "zisefeizhu",
gender:"male",
age: 18,
id: 1000,
score: 99.98,
}
fmt.Println(stu.say())
}
//输出:student 的信息 name = [zisefeizhu] gender = [male] age = [18] id = [1000] score = [99.98]
小狗案例
-
编写一个Dog结构体,包含name、age、weight字段
-
结构体中声明一个say方法,返回string类型,方法返回信息中包含所有字段值
-
在main方法中,创建Dog结构体实例(变量),并返回say方法,将调用结果打印输出
type Dog struct {
name string
age int
wgight float64
}
func (dog *Dog) say() string {
infoStr := fmt.Sprintf("dog 的信息 name = [%v] age = [%v] weight = [%v]",
dog.name, dog.age, dog.wgight)
return infoStr
}
func main() {
//创建一个Student实例变量
var stu = Dog{
name: "xiaohua",
age: 18,
wgight: 23,
}
fmt.Println(stu.say())
}
//输出:dog 的信息 name = [xiaohua] age = [18] weight = [23]
盒子案例
-
编程创建一个Box结构体,在其中声明三个字段表示一个立方体的长、宽和高,长宽高要从终端获取
-
声明一个方法获取立方体的体积
-
创建一个Box结构体变量,打印给定尺寸的立方体的体积
type Box struct {
len float64
width float64
height float64
}
//声明一个方法获取立方体的体积
func (box *Box) getVolum() float64 {
return box.len * box.width * box.height
}
func main() {
var box Box
box.len = 1.1
box.width = 2.0
box.height = 3.0
volumn := box.getVolum()
fmt.Printf("体积为=%.2f",volumn)
}
//输出:体积为=6.60
景区门票案例
-
一个景区根据游人的年龄收取不同价格的门票,比如年龄大于18,收费20元,其它情况门票免费
-
请编写Visitor结构体,根据年龄段决定能够购买的门票价格并输出
type Visitor struct {
Name string
Age int
}
//声明一个方法获取立方体的体积
func (visitor *Visitor) showPrice() {
if visitor.Age >= 90 || visitor.Age <= 8 {
fmt.Println("考虑到安全,就不要玩了")
return
}
if visitor.Age > 18 {
fmt.Printf("游客的名字为 %v 年龄为 %v 收费20元\n", visitor.Name, visitor.Age)
} else {
fmt.Printf("游客的名字为 %v 年龄为%v 免费 \n", visitor.Name, visitor.Age)
}
}
func main() {
var v Visitor
for {
fmt.Println("请输入你的名字")
fmt.Scanln(&v.Name)
if v.Name == "n" {
fmt.Println("退出程序...")
break
}
fmt.Println("请输入你的年龄")
fmt.Scanln(&v.Age)
v.showPrice()
}
}
//输出:请输入你的名字
//zisefeizhu
//请输入你的年龄
//20
//游客的名字为 zisefeizhu 年龄为 20 收费20元
//请输入你的名字
//n
//退出程序...
创建结构体变量时指定字段值
Go在创建结构体实例(变量)时,可以直接指定字段的值
方式1:
type Stu struct {
Name string
Age int
}
func main() {
//方式1
//在创建结构体变量时,就直接指定字段的值
var stu1 = Stu{"zisefeizhu",20} // stu1 --> 结构体数据空间
stu2 := Stu{"jingxing",20}
//在创建结构体变量时,把字段名和字段值写在一起,这种写法,就不依赖字段的定义顺序
var stu3 = Stu{
Name: "yike",
Age: 20,
}
var stu4 = Stu{
Name: "gengpan",
Age: 20,
}
fmt.Println(stu1, stu2, stu3, stu4)
}
//输出:{zisefeizhu 20} {jingxing 20} {yike 20} {gengpan 20}
方式2:
type Stu struct {
Name string
Age int
}
func main() {
//方式2
var stu5 *Stu = &Stu{"小王", 20} //stu5 --> 地址 --> 结构体数据[xxxx,xxxx]
stu6 := &Stu{"小紫", 20}
//在创建结构体指针变量时。把字段名和字段值写在一起,这种写法,就不依赖字段的定义顺序
var stu7 = &Stu{
Name: "小林",
Age: 20,
}
var stu8 = &Stu{
Age: 20,
Name: "小耿",
}
fmt.Println(*stu5, *stu6, *stu7, *stu8)
}
//输出:{小王 20} {小紫 20} {小林 20} {小耿 20}
工厂模式
Go的结构体没有构造函数,通常可以使用工厂模式来解决这个问题
看一个需求
一个结构体的声明是这样的:
pachage model
type Student struct {
Name string ...
}
因为这里的Student的首字母S是大写的,如果我们想在其它包创建Student的实例(比如main包),引入model包后,就可以直接创建Student结构体的变量(实例)。但是问题来了,如果首字母是小写的,比如是type student struct {...}就不行了,怎么办-->工厂模式来解决>
工厂模式来解决问题
使用工厂模式实现跨包创建结构体实例(变量)的案例:
如果model包的*结构体变**量**首字母大写,引入后,直接使用*,没有问题
![img](file:///C:\Users\linkun\AppData\Local\Temp\ksohtml1372\wps1.jpg)
如果model包的结构体变量首字母小写,引入后,不能直接使用,可以工厂模式解决
student.go
package model
//定义一个结构体
type student struct {
Name string
Score float64
}
//因为student结构体首字母是小写,因此只能在model使用
//通过工厂模式来解决
func NewStudent(n string, s float64) *student {
return &student{
Name: n,
Score: s,
}
}
main.go
package main
import (
"2020-04-04/model"
"fmt"
)
func main() {
var stu = model.NewStudent("tom", 21)
fmt.Println(*stu) //&{...}
fmt.Println("name = ", stu.Name, "score = ", stu.Score)
}
//name = tom score = 21
思考题
如果model包的student 的结构体的字段Score 改成score, 我们还能正常访问吗?又应该如何解决这个问题呢?
解决方法如下:
student.go
//定义一个结构体
type student struct {
Name string
score float64
}
//因为student结构体首字母是小写,因此只能在model使用
//通过工厂模式来解决
func NewStudent(n string, s float64) *student {
return &student{
Name: n,
score: s,
}
}
//如果score字段首字母小写,则,在其它包不可以直接访问,可以提供一个方法
func (s *student) GetScore() float64 {
return s.score
}
main.go
import (
"2020-04-04/model"
"fmt"
)
func main() {
var stu = model.NewStudent("tom", 22)
fmt.Println(*stu) //&{...}
fmt.Println("name = ", stu.Name, "score = ", stu.GetScore())
}
//{tom 22}
//name = tom score = 22
面向对象编程思想-抽象
定义一个结构体的时候,实际上就是把一类事物的共有的属性(字段)和行为(方法)提取出来,形成一个物理模型(结构) 。这种研究问题的方法称为抽象。
快速入门案例
//定义一个结构体Account
type Account struct {
AccountNo string
Pwd string
Balance float64
}
//方法
//1.存款
func (account *Account) Deposite(money float64,pwd string){
//看下输入的密码是否正确
if pwd != account.Pwd {
fmt.Println("你输入的密码不正确")
return
}
//看看存款是否正确
if money <= 0 {
fmt.Println("你输入的金额不正确")
return
}
account.Balance += money
fmt.Println("存款成功!")
}
//取款
func (account *Account) WithDraw(money float64, pwd string) {
//看一下输入的密码是否正确
if pwd != account.Pwd {
fmt.Println("你输入的密码不正确")
return
}
//看看存款金额是否正确
if money <= 0 || money > account.Balance{
fmt.Println("你输入的金额不正确")
return
}
account.Balance -= money
fmt.Println("取款成功~")
}
//查询余额
func (account *Account) Query(pwd string) {
//看一下输入的密码是否正确
if pwd != account.Pwd {
fmt.Println("你输入的密码不正确")
return
}
fmt.Printf("你的账号为=%v 余额=%v \n",account.AccountNo,account.Balance)
}
func main() {
var pwd string
var balance float64
account := Account{
AccountNo: "1111111",
Pwd: "666666",
Balance: 0.0,
}
fmt.Println("请输入密码")
fmt.Scanln(&pwd)
if pwd == account.Pwd {
fmt.Println("请输入金额")
fmt.Scanln(&balance)
account.Query(pwd)
account.Deposite(balance,pwd)
account.Query(pwd)
}
}
//请输入密码
//666666
//请输入金额
//2000
//你的账号为=1111111 余额=0
//存款成功!
//你的账号为=1111111 余额=2000
对上面代码进行修饰:增加一个控制台的菜单,可以让用户动态的输入选项
package main
import "fmt"
//定义一个结构体Account
type Account struct {
AccountNo string
Pwd string
Balance float64
}
//方法
//1.存款
func (account *Account) Deposite(money float64,pwd string){
//看下输入的密码是否正确
if pwd != account.Pwd {
fmt.Println("你输入的密码不正确")
return
}
//看看存款是否正确
if money <= 0 {
fmt.Println("你输入的金额不正确")
return
}
account.Balance += money
fmt.Println("存款成功!")
}
//取款
func (account *Account) WithDraw(money float64, pwd string) {
//看一下输入的密码是否正确
if pwd != account.Pwd {
fmt.Println("你输入的密码不正确")
return
}
//看看存款金额是否正确
if money <= 0 || money > account.Balance{
fmt.Println("你输入的金额不正确")
return
}
account.Balance -= money
fmt.Println("取款成功~")
}
//查询余额
func (account *Account) Query(pwd string) {
//看一下输入的密码是否正确
if pwd != account.Pwd {
fmt.Println("你输入的密码不正确")
return
}
fmt.Printf("你的账号为=%v 余额=%v \n",account.AccountNo,account.Balance)
}
func main() {
var xuanxiang byte
var pwd string
var balance float64
account := Account{
AccountNo: "1111111",
Pwd: "666666",
Balance: 0.0,
}
fmt.Println("请输入密码")
fmt.Scanln(&pwd)
if pwd == account.Pwd {
for {
fmt.Println("请输入菜单选项:")
fmt.Println("1. 存款")
fmt.Println("2. 取款")
fmt.Println("3. 余额")
fmt.Println("4. 退出")
fmt.Println("请输入菜单选项:")
fmt.Scanln(&xuanxiang)
switch xuanxiang {
case 1:
fmt.Println("请输入金额")
fmt.Scanln(&balance)
account.Query(pwd)
account.Deposite(balance,pwd)
account.Query(pwd)
//fmt.Println("请输入金额")
//fmt.Scanln(&balance)
//account.Deposite(balance,pwd)
case 2:
fmt.Println("请输入金额")
fmt.Scanln(&balance)
account.Query(pwd)
account.WithDraw(balance,pwd)
account.Query(pwd)
case 3:
//
account.Query(pwd)
default:
return
}
}
//fmt.Println("请输入金额")
//fmt.Scanln(&balance)
//account.Query(pwd)
//account.Deposite(balance,pwd)
//account.Query(pwd)
}
}
//请输入密码
//666666
//请输入金额
//2000
//你的账号为=1111111 余额=0
//存款成功!
//你的账号为=1111111 余额=2000
面向对象编程三大特性-封装
Go仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不一样
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作
封装的好处
-
隐藏实现细节
-
可以对数据进行验证,保证安全合理(Age)
如何体现封装
-
对结构体中的属性进行封装
-
通过方法,包实现封装
封装的实现步骤
-
将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似private)
-
给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
-
提供一个首字母大写的Set方法(类似其它语言的public),用于对属性判断并赋值
func(var 结构体类型名) SetXxx(参数列表) (返回值列表) {
//加入数据验证的业务逻辑
var.字段 = 参数
}
- 提供一个首字母大写的Get方法(类似其它语言的public),用于获取属性的值
func (var 结构体类型名) GetXxx() {
return var.age
}
*特别说明*:在Go开发中并没有特别强调封装,这点并不像Java,所以不用总是用Java的语法特性来看待Go,Go本身对面向对象的特性做了简化的
快速入门案例
编写一个程序(person.go),不能随便查看人的年龄、工资等隐私,并对输入的年龄进行合理的验证。
设计:model包(person.go),main包(main.go调用Person结构体)
person.go
type person struct {
Name string
age int //其它包不能直接访问
sal float64
}
//写一个工厂模式的函数,相当于构造函数
func NewPerson(name string) *person {
return &person{
Name: name,
}
}
//为了访问age和sal 编写一对SetXxx的方法和GetXxx的方法
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) SetSal(sal float64) {
if sal >= 3000 && sal <= 30000 {
p.sal = sal
} else {
fmt.Println("薪水范围不正确...")
}
}
func (p *person) GetSal() float64 {
return p.sal
}
main.go
func main() {
p := model.NewPerson("smith")
p.SetAge(18)
p.SetSal(5000)
fmt.Println(p)
fmt.Println(p.Name, "age =", p.GetAge(), "sal =", p.GetSal())
}
//输出:&{smith 18 5000}
//smith age = 18 sal = 5000
改
type person struct {
name string
age int //其它包不能直接访问
sal float64
}
//写一个工厂模式的函数,相当于构造函数
func NewPerson() *person {
return &person{
name: "",
age: 0,
sal: 0,
}
}
//为了访问age和sal 编写一对SetXxx的方法和GetXxx的方法
func (p *person) SetAge(name string ,age int, sal float64) {
if name != "" {
p.name = name
}
if age > 0 && age < 150 {
p.age = age
} else {
fmt.Println("年龄范围不正确..")
//给程序员一个默认值
}
if sal >= 3000 && sal <= 30000 {
p.sal = sal
} else {
fmt.Println("薪水范围不正确...")
}
}
func (p *person) GetAge() (string,int, float64) {
return p.name ,p.age, p.sal
}
func main() {
//p := model.NewPerson("smith")
p := model.NewPerson()
p.SetAge("smith",18, 5000)
fmt.Println(p)
fmt.Println(p.GetAge())
}
再改
//写一个工厂模式的函数,相当于构造函数
func NewPerson(name string, age int, sal float64) *person {
if name == "" {
fmt.Println("输入名字错误")
return nil
}
if age < 0 && age > 150 {
fmt.Println("年龄范围不对...")
return nil
}
if sal < 3000 && sal > 30000 {
fmt.Println("薪水范围不正确...")
return nil
}
return &person{
name: name,
age: age,
sal: sal,
}
}
func (p *person) GetAge() (string,int, float64) {
return p.name ,p.age, p.sal
}
func main() {
p := model.NewPerson("smith",18,50000)
fmt.Println(p)
fmt.Println(p.GetAge())
}
要求
-
创建程序,在model包中定义Account结构体:在main函数中体现Go的封装性
-
Account结构体要求具有字段:账号(长度在6-10之间)、余额(必须>20)、密码(必须是6位数)
-
通过SetXxx的方法给Account的字段赋值
-
在main函数中测试
account.go
//定义一个结构体account
type account struct {
accountNo string
pwd string
balance float64
}
//工厂模式的函数-构造函数
func NewAccount(accountNo string, pwd string, balance float64) *account {
if len(accountNo) < 6 || len(accountNo) > 10 {
fmt.Println("账号的长度不对")
return nil
}
if len(pwd) != 6 {
fmt.Println("密码的长度不对...")
return nil
}
if balance < 20 {
fmt.Println("余额数目不对...")
return nil
}
return &account{
accountNo: accountNo,
pwd: pwd,
balance: balance,
}
}
//方法
//存款
func (account *account) Deposite(money float64, pwd string) {
//看下输入的密码是否正确
if pwd != account.pwd {
fmt.Println("你输入的密码不正确")
return
}
//看看存款金额是否正确
if money <= 0 {
fmt.Println("你输入的金额不正确")
return
}
account.balance += money
fmt.Println("存款成功~")
}
//取款
func (account *account) WithDraw(money float64, pwd string) {
//看下输入的密码是否正确
if pwd != account.pwd {
fmt.Println("你输入的密码不正确")
return
}
//看看存款金额是否正确
if money <= 0 || money > account.balance {
fmt.Println("你输入的金额不正确")
return
}
account.balance -= money
fmt.Println("取款成功~")
}
//查询余额
func (account *account) Query(pwd string) {
//看下输入的密码是否正确
if pwd != account.pwd {
fmt.Println("你输入的密码不正确")
return
}
fmt.Printf("你的账号为 = %v 余额 = %v \n", account.accountNo, account.balance)
}
main.go
func main() {
//创建一个account变量
account := model.NewAccount("zisefeizhu","000",40)
if account != nil {
fmt.Println("创建成功 = ", account)
} else {
fmt.Println("创建失败")
}
}
//输出:密码的长度不对...
//创建失败
增加如下功能:通过SetXxx的方法给Account的字段赋值通过GetXxx方法获取字段的值
account.go
package model
import "fmt"
//定义一个结构体account
type account struct {
accountNo string
pwd string
balance float64
}
//工厂模式的函数-构造函数
func NewAccount(accountNo string, pwd string, balance float64) *account {
return &account{
accountNo: accountNo,
pwd: pwd,
balance: balance,
}
}
func (accounter *account) SetAccountNo(accountNo string) {
if len(accountNo) < 6 || len(accountNo) > 10 {
fmt.Println("账号的长度不对")
}
}
func (accounter *account) GetAccountNo() string {
return accounter.accountNo
}
//方法
//存款
func (account *account) Deposite(money float64, pwd string) {
//看下输入的密码是否正确
if pwd != account.pwd {
fmt.Println("你输入的密码不正确")
return
}
//看看存款金额是否正确
if money <= 0 {
fmt.Println("你输入的金额不正确")
return
}
account.balance += money
fmt.Println("存款成功~")
}
//取款
func (account *account) WithDraw(money float64, pwd string) {
//看下输入的密码是否正确
if pwd != account.pwd {
fmt.Println("你输入的密码不正确")
return
}
//看看存款金额是否正确
if money <= 0 || money > account.balance {
fmt.Println("你输入的金额不正确")
return
}
account.balance -= money
fmt.Println("取款成功~")
}
//查询余额
func (account *account) Query(pwd string) {
//看下输入的密码是否正确
if pwd != account.pwd {
fmt.Println("你输入的密码不正确")
return
}
fmt.Printf("你的账号为 = %v 余额 = %v \n", account.accountNo, account.balance)
}
main.go
package main
import (
"2020-04-04/model"
"fmt"
)
func main() {
//创建一个account变量
account := model.NewAccount("zisefeizhu","000",40)
fmt.Println(account)
fmt.Println(account.GetAccountNo())
}
//&{zisefeizhu 000 40}
//zisefeizhu
类似改法
面向对象编程三大特性-继承
看一个问题,引出继承的必要性
看一个学生考试系统的程序extend01.go,提出代码复用的问题
//编写一个学生考试系统
//小学生
type Pupil struct {
Name string
Age int
Score int
}
//显示他的成绩
func (p *Pupil) ShowInfo() {
fmt.Printf("学生名 = %v 年龄 = %v 成绩 = %v\n", p.Name, p.Age, p.Score)
}
func (p *Pupil) SetScore(score int) {
//业务判断
p.Score = score
}
func (p *Pupil) testing() {
fmt.Println("小学生正在考试中...")
}
//大学生,研究生...
//大学生
type Graduate struct {
Name string
Age int
Score int
}
//显示他的成绩
func (p *Graduate) ShowInfo() {
fmt.Printf("学生名 = %v 年龄 = %v 成绩 = %v\n", p.Name, p.Age, p.Score)
}
func (p *Graduate) SetScore(score int) {
//业务判断
p.Score = score
}
func (p *Graduate) testing() {
fmt.Println("大学生正在考试中...")
}
//代码冗余... 研究生
//代码冗余... 高中生
func main() {
//测试
var pupil = &Pupil{
Name: "tom",
Age: 10,
}
pupil.testing()
pupil.SetScore(90)
pupil.ShowInfo()
//测试
var graduate = &Graduate{
Name: "tom",
Age: 20,
}
graduate.testing()
graduate.SetScore(90)
graduate.ShowInfo()
}
//输出:小学生正在考试中...
//学生名 = tom 年龄 = 10 成绩 = 90
//大学生正在考试中...
//学生名 = tom 年龄 = 20 成绩 = 90
对上面代码的小结
1)Pupil和Graduate两个结构体的字段和方法几乎一样,但是我们却写了相同的代码,代码复用性不强
2)出现代码冗余,而且代码不利于维护,同时也不利于功能的扩展
3)解决方法 - 通过继承方式来解决
继承基本介绍和示意图
继承可以解决代码复用,让编程更加靠近人类思维
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体(比如刚才的Student),在该结构体中定义这些相同的属性和方法。
其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个Student匿名结构体即可
在Go中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性
嵌套匿名结构体的基本语法
type Goods struct {
Name string
Price int
}
type Book struct {
Goods //这里就是嵌套匿名结构体Goods
Writer string
}
快速入门案例
对extends01.go改进,使用嵌套匿名结构体的方式来实现继承特性,体会继承的好处
//编写一个学生考试系统
//小学生
type Student struct {
Name string
Age int
Score int
}
//将Pupil 和 Graduate 共有的方法也绑定到 *Student
func (stu *Student) ShowInfo() {
fmt.Printf("学生名 = %v 年龄 = %v 成绩 = %v\n", stu.Name, stu.Age, stu.Score)
}
func (stu *Student) SetScore(score int) {
//业务判断
stu.Score = score
}
//小学生
type Pupil struct {
Student //嵌入了Student匿名结构体
}
//显示他的成绩
//这时Pupil结构体特有的方法,保留
func (p *Pupil) testing() {
fmt.Println("小学生正在考试中...")
}
//大学生,研究生...
//大学生
type Graduate struct {
Student //嵌入了Student匿名结构体
}
//显示他的成绩
//这时Graduate结构体特有的方法,保留
func (p *Graduate) testing() {
fmt.Println("大学生正在考试中...")
}
//代码冗余... 研究生
//代码冗余... 高中生
func main() {
//测试
//当我们对结构体嵌入了匿名结构体使用方法会发生变化
pupil := &Pupil{}
pupil.Student.Name = "tom"
pupil.Student.Age = 8
pupil.testing()
pupil.SetScore(70)
pupil.ShowInfo()
//测试
graduate := &Graduate{}
graduate.Student.Name = "marry"
graduate.Student.Age = 28
graduate.testing()
graduate.SetScore(90)
graduate.ShowInfo()
}
//输出:小学生正在考试中...
//学生名 = tom 年龄 = 8 成绩 = 70
//大学生正在考试中...
//学生名 = marry 年龄 = 28 成绩 = 90
继承给编程带来的便利
-
代码的复用性提高了
-
代码的扩展性和维护性提高了
继承的深入讨论
1)结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用
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
}
func main() {
var b B
b.A.Name = "zisefeizhu"
b.A.age = 19
b.A.SayOk()
b.A.Hello()
}
//输出:A SayOk zisefeizhu
//A Hello zisefeizhu
-
匿名结构体字段访问可以简化
对上面的代码小结
(1)当我们直接通过b访问字段或方法时,其执行流程如下比如b.Name
(2)编译器会先看b对应的类型有没有Name,如果有,则直接调用B类型的Name字段
(3)如果没有就去看B中嵌入的匿名结构体A有没有声明Name字段,如果有就调用,如果没有继续查找...如果都找不到就报错
3) 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
-
结构体嵌入两个(或多个)匿名结构体,如果两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明指定匿名结构体名字,否则编译报错
-
如果一个struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
6 ) 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
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() {
tv := TV{ Goods{"电视机001", 5000.99},Brand{"海尔","山东"},}
tv2 := TV{
Goods{
Price: 5000.99,
Name: "电视机002",
},
Brand{
Name: "夏普",
Address: "北京",
},
}
fmt.Println("tv", tv)
fmt.Println("tv2", tv2)
tv3 := TV2{ &Goods{"电视机003", 7000.99},&Brand{"创维","河南"},}
tv4 := TV2{
&Goods{
Name: "电视机004",
Price: 9000.99,
},
&Brand{
Name: "长虹",
Address: "四川",
},
}
fmt.Println("tv3", *tv3.Goods, *tv3.Brand)
fmt.Println("tv4", *tv4.Goods, *tv4.Brand)
}
//tv {{电视机001 5000.99} {海尔 山东}}
//tv2 {{电视机002 5000.99} {夏普 北京}}
//tv3 {电视机003 7000.99} {创维 河南}
//tv4 {电视机004 9000.99} {长虹 四川}
课堂练习
结构体的匿名字段是基本数据类型,如何访问?
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)
}
//输出:e = {{狐狸精 300} 20 40}
说明
1)如果一个结构体有int类型的匿名字段,就不能有第二个
2)如果需要有多个int的字段,则必须给int字段指定名字
面向对象编程-多重继承
多重继承说明
如一个struct嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。
案例演示
通过一个案例来说明多重继承使用
多重继承细节
- 若嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分
2)为了保证代码的简洁性,建议尽量不使用多重继承
接口
Go中多态特性主要是通过接口来体现的
usb插槽就是现实中的接口你可以把手机,相机,u盘都插在usb插槽上,而不用担心那个插槽是专门插哪个的,原因是做usb插槽的厂家和做各种设备的厂家都遵守了统一的规定包括尺寸,排线等等。
这样的设计需求在Go编程中也是会大量存在的,一个程序就是一个世界,在现实世界存在的情况,在程序中也会出现。用程序来模拟一个前面的应用场景
//声明/定义一个接口
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变量会根据传入的实参,来判断到底是Phone,还是Camera
//通过usb接口变量来调用Start和Stop方法
usb.Start()
usb.Stop()
}
func main() {
//测试
//先创建结构体变量
computer := Computer{}
phone := Phone{}
camera := Camera{}
//关键点
computer.Working(phone)
computer.Working(camera)
}
interface类型可以定义一组方法,但是这些不需要实现。并且interface不能包含任何变量。到某个自定义类型(比如结构体Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)
基本语法
小结说明:
-
接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想
-
Go中的接口,不需要显示的实现。只要一个变量,含有接口类型中的所有方法,那么变量就实现了这个接口,因此,Go中没有implement这样的关键字
接口使用的应用场景
-
中国要制造的轰炸机,专家只需要把飞机需要的功能/规格定下来即可,然后让别的人具体实现即可
-
现在有一个项目经理,管理三个程序员,开发一个软件,为了控制和管理软件,项目经理可以定义一些接口,然后由程序员具体实现
......
注意事项和细节
接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)
type AInterface interface {
say()
}
type stu struct {
Name string
}
func (stu stu) say() {
fmt.Println("stu say()")
}
func main() {
var stu stu //结构体变量,实现了say() 实现了AInterface
var a AInterface = stu
a.say()
}
接口中所有的方法都没有方法体,即都是没有实现的方法
在Go中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口
一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型
type AInterface interface {
say()
}
type stu struct {
Name string
}
type integer int
func (i integer) say() {
fmt.Println("integer say i =", i)
}
func (stu stu) say() {
fmt.Println("stu say()")
}
func main() {
var i integer = 10
var b AInterface = i
b.say()
var stu stu //结构体变量,实现了say() 实现了AInterface
var a AInterface = stu
a.say()
}
//输出:integer say i = 10
//stu say()
一个自定义类型可以实现多个接口
type AInterface interface {
say()
}
type BInterface interface {
Hello()
}
type Monster struct {
}
func (m Monster) Hello() {
fmt.Println("Monster Hello()")
}
func (m Monster) say() {
fmt.Println("Monster say ")
}
func main() {
//Monster实现了AInterface 和BInterface
var monster Monster
var a2 AInterface = monster
var b2 BInterface = monster
a2.say()
b2.Hello()
}
//输出:Monster say
//Monster Hello()
Go接口中不能有任何变量
一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必须将B,C接口的方法也全部实现
package main
type BInterface interface {
test01()
}
type CInterface interface {
test02()
}
type AInterface interface {
BInterface
CInterface
test03()
}
//如果需要实现AInterface,就需要将BInterface CInterface的方法都实现
type Stu struct {
}
func (stu Stu) test01() {
}
func (stu Stu) test02() {
}
func (stu Stu) test03() {
}
func main() {
var stu Stu
var a AInterface = stu
a.test01()
}
interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil
空接口interface{}没有任何方法,所以所有类型都实现了空接口,即可以把任何一个变量赋给空接口
type BInterface interface {
test01()
}
type CInterface interface {
test02()
}
type AInterface interface {
BInterface
CInterface
test03()
}
//如果需要实现AInterface,就需要将BInterface CInterface的方法都实现
type Stu struct {
}
func (stu Stu) test01() {
}
func (stu Stu) test02() {
}
func (stu Stu) test03() {
}
type T interface {
//空接口
}
func main() {
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)
var a AInterface = stu
a.test01()
}
//输出:{}
//8.8 8.8
接口编程最佳实践
import (
"fmt"
"math/rand"
"sort"
)
//1.声明Hero结构体
type Hero struct {
Name string
Age int
}
//2.声明一个Hero结构体切片类型
type HeroSlice []Hero
//3.实现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) {
//交换
hs[i], hs[j] = hs[j], hs[i]
}
//1.声明Student结构体
type Student struct {
Name string
Age int
Score float64
}
//将Student的切片,按Score从大到小排序!
func main() {
//先定义一个数组/切片
var intSlice = []int{0, -1, 10, 7, 90}
//要求对intSlice切片进行排序
//1. 冒泡排序...
//2. 也可以使用系统提供的方法
sort.Ints(intSlice)
fmt.Println(intSlice)
//对结构体切片进行排序
//1. 冒泡排序...
//2. 也可以使用系统提供的方法
//测试看看我们是否可以对结构体切片进行排序
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)
}
i := 10
j := 20
i,j = j,i
fmt.Println("i = ", i, "j = ", j)
}
//[-1 0 7 10 90]
//{英雄 81 87}
//{英雄 47 59}
//{英雄 81 18}
//{英雄 25 40}
//{英雄 56 0}
//{英雄 94 11}
//{英雄 62 89}
//{英雄 28 74}
//{英雄 11 45}
//{英雄 37 6}
//______________排序后______________
//{英雄 56 0}
//{英雄 37 6}
//{英雄 94 11}
//{英雄 81 18}
//{英雄 25 40}
//{英雄 11 45}
//{英雄 47 59}
//{英雄 28 74}
//{英雄 81 87}
//{英雄 62 89}
//i = 20 j = 10
接口练习题
实现接口 vs 继承
package main
import "fmt"
//Monkey结构体
type Monkey struct {
Name string
}
//声明接口
type BirdAble interface {
Flying()
}
type FishAble interface {
Swimming()
}
func (this *Monkey) climbing() {
fmt.Println(this.Name,"生来会爬树..")
}
//LittleMonkey 结构体
type LittleMonkey struct {
Monkey //继承
}
//让LittleMonkey 实现BirdAble
func (this *LittleMonkey) Flying() {
fmt.Println(this.Name,"通过学习,会飞翔...")
}
//让LittleMonkey 实现FishAble
func (this *LittleMonkey) Swimming() {
fmt.Println(this.Name,"通过学习,会游泳...")
}
func main() {
//创建一个LittleMonkey实例
monkey := LittleMonkey{
Monkey{
Name: "悟空",
},
}
monkey.climbing()
monkey.Flying()
monkey.Swimming()
}
//输出:悟空 生来会爬树..
//悟空 通过学习,会飞翔...
//悟空 通过学习,会游泳...
对上面代码的小结
当A结构体继承了B结构体,那么A结构就自动的继承了B结构体的字段和方法,并且可以直接使用
当A结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此可以认为:实现接口是对继承机制的补充
实现接口可以看作是对继承的一种补充
接口和继承解决的问题不同
继承的价值主要在于:解决代码的*复用性*和*可维护性*
接口的价值主要在于:*设计*,设计好各种规范(方法),让其它自定义类型去实现这些方法
接口比继承更加灵活 Person Student BirdAble LittleMonkey
接口比继承更加灵活,继承是满足is - a的关系,而接口只需满足like - a的关系
接口在一定程度上实现*代码*解耦
面向对象编程三大特性 - 多态
变量(实例)具有多种形态。面向对象的第三大特性,在Go语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的变量实现。这时接口变量就呈现不同的形态
快速入门案例
在前面的Usb接口案例中,Usb usb 即可以接收手机变量,又可以接收相机变量,就体现了Usb接口多态特性。
//声明/定义一个接口
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变量会根据传入的实参,来判断到底是Phone,还是Camera //usb接口变量就体现出多态的特点
//通过usb接口变量来调用Start和Stop方法
usb.Start()
usb.Stop()
}
func main() {
//测试
//先创建结构体变量
computer := Computer{}
phone := Phone{}
camera := Camera{}
//关键点
computer.Working(phone)
computer.Working(camera)
}
接口体现多态的两种形式
多态参数
在前面的Usb接口案例中,Usb usb 即可以接收手机变量,又可以接收相机变量,就体现了Usb接口多态特性。
多态数组
演示一个案例:在Usb数组中,存放Phone结构体和Camera结构体变量
//声明/定义一个接口
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 {
}
func main() {
//定义一个Usb接口数组,可以存放Phone和Camera的结构体变量
//这里体现出多态数组
var usbArr [3]Usb
usbArr[0] = Phone{"vivo"}
usbArr[1] = Phone{"华为"}
usbArr[2] = Phone{"小米"}
fmt.Println(usbArr)
}
类型断言
类型断言:由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言
案例演示
func main() {
//类型断言的其它案例
var x interface{}
var b2 float32 = 2.2
x = b2 //空接口,可以接收任意类型
//x => float32 [使用类型那个断言]
y := x.(float32) //转成具体类型
fmt.Printf("y 的类型是 %T 值是 = %v", y, y)
}
//输出:y 的类型是 float32 值是 = 2.2
对上面代码的说明:
在进行类型断言时,如果类型不匹配,就会报panic,因此进行类型断言时, 要确保原来的空接口指向的就是断言的类型
如何在进行断言时,带上检测机制,如果成功就OK,否则也不要报panic
func main() {
//类型断言的其它案例
var x interface{}
var b2 float32 = 3.3
x = b2 //空接口,可以接收任意类型
//x => float32 [使用类型那个断言]
if y, ok := x.(float32); ok {
fmt.Println("convert success")
fmt.Printf("y 的类型是%T 值是%v", y, y)
} else {
fmt.Println("convert fail")
}
fmt.Println("继续执行...")
//y := x.(float32) //转成具体类型
//fmt.Printf("y 的类型是 %T 值是 = %v", y, y)
}
//输出:y 的类型是float32 值是3.3继续执行...
类型断言的最佳实践
在前面的Usb接口案例做改进:
给Phone结构体增加一个特有的方法call(),当Usb接口接收的是Phone变量时,还需要调用call方法
package main
import "fmt"
//声明/定义一个接口
type Usb interface {
//声明了两个没有实现的方法
Start()
Stop()
}
type Phone struct {
name string
}
//让Phone实现Usb接口的方法
func (p Phone) Start() {
fmt.Println(p.name, "手机开始工作...")
}
func (p Phone) Stop() {
fmt.Println(p.name, "手机停止工作...")
}
type Camera struct {
name string
}
//让Camera实现Usb接口的方法
func (c Camera) Start() {
fmt.Println(c.name, "相机开始工作...")
}
func (c Camera) Stop() {
fmt.Println(c.name, "相机停止工作...")
}
//计算机
type Computer struct {
}
//编写一个方法Working方法,接收一个Usb接口类型变量
//只要是实现了Usb接口(所谓实现Usb接口,就是指实现了Usb接口声明所有方法)
func (c Computer) Working(usb Usb) { //usb变量会根据传入的实参,来判断到底是Phone,还是Camera //usb接口变量就体现出多态的特点
//通过usb接口变量来调用Start和Stop方法
usb.Start()
usb.Stop()
}
func main() {
//测试
computer := Computer{}
var usbArr [3]Usb
usbArr[0] = Phone{"华为"}
usbArr[1] = Phone{"vivo"}
usbArr[2] = Camera{"华为"}
fmt.Println(usbArr)
for i := 0; i < len(usbArr); i++ {
computer.Working(usbArr[i])
}
}
//[{华为} {vivo} {华为}]
//华为 手机开始工作...
//华为 手机停止工作...
//vivo 手机开始工作...
//vivo 手机停止工作...
//华为 相机开始工作...
//华为 相机停止工作...
写一函数,循环判断传入参数的类型:
/编写一个函数,可以判断输入的参数是什么类型
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 类型,值是%v\n", index, x)
case int, int32, int64:
fmt.Printf("第%v个参数是 整数 类型,值是%v\n", index, x)
case string:
fmt.Printf("第%v个参数是 string 类型,值是%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
TypeJudge(n1, n2, n3, name, address, n4)
}
//第0个参数是 float32 类型,值是1.1
//第1个参数是 float64 类型,值是2.3
//第2个参数是 整数 类型,值是30
//第3个参数是 string 类型,值是tom
//第4个参数是 string 类型,值是北京
//第5个参数是 整数 类型,值是300
在前面代码的基础上,增加判断Student类型和*Student类型
package main
import (
"fmt"
)
type Student struct {
name string
}
//编写一个函数,可以判断输入的参数是什么类型
func TypeJudge(items... interface{}) {
for index, x := range items {
switch x.(type) {
case Student:
fmt.Printf("第%v个参数是 Student 类型,值是%v\n", index, x)
case *Student:
fmt.Printf("第%v个参数是 *Student 类型,值是%v\n", index, x)
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 类型,值是%v\n", index, x)
case int, int32, int64:
fmt.Printf("第%v个参数是 整数 类型,值是%v\n", index, x)
case string:
fmt.Printf("第%v个参数是 string 类型,值是%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
var b bool = false
stu1 := Student{"zise"}
stu2 := &Student{"feizhzu"}
TypeJudge(n1, n2, n3, name, address, n4, b, stu1, stu2)
}
//第0个参数是 float32 类型,值是1.1
//第1个参数是 float64 类型,值是2.3
//第2个参数是 整数 类型,值是30
//第3个参数是 string 类型,值是tom
//第4个参数是 string 类型,值是北京
//第5个参数是 整数 类型,值是300
//第6个参数是 bool 类型,值是false
//第7个参数是 Student 类型,值是{zise}
//第8个参数是 *Student 类型,值是&{feizhzu}