面向对象
-
由一系列具有相同类型或不同类型的数据构成的数据集合(类似
Java
中的类
) -
没有
class
、extends
、implements
之类的关键字和相应的概念,借助结构体实现类的声明 -
不支持构造函数、析构函数,通过定义
NewXXX
这样的全局函数作为类的初始化函数 -
指针方法与值方法(Go 语言不支持隐藏的
this
指针,所有的东西都是显式声明) -
toString
实现:方法名固定为String
;手动实现;无需显示调用 -
使用点号 (
.
) 操作符访问结构体成员,格式为:“结构体.成员名”
定义与初始化
// 结构体声明
type struct_variable_type struct {
field1 type
field2 type
...
field3 type
}
// 结构体变量声明
var variable_name struct_variable_type
variable_name := structure_variable_type {value1, value2...valueN}
// 或者定义 NewXXX 方法
/* 结构体定义 */
type Circle struct {
radius float64
}
func NewCircle(radius float64) *Circle{
return &Circle(radius: radius)
}
成员方法
在 func
和方法名之间声明方法所属的类型(有的地方将其称之为接收者声明)
// 该方法属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}
// 方法定义没错,但不属于 Circle 类型对象的方法
func getArea2(c Circle) float64 {
return math.Pi * c.radius * c.radius
}
// 方法可以接收入参
func (c Circle) getPerimeter(x, y int) float64 {
fmt.Println("x=", x, ",y=", y)
return math.Pi * c.radius * 2
}
// circle.SetRadius1(100),circle的radius不会变
func (c Circle) SetRadius1(radius float64) {
c.radius = radius
}
// circle.SetRadius2(100),circle的radius会设置为参数值
func (c *Circle) SetRadius2(radius float64) {
c.radius = radius
}
func main() {
var c1 Circle
c1.radius = 10.00
// 方法调用
fmt.Println("Area of Circle(c1) = ", c1.getArea())
// getArea2 不是 Circle 类对象的方法
// fmt.Println("Area of Circle(c1) = ", c1.getArea2())
area := getArea2(c1)
fmt.Println("area = ", area)
fmt.Println("Perimeter of Circle(c1) = ", c1.getPerimeter(10, 15))
// c1 属性值不变
c1.SetRadius1(100)
// c1 属性值正常修改
c1.SetRadius2(100)
}
指针方法:接收者类型为指针的成员方法(如:func (c *Circle) SetRadius(){}
)
值方法:接收者类型为非指针的成员方法(如:func (c Circle) SetRadius(){}
)
(传入的结构体变量是值类型(类型本身为指针类型除外),因此传入函数内部的是外部结构体实例的值拷贝,修改不会作用到外部结构体实例)
区别:
- 归属于
struct_variable_type
的成员方法只是该类型下所有可用成员方法的子集
归属于*struct_variable_type
的成员方法才是该类型下的完整可用方法集合 - 调用指针方法时,Go 底层会自动将
struct_variable_type
转换为对应的指针类型*struct_variable_type
,即(&struct_variable_type).method()
- 自定义数据类型的方法集合中仅会包含它的所有「值方法」
- 该类型对应的指针类型包含的方法集合才囊括了该类型的所有方法,包括所有「值方法」和「指针方法」
- 指针方法可以修改所属类型的属性值,而值方法则不能
组合实现继承与方法重写
封装:结构体
继承:组合(将一个类型嵌入另一个类型,构建新的类型结构)
推荐使用指针
方式实现继承,组合指针类型性能更好
多态:方法重写
(组合的不同类型包含同名方法,若子类没有重写则无法直接调用【只能显式调用】)
type Animal struct {
Name string
}
type Pet struct {
Name string
}
func (a Animal) Call() string {
return "Animal的叫声..."
}
func (a Animal) GetName() string {
return a.Name
}
func (p Pet) GetName() string {
return p.Name
}
// 通过组合实现继承
type Dog struct {
// 设置别名
animal Animal
// 以指针方式继承某个类型的属性和方法(调用不变;性能更好)
*Pet
}
// ”继承“实现方法重写
func (d Dog) Call() string {
return "汪汪汪。。。"
}
func main() {
animal := models.Animal{Name: "中华田园犬"}
dog := models.Dog{Animal: animal}
// 调用重写的方法
fmt.Println(dog.Call())
// 调用”父类“方法
fmt.Println(dog.Animal.Call())
// 组合的多个结构体拥有同名方法且子类没有重写的话,只能够显式调用
//fmt.Println(dog.GetName())
fmt.Println(dog.Pet.GetName())
fmt.Println(dog.Animal.GetName())
}
属性&成员方法可见性
-
不是传统的面向对象编程语言的可见性,而是基于包的维度
-
包与文件系统的目录结构存在映射关系
-
归属同一个包的 Go 代码具备以下特性:
- 归属于同一个包的源文件包声明语句要一致,即同一级目录的源文件必须属于同一个包;
- 在同一个包下不同的源文件中不能重复声明同一个变量、函数和类(结构体);
-
main
函数作为程序的入口函数,只能存在于main
包中 -
Go 语言类属性和成员方法的可见性都是包一级的,而不是类一级的
(所有变量、函数、自定义类属性或成员方法,可见性都根据其首字母大小写
来决定。大写则包外可访问)
接口
如果说 goroutine 和 channel 是支撑起 Go 语言并发模型的基石,那么接口就是 Go 语言整个类型系统的基石
- 侵入式&非侵入式
侵入式接口:实现类必须明确声明自己实现了某个接口(如:Java
)
非侵入式接口:类与接口的实现关系不通过显式声明,而是系统根据两者的方法集合进行判断 - Go 从设计上避免了侵入式接口
- 一个类只要实现了某个接口要求的所有方法,我们就说这个类实现了该接口(不用显式声明)
(如果一个接口的方法集合是某个类成员方法集合的子集,我们就认为该类实现了这个接口) - 通过关键字
interface
来声明接口 - 通过组合实现接口继承
- 组合的情况下,只有实现了全部接口定义才会判定为完整实现(否则为单接口实现或不实现)
(接口的实现不是强制的,是根据类实现的方法来动态判定的) interface{}
是一个空接口,可以用于表示任意类型
(范围太宽泛了,需要在运行时通过反射对数据进行类型检查)
// 接口定义
type IFile interface {
Read(buf []byte) (n int, err error)
}
// 接口实现
type File struct {
}
func (f *File) Read(buf []byte) (n int, err error) {
return 0, nil
}
// 继承 & 完整实现
type A interface {
Foo()
}
type B interface {
A
Bar()
}
type T struct {}
//func (t T) Foo() {}
// 只有同时实现组合类中的所有方法(包含依赖类)才会被判定为实现
func (t T) Bar() {}
- 如果实现类中的成员方法都是值方法,进行接口赋值时,传递类实例的值类型或指针类型均可,否则只能传递指针类型实例
从代码性能角度来说,值拷贝需要消耗更多的内存空间,统一使用指针类型代码性能会更好
type Integer int
type Math interface {
Add(i Integer) Integer
}
// 这种情况属于是 Integer 实现了接口,可以通过 Integer 或 &Integer 调用
type (i Integer) Add(b Integer) Integer {
return
}
// 这种情况属于是 *Integer 实现了接口,只能通过 &Integer 调用
type (i *Integer) Add(b Integer) Integer {
return
}
func main() {
var a Integer = 1
// 将类实例赋值给接口
// 值类型实现方法,可以通过值或指针类型调用
var m1 Math = a
// 指针类型实现方法,只能通过指针类型调用
var m1 Math = &a
}
- 在 Go 语言中,只要两个接口拥有相同的方法列表(与顺序无关),那么它们就是等同的,可以相互赋值
(前提:接口变量持有的是基于对应实现类的实例值,即接口与接口间的赋值基于类实例与接口间的赋值)
类型断言
实现方式:
.(type)
:type
对应的就是要断言的类型,一般用于基本数据类型- 反射:
reflect
包提供的TypeOf
函数,一般用于结构体
类型断言是否成功需要在运行期才能够确定
断言语法.
左边的变量必需是接口类型(建议使用空接口转换,避免引入多余的接口定义)
Go中的父类与子类(不确定官方是否也这么称呼)与面向对象编程语言(如
Java
)中的概念完全不同,决不能够映射过去理解Go语言结构体类型的断言,即使子类和父类属性名和成员方法列表完全一致,子类的实例并不归属于父类;
同理,父类实现了某个接口,并不代表组合类的子类也实现了这个接口Go 使用
组合
而非继承来构建类与类之间的层级关系,所以子类实例并不是同时父类类型
type Integer int
type Number interface {}
// 接口断言
var i Integer = 1
var n Number = &i
// n 必须是接口才能进行类型断言
if _, ok := n.(Number); ok {
// 只能在运行期间确定结果
}
// 子类的实例并不是父类实例
type IAnimal interface {}
type Animal struct {}
type Dog struct {
animal *Animal
name string
// 同时 Dog 实现接口 IAnimal
}
// var dog IAnimal = dog
// 引入空接口,避免引入冗余接口
var dog interface{} = dog
// ok=true,类型断言:满足接口及自身类型
_, ok := dog.(IAnimal|Dog)
// ok=false,类型断言:”子类型“并不是”父类型“ ==》 组合
_, ok := dog.(Animal)
// 使用反射获取实际类型
func myPringf(args ...interface{}) {
for _, arg := range args {
switch reflect.TypeOf(arg).Kind() {
case reflect.Int:
// codes
case reflect.Int:
// codes
}
}
}
// 通过 reflect.TypeOf(arg) 可获取真实类型
空接口、反射、泛型
Go 打破了传统面向对象编程中类与类之间继承的概念,通过组合实现方法和属性的复用,也不存在类似的继承关系树,也没有所谓的祖宗类(如:java.lang.Object
);接口与实现也没有关键字进行约
空接口
- 类与接口的实现关系是通过类所实现的方法在编译期推断出来的
- 所有类都实现了空接口,反过来,空接口也可以指向任意类型
- 最典型的应用场景是声明函数支持任意类型的参数
// 空接口可以指向任意类型
var v1 interface{} = "这里可以是任意类型"
func method(args ...interface{}) {}
反射
常用的,分别可以通过 reflect.TypeOf
和 reflect.ValueOf
函数获取变量的类型与存储任何类型的值
反射的解析在运行时完成,对性能有一定影响;如非必须,尽量不要使用反射
// 通过反射获取成员变量、方法及执行方法
// 获取类型值:如果包含指针方法则使用如下方法获取类型值
//dogValue := reflect.ValueOf(&dog).Elem()
dogValue := reflect.ValueOf(dog)
// 获取所有属性和成员方法
for i := 0; i < dogValue.NumField(); i++ {
fmt.Println("name:", dogValue.Type().Field(i).Name)
fmt.Println("type:", dogValue.Type().Field(i).Type)
fmt.Println("value:", dogValue.Field(i))
}
// 获取所有方法并执行
for i := 0; i < dogValue.NumMethod(); i++ {
fmt.Println("name:", dogValue.Type().Method(i).Name)
fmt.Println("type:", dogValue.Type().Method(i).Type)
fmt.Println("exec result:", dogValue.Method(i).Call([]reflect.Value{}))
}
泛型(空接口&反射)
当前版本go version go1.17.2
官方还未支持泛型
fixme TODO
空结构体
struct {}
该类型实例值只有一个,即struct{}{}
,且 Go 程序中永远只会存一份,且占据的内存空间是0
典型应用:通道(channel)作为传递简单信号的介质时使用空结构体进行声明
错误处理
error 类型
- 标准模式,
error
接口 - 自定义错误:组合
error
接口并实现Error()
方法
// 「卫述语句」 模板
n, err := Foo(0)
if err != nil {
// 错误处理
} else {
// 使用返回值 n
}
// 构建错误实例
err := errors.New('错误信息')
// fmt.Errorf() 格式化错误信息
// 自定义错误
type PathError struct {
Op string
Path string
Err error
}
func (pe PathError) Error() string {
return pe.Op + pe.Path + pe.Err.Error()
}
panic & recover & defer
类比:
panic recover defer
组合起来实现了面向对象编程中的try...catch...finally
功能
一个完整的示例:
func divide() {
// 通过 defer 提前定义兜底逻辑(先入后出;不论是否发生 panic 都会执行)
defer func() {
// 通过 recover 捕获 panic(当前函数退出执行,回到调用的地方继续)
if err := recover(); err != nil {
fmt.Printf("Runtime panic caught: %v\n", err)
}
// 不使用 recover 恢复的话,整个程序会直接停止
fmt.Println("程序异常")
}()
var i = 1
var j = 0
// 抛出 panic 的函数(手动或默认)
if j == 0 {
panic("参数异常")
}
var k = i / j
fmt.Printf("%d / %d = %d\n", i, j, k)
}
func main() {
// 执行可能发生 panic 的业务
divide()
// 恢复以后继续执行
fmt.Println("继续执行main函数")
}
panic
相当于是Go
语言版的异常(类比Java
中的try
)
当代码运行异常且又没有在编码时显式返回错误时,Go 会抛出panic
,或「运行时恐慌」
panic
函数支持的入参是interface{}
遇到
panic
的执行逻辑:
- 中断当前协程后续代码执行
- 执行终端代码之前定义的
defer
语句(按先入后出顺序)- 程序退出并输出
panic
错误信息
func main() {
i := 1
j := 0
if j == 0 {
// 手动抛出 panic
panic("除数不能是0")
}
}
recover
通过recover()
函数对panic
进行捕获和处理(类比Java
中的catch
)
在defer
中捕获panic
运行时恐慌,defer
执行完成后,退出抛出panic
的当前函数再回到调用它的地方继续执行后续代码
defer
-
用于释放资源或程序运行过程中抛异常执行的兜底逻辑(似
Java
中的finally
;不论是否异常都会执行) -
可以是简单的一行语句或使用匿名函数
-
一个函数/方法中可以存在多个
defer
语句,defer
语句的调用顺序遵循先进后出的原则
(最后一个defer
语句将最先被执行;即使在循环中,依然遵循先进后出) -
尽量在函数/方法的前面定义
defer
,避免遗漏 -
抛出异常后,Go 会中断后续代码的执行,因此定义在异常代码以后的
defer
不会执行
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
// 在函数执行完成或执行抛异常时执行
defer f.Close()
defer func () {
// 一条语句无法执行的动作可使用匿名函数实现
}
}
变量
-
init
函数在main
函数之前执行func init() { fmt.Println("This is init function") } func main() { fmt.Println("This is main function") } // 输出内容 // This is init function // This is main function
-
不同类型的值不能使用
==
或!=
运算符比较 -
标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,那使用这种标识符的对象就可以被外部包的代码所使用(客户端需导入),这被称为导出
(类似于面向对象中的public
); -
标识符如果以小写字母开头,则对包外不可见,但在整个包的内部是不可见且可用的
(类似于面向对象中的private
) -
一行代表一个语句结束(不需要以分号
;
结束,由编译器自动完成)
如果打算将多个语句写在同一行,则必须使用;
进行区分(不建议使用) -
变量
-
全局变量与局部变量可以同名,参考全局变量
-
类型推导动作在编译期完成 ==> Go是静态语言
// 变量声明 var identifier type // 1、指定变量类型(不赋值则使用类型默认值) var v_name v_type var v_name v_type = value // 2、类型推导 var v_name = value // 3、省略 var,使用 := 进行声明;只能用于声明局部变量 v_name := value // 下面使用 := 的定义是正确的 var outer = true func main() { // a、打印方法外定义的变量定义 fmt.Println(outer) // b、如果是全局变量,可以进行重新定义(包括类型可以不一样)(fixme 应该不是同一个变量了) outer := "使用 := 重新定义变量" fmt.Println(outer) // c、如下定义不能编译通过 // var inner [string] = "局部变量" // fmt.Println(inner) // inner := "使用 := 重新定义局部变量无法编译通过" // fmt.Println(inner) } // 局部变量:同类型多个变量 // 1、指定类型(三个变量类型一致;可以是全局变量) var vname1,vname2,vname3 type vname1,vname2,vname3 = v1, v2, v3 // 2、类型推导(可以是不同的类型;可以是全局变量;并行|同时赋值) var vname1,vname2,vname3 = v1, v2, v3 // 3、初始化声明(可以是不同的类型;变量必须不能是已经在方法内部声明过的;并行|同时赋值) vname1,vname2,vname3 := v1, v2, v3 // 全局变量:类型不同的多个变量声明(只能是全局变量) var ( v_name_1 type1 v_name_2 type2 )
-
-
值类型与引用类型
- 值类型
- 所有像
int
、float
、bool
和string
这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值 - 当使用等号
=
将一个变量的值赋值给另一个变量时,如:j = i
,实际上是在内存中将 i 的值进行了拷贝 - 可以通过
&i
来获取变量 i 的内存地址(取址符) - 值类型的变量的值存储在栈中
- 所有像
- 引用类型
- 更复杂的数据通常会需要使用多个值,这些数据一般使用引用类型保存
- 一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个值所在的位置(这个内存地址也称指针)
- 同一个引用类型的指针指向的多个值在内存中可以是连续的,也可以是分散的
- 值类型
-
局部变量禁止只声明不使用;全局变量允许只声明不使用
var outer string = "全局变量允许只声明不使用" func main() { // Unused variable 'inner' var inner = "局部变量禁止只声明不使用" }
-
如果想要简单的交换两个变量的值,可以使用
a, b=b, a
-
空白标识符
_
也被用于抛弃值,如值 5 在_, b := 5, 7
中被抛弃
_
实际上只是一个可写变量,不能获取其值
(Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值) -
并行赋值也被用于当一个函数返回多个返回值
val, err = Func1(var1)
常量
-
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型
-
不能出现任何需要运行期才能获取结果的表达式
-
数字型的常量是没有大小和符号的,并且可以使用任何精度而不会导致溢出
-
定义格式
// 类型说明符可省略 const identifier [type] = value // 支持同时定义多个同类型变量 const c_name_1, c_name_2 = value1, value2 // 可以定义同名的局部和全局常量 const a1, a2, a3 = "a1", 23, true func method() { // 这么定义不会报错 const a1, a2 = "aa1", "aa2" // 优先输出局部变量定义,没有则输出全局变量值 fmt.Println(a1, a2, a3) }
-
常量还可以用作枚举
const ( Unknown = 0 Female = 1 Male = 2 )
-
常量定义中可以使用
len()
、cap()
、unsafe.Sizeof()
计算表达式的值(必需是内置函数)const ( a = "abc" b = len(a) c = unsafe.Sizeof(a) )
-
iota
,特殊常量,可以认为是一个可以被编译器修改的常量
在每一个const
关键字出现时,被重置为0
,然后再下一个const
出现之前,每出现一次iota
,其所代表的数字就会自动增加1
// 定义一 const ( a = iota b = iota c = iota ) // 接上定义二 // const出现,值被重置 const ( d = iota e = iota f = iota ) // a=0,b=1,c=2 // d=0,e=1,f=2 // 简写为 const ( a = iota b c )
-
复杂一些的用法
// 示例一 const ( a = iota // a=0 b // b=1 c // c=2 d = "ha" // d="ha",iota+=1=3 e // e="ha",iota+=1=4 f = 100 // f=100,,iota+=1=5 g // g=100,,iota+=1=6 h = iota // 恢复计数 h=7 i // i=8 ) // 示例二 const ( i = 1 << iota // i=1<<0=1 j = 3 << iota // j=3<<1=6 k // k=3<<2=12 l // l=3<<3=24 )
参考资料: