面向对象
4.1 面向对象介绍
我们都知道,世间万物都是其属性和功能的集合。
我们人能够描述某种事物,那么计算机也得具备这样的功能。
面向对象说的及其简单和通俗的一点就是:用程序来描述事物的属性和功能
4.2结构体(struct)
我们人能够描述某种事物,那么计算机也得具备这样的功能。
这就引出了我们下面的知识点,结构体
4.2.1认识结构体
1、什么是结构体
结构体是一堆字段的集合,我可以存任意类型的数据,可以是数字、字符串、数组、也可以是另一个结构体。
思考:数组、切片、映射也都可以存一堆数据,为什么还要有结构体?
2、为什么要有结构
人能够描述世间万物,为了让计算机像人一样去工作,同样也需要某种机制来描述世间万物。
4.2.2结构体的定义
1、结构体的格式
struct {
字段1 类型
字段2 字段3 类型
...
}
2、声明一个结构体类型
我们使用type关键字来声明一个结构体类型
记住,记住,我们声明的只是一个类型!和int,string,slice一样,只是类型
type student struct{
name string
sex string
id int
score int
}
###4.2.3 实例化结构体
我们声明一个可以描述学生这个事物的数据结构,那么我们要描述某一个具体学生的时候,是不是得给字段赋值啊?这个赋值的操作叫做实例化:
type student struct{
name string
sex string
id int
score int
}
func main(){
var xiaoming = student {
name:"小明",
sex:"男",
id:01,
score:90,
}
fmt.Println(xiaoming)
}
//{小明 男 1 90}
如果我们实例化的时候不给字段赋值,那么每个字段就会使用初始值:
type student struct{
name string
sex string
id int
score int
}
func main(){
var xiaoming = student {} //var 变量名 = 类型{值}
//var xiaoming student //var 变量名 类型
fmt.Println(xiaoming)
}
//{ 0 0}
###4.2.4 访问实例字段
存不是目的,目的是取,那我们怎么取呢?我们通过实例.字段名
去访问字段:
type student struct{
name string
sex string
id int
score int
}
func main(){
var xiaoming student
xiaoming.name = "小明"
xiaoming.sex = "男"
xiaoming.id = 1
xiaoming.score = 90
fmt.Println(xiaoming.name)
fmt.Println(xiaoming)
}
//小明
//{小明 男 1 90}
4.2.5实例指针
我们把struct实例传给函数的时候,是值传递还是引用传递啊?试试憋
type student struct{
name string
}
func test(aa student) {
aa.name = "试试就试试"
}
func main(){
var xiaoming = student{
name:"小明",
}
test(xiaoming)
fmt.Println(xiaoming)
}
//{小明}
很明显,我的值没改掉,这货是值传递,传给函数的是个副本。
那指针就少不了了!
创建指针就那几种格式:
var aaa = &变量
var bbb = &类型{值}
var ccc = new(类型)
超级简单有没有?!市面上的很多书都哔哔哒哒一大堆,其实真没啥?
我就拿数组和结构体来演示给你们看看有多容易:
//数组的例子:
var myArry = [2]int{1,2}
var pointer1 = &myArry //var aaa = &变量
var pointer2 = &[2]int{1,2} //var bbb = &类型{值}
var pointer3 = new([2]int) //var ccc = new(类型)
//结构体实例的例子
type student struct{
name string
}
var xiaoming = student{name:"小明"}
var pointer1 = &xiaoming //var aaa = &变量
var pointer2 = &student{name:"小明"} //var bbb = &类型{值}
var pointer3 = new(student) //var ccc = new(类型)
通过指针访问实例字段
实例指针我们直接通过实例指针.字段名
就可以访问其字段
4.2.6匿名结构体
匿名结构体顾名思义,就是没有名字的结构体。
形如下方代码的,叫做匿名结构体:
struct{
字段1 类型
字段2 类型
...
}
实例化匿名结构体:
var test1 = struct{
name string
id int
}{
name:"小明",
id : 01,
}
//当然我们也可以不赋值,使用初始值
var test2 struct{
name string
id int
}
//结果:{ 0}
匿名结构体正常多用来存储不同类型的数据,很少用来去描述一个事物
4.2.7 嵌套结构体
什么是嵌套结构体
很好理解啊,就是一个结构体套着另一个结构啊
为什么要有嵌套结构体
我可以用结构体来描述世间万物。
世间万物显然也是存在着嵌套关系的,比如果,我们用结构体描述了班级这个事物,班级这个事物里又有着老师和学生这两个事物。为了能够描述这种包含和被包含的关系,就需要嵌套结构体的存在了。
怎么用嵌套结构体
//首先,我需要三个结构体,分别是班级、老师、学生
type classes struct{
className string
}
type teachers struct{
teacherName string
}
type students struct{
structName string
}
那我们是把classes放在teachers、students里,还是把teachers、students放在classes里啊?
显然的,把teachers、students放在classes里这种做法是不合理的,如果这样,我们每次实例化classes的时候,就必须要实例化一个老师和学生,难道一个班级就只有一个老师和一个学生吗?
所以,应该是把classes放在teachers、students里:
type classes struct{
className string
}
type teachers struct{
teacherName string
class classes
}
type students struct{
studentName string
class classes
}
func main(){
class1 := classes{
className:"一年级一班",
}
teacher1 := teachers{
teacherName:"mac",
class:class1,
}
student1 := students{
studentName:"小明",
class:class1,
}
fmt.Println(teacher1)
fmt.Println(student1)
}
//{mac {一年级一班}}
//{小明 {一年级一班}}
4.2.8 匿名字段
匿名字段很好理解,就是没有名字的字段
我们看看怎么用:
type students struct{
studentName string
int //只写类型,不写字段名
}
func main(){
student1 := students{
studentName:"小明",
int:90, //赋值的时候,字段名就是类型名
}
fmt.Println(student1.int) //访问的时候,直接 .类型名 访问
}
//结果:90
可是!!!
你这么写,你同事知道你的int表示的啥嘛?时间久了,你知道自己的int表示的啥嘛?这么写很快就会被同事打死!
匿名字段适用场景
匿名字段多用于嵌套关系,它的优点,我们往下看:
type classes struct{
className string
}
type teachers struct{
teacherName string
classes //只写了类型
}
func main(){
class1 := classes{
className:"一年级一班",
}
teacher1 := teachers{
teacherName:"mac",
classes:class1,
}
//关键的地方来了
fmt.Println(teacher1.className)
//结果:"一年级一班"
}
//如果我只写类型名,那么我访问元素的时候,可以直接访问到该类型下的属性,就像访问自己的属性般,毫无违和感
##4.3方法
我们一开始就说了,世间万物都是其属性和功能的集合。我们之前说的结构体只能描述这类事物的属性,但却不能描述事物的功能。
聪明的童鞋就要反驳我了!what a fuck!老师,你说错了,我完全可以给字段指定函数类型啊,然后".字段()"运行啊!
这位同学,你说的没错,可以是可以,我们来看看有多繁琐!
type test struct {
name string
fn1 func(string)
fn2 func(string)
}
func testfn1(s string){
fmt.Println("我是",s)
}
func testfn2(s string){
fmt.Println("我是",s,"哈哈哈")
}
func main(){
c := test{
name:"小明",
fn1:testfn1, //每次实例化的时候,我都要去指定功能
fn2:testfn2,
}
c.fn1(c.name) //每次调用的时候,我都要去指定参数
c.fn2(c.name)
}
//我是 小明
//我是 小明 哈哈哈
就说烦不烦、方不方便!(除非你睁着眼睛说瞎话)
既然很烦,我们就来学习方法
4.3.1 方法的定义
方法其实就是函数,只是这个函数是绑定给指定类型的,只有该类型的数据才能调用
格式如下:
func (参数 指定类型) 函数名 (参数及类型)返回值类型{
代码体....
}
具体使用如下:
type students struct {
name string //存学生姓名
score []int //存学生成绩的切片
}
//定义一个方法,用来给学生添加成绩
func (student students) appendScore (score int) {
student.score = append(student.score,score)
}
func main(){
student1 := students{
name:"小明", //实例化一个学生,score切片为默认值
}
student1.appendScore(90) //调用绑定方法,传入成绩
student1.appendScore(100)
fmt.Println(student1) //{小明 []}
}
**发现什么问题没有?**我添加的成绩并没有如期出现?
为什么?很显然,传给接收器的是个副本,是值传递!
那怎么办啊?
1、我给方法设定一个返回值,然后用student1去接收。但是!这样的方法显然不好,涉及了多次值的拷贝
2、我传递指针,直接函数内部修改。这个方法显然是最好的
接收器接收指针:
type students struct {
name string //存学生姓名
score []int //存学生成绩的切片
}
//定义一个方法,用来给学生添加成绩
func (student *students) appendScore (score int) { //接收器接收指针类型
student.score = append(student.score,score)
}
func main(){
student1 := students{
name:"小明", //实例化一个学生,score切片为默认值
}
student1.appendScore(90) //调用绑定方法,传入成绩
student1.appendScore(100)
fmt.Println(student1) //{小明 [90 100]}
}
补充:
方法是一种绑定函数的机制,当然不只是可以绑定给struct类型,只要是自定义类型都可以绑定:
type myint int //自定义一个名字为myint的int类型
func (x myint) test(){
fmt.Println("我的值是",x)
}
func main(){
a := myint(666)
a.test() //我的值是 666
}
4.4 接口
###4.4.1 接口基础
我们来思考一个问题。
我有3个类型的数据,这3个类型的数据呢我想用同一个函数去处理?怎么玩?
问题是,我一个函数只能传一个类型,其它类型传不进去!
类比生活中的例子:
1、我不可能手机专门差手机的地方,键盘鼠标专门插在键盘鼠标的地方(这样的话电脑上得开多少洞啊!)
2、这个时候电脑就提供了一个叫做usb的东西,usb呢就相当于手机鼠标键盘和电脑签订的协议。
usb说"我不负责操作数据,但我不管你是谁,你把数据传给我,我来统一格式再传给电脑"
3、这里的usb就是电脑和手机鼠标键盘之间的一个接口
go语言里,也有像usb这样不负责数据操作,只管统一格式的接口(interface)
接口(interface)核心知识点:
1、任何类型的数据都可以传给interface(得符合interface的要求)
2、interface统一了功能调用方式
3、interface不管功能的实现
怎么去理解这3点,别急,我们一个一个的用例子去看:
1、任何类型的数据都可以传给interface:
//首先,我们定义一个接口类型
type myinterface interface{} //先不纠结接口里写啥
//我们来定义3个类型的数据
type mystruct struct{
name string
}
type myint int //定义一个名叫myint的int类型
type myarry [3]int //定义一个名叫myarry的数组类型
func myfunc(x myinterface){
fmt.Println("我被传进来了") //定义一个函数来接受接口
}
func main(){
var testinfetface myinterface //声明一个接口
struct1 := mystruct{ //声明一个结构体
name:"mac",
}
int1 := myint(10) //声明一个整数
arry1 := myarry{1,2,3} //声明一个数组
testinfetface = struct1 //把struct1传给接口
myfunc(testinfetface)
testinfetface = int1 //把int1传给接口
myfunc(testinfetface)
testinfetface = arry1 //把arry1传给接口
myfunc(testinfetface)
}
//结果:
//我被传进来了
//我被传进来了
//我被传进来了
2、interface统一了功能调用方式
这两点我们放在一块说:
//定义一个接口类型
type myinterface interface{
test1()
test2() string
}
这里定义了一个接口,接口里只写了函数,不写函数体代码。
那怎么就叫统一功能调用方式了呢?
type myinterface interface{
test1()
test2() string
}
type mystruct struct{
name string
}
func main(){
var interface1 myinterface
struct1 := mystruct{
name:"mac",
}
interface1 = struct1
interface1.test1() //接口的调用方式是一样的,不管你传进来的是谁?我调用方式是一样的
interface2.test2()
}
//这里会报错,因为我函数体代码还没写
统一调用方式的意思是:我接口不管你传进来的是谁,调用方式都是一样的
3、interface不管功能的实现
接口只管统一功能调用方式,不管函数怎么实现的
那函数实现交给谁啊?
a、谁要传进来就由谁来实现。
b、实现了接口里所有函数的,叫做实现了这个接口
那怎么实现啊?绑定方法啊!
type myinterface interface{
test1()
test2() string
}
type mystruct struct{
name string
}
func (x mystruct) test1(){
fmt.Println("我是test1",x.name)
}
func (x mystruct) test2() string {
msg := "我是test2 "+x.name
return msg
}
func main(){
struct1 := mystruct{
name:"mac",
}
var interface1 myinterface
interface1 = struct1
interface1.test1()
fmt.Println(interface1.test2())
}
//结果:
//我是test1 mac
//我是test2 mac
接口最核心的知识点我们这就讲完了,很简单对不对。
下面我们来说说接口的其它知识点。
###4.4.2 接口断言
我们已经知道了接口的核心是接收任意类型、统一调用方式。
现在由这么个需求,假设我有一个接口,可以接收结构体类型a和字符串类型b,但我现在需要根据不用的类型写不同的代码。这怎么处理?很明显的,这里需要一个类型判断
而断言,就是用来处理接口类型判断的
var v,ok := 具体接口.(实现了接口的类型a)
//ok得到的是bool值,用来判断传入接口的类型是不是类型a
//如果ok为true,则v就会得到该类型的值,否则就是类型a的初始值
type myinterface interface{
test1()
}
type mystruct struct {
name string
}
func (x mystruct) test1() {
fmt.Println("我是mystruct的test1")
}
type mystring string
func (x mystring) test1() {
fmt.Println("我是mystring的test1")
}
func main(){
struct1 := mystruct{
name:"mac",
}
var interface1 myinterface
interface1 = struct1
v1,ok1 := interface1.(mystring) //判断传给接口的是不是mystring类型
fmt.Println(v1,ok1) //结果:"" false
v2,ok2 := interface1.(mystruct) //判断传给接口的是不是mystruct类型
fmt.Println(v2,ok2) //结果:{mac} true
}
为了简化连续的类型判断,go提供了type switch功能
4.4.3 type switch
type switch和switch的用法基本上一致,很简单:
switch t := 具体接口.(type) { //具体接口.(type)只能用在switch判断中
case mystring:
代码
case mystruct:
代码
...
default:
代码
}
4.4.4 接口的嵌套
很简单,我们直接看代码:
type mine1 interface {
test1()
test2()
}
type mine2 interface {
mine1 //程序会自动扩展
test3()
}
//mine2就相当于:
//type mine2 interface {
// test1()
// test2()
// test3()
//}
##4.5反射
在说反射这个概念之前,我先来说说动态语言和静态语言。
动态语言地特点:数据结构是在程序运行地时候才会确定,运行过程当中可以对它进行修改,比如说python中我可以给实例化对象增加字段。
静态语言地特点:数据结构在运行前就会确定,比如说go里地struct,我规定了几个字段就是几个字段,运行过程当中我是没法增加字段的。
就灵活性上而言,动态语言是大于静态语言的。你定义变量的时候,必须规定死它的数据类型,运行时类型已经确定,无法修改。go语言就是一种静态语言,但为了弥补灵活性上的不足,就提供了反射机制。允许你通过反射去查看和修改其类型信息
4.5.1 reflect.TypeOf()
我们之前一直在用reflect.Typeof()来查看类型,但事实上它的功能远远不止这么简单。
我们使用reflect.Typeof(变量)得到的是它的反射类型对象:
type mystruct struct{
name string
}
func mian(){
struct1 := mystruct{
name:"mac",
}
a := reflect.Typeof(struct1)
fmt.Ptintf("%T",a) //*reflect.rtype
}
它首先是个指针类型,指针意味着什么?是不是意味着我对它的修改是修改的它的本身,而不是副本啊!
是个毛线啊,虽然它是指针,但是不允许你修改,它只是通过内存地址去访问类型信息
接着它又是个对象,对象意味着什么?是不是它封装了很多方法可以供我使用啊!
我们下面来看看它有哪些方法可以使用:
1、.Name()/.Kind()
type mystruct struct{
name string
}
func main(){
struct1 := mystruct{
name:"mac",
}
a := reflect.TypeOf(struct1)
fmt.Println(a.Name()) //mystruct
fmt.Println(a.Kind()) //struct
}
.Name( ) 得到的是类型名称
.Kind( ) 得到的是类型种类
2、.Elem()
如果我传入的是个指针,那么我获得的类型名称和类型种类如下:
type mystruct struct{
name string
}
func main(){
struct1 := mystruct{
name:"mac",
}
a := reflect.TypeOf(&struct1)
fmt.Println(a.Name()) //""
fmt.Println(a.Kind()) //Ptr
}
这个时候我可以通过.Elem()
来获取指针指向的元素:
func main(){
struct1 := mystruct{
name:"mac",
}
a := reflect.TypeOf(&struct1)
//a.Elem()相当于 reflect.TypeOf(struct1) 或者 reflect.TypeOf(*&struct1)
fmt.Println(a.Elem().Name()) //mystruct
fmt.Println(a.Elem().Kind()) //struct
}
下面几点是针对结构体的操作
3、.Field()
我们通过.Field(索引)
可以拿到结构体中指定字段的详细信息,它的结果是个结构体。
我们看看源码里拿到的这个结构体里有些啥:
name是字段名;pkgpath是所属包名;type是字段类型;tag是标签;index是索引;Anonymous是判断字段是否是匿名字段。
type mystruct struct{
name string
}
func main(){
struct1 := mystruct{
name:"mac",
}
a := reflect.TypeOf(struct1)
afield_1 := a.Field(0)
fmt.Println(afield_1) //{{name main string 0 [0] false}}
fmt.Println(afield_1.Name) //name
fmt.Println(afield_1.PkgPath) //main
fmt.Println(afield_1.Type) //string
fmt.Println(afield_1.Tag) //""
fmt.Println(afield_1.Index) //[0]
fmt.Println(afield_1.Anonymous) //false
}
我们重点看一下tag,又叫标签,是用来描述字段的。
如何描述字段:
type mystruct struct{
name string `我是描述信息`
}
........
fmt.Println(afield_1.Tag) //我是描述信息
但正常我们不这么用,我们通常会给描述信息建立字段
type mystruct struct{
name string `key1:"value1" key2:"value2"`
}
........
fmt.Println(afield_1.Tag) //key1:"value1" key2:"value2"
fmt.Println(afield_1.Tag.Get("key1")) //value1
fmt.Println(afield_1.Tag.Get("key2")) //value2
4、NumField()**
NumFild()
可以得到结构体字段数
type mystruct struct{
name string
id int
sex string
address string
}
........
struct1 := mystruct{}
a := reflect.TypeOf(struct1)
fmt.Println(a.NumField()) // 4
5、FieldByName()
根据给定字符串去找字段,返回字段信息的结构体。
type mystruct struct{
name string
}
........
struct1 := mystruct{}
a := reflect.TypeOf(struct1)
fieldmsg1,exists1 := a.FieldByName("name")
fmt.Println(exists1) //true
fmt.Println(fieldmsg1) //{name main string 0 [0] false}
fieldmsg2,exists2 := a.FieldByName("id")
fmt.Println(exists2) //false
6、FieldByIndex()
但结构体有多层的时候,我们通过FiendByIndex()去访问里层结构体字段
FieldByIndex()
里面传一个切片,[]int{第一次索引,第二层索引,…
type mystruct struct{
name string
address struct{
country string
city string
}
}
........
struct1 := mystruct{}
a := reflect.TypeOf(struct1)
fmt.Println(a.FieldByIndex([]int{1,1})) //{city main string 16 [1] false}
fmt.Println(a.FieldByIndex([]int{1,1}).Name) //city
4.5.2 reflect.ValueOf()
我们使用reflect.ValueOf(变量)得到的是它的反射值对象:
type mystruct struct{
name string
}
func mian(){
struct1 := mystruct{
name:"mac",
}
a := reflect.ValueOf(struct1)
fmt.Ptintf("%T",a) //reflect.Value
}
同样的,它也封装了很多方法供我们使用:
1、Interface()
将值以空接口的形式返回:
type mystruct struct{
name string
}
func mian(){
struct1 := mystruct{
name:"mac",
}
a := reflect.ValueOf(struct1).Interface()
fmt.Println(a.(mystruct).name) //mac
}
同样的,还有:
**Int() / Uint() / Floaat() / Bool() / Bytes() / String() **
下面几点是针对结构体的操作
2、Field()
Field(索引),根据索引,得到对应的字段的值对象:
type mystruct struct{
name string
}
func mian(){
struct1 := mystruct{
name:"mac",
}
a := reflect.ValueOf(struct1)
fmt.Println(a.Field(0)) //mac 类型不是string,是reflect.Value
fmt.Println(a.Field(0).String()) //mac 类型string
}
3、NumField()
返回字段数量
4、FieldByName()/FieldByIndex()
这和TypeOf()方法中的FieldByName()/ FieldByIndex()用法一样,只不过这里的FieldByName()/FieldByIndex()
得到的是字段的值对象(reflect.Value)
5、CanSet()
判断值是否能被修改:
func main() {
var a int = 1000
avalue := reflect.ValueOf(a)
apvalue := reflect.ValueOf(&a).Elem()
fmt.Println(avalue.CanSet()) //false
fmt.Println(apvalue.CanSet()) //true
}
很明显,reflect.ValueOf(变量),传进去的是个副本,要想改值,就得转指针进去
6、SetInt() / SetUint() / SetFloat()/SetBool()/SetBytes()/SetString()
这些方法都是通过反射来设置变量的值。
可以被改值的前提:
a、需要传入指针
设置值的第一个要求我们将CanSet()的时候已经提过,就是需要传入指针才可以设置
func main() {
var a int = 1000
apvalue := reflect.ValueOf(&a).Elem()
apvalue.SetInt(2000)
fmt.Println(a) //2000
}
b、如果是结构体,字段首字母要大写
type mystruct struct{
name string
}
func main(){
struct1 := mystruct{
name:"mac",
}
valueOfStruct1 := reflect.ValueOf(&struct1).Elem()
fmt.Println(valueOfStruct1.FieldByName("name").CanSet()) // false
}
type mystruct struct{
Name string
}
func main(){
struct1 := mystruct{
Name:"mac",
}
valueOfStruct1 := reflect.ValueOf(&struct1).Elem()
fmt.Println(valueOfStruct1.FieldByName("Name").CanSet()) // true
valueOfStruct1.FieldByName("Name").SetString("supermac")
fmt.Println(valueOfStruct1.FieldByName("Name").String()) //supermac
}