严格来说 Go语言既不是一个面向对象的语言 也不是一个面向过程的语言
所以说Go语言中并没有封装 继承 多态的概念 但是它通过一些别的方式实现了这些特性
一般情况下 定义结构体的时候是字段和类型名是一一对应的 但是实际上Go语言也支持我们只写类型 而不写字段名的方式 被称为嵌入字段
当匿名字段也是一个结构体的时候 那么这个结构体所拥有的全部字段都被隐式的引入了当前这个结构体
type Person struct {
name string
sex byte
age int
}
type Student Struct {
Person // 匿名字段 默认了Student就包含了Person的所有字段
id int
address string
}
这里的初始化其实就是结构体初始化 代码表示如下
s1 := Student{Person{"mike" , 'm', 18} , 1 , "address"}
成员这里也没有什么好说的
我们可以直接通过 . 操作符去访问成员变量
如果是匿名字段 我们可以通过 . 操作符先访问它再访问它的成员变量
如果说我们使用嵌入字段的时候有重复的字段
type Person struct {
name string
sex byte
age int
}
type Student struct {
Person // 匿名字段 默认了Student就包含了Person的所有字段
name string
id int
address string
}
那么此时 我们使用 . 赋值就只能给最外层的变量赋值 如果想要给嵌入字段的变量赋值就需要通过 . 操作符先访问该字段
所有的内置类型和自定义类型都可以作为匿名字段
声明和初始化方式和上面并无区别
在面向对象编程中 一个对象其实也就是一个简单的值或者是一个变量
在这个对象中会包含一些函数 这个都带有接受者的函数 我们称之为方法
本质上 一个方法则是一个和特殊类型关联的函数
一个面向对象的程序会使用方法来表达其属性和对应的操作 这样使用这个对象的用户就不必去直接操作对象 而是使用方法去完成这些操作
在Go语言中 我们可以给任意自定义类型(包括内置类型 但是不包括指针类型) 添加相应的方法
方法总是绑定对象实例 并且隐式的将实例作为第一实参 方法的语法如下
func (receiver ReceiverType) funcName(parameters) (results)
我们下面将int 重新命名 之后给他重写了一个方法
之后我们就可以在函数中调用该方法来实现各种功能
package main
import "fmt"
type Myint int
// 在函数定义嵌 在其名字之间放上一个变量 即是一个方法
func (a Myint) Add(b Myint) (result Myint) {
return a + b
}
func main() {
var a Myint = 1
fmt.Println("a.Add(2) = ", a.Add(2)) // 答案是3
}
我们从上面的例子就能看出 面向对象只是换了一种语法形式来表达 方法是函数的语法糖
因为receviver其实就是方法所接受的第一个参数
** 需要注意的是 虽然方法的名字一模一样 但是如果接受者不一样 那么方法就不一样**
方法里面可以访问接收者的字段 调用方法通过点(.)来访问 就像struct里面访问字段一样
type Person struct {
name string
sex byte
age int
}
func (P Person) Print() {
fmt.Println(P.age , P.name , P.sex)
}
之后如果我们定义了一个Person对象 我们就可以使用Print方法
这里和之前的结构体传参一样 如果我们使用的是值传递 那么就算我们在方法里面修改内部的变量 也不会有什么改变(因为本质就是一个临时拷贝)
如果是指针传递 那么效果就类似于C++中的引用传递
类型的方法集是指可以被该类型的值调用的所有方法的集合
用实例value 和 pointer 调用方法 不受方法集的约束 编译器总是查找全部方法 并且自动转化为 recevier接受参数
一个指向自定义类型的指针 它的方法集是由该类型定义的所有方法组成 无论这些方法接受的是一个值还是一个指针
如果指针上调用一个接受值的方法 那么Go语言会自动将该指针解引用 并且将值作为参数传递给方法的接受者
类型*T方法集包含全部recevierT +*T的方法
和指针方法集对应的是 如果我们只有一个值 仍然可以借助于Go语言的传值的地址能力调用指针方法
总结下
如果说匿名字段实现了一个方法 那么包含这个匿名字段的struct也能调用该方法
type Person struct {
name string
sex byte
age int
}
func (P Person) Print() {
fmt.Println(P.age , P.name , P.sex)
}
type Student struct{
Person
id int
}
此时我们Student结构体也可以调用Person的方法
由于局部性原则 值和指针会优先访问最近的作用域
所以说如果我们结构体和嵌入字段有相同的方法 那么我们调用该方法时就会优先使用当前结构体的方法
如果我们想要使用嵌入字段的方法 那么我们应该先使用 . 操作符指定该字段 之后在调用方法
类似于我们可以对于函数进行赋值和传递一样 方法也可以进行赋值和传递
根据调用者不同 方法分为两种形式 方法值和方法表达式 这两者都可以像普通函数那样赋值和传参 区别在于方法值绑定实例 而方法表达式则须显式传参
首先我们来了解下方法表达式和值传参的语法
值传参
pFunc1 := p.PrintInfoPointer //方法值,隐式传递 receiver
方法表达式
pFunc1 := (*Person).PrintInfoPointer
他们的区别就是调用方式
如果我们使用值传参我们可以直接调用
如果是方法表达式 则我们需要传递参数 pFunc1(&p)
在Go语言中 接口 是一个自定义类型 接口类型具体描述了一系列方法的集合
接口类型是一种抽象的类型 它不会暴露出自己所代表的对象的值的结构和这个对象支持的基础操作的集合 他们只会展示出自己的方法 因此我们无法实例化接口类型
Go语言通过接口实现了鸭子类型 – 当一只鸟看起来像鸭子 走起来像鸭子 吃起来也像鸭子 那么我们就认为这只鸟是鸭子
我们不关心对象是什么类型 是不是鸭子 只关心行为
接口定义的语法如下
type Humaner interface {
Sayhi()
}
接口是用来定义行为的类型 这些被定义的行为不由接口直接实现 而是通过方法由用户定义的类型实现 一个实现了这些方法的具体类型 就是该接口的实例
如果用户定义的类型 实现了接口类型定义的一组方法 那么这个用户定义类型的值就可以赋给该接口类型的值
这个赋值会把用户定义类型的值存入接口类型的值
type Humaner interface {
Sayhi()
}
type Student struct {
name string
id int
}
type Teacher struct {
name string
id int
}
// 两个自定类型都实现自己的Sayhi方法
func (s *Student) Sayhi() {
fmt.Println("student say hi")
}
func (t* Teacher) Sayhi() {
fmt.Println("teacher say hi")
}
// 普通函数 参数为Humaner类型的变量i
func WhoSayhi(i Humaner) {
i.Sayhi()
}
func main() {
t := &Teacher{"mike" , 1}
s := &Student{"KK" , 2}
WhoSayhi(t)
WhoSayhi(s)
}
解释下上面的代码
如果说interface1作为interface2的一个嵌入字段 那么在interface2中就隐式的包含了interface1里面的所有函数
超集接口可以转化为子集接口 而子集接口不能转化为超集接
就拿上面的interface1和interface2举例 in1是子集 in2是超集
所以in2能转化为in1 反之则不行
空接口(interface{})不包含任何的方法 正因为如此 所有的类型都实现了空接口 因此 空接口可以存储任意类型的数值 它有点类似于C语言的void* 类型
当函数中可以接受任意对象实例的时候 我们会将其声明为interface{} 最典型的例子就是标准库中的Print系列函数
func Println(args ...interface{})
我们知道interface类型的变量可以存储任意类型的数值 那么我们怎么反向知道存储的是什么类型呢? 目前常用的有两种方法
Go语言中有一个语法 可以直接判断是否是该类型的变量
代码表示如下
value, ok = element.(T),
标识符含义如下
如果element中确实存储了T类型的变量 那么ok返回true 否则返回false
switch测试没有什么好讲解的 就是我们拿到value之后一个个测类型即可