Go(geekr.dev)学习二

面向对象

  1. 由一系列具有相同类型或不同类型的数据构成的数据集合(类似Java中的

  2. 没有 classextendsimplements 之类的关键字和相应的概念,借助结构体实现类的声明

  3. 不支持构造函数、析构函数,通过定义NewXXX这样的全局函数作为类的初始化函数

  4. 指针方法值方法(Go 语言不支持隐藏的 this 指针,所有的东西都是显式声明)

  5. toString实现:方法名固定为String;手动实现;无需显示调用

  6. 使用点号 (.) 操作符访问结构体成员,格式为:“结构体.成员名”

定义与初始化

// 结构体声明
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(){}
(传入的结构体变量是值类型(类型本身为指针类型除外),因此传入函数内部的是外部结构体实例的值拷贝,修改不会作用到外部结构体实例)

区别:

  1. 归属于struct_variable_type的成员方法只是该类型下所有可用成员方法的子集
    归属于*struct_variable_type的成员方法才是该类型下的完整可用方法集合
  2. 调用指针方法时,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())
}

属性&成员方法可见性

  1. 不是传统的面向对象编程语言的可见性,而是基于的维度

  2. 包与文件系统的目录结构存在映射关系

  3. 归属同一个包的 Go 代码具备以下特性:

    • 归属于同一个包的源文件包声明语句要一致,即同一级目录的源文件必须属于同一个包;
    • 在同一个包下不同的源文件中不能重复声明同一个变量、函数和类(结构体);
  4. main 函数作为程序的入口函数,只能存在于 main 包中

  5. Go 语言类属性和成员方法的可见性都是包一级的,而不是类一级的
    (所有变量、函数、自定义类属性或成员方法,可见性都根据其首字母大小写来决定。大写则包外可访问)

接口

如果说 goroutine 和 channel 是支撑起 Go 语言并发模型的基石,那么接口就是 Go 语言整个类型系统的基石

  1. 侵入式&非侵入式
    侵入式接口:实现类必须明确声明自己实现了某个接口(如:Java
    非侵入式接口:类与接口的实现关系不通过显式声明,而是系统根据两者的方法集合进行判断
  2. Go 从设计上避免了侵入式接口
  3. 一个类只要实现了某个接口要求的所有方法,我们就说这个类实现了该接口(不用显式声明)
    (如果一个接口的方法集合是某个类成员方法集合的子集,我们就认为该类实现了这个接口)
  4. 通过关键字 interface 来声明接口
  5. 通过组合实现接口继承
  6. 组合的情况下,只有实现了全部接口定义才会判定为完整实现(否则为单接口实现或不实现)
    (接口的实现不是强制的,是根据类实现的方法来动态判定的)
  7. 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() {}
  1. 如果实现类中的成员方法都是值方法,进行接口赋值时,传递类实例的值类型或指针类型均可,否则只能传递指针类型实例
    从代码性能角度来说,值拷贝需要消耗更多的内存空间,统一使用指针类型代码性能会更好
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
}
  1. 在 Go 语言中,只要两个接口拥有相同的方法列表(与顺序无关),那么它们就是等同的,可以相互赋值
    (前提:接口变量持有的是基于对应实现类的实例值,即接口与接口间的赋值基于类实例与接口间的赋值)

类型断言

实现方式:

  1. .(type)type 对应的就是要断言的类型,一般用于基本数据类型
  2. 反射: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);接口与实现也没有关键字进行约

空接口
  1. 类与接口的实现关系是通过类所实现的方法在编译期推断出来的
  2. 所有类都实现了空接口,反过来,空接口也可以指向任意类型
  3. 最典型的应用场景是声明函数支持任意类型的参数
// 空接口可以指向任意类型
var v1 interface{} = "这里可以是任意类型"

func method(args ...interface{}) {}
反射

常用的,分别可以通过 reflect.TypeOfreflect.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 类型

  1. 标准模式,error接口
  2. 自定义错误:组合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的执行逻辑:

  1. 中断当前协程后续代码执行
  2. 执行终端代码之前定义的defer语句(按先入后出顺序)
  3. 程序退出并输出panic错误信息
func main() {
    i := 1
    j := 0
    if j == 0 {
        // 手动抛出 panic
        panic("除数不能是0")
    }
}
recover

通过recover()函数对panic进行捕获和处理(类比Java中的catch

defer中捕获panic运行时恐慌,defer执行完成后,退出抛出panic的当前函数再回到调用它的地方继续执行后续代码

defer
  1. 用于释放资源或程序运行过程中抛异常执行的兜底逻辑(似Java中的finally;不论是否异常都会执行)

  2. 可以是简单的一行语句或使用匿名函数

  3. 一个函数/方法中可以存在多个 defer 语句,defer 语句的调用顺序遵循先进后出的原则
    (最后一个 defer 语句将最先被执行;即使在循环中,依然遵循先进后出)

  4. 尽量在函数/方法的前面定义defer,避免遗漏

  5. 抛出异常后,Go 会中断后续代码的执行,因此定义在异常代码以后的defer不会执行

func ReadFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    // 在函数执行完成或执行抛异常时执行
    defer f.Close()
    defer func () {
        // 一条语句无法执行的动作可使用匿名函数实现
    }
}

变量

  1. 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
    
  2. 不同类型的值不能使用==!=运算符比较

  3. 标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,那使用这种标识符的对象就可以被外部包的代码所使用(客户端需导入),这被称为导出
    (类似于面向对象中的public);

  4. 标识符如果以小写字母开头,则对包外不可见,但在整个包的内部是不可见且可用的
    (类似于面向对象中的private

  5. 一行代表一个语句结束(不需要以分号;结束,由编译器自动完成)
    如果打算将多个语句写在同一行,则必须使用;进行区分(不建议使用

  6. 变量

    1. 全局变量与局部变量可以同名,参考全局变量

    2. 类型推导动作在编译期完成 ==> 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
    )
    
  7. 值类型与引用类型

    1. 值类型
      1. 所有像intfloatboolstring这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值
      2. 当使用等号=将一个变量的值赋值给另一个变量时,如:j = i,实际上是在内存中将 i 的值进行了拷贝
      3. 可以通过&i来获取变量 i 的内存地址(取址符)
      4. 值类型的变量的值存储在栈中
    2. 引用类型
      1. 更复杂的数据通常会需要使用多个值,这些数据一般使用引用类型保存
      2. 一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个值所在的位置(这个内存地址也称指针
      3. 同一个引用类型的指针指向的多个值在内存中可以是连续的,也可以是分散的
  8. 局部变量禁止只声明不使用;全局变量允许只声明不使用

    var outer string = "全局变量允许只声明不使用"
    func main() {
        // Unused variable 'inner'
        var inner = "局部变量禁止只声明不使用"
    }
    
  9. 如果想要简单的交换两个变量的值,可以使用a, b=b, a

  10. 空白标识符_也被用于抛弃值,如值 5 在_, b := 5, 7中被抛弃
    _ 实际上只是一个可写变量,不能获取其值
    (Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值)

  11. 并行赋值也被用于当一个函数返回多个返回值

    val, err = Func1(var1)
    

常量

  1. 常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型

  2. 不能出现任何需要运行期才能获取结果的表达式

  3. 数字型的常量是没有大小和符号的,并且可以使用任何精度而不会导致溢出

  4. 定义格式

    // 类型说明符可省略
    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)
    }
    
  5. 常量还可以用作枚举

    const (
    	Unknown = 0
    	Female  = 1
    	Male    = 2
    )
    
  6. 常量定义中可以使用len()cap()unsafe.Sizeof()计算表达式的值(必需是内置函数

    const (
    	a = "abc"
    	b = len(a)
    	c = unsafe.Sizeof(a)
    )
    
  7. 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
    )
    
  8. 复杂一些的用法

    // 示例一
    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
    )
    

参考资料:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值